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
@@ -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,
@@ -21,42 +21,55 @@ import {
21
21
  lanConsoleUrls
22
22
  } from '../server/lan-access.js';
23
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';
24
31
 
25
32
  export {
26
33
  dockerInfoIndicatesDaemon,
27
34
  selectDockerEnvironment
28
35
  } from './container-runtime.js';
29
36
 
30
- const PERSONAL_PROVIDER_URL = 'http://127.0.0.1:8787';
31
- const WORKSPACE_PROVIDER_URL = 'http://127.0.0.1:8788';
32
-
33
- export const MANAGED_PERSONAL_PROVIDER_URL = PERSONAL_PROVIDER_URL;
34
- export const MANAGED_WORKSPACE_PROVIDER_URL = WORKSPACE_PROVIDER_URL;
35
-
36
37
  export async function ensureGraphRuntime(input, dependencies = {}) {
37
38
  const deps = graphDependencies(dependencies);
38
- const lan = input.lan === true;
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;
39
46
  const lanAddresses = lan ? deps.discoverLanAddresses() : [];
40
47
  if (lan && lanAddresses.length === 0) {
41
- throw new Error('未发现可用的私有 IPv4 局域网地址,无法启动 LAN 模式。');
48
+ throw new Error('No private IPv4 LAN address is available, so LAN mode cannot start.');
42
49
  }
43
50
  const containerRuntime = await deps.ensureContainerRuntime({
44
51
  env: input.env ?? process.env,
45
52
  onProgress: input.onProgress
46
53
  });
47
54
  deps.ensureDirectory(input.paths.dataDir);
48
- 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);
49
61
  deps.startProviders(input.paths, input.paths.graphEnvPath, {
50
62
  personalOnly: input.personalOnly === true,
51
63
  containerRuntime,
52
64
  ...(input.buildProviders === false ? { build: false } : {})
53
65
  });
54
- await waitForProvider(PERSONAL_PROVIDER_URL, deps.fetch);
55
- 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);
56
68
 
57
69
  let config = deps.readConfig(input.paths.graphRuntimeConfigPath);
58
70
  if (!config) {
59
71
  config = await bootstrapGraph({
72
+ urls,
60
73
  personalSpaceName: input.personalSpaceName,
61
74
  secrets,
62
75
  personalOnly: input.personalOnly === true,
@@ -64,8 +77,22 @@ export async function ensureGraphRuntime(input, dependencies = {}) {
64
77
  });
65
78
  deps.writeConfig(input.paths.graphRuntimeConfigPath, config);
66
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
+ }
67
95
  }
68
- stopOwnedLegacyRuntime(input.paths, deps);
69
96
 
70
97
  if (input.noStart) return { status: 'initialized', url: null, pid: null };
71
98
  const existing = deps.readState(input.paths.graphRuntimeStatePath);
@@ -73,7 +100,7 @@ export async function ensureGraphRuntime(input, dependencies = {}) {
73
100
  const healthy = await deps.webHealth(existing.url, existing.pid, existing.version);
74
101
  if (
75
102
  healthy &&
76
- existing.port === input.port &&
103
+ existing.port === runtimeSettings.ports.console &&
77
104
  exposureMatches(existing, { lan, lanAddresses })
78
105
  ) {
79
106
  return runtimeResult('running', existing);
@@ -88,25 +115,26 @@ export async function ensureGraphRuntime(input, dependencies = {}) {
88
115
  const child = deps.spawnWebRuntime({
89
116
  paths: input.paths,
90
117
  nodePath: process.execPath,
91
- port: input.port,
118
+ port: runtimeSettings.ports.console,
92
119
  lan,
93
120
  lanAccessToken,
94
121
  env: input.env ?? process.env
95
122
  });
96
- const url = `http://127.0.0.1:${input.port}`;
123
+ const url = `http://127.0.0.1:${runtimeSettings.ports.console}`;
97
124
  if (!Number.isInteger(child.pid) || !await waitForWeb(url, child.pid, deps)) {
98
125
  if (Number.isInteger(child.pid)) deps.stopProcess(child.pid);
99
126
  throw new Error('Fuli Graphiti console could not start');
100
127
  }
101
128
  const state = {
102
- version: 3,
129
+ version: 4,
103
130
  pid: child.pid,
104
131
  url,
105
- port: input.port,
132
+ port: runtimeSettings.ports.console,
133
+ runtimeSettings,
106
134
  ...(lan
107
135
  ? {
108
136
  lan: true,
109
- lanUrls: lanConsoleUrls(lanAddresses, input.port)
137
+ lanUrls: lanConsoleUrls(lanAddresses, runtimeSettings.ports.console)
110
138
  }
111
139
  : {}),
112
140
  managedProviders: input.personalOnly
@@ -119,28 +147,35 @@ export async function ensureGraphRuntime(input, dependencies = {}) {
119
147
  return runtimeResult('started', state, lanAccessToken);
120
148
  }
121
149
 
122
- function ensureProviderSecrets(path, deps) {
123
- 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)) : {};
124
152
  const values = {
125
- FULI_PERSONAL_NEO4J_PASSWORD: secret(),
126
- FULI_WORKSPACE_NEO4J_PASSWORD: secret(),
127
- FULI_PERSONAL_BOOTSTRAP_TOKEN: secret(),
128
- 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)
129
164
  };
130
165
  const body = Object.entries(values).map(([key, value]) => `${key}=${value}`).join('\n') + '\n';
131
- deps.writeText(path, body);
166
+ if (!deps.fileExists(path) || deps.readText(path) !== body) deps.writeText(path, body);
132
167
  deps.secureFile(path);
133
168
  return values;
134
169
  }
135
170
 
136
- async function bootstrapGraph({ personalSpaceName, secrets, personalOnly, fetchImpl }) {
171
+ async function bootstrapGraph({ urls, personalSpaceName, secrets, personalOnly, fetchImpl }) {
137
172
  const personalIdentity = await bootstrapProvider(
138
- PERSONAL_PROVIDER_URL,
173
+ urls.personal,
139
174
  secrets.FULI_PERSONAL_BOOTSTRAP_TOKEN,
140
175
  personalSpaceName,
141
176
  fetchImpl
142
177
  );
143
- const personalSpace = await findOrCreateSpace(PERSONAL_PROVIDER_URL, {
178
+ const personalSpace = await findOrCreateSpace(urls.personal, {
144
179
  token: personalIdentity.access_token,
145
180
  name: personalSpaceName,
146
181
  kind: 'personal',
@@ -149,7 +184,7 @@ async function bootstrapGraph({ personalSpaceName, secrets, personalOnly, fetchI
149
184
  const config = {
150
185
  version: 1,
151
186
  personal: {
152
- providerUrl: PERSONAL_PROVIDER_URL,
187
+ providerUrl: urls.personal,
153
188
  accessToken: personalIdentity.access_token,
154
189
  principalId: personalIdentity.principal_id,
155
190
  spaceId: personalSpace.id
@@ -159,35 +194,108 @@ async function bootstrapGraph({ personalSpaceName, secrets, personalOnly, fetchI
159
194
  if (personalOnly) return config;
160
195
 
161
196
  const workspaceIdentity = await bootstrapProvider(
162
- WORKSPACE_PROVIDER_URL,
197
+ urls.workspace,
163
198
  secrets.FULI_WORKSPACE_BOOTSTRAP_TOKEN,
164
199
  personalSpaceName,
165
200
  fetchImpl
166
201
  );
167
- const project = await findOrCreateSpace(WORKSPACE_PROVIDER_URL, {
202
+ const project = await findOrCreateSpace(urls.workspace, {
168
203
  token: workspaceIdentity.access_token,
169
204
  name: '工作',
170
205
  kind: 'project',
171
206
  fetchImpl
172
207
  });
173
- await providerRequest(PERSONAL_PROVIDER_URL, '/v1/subscriptions', {
208
+ await providerRequest(urls.personal, '/v1/subscriptions', {
174
209
  token: personalIdentity.access_token,
175
210
  body: {
176
211
  personal_space_id: personalSpace.id,
177
212
  project_id: project.id,
178
- provider_url: WORKSPACE_PROVIDER_URL,
213
+ provider_url: urls.workspace,
179
214
  project_name: project.name
180
215
  },
181
216
  fetchImpl
182
217
  });
183
218
  config.workspaces.push({
184
- providerUrl: WORKSPACE_PROVIDER_URL,
185
- accessToken: workspaceIdentity.access_token,
186
- principalId: workspaceIdentity.principal_id
219
+ providerUrl: urls.workspace,
220
+ accessToken: workspaceIdentity.access_token,
221
+ principalId: workspaceIdentity.principal_id,
222
+ managedDevelopment: true
187
223
  });
188
224
  return config;
189
225
  }
190
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
+
191
299
  async function bootstrapProvider(url, bootstrapToken, principalName, fetchImpl) {
192
300
  const response = await fetchImpl(`${url}/v1/bootstrap`, {
193
301
  method: 'POST',
@@ -246,7 +354,7 @@ async function waitForProvider(url, fetchImpl) {
246
354
  async function waitForWeb(url, pid, deps) {
247
355
  for (let attempt = 0; attempt < 100; attempt += 1) {
248
356
  if (!deps.isProcessAlive(pid)) return false;
249
- if (await deps.webHealth(url, pid, 3)) return true;
357
+ if (await deps.webHealth(url, pid, 4)) return true;
250
358
  await delay(100);
251
359
  }
252
360
  return false;
@@ -300,13 +408,6 @@ function spawnWebRuntime(input) {
300
408
  }
301
409
  }
302
410
 
303
- function stopOwnedLegacyRuntime(paths, deps) {
304
- deps.stopLegacyService();
305
- const state = deps.readState(paths.statePath);
306
- if (state?.version !== 1 || state.dbPath !== paths.dbPath || !Number.isInteger(state.pid)) return;
307
- if (deps.isProcessAlive(state.pid)) deps.stopProcess(state.pid);
308
- }
309
-
310
411
  function graphDependencies(overrides) {
311
412
  return {
312
413
  fetch: globalThis.fetch,
@@ -317,6 +418,7 @@ function graphDependencies(overrides) {
317
418
  ensureDirectory: (path) => mkdirSync(path, { recursive: true }),
318
419
  readConfig: (path) => readJsonFile(path, null),
319
420
  writeConfig: writeJsonFileAtomic,
421
+ writeRuntimeSettings,
320
422
  readState: (path) => readJsonFile(path, null),
321
423
  writeState: writeJsonFileAtomic,
322
424
  ensureContainerRuntime,
@@ -324,7 +426,6 @@ function graphDependencies(overrides) {
324
426
  spawnWebRuntime,
325
427
  isProcessAlive,
326
428
  stopProcess,
327
- stopLegacyService,
328
429
  webHealth: checkLocalConsoleHealth,
329
430
  discoverLanAddresses,
330
431
  createLanAccessToken: secret,
@@ -356,7 +457,7 @@ export async function checkLocalConsoleHealth(url, expectedPid, stateVersion = 3
356
457
  }
357
458
 
358
459
  function isGraphRuntimeState(state) {
359
- return (state?.version === 2 || state?.version === 3) &&
460
+ return (state?.version === 2 || state?.version === 3 || state?.version === 4) &&
360
461
  Number.isInteger(state.pid) && typeof state.url === 'string';
361
462
  }
362
463
 
@@ -384,15 +485,6 @@ function runtimeResult(status, state, lanAccessToken = null) {
384
485
  };
385
486
  }
386
487
 
387
- function stopLegacyService() {
388
- if (process.platform !== 'darwin') return;
389
- spawnSync('launchctl', ['remove', 'dev.jevonscode.fuli.local'], {
390
- encoding: 'utf8',
391
- windowsHide: true,
392
- stdio: 'ignore'
393
- });
394
- }
395
-
396
488
  function parseEnv(body) {
397
489
  return Object.fromEntries(body.split(/\r?\n/).filter(Boolean).map((line) => {
398
490
  const index = line.indexOf('=');
@@ -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
+ }
@@ -4,27 +4,41 @@ import { connectAgent, discoverAgents } from './agents.js';
4
4
  import { resolveSetupPaths } from './paths.js';
5
5
  import { ensureGraphRuntime } from './graph-runtime.js';
6
6
  import { installSessionSkill } from './session-skill.js';
7
+ import {
8
+ DEFAULT_RUNTIME_SETTINGS,
9
+ readRuntimeSettings,
10
+ runtimeSettingsWithOverrides
11
+ } from '../system/runtime-settings.js';
7
12
 
8
13
  export function planLocalSetup(options, {
9
14
  env = process.env,
10
15
  resolvePaths = resolveSetupPaths,
11
16
  discover = discoverAgents,
12
17
  inspectInstallations = inspectAgentInstallations,
18
+ readSettings = readRuntimeSettings,
13
19
  nodePath = process.execPath
14
20
  } = {}) {
15
21
  const paths = resolvePaths({ dataDir: options.dataDir, env });
22
+ const savedRuntimeSettings = paths.runtimeSettingsPath
23
+ ? readSettings(paths.runtimeSettingsPath)
24
+ : DEFAULT_RUNTIME_SETTINGS;
25
+ const runtimeSettings = runtimeSettingsWithOverrides(savedRuntimeSettings, {
26
+ consolePort: options.port
27
+ });
16
28
  const discovered = options.skipAgents ? [] : discover({ env });
17
29
  const agents = options.codexOnly
18
30
  ? discovered.filter(({ id }) => id === 'codex')
19
31
  : discovered;
20
32
  return {
21
33
  paths,
34
+ runtimeSettings,
22
35
  agents: inspectInstallations(agents, {
23
36
  nodePath,
24
37
  mcpServerPath: paths.mcpServerPath,
25
38
  runtimeConfigPath: paths.graphRuntimeConfigPath,
26
39
  sessionSkillPath: paths.sessionSkillPath,
27
- projectSkillPath: paths.projectSkillPath
40
+ projectSkillPath: paths.projectSkillPath,
41
+ reviewSkillPath: paths.reviewSkillPath
28
42
  })
29
43
  };
30
44
  }
@@ -34,10 +48,16 @@ export async function applyLocalSetup(plan, options, dependencies = {}) {
34
48
  const backupConfig = dependencies.backupConfig ?? backupAgentConfig;
35
49
  const connect = dependencies.connect ?? connectAgent;
36
50
  const installSkill = dependencies.installSkill ?? installSessionSkill;
51
+ const runtimeSettings = plan.runtimeSettings ?? runtimeSettingsWithOverrides(
52
+ DEFAULT_RUNTIME_SETTINGS,
53
+ { consolePort: options.port }
54
+ );
37
55
  const runtime = await ensureRuntime({
38
56
  paths: plan.paths,
39
57
  personalSpaceName: options.personalSpaceName,
40
- port: options.port,
58
+ port: runtimeSettings.ports.console,
59
+ lan: runtimeSettings.lanAccess,
60
+ runtimeSettings,
41
61
  personalOnly: options.personalOnly,
42
62
  noStart: options.noStart,
43
63
  env: dependencies.env ?? process.env,
@@ -64,6 +84,10 @@ export async function applyLocalSetup(plan, options, dependencies = {}) {
64
84
  installSkill({ ...agent, skillPath: agent.projectSkillPath }, {
65
85
  sourcePath: plan.paths.projectSkillPath,
66
86
  backupDir: plan.paths.backupDir
87
+ }),
88
+ installSkill({ ...agent, skillPath: agent.reviewSkillPath }, {
89
+ sourcePath: plan.paths.reviewSkillPath,
90
+ backupDir: plan.paths.backupDir
67
91
  })
68
92
  ];
69
93
  agents.push({ ...connected, backupPath, skills });
@@ -20,7 +20,8 @@ export function planLocalUninstall(options, {
20
20
  (agent.globalInstructionsOverridePath &&
21
21
  fileExists(agent.globalInstructionsOverridePath)) ||
22
22
  (agent.skillPath && fileExists(agent.skillPath)) ||
23
- (agent.projectSkillPath && fileExists(agent.projectSkillPath))
23
+ (agent.projectSkillPath && fileExists(agent.projectSkillPath)) ||
24
+ (agent.reviewSkillPath && fileExists(agent.reviewSkillPath))
24
25
  ));
25
26
  return { paths, agents };
26
27
  }
@@ -60,7 +61,8 @@ export async function applyLocalUninstall(plan, dependencies = {}) {
60
61
 
61
62
  for (const [skillPath, sourcePath] of [
62
63
  [agent.skillPath, plan.paths.sessionSkillPath],
63
- [agent.projectSkillPath, plan.paths.projectSkillPath]
64
+ [agent.projectSkillPath, plan.paths.projectSkillPath],
65
+ [agent.reviewSkillPath, plan.paths.reviewSkillPath]
64
66
  ]) {
65
67
  try {
66
68
  skills.push(removeSkill({ ...agent, skillPath }, { sourcePath }));