fuli-context 0.7.4-beta.3 → 0.7.6

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 (130) hide show
  1. package/README.md +77 -6
  2. package/README.zh-CN.md +69 -6
  3. package/compose.graphiti.yml +6 -6
  4. package/dist/web/assets/AboutPage-sLV9WChi.js +1 -0
  5. package/dist/web/assets/ConnectionsPage-BWn4ywGJ.js +1 -0
  6. package/dist/web/assets/KnowledgeEditDialog-CZ5ziPrD.js +1 -0
  7. package/dist/web/assets/KnowledgeInspector-CRQWOaos.js +1 -0
  8. package/dist/web/assets/KnowledgeOrganizerPage-Dfj3vIpU.js +1 -0
  9. package/dist/web/assets/KnowledgePage-BRYC9S5Y.js +1 -0
  10. package/dist/web/assets/KnowledgeWorkspace-ChEB5Bwk.js +4 -0
  11. package/dist/web/assets/OverviewPage-Cd66M0RT.js +1 -0
  12. package/dist/web/assets/PersonalProfilePage-RCFHL0Wz.js +1 -0
  13. package/dist/web/assets/PersonalProjectsPage-DEh8XWby.js +1 -0
  14. package/dist/web/assets/ProjectAgentsPage-Crh7PrXa.css +1 -0
  15. package/dist/web/assets/ProjectAgentsPage-DPnaH-TR.js +11 -0
  16. package/dist/web/assets/PublicProjectsPage-mstPEjHe.js +1 -0
  17. package/dist/web/assets/ReviewPage-6Y1SmfZy.js +1 -0
  18. package/dist/web/assets/SearchableSelect-D3HM8YF8.js +1 -0
  19. package/dist/web/assets/SettingsPage--LdTMUm7.js +1 -0
  20. package/dist/web/assets/SettingsPage-CwZp2ka4.css +1 -0
  21. package/dist/web/assets/WritingTastePage-CIpKswvs.js +1 -0
  22. package/dist/web/assets/{console-D_2zC4lm.js → console-BT1P2Db4.js} +1 -1
  23. package/dist/web/assets/{conversation-launcher-settings-DQmMH2j7.js → conversation-launcher-settings-DrJEotdf.js} +1 -1
  24. package/dist/web/assets/i18n-js6gfc7j.js +9 -0
  25. package/dist/web/assets/{identity-DmGcY3Y6.js → identity-CfDfanpa.js} +1 -1
  26. package/dist/web/assets/index-BnH5mcZx.css +1 -0
  27. package/dist/web/assets/index-GqJf1xqg.js +2 -0
  28. package/dist/web/assets/{model-CVoOdRsL.js → model-DJkLAJsg.js} +1 -1
  29. package/dist/web/assets/{useApi-CROJJdhE-CBNg4CdH.js → useApi-CROJJdhE-BdSti7Y2.js} +1 -1
  30. package/dist/web/assets/vue-router-Kk67EaCP.js +1 -0
  31. package/dist/web/index.html +6 -6
  32. package/graph-provider/fuli_graph/app.py +8 -0
  33. package/graph-provider/fuli_graph/collaboration_context.py +69 -6
  34. package/graph-provider/fuli_graph/knowledge_search.py +84 -5
  35. package/graph-provider/fuli_graph/model_validation.py +130 -0
  36. package/graph-provider/fuli_graph/models.py +24 -5
  37. package/graph-provider/fuli_graph/project_agent_access.py +54 -0
  38. package/graph-provider/fuli_graph/project_agent_coordination_models.py +22 -0
  39. package/graph-provider/fuli_graph/project_agent_executor_models.py +635 -0
  40. package/graph-provider/fuli_graph/project_agent_models.py +278 -0
  41. package/graph-provider/fuli_graph/project_agent_routes.py +626 -0
  42. package/graph-provider/fuli_graph/project_agent_task_models.py +545 -0
  43. package/graph-provider/fuli_graph/runtime.py +259 -0
  44. package/graph-provider/fuli_graph/search_projection.py +3 -0
  45. package/graph-provider/fuli_graph/store.py +6 -0
  46. package/graph-provider/fuli_graph/store_knowledge.py +62 -9
  47. package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +122 -0
  48. package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +756 -0
  49. package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +601 -0
  50. package/graph-provider/fuli_graph/store_project_agent_executors.py +1087 -0
  51. package/graph-provider/fuli_graph/store_project_agent_task_reads.py +458 -0
  52. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +705 -0
  53. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +586 -0
  54. package/graph-provider/fuli_graph/store_project_agent_tasks.py +1151 -0
  55. package/graph-provider/fuli_graph/store_project_agents.py +965 -0
  56. package/graph-provider/pyproject.toml +2 -1
  57. package/npm-shrinkwrap.json +2 -2
  58. package/package.json +1 -1
  59. package/skills/capturing-session-knowledge/SKILL.md +39 -0
  60. package/src/adaptive-runtime/executor-pool.js +127 -0
  61. package/src/adaptive-runtime/graph-services.js +166 -0
  62. package/src/adaptive-runtime/lease-client.js +335 -0
  63. package/src/adaptive-runtime/runtime-broker.js +318 -0
  64. package/src/adaptive-runtime/settings.js +111 -0
  65. package/src/adaptive-runtime/state.js +74 -0
  66. package/src/agent-tools/graph-definitions.js +419 -0
  67. package/src/agent-tools/graph-handlers.js +65 -0
  68. package/src/agent-tools/project-agent-definitions.js +307 -0
  69. package/src/backup/graph-backup.js +289 -0
  70. package/src/backup/identity-reconciliation.js +208 -0
  71. package/src/backup/runtime-adapters.js +269 -0
  72. package/src/cli/command-registry.js +6 -2
  73. package/src/cli/graph-data-command.js +112 -0
  74. package/src/cli/local-runtime-command.js +16 -2
  75. package/src/cli/setup-command.js +19 -3
  76. package/src/cli/update-command.js +4 -0
  77. package/src/cli.js +6 -0
  78. package/src/graphiti/collaboration-preference-projection.js +29 -1
  79. package/src/graphiti/collaboration-preference-workflow.js +121 -0
  80. package/src/graphiti/federated-application.js +53 -90
  81. package/src/graphiti/project-agent-control-plane.js +275 -0
  82. package/src/graphiti/project-agent-mapping.js +1024 -0
  83. package/src/graphiti/project-agent-workflows.js +576 -0
  84. package/src/graphiti/provider-client.js +282 -1
  85. package/src/http/api-router.js +63 -9
  86. package/src/http/graph-api-router.js +290 -0
  87. package/src/local-runtime/lifecycle.js +46 -10
  88. package/src/mcp/context-resources.js +15 -6
  89. package/src/mcp/create-mcp-server.js +51 -16
  90. package/src/mcp/instructions.js +7 -5
  91. package/src/mcp/runtime.js +17 -5
  92. package/src/mcp/session-id.js +38 -1
  93. package/src/mcp/tool-annotations.js +71 -0
  94. package/src/mcp-server.js +23 -6
  95. package/src/native-runtime/runtime.js +717 -0
  96. package/src/server.js +6 -2
  97. package/src/setup/agent-installation-status.js +7 -1
  98. package/src/setup/agents.js +3 -1
  99. package/src/setup/claude-code-config.js +3 -1
  100. package/src/setup/codex-bootstrap.js +11 -10
  101. package/src/setup/codex-config.js +7 -1
  102. package/src/setup/cursor-config.js +3 -1
  103. package/src/setup/graph-runtime.js +158 -13
  104. package/src/setup/neo4j-memory-profile.js +43 -0
  105. package/src/setup/options.js +38 -2
  106. package/src/setup/paths.js +14 -0
  107. package/src/setup/setup.js +37 -1
  108. package/src/system/resource-monitor.js +181 -16
  109. package/src/system/runtime-settings.js +10 -1
  110. package/src/system/system-service.js +27 -2
  111. package/dist/web/assets/AboutPage-Dpf5fRWv.js +0 -1
  112. package/dist/web/assets/ConnectionsPage-D21aUDCP.js +0 -1
  113. package/dist/web/assets/KnowledgeEditDialog-u7lpfN3j.js +0 -1
  114. package/dist/web/assets/KnowledgeInspector-k4xnHtXd.js +0 -1
  115. package/dist/web/assets/KnowledgeOrganizerPage-Bdj7Rn84.js +0 -1
  116. package/dist/web/assets/KnowledgePage-UHVAbC1W.js +0 -1
  117. package/dist/web/assets/KnowledgeWorkspace-Lq3d-MeC.js +0 -4
  118. package/dist/web/assets/OverviewPage-CycAgA1V.js +0 -1
  119. package/dist/web/assets/PersonalProfilePage-U9oIV5FT.js +0 -1
  120. package/dist/web/assets/PersonalProjectsPage-C5RpXKcA.js +0 -1
  121. package/dist/web/assets/PublicProjectsPage-DiWxm35E.js +0 -1
  122. package/dist/web/assets/ReviewPage-jxRfGSJK.js +0 -1
  123. package/dist/web/assets/SearchableSelect-B3-5IPup.js +0 -1
  124. package/dist/web/assets/SettingsPage-BqfGDOho.js +0 -1
  125. package/dist/web/assets/SettingsPage-CAfz4qiV.css +0 -1
  126. package/dist/web/assets/WritingTastePage-BJxhMqJR.js +0 -1
  127. package/dist/web/assets/i18n-CwqXDI1C.js +0 -5
  128. package/dist/web/assets/index-IUGLt4lG.js +0 -2
  129. package/dist/web/assets/index-X2j29j8i.css +0 -1
  130. package/dist/web/assets/vue-router-CWSsUUio.js +0 -1
@@ -0,0 +1,717 @@
1
+ import { execFile, spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import {
4
+ chmodSync,
5
+ closeSync,
6
+ createReadStream,
7
+ createWriteStream,
8
+ existsSync,
9
+ mkdirSync,
10
+ openSync,
11
+ readFileSync,
12
+ unlinkSync
13
+ } from 'node:fs';
14
+ import {
15
+ mkdir,
16
+ readFile,
17
+ rename,
18
+ rm,
19
+ stat,
20
+ writeFile
21
+ } from 'node:fs/promises';
22
+ import { Readable } from 'node:stream';
23
+ import { pipeline } from 'node:stream/promises';
24
+ import { createConnection } from 'node:net';
25
+ import { dirname, join } from 'node:path';
26
+
27
+ import { FULI_VERSION } from '../package-metadata.js';
28
+ import { readJsonFile, writeJsonFileAtomic } from '../storage/json-file.js';
29
+ import {
30
+ DEFAULT_RUNTIME_SETTINGS,
31
+ managedProviderUrls,
32
+ readRuntimeSettings
33
+ } from '../system/runtime-settings.js';
34
+
35
+ export const NATIVE_NEO4J_VERSION = '5.26.28';
36
+ export const NATIVE_NEO4J_SHA256 =
37
+ '9d4064cdd87627cae376a741c893848c4faa3c4fb980362b6dae541c203e8072';
38
+ export const NATIVE_NEO4J_URL =
39
+ `https://dist.neo4j.org/neo4j-community-${NATIVE_NEO4J_VERSION}-unix.tar.gz`;
40
+
41
+ const MANIFEST_VERSION = 1;
42
+ const PROVIDER_RUNTIME_REVISION = 2;
43
+ const COMMAND_TIMEOUT_MS = 10 * 60_000;
44
+ const DOWNLOAD_TIMEOUT_MS = 30 * 60_000;
45
+
46
+ export function createNativeGraphServices({
47
+ paths,
48
+ runtimeDescriptor = null,
49
+ personalOnly = null,
50
+ readText = (path) => readFile(path, 'utf8'),
51
+ writeText = secureWriteText,
52
+ pathExists = existsSync,
53
+ markInitialized = secureMarker,
54
+ readJson = (path) => readJsonFile(path, null),
55
+ readSettings = readRuntimeSettings,
56
+ run = runCommand,
57
+ waitForDatabase = waitForTcp,
58
+ spawnDatabase = spawnNativeDatabase,
59
+ spawnProvider = spawnNativeProvider,
60
+ readProcessState = (path) => readJsonFile(path, null),
61
+ writeProcessState = writeJsonFileAtomic,
62
+ processMatches = processMatchesCommand,
63
+ stopProcess = stopOwnedProcess,
64
+ fetchImpl = globalThis.fetch,
65
+ wait = delay,
66
+ now = () => new Date()
67
+ }) {
68
+ const descriptor = runtimeDescriptor ?? readJson(paths.nativeRuntimeManifestPath);
69
+
70
+ async function start({ providers: startProviders = true } = {}) {
71
+ assertDescriptor(descriptor);
72
+ const specs = await serviceSpecs();
73
+ const previousState = readProcessState(paths.nativeProcessStatePath) ?? {};
74
+ const databases = {};
75
+ const providers = {};
76
+ try {
77
+ for (const spec of specs) {
78
+ await prepareInstance(spec);
79
+ const existing = previousState.databases?.[spec.id];
80
+ if (
81
+ Number.isInteger(existing?.pid) &&
82
+ typeof existing.command === 'string' &&
83
+ await processMatches(existing.pid, existing.command)
84
+ ) {
85
+ databases[spec.id] = existing;
86
+ } else {
87
+ const child = spawnDatabase(databaseSpec(spec));
88
+ if (!Number.isInteger(child?.pid)) {
89
+ throw new Error(`The native ${spec.id} Neo4j process did not return a process ID.`);
90
+ }
91
+ databases[spec.id] = {
92
+ pid: child.pid,
93
+ command: descriptor.neo4jHome,
94
+ startedAt: now().toISOString()
95
+ };
96
+ }
97
+ persistProcessState(databases, providers);
98
+ await waitForDatabase('127.0.0.1', spec.boltPort);
99
+ }
100
+ if (!startProviders) {
101
+ persistProcessState(databases, providers);
102
+ return;
103
+ }
104
+ for (const spec of specs) {
105
+ const existing = previousState.providers?.[spec.id];
106
+ if (
107
+ Number.isInteger(existing?.pid) &&
108
+ typeof existing.command === 'string' &&
109
+ await processMatches(existing.pid, existing.command)
110
+ ) {
111
+ providers[spec.id] = existing;
112
+ } else {
113
+ const child = spawnProvider(providerSpec(spec));
114
+ if (!Number.isInteger(child?.pid)) {
115
+ throw new Error(`The native ${spec.id} Provider did not return a process ID.`);
116
+ }
117
+ providers[spec.id] = {
118
+ pid: child.pid,
119
+ command: descriptor.providerPython,
120
+ startedAt: now().toISOString()
121
+ };
122
+ }
123
+ }
124
+ persistProcessState(databases, providers);
125
+ for (const spec of specs) await waitForProvider(spec.providerUrl, fetchImpl, wait);
126
+ } catch (error) {
127
+ await stopProviderEntries(providers);
128
+ await stopProviderEntries(databases);
129
+ persistProcessState({}, {});
130
+ throw error;
131
+ }
132
+ }
133
+
134
+ async function stopProviders() {
135
+ const state = readProcessState(paths.nativeProcessStatePath) ?? {};
136
+ await stopProviderEntries(state.providers ?? {});
137
+ writeProcessState(paths.nativeProcessStatePath, {
138
+ ...state,
139
+ version: 1,
140
+ mode: 'native',
141
+ databases: state.databases ?? {},
142
+ providers: {},
143
+ updatedAt: now().toISOString()
144
+ });
145
+ }
146
+
147
+ async function stopDatabases() {
148
+ await stopProviders();
149
+ if (!descriptor) return;
150
+ const state = readProcessState(paths.nativeProcessStatePath) ?? {};
151
+ if (Object.keys(state.databases ?? {}).length > 0) {
152
+ await stopProviderEntries(state.databases);
153
+ } else {
154
+ // Compatibility with native runtimes created before database PIDs were recorded.
155
+ for (const spec of await serviceSpecs()) {
156
+ await run(join(descriptor.neo4jHome, 'bin', 'neo4j'), ['stop'], {
157
+ env: neo4jEnvironment(spec)
158
+ }).catch(() => {});
159
+ }
160
+ }
161
+ persistProcessState({}, {});
162
+ }
163
+
164
+ async function ready() {
165
+ try {
166
+ const specs = await serviceSpecs();
167
+ const checks = await Promise.all(specs.map((spec) => providerReady(
168
+ spec.providerUrl,
169
+ fetchImpl
170
+ )));
171
+ return checks.every(Boolean);
172
+ } catch {
173
+ return false;
174
+ }
175
+ }
176
+
177
+ async function serviceSpecs() {
178
+ const settings = paths.runtimeSettingsPath
179
+ ? readSettings(paths.runtimeSettingsPath)
180
+ : DEFAULT_RUNTIME_SETTINGS;
181
+ const values = parseEnv(await readText(paths.graphEnvPath));
182
+ const includeWorkspace = personalOnly === null
183
+ ? managedWorkspaceEnabled(paths, readJson)
184
+ : !personalOnly;
185
+ return nativeInstanceSpecs(paths, settings, values, includeWorkspace);
186
+ }
187
+
188
+ async function prepareInstance(spec) {
189
+ const confPath = join(spec.instanceDir, 'conf', 'neo4j.conf');
190
+ await writeText(confPath, nativeNeo4jConfiguration({
191
+ instanceDir: spec.instanceDir,
192
+ httpPort: spec.httpPort,
193
+ boltPort: spec.boltPort,
194
+ memory: spec.memory
195
+ }));
196
+ const marker = join(spec.instanceDir, '.password-initialized');
197
+ if (pathExists(marker)) return;
198
+ await run(join(descriptor.neo4jHome, 'bin', 'neo4j-admin'), [
199
+ 'dbms', 'set-initial-password', spec.password
200
+ ], { env: neo4jEnvironment(spec) });
201
+ await markInitialized(marker);
202
+ }
203
+
204
+ function neo4jEnvironment(spec) {
205
+ return {
206
+ ...process.env,
207
+ JAVA_HOME: descriptor.javaHome,
208
+ NEO4J_HOME: descriptor.neo4jHome,
209
+ NEO4J_CONF: join(spec.instanceDir, 'conf')
210
+ };
211
+ }
212
+
213
+ function providerSpec(spec) {
214
+ return {
215
+ id: spec.id,
216
+ python: descriptor.providerPython,
217
+ port: spec.providerPort,
218
+ logPath: join(spec.instanceDir, 'logs', 'provider.log'),
219
+ environment: {
220
+ ...process.env,
221
+ PYTHONDONTWRITEBYTECODE: '1',
222
+ PYTHONUNBUFFERED: '1',
223
+ GRAPHITI_TELEMETRY_ENABLED: 'false',
224
+ EMBEDDING_DIM: '384',
225
+ FULI_PROVIDER_MODE: spec.providerMode,
226
+ FULI_PROVIDER_ID: spec.providerId,
227
+ FULI_PROVIDER_NAME: spec.providerName,
228
+ FULI_BOOTSTRAP_TOKEN: spec.bootstrapToken,
229
+ ...(spec.humanReviewToken
230
+ ? { FULI_HUMAN_REVIEW_TOKEN: spec.humanReviewToken }
231
+ : {}),
232
+ ...(spec.workflowObservationToken
233
+ ? { FULI_WORKFLOW_OBSERVATION_TOKEN: spec.workflowObservationToken }
234
+ : {}),
235
+ FULI_NEO4J_URI: `bolt://127.0.0.1:${spec.boltPort}`,
236
+ FULI_NEO4J_USER: 'neo4j',
237
+ FULI_NEO4J_PASSWORD: spec.password,
238
+ FULI_EMBEDDING_DIM: '384'
239
+ }
240
+ };
241
+ }
242
+
243
+ function databaseSpec(spec) {
244
+ return {
245
+ id: spec.id,
246
+ command: join(descriptor.neo4jHome, 'bin', 'neo4j'),
247
+ args: ['console'],
248
+ pidPath: join(spec.instanceDir, 'run', 'neo4j.pid'),
249
+ logPath: join(spec.instanceDir, 'logs', 'neo4j-console.log'),
250
+ environment: neo4jEnvironment(spec)
251
+ };
252
+ }
253
+
254
+ async function stopProviderEntries(entries) {
255
+ for (const entry of Object.values(entries)) {
256
+ if (!Number.isInteger(entry?.pid) || typeof entry.command !== 'string') continue;
257
+ if (await processMatches(entry.pid, entry.command)) await stopProcess(entry.pid);
258
+ }
259
+ }
260
+
261
+ function persistProcessState(databases, providers) {
262
+ writeProcessState(paths.nativeProcessStatePath, {
263
+ version: 1,
264
+ mode: 'native',
265
+ databases,
266
+ providers,
267
+ updatedAt: now().toISOString()
268
+ });
269
+ }
270
+
271
+ return { start, stopProviders, stopDatabases, ready };
272
+ }
273
+
274
+ export async function ensureNativeRuntime(input, dependencies = {}) {
275
+ const deps = nativeDependencies(dependencies);
276
+ if (!['darwin', 'linux'].includes(deps.platform)) {
277
+ throw new Error('Native graph mode currently supports macOS and Linux. Use container mode here.');
278
+ }
279
+ const javaHome = await deps.resolveJavaHome(input.env ?? process.env);
280
+ if (!javaHome) throw nativeJavaError(deps.platform);
281
+ const uvCommand = await deps.resolveUvCommand(input.env ?? process.env);
282
+ if (!uvCommand) {
283
+ throw new Error('Native graph mode requires uv with Python 3.12 support. Install uv, then run setup again.');
284
+ }
285
+
286
+ const providerPython = join(input.paths.nativeProviderVenvPath, 'bin', 'python');
287
+ const expected = {
288
+ version: MANIFEST_VERSION,
289
+ status: 'ready',
290
+ mode: 'native',
291
+ neo4jVersion: NATIVE_NEO4J_VERSION,
292
+ providerVersion: `${FULI_VERSION}+native.${PROVIDER_RUNTIME_REVISION}`,
293
+ javaHome,
294
+ uvCommand,
295
+ neo4jHome: input.paths.nativeNeo4jHome,
296
+ providerPython
297
+ };
298
+ const saved = deps.readManifest(input.paths.nativeRuntimeManifestPath);
299
+ if (manifestReady(saved, expected, input.paths, deps.pathExists)) return saved;
300
+
301
+ deps.onProgress(input, `Installing native Neo4j ${NATIVE_NEO4J_VERSION}…`);
302
+ if (!neo4jReady(input.paths, deps.pathExists)) {
303
+ await deps.installNeo4j({
304
+ paths: input.paths,
305
+ home: input.paths.nativeNeo4jHome,
306
+ url: NATIVE_NEO4J_URL,
307
+ sha256: NATIVE_NEO4J_SHA256
308
+ });
309
+ }
310
+ if (!deps.pathExists(providerPython) || saved?.providerVersion !== expected.providerVersion) {
311
+ deps.onProgress(input, 'Installing the native Graph Provider…');
312
+ await deps.installProvider({
313
+ uvCommand,
314
+ venvPath: input.paths.nativeProviderVenvPath,
315
+ providerPython,
316
+ providerSource: join(dirname(dirname(input.paths.graphComposePath)), 'graph-provider'),
317
+ packageRoot: dirname(input.paths.graphComposePath)
318
+ });
319
+ }
320
+ if (!neo4jReady(input.paths, deps.pathExists) || !deps.pathExists(providerPython)) {
321
+ throw new Error('Native graph runtime installation did not produce the required executables.');
322
+ }
323
+ const manifest = { ...expected, installedAt: new Date().toISOString() };
324
+ deps.writeManifest(input.paths.nativeRuntimeManifestPath, manifest);
325
+ return manifest;
326
+ }
327
+
328
+ export function nativeNeo4jConfiguration({
329
+ instanceDir,
330
+ httpPort,
331
+ boltPort,
332
+ memory
333
+ }) {
334
+ return [
335
+ 'server.default_listen_address=127.0.0.1',
336
+ 'server.default_advertised_address=127.0.0.1',
337
+ `server.http.listen_address=127.0.0.1:${httpPort}`,
338
+ `server.http.advertised_address=127.0.0.1:${httpPort}`,
339
+ `server.bolt.listen_address=127.0.0.1:${boltPort}`,
340
+ `server.bolt.advertised_address=127.0.0.1:${boltPort}`,
341
+ `server.directories.data=${join(instanceDir, 'data')}`,
342
+ `server.directories.logs=${join(instanceDir, 'logs')}`,
343
+ `server.directories.run=${join(instanceDir, 'run')}`,
344
+ `server.directories.transaction.logs.root=${join(instanceDir, 'transactions')}`,
345
+ `server.memory.heap.initial_size=${memory.heapInitial}`,
346
+ `server.memory.heap.max_size=${memory.heapMax}`,
347
+ `server.memory.pagecache.size=${memory.pageCache}`,
348
+ 'dbms.security.auth_enabled=true',
349
+ 'dbms.usage_report.enabled=false',
350
+ ''
351
+ ].join('\n');
352
+ }
353
+
354
+ function manifestReady(saved, expected, paths, pathExists) {
355
+ return saved?.version === expected.version &&
356
+ saved.mode === expected.mode &&
357
+ saved.neo4jVersion === expected.neo4jVersion &&
358
+ saved.providerVersion === expected.providerVersion &&
359
+ saved.javaHome === expected.javaHome &&
360
+ saved.uvCommand === expected.uvCommand &&
361
+ saved.neo4jHome === expected.neo4jHome &&
362
+ saved.providerPython === expected.providerPython &&
363
+ neo4jReady(paths, pathExists) &&
364
+ pathExists(expected.providerPython);
365
+ }
366
+
367
+ function neo4jReady(paths, pathExists) {
368
+ return pathExists(join(paths.nativeNeo4jHome, 'bin', 'neo4j')) &&
369
+ pathExists(join(paths.nativeNeo4jHome, 'bin', 'neo4j-admin'));
370
+ }
371
+
372
+ function nativeJavaError(platform) {
373
+ return new Error(platform === 'darwin'
374
+ ? 'Native graph mode requires Java 21. Install it with `brew install openjdk@21`, then run setup again.'
375
+ : 'Native graph mode requires Java 21. Install an OpenJDK 21 package, then run setup again.');
376
+ }
377
+
378
+ function nativeDependencies(overrides) {
379
+ return {
380
+ platform: process.platform,
381
+ pathExists: existsSync,
382
+ readManifest: (path) => readJsonFile(path, null),
383
+ writeManifest: writeJsonFileAtomic,
384
+ resolveJavaHome,
385
+ resolveUvCommand,
386
+ installNeo4j,
387
+ installProvider,
388
+ onProgress: (input, message) => input.onProgress?.(message),
389
+ ...overrides
390
+ };
391
+ }
392
+
393
+ async function resolveJavaHome(env) {
394
+ const configured = nonEmpty(env.FULI_NATIVE_JAVA_HOME) ?? nonEmpty(env.JAVA_HOME);
395
+ if (configured && existsSync(join(configured, 'bin', 'java'))) return configured;
396
+ if (process.platform === 'darwin') {
397
+ const apple = await commandResult('/usr/libexec/java_home', ['-v', '21'], { env });
398
+ const appleHome = nonEmpty(apple.stdout);
399
+ if (apple.status === 0 && appleHome && existsSync(join(appleHome, 'bin', 'java'))) {
400
+ return appleHome;
401
+ }
402
+ const brew = await commandResult('brew', ['--prefix', 'openjdk@21'], { env });
403
+ const brewHome = nonEmpty(brew.stdout);
404
+ if (brew.status === 0 && brewHome && existsSync(join(brewHome, 'bin', 'java'))) {
405
+ return brewHome;
406
+ }
407
+ }
408
+ const java = await commandResult('java', ['-version'], { env });
409
+ if (java.status === 0 && /(?:version\s+")?21(?:[.\s"]|$)/i.test(java.stderr + java.stdout)) {
410
+ return dirname(dirname(java.command ?? 'java'));
411
+ }
412
+ return null;
413
+ }
414
+
415
+ async function resolveUvCommand(env) {
416
+ const result = await commandResult('uv', ['--version'], { env });
417
+ return result.status === 0 ? 'uv' : null;
418
+ }
419
+
420
+ async function installNeo4j({ paths, home, url, sha256 }) {
421
+ const downloadDir = join(paths.nativeRuntimeDir, 'downloads');
422
+ const archive = join(downloadDir, `neo4j-community-${NATIVE_NEO4J_VERSION}-unix.tar.gz`);
423
+ await mkdir(downloadDir, { recursive: true });
424
+ if (!existsSync(archive) || await sha256File(archive) !== sha256) {
425
+ await downloadFile(url, archive);
426
+ }
427
+ if (await sha256File(archive) !== sha256) {
428
+ throw new Error('The downloaded Neo4j archive failed checksum verification.');
429
+ }
430
+ await rm(home, { recursive: true, force: true });
431
+ await runCommand('tar', ['-xzf', archive, '-C', paths.nativeRuntimeDir]);
432
+ }
433
+
434
+ async function installProvider({ uvCommand, venvPath, providerPython, packageRoot }) {
435
+ await mkdir(dirname(venvPath), { recursive: true });
436
+ if (!existsSync(providerPython)) {
437
+ await runCommand(uvCommand, ['venv', '--python', '3.12', '--seed', venvPath]);
438
+ }
439
+ await runCommand(uvCommand, [
440
+ 'pip', 'install', '--python', providerPython, '--upgrade',
441
+ join(packageRoot, 'graph-provider')
442
+ ]);
443
+ }
444
+
445
+ async function downloadFile(url, destination) {
446
+ const partial = `${destination}.partial`;
447
+ let existingBytes = 0;
448
+ try {
449
+ existingBytes = (await stat(partial)).size;
450
+ } catch {
451
+ // No partial download is available to resume.
452
+ }
453
+ const response = await fetch(url, {
454
+ headers: existingBytes > 0 ? { range: `bytes=${existingBytes}-` } : {},
455
+ signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
456
+ });
457
+ if (!response.ok || !response.body) {
458
+ throw new Error(`Neo4j download failed with HTTP ${response.status}.`);
459
+ }
460
+ const resumed = existingBytes > 0 && response.status === 206;
461
+ if (!resumed && existingBytes > 0) {
462
+ await rm(partial, { force: true });
463
+ }
464
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(partial, {
465
+ mode: 0o600,
466
+ flags: resumed ? 'a' : 'w'
467
+ }));
468
+ await rename(partial, destination);
469
+ }
470
+
471
+ async function sha256File(path) {
472
+ const hash = createHash('sha256');
473
+ await pipeline(createReadStream(path), hash);
474
+ return hash.digest('hex');
475
+ }
476
+
477
+ function runCommand(command, args, options = {}) {
478
+ return new Promise((resolve, reject) => {
479
+ execFile(command, args, {
480
+ env: options.env ?? process.env,
481
+ timeout: COMMAND_TIMEOUT_MS,
482
+ encoding: 'utf8',
483
+ maxBuffer: 4 * 1024 * 1024,
484
+ windowsHide: true
485
+ }, (error, stdout, stderr) => {
486
+ if (error) {
487
+ reject(new Error(nonEmpty(stderr) ?? nonEmpty(stdout) ?? error.message));
488
+ } else {
489
+ resolve({ stdout, stderr });
490
+ }
491
+ });
492
+ });
493
+ }
494
+
495
+ function commandResult(command, args, options = {}) {
496
+ return new Promise((resolve) => {
497
+ execFile(command, args, {
498
+ env: options.env ?? process.env,
499
+ timeout: 10_000,
500
+ encoding: 'utf8',
501
+ windowsHide: true
502
+ }, (error, stdout, stderr) => {
503
+ resolve({ status: error ? 1 : 0, stdout, stderr, command });
504
+ });
505
+ });
506
+ }
507
+
508
+ function nonEmpty(value) {
509
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
510
+ }
511
+
512
+ function nativeInstanceSpecs(paths, settings, values, includeWorkspace) {
513
+ const memory = {
514
+ heapInitial: values.FULI_NEO4J_HEAP_INITIAL_SIZE ?? '128m',
515
+ heapMax: values.FULI_NEO4J_HEAP_MAX_SIZE ?? '256m',
516
+ pageCache: values.FULI_NEO4J_PAGECACHE_SIZE ?? '64m'
517
+ };
518
+ const urls = managedProviderUrls(settings);
519
+ const personal = {
520
+ id: 'personal',
521
+ instanceDir: paths.nativePersonalDir,
522
+ httpPort: settings.ports.personalNeo4jHttp,
523
+ boltPort: settings.ports.personalNeo4jBolt,
524
+ providerPort: settings.ports.personalProvider,
525
+ providerUrl: urls.personal,
526
+ providerMode: 'personal',
527
+ providerId: 'local-personal',
528
+ providerName: 'Fuli Personal Provider',
529
+ password: requiredEnv(values, 'FULI_PERSONAL_NEO4J_PASSWORD'),
530
+ bootstrapToken: requiredEnv(values, 'FULI_PERSONAL_BOOTSTRAP_TOKEN'),
531
+ humanReviewToken: values.FULI_PERSONAL_HUMAN_REVIEW_TOKEN,
532
+ workflowObservationToken: values.FULI_PERSONAL_WORKFLOW_OBSERVATION_TOKEN,
533
+ memory
534
+ };
535
+ if (!includeWorkspace) return [personal];
536
+ return [personal, {
537
+ id: 'workspace',
538
+ instanceDir: paths.nativeWorkspaceDir,
539
+ httpPort: settings.ports.workspaceNeo4jHttp,
540
+ boltPort: settings.ports.workspaceNeo4jBolt,
541
+ providerPort: settings.ports.workspaceProvider,
542
+ providerUrl: urls.workspace,
543
+ providerMode: 'workspace',
544
+ providerId: 'development-workspace',
545
+ providerName: 'Fuli Workspace Provider',
546
+ password: requiredEnv(values, 'FULI_WORKSPACE_NEO4J_PASSWORD'),
547
+ bootstrapToken: requiredEnv(values, 'FULI_WORKSPACE_BOOTSTRAP_TOKEN'),
548
+ humanReviewToken: null,
549
+ workflowObservationToken: null,
550
+ memory
551
+ }];
552
+ }
553
+
554
+ function managedWorkspaceEnabled(paths, readJson) {
555
+ const state = readJson(paths.graphRuntimeStatePath) ?? {};
556
+ if (state.managedProviders?.includes('development-workspace')) return true;
557
+ const config = readJson(paths.graphRuntimeConfigPath) ?? {};
558
+ return (config.workspaces ?? []).some((workspace) =>
559
+ workspace?.managedDevelopment === true);
560
+ }
561
+
562
+ function parseEnv(body) {
563
+ const values = {};
564
+ for (const line of String(body).split(/\r?\n/)) {
565
+ const trimmed = line.trim();
566
+ if (!trimmed || trimmed.startsWith('#')) continue;
567
+ const separator = trimmed.indexOf('=');
568
+ if (separator <= 0) continue;
569
+ values[trimmed.slice(0, separator)] = trimmed.slice(separator + 1);
570
+ }
571
+ return values;
572
+ }
573
+
574
+ function requiredEnv(values, name) {
575
+ const value = nonEmpty(values[name]);
576
+ if (!value) throw new Error(`Native graph runtime is missing ${name}. Run setup again.`);
577
+ return value;
578
+ }
579
+
580
+ function assertDescriptor(descriptor) {
581
+ if (
582
+ descriptor?.status !== 'ready' ||
583
+ descriptor.mode !== 'native' ||
584
+ !nonEmpty(descriptor.javaHome) ||
585
+ !nonEmpty(descriptor.neo4jHome) ||
586
+ !nonEmpty(descriptor.providerPython)
587
+ ) {
588
+ throw new Error('The native graph runtime is not installed. Run setup in native mode first.');
589
+ }
590
+ }
591
+
592
+ async function secureWriteText(path, body) {
593
+ await mkdir(dirname(path), { recursive: true });
594
+ await writeFile(path, body, { mode: 0o600 });
595
+ chmodSync(path, 0o600);
596
+ }
597
+
598
+ async function secureMarker(path) {
599
+ await mkdir(dirname(path), { recursive: true });
600
+ await writeFile(path, '', { mode: 0o600 });
601
+ }
602
+
603
+ function spawnNativeDatabase(spec) {
604
+ if (existsSync(spec.pidPath)) {
605
+ const pid = Number(readFileSync(spec.pidPath, 'utf8').trim());
606
+ if (Number.isInteger(pid) && processAlive(pid)) return { pid };
607
+ unlinkSync(spec.pidPath);
608
+ }
609
+ mkdirSync(dirname(spec.logPath), { recursive: true });
610
+ const log = openSync(spec.logPath, 'a');
611
+ try {
612
+ const child = spawn(spec.command, spec.args, {
613
+ detached: true,
614
+ windowsHide: true,
615
+ stdio: ['ignore', log, log],
616
+ env: spec.environment
617
+ });
618
+ child.unref();
619
+ return child;
620
+ } finally {
621
+ closeSync(log);
622
+ }
623
+ }
624
+
625
+ function spawnNativeProvider(spec) {
626
+ mkdirSync(dirname(spec.logPath), { recursive: true });
627
+ const log = openSync(spec.logPath, 'a');
628
+ try {
629
+ const child = spawn(spec.python, [
630
+ '-m', 'uvicorn', 'fuli_graph.app:app',
631
+ '--host', '127.0.0.1',
632
+ '--port', String(spec.port)
633
+ ], {
634
+ detached: true,
635
+ windowsHide: true,
636
+ stdio: ['ignore', log, log],
637
+ env: spec.environment
638
+ });
639
+ child.unref();
640
+ return child;
641
+ } finally {
642
+ closeSync(log);
643
+ }
644
+ }
645
+
646
+ async function waitForProvider(url, fetchImpl, wait) {
647
+ for (let attempt = 0; attempt < 120; attempt += 1) {
648
+ if (await providerReady(url, fetchImpl)) return;
649
+ if (attempt < 119) await wait(1000);
650
+ }
651
+ throw new Error('The native graph Provider did not become ready after starting.');
652
+ }
653
+
654
+ async function providerReady(url, fetchImpl) {
655
+ try {
656
+ const response = await fetchImpl(`${String(url).replace(/\/$/, '')}/health`, {
657
+ signal: AbortSignal.timeout(2500)
658
+ });
659
+ return response.ok;
660
+ } catch {
661
+ return false;
662
+ }
663
+ }
664
+
665
+ async function waitForTcp(host, port) {
666
+ for (let attempt = 0; attempt < 120; attempt += 1) {
667
+ if (await tcpReady(host, port)) return;
668
+ if (attempt < 119) await delay(1000);
669
+ }
670
+ throw new Error(`Native Neo4j did not open its local Bolt port ${port}.`);
671
+ }
672
+
673
+ function tcpReady(host, port) {
674
+ return new Promise((resolve) => {
675
+ const socket = createConnection({ host, port });
676
+ const done = (ready) => {
677
+ socket.destroy();
678
+ resolve(ready);
679
+ };
680
+ socket.setTimeout(1000, () => done(false));
681
+ socket.once('connect', () => done(true));
682
+ socket.once('error', () => done(false));
683
+ });
684
+ }
685
+
686
+ async function processMatchesCommand(pid, command) {
687
+ const result = await commandResult('ps', ['-p', String(pid), '-o', 'command=']);
688
+ return result.status === 0 && result.stdout.includes(command);
689
+ }
690
+
691
+ async function stopOwnedProcess(pid) {
692
+ const target = process.platform === 'win32' ? pid : -pid;
693
+ try {
694
+ process.kill(target, 'SIGTERM');
695
+ } catch (error) {
696
+ if (error?.code === 'ESRCH') return;
697
+ throw error;
698
+ }
699
+ for (let attempt = 0; attempt < 100; attempt += 1) {
700
+ if (!processAlive(pid)) return;
701
+ await delay(100);
702
+ }
703
+ if (processAlive(pid)) process.kill(target, 'SIGKILL');
704
+ }
705
+
706
+ function processAlive(pid) {
707
+ try {
708
+ process.kill(pid, 0);
709
+ return true;
710
+ } catch {
711
+ return false;
712
+ }
713
+ }
714
+
715
+ function delay(ms) {
716
+ return new Promise((resolve) => setTimeout(resolve, ms));
717
+ }