@stackmemoryai/stackmemory 1.2.0 → 1.2.1

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.
@@ -5,6 +5,10 @@
5
5
  * This runs as a postinstall script to set up tracing hooks and daemon
6
6
  *
7
7
  * INTERACTIVE: Asks for user consent before modifying ~/.claude
8
+ *
9
+ * Writes to ~/.claude/settings.json (the current Claude Code config format).
10
+ * Uses CANONICAL_HOOKS from hook-installer when dist is available, otherwise
11
+ * falls back to an inline definition.
8
12
  */
9
13
 
10
14
  import {
@@ -13,6 +17,8 @@ import {
13
17
  copyFileSync,
14
18
  readFileSync,
15
19
  writeFileSync,
20
+ renameSync,
21
+ chmodSync,
16
22
  } from 'fs';
17
23
  import { join } from 'path';
18
24
  import { homedir } from 'os';
@@ -25,11 +31,144 @@ const __filename = fileURLToPath(import.meta.url);
25
31
  const __dirname = dirname(__filename);
26
32
 
27
33
  const claudeHooksDir = join(homedir(), '.claude', 'hooks');
28
- const claudeConfigFile = join(homedir(), '.claude', 'hooks.json');
34
+ const claudeSettingsFile = join(homedir(), '.claude', 'settings.json');
29
35
  const templatesDir = join(__dirname, '..', 'templates', 'claude-hooks');
30
36
  const stackmemoryBinDir = join(homedir(), '.stackmemory', 'bin');
31
37
  const distDir = join(__dirname, '..', 'dist');
32
38
 
39
+ // ---------------------------------------------------------------------------
40
+ // Try to import from compiled hook-installer; fall back to inline definitions
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /** @type {Array<{scriptName: string, eventType: string, matcher?: string, timeout?: number, commandPrefix?: string, required: boolean}>} */
44
+ let CANONICAL_HOOKS;
45
+ let mergeSettingsFn;
46
+ let readSettingsFn;
47
+ let writeSettingsAtomicFn;
48
+
49
+ try {
50
+ const mod = await import('../dist/src/utils/hook-installer.js');
51
+ CANONICAL_HOOKS = mod.CANONICAL_HOOKS;
52
+ mergeSettingsFn = mod.mergeSettings;
53
+ readSettingsFn = mod.readSettings;
54
+ writeSettingsAtomicFn = mod.writeSettingsAtomic;
55
+ } catch {
56
+ // dist not built yet — use inline fallback
57
+ CANONICAL_HOOKS = [
58
+ {
59
+ scriptName: 'session-rescue.sh',
60
+ eventType: 'Stop',
61
+ timeout: 12,
62
+ required: true,
63
+ },
64
+ {
65
+ scriptName: 'stop-checkpoint.js',
66
+ eventType: 'Stop',
67
+ timeout: 5,
68
+ commandPrefix: 'node',
69
+ required: true,
70
+ },
71
+ {
72
+ scriptName: 'chime-on-stop.sh',
73
+ eventType: 'Stop',
74
+ timeout: 2,
75
+ required: true,
76
+ },
77
+ {
78
+ scriptName: 'auto-checkpoint.js',
79
+ eventType: 'PostToolUse',
80
+ timeout: 2,
81
+ commandPrefix: 'node',
82
+ required: true,
83
+ },
84
+ ];
85
+
86
+ const DEAD_HOOKS = ['sms-response-handler.js'];
87
+
88
+ readSettingsFn = (p) => {
89
+ try {
90
+ if (existsSync(p)) return JSON.parse(readFileSync(p, 'utf8'));
91
+ } catch {
92
+ /* parse error */
93
+ }
94
+ return {};
95
+ };
96
+
97
+ writeSettingsAtomicFn = (settings, p) => {
98
+ const dir = dirname(p);
99
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
100
+ const tmp = p + '.tmp';
101
+ writeFileSync(tmp, JSON.stringify(settings, null, 2) + '\n');
102
+ renameSync(tmp, p);
103
+ };
104
+
105
+ mergeSettingsFn = (existing, hooksDir) => {
106
+ const merged = JSON.parse(JSON.stringify(existing));
107
+ if (!merged.hooks) merged.hooks = {};
108
+
109
+ // Remove dead hooks
110
+ for (const eventType of Object.keys(merged.hooks)) {
111
+ const groups = merged.hooks[eventType];
112
+ for (const group of groups) {
113
+ group.hooks = group.hooks.filter((h) => {
114
+ for (const dead of DEAD_HOOKS) {
115
+ if (h.command.includes(dead)) return false;
116
+ }
117
+ return true;
118
+ });
119
+ }
120
+ merged.hooks[eventType] = groups.filter((g) => g.hooks.length > 0);
121
+ if (merged.hooks[eventType].length === 0) delete merged.hooks[eventType];
122
+ }
123
+
124
+ // Add missing canonical hooks
125
+ for (const entry of CANONICAL_HOOKS) {
126
+ const exists = (merged.hooks[entry.eventType] || []).some((group) =>
127
+ group.hooks.some((h) => h.command.includes(entry.scriptName))
128
+ );
129
+ if (exists) continue;
130
+
131
+ const scriptPath = join(hooksDir, entry.scriptName);
132
+ const command = entry.commandPrefix
133
+ ? `${entry.commandPrefix} ${scriptPath}`
134
+ : scriptPath;
135
+ const hookCmd = { type: 'command', command };
136
+ if (entry.timeout) hookCmd.timeout = entry.timeout;
137
+
138
+ const eventGroups = merged.hooks[entry.eventType] || [];
139
+ const matcherValue = entry.matcher;
140
+ let targetGroup = eventGroups.find((g) =>
141
+ matcherValue ? g.matcher === matcherValue : !g.matcher
142
+ );
143
+
144
+ if (targetGroup) {
145
+ targetGroup.hooks.push(hookCmd);
146
+ } else {
147
+ const newGroup = { hooks: [hookCmd] };
148
+ if (matcherValue) newGroup.matcher = matcherValue;
149
+ eventGroups.push(newGroup);
150
+ }
151
+ merged.hooks[entry.eventType] = eventGroups;
152
+ }
153
+
154
+ return merged;
155
+ };
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Legacy hook files (still installed for backward-compat tooling hooks)
160
+ // ---------------------------------------------------------------------------
161
+
162
+ const LEGACY_HOOK_FILES = [
163
+ 'tool-use-trace.js',
164
+ 'on-startup.js',
165
+ 'on-clear.js',
166
+ 'on-task-complete.js',
167
+ 'skill-eval.sh',
168
+ 'skill-eval.cjs',
169
+ ];
170
+ const LEGACY_DATA_FILES = ['skill-rules.json'];
171
+
33
172
  /**
34
173
  * Ask user for confirmation before installing hooks
35
174
  * Returns true if user consents, false otherwise
@@ -51,14 +190,13 @@ async function askForConsent() {
51
190
  return false;
52
191
  }
53
192
 
54
- console.log('\n📦 StackMemory Post-Install\n');
193
+ console.log('\nStackMemory Post-Install\n');
55
194
  console.log(
56
195
  'StackMemory can integrate with Claude Code by installing hooks that:'
57
196
  );
58
197
  console.log(' - Track tool usage for better context');
59
198
  console.log(' - Enable session persistence across restarts');
60
- console.log(' - Sync context with Linear (optional)');
61
- console.log(' - Auto-update PROMPT_PLAN on task completion');
199
+ console.log(' - Auto-checkpoint and session rescue');
62
200
  console.log(' - Recommend relevant skills based on your prompts');
63
201
  console.log('\nThis will modify files in ~/.claude/\n');
64
202
 
@@ -91,24 +229,38 @@ async function installClaudeHooks() {
91
229
  console.log('Created ~/.claude/hooks directory');
92
230
  }
93
231
 
94
- // Copy hook files (scripts + data files)
95
- const hookFiles = [
96
- 'tool-use-trace.js',
97
- 'on-startup.js',
98
- 'on-clear.js',
99
- 'on-task-complete.js',
100
- 'skill-eval.sh',
101
- 'skill-eval.cjs',
102
- ];
103
- const dataFiles = ['skill-rules.json'];
104
232
  let installed = 0;
105
233
 
106
- for (const hookFile of [...hookFiles, ...dataFiles]) {
234
+ // --- Install canonical session-persistence hooks ---
235
+ for (const entry of CANONICAL_HOOKS) {
236
+ const srcPath = join(templatesDir, entry.scriptName);
237
+ const destPath = join(claudeHooksDir, entry.scriptName);
238
+
239
+ if (existsSync(srcPath)) {
240
+ // Backup existing hook if it exists
241
+ if (existsSync(destPath)) {
242
+ const backupPath = `${destPath}.backup-${Date.now()}`;
243
+ copyFileSync(destPath, backupPath);
244
+ console.log(` Backed up: ${entry.scriptName}`);
245
+ }
246
+
247
+ copyFileSync(srcPath, destPath);
248
+ try {
249
+ chmodSync(destPath, 0o755);
250
+ } catch {
251
+ // Silent fail on chmod
252
+ }
253
+ installed++;
254
+ console.log(` Installed: ${entry.scriptName}`);
255
+ }
256
+ }
257
+
258
+ // --- Install legacy tooling hooks ---
259
+ for (const hookFile of [...LEGACY_HOOK_FILES, ...LEGACY_DATA_FILES]) {
107
260
  const srcPath = join(templatesDir, hookFile);
108
261
  const destPath = join(claudeHooksDir, hookFile);
109
262
 
110
263
  if (existsSync(srcPath)) {
111
- // Backup existing hook if it exists
112
264
  if (existsSync(destPath)) {
113
265
  const backupPath = `${destPath}.backup-${Date.now()}`;
114
266
  copyFileSync(destPath, backupPath);
@@ -117,11 +269,9 @@ async function installClaudeHooks() {
117
269
 
118
270
  copyFileSync(srcPath, destPath);
119
271
 
120
- // Make executable (scripts only, not data files)
121
- if (!dataFiles.includes(hookFile)) {
272
+ if (!LEGACY_DATA_FILES.includes(hookFile)) {
122
273
  try {
123
- const { execSync } = await import('child_process');
124
- execSync(`chmod +x "${destPath}"`, { stdio: 'ignore' });
274
+ chmodSync(destPath, 0o755);
125
275
  } catch {
126
276
  // Silent fail on chmod
127
277
  }
@@ -132,30 +282,14 @@ async function installClaudeHooks() {
132
282
  }
133
283
  }
134
284
 
135
- // Update hooks.json configuration
136
- let hooksConfig = {};
137
- if (existsSync(claudeConfigFile)) {
138
- try {
139
- hooksConfig = JSON.parse(readFileSync(claudeConfigFile, 'utf8'));
140
- } catch {
141
- // Start fresh if parse fails
142
- }
143
- }
144
-
145
- // Add our hooks (don't overwrite existing hooks unless they're ours)
146
- const newHooksConfig = {
147
- ...hooksConfig,
148
- 'tool-use-approval': join(claudeHooksDir, 'tool-use-trace.js'),
149
- 'on-startup': join(claudeHooksDir, 'on-startup.js'),
150
- 'on-clear': join(claudeHooksDir, 'on-clear.js'),
151
- 'on-task-complete': join(claudeHooksDir, 'on-task-complete.js'),
152
- 'user-prompt-submit': join(claudeHooksDir, 'skill-eval.sh'),
153
- };
154
-
155
- writeFileSync(claudeConfigFile, JSON.stringify(newHooksConfig, null, 2));
285
+ // --- Update settings.json (not the deprecated hooks.json) ---
286
+ const currentSettings = readSettingsFn(claudeSettingsFile);
287
+ const merged = mergeSettingsFn(currentSettings, claudeHooksDir);
288
+ writeSettingsAtomicFn(merged, claudeSettingsFile);
156
289
 
157
290
  if (installed > 0) {
158
291
  console.log(`\n[OK] Installed ${installed} Claude hooks`);
292
+ console.log(` Config: ~/.claude/settings.json`);
159
293
  console.log(` Traces: ~/.stackmemory/traces/`);
160
294
  console.log(' To disable: set DEBUG_TRACE=false in .env');
161
295
  }
@@ -189,10 +323,8 @@ async function installSessionDaemon() {
189
323
  if (existsSync(daemonSrc)) {
190
324
  copyFileSync(daemonSrc, daemonDest);
191
325
 
192
- // Make executable
193
326
  try {
194
- const { execSync } = await import('child_process');
195
- execSync(`chmod +x "${daemonDest}"`, { stdio: 'ignore' });
327
+ chmodSync(daemonDest, 0o755);
196
328
  } catch {
197
329
  // Silent fail on chmod
198
330
  }
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Auto-Checkpoint Hook (PostToolUse)
5
+ *
6
+ * Counts tool calls per project session. Every 25 calls, writes a lightweight
7
+ * checkpoint to ~/.stackmemory/checkpoints/{project-hash}/latest.json.
8
+ * Must complete in <50ms -- pure file I/O only.
9
+ *
10
+ * State is keyed per-session (process.ppid) to avoid race conditions
11
+ * when multiple Claude instances run concurrently.
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const crypto = require('crypto');
17
+
18
+ const CHECKPOINT_INTERVAL = 25;
19
+ const SAVE_INTERVAL = 5;
20
+ const MAX_RECENT_TOOLS = 20;
21
+ const MAX_FILES_TRACKED = 50;
22
+ const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
23
+
24
+ const HOME = process.env.HOME || '/tmp';
25
+ const SESSION_ID = process.env.CLAUDE_INSTANCE_ID || String(process.ppid);
26
+ const STATE_FILE = path.join(
27
+ HOME,
28
+ '.stackmemory',
29
+ `checkpoint-state-${SESSION_ID}.json`
30
+ );
31
+
32
+ function projectHash(cwd) {
33
+ return crypto.createHash('md5').update(cwd).digest('hex').slice(0, 12);
34
+ }
35
+
36
+ function ensureDir(dir) {
37
+ if (!fs.existsSync(dir)) {
38
+ fs.mkdirSync(dir, { recursive: true });
39
+ }
40
+ }
41
+
42
+ function safeWriteFile(filePath, content) {
43
+ const tmp = filePath + '.tmp';
44
+ fs.writeFileSync(tmp, content);
45
+ fs.renameSync(tmp, filePath);
46
+ }
47
+
48
+ function loadState() {
49
+ try {
50
+ if (fs.existsSync(STATE_FILE)) {
51
+ const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
52
+ const cutoff = Date.now() - TTL_MS;
53
+ for (const [cwd, proj] of Object.entries(state.projects)) {
54
+ if (
55
+ proj.sessionStart &&
56
+ new Date(proj.sessionStart).getTime() < cutoff
57
+ ) {
58
+ delete state.projects[cwd];
59
+ }
60
+ }
61
+ return state;
62
+ }
63
+ } catch {
64
+ // Corrupted state, start fresh
65
+ }
66
+ return { projects: {} };
67
+ }
68
+
69
+ function saveState(state) {
70
+ try {
71
+ ensureDir(path.dirname(STATE_FILE));
72
+ safeWriteFile(STATE_FILE, JSON.stringify(state));
73
+ } catch {
74
+ // Best-effort
75
+ }
76
+ }
77
+
78
+ function getProjectState(state, cwd) {
79
+ if (!state.projects[cwd]) {
80
+ state.projects[cwd] = {
81
+ toolCount: 0,
82
+ checkpointCount: 0,
83
+ lastCheckpoint: null,
84
+ sessionStart: new Date().toISOString(),
85
+ filesModified: [],
86
+ recentTools: [],
87
+ };
88
+ }
89
+ return state.projects[cwd];
90
+ }
91
+
92
+ function writeCheckpoint(proj, cwd) {
93
+ const hash = projectHash(cwd);
94
+ const cpDir = path.join(HOME, '.stackmemory', 'checkpoints', hash);
95
+ ensureDir(cpDir);
96
+
97
+ const checkpoint = {
98
+ toolCount: proj.toolCount,
99
+ checkpointNumber: proj.checkpointCount,
100
+ timestamp: new Date().toISOString(),
101
+ filesModified: proj.filesModified.slice(-MAX_FILES_TRACKED),
102
+ recentTools: proj.recentTools.slice(-MAX_RECENT_TOOLS),
103
+ cwd,
104
+ };
105
+
106
+ safeWriteFile(
107
+ path.join(cpDir, 'latest.json'),
108
+ JSON.stringify(checkpoint, null, 2)
109
+ );
110
+ }
111
+
112
+ function trackFile(proj, toolInput) {
113
+ const filePath = toolInput?.file_path;
114
+ if (filePath && !proj.filesModified.includes(filePath)) {
115
+ proj.filesModified.push(filePath);
116
+ if (proj.filesModified.length > MAX_FILES_TRACKED) {
117
+ proj.filesModified = proj.filesModified.slice(-MAX_FILES_TRACKED);
118
+ }
119
+ }
120
+ }
121
+
122
+ async function readInput() {
123
+ let input = '';
124
+ for await (const chunk of process.stdin) {
125
+ input += chunk;
126
+ }
127
+ return JSON.parse(input);
128
+ }
129
+
130
+ async function main() {
131
+ try {
132
+ const input = await readInput();
133
+ const { tool_name, tool_input } = input;
134
+
135
+ const cwd = process.cwd();
136
+ const state = loadState();
137
+ const proj = getProjectState(state, cwd);
138
+
139
+ // Increment counter
140
+ proj.toolCount++;
141
+
142
+ // Track tool name
143
+ proj.recentTools.push(tool_name);
144
+ if (proj.recentTools.length > MAX_RECENT_TOOLS) {
145
+ proj.recentTools = proj.recentTools.slice(-MAX_RECENT_TOOLS);
146
+ }
147
+
148
+ // Track modified files from Edit/Write
149
+ if (
150
+ tool_name === 'Edit' ||
151
+ tool_name === 'Write' ||
152
+ tool_name === 'MultiEdit'
153
+ ) {
154
+ trackFile(proj, tool_input);
155
+ }
156
+
157
+ // Checkpoint every N calls
158
+ const isCheckpoint = proj.toolCount % CHECKPOINT_INTERVAL === 0;
159
+ if (isCheckpoint) {
160
+ proj.checkpointCount++;
161
+ proj.lastCheckpoint = new Date().toISOString();
162
+ writeCheckpoint(proj, cwd);
163
+ }
164
+
165
+ // Only persist state every SAVE_INTERVAL calls or on checkpoint boundaries
166
+ if (isCheckpoint || proj.toolCount % SAVE_INTERVAL === 0) {
167
+ saveState(state);
168
+ }
169
+ } catch {
170
+ // Silent fail -- never block the agent
171
+ }
172
+ }
173
+
174
+ main();
@@ -0,0 +1,22 @@
1
+ #!/bin/bash
2
+ # Chime once when Claude Code needs user input
3
+ # Uses macOS system sound
4
+
5
+ # Prevent multiple chimes by checking if we recently played
6
+ CHIME_LOCK="/tmp/claude-chime-lock"
7
+ CHIME_COOLDOWN=2 # seconds
8
+
9
+ if [ -f "$CHIME_LOCK" ]; then
10
+ LAST_CHIME=$(cat "$CHIME_LOCK")
11
+ NOW=$(date +%s)
12
+ DIFF=$((NOW - LAST_CHIME))
13
+ if [ "$DIFF" -lt "$CHIME_COOLDOWN" ]; then
14
+ exit 0 # Skip chime if too recent
15
+ fi
16
+ fi
17
+
18
+ # Record chime time
19
+ date +%s > "$CHIME_LOCK"
20
+
21
+ # Play system sound (Glass is a nice subtle chime)
22
+ afplay /System/Library/Sounds/Glass.aiff &
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+ # session-rescue.sh — Auto-capture stackmemory handoff on session close
3
+ # Runs as a Stop hook. Silent on failure. Skips non-stackmemory projects.
4
+
5
+ set -euo pipefail
6
+
7
+ # Only run in stackmemory-initialized projects
8
+ [ -d ".stackmemory" ] || exit 0
9
+
10
+ # Require stackmemory CLI
11
+ command -v stackmemory >/dev/null 2>&1 || exit 0
12
+
13
+ # Capture without committing, compact format, 10s timeout
14
+ timeout 10 stackmemory capture --no-commit --format compact \
15
+ -m "auto-rescue on session close" >/dev/null 2>&1 || true
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Stop-Checkpoint Hook
5
+ *
6
+ * Fires on session stop. Reads checkpoint state for current project.
7
+ * If 3+ checkpoints accumulated, outputs a "context heavy" message.
8
+ * Writes a final checkpoint with session stop metadata.
9
+ *
10
+ * State is keyed per-session (process.ppid) to avoid race conditions
11
+ * when multiple Claude instances run concurrently.
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const crypto = require('crypto');
17
+
18
+ const HOME = process.env.HOME || '/tmp';
19
+ const SESSION_ID = process.env.CLAUDE_INSTANCE_ID || String(process.ppid);
20
+ const STATE_FILE = path.join(
21
+ HOME,
22
+ '.stackmemory',
23
+ `checkpoint-state-${SESSION_ID}.json`
24
+ );
25
+ const MAX_FILES_TRACKED = 50;
26
+ const MAX_RECENT_TOOLS = 20;
27
+
28
+ function projectHash(cwd) {
29
+ return crypto.createHash('md5').update(cwd).digest('hex').slice(0, 12);
30
+ }
31
+
32
+ function ensureDir(dir) {
33
+ if (!fs.existsSync(dir)) {
34
+ fs.mkdirSync(dir, { recursive: true });
35
+ }
36
+ }
37
+
38
+ function safeWriteFile(filePath, content) {
39
+ const tmp = filePath + '.tmp';
40
+ fs.writeFileSync(tmp, content);
41
+ fs.renameSync(tmp, filePath);
42
+ }
43
+
44
+ function loadState() {
45
+ try {
46
+ if (fs.existsSync(STATE_FILE)) {
47
+ const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
48
+ const cutoff = Date.now() - 24 * 60 * 60 * 1000;
49
+ for (const [cwd, proj] of Object.entries(state.projects)) {
50
+ if (
51
+ proj.sessionStart &&
52
+ new Date(proj.sessionStart).getTime() < cutoff
53
+ ) {
54
+ delete state.projects[cwd];
55
+ }
56
+ }
57
+ return state;
58
+ }
59
+ } catch {
60
+ // Corrupted state, start fresh
61
+ }
62
+ return { projects: {} };
63
+ }
64
+
65
+ function main() {
66
+ try {
67
+ const cwd = process.cwd();
68
+ const state = loadState();
69
+ const proj = state.projects?.[cwd];
70
+
71
+ if (!proj) return;
72
+
73
+ const { toolCount, checkpointCount, sessionStart, filesModified } = proj;
74
+
75
+ // Write final checkpoint
76
+ const hash = projectHash(cwd);
77
+ const cpDir = path.join(HOME, '.stackmemory', 'checkpoints', hash);
78
+ ensureDir(cpDir);
79
+
80
+ // NaN duration guard
81
+ const startMs = sessionStart ? new Date(sessionStart).getTime() : 0;
82
+ const duration =
83
+ startMs > 0 ? Math.round((Date.now() - startMs) / 1000) : 0;
84
+
85
+ const finalCheckpoint = {
86
+ reason: 'session_stop',
87
+ toolCount,
88
+ checkpointNumber: checkpointCount,
89
+ totalFiles: filesModified?.length || 0,
90
+ duration,
91
+ timestamp: new Date().toISOString(),
92
+ filesModified: (filesModified || []).slice(-MAX_FILES_TRACKED),
93
+ recentTools: (proj.recentTools || []).slice(-MAX_RECENT_TOOLS),
94
+ cwd,
95
+ };
96
+
97
+ safeWriteFile(
98
+ path.join(cpDir, 'latest.json'),
99
+ JSON.stringify(finalCheckpoint, null, 2)
100
+ );
101
+
102
+ // Output context-heavy message if 3+ checkpoints
103
+ if (checkpointCount >= 3) {
104
+ console.error(
105
+ `\nContext heavy (${checkpointCount} checkpoints, ${toolCount} tool calls). State saved. Consider /clear to reload from checkpoint.\n`
106
+ );
107
+ }
108
+
109
+ // Clean up session state file since session is ending
110
+ try {
111
+ fs.unlinkSync(STATE_FILE);
112
+ } catch {
113
+ // Best-effort cleanup
114
+ }
115
+ } catch {
116
+ // Silent fail -- never block the agent
117
+ }
118
+ }
119
+
120
+ main();