@tyroneross/bookmark 0.1.0 → 0.3.2

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.
Files changed (109) hide show
  1. package/.claude-plugin/marketplace.json +29 -12
  2. package/.claude-plugin/plugin.json +12 -8
  3. package/.codex-plugin/plugin.json +31 -0
  4. package/.mcp.json +8 -0
  5. package/CLAUDE.md +83 -49
  6. package/LICENSE +202 -21
  7. package/README.md +150 -57
  8. package/agents/snapshot-analyst.md +1 -1
  9. package/commands/bookmark.md +29 -0
  10. package/commands/feedback.md +37 -0
  11. package/commands/restore.md +14 -4
  12. package/commands/snapshot.md +19 -7
  13. package/commands/status.md +1 -1
  14. package/dist/cli/index.js +627 -87
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/cli/snapshot-resolution.d.ts +15 -0
  17. package/dist/cli/snapshot-resolution.d.ts.map +1 -0
  18. package/dist/cli/snapshot-resolution.js +21 -0
  19. package/dist/cli/snapshot-resolution.js.map +1 -0
  20. package/dist/config.d.ts +19 -0
  21. package/dist/config.d.ts.map +1 -1
  22. package/dist/config.js +149 -23
  23. package/dist/config.js.map +1 -1
  24. package/dist/context/freshness.d.ts +3 -0
  25. package/dist/context/freshness.d.ts.map +1 -0
  26. package/dist/context/freshness.js +29 -0
  27. package/dist/context/freshness.js.map +1 -0
  28. package/dist/context/handoff-prompt.d.ts +12 -0
  29. package/dist/context/handoff-prompt.d.ts.map +1 -0
  30. package/dist/context/handoff-prompt.js +29 -0
  31. package/dist/context/handoff-prompt.js.map +1 -0
  32. package/dist/index.d.ts +7 -4
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +5 -3
  35. package/dist/index.js.map +1 -1
  36. package/dist/mcp/server.d.ts +9 -0
  37. package/dist/mcp/server.d.ts.map +1 -0
  38. package/dist/mcp/server.js +85 -0
  39. package/dist/mcp/server.js.map +1 -0
  40. package/dist/mcp/tools.d.ts +97 -0
  41. package/dist/mcp/tools.d.ts.map +1 -0
  42. package/dist/mcp/tools.js +304 -0
  43. package/dist/mcp/tools.js.map +1 -0
  44. package/dist/registry.d.ts +40 -0
  45. package/dist/registry.d.ts.map +1 -0
  46. package/dist/registry.js +134 -0
  47. package/dist/registry.js.map +1 -0
  48. package/dist/restore/index.d.ts +13 -1
  49. package/dist/restore/index.d.ts.map +1 -1
  50. package/dist/restore/index.js +163 -43
  51. package/dist/restore/index.js.map +1 -1
  52. package/dist/setup/auto-setup.d.ts +8 -2
  53. package/dist/setup/auto-setup.d.ts.map +1 -1
  54. package/dist/setup/auto-setup.js +287 -67
  55. package/dist/setup/auto-setup.js.map +1 -1
  56. package/dist/setup/configure-hooks.d.ts +14 -3
  57. package/dist/setup/configure-hooks.d.ts.map +1 -1
  58. package/dist/setup/configure-hooks.js +156 -24
  59. package/dist/setup/configure-hooks.js.map +1 -1
  60. package/dist/snapshot/capture.d.ts +13 -9
  61. package/dist/snapshot/capture.d.ts.map +1 -1
  62. package/dist/snapshot/capture.js +42 -105
  63. package/dist/snapshot/capture.js.map +1 -1
  64. package/dist/snapshot/compress.d.ts +3 -3
  65. package/dist/snapshot/compress.d.ts.map +1 -1
  66. package/dist/snapshot/compress.js +35 -67
  67. package/dist/snapshot/compress.js.map +1 -1
  68. package/dist/snapshot/storage.d.ts +7 -0
  69. package/dist/snapshot/storage.d.ts.map +1 -1
  70. package/dist/snapshot/storage.js +31 -6
  71. package/dist/snapshot/storage.js.map +1 -1
  72. package/dist/threshold/state.d.ts +5 -0
  73. package/dist/threshold/state.d.ts.map +1 -1
  74. package/dist/threshold/state.js +28 -2
  75. package/dist/threshold/state.js.map +1 -1
  76. package/dist/threshold/token-usage.d.ts +29 -0
  77. package/dist/threshold/token-usage.d.ts.map +1 -0
  78. package/dist/threshold/token-usage.js +128 -0
  79. package/dist/threshold/token-usage.js.map +1 -0
  80. package/dist/trails/identity.d.ts +76 -0
  81. package/dist/trails/identity.d.ts.map +1 -0
  82. package/dist/trails/identity.js +117 -0
  83. package/dist/trails/identity.js.map +1 -0
  84. package/dist/trails/reader.d.ts +7 -0
  85. package/dist/trails/reader.d.ts.map +1 -0
  86. package/dist/trails/reader.js +22 -0
  87. package/dist/trails/reader.js.map +1 -0
  88. package/dist/trails/writer.d.ts +9 -0
  89. package/dist/trails/writer.d.ts.map +1 -0
  90. package/dist/trails/writer.js +110 -0
  91. package/dist/trails/writer.js.map +1 -0
  92. package/dist/transcript/extractor.d.ts +17 -4
  93. package/dist/transcript/extractor.d.ts.map +1 -1
  94. package/dist/transcript/extractor.js +141 -174
  95. package/dist/transcript/extractor.js.map +1 -1
  96. package/dist/transcript/parser.d.ts.map +1 -1
  97. package/dist/transcript/parser.js +69 -7
  98. package/dist/transcript/parser.js.map +1 -1
  99. package/dist/types.d.ts +37 -38
  100. package/dist/types.d.ts.map +1 -1
  101. package/hooks/hooks.json +12 -14
  102. package/package.json +13 -10
  103. package/scripts/install-plugin.sh +48 -0
  104. package/skills/context-continuity/SKILL.md +43 -24
  105. package/commands/activate.md +0 -20
  106. package/dist/transcript/estimator.d.ts +0 -20
  107. package/dist/transcript/estimator.d.ts.map +0 -1
  108. package/dist/transcript/estimator.js +0 -95
  109. package/dist/transcript/estimator.js.map +0 -1
package/dist/cli/index.js CHANGED
@@ -1,30 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import { existsSync, readdirSync, statSync } from 'node:fs';
4
- import { join } from 'node:path';
3
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
4
+ import { join, basename } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
6
  import { createInterface } from 'node:readline';
7
7
  import { captureSnapshot } from '../snapshot/capture.js';
8
8
  import { loadSnapshot, listSnapshots, readLatestMd, getSnapshotCount } from '../snapshot/storage.js';
9
9
  import { compressToMarkdown } from '../snapshot/compress.js';
10
+ import { readContextMd } from '../trails/reader.js';
10
11
  import { restoreContext } from '../restore/index.js';
11
12
  import { loadState, saveState } from '../threshold/state.js';
12
13
  import { checkTimeInterval } from '../threshold/time-based.js';
13
- import { shouldSnapshotByThreshold } from '../threshold/adaptive.js';
14
- import { quickEstimate } from '../transcript/estimator.js';
14
+ import { handledThresholdsForUsage, newlyCrossedThresholds, readLatestContextUsage, } from '../threshold/token-usage.js';
15
+ import { buildHandoffPrompt } from '../context/handoff-prompt.js';
16
+ import { isContextMdFresh } from '../context/freshness.js';
15
17
  import { loadConfig, getStoragePath, writeConfig } from '../config.js';
16
18
  import { ensureProjectBootstrapped, setupProject } from '../setup/auto-setup.js';
19
+ import { findById, pruneStale, getLastProject, loadRegistry } from '../registry.js';
20
+ import { parseIdentity } from '../trails/identity.js';
17
21
  const program = new Command();
18
22
  program
19
23
  .name('bookmark')
20
24
  .description('Context snapshots for Claude Code — session continuity across compactions and terminals')
21
- .version('0.1.0');
25
+ .version('0.3.2');
22
26
  // ─── Hook Commands (invoked by hooks, not users) ───
23
27
  program
24
28
  .command('snapshot')
25
29
  .description('Capture a context snapshot')
26
- .option('--trigger <type>', 'Trigger type: pre_compact|time_interval|manual|session_end', 'manual')
27
- .option('--smart', 'Use Claude Haiku for enhanced extraction')
30
+ .option('--trigger <type>', 'Trigger type: pre_compact|token_threshold|time_interval|manual|session_end', 'manual')
28
31
  .option('--transcript <path>', 'Path to transcript JSONL')
29
32
  .option('--session-id <id>', 'Session ID')
30
33
  .option('--cwd <path>', 'Working directory')
@@ -36,11 +39,15 @@ program
36
39
  let transcriptPath = opts.transcript ?? hookInput?.transcript_path;
37
40
  const sessionId = opts.sessionId ?? hookInput?.session_id;
38
41
  if (!transcriptPath) {
39
- // Try to discover transcript from ~/.claude/projects/
40
42
  transcriptPath = discoverTranscriptPath(cwd);
41
43
  if (!transcriptPath) {
42
- console.error('No transcript found. This command is typically called by hooks.');
43
- process.exit(1);
44
+ if (opts.trigger === 'manual') {
45
+ console.log('No transcript found. Using existing Bookmark trails for this manual checkpoint.');
46
+ }
47
+ else {
48
+ console.error('No transcript found. This command is typically called by hooks.');
49
+ process.exit(1);
50
+ }
44
51
  }
45
52
  }
46
53
  const snapshot = await captureSnapshot({
@@ -48,14 +55,11 @@ program
48
55
  transcriptPath,
49
56
  cwd,
50
57
  sessionId,
51
- smart: opts.smart,
52
58
  });
53
59
  console.log(`Snapshot captured: ${snapshot.snapshot_id}`);
54
60
  console.log(` Trigger: ${snapshot.trigger}`);
55
- console.log(` Decisions: ${snapshot.decisions.length}`);
56
61
  console.log(` Files changed: ${snapshot.files_changed.length}`);
57
- console.log(` Open items: ${snapshot.open_items.length}`);
58
- console.log(` Context remaining: ${Math.round(snapshot.context_remaining_pct * 100)}%`);
62
+ console.log(` Tools tracked: ${Object.keys(snapshot.tools_summary).length}`);
59
63
  }
60
64
  catch (err) {
61
65
  console.error('Snapshot failed:', err.message);
@@ -91,7 +95,7 @@ program
91
95
  });
92
96
  program
93
97
  .command('check')
94
- .description('Check time/threshold intervals (for UserPromptSubmit hook)')
98
+ .description('Check the time interval (async UserPromptSubmit hook)')
95
99
  .option('--transcript <path>', 'Path to transcript JSONL')
96
100
  .option('--cwd <path>', 'Working directory')
97
101
  .action(async (opts) => {
@@ -105,20 +109,13 @@ program
105
109
  const state = loadState(storagePath);
106
110
  let shouldCapture = false;
107
111
  let reason = '';
108
- // Check time interval
112
+ // Legacy interval-only command. Installed hooks use context-check so
113
+ // interval capture and measured token usage share one state writer.
109
114
  const timeCheck = checkTimeInterval(state);
110
115
  if (timeCheck.shouldSnapshot) {
111
116
  shouldCapture = true;
112
117
  reason = timeCheck.reason ?? 'time interval';
113
118
  }
114
- // Check adaptive threshold
115
- if (!shouldCapture && transcriptPath) {
116
- const { remainingPct } = quickEstimate(transcriptPath, config.contextLimitTokens, config.charsPerToken);
117
- if (shouldSnapshotByThreshold(remainingPct, state.compaction_count, config)) {
118
- shouldCapture = true;
119
- reason = `context at ${Math.round(remainingPct * 100)}% remaining (threshold: ${Math.round(state.current_threshold * 100)}%)`;
120
- }
121
- }
122
119
  if (shouldCapture && transcriptPath) {
123
120
  const snapshot = await captureSnapshot({
124
121
  trigger: 'time_interval',
@@ -137,6 +134,269 @@ program
137
134
  // Silent — never break user prompt flow
138
135
  }
139
136
  });
137
+ program
138
+ .command('context-check')
139
+ .description('Capture and notify when context usage crosses a token threshold')
140
+ .option('--transcript <path>', 'Path to transcript JSONL')
141
+ .option('--cwd <path>', 'Working directory')
142
+ .action(async (opts) => {
143
+ try {
144
+ const hookInput = await readHookInput();
145
+ const cwd = opts.cwd ?? hookInput?.cwd ?? process.cwd();
146
+ ensureProjectBootstrapped(cwd);
147
+ const transcriptPath = opts.transcript ?? hookInput?.transcript_path;
148
+ if (!transcriptPath) {
149
+ console.log(JSON.stringify({}));
150
+ return;
151
+ }
152
+ const config = loadConfig(cwd);
153
+ const storagePath = getStoragePath(cwd, config);
154
+ const state = loadState(storagePath);
155
+ const timeCheck = checkTimeInterval(state);
156
+ const observation = readLatestContextUsage(transcriptPath, config.contextLimitTokens, hookInput?.prompt);
157
+ if (!observation) {
158
+ if (timeCheck.shouldSnapshot) {
159
+ await captureSnapshot({
160
+ trigger: 'time_interval',
161
+ transcriptPath,
162
+ cwd,
163
+ sessionId: hookInput?.session_id,
164
+ });
165
+ }
166
+ console.log(JSON.stringify({}));
167
+ return;
168
+ }
169
+ if (observation.status === 'unknown_context_limit') {
170
+ const shouldNotify = state.unknown_context_limit_notified_model !== observation.model;
171
+ const observedState = {
172
+ ...state,
173
+ last_event_time: Date.now(),
174
+ latest_model: observation.model,
175
+ latest_context_tokens: observation.usedTokens,
176
+ latest_context_limit_tokens: undefined,
177
+ latest_context_used_pct: undefined,
178
+ latest_context_observed_at: Date.now(),
179
+ unknown_context_limit_notified_model: observation.model,
180
+ };
181
+ if (timeCheck.shouldSnapshot) {
182
+ await captureSnapshot({
183
+ trigger: 'time_interval',
184
+ transcriptPath,
185
+ cwd,
186
+ sessionId: hookInput?.session_id,
187
+ });
188
+ const refreshedState = loadState(storagePath);
189
+ saveState(storagePath, {
190
+ ...refreshedState,
191
+ last_event_time: observedState.last_event_time,
192
+ latest_model: observedState.latest_model,
193
+ latest_context_tokens: observedState.latest_context_tokens,
194
+ latest_context_limit_tokens: observedState.latest_context_limit_tokens,
195
+ latest_context_used_pct: observedState.latest_context_used_pct,
196
+ latest_context_observed_at: observedState.latest_context_observed_at,
197
+ unknown_context_limit_notified_model: observedState.unknown_context_limit_notified_model,
198
+ });
199
+ }
200
+ else {
201
+ saveState(storagePath, observedState);
202
+ }
203
+ if (shouldNotify) {
204
+ console.log(JSON.stringify({
205
+ systemMessage: `Bookmark detected ${observation.model}, but its context limit is not verified. ` +
206
+ 'Token-threshold capture is paused for this model. ' +
207
+ 'Set the verified limit with `bookmark config --context-limit <tokens>`. ' +
208
+ `Manual snapshots and ${state.snapshot_interval_minutes}-minute periodic snapshots remain active.`,
209
+ }));
210
+ }
211
+ else {
212
+ console.log(JSON.stringify({}));
213
+ }
214
+ return;
215
+ }
216
+ const usage = observation;
217
+ const observedState = {
218
+ ...state,
219
+ last_event_time: Date.now(),
220
+ latest_model: usage.model,
221
+ latest_context_tokens: usage.usedTokens,
222
+ latest_context_limit_tokens: usage.contextLimitTokens,
223
+ latest_context_used_pct: usage.usedFraction,
224
+ latest_context_observed_at: Date.now(),
225
+ unknown_context_limit_notified_model: undefined,
226
+ };
227
+ const handledThresholds = handledThresholdsForUsage(usage.usedFraction, config.tokenThresholds, state.token_thresholds_triggered);
228
+ observedState.token_thresholds_triggered = handledThresholds;
229
+ const crossed = newlyCrossedThresholds(usage.usedFraction, config.tokenThresholds, handledThresholds);
230
+ if (crossed.length === 0) {
231
+ if (timeCheck.shouldSnapshot) {
232
+ await captureSnapshot({
233
+ trigger: 'time_interval',
234
+ transcriptPath,
235
+ cwd,
236
+ sessionId: hookInput?.session_id,
237
+ });
238
+ const refreshedState = loadState(storagePath);
239
+ saveState(storagePath, {
240
+ ...refreshedState,
241
+ last_event_time: observedState.last_event_time,
242
+ latest_model: observedState.latest_model,
243
+ latest_context_tokens: observedState.latest_context_tokens,
244
+ latest_context_limit_tokens: observedState.latest_context_limit_tokens,
245
+ latest_context_used_pct: observedState.latest_context_used_pct,
246
+ latest_context_observed_at: observedState.latest_context_observed_at,
247
+ unknown_context_limit_notified_model: observedState.unknown_context_limit_notified_model,
248
+ token_thresholds_triggered: observedState.token_thresholds_triggered,
249
+ });
250
+ }
251
+ else {
252
+ saveState(storagePath, observedState);
253
+ }
254
+ console.log(JSON.stringify({}));
255
+ return;
256
+ }
257
+ const snapshot = await captureSnapshot({
258
+ trigger: 'token_threshold',
259
+ transcriptPath,
260
+ cwd,
261
+ sessionId: hookInput?.session_id,
262
+ contextUsage: usage,
263
+ });
264
+ const refreshedState = loadState(storagePath);
265
+ saveState(storagePath, {
266
+ ...refreshedState,
267
+ latest_model: usage.model,
268
+ latest_context_tokens: usage.usedTokens,
269
+ latest_context_limit_tokens: usage.contextLimitTokens,
270
+ latest_context_used_pct: usage.usedFraction,
271
+ latest_context_observed_at: Date.now(),
272
+ unknown_context_limit_notified_model: undefined,
273
+ token_thresholds_triggered: [
274
+ ...new Set([...(handledThresholds ?? []), ...crossed]),
275
+ ],
276
+ });
277
+ const threshold = crossed[crossed.length - 1];
278
+ const thresholdPct = Math.round(threshold * 100);
279
+ const usedPct = Math.round(usage.usedFraction * 100);
280
+ const reason = `Context usage crossed ${thresholdPct}% (${usedPct}% observed; snapshot ${snapshot.snapshot_id}).`;
281
+ const handoffPrompt = buildHandoffPrompt({ cwd, reason });
282
+ console.log(JSON.stringify({
283
+ systemMessage: `Bookmark captured ${snapshot.snapshot_id} at ${usedPct}% of the ${usage.model} context window. ` +
284
+ 'Bookmark asked Claude to update the handoff. Start a new session after this response to restore it.',
285
+ hookSpecificOutput: {
286
+ hookEventName: 'UserPromptSubmit',
287
+ additionalContext: `${handoffPrompt}\nAfter successfully writing it, tell the user to start a new Claude session with /clear or by exiting and reopening Claude Code.`,
288
+ },
289
+ }));
290
+ }
291
+ catch {
292
+ // Context protection must never block the user's prompt.
293
+ console.log(JSON.stringify({}));
294
+ }
295
+ });
296
+ program
297
+ .command('stop')
298
+ .description('Stop hook — capture files, conditionally block for bookmark.context.md (for Stop hook)')
299
+ .option('--cwd <path>', 'Working directory')
300
+ .action(async (opts) => {
301
+ try {
302
+ const hookInput = await readHookInput();
303
+ const cwd = opts.cwd ?? hookInput?.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
304
+ ensureProjectBootstrapped(cwd);
305
+ // Always capture file tracking snapshot
306
+ const transcriptPath = hookInput?.transcript_path ?? discoverTranscriptPath(cwd);
307
+ if (transcriptPath) {
308
+ try {
309
+ await captureSnapshot({
310
+ trigger: 'session_end',
311
+ transcriptPath,
312
+ cwd,
313
+ sessionId: hookInput?.session_id ?? process.env.CLAUDE_SESSION_ID,
314
+ });
315
+ }
316
+ catch { /* file tracking is best-effort */ }
317
+ }
318
+ // Update home-scope pointer to track the most recently active project
319
+ updateHomePointer(cwd);
320
+ const config = loadConfig(cwd);
321
+ const storagePath = getStoragePath(cwd, config);
322
+ const contextPath = join(storagePath, 'bookmark.context.md');
323
+ const markerPath = join(storagePath, '.stop-requested');
324
+ // Quality gate: check if bookmark.context.md has real content
325
+ if (isContextMdFresh(contextPath, markerPath)) {
326
+ console.log(JSON.stringify({ decision: 'approve' }));
327
+ return;
328
+ }
329
+ // Check .stop-requested marker — max 1 block to prevent loops
330
+ if (existsSync(markerPath)) {
331
+ // Already blocked once — approve to prevent infinite loop
332
+ console.log(JSON.stringify({ decision: 'approve' }));
333
+ return;
334
+ }
335
+ // First time — write JSON marker, track the block, and block
336
+ const marker = JSON.stringify({
337
+ timestamp: Date.now(),
338
+ session_id: hookInput?.session_id ?? process.env.CLAUDE_SESSION_ID ?? 'unknown',
339
+ });
340
+ writeFileSync(markerPath, marker, 'utf-8');
341
+ try {
342
+ const st = loadState(storagePath);
343
+ st.quality_blocks = (st.quality_blocks ?? 0) + 1;
344
+ saveState(storagePath, st);
345
+ }
346
+ catch { /* tracking is best-effort */ }
347
+ const handoffPrompt = buildHandoffPrompt({
348
+ cwd,
349
+ reason: 'The session is stopping and needs a durable handoff.',
350
+ });
351
+ console.log(JSON.stringify({
352
+ decision: 'block',
353
+ reason: handoffPrompt,
354
+ systemMessage: 'Bookmark needs Claude to refresh the session handoff before stopping.',
355
+ }));
356
+ }
357
+ catch (err) {
358
+ // Log error for debugging, then approve to never block stop
359
+ try {
360
+ const config = loadConfig(opts.cwd ?? process.cwd());
361
+ const storagePath = getStoragePath(opts.cwd ?? process.cwd(), config);
362
+ const errLog = join(storagePath, '.errors.log');
363
+ appendFileSync(errLog, `[${new Date().toISOString()}] stop: ${err.message}\n`);
364
+ }
365
+ catch { /* truly silent */ }
366
+ console.log(JSON.stringify({ decision: 'approve' }));
367
+ }
368
+ });
369
+ program
370
+ .command('precompact')
371
+ .description('PreCompact hook — capture a mechanical checkpoint')
372
+ .option('--cwd <path>', 'Working directory')
373
+ .action(async (opts) => {
374
+ try {
375
+ const hookInput = await readHookInput();
376
+ const cwd = opts.cwd ?? hookInput?.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
377
+ ensureProjectBootstrapped(cwd);
378
+ // Always capture file tracking snapshot
379
+ const transcriptPath = hookInput?.transcript_path ?? discoverTranscriptPath(cwd);
380
+ if (transcriptPath) {
381
+ try {
382
+ await captureSnapshot({
383
+ trigger: 'pre_compact',
384
+ transcriptPath,
385
+ cwd,
386
+ sessionId: hookInput?.session_id ?? process.env.CLAUDE_SESSION_ID,
387
+ });
388
+ }
389
+ catch { /* file tracking is best-effort */ }
390
+ }
391
+ // Claude Code discards PreCompact systemMessage output. The synchronous
392
+ // 75% UserPromptSubmit hook is the semantic-handoff activation path.
393
+ console.log(JSON.stringify({}));
394
+ }
395
+ catch {
396
+ // Never block compaction
397
+ console.log(JSON.stringify({}));
398
+ }
399
+ });
140
400
  // ─── User-Facing Commands ───
141
401
  program
142
402
  .command('status')
@@ -154,8 +414,21 @@ program
154
414
  console.log('═══════════════');
155
415
  console.log(` Snapshots: ${count}`);
156
416
  console.log(` Compaction cycles: ${state.compaction_count}`);
157
- console.log(` Current threshold: ${Math.round(state.current_threshold * 100)}% remaining`);
158
417
  console.log(` Snapshot interval: ${state.snapshot_interval_minutes} minutes`);
418
+ console.log(` New-session alert: ${config.tokenThresholds.map(t => `${Math.round(t * 100)}%`).join(', ')} used when limit is known`);
419
+ if (state.latest_context_used_pct !== undefined) {
420
+ const usedPct = Math.round(state.latest_context_used_pct * 100);
421
+ const usedTokens = state.latest_context_tokens ?? 0;
422
+ const limitTokens = state.latest_context_limit_tokens ?? 0;
423
+ console.log(` Context usage: ${usedPct}% (${formatTokenCount(usedTokens)} / ${formatTokenCount(limitTokens)})`);
424
+ if (state.latest_model)
425
+ console.log(` Active model: ${state.latest_model}`);
426
+ }
427
+ else if (state.latest_model && state.latest_context_tokens !== undefined) {
428
+ console.log(` Context usage: ${formatTokenCount(state.latest_context_tokens)} tokens; limit unknown`);
429
+ console.log(` Active model: ${state.latest_model}`);
430
+ console.log(' Required action: bookmark config --context-limit <tokens>');
431
+ }
159
432
  if (state.last_snapshot_time > 0) {
160
433
  const ago = Math.round((Date.now() - state.last_snapshot_time) / 60_000);
161
434
  console.log(` Last snapshot: ${ago} minutes ago`);
@@ -163,13 +436,31 @@ program
163
436
  else {
164
437
  console.log(` Last snapshot: never`);
165
438
  }
439
+ // Usage counters — only show when there's data
440
+ const restores = state.restores_performed ?? 0;
441
+ const tokensInjected = state.tokens_injected ?? 0;
442
+ const blocks = state.quality_blocks ?? 0;
443
+ const caught = state.boilerplate_caught ?? 0;
444
+ if (restores > 0 || blocks > 0) {
445
+ console.log('');
446
+ console.log('Usage:');
447
+ if (restores > 0) {
448
+ console.log(` Sessions restored: ${restores}`);
449
+ console.log(` Tokens injected: ~${tokensInjected}`);
450
+ }
451
+ if (blocks > 0) {
452
+ console.log(` Quality blocks: ${blocks} (asked Claude to write bookmark.context.md)`);
453
+ }
454
+ if (caught > 0) {
455
+ console.log(` Boilerplate caught: ${caught} (skipped stale restore)`);
456
+ }
457
+ }
166
458
  if (entries.length > 0) {
167
459
  console.log('');
168
460
  console.log('Recent Snapshots:');
169
461
  for (const entry of entries) {
170
462
  const date = new Date(entry.timestamp).toLocaleString();
171
- const pct = Math.round(entry.context_remaining_pct * 100);
172
- console.log(` ${entry.id} ${entry.trigger.padEnd(14)} ${pct}% ctx ${date}`);
463
+ console.log(` ${entry.id} ${entry.trigger.padEnd(14)} ${date}`);
173
464
  }
174
465
  }
175
466
  console.log('');
@@ -178,23 +469,50 @@ program
178
469
  .command('list')
179
470
  .description('List available snapshots')
180
471
  .option('--limit <n>', 'Max snapshots to show', '10')
472
+ .option('--all', 'List snapshots across all projects (from global registry)')
181
473
  .option('--cwd <path>', 'Working directory')
182
474
  .action((opts) => {
475
+ const limit = parseInt(opts.limit, 10);
476
+ if (opts.all) {
477
+ const registry = pruneStale();
478
+ const entries = registry.snapshots.slice(0, limit);
479
+ if (entries.length === 0) {
480
+ console.log('No snapshots in global registry yet.');
481
+ return;
482
+ }
483
+ console.log('');
484
+ console.log('ID Project Trigger Files Time');
485
+ console.log('──────────────────── ──────────────────────── ─────────────── ───── ────────────────────');
486
+ for (const entry of entries) {
487
+ const date = new Date(entry.timestamp).toLocaleString();
488
+ const proj = entry.project_name.length > 24 ? entry.project_name.slice(0, 23) + '…' : entry.project_name;
489
+ console.log(`${entry.id} ${proj.padEnd(24)} ${entry.trigger.padEnd(15)} ${String(entry.files_changed_count).padStart(5)} ${date}`);
490
+ }
491
+ console.log('');
492
+ return;
493
+ }
183
494
  const cwd = opts.cwd ?? process.cwd();
184
495
  const config = loadConfig(cwd);
185
496
  const storagePath = getStoragePath(cwd, config);
186
- const entries = listSnapshots(storagePath, parseInt(opts.limit, 10));
497
+ const entries = listSnapshots(storagePath, limit);
187
498
  if (entries.length === 0) {
188
- console.log('No snapshots found. Run `/bookmark:snapshot` to create one.');
499
+ const last = getLastProject();
500
+ if (last && last.path !== cwd) {
501
+ console.log('No snapshots in this project.');
502
+ console.log(`Last active project: ${last.name} (${last.path})`);
503
+ console.log('Run `bookmark list --all` for the cross-project view.');
504
+ }
505
+ else {
506
+ console.log('No snapshots found. Run `/bookmark:snapshot` to create one.');
507
+ }
189
508
  return;
190
509
  }
191
510
  console.log('');
192
- console.log('ID Trigger Ctx% Decisions Files Open Items Time');
193
- console.log('──────────────────── ─────────────── ───── ───────── ───── ────────── ────────────────────');
511
+ console.log('ID Trigger Files Time');
512
+ console.log('──────────────────── ─────────────── ───── ────────────────────');
194
513
  for (const entry of entries) {
195
514
  const date = new Date(entry.timestamp).toLocaleString();
196
- const pct = `${Math.round(entry.context_remaining_pct * 100)}%`.padStart(4);
197
- console.log(`${entry.id} ${entry.trigger.padEnd(15)} ${pct} ${String(entry.decisions_count).padStart(9)} ${String(entry.files_changed_count).padStart(5)} ${String(entry.open_items_count).padStart(10)} ${date}`);
515
+ console.log(`${entry.id} ${entry.trigger.padEnd(15)} ${String(entry.files_changed_count).padStart(5)} ${date}`);
198
516
  }
199
517
  console.log('');
200
518
  });
@@ -208,38 +526,192 @@ program
208
526
  const config = loadConfig(cwd);
209
527
  const storagePath = getStoragePath(cwd, config);
210
528
  if (opts.latest || !snapshotId) {
211
- const md = readLatestMd(storagePath);
212
- if (md) {
213
- console.log(md);
214
- }
215
- else {
529
+ const contextMd = readContextMd(storagePath);
530
+ const latestMd = readLatestMd(storagePath);
531
+ if (!contextMd && !latestMd) {
216
532
  console.log('No snapshots found.');
533
+ return;
534
+ }
535
+ if (contextMd) {
536
+ console.log(contextMd);
537
+ }
538
+ if (contextMd && latestMd) {
539
+ console.log('');
540
+ console.log('─── File Trail ───');
541
+ console.log('');
542
+ }
543
+ if (latestMd) {
544
+ console.log(latestMd);
217
545
  }
218
546
  return;
219
547
  }
220
- const snapshot = loadSnapshot(storagePath, snapshotId);
548
+ let snapshot = loadSnapshot(storagePath, snapshotId);
549
+ // Registry fallback: not in current CWD, maybe it belongs to another project
550
+ if (!snapshot) {
551
+ const entry = findById(snapshotId);
552
+ if (entry) {
553
+ const otherStorage = join(entry.project_path, config.storagePath);
554
+ snapshot = loadSnapshot(otherStorage, snapshotId);
555
+ if (snapshot) {
556
+ console.log(`[bookmark: resolved via registry → ${entry.project_name} (${entry.project_path})]`);
557
+ console.log('');
558
+ }
559
+ }
560
+ }
221
561
  if (!snapshot) {
222
562
  console.log(`Snapshot not found: ${snapshotId}`);
223
563
  return;
224
564
  }
225
565
  console.log(compressToMarkdown(snapshot));
226
566
  });
567
+ program
568
+ .command('clear')
569
+ .description('Write a home-scope pointer to the most-recent project and print its context')
570
+ .option('--project <path>', 'Point at a specific project root instead of the last-active one')
571
+ .option('--no-print', 'Skip printing the canonical context to stdout')
572
+ .option('--no-pointer', 'Skip writing ~/.bookmark/bookmark.context.md pointer')
573
+ .action((opts) => {
574
+ // Resolve target project:
575
+ // 1. explicit --project wins
576
+ // 2. else registry.last_project, but skip if it points at $HOME (stale/bad state)
577
+ // 3. else most-recent snapshot whose project_path !== $HOME and has a bookmark.context.md
578
+ const home = homedir();
579
+ let targetPath = null;
580
+ let targetName = null;
581
+ if (opts.project) {
582
+ targetPath = opts.project;
583
+ targetName = basename(opts.project);
584
+ }
585
+ else {
586
+ const last = getLastProject();
587
+ if (last && last.path !== home) {
588
+ targetPath = last.path;
589
+ targetName = last.name;
590
+ }
591
+ else {
592
+ // Fall back: scan registry snapshots for the most recent real project.
593
+ const registry = loadRegistry();
594
+ for (const entry of registry.snapshots) {
595
+ if (entry.project_path === home)
596
+ continue;
597
+ const candidate = join(entry.project_path, '.bookmark', 'bookmark.context.md');
598
+ if (existsSync(candidate)) {
599
+ targetPath = entry.project_path;
600
+ targetName = entry.project_name;
601
+ break;
602
+ }
603
+ }
604
+ }
605
+ }
606
+ if (!targetPath || !targetName) {
607
+ console.error('No last-active project found in the registry. Run `bookmark list --all`.');
608
+ console.error('Or pass --project <path> to point at a specific project.');
609
+ process.exit(1);
610
+ }
611
+ let canonicalPath = join(targetPath, '.bookmark', 'bookmark.context.md');
612
+ if (!existsSync(canonicalPath)) {
613
+ console.error(`No bookmark.context.md at ${canonicalPath}.`);
614
+ console.error('That project has no session summary to restore from.');
615
+ process.exit(1);
616
+ }
617
+ let canonicalContent = readFileSync(canonicalPath, 'utf-8');
618
+ // If target is itself a home-scope pointer, follow it to the real canonical file.
619
+ const { identity } = parseIdentity(canonicalContent);
620
+ if (identity?.scope === 'home' && identity.points_to_canonical && existsSync(identity.points_to_canonical)) {
621
+ canonicalPath = identity.points_to_canonical;
622
+ canonicalContent = readFileSync(canonicalPath, 'utf-8');
623
+ if (identity.points_to_project)
624
+ targetName = identity.points_to_project;
625
+ const marker = '/.bookmark/';
626
+ const idx = canonicalPath.lastIndexOf(marker);
627
+ if (idx > 0)
628
+ targetPath = canonicalPath.slice(0, idx);
629
+ }
630
+ if (opts.pointer !== false) {
631
+ const homeDir = join(homedir(), '.bookmark');
632
+ if (!existsSync(homeDir))
633
+ mkdirSync(homeDir, { recursive: true });
634
+ const pointerBody = [
635
+ `# Bookmark Pointer — ${targetName}`,
636
+ '',
637
+ '<!-- BOOKMARK_IDENTITY',
638
+ 'scope: home',
639
+ 'project: POINTER_ONLY',
640
+ `points_to_project: ${targetName}`,
641
+ `points_to_canonical: ${canonicalPath}`,
642
+ `written: ${new Date().toISOString().slice(0, 10)}`,
643
+ 'written_by: bookmark-clear',
644
+ '-->',
645
+ '',
646
+ `Redirects SessionStart to: ${targetName}`,
647
+ `Canonical file: ${canonicalPath}`,
648
+ '',
649
+ '*This file was written by `bookmark clear`. The next Claude Code session',
650
+ 'started from this directory (or any directory without its own .bookmark/)',
651
+ 'will follow the pointer above and restore the canonical context.*',
652
+ ].join('\n');
653
+ writeFileSync(join(homeDir, 'bookmark.context.md'), pointerBody, 'utf-8');
654
+ console.error(`[bookmark] Home pointer → ${targetName} (${canonicalPath})`);
655
+ }
656
+ if (opts.print !== false) {
657
+ console.log(canonicalContent);
658
+ }
659
+ });
227
660
  program
228
661
  .command('config')
229
662
  .description('Show or set configuration')
230
663
  .option('--interval <minutes>', 'Set time-based interval')
231
- .option('--smart-default', 'Enable LLM-enhanced extraction by default')
232
- .option('--no-smart-default', 'Disable LLM-enhanced extraction')
664
+ .option('--token-threshold <fraction>', 'Set context-used threshold (0.75 or 75)')
665
+ .option('--context-limit <tokens>', 'Override model context limit')
233
666
  .option('--cwd <path>', 'Working directory')
234
667
  .action((opts) => {
235
668
  const cwd = opts.cwd ?? process.cwd();
236
669
  const config = loadConfig(cwd);
237
670
  const storagePath = getStoragePath(cwd, config);
238
- if (opts.interval) {
239
- const state = loadState(storagePath);
240
- state.snapshot_interval_minutes = parseInt(opts.interval, 10);
241
- saveState(storagePath, state);
242
- console.log(`Snapshot interval set to ${opts.interval} minutes`);
671
+ if (opts.interval || opts.tokenThreshold || opts.contextLimit) {
672
+ const preferences = {};
673
+ if (opts.interval) {
674
+ const interval = Number.parseInt(opts.interval, 10);
675
+ if (!Number.isFinite(interval) || interval <= 0) {
676
+ console.error('Interval must be a positive number of minutes.');
677
+ process.exitCode = 1;
678
+ return;
679
+ }
680
+ preferences.intervalMinutes = interval;
681
+ }
682
+ if (opts.tokenThreshold) {
683
+ const threshold = normalizeThreshold(Number(opts.tokenThreshold));
684
+ if (threshold === null) {
685
+ console.error('Token threshold must be between 0 and 1, or between 1 and 100 percent.');
686
+ process.exitCode = 1;
687
+ return;
688
+ }
689
+ preferences.tokenThresholds = [threshold];
690
+ }
691
+ if (opts.contextLimit) {
692
+ const limit = Number.parseInt(opts.contextLimit, 10);
693
+ if (!Number.isFinite(limit) || limit <= 0) {
694
+ console.error('Context limit must be a positive token count.');
695
+ process.exitCode = 1;
696
+ return;
697
+ }
698
+ preferences.contextLimitTokens = limit;
699
+ }
700
+ writeConfig(cwd, preferences);
701
+ if (preferences.intervalMinutes !== undefined) {
702
+ const state = loadState(storagePath);
703
+ state.snapshot_interval_minutes = preferences.intervalMinutes;
704
+ saveState(storagePath, state);
705
+ }
706
+ if (preferences.intervalMinutes !== undefined) {
707
+ console.log(`Snapshot interval set to ${preferences.intervalMinutes} minutes`);
708
+ }
709
+ if (preferences.tokenThresholds) {
710
+ console.log(`Token threshold set to ${Math.round(preferences.tokenThresholds[0] * 100)}% used`);
711
+ }
712
+ if (preferences.contextLimitTokens) {
713
+ console.log(`Context limit set to ${preferences.contextLimitTokens} tokens`);
714
+ }
243
715
  return;
244
716
  }
245
717
  console.log('');
@@ -247,15 +719,15 @@ program
247
719
  console.log('══════════════════════');
248
720
  console.log(` Storage path: ${config.storagePath}`);
249
721
  console.log(` Interval: ${config.intervalMinutes} minutes`);
250
- console.log(` Thresholds: ${config.thresholds.map(t => `${Math.round(t * 100)}%`).join(', ')}`);
251
- console.log(` Context limit: ${config.contextLimitTokens.toLocaleString()} tokens`);
252
- console.log(` Smart default: ${config.smartDefault}`);
722
+ console.log(` Token thresholds: ${config.tokenThresholds.map(t => `${Math.round(t * 100)}% used`).join(', ')}`);
723
+ console.log(` Context limit: ${config.contextLimitTokens ? formatTokenCount(config.contextLimitTokens) : 'model-aware'}`);
253
724
  console.log(` Max snapshots: ${config.maxActiveSnapshots}`);
254
725
  console.log(` Archive after: ${config.archiveAfterDays} days`);
255
726
  console.log('');
256
727
  console.log('Environment overrides:');
257
- console.log(' BOOKMARK_INTERVAL, BOOKMARK_THRESHOLD, BOOKMARK_STORAGE_PATH');
258
- console.log(' BOOKMARK_CONTEXT_LIMIT, BOOKMARK_SMART, ANTHROPIC_API_KEY');
728
+ console.log(' BOOKMARK_INTERVAL, BOOKMARK_TOKEN_THRESHOLD, BOOKMARK_CONTEXT_LIMIT');
729
+ console.log(' BOOKMARK_STORAGE_PATH');
730
+ console.log(' BOOKMARK_VERBOSE');
259
731
  console.log('');
260
732
  });
261
733
  program
@@ -292,6 +764,70 @@ program
292
764
  }
293
765
  });
294
766
  // ─── Helpers ───
767
+ /**
768
+ * Update the home-directory pointer at ~/.bookmark/bookmark.context.md to point
769
+ * at the project that just ended a session.
770
+ *
771
+ * Only writes when:
772
+ * - cwd is NOT the home directory (no self-pointing)
773
+ * - the project's .bookmark/bookmark.context.md actually exists
774
+ *
775
+ * Wrapped in try/catch — never breaks the main stop flow.
776
+ */
777
+ function updateHomePointer(cwd) {
778
+ try {
779
+ const home = homedir();
780
+ if (!home || cwd === home)
781
+ return;
782
+ const projectContextPath = join(cwd, '.bookmark', 'bookmark.context.md');
783
+ if (!existsSync(projectContextPath))
784
+ return;
785
+ // Derive a short project name from the directory basename
786
+ const projectName = basename(cwd)
787
+ .toLowerCase()
788
+ .replace(/\s+/g, '-')
789
+ .replace(/[^a-z0-9-]/g, '');
790
+ const homeBookmarkDir = join(home, '.bookmark');
791
+ if (!existsSync(homeBookmarkDir)) {
792
+ mkdirSync(homeBookmarkDir, { recursive: true });
793
+ }
794
+ const today = new Date().toISOString().slice(0, 10);
795
+ const pointerContent = [
796
+ '# Active Work Pointer (home-scoped bookmark)',
797
+ '',
798
+ '<!-- BOOKMARK_IDENTITY',
799
+ 'scope: home',
800
+ 'project: POINTER_ONLY',
801
+ `points_to_project: ${projectName}`,
802
+ `points_to_repo: ${cwd}`,
803
+ `points_to_canonical: ${projectContextPath}`,
804
+ `written: ${today}`,
805
+ 'written_by: bookmark-plugin',
806
+ '-->',
807
+ '',
808
+ '> **This is the HOME-directory pointer**, not a full session context.',
809
+ `> It delegates to the repo-scope bookmark at \`${projectContextPath}\`.`,
810
+ '',
811
+ ].join('\n');
812
+ writeFileSync(join(homeBookmarkDir, 'bookmark.context.md'), pointerContent, 'utf-8');
813
+ }
814
+ catch {
815
+ // Never propagate — pointer update is best-effort
816
+ }
817
+ }
818
+ function formatTokenCount(value) {
819
+ if (value >= 1_000_000)
820
+ return `${(value / 1_000_000).toFixed(value % 1_000_000 === 0 ? 0 : 1)}M`;
821
+ if (value >= 1_000)
822
+ return `${Math.round(value / 1_000)}K`;
823
+ return String(value);
824
+ }
825
+ function normalizeThreshold(value) {
826
+ if (!Number.isFinite(value) || value <= 0)
827
+ return null;
828
+ const normalized = value > 1 ? value / 100 : value;
829
+ return normalized > 0 && normalized < 1 ? normalized : null;
830
+ }
295
831
  const GREEN = '\x1b[32m';
296
832
  const CYAN = '\x1b[36m';
297
833
  const DIM = '\x1b[2m';
@@ -300,6 +836,9 @@ const RESET = '\x1b[0m';
300
836
  /**
301
837
  * Discover the most recent transcript file for the given working directory.
302
838
  * Claude Code stores transcripts in ~/.claude/projects/<encoded-path>/*.jsonl
839
+ *
840
+ * Uses CLAUDE_SESSION_ID env var if available (set by Claude Code hooks),
841
+ * otherwise falls back to finding the most recent JSONL by file size + recency.
303
842
  */
304
843
  function discoverTranscriptPath(cwd) {
305
844
  const claudeProjectsDir = join(homedir(), '.claude', 'projects');
@@ -310,21 +849,38 @@ function discoverTranscriptPath(cwd) {
310
849
  // Claude Code encodes paths by replacing / with -
311
850
  // e.g., /Users/tyroneross/myproject → -Users-tyroneross-myproject
312
851
  const encodedCwd = cwd.replace(/\//g, '-');
313
- const matchingDir = projectDirs.find(d => d === encodedCwd || d.includes(encodedCwd) || encodedCwd.includes(d));
852
+ // Exact match only fuzzy matching caused cross-project contamination
853
+ // where e.g. FloDoro would match bookmark's transcript dir via startsWith
854
+ const matchingDir = projectDirs.find(d => d === encodedCwd);
314
855
  if (!matchingDir)
315
856
  return null;
316
857
  const transcriptDir = join(claudeProjectsDir, matchingDir);
317
- const stat = statSync(transcriptDir);
318
- if (!stat.isDirectory())
858
+ const dirStat = statSync(transcriptDir);
859
+ if (!dirStat.isDirectory())
319
860
  return null;
320
- // Find the most recent .jsonl file by modification time
861
+ const sessionId = process.env.CLAUDE_SESSION_ID;
862
+ // If we have a session ID, look for that specific file first
863
+ if (sessionId) {
864
+ const sessionFile = join(transcriptDir, `${sessionId}.jsonl`);
865
+ if (existsSync(sessionFile))
866
+ return sessionFile;
867
+ }
868
+ // Fall back: find the most recent .jsonl file by modification time,
869
+ // but skip tiny files (likely just file-history-snapshot entries)
321
870
  const files = readdirSync(transcriptDir)
322
871
  .filter(f => f.endsWith('.jsonl'))
323
- .map(f => ({
324
- name: f,
325
- mtime: statSync(join(transcriptDir, f)).mtimeMs,
326
- }))
327
- .sort((a, b) => b.mtime - a.mtime);
872
+ .map(f => {
873
+ const s = statSync(join(transcriptDir, f));
874
+ return { name: f, mtime: s.mtimeMs, size: s.size };
875
+ })
876
+ // Prefer files >10KB (actual conversations, not just metadata)
877
+ .sort((a, b) => {
878
+ const aSubstantial = a.size > 10_000 ? 1 : 0;
879
+ const bSubstantial = b.size > 10_000 ? 1 : 0;
880
+ if (aSubstantial !== bSubstantial)
881
+ return bSubstantial - aSubstantial;
882
+ return b.mtime - a.mtime;
883
+ });
328
884
  return files[0] ? join(transcriptDir, files[0].name) : null;
329
885
  }
330
886
  catch {
@@ -347,8 +903,7 @@ function askQuestion(question) {
347
903
  * Interactive (or default) setup for bookmark in a project.
348
904
  */
349
905
  async function runSetup(cwd, useDefaults) {
350
- let intervalMinutes = 20;
351
- let smartDefault = false;
906
+ let intervalMinutes = 5;
352
907
  console.log('');
353
908
  console.log(`${BOLD}Bookmark — Context Snapshot Setup${RESET}`);
354
909
  console.log('═════════════════════════════════');
@@ -356,36 +911,22 @@ async function runSetup(cwd, useDefaults) {
356
911
  if (!useDefaults) {
357
912
  // Prompt for interval
358
913
  console.log('Snapshot interval?');
359
- console.log(' 1) 10 minutes (frequent)');
360
- console.log(' 2) 15 minutes');
361
- console.log(` 3) 20 minutes ${DIM}(recommended)${RESET}`);
362
- console.log(' 4) 30 minutes (conservative)');
914
+ console.log(` 1) 5 minutes ${DIM}(recommended)${RESET}`);
915
+ console.log(' 2) 10 minutes');
916
+ console.log(' 3) 15 minutes');
917
+ console.log(' 4) 20 minutes');
918
+ console.log(' 5) 30 minutes (conservative)');
363
919
  console.log('');
364
920
  const intervalAnswer = await askQuestion(`${DIM}> ${RESET}`);
365
- const intervalMap = { '1': 10, '2': 15, '3': 20, '4': 30 };
366
- intervalMinutes = intervalMap[intervalAnswer] ?? 20;
367
- console.log('');
368
- // Prompt for smart mode
369
- const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
370
- console.log('Smart mode (uses Claude Haiku for better extraction, ~$0.001/snapshot)?');
371
- if (hasApiKey) {
372
- console.log(` ${GREEN}ANTHROPIC_API_KEY detected.${RESET}`);
373
- }
374
- else {
375
- console.log(` ${DIM}No ANTHROPIC_API_KEY found — smart mode requires it.${RESET}`);
376
- }
377
- console.log(' 1) Enable smart mode');
378
- console.log(` 2) Pattern-matching only ${DIM}(recommended)${RESET}`);
379
- console.log('');
380
- const smartAnswer = await askQuestion(`${DIM}> ${RESET}`);
381
- smartDefault = smartAnswer === '1';
921
+ const intervalMap = { '1': 5, '2': 10, '3': 15, '4': 20, '5': 30 };
922
+ intervalMinutes = intervalMap[intervalAnswer] ?? 5;
382
923
  console.log('');
383
924
  }
384
925
  // Run core setup (dirs, hooks, gitignore, CLAUDE.md)
385
926
  const steps = setupProject(cwd);
386
927
  // Write user preferences
387
- writeConfig(cwd, { intervalMinutes, smartDefault });
388
- steps.push('Saved config to .claude/bookmarks/config.json');
928
+ writeConfig(cwd, { intervalMinutes });
929
+ steps.push('Saved config to .bookmark/config.json');
389
930
  // Initialize state with user preferences
390
931
  const sp = getStoragePath(cwd);
391
932
  const state = loadState(sp);
@@ -399,8 +940,7 @@ async function runSetup(cwd, useDefaults) {
399
940
  console.log('');
400
941
  console.log('Defaults:');
401
942
  console.log(` Interval: ${intervalMinutes} minutes`);
402
- console.log(` Thresholds: 20% 30% 40% → 50% (adaptive)`);
403
- console.log(` Smart mode: ${smartDefault ? 'on' : 'off'}`);
943
+ console.log(' New session: suggest at 75% context used');
404
944
  console.log('');
405
945
  console.log(`${GREEN}Ready.${RESET} Start a Claude Code session — snapshots will be captured automatically.`);
406
946
  console.log('');