fuli-context 0.7.3 → 0.7.4-beta.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 +170 -18
- package/README.zh-CN.md +146 -12
- package/compose.graphiti.yml +2 -0
- package/dist/web/assets/{AboutPage-qeO_vm0T.js → AboutPage-CftRexSY.js} +1 -1
- package/dist/web/assets/{ConnectionsPage-DO0GR9Xg.js → ConnectionsPage-D5J91x6d.js} +1 -1
- package/dist/web/assets/{useMinimumLoadingDisplay-BpCyBeko.css → KnowledgeEditDialog-DWUV_NPp.css} +1 -1
- package/dist/web/assets/KnowledgeEditDialog-wotVahvr.js +1 -0
- package/dist/web/assets/{KnowledgeInspector-Bjhdu2mf.css → KnowledgeInspector--1NujtXW.css} +1 -1
- package/dist/web/assets/KnowledgeInspector-BzkW0mVs.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-BebUDiHt.js +1 -0
- package/dist/web/assets/KnowledgePage-OUT07aKH.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-1pHJ0MNZ.js +4 -0
- package/dist/web/assets/{OverviewPage-CmCcDCAs.js → OverviewPage-CHE6XhdS.js} +1 -1
- package/dist/web/assets/PersonalProfilePage-B7T09bow.css +1 -0
- package/dist/web/assets/PersonalProfilePage-CXXiabUy.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-d2wYGW8n.js +1 -0
- package/dist/web/assets/PublicProjectsPage-hxLShtnZ.js +1 -0
- package/dist/web/assets/{ReviewPage-B3vnZ6gn.js → ReviewPage-DUeyu7E8.js} +1 -1
- package/dist/web/assets/{SearchableSelect-ih69qbcS.js → SearchableSelect-C5RiB5F8.js} +1 -1
- package/dist/web/assets/SettingsPage-BMuASSzQ.js +1 -0
- package/dist/web/assets/SettingsPage-CAfz4qiV.css +1 -0
- package/dist/web/assets/WritingTastePage-B9BCYIwQ.js +1 -0
- package/dist/web/assets/WritingTastePage-DSgvTH6x.css +1 -0
- package/dist/web/assets/{console-DE40hXGC.js → console-DMvHxKf9.js} +1 -1
- package/dist/web/assets/conversation-launcher-settings-Dnlcw7Rj.js +1 -0
- package/dist/web/assets/{i18n-CDxBs1sz.js → i18n-CWfd8Dtt.js} +1 -1
- package/dist/web/assets/{identity-BCxb1mcy.js → identity-C9mLdR8L.js} +1 -1
- package/dist/web/assets/index-F5e2sQ7p.js +2 -0
- package/dist/web/assets/{index-CCOGfZzE.css → index-X2j29j8i.css} +1 -1
- package/dist/web/assets/{model-CCMdt10Q.js → model-CsYAR8O3.js} +1 -1
- package/dist/web/assets/paths-CDREouhi.js +1 -0
- package/dist/web/assets/useApi-CROJJdhE-BLQiPVw0.js +1 -0
- package/dist/web/assets/vue-router-DN8x6iY1.js +1 -0
- package/dist/web/index.html +7 -6
- package/graph-provider/fuli_graph/app.py +238 -2
- package/graph-provider/fuli_graph/auth.py +18 -0
- package/graph-provider/fuli_graph/collaboration_context.py +253 -17
- package/graph-provider/fuli_graph/common_knowledge.py +6 -2
- package/graph-provider/fuli_graph/config.py +5 -0
- package/graph-provider/fuli_graph/graph_models.py +6 -0
- package/graph-provider/fuli_graph/graph_projection.py +14 -0
- package/graph-provider/fuli_graph/graph_query.py +27 -3
- package/graph-provider/fuli_graph/knowledge_management.py +19 -1
- package/graph-provider/fuli_graph/knowledge_search.py +11 -0
- package/graph-provider/fuli_graph/models.py +15 -2
- package/graph-provider/fuli_graph/personal_global_preference_models.py +234 -0
- package/graph-provider/fuli_graph/personal_global_preferences.py +1247 -0
- package/graph-provider/fuli_graph/project_action_models.py +22 -0
- package/graph-provider/fuli_graph/project_knowledge.py +121 -2
- package/graph-provider/fuli_graph/provider_values.py +24 -0
- package/graph-provider/fuli_graph/runtime.py +78 -0
- package/graph-provider/fuli_graph/search_projection.py +14 -1
- package/graph-provider/fuli_graph/store_knowledge.py +116 -2
- package/graph-provider/fuli_graph/workflow_candidate_models.py +168 -0
- package/graph-provider/fuli_graph/workflow_candidates.py +996 -0
- package/graph-provider/fuli_graph/workflow_observation_models.py +30 -0
- package/graph-provider/fuli_graph/workflow_observations.py +84 -0
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +18 -18
- package/package.json +4 -4
- package/skills/capturing-session-knowledge/SKILL.md +32 -5
- package/skills/flreview/SKILL.md +4 -3
- package/src/agent-tools/graph-definitions.js +136 -20
- package/src/agent-tools/graph-handlers.js +10 -4
- package/src/cli/command-registry.js +1 -1
- package/src/cli/local-runtime-command.js +69 -2
- package/src/graphiti/agent-knowledge-workflows.js +497 -16
- package/src/graphiti/collaboration-preference-projection.js +162 -0
- package/src/graphiti/federated-application.js +155 -161
- package/src/graphiti/federated-search-support.js +102 -0
- package/src/graphiti/knowledge-provider-mapping.js +15 -0
- package/src/graphiti/provider-client.js +162 -10
- package/src/graphiti/related-project-suggestions.js +109 -0
- package/src/graphiti/runtime-config.js +14 -0
- package/src/graphiti/source-marker.js +2 -2
- package/src/graphiti/user-taste-skill.js +241 -0
- package/src/graphiti/workflow-candidate-mapping.js +87 -0
- package/src/graphiti/workflow-observation.js +82 -0
- package/src/graphiti/writing-taste-profile-workflow.js +25 -0
- package/src/graphiti/writing-taste-profile.js +490 -0
- package/src/http/graph-api-router.js +15 -2
- package/src/mcp/context-resources.js +158 -0
- package/src/mcp/create-mcp-server.js +39 -5
- package/src/mcp/instructions.js +5 -7
- package/src/mcp/session-id.js +60 -0
- package/src/mcp/task-context-registry.js +15 -3
- package/src/mcp/tool-annotations.js +8 -1
- package/src/mcp/tool-result.js +28 -4
- package/src/server.js +0 -1
- package/src/setup/claude-code-config.js +17 -6
- package/src/setup/graph-runtime.js +20 -2
- package/src/system/runtime-settings.js +121 -2
- package/src/system/system-service.js +5 -1
- package/dist/web/assets/KnowledgeInspector-DEW5lA1m.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-BkM568Ld.js +0 -1
- package/dist/web/assets/KnowledgePage-BAT4aZIm.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-DyGtBz--.js +0 -4
- package/dist/web/assets/PersonalProfilePage-C42mwKN8.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CdmEzyvZ.css +0 -1
- package/dist/web/assets/PersonalProjectsPage-BwaXR62K.js +0 -1
- package/dist/web/assets/PublicProjectsPage-egKBulOq.js +0 -1
- package/dist/web/assets/SettingsPage-C6ouU0sR.css +0 -1
- package/dist/web/assets/SettingsPage-oLE8mNs5.js +0 -1
- package/dist/web/assets/index-BzhNfzU_.js +0 -2
- package/dist/web/assets/paths-DwHeiGMT.js +0 -1
- package/dist/web/assets/useMinimumLoadingDisplay-HdI7XAmu.js +0 -1
- package/dist/web/assets/vue-router-Drfl1F8i.js +0 -1
|
@@ -7,6 +7,7 @@ import { MCP_INSTRUCTIONS } from './instructions.js';
|
|
|
7
7
|
import { createProjectActionPreviewTokens } from './project-action-preview-tokens.js';
|
|
8
8
|
import { createCommonKnowledgePreviewTokens } from './common-knowledge-preview-tokens.js';
|
|
9
9
|
import { auditLifecycleTool } from './lifecycle-audit.js';
|
|
10
|
+
import { registerFuliContextResources } from './context-resources.js';
|
|
10
11
|
import { annotationsFor } from './tool-annotations.js';
|
|
11
12
|
import {
|
|
12
13
|
errorToolResult,
|
|
@@ -15,31 +16,58 @@ import {
|
|
|
15
16
|
successToolResult
|
|
16
17
|
} from './tool-result.js';
|
|
17
18
|
import { jsonSchemaToZod, openObjectSchema } from './tool-schema.js';
|
|
19
|
+
import {
|
|
20
|
+
mcpHostSessionId,
|
|
21
|
+
nativeCodexThreadId,
|
|
22
|
+
normalizeAgentSessionInput
|
|
23
|
+
} from './session-id.js';
|
|
18
24
|
|
|
19
25
|
const TOOL_RESULT_LIMIT_BYTES = Object.freeze({
|
|
20
26
|
begin_task_context: 16 * 1024,
|
|
21
27
|
get_collaboration_preferences: 16 * 1024,
|
|
28
|
+
get_user_taste_skill: 32 * 1024,
|
|
22
29
|
search_knowledge_graph: 32 * 1024,
|
|
23
30
|
search_connected_knowledge: 64 * 1024,
|
|
24
31
|
search_current_project_knowledge: 64 * 1024,
|
|
25
32
|
discover_common_knowledge_candidates: 32 * 1024,
|
|
33
|
+
discover_personal_global_preference_candidates: 32 * 1024,
|
|
34
|
+
preview_personal_global_preference_decision: 32 * 1024,
|
|
26
35
|
list_personal_projects: 32 * 1024,
|
|
27
|
-
list_knowledge_review_candidates: 32 * 1024
|
|
36
|
+
list_knowledge_review_candidates: 32 * 1024,
|
|
37
|
+
list_workflow_candidates: 32 * 1024,
|
|
38
|
+
recommend_next_workflow_steps: 32 * 1024
|
|
28
39
|
});
|
|
29
40
|
|
|
30
|
-
export function createMcpServer(
|
|
41
|
+
export function createMcpServer(
|
|
42
|
+
app,
|
|
43
|
+
{ env = process.env, clock = () => new Date() } = {}
|
|
44
|
+
) {
|
|
31
45
|
const server = new McpServer(
|
|
32
46
|
{ name: 'fuli', version: FULI_VERSION },
|
|
33
47
|
{ instructions: MCP_INSTRUCTIONS }
|
|
34
48
|
);
|
|
35
49
|
|
|
36
|
-
const
|
|
50
|
+
const nativeThreadId = nativeCodexThreadId(env);
|
|
51
|
+
const tools = createToolMap(
|
|
52
|
+
app,
|
|
53
|
+
createProjectActionPreviewTokens(),
|
|
54
|
+
nativeThreadId,
|
|
55
|
+
mcpHostSessionId(env),
|
|
56
|
+
clock
|
|
57
|
+
);
|
|
37
58
|
for (const tool of tools.values()) registerTool(server, tool);
|
|
59
|
+
registerFuliContextResources(server, app);
|
|
38
60
|
registerCallHandler(server, tools);
|
|
39
61
|
return server;
|
|
40
62
|
}
|
|
41
63
|
|
|
42
|
-
function createToolMap(
|
|
64
|
+
function createToolMap(
|
|
65
|
+
app,
|
|
66
|
+
projectActionPreviews,
|
|
67
|
+
nativeThreadId,
|
|
68
|
+
hostSessionId,
|
|
69
|
+
clock
|
|
70
|
+
) {
|
|
43
71
|
const commonKnowledgePreviews = createCommonKnowledgePreviewTokens();
|
|
44
72
|
return new Map(listAgentTools().map((definition) => [definition.name, {
|
|
45
73
|
definition,
|
|
@@ -47,7 +75,13 @@ function createToolMap(app, projectActionPreviews) {
|
|
|
47
75
|
invoke: (input) => invokeAgentTool(
|
|
48
76
|
app,
|
|
49
77
|
definition.name,
|
|
50
|
-
|
|
78
|
+
normalizeAgentSessionInput(
|
|
79
|
+
definition.name,
|
|
80
|
+
input,
|
|
81
|
+
nativeThreadId,
|
|
82
|
+
hostSessionId,
|
|
83
|
+
clock
|
|
84
|
+
),
|
|
51
85
|
projectActionPreviews,
|
|
52
86
|
commonKnowledgePreviews
|
|
53
87
|
)
|
package/src/mcp/instructions.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
export const MCP_INSTRUCTIONS = [
|
|
2
|
-
|
|
3
|
-
'Before asking
|
|
4
|
-
'Before
|
|
5
|
-
'Use record_decision_trace
|
|
6
|
-
'
|
|
7
|
-
'After material personal-knowledge use call record_knowledge_usage; retrieval alone never counts or permits public use.',
|
|
8
|
-
'Terminal-safe Markdown. Final MUST begin with sourceMarker.leadMarkdown; append sourceMarker.markdown unchanged on match. On miss use noMatchSourceMarker leadMarkdown only (empty markdown). Never wrap markers in HTML.'
|
|
2
|
+
'Each user task: hook-provided task context from begin_task_context. Otherwise, before other tools/answer, call exactly get_collaboration_preferences with projectPath=cwd and taskPrompt=current user request as fallback; never substitute a project action. Apply all effective_preferences: personal-global everywhere, matched project, authorized inheritable parent. For taste/judgment, call get_user_taste_skill; never replaces a user-authored Skill. FULI fuli:// exposes exact projects/global taste; an @ selection is explicit and adds no project. Resolve relevant deferred_conflict first; never guess personalProjectId. Writes enforce actual payload; final text is not compliance.',
|
|
3
|
+
'Before asking user to repeat a stable project fact or method, inspect task_knowledge_recall. On miss call search_current_project_knowledge with focused action, artifact, target-system, or identifier queries; never use the whole conversational request as the only query. Search active child first, then inheritable parent via authorized PART_OF/USES_KNOWLEDGE_FROM, 2 hops; parent prefs need descendants/selected_projects; local_only/RELATED_TO never inherit; authority > distance > weight; equal conflicts defer.',
|
|
4
|
+
'Before saying unknown, call search_knowledge_graph. Use exact IDs for extra projects. RELATED_TO suggests; ask first. Use all_local_confirmed only after consent, then rg only in safe current repo/workspace; never scan outside it. Live state: monitoring/Git MCP. External/public: search_connected_knowledge; agent_decide is response-only.',
|
|
5
|
+
'Use record_decision_trace; record_knowledge_feedback on rejected/outdated/failed items. Batch durable confirmed knowledge with capture_session_knowledge; no secrets/guesses. Finish with checkpoint_task_knowledge: capture_candidates or retain_nothing. Then record_knowledge_usage.',
|
|
6
|
+
'Terminal-safe Markdown. Final MUST begin with sourceMarker.leadMarkdown and append sourceMarker.markdown unchanged on match. On miss use noMatchSourceMarker leadMarkdown only (empty markdown); never wrap markers in HTML.'
|
|
9
7
|
].join(' ');
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4
|
+
const WORKFLOW_OBSERVATION_TOOL = 'record_workflow_transition_observation';
|
|
5
|
+
|
|
6
|
+
const SESSION_ID_TOOLS = new Set([
|
|
7
|
+
'begin_task_context',
|
|
8
|
+
'verify_task_checkpoint',
|
|
9
|
+
'capture_session_knowledge',
|
|
10
|
+
'record_decision_trace',
|
|
11
|
+
'record_knowledge_usage',
|
|
12
|
+
'record_knowledge_feedback'
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export function nativeCodexThreadId(env = process.env) {
|
|
16
|
+
const value = typeof env?.CODEX_THREAD_ID === 'string'
|
|
17
|
+
? env.CODEX_THREAD_ID.trim()
|
|
18
|
+
: '';
|
|
19
|
+
return UUID.test(value) ? value : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function mcpHostSessionId(
|
|
23
|
+
env = process.env,
|
|
24
|
+
idFactory = randomUUID
|
|
25
|
+
) {
|
|
26
|
+
return nativeCodexThreadId(env) ?? `fuli-host-${idFactory()}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function normalizeAgentSessionInput(
|
|
30
|
+
name,
|
|
31
|
+
input,
|
|
32
|
+
nativeThreadId,
|
|
33
|
+
hostSessionId = nativeThreadId,
|
|
34
|
+
clock = () => new Date()
|
|
35
|
+
) {
|
|
36
|
+
if (name === WORKFLOW_OBSERVATION_TOOL) {
|
|
37
|
+
if (!hostSessionId) {
|
|
38
|
+
throw new TypeError('Workflow observations require an MCP host session');
|
|
39
|
+
}
|
|
40
|
+
const {
|
|
41
|
+
sessionId: _sessionId,
|
|
42
|
+
idempotencyKey: _idempotencyKey,
|
|
43
|
+
observedAt: _observedAt,
|
|
44
|
+
hostSessionId: _hostSessionId,
|
|
45
|
+
hostObservedAt: _hostObservedAt,
|
|
46
|
+
...safeInput
|
|
47
|
+
} = input ?? {};
|
|
48
|
+
return {
|
|
49
|
+
...safeInput,
|
|
50
|
+
hostSessionId,
|
|
51
|
+
hostObservedAt: clock().toISOString()
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (!nativeThreadId || !SESSION_ID_TOOLS.has(name)) return input;
|
|
55
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return input;
|
|
56
|
+
if (typeof input.sessionId !== 'string' || input.sessionId.trim() === nativeThreadId) {
|
|
57
|
+
return input;
|
|
58
|
+
}
|
|
59
|
+
return { ...input, sessionId: nativeThreadId };
|
|
60
|
+
}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
|
+
import { ApplicationError, ApplicationErrorCode } from '../app/application-error.js';
|
|
4
|
+
|
|
3
5
|
const MAX_ACTIVE_SESSIONS = 256;
|
|
4
6
|
|
|
7
|
+
function unknownTokenError() {
|
|
8
|
+
return new ApplicationError(
|
|
9
|
+
ApplicationErrorCode.NOT_FOUND,
|
|
10
|
+
'Fuli task context token is unknown or superseded. Call begin_task_context for the current session and use the token it returns.'
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
export class TaskContextRegistry {
|
|
6
15
|
constructor({ tokenFactory = () => `fuli-task-${randomUUID()}` } = {}) {
|
|
7
16
|
this.tokenFactory = tokenFactory;
|
|
@@ -33,10 +42,13 @@ export class TaskContextRegistry {
|
|
|
33
42
|
checkpoint(token, checkpoint) {
|
|
34
43
|
assertNonEmpty(token, 'taskContextToken');
|
|
35
44
|
const task = this.tokens.get(token);
|
|
36
|
-
if (!task) throw
|
|
45
|
+
if (!task) throw unknownTokenError();
|
|
37
46
|
if (task.checkpoint) {
|
|
38
47
|
if (sameCheckpoint(task.checkpoint, checkpoint)) return task;
|
|
39
|
-
throw new
|
|
48
|
+
throw new ApplicationError(
|
|
49
|
+
ApplicationErrorCode.VALIDATION,
|
|
50
|
+
'Fuli task context has already been checkpointed with a different disposition'
|
|
51
|
+
);
|
|
40
52
|
}
|
|
41
53
|
task.checkpoint = Object.freeze({ ...checkpoint });
|
|
42
54
|
return task;
|
|
@@ -45,7 +57,7 @@ export class TaskContextRegistry {
|
|
|
45
57
|
context(token) {
|
|
46
58
|
assertNonEmpty(token, 'taskContextToken');
|
|
47
59
|
const task = this.tokens.get(token);
|
|
48
|
-
if (!task) throw
|
|
60
|
+
if (!task) throw unknownTokenError();
|
|
49
61
|
return task;
|
|
50
62
|
}
|
|
51
63
|
|
|
@@ -2,10 +2,14 @@ const READ_TOOLS = new Set([
|
|
|
2
2
|
'verify_task_checkpoint',
|
|
3
3
|
'search_current_project_knowledge',
|
|
4
4
|
'discover_common_knowledge_candidates',
|
|
5
|
+
'discover_personal_global_preference_candidates',
|
|
6
|
+
'preview_personal_global_preference_decision',
|
|
5
7
|
'preview_common_knowledge_promotion',
|
|
6
8
|
'list_knowledge_spaces',
|
|
7
9
|
'list_personal_projects',
|
|
8
10
|
'list_knowledge_review_candidates',
|
|
11
|
+
'list_workflow_candidates',
|
|
12
|
+
'recommend_next_workflow_steps',
|
|
9
13
|
'preview_personal_project_action',
|
|
10
14
|
'list_project_releases',
|
|
11
15
|
'list_project_relations',
|
|
@@ -18,8 +22,10 @@ const WRITE_TOOLS = new Set([
|
|
|
18
22
|
'begin_task_context',
|
|
19
23
|
'checkpoint_task_knowledge',
|
|
20
24
|
'get_collaboration_preferences',
|
|
25
|
+
'get_user_taste_skill',
|
|
21
26
|
'resolve_deferred_preference_conflict',
|
|
22
27
|
'capture_session_knowledge',
|
|
28
|
+
'record_workflow_transition_observation',
|
|
23
29
|
'record_decision_trace',
|
|
24
30
|
'search_knowledge_graph',
|
|
25
31
|
'search_connected_knowledge',
|
|
@@ -35,7 +41,6 @@ const WRITE_TOOLS = new Set([
|
|
|
35
41
|
'finish_knowledge_review',
|
|
36
42
|
'revise_personal_knowledge',
|
|
37
43
|
'reassign_personal_knowledge',
|
|
38
|
-
'set_personal_preference_scope',
|
|
39
44
|
'apply_personal_project_action',
|
|
40
45
|
'publish_personal_project',
|
|
41
46
|
'create_project_relation',
|
|
@@ -67,8 +72,10 @@ export function annotationsFor(name) {
|
|
|
67
72
|
name === 'publish_personal_project' || name === 'unsubscribe_public_project' ||
|
|
68
73
|
name === 'record_knowledge_usage' || name === 'record_decision_trace' ||
|
|
69
74
|
name === 'record_knowledge_feedback' ||
|
|
75
|
+
name === 'record_workflow_transition_observation' ||
|
|
70
76
|
name === 'checkpoint_task_knowledge',
|
|
71
77
|
openWorldHint: name === 'capture_session_knowledge' ||
|
|
78
|
+
name === 'record_workflow_transition_observation' ||
|
|
72
79
|
name === 'search_connected_knowledge' ||
|
|
73
80
|
name === 'record_decision_trace' ||
|
|
74
81
|
name === 'record_knowledge_feedback' ||
|
package/src/mcp/tool-result.js
CHANGED
|
@@ -15,6 +15,7 @@ const RESULT_PROPERTY_LIMIT = 64;
|
|
|
15
15
|
const RESULT_DEPTH_LIMIT = 8;
|
|
16
16
|
const MESSAGE_LIMIT = 240;
|
|
17
17
|
const TRUNCATION_MARKER = '...[truncated]';
|
|
18
|
+
const VALIDATION_ERROR_LIMIT = 5;
|
|
18
19
|
|
|
19
20
|
export function successToolResult(value, { limitBytes = RESULT_LIMIT_BYTES } = {}) {
|
|
20
21
|
const state = { truncated: false };
|
|
@@ -63,17 +64,40 @@ export function errorToolResult(error) {
|
|
|
63
64
|
const message = controlled
|
|
64
65
|
? applicationErrorMessage(error)
|
|
65
66
|
: 'Tool execution failed';
|
|
66
|
-
const
|
|
67
|
+
const validationErrors = controlled ? reportedValidationErrors(error) : [];
|
|
68
|
+
const structuredContent = {
|
|
69
|
+
error: {
|
|
70
|
+
code,
|
|
71
|
+
message,
|
|
72
|
+
...(validationErrors.length ? { validationErrors } : {})
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const detail = validationErrors
|
|
76
|
+
.map(({ field, message: reason }) => (field ? `${field} — ${reason}` : reason))
|
|
77
|
+
.join('; ');
|
|
67
78
|
return {
|
|
68
|
-
content: [{
|
|
79
|
+
content: [{
|
|
80
|
+
type: 'text',
|
|
81
|
+
text: detail ? `${code}: ${message}\n${detail}` : `${code}: ${message}`
|
|
82
|
+
}],
|
|
69
83
|
structuredContent,
|
|
70
84
|
isError: true
|
|
71
85
|
};
|
|
72
86
|
}
|
|
73
87
|
|
|
88
|
+
function reportedValidationErrors(error) {
|
|
89
|
+
if (!Array.isArray(error?.validationErrors)) return [];
|
|
90
|
+
return error.validationErrors
|
|
91
|
+
.filter((entry) => entry && typeof entry.message === 'string' && entry.message.trim())
|
|
92
|
+
.slice(0, VALIDATION_ERROR_LIMIT)
|
|
93
|
+
.map(({ field, message }) => ({
|
|
94
|
+
field: typeof field === 'string' ? boundedMessage(field) : '',
|
|
95
|
+
message: boundedMessage(message)
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
74
99
|
export function applicationErrorMessage(error) {
|
|
75
|
-
|
|
76
|
-
return boundedMessage(stableMessage);
|
|
100
|
+
return boundedMessage(String(error.message));
|
|
77
101
|
}
|
|
78
102
|
|
|
79
103
|
export function protocolErrorResult(message) {
|
package/src/server.js
CHANGED
|
@@ -38,7 +38,9 @@ export function connectClaudeCode(agent, context, {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
|
-
const nextSettings = withManagedHooks(settings
|
|
41
|
+
const nextSettings = withManagedHooks(settings, {
|
|
42
|
+
hookTimeoutSec: context.hookTimeoutSec
|
|
43
|
+
});
|
|
42
44
|
const registrationChanged = !sameJson(current, next);
|
|
43
45
|
const hooksChanged = !sameJson(settings, nextSettings);
|
|
44
46
|
if (registrationChanged) writeConfig(agent.configPath, next);
|
|
@@ -108,7 +110,8 @@ export function hasCurrentClaudeCodeHooks(settings) {
|
|
|
108
110
|
));
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
function withManagedHooks(settings) {
|
|
113
|
+
function withManagedHooks(settings, { hookTimeoutSec = 30 } = {}) {
|
|
114
|
+
const normalizedHookTimeoutSec = positiveHookTimeout(hookTimeoutSec);
|
|
112
115
|
const clean = withoutManagedHooks(settings);
|
|
113
116
|
const hooks = { ...(clean.hooks ?? {}) };
|
|
114
117
|
hooks.UserPromptSubmit = [
|
|
@@ -117,14 +120,15 @@ function withManagedHooks(settings) {
|
|
|
117
120
|
sessionId: '${session_id}',
|
|
118
121
|
projectPath: '${cwd}',
|
|
119
122
|
taskPrompt: '${prompt}'
|
|
120
|
-
}, 'Loading Fuli task context')
|
|
123
|
+
}, 'Loading Fuli task context', normalizedHookTimeoutSec)
|
|
121
124
|
];
|
|
122
125
|
hooks.Stop = [
|
|
123
126
|
...(hooks.Stop ?? []),
|
|
124
127
|
managedHookGroup(
|
|
125
128
|
'verify_task_checkpoint',
|
|
126
129
|
{ sessionId: '${session_id}' },
|
|
127
|
-
'Checking Fuli task checkpoint'
|
|
130
|
+
'Checking Fuli task checkpoint',
|
|
131
|
+
normalizedHookTimeoutSec
|
|
128
132
|
)
|
|
129
133
|
];
|
|
130
134
|
return { ...clean, hooks };
|
|
@@ -149,19 +153,26 @@ function withoutManagedHooks(settings) {
|
|
|
149
153
|
return { ...settings, hooks };
|
|
150
154
|
}
|
|
151
155
|
|
|
152
|
-
function managedHookGroup(tool, input, statusMessage) {
|
|
156
|
+
function managedHookGroup(tool, input, statusMessage, timeout) {
|
|
153
157
|
return {
|
|
154
158
|
hooks: [{
|
|
155
159
|
type: 'mcp_tool',
|
|
156
160
|
server: FULI_SERVER,
|
|
157
161
|
tool,
|
|
158
162
|
input,
|
|
159
|
-
timeout
|
|
163
|
+
timeout,
|
|
160
164
|
statusMessage
|
|
161
165
|
}]
|
|
162
166
|
};
|
|
163
167
|
}
|
|
164
168
|
|
|
169
|
+
function positiveHookTimeout(value) {
|
|
170
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 2_147_483_647) {
|
|
171
|
+
throw new TypeError('Claude Code hook timeout must be a positive safe integer');
|
|
172
|
+
}
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
|
|
165
176
|
function isManagedHook(hook, tool = null) {
|
|
166
177
|
return hook?.type === 'mcp_tool'
|
|
167
178
|
&& hook.server === FULI_SERVER
|
|
@@ -79,7 +79,9 @@ export async function ensureGraphRuntime(input, dependencies = {}) {
|
|
|
79
79
|
deps.secureFile(input.paths.graphRuntimeConfigPath);
|
|
80
80
|
} else {
|
|
81
81
|
const synchronized = synchronizeManagedProviderUrls(config, urls, {
|
|
82
|
-
personalOnly: input.personalOnly === true
|
|
82
|
+
personalOnly: input.personalOnly === true,
|
|
83
|
+
workflowObservationToken:
|
|
84
|
+
secrets.FULI_PERSONAL_WORKFLOW_OBSERVATION_TOKEN
|
|
83
85
|
});
|
|
84
86
|
if (synchronized.changed) {
|
|
85
87
|
await reconcileManagedWorkspaceSubscriptions({
|
|
@@ -154,6 +156,10 @@ function ensureProviderEnvironment(path, settings, deps) {
|
|
|
154
156
|
FULI_PERSONAL_NEO4J_PASSWORD: previous.FULI_PERSONAL_NEO4J_PASSWORD ?? secret(),
|
|
155
157
|
FULI_WORKSPACE_NEO4J_PASSWORD: previous.FULI_WORKSPACE_NEO4J_PASSWORD ?? secret(),
|
|
156
158
|
FULI_PERSONAL_BOOTSTRAP_TOKEN: previous.FULI_PERSONAL_BOOTSTRAP_TOKEN ?? secret(),
|
|
159
|
+
FULI_PERSONAL_HUMAN_REVIEW_TOKEN:
|
|
160
|
+
previous.FULI_PERSONAL_HUMAN_REVIEW_TOKEN ?? secret(),
|
|
161
|
+
FULI_PERSONAL_WORKFLOW_OBSERVATION_TOKEN:
|
|
162
|
+
previous.FULI_PERSONAL_WORKFLOW_OBSERVATION_TOKEN ?? secret(),
|
|
157
163
|
FULI_WORKSPACE_BOOTSTRAP_TOKEN: previous.FULI_WORKSPACE_BOOTSTRAP_TOKEN ?? secret(),
|
|
158
164
|
FULI_PERSONAL_NEO4J_HTTP_PORT: String(settings.ports.personalNeo4jHttp),
|
|
159
165
|
FULI_PERSONAL_NEO4J_BOLT_PORT: String(settings.ports.personalNeo4jBolt),
|
|
@@ -186,6 +192,8 @@ async function bootstrapGraph({ urls, personalSpaceName, secrets, personalOnly,
|
|
|
186
192
|
personal: {
|
|
187
193
|
providerUrl: urls.personal,
|
|
188
194
|
accessToken: personalIdentity.access_token,
|
|
195
|
+
workflowObservationToken:
|
|
196
|
+
secrets.FULI_PERSONAL_WORKFLOW_OBSERVATION_TOKEN,
|
|
189
197
|
principalId: personalIdentity.principal_id,
|
|
190
198
|
spaceId: personalSpace.id
|
|
191
199
|
},
|
|
@@ -224,10 +232,20 @@ async function bootstrapGraph({ urls, personalSpaceName, secrets, personalOnly,
|
|
|
224
232
|
return config;
|
|
225
233
|
}
|
|
226
234
|
|
|
227
|
-
export function synchronizeManagedProviderUrls(config, urls, {
|
|
235
|
+
export function synchronizeManagedProviderUrls(config, urls, {
|
|
236
|
+
personalOnly = true,
|
|
237
|
+
workflowObservationToken = null
|
|
238
|
+
} = {}) {
|
|
228
239
|
const next = structuredClone(config);
|
|
229
240
|
let changed = next.personal?.providerUrl !== urls.personal;
|
|
230
241
|
next.personal.providerUrl = urls.personal;
|
|
242
|
+
if (
|
|
243
|
+
nonEmpty(workflowObservationToken) &&
|
|
244
|
+
next.personal.workflowObservationToken !== workflowObservationToken
|
|
245
|
+
) {
|
|
246
|
+
next.personal.workflowObservationToken = workflowObservationToken;
|
|
247
|
+
changed = true;
|
|
248
|
+
}
|
|
231
249
|
if (!personalOnly) {
|
|
232
250
|
for (const workspace of next.workspaces ?? []) {
|
|
233
251
|
if (!isManagedWorkspace(workspace)) continue;
|
|
@@ -5,6 +5,15 @@ import { readJsonFile, writeJsonFileAtomic } from '../storage/json-file.js';
|
|
|
5
5
|
|
|
6
6
|
export const RUNTIME_SETTINGS_VERSION = 1;
|
|
7
7
|
export const RESOURCE_REFRESH_OPTIONS = Object.freeze([5, 10, 30, 60]);
|
|
8
|
+
export const CONVERSATION_LAUNCHER_APPLICATIONS = Object.freeze([
|
|
9
|
+
'codex',
|
|
10
|
+
'claude_code',
|
|
11
|
+
'cursor',
|
|
12
|
+
'gemini_cli',
|
|
13
|
+
'kiro',
|
|
14
|
+
'other'
|
|
15
|
+
]);
|
|
16
|
+
export const CONVERSATION_ID_FORMATS = Object.freeze(['any', 'uuid']);
|
|
8
17
|
export const PORT_KEYS = Object.freeze([
|
|
9
18
|
'console',
|
|
10
19
|
'personalProvider',
|
|
@@ -15,6 +24,45 @@ export const PORT_KEYS = Object.freeze([
|
|
|
15
24
|
'workspaceNeo4jBolt'
|
|
16
25
|
]);
|
|
17
26
|
|
|
27
|
+
export const DEFAULT_CONVERSATION_LAUNCHERS = deepFreeze({
|
|
28
|
+
codex: {
|
|
29
|
+
enabled: true,
|
|
30
|
+
idFormat: 'uuid',
|
|
31
|
+
appName: 'Codex',
|
|
32
|
+
urlTemplate: 'codex://threads/{id}'
|
|
33
|
+
},
|
|
34
|
+
claude_code: {
|
|
35
|
+
enabled: false,
|
|
36
|
+
idFormat: 'any',
|
|
37
|
+
appName: 'Claude Code',
|
|
38
|
+
urlTemplate: ''
|
|
39
|
+
},
|
|
40
|
+
cursor: {
|
|
41
|
+
enabled: false,
|
|
42
|
+
idFormat: 'any',
|
|
43
|
+
appName: 'Cursor',
|
|
44
|
+
urlTemplate: ''
|
|
45
|
+
},
|
|
46
|
+
gemini_cli: {
|
|
47
|
+
enabled: false,
|
|
48
|
+
idFormat: 'any',
|
|
49
|
+
appName: 'Gemini CLI',
|
|
50
|
+
urlTemplate: ''
|
|
51
|
+
},
|
|
52
|
+
kiro: {
|
|
53
|
+
enabled: false,
|
|
54
|
+
idFormat: 'any',
|
|
55
|
+
appName: 'Kiro',
|
|
56
|
+
urlTemplate: ''
|
|
57
|
+
},
|
|
58
|
+
other: {
|
|
59
|
+
enabled: false,
|
|
60
|
+
idFormat: 'any',
|
|
61
|
+
appName: 'Other Agent',
|
|
62
|
+
urlTemplate: ''
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
18
66
|
export const DEFAULT_RUNTIME_SETTINGS = deepFreeze({
|
|
19
67
|
version: RUNTIME_SETTINGS_VERSION,
|
|
20
68
|
ports: {
|
|
@@ -27,7 +75,8 @@ export const DEFAULT_RUNTIME_SETTINGS = deepFreeze({
|
|
|
27
75
|
workspaceNeo4jBolt: 7688
|
|
28
76
|
},
|
|
29
77
|
lanAccess: false,
|
|
30
|
-
resourceRefreshSeconds: 5
|
|
78
|
+
resourceRefreshSeconds: 5,
|
|
79
|
+
conversationLaunchers: DEFAULT_CONVERSATION_LAUNCHERS
|
|
31
80
|
});
|
|
32
81
|
|
|
33
82
|
export function readRuntimeSettings(path, {
|
|
@@ -81,14 +130,69 @@ export function normalizeRuntimeSettings(input, {
|
|
|
81
130
|
if (!RESOURCE_REFRESH_OPTIONS.includes(resourceRefreshSeconds)) {
|
|
82
131
|
throw new TypeError('resourceRefreshSeconds must be 5, 10, 30, or 60');
|
|
83
132
|
}
|
|
133
|
+
const conversationLaunchers = normalizeConversationLaunchers(
|
|
134
|
+
input.conversationLaunchers,
|
|
135
|
+
{ base: base.conversationLaunchers ?? DEFAULT_CONVERSATION_LAUNCHERS }
|
|
136
|
+
);
|
|
84
137
|
return {
|
|
85
138
|
version: RUNTIME_SETTINGS_VERSION,
|
|
86
139
|
ports,
|
|
87
140
|
lanAccess,
|
|
88
|
-
resourceRefreshSeconds
|
|
141
|
+
resourceRefreshSeconds,
|
|
142
|
+
conversationLaunchers
|
|
89
143
|
};
|
|
90
144
|
}
|
|
91
145
|
|
|
146
|
+
export function normalizeConversationLaunchers(input, {
|
|
147
|
+
base = DEFAULT_CONVERSATION_LAUNCHERS
|
|
148
|
+
} = {}) {
|
|
149
|
+
if (input !== undefined && (!input || typeof input !== 'object' || Array.isArray(input))) {
|
|
150
|
+
throw new TypeError('conversationLaunchers must be an object');
|
|
151
|
+
}
|
|
152
|
+
for (const key of Object.keys(input ?? {})) {
|
|
153
|
+
if (!CONVERSATION_LAUNCHER_APPLICATIONS.includes(key)) {
|
|
154
|
+
throw new TypeError(`Unsupported conversation launcher: ${key}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return Object.fromEntries(CONVERSATION_LAUNCHER_APPLICATIONS.map((application) => {
|
|
158
|
+
const fallback = base[application] ?? DEFAULT_CONVERSATION_LAUNCHERS[application];
|
|
159
|
+
const candidate = input?.[application] ?? fallback;
|
|
160
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
|
|
161
|
+
throw new TypeError(`${application} conversation launcher must be an object`);
|
|
162
|
+
}
|
|
163
|
+
const enabled = candidate.enabled ?? fallback.enabled;
|
|
164
|
+
if (typeof enabled !== 'boolean') {
|
|
165
|
+
throw new TypeError(`${application} conversation launcher enabled must be a boolean`);
|
|
166
|
+
}
|
|
167
|
+
const idFormat = candidate.idFormat ?? fallback.idFormat;
|
|
168
|
+
if (!CONVERSATION_ID_FORMATS.includes(idFormat)) {
|
|
169
|
+
throw new TypeError(`${application} conversation launcher idFormat is unsupported`);
|
|
170
|
+
}
|
|
171
|
+
const appName = launcherText(
|
|
172
|
+
candidate.appName ?? fallback.appName,
|
|
173
|
+
`${application} conversation launcher appName`,
|
|
174
|
+
64
|
|
175
|
+
);
|
|
176
|
+
const urlTemplate = launcherText(
|
|
177
|
+
candidate.urlTemplate ?? fallback.urlTemplate,
|
|
178
|
+
`${application} conversation launcher urlTemplate`,
|
|
179
|
+
1024
|
|
180
|
+
);
|
|
181
|
+
if (enabled && !appName) {
|
|
182
|
+
throw new TypeError(`${application} conversation launcher appName is required`);
|
|
183
|
+
}
|
|
184
|
+
if (urlTemplate && !validConversationUrlTemplate(urlTemplate)) {
|
|
185
|
+
throw new TypeError(
|
|
186
|
+
`${application} conversation launcher urlTemplate must use a safe URL scheme and contain {id}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (enabled && !urlTemplate) {
|
|
190
|
+
throw new TypeError(`${application} conversation launcher urlTemplate is required`);
|
|
191
|
+
}
|
|
192
|
+
return [application, { enabled, idFormat, appName, urlTemplate }];
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
|
|
92
196
|
export function runtimeSettingsWithOverrides(settings, {
|
|
93
197
|
consolePort = null,
|
|
94
198
|
lanAccess = null
|
|
@@ -122,6 +226,21 @@ function validPort(value, key) {
|
|
|
122
226
|
return value;
|
|
123
227
|
}
|
|
124
228
|
|
|
229
|
+
function launcherText(value, label, maximumLength) {
|
|
230
|
+
if (typeof value !== 'string') throw new TypeError(`${label} must be a string`);
|
|
231
|
+
const trimmed = value.trim();
|
|
232
|
+
if (trimmed.length > maximumLength || /[\u0000-\u001f\u007f]/.test(trimmed)) {
|
|
233
|
+
throw new TypeError(`${label} is invalid`);
|
|
234
|
+
}
|
|
235
|
+
return trimmed;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function validConversationUrlTemplate(value) {
|
|
239
|
+
if (value.split('{id}').length !== 2 || /\s/.test(value)) return false;
|
|
240
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(value)?.[1]?.toLowerCase();
|
|
241
|
+
return Boolean(scheme) && !['about', 'blob', 'data', 'file', 'javascript'].includes(scheme);
|
|
242
|
+
}
|
|
243
|
+
|
|
125
244
|
function deepFreeze(value) {
|
|
126
245
|
Object.freeze(value);
|
|
127
246
|
for (const child of Object.values(value)) {
|
|
@@ -86,7 +86,11 @@ function settingsResult(configured, active) {
|
|
|
86
86
|
configured,
|
|
87
87
|
active,
|
|
88
88
|
restartRequired: !runtimeSettingsEqual(
|
|
89
|
-
{
|
|
89
|
+
{
|
|
90
|
+
...configured,
|
|
91
|
+
resourceRefreshSeconds: active.resourceRefreshSeconds,
|
|
92
|
+
conversationLaunchers: active.conversationLaunchers
|
|
93
|
+
},
|
|
90
94
|
active
|
|
91
95
|
)
|
|
92
96
|
};
|