fluffy-context 0.3.0 → 0.5.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.
Files changed (35) hide show
  1. package/README.md +39 -4
  2. package/dist/src/agent/api.js +20 -9
  3. package/dist/src/cli/main.js +96 -2
  4. package/dist/src/cognition/git-observer.d.ts +13 -0
  5. package/dist/src/cognition/git-observer.js +61 -0
  6. package/dist/src/cognition/journal.d.ts +24 -0
  7. package/dist/src/cognition/journal.js +113 -0
  8. package/dist/src/cognition/migration-v1.d.ts +7 -0
  9. package/dist/src/cognition/migration-v1.js +199 -0
  10. package/dist/src/cognition/orient.d.ts +9 -0
  11. package/dist/src/cognition/orient.js +85 -0
  12. package/dist/src/cognition/projections.d.ts +7 -0
  13. package/dist/src/cognition/projections.js +127 -0
  14. package/dist/src/cognition/readiness.d.ts +2 -0
  15. package/dist/src/cognition/readiness.js +81 -0
  16. package/dist/src/cognition/runtime.d.ts +12 -0
  17. package/dist/src/cognition/runtime.js +295 -0
  18. package/dist/src/cognition/types.d.ts +132 -0
  19. package/dist/src/cognition/types.js +1 -0
  20. package/dist/src/cognition/v1-bridge.d.ts +7 -0
  21. package/dist/src/cognition/v1-bridge.js +74 -0
  22. package/dist/src/git/git-adapter.d.ts +7 -0
  23. package/dist/src/git/git-adapter.js +16 -6
  24. package/dist/src/hooks/claude-code.js +53 -0
  25. package/dist/src/integrations/claude-code.js +36 -13
  26. package/dist/src/runtime/diagnostics.js +6 -0
  27. package/dist/src/runtime/knowledge.js +5 -0
  28. package/dist/src/runtime/notes.js +8 -1
  29. package/dist/src/runtime/runtime.d.ts +1 -0
  30. package/dist/src/runtime/runtime.js +28 -0
  31. package/dist/src/storage/layout.d.ts +18 -0
  32. package/dist/src/storage/layout.js +54 -0
  33. package/dist/src/version.d.ts +1 -1
  34. package/dist/src/version.js +1 -1
  35. package/package.json +1 -1
@@ -1,14 +1,22 @@
1
1
  import { access } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
3
4
  import { resolveProjectRoot } from '../project/project-resolver.js';
4
5
  import { atomicWriteJson } from '../storage/atomic-write.js';
5
6
  import { isRecord, readJson } from '../storage/json-store.js';
6
7
  import { manifestPath } from '../storage/layout.js';
8
+ const cliPath = fileURLToPath(new URL('../cli/main.js', import.meta.url));
9
+ const cliCommand = `"${process.execPath}" "${cliPath}"`;
7
10
  const HOOK_COMMANDS = {
11
+ sessionStart: `${cliCommand} hook claude-code session-start`,
12
+ userPrompt: `${cliCommand} hook claude-code user-prompt`,
13
+ };
14
+ const LEGACY_HOOK_COMMANDS = {
8
15
  sessionStart: 'ctx hook claude-code session-start',
9
16
  userPrompt: 'ctx hook claude-code user-prompt',
10
17
  };
11
- const MCP_SERVER = { command: 'ctx', args: ['agent', 'serve'] };
18
+ const MCP_SERVER = { command: process.execPath, args: [cliPath, 'agent', 'serve'] };
19
+ const LEGACY_MCP_SERVER = { command: 'ctx', args: ['agent', 'serve'] };
12
20
  function object(value) {
13
21
  return isRecord(value) ? { ...value } : {};
14
22
  }
@@ -19,11 +27,23 @@ function hookInstalled(value, command) {
19
27
  && Array.isArray(group.hooks)
20
28
  && group.hooks.some((hook) => isRecord(hook) && hook.type === 'command' && hook.command === command));
21
29
  }
22
- function addHook(value, command) {
30
+ function addHook(value, command, legacyCommand) {
23
31
  const groups = Array.isArray(value) ? value.filter(isRecord).map((group) => ({ ...group })) : [];
24
32
  if (hookInstalled(groups, command))
25
33
  return groups;
26
- return [...groups, { hooks: [{ type: 'command', command }] }];
34
+ let replaced = false;
35
+ const updated = groups.map((group) => ({
36
+ ...group,
37
+ hooks: Array.isArray(group.hooks)
38
+ ? group.hooks.map((hook) => {
39
+ if (!isRecord(hook) || hook.type !== 'command' || hook.command !== legacyCommand)
40
+ return hook;
41
+ replaced = true;
42
+ return { ...hook, command };
43
+ })
44
+ : group.hooks,
45
+ }));
46
+ return replaced ? updated : [...updated, { hooks: [{ type: 'command', command }] }];
27
47
  }
28
48
  async function exists(filePath) {
29
49
  try {
@@ -50,18 +70,21 @@ function validateConfig(settings, mcp) {
50
70
  if (mcp.mcpServers !== undefined && !isRecord(mcp.mcpServers))
51
71
  throw new Error('Claude Code MCP servers must be an object');
52
72
  }
73
+ function mcpInstalled(value, server = MCP_SERVER) {
74
+ return isRecord(value)
75
+ && value.command === server.command
76
+ && Array.isArray(value.args)
77
+ && value.args.length === server.args.length
78
+ && value.args.every((item, index) => item === server.args[index]);
79
+ }
53
80
  function status(projectRoot, settingsPath, mcpPath, settings, mcp, settingsExists, mcpExists, initialized) {
54
81
  const hooks = object(settings.hooks);
55
82
  const sessionStartInstalled = hookInstalled(hooks.SessionStart, HOOK_COMMANDS.sessionStart);
56
83
  const userPromptInstalled = hookInstalled(hooks.UserPromptSubmit, HOOK_COMMANDS.userPrompt);
57
84
  const mcpServers = object(mcp.mcpServers);
58
85
  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;
86
+ const installed = mcpInstalled(configuredMcp);
87
+ const mcpConflict = configuredMcp !== undefined && !installed && !mcpInstalled(configuredMcp, LEGACY_MCP_SERVER);
65
88
  return {
66
89
  projectRoot,
67
90
  initialized,
@@ -71,9 +94,9 @@ function status(projectRoot, settingsPath, mcpPath, settings, mcp, settingsExist
71
94
  mcpExists,
72
95
  sessionStartInstalled,
73
96
  userPromptInstalled,
74
- mcpInstalled,
97
+ mcpInstalled: installed,
75
98
  mcpConflict,
76
- installationNeeded: !sessionStartInstalled || !userPromptInstalled || !mcpInstalled,
99
+ installationNeeded: !sessionStartInstalled || !userPromptInstalled || !installed,
77
100
  };
78
101
  }
79
102
  async function inspect(projectRoot) {
@@ -105,8 +128,8 @@ export async function installClaudeIntegration(startPath, apply) {
105
128
  ...inspected.settings,
106
129
  hooks: {
107
130
  ...settingsHooks,
108
- SessionStart: addHook(settingsHooks.SessionStart, HOOK_COMMANDS.sessionStart),
109
- UserPromptSubmit: addHook(settingsHooks.UserPromptSubmit, HOOK_COMMANDS.userPrompt),
131
+ SessionStart: addHook(settingsHooks.SessionStart, HOOK_COMMANDS.sessionStart, LEGACY_HOOK_COMMANDS.sessionStart),
132
+ UserPromptSubmit: addHook(settingsHooks.UserPromptSubmit, HOOK_COMMANDS.userPrompt, LEGACY_HOOK_COMMANDS.userPrompt),
110
133
  },
111
134
  };
112
135
  const mcpServers = object(inspected.mcp.mcpServers);
@@ -4,6 +4,7 @@ import { readJson } from '../storage/json-store.js';
4
4
  import { contextRoot, contextMetadataPath, contextsRoot, indexPath, manifestPath, notesPath, snapshotPath } from '../storage/layout.js';
5
5
  import { rebuildIndex, rebuildSnapshot } from './runtime.js';
6
6
  import { isValidNote } from './notes.js';
7
+ import { migrationReadiness } from '../cognition/readiness.js';
7
8
  import { filterPaths } from '../capture/context-filter.js';
8
9
  export async function status(startPath) {
9
10
  const projectRoot = await resolveProjectRoot(startPath);
@@ -95,5 +96,10 @@ export async function doctor(startPath) {
95
96
  if (rebuiltIds.size !== storedIds.size || [...rebuiltIds].some((id) => !storedIds.has(id)))
96
97
  throw new Error('index rebuild mismatch');
97
98
  });
99
+ await check('v2 migration readiness', async () => {
100
+ const readiness = await migrationReadiness(projectRoot);
101
+ if (!readiness.eligible)
102
+ throw new Error(readiness.issues.join('; '));
103
+ });
98
104
  return { healthy, projectRoot, checks };
99
105
  }
@@ -5,6 +5,7 @@ import { isRecord, readJson } from '../storage/json-store.js';
5
5
  import { withLock } from '../storage/lock.js';
6
6
  import { contextMetadataPath, deadendsPath, knowledgePath, locksDirectory, snapshotPath } from '../storage/layout.js';
7
7
  import { rebuildSnapshot } from './runtime.js';
8
+ import { mirrorDeadend, mirrorKnowledge } from '../cognition/v1-bridge.js';
8
9
  function id(prefix) {
9
10
  return `${prefix}-${Date.now().toString(36)}-${crypto.randomBytes(4).toString('hex')}`;
10
11
  }
@@ -119,6 +120,7 @@ export async function learnKnowledge(startPath, statement, scopeOrInput = 'proje
119
120
  updatedAt: now,
120
121
  };
121
122
  await atomicWriteJson(knowledgePath(projectRoot), { schemaVersion: 1, items: [...file.items, item] });
123
+ await mirrorKnowledge(projectRoot, item);
122
124
  return item;
123
125
  });
124
126
  }
@@ -210,6 +212,7 @@ export async function verifyKnowledge(startPath, knowledgeId) {
210
212
  item.confidence = Math.max(item.confidence, 0.8);
211
213
  item.updatedAt = new Date().toISOString();
212
214
  await atomicWriteJson(knowledgePath(projectRoot), file);
215
+ await mirrorKnowledge(projectRoot, item);
213
216
  return item;
214
217
  });
215
218
  }
@@ -233,6 +236,7 @@ export async function recordDeadend(startPath, attempt, reason, scopeOrInput = '
233
236
  updatedAt: now,
234
237
  };
235
238
  await atomicWriteJson(deadendsPath(projectRoot), { schemaVersion: 1, items: [...file.items, item] });
239
+ await mirrorDeadend(projectRoot, item);
236
240
  return item;
237
241
  });
238
242
  }
@@ -294,6 +298,7 @@ export async function verifyDeadend(startPath, deadendId) {
294
298
  item.status = 'verified';
295
299
  item.updatedAt = new Date().toISOString();
296
300
  await atomicWriteJson(deadendsPath(projectRoot), file);
301
+ await mirrorDeadend(projectRoot, item);
297
302
  return item;
298
303
  });
299
304
  }
@@ -6,6 +6,7 @@ import { atomicWriteJson } from '../storage/atomic-write.js';
6
6
  import { isRecord, readJson } from '../storage/json-store.js';
7
7
  import { contextMetadataPath, contextsRoot, locksDirectory, notesPath, snapshotPath, snapshotsDirectory } from '../storage/layout.js';
8
8
  import { withLock } from '../storage/lock.js';
9
+ import { mirrorNote } from '../cognition/v1-bridge.js';
9
10
  function id() {
10
11
  return `note-${Date.now().toString(36)}-${crypto.randomBytes(4).toString('hex')}`;
11
12
  }
@@ -78,6 +79,7 @@ export async function addNote(startPath, message, input = {}) {
78
79
  updatedAt: now,
79
80
  };
80
81
  await atomicWriteJson(notesPath(projectRoot), { schemaVersion: 1, items: [...file.items, note] });
82
+ await mirrorNote(projectRoot, note);
81
83
  return note;
82
84
  });
83
85
  }
@@ -163,8 +165,13 @@ export async function absorbNotes(startPath, contextId, snapshotId) {
163
165
  absorbed += 1;
164
166
  return { ...note, absorbedBySnapshotId: snapshotId, updatedAt: new Date().toISOString() };
165
167
  });
166
- if (absorbed > 0)
168
+ if (absorbed > 0) {
167
169
  await atomicWriteJson(notesPath(projectRoot), { schemaVersion: 1, items });
170
+ for (const note of items) {
171
+ if (note.contextId === contextId && note.absorbedBySnapshotId === snapshotId)
172
+ await mirrorNote(projectRoot, note, true);
173
+ }
174
+ }
168
175
  return absorbed;
169
176
  });
170
177
  }
@@ -8,4 +8,5 @@ export declare function checkpoint(startPath: string | undefined, input: Checkpo
8
8
  export interface ResumeOptions {
9
9
  touchLastUsedAt?: boolean;
10
10
  }
11
+ export declare function resumeSnapshot(startPath: string | undefined, contextId: string, snapshotId: string, maxChars?: number): Promise<ResumeResult>;
11
12
  export declare function resume(startPath?: string, contextId?: string, maxChars?: number, options?: ResumeOptions): Promise<ResumeResult>;
@@ -9,6 +9,7 @@ import { contextDirectory, contextMetadataPath, contextsRoot, indexPath, locksDi
9
9
  import { loadManifest } from './init.js';
10
10
  import { filterPaths } from '../capture/context-filter.js';
11
11
  import { absorbNotes } from './notes.js';
12
+ import { mirrorCheckpoint } from '../cognition/v1-bridge.js';
12
13
  const emptyContext = () => ({
13
14
  progressSummary: '',
14
15
  lastError: null,
@@ -307,9 +308,36 @@ export async function checkpoint(startPath, input, options = {}) {
307
308
  });
308
309
  if (input.absorbNotes)
309
310
  await absorbNotes(projectRoot, contextId, snapshotId);
311
+ await mirrorCheckpoint(projectRoot, updatedContext, snapshot);
310
312
  return { status: 'saved', context: updatedContext, snapshot };
311
313
  });
312
314
  }
315
+ export async function resumeSnapshot(startPath, contextId, snapshotId, maxChars = 4000) {
316
+ const projectRoot = await resolveProjectRoot(startPath);
317
+ await loadManifest(projectRoot);
318
+ const [git, context, snapshot] = await Promise.all([
319
+ readGitState(projectRoot),
320
+ loadContext(projectRoot, contextId),
321
+ loadSnapshot(projectRoot, contextId, snapshotId),
322
+ ]);
323
+ const details = await rebuildSnapshot(projectRoot, contextId, snapshotId);
324
+ const budget = { remaining: Math.max(0, maxChars) };
325
+ return {
326
+ context,
327
+ snapshot,
328
+ details,
329
+ resumeSummary: {
330
+ progressSummary: limited(details.progressSummary, budget.remaining),
331
+ lastError: details.lastError === null ? null : limited(details.lastError, budget.remaining),
332
+ completed: limitedList(details.completed, budget),
333
+ pendingTasks: limitedList(details.pendingTasks, budget),
334
+ decisions: limitedList(details.decisions, budget),
335
+ risks: limitedList(details.risks, budget),
336
+ relatedFiles: limitedList(details.relatedFiles, budget),
337
+ branchOrCommitDrift: context.branch !== git.branch || context.commit !== git.commit,
338
+ },
339
+ };
340
+ }
313
341
  export async function resume(startPath, contextId, maxChars = 4000, options = {}) {
314
342
  const projectRoot = await resolveProjectRoot(startPath);
315
343
  await loadManifest(projectRoot);
@@ -11,3 +11,21 @@ export declare function locksDirectory(projectRoot: string): string;
11
11
  export declare function knowledgePath(projectRoot: string): string;
12
12
  export declare function deadendsPath(projectRoot: string): string;
13
13
  export declare function notesPath(projectRoot: string): string;
14
+ export declare function cognitionRoot(projectRoot: string): string;
15
+ export declare function cognitionManifestPath(projectRoot: string): string;
16
+ export declare function cognitionPolicyPath(projectRoot: string): string;
17
+ export declare function cognitionJournalDirectory(projectRoot: string): string;
18
+ export declare function cognitionProjectionsDirectory(projectRoot: string): string;
19
+ export declare function cognitionProjectionCursorPath(projectRoot: string): string;
20
+ export declare function cognitionBranchesDirectory(projectRoot: string): string;
21
+ export declare function cognitionContextsProjectionPath(projectRoot: string): string;
22
+ export declare function cognitionKnowledgeProjectionPath(projectRoot: string): string;
23
+ export declare function cognitionDeadendsProjectionPath(projectRoot: string): string;
24
+ export declare function cognitionNotesProjectionPath(projectRoot: string): string;
25
+ export declare function cognitionMigrationsDirectory(projectRoot: string): string;
26
+ export declare function cognitionV1ImportPath(projectRoot: string): string;
27
+ export declare function cognitionRuntimeDirectory(projectRoot: string): string;
28
+ export declare function cognitionRuntimeLeasePath(projectRoot: string): string;
29
+ export declare function cognitionRuntimeSecretPath(projectRoot: string): string;
30
+ export declare function cognitionV1SyncPath(projectRoot: string): string;
31
+ export declare function cognitionQuarantineDirectory(projectRoot: string): string;
@@ -38,3 +38,57 @@ export function deadendsPath(projectRoot) {
38
38
  export function notesPath(projectRoot) {
39
39
  return path.join(contextRoot(projectRoot), 'notes.json');
40
40
  }
41
+ export function cognitionRoot(projectRoot) {
42
+ return path.join(contextRoot(projectRoot), 'v2');
43
+ }
44
+ export function cognitionManifestPath(projectRoot) {
45
+ return path.join(cognitionRoot(projectRoot), 'manifest.json');
46
+ }
47
+ export function cognitionPolicyPath(projectRoot) {
48
+ return path.join(cognitionRoot(projectRoot), 'policy.json');
49
+ }
50
+ export function cognitionJournalDirectory(projectRoot) {
51
+ return path.join(cognitionRoot(projectRoot), 'journal');
52
+ }
53
+ export function cognitionProjectionsDirectory(projectRoot) {
54
+ return path.join(cognitionRoot(projectRoot), 'projections');
55
+ }
56
+ export function cognitionProjectionCursorPath(projectRoot) {
57
+ return path.join(cognitionProjectionsDirectory(projectRoot), 'cursor.json');
58
+ }
59
+ export function cognitionBranchesDirectory(projectRoot) {
60
+ return path.join(cognitionProjectionsDirectory(projectRoot), 'branches');
61
+ }
62
+ export function cognitionContextsProjectionPath(projectRoot) {
63
+ return path.join(cognitionProjectionsDirectory(projectRoot), 'contexts.json');
64
+ }
65
+ export function cognitionKnowledgeProjectionPath(projectRoot) {
66
+ return path.join(cognitionProjectionsDirectory(projectRoot), 'knowledge.json');
67
+ }
68
+ export function cognitionDeadendsProjectionPath(projectRoot) {
69
+ return path.join(cognitionProjectionsDirectory(projectRoot), 'deadends.json');
70
+ }
71
+ export function cognitionNotesProjectionPath(projectRoot) {
72
+ return path.join(cognitionProjectionsDirectory(projectRoot), 'notes.json');
73
+ }
74
+ export function cognitionMigrationsDirectory(projectRoot) {
75
+ return path.join(cognitionRoot(projectRoot), 'migrations');
76
+ }
77
+ export function cognitionV1ImportPath(projectRoot) {
78
+ return path.join(cognitionMigrationsDirectory(projectRoot), 'v1-import.json');
79
+ }
80
+ export function cognitionRuntimeDirectory(projectRoot) {
81
+ return path.join(cognitionRoot(projectRoot), 'runtime');
82
+ }
83
+ export function cognitionRuntimeLeasePath(projectRoot) {
84
+ return path.join(cognitionRuntimeDirectory(projectRoot), 'lease.json');
85
+ }
86
+ export function cognitionRuntimeSecretPath(projectRoot) {
87
+ return path.join(cognitionRuntimeDirectory(projectRoot), 'secret');
88
+ }
89
+ export function cognitionV1SyncPath(projectRoot) {
90
+ return path.join(cognitionRuntimeDirectory(projectRoot), 'v1-sync.json');
91
+ }
92
+ export function cognitionQuarantineDirectory(projectRoot) {
93
+ return path.join(cognitionRoot(projectRoot), 'quarantine');
94
+ }
@@ -1 +1 @@
1
- export declare const VERSION = "0.3.0";
1
+ export declare const VERSION = "0.5.0";
@@ -1 +1 @@
1
- export const VERSION = '0.3.0';
1
+ export const VERSION = '0.5.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluffy-context",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Local context management CLI and MCP tools for AI coding agents",
5
5
  "license": "MIT",
6
6
  "author": "FluffyChi-Xing",