-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtouch-session.mjs
More file actions
119 lines (107 loc) · 4.12 KB
/
Copy pathtouch-session.mjs
File metadata and controls
119 lines (107 loc) · 4.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#!/usr/bin/env node
// projectstore — touch-session.mjs
//
// Called by the PreToolUse hook on every tool call.
//
// Responsibilities:
// 1. Liveness — touches this session's registration file (mtime) so other
// sessions can see we are active. Bootstraps the record on first call
// when SessionStart did not run (plugin installed mid-session via
// /reload-plugins, or rare path where SessionStart was skipped).
// 2. Activity log — extracts the target file path from the tool input
// and, if it lives inside the vault, appends an entry to
// `recent_activity` in the session file (capped at 50, deduped).
// The PreCompact hook reads this list to build a survival packet.
//
// Session identity comes from Claude's own `session_id` field in the
// hook input JSON (stdin), so two Claude Code instances open on the
// same project get distinct session files.
//
// Silent no-op when there is no projectstore config, when stdin lacks
// session_id, or on any error — a PreToolUse hook must never crash the
// user's tool call.
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import {
readConfig,
readStdinJson,
touchSession,
writeSession,
cleanupStaleSessions,
sessionFilePath,
projectRoot,
appendActivity,
isInsideVault,
parseFrontmatter,
readSessionState,
writeSessionState,
} from "./lib.mjs";
const WRITE_TOOLS = /^(Edit|Write|MultiEdit|NotebookEdit)$/;
const NUDGE_INTERVAL_MS = 10 * 60 * 1000;
// Per-session active pointer (ADR-006) — denormalized titles/status captured
// at write time so the statusline renders with zero vault reads — plus the
// raw-edit nudge (PS-IMPROVE story-003): throttled, never blocking.
function updatePointerAndNudge(cfg, proj, sid, filePath, toolName) {
const rel = filePath.slice(cfg.vault_path.length + 1);
let patch = null;
const m = rel.match(/^epics\/([^/]+)\//);
if (m) {
const epicId = m[1];
patch = { active_epic: epicId };
try {
const { data } = parseFrontmatter(
readFileSync(join(cfg.vault_path, "epics", epicId, "epic.md"), "utf8"));
if (data.title) patch.epic_title = String(data.title);
} catch {}
const sm = rel.match(/^epics\/[^/]+\/stories\/([^/]+\.md)$/);
if (sm) {
patch.active_story = sm[1].replace(/\.md$/, "");
try {
const { data } = parseFrontmatter(readFileSync(filePath, "utf8"));
if (data.title) patch.story_title = String(data.title);
if (data.status) patch.story_status = String(data.status);
} catch {}
}
}
let nudge = null;
if (WRITE_TOOLS.test(toolName || "") && cfg.guard !== "off") {
const st = readSessionState(proj, sid);
const last = st && st.nudged_at ? Date.parse(st.nudged_at) : 0;
if (Date.now() - last > NUDGE_INTERVAL_MS) {
nudge =
"projectstore: vault file edited directly — if this bypassed a /projectstore:* command, run /projectstore:reconcile (or doctor) afterwards so the board/indexes stay in sync.";
patch = { ...(patch || {}), nudged_at: new Date().toISOString() };
}
}
if (patch) writeSessionState(proj, sid, patch);
if (nudge) process.stdout.write(JSON.stringify({ systemMessage: nudge }) + "\n");
}
function extractToolPath(input) {
if (!input || !input.tool_input) return null;
const ti = input.tool_input;
return ti.file_path || ti.notebook_path || ti.path || null;
}
function main() {
const cfg = readConfig();
if (!cfg) return;
const input = readStdinJson();
if (!input) return;
const sid = input.session_id;
if (!sid) return;
const proj = projectRoot();
if (existsSync(sessionFilePath(cfg.vault_path, sid))) {
touchSession(cfg.vault_path, sid);
} else {
cleanupStaleSessions(cfg.vault_path);
writeSession(cfg.vault_path, sid, proj);
}
if (!input.tool_name) return;
const filePath = extractToolPath(input);
if (filePath && isInsideVault(filePath, cfg.vault_path)) {
try { appendActivity(cfg.vault_path, sid, filePath, input.tool_name); } catch {}
try { updatePointerAndNudge(cfg, proj, sid, filePath, input.tool_name); } catch {}
}
}
try { main(); } catch {
// PreToolUse must never crash the user's tool call.
}