fluffy-context 0.2.0 → 0.3.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.
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { checkpoint, resume } from '../runtime/runtime.js';
3
+ import { contextOrient } from '../agent/api.js';
4
+ import { runClaudeCodeHook } from '../hooks/claude-code.js';
5
+ import { inspectClaudeIntegration, installClaudeIntegration } from '../integrations/claude-code.js';
3
6
  import { doctor, status } from '../runtime/diagnostics.js';
4
7
  import { initProject } from '../runtime/init.js';
5
8
  import { discoverKnowledge, learnKnowledge, listKnowledge, verifyKnowledge, recordDeadend, listDeadends, verifyDeadend } from '../runtime/knowledge.js';
@@ -93,6 +96,46 @@ Options:
93
96
  --path <path> Project path
94
97
  --context <id> Context ID
95
98
  --max-chars <number> Maximum summary characters`,
99
+ orient: `usage: ctx orient [query] [options]
100
+
101
+ Prepare bounded task context from the current work, verified consensus, matching deadends, and open notes.
102
+
103
+ Options:
104
+ --path <path> Project path
105
+ --context <id> Context ID
106
+ --scope <scope> Exact consensus scope
107
+ --max-chars <number> Maximum display characters
108
+ --knowledge-limit <number> Maximum knowledge matches (default: 10)
109
+ --deadend-limit <number> Maximum deadend matches (default: 10)
110
+ --note-limit <number> Maximum open notes (default: 20)`,
111
+ agent: `usage: ctx agent serve
112
+
113
+ Start an MCP stdio server for Agent integrations.
114
+
115
+ Run "ctx agent serve --help" for details.`,
116
+ 'agent serve': `usage: ctx agent serve
117
+
118
+ Start an MCP stdio server exposing the context_orient tool.
119
+
120
+ The server owns standard input/output; do not use it interactively.`,
121
+ hook: `usage: ctx hook claude-code session-start|user-prompt
122
+
123
+ Run a Claude Code lifecycle hook. This command reads Hook JSON from standard input.`,
124
+ 'hook claude-code': `usage: ctx hook claude-code session-start|user-prompt
125
+
126
+ Emit best-effort, read-only phased Context guidance for Claude Code.`,
127
+ integrate: `usage: ctx integrate claude inspect|install [--path <path>] [--apply]
128
+
129
+ Inspect or explicitly install project-local Claude Code hooks and MCP configuration.`,
130
+ 'integrate claude': `usage: ctx integrate claude inspect|install [--path <path>] [--apply]
131
+
132
+ Inspect configuration, or preview/install project-local Claude Code integration.`,
133
+ 'integrate claude inspect': `usage: ctx integrate claude inspect [--path <path>]
134
+
135
+ Report whether the project has the Claude Code hooks and MCP server configured.`,
136
+ 'integrate claude install': `usage: ctx integrate claude install [--path <path>] [--apply]
137
+
138
+ Preview configuration changes by default. Pass --apply to merge project-local .claude/settings.json and .mcp.json.`,
96
139
  status: `usage: ctx status [--path <path>]
97
140
 
98
141
  Show the initialized project and context index.`,
@@ -194,21 +237,28 @@ Options:
194
237
  --max-chars <number> Maximum message characters`,
195
238
  };
196
239
  function usage() {
197
- return `usage: ctx init|checkpoint|resume|status|doctor|learn|knowledge|deadend|deadends|note|activity [options]
240
+ return `usage: ctx init|checkpoint|resume|orient|agent|hook|integrate|status|doctor|learn|knowledge|deadend|deadends|note|activity [options]
198
241
 
199
242
  Run \"ctx <command> --help\" for command details.`;
200
243
  }
201
244
  function print(value) {
202
245
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
203
246
  }
247
+ async function stdin() {
248
+ let value = '';
249
+ for await (const chunk of process.stdin)
250
+ value += chunk.toString();
251
+ return value;
252
+ }
204
253
  function printHelp(args) {
205
254
  const command = args[0];
206
255
  if (!command) {
207
256
  process.stdout.write(`${usage()}\n`);
208
257
  return true;
209
258
  }
210
- const key = args[1] === 'verify' || args[1] === 'add' || args[1] === 'list' || args[1] === 'discover' ? `${command} ${args[1]}` : command;
211
- const help = HELP[key];
259
+ const key = args[1] === 'verify' || args[1] === 'add' || args[1] === 'list' || args[1] === 'discover' || args[1] === 'serve' || args[1] === 'claude' ? `${command} ${args[1]}` : command;
260
+ const nestedKey = key === 'integrate claude' && (args[2] === 'inspect' || args[2] === 'install') ? `${key} ${args[2]}` : key;
261
+ const help = HELP[nestedKey];
212
262
  if (!help)
213
263
  return false;
214
264
  if (args.includes('--help')) {
@@ -265,6 +315,50 @@ async function run(args) {
265
315
  validatePositionals(positionals(args.slice(1), ['--path', '--context', '--max-chars']), 0, HELP.resume);
266
316
  print(await resume(target, option(args, '--context'), numericOption(args, '--max-chars', 4000)));
267
317
  return;
318
+ case 'orient': {
319
+ const valueOptions = ['--path', '--context', '--scope', '--max-chars', '--knowledge-limit', '--deadend-limit', '--note-limit'];
320
+ validateOptions(args.slice(1), valueOptions, valueOptions);
321
+ const query = positionals(args.slice(1), valueOptions);
322
+ validatePositionals(query, 1, HELP.orient);
323
+ print(await contextOrient(target, {
324
+ contextId: option(args, '--context'),
325
+ query: query[0],
326
+ scope: option(args, '--scope'),
327
+ maxChars: numericOption(args, '--max-chars', 4000),
328
+ knowledgeLimit: numericOption(args, '--knowledge-limit', 10),
329
+ deadendLimit: numericOption(args, '--deadend-limit', 10),
330
+ noteLimit: numericOption(args, '--note-limit', 20),
331
+ }));
332
+ return;
333
+ }
334
+ case 'agent':
335
+ if (args[1] !== 'serve')
336
+ throw new Error(HELP.agent);
337
+ validateOptions(args.slice(2), [], []);
338
+ validatePositionals(positionals(args.slice(2), []), 0, HELP['agent serve']);
339
+ await import('../mcp/main.js');
340
+ return;
341
+ case 'hook': {
342
+ if (args[1] !== 'claude-code' || !['session-start', 'user-prompt'].includes(args[2]))
343
+ throw new Error(HELP['hook claude-code']);
344
+ validateOptions(args.slice(3), [], []);
345
+ validatePositionals(positionals(args.slice(3), []), 0, HELP['hook claude-code']);
346
+ process.stdout.write(await runClaudeCodeHook(args[2], await stdin()));
347
+ return;
348
+ }
349
+ case 'integrate': {
350
+ if (args[1] !== 'claude' || !['inspect', 'install'].includes(args[2]))
351
+ throw new Error(HELP['integrate claude']);
352
+ const valueOptions = ['--path'];
353
+ const allowed = args[2] === 'install' ? [...valueOptions, '--apply'] : valueOptions;
354
+ validateOptions(args.slice(3), allowed, valueOptions);
355
+ validatePositionals(positionals(args.slice(3), valueOptions), 0, HELP[`integrate claude ${args[2]}`]);
356
+ if (args[2] === 'inspect')
357
+ print(await inspectClaudeIntegration(target));
358
+ else
359
+ print(await installClaudeIntegration(target, args.includes('--apply')));
360
+ return;
361
+ }
268
362
  case 'status':
269
363
  validateOptions(args.slice(1), ['--path'], ['--path']);
270
364
  validatePositionals(positionals(args.slice(1), ['--path']), 0, HELP.status);
@@ -0,0 +1,2 @@
1
+ import type { GitState } from '../runtime/types.js';
2
+ export declare function readGitState(projectRoot: string): Promise<GitState>;
@@ -0,0 +1 @@
1
+ export declare function runClaudeCodeHook(event: 'session-start' | 'user-prompt', raw: string): Promise<string>;
@@ -0,0 +1,84 @@
1
+ import { contextOrient } from '../agent/api.js';
2
+ const HOOK_RESPONSE = '{}\n';
3
+ function text(value) {
4
+ return typeof value === 'string' ? value : undefined;
5
+ }
6
+ function compact(value, maxChars) {
7
+ const normalized = value.replace(/\s+/g, ' ').trim();
8
+ return normalized.length <= maxChars ? normalized : `${normalized.slice(0, maxChars - 1)}…`;
9
+ }
10
+ function lines(label, values, limit) {
11
+ return values.slice(0, limit).map((value) => `- ${label}: ${compact(value, 240)}`);
12
+ }
13
+ function readyContext(result) {
14
+ const content = [
15
+ `当前 Context:${compact(result.context.title, 160)}`,
16
+ `进度:${compact(result.resumeSummary.progressSummary, 360)}`,
17
+ ...lines('待办', result.resumeSummary.pendingTasks, 4),
18
+ ...lines('决策', result.resumeSummary.decisions, 3),
19
+ ...lines('风险', result.resumeSummary.risks, 3),
20
+ ...result.knowledge.hits.slice(0, 5).map((hit) => `- 已验证知识:${compact(hit.knowledge.statement, 240)}`),
21
+ ...result.deadends.hits.slice(0, 3).map((hit) => `- 已验证死路(避免):${compact(hit.deadend.attempt, 180)};${compact(hit.deadend.reason, 180)}`),
22
+ ...result.notes.slice(0, 8).map((note) => `- 开放 Note:${compact(note.message, 220)}`),
23
+ ];
24
+ if (result.truncated)
25
+ content.push('- 上下文已按预算截断;需要时使用 context_orient 或 ctx orient 进一步查询。');
26
+ return content;
27
+ }
28
+ function sessionContext(result) {
29
+ const phases = [
30
+ '开发阶段:Orient → Plan → Implement → Verify → Handoff。',
31
+ '先基于以下上下文明确计划;实现中记录重要观察/决策/阻塞为 ctx note;验证后显式 ctx checkpoint,总结完成、待办、决策和风险。不要自动保存 Snapshot。',
32
+ ];
33
+ if (result.status === 'no_context') {
34
+ phases.push('当前没有可恢复的 Context。开始有意义的工作后,使用 ctx checkpoint 创建一个。');
35
+ }
36
+ else {
37
+ phases.push(...readyContext(result));
38
+ }
39
+ return phases.join('\n');
40
+ }
41
+ function promptContext(result) {
42
+ const phases = [
43
+ '开发阶段:Implement → Verify → Handoff。',
44
+ '实现时用 ctx note 记录重要观察、决策或阻塞;完成后运行适用的验证。阶段结束时显式 ctx checkpoint(可 --absorb-notes),不要由 Hook 自动保存。',
45
+ ];
46
+ if (result.status === 'no_context') {
47
+ phases.push('当前没有可恢复的 Context;如果这项工作会继续,先建立 checkpoint。');
48
+ }
49
+ else {
50
+ phases.push(...readyContext(result));
51
+ }
52
+ return phases.join('\n');
53
+ }
54
+ async function parseInput(raw) {
55
+ try {
56
+ const value = JSON.parse(raw);
57
+ return value !== null && typeof value === 'object' ? value : null;
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ export async function runClaudeCodeHook(event, raw) {
64
+ const input = await parseInput(raw);
65
+ if (!input)
66
+ return HOOK_RESPONSE;
67
+ try {
68
+ const cwd = text(input.cwd);
69
+ const result = event === 'session-start'
70
+ ? await contextOrient(cwd, { maxChars: 1200, noteLimit: 8 })
71
+ : await contextOrient(cwd, {
72
+ query: compact(text(input.prompt) ?? '', 500) || undefined,
73
+ maxChars: 1800,
74
+ knowledgeLimit: 5,
75
+ deadendLimit: 3,
76
+ noteLimit: 8,
77
+ });
78
+ const additionalContext = event === 'session-start' ? sessionContext(result) : promptContext(result);
79
+ return `${JSON.stringify({ additionalContext })}\n`;
80
+ }
81
+ catch {
82
+ return HOOK_RESPONSE;
83
+ }
84
+ }
@@ -0,0 +1,19 @@
1
+ export interface ClaudeIntegrationStatus {
2
+ projectRoot: string;
3
+ initialized: boolean;
4
+ settingsPath: string;
5
+ mcpPath: string;
6
+ settingsExists: boolean;
7
+ mcpExists: boolean;
8
+ sessionStartInstalled: boolean;
9
+ userPromptInstalled: boolean;
10
+ mcpInstalled: boolean;
11
+ mcpConflict: boolean;
12
+ installationNeeded: boolean;
13
+ }
14
+ export interface ClaudeIntegrationInstallResult extends ClaudeIntegrationStatus {
15
+ applied: boolean;
16
+ targetFiles: string[];
17
+ }
18
+ export declare function inspectClaudeIntegration(startPath: string | undefined): Promise<ClaudeIntegrationStatus>;
19
+ export declare function installClaudeIntegration(startPath: string | undefined, apply: boolean): Promise<ClaudeIntegrationInstallResult>;
@@ -0,0 +1,122 @@
1
+ import { access } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { resolveProjectRoot } from '../project/project-resolver.js';
4
+ import { atomicWriteJson } from '../storage/atomic-write.js';
5
+ import { isRecord, readJson } from '../storage/json-store.js';
6
+ import { manifestPath } from '../storage/layout.js';
7
+ const HOOK_COMMANDS = {
8
+ sessionStart: 'ctx hook claude-code session-start',
9
+ userPrompt: 'ctx hook claude-code user-prompt',
10
+ };
11
+ const MCP_SERVER = { command: 'ctx', args: ['agent', 'serve'] };
12
+ function object(value) {
13
+ return isRecord(value) ? { ...value } : {};
14
+ }
15
+ function hookInstalled(value, command) {
16
+ if (!Array.isArray(value))
17
+ return false;
18
+ return value.some((group) => isRecord(group)
19
+ && Array.isArray(group.hooks)
20
+ && group.hooks.some((hook) => isRecord(hook) && hook.type === 'command' && hook.command === command));
21
+ }
22
+ function addHook(value, command) {
23
+ const groups = Array.isArray(value) ? value.filter(isRecord).map((group) => ({ ...group })) : [];
24
+ if (hookInstalled(groups, command))
25
+ return groups;
26
+ return [...groups, { hooks: [{ type: 'command', command }] }];
27
+ }
28
+ async function exists(filePath) {
29
+ try {
30
+ await access(filePath);
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ async function loadObject(filePath) {
38
+ if (!await exists(filePath))
39
+ return {};
40
+ return readJson(filePath, isRecord);
41
+ }
42
+ function validateConfig(settings, mcp) {
43
+ if (settings.hooks !== undefined && !isRecord(settings.hooks))
44
+ throw new Error('Claude Code settings hooks must be an object');
45
+ const hooks = object(settings.hooks);
46
+ for (const event of ['SessionStart', 'UserPromptSubmit']) {
47
+ if (hooks[event] !== undefined && !Array.isArray(hooks[event]))
48
+ throw new Error(`Claude Code ${event} hooks must be an array`);
49
+ }
50
+ if (mcp.mcpServers !== undefined && !isRecord(mcp.mcpServers))
51
+ throw new Error('Claude Code MCP servers must be an object');
52
+ }
53
+ function status(projectRoot, settingsPath, mcpPath, settings, mcp, settingsExists, mcpExists, initialized) {
54
+ const hooks = object(settings.hooks);
55
+ const sessionStartInstalled = hookInstalled(hooks.SessionStart, HOOK_COMMANDS.sessionStart);
56
+ const userPromptInstalled = hookInstalled(hooks.UserPromptSubmit, HOOK_COMMANDS.userPrompt);
57
+ const mcpServers = object(mcp.mcpServers);
58
+ const configuredMcp = mcpServers['fluffy-context'];
59
+ const mcpInstalled = isRecord(configuredMcp)
60
+ && configuredMcp.command === MCP_SERVER.command
61
+ && Array.isArray(configuredMcp.args)
62
+ && configuredMcp.args.length === MCP_SERVER.args.length
63
+ && configuredMcp.args.every((value, index) => value === MCP_SERVER.args[index]);
64
+ const mcpConflict = configuredMcp !== undefined && !mcpInstalled;
65
+ return {
66
+ projectRoot,
67
+ initialized,
68
+ settingsPath,
69
+ mcpPath,
70
+ settingsExists,
71
+ mcpExists,
72
+ sessionStartInstalled,
73
+ userPromptInstalled,
74
+ mcpInstalled,
75
+ mcpConflict,
76
+ installationNeeded: !sessionStartInstalled || !userPromptInstalled || !mcpInstalled,
77
+ };
78
+ }
79
+ async function inspect(projectRoot) {
80
+ const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
81
+ const mcpPath = path.join(projectRoot, '.mcp.json');
82
+ const [settingsExists, mcpExists, initialized] = await Promise.all([
83
+ exists(settingsPath),
84
+ exists(mcpPath),
85
+ exists(manifestPath(projectRoot)),
86
+ ]);
87
+ const [settings, mcp] = await Promise.all([loadObject(settingsPath), loadObject(mcpPath)]);
88
+ validateConfig(settings, mcp);
89
+ return { status: status(projectRoot, settingsPath, mcpPath, settings, mcp, settingsExists, mcpExists, initialized), settings, mcp };
90
+ }
91
+ export async function inspectClaudeIntegration(startPath) {
92
+ const projectRoot = await resolveProjectRoot(startPath);
93
+ return (await inspect(projectRoot)).status;
94
+ }
95
+ export async function installClaudeIntegration(startPath, apply) {
96
+ const projectRoot = await resolveProjectRoot(startPath);
97
+ const inspected = await inspect(projectRoot);
98
+ const targetFiles = [inspected.status.settingsPath, inspected.status.mcpPath];
99
+ if (!apply || !inspected.status.installationNeeded)
100
+ return { ...inspected.status, applied: false, targetFiles };
101
+ if (inspected.status.mcpConflict)
102
+ throw new Error('existing fluffy-context MCP server configuration conflicts with the generated integration');
103
+ const settingsHooks = object(inspected.settings.hooks);
104
+ const settings = {
105
+ ...inspected.settings,
106
+ hooks: {
107
+ ...settingsHooks,
108
+ SessionStart: addHook(settingsHooks.SessionStart, HOOK_COMMANDS.sessionStart),
109
+ UserPromptSubmit: addHook(settingsHooks.UserPromptSubmit, HOOK_COMMANDS.userPrompt),
110
+ },
111
+ };
112
+ const mcpServers = object(inspected.mcp.mcpServers);
113
+ const mcp = {
114
+ ...inspected.mcp,
115
+ mcpServers: { ...mcpServers, 'fluffy-context': MCP_SERVER },
116
+ };
117
+ await Promise.all([
118
+ atomicWriteJson(inspected.status.settingsPath, settings),
119
+ atomicWriteJson(inspected.status.mcpPath, mcp),
120
+ ]);
121
+ return { ...(await inspect(projectRoot)).status, applied: true, targetFiles };
122
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import { serveStdio } from '@modelcontextprotocol/server/stdio';
2
+ import { createMcpServer } from './server.js';
3
+ serveStdio(createMcpServer, {
4
+ onerror: (error) => process.stderr.write(`MCP server error: ${error.message}\n`),
5
+ });
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
+ export declare function createMcpServer(): McpServer;
@@ -0,0 +1,40 @@
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
+ import { z } from 'zod';
3
+ import { contextOrient } from '../agent/api.js';
4
+ import { VERSION } from '../version.js';
5
+ const orientInput = z.object({
6
+ path: z.string().optional(),
7
+ contextId: z.string().optional(),
8
+ query: z.string().optional(),
9
+ scope: z.string().optional(),
10
+ maxChars: z.int().nonnegative().optional(),
11
+ knowledgeLimit: z.int().nonnegative().optional(),
12
+ deadendLimit: z.int().nonnegative().optional(),
13
+ noteLimit: z.int().nonnegative().optional(),
14
+ });
15
+ function errorMessage(error) {
16
+ return error instanceof Error ? error.message : String(error);
17
+ }
18
+ export function createMcpServer() {
19
+ const server = new McpServer({ name: 'fluffy-context', version: VERSION });
20
+ server.registerTool('context_orient', {
21
+ title: 'Orient context',
22
+ description: 'Load bounded, read-only task context with verified consensus and open notes.',
23
+ inputSchema: orientInput,
24
+ }, async ({ path, ...options }) => {
25
+ try {
26
+ const result = await contextOrient(path, options);
27
+ return {
28
+ content: [{ type: 'text', text: JSON.stringify(result) }],
29
+ structuredContent: result,
30
+ };
31
+ }
32
+ catch (error) {
33
+ return {
34
+ isError: true,
35
+ content: [{ type: 'text', text: `context_orient failed: ${errorMessage(error)}` }],
36
+ };
37
+ }
38
+ });
39
+ return server;
40
+ }
@@ -0,0 +1 @@
1
+ export declare function resolveProjectRoot(startPath?: string): Promise<string>;
@@ -0,0 +1,6 @@
1
+ export declare function status(startPath?: string): Promise<Record<string, unknown>>;
2
+ export declare function doctor(startPath?: string): Promise<{
3
+ healthy: boolean;
4
+ projectRoot: string;
5
+ checks: string[];
6
+ }>;
@@ -0,0 +1,7 @@
1
+ import type { Manifest } from './types.js';
2
+ export interface InitResult {
3
+ projectRoot: string;
4
+ alreadyInitialized: boolean;
5
+ }
6
+ export declare function initProject(startPath?: string): Promise<InitResult>;
7
+ export declare function loadManifest(projectRoot: string): Promise<Manifest>;
@@ -0,0 +1,9 @@
1
+ import type { Deadend, DeadendDiscoverOptions, DeadendDiscoveryResult, DeadendInput, Knowledge, KnowledgeDiscoverOptions, KnowledgeDiscoveryResult, KnowledgeInput } from './types.js';
2
+ export declare function learnKnowledge(startPath: string | undefined, statement: string, scopeOrInput?: string | KnowledgeInput): Promise<Knowledge>;
3
+ export declare function listKnowledge(startPath?: string, includeUnverified?: boolean): Promise<Knowledge[]>;
4
+ export declare function discoverKnowledge(startPath: string | undefined, query: string, options?: KnowledgeDiscoverOptions): Promise<KnowledgeDiscoveryResult>;
5
+ export declare function verifyKnowledge(startPath: string | undefined, knowledgeId: string): Promise<Knowledge>;
6
+ export declare function recordDeadend(startPath: string | undefined, attempt: string, reason: string, scopeOrInput?: string | DeadendInput): Promise<Deadend>;
7
+ export declare function listDeadends(startPath?: string, includeUnverified?: boolean): Promise<Deadend[]>;
8
+ export declare function discoverDeadends(startPath: string | undefined, query: string, options?: DeadendDiscoverOptions): Promise<DeadendDiscoveryResult>;
9
+ export declare function verifyDeadend(startPath: string | undefined, deadendId: string): Promise<Deadend>;
@@ -0,0 +1,6 @@
1
+ import type { ActivityQuery, ActivityResult, Note, NoteInput, NoteQuery } from './types.js';
2
+ export declare function addNote(startPath: string | undefined, message: string, input?: NoteInput): Promise<Note>;
3
+ export declare function listNotes(startPath: string | undefined, query?: NoteQuery): Promise<Note[]>;
4
+ export declare function listActivity(startPath: string | undefined, query?: ActivityQuery): Promise<ActivityResult>;
5
+ export declare function absorbNotes(startPath: string | undefined, contextId: string, snapshotId: string): Promise<number>;
6
+ export declare function isValidNote(value: unknown): value is Note;
@@ -0,0 +1,11 @@
1
+ import type { CheckpointInput, CheckpointResult, IndexFile, ResumeResult, StructuredContext } from './types.js';
2
+ export interface CheckpointOptions {
3
+ minSaveIntervalMs?: number;
4
+ }
5
+ export declare function rebuildSnapshot(projectRoot: string, contextId: string, snapshotId: string, seen?: Set<string>): Promise<StructuredContext>;
6
+ export declare function rebuildIndex(projectRoot: string, persist?: boolean): Promise<IndexFile>;
7
+ export declare function checkpoint(startPath: string | undefined, input: CheckpointInput, options?: CheckpointOptions): Promise<CheckpointResult>;
8
+ export interface ResumeOptions {
9
+ touchLastUsedAt?: boolean;
10
+ }
11
+ export declare function resume(startPath?: string, contextId?: string, maxChars?: number, options?: ResumeOptions): Promise<ResumeResult>;
@@ -310,11 +310,11 @@ export async function checkpoint(startPath, input, options = {}) {
310
310
  return { status: 'saved', context: updatedContext, snapshot };
311
311
  });
312
312
  }
313
- export async function resume(startPath, contextId, maxChars = 4000) {
313
+ export async function resume(startPath, contextId, maxChars = 4000, options = {}) {
314
314
  const projectRoot = await resolveProjectRoot(startPath);
315
315
  await loadManifest(projectRoot);
316
316
  const git = await readGitState(projectRoot);
317
- const index = await rebuildIndex(projectRoot);
317
+ const index = await rebuildIndex(projectRoot, options.touchLastUsedAt !== false);
318
318
  const candidates = index.contexts
319
319
  .filter((entry) => !contextId || entry.contextId === contextId)
320
320
  .filter((entry) => ['active', 'stable'].includes(entry.status))
@@ -328,8 +328,10 @@ export async function resume(startPath, contextId, maxChars = 4000) {
328
328
  const snapshot = await loadSnapshot(projectRoot, candidate.contextId, context.currentSnapshotId);
329
329
  const content = await rebuildSnapshot(projectRoot, candidate.contextId, snapshot.snapshotId);
330
330
  const drift = context.branch !== git.branch || context.commit !== git.commit;
331
- context.lastUsedAt = new Date().toISOString();
332
- await atomicWriteJson(contextMetadataPath(projectRoot, context.id), context);
331
+ if (options.touchLastUsedAt !== false) {
332
+ context.lastUsedAt = new Date().toISOString();
333
+ await atomicWriteJson(contextMetadataPath(projectRoot, context.id), context);
334
+ }
333
335
  const budget = { remaining: Math.max(0, maxChars) };
334
336
  const resumeSummary = {
335
337
  progressSummary: limited(content.progressSummary, budget.remaining),