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
@@ -14,6 +14,12 @@ const label = boundedString(512);
14
14
  const shortText = boundedString(2048);
15
15
  const longText = boundedString(8192);
16
16
  const dateTime = { ...boundedString(64), format: 'date-time' };
17
+ const sourceUri = {
18
+ type: ['string', 'null'],
19
+ minLength: 1,
20
+ maxLength: 2048,
21
+ pattern: '^[Hh][Tt][Tt][Pp][Ss]?://\\S+$'
22
+ };
17
23
  const attributes = { type: 'object', additionalProperties: true };
18
24
  const epistemicQuadrant = enumSchema([
19
25
  'known_known', 'known_unknown', 'unknown_known', 'unknown_unknown'
@@ -29,6 +35,10 @@ const confirmationActor = objectSchema({
29
35
  kind: enumSchema(['user', 'agent', 'authoritative_source', 'import']),
30
36
  label: nullableStringSchema()
31
37
  }, ['kind']);
38
+ const decisionActor = objectSchema({
39
+ kind: enumSchema(['user', 'agent', 'authoritative_source']),
40
+ label: nullableStringSchema()
41
+ }, ['kind']);
32
42
  const confirmationBasis = objectSchema({
33
43
  existenceReason: boundedString(4096),
34
44
  quadrantReason: boundedString(4096),
@@ -155,12 +165,70 @@ const personalProjectActionIntent = {
155
165
  const personalProjectActionRequired = [
156
166
  'personalSpaceId', 'itemKind', 'itemId', 'mode', 'reason'
157
167
  ];
168
+ const commonKnowledgePromotionIntent = {
169
+ personalSpaceId: id,
170
+ parentProjectId: id,
171
+ itemKind: knowledgeItemKind,
172
+ canonicalItemId: id,
173
+ duplicateItemIds: arraySchema(id, { minItems: 1, maxItems: 31 }),
174
+ reason: boundedString(2000),
175
+ humanConfirmationReason: boundedString(2000)
176
+ };
177
+ const commonKnowledgePromotionRequired = [
178
+ 'personalSpaceId', 'parentProjectId', 'itemKind', 'canonicalItemId',
179
+ 'duplicateItemIds', 'reason', 'humanConfirmationReason'
180
+ ];
181
+ const captureEpisodeFields = {
182
+ idempotencyKey: id,
183
+ name: label,
184
+ sourceKind: boundedString(128),
185
+ sourceDescription: boundedString(1024),
186
+ sourceUri,
187
+ sourceApplication,
188
+ sourceTurnId: { type: ['string', 'null'], minLength: 1, maxLength: 256 },
189
+ sourceExcerpt: { type: ['string', 'null'], minLength: 1, maxLength: 2048 },
190
+ referenceTime: dateTime,
191
+ summary: longText,
192
+ sensitivity: enumSchema(['normal', 'private', 'restricted']),
193
+ entities: arraySchema(entity, { minItems: 1, maxItems: 128 }),
194
+ relationships: arraySchema(relationship, { maxItems: 256 })
195
+ };
196
+ const captureEpisodeRequired = [
197
+ 'idempotencyKey', 'name', 'sourceKind', 'sourceDescription',
198
+ 'referenceTime', 'entities', 'relationships'
199
+ ];
158
200
 
159
201
  export const GRAPH_TOOL_DEFINITIONS = [
202
+ {
203
+ name: 'begin_task_context',
204
+ title: 'LIFECYCLE · Begin a Fuli-aware task',
205
+ description: 'Lifecycle entry used by supported Agent hooks before the model processes a user prompt. It resolves the exact local project from projectPath, loads effective collaboration preferences, and returns an opaque taskContextToken. It never stores projectPath. Claude Code setup installs this as a deterministic UserPromptSubmit hook; manual Agent calls are a fallback.',
206
+ inputSchema: objectSchema({
207
+ sessionId: id,
208
+ projectPath: boundedString(4096)
209
+ }, ['sessionId', 'projectPath'])
210
+ },
211
+ {
212
+ name: 'checkpoint_task_knowledge',
213
+ title: 'LIFECYCLE · Finish knowledge review',
214
+ description: 'Complete exactly one end-of-task knowledge review. Use capture_candidates only for a small durable batch supported by the task; captured Agent proposals remain pending unless the payload contains valid human or authoritative-source confirmation. Use retain_nothing when the turn produced no reusable knowledge. Never store raw transcripts, guesses, temporary logs, credentials, or disposable output.',
215
+ inputSchema: objectSchema({
216
+ taskContextToken: id,
217
+ disposition: enumSchema(['capture_candidates', 'retain_nothing']),
218
+ reason: boundedString(2000),
219
+ capture: objectSchema(captureEpisodeFields, captureEpisodeRequired)
220
+ }, ['taskContextToken', 'disposition', 'reason'])
221
+ },
222
+ {
223
+ name: 'verify_task_checkpoint',
224
+ title: 'LIFECYCLE · Verify end-of-task review',
225
+ description: 'Hook-facing read that returns a Claude Code Stop decision. It blocks stopping only when begin_task_context succeeded for the session and checkpoint_task_knowledge has not yet recorded capture_candidates or retain_nothing.',
226
+ inputSchema: objectSchema({ sessionId: id }, ['sessionId'])
227
+ },
160
228
  {
161
229
  name: 'get_collaboration_preferences',
162
230
  title: 'READ FIRST · Load task collaboration preferences',
163
- description: 'Call this exact tool name at the start of every user task before any other tool or answer; never substitute a project action tool. Pass projectPath as the current working directory; Fuli uses it transiently, never stores or returns it, and layers preferences only for one exact registered local project match. Returns effective_preferences plus deferred_conflicts intentionally queued for AI. Apply effective_preferences before answering or constructing tool arguments. Human-confirmed preferences outrank explicitly marked agent-confirmed preferences. If the current task would use a deferred conflict, call resolve_deferred_preference_conflict before applying either side; ignore unrelated deferred conflicts. For write tools, enforce preferences in the actual payload; mentioning them only in the final answer is not compliance. Personal-global preferences always apply; other conflicted, pending, invalid, ambiguous-project, and unrelated-project items never auto-apply. Automatic injection does not count as usage evidence. personalProjectId remains an explicit compatibility override.',
231
+ description: 'Fallback for Agents or tasks without begin_task_context hook context. When no hook context was supplied, call this exact tool name at the start of every user task before any other tool or answer; never substitute a project action tool. Do not call it redundantly when the entry hook already supplied preferences. Pass projectPath as the current working directory; Fuli uses it transiently, never stores or returns it, and layers preferences only for one exact registered local project match. Returns effective_preferences plus deferred_conflicts intentionally queued for AI. Apply effective_preferences before answering or constructing tool arguments. Human-confirmed preferences outrank explicitly marked agent-confirmed preferences. If the current task would use a deferred conflict, call resolve_deferred_preference_conflict before applying either side; ignore unrelated deferred conflicts. For write tools, enforce preferences in the actual payload; mentioning them only in the final answer is not compliance. Personal-global preferences always apply; other conflicted, pending, invalid, ambiguous-project, and unrelated-project items never auto-apply. Automatic injection does not count as usage evidence. personalProjectId remains an explicit compatibility override.',
164
232
  inputSchema: objectSchema({
165
233
  projectPath: boundedString(4096),
166
234
  personalProjectId: nullableStringSchema(),
@@ -185,34 +253,64 @@ export const GRAPH_TOOL_DEFINITIONS = [
185
253
  },
186
254
  {
187
255
  name: 'capture_session_knowledge',
188
- description: 'Silently batch durable session knowledge when the user-enabled capture policy allows it. Every item records its discovery quadrant, structured confirmation basis, and pending or confirmed state. A disabled policy returns capture_disabled without writing. Personal profile knowledge stays personal; only project knowledge with an auditable confirmation can enter public review.',
256
+ description: 'Silently batch durable session knowledge when the user-enabled capture policy allows it. When knowledge was read from an online source, sourceUri preserves the exact original HTTP(S) link so a capable Agent can re-read it later and refresh Fuli knowledge; it does not authorize source-document writeback. Every item records its discovery quadrant, structured confirmation basis, and pending or confirmed state. A disabled policy returns capture_disabled without writing. Personal profile knowledge stays personal; only project knowledge with an auditable confirmation can enter public review.',
189
257
  inputSchema: objectSchema({
190
258
  targetKind: enumSchema(['personal', 'project']),
191
259
  spaceId: id,
192
260
  personalProjectId: nullableStringSchema(),
193
261
  providerUrl: nullableStringSchema(),
194
- idempotencyKey: id,
195
262
  sessionId: id,
196
- name: label,
263
+ ...captureEpisodeFields
264
+ }, [
265
+ 'targetKind', 'spaceId', 'sessionId', ...captureEpisodeRequired
266
+ ])
267
+ },
268
+ {
269
+ name: 'record_decision_trace',
270
+ title: 'WRITE · Store a decision with its rationale',
271
+ description: 'Store one bounded, searchable project decision as a linked Decision, selected and rejected DecisionOption nodes, a DecisionRationale, and optional ValidationResult nodes. reason is mandatory: do not store a bare conclusion. User or authoritative-source decisions are confirmed with their audit basis; Agent-only proposals remain pending. Do not copy raw transcripts, credentials, private contact details, or disposable deliberation.',
272
+ inputSchema: objectSchema({
273
+ personalSpaceId: id,
274
+ personalProjectId: nullableStringSchema(),
275
+ sessionId: id,
276
+ idempotencyKey: id,
277
+ decisionKey: id,
278
+ title: label,
279
+ question: shortText,
280
+ selectedOption: objectSchema({
281
+ key: id,
282
+ label,
283
+ summary: nullableStringSchema()
284
+ }, ['key', 'label']),
285
+ rejectedOptions: arraySchema(objectSchema({
286
+ key: id,
287
+ label,
288
+ summary: nullableStringSchema()
289
+ }, ['key', 'label']), { maxItems: 16 }),
290
+ reason: boundedString(4096),
291
+ validationResults: arraySchema(objectSchema({
292
+ key: id,
293
+ outcome: enumSchema(['pass', 'fail', 'inconclusive']),
294
+ summary: shortText
295
+ }, ['key', 'outcome', 'summary']), { maxItems: 16 }),
296
+ decidedBy: decisionActor,
297
+ referenceTime: dateTime,
197
298
  sourceKind: boundedString(128),
198
299
  sourceDescription: boundedString(1024),
300
+ sourceUri,
199
301
  sourceApplication,
200
302
  sourceTurnId: { type: ['string', 'null'], minLength: 1, maxLength: 256 },
201
- sourceExcerpt: { type: ['string', 'null'], minLength: 1, maxLength: 2048 },
202
- referenceTime: dateTime,
203
- summary: longText,
204
- sensitivity: enumSchema(['normal', 'private', 'restricted']),
205
- entities: arraySchema(entity, { minItems: 1, maxItems: 128 }),
206
- relationships: arraySchema(relationship, { maxItems: 256 })
303
+ sensitivity: enumSchema(['normal', 'private', 'restricted'])
207
304
  }, [
208
- 'targetKind', 'spaceId', 'idempotencyKey', 'sessionId', 'name',
209
- 'sourceKind', 'sourceDescription', 'referenceTime', 'entities', 'relationships'
305
+ 'personalSpaceId', 'sessionId', 'idempotencyKey', 'decisionKey',
306
+ 'title', 'question', 'selectedOption', 'reason', 'decidedBy',
307
+ 'referenceTime', 'sourceKind', 'sourceDescription'
210
308
  ])
211
309
  },
212
310
  {
213
311
  name: 'search_knowledge_graph',
214
312
  title: 'READ · Search the scoped knowledge graph',
215
- description: 'Search durable context before saying you do not know when a task may depend on remembered URLs, routes, requirements, architecture, prior decisions, runbooks, rationale, or personal preferences. Pending knowledge is searchable and explicitly marked; agent-confirmed knowledge ranks below human-confirmed knowledge. The bounded scope includes the personal-global profile, exact active local project, selectively inheritable knowledge reached through PART_OF or USES_KNOWLEDGE_FROM, explicitly selected additional personal projects, and selected subscribed team projects. Generic RELATED_TO links never expand scope. Use all_local_confirmed only after explicit user confirmation; it searches registered local personal projects for this query and never expands public projects. If that still has no support, use read-only local file search in the current repository or workspace files within a safe root. The response includes sourceMarker for supporting results, noMatchSourceMarker when returned items do not support the answer, and retrievalGuidance with the required next action.',
313
+ description: 'Search durable context before saying you do not know when a task may depend on remembered URLs, routes, requirements, architecture, prior decisions, runbooks, rationale, or personal preferences. Supporting facts and entities expose bounded source_uris when their evidence came from online sources, allowing a capable Agent to re-read the original source before refreshing Fuli knowledge. Pending knowledge is searchable and explicitly marked; agent-confirmed knowledge ranks below human-confirmed knowledge. The bounded scope includes the personal-global profile, exact active local project, selectively inheritable knowledge reached through PART_OF or USES_KNOWLEDGE_FROM, explicitly selected additional personal projects, and selected subscribed team projects. Generic RELATED_TO links never expand scope. Use all_local_confirmed only after explicit user confirmation; it searches registered local personal projects for this query and never expands public projects. If that still has no support, use read-only local file search in the current repository or workspace files within a safe root. The response includes sourceMarker for supporting results, noMatchSourceMarker when returned items do not support the answer, and retrievalGuidance with the required next action.',
216
314
  inputSchema: objectSchema({
217
315
  personalSpaceId: id,
218
316
  query: shortText,
@@ -240,6 +338,76 @@ export const GRAPH_TOOL_DEFINITIONS = [
240
338
  }, ['itemId', 'itemKind', 'useKind']), { minItems: 1, maxItems: 200 })
241
339
  }, ['personalSpaceId', 'taskId', 'items'])
242
340
  },
341
+ {
342
+ name: 'record_knowledge_feedback',
343
+ title: 'WRITE · Record negative knowledge evidence',
344
+ description: 'Record bounded negative evidence when retained knowledge is rejected, fails validation, is contradicted, or becomes outdated. Each event requires a reason and evidence summary and is idempotent per task, item, feedback kind, and content generation. It lowers ranking signals and marks the item requires_attention. It never deletes evidence or silently overrides human-confirmed authority; trusted human/source contradiction can return agent-confirmed knowledge to pending.',
345
+ inputSchema: objectSchema({
346
+ personalSpaceId: id,
347
+ taskId: id,
348
+ sessionId: nullableStringSchema(),
349
+ toolName: nullableStringSchema(),
350
+ items: arraySchema(objectSchema({
351
+ itemId: id,
352
+ itemKind: knowledgeItemKind,
353
+ feedbackKind: enumSchema([
354
+ 'rejected', 'validation_failed', 'contradicted', 'outdated'
355
+ ]),
356
+ reason: boundedString(2000),
357
+ evidenceSummary: boundedString(4096),
358
+ reportedByKind: enumSchema([
359
+ 'user', 'agent', 'authoritative_source'
360
+ ]),
361
+ sourceUri
362
+ }, [
363
+ 'itemId', 'itemKind', 'feedbackKind', 'reason',
364
+ 'evidenceSummary', 'reportedByKind'
365
+ ]), { minItems: 1, maxItems: 200 })
366
+ }, ['personalSpaceId', 'taskId', 'items'])
367
+ },
368
+ {
369
+ name: 'search_current_project_knowledge',
370
+ title: 'READ · Search current project and its knowledge sources',
371
+ description: 'High-level project search for normal Agent work. Pass projectPath plus one or more focused queries; Fuli resolves the exact local project, searches its local knowledge first, then follows outgoing PART_OF or USES_KNOWLEDGE_FROM relations to authorized parent/source knowledge. An exact current-project item with the same stable key overrides an inherited item. The tool never guesses an ambiguous project and never traverses RELATED_TO.',
372
+ inputSchema: objectSchema({
373
+ projectPath: boundedString(4096),
374
+ queries: arraySchema(shortText, { minItems: 1, maxItems: 10 }),
375
+ limitPerQuery: integerSchema({ minimum: 1, maximum: 50 }),
376
+ includeHistorical: booleanSchema(),
377
+ includePending: booleanSchema()
378
+ }, ['projectPath', 'queries'])
379
+ },
380
+ {
381
+ name: 'discover_common_knowledge_candidates',
382
+ title: 'READ · Find possible parent-project knowledge',
383
+ description: 'Read-only governance aid for one parent personal project. It searches each direct PART_OF child independently, excludes inherited and personal-global results, and groups lexically similar knowledge found in multiple children. Similarity is only an inferred candidate signal: the tool never merges, promotes, rewrites, or invalidates knowledge, and every candidate requires explicit human confirmation before a separate promotion action.',
384
+ inputSchema: objectSchema({
385
+ personalSpaceId: id,
386
+ parentProjectId: id,
387
+ query: shortText,
388
+ minChildProjects: integerSchema({ minimum: 2, maximum: 32 }),
389
+ similarityThreshold: numberSchema({ minimum: 0, maximum: 1 }),
390
+ limitPerProject: integerSchema({ minimum: 1, maximum: 50 })
391
+ }, ['personalSpaceId', 'parentProjectId', 'query'])
392
+ },
393
+ {
394
+ name: 'preview_common_knowledge_promotion',
395
+ title: 'CONFIRM · Preview parent-project promotion',
396
+ description: 'Validate the exact common-knowledge promotion after a human has explicitly chosen the canonical item, duplicates, parent project, and rationale. The preview is read-only, requires items from distinct direct PART_OF children, and returns a short-lived token binding the exact intent. Do not treat inferred lexical similarity as human confirmation.',
397
+ inputSchema: objectSchema(
398
+ commonKnowledgePromotionIntent,
399
+ commonKnowledgePromotionRequired
400
+ )
401
+ },
402
+ {
403
+ name: 'apply_common_knowledge_promotion',
404
+ title: 'WRITE · Atomically promote parent knowledge',
405
+ description: 'Apply one human-confirmed preview as a single Provider mutation. It moves the canonical item to the parent, enables descendants inheritance, invalidates selected child duplicates with replacement links, and stores both the scope reason and human-confirmation reason in permanent revision history. A valid one-time previewToken is mandatory.',
406
+ inputSchema: objectSchema({
407
+ ...commonKnowledgePromotionIntent,
408
+ previewToken: id
409
+ }, [...commonKnowledgePromotionRequired, 'previewToken'])
410
+ },
243
411
  {
244
412
  name: 'get_knowledge_graph',
245
413
  description: 'Return a bounded node-edge graph for a personal space, one exact local personal project, or a team-shared project.',
@@ -1,6 +1,10 @@
1
1
  import { ApplicationError } from '../app/application-error.js';
2
2
 
3
3
  const HANDLERS = Object.freeze({
4
+ begin_task_context: (app, input) => app.beginTaskContext(input),
5
+ checkpoint_task_knowledge: (app, input) =>
6
+ app.checkpointTaskKnowledge(input),
7
+ verify_task_checkpoint: (app, input) => app.verifyTaskCheckpoint(input),
4
8
  get_collaboration_preferences: (app, input) => app.getCollaborationPreferences({
5
9
  ...input,
6
10
  agentInvocation: true,
@@ -12,12 +16,27 @@ const HANDLERS = Object.freeze({
12
16
  operationActor: 'agent'
13
17
  }),
14
18
  capture_session_knowledge: (app, input) => app.captureSessionKnowledge(input),
19
+ record_decision_trace: (app, input) => app.recordDecisionTrace(input),
15
20
  search_knowledge_graph: (app, input) => app.searchKnowledge({
16
21
  ...input,
17
22
  agentInvocation: true,
18
23
  agentToolName: 'search_knowledge_graph'
19
24
  }),
20
25
  record_knowledge_usage: (app, input) => app.recordKnowledgeUsage(input),
26
+ record_knowledge_feedback: (app, input) => app.recordKnowledgeFeedback(input),
27
+ search_current_project_knowledge: (app, input) =>
28
+ app.searchCurrentProjectKnowledge(input),
29
+ discover_common_knowledge_candidates: (app, input) =>
30
+ app.discoverCommonKnowledgeCandidates(input),
31
+ preview_common_knowledge_promotion: (app, input) =>
32
+ app.previewCommonKnowledgePromotion(input),
33
+ apply_common_knowledge_promotion: (app, input) => {
34
+ const { previewToken: _previewToken, ...promotion } = input;
35
+ return app.applyCommonKnowledgePromotion({
36
+ ...promotion,
37
+ operationActor: 'agent'
38
+ });
39
+ },
21
40
  get_knowledge_graph: (app, input) => app.getKnowledgeGraph({
22
41
  ...input,
23
42
  agentInvocation: true,
@@ -1,78 +1,25 @@
1
- import { candidate, candidates } from './commands/candidates.js';
2
- import { observe, remember } from './commands/ingestion.js';
3
- import { context, history, rules, search, timeline } from './commands/reads.js';
4
- import { createSpace, subscribe } from './commands/spaces.js';
5
-
6
- const COMMANDS = Object.freeze({
7
- space: createSpace,
8
- subscribe,
9
- remember,
10
- observe,
11
- search,
12
- timeline,
13
- rules,
14
- history,
15
- context,
16
- candidates,
17
- candidate
18
- });
19
-
20
1
  const LOCAL_RUNTIME_COMMANDS = new Set(['start', 'stop', 'restart', 'status', 'open']);
21
2
 
22
- export function dispatchCommand(app, command, args) {
23
- const handler = COMMANDS[command];
24
- if (!handler) throw new Error(`Unknown command: ${command}`);
25
- return handler(app, args);
26
- }
27
-
28
- export function isRegisteredCommand(command) {
29
- return command === '--help' || command === '-h' ||
30
- command === '--version' || command === '-v' ||
31
- Object.hasOwn(COMMANDS, command);
32
- }
33
-
34
- export function isCliCommand(command) {
35
- return command === 'setup' || command === 'uninstall' || command === 'migrate' ||
36
- LOCAL_RUNTIME_COMMANDS.has(command) || isRegisteredCommand(command);
37
- }
38
-
39
3
  export function isLocalRuntimeCommand(command) {
40
4
  return LOCAL_RUNTIME_COMMANDS.has(command);
41
5
  }
42
6
 
43
7
  export function printHelp() {
44
- console.log(`fl <command>
8
+ console.log(`fuli <command> (short alias: fl)
45
9
 
46
10
  General:
47
11
  --help, -h
48
12
  --version, -v
49
13
 
50
14
  Local service:
51
- start [--port 2727] [--open] [--rebuild] [--data-dir DIR]
15
+ start [--port 2727] [--open] [--rebuild] [--data-dir DIR] [--personal-space NAME]
52
16
  stop [--data-dir DIR]
53
- restart [--port 2727] [--open] [--rebuild] [--data-dir DIR]
17
+ restart [--port 2727] [--open] [--rebuild] [--data-dir DIR] [--personal-space NAME]
54
18
  status [--json] [--data-dir DIR]
55
19
  open [--data-dir DIR]
56
20
 
57
21
  Install and Agent connection:
58
- setup [--yes] [--codex-only] [--data-dir DIR] [--personal-space NAME] [--port PORT] [--skip-agents] [--no-start] [--with-dev-public]
59
- uninstall [--yes] [--data-dir DIR]
60
-
61
- Legacy local knowledge commands:
62
- fl [--db SQLITE_DB] [--personal-space 我] <command>
63
- Global options must appear before the command.
64
-
65
- Commands:
66
- space create NAME --kind personal|public
67
- subscribe PERSONAL_SPACE PUBLIC_SPACE
68
- remember PERSONAL_SPACE --target SPACE --source-kind prd --text TEXT
69
- observe PERSONAL_SPACE --target SPACE
70
- search PERSONAL_SPACE QUERY
71
- timeline SPACE SUBJECT
72
- rules SPACE
73
- history SPACE PREDICATE
74
- context PERSONAL_SPACE SPACE QUERY
75
- candidates PERSONAL_SPACE
76
- candidate CANDIDATE_ID sync|personal_only|ignore
77
- migrate --from LEGACY_JSON --to SQLITE_DB`);
22
+ setup [--yes] [--codex-only] [--data-dir DIR] [--personal-space NAME] [--port PORT] [--skip-agents] [--no-start] [--personal-only|--with-dev-public]
23
+ update [setup options]
24
+ uninstall [--yes] [--data-dir DIR]`);
78
25
  }
@@ -0,0 +1,293 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { posix, win32 } from 'node:path';
4
+ import { createInterface } from 'node:readline/promises';
5
+
6
+ import {
7
+ FULI_PACKAGE_NAME,
8
+ FULI_VERSION
9
+ } from '../package-metadata.js';
10
+ import { quoteShellArgument } from '../runtime-options.js';
11
+ import { parseUpdateOptions } from '../setup/options.js';
12
+ import { runLocalRuntimeCommand } from './local-runtime-command.js';
13
+
14
+ const LATEST_PACKAGE = `${FULI_PACKAGE_NAME}@latest`;
15
+
16
+ export async function runUpdateCommand(args = [], dependencies = {}) {
17
+ const options = parseUpdateOptions(args);
18
+ const env = dependencies.env ?? process.env;
19
+ const platform = dependencies.platform ?? process.platform;
20
+ const nodePath = dependencies.nodePath ?? process.execPath;
21
+ const npmCommand = dependencies.npmCommand ??
22
+ (platform === 'win32' ? 'npm.cmd' : 'npm');
23
+ const confirm = dependencies.confirm ?? confirmInTerminal;
24
+ const write = dependencies.write ?? writeLine;
25
+ const stopRuntime = dependencies.stopRuntime ?? runLocalRuntimeCommand;
26
+ const runInherited = dependencies.runInherited ?? spawnInherited;
27
+ const runCaptured = dependencies.runCaptured ?? spawnCaptured;
28
+ const fileExists = dependencies.fileExists ?? existsSync;
29
+ const resolveLatestVersion = dependencies.resolveLatestVersion ??
30
+ (() => queryLatestVersion(npmCommand, runCaptured, env));
31
+
32
+ write(formatUpdatePreview(options));
33
+ if (!options.yes && !await confirm()) {
34
+ write('已取消,没有修改任何内容。');
35
+ return { status: 'cancelled' };
36
+ }
37
+
38
+ const latestVersion = resolveLatestVersion();
39
+ if (compareSemanticVersions(latestVersion, FULI_VERSION) < 0) {
40
+ const result = {
41
+ status: 'ahead',
42
+ previousVersion: FULI_VERSION,
43
+ version: FULI_VERSION,
44
+ latestVersion
45
+ };
46
+ write(formatUpdateResult(result));
47
+ return result;
48
+ }
49
+
50
+ const stopArgs = options.dataDir ? ['--data-dir', options.dataDir] : [];
51
+ const stopped = await stopRuntime('stop', stopArgs, { env, write });
52
+ if (stopped.status === 'partial') {
53
+ throw new Error([
54
+ '无法安全确认旧版界面进程已停止,更新尚未开始。',
55
+ '请先运行 fuli status 检查本地状态,再重试 fuli update。'
56
+ ].join('\n'));
57
+ }
58
+
59
+ const selectedPackage = `${FULI_PACKAGE_NAME}@${latestVersion}`;
60
+ write(`正在安装 ${selectedPackage}(npm latest)…`);
61
+ const installed = runInherited(npmCommand, [
62
+ 'install',
63
+ '--global',
64
+ selectedPackage,
65
+ '--no-audit',
66
+ '--no-fund'
67
+ ], { env });
68
+ if (!processSucceeded(installed)) {
69
+ throw new Error([
70
+ `全局包安装失败(${describeProcessFailure(installed)})。知识数据未删除。`,
71
+ `请先运行 npm install --global ${LATEST_PACKAGE},再运行 fuli setup --yes。`
72
+ ].join('\n'));
73
+ }
74
+
75
+ const globalRootResult = runCaptured(npmCommand, ['root', '--global'], { env });
76
+ if (!processSucceeded(globalRootResult)) {
77
+ throw postInstallError(
78
+ `无法定位 npm 全局安装目录(${describeProcessFailure(globalRootResult)})`,
79
+ args,
80
+ platform
81
+ );
82
+ }
83
+ const globalRoot = String(globalRootResult.stdout ?? '').trim();
84
+ const cliPath = resolveGlobalCliPath(globalRoot, platform);
85
+ if (!cliPath || !fileExists(cliPath)) {
86
+ throw postInstallError('新版 CLI 文件不存在,请重新安装全局包', args, platform);
87
+ }
88
+
89
+ const versionResult = runCaptured(nodePath, [cliPath, '--version'], { env });
90
+ if (!processSucceeded(versionResult)) {
91
+ throw postInstallError(
92
+ `新版 CLI 无法启动(${describeProcessFailure(versionResult)})`,
93
+ args,
94
+ platform
95
+ );
96
+ }
97
+ const version = String(versionResult.stdout ?? '').trim();
98
+ if (!version) throw postInstallError('新版 CLI 未返回版本号', args, platform);
99
+ if (compareSemanticVersions(version, latestVersion) !== 0) {
100
+ throw postInstallError(
101
+ `新版 CLI 版本 ${version} 与更新前确认的 npm latest ${latestVersion} 不一致`,
102
+ args,
103
+ platform
104
+ );
105
+ }
106
+
107
+ write('正在使用新版 CLI 刷新本机服务、Agent 接入和 Skills…');
108
+ const setupArgs = ['setup', '--yes', ...args.filter((arg) => arg !== '--yes')];
109
+ const setupResult = runInherited(nodePath, [cliPath, ...setupArgs], { env });
110
+ if (!processSucceeded(setupResult)) {
111
+ throw postInstallError(
112
+ `新版已安装,但 setup 未完成(${describeProcessFailure(setupResult)})`,
113
+ args,
114
+ platform
115
+ );
116
+ }
117
+
118
+ const result = {
119
+ status: version === FULI_VERSION ? 'current' : 'updated',
120
+ previousVersion: FULI_VERSION,
121
+ version
122
+ };
123
+ write(formatUpdateResult(result));
124
+ return result;
125
+ }
126
+
127
+ export function formatUpdatePreview(options) {
128
+ return [
129
+ '准备更新复利',
130
+ `npm 包:${LATEST_PACKAGE}`,
131
+ options.noStart
132
+ ? '本地界面:安全停止;更新后保持关闭'
133
+ : '本地服务:安全停止;更新后由新版重新启动',
134
+ options.dataDir ? `数据:保留 ${options.dataDir}` : '数据:保留当前默认数据目录',
135
+ `Agent 接入与 Skills:${options.skipAgents ? '按参数跳过刷新' : '由新版 setup 刷新'}`
136
+ ].join('\n');
137
+ }
138
+
139
+ export function formatUpdateResult(result) {
140
+ if (result.status === 'ahead') {
141
+ return [
142
+ `当前 CLI 版本 ${result.version} 高于 npm latest ${result.latestVersion};`,
143
+ '为避免降级,未停止服务、未安装包,也未修改 Agent 接入。'
144
+ ].join('');
145
+ }
146
+ if (result.status === 'current') {
147
+ return `复利已是最新版本 ${result.version};本机接入已刷新。`;
148
+ }
149
+ return `复利已从 ${result.previousVersion} 更新到 ${result.version}。`;
150
+ }
151
+
152
+ export function compareSemanticVersions(left, right) {
153
+ const parsedLeft = parseSemanticVersion(left);
154
+ const parsedRight = parseSemanticVersion(right);
155
+ for (let index = 0; index < 3; index += 1) {
156
+ if (parsedLeft.core[index] > parsedRight.core[index]) return 1;
157
+ if (parsedLeft.core[index] < parsedRight.core[index]) return -1;
158
+ }
159
+ if (!parsedLeft.prerelease.length && !parsedRight.prerelease.length) return 0;
160
+ if (!parsedLeft.prerelease.length) return 1;
161
+ if (!parsedRight.prerelease.length) return -1;
162
+ const length = Math.max(parsedLeft.prerelease.length, parsedRight.prerelease.length);
163
+ for (let index = 0; index < length; index += 1) {
164
+ const leftPart = parsedLeft.prerelease[index];
165
+ const rightPart = parsedRight.prerelease[index];
166
+ if (leftPart === undefined) return -1;
167
+ if (rightPart === undefined) return 1;
168
+ if (leftPart === rightPart) continue;
169
+ const leftNumeric = /^\d+$/.test(leftPart);
170
+ const rightNumeric = /^\d+$/.test(rightPart);
171
+ if (leftNumeric && rightNumeric) {
172
+ return BigInt(leftPart) > BigInt(rightPart) ? 1 : -1;
173
+ }
174
+ if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
175
+ return leftPart > rightPart ? 1 : -1;
176
+ }
177
+ return 0;
178
+ }
179
+
180
+ export function resolveGlobalCliPath(globalRoot, platform = process.platform) {
181
+ const path = platform === 'win32' ? win32 : posix;
182
+ if (typeof globalRoot !== 'string' || !path.isAbsolute(globalRoot.trim())) return null;
183
+ return path.join(globalRoot.trim(), FULI_PACKAGE_NAME, 'src', 'cli.js');
184
+ }
185
+
186
+ function postInstallError(message, args, platform) {
187
+ return new Error([
188
+ `${message}。知识数据未删除。`,
189
+ `请重新运行:${formatSetupRecoveryCommand(args, platform)}`
190
+ ].join('\n'));
191
+ }
192
+
193
+ function queryLatestVersion(npmCommand, runCaptured, env) {
194
+ const result = runCaptured(
195
+ npmCommand,
196
+ ['view', LATEST_PACKAGE, 'version', '--json'],
197
+ { env }
198
+ );
199
+ if (!processSucceeded(result)) {
200
+ throw new Error([
201
+ `无法检查 ${LATEST_PACKAGE}(${describeProcessFailure(result)})。`,
202
+ '本地服务尚未停止,也没有安装或修改任何包。'
203
+ ].join('\n'));
204
+ }
205
+ const raw = String(result.stdout ?? '').trim();
206
+ let version = raw;
207
+ try {
208
+ const parsed = JSON.parse(raw);
209
+ if (typeof parsed === 'string') version = parsed;
210
+ } catch {
211
+ // npm can also return one unquoted version line.
212
+ }
213
+ parseSemanticVersion(version);
214
+ return version;
215
+ }
216
+
217
+ function parseSemanticVersion(value) {
218
+ const text = String(value ?? '').trim();
219
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/
220
+ .exec(text);
221
+ if (!match) throw new TypeError(`Invalid package version: ${text || '<empty>'}`);
222
+ const numeric = match.slice(1, 4);
223
+ if (numeric.some((part) => part.length > 1 && part.startsWith('0'))) {
224
+ throw new TypeError(`Invalid package version: ${text}`);
225
+ }
226
+ return {
227
+ core: numeric.map((part) => BigInt(part)),
228
+ prerelease: match[4]?.split('.') ?? []
229
+ };
230
+ }
231
+
232
+ function formatSetupRecoveryCommand(args, platform) {
233
+ const forwarded = args
234
+ .filter((arg) => arg !== '--yes')
235
+ .map((arg) => formatCommandArgument(arg, platform));
236
+ return ['fuli', 'setup', '--yes', ...forwarded].join(' ');
237
+ }
238
+
239
+ function formatCommandArgument(value, platform) {
240
+ const simple = platform === 'win32'
241
+ ? /^[A-Za-z0-9_./\\:@+-]+$/
242
+ : /^[A-Za-z0-9_./:@+-]+$/;
243
+ if (simple.test(value)) return value;
244
+ try {
245
+ return quoteShellArgument(value, platform);
246
+ } catch {
247
+ return '<复用原参数>';
248
+ }
249
+ }
250
+
251
+ function processSucceeded(result) {
252
+ return result?.status === 0 && !result.error && !result.signal;
253
+ }
254
+
255
+ function describeProcessFailure(result) {
256
+ if (result?.error?.message) return result.error.message;
257
+ if (result?.signal) return `signal ${result.signal}`;
258
+ if (Number.isInteger(result?.status)) return `exit code ${result.status}`;
259
+ return 'unknown process error';
260
+ }
261
+
262
+ function spawnInherited(command, args, { env }) {
263
+ return spawnSync(command, args, {
264
+ env,
265
+ stdio: 'inherit',
266
+ windowsHide: true
267
+ });
268
+ }
269
+
270
+ function spawnCaptured(command, args, { env }) {
271
+ return spawnSync(command, args, {
272
+ env,
273
+ encoding: 'utf8',
274
+ stdio: ['ignore', 'pipe', 'pipe'],
275
+ windowsHide: true
276
+ });
277
+ }
278
+
279
+ async function confirmInTerminal() {
280
+ const input = createInterface({ input: process.stdin, output: process.stdout });
281
+ try {
282
+ const answer = (await input.question('将修改全局 npm 安装,继续?[Y/n] '))
283
+ .trim()
284
+ .toLowerCase();
285
+ return answer === '' || answer === 'y' || answer === 'yes';
286
+ } finally {
287
+ input.close();
288
+ }
289
+ }
290
+
291
+ function writeLine(value) {
292
+ process.stdout.write(`${value}\n`);
293
+ }