fuli-context 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +178 -0
  3. package/compose.graphiti.yml +86 -0
  4. package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +1 -0
  5. package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +1 -0
  6. package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +1 -0
  7. package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +1 -0
  8. package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +1 -0
  9. package/dist/web/assets/KnowledgePage-Khuo3cNs.js +1 -0
  10. package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +4 -0
  11. package/dist/web/assets/OverviewPage-DC4C4Fhn.js +1 -0
  12. package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +1 -0
  13. package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +1 -0
  14. package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +1 -0
  15. package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +1 -0
  16. package/dist/web/assets/ReviewPage-Dztk2jDG.js +1 -0
  17. package/dist/web/assets/console-tbvTUi6p.js +1 -0
  18. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  19. package/dist/web/assets/identity-B3s5CXhv.js +1 -0
  20. package/dist/web/assets/index-Devf7glZ.css +1 -0
  21. package/dist/web/assets/index-DwUDhQKc.js +6 -0
  22. package/dist/web/assets/model-DVT8sjy9.js +1 -0
  23. package/dist/web/assets/paths-B790iniO.js +1 -0
  24. package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +1 -0
  25. package/dist/web/assets/vue-router-BYTAoWT3.js +1 -0
  26. package/dist/web/index.html +22 -0
  27. package/graph-provider/Dockerfile +16 -0
  28. package/graph-provider/fuli_graph/__init__.py +5 -0
  29. package/graph-provider/fuli_graph/app.py +547 -0
  30. package/graph-provider/fuli_graph/auth.py +17 -0
  31. package/graph-provider/fuli_graph/collaboration_context.py +290 -0
  32. package/graph-provider/fuli_graph/config.py +34 -0
  33. package/graph-provider/fuli_graph/graph_models.py +123 -0
  34. package/graph-provider/fuli_graph/graph_projection.py +405 -0
  35. package/graph-provider/fuli_graph/graph_query.py +561 -0
  36. package/graph-provider/fuli_graph/knowledge_audit.py +765 -0
  37. package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +246 -0
  38. package/graph-provider/fuli_graph/knowledge_management.py +816 -0
  39. package/graph-provider/fuli_graph/knowledge_records.py +65 -0
  40. package/graph-provider/fuli_graph/knowledge_search.py +723 -0
  41. package/graph-provider/fuli_graph/knowledge_usage_models.py +52 -0
  42. package/graph-provider/fuli_graph/model_validation.py +58 -0
  43. package/graph-provider/fuli_graph/models.py +1179 -0
  44. package/graph-provider/fuli_graph/personal_project_access.py +29 -0
  45. package/graph-provider/fuli_graph/preference_conflicts.py +420 -0
  46. package/graph-provider/fuli_graph/project_action_models.py +142 -0
  47. package/graph-provider/fuli_graph/project_knowledge.py +707 -0
  48. package/graph-provider/fuli_graph/project_management.py +251 -0
  49. package/graph-provider/fuli_graph/provider_values.py +40 -0
  50. package/graph-provider/fuli_graph/runtime.py +350 -0
  51. package/graph-provider/fuli_graph/search_projection.py +156 -0
  52. package/graph-provider/fuli_graph/store.py +24 -0
  53. package/graph-provider/fuli_graph/store_identity.py +237 -0
  54. package/graph-provider/fuli_graph/store_knowledge.py +447 -0
  55. package/graph-provider/fuli_graph/store_projects.py +242 -0
  56. package/graph-provider/fuli_graph/store_publication.py +370 -0
  57. package/graph-provider/fuli_graph/store_records.py +134 -0
  58. package/graph-provider/pyproject.toml +35 -0
  59. package/npm-shrinkwrap.json +6045 -0
  60. package/package.json +99 -0
  61. package/skills/capturing-session-knowledge/SKILL.md +226 -0
  62. package/skills/capturing-session-knowledge/agents/openai.yaml +13 -0
  63. package/skills/grilling-project/SKILL.md +129 -0
  64. package/skills/grilling-project/agents/openai.yaml +4 -0
  65. package/skills/grilling-project/references/project-profile.md +105 -0
  66. package/src/agent/candidate-read-service.js +53 -0
  67. package/src/agent/context-pack-service.js +83 -0
  68. package/src/agent/public-read-policy.js +39 -0
  69. package/src/agent/public-read-service.js +69 -0
  70. package/src/agent/read-projection.js +107 -0
  71. package/src/agent/search-service.js +61 -0
  72. package/src/agent/space-boundary.js +33 -0
  73. package/src/agent-tools/graph-definitions.js +479 -0
  74. package/src/agent-tools/graph-handlers.js +96 -0
  75. package/src/agent-tools/schema.js +37 -0
  76. package/src/agent-tools.js +16 -0
  77. package/src/app/application-error.js +13 -0
  78. package/src/app/bootstrap.js +8 -0
  79. package/src/app/create-application.js +143 -0
  80. package/src/app/state-service.js +38 -0
  81. package/src/candidates.js +78 -0
  82. package/src/classifier.js +63 -0
  83. package/src/cli/command-arguments.js +14 -0
  84. package/src/cli/command-registry.js +78 -0
  85. package/src/cli/commands/candidates.js +17 -0
  86. package/src/cli/commands/ingestion.js +32 -0
  87. package/src/cli/commands/reads.js +50 -0
  88. package/src/cli/commands/spaces.js +20 -0
  89. package/src/cli/invocation.js +47 -0
  90. package/src/cli/local-runtime-command.js +94 -0
  91. package/src/cli/local-runtime-options.js +64 -0
  92. package/src/cli/migrate-command.js +71 -0
  93. package/src/cli/migration-destination.js +117 -0
  94. package/src/cli/setup-agent-selection.js +195 -0
  95. package/src/cli/setup-command.js +85 -0
  96. package/src/cli/uninstall-command.js +67 -0
  97. package/src/cli.js +90 -0
  98. package/src/context-pack.js +206 -0
  99. package/src/defaults.js +1 -0
  100. package/src/extractor.js +77 -0
  101. package/src/fact-history.js +39 -0
  102. package/src/facts/fact-service.js +89 -0
  103. package/src/graphiti/agent-access-policy.js +88 -0
  104. package/src/graphiti/capture-policy.js +84 -0
  105. package/src/graphiti/federated-application.js +1292 -0
  106. package/src/graphiti/personal-context.js +29 -0
  107. package/src/graphiti/project-path-context.js +159 -0
  108. package/src/graphiti/provider-client.js +288 -0
  109. package/src/graphiti/runtime-config.js +85 -0
  110. package/src/graphiti/setup.js +43 -0
  111. package/src/graphiti/source-marker.js +142 -0
  112. package/src/http/api-router.js +133 -0
  113. package/src/http/bounded-json-array.js +19 -0
  114. package/src/http/error-mapping.js +36 -0
  115. package/src/http/graph-api-router.js +257 -0
  116. package/src/http/lens-route.js +50 -0
  117. package/src/http/request-policy.js +41 -0
  118. package/src/http/response.js +29 -0
  119. package/src/http/static-handler.js +55 -0
  120. package/src/ingestion.js +161 -0
  121. package/src/lens/lens-correction-service.js +81 -0
  122. package/src/lens/lens-fact-writer.js +85 -0
  123. package/src/lens/lens-input.js +62 -0
  124. package/src/lens/lens-query-projection.js +87 -0
  125. package/src/lens/lens-query-ranking.js +62 -0
  126. package/src/lens/lens-query.js +143 -0
  127. package/src/lens/lens-resource-projection.js +93 -0
  128. package/src/lens/lens-resource-selection.js +45 -0
  129. package/src/lens/lens-resource-service.js +103 -0
  130. package/src/lens/lens-search-query.js +122 -0
  131. package/src/lens/lens-service.js +183 -0
  132. package/src/local-runtime/lifecycle.js +254 -0
  133. package/src/mcp/create-mcp-server.js +85 -0
  134. package/src/mcp/instructions.js +10 -0
  135. package/src/mcp/project-action-preview-tokens.js +72 -0
  136. package/src/mcp/runtime.js +71 -0
  137. package/src/mcp/tool-annotations.js +63 -0
  138. package/src/mcp/tool-result.js +167 -0
  139. package/src/mcp/tool-schema.js +60 -0
  140. package/src/mcp-server.js +76 -0
  141. package/src/models.js +44 -0
  142. package/src/observer.js +83 -0
  143. package/src/package-metadata.js +6 -0
  144. package/src/project-rules.js +33 -0
  145. package/src/publication/canonical-json.js +39 -0
  146. package/src/publication/outbox-service.js +46 -0
  147. package/src/publication/publication-envelope-schema.js +75 -0
  148. package/src/publication/publication-envelope.js +37 -0
  149. package/src/publication/publication-policy.js +82 -0
  150. package/src/publication/publication-service.js +48 -0
  151. package/src/router.js +58 -0
  152. package/src/runtime-options.js +139 -0
  153. package/src/security/episode-visibility.js +14 -0
  154. package/src/security/fact-visibility.js +15 -0
  155. package/src/security/sensitive-content.js +18 -0
  156. package/src/security/source-metadata-policy.js +8 -0
  157. package/src/server/application-lifecycle.js +34 -0
  158. package/src/server/listen.js +24 -0
  159. package/src/server.js +135 -0
  160. package/src/setup/agent-installation-status.js +97 -0
  161. package/src/setup/agents.js +123 -0
  162. package/src/setup/codex-bootstrap.js +118 -0
  163. package/src/setup/codex-config.js +122 -0
  164. package/src/setup/config-backup.js +22 -0
  165. package/src/setup/container-runtime.js +388 -0
  166. package/src/setup/cursor-config.js +61 -0
  167. package/src/setup/graph-runtime.js +360 -0
  168. package/src/setup/node-runtime.js +24 -0
  169. package/src/setup/options.js +91 -0
  170. package/src/setup/paths.js +47 -0
  171. package/src/setup/session-skill.js +91 -0
  172. package/src/setup/setup.js +85 -0
  173. package/src/setup/uninstall.js +90 -0
  174. package/src/storage/episode-evidence-preview.js +100 -0
  175. package/src/storage/fact-search-order.js +18 -0
  176. package/src/storage/file/episode-evidence.js +37 -0
  177. package/src/storage/file/episode-index.js +59 -0
  178. package/src/storage/file/fact-search.js +60 -0
  179. package/src/storage/file-store.js +437 -0
  180. package/src/storage/import-json.js +70 -0
  181. package/src/storage/json-file.js +41 -0
  182. package/src/storage/legacy-record-validation.js +152 -0
  183. package/src/storage/legacy-snapshot-validation.js +32 -0
  184. package/src/storage/migrate.js +128 -0
  185. package/src/storage/migrations/001-initial.sql +83 -0
  186. package/src/storage/migrations/002-storage-integrity.sql +113 -0
  187. package/src/storage/record-validation.js +46 -0
  188. package/src/storage/snapshot-defaults.js +54 -0
  189. package/src/storage/sqlite/candidates-repository.js +84 -0
  190. package/src/storage/sqlite/episode-evidence-repository.js +154 -0
  191. package/src/storage/sqlite/episode-privacy-sql.js +33 -0
  192. package/src/storage/sqlite/episodes-repository.js +52 -0
  193. package/src/storage/sqlite/fact-search-repository.js +162 -0
  194. package/src/storage/sqlite/facts-repository.js +146 -0
  195. package/src/storage/sqlite/imports-repository.js +39 -0
  196. package/src/storage/sqlite/mapper.js +84 -0
  197. package/src/storage/sqlite/open-database.js +15 -0
  198. package/src/storage/sqlite/outbox-repository.js +103 -0
  199. package/src/storage/sqlite/snapshot-repository.js +69 -0
  200. package/src/storage/sqlite/spaces-repository.js +52 -0
  201. package/src/storage/sqlite/subscriptions-repository.js +54 -0
  202. package/src/storage/sqlite-store.js +159 -0
  203. package/src/storage/stable-json.js +24 -0
  204. package/src/storage/store-port.js +20 -0
  205. package/src/store.js +2 -0
  206. package/src/workspace-protocol/array-validation.js +19 -0
  207. package/src/workspace-protocol/base64url-contract.js +27 -0
  208. package/src/workspace-protocol/constants.js +37 -0
  209. package/src/workspace-protocol/error-contract.js +24 -0
  210. package/src/workspace-protocol/fact-contract.js +110 -0
  211. package/src/workspace-protocol/governance-contract.js +47 -0
  212. package/src/workspace-protocol/index.js +53 -0
  213. package/src/workspace-protocol/json-budget.js +19 -0
  214. package/src/workspace-protocol/manifest-contract.js +186 -0
  215. package/src/workspace-protocol/publication-contract.js +212 -0
  216. package/src/workspace-protocol/query-contract.js +149 -0
  217. package/src/workspace-protocol/scalar-contract.js +192 -0
  218. package/src/workspace-protocol/sync-contract.js +184 -0
  219. package/src/workspace-protocol/text-contract.js +39 -0
  220. package/src/workspace-protocol/validation.js +66 -0
@@ -0,0 +1,360 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { randomBytes } from 'node:crypto';
3
+ import {
4
+ chmodSync,
5
+ closeSync,
6
+ existsSync,
7
+ mkdirSync,
8
+ openSync,
9
+ readFileSync,
10
+ writeFileSync
11
+ } from 'node:fs';
12
+ import { dirname } from 'node:path';
13
+
14
+ import {
15
+ ensureContainerRuntime,
16
+ runDockerCompose
17
+ } from './container-runtime.js';
18
+ import { readJsonFile, writeJsonFileAtomic } from '../storage/json-file.js';
19
+
20
+ export {
21
+ dockerInfoIndicatesDaemon,
22
+ selectDockerEnvironment
23
+ } from './container-runtime.js';
24
+
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
+ export async function ensureGraphRuntime(input, dependencies = {}) {
32
+ const deps = graphDependencies(dependencies);
33
+ const containerRuntime = await deps.ensureContainerRuntime({
34
+ env: input.env ?? process.env,
35
+ onProgress: input.onProgress
36
+ });
37
+ deps.ensureDirectory(input.paths.dataDir);
38
+ const secrets = ensureProviderSecrets(input.paths.graphEnvPath, deps);
39
+ deps.startProviders(input.paths, input.paths.graphEnvPath, {
40
+ personalOnly: input.personalOnly === true,
41
+ containerRuntime,
42
+ ...(input.buildProviders === false ? { build: false } : {})
43
+ });
44
+ await waitForProvider(PERSONAL_PROVIDER_URL, deps.fetch);
45
+ if (!input.personalOnly) await waitForProvider(WORKSPACE_PROVIDER_URL, deps.fetch);
46
+
47
+ let config = deps.readConfig(input.paths.graphRuntimeConfigPath);
48
+ if (!config) {
49
+ config = await bootstrapGraph({
50
+ personalSpaceName: input.personalSpaceName,
51
+ secrets,
52
+ personalOnly: input.personalOnly === true,
53
+ fetchImpl: deps.fetch
54
+ });
55
+ deps.writeConfig(input.paths.graphRuntimeConfigPath, config);
56
+ deps.secureFile(input.paths.graphRuntimeConfigPath);
57
+ }
58
+ stopOwnedLegacyRuntime(input.paths, deps);
59
+
60
+ if (input.noStart) return { status: 'initialized', url: null, pid: null };
61
+ const existing = deps.readState(input.paths.graphRuntimeStatePath);
62
+ if (isGraphRuntimeState(existing) && deps.isProcessAlive(existing.pid)) {
63
+ 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 };
66
+ }
67
+ if (!healthy) throw new Error('Recorded Fuli Graphiti runtime is not healthy');
68
+ deps.stopProcess(existing.pid);
69
+ }
70
+ const child = deps.spawnWebRuntime({
71
+ paths: input.paths,
72
+ nodePath: process.execPath,
73
+ port: input.port
74
+ });
75
+ const url = `http://127.0.0.1:${input.port}`;
76
+ if (!Number.isInteger(child.pid) || !await waitForWeb(url, child.pid, deps)) {
77
+ if (Number.isInteger(child.pid)) deps.stopProcess(child.pid);
78
+ throw new Error('Fuli Graphiti console could not start');
79
+ }
80
+ deps.writeState(input.paths.graphRuntimeStatePath, {
81
+ version: 3,
82
+ pid: child.pid,
83
+ url,
84
+ port: input.port,
85
+ managedProviders: input.personalOnly
86
+ ? ['personal']
87
+ : ['personal', 'development-workspace'],
88
+ startedAt: new Date().toISOString()
89
+ });
90
+ return { status: 'started', url, pid: child.pid };
91
+ }
92
+
93
+ function ensureProviderSecrets(path, deps) {
94
+ if (deps.fileExists(path)) return parseEnv(deps.readText(path));
95
+ 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()
100
+ };
101
+ const body = Object.entries(values).map(([key, value]) => `${key}=${value}`).join('\n') + '\n';
102
+ deps.writeText(path, body);
103
+ deps.secureFile(path);
104
+ return values;
105
+ }
106
+
107
+ async function bootstrapGraph({ personalSpaceName, secrets, personalOnly, fetchImpl }) {
108
+ const personalIdentity = await bootstrapProvider(
109
+ PERSONAL_PROVIDER_URL,
110
+ secrets.FULI_PERSONAL_BOOTSTRAP_TOKEN,
111
+ personalSpaceName,
112
+ fetchImpl
113
+ );
114
+ const personalSpace = await findOrCreateSpace(PERSONAL_PROVIDER_URL, {
115
+ token: personalIdentity.access_token,
116
+ name: personalSpaceName,
117
+ kind: 'personal',
118
+ fetchImpl
119
+ });
120
+ const config = {
121
+ version: 1,
122
+ personal: {
123
+ providerUrl: PERSONAL_PROVIDER_URL,
124
+ accessToken: personalIdentity.access_token,
125
+ principalId: personalIdentity.principal_id,
126
+ spaceId: personalSpace.id
127
+ },
128
+ workspaces: []
129
+ };
130
+ if (personalOnly) return config;
131
+
132
+ const workspaceIdentity = await bootstrapProvider(
133
+ WORKSPACE_PROVIDER_URL,
134
+ secrets.FULI_WORKSPACE_BOOTSTRAP_TOKEN,
135
+ personalSpaceName,
136
+ fetchImpl
137
+ );
138
+ const project = await findOrCreateSpace(WORKSPACE_PROVIDER_URL, {
139
+ token: workspaceIdentity.access_token,
140
+ name: '工作',
141
+ kind: 'project',
142
+ fetchImpl
143
+ });
144
+ await providerRequest(PERSONAL_PROVIDER_URL, '/v1/subscriptions', {
145
+ token: personalIdentity.access_token,
146
+ body: {
147
+ personal_space_id: personalSpace.id,
148
+ project_id: project.id,
149
+ provider_url: WORKSPACE_PROVIDER_URL,
150
+ project_name: project.name
151
+ },
152
+ fetchImpl
153
+ });
154
+ config.workspaces.push({
155
+ providerUrl: WORKSPACE_PROVIDER_URL,
156
+ accessToken: workspaceIdentity.access_token,
157
+ principalId: workspaceIdentity.principal_id
158
+ });
159
+ return config;
160
+ }
161
+
162
+ async function bootstrapProvider(url, bootstrapToken, principalName, fetchImpl) {
163
+ const response = await fetchImpl(`${url}/v1/bootstrap`, {
164
+ method: 'POST',
165
+ headers: {
166
+ 'content-type': 'application/json',
167
+ 'x-fuli-bootstrap-token': bootstrapToken
168
+ },
169
+ body: JSON.stringify({ principal_name: principalName })
170
+ });
171
+ if (!response.ok) throw new Error(`Could not bootstrap ${url}`);
172
+ return response.json();
173
+ }
174
+
175
+ async function providerRequest(baseUrl, path, { token, body, fetchImpl }) {
176
+ const response = await fetchImpl(`${baseUrl}${path}`, {
177
+ method: 'POST',
178
+ headers: {
179
+ authorization: `Bearer ${token}`,
180
+ 'content-type': 'application/json'
181
+ },
182
+ body: JSON.stringify(body)
183
+ });
184
+ if (!response.ok) throw new Error(`Provider setup failed at ${path}`);
185
+ return response.json();
186
+ }
187
+
188
+ async function findOrCreateSpace(baseUrl, { token, name, kind, fetchImpl }) {
189
+ const response = await fetchImpl(`${baseUrl}/v1/spaces`, {
190
+ headers: { authorization: `Bearer ${token}` }
191
+ });
192
+ if (!response.ok) throw new Error('Could not list Provider spaces during setup');
193
+ const existing = (await response.json()).find(
194
+ (space) => space.kind === kind && space.name === name
195
+ );
196
+ if (existing) return existing;
197
+ return providerRequest(baseUrl, '/v1/spaces', {
198
+ token,
199
+ body: { name, kind },
200
+ fetchImpl
201
+ });
202
+ }
203
+
204
+ async function waitForProvider(url, fetchImpl) {
205
+ for (let attempt = 0; attempt < 120; attempt += 1) {
206
+ try {
207
+ const response = await fetchImpl(`${url}/health`);
208
+ if (response.ok) return;
209
+ } catch {
210
+ // Containers and Graphiti indices may still be starting.
211
+ }
212
+ await delay(1000);
213
+ }
214
+ throw new Error(`Graphiti provider did not become ready: ${url}`);
215
+ }
216
+
217
+ async function waitForWeb(url, pid, deps) {
218
+ for (let attempt = 0; attempt < 100; attempt += 1) {
219
+ if (!deps.isProcessAlive(pid)) return false;
220
+ if (await deps.webHealth(url, pid, 3)) return true;
221
+ await delay(100);
222
+ }
223
+ return false;
224
+ }
225
+
226
+ export function startGraphProviders(paths, envPath, options = {}) {
227
+ const args = [
228
+ 'compose',
229
+ '--env-file', envPath,
230
+ '-f', paths.graphComposePath,
231
+ 'up', '-d'
232
+ ];
233
+ if (options.build !== false) args.push('--build');
234
+ if (options.personalOnly) args.push('personal-neo4j', 'personal-provider');
235
+ runDockerCompose(args, options.containerRuntime);
236
+ }
237
+
238
+ export function stopGraphProviders(paths, envPath, options = {}) {
239
+ const args = [
240
+ 'compose',
241
+ '--env-file', envPath,
242
+ '-f', paths.graphComposePath,
243
+ 'stop'
244
+ ];
245
+ if (options.personalOnly) args.push('personal-provider', 'personal-neo4j');
246
+ runDockerCompose(args);
247
+ }
248
+
249
+ function spawnWebRuntime(input) {
250
+ mkdirSync(dirname(input.paths.logPath), { recursive: true });
251
+ const log = openSync(input.paths.logPath, 'a');
252
+ try {
253
+ const child = spawn(input.nodePath, [
254
+ input.paths.serverPath,
255
+ '--runtime-config', input.paths.graphRuntimeConfigPath,
256
+ '--port', String(input.port)
257
+ ], {
258
+ detached: true,
259
+ windowsHide: true,
260
+ stdio: ['ignore', log, log]
261
+ });
262
+ child.unref();
263
+ return child;
264
+ } finally {
265
+ closeSync(log);
266
+ }
267
+ }
268
+
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
+ function graphDependencies(overrides) {
277
+ return {
278
+ fetch: globalThis.fetch,
279
+ fileExists: existsSync,
280
+ readText: (path) => readFileSync(path, 'utf8'),
281
+ writeText: (path, value) => writeFileSync(path, value, { mode: 0o600 }),
282
+ secureFile: (path) => chmodSync(path, 0o600),
283
+ ensureDirectory: (path) => mkdirSync(path, { recursive: true }),
284
+ readConfig: (path) => readJsonFile(path, null),
285
+ writeConfig: writeJsonFileAtomic,
286
+ readState: (path) => readJsonFile(path, null),
287
+ writeState: writeJsonFileAtomic,
288
+ ensureContainerRuntime,
289
+ startProviders: startGraphProviders,
290
+ spawnWebRuntime,
291
+ isProcessAlive,
292
+ stopProcess,
293
+ stopLegacyService,
294
+ webHealth: checkLocalConsoleHealth,
295
+ ...overrides
296
+ };
297
+ }
298
+
299
+ export async function checkLocalConsoleHealth(url, expectedPid, stateVersion = 3) {
300
+ try {
301
+ const response = await fetch(`${url}/api/health`, {
302
+ signal: AbortSignal.timeout(2500)
303
+ });
304
+ if (response.ok) {
305
+ const body = await response.json();
306
+ return body?.service === 'fuli-local-console' &&
307
+ (!Number.isInteger(expectedPid) || body.pid === expectedPid);
308
+ }
309
+ if (response.status !== 404 || stateVersion !== 2) return false;
310
+ } catch {
311
+ return false;
312
+ }
313
+
314
+ try {
315
+ return (await fetch(`${url}/api/state`, { signal: AbortSignal.timeout(2500) })).ok;
316
+ } catch {
317
+ return false;
318
+ }
319
+ }
320
+
321
+ function isGraphRuntimeState(state) {
322
+ return (state?.version === 2 || state?.version === 3) &&
323
+ Number.isInteger(state.pid) && typeof state.url === 'string';
324
+ }
325
+
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
+ });
333
+ }
334
+
335
+ function parseEnv(body) {
336
+ return Object.fromEntries(body.split(/\r?\n/).filter(Boolean).map((line) => {
337
+ const index = line.indexOf('=');
338
+ return [line.slice(0, index), line.slice(index + 1)];
339
+ }));
340
+ }
341
+
342
+ function nonEmpty(value) {
343
+ return typeof value === 'string' && value.trim().length > 0;
344
+ }
345
+
346
+ function secret() {
347
+ return randomBytes(32).toString('base64url');
348
+ }
349
+
350
+ function isProcessAlive(pid) {
351
+ try { process.kill(pid, 0); return true; } catch { return false; }
352
+ }
353
+
354
+ function stopProcess(pid) {
355
+ try { process.kill(pid, 'SIGTERM'); } catch { /* already stopped */ }
356
+ }
357
+
358
+ function delay(ms) {
359
+ return new Promise((resolve) => setTimeout(resolve, ms));
360
+ }
@@ -0,0 +1,24 @@
1
+ const MINIMUM_NODE_VERSION = Object.freeze([24, 12, 0]);
2
+
3
+ export function assertSupportedNodeVersion(version = process.versions.node) {
4
+ const parsed = parseNodeVersion(version);
5
+ if (compareVersions(parsed, MINIMUM_NODE_VERSION) >= 0) return;
6
+ throw new Error(
7
+ `fl setup 需要 Node.js 24.12 或更高版本;当前为 ${version}。` +
8
+ `请升级 Node.js 后重新运行 fl setup。`
9
+ );
10
+ }
11
+
12
+ function parseNodeVersion(value) {
13
+ const match = String(value).match(/^v?(\d+)\.(\d+)\.(\d+)/);
14
+ if (!match) throw new Error(`无法识别 Node.js 版本:${value}`);
15
+ return match.slice(1).map(Number);
16
+ }
17
+
18
+ function compareVersions(left, right) {
19
+ for (let index = 0; index < right.length; index += 1) {
20
+ const difference = left[index] - right[index];
21
+ if (difference !== 0) return difference;
22
+ }
23
+ return 0;
24
+ }
@@ -0,0 +1,91 @@
1
+ import { DEFAULT_FULI_PORT } from '../defaults.js';
2
+
3
+ const VALUE_OPTIONS = Object.freeze({
4
+ '--data-dir': 'dataDir',
5
+ '--personal-space': 'personalSpaceName',
6
+ '--port': 'port'
7
+ });
8
+
9
+ const BOOLEAN_OPTIONS = Object.freeze({
10
+ '--yes': 'yes',
11
+ '--codex-only': 'codexOnly',
12
+ '--skip-agents': 'skipAgents',
13
+ '--no-start': 'noStart'
14
+ });
15
+
16
+ const PROVIDER_MODE_OPTIONS = new Set(['--personal-only', '--with-dev-public']);
17
+
18
+ export function parseSetupOptions(args = []) {
19
+ const result = {
20
+ dataDir: null,
21
+ personalSpaceName: '我',
22
+ port: DEFAULT_FULI_PORT,
23
+ yes: false,
24
+ codexOnly: false,
25
+ skipAgents: false,
26
+ personalOnly: true,
27
+ noStart: false
28
+ };
29
+ const seen = new Set();
30
+
31
+ for (let index = 0; index < args.length; index += 1) {
32
+ const flag = args[index];
33
+ if (seen.has(flag)) throw new TypeError(`Duplicate ${flag}`);
34
+ if (PROVIDER_MODE_OPTIONS.has(flag)) {
35
+ const conflicting = flag === '--personal-only' ? '--with-dev-public' : '--personal-only';
36
+ if (seen.has(conflicting)) {
37
+ throw new TypeError('--personal-only and --with-dev-public cannot be combined');
38
+ }
39
+ result.personalOnly = flag !== '--with-dev-public';
40
+ seen.add(flag);
41
+ continue;
42
+ }
43
+ const booleanKey = BOOLEAN_OPTIONS[flag];
44
+ if (booleanKey) {
45
+ result[booleanKey] = true;
46
+ seen.add(flag);
47
+ continue;
48
+ }
49
+ const valueKey = VALUE_OPTIONS[flag];
50
+ if (!valueKey) throw new TypeError(`Unknown setup option: ${flag}`);
51
+ const value = args[index + 1];
52
+ if (typeof value !== 'string' || !value.trim() || value.startsWith('--')) {
53
+ throw new TypeError(`Missing value for ${flag}`);
54
+ }
55
+ result[valueKey] = valueKey === 'port' ? parsePort(value) : value.trim();
56
+ seen.add(flag);
57
+ index += 1;
58
+ }
59
+ return result;
60
+ }
61
+
62
+ export function parseUninstallOptions(args = []) {
63
+ const result = { dataDir: null, yes: false };
64
+ const seen = new Set();
65
+ for (let index = 0; index < args.length; index += 1) {
66
+ const flag = args[index];
67
+ if (seen.has(flag)) throw new TypeError(`Duplicate ${flag}`);
68
+ if (flag === '--yes') {
69
+ result.yes = true;
70
+ seen.add(flag);
71
+ continue;
72
+ }
73
+ if (flag !== '--data-dir') throw new TypeError(`Unknown uninstall option: ${flag}`);
74
+ const value = args[index + 1];
75
+ if (typeof value !== 'string' || !value.trim() || value.startsWith('--')) {
76
+ throw new TypeError('Missing value for --data-dir');
77
+ }
78
+ result.dataDir = value.trim();
79
+ seen.add(flag);
80
+ index += 1;
81
+ }
82
+ return result;
83
+ }
84
+
85
+ function parsePort(value) {
86
+ const port = Number(value);
87
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
88
+ throw new TypeError('--port must be between 1 and 65535');
89
+ }
90
+ return port;
91
+ }
@@ -0,0 +1,47 @@
1
+ import { homedir } from 'node:os';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const DEFAULT_PACKAGE_ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
6
+
7
+ export function resolveSetupPaths({
8
+ dataDir = null,
9
+ cwd = process.cwd(),
10
+ platform = process.platform,
11
+ env = process.env,
12
+ homeDir = homedir(),
13
+ packageRoot = DEFAULT_PACKAGE_ROOT
14
+ } = {}) {
15
+ const resolvedDataDir = resolve(cwd, dataDir ?? defaultDataDir({ platform, env, homeDir }));
16
+ const root = resolve(packageRoot);
17
+ return {
18
+ dataDir: resolvedDataDir,
19
+ dbPath: join(resolvedDataDir, 'context.db'),
20
+ graphEnvPath: join(resolvedDataDir, 'graph-provider.env'),
21
+ graphRuntimeConfigPath: join(resolvedDataDir, 'graph-runtime.json'),
22
+ graphRuntimeStatePath: join(resolvedDataDir, 'graph-runtime-state.json'),
23
+ backupDir: join(resolvedDataDir, 'backups', 'agents'),
24
+ logPath: join(resolvedDataDir, 'logs', 'runtime.log'),
25
+ statePath: join(resolvedDataDir, 'runtime.json'),
26
+ serverPath: join(root, 'src', 'server.js'),
27
+ mcpServerPath: join(root, 'src', 'mcp-server.js'),
28
+ graphSetupPath: join(root, 'src', 'graphiti', 'setup.js'),
29
+ graphComposePath: join(root, 'compose.graphiti.yml'),
30
+ sessionSkillPath: join(root, 'skills', 'capturing-session-knowledge'),
31
+ projectSkillPath: join(root, 'skills', 'grilling-project')
32
+ };
33
+ }
34
+
35
+ function defaultDataDir({ platform, env, homeDir }) {
36
+ if (platform === 'win32') {
37
+ return join(nonEmpty(env.LOCALAPPDATA) ?? join(homeDir, 'AppData', 'Local'), 'Fuli');
38
+ }
39
+ if (platform === 'darwin') {
40
+ return join(homeDir, 'Library', 'Application Support', 'Fuli');
41
+ }
42
+ return join(nonEmpty(env.XDG_DATA_HOME) ?? join(homeDir, '.local', 'share'), 'fuli');
43
+ }
44
+
45
+ function nonEmpty(value) {
46
+ return typeof value === 'string' && value.trim() ? value : null;
47
+ }
@@ -0,0 +1,91 @@
1
+ import {
2
+ cpSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readdirSync,
6
+ readFileSync,
7
+ renameSync,
8
+ rmSync
9
+ } from 'node:fs';
10
+ import { basename, dirname, join } from 'node:path';
11
+
12
+ export function installSessionSkill(agent, {
13
+ sourcePath,
14
+ backupDir,
15
+ clock = () => new Date()
16
+ }) {
17
+ if (!agent.skillPath) throw new TypeError(`Missing Skill path for ${agent.label}`);
18
+ const sourceEntry = join(sourcePath, 'SKILL.md');
19
+ if (!existsSync(sourceEntry)) throw new Error('Fuli session Skill source is missing');
20
+
21
+ if (isSessionSkillCurrent(sourcePath, agent.skillPath)) {
22
+ return { status: 'current', path: agent.skillPath, backupPath: null };
23
+ }
24
+
25
+ mkdirSync(dirname(agent.skillPath), { recursive: true });
26
+ let backupPath = null;
27
+ if (existsSync(agent.skillPath)) {
28
+ const stamp = clock().toISOString().replaceAll(/[:.]/g, '-');
29
+ backupPath = join(backupDir, 'skills', `${agent.id}-${stamp}`, basename(agent.skillPath));
30
+ mkdirSync(dirname(backupPath), { recursive: true });
31
+ renameSync(agent.skillPath, backupPath);
32
+ }
33
+
34
+ const stagingPath = `${agent.skillPath}.fuli-installing`;
35
+ rmSync(stagingPath, { recursive: true, force: true });
36
+ try {
37
+ cpSync(sourcePath, stagingPath, { recursive: true, errorOnExist: true });
38
+ renameSync(stagingPath, agent.skillPath);
39
+ } catch (error) {
40
+ rmSync(stagingPath, { recursive: true, force: true });
41
+ if (backupPath && !existsSync(agent.skillPath)) renameSync(backupPath, agent.skillPath);
42
+ throw error;
43
+ }
44
+
45
+ return { status: 'installed', path: agent.skillPath, backupPath };
46
+ }
47
+
48
+ export function isSessionSkillCurrent(sourcePath, targetPath) {
49
+ const sourceEntry = join(sourcePath, 'SKILL.md');
50
+ const targetEntry = join(targetPath, 'SKILL.md');
51
+ if (!existsSync(sourceEntry) || !existsSync(targetEntry)) return false;
52
+ try {
53
+ const sourceFiles = listFiles(sourcePath);
54
+ const targetFiles = listFiles(targetPath);
55
+ if (
56
+ sourceFiles.length !== targetFiles.length ||
57
+ sourceFiles.some((file, index) => file !== targetFiles[index])
58
+ ) {
59
+ return false;
60
+ }
61
+ return sourceFiles.every((file) => (
62
+ readFileSync(join(sourcePath, file)).equals(readFileSync(join(targetPath, file)))
63
+ ));
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+
69
+ export function removeBundledSkill(agent, {
70
+ sourcePath,
71
+ fileExists = existsSync,
72
+ remove = rmSync
73
+ }) {
74
+ if (!agent.skillPath || !fileExists(agent.skillPath)) {
75
+ return { status: 'not_installed', path: agent.skillPath ?? null };
76
+ }
77
+ if (!isSessionSkillCurrent(sourcePath, agent.skillPath)) {
78
+ return { status: 'preserved_modified', path: agent.skillPath };
79
+ }
80
+ remove(agent.skillPath, { recursive: true, force: false });
81
+ return { status: 'removed', path: agent.skillPath };
82
+ }
83
+
84
+ function listFiles(root, directory = '') {
85
+ return readdirSync(join(root, directory), { withFileTypes: true })
86
+ .flatMap((entry) => {
87
+ const relativePath = join(directory, entry.name);
88
+ return entry.isDirectory() ? listFiles(root, relativePath) : [relativePath];
89
+ })
90
+ .sort();
91
+ }
@@ -0,0 +1,85 @@
1
+ import { backupAgentConfig } from './config-backup.js';
2
+ import { inspectAgentInstallations } from './agent-installation-status.js';
3
+ import { connectAgent, discoverAgents } from './agents.js';
4
+ import { resolveSetupPaths } from './paths.js';
5
+ import { ensureGraphRuntime } from './graph-runtime.js';
6
+ import { installSessionSkill } from './session-skill.js';
7
+
8
+ export function planLocalSetup(options, {
9
+ env = process.env,
10
+ resolvePaths = resolveSetupPaths,
11
+ discover = discoverAgents,
12
+ inspectInstallations = inspectAgentInstallations,
13
+ nodePath = process.execPath
14
+ } = {}) {
15
+ const paths = resolvePaths({ dataDir: options.dataDir, env });
16
+ const discovered = options.skipAgents ? [] : discover({ env });
17
+ const agents = options.codexOnly
18
+ ? discovered.filter(({ id }) => id === 'codex')
19
+ : discovered;
20
+ return {
21
+ paths,
22
+ agents: inspectInstallations(agents, {
23
+ nodePath,
24
+ mcpServerPath: paths.mcpServerPath,
25
+ runtimeConfigPath: paths.graphRuntimeConfigPath,
26
+ sessionSkillPath: paths.sessionSkillPath,
27
+ projectSkillPath: paths.projectSkillPath
28
+ })
29
+ };
30
+ }
31
+
32
+ export async function applyLocalSetup(plan, options, dependencies = {}) {
33
+ const ensureRuntime = dependencies.ensureRuntime ?? ensureGraphRuntime;
34
+ const backupConfig = dependencies.backupConfig ?? backupAgentConfig;
35
+ const connect = dependencies.connect ?? connectAgent;
36
+ const installSkill = dependencies.installSkill ?? installSessionSkill;
37
+ const runtime = await ensureRuntime({
38
+ paths: plan.paths,
39
+ personalSpaceName: options.personalSpaceName,
40
+ port: options.port,
41
+ personalOnly: options.personalOnly,
42
+ noStart: options.noStart,
43
+ env: dependencies.env ?? process.env,
44
+ onProgress: dependencies.onProgress
45
+ });
46
+ const context = {
47
+ nodePath: dependencies.nodePath ?? process.execPath,
48
+ mcpServerPath: plan.paths.mcpServerPath,
49
+ runtimeConfigPath: plan.paths.graphRuntimeConfigPath
50
+ };
51
+ const agents = [];
52
+
53
+ for (const agent of plan.agents.filter(
54
+ ({ available, selected }) => available && selected !== false
55
+ )) {
56
+ try {
57
+ const backupPath = backupConfig(agent, { backupDir: plan.paths.backupDir });
58
+ const connected = connect(agent, context);
59
+ const skills = [
60
+ installSkill(agent, {
61
+ sourcePath: plan.paths.sessionSkillPath,
62
+ backupDir: plan.paths.backupDir
63
+ }),
64
+ installSkill({ ...agent, skillPath: agent.projectSkillPath }, {
65
+ sourcePath: plan.paths.projectSkillPath,
66
+ backupDir: plan.paths.backupDir
67
+ })
68
+ ];
69
+ agents.push({ ...connected, backupPath, skills });
70
+ } catch (error) {
71
+ agents.push({
72
+ id: agent.id,
73
+ label: agent.label,
74
+ status: 'failed',
75
+ message: error instanceof Error ? error.message : 'Agent connection failed'
76
+ });
77
+ }
78
+ }
79
+
80
+ return {
81
+ status: agents.some(({ status }) => status === 'failed') ? 'partial' : 'ready',
82
+ runtime,
83
+ agents
84
+ };
85
+ }