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
@@ -23,6 +23,23 @@ import {
23
23
  sourceConsoleUrl
24
24
  } from './source-marker.js';
25
25
  import { resolvePersonalProjectPath } from './project-path-context.js';
26
+ import { TaskContextRegistry } from '../mcp/task-context-registry.js';
27
+ import {
28
+ agentProjectResolution,
29
+ beginTaskContext as beginTaskContextWorkflow,
30
+ checkpointTaskKnowledge as checkpointTaskKnowledgeWorkflow,
31
+ discoverCommonKnowledgeCandidates as discoverCommonKnowledgeCandidatesWorkflow,
32
+ providerCommonKnowledgePromotion,
33
+ recordDecisionTrace as recordDecisionTraceWorkflow,
34
+ recordKnowledgeFeedback as recordKnowledgeFeedbackWorkflow,
35
+ searchCurrentProjectKnowledge as searchCurrentProjectKnowledgeWorkflow
36
+ } from './agent-knowledge-workflows.js';
37
+ import {
38
+ assertPublicKnowledgeEligible,
39
+ providerConfirmationActor,
40
+ providerConfirmationBasis,
41
+ providerEpisode
42
+ } from './knowledge-provider-mapping.js';
26
43
 
27
44
  export function openFederatedGraphApplication({
28
45
  runtimeConfigPath,
@@ -51,7 +68,9 @@ export class FederatedGraphApplication {
51
68
  capturePolicyStore = new CapturePolicyStore(),
52
69
  agentAccessPolicyStore = new AgentAccessPolicyStore(),
53
70
  consoleUrl = sourceConsoleUrl(null),
54
- projectPathResolver = resolvePersonalProjectPath
71
+ projectPathResolver = resolvePersonalProjectPath,
72
+ taskContextRegistry = new TaskContextRegistry(),
73
+ providerRequestTimeoutMs = undefined
55
74
  } = {}) {
56
75
  this.graphiti = true;
57
76
  this.config = config;
@@ -59,10 +78,12 @@ export class FederatedGraphApplication {
59
78
  this.agentAccessPolicyStore = agentAccessPolicyStore;
60
79
  this.consoleUrl = consoleUrl;
61
80
  this.projectPathResolver = projectPathResolver;
81
+ this.taskContextRegistry = taskContextRegistry;
62
82
  this.personal = new GraphitiProviderClient({
63
83
  baseUrl: config.personal.providerUrl,
64
84
  accessToken: config.personal.accessToken,
65
- fetchImpl
85
+ fetchImpl,
86
+ requestTimeoutMs: providerRequestTimeoutMs
66
87
  });
67
88
  this.workspaces = new Map(
68
89
  config.workspaces.map((workspace) => {
@@ -72,13 +93,31 @@ export class FederatedGraphApplication {
72
93
  client: new GraphitiProviderClient({
73
94
  baseUrl: url,
74
95
  accessToken: workspace.accessToken,
75
- fetchImpl
96
+ fetchImpl,
97
+ requestTimeoutMs: providerRequestTimeoutMs
76
98
  })
77
99
  }];
78
100
  })
79
101
  );
80
102
  }
81
103
 
104
+ async beginTaskContext(input) {
105
+ return beginTaskContextWorkflow(this, input);
106
+ }
107
+
108
+ async checkpointTaskKnowledge(input) {
109
+ return checkpointTaskKnowledgeWorkflow(this, input);
110
+ }
111
+
112
+ verifyTaskCheckpoint({ sessionId }) {
113
+ return this.taskContextRegistry.verify(sessionId);
114
+ }
115
+
116
+ async recordDecisionTrace(input) {
117
+ this.#assertActivePersonalSpace(input.personalSpaceId);
118
+ return recordDecisionTraceWorkflow(this, input);
119
+ }
120
+
82
121
  async captureSessionKnowledge(input) {
83
122
  const capturePolicy = this.getCapturePolicy();
84
123
  if (!capturePolicy.enabled) {
@@ -377,6 +416,36 @@ export class FederatedGraphApplication {
377
416
  };
378
417
  }
379
418
 
419
+ async searchCurrentProjectKnowledge(input) {
420
+ const resolution = await this.#resolvePreferenceProject({
421
+ personalProjectId: null,
422
+ projectPath: input.projectPath
423
+ });
424
+ return searchCurrentProjectKnowledgeWorkflow(this, resolution, input);
425
+ }
426
+
427
+ async discoverCommonKnowledgeCandidates(input) {
428
+ this.#assertActivePersonalSpace(input.personalSpaceId);
429
+ return discoverCommonKnowledgeCandidatesWorkflow(this, input);
430
+ }
431
+
432
+ async previewCommonKnowledgePromotion(input) {
433
+ this.#assertActivePersonalSpace(input.personalSpaceId);
434
+ assertSafeKnowledgeMutation(input);
435
+ return this.personal.previewCommonKnowledgePromotion(
436
+ providerCommonKnowledgePromotion(input)
437
+ );
438
+ }
439
+
440
+ async applyCommonKnowledgePromotion(input) {
441
+ this.#assertActivePersonalSpace(input.personalSpaceId);
442
+ assertSafeKnowledgeMutation(input);
443
+ return this.personal.applyCommonKnowledgePromotion({
444
+ ...providerCommonKnowledgePromotion(input),
445
+ operation_actor: input.operationActor ?? 'agent'
446
+ });
447
+ }
448
+
380
449
  async getKnowledgeGraph({
381
450
  spaceId,
382
451
  providerUrl = null,
@@ -425,6 +494,17 @@ export class FederatedGraphApplication {
425
494
  });
426
495
  }
427
496
 
497
+ async recordKnowledgeFeedback(input) {
498
+ this.#assertActivePersonalSpace(input.personalSpaceId);
499
+ assertSafeKnowledgeMutation({
500
+ items: input.items.map((item) => ({
501
+ existenceReason: item.reason,
502
+ quadrantReason: item.evidenceSummary
503
+ }))
504
+ });
505
+ return recordKnowledgeFeedbackWorkflow(this, input);
506
+ }
507
+
428
508
  async reviseKnowledgeItem(input) {
429
509
  this.#assertActivePersonalSpace(input.personalSpaceId);
430
510
  assertSafeKnowledgeMutation(input);
@@ -1039,22 +1119,11 @@ function preferenceConflictPairKey(leftId, rightId) {
1039
1119
  return [leftId, rightId].sort().join('\u0000');
1040
1120
  }
1041
1121
 
1042
- function agentProjectResolution(resolution) {
1043
- const result = {
1044
- status: resolution.status,
1045
- basis: resolution.basis,
1046
- personal_project_id: resolution.personalProjectId
1047
- };
1048
- if (resolution.candidateCount !== undefined) {
1049
- result.candidate_count = resolution.candidateCount;
1050
- }
1051
- return result;
1052
- }
1053
-
1054
1122
  function assertNoCredentials(input) {
1055
1123
  const content = JSON.stringify({
1056
1124
  name: input.name,
1057
1125
  sourceDescription: input.sourceDescription,
1126
+ sourceUri: input.sourceUri,
1058
1127
  sourceExcerpt: input.sourceExcerpt,
1059
1128
  summary: input.summary,
1060
1129
  entities: input.entities,
@@ -1120,6 +1189,7 @@ function assertSafeKnowledgeMutation(input) {
1120
1189
  note: input.note,
1121
1190
  newProjectName: input.newProjectName,
1122
1191
  newProjectPurpose: input.newProjectPurpose,
1192
+ humanConfirmationReason: input.humanConfirmationReason,
1123
1193
  batchItems: input.items?.map((item) => ({
1124
1194
  existenceReason: item.existenceReason,
1125
1195
  quadrantReason: item.quadrantReason
@@ -1133,95 +1203,6 @@ function assertSafeKnowledgeMutation(input) {
1133
1203
  }
1134
1204
  }
1135
1205
 
1136
- function providerEpisode(input) {
1137
- return {
1138
- idempotency_key: input.idempotencyKey,
1139
- session_id: input.sessionId,
1140
- name: input.name,
1141
- source_kind: input.sourceKind,
1142
- source_description: input.sourceDescription,
1143
- source_application: input.sourceApplication ?? null,
1144
- source_turn_id: input.sourceTurnId ?? null,
1145
- source_excerpt: input.sourceExcerpt ?? null,
1146
- reference_time: input.referenceTime,
1147
- summary: input.summary ?? '',
1148
- sensitivity: input.sensitivity ?? 'normal',
1149
- entities: input.entities.map((entity) => ({
1150
- key: entity.key,
1151
- name: entity.name,
1152
- type: entity.type,
1153
- summary: entity.summary ?? '',
1154
- origin_quadrant: entity.originQuadrant ?? 'known_known',
1155
- current_quadrant: entity.currentQuadrant ?? entity.originQuadrant ?? 'known_known',
1156
- epistemic_status: entity.epistemicStatus ?? 'confirmed',
1157
- confirmation_status: entity.confirmationStatus,
1158
- confirmation_basis: providerConfirmationBasis(entity.confirmationBasis),
1159
- reasoning_summary: entity.reasoningSummary ?? null,
1160
- profile_aspect: entity.profileAspect ?? null,
1161
- inheritance_mode: entity.inheritanceMode ?? 'local_only',
1162
- inherited_project_ids: entity.inheritedProjectIds ?? [],
1163
- attributes: entity.attributes ?? {}
1164
- })),
1165
- relationships: input.relationships.map((relationship) => ({
1166
- key: relationship.key,
1167
- source: relationship.source,
1168
- target: relationship.target,
1169
- type: relationship.type,
1170
- fact: relationship.fact,
1171
- valid_at: relationship.validAt ?? null,
1172
- invalid_at: relationship.invalidAt ?? null,
1173
- supersedes: relationship.supersedes ?? [],
1174
- confidence: relationship.confidence ?? 1,
1175
- origin_quadrant: relationship.originQuadrant ?? 'known_known',
1176
- current_quadrant: relationship.currentQuadrant ??
1177
- relationship.originQuadrant ?? 'known_known',
1178
- epistemic_status: relationship.epistemicStatus ?? 'confirmed',
1179
- confirmation_status: relationship.confirmationStatus,
1180
- confirmation_basis: providerConfirmationBasis(relationship.confirmationBasis),
1181
- reasoning_summary: relationship.reasoningSummary ?? null,
1182
- profile_aspect: relationship.profileAspect ?? null,
1183
- inheritance_mode: relationship.inheritanceMode ?? 'local_only',
1184
- inherited_project_ids: relationship.inheritedProjectIds ?? [],
1185
- attributes: relationship.attributes ?? {}
1186
- }))
1187
- };
1188
- }
1189
-
1190
- function assertPublicKnowledgeEligible(episode) {
1191
- const blocked = [...episode.entities, ...episode.relationships].find((item) =>
1192
- item.profile_aspect ||
1193
- item.confirmation_status !== 'confirmed' ||
1194
- !item.confirmation_basis?.confirmed_by ||
1195
- !item.confirmation_basis?.confirmed_at ||
1196
- !['user', 'authoritative_source'].includes(item.confirmation_basis.confirmed_by.kind)
1197
- );
1198
- if (blocked) {
1199
- throw new TypeError(
1200
- 'Only knowledge with an auditable confirmation can enter public review'
1201
- );
1202
- }
1203
- }
1204
-
1205
- function providerConfirmationBasis(basis) {
1206
- if (!basis) return undefined;
1207
- return {
1208
- existence_reason: basis.existenceReason,
1209
- quadrant_reason: basis.quadrantReason,
1210
- proposed_by: providerConfirmationActor(basis.proposedBy),
1211
- confirmed_by: providerConfirmationActor(basis.confirmedBy),
1212
- confirmed_at: basis.confirmedAt ?? null,
1213
- agent_policy_version: basis.agentPolicyVersion ?? null
1214
- };
1215
- }
1216
-
1217
- function providerConfirmationActor(actor) {
1218
- if (!actor) return null;
1219
- return {
1220
- kind: actor.kind,
1221
- label: actor.label ?? null
1222
- };
1223
- }
1224
-
1225
1206
  function providerProjectProfile(profile) {
1226
1207
  if (!profile || typeof profile !== 'object') throw new TypeError('Project profile is required');
1227
1208
  const assessment = profile.assessment ? sanitizeProjectAssessment(profile.assessment) : null;
@@ -0,0 +1,91 @@
1
+ import { onlineSourceUri } from './source-uri.js';
2
+
3
+ export function providerEpisode(input) {
4
+ return {
5
+ idempotency_key: input.idempotencyKey,
6
+ session_id: input.sessionId,
7
+ name: input.name,
8
+ source_kind: input.sourceKind,
9
+ source_description: input.sourceDescription,
10
+ source_uri: onlineSourceUri(input.sourceUri),
11
+ source_application: input.sourceApplication ?? null,
12
+ source_turn_id: input.sourceTurnId ?? null,
13
+ source_excerpt: input.sourceExcerpt ?? null,
14
+ reference_time: input.referenceTime,
15
+ summary: input.summary ?? '',
16
+ sensitivity: input.sensitivity ?? 'normal',
17
+ entities: input.entities.map((entity) => ({
18
+ key: entity.key,
19
+ name: entity.name,
20
+ type: entity.type,
21
+ summary: entity.summary ?? '',
22
+ origin_quadrant: entity.originQuadrant ?? 'known_known',
23
+ current_quadrant: entity.currentQuadrant ?? entity.originQuadrant ?? 'known_known',
24
+ epistemic_status: entity.epistemicStatus ?? 'confirmed',
25
+ confirmation_status: entity.confirmationStatus,
26
+ confirmation_basis: providerConfirmationBasis(entity.confirmationBasis),
27
+ reasoning_summary: entity.reasoningSummary ?? null,
28
+ profile_aspect: entity.profileAspect ?? null,
29
+ inheritance_mode: entity.inheritanceMode ?? 'local_only',
30
+ inherited_project_ids: entity.inheritedProjectIds ?? [],
31
+ attributes: entity.attributes ?? {}
32
+ })),
33
+ relationships: input.relationships.map((relationship) => ({
34
+ key: relationship.key,
35
+ source: relationship.source,
36
+ target: relationship.target,
37
+ type: relationship.type,
38
+ fact: relationship.fact,
39
+ valid_at: relationship.validAt ?? null,
40
+ invalid_at: relationship.invalidAt ?? null,
41
+ supersedes: relationship.supersedes ?? [],
42
+ confidence: relationship.confidence ?? 1,
43
+ origin_quadrant: relationship.originQuadrant ?? 'known_known',
44
+ current_quadrant: relationship.currentQuadrant ??
45
+ relationship.originQuadrant ?? 'known_known',
46
+ epistemic_status: relationship.epistemicStatus ?? 'confirmed',
47
+ confirmation_status: relationship.confirmationStatus,
48
+ confirmation_basis: providerConfirmationBasis(relationship.confirmationBasis),
49
+ reasoning_summary: relationship.reasoningSummary ?? null,
50
+ profile_aspect: relationship.profileAspect ?? null,
51
+ inheritance_mode: relationship.inheritanceMode ?? 'local_only',
52
+ inherited_project_ids: relationship.inheritedProjectIds ?? [],
53
+ attributes: relationship.attributes ?? {}
54
+ }))
55
+ };
56
+ }
57
+
58
+ export function assertPublicKnowledgeEligible(episode) {
59
+ const blocked = [...episode.entities, ...episode.relationships].find((item) =>
60
+ item.profile_aspect ||
61
+ item.confirmation_status !== 'confirmed' ||
62
+ !item.confirmation_basis?.confirmed_by ||
63
+ !item.confirmation_basis?.confirmed_at ||
64
+ !['user', 'authoritative_source'].includes(item.confirmation_basis.confirmed_by.kind)
65
+ );
66
+ if (blocked) {
67
+ throw new TypeError(
68
+ 'Only knowledge with an auditable confirmation can enter public review'
69
+ );
70
+ }
71
+ }
72
+
73
+ export function providerConfirmationBasis(basis) {
74
+ if (!basis) return undefined;
75
+ return {
76
+ existence_reason: basis.existenceReason,
77
+ quadrant_reason: basis.quadrantReason,
78
+ proposed_by: providerConfirmationActor(basis.proposedBy),
79
+ confirmed_by: providerConfirmationActor(basis.confirmedBy),
80
+ confirmed_at: basis.confirmedAt ?? null,
81
+ agent_policy_version: basis.agentPolicyVersion ?? null
82
+ };
83
+ }
84
+
85
+ export function providerConfirmationActor(actor) {
86
+ if (!actor) return null;
87
+ return {
88
+ kind: actor.kind,
89
+ label: actor.label ?? null
90
+ };
91
+ }
@@ -133,6 +133,11 @@ export class GraphitiProviderClient {
133
133
  method: 'POST', body: input
134
134
  });
135
135
  }
136
+ recordKnowledgeFeedback(input) {
137
+ return this.#request('/v1/knowledge/feedback', {
138
+ method: 'POST', body: input
139
+ });
140
+ }
136
141
  reviewHumanChange(itemId, input) {
137
142
  return this.#request(
138
143
  `/v1/knowledge/items/${encodeURIComponent(itemId)}/agent-review`,
@@ -161,6 +166,16 @@ export class GraphitiProviderClient {
161
166
  { method: 'POST', body: input }
162
167
  );
163
168
  }
169
+ previewCommonKnowledgePromotion(input) {
170
+ return this.#request('/v1/knowledge/common-promotions/preview', {
171
+ method: 'POST', body: input
172
+ });
173
+ }
174
+ applyCommonKnowledgePromotion(input) {
175
+ return this.#request('/v1/knowledge/common-promotions', {
176
+ method: 'POST', body: input
177
+ });
178
+ }
164
179
  deferPreferenceConflict(input) {
165
180
  return this.#request('/v1/preference-conflicts/defer', {
166
181
  method: 'POST', body: input
@@ -90,13 +90,7 @@ function sourceLeadMarkdown({ consoleUrl, citations }) {
90
90
  }
91
91
 
92
92
  function sourceMarkerMarkdown({ consoleUrl, citations }) {
93
- if (!citations.length) {
94
- return [
95
- '**FULI 来源 · 未命中**',
96
- '',
97
- `- [打开 Fuli](${safeLoopbackOrigin(consoleUrl) ?? DEFAULT_CONSOLE_URL}/)`
98
- ].join('\n');
99
- }
93
+ if (!citations.length) return '';
100
94
  const shown = citations.slice(0, MAX_CITATIONS);
101
95
  const hidden = citations.length - shown.length;
102
96
  return [
@@ -0,0 +1,29 @@
1
+ const SOURCE_URI_ERROR =
2
+ 'Source URI must be an absolute HTTP(S) URI without credentials';
3
+
4
+ export function onlineSourceUri(value) {
5
+ if (value === undefined || value === null) return null;
6
+ if (
7
+ typeof value !== 'string'
8
+ || value.length < 1
9
+ || value.length > 2048
10
+ || /(?:\s|\p{C})/u.test(value)
11
+ ) {
12
+ throw new TypeError(SOURCE_URI_ERROR);
13
+ }
14
+ let parsed;
15
+ try {
16
+ parsed = new URL(value);
17
+ } catch {
18
+ throw new TypeError(SOURCE_URI_ERROR);
19
+ }
20
+ if (
21
+ !['http:', 'https:'].includes(parsed.protocol)
22
+ || !parsed.hostname
23
+ || parsed.username
24
+ || parsed.password
25
+ ) {
26
+ throw new TypeError(SOURCE_URI_ERROR);
27
+ }
28
+ return value;
29
+ }
@@ -1,9 +1,10 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
2
  import { extname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
 
5
5
  const PROJECT_ROOT = resolve(fileURLToPath(new URL('../..', import.meta.url)));
6
6
  export const WEB_ROOT = join(PROJECT_ROOT, 'dist', 'web');
7
+ const FULI_LOGO_PATTERN = /^fuli-logo(?:-[A-Za-z0-9_-]+)?\.png$/;
7
8
  const MIME_TYPES = {
8
9
  '.html': 'text/html; charset=utf-8',
9
10
  '.css': 'text/css; charset=utf-8',
@@ -18,8 +19,17 @@ const MIME_TYPES = {
18
19
 
19
20
  export function serveStatic(pathname, response) {
20
21
  if (pathname === '/favicon.ico') {
21
- response.writeHead(204);
22
- response.end();
22
+ const filePath = faviconFilePath();
23
+ if (!filePath) {
24
+ response.writeHead(204);
25
+ response.end();
26
+ return;
27
+ }
28
+ response.writeHead(200, {
29
+ 'cache-control': 'public, max-age=86400',
30
+ 'content-type': 'image/png'
31
+ });
32
+ response.end(readFileSync(filePath));
23
33
  return;
24
34
  }
25
35
 
@@ -50,6 +60,22 @@ export function serveStatic(pathname, response) {
50
60
  response.end(readFileSync(filePath));
51
61
  }
52
62
 
63
+ function faviconFilePath() {
64
+ const directPath = join(WEB_ROOT, 'favicon.png');
65
+ if (existsSync(directPath)) return directPath;
66
+
67
+ const assetsPath = join(WEB_ROOT, 'assets');
68
+ if (!existsSync(assetsPath)) return null;
69
+ try {
70
+ const filename = readdirSync(assetsPath)
71
+ .sort()
72
+ .find((candidate) => FULI_LOGO_PATTERN.test(candidate));
73
+ return filename ? join(assetsPath, filename) : null;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
53
79
  function shouldServeApplication(pathname) {
54
80
  return extname(pathname) === '' && !pathname.startsWith('/api/');
55
81
  }
@@ -0,0 +1,67 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import { ApplicationError } from '../app/application-error.js';
4
+
5
+ const DEFAULT_TTL_MS = 5 * 60 * 1000;
6
+
7
+ export function createCommonKnowledgePreviewTokens({
8
+ now = Date.now,
9
+ createToken = randomUUID,
10
+ ttlMs = DEFAULT_TTL_MS
11
+ } = {}) {
12
+ const previews = new Map();
13
+
14
+ return Object.freeze({
15
+ issue(input) {
16
+ pruneExpired(previews, now());
17
+ const token = createToken();
18
+ previews.set(token, {
19
+ expiresAt: now() + ttlMs,
20
+ intent: promotionIntent(input)
21
+ });
22
+ return token;
23
+ },
24
+
25
+ consume(token, input) {
26
+ if (!token) {
27
+ throw new ApplicationError(
28
+ 'preview_required',
29
+ 'Preview this exact common-knowledge promotion before applying it'
30
+ );
31
+ }
32
+ const preview = previews.get(token);
33
+ if (!preview || preview.expiresAt <= now()) {
34
+ previews.delete(token);
35
+ throw new ApplicationError(
36
+ 'preview_expired',
37
+ 'Common-knowledge preview authorization is missing or expired'
38
+ );
39
+ }
40
+ if (preview.intent !== promotionIntent(input)) {
41
+ throw new ApplicationError(
42
+ 'preview_mismatch',
43
+ 'Common-knowledge promotion no longer matches its preview'
44
+ );
45
+ }
46
+ previews.delete(token);
47
+ }
48
+ });
49
+ }
50
+
51
+ function promotionIntent(input) {
52
+ return JSON.stringify({
53
+ personalSpaceId: input.personalSpaceId,
54
+ parentProjectId: input.parentProjectId,
55
+ itemKind: input.itemKind,
56
+ canonicalItemId: input.canonicalItemId,
57
+ duplicateItemIds: [...(input.duplicateItemIds ?? [])].sort(),
58
+ reason: input.reason,
59
+ humanConfirmationReason: input.humanConfirmationReason
60
+ });
61
+ }
62
+
63
+ function pruneExpired(previews, currentTime) {
64
+ for (const [token, preview] of previews) {
65
+ if (preview.expiresAt <= currentTime) previews.delete(token);
66
+ }
67
+ }
@@ -5,13 +5,23 @@ import { callAgentTool, listAgentTools } from '../agent-tools.js';
5
5
  import { FULI_VERSION } from '../package-metadata.js';
6
6
  import { MCP_INSTRUCTIONS } from './instructions.js';
7
7
  import { createProjectActionPreviewTokens } from './project-action-preview-tokens.js';
8
+ import { createCommonKnowledgePreviewTokens } from './common-knowledge-preview-tokens.js';
9
+ import { auditLifecycleTool } from './lifecycle-audit.js';
8
10
  import { annotationsFor } from './tool-annotations.js';
9
- import { errorToolResult, protocolErrorResult, successToolResult } from './tool-result.js';
11
+ import {
12
+ errorToolResult,
13
+ hookAdditionalContextToolResult,
14
+ protocolErrorResult,
15
+ successToolResult
16
+ } from './tool-result.js';
10
17
  import { jsonSchemaToZod, openObjectSchema } from './tool-schema.js';
11
18
 
12
19
  const TOOL_RESULT_LIMIT_BYTES = Object.freeze({
20
+ begin_task_context: 16 * 1024,
13
21
  get_collaboration_preferences: 16 * 1024,
14
- search_knowledge_graph: 32 * 1024
22
+ search_knowledge_graph: 32 * 1024,
23
+ search_current_project_knowledge: 64 * 1024,
24
+ discover_common_knowledge_candidates: 32 * 1024
15
25
  });
16
26
 
17
27
  export function createMcpServer(app) {
@@ -27,6 +37,7 @@ export function createMcpServer(app) {
27
37
  }
28
38
 
29
39
  function createToolMap(app, projectActionPreviews) {
40
+ const commonKnowledgePreviews = createCommonKnowledgePreviewTokens();
30
41
  return new Map(listAgentTools().map((definition) => [definition.name, {
31
42
  definition,
32
43
  schema: jsonSchemaToZod(definition.inputSchema),
@@ -34,7 +45,8 @@ function createToolMap(app, projectActionPreviews) {
34
45
  app,
35
46
  definition.name,
36
47
  input,
37
- projectActionPreviews
48
+ projectActionPreviews,
49
+ commonKnowledgePreviews
38
50
  )
39
51
  }]));
40
52
  }
@@ -62,15 +74,29 @@ function registerCallHandler(server, tools) {
62
74
 
63
75
  async function invokeTool(tool, input) {
64
76
  try {
65
- return successToolResult(await tool.invoke(input), {
66
- limitBytes: TOOL_RESULT_LIMIT_BYTES[tool.definition.name]
67
- });
77
+ const value = await tool.invoke(input);
78
+ auditLifecycleTool(tool.definition.name);
79
+ const limitBytes = TOOL_RESULT_LIMIT_BYTES[tool.definition.name];
80
+ if (tool.definition.name === 'begin_task_context') {
81
+ return hookAdditionalContextToolResult(value, {
82
+ hookEventName: 'UserPromptSubmit',
83
+ label: 'Fuli task context. Apply effective_preferences and use taskContextToken for the final checkpoint.',
84
+ limitBytes
85
+ });
86
+ }
87
+ return successToolResult(value, { limitBytes });
68
88
  } catch (error) {
69
89
  return errorToolResult(error);
70
90
  }
71
91
  }
72
92
 
73
- async function invokeAgentTool(app, name, input, projectActionPreviews) {
93
+ async function invokeAgentTool(
94
+ app,
95
+ name,
96
+ input,
97
+ projectActionPreviews,
98
+ commonKnowledgePreviews
99
+ ) {
74
100
  if (name === 'preview_personal_project_action') {
75
101
  const result = await callAgentTool(app, name, input);
76
102
  return {
@@ -81,5 +107,15 @@ async function invokeAgentTool(app, name, input, projectActionPreviews) {
81
107
  if (name === 'apply_personal_project_action') {
82
108
  projectActionPreviews.consume(input.previewToken, input);
83
109
  }
110
+ if (name === 'preview_common_knowledge_promotion') {
111
+ const result = await callAgentTool(app, name, input);
112
+ return {
113
+ ...result,
114
+ previewToken: commonKnowledgePreviews.issue(input)
115
+ };
116
+ }
117
+ if (name === 'apply_common_knowledge_promotion') {
118
+ commonKnowledgePreviews.consume(input.previewToken, input);
119
+ }
84
120
  return callAgentTool(app, name, input);
85
121
  }
@@ -1,10 +1,9 @@
1
1
  export const MCP_INSTRUCTIONS = [
2
- "Each user task: call exactly get_collaboration_preferences before other tools/answer with projectPath=cwd; never substitute a project action or Fuli tool. Apply all effective_preferences: personal-global everywhere plus only Fuli's matched project. Resolve relevant deferred_conflict via resolve_deferred_preference_conflict; ignore others. Pending is search-only. Never guess personalProjectId. Writes must enforce preferences in the actual payload; final text is not compliance.",
3
- 'Before claiming URLs, routes, requirements, prior decisions, or runbooks are unknown, call search_knowledge_graph; resolve IDs via list_knowledge_spaces/list_personal_projects.',
4
- 'Search personal-global profile, exact personalProjectId, inheritable knowledge reached via PART_OF or USES_KNOWLEDGE_FROM, named contextPersonalProjectIds, and relevant subscribed public project IDs. Never inherit project preferences or traverse RELATED_TO. Pending results stay marked. On a location miss, ask to widen to all registered local personal projects; use all_local_confirmed only after explicit confirmation. It never expands public projects.',
5
- 'After a confirmed all-local miss, search current repository or workspace files with Grep or rg. Never scan user home, filesystem root, outside the workspace, credentials, browser data, or unrelated repositories. Require deployment evidence for live URLs.',
6
- 'Changing/live: use monitoring or Git MCP',
7
- 'Silently batch via capture_session_knowledge under personalProjectId. Never store secrets, transcripts, or guesses. Only human/source confirmed knowledge enters public review. Final checkpoint: flush or retain nothing.',
8
- 'When personal knowledge affects the final answer/action, call record_knowledge_usage. Retrieval/injection never count; it creates at most agent_confirmed, never public eligibility.',
9
- 'Searches return terminal-safe Markdown. Final MUST begin with sourceMarker.leadMarkdown and append sourceMarker.markdown unchanged; without support use noMatchSourceMarker. Never wrap markers in HTML, rewrite them, or cite writes as reads.'
2
+ "Each user task: if begin_task_context supplied hook-provided task context, apply. Otherwise call exactly get_collaboration_preferences with projectPath=cwd before other tools/answer as the fallback; never substitute a project action or Fuli tool. Apply all effective_preferences: personal-global everywhere plus only Fuli's matched project. Resolve relevant deferred_conflict first. Never guess personalProjectId. Writes enforce preferences in the actual payload; final text is not compliance.",
3
+ 'For current-project facts use search_current_project_knowledge: exact active child first, then inheritable parent knowledge via outgoing PART_OF/USES_KNOWLEDGE_FROM within two hops. Child stable keys override. Never inherit project preferences or traverse RELATED_TO.',
4
+ 'Before claiming facts unknown, use search_knowledge_graph. Use exact IDs for extra projects; mark pending. Widen a location miss to all_local_confirmed only after consent, then use rg only in the safe current repo/workspace. Never scan outside it. Live URLs need deployment evidence; changing state needs monitoring/Git MCP.',
5
+ 'Use record_decision_trace for confirmed choices/rationale/validation. Use record_knowledge_feedback when retained knowledge is rejected, contradicted, outdated, or fails validation; preserve history.',
6
+ 'Batch durable confirmed knowledge via capture_session_knowledge in exact scope. Never store secrets, transcripts, or guesses. Only human/source confirmation permits public review. Finish a hook-managed task with checkpoint_task_knowledge once: capture_candidates or retain_nothing; fallback Agents likewise flush or retain nothing.',
7
+ 'When personal knowledge materially affects the answer/action, call record_knowledge_usage. Retrieval/injection never counts; it creates at most agent_confirmed and is never public-eligible.',
8
+ 'Terminal-safe Markdown. Final MUST begin with sourceMarker.leadMarkdown; append sourceMarker.markdown unchanged only on match. On miss use noMatchSourceMarker leadMarkdown only (empty markdown). Never wrap markers in HTML or cite writes.'
10
9
  ].join(' ');