fuli-context 0.4.0 → 0.7.2

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 (207) hide show
  1. package/README.en.md +5 -0
  2. package/README.md +411 -173
  3. package/README.zh-CN.md +448 -0
  4. package/compose.graphiti.yml +6 -6
  5. package/dist/web/assets/AboutPage-DXh7Fwxt.css +1 -0
  6. package/dist/web/assets/AboutPage-DfnZo7Lo.js +1 -0
  7. package/dist/web/assets/ConnectionsPage-BloaSKmt.js +1 -0
  8. package/dist/web/assets/ConnectionsPage-CBHAdZXT.css +1 -0
  9. package/dist/web/assets/KnowledgeInspector-Bjhdu2mf.css +1 -0
  10. package/dist/web/assets/KnowledgeInspector-QvNpv0_k.js +1 -0
  11. package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +1 -0
  12. package/dist/web/assets/KnowledgeOrganizerPage-DKq1jVD5.js +1 -0
  13. package/dist/web/assets/KnowledgePage-BWL0jT01.js +1 -0
  14. package/dist/web/assets/KnowledgeWorkspace-Czq6aZ7b.js +4 -0
  15. package/dist/web/assets/OverviewPage-5sJoO1Gh.js +1 -0
  16. package/dist/web/assets/{PersonalProfilePage-S8ZcG2LU.css → PersonalProfilePage-CdmEzyvZ.css} +1 -1
  17. package/dist/web/assets/PersonalProfilePage-tVChM9ik.js +1 -0
  18. package/dist/web/assets/PersonalProjectsPage-BPFrup4V.js +1 -0
  19. package/dist/web/assets/PublicProjectsPage-BPJSeREN.js +1 -0
  20. package/dist/web/assets/ReviewPage-B7EN817i.js +1 -0
  21. package/dist/web/assets/SearchableSelect-ClLN0rIb.js +1 -0
  22. package/dist/web/assets/SettingsPage-C6ouU0sR.css +1 -0
  23. package/dist/web/assets/SettingsPage-Chd_B3eH.js +1 -0
  24. package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
  25. package/dist/web/assets/console-CbDsKrsO.js +1 -0
  26. package/dist/web/assets/fuli-logo-Bu9WBuQe.js +1 -0
  27. package/dist/web/assets/fuli-logo-ER1wQ8Kk.png +0 -0
  28. package/dist/web/assets/i18n-BQcns1PH.js +5 -0
  29. package/dist/web/assets/identity-B8K4VYg6.js +1 -0
  30. package/dist/web/assets/index-CB8Xmsc2.js +2 -0
  31. package/dist/web/assets/index-CZeuPh-U.css +1 -0
  32. package/dist/web/assets/model-eF52GgPF.js +1 -0
  33. package/dist/web/assets/paths-hwz-Ac1g.js +1 -0
  34. package/dist/web/assets/useMinimumLoadingDisplay-BpCyBeko.css +1 -0
  35. package/dist/web/assets/useMinimumLoadingDisplay-wCgGDmHg.js +1 -0
  36. package/dist/web/assets/vue-router-BsEp_XwI.js +1 -0
  37. package/dist/web/index.html +9 -6
  38. package/docs/external-knowledge-architecture.md +279 -0
  39. package/examples/external-knowledge/markdown-folder.mjs +235 -0
  40. package/graph-provider/fuli_graph/app.py +57 -1
  41. package/graph-provider/fuli_graph/graph_models.py +3 -0
  42. package/graph-provider/fuli_graph/graph_query.py +62 -16
  43. package/graph-provider/fuli_graph/knowledge_review.py +612 -0
  44. package/graph-provider/fuli_graph/knowledge_review_models.py +127 -0
  45. package/graph-provider/fuli_graph/knowledge_search.py +51 -4
  46. package/graph-provider/fuli_graph/models.py +19 -0
  47. package/graph-provider/fuli_graph/runtime.py +15 -0
  48. package/graph-provider/fuli_graph/store_knowledge.py +2 -1
  49. package/graph-provider/pyproject.toml +1 -1
  50. package/npm-shrinkwrap.json +5 -407
  51. package/package.json +7 -4
  52. package/skills/capturing-session-knowledge/SKILL.md +24 -4
  53. package/skills/flreview/SKILL.md +73 -0
  54. package/skills/flreview/agents/openai.yaml +6 -0
  55. package/src/agent-tools/graph-definitions.js +79 -4
  56. package/src/agent-tools/graph-handlers.js +12 -0
  57. package/src/cli/command-registry.js +3 -3
  58. package/src/cli/local-runtime-command.js +60 -23
  59. package/src/cli/local-runtime-options.js +16 -6
  60. package/src/cli/setup-agent-selection.js +12 -12
  61. package/src/cli/setup-command.js +41 -18
  62. package/src/cli/shell-argument.js +10 -0
  63. package/src/cli/uninstall-command.js +17 -15
  64. package/src/cli/update-command.js +38 -30
  65. package/src/external-knowledge/catalog.js +61 -0
  66. package/src/external-knowledge/conflict-policy.js +58 -0
  67. package/src/external-knowledge/connected-search.js +166 -0
  68. package/src/external-knowledge/connectors/custom.js +133 -0
  69. package/src/external-knowledge/connectors/feishu.js +461 -0
  70. package/src/external-knowledge/connectors/mcp.js +438 -0
  71. package/src/external-knowledge/connectors/notion.js +307 -0
  72. package/src/external-knowledge/connectors/retrieval-api.js +222 -0
  73. package/src/external-knowledge/document-mapping.js +151 -0
  74. package/src/external-knowledge/graph-projection.js +17 -0
  75. package/src/external-knowledge/index.js +8 -0
  76. package/src/external-knowledge/registry.js +91 -0
  77. package/src/external-knowledge/runtime.js +68 -0
  78. package/src/external-knowledge/safe-diagnostic.js +14 -0
  79. package/src/external-knowledge/service.js +811 -0
  80. package/src/graphiti/agent-knowledge-workflows.js +7 -2
  81. package/src/graphiti/federated-application.js +66 -41
  82. package/src/graphiti/knowledge-item-role.js +21 -0
  83. package/src/graphiti/knowledge-review.js +43 -0
  84. package/src/graphiti/project-profile-mapping.js +32 -0
  85. package/src/graphiti/provider-client.js +22 -1
  86. package/src/graphiti/source-marker.js +54 -3
  87. package/src/graphiti/task-knowledge-recall.js +250 -0
  88. package/src/http/api-router.js +23 -108
  89. package/src/http/external-knowledge-api-router.js +104 -0
  90. package/src/http/graph-api-router.js +12 -1
  91. package/src/http/request-policy.js +38 -3
  92. package/src/local-runtime/lifecycle.js +37 -16
  93. package/src/mcp/create-mcp-server.js +1 -0
  94. package/src/mcp/instructions.js +7 -7
  95. package/src/mcp/tool-annotations.js +6 -0
  96. package/src/mcp/tool-result.js +1 -0
  97. package/src/server/application-lifecycle.js +2 -11
  98. package/src/server/blocked-ports.js +11 -0
  99. package/src/server/lan-access.js +52 -0
  100. package/src/server.js +103 -25
  101. package/src/setup/agent-installation-status.js +2 -1
  102. package/src/setup/agents.js +2 -1
  103. package/src/setup/claude-code-config.js +2 -1
  104. package/src/setup/codex-bootstrap.js +12 -7
  105. package/src/setup/container-runtime.js +25 -21
  106. package/src/setup/graph-runtime.js +219 -58
  107. package/src/setup/node-runtime.js +3 -3
  108. package/src/setup/options.js +2 -4
  109. package/src/setup/paths.js +18 -3
  110. package/src/setup/review-skill-adapters.js +49 -0
  111. package/src/setup/setup.js +26 -2
  112. package/src/setup/uninstall.js +4 -2
  113. package/src/system/resource-monitor.js +317 -0
  114. package/src/system/runtime-settings.js +131 -0
  115. package/src/system/system-service.js +119 -0
  116. package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +0 -1
  117. package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +0 -1
  118. package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +0 -1
  119. package/dist/web/assets/KnowledgeOrganizerPage-CJCNI2Wd.css +0 -1
  120. package/dist/web/assets/KnowledgeOrganizerPage-PXH-TlGt.js +0 -1
  121. package/dist/web/assets/KnowledgePage-CNQXd15y.js +0 -1
  122. package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +0 -4
  123. package/dist/web/assets/OverviewPage-ul7g7HXh.js +0 -1
  124. package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +0 -1
  125. package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +0 -1
  126. package/dist/web/assets/PublicProjectsPage-C700sR7z.js +0 -1
  127. package/dist/web/assets/ReviewPage-7MDDUDgA.js +0 -1
  128. package/dist/web/assets/console-B_Ena2DS.js +0 -5
  129. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  130. package/dist/web/assets/identity-CjBwYSMX.js +0 -1
  131. package/dist/web/assets/index-BQRkOwPx.js +0 -2
  132. package/dist/web/assets/index-CpLDx3of.css +0 -1
  133. package/dist/web/assets/model-C-5RVIJ2.js +0 -1
  134. package/dist/web/assets/paths-BkULqmGI.js +0 -1
  135. package/dist/web/assets/vue-router-DnD3hUm7.js +0 -1
  136. package/src/agent/candidate-read-service.js +0 -53
  137. package/src/agent/context-pack-service.js +0 -83
  138. package/src/agent/public-read-policy.js +0 -39
  139. package/src/agent/public-read-service.js +0 -69
  140. package/src/agent/read-projection.js +0 -107
  141. package/src/agent/search-service.js +0 -61
  142. package/src/agent/space-boundary.js +0 -33
  143. package/src/app/bootstrap.js +0 -8
  144. package/src/app/create-application.js +0 -143
  145. package/src/app/state-service.js +0 -38
  146. package/src/candidates.js +0 -78
  147. package/src/classifier.js +0 -63
  148. package/src/cli/command-arguments.js +0 -14
  149. package/src/context-pack.js +0 -206
  150. package/src/extractor.js +0 -77
  151. package/src/fact-history.js +0 -39
  152. package/src/facts/fact-service.js +0 -89
  153. package/src/http/bounded-json-array.js +0 -19
  154. package/src/http/lens-route.js +0 -50
  155. package/src/ingestion.js +0 -161
  156. package/src/lens/lens-correction-service.js +0 -81
  157. package/src/lens/lens-fact-writer.js +0 -85
  158. package/src/lens/lens-input.js +0 -62
  159. package/src/lens/lens-query-projection.js +0 -87
  160. package/src/lens/lens-query-ranking.js +0 -62
  161. package/src/lens/lens-query.js +0 -143
  162. package/src/lens/lens-resource-projection.js +0 -93
  163. package/src/lens/lens-resource-selection.js +0 -45
  164. package/src/lens/lens-resource-service.js +0 -103
  165. package/src/lens/lens-search-query.js +0 -122
  166. package/src/lens/lens-service.js +0 -183
  167. package/src/models.js +0 -44
  168. package/src/observer.js +0 -83
  169. package/src/project-rules.js +0 -33
  170. package/src/publication/outbox-service.js +0 -46
  171. package/src/publication/publication-envelope-schema.js +0 -75
  172. package/src/publication/publication-envelope.js +0 -37
  173. package/src/publication/publication-policy.js +0 -82
  174. package/src/publication/publication-service.js +0 -48
  175. package/src/router.js +0 -58
  176. package/src/runtime-options.js +0 -138
  177. package/src/security/episode-visibility.js +0 -14
  178. package/src/security/fact-visibility.js +0 -15
  179. package/src/security/source-metadata-policy.js +0 -8
  180. package/src/storage/episode-evidence-preview.js +0 -100
  181. package/src/storage/fact-search-order.js +0 -18
  182. package/src/storage/file/episode-evidence.js +0 -37
  183. package/src/storage/file/episode-index.js +0 -59
  184. package/src/storage/file/fact-search.js +0 -60
  185. package/src/storage/file-store.js +0 -437
  186. package/src/storage/migrate.js +0 -128
  187. package/src/storage/migrations/001-initial.sql +0 -83
  188. package/src/storage/migrations/002-storage-integrity.sql +0 -113
  189. package/src/storage/record-validation.js +0 -46
  190. package/src/storage/snapshot-defaults.js +0 -54
  191. package/src/storage/sqlite/candidates-repository.js +0 -84
  192. package/src/storage/sqlite/episode-evidence-repository.js +0 -154
  193. package/src/storage/sqlite/episode-privacy-sql.js +0 -33
  194. package/src/storage/sqlite/episodes-repository.js +0 -52
  195. package/src/storage/sqlite/fact-search-repository.js +0 -162
  196. package/src/storage/sqlite/facts-repository.js +0 -146
  197. package/src/storage/sqlite/imports-repository.js +0 -39
  198. package/src/storage/sqlite/mapper.js +0 -84
  199. package/src/storage/sqlite/open-database.js +0 -15
  200. package/src/storage/sqlite/outbox-repository.js +0 -103
  201. package/src/storage/sqlite/snapshot-repository.js +0 -69
  202. package/src/storage/sqlite/spaces-repository.js +0 -52
  203. package/src/storage/sqlite/subscriptions-repository.js +0 -54
  204. package/src/storage/sqlite-store.js +0 -159
  205. package/src/storage/stable-json.js +0 -24
  206. package/src/storage/store-port.js +0 -20
  207. package/src/store.js +0 -2
@@ -86,7 +86,7 @@ export async function ensureContainerRuntime(options = {}, dependencies = {}) {
86
86
  throw containerRuntimeError(runtime, options.platform ?? process.platform);
87
87
  }
88
88
 
89
- onProgress(`正在启动 ${runtime.desktop.label},首次启动可能需要几分钟…`);
89
+ onProgress(`Starting ${runtime.desktop.label}; the first launch may take a few minutes...`);
90
90
  await launch(runtime.desktop, dependencies);
91
91
 
92
92
  const attempts = Math.max(1, Math.ceil(maxWaitMs / pollIntervalMs));
@@ -100,9 +100,9 @@ export async function ensureContainerRuntime(options = {}, dependencies = {}) {
100
100
  }
101
101
 
102
102
  throw new Error(
103
- `${runtime.desktop?.label ?? runtime.label} 已启动,但容器引擎未在 ` +
104
- `${Math.ceil(maxWaitMs / 1000)} 秒内就绪。请打开该应用查看错误,确认 docker info ` +
105
- `可以成功后重新运行 fl setup。${detailSuffix(runtime.detail)}`
103
+ `${runtime.desktop?.label ?? runtime.label} started, but the container engine was not ready ` +
104
+ `within ${Math.ceil(maxWaitMs / 1000)} seconds. Open the application to inspect errors, ` +
105
+ `confirm that docker info succeeds, then run fl setup again.${detailSuffix(runtime.detail)}`
106
106
  );
107
107
  }
108
108
 
@@ -124,12 +124,14 @@ export function runDockerCompose(args, runtime = null, {
124
124
 
125
125
  const detail = safeProcessDetail(result, homeDir);
126
126
  if (/port is already allocated|address already in use|bind.+failed/i.test(detail)) {
127
- throw new Error(`Fuli 需要的本地端口已被占用。${detailSuffix(detail)}`);
127
+ throw new Error(`A local port required by Fuli is already in use.${detailSuffix(detail)}`);
128
128
  }
129
129
  if (/cannot connect|connection refused|error during connect|docker daemon|\bEOF\b/i.test(detail)) {
130
- throw new Error(`容器运行时在启动 Fuli Provider 时失去连接。${detailSuffix(detail)}`);
130
+ throw new Error(
131
+ `The container runtime disconnected while starting the Fuli Provider.${detailSuffix(detail)}`
132
+ );
131
133
  }
132
- throw new Error(`Docker Compose 无法启动 Fuli Provider。${detailSuffix(detail)}`);
134
+ throw new Error(`Docker Compose could not start the Fuli Provider.${detailSuffix(detail)}`);
133
135
  }
134
136
 
135
137
  export function selectDockerEnvironment({
@@ -161,19 +163,21 @@ export function containerRuntimeError(runtime, platform = process.platform) {
161
163
  }
162
164
  if (runtime.status === 'missing-compose') {
163
165
  return new Error(
164
- `已找到 Docker,但缺少 Docker Compose v2。请安装 Compose v2 后重新运行 fl setup。` +
166
+ 'Docker was found, but Docker Compose v2 is missing. Install Compose v2, then run ' +
167
+ 'fl setup again.' +
165
168
  detailSuffix(runtime.detail)
166
169
  );
167
170
  }
168
171
  if (runtime.explicitTarget) {
169
172
  return new Error(
170
- `DOCKER_HOST 或 DOCKER_CONTEXT 指向的容器引擎不可用。请修复该配置并确认 ` +
171
- `docker info 可以成功后重新运行 fl setup。${detailSuffix(runtime.detail)}`
173
+ 'The container engine selected by DOCKER_HOST or DOCKER_CONTEXT is unavailable. Fix the ' +
174
+ `configuration, confirm that docker info succeeds, then run fl setup again.${detailSuffix(runtime.detail)}`
172
175
  );
173
176
  }
174
177
  return new Error(
175
- `已找到 Docker 和 Compose,但容器引擎没有运行。请启动 Docker Desktop、` +
176
- `Rancher Desktop 或当前 Docker 服务,确认 docker info 可以成功后重新运行 fl setup。` +
178
+ 'Docker and Compose were found, but the container engine is not running. Start Docker ' +
179
+ 'Desktop, Rancher Desktop, or the current Docker service; confirm that docker info succeeds; ' +
180
+ 'then run fl setup again.' +
177
181
  detailSuffix(runtime.detail)
178
182
  );
179
183
  }
@@ -330,17 +334,17 @@ async function launchDesktopRuntime(desktop, { spawnProcess = spawn } = {}) {
330
334
 
331
335
  function missingRuntimeMessage(platform) {
332
336
  if (platform === 'darwin') {
333
- return `未检测到可用的 Docker Compose 容器运行时。请安装并首次打开 Rancher Desktop ` +
334
- `(${RANCHER_DOWNLOAD_URL}) 或 Docker Desktop (${DOCKER_DOWNLOAD_URL}),` +
335
- `然后重新运行 fl setup。`;
337
+ return 'No usable Docker Compose container runtime was detected. Install and open Rancher ' +
338
+ `Desktop (${RANCHER_DOWNLOAD_URL}) or Docker Desktop (${DOCKER_DOWNLOAD_URL}), then run ` +
339
+ 'fl setup again.';
336
340
  }
337
341
  if (platform === 'win32') {
338
- return `未检测到可用的 Docker Compose 容器运行时。请先启用 WSL 2,再安装并首次打开 ` +
339
- `Rancher Desktop (${RANCHER_DOWNLOAD_URL}) 或 Docker Desktop ` +
340
- `(${DOCKER_DOWNLOAD_URL}),然后重新运行 fl setup。`;
342
+ return 'No usable Docker Compose container runtime was detected. Enable WSL 2, then install ' +
343
+ `and open Rancher Desktop (${RANCHER_DOWNLOAD_URL}) or Docker Desktop ` +
344
+ `(${DOCKER_DOWNLOAD_URL}), and run fl setup again.`;
341
345
  }
342
- return `未检测到可用的 Docker Compose 容器运行时。请安装 Docker Engine + Compose v2,` +
343
- `或 Rancher Desktop (${RANCHER_DOWNLOAD_URL}),然后重新运行 fl setup。`;
346
+ return 'No usable Docker Compose container runtime was detected. Install Docker Engine with ' +
347
+ `Compose v2, or Rancher Desktop (${RANCHER_DOWNLOAD_URL}), then run fl setup again.`;
344
348
  }
345
349
 
346
350
  function defaultDockerAvailable(env) {
@@ -372,7 +376,7 @@ function safeProcessDetail(result, homeDir) {
372
376
  }
373
377
 
374
378
  function detailSuffix(detail) {
375
- return nonEmpty(detail) ? ` Docker 返回:${detail}` : '';
379
+ return nonEmpty(detail) ? ` Docker returned: ${detail}` : '';
376
380
  }
377
381
 
378
382
  function processSucceeded(result) {
@@ -1,4 +1,4 @@
1
- import { spawn, spawnSync } from 'node:child_process';
1
+ import { spawn } from 'node:child_process';
2
2
  import { randomBytes } from 'node:crypto';
3
3
  import {
4
4
  chmodSync,
@@ -15,38 +15,61 @@ import {
15
15
  ensureContainerRuntime,
16
16
  runDockerCompose
17
17
  } from './container-runtime.js';
18
+ import {
19
+ discoverLanAddresses,
20
+ LAN_ACCESS_USERNAME,
21
+ lanConsoleUrls
22
+ } from '../server/lan-access.js';
18
23
  import { readJsonFile, writeJsonFileAtomic } from '../storage/json-file.js';
24
+ import {
25
+ DEFAULT_RUNTIME_SETTINGS,
26
+ managedProviderUrls,
27
+ normalizeRuntimeSettings,
28
+ runtimeSettingsWithOverrides,
29
+ writeRuntimeSettings
30
+ } from '../system/runtime-settings.js';
19
31
 
20
32
  export {
21
33
  dockerInfoIndicatesDaemon,
22
34
  selectDockerEnvironment
23
35
  } from './container-runtime.js';
24
36
 
25
- const PERSONAL_PROVIDER_URL = 'http://127.0.0.1:8787';
26
- const WORKSPACE_PROVIDER_URL = 'http://127.0.0.1:8788';
27
-
28
- export const MANAGED_PERSONAL_PROVIDER_URL = PERSONAL_PROVIDER_URL;
29
- export const MANAGED_WORKSPACE_PROVIDER_URL = WORKSPACE_PROVIDER_URL;
30
-
31
37
  export async function ensureGraphRuntime(input, dependencies = {}) {
32
38
  const deps = graphDependencies(dependencies);
39
+ const runtimeSettings = normalizeRuntimeSettings(input.runtimeSettings ??
40
+ runtimeSettingsWithOverrides(DEFAULT_RUNTIME_SETTINGS, {
41
+ consolePort: input.port,
42
+ lanAccess: input.lan === true
43
+ }));
44
+ const urls = managedProviderUrls(runtimeSettings);
45
+ const lan = runtimeSettings.lanAccess;
46
+ const lanAddresses = lan ? deps.discoverLanAddresses() : [];
47
+ if (lan && lanAddresses.length === 0) {
48
+ throw new Error('No private IPv4 LAN address is available, so LAN mode cannot start.');
49
+ }
33
50
  const containerRuntime = await deps.ensureContainerRuntime({
34
51
  env: input.env ?? process.env,
35
52
  onProgress: input.onProgress
36
53
  });
37
54
  deps.ensureDirectory(input.paths.dataDir);
38
- const secrets = ensureProviderSecrets(input.paths.graphEnvPath, deps);
55
+ const secrets = ensureProviderEnvironment(
56
+ input.paths.graphEnvPath,
57
+ runtimeSettings,
58
+ deps
59
+ );
60
+ deps.writeRuntimeSettings(input.paths.runtimeSettingsPath, runtimeSettings);
39
61
  deps.startProviders(input.paths, input.paths.graphEnvPath, {
40
62
  personalOnly: input.personalOnly === true,
41
63
  containerRuntime,
42
64
  ...(input.buildProviders === false ? { build: false } : {})
43
65
  });
44
- await waitForProvider(PERSONAL_PROVIDER_URL, deps.fetch);
45
- if (!input.personalOnly) await waitForProvider(WORKSPACE_PROVIDER_URL, deps.fetch);
66
+ await waitForProvider(urls.personal, deps.fetch);
67
+ if (!input.personalOnly) await waitForProvider(urls.workspace, deps.fetch);
46
68
 
47
69
  let config = deps.readConfig(input.paths.graphRuntimeConfigPath);
48
70
  if (!config) {
49
71
  config = await bootstrapGraph({
72
+ urls,
50
73
  personalSpaceName: input.personalSpaceName,
51
74
  secrets,
52
75
  personalOnly: input.personalOnly === true,
@@ -54,64 +77,105 @@ export async function ensureGraphRuntime(input, dependencies = {}) {
54
77
  });
55
78
  deps.writeConfig(input.paths.graphRuntimeConfigPath, config);
56
79
  deps.secureFile(input.paths.graphRuntimeConfigPath);
80
+ } else {
81
+ const synchronized = synchronizeManagedProviderUrls(config, urls, {
82
+ personalOnly: input.personalOnly === true
83
+ });
84
+ if (synchronized.changed) {
85
+ await reconcileManagedWorkspaceSubscriptions({
86
+ previousConfig: config,
87
+ nextConfig: synchronized.config,
88
+ personalUrl: urls.personal,
89
+ fetchImpl: deps.fetch
90
+ });
91
+ config = synchronized.config;
92
+ deps.writeConfig(input.paths.graphRuntimeConfigPath, config);
93
+ deps.secureFile(input.paths.graphRuntimeConfigPath);
94
+ }
57
95
  }
58
- stopOwnedLegacyRuntime(input.paths, deps);
59
96
 
60
97
  if (input.noStart) return { status: 'initialized', url: null, pid: null };
61
98
  const existing = deps.readState(input.paths.graphRuntimeStatePath);
62
99
  if (isGraphRuntimeState(existing) && deps.isProcessAlive(existing.pid)) {
63
100
  const healthy = await deps.webHealth(existing.url, existing.pid, existing.version);
64
- if (healthy && existing.port === input.port) {
65
- return { status: 'running', url: existing.url, pid: existing.pid };
101
+ if (
102
+ healthy &&
103
+ existing.port === runtimeSettings.ports.console &&
104
+ exposureMatches(existing, { lan, lanAddresses })
105
+ ) {
106
+ return runtimeResult('running', existing);
66
107
  }
67
108
  if (!healthy) throw new Error('Recorded Fuli Graphiti runtime is not healthy');
68
109
  deps.stopProcess(existing.pid);
110
+ if (!await deps.waitForExit(existing.pid)) {
111
+ throw new Error('Fuli Graphiti console did not stop before changing exposure mode');
112
+ }
69
113
  }
114
+ const lanAccessToken = lan ? deps.createLanAccessToken() : null;
70
115
  const child = deps.spawnWebRuntime({
71
116
  paths: input.paths,
72
117
  nodePath: process.execPath,
73
- port: input.port
118
+ port: runtimeSettings.ports.console,
119
+ lan,
120
+ lanAccessToken,
121
+ env: input.env ?? process.env
74
122
  });
75
- const url = `http://127.0.0.1:${input.port}`;
123
+ const url = `http://127.0.0.1:${runtimeSettings.ports.console}`;
76
124
  if (!Number.isInteger(child.pid) || !await waitForWeb(url, child.pid, deps)) {
77
125
  if (Number.isInteger(child.pid)) deps.stopProcess(child.pid);
78
126
  throw new Error('Fuli Graphiti console could not start');
79
127
  }
80
- deps.writeState(input.paths.graphRuntimeStatePath, {
81
- version: 3,
128
+ const state = {
129
+ version: 4,
82
130
  pid: child.pid,
83
131
  url,
84
- port: input.port,
132
+ port: runtimeSettings.ports.console,
133
+ runtimeSettings,
134
+ ...(lan
135
+ ? {
136
+ lan: true,
137
+ lanUrls: lanConsoleUrls(lanAddresses, runtimeSettings.ports.console)
138
+ }
139
+ : {}),
85
140
  managedProviders: input.personalOnly
86
141
  ? ['personal']
87
142
  : ['personal', 'development-workspace'],
88
143
  startedAt: new Date().toISOString()
89
- });
90
- return { status: 'started', url, pid: child.pid };
144
+ };
145
+ deps.writeState(input.paths.graphRuntimeStatePath, state);
146
+ deps.secureFile(input.paths.graphRuntimeStatePath);
147
+ return runtimeResult('started', state, lanAccessToken);
91
148
  }
92
149
 
93
- function ensureProviderSecrets(path, deps) {
94
- if (deps.fileExists(path)) return parseEnv(deps.readText(path));
150
+ function ensureProviderEnvironment(path, settings, deps) {
151
+ const previous = deps.fileExists(path) ? parseEnv(deps.readText(path)) : {};
95
152
  const values = {
96
- FULI_PERSONAL_NEO4J_PASSWORD: secret(),
97
- FULI_WORKSPACE_NEO4J_PASSWORD: secret(),
98
- FULI_PERSONAL_BOOTSTRAP_TOKEN: secret(),
99
- FULI_WORKSPACE_BOOTSTRAP_TOKEN: secret()
153
+ ...previous,
154
+ FULI_PERSONAL_NEO4J_PASSWORD: previous.FULI_PERSONAL_NEO4J_PASSWORD ?? secret(),
155
+ FULI_WORKSPACE_NEO4J_PASSWORD: previous.FULI_WORKSPACE_NEO4J_PASSWORD ?? secret(),
156
+ FULI_PERSONAL_BOOTSTRAP_TOKEN: previous.FULI_PERSONAL_BOOTSTRAP_TOKEN ?? secret(),
157
+ FULI_WORKSPACE_BOOTSTRAP_TOKEN: previous.FULI_WORKSPACE_BOOTSTRAP_TOKEN ?? secret(),
158
+ FULI_PERSONAL_NEO4J_HTTP_PORT: String(settings.ports.personalNeo4jHttp),
159
+ FULI_PERSONAL_NEO4J_BOLT_PORT: String(settings.ports.personalNeo4jBolt),
160
+ FULI_WORKSPACE_NEO4J_HTTP_PORT: String(settings.ports.workspaceNeo4jHttp),
161
+ FULI_WORKSPACE_NEO4J_BOLT_PORT: String(settings.ports.workspaceNeo4jBolt),
162
+ FULI_PERSONAL_PROVIDER_PORT: String(settings.ports.personalProvider),
163
+ FULI_WORKSPACE_PROVIDER_PORT: String(settings.ports.workspaceProvider)
100
164
  };
101
165
  const body = Object.entries(values).map(([key, value]) => `${key}=${value}`).join('\n') + '\n';
102
- deps.writeText(path, body);
166
+ if (!deps.fileExists(path) || deps.readText(path) !== body) deps.writeText(path, body);
103
167
  deps.secureFile(path);
104
168
  return values;
105
169
  }
106
170
 
107
- async function bootstrapGraph({ personalSpaceName, secrets, personalOnly, fetchImpl }) {
171
+ async function bootstrapGraph({ urls, personalSpaceName, secrets, personalOnly, fetchImpl }) {
108
172
  const personalIdentity = await bootstrapProvider(
109
- PERSONAL_PROVIDER_URL,
173
+ urls.personal,
110
174
  secrets.FULI_PERSONAL_BOOTSTRAP_TOKEN,
111
175
  personalSpaceName,
112
176
  fetchImpl
113
177
  );
114
- const personalSpace = await findOrCreateSpace(PERSONAL_PROVIDER_URL, {
178
+ const personalSpace = await findOrCreateSpace(urls.personal, {
115
179
  token: personalIdentity.access_token,
116
180
  name: personalSpaceName,
117
181
  kind: 'personal',
@@ -120,7 +184,7 @@ async function bootstrapGraph({ personalSpaceName, secrets, personalOnly, fetchI
120
184
  const config = {
121
185
  version: 1,
122
186
  personal: {
123
- providerUrl: PERSONAL_PROVIDER_URL,
187
+ providerUrl: urls.personal,
124
188
  accessToken: personalIdentity.access_token,
125
189
  principalId: personalIdentity.principal_id,
126
190
  spaceId: personalSpace.id
@@ -130,35 +194,108 @@ async function bootstrapGraph({ personalSpaceName, secrets, personalOnly, fetchI
130
194
  if (personalOnly) return config;
131
195
 
132
196
  const workspaceIdentity = await bootstrapProvider(
133
- WORKSPACE_PROVIDER_URL,
197
+ urls.workspace,
134
198
  secrets.FULI_WORKSPACE_BOOTSTRAP_TOKEN,
135
199
  personalSpaceName,
136
200
  fetchImpl
137
201
  );
138
- const project = await findOrCreateSpace(WORKSPACE_PROVIDER_URL, {
202
+ const project = await findOrCreateSpace(urls.workspace, {
139
203
  token: workspaceIdentity.access_token,
140
204
  name: '工作',
141
205
  kind: 'project',
142
206
  fetchImpl
143
207
  });
144
- await providerRequest(PERSONAL_PROVIDER_URL, '/v1/subscriptions', {
208
+ await providerRequest(urls.personal, '/v1/subscriptions', {
145
209
  token: personalIdentity.access_token,
146
210
  body: {
147
211
  personal_space_id: personalSpace.id,
148
212
  project_id: project.id,
149
- provider_url: WORKSPACE_PROVIDER_URL,
213
+ provider_url: urls.workspace,
150
214
  project_name: project.name
151
215
  },
152
216
  fetchImpl
153
217
  });
154
218
  config.workspaces.push({
155
- providerUrl: WORKSPACE_PROVIDER_URL,
156
- accessToken: workspaceIdentity.access_token,
157
- principalId: workspaceIdentity.principal_id
219
+ providerUrl: urls.workspace,
220
+ accessToken: workspaceIdentity.access_token,
221
+ principalId: workspaceIdentity.principal_id,
222
+ managedDevelopment: true
158
223
  });
159
224
  return config;
160
225
  }
161
226
 
227
+ export function synchronizeManagedProviderUrls(config, urls, { personalOnly = true } = {}) {
228
+ const next = structuredClone(config);
229
+ let changed = next.personal?.providerUrl !== urls.personal;
230
+ next.personal.providerUrl = urls.personal;
231
+ if (!personalOnly) {
232
+ for (const workspace of next.workspaces ?? []) {
233
+ if (!isManagedWorkspace(workspace)) continue;
234
+ if (workspace.providerUrl !== urls.workspace || workspace.managedDevelopment !== true) {
235
+ changed = true;
236
+ }
237
+ workspace.providerUrl = urls.workspace;
238
+ workspace.managedDevelopment = true;
239
+ }
240
+ }
241
+ return { config: next, changed };
242
+ }
243
+
244
+ async function reconcileManagedWorkspaceSubscriptions({
245
+ previousConfig,
246
+ nextConfig,
247
+ personalUrl,
248
+ fetchImpl
249
+ }) {
250
+ const previous = (previousConfig.workspaces ?? []).find(isManagedWorkspace);
251
+ const next = (nextConfig.workspaces ?? []).find(isManagedWorkspace);
252
+ if (!previous || !next || previous.providerUrl === next.providerUrl) return;
253
+ const query = new URLSearchParams({ personal_space_id: nextConfig.personal.spaceId });
254
+ const response = await fetchImpl(`${personalUrl}/v1/subscriptions?${query}`, {
255
+ headers: { authorization: `Bearer ${nextConfig.personal.accessToken}` }
256
+ });
257
+ if (!response.ok) throw new Error('Could not reconcile managed workspace subscriptions');
258
+ const subscriptions = await response.json();
259
+ for (const subscription of subscriptions.filter(
260
+ (item) => item.provider_url === previous.providerUrl
261
+ )) {
262
+ const removal = new URLSearchParams({
263
+ personal_space_id: nextConfig.personal.spaceId,
264
+ provider_url: previous.providerUrl
265
+ });
266
+ const removed = await fetchImpl(
267
+ `${personalUrl}/v1/subscriptions/${encodeURIComponent(subscription.project_id)}?${removal}`,
268
+ {
269
+ method: 'DELETE',
270
+ headers: { authorization: `Bearer ${nextConfig.personal.accessToken}` }
271
+ }
272
+ );
273
+ if (!removed.ok) throw new Error('Could not remove the previous managed workspace port');
274
+ await providerRequest(personalUrl, '/v1/subscriptions', {
275
+ token: nextConfig.personal.accessToken,
276
+ body: {
277
+ personal_space_id: nextConfig.personal.spaceId,
278
+ project_id: subscription.project_id,
279
+ provider_url: next.providerUrl,
280
+ project_name: subscription.project_name
281
+ },
282
+ fetchImpl
283
+ });
284
+ }
285
+ }
286
+
287
+ function isManagedWorkspace(workspace) {
288
+ if (workspace?.managedDevelopment === true) return true;
289
+ try {
290
+ const url = new URL(workspace?.providerUrl);
291
+ return url.protocol === 'http:' &&
292
+ ['127.0.0.1', 'localhost', '::1', '[::1]'].includes(url.hostname) &&
293
+ url.port === '8788';
294
+ } catch {
295
+ return false;
296
+ }
297
+ }
298
+
162
299
  async function bootstrapProvider(url, bootstrapToken, principalName, fetchImpl) {
163
300
  const response = await fetchImpl(`${url}/v1/bootstrap`, {
164
301
  method: 'POST',
@@ -217,7 +354,7 @@ async function waitForProvider(url, fetchImpl) {
217
354
  async function waitForWeb(url, pid, deps) {
218
355
  for (let attempt = 0; attempt < 100; attempt += 1) {
219
356
  if (!deps.isProcessAlive(pid)) return false;
220
- if (await deps.webHealth(url, pid, 3)) return true;
357
+ if (await deps.webHealth(url, pid, 4)) return true;
221
358
  await delay(100);
222
359
  }
223
360
  return false;
@@ -250,14 +387,19 @@ function spawnWebRuntime(input) {
250
387
  mkdirSync(dirname(input.paths.logPath), { recursive: true });
251
388
  const log = openSync(input.paths.logPath, 'a');
252
389
  try {
253
- const child = spawn(input.nodePath, [
390
+ const args = [
254
391
  input.paths.serverPath,
255
392
  '--runtime-config', input.paths.graphRuntimeConfigPath,
256
393
  '--port', String(input.port)
257
- ], {
394
+ ];
395
+ if (input.lan) args.push('--lan');
396
+ const child = spawn(input.nodePath, args, {
258
397
  detached: true,
259
398
  windowsHide: true,
260
- stdio: ['ignore', log, log]
399
+ stdio: ['ignore', log, log],
400
+ env: input.lan
401
+ ? { ...input.env, FULI_LAN_ACCESS_TOKEN: input.lanAccessToken }
402
+ : input.env
261
403
  });
262
404
  child.unref();
263
405
  return child;
@@ -266,13 +408,6 @@ function spawnWebRuntime(input) {
266
408
  }
267
409
  }
268
410
 
269
- function stopOwnedLegacyRuntime(paths, deps) {
270
- deps.stopLegacyService();
271
- const state = deps.readState(paths.statePath);
272
- if (state?.version !== 1 || state.dbPath !== paths.dbPath || !Number.isInteger(state.pid)) return;
273
- if (deps.isProcessAlive(state.pid)) deps.stopProcess(state.pid);
274
- }
275
-
276
411
  function graphDependencies(overrides) {
277
412
  return {
278
413
  fetch: globalThis.fetch,
@@ -283,6 +418,7 @@ function graphDependencies(overrides) {
283
418
  ensureDirectory: (path) => mkdirSync(path, { recursive: true }),
284
419
  readConfig: (path) => readJsonFile(path, null),
285
420
  writeConfig: writeJsonFileAtomic,
421
+ writeRuntimeSettings,
286
422
  readState: (path) => readJsonFile(path, null),
287
423
  writeState: writeJsonFileAtomic,
288
424
  ensureContainerRuntime,
@@ -290,8 +426,10 @@ function graphDependencies(overrides) {
290
426
  spawnWebRuntime,
291
427
  isProcessAlive,
292
428
  stopProcess,
293
- stopLegacyService,
294
429
  webHealth: checkLocalConsoleHealth,
430
+ discoverLanAddresses,
431
+ createLanAccessToken: secret,
432
+ waitForExit: waitForProcessExit,
295
433
  ...overrides
296
434
  };
297
435
  }
@@ -319,17 +457,32 @@ export async function checkLocalConsoleHealth(url, expectedPid, stateVersion = 3
319
457
  }
320
458
 
321
459
  function isGraphRuntimeState(state) {
322
- return (state?.version === 2 || state?.version === 3) &&
460
+ return (state?.version === 2 || state?.version === 3 || state?.version === 4) &&
323
461
  Number.isInteger(state.pid) && typeof state.url === 'string';
324
462
  }
325
463
 
326
- function stopLegacyService() {
327
- if (process.platform !== 'darwin') return;
328
- spawnSync('launchctl', ['remove', 'dev.jevonscode.fuli.local'], {
329
- encoding: 'utf8',
330
- windowsHide: true,
331
- stdio: 'ignore'
332
- });
464
+ function exposureMatches(state, { lan }) {
465
+ if ((state.lan === true) !== lan) return false;
466
+ if (!lan) return true;
467
+ // A repeated explicit LAN start rotates the in-memory access code.
468
+ return false;
469
+ }
470
+
471
+ function runtimeResult(status, state, lanAccessToken = null) {
472
+ const result = { status, url: state.url, pid: state.pid };
473
+ if (state.lan !== true) return result;
474
+ if (typeof lanAccessToken !== 'string' || lanAccessToken.length < 16) {
475
+ throw new Error('LAN runtime started without a valid access code');
476
+ }
477
+ return {
478
+ ...result,
479
+ lan: true,
480
+ lanUrls: [...state.lanUrls],
481
+ lanAccess: {
482
+ username: LAN_ACCESS_USERNAME,
483
+ accessCode: lanAccessToken
484
+ }
485
+ };
333
486
  }
334
487
 
335
488
  function parseEnv(body) {
@@ -355,6 +508,14 @@ function stopProcess(pid) {
355
508
  try { process.kill(pid, 'SIGTERM'); } catch { /* already stopped */ }
356
509
  }
357
510
 
511
+ async function waitForProcessExit(pid) {
512
+ for (let attempt = 0; attempt < 50; attempt += 1) {
513
+ if (!isProcessAlive(pid)) return true;
514
+ await delay(100);
515
+ }
516
+ return false;
517
+ }
518
+
358
519
  function delay(ms) {
359
520
  return new Promise((resolve) => setTimeout(resolve, ms));
360
521
  }
@@ -4,14 +4,14 @@ export function assertSupportedNodeVersion(version = process.versions.node) {
4
4
  const parsed = parseNodeVersion(version);
5
5
  if (compareVersions(parsed, MINIMUM_NODE_VERSION) >= 0) return;
6
6
  throw new Error(
7
- `fl setup 需要 Node.js 24.12 或更高版本;当前为 ${version}。` +
8
- `请升级 Node.js 后重新运行 fl setup。`
7
+ `fl setup requires Node.js 24.12 or later; the current version is ${version}. ` +
8
+ 'Upgrade Node.js, then run fl setup again.'
9
9
  );
10
10
  }
11
11
 
12
12
  function parseNodeVersion(value) {
13
13
  const match = String(value).match(/^v?(\d+)\.(\d+)\.(\d+)/);
14
- if (!match) throw new Error(`无法识别 Node.js 版本:${value}`);
14
+ if (!match) throw new Error(`Could not parse Node.js version: ${value}`);
15
15
  return match.slice(1).map(Number);
16
16
  }
17
17
 
@@ -1,5 +1,3 @@
1
- import { DEFAULT_FULI_PORT } from '../defaults.js';
2
-
3
1
  const VALUE_OPTIONS = Object.freeze({
4
2
  '--data-dir': 'dataDir',
5
3
  '--personal-space': 'personalSpaceName',
@@ -26,8 +24,8 @@ export function parseUpdateOptions(args = []) {
26
24
  function parseSetupLikeOptions(args, command) {
27
25
  const result = {
28
26
  dataDir: null,
29
- personalSpaceName: '我',
30
- port: DEFAULT_FULI_PORT,
27
+ personalSpaceName: 'Personal',
28
+ port: null,
31
29
  yes: false,
32
30
  codexOnly: false,
33
31
  skipAgents: false,
@@ -16,19 +16,34 @@ export function resolveSetupPaths({
16
16
  const root = resolve(packageRoot);
17
17
  return {
18
18
  dataDir: resolvedDataDir,
19
- dbPath: join(resolvedDataDir, 'context.db'),
20
19
  graphEnvPath: join(resolvedDataDir, 'graph-provider.env'),
21
20
  graphRuntimeConfigPath: join(resolvedDataDir, 'graph-runtime.json'),
22
21
  graphRuntimeStatePath: join(resolvedDataDir, 'graph-runtime-state.json'),
22
+ runtimeSettingsPath: join(resolvedDataDir, 'runtime-settings.json'),
23
+ externalKnowledgeRegistryPath: join(
24
+ resolvedDataDir,
25
+ 'external-knowledge',
26
+ 'bindings.json'
27
+ ),
28
+ externalKnowledgeConflictPolicyPath: join(
29
+ resolvedDataDir,
30
+ 'external-knowledge',
31
+ 'conflict-policies.json'
32
+ ),
33
+ externalKnowledgeConnectorDir: join(
34
+ resolvedDataDir,
35
+ 'external-knowledge',
36
+ 'connectors'
37
+ ),
23
38
  backupDir: join(resolvedDataDir, 'backups', 'agents'),
24
39
  logPath: join(resolvedDataDir, 'logs', 'runtime.log'),
25
- statePath: join(resolvedDataDir, 'runtime.json'),
26
40
  serverPath: join(root, 'src', 'server.js'),
27
41
  mcpServerPath: join(root, 'src', 'mcp-server.js'),
28
42
  graphSetupPath: join(root, 'src', 'graphiti', 'setup.js'),
29
43
  graphComposePath: join(root, 'compose.graphiti.yml'),
30
44
  sessionSkillPath: join(root, 'skills', 'capturing-session-knowledge'),
31
- projectSkillPath: join(root, 'skills', 'grilling-project')
45
+ projectSkillPath: join(root, 'skills', 'grilling-project'),
46
+ reviewSkillPath: join(root, 'skills', 'flreview')
32
47
  };
33
48
  }
34
49
 
@@ -0,0 +1,49 @@
1
+ const REVIEW_SKILL_NAME = 'flreview';
2
+
3
+ // Keep every host-specific difference for the review Skill here. The shared
4
+ // SKILL.md intentionally contains no Codex/Claude/Cursor branching.
5
+ const ADAPTERS = Object.freeze({
6
+ codex: Object.freeze({
7
+ root: ['.agents', 'skills'],
8
+ nativeInvocation: '$flreview',
9
+ needsCommandBridge: true
10
+ }),
11
+ 'claude-code': Object.freeze({
12
+ root: ['.claude', 'skills'],
13
+ nativeInvocation: '/flreview',
14
+ needsCommandBridge: false
15
+ }),
16
+ cursor: Object.freeze({
17
+ root: ['.cursor', 'skills'],
18
+ nativeInvocation: '/flreview',
19
+ needsCommandBridge: false
20
+ })
21
+ });
22
+
23
+ export function adaptAgentForReviewSkill(agent, { homeDir, pathApi }) {
24
+ const adapter = adapterFor(agent.id);
25
+ return {
26
+ ...agent,
27
+ reviewSkillPath: pathApi.join(homeDir, ...adapter.root, REVIEW_SKILL_NAME),
28
+ reviewSkillTrigger: reviewSkillTrigger(agent.id)
29
+ };
30
+ }
31
+
32
+ export function reviewSkillTrigger(agentId) {
33
+ const adapter = adapterFor(agentId);
34
+ return {
35
+ userCommand: '/flreview',
36
+ nativeInvocation: adapter.nativeInvocation,
37
+ needsCommandBridge: adapter.needsCommandBridge
38
+ };
39
+ }
40
+
41
+ export function codexReviewCommandBridge() {
42
+ return 'Exact `/flreview`: load installed `flreview` Skill.';
43
+ }
44
+
45
+ function adapterFor(agentId) {
46
+ const adapter = ADAPTERS[agentId];
47
+ if (!adapter) throw new TypeError(`Unsupported review Skill agent: ${agentId}`);
48
+ return adapter;
49
+ }