fuli-context 0.5.0 → 0.7.3

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 (206) hide show
  1. package/README.md +370 -202
  2. package/README.zh-CN.md +448 -0
  3. package/compose.graphiti.yml +6 -6
  4. package/dist/web/assets/AboutPage-DXh7Fwxt.css +1 -0
  5. package/dist/web/assets/AboutPage-qeO_vm0T.js +1 -0
  6. package/dist/web/assets/ConnectionsPage-CBHAdZXT.css +1 -0
  7. package/dist/web/assets/ConnectionsPage-DO0GR9Xg.js +1 -0
  8. package/dist/web/assets/KnowledgeInspector-Bjhdu2mf.css +1 -0
  9. package/dist/web/assets/KnowledgeInspector-DEW5lA1m.js +1 -0
  10. package/dist/web/assets/KnowledgeOrganizerPage-BkM568Ld.js +1 -0
  11. package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +1 -0
  12. package/dist/web/assets/KnowledgePage-BAT4aZIm.js +1 -0
  13. package/dist/web/assets/KnowledgeWorkspace-DyGtBz--.js +4 -0
  14. package/dist/web/assets/OverviewPage-CmCcDCAs.js +1 -0
  15. package/dist/web/assets/PersonalProfilePage-C42mwKN8.js +1 -0
  16. package/dist/web/assets/{PersonalProfilePage-S8ZcG2LU.css → PersonalProfilePage-CdmEzyvZ.css} +1 -1
  17. package/dist/web/assets/PersonalProjectsPage-BwaXR62K.js +1 -0
  18. package/dist/web/assets/PublicProjectsPage-egKBulOq.js +1 -0
  19. package/dist/web/assets/ReviewPage-B3vnZ6gn.js +1 -0
  20. package/dist/web/assets/SearchableSelect-ih69qbcS.js +1 -0
  21. package/dist/web/assets/SettingsPage-C6ouU0sR.css +1 -0
  22. package/dist/web/assets/SettingsPage-oLE8mNs5.js +1 -0
  23. package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
  24. package/dist/web/assets/console-DE40hXGC.js +1 -0
  25. package/dist/web/assets/fuli-logo-Bu9WBuQe.js +1 -0
  26. package/dist/web/assets/fuli-logo-ER1wQ8Kk.png +0 -0
  27. package/dist/web/assets/i18n-CDxBs1sz.js +5 -0
  28. package/dist/web/assets/identity-BCxb1mcy.js +1 -0
  29. package/dist/web/assets/index-BzhNfzU_.js +2 -0
  30. package/dist/web/assets/index-CCOGfZzE.css +1 -0
  31. package/dist/web/assets/model-CCMdt10Q.js +1 -0
  32. package/dist/web/assets/paths-DwHeiGMT.js +1 -0
  33. package/dist/web/assets/useMinimumLoadingDisplay-BpCyBeko.css +1 -0
  34. package/dist/web/assets/useMinimumLoadingDisplay-HdI7XAmu.js +1 -0
  35. package/dist/web/assets/vue-router-Drfl1F8i.js +1 -0
  36. package/dist/web/index.html +9 -6
  37. package/docs/external-knowledge-architecture.md +279 -0
  38. package/examples/external-knowledge/markdown-folder.mjs +235 -0
  39. package/graph-provider/fuli_graph/app.py +57 -1
  40. package/graph-provider/fuli_graph/graph_models.py +3 -0
  41. package/graph-provider/fuli_graph/graph_query.py +62 -16
  42. package/graph-provider/fuli_graph/knowledge_review.py +657 -0
  43. package/graph-provider/fuli_graph/knowledge_review_models.py +133 -0
  44. package/graph-provider/fuli_graph/knowledge_search.py +51 -4
  45. package/graph-provider/fuli_graph/models.py +19 -0
  46. package/graph-provider/fuli_graph/runtime.py +15 -0
  47. package/graph-provider/fuli_graph/store_knowledge.py +2 -1
  48. package/graph-provider/pyproject.toml +1 -1
  49. package/npm-shrinkwrap.json +5 -407
  50. package/package.json +7 -5
  51. package/skills/capturing-session-knowledge/SKILL.md +24 -4
  52. package/skills/flreview/SKILL.md +100 -0
  53. package/skills/flreview/agents/openai.yaml +6 -0
  54. package/src/agent-tools/graph-definitions.js +81 -6
  55. package/src/agent-tools/graph-handlers.js +12 -0
  56. package/src/cli/command-registry.js +3 -3
  57. package/src/cli/local-runtime-command.js +51 -28
  58. package/src/cli/local-runtime-options.js +14 -8
  59. package/src/cli/setup-agent-selection.js +12 -12
  60. package/src/cli/setup-command.js +41 -18
  61. package/src/cli/shell-argument.js +10 -0
  62. package/src/cli/uninstall-command.js +17 -15
  63. package/src/cli/update-command.js +38 -30
  64. package/src/external-knowledge/catalog.js +61 -0
  65. package/src/external-knowledge/conflict-policy.js +58 -0
  66. package/src/external-knowledge/connected-search.js +166 -0
  67. package/src/external-knowledge/connectors/custom.js +133 -0
  68. package/src/external-knowledge/connectors/feishu.js +461 -0
  69. package/src/external-knowledge/connectors/mcp.js +438 -0
  70. package/src/external-knowledge/connectors/notion.js +307 -0
  71. package/src/external-knowledge/connectors/retrieval-api.js +222 -0
  72. package/src/external-knowledge/document-mapping.js +151 -0
  73. package/src/external-knowledge/graph-projection.js +17 -0
  74. package/src/external-knowledge/index.js +8 -0
  75. package/src/external-knowledge/registry.js +91 -0
  76. package/src/external-knowledge/runtime.js +68 -0
  77. package/src/external-knowledge/safe-diagnostic.js +14 -0
  78. package/src/external-knowledge/service.js +811 -0
  79. package/src/graphiti/agent-knowledge-workflows.js +7 -2
  80. package/src/graphiti/federated-application.js +68 -42
  81. package/src/graphiti/knowledge-item-role.js +21 -0
  82. package/src/graphiti/knowledge-review.js +43 -0
  83. package/src/graphiti/project-profile-mapping.js +32 -0
  84. package/src/graphiti/provider-client.js +22 -1
  85. package/src/graphiti/source-marker.js +54 -3
  86. package/src/graphiti/task-knowledge-recall.js +250 -0
  87. package/src/http/api-router.js +23 -108
  88. package/src/http/external-knowledge-api-router.js +104 -0
  89. package/src/http/graph-api-router.js +12 -1
  90. package/src/http/request-policy.js +1 -1
  91. package/src/local-runtime/lifecycle.js +26 -15
  92. package/src/mcp/create-mcp-server.js +4 -1
  93. package/src/mcp/instructions.js +7 -7
  94. package/src/mcp/tool-annotations.js +6 -0
  95. package/src/mcp/tool-result.js +1 -0
  96. package/src/server/application-lifecycle.js +2 -11
  97. package/src/server/blocked-ports.js +11 -0
  98. package/src/server.js +36 -17
  99. package/src/setup/agent-installation-status.js +2 -1
  100. package/src/setup/agents.js +2 -1
  101. package/src/setup/claude-code-config.js +2 -1
  102. package/src/setup/codex-bootstrap.js +12 -7
  103. package/src/setup/container-runtime.js +25 -21
  104. package/src/setup/graph-runtime.js +148 -56
  105. package/src/setup/node-runtime.js +3 -3
  106. package/src/setup/options.js +2 -4
  107. package/src/setup/paths.js +18 -3
  108. package/src/setup/review-skill-adapters.js +49 -0
  109. package/src/setup/setup.js +26 -2
  110. package/src/setup/uninstall.js +4 -2
  111. package/src/system/resource-monitor.js +317 -0
  112. package/src/system/runtime-settings.js +131 -0
  113. package/src/system/system-service.js +119 -0
  114. package/README.en.md +0 -336
  115. package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +0 -1
  116. package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +0 -1
  117. package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +0 -1
  118. package/dist/web/assets/KnowledgeOrganizerPage-CZAF_OlN.css +0 -1
  119. package/dist/web/assets/KnowledgeOrganizerPage-DlvcLwlL.js +0 -1
  120. package/dist/web/assets/KnowledgePage-CNQXd15y.js +0 -1
  121. package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +0 -4
  122. package/dist/web/assets/OverviewPage-ul7g7HXh.js +0 -1
  123. package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +0 -1
  124. package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +0 -1
  125. package/dist/web/assets/PublicProjectsPage-C700sR7z.js +0 -1
  126. package/dist/web/assets/ReviewPage-7MDDUDgA.js +0 -1
  127. package/dist/web/assets/console-B_Ena2DS.js +0 -5
  128. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  129. package/dist/web/assets/identity-CjBwYSMX.js +0 -1
  130. package/dist/web/assets/index-CpLDx3of.css +0 -1
  131. package/dist/web/assets/index-DAzeFJSc.js +0 -2
  132. package/dist/web/assets/model-C-5RVIJ2.js +0 -1
  133. package/dist/web/assets/paths-BkULqmGI.js +0 -1
  134. package/dist/web/assets/vue-router-DnD3hUm7.js +0 -1
  135. package/src/agent/candidate-read-service.js +0 -53
  136. package/src/agent/context-pack-service.js +0 -83
  137. package/src/agent/public-read-policy.js +0 -39
  138. package/src/agent/public-read-service.js +0 -69
  139. package/src/agent/read-projection.js +0 -107
  140. package/src/agent/search-service.js +0 -61
  141. package/src/agent/space-boundary.js +0 -33
  142. package/src/app/bootstrap.js +0 -8
  143. package/src/app/create-application.js +0 -143
  144. package/src/app/state-service.js +0 -38
  145. package/src/candidates.js +0 -78
  146. package/src/classifier.js +0 -63
  147. package/src/cli/command-arguments.js +0 -14
  148. package/src/context-pack.js +0 -206
  149. package/src/extractor.js +0 -77
  150. package/src/fact-history.js +0 -39
  151. package/src/facts/fact-service.js +0 -89
  152. package/src/http/bounded-json-array.js +0 -19
  153. package/src/http/lens-route.js +0 -50
  154. package/src/ingestion.js +0 -161
  155. package/src/lens/lens-correction-service.js +0 -81
  156. package/src/lens/lens-fact-writer.js +0 -85
  157. package/src/lens/lens-input.js +0 -62
  158. package/src/lens/lens-query-projection.js +0 -87
  159. package/src/lens/lens-query-ranking.js +0 -62
  160. package/src/lens/lens-query.js +0 -143
  161. package/src/lens/lens-resource-projection.js +0 -93
  162. package/src/lens/lens-resource-selection.js +0 -45
  163. package/src/lens/lens-resource-service.js +0 -103
  164. package/src/lens/lens-search-query.js +0 -122
  165. package/src/lens/lens-service.js +0 -183
  166. package/src/models.js +0 -44
  167. package/src/observer.js +0 -83
  168. package/src/project-rules.js +0 -33
  169. package/src/publication/outbox-service.js +0 -46
  170. package/src/publication/publication-envelope-schema.js +0 -75
  171. package/src/publication/publication-envelope.js +0 -37
  172. package/src/publication/publication-policy.js +0 -82
  173. package/src/publication/publication-service.js +0 -48
  174. package/src/router.js +0 -58
  175. package/src/runtime-options.js +0 -138
  176. package/src/security/episode-visibility.js +0 -14
  177. package/src/security/fact-visibility.js +0 -15
  178. package/src/security/source-metadata-policy.js +0 -8
  179. package/src/storage/episode-evidence-preview.js +0 -100
  180. package/src/storage/fact-search-order.js +0 -18
  181. package/src/storage/file/episode-evidence.js +0 -37
  182. package/src/storage/file/episode-index.js +0 -59
  183. package/src/storage/file/fact-search.js +0 -60
  184. package/src/storage/file-store.js +0 -437
  185. package/src/storage/migrate.js +0 -128
  186. package/src/storage/migrations/001-initial.sql +0 -83
  187. package/src/storage/migrations/002-storage-integrity.sql +0 -113
  188. package/src/storage/record-validation.js +0 -46
  189. package/src/storage/snapshot-defaults.js +0 -54
  190. package/src/storage/sqlite/candidates-repository.js +0 -84
  191. package/src/storage/sqlite/episode-evidence-repository.js +0 -154
  192. package/src/storage/sqlite/episode-privacy-sql.js +0 -33
  193. package/src/storage/sqlite/episodes-repository.js +0 -52
  194. package/src/storage/sqlite/fact-search-repository.js +0 -162
  195. package/src/storage/sqlite/facts-repository.js +0 -146
  196. package/src/storage/sqlite/imports-repository.js +0 -39
  197. package/src/storage/sqlite/mapper.js +0 -84
  198. package/src/storage/sqlite/open-database.js +0 -15
  199. package/src/storage/sqlite/outbox-repository.js +0 -103
  200. package/src/storage/sqlite/snapshot-repository.js +0 -69
  201. package/src/storage/sqlite/spaces-repository.js +0 -52
  202. package/src/storage/sqlite/subscriptions-repository.js +0 -54
  203. package/src/storage/sqlite-store.js +0 -159
  204. package/src/storage/stable-json.js +0 -24
  205. package/src/storage/store-port.js +0 -20
  206. package/src/store.js +0 -2
@@ -0,0 +1,250 @@
1
+ import { createFuliSourceMarker } from './source-marker.js';
2
+ import { knowledgeItemRole } from './knowledge-item-role.js';
3
+ import { detectSensitiveContent } from '../security/sensitive-content.js';
4
+
5
+ const MAX_QUERIES = 4;
6
+ const MAX_ITEMS_PER_KIND = 3;
7
+ const MAX_ITEM_TEXT = 1000;
8
+
9
+ const RECALL_INTENTS = Object.freeze([
10
+ {
11
+ category: 'release_delivery',
12
+ pattern: /(?:发布|发版|上线|部署|推送|再推|release|publish|push|deploy|ship|rollout|commit|github|\bgit\b)/iu,
13
+ queries: [
14
+ '发布 发版 版本 release publish runbook',
15
+ '推送 提交 push commit GitHub Connector runbook'
16
+ ]
17
+ },
18
+ {
19
+ category: 'access_authentication',
20
+ pattern: /(?:认证|登录|凭据|身份|令牌|密钥|credential|authentication|authorization|login|token|ssh)/iu,
21
+ queries: ['认证 登录 凭据 身份 credential authentication SSH token runbook']
22
+ },
23
+ {
24
+ category: 'artifact_location',
25
+ pattern: /(?:网址|地址|路由|端点|在哪里|在哪儿|哪里|(?:文档|文件).{0,12}(?:位置|路径|在哪)|(?:找|查).{0,12}(?:文档|文件)|\burl\b|route|endpoint|document location|file path|where)/iu,
26
+ queries: ['地址 URL 路由 route endpoint 部署 deployment 文档 document']
27
+ },
28
+ {
29
+ category: 'requirements_architecture_decision',
30
+ pattern: /(?:需求|约束|架构|设计决定|决策|原因|取舍|requirement|constraint|architecture|decision|rationale|tradeoff)/iu,
31
+ queries: ['需求 约束 架构 决策 原因 requirement constraint architecture decision rationale']
32
+ },
33
+ {
34
+ category: 'runbook_method',
35
+ pattern: /(?:方法|方式|流程|步骤|怎么做|如何做|runbook|workflow|procedure|how to)/iu,
36
+ queries: ['方法 方式 流程 步骤 runbook workflow procedure']
37
+ },
38
+ {
39
+ category: 'prior_context',
40
+ pattern: /(?:之前|以前|上次|已经存|保存过|记得|照旧|同样|还是用|不用再|先前|previous|earlier|remember|same as|again)/iu,
41
+ queries: ['先前 方法 规则 决策 runbook previous method decision']
42
+ }
43
+ ]);
44
+
45
+ export const TASK_KNOWLEDGE_RETRIEVAL_GUIDANCE = Object.freeze({
46
+ before_reasking:
47
+ 'Before asking the user to repeat a stable project fact or method, inspect task_knowledge_recall and search Fuli when needed.',
48
+ focused_queries:
49
+ 'Use one to four focused action, artifact, target-system, or identifier queries; never use the full conversational request as the only query.',
50
+ scope:
51
+ 'Search only the exact active personal project, its explicitly inheritable knowledge, and the bounded personal-global profile.'
52
+ });
53
+
54
+ export function planTaskKnowledgeRecall(taskPrompt) {
55
+ if (typeof taskPrompt !== 'string' || !taskPrompt.trim()) {
56
+ return { status: 'not_requested', trigger_categories: [], queries: [] };
57
+ }
58
+ const prompt = singleLine(taskPrompt).slice(0, 8192);
59
+ if (detectSensitiveContent(prompt).restricted) {
60
+ return {
61
+ status: 'suppressed_sensitive',
62
+ trigger_categories: [],
63
+ queries: []
64
+ };
65
+ }
66
+ const matched = RECALL_INTENTS.filter(({ pattern }) => pattern.test(prompt));
67
+ if (!matched.length) {
68
+ return { status: 'not_needed', trigger_categories: [], queries: [] };
69
+ }
70
+ const queries = unique(matched.flatMap(({ queries: values }) => values));
71
+ const distinctive = distinctivePromptQuery(prompt);
72
+ if (distinctive) queries.push(distinctive);
73
+ return {
74
+ status: 'planned',
75
+ trigger_categories: matched.map(({ category }) => category),
76
+ queries: unique(queries).slice(0, MAX_QUERIES)
77
+ };
78
+ }
79
+
80
+ export async function recallTaskKnowledge(application, resolution, taskPrompt) {
81
+ const plan = planTaskKnowledgeRecall(taskPrompt);
82
+ if (plan.status !== 'planned') return recallWithoutSearch(plan.status, plan);
83
+ if (!resolution.personalProjectId) {
84
+ return {
85
+ status: 'project_unresolved',
86
+ trigger_categories: plan.trigger_categories,
87
+ query_count: plan.queries.length,
88
+ guidance: TASK_KNOWLEDGE_RETRIEVAL_GUIDANCE,
89
+ facts: [],
90
+ entities: []
91
+ };
92
+ }
93
+
94
+ const settlements = await Promise.allSettled(plan.queries.map((query) =>
95
+ application.searchKnowledge({
96
+ personalSpaceId: application.config.personal.spaceId,
97
+ personalProjectId: resolution.personalProjectId,
98
+ query,
99
+ limit: 6,
100
+ includeHistorical: false,
101
+ includePending: false,
102
+ agentInvocation: true,
103
+ agentToolName: 'automatic_task_knowledge_recall'
104
+ })
105
+ ));
106
+ const results = settlements
107
+ .filter(({ status }) => status === 'fulfilled')
108
+ .map(({ value }) => value);
109
+ const failedQueryCount = settlements.length - results.length;
110
+ const facts = rankedUnique(
111
+ results.flatMap(({ facts = [] }) => facts),
112
+ 'relationship'
113
+ ).slice(0, MAX_ITEMS_PER_KIND).map(compactFact);
114
+ const entities = rankedUnique(
115
+ results.flatMap(({ entities = [] }) => entities),
116
+ 'entity'
117
+ ).slice(0, MAX_ITEMS_PER_KIND).map(compactEntity);
118
+ const sourceMarker = createFuliSourceMarker({
119
+ consoleUrl: application.consoleUrl,
120
+ facts,
121
+ entities,
122
+ projectScopePriority: 'strict'
123
+ });
124
+ const searched = results.length > 0;
125
+ return {
126
+ status: facts.length || entities.length
127
+ ? 'matched'
128
+ : failedQueryCount === settlements.length ? 'unavailable' : 'no_match',
129
+ partial: failedQueryCount > 0,
130
+ failed_query_count: failedQueryCount,
131
+ trigger_categories: plan.trigger_categories,
132
+ query_count: plan.queries.length,
133
+ guidance: TASK_KNOWLEDGE_RETRIEVAL_GUIDANCE,
134
+ ...(searched ? {
135
+ sourceMarker,
136
+ noMatchSourceMarker: createFuliSourceMarker({
137
+ consoleUrl: application.consoleUrl
138
+ })
139
+ } : {}),
140
+ facts,
141
+ entities
142
+ };
143
+ }
144
+
145
+ function recallWithoutSearch(status, plan) {
146
+ return {
147
+ status,
148
+ trigger_categories: plan.trigger_categories,
149
+ query_count: plan.queries.length,
150
+ guidance: TASK_KNOWLEDGE_RETRIEVAL_GUIDANCE,
151
+ facts: [],
152
+ entities: []
153
+ };
154
+ }
155
+
156
+ function rankedUnique(items, itemKind) {
157
+ const selected = new Map();
158
+ for (const item of items) {
159
+ const key = `${itemKind}:${item.id}`;
160
+ const current = selected.get(key);
161
+ if (!current || compareRecallItems(item, current) < 0) selected.set(key, item);
162
+ }
163
+ return [...selected.values()].sort(compareRecallItems);
164
+ }
165
+
166
+ function compareRecallItems(left, right) {
167
+ const leftLocal = left.defined_project_id ? 1 : 0;
168
+ const rightLocal = right.defined_project_id ? 1 : 0;
169
+ if (leftLocal !== rightLocal) return rightLocal - leftLocal;
170
+ const leftDistance = Number(left.scope_distance ?? 0);
171
+ const rightDistance = Number(right.scope_distance ?? 0);
172
+ if (leftDistance !== rightDistance) return leftDistance - rightDistance;
173
+ const leftSupporting = knowledgeItemRole(left) === 'supporting' ? 1 : 0;
174
+ const rightSupporting = knowledgeItemRole(right) === 'supporting' ? 1 : 0;
175
+ if (leftSupporting !== rightSupporting) return leftSupporting - rightSupporting;
176
+ return Number(right.score ?? 0) - Number(left.score ?? 0);
177
+ }
178
+
179
+ function compactFact(fact) {
180
+ return compactItem(fact, {
181
+ source_entity: fact.source_entity,
182
+ target_entity: fact.target_entity,
183
+ relationship: fact.relationship,
184
+ fact: boundedText(fact.fact)
185
+ });
186
+ }
187
+
188
+ function compactEntity(entity) {
189
+ return compactItem(entity, {
190
+ name: entity.name,
191
+ type: entity.type,
192
+ summary: boundedText(entity.summary)
193
+ });
194
+ }
195
+
196
+ function compactItem(item, content) {
197
+ return {
198
+ id: item.id,
199
+ space_id: item.space_id ?? item.spaceId,
200
+ scope: item.scope,
201
+ key: item.key ?? null,
202
+ defined_project_id: item.defined_project_id ?? null,
203
+ inherited_from_project_id: item.inherited_from_project_id ?? null,
204
+ scope_distance: item.scope_distance ?? 0,
205
+ confirmation_status: item.confirmation_status ?? null,
206
+ source_uris: item.source_uris ?? [],
207
+ score: item.score ?? null,
208
+ ...content
209
+ };
210
+ }
211
+
212
+ function distinctivePromptQuery(prompt) {
213
+ const quoted = [...prompt.matchAll(/[“”"']([^“”"']{2,80})[“”"']/gu)]
214
+ .map((match) => match[1]);
215
+ const versions = prompt.match(
216
+ /\bv?\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?\b/gu
217
+ ) ?? [];
218
+ const identifiers = prompt.match(
219
+ /\b[A-Za-z][A-Za-z0-9]{0,63}[_./-][A-Za-z0-9_.\/-]{1,127}\b/gu
220
+ ) ?? [];
221
+ const urls = [...prompt.matchAll(/https?:\/\/[^\s<>()]+/gu)]
222
+ .map((match) => safeUrlSearchTerm(match[0]))
223
+ .filter(Boolean);
224
+ return unique([...quoted, ...versions, ...identifiers, ...urls])
225
+ .slice(0, 4)
226
+ .join(' ')
227
+ .slice(0, 180);
228
+ }
229
+
230
+ function safeUrlSearchTerm(value) {
231
+ try {
232
+ const url = new URL(value);
233
+ if (url.username || url.password) return null;
234
+ return `${url.hostname}${url.pathname}`.slice(0, 120);
235
+ } catch {
236
+ return null;
237
+ }
238
+ }
239
+
240
+ function unique(values) {
241
+ return [...new Set(values.filter(Boolean))];
242
+ }
243
+
244
+ function singleLine(value) {
245
+ return String(value).replace(/[\r\n\t]+/gu, ' ').replace(/\s+/gu, ' ').trim();
246
+ }
247
+
248
+ function boundedText(value) {
249
+ return singleLine(value ?? '').slice(0, MAX_ITEM_TEXT);
250
+ }
@@ -1,8 +1,15 @@
1
1
  import { readJson, sendJson } from './response.js';
2
- import { handleLensRequest } from './lens-route.js';
2
+ import { handleExternalKnowledgeApiRequest } from './external-knowledge-api-router.js';
3
3
  import { handleGraphApiRequest } from './graph-api-router.js';
4
4
 
5
- export async function handleApiRequest({ request, response, app }) {
5
+ export async function handleApiRequest({
6
+ request,
7
+ response,
8
+ app,
9
+ system = null,
10
+ externalKnowledge = null,
11
+ connectedKnowledge = null
12
+ }) {
6
13
  const url = new URL(request.url, 'http://127.0.0.1');
7
14
 
8
15
  if (url.pathname === '/api/health' && request.method === 'GET') {
@@ -14,120 +21,28 @@ export async function handleApiRequest({ request, response, app }) {
14
21
  return true;
15
22
  }
16
23
 
17
- if (app.graphiti) {
18
- return handleGraphApiRequest({ request, response, url, app });
19
- }
20
-
21
- if (handleLensRequest({ request, response, url, app })) return true;
22
-
23
- if (url.pathname === '/api/state' && request.method === 'GET') {
24
- sendJson(response, 200, app.state());
25
- return true;
26
- }
27
-
28
- if (url.pathname === '/api/spaces' && request.method === 'POST') {
29
- const body = await readJson(request);
30
- const space = app.createSpace(body.name, body.kind, body.description ?? null);
31
- sendJson(response, 200, { space });
32
- return true;
33
- }
34
-
35
- if (url.pathname === '/api/bootstrap' && request.method === 'POST') {
36
- sendJson(response, 200, app.bootstrap());
37
- return true;
38
- }
39
-
40
- if (url.pathname === '/api/subscriptions' && request.method === 'POST') {
41
- const body = await readJson(request);
42
- const subscription = app.subscribe(
43
- body.personalSpaceId,
44
- body.spaceId,
45
- body.mode ?? 'latest'
46
- );
47
- sendJson(response, 200, { subscription });
48
- return true;
49
- }
50
-
51
- if (url.pathname === '/api/remember' && request.method === 'POST') {
52
- const body = await readJson(request);
53
- const result = app.remember({
54
- personalSpaceId: app.requireSpaceId({
55
- id: body.personalSpaceId,
56
- name: body.personalSpaceName,
57
- label: 'Personal space'
58
- }),
59
- targetSpaceId: optionalSpaceId(app, {
60
- id: body.targetSpaceId,
61
- name: body.targetSpaceName,
62
- label: 'Target space'
63
- }),
64
- sourceKind: body.sourceKind ?? 'agent',
65
- body: body.body,
66
- sourceUri: body.sourceUri ?? null
67
- });
68
- sendJson(response, 200, result);
69
- return true;
70
- }
71
-
72
- if (url.pathname === '/api/observe/git-diff' && request.method === 'POST') {
73
- const body = await readJson(request);
74
- const result = app.observe({
75
- personalSpaceId: body.personalSpaceId,
76
- targetSpaceId: body.targetSpaceId || null,
77
- cwd: body.cwd ?? process.cwd()
78
- });
79
- sendJson(response, 200, result);
80
- return true;
81
- }
82
-
83
- const candidateDecisionMatch = url.pathname.match(/^\/api\/candidates\/([^/]+)\/decision$/);
84
- if (candidateDecisionMatch && request.method === 'POST') {
85
- const body = await readJson(request);
86
- const candidate = app.decideCandidate(candidateDecisionMatch[1], body.decision);
87
- sendJson(response, 200, { candidate });
24
+ if (system && url.pathname === '/api/system/settings' && request.method === 'GET') {
25
+ sendJson(response, 200, system.getSettings());
88
26
  return true;
89
27
  }
90
28
 
91
- if (url.pathname === '/api/search' && request.method === 'GET') {
92
- const result = app.search({
93
- personalSpaceId: requiredSpaceId(app, {
94
- id: url.searchParams.get('personalSpaceId'),
95
- name: url.searchParams.get('personalSpaceName'),
96
- label: 'Personal space'
97
- }),
98
- query: url.searchParams.get('q') ?? '',
99
- includeHistorical: url.searchParams.get('historical') === 'true'
100
- });
101
- sendJson(response, 200, result);
29
+ if (system && url.pathname === '/api/system/settings' && request.method === 'PUT') {
30
+ sendJson(response, 200, system.updateSettings(await readJson(request)));
102
31
  return true;
103
32
  }
104
33
 
105
- if (url.pathname === '/api/context-pack' && request.method === 'GET') {
106
- const result = app.contextPack({
107
- personalSpaceId: requiredSpaceId(app, {
108
- id: url.searchParams.get('personalSpaceId'),
109
- name: url.searchParams.get('personalSpaceName'),
110
- label: 'Personal space'
111
- }),
112
- spaceId: requiredSpaceId(app, {
113
- id: url.searchParams.get('spaceId'),
114
- name: url.searchParams.get('spaceName'),
115
- label: 'Space'
116
- }),
117
- query: url.searchParams.get('q') ?? ''
118
- });
119
- sendJson(response, 200, result);
34
+ if (system && url.pathname === '/api/system/resources' && request.method === 'GET') {
35
+ sendJson(response, 200, await system.resources());
120
36
  return true;
121
37
  }
122
38
 
123
- return false;
124
- }
125
-
126
- function requiredSpaceId(app, input) {
127
- return app.requireSpaceId(input);
128
- }
39
+ if (await handleExternalKnowledgeApiRequest({
40
+ request,
41
+ response,
42
+ url,
43
+ externalKnowledge,
44
+ connectedKnowledge
45
+ })) return true;
129
46
 
130
- function optionalSpaceId(app, input) {
131
- if (!input.id && !input.name) return null;
132
- return requiredSpaceId(app, input);
47
+ return handleGraphApiRequest({ request, response, url, app });
133
48
  }
@@ -0,0 +1,104 @@
1
+ import { readJson, sendJson } from './response.js';
2
+
3
+ export async function handleExternalKnowledgeApiRequest({
4
+ request,
5
+ response,
6
+ url,
7
+ externalKnowledge,
8
+ connectedKnowledge
9
+ }) {
10
+ if (!externalKnowledge && !connectedKnowledge) return false;
11
+
12
+ if (externalKnowledge &&
13
+ url.pathname === '/api/external-knowledge/connectors' &&
14
+ request.method === 'GET') {
15
+ sendJson(response, 200, externalKnowledge.listConnectorTypes());
16
+ return true;
17
+ }
18
+ if (externalKnowledge &&
19
+ url.pathname === '/api/external-knowledge/discover' &&
20
+ request.method === 'POST') {
21
+ sendJson(response, 200, await externalKnowledge.discover(await readJson(request)));
22
+ return true;
23
+ }
24
+ if (externalKnowledge &&
25
+ url.pathname === '/api/external-knowledge/bindings' &&
26
+ request.method === 'GET') {
27
+ sendJson(response, 200, await externalKnowledge.listBindings());
28
+ return true;
29
+ }
30
+ if (externalKnowledge &&
31
+ url.pathname === '/api/external-knowledge/bindings' &&
32
+ request.method === 'POST') {
33
+ sendJson(response, 201, await externalKnowledge.createBinding(await readJson(request)));
34
+ return true;
35
+ }
36
+
37
+ const action = url.pathname.match(
38
+ /^\/api\/external-knowledge\/bindings\/([^/]+)\/(check|sync|retrieve)$/
39
+ );
40
+ if (externalKnowledge && action && request.method === 'POST') {
41
+ const id = decodeURIComponent(action[1]);
42
+ const body = await readJson(request);
43
+ const result = action[2] === 'check'
44
+ ? await externalKnowledge.checkBinding(id)
45
+ : action[2] === 'sync'
46
+ ? await externalKnowledge.syncBinding(id, body)
47
+ : await externalKnowledge.retrieveBinding(id, body);
48
+ sendJson(response, 200, result);
49
+ return true;
50
+ }
51
+
52
+ const targets = url.pathname.match(
53
+ /^\/api\/external-knowledge\/bindings\/([^/]+)\/targets$/
54
+ );
55
+ if (externalKnowledge && targets && request.method === 'PATCH') {
56
+ sendJson(
57
+ response,
58
+ 200,
59
+ await externalKnowledge.updateBindingTargets(
60
+ decodeURIComponent(targets[1]),
61
+ await readJson(request)
62
+ )
63
+ );
64
+ return true;
65
+ }
66
+
67
+ const binding = url.pathname.match(
68
+ /^\/api\/external-knowledge\/bindings\/([^/]+)$/
69
+ );
70
+ if (externalKnowledge && binding && request.method === 'DELETE') {
71
+ sendJson(
72
+ response,
73
+ 200,
74
+ await externalKnowledge.deleteBinding(decodeURIComponent(binding[1]))
75
+ );
76
+ return true;
77
+ }
78
+
79
+ if (connectedKnowledge &&
80
+ url.pathname === '/api/external-knowledge/conflict-policy' &&
81
+ request.method === 'GET') {
82
+ sendJson(response, 200, connectedKnowledge.getConflictPolicy({
83
+ personalProjectId: url.searchParams.get('personalProjectId')
84
+ }));
85
+ return true;
86
+ }
87
+ if (connectedKnowledge &&
88
+ url.pathname === '/api/external-knowledge/conflict-policy' &&
89
+ request.method === 'PATCH') {
90
+ sendJson(
91
+ response,
92
+ 200,
93
+ await connectedKnowledge.updateConflictPolicy(await readJson(request))
94
+ );
95
+ return true;
96
+ }
97
+ if (connectedKnowledge &&
98
+ url.pathname === '/api/connected-knowledge/search' &&
99
+ request.method === 'POST') {
100
+ sendJson(response, 200, await connectedKnowledge.query(await readJson(request)));
101
+ return true;
102
+ }
103
+ return false;
104
+ }
@@ -44,7 +44,8 @@ export async function handleGraphApiRequest({ request, response, url, app }) {
44
44
  spaceId: url.searchParams.get('spaceId'),
45
45
  providerUrl: url.searchParams.get('providerUrl'),
46
46
  personalProjectId: url.searchParams.get('personalProjectId'),
47
- limit: numberParam(url, 'limit', 500)
47
+ limit: numberParam(url, 'limit', 500),
48
+ offset: nonNegativeNumberParam(url, 'offset', null)
48
49
  }));
49
50
  return true;
50
51
  }
@@ -255,3 +256,13 @@ function numberParam(url, name, fallback) {
255
256
  }
256
257
  return value;
257
258
  }
259
+
260
+ function nonNegativeNumberParam(url, name, fallback) {
261
+ const raw = url.searchParams.get(name);
262
+ if (raw === null) return fallback;
263
+ const value = Number(raw);
264
+ if (!Number.isInteger(value) || value < 0) {
265
+ throw new TypeError(`${name} must be a non-negative integer`);
266
+ }
267
+ return value;
268
+ }
@@ -65,7 +65,7 @@ function isLoopbackAddress(address) {
65
65
  }
66
66
 
67
67
  function requiresJson(request) {
68
- if (request.method !== 'POST') return false;
68
+ if (!['PATCH', 'POST', 'PUT'].includes(request.method)) return false;
69
69
  const pathname = new URL(request.url, 'http://127.0.0.1').pathname;
70
70
  return pathname.startsWith('/api/');
71
71
  }
@@ -4,16 +4,22 @@ import { existsSync, unlinkSync } from 'node:fs';
4
4
  import {
5
5
  checkLocalConsoleHealth,
6
6
  ensureGraphRuntime,
7
- MANAGED_PERSONAL_PROVIDER_URL,
8
- MANAGED_WORKSPACE_PROVIDER_URL,
9
7
  stopGraphProviders
10
8
  } from '../setup/graph-runtime.js';
11
9
  import { readJsonFile } from '../storage/json-file.js';
10
+ import {
11
+ DEFAULT_RUNTIME_SETTINGS,
12
+ managedProviderUrls
13
+ } from '../system/runtime-settings.js';
12
14
 
13
15
  export async function startLocalRuntime(input, dependencies = {}) {
14
16
  const deps = lifecycleDependencies(dependencies);
17
+ const runtimeSettings = input.runtimeSettings ?? DEFAULT_RUNTIME_SETTINGS;
15
18
  const config = deps.readConfig(input.paths.graphRuntimeConfigPath);
16
- const managesDevelopmentWorkspace = hasManagedDevelopmentWorkspace(config);
19
+ const managesDevelopmentWorkspace = hasManagedDevelopmentWorkspace(
20
+ config,
21
+ runtimeSettings
22
+ );
17
23
  const buildProviders = input.rebuild === true || !config ||
18
24
  !deps.fileExists(input.paths.graphEnvPath);
19
25
  const runtime = await deps.ensureRuntime({
@@ -21,6 +27,7 @@ export async function startLocalRuntime(input, dependencies = {}) {
21
27
  personalSpaceName: input.personalSpaceName,
22
28
  port: input.port,
23
29
  lan: input.lan === true,
30
+ runtimeSettings,
24
31
  personalOnly: !managesDevelopmentWorkspace,
25
32
  buildProviders,
26
33
  noStart: false,
@@ -37,12 +44,13 @@ export async function startLocalRuntime(input, dependencies = {}) {
37
44
 
38
45
  export async function stopLocalRuntime(input, dependencies = {}) {
39
46
  const deps = lifecycleDependencies(dependencies);
47
+ const runtimeSettings = input.runtimeSettings ?? DEFAULT_RUNTIME_SETTINGS;
40
48
  const config = deps.readConfig(input.paths.graphRuntimeConfigPath);
41
49
  const state = deps.readState(input.paths.graphRuntimeStatePath);
42
50
  const consoleResult = await stopRecordedConsole(state, input.paths, deps);
43
51
  const managesDevelopmentWorkspace = state?.managedProviders?.includes(
44
52
  'development-workspace'
45
- ) || hasManagedDevelopmentWorkspace(config);
53
+ ) || hasManagedDevelopmentWorkspace(config, runtimeSettings);
46
54
 
47
55
  let providers = 'not_initialized';
48
56
  if (deps.fileExists(input.paths.graphEnvPath)) {
@@ -64,7 +72,7 @@ export async function stopLocalRuntime(input, dependencies = {}) {
64
72
  export async function restartLocalRuntime(input, dependencies = {}) {
65
73
  const stopped = await stopLocalRuntime(input, dependencies);
66
74
  if (stopped.status === 'partial') {
67
- throw new Error('无法安全确认原本地界面的进程身份,已拒绝重启。');
75
+ throw new Error('Could not safely verify the existing local UI process; restart was refused.');
68
76
  }
69
77
  const started = await startLocalRuntime(input, dependencies);
70
78
  return { stopped, ...started, status: 'restarted' };
@@ -72,6 +80,7 @@ export async function restartLocalRuntime(input, dependencies = {}) {
72
80
 
73
81
  export async function inspectLocalRuntime(input, dependencies = {}) {
74
82
  const deps = lifecycleDependencies(dependencies);
83
+ const runtimeSettings = input.runtimeSettings ?? DEFAULT_RUNTIME_SETTINGS;
75
84
  const config = deps.readConfig(input.paths.graphRuntimeConfigPath);
76
85
  const state = deps.readState(input.paths.graphRuntimeStatePath);
77
86
  const stateLooksOwned = isRuntimeState(state) && isLoopbackConsoleUrl(state.url);
@@ -81,13 +90,14 @@ export async function inspectLocalRuntime(input, dependencies = {}) {
81
90
  state.pid,
82
91
  state.version
83
92
  );
84
- const personalUrl = config?.personal?.providerUrl ?? MANAGED_PERSONAL_PROVIDER_URL;
93
+ const managedUrls = managedProviderUrls(runtimeSettings);
94
+ const personalUrl = config?.personal?.providerUrl ?? managedUrls.personal;
85
95
  const personal = config
86
96
  ? await deps.providerHealth(personalUrl)
87
97
  : { url: personalUrl, status: 'not_configured' };
88
98
  const workspaces = await Promise.all((config?.workspaces ?? []).map(async (workspace) => ({
89
99
  ...(await deps.providerHealth(workspace.providerUrl)),
90
- managedDevelopment: isManagedDevelopmentUrl(workspace.providerUrl)
100
+ managedDevelopment: isManagedDevelopmentUrl(workspace, runtimeSettings)
91
101
  })));
92
102
 
93
103
  let status = 'stopped';
@@ -120,7 +130,7 @@ export async function openLocalConsole(input, dependencies = {}) {
120
130
  if (!isRuntimeState(state) || !isLoopbackConsoleUrl(state.url) ||
121
131
  !deps.isProcessAlive(state.pid) ||
122
132
  !await deps.consoleHealth(state.url, state.pid, state.version)) {
123
- throw new Error('本地界面尚未运行,请先执行 fl start。');
133
+ throw new Error('The local UI is not running. Run fl start first.');
124
134
  }
125
135
  await deps.openExternal(state.url);
126
136
  return { status: 'opened', url: state.url };
@@ -201,17 +211,18 @@ export function externalOpenInvocation(url, platform = process.platform) {
201
211
  return { command: 'xdg-open', args: [url] };
202
212
  }
203
213
 
204
- function hasManagedDevelopmentWorkspace(config) {
205
- return (config?.workspaces ?? []).some(({ providerUrl }) =>
206
- isManagedDevelopmentUrl(providerUrl));
214
+ function hasManagedDevelopmentWorkspace(config, runtimeSettings) {
215
+ return (config?.workspaces ?? []).some((workspace) =>
216
+ isManagedDevelopmentUrl(workspace, runtimeSettings));
207
217
  }
208
218
 
209
- function isManagedDevelopmentUrl(value) {
219
+ function isManagedDevelopmentUrl(workspace, runtimeSettings) {
220
+ if (workspace?.managedDevelopment === true) return true;
210
221
  try {
211
- const url = new URL(value);
222
+ const url = new URL(workspace?.providerUrl);
212
223
  return url.protocol === 'http:' &&
213
224
  ['127.0.0.1', 'localhost', '::1', '[::1]'].includes(url.hostname) &&
214
- url.port === new URL(MANAGED_WORKSPACE_PROVIDER_URL).port;
225
+ [8788, runtimeSettings.ports.workspaceProvider].includes(Number(url.port));
215
226
  } catch {
216
227
  return false;
217
228
  }
@@ -237,7 +248,7 @@ function publicProviderStatus(workspaces) {
237
248
  }
238
249
 
239
250
  function isRuntimeState(state) {
240
- return (state?.version === 2 || state?.version === 3) &&
251
+ return (state?.version === 2 || state?.version === 3 || state?.version === 4) &&
241
252
  Number.isInteger(state.pid) && typeof state.url === 'string';
242
253
  }
243
254
 
@@ -20,8 +20,11 @@ const TOOL_RESULT_LIMIT_BYTES = Object.freeze({
20
20
  begin_task_context: 16 * 1024,
21
21
  get_collaboration_preferences: 16 * 1024,
22
22
  search_knowledge_graph: 32 * 1024,
23
+ search_connected_knowledge: 64 * 1024,
23
24
  search_current_project_knowledge: 64 * 1024,
24
- discover_common_knowledge_candidates: 32 * 1024
25
+ discover_common_knowledge_candidates: 32 * 1024,
26
+ list_personal_projects: 32 * 1024,
27
+ list_knowledge_review_candidates: 32 * 1024
25
28
  });
26
29
 
27
30
  export function createMcpServer(app) {