fluffy-context 0.2.0 → 0.3.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.
- package/README.md +81 -182
- package/dist/src/agent/api.d.ts +8 -0
- package/dist/src/agent/api.js +152 -0
- package/dist/src/agent/index.d.ts +3 -0
- package/dist/src/agent/types.d.ts +50 -0
- package/dist/src/agent/types.js +1 -0
- package/dist/src/capture/context-filter.d.ts +7 -0
- package/dist/src/cli/main.d.ts +2 -0
- package/dist/src/cli/main.js +97 -3
- package/dist/src/git/git-adapter.d.ts +2 -0
- package/dist/src/hooks/claude-code.d.ts +1 -0
- package/dist/src/hooks/claude-code.js +84 -0
- package/dist/src/integrations/claude-code.d.ts +19 -0
- package/dist/src/integrations/claude-code.js +145 -0
- package/dist/src/mcp/main.d.ts +1 -0
- package/dist/src/mcp/main.js +5 -0
- package/dist/src/mcp/server.d.ts +2 -0
- package/dist/src/mcp/server.js +40 -0
- package/dist/src/project/project-resolver.d.ts +1 -0
- package/dist/src/runtime/diagnostics.d.ts +6 -0
- package/dist/src/runtime/init.d.ts +7 -0
- package/dist/src/runtime/knowledge.d.ts +9 -0
- package/dist/src/runtime/notes.d.ts +6 -0
- package/dist/src/runtime/runtime.d.ts +11 -0
- package/dist/src/runtime/runtime.js +6 -4
- package/dist/src/runtime/types.d.ts +294 -0
- package/dist/src/storage/atomic-write.d.ts +1 -0
- package/dist/src/storage/json-store.d.ts +6 -0
- package/dist/src/storage/layout.d.ts +13 -0
- package/dist/src/storage/lock.d.ts +1 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +1 -1
- package/package.json +46 -14
- package/skills/fluffy-context/SKILL.md +345 -0
package/dist/src/cli/main.js
CHANGED
|
@@ -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
|
|
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 @@
|
|
|
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,145 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { resolveProjectRoot } from '../project/project-resolver.js';
|
|
5
|
+
import { atomicWriteJson } from '../storage/atomic-write.js';
|
|
6
|
+
import { isRecord, readJson } from '../storage/json-store.js';
|
|
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}"`;
|
|
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 = {
|
|
15
|
+
sessionStart: 'ctx hook claude-code session-start',
|
|
16
|
+
userPrompt: 'ctx hook claude-code user-prompt',
|
|
17
|
+
};
|
|
18
|
+
const MCP_SERVER = { command: process.execPath, args: [cliPath, 'agent', 'serve'] };
|
|
19
|
+
const LEGACY_MCP_SERVER = { command: 'ctx', args: ['agent', 'serve'] };
|
|
20
|
+
function object(value) {
|
|
21
|
+
return isRecord(value) ? { ...value } : {};
|
|
22
|
+
}
|
|
23
|
+
function hookInstalled(value, command) {
|
|
24
|
+
if (!Array.isArray(value))
|
|
25
|
+
return false;
|
|
26
|
+
return value.some((group) => isRecord(group)
|
|
27
|
+
&& Array.isArray(group.hooks)
|
|
28
|
+
&& group.hooks.some((hook) => isRecord(hook) && hook.type === 'command' && hook.command === command));
|
|
29
|
+
}
|
|
30
|
+
function addHook(value, command, legacyCommand) {
|
|
31
|
+
const groups = Array.isArray(value) ? value.filter(isRecord).map((group) => ({ ...group })) : [];
|
|
32
|
+
if (hookInstalled(groups, command))
|
|
33
|
+
return groups;
|
|
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 }] }];
|
|
47
|
+
}
|
|
48
|
+
async function exists(filePath) {
|
|
49
|
+
try {
|
|
50
|
+
await access(filePath);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function loadObject(filePath) {
|
|
58
|
+
if (!await exists(filePath))
|
|
59
|
+
return {};
|
|
60
|
+
return readJson(filePath, isRecord);
|
|
61
|
+
}
|
|
62
|
+
function validateConfig(settings, mcp) {
|
|
63
|
+
if (settings.hooks !== undefined && !isRecord(settings.hooks))
|
|
64
|
+
throw new Error('Claude Code settings hooks must be an object');
|
|
65
|
+
const hooks = object(settings.hooks);
|
|
66
|
+
for (const event of ['SessionStart', 'UserPromptSubmit']) {
|
|
67
|
+
if (hooks[event] !== undefined && !Array.isArray(hooks[event]))
|
|
68
|
+
throw new Error(`Claude Code ${event} hooks must be an array`);
|
|
69
|
+
}
|
|
70
|
+
if (mcp.mcpServers !== undefined && !isRecord(mcp.mcpServers))
|
|
71
|
+
throw new Error('Claude Code MCP servers must be an object');
|
|
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
|
+
}
|
|
80
|
+
function status(projectRoot, settingsPath, mcpPath, settings, mcp, settingsExists, mcpExists, initialized) {
|
|
81
|
+
const hooks = object(settings.hooks);
|
|
82
|
+
const sessionStartInstalled = hookInstalled(hooks.SessionStart, HOOK_COMMANDS.sessionStart);
|
|
83
|
+
const userPromptInstalled = hookInstalled(hooks.UserPromptSubmit, HOOK_COMMANDS.userPrompt);
|
|
84
|
+
const mcpServers = object(mcp.mcpServers);
|
|
85
|
+
const configuredMcp = mcpServers['fluffy-context'];
|
|
86
|
+
const installed = mcpInstalled(configuredMcp);
|
|
87
|
+
const mcpConflict = configuredMcp !== undefined && !installed && !mcpInstalled(configuredMcp, LEGACY_MCP_SERVER);
|
|
88
|
+
return {
|
|
89
|
+
projectRoot,
|
|
90
|
+
initialized,
|
|
91
|
+
settingsPath,
|
|
92
|
+
mcpPath,
|
|
93
|
+
settingsExists,
|
|
94
|
+
mcpExists,
|
|
95
|
+
sessionStartInstalled,
|
|
96
|
+
userPromptInstalled,
|
|
97
|
+
mcpInstalled: installed,
|
|
98
|
+
mcpConflict,
|
|
99
|
+
installationNeeded: !sessionStartInstalled || !userPromptInstalled || !installed,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async function inspect(projectRoot) {
|
|
103
|
+
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
104
|
+
const mcpPath = path.join(projectRoot, '.mcp.json');
|
|
105
|
+
const [settingsExists, mcpExists, initialized] = await Promise.all([
|
|
106
|
+
exists(settingsPath),
|
|
107
|
+
exists(mcpPath),
|
|
108
|
+
exists(manifestPath(projectRoot)),
|
|
109
|
+
]);
|
|
110
|
+
const [settings, mcp] = await Promise.all([loadObject(settingsPath), loadObject(mcpPath)]);
|
|
111
|
+
validateConfig(settings, mcp);
|
|
112
|
+
return { status: status(projectRoot, settingsPath, mcpPath, settings, mcp, settingsExists, mcpExists, initialized), settings, mcp };
|
|
113
|
+
}
|
|
114
|
+
export async function inspectClaudeIntegration(startPath) {
|
|
115
|
+
const projectRoot = await resolveProjectRoot(startPath);
|
|
116
|
+
return (await inspect(projectRoot)).status;
|
|
117
|
+
}
|
|
118
|
+
export async function installClaudeIntegration(startPath, apply) {
|
|
119
|
+
const projectRoot = await resolveProjectRoot(startPath);
|
|
120
|
+
const inspected = await inspect(projectRoot);
|
|
121
|
+
const targetFiles = [inspected.status.settingsPath, inspected.status.mcpPath];
|
|
122
|
+
if (!apply || !inspected.status.installationNeeded)
|
|
123
|
+
return { ...inspected.status, applied: false, targetFiles };
|
|
124
|
+
if (inspected.status.mcpConflict)
|
|
125
|
+
throw new Error('existing fluffy-context MCP server configuration conflicts with the generated integration');
|
|
126
|
+
const settingsHooks = object(inspected.settings.hooks);
|
|
127
|
+
const settings = {
|
|
128
|
+
...inspected.settings,
|
|
129
|
+
hooks: {
|
|
130
|
+
...settingsHooks,
|
|
131
|
+
SessionStart: addHook(settingsHooks.SessionStart, HOOK_COMMANDS.sessionStart, LEGACY_HOOK_COMMANDS.sessionStart),
|
|
132
|
+
UserPromptSubmit: addHook(settingsHooks.UserPromptSubmit, HOOK_COMMANDS.userPrompt, LEGACY_HOOK_COMMANDS.userPrompt),
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
const mcpServers = object(inspected.mcp.mcpServers);
|
|
136
|
+
const mcp = {
|
|
137
|
+
...inspected.mcp,
|
|
138
|
+
mcpServers: { ...mcpServers, 'fluffy-context': MCP_SERVER },
|
|
139
|
+
};
|
|
140
|
+
await Promise.all([
|
|
141
|
+
atomicWriteJson(inspected.status.settingsPath, settings),
|
|
142
|
+
atomicWriteJson(inspected.status.mcpPath, mcp),
|
|
143
|
+
]);
|
|
144
|
+
return { ...(await inspect(projectRoot)).status, applied: true, targetFiles };
|
|
145
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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,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
|
-
|
|
332
|
-
|
|
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),
|