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.
- package/README.md +94 -25
- package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +1 -0
- package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +1 -0
- package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-CJCNI2Wd.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-PXH-TlGt.js +1 -0
- package/dist/web/assets/KnowledgePage-CNQXd15y.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +4 -0
- package/dist/web/assets/OverviewPage-ul7g7HXh.js +1 -0
- package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +1 -0
- package/dist/web/assets/PersonalProfilePage-S8ZcG2LU.css +1 -0
- package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +1 -0
- package/dist/web/assets/PublicProjectsPage-C700sR7z.js +1 -0
- package/dist/web/assets/ReviewPage-7MDDUDgA.js +1 -0
- package/dist/web/assets/console-B_Ena2DS.js +5 -0
- package/dist/web/assets/identity-CjBwYSMX.js +1 -0
- package/dist/web/assets/index-BQRkOwPx.js +2 -0
- package/dist/web/assets/{index-Devf7glZ.css → index-CpLDx3of.css} +1 -1
- package/dist/web/assets/model-C-5RVIJ2.js +1 -0
- package/dist/web/assets/{paths-B790iniO.js → paths-BkULqmGI.js} +1 -1
- package/dist/web/assets/{vue-router-BYTAoWT3.js → vue-router-DnD3hUm7.js} +1 -1
- package/dist/web/index.html +5 -6
- package/graph-provider/fuli_graph/app.py +48 -0
- package/graph-provider/fuli_graph/common_knowledge.py +443 -0
- package/graph-provider/fuli_graph/common_knowledge_models.py +54 -0
- package/graph-provider/fuli_graph/graph_models.py +9 -0
- package/graph-provider/fuli_graph/graph_query.py +25 -0
- package/graph-provider/fuli_graph/knowledge_audit.py +211 -0
- package/graph-provider/fuli_graph/knowledge_feedback_models.py +69 -0
- package/graph-provider/fuli_graph/knowledge_management.py +37 -0
- package/graph-provider/fuli_graph/knowledge_search.py +24 -2
- package/graph-provider/fuli_graph/models.py +69 -0
- package/graph-provider/fuli_graph/search_projection.py +59 -0
- package/graph-provider/fuli_graph/store_knowledge.py +2 -0
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +97 -2
- package/package.json +2 -1
- package/skills/capturing-session-knowledge/SKILL.md +86 -15
- package/src/agent-tools/graph-definitions.js +181 -13
- package/src/agent-tools/graph-handlers.js +19 -0
- package/src/cli/command-registry.js +6 -59
- package/src/cli/update-command.js +293 -0
- package/src/cli.js +7 -34
- package/src/graphiti/agent-knowledge-workflows.js +543 -0
- package/src/graphiti/federated-application.js +85 -104
- package/src/graphiti/knowledge-provider-mapping.js +91 -0
- package/src/graphiti/provider-client.js +15 -0
- package/src/graphiti/source-marker.js +1 -7
- package/src/graphiti/source-uri.js +29 -0
- package/src/http/static-handler.js +29 -3
- package/src/mcp/common-knowledge-preview-tokens.js +67 -0
- package/src/mcp/create-mcp-server.js +43 -7
- package/src/mcp/instructions.js +7 -8
- package/src/mcp/lifecycle-audit.js +27 -0
- package/src/mcp/task-context-registry.js +93 -0
- package/src/mcp/tool-annotations.js +16 -1
- package/src/mcp/tool-result.js +24 -0
- package/src/runtime-options.js +7 -8
- package/src/setup/agent-installation-status.js +14 -2
- package/src/setup/agents.js +12 -1
- package/src/setup/claude-code-config.js +194 -0
- package/src/setup/config-backup.js +20 -5
- package/src/setup/options.js +9 -1
- package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +0 -1
- package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +0 -1
- package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +0 -1
- package/dist/web/assets/KnowledgePage-Khuo3cNs.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +0 -4
- package/dist/web/assets/OverviewPage-DC4C4Fhn.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +0 -1
- package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +0 -1
- package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +0 -1
- package/dist/web/assets/ReviewPage-Dztk2jDG.js +0 -1
- package/dist/web/assets/console-tbvTUi6p.js +0 -1
- package/dist/web/assets/identity-B3s5CXhv.js +0 -1
- package/dist/web/assets/index-DwUDhQKc.js +0 -6
- package/dist/web/assets/model-DVT8sjy9.js +0 -1
- package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +0 -1
- package/src/cli/commands/candidates.js +0 -17
- package/src/cli/commands/ingestion.js +0 -32
- package/src/cli/commands/reads.js +0 -50
- package/src/cli/commands/spaces.js +0 -20
- package/src/cli/invocation.js +0 -47
- package/src/cli/migrate-command.js +0 -71
- package/src/cli/migration-destination.js +0 -117
- package/src/storage/import-json.js +0 -70
- package/src/storage/legacy-record-validation.js +0 -152
- 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' ||
|
package/src/mcp/tool-result.js
CHANGED
|
@@ -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';
|
package/src/runtime-options.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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(
|
|
65
|
+
throw new RuntimeConfigurationError(legacyJsonMessage(legacyPath));
|
|
67
66
|
}
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
function
|
|
71
|
-
return
|
|
72
|
-
|
|
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 (
|
|
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 };
|
package/src/setup/agents.js
CHANGED
|
@@ -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 === '
|
|
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
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
}
|
package/src/setup/options.js
CHANGED
|
@@ -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
|
|
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};
|