fuli-context 0.2.0 → 0.4.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 (91) hide show
  1. package/README.md +94 -25
  2. package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +1 -0
  3. package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +1 -0
  4. package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +1 -0
  5. package/dist/web/assets/KnowledgeOrganizerPage-CJCNI2Wd.css +1 -0
  6. package/dist/web/assets/KnowledgeOrganizerPage-PXH-TlGt.js +1 -0
  7. package/dist/web/assets/KnowledgePage-CNQXd15y.js +1 -0
  8. package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +4 -0
  9. package/dist/web/assets/OverviewPage-ul7g7HXh.js +1 -0
  10. package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +1 -0
  11. package/dist/web/assets/PersonalProfilePage-S8ZcG2LU.css +1 -0
  12. package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +1 -0
  13. package/dist/web/assets/PublicProjectsPage-C700sR7z.js +1 -0
  14. package/dist/web/assets/ReviewPage-7MDDUDgA.js +1 -0
  15. package/dist/web/assets/console-B_Ena2DS.js +5 -0
  16. package/dist/web/assets/identity-CjBwYSMX.js +1 -0
  17. package/dist/web/assets/index-BQRkOwPx.js +2 -0
  18. package/dist/web/assets/{index-Devf7glZ.css → index-CpLDx3of.css} +1 -1
  19. package/dist/web/assets/model-C-5RVIJ2.js +1 -0
  20. package/dist/web/assets/{paths-B790iniO.js → paths-BkULqmGI.js} +1 -1
  21. package/dist/web/assets/{vue-router-BYTAoWT3.js → vue-router-DnD3hUm7.js} +1 -1
  22. package/dist/web/index.html +5 -6
  23. package/graph-provider/fuli_graph/app.py +48 -0
  24. package/graph-provider/fuli_graph/common_knowledge.py +443 -0
  25. package/graph-provider/fuli_graph/common_knowledge_models.py +54 -0
  26. package/graph-provider/fuli_graph/graph_models.py +9 -0
  27. package/graph-provider/fuli_graph/graph_query.py +25 -0
  28. package/graph-provider/fuli_graph/knowledge_audit.py +211 -0
  29. package/graph-provider/fuli_graph/knowledge_feedback_models.py +69 -0
  30. package/graph-provider/fuli_graph/knowledge_management.py +37 -0
  31. package/graph-provider/fuli_graph/knowledge_search.py +24 -2
  32. package/graph-provider/fuli_graph/models.py +69 -0
  33. package/graph-provider/fuli_graph/search_projection.py +59 -0
  34. package/graph-provider/fuli_graph/store_knowledge.py +2 -0
  35. package/graph-provider/pyproject.toml +1 -1
  36. package/npm-shrinkwrap.json +97 -2
  37. package/package.json +2 -1
  38. package/skills/capturing-session-knowledge/SKILL.md +86 -15
  39. package/src/agent-tools/graph-definitions.js +181 -13
  40. package/src/agent-tools/graph-handlers.js +19 -0
  41. package/src/cli/command-registry.js +6 -59
  42. package/src/cli/update-command.js +293 -0
  43. package/src/cli.js +7 -34
  44. package/src/graphiti/agent-knowledge-workflows.js +543 -0
  45. package/src/graphiti/federated-application.js +85 -104
  46. package/src/graphiti/knowledge-provider-mapping.js +91 -0
  47. package/src/graphiti/provider-client.js +15 -0
  48. package/src/graphiti/source-marker.js +1 -7
  49. package/src/graphiti/source-uri.js +29 -0
  50. package/src/http/static-handler.js +29 -3
  51. package/src/mcp/common-knowledge-preview-tokens.js +67 -0
  52. package/src/mcp/create-mcp-server.js +43 -7
  53. package/src/mcp/instructions.js +7 -8
  54. package/src/mcp/lifecycle-audit.js +27 -0
  55. package/src/mcp/task-context-registry.js +93 -0
  56. package/src/mcp/tool-annotations.js +16 -1
  57. package/src/mcp/tool-result.js +24 -0
  58. package/src/runtime-options.js +7 -8
  59. package/src/setup/agent-installation-status.js +14 -2
  60. package/src/setup/agents.js +12 -1
  61. package/src/setup/claude-code-config.js +194 -0
  62. package/src/setup/config-backup.js +20 -5
  63. package/src/setup/options.js +9 -1
  64. package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +0 -1
  65. package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +0 -1
  66. package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +0 -1
  67. package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +0 -1
  68. package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +0 -1
  69. package/dist/web/assets/KnowledgePage-Khuo3cNs.js +0 -1
  70. package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +0 -4
  71. package/dist/web/assets/OverviewPage-DC4C4Fhn.js +0 -1
  72. package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +0 -1
  73. package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +0 -1
  74. package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +0 -1
  75. package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +0 -1
  76. package/dist/web/assets/ReviewPage-Dztk2jDG.js +0 -1
  77. package/dist/web/assets/console-tbvTUi6p.js +0 -1
  78. package/dist/web/assets/identity-B3s5CXhv.js +0 -1
  79. package/dist/web/assets/index-DwUDhQKc.js +0 -6
  80. package/dist/web/assets/model-DVT8sjy9.js +0 -1
  81. package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +0 -1
  82. package/src/cli/commands/candidates.js +0 -17
  83. package/src/cli/commands/ingestion.js +0 -32
  84. package/src/cli/commands/reads.js +0 -50
  85. package/src/cli/commands/spaces.js +0 -20
  86. package/src/cli/invocation.js +0 -47
  87. package/src/cli/migrate-command.js +0 -71
  88. package/src/cli/migration-destination.js +0 -117
  89. package/src/storage/import-json.js +0 -70
  90. package/src/storage/legacy-record-validation.js +0 -152
  91. package/src/storage/legacy-snapshot-validation.js +0 -32
@@ -0,0 +1,27 @@
1
+ import { appendFileSync } from 'node:fs';
2
+
3
+ const AUDITED_TOOLS = new Set([
4
+ 'begin_task_context',
5
+ 'checkpoint_task_knowledge',
6
+ 'verify_task_checkpoint'
7
+ ]);
8
+
9
+ export function auditLifecycleTool(
10
+ toolName,
11
+ {
12
+ auditPath = process.env.FULI_ACCEPTANCE_LIFECYCLE_AUDIT_PATH,
13
+ append = appendFileSync
14
+ } = {}
15
+ ) {
16
+ if (!auditPath || !AUDITED_TOOLS.has(toolName)) return false;
17
+ try {
18
+ append(
19
+ auditPath,
20
+ `${JSON.stringify({ event: toolName })}\n`,
21
+ { encoding: 'utf8', mode: 0o600 }
22
+ );
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
@@ -0,0 +1,93 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ const MAX_ACTIVE_SESSIONS = 256;
4
+
5
+ export class TaskContextRegistry {
6
+ constructor({ tokenFactory = () => `fuli-task-${randomUUID()}` } = {}) {
7
+ this.tokenFactory = tokenFactory;
8
+ this.sessions = new Map();
9
+ this.tokens = new Map();
10
+ }
11
+
12
+ begin({ sessionId, personalProjectId }) {
13
+ assertNonEmpty(sessionId, 'sessionId');
14
+ const previous = this.sessions.get(sessionId) ?? null;
15
+ if (previous) this.tokens.delete(previous.token);
16
+
17
+ const task = {
18
+ token: this.tokenFactory(),
19
+ sessionId,
20
+ personalProjectId: personalProjectId ?? null,
21
+ checkpoint: null
22
+ };
23
+ this.sessions.delete(sessionId);
24
+ this.sessions.set(sessionId, task);
25
+ this.tokens.set(task.token, task);
26
+ this.#evictOldest();
27
+ return {
28
+ ...task,
29
+ previousCheckpointMissing: Boolean(previous && !previous.checkpoint)
30
+ };
31
+ }
32
+
33
+ checkpoint(token, checkpoint) {
34
+ assertNonEmpty(token, 'taskContextToken');
35
+ const task = this.tokens.get(token);
36
+ if (!task) throw new TypeError('Unknown or expired Fuli task context token');
37
+ if (task.checkpoint) {
38
+ if (sameCheckpoint(task.checkpoint, checkpoint)) return task;
39
+ throw new TypeError('Fuli task context has already been checkpointed');
40
+ }
41
+ task.checkpoint = Object.freeze({ ...checkpoint });
42
+ return task;
43
+ }
44
+
45
+ context(token) {
46
+ assertNonEmpty(token, 'taskContextToken');
47
+ const task = this.tokens.get(token);
48
+ if (!task) throw new TypeError('Unknown or expired Fuli task context token');
49
+ return task;
50
+ }
51
+
52
+ verify(sessionId) {
53
+ assertNonEmpty(sessionId, 'sessionId');
54
+ const task = this.sessions.get(sessionId);
55
+ if (!task) {
56
+ return {
57
+ status: 'not_started',
58
+ guidance: 'No Fuli task context was started; do not block the Agent.'
59
+ };
60
+ }
61
+ if (task.checkpoint) {
62
+ return {
63
+ status: 'checkpointed',
64
+ disposition: task.checkpoint.disposition
65
+ };
66
+ }
67
+ return {
68
+ status: 'checkpoint_required',
69
+ decision: 'block',
70
+ reason: 'Before finishing, call checkpoint_task_knowledge once with either a bounded durable candidate batch or retain_nothing. Do not store raw transcripts, guesses, or temporary output.'
71
+ };
72
+ }
73
+
74
+ #evictOldest() {
75
+ while (this.sessions.size > MAX_ACTIVE_SESSIONS) {
76
+ const [sessionId, task] = this.sessions.entries().next().value;
77
+ this.sessions.delete(sessionId);
78
+ this.tokens.delete(task.token);
79
+ }
80
+ }
81
+ }
82
+
83
+ function assertNonEmpty(value, label) {
84
+ if (typeof value !== 'string' || !value.trim()) {
85
+ throw new TypeError(`${label} must be a nonempty string`);
86
+ }
87
+ }
88
+
89
+ function sameCheckpoint(left, right) {
90
+ return left.disposition === right.disposition
91
+ && left.reason === right.reason
92
+ && left.captureStatus === right.captureStatus;
93
+ }
@@ -1,4 +1,8 @@
1
1
  const READ_TOOLS = new Set([
2
+ 'verify_task_checkpoint',
3
+ 'search_current_project_knowledge',
4
+ 'discover_common_knowledge_candidates',
5
+ 'preview_common_knowledge_promotion',
2
6
  'list_knowledge_spaces',
3
7
  'list_personal_projects',
4
8
  'preview_personal_project_action',
@@ -10,11 +14,16 @@ const READ_TOOLS = new Set([
10
14
  ]);
11
15
 
12
16
  const WRITE_TOOLS = new Set([
17
+ 'begin_task_context',
18
+ 'checkpoint_task_knowledge',
13
19
  'get_collaboration_preferences',
14
20
  'resolve_deferred_preference_conflict',
15
21
  'capture_session_knowledge',
22
+ 'record_decision_trace',
16
23
  'search_knowledge_graph',
17
24
  'record_knowledge_usage',
25
+ 'record_knowledge_feedback',
26
+ 'apply_common_knowledge_promotion',
18
27
  'get_knowledge_graph',
19
28
  'search_human_knowledge_changes',
20
29
  'review_human_knowledge_change',
@@ -34,6 +43,7 @@ const WRITE_TOOLS = new Set([
34
43
 
35
44
  const DESTRUCTIVE_TOOLS = new Set([
36
45
  'resolve_deferred_preference_conflict',
46
+ 'apply_common_knowledge_promotion',
37
47
  'review_project_proposal',
38
48
  'review_project_relation',
39
49
  'review_personal_draft',
@@ -50,8 +60,13 @@ export function annotationsFor(name) {
50
60
  destructiveHint: DESTRUCTIVE_TOOLS.has(name),
51
61
  idempotentHint: readOnly || name === 'upsert_personal_project' ||
52
62
  name === 'publish_personal_project' || name === 'unsubscribe_public_project' ||
53
- name === 'record_knowledge_usage',
63
+ name === 'record_knowledge_usage' || name === 'record_decision_trace' ||
64
+ name === 'record_knowledge_feedback' ||
65
+ name === 'checkpoint_task_knowledge',
54
66
  openWorldHint: name === 'capture_session_knowledge' ||
67
+ name === 'record_decision_trace' ||
68
+ name === 'record_knowledge_feedback' ||
69
+ name === 'checkpoint_task_knowledge' ||
55
70
  name === 'publish_personal_project' ||
56
71
  name === 'create_project_relation' ||
57
72
  name === 'review_project_relation' ||
@@ -32,6 +32,30 @@ export function successToolResult(value, { limitBytes = RESULT_LIMIT_BYTES } = {
32
32
  };
33
33
  }
34
34
 
35
+ export function hookAdditionalContextToolResult(
36
+ value,
37
+ {
38
+ hookEventName,
39
+ label,
40
+ limitBytes = RESULT_LIMIT_BYTES
41
+ }
42
+ ) {
43
+ const result = successToolResult(value, { limitBytes });
44
+ const additionalContext = `${label}\n${JSON.stringify(result.structuredContent)}`;
45
+ return {
46
+ ...result,
47
+ content: [{
48
+ type: 'text',
49
+ text: JSON.stringify({
50
+ hookSpecificOutput: {
51
+ hookEventName,
52
+ additionalContext
53
+ }
54
+ })
55
+ }]
56
+ };
57
+ }
58
+
35
59
  export function errorToolResult(error) {
36
60
  const controlled = error instanceof ApplicationError;
37
61
  const code = controlled ? error.code : 'internal_error';
@@ -25,7 +25,7 @@ export function resolveRuntimeOptions(args = [], env = process.env, {
25
25
  let defaultDbPath = systemDbPath;
26
26
  if (cliDbPath === null && envDbPath === null) {
27
27
  defaultDbPath = resolveDefaultDbPath({ cwd, existsSync, systemDbPath });
28
- guardImplicitDefaultUpgrade({ cwd, existsSync, systemDbPath });
28
+ guardImplicitDefaultUpgrade({ cwd, existsSync });
29
29
  }
30
30
  return {
31
31
  dbPath: cliDbPath ?? envDbPath ?? defaultDbPath,
@@ -36,8 +36,7 @@ export function resolveRuntimeOptions(args = [], env = process.env, {
36
36
 
37
37
  export function resolveStore({ dbPath }) {
38
38
  if (extname(dbPath).toLowerCase() === '.json') {
39
- const destination = dbPath.slice(0, -extname(dbPath).length) + '.db';
40
- throw new RuntimeConfigurationError(migrationMessage(dbPath, destination));
39
+ throw new RuntimeConfigurationError(legacyJsonMessage(dbPath));
41
40
  }
42
41
  return openSqliteStore(dbPath);
43
42
  }
@@ -59,17 +58,17 @@ function resolveDefaultDbPath({ cwd, existsSync, systemDbPath }) {
59
58
  return systemDbPath;
60
59
  }
61
60
 
62
- function guardImplicitDefaultUpgrade({ cwd, existsSync, systemDbPath }) {
61
+ function guardImplicitDefaultUpgrade({ cwd, existsSync }) {
63
62
  const legacyPath = resolve(cwd, '.fuli/context.json');
64
63
  const localDatabasePath = resolve(cwd, LEGACY_LOCAL_DB_PATH);
65
64
  if (existsSync(legacyPath) && !existsSync(localDatabasePath)) {
66
- throw new RuntimeConfigurationError(migrationMessage(legacyPath, systemDbPath));
65
+ throw new RuntimeConfigurationError(legacyJsonMessage(legacyPath));
67
66
  }
68
67
  }
69
68
 
70
- function migrationMessage(source, destination) {
71
- return 'JSON is a legacy import format. Run once: node src/cli.js migrate ' +
72
- `--from ${quoteShellArgument(source)} --to ${quoteShellArgument(destination)}`;
69
+ function legacyJsonMessage(source) {
70
+ return `JSON is a retired legacy format (${quoteShellArgument(source)}). ` +
71
+ 'The current Fuli CLI no longer provides SQLite knowledge commands or JSON migration.';
73
72
  }
74
73
 
75
74
  export function openLocalApplication({ dbPath, personalSpaceName = DEFAULT_PERSONAL_SPACE }) {
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
 
3
3
  import { replaceFuliTable } from './codex-config.js';
4
4
  import { isCodexBootstrapCurrent } from './codex-bootstrap.js';
5
+ import { hasCurrentClaudeCodeHooks } from './claude-code-config.js';
5
6
  import { isSessionSkillCurrent } from './session-skill.js';
6
7
  import { readJsonFile } from '../storage/json-file.js';
7
8
 
@@ -27,9 +28,16 @@ export function inspectAgentInstallations(agents, context, {
27
28
  context,
28
29
  { fileExists, readText }
29
30
  );
31
+ const claudeLifecycleCurrent = agent.id !== 'claude-code' ||
32
+ hasCurrentClaudeCodeHooks(readJson(agent.settingsPath));
30
33
 
31
34
  let integrationStatus = 'not_connected';
32
- if (registration.current && skillsCurrent && codexBootstrapCurrent) {
35
+ if (
36
+ registration.current
37
+ && skillsCurrent
38
+ && codexBootstrapCurrent
39
+ && claudeLifecycleCurrent
40
+ ) {
33
41
  integrationStatus = 'connected';
34
42
  }
35
43
  else if (registration.present || skillsPresent) integrationStatus = 'update_available';
@@ -42,6 +50,9 @@ export function inspectAgentInstallations(agents, context, {
42
50
  skills: skillsCurrent ? 'current' : skillsPresent ? 'outdated' : 'missing',
43
51
  ...(agent.id === 'codex'
44
52
  ? { bootstrap: codexBootstrapCurrent ? 'current' : 'outdated' }
53
+ : {}),
54
+ ...(agent.id === 'claude-code'
55
+ ? { lifecycleHooks: claudeLifecycleCurrent ? 'current' : 'outdated' }
45
56
  : {})
46
57
  }
47
58
  };
@@ -71,7 +82,8 @@ function inspectRegistration(agent, context, readers) {
71
82
  present,
72
83
  current: present &&
73
84
  server.command === expected.command &&
74
- sameStrings(server.args, expected.args)
85
+ sameStrings(server.args, expected.args) &&
86
+ (agent.id !== 'claude-code' || server.alwaysLoad === true)
75
87
  };
76
88
  } catch {
77
89
  return { present: false, current: false };
@@ -3,6 +3,10 @@ import { existsSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import path from 'node:path';
5
5
 
6
+ import {
7
+ connectClaudeCode,
8
+ disconnectClaudeCode
9
+ } from './claude-code-config.js';
6
10
  import { connectCursor, disconnectCursor } from './cursor-config.js';
7
11
  import { connectCodex, disconnectCodex } from './codex-config.js';
8
12
 
@@ -34,6 +38,7 @@ export function discoverAgents({
34
38
  label: 'Claude Code',
35
39
  command: 'claude',
36
40
  configPath: pathApi.join(homeDir, '.claude.json'),
41
+ settingsPath: pathApi.join(homeDir, '.claude', 'settings.json'),
37
42
  skillPath: pathApi.join(homeDir, '.claude', 'skills', skillName),
38
43
  projectSkillPath: pathApi.join(homeDir, '.claude', 'skills', projectSkillName),
39
44
  available: Boolean(commandExists('claude'))
@@ -75,9 +80,13 @@ export function buildAgentCommands(agent, context) {
75
80
  export function connectAgent(agent, context, {
76
81
  runCommand = defaultRunCommand,
77
82
  connectCodexConfig = connectCodex,
83
+ connectClaudeCodeConfig = connectClaudeCode,
78
84
  connectCursorConfig = connectCursor
79
85
  } = {}) {
80
86
  if (agent.id === 'codex') return connectCodexConfig(agent, context);
87
+ if (agent.id === 'claude-code') {
88
+ return connectClaudeCodeConfig(agent, context);
89
+ }
81
90
  if (agent.id === 'cursor') return connectCursorConfig(agent, context);
82
91
 
83
92
  const commands = buildAgentCommands(agent, context);
@@ -91,10 +100,12 @@ export function connectAgent(agent, context, {
91
100
 
92
101
  export function disconnectAgent(agent, {
93
102
  disconnectCodexConfig = disconnectCodex,
103
+ disconnectClaudeCodeConfig = disconnectClaudeCode,
94
104
  disconnectJsonConfig = disconnectCursor
95
105
  } = {}) {
96
106
  if (agent.id === 'codex') return disconnectCodexConfig(agent);
97
- if (agent.id === 'cursor' || agent.id === 'claude-code') {
107
+ if (agent.id === 'claude-code') return disconnectClaudeCodeConfig(agent);
108
+ if (agent.id === 'cursor') {
98
109
  return disconnectJsonConfig(agent);
99
110
  }
100
111
  throw new TypeError(`Unsupported agent: ${agent.id}`);
@@ -0,0 +1,194 @@
1
+ import { existsSync } from 'node:fs';
2
+
3
+ import {
4
+ readJsonFile,
5
+ writeJsonFileAtomic
6
+ } from '../storage/json-file.js';
7
+
8
+ const FULI_SERVER = 'fuli';
9
+ const MANAGED_HOOK_TOOLS = new Set([
10
+ 'begin_task_context',
11
+ 'verify_task_checkpoint'
12
+ ]);
13
+
14
+ export function connectClaudeCode(agent, context, {
15
+ readConfig = readJsonFile,
16
+ writeConfig = writeJsonFileAtomic
17
+ } = {}) {
18
+ const current = readConfig(agent.configPath, {});
19
+ const settings = readConfig(agent.settingsPath, {});
20
+ assertObject(current, 'Claude Code config');
21
+ assertObject(settings, 'Claude Code settings');
22
+ assertObject(current.mcpServers ?? {}, 'Claude Code mcpServers');
23
+ assertHookSettings(settings.hooks ?? {});
24
+
25
+ const next = {
26
+ ...current,
27
+ mcpServers: {
28
+ ...(current.mcpServers ?? {}),
29
+ [FULI_SERVER]: {
30
+ type: 'stdio',
31
+ command: context.nodePath,
32
+ args: [
33
+ context.mcpServerPath,
34
+ '--runtime-config',
35
+ context.runtimeConfigPath
36
+ ],
37
+ alwaysLoad: true
38
+ }
39
+ }
40
+ };
41
+ const nextSettings = withManagedHooks(settings);
42
+ const registrationChanged = !sameJson(current, next);
43
+ const hooksChanged = !sameJson(settings, nextSettings);
44
+ if (registrationChanged) writeConfig(agent.configPath, next);
45
+ if (hooksChanged) writeConfig(agent.settingsPath, nextSettings);
46
+
47
+ return {
48
+ id: agent.id,
49
+ label: agent.label,
50
+ status: 'connected',
51
+ newTaskRequired: registrationChanged || hooksChanged
52
+ };
53
+ }
54
+
55
+ export function disconnectClaudeCode(agent, {
56
+ fileExists = existsSync,
57
+ readConfig = readJsonFile,
58
+ writeConfig = writeJsonFileAtomic
59
+ } = {}) {
60
+ let changed = false;
61
+ if (fileExists(agent.configPath)) {
62
+ const current = readConfig(agent.configPath, {});
63
+ assertObject(current, 'Claude Code config');
64
+ assertObject(current.mcpServers ?? {}, 'Claude Code mcpServers');
65
+ if (Object.hasOwn(current.mcpServers ?? {}, FULI_SERVER)) {
66
+ const { [FULI_SERVER]: _removed, ...remainingServers } =
67
+ current.mcpServers;
68
+ writeConfig(agent.configPath, {
69
+ ...current,
70
+ mcpServers: remainingServers
71
+ });
72
+ changed = true;
73
+ }
74
+ }
75
+
76
+ if (agent.settingsPath && fileExists(agent.settingsPath)) {
77
+ const settings = readConfig(agent.settingsPath, {});
78
+ assertObject(settings, 'Claude Code settings');
79
+ assertHookSettings(settings.hooks ?? {});
80
+ const nextSettings = withoutManagedHooks(settings);
81
+ if (!sameJson(settings, nextSettings)) {
82
+ writeConfig(agent.settingsPath, nextSettings);
83
+ changed = true;
84
+ }
85
+ }
86
+
87
+ return {
88
+ id: agent.id,
89
+ label: agent.label,
90
+ status: changed ? 'disconnected' : 'not_connected'
91
+ };
92
+ }
93
+
94
+ export function hasCurrentClaudeCodeHooks(settings) {
95
+ try {
96
+ assertObject(settings, 'Claude Code settings');
97
+ assertHookSettings(settings.hooks ?? {});
98
+ } catch {
99
+ return false;
100
+ }
101
+ return [
102
+ ['UserPromptSubmit', 'begin_task_context'],
103
+ ['Stop', 'verify_task_checkpoint']
104
+ ].every(([event, tool]) => (
105
+ (settings.hooks?.[event] ?? []).some((group) =>
106
+ group.hooks.some((hook) => isManagedHook(hook, tool))
107
+ )
108
+ ));
109
+ }
110
+
111
+ function withManagedHooks(settings) {
112
+ const clean = withoutManagedHooks(settings);
113
+ const hooks = { ...(clean.hooks ?? {}) };
114
+ hooks.UserPromptSubmit = [
115
+ ...(hooks.UserPromptSubmit ?? []),
116
+ managedHookGroup('begin_task_context', {
117
+ sessionId: '${session_id}',
118
+ projectPath: '${cwd}'
119
+ }, 'Loading Fuli task context')
120
+ ];
121
+ hooks.Stop = [
122
+ ...(hooks.Stop ?? []),
123
+ managedHookGroup(
124
+ 'verify_task_checkpoint',
125
+ { sessionId: '${session_id}' },
126
+ 'Checking Fuli task checkpoint'
127
+ )
128
+ ];
129
+ return { ...clean, hooks };
130
+ }
131
+
132
+ function withoutManagedHooks(settings) {
133
+ if (!settings.hooks) return settings;
134
+ const hooks = {};
135
+ for (const [event, groups] of Object.entries(settings.hooks)) {
136
+ const remainingGroups = groups
137
+ .map((group) => ({
138
+ ...group,
139
+ hooks: group.hooks.filter((hook) => !isManagedHook(hook))
140
+ }))
141
+ .filter((group) => group.hooks.length > 0);
142
+ if (remainingGroups.length) hooks[event] = remainingGroups;
143
+ }
144
+ if (Object.keys(hooks).length === 0) {
145
+ const { hooks: _removed, ...withoutHooks } = settings;
146
+ return withoutHooks;
147
+ }
148
+ return { ...settings, hooks };
149
+ }
150
+
151
+ function managedHookGroup(tool, input, statusMessage) {
152
+ return {
153
+ hooks: [{
154
+ type: 'mcp_tool',
155
+ server: FULI_SERVER,
156
+ tool,
157
+ input,
158
+ timeout: 30,
159
+ statusMessage
160
+ }]
161
+ };
162
+ }
163
+
164
+ function isManagedHook(hook, tool = null) {
165
+ return hook?.type === 'mcp_tool'
166
+ && hook.server === FULI_SERVER
167
+ && MANAGED_HOOK_TOOLS.has(hook.tool)
168
+ && (tool === null || hook.tool === tool);
169
+ }
170
+
171
+ function assertHookSettings(value) {
172
+ assertObject(value, 'Claude Code hooks');
173
+ for (const [event, groups] of Object.entries(value)) {
174
+ if (!Array.isArray(groups)) {
175
+ throw new TypeError(`Claude Code hook event ${event} must be an array`);
176
+ }
177
+ for (const group of groups) {
178
+ assertObject(group, `Claude Code ${event} hook group`);
179
+ if (!Array.isArray(group.hooks)) {
180
+ throw new TypeError(`Claude Code ${event} hook group must contain hooks`);
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ function assertObject(value, label) {
187
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
188
+ throw new TypeError(`${label} must be a JSON object`);
189
+ }
190
+ }
191
+
192
+ function sameJson(left, right) {
193
+ return JSON.stringify(left) === JSON.stringify(right);
194
+ }
@@ -12,11 +12,26 @@ export function backupAgentConfig(agent, {
12
12
  makeDirectory = mkdirSync,
13
13
  copyFile = copyFileSync
14
14
  }) {
15
- if (!fileExists(agent.configPath)) return null;
15
+ const sources = [
16
+ { path: agent.configPath, suffix: '' },
17
+ ...(agent.settingsPath
18
+ ? [{ path: agent.settingsPath, suffix: '-settings' }]
19
+ : [])
20
+ ].filter(({ path }) => path && fileExists(path));
21
+ if (sources.length === 0) return null;
16
22
  makeDirectory(backupDir, { recursive: true });
17
23
  const timestamp = now().toISOString().replaceAll(':', '-').replaceAll('.', '-');
18
- const extension = extname(agent.configPath) || '.bak';
19
- const backupPath = join(backupDir, `${agent.id}-${timestamp}${extension}`);
20
- copyFile(agent.configPath, backupPath);
21
- return backupPath;
24
+ const backups = sources.map(({ path, suffix }) => {
25
+ const extension = extname(path) || '.bak';
26
+ const backupPath = join(
27
+ backupDir,
28
+ `${agent.id}${suffix}-${timestamp}${extension}`
29
+ );
30
+ copyFile(path, backupPath);
31
+ return { path, backupPath };
32
+ });
33
+ return (
34
+ backups.find(({ path }) => path === agent.configPath)
35
+ ?? backups[0]
36
+ ).backupPath;
22
37
  }
@@ -16,6 +16,14 @@ const BOOLEAN_OPTIONS = Object.freeze({
16
16
  const PROVIDER_MODE_OPTIONS = new Set(['--personal-only', '--with-dev-public']);
17
17
 
18
18
  export function parseSetupOptions(args = []) {
19
+ return parseSetupLikeOptions(args, 'setup');
20
+ }
21
+
22
+ export function parseUpdateOptions(args = []) {
23
+ return parseSetupLikeOptions(args, 'update');
24
+ }
25
+
26
+ function parseSetupLikeOptions(args, command) {
19
27
  const result = {
20
28
  dataDir: null,
21
29
  personalSpaceName: '我',
@@ -47,7 +55,7 @@ export function parseSetupOptions(args = []) {
47
55
  continue;
48
56
  }
49
57
  const valueKey = VALUE_OPTIONS[flag];
50
- if (!valueKey) throw new TypeError(`Unknown setup option: ${flag}`);
58
+ if (!valueKey) throw new TypeError(`Unknown ${command} option: ${flag}`);
51
59
  const value = args[index + 1];
52
60
  if (typeof value !== 'string' || !value.trim() || value.startsWith('--')) {
53
61
  throw new TypeError(`Missing value for ${flag}`);
@@ -1 +0,0 @@
1
- import{D as e,L as t,P as n,T as r,a as i,c as a,d as o,m as s,n as c,nt as l,p as u,t as d,u as f,v as p,y as m}from"./console-tbvTUi6p.js";import{o as h}from"./runtime-dom.esm-bundler-DttsK21F.js";import{n as g,r as _,t as v}from"./identity-B3s5CXhv.js";var y={class:`view connections-view`},b={class:`service-connection-grid`,"aria-label":`知识服务连接状态`},x=[`data-status`],S={class:`service-state`},C=[`data-status`],w={class:`service-state`},T={key:0,class:`connection-subscriptions`},E={class:`subscription-list`},D=[`onClick`],O={key:0,class:`empty-state`},k=m({__name:`ConnectionsPage`,setup(m){let k=d(),A=n(``),j=f(()=>k.state?.providers?.personal?.status===`ready`),M=f(()=>k.state?.providers?.workspaces??[]),N=f(()=>M.value.filter(({status:e})=>e===`ready`).length),P=f(()=>k.state?.subscriptions??[]),F=f(()=>new Set(P.value.map(({provider_url:e,project_id:t})=>`${e}::${t}`))),I=f(()=>(k.state?.projects??[]).filter(e=>!F.value.has(R(e)))),L=f(()=>I.value.map(e=>({value:R(e),label:e.name,meta:`#${v(e.id,26)}`,search:g(e.id)})));function R(e){return`${e.providerUrl}::${e.id}`}async function z(){let e=I.value.find(e=>R(e)===A.value);if(e)try{await i(`/api/subscriptions`,{personalSpaceId:k.activePersonalSpace?.id,projectId:e.id,providerUrl:e.providerUrl,projectName:e.name}),A.value=``,k.notify(`已订阅“${e.name}”。`),await k.refresh()}catch(e){k.reportError(e)}}async function B(e){try{let t=new URLSearchParams({personalSpaceId:k.activePersonalSpace?.id??``,providerUrl:e.provider_url});await c(`/api/subscriptions/${encodeURIComponent(e.project_id)}?${t}`),k.notify(`已取消订阅“${e.project_name??e.project_id}”。`),await k.refresh()}catch(e){k.reportError(e)}}return(n,i)=>(r(),s(`section`,y,[i[12]||=o(`div`,{class:`connection-intro`},[o(`h3`,null,`服务连接与订阅`),o(`p`,null,`本地知识库始终独立运行;公共能力只在连接共享 Provider 后开启。`)],-1),o(`div`,b,[o(`article`,{class:`service-connection-card`,"data-status":j.value?`ready`:`error`},[o(`header`,null,[i[1]||=o(`span`,{class:`service-connection-icon local`,"aria-hidden":`true`},[o(`span`,{class:`nav-icon nav-icon-personal-project`})],-1),i[2]||=o(`div`,null,[o(`p`,null,`LOCAL KNOWLEDGE`),o(`h3`,null,`本地 Graphiti`)],-1),o(`span`,S,l(j.value?`已连接`:`连接异常`),1)]),o(`p`,null,l(j.value?`个人项目、协作偏好和会话知识正在写入本机图谱。`:`本地知识库暂时无法使用,请检查 Graphiti 与 Neo4j。`),1),i[3]||=o(`dl`,null,[o(`div`,null,[o(`dt`,null,`存储`),o(`dd`,null,`Neo4j`)]),o(`div`,null,[o(`dt`,null,`用途`),o(`dd`,null,`个人项目与协作偏好`)])],-1)],8,x),o(`article`,{class:`service-connection-card`,"data-status":t(k).publicRuntimeStatus},[o(`header`,null,[i[4]||=o(`span`,{class:`service-connection-icon public`,"aria-hidden":`true`},[o(`span`,{class:`nav-icon nav-icon-public-project`})],-1),i[5]||=o(`div`,null,[o(`p`,null,`PUBLIC PROVIDER`),o(`h3`,null,`公共服务`)],-1),o(`span`,w,l(t(k).publicRuntimeStatus===`ready`?`已连接`:t(k).publicRuntimeStatus===`error`?`连接异常`:`未连接`),1)]),o(`p`,null,l(t(k).publicRuntimeStatus===`ready`?`公共项目、订阅、发布与团队协作功能已经可以使用。`:t(k).publicRuntimeStatus===`error`?`公共服务已经配置,但当前无法访问;本地知识库仍可正常使用。`:`当前未连接公共服务;公共能力保持关闭。`),1),o(`dl`,null,[o(`div`,null,[i[6]||=o(`dt`,null,`当前状态`,-1),o(`dd`,null,l(t(k).publicRuntimeStatus===`ready`?`${N.value||M.value.length} 个共享服务可用`:`尚未可用`),1)]),i[7]||=o(`div`,null,[o(`dt`,null,`用途`),o(`dd`,null,`发现、订阅、发布与协作`)],-1)])],8,C)]),t(k).state?.capabilities?.subscribeProject?(r(),s(`section`,T,[i[11]||=o(`div`,{class:`section-title`},[o(`h3`,null,`公共项目订阅`),o(`p`,null,`只把你主动选择的公共项目加入个人检索范围。`)],-1),o(`div`,E,[(r(!0),s(a,null,e(P.value,e=>(r(),s(`div`,{key:`${e.provider_url}:${e.project_id}`,class:`subscription-row`},[i[9]||=o(`span`,null,`↗`,-1),o(`div`,null,[o(`strong`,null,l(e.project_name??e.project_id),1),i[8]||=o(`span`,null,`团队共享项目`,-1)]),o(`button`,{class:`secondary-action subscription-action`,type:`button`,onClick:t=>B(e)},`取消订阅`,8,D)]))),128)),P.value.length?u(``,!0):(r(),s(`div`,O,`尚未订阅团队共享项目`))]),o(`form`,{class:`subscription-form`,onSubmit:h(z,[`prevent`])},[p(_,{modelValue:A.value,"onUpdate:modelValue":i[0]||=e=>A.value=e,options:L.value,label:`团队共享项目`,placeholder:`选择公共项目`,searchable:``,required:``},null,8,[`modelValue`,`options`]),i[10]||=o(`button`,{type:`submit`},`订阅项目`,-1)],32)])):u(``,!0)]))}});export{k as default};