supermind-claude 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env node
2
+ // PreToolUse hook for Bash — classifies commands as safe or needing approval.
3
+ // Splits compound commands on && || ; and checks each segment (including pipes).
4
+ // Returns permissionDecision: "allow" | "ask"
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ // ─── User-approved commands ──────────────────────────────────────────────────
11
+ // Reads ~/.claude/supermind-approved.json — an array of command strings/patterns
12
+ // that the user has explicitly approved for auto-allow.
13
+
14
+ const APPROVED_FILE = path.join(os.homedir(), '.claude', 'supermind-approved.json');
15
+
16
+ function loadApprovedCommands() {
17
+ try {
18
+ const data = JSON.parse(fs.readFileSync(APPROVED_FILE, 'utf-8'));
19
+ return Array.isArray(data) ? data : [];
20
+ } catch {
21
+ return [];
22
+ }
23
+ }
24
+
25
+ function isUserApproved(cmd) {
26
+ const approved = loadApprovedCommands();
27
+ const trimmed = cmd.trim();
28
+ for (const pattern of approved) {
29
+ // Exact match
30
+ if (trimmed === pattern) return true;
31
+ // Prefix match (e.g., "git push" approves "git push origin main")
32
+ if (trimmed.startsWith(pattern + ' ') || trimmed.startsWith(pattern + '\t')) return true;
33
+ // Regex match (patterns starting with / are treated as regex)
34
+ if (pattern.startsWith('/') && pattern.endsWith('/')) {
35
+ try {
36
+ if (new RegExp(pattern.slice(1, -1)).test(trimmed)) return true;
37
+ } catch { /* invalid regex, skip */ }
38
+ }
39
+ }
40
+ return false;
41
+ }
42
+
43
+ // ─── Constants ───────────────────────────────────────────────────────────────
44
+
45
+ // Filesystem + shell read-only commands (match by first word or first two words)
46
+ const SAFE_READ_COMMANDS = [
47
+ // Filesystem read-only
48
+ "ls", "cat", "head", "tail", "find", "tree", "du", "df", "wc",
49
+ "file", "which", "type", "readlink", "realpath", "stat", "test",
50
+ // Shell builtins / safe
51
+ "echo", "printf", "pwd", "true", "false", "set", "export",
52
+ "cd", "pushd", "popd", "source", ".",
53
+ // Text processing (read-only)
54
+ "grep", "rg", "awk", "sort", "uniq", "cut", "tr", "tee",
55
+ "diff", "comm", "paste", "column", "fmt", "fold", "rev",
56
+ "jq", "yq", "xargs",
57
+ // System info
58
+ "uname", "whoami", "hostname", "date", "env", "printenv",
59
+ "nproc", "free", "uptime", "id",
60
+ // Node/npm/npx (two-word matches)
61
+ "node -e", "node -p", "npm ls", "npm list", "npm view", "npm info",
62
+ "npx which", "npx tsc", "npx eslint", "npx prettier", "npx vitest",
63
+ "npx jest", "npx tsx", "npx ts-node",
64
+ ];
65
+
66
+ // Prefix-matched safe commands (not just first word)
67
+ const SAFE_PREFIXES = [
68
+ "sed -n", // read-only sed
69
+ "sed -e", // expression sed (read-only when no -i)
70
+ "[[ ", // bash conditional
71
+ "[ ", // test
72
+ ];
73
+
74
+ // Safe write commands (mkdir, touch, etc.) — still checked against DANGEROUS_PATTERNS
75
+ const SAFE_WRITE_COMMANDS = [
76
+ "mkdir", "touch", "cp", "mv",
77
+ ];
78
+
79
+ // ─── Git classification lists ────────────────────────────────────────────────
80
+
81
+ // Git read-only subcommands (always auto-approved)
82
+ const GIT_SAFE_READ = [
83
+ "status", "diff", "log", "show", "blame", "rev-parse", "symbolic-ref",
84
+ "remote", "ls-files", "shortlog", "tag -l", "tag --list", "config --get",
85
+ "config --list", "check-ignore", "rev-list", "name-rev", "describe",
86
+ "for-each-ref", "cat-file", "ls-tree", "verify-commit", "branch -a",
87
+ "branch -r", "branch --list", "branch -l", "branch -v",
88
+ ];
89
+
90
+ // Git non-destructive write subcommands (always auto-approved)
91
+ const GIT_SAFE_WRITE = [
92
+ "add", "commit", "stash push", "stash save", "stash list", "stash show",
93
+ "worktree add", "worktree list",
94
+ "branch -m",
95
+ ];
96
+
97
+ // Git stash subcommands that destroy stash entries
98
+ const GIT_STASH_DESTRUCTIVE = [
99
+ "stash drop", "stash pop", "stash clear",
100
+ ];
101
+
102
+ // Git commands auto-approved ONLY inside a worktree directory
103
+ const GIT_WORKTREE_ONLY = [
104
+ "worktree remove", "worktree prune", "merge", "branch -d",
105
+ ];
106
+
107
+ // Git commands that always require human approval
108
+ const GIT_DANGEROUS = [
109
+ "push", "pull", "fetch", "reset", "revert", "rebase", "clean",
110
+ "checkout -- ", "checkout .", "restore",
111
+ "branch -D",
112
+ ];
113
+
114
+ // ─── Dangerous patterns (checked across all command types) ───────────────────
115
+
116
+ const DANGEROUS_PATTERNS = [
117
+ /--force/,
118
+ /--hard/,
119
+ /-rf\s/,
120
+ /\brm\b/,
121
+ /\brmdir\b/,
122
+ /\bdel\b/,
123
+ // Note: redirects to /dev/null are safe and handled by allowing the base command
124
+ ];
125
+
126
+ // ─── GitHub CLI patterns ─────────────────────────────────────────────────────
127
+
128
+ const GH_DANGEROUS_PATTERNS = [
129
+ /^gh\s+pr\s+merge/,
130
+ /^gh\s+pr\s+close/,
131
+ /^gh\s+pr\s+ready/,
132
+ /^gh\s+issue\s+close/,
133
+ /^gh\s+issue\s+delete/,
134
+ /^gh\s+repo\s+delete/,
135
+ /^gh\s+repo\s+archive/,
136
+ /^gh\s+release\s+(create|delete|edit)/,
137
+ /^gh\s+api\s+-X\s+(DELETE|PUT|PATCH|POST)/,
138
+ /^gh\s+api\s+--method\s+(DELETE|PUT|PATCH|POST)/,
139
+ ];
140
+
141
+ // ─── Git global flag stripping ───────────────────────────────────────────────
142
+ // Strips flags like -C <path>, -c <key=val>, --git-dir, --work-tree, --no-pager
143
+ // that appear before the actual git subcommand.
144
+
145
+ function stripGitGlobalFlags(gitArgs) {
146
+ let args = gitArgs;
147
+ // Flags that consume the next argument
148
+ args = args.replace(/^(-C\s+"[^"]*"|-C\s+'[^']*'|-C\s+\S+)\s*/g, "");
149
+ args = args.replace(/^(-c\s+"[^"]*"|-c\s+'[^']*'|-c\s+\S+)\s*/g, "");
150
+ args = args.replace(/^(--git-dir[= ]\S+)\s*/g, "");
151
+ args = args.replace(/^(--work-tree[= ]\S+)\s*/g, "");
152
+ // Standalone flags
153
+ args = args.replace(/^(--no-pager|--bare|--no-replace-objects|--literal-pathspecs|--no-optional-locks)\s*/g, "");
154
+ // Recurse in case multiple global flags are chained
155
+ if (args !== gitArgs) return stripGitGlobalFlags(args);
156
+ return args;
157
+ }
158
+
159
+ // ─── Command classifiers ─────────────────────────────────────────────────────
160
+
161
+ function classifyGhCommand(cmd) {
162
+ for (const pattern of GH_DANGEROUS_PATTERNS) {
163
+ if (pattern.test(cmd)) return "ask";
164
+ }
165
+ return "allow";
166
+ }
167
+
168
+ function classifyGitCommand(cmd, { inWorktree = false } = {}) {
169
+ const gitCmd = stripGitGlobalFlags(cmd.slice(4).trim());
170
+
171
+ // Dangerous subcommands take priority
172
+ for (const d of GIT_DANGEROUS) {
173
+ if (gitCmd.startsWith(d)) return "ask";
174
+ }
175
+
176
+ // Stash destructive subcommands (drop, pop, clear)
177
+ for (const s of GIT_STASH_DESTRUCTIVE) {
178
+ if (gitCmd.startsWith(s)) return "ask";
179
+ }
180
+
181
+ // Bare "git stash" (no subcommand) is safe — equivalent to stash push
182
+ if (gitCmd === "stash" || /^stash\s*$/.test(gitCmd)) return "allow";
183
+
184
+ // Dangerous patterns (--force, --hard, rm, etc.)
185
+ for (const p of DANGEROUS_PATTERNS) {
186
+ if (p.test(cmd)) return "ask";
187
+ }
188
+
189
+ // Safe read-only subcommands
190
+ for (const r of GIT_SAFE_READ) {
191
+ if (gitCmd.startsWith(r)) return "allow";
192
+ }
193
+
194
+ // Safe write subcommands
195
+ for (const w of GIT_SAFE_WRITE) {
196
+ if (gitCmd.startsWith(w)) return "allow";
197
+ }
198
+
199
+ // Worktree-only commands — auto-approve only inside a worktree dir
200
+ for (const w of GIT_WORKTREE_ONLY) {
201
+ if (gitCmd.startsWith(w)) return inWorktree ? "allow" : "ask";
202
+ }
203
+
204
+ // Bare "git branch <name>" (create branch) — safe
205
+ if (/^branch\s+[^-]/.test(gitCmd)) return "allow";
206
+
207
+ // Unknown git subcommand — ask
208
+ return "ask";
209
+ }
210
+
211
+ function isSedSafe(cmd) {
212
+ // sed is safe only when it does NOT have -i (in-place edit)
213
+ return /^sed\s/.test(cmd) && !/-i/.test(cmd);
214
+ }
215
+
216
+ // Classify a single command segment (no pipes, no compound operators)
217
+ function classifySegment(raw, { inWorktree = false } = {}) {
218
+ const cmd = raw.trim();
219
+ if (!cmd || cmd === "true" || cmd === "false") return "allow";
220
+
221
+ // Check user-approved commands first (overrides all other classification)
222
+ if (isUserApproved(cmd)) return "allow";
223
+
224
+ // Strip leading environment variable assignments: FOO=bar BAZ=qux cmd ...
225
+ const withoutEnv = cmd.replace(/^(\w+=\S+\s+)+/, "");
226
+
227
+ // gh CLI
228
+ if (withoutEnv.startsWith("gh ")) return classifyGhCommand(withoutEnv);
229
+
230
+ // git
231
+ if (withoutEnv.startsWith("git ")) return classifyGitCommand(withoutEnv, { inWorktree });
232
+
233
+ // sed special handling (safe only without -i)
234
+ if (withoutEnv.startsWith("sed ")) return isSedSafe(withoutEnv) ? "allow" : "ask";
235
+
236
+ // Safe prefixes (sed -n, [[ , etc.)
237
+ for (const p of SAFE_PREFIXES) {
238
+ if (withoutEnv.startsWith(p)) return "allow";
239
+ }
240
+
241
+ // Safe read commands — match first word
242
+ const firstWord = withoutEnv.split(/\s/)[0];
243
+ if (SAFE_READ_COMMANDS.includes(firstWord)) return "allow";
244
+
245
+ // Safe write commands — still check dangerous patterns
246
+ if (SAFE_WRITE_COMMANDS.includes(firstWord)) {
247
+ for (const p of DANGEROUS_PATTERNS) {
248
+ if (p.test(cmd)) return "ask";
249
+ }
250
+ return "allow";
251
+ }
252
+
253
+ // Multi-word safe read commands (e.g., "node -e", "npm ls")
254
+ const firstTwo = withoutEnv.split(/\s/).slice(0, 2).join(" ");
255
+ if (SAFE_READ_COMMANDS.includes(firstTwo)) return "allow";
256
+
257
+ // Unknown command — ask
258
+ return "ask";
259
+ }
260
+
261
+ // ─── Worktree context detection ──────────────────────────────────────────────
262
+ // Checks if any segment cd's into a .worktrees/ path or references one in
263
+ // a git worktree command.
264
+
265
+ function detectWorktreeContext(segments) {
266
+ for (const seg of segments) {
267
+ const trimmed = seg.trim();
268
+ // cd into a worktree
269
+ if (trimmed.startsWith("cd ")) {
270
+ const target = trimmed.slice(3).trim().replace(/^["']|["']$/g, "");
271
+ if (/[/\\]\.worktrees?[/\\]/.test(target) || /^\.worktrees?[/\\]/.test(target)) {
272
+ return true;
273
+ }
274
+ }
275
+ // git worktree remove targeting a .worktrees/ path
276
+ if (/git\s+worktree\s+remove\s+.*\.worktrees?[/\\]/.test(trimmed)) {
277
+ return true;
278
+ }
279
+ // git worktree remove with a relative .worktrees path
280
+ if (/git\s+worktree\s+remove\s+\.worktrees?[/\\]/.test(trimmed)) {
281
+ return true;
282
+ }
283
+ }
284
+ return false;
285
+ }
286
+
287
+ // ─── Compound command parser ─────────────────────────────────────────────────
288
+ // Splits on && || ; while respecting single and double quotes.
289
+ // Then splits each segment on pipes. If ANY part is "ask", the whole command is "ask".
290
+
291
+ function classifyCommand(command, { cwdInWorktree = false } = {}) {
292
+ // Split compound commands on && || ; while respecting quotes
293
+ const segments = [];
294
+ let current = "";
295
+ let inSingle = false;
296
+ let inDouble = false;
297
+ let escaped = false;
298
+
299
+ for (let i = 0; i < command.length; i++) {
300
+ const ch = command[i];
301
+
302
+ if (escaped) {
303
+ current += ch;
304
+ escaped = false;
305
+ continue;
306
+ }
307
+
308
+ if (ch === "\\") {
309
+ current += ch;
310
+ escaped = true;
311
+ continue;
312
+ }
313
+
314
+ if (ch === "'" && !inDouble) {
315
+ inSingle = !inSingle;
316
+ current += ch;
317
+ continue;
318
+ }
319
+
320
+ if (ch === '"' && !inSingle) {
321
+ inDouble = !inDouble;
322
+ current += ch;
323
+ continue;
324
+ }
325
+
326
+ if (!inSingle && !inDouble) {
327
+ if (ch === "&" && command[i + 1] === "&") {
328
+ segments.push(current);
329
+ current = "";
330
+ i++; // skip second &
331
+ continue;
332
+ }
333
+ if (ch === "|" && command[i + 1] === "|") {
334
+ segments.push(current);
335
+ current = "";
336
+ i++; // skip second |
337
+ continue;
338
+ }
339
+ if (ch === ";") {
340
+ segments.push(current);
341
+ current = "";
342
+ continue;
343
+ }
344
+ }
345
+
346
+ current += ch;
347
+ }
348
+ if (current.trim()) segments.push(current);
349
+
350
+ // Detect worktree context from cd targets or CWD
351
+ const inWorktree = cwdInWorktree || detectWorktreeContext(segments);
352
+
353
+ // Classify each segment — if ANY is "ask", the whole command is "ask"
354
+ for (const seg of segments) {
355
+ const trimmed = seg.trim();
356
+ if (!trimmed) continue;
357
+
358
+ // Split pipes within segment (quote-aware)
359
+ const pipeParts = [];
360
+ let pipeCurrent = "";
361
+ let pInSingle = false;
362
+ let pInDouble = false;
363
+
364
+ for (let i = 0; i < trimmed.length; i++) {
365
+ const ch = trimmed[i];
366
+ if (ch === "'" && !pInDouble) pInSingle = !pInSingle;
367
+ if (ch === '"' && !pInSingle) pInDouble = !pInDouble;
368
+ if (ch === "|" && trimmed[i + 1] !== "|" && !pInSingle && !pInDouble) {
369
+ pipeParts.push(pipeCurrent);
370
+ pipeCurrent = "";
371
+ continue;
372
+ }
373
+ pipeCurrent += ch;
374
+ }
375
+ pipeParts.push(pipeCurrent);
376
+
377
+ // Check each pipe part
378
+ for (const pipePart of pipeParts) {
379
+ const result = classifySegment(pipePart, { inWorktree });
380
+ if (result === "ask") {
381
+ return { decision: "ask", segment: pipePart.trim() };
382
+ }
383
+ }
384
+ }
385
+
386
+ return { decision: "allow" };
387
+ }
388
+
389
+ // ─── Main ────────────────────────────────────────────────────────────────────
390
+
391
+ function main() {
392
+ let input = "";
393
+ process.stdin.setEncoding("utf-8");
394
+ process.stdin.on("data", (chunk) => { input += chunk; });
395
+ process.stdin.on("end", () => {
396
+ try {
397
+ const data = JSON.parse(input);
398
+ const command = data.tool_input?.command || "";
399
+
400
+ // Check if CWD is inside a worktree directory
401
+ const cwd = process.cwd();
402
+ const cwdInWorktree = /[/\\]\.worktrees?[/\\]/.test(cwd);
403
+
404
+ const { decision, segment } = classifyCommand(command, { cwdInWorktree });
405
+
406
+ const output = {
407
+ hookSpecificOutput: {
408
+ hookEventName: "PreToolUse",
409
+ permissionDecision: decision,
410
+ permissionDecisionReason: decision === "allow"
411
+ ? "All command segments classified as safe"
412
+ : `Segment needs approval: ${segment}`,
413
+ },
414
+ };
415
+
416
+ console.log(JSON.stringify(output));
417
+ } catch (err) {
418
+ // On parse error, don't block — let the normal permission system handle it
419
+ console.log(JSON.stringify({
420
+ hookSpecificOutput: {
421
+ hookEventName: "PreToolUse",
422
+ permissionDecision: "ask",
423
+ permissionDecisionReason: `Hook error: ${err.message}`,
424
+ },
425
+ }));
426
+ }
427
+ });
428
+ }
429
+
430
+ main();
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ // Cost Tracker Hook — logs session cost estimate to JSONL
3
+ // Fires on: Stop (async)
4
+
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const os = require("os");
8
+
9
+ const LOG_FILE = path.join(os.homedir(), ".claude", "cost-log.jsonl");
10
+
11
+ function main() {
12
+ const entry = {
13
+ timestamp: new Date().toISOString(),
14
+ project: process.env.PROJECT_DIR || process.cwd(),
15
+ sessionId: process.env.SESSION_ID || "unknown",
16
+ costUsd: process.env.CLAUDE_SESSION_COST_USD || null,
17
+ };
18
+
19
+ try {
20
+ fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + "\n");
21
+ } catch {
22
+ // Non-critical — silently fail
23
+ }
24
+ }
25
+
26
+ main();
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ // Session End Hook — saves session summary for next session
3
+ // Fires on: Stop
4
+
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const os = require("os");
8
+ const { execSync } = require("child_process");
9
+
10
+ const SESSION_DIR = path.join(os.homedir(), ".claude", "sessions");
11
+ const MAX_SESSIONS = 20;
12
+
13
+ // ─── Git info collection ────────────────────────────────────────────────────
14
+
15
+ function getGitInfo(cwd) {
16
+ try {
17
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
18
+ cwd, encoding: "utf-8",
19
+ }).trim();
20
+ let diff = '';
21
+ try {
22
+ diff = execSync("git diff --name-only HEAD~1 HEAD", {
23
+ cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"],
24
+ }).trim();
25
+ } catch {
26
+ diff = execSync("git diff --name-only", {
27
+ cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"],
28
+ }).trim();
29
+ }
30
+ const filesModified = diff ? diff.split("\n").filter(Boolean) : [];
31
+ return { branch, filesModified };
32
+ } catch {
33
+ return { branch: null, filesModified: [] };
34
+ }
35
+ }
36
+
37
+ // ─── Session cleanup ─────────────────────────────────────────────────────────
38
+
39
+ function cleanOldSessions() {
40
+ if (!fs.existsSync(SESSION_DIR)) return;
41
+
42
+ const files = fs.readdirSync(SESSION_DIR)
43
+ .filter(f => f.endsWith(".json"))
44
+ .map(f => {
45
+ const filepath = path.join(SESSION_DIR, f);
46
+ return { filepath, mtime: fs.statSync(filepath).mtimeMs };
47
+ })
48
+ .sort((a, b) => b.mtime - a.mtime);
49
+
50
+ // Keep only MAX_SESSIONS most recent
51
+ for (const file of files.slice(MAX_SESSIONS)) {
52
+ try { fs.unlinkSync(file.filepath); } catch {}
53
+ }
54
+ }
55
+
56
+ // ─── Main ────────────────────────────────────────────────────────────────────
57
+
58
+ function main() {
59
+ fs.mkdirSync(SESSION_DIR, { recursive: true });
60
+
61
+ const cwd = process.env.PROJECT_DIR || process.cwd();
62
+ const gitInfo = getGitInfo(cwd);
63
+
64
+ const summary = process.env.SESSION_SUMMARY || "Session ended (no summary provided)";
65
+
66
+ const session = {
67
+ timestamp: new Date().toISOString(),
68
+ project: cwd,
69
+ branch: gitInfo.branch,
70
+ filesModified: gitInfo.filesModified,
71
+ summary,
72
+ decisions: [],
73
+ nextSteps: "",
74
+ };
75
+
76
+ const filename = `session-${Date.now()}.json`;
77
+ fs.writeFileSync(path.join(SESSION_DIR, filename), JSON.stringify(session, null, 2));
78
+
79
+ cleanOldSessions();
80
+ }
81
+
82
+ main();
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ // Session Start Hook — loads previous session context + living docs summaries
3
+ // Fires on: SessionStart
4
+ // Outputs combined context: session summary + ARCHITECTURE.md + DESIGN.md
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+ const os = require("os");
9
+
10
+ const SESSION_DIR = path.join(os.homedir(), ".claude", "sessions");
11
+ const MAX_AGE_DAYS = 7;
12
+
13
+ // ─── Session loading ─────────────────────────────────────────────────────────
14
+
15
+ function getLatestSession(projectDir) {
16
+ if (!fs.existsSync(SESSION_DIR)) return null;
17
+
18
+ const files = fs.readdirSync(SESSION_DIR)
19
+ .filter(f => f.endsWith(".json"))
20
+ .map(f => {
21
+ const filepath = path.join(SESSION_DIR, f);
22
+ const stat = fs.statSync(filepath);
23
+ return { filepath, mtime: stat.mtimeMs };
24
+ })
25
+ .sort((a, b) => b.mtime - a.mtime);
26
+
27
+ // Find the most recent session for this project (or any session)
28
+ for (const file of files) {
29
+ const ageDays = (Date.now() - file.mtime) / (1000 * 60 * 60 * 24);
30
+ if (ageDays > MAX_AGE_DAYS) continue;
31
+
32
+ try {
33
+ const data = JSON.parse(fs.readFileSync(file.filepath, "utf-8"));
34
+ if (!projectDir || data.project === projectDir) {
35
+ return data;
36
+ }
37
+ } catch {
38
+ continue;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+
44
+ function formatSessionContext(session) {
45
+ if (!session) return "[Session] No previous session found. Starting fresh.";
46
+
47
+ const age = Math.round((Date.now() - new Date(session.timestamp).getTime()) / (1000 * 60 * 60));
48
+ const ageStr = age < 24 ? `${age}h ago` : `${Math.round(age / 24)}d ago`;
49
+
50
+ const parts = [`[Previous Session: ${ageStr}]`];
51
+
52
+ if (session.summary) parts.push(`Summary: ${session.summary}`);
53
+ if (session.branch) parts.push(`Branch: ${session.branch}`);
54
+ if (session.filesModified?.length) {
55
+ parts.push(`Modified: ${session.filesModified.slice(0, 15).join(", ")}`);
56
+ }
57
+ if (session.decisions?.length) {
58
+ parts.push(`Key decisions: ${session.decisions.join("; ")}`);
59
+ }
60
+ if (session.nextSteps) parts.push(`Next steps: ${session.nextSteps}`);
61
+
62
+ return parts.join("\n");
63
+ }
64
+
65
+ // ─── Living docs extraction ──────────────────────────────────────────────────
66
+
67
+ function extractDocSummary(content, maxChars) {
68
+ const sections = content.split(/^## /m);
69
+ const headings = [];
70
+ let overview = "";
71
+ let techStack = "";
72
+
73
+ for (const section of sections) {
74
+ const lines = section.split("\n");
75
+ const title = lines[0]?.trim();
76
+ if (!title) continue;
77
+ headings.push(title);
78
+
79
+ if (/overview/i.test(title)) {
80
+ const body = lines.slice(1).join("\n").trim();
81
+ overview = body.split(/\n\s*\n/)[0]?.slice(0, 400) || "";
82
+ }
83
+ if (/tech stack/i.test(title)) {
84
+ techStack = lines.filter(l => l.trim().startsWith("|")).join("\n").slice(0, 300);
85
+ }
86
+ }
87
+
88
+ const parts = [];
89
+ if (overview) parts.push("Overview: " + overview);
90
+ if (techStack) parts.push("Tech Stack:\n" + techStack);
91
+ if (headings.length) parts.push("Sections: " + headings.join(", "));
92
+ return parts.join("\n").slice(0, maxChars);
93
+ }
94
+
95
+ function formatLivingDocs(projectDir) {
96
+ const parts = [];
97
+
98
+ // ARCHITECTURE.md — always check
99
+ const archPath = path.join(projectDir, "ARCHITECTURE.md");
100
+ if (fs.existsSync(archPath)) {
101
+ try {
102
+ const content = fs.readFileSync(archPath, "utf-8");
103
+ const summary = extractDocSummary(content, 800);
104
+ if (summary) parts.push(`[Architecture]\n${summary}`);
105
+ } catch {}
106
+ } else {
107
+ parts.push("[Setup] No ARCHITECTURE.md found. Run /supermind-init to create one.");
108
+ }
109
+
110
+ // DESIGN.md — only if it exists
111
+ const designPath = path.join(projectDir, "DESIGN.md");
112
+ if (fs.existsSync(designPath)) {
113
+ try {
114
+ const content = fs.readFileSync(designPath, "utf-8");
115
+ const summary = extractDocSummary(content, 400);
116
+ if (summary) parts.push(`[Design]\n${summary}`);
117
+ } catch {}
118
+ }
119
+
120
+ return parts.join("\n");
121
+ }
122
+
123
+ // ─── Project health check ────────────────────────────────────────────────────
124
+
125
+ function checkProjectHealth(projectDir) {
126
+ const missing = [];
127
+ if (!fs.existsSync(path.join(projectDir, "CLAUDE.md"))) {
128
+ missing.push("CLAUDE.md");
129
+ }
130
+ if (missing.length > 0) {
131
+ return `[Setup] Missing: ${missing.join(", ")} — consider creating one for this project.`;
132
+ }
133
+ return null;
134
+ }
135
+
136
+ // ─── Main ────────────────────────────────────────────────────────────────────
137
+
138
+ function main() {
139
+ try {
140
+ const projectDir = process.env.PROJECT_DIR || process.cwd();
141
+
142
+ const outputParts = [];
143
+
144
+ // Project health check
145
+ const healthWarning = checkProjectHealth(projectDir);
146
+ if (healthWarning) outputParts.push(healthWarning);
147
+
148
+ // Session context
149
+ const session = getLatestSession(projectDir);
150
+ outputParts.push(formatSessionContext(session));
151
+
152
+ // Living docs
153
+ const docsContext = formatLivingDocs(projectDir);
154
+ if (docsContext) outputParts.push(docsContext);
155
+
156
+ console.log(outputParts.join("\n---\n"));
157
+ } catch (err) {
158
+ console.log("[Session] Hook error: " + err.message);
159
+ }
160
+ }
161
+
162
+ main();