fuli-context 0.7.4-beta.2 → 0.7.5

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 (133) hide show
  1. package/README.md +93 -7
  2. package/README.zh-CN.md +84 -7
  3. package/compose.graphiti.yml +6 -6
  4. package/dist/web/assets/AboutPage-ByCUor-j.js +1 -0
  5. package/dist/web/assets/ConnectionsPage-DkdkZ40Q.js +1 -0
  6. package/dist/web/assets/KnowledgeEditDialog-DSe3Gwep.js +1 -0
  7. package/dist/web/assets/KnowledgeInspector-CYAjAb9J.js +1 -0
  8. package/dist/web/assets/KnowledgeOrganizerPage-CV4F54R_.js +1 -0
  9. package/dist/web/assets/KnowledgePage-vYYXipOi.js +1 -0
  10. package/dist/web/assets/KnowledgeWorkspace-z4u5mXOG.js +4 -0
  11. package/dist/web/assets/OverviewPage-BWOfeS6k.js +1 -0
  12. package/dist/web/assets/PersonalProfilePage-c3w4-h7a.js +1 -0
  13. package/dist/web/assets/PersonalProjectsPage-B7AlLEwf.js +1 -0
  14. package/dist/web/assets/ProjectAgentsPage-CgftpIPB.css +1 -0
  15. package/dist/web/assets/ProjectAgentsPage-DTHcL_ox.js +11 -0
  16. package/dist/web/assets/PublicProjectsPage-DdH0oGB3.js +1 -0
  17. package/dist/web/assets/ReviewPage-BYIpcIHG.js +1 -0
  18. package/dist/web/assets/SearchableSelect-Dz13ClQb.js +1 -0
  19. package/dist/web/assets/SettingsPage-2VkbXgZi.js +1 -0
  20. package/dist/web/assets/SettingsPage-CwZp2ka4.css +1 -0
  21. package/dist/web/assets/WritingTastePage-BOPQZuva.js +1 -0
  22. package/dist/web/assets/{console-DMvHxKf9.js → console-BlAhujsH.js} +1 -1
  23. package/dist/web/assets/{conversation-launcher-settings-Dnlcw7Rj.js → conversation-launcher-settings-DTYxiLJt.js} +1 -1
  24. package/dist/web/assets/i18n-Ci3heuiL.js +9 -0
  25. package/dist/web/assets/{identity-C9mLdR8L.js → identity-4_-v6cB7.js} +1 -1
  26. package/dist/web/assets/index-BnH5mcZx.css +1 -0
  27. package/dist/web/assets/index-C67Q0JY2.js +2 -0
  28. package/dist/web/assets/{model-CsYAR8O3.js → model-yG0YqUyZ.js} +1 -1
  29. package/dist/web/assets/{useApi-CROJJdhE-BLQiPVw0.js → useApi-CROJJdhE-twhdZArA.js} +1 -1
  30. package/dist/web/assets/vue-router-C-wot_Di.js +1 -0
  31. package/dist/web/index.html +6 -6
  32. package/docs/public-personal-architecture.md +73 -0
  33. package/graph-provider/fuli_graph/app.py +8 -0
  34. package/graph-provider/fuli_graph/collaboration_context.py +69 -6
  35. package/graph-provider/fuli_graph/knowledge_search.py +84 -5
  36. package/graph-provider/fuli_graph/model_validation.py +134 -0
  37. package/graph-provider/fuli_graph/models.py +24 -5
  38. package/graph-provider/fuli_graph/project_agent_access.py +54 -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 +597 -0
  42. package/graph-provider/fuli_graph/project_agent_task_models.py +542 -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_executor_learning.py +756 -0
  48. package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +601 -0
  49. package/graph-provider/fuli_graph/store_project_agent_executors.py +1087 -0
  50. package/graph-provider/fuli_graph/store_project_agent_task_reads.py +458 -0
  51. package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +699 -0
  52. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +409 -0
  53. package/graph-provider/fuli_graph/store_project_agent_tasks.py +1144 -0
  54. package/graph-provider/fuli_graph/store_project_agents.py +965 -0
  55. package/graph-provider/pyproject.toml +2 -1
  56. package/npm-shrinkwrap.json +2 -2
  57. package/package.json +2 -1
  58. package/skills/capturing-session-knowledge/SKILL.md +43 -2
  59. package/src/adaptive-runtime/executor-pool.js +127 -0
  60. package/src/adaptive-runtime/graph-services.js +166 -0
  61. package/src/adaptive-runtime/lease-client.js +335 -0
  62. package/src/adaptive-runtime/runtime-broker.js +318 -0
  63. package/src/adaptive-runtime/settings.js +111 -0
  64. package/src/adaptive-runtime/state.js +74 -0
  65. package/src/agent-tools/graph-definitions.js +396 -0
  66. package/src/agent-tools/graph-handlers.js +61 -0
  67. package/src/agent-tools/project-agent-definitions.js +307 -0
  68. package/src/backup/graph-backup.js +289 -0
  69. package/src/backup/identity-reconciliation.js +208 -0
  70. package/src/backup/runtime-adapters.js +269 -0
  71. package/src/cli/command-registry.js +7 -2
  72. package/src/cli/graph-data-command.js +112 -0
  73. package/src/cli/local-runtime-command.js +16 -2
  74. package/src/cli/setup-command.js +19 -3
  75. package/src/cli/update-command.js +4 -0
  76. package/src/cli/workspace-connection-command.js +333 -0
  77. package/src/cli.js +11 -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 +74 -112
  81. package/src/graphiti/knowledge-provider-mapping.js +7 -1
  82. package/src/graphiti/project-agent-control-plane.js +263 -0
  83. package/src/graphiti/project-agent-mapping.js +1003 -0
  84. package/src/graphiti/project-agent-workflows.js +557 -0
  85. package/src/graphiti/provider-client.js +270 -1
  86. package/src/graphiti/runtime-config.js +6 -1
  87. package/src/graphiti/workspace-provider-client.js +403 -0
  88. package/src/http/api-router.js +63 -9
  89. package/src/http/graph-api-router.js +278 -0
  90. package/src/local-runtime/lifecycle.js +46 -10
  91. package/src/mcp/context-resources.js +15 -6
  92. package/src/mcp/create-mcp-server.js +51 -16
  93. package/src/mcp/instructions.js +7 -5
  94. package/src/mcp/runtime.js +17 -5
  95. package/src/mcp/session-id.js +38 -1
  96. package/src/mcp/tool-annotations.js +69 -0
  97. package/src/mcp-server.js +23 -6
  98. package/src/native-runtime/runtime.js +717 -0
  99. package/src/server.js +6 -2
  100. package/src/setup/agent-installation-status.js +7 -1
  101. package/src/setup/agents.js +3 -1
  102. package/src/setup/claude-code-config.js +3 -1
  103. package/src/setup/codex-bootstrap.js +11 -10
  104. package/src/setup/codex-config.js +7 -1
  105. package/src/setup/cursor-config.js +3 -1
  106. package/src/setup/graph-runtime.js +158 -13
  107. package/src/setup/neo4j-memory-profile.js +43 -0
  108. package/src/setup/options.js +38 -2
  109. package/src/setup/paths.js +14 -0
  110. package/src/setup/setup.js +37 -1
  111. package/src/system/resource-monitor.js +181 -16
  112. package/src/system/runtime-settings.js +10 -1
  113. package/src/system/system-service.js +27 -2
  114. package/dist/web/assets/AboutPage-CftRexSY.js +0 -1
  115. package/dist/web/assets/ConnectionsPage-D5J91x6d.js +0 -1
  116. package/dist/web/assets/KnowledgeEditDialog-wotVahvr.js +0 -1
  117. package/dist/web/assets/KnowledgeInspector-BzkW0mVs.js +0 -1
  118. package/dist/web/assets/KnowledgeOrganizerPage-BebUDiHt.js +0 -1
  119. package/dist/web/assets/KnowledgePage-OUT07aKH.js +0 -1
  120. package/dist/web/assets/KnowledgeWorkspace-1pHJ0MNZ.js +0 -4
  121. package/dist/web/assets/OverviewPage-CHE6XhdS.js +0 -1
  122. package/dist/web/assets/PersonalProfilePage-CXXiabUy.js +0 -1
  123. package/dist/web/assets/PersonalProjectsPage-d2wYGW8n.js +0 -1
  124. package/dist/web/assets/PublicProjectsPage-hxLShtnZ.js +0 -1
  125. package/dist/web/assets/ReviewPage-DUeyu7E8.js +0 -1
  126. package/dist/web/assets/SearchableSelect-C5RiB5F8.js +0 -1
  127. package/dist/web/assets/SettingsPage-BMuASSzQ.js +0 -1
  128. package/dist/web/assets/SettingsPage-CAfz4qiV.css +0 -1
  129. package/dist/web/assets/WritingTastePage-B9BCYIwQ.js +0 -1
  130. package/dist/web/assets/i18n-CWfd8Dtt.js +0 -5
  131. package/dist/web/assets/index-X2j29j8i.css +0 -1
  132. package/dist/web/assets/index-nP5p5OaY.js +0 -2
  133. package/dist/web/assets/vue-router-DN8x6iY1.js +0 -1
@@ -0,0 +1,208 @@
1
+ import { chmodSync } from 'node:fs';
2
+
3
+ import { readJsonFile, writeJsonFileAtomic } from '../storage/json-file.js';
4
+ import {
5
+ managedProviderUrls,
6
+ readRuntimeSettings
7
+ } from '../system/runtime-settings.js';
8
+
9
+ const PROVIDER_READY_ATTEMPTS = 120;
10
+
11
+ export function graphBackupSelection(paths, {
12
+ readConfig = (path) => readJsonFile(path, null)
13
+ } = {}) {
14
+ const config = readConfig(paths.graphRuntimeConfigPath);
15
+ const personalSpaceId = nonEmpty(config?.personal?.spaceId);
16
+ return personalSpaceId ? { personalSpaceId } : null;
17
+ }
18
+
19
+ export async function reconcileImportedGraphIdentity({
20
+ paths,
21
+ instances,
22
+ selection = {},
23
+ sourceMode,
24
+ targetMode,
25
+ fetchImpl = globalThis.fetch,
26
+ wait = delay,
27
+ readConfig = (path) => readJsonFile(path, null),
28
+ writeConfig = writeJsonFileAtomic,
29
+ secureConfig = (path) => chmodSync(path, 0o600),
30
+ readSettings = readRuntimeSettings,
31
+ readEnvironment = readEnvironmentFile
32
+ }) {
33
+ const current = readConfig(paths.graphRuntimeConfigPath);
34
+ if (!current?.personal) {
35
+ throw new Error('The target graph mode is not configured. Run setup before importing.');
36
+ }
37
+ const settings = readSettings(paths.runtimeSettingsPath);
38
+ const urls = managedProviderUrls(settings);
39
+ const values = await readEnvironment(paths.graphEnvPath);
40
+ const sourceConfigPath = profilePath(paths, sourceMode);
41
+ const source = sourceMode === targetMode || !sourceConfigPath
42
+ ? null
43
+ : readConfig(sourceConfigPath);
44
+ const next = structuredClone(current);
45
+ next.workspaces = mergedExternalWorkspaces(current, source);
46
+
47
+ await waitForProvider(urls.personal, fetchImpl, wait);
48
+ const personal = await bootstrapProvider({
49
+ url: urls.personal,
50
+ bootstrapToken: requiredValue(values, 'FULI_PERSONAL_BOOTSTRAP_TOKEN'),
51
+ fetchImpl
52
+ });
53
+ const spaces = await listSpaces(urls.personal, personal.accessToken, fetchImpl);
54
+ const personalSpaceId = selectPersonalSpace({
55
+ spaces,
56
+ requestedId: selection.personalSpaceId,
57
+ currentId: source?.personal?.spaceId ?? current.personal.spaceId
58
+ });
59
+ next.personal = {
60
+ ...next.personal,
61
+ providerUrl: urls.personal,
62
+ accessToken: personal.accessToken,
63
+ principalId: personal.principalId,
64
+ spaceId: personalSpaceId,
65
+ ...(nonEmpty(values.FULI_PERSONAL_WORKFLOW_OBSERVATION_TOKEN)
66
+ ? { workflowObservationToken: values.FULI_PERSONAL_WORKFLOW_OBSERVATION_TOKEN }
67
+ : {})
68
+ };
69
+
70
+ if (instances.includes('workspace')) {
71
+ await waitForProvider(urls.workspace, fetchImpl, wait);
72
+ const workspace = await bootstrapProvider({
73
+ url: urls.workspace,
74
+ bootstrapToken: requiredValue(values, 'FULI_WORKSPACE_BOOTSTRAP_TOKEN'),
75
+ fetchImpl
76
+ });
77
+ next.workspaces.push({
78
+ providerUrl: urls.workspace,
79
+ accessToken: workspace.accessToken,
80
+ principalId: workspace.principalId,
81
+ managedDevelopment: true
82
+ });
83
+ }
84
+
85
+ try {
86
+ writeConfig(paths.graphRuntimeConfigPath, next);
87
+ secureConfig(paths.graphRuntimeConfigPath);
88
+ } catch (error) {
89
+ try {
90
+ writeConfig(paths.graphRuntimeConfigPath, current);
91
+ secureConfig(paths.graphRuntimeConfigPath);
92
+ } catch (restoreError) {
93
+ throw new AggregateError(
94
+ [error, restoreError],
95
+ 'Imported graph credentials could not be saved or restored.'
96
+ );
97
+ }
98
+ throw error;
99
+ }
100
+ return { personalSpaceId, instances: [...instances] };
101
+ }
102
+
103
+ async function waitForProvider(url, fetchImpl, wait) {
104
+ for (let attempt = 0; attempt < PROVIDER_READY_ATTEMPTS; attempt += 1) {
105
+ try {
106
+ const response = await fetchImpl(`${url}/health`, {
107
+ signal: AbortSignal.timeout(2500)
108
+ });
109
+ if (response.ok) return;
110
+ } catch {
111
+ // The target is still starting.
112
+ }
113
+ if (attempt < PROVIDER_READY_ATTEMPTS - 1) await wait(1000);
114
+ }
115
+ throw new Error('The imported graph Provider did not become ready for credential rotation.');
116
+ }
117
+
118
+ async function bootstrapProvider({ url, bootstrapToken, fetchImpl }) {
119
+ const response = await fetchImpl(`${url}/v1/bootstrap`, {
120
+ method: 'POST',
121
+ headers: {
122
+ 'content-type': 'application/json',
123
+ 'x-fuli-bootstrap-token': bootstrapToken
124
+ },
125
+ body: JSON.stringify({ principal_name: 'Fuli migration' }),
126
+ signal: AbortSignal.timeout(10_000)
127
+ });
128
+ if (!response.ok) throw new Error('The imported graph Provider rejected credential rotation.');
129
+ const body = await response.json();
130
+ const principalId = nonEmpty(body?.principal_id);
131
+ const accessToken = nonEmpty(body?.access_token);
132
+ if (!principalId || !accessToken) {
133
+ throw new Error('The imported graph Provider returned an invalid bootstrap identity.');
134
+ }
135
+ return { principalId, accessToken };
136
+ }
137
+
138
+ async function listSpaces(url, accessToken, fetchImpl) {
139
+ const response = await fetchImpl(`${url}/v1/spaces`, {
140
+ headers: { authorization: `Bearer ${accessToken}` },
141
+ signal: AbortSignal.timeout(10_000)
142
+ });
143
+ if (!response.ok) throw new Error('The imported graph Provider could not list personal spaces.');
144
+ const body = await response.json();
145
+ if (!Array.isArray(body)) throw new Error('The imported graph Provider returned invalid spaces.');
146
+ return body.filter((space) => space?.kind === 'personal' && nonEmpty(space.id));
147
+ }
148
+
149
+ function selectPersonalSpace({ spaces, requestedId, currentId }) {
150
+ for (const candidate of [requestedId, currentId]) {
151
+ const id = nonEmpty(candidate);
152
+ if (id && spaces.some((space) => space.id === id)) return id;
153
+ }
154
+ if (spaces.length === 1) return spaces[0].id;
155
+ throw new Error(
156
+ 'The imported graph contains multiple personal spaces and the backup does not select one.'
157
+ );
158
+ }
159
+
160
+ function mergedExternalWorkspaces(current, source) {
161
+ const result = new Map();
162
+ for (const workspace of [
163
+ ...(source?.workspaces ?? []),
164
+ ...(current?.workspaces ?? [])
165
+ ]) {
166
+ if (isManagedWorkspace(workspace)) continue;
167
+ const providerUrl = nonEmpty(workspace?.providerUrl);
168
+ if (providerUrl) result.set(providerUrl, structuredClone(workspace));
169
+ }
170
+ return [...result.values()];
171
+ }
172
+
173
+ function isManagedWorkspace(workspace) {
174
+ return workspace?.managedDevelopment === true;
175
+ }
176
+
177
+ function profilePath(paths, mode) {
178
+ if (mode === 'container') return paths.containerGraphConfigProfilePath;
179
+ if (mode === 'native') return paths.nativeGraphConfigProfilePath;
180
+ return null;
181
+ }
182
+
183
+ async function readEnvironmentFile(path) {
184
+ const { readFile } = await import('node:fs/promises');
185
+ const body = await readFile(path, 'utf8');
186
+ const values = {};
187
+ for (const line of body.split(/\r?\n/)) {
188
+ const trimmed = line.trim();
189
+ if (!trimmed || trimmed.startsWith('#')) continue;
190
+ const separator = trimmed.indexOf('=');
191
+ if (separator > 0) values[trimmed.slice(0, separator)] = trimmed.slice(separator + 1);
192
+ }
193
+ return values;
194
+ }
195
+
196
+ function requiredValue(values, name) {
197
+ const value = nonEmpty(values[name]);
198
+ if (!value) throw new Error(`The target graph environment is missing ${name}.`);
199
+ return value;
200
+ }
201
+
202
+ function nonEmpty(value) {
203
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
204
+ }
205
+
206
+ function delay(ms) {
207
+ return new Promise((resolve) => setTimeout(resolve, ms));
208
+ }
@@ -0,0 +1,269 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { copyFile, mkdir, rename, rm } from 'node:fs/promises';
4
+ import { dirname, join } from 'node:path';
5
+ import { randomUUID } from 'node:crypto';
6
+
7
+ import { createNativeGraphServices } from '../native-runtime/runtime.js';
8
+ import { ensureContainerRuntime } from '../setup/container-runtime.js';
9
+ import { readJsonFile } from '../storage/json-file.js';
10
+ import {
11
+ graphBackupSelection,
12
+ reconcileImportedGraphIdentity
13
+ } from './identity-reconciliation.js';
14
+
15
+ const COMMAND_TIMEOUT_MS = 10 * 60_000;
16
+
17
+ export async function createGraphBackupAdapter({
18
+ mode,
19
+ paths,
20
+ onProgress = () => {},
21
+ ensureContainer = ensureContainerRuntime,
22
+ run = runCommand
23
+ }) {
24
+ const instances = managedGraphInstances(paths);
25
+ if (mode === 'native') return createNativeBackupAdapter({ paths, instances, run });
26
+ if (mode === 'container') {
27
+ const runtime = await ensureContainer({ onProgress });
28
+ return createContainerBackupAdapter({ paths, instances, runtime, run });
29
+ }
30
+ throw new TypeError('Graph backup runtime mode must be container or native.');
31
+ }
32
+
33
+ export function managedGraphInstances(paths) {
34
+ const state = readJsonFile(paths.graphRuntimeStatePath, null);
35
+ const config = readJsonFile(paths.graphRuntimeConfigPath, null);
36
+ const workspace = state?.managedProviders?.includes('development-workspace') ||
37
+ (config?.workspaces ?? []).some(({ managedDevelopment }) => managedDevelopment === true);
38
+ return workspace ? ['personal', 'workspace'] : ['personal'];
39
+ }
40
+
41
+ function createNativeBackupAdapter({ paths, instances, run }) {
42
+ const descriptor = readJsonFile(paths.nativeRuntimeManifestPath, null);
43
+ if (descriptor?.status !== 'ready' || descriptor.mode !== 'native') {
44
+ throw new Error(
45
+ 'Native graph mode is not initialized. Run `fuli setup --runtime-mode native` first.'
46
+ );
47
+ }
48
+ const services = createNativeGraphServices({
49
+ paths,
50
+ runtimeDescriptor: descriptor,
51
+ personalOnly: !instances.includes('workspace')
52
+ });
53
+
54
+ async function stop() {
55
+ const lifecycle = nativeRuntimeLifecycle(paths, instances);
56
+ await services.stopDatabases();
57
+ return lifecycle;
58
+ }
59
+
60
+ async function start(lifecycle = {}) {
61
+ if (lifecycle.resumeProviders === true) {
62
+ await services.start();
63
+ } else if (lifecycle.resumeDatabases === true) {
64
+ await services.start({ providers: false });
65
+ }
66
+ }
67
+
68
+ async function dump(instance, destination) {
69
+ const spec = nativeSpec(paths, descriptor, instance);
70
+ await withStagingDirectory(destination, async (stage) => {
71
+ await run(join(descriptor.neo4jHome, 'bin', 'neo4j-admin'), [
72
+ 'database', 'dump', 'neo4j', `--to-path=${stage}`
73
+ ], { env: spec.environment });
74
+ await rename(join(stage, 'neo4j.dump'), destination);
75
+ });
76
+ }
77
+
78
+ async function load(instance, source) {
79
+ const spec = nativeSpec(paths, descriptor, instance);
80
+ await withStagingDirectory(source, async (stage) => {
81
+ await copyFile(source, join(stage, 'neo4j.dump'));
82
+ await run(join(descriptor.neo4jHome, 'bin', 'neo4j-admin'), [
83
+ 'database', 'load', 'neo4j', `--from-path=${stage}`, '--overwrite-destination=true'
84
+ ], { env: spec.environment });
85
+ });
86
+ }
87
+
88
+ function hasData(instance) {
89
+ return existsSync(join(nativeInstanceDir(paths, instance), 'data', 'databases', 'neo4j'));
90
+ }
91
+
92
+ async function reconcile(input) {
93
+ await services.start();
94
+ try {
95
+ return await reconcileImportedGraphIdentity({ paths, instances, ...input });
96
+ } finally {
97
+ await services.stopDatabases();
98
+ }
99
+ }
100
+
101
+ return {
102
+ mode: 'native',
103
+ instances,
104
+ stop,
105
+ start,
106
+ dump,
107
+ load,
108
+ hasData,
109
+ selection: () => graphBackupSelection(paths),
110
+ reconcile
111
+ };
112
+ }
113
+
114
+ function createContainerBackupAdapter({ paths, instances, runtime, run }) {
115
+ const composePrefix = [
116
+ 'compose', '--env-file', paths.graphEnvPath, '-f', paths.graphComposePath
117
+ ];
118
+ const services = instances.flatMap((instance) => [
119
+ `${instance}-provider`, `${instance}-neo4j`
120
+ ]);
121
+
122
+ async function docker(args) {
123
+ return run(runtime.dockerCommand, args, { env: runtime.dockerEnvironment });
124
+ }
125
+
126
+ async function stop() {
127
+ const running = await docker([
128
+ ...composePrefix, 'ps', '--status', 'running', '--services', ...services
129
+ ]);
130
+ const resumeServices = String(running).split(/\r?\n/)
131
+ .map((service) => service.trim())
132
+ .filter((service) => services.includes(service));
133
+ await docker([...composePrefix, 'stop', '-t', '30', ...services]);
134
+ return { resume: resumeServices.length > 0, resumeServices };
135
+ }
136
+
137
+ async function start(lifecycle = {}) {
138
+ const resumeServices = Array.isArray(lifecycle.resumeServices)
139
+ ? lifecycle.resumeServices.filter((service) => services.includes(service))
140
+ : (lifecycle.resume === true ? services : []);
141
+ if (resumeServices.length === 0) return;
142
+ await docker([...composePrefix, 'up', '-d', '--no-build', ...resumeServices]);
143
+ }
144
+
145
+ async function dump(instance, destination) {
146
+ await withBackupHelper(instance, async (helper) => {
147
+ await docker([
148
+ 'exec', helper,
149
+ 'neo4j-admin', 'database', 'dump', 'neo4j', '--to-path=/backup'
150
+ ]);
151
+ await docker(['cp', `${helper}:/backup/neo4j.dump`, destination]);
152
+ });
153
+ }
154
+
155
+ async function load(instance, source) {
156
+ await withBackupHelper(instance, async (helper) => {
157
+ await docker(['cp', source, `${helper}:/backup/neo4j.dump`]);
158
+ await docker([
159
+ 'exec', helper,
160
+ 'neo4j-admin', 'database', 'load', 'neo4j',
161
+ '--from-path=/backup', '--overwrite-destination=true'
162
+ ]);
163
+ });
164
+ }
165
+
166
+ async function hasData(instance) {
167
+ return withBackupHelper(instance, async (helper) => {
168
+ try {
169
+ await docker(['exec', helper, 'test', '-d', '/data/databases/neo4j']);
170
+ return true;
171
+ } catch {
172
+ return false;
173
+ }
174
+ });
175
+ }
176
+
177
+ async function reconcile(input) {
178
+ await docker([...composePrefix, 'up', '-d', '--no-build', ...services]);
179
+ try {
180
+ return await reconcileImportedGraphIdentity({ paths, instances, ...input });
181
+ } finally {
182
+ await docker([...composePrefix, 'stop', '-t', '30', ...services]);
183
+ }
184
+ }
185
+
186
+ async function withBackupHelper(instance, operation) {
187
+ const helper = `fuli-neo4j-backup-${randomUUID()}`;
188
+ try {
189
+ await docker([
190
+ ...composePrefix,
191
+ 'run', '--detach', '--name', helper, '--no-deps', '-T',
192
+ `${instance}-neo4j`, 'tail', '-f', '/dev/null'
193
+ ]);
194
+ await docker(['exec', helper, 'mkdir', '-p', '/backup']);
195
+ return await operation(helper);
196
+ } finally {
197
+ await docker(['rm', '--force', helper]).catch(() => {});
198
+ }
199
+ }
200
+
201
+ return {
202
+ mode: 'container',
203
+ instances,
204
+ stop,
205
+ start,
206
+ dump,
207
+ load,
208
+ hasData,
209
+ selection: () => graphBackupSelection(paths),
210
+ reconcile
211
+ };
212
+ }
213
+
214
+ function nativeSpec(paths, descriptor, instance) {
215
+ const instanceDir = nativeInstanceDir(paths, instance);
216
+ return {
217
+ instanceDir,
218
+ environment: {
219
+ ...process.env,
220
+ JAVA_HOME: descriptor.javaHome,
221
+ NEO4J_HOME: descriptor.neo4jHome,
222
+ NEO4J_CONF: join(instanceDir, 'conf')
223
+ }
224
+ };
225
+ }
226
+
227
+ function nativeInstanceDir(paths, instance) {
228
+ if (instance === 'personal') return paths.nativePersonalDir;
229
+ if (instance === 'workspace') return paths.nativeWorkspaceDir;
230
+ throw new TypeError(`Unsupported graph instance: ${instance}`);
231
+ }
232
+
233
+ function nativeRuntimeLifecycle(paths, instances) {
234
+ const processes = readJsonFile(paths.nativeProcessStatePath, null);
235
+ const resumeProviders = Object.keys(processes?.providers ?? {}).length > 0;
236
+ const resumeDatabases = Object.keys(processes?.databases ?? {}).length > 0 || instances.some((instance) =>
237
+ existsSync(join(nativeInstanceDir(paths, instance), 'run', 'neo4j.pid')));
238
+ return {
239
+ resume: resumeProviders || resumeDatabases,
240
+ resumeProviders,
241
+ resumeDatabases: resumeProviders || resumeDatabases
242
+ };
243
+ }
244
+
245
+ async function withStagingDirectory(referencePath, operation) {
246
+ const parent = dirname(referencePath);
247
+ const stage = join(parent, `.fuli-neo4j-${randomUUID()}`);
248
+ await mkdir(stage, { recursive: false, mode: 0o700 });
249
+ try {
250
+ return await operation(stage);
251
+ } finally {
252
+ await rm(stage, { recursive: true, force: true });
253
+ }
254
+ }
255
+
256
+ function runCommand(command, args, { env } = {}) {
257
+ return new Promise((resolve, reject) => {
258
+ execFile(command, args, {
259
+ env: env ?? process.env,
260
+ encoding: 'utf8',
261
+ timeout: COMMAND_TIMEOUT_MS,
262
+ maxBuffer: 4 * 1024 * 1024,
263
+ windowsHide: true
264
+ }, (error, stdout, stderr) => {
265
+ if (error) reject(new Error(String(stderr || stdout || error.message).trim()));
266
+ else resolve(stdout);
267
+ });
268
+ });
269
+ }
@@ -19,7 +19,12 @@ Local service:
19
19
  open [--data-dir DIR]
20
20
 
21
21
  Install and Agent connection:
22
- setup [--yes] [--codex-only] [--data-dir DIR] [--personal-space NAME] [--port PORT] [--skip-agents] [--no-start] [--personal-only|--with-dev-public]
22
+ setup [--yes] [--codex-only] [--data-dir DIR] [--personal-space NAME] [--port PORT] [--runtime-mode container|native] [--memory-profile low|balanced] [--adaptive-memory|--no-adaptive-memory] [--skip-agents] [--no-start] [--personal-only|--with-dev-public]
23
+ connect-workspace --url URL --token-file FILE [--data-dir DIR]
23
24
  update [setup options]
24
- uninstall [--yes] [--data-dir DIR]`);
25
+ uninstall [--yes] [--data-dir DIR]
26
+
27
+ Graph data portability:
28
+ graph export --output DIR [--mode container|native] [--data-dir DIR]
29
+ graph import --input DIR [--target-mode container|native] [--yes] [--data-dir DIR]`);
25
30
  }
@@ -0,0 +1,112 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+
3
+ import { exportGraphData, importGraphData } from '../backup/graph-backup.js';
4
+ import { createGraphBackupAdapter } from '../backup/runtime-adapters.js';
5
+ import { resolveSetupPaths } from '../setup/paths.js';
6
+ import { readRuntimeSettings } from '../system/runtime-settings.js';
7
+
8
+ export async function runGraphDataCommand(action, args, dependencies = {}) {
9
+ const options = parseGraphDataOptions(action, args);
10
+ const resolvePaths = dependencies.resolvePaths ?? resolveSetupPaths;
11
+ const paths = resolvePaths({ dataDir: options.dataDir, env: dependencies.env ?? process.env });
12
+ const readSettings = dependencies.readSettings ?? readRuntimeSettings;
13
+ const saved = readSettings(paths.runtimeSettingsPath);
14
+ const mode = options.mode ?? saved.graphRuntimeMode;
15
+ const write = dependencies.write ?? writeLine;
16
+
17
+ if (action === 'import' && !options.yes) {
18
+ const confirm = dependencies.confirm ?? confirmImport;
19
+ if (!await confirm(mode)) {
20
+ write('Cancelled. Graph data was not changed.');
21
+ return { status: 'cancelled' };
22
+ }
23
+ }
24
+
25
+ const createAdapter = dependencies.createAdapter ?? createGraphBackupAdapter;
26
+ const adapter = await createAdapter({
27
+ mode,
28
+ paths,
29
+ onProgress: write
30
+ });
31
+ if (action === 'export') {
32
+ const exportData = dependencies.exportData ?? exportGraphData;
33
+ const result = await exportData({
34
+ outputDir: options.outputDir,
35
+ sourceMode: mode,
36
+ instances: adapter.instances,
37
+ adapter
38
+ });
39
+ write(`Graph data exported from ${mode} mode to ${options.outputDir}.`);
40
+ return result;
41
+ }
42
+
43
+ const importData = dependencies.importData ?? importGraphData;
44
+ const result = await importData({
45
+ inputDir: options.inputDir,
46
+ targetMode: mode,
47
+ rollbackDir: paths.graphBackupDir,
48
+ adapter
49
+ });
50
+ write(result.rollbackDir
51
+ ? `Graph data imported into ${mode} mode. A pre-import rollback backup was preserved.`
52
+ : `Graph data imported into ${mode} mode. The target had no prior data to preserve.`);
53
+ return result;
54
+ }
55
+
56
+ export function parseGraphDataOptions(action, args = []) {
57
+ if (!['export', 'import'].includes(action)) {
58
+ throw new TypeError('Graph command must be export or import.');
59
+ }
60
+ const result = {
61
+ action,
62
+ outputDir: null,
63
+ inputDir: null,
64
+ mode: null,
65
+ dataDir: null,
66
+ yes: false
67
+ };
68
+ const values = action === 'export'
69
+ ? { '--output': 'outputDir', '--mode': 'mode', '--data-dir': 'dataDir' }
70
+ : { '--input': 'inputDir', '--target-mode': 'mode', '--data-dir': 'dataDir' };
71
+ const seen = new Set();
72
+ for (let index = 0; index < args.length; index += 1) {
73
+ const flag = args[index];
74
+ if (seen.has(flag)) throw new TypeError(`Duplicate ${flag}`);
75
+ if (flag === '--yes' && action === 'import') {
76
+ result.yes = true;
77
+ seen.add(flag);
78
+ continue;
79
+ }
80
+ const key = values[flag];
81
+ if (!key) throw new TypeError(`Unknown graph ${action} option: ${flag}`);
82
+ const value = args[index + 1];
83
+ if (typeof value !== 'string' || !value.trim() || value.startsWith('--')) {
84
+ throw new TypeError(`Missing value for ${flag}`);
85
+ }
86
+ result[key] = value.trim();
87
+ seen.add(flag);
88
+ index += 1;
89
+ }
90
+ if (action === 'export' && !result.outputDir) throw new TypeError('--output is required');
91
+ if (action === 'import' && !result.inputDir) throw new TypeError('--input is required');
92
+ if (result.mode && !['container', 'native'].includes(result.mode)) {
93
+ throw new TypeError('Graph runtime mode must be container or native.');
94
+ }
95
+ return result;
96
+ }
97
+
98
+ async function confirmImport(mode) {
99
+ const input = createInterface({ input: process.stdin, output: process.stdout });
100
+ try {
101
+ const answer = (await input.question(
102
+ `Import will replace the current ${mode} graph after making a rollback backup. Continue? [y/N] `
103
+ )).trim().toLowerCase();
104
+ return answer === 'y' || answer === 'yes';
105
+ } finally {
106
+ input.close();
107
+ }
108
+ }
109
+
110
+ function writeLine(value) {
111
+ process.stdout.write(`${value}\n`);
112
+ }
@@ -180,16 +180,30 @@ function formatStatus(result) {
180
180
  const publicSummary = result.public.configured
181
181
  ? `${labels[result.public.status] ?? result.public.status} (${result.public.providers.length} Providers)`
182
182
  : labels.not_connected;
183
- return [
183
+ const lines = [
184
184
  `Fuli local status: ${labels[result.status] ?? result.status}`,
185
185
  `Management UI: ${labels[result.console.status] ?? result.console.status} · ${result.console.url}`,
186
186
  `Personal graph: ${providerLabel(result.personal.status, labels)}`,
187
187
  `Shared services: ${publicSummary}`
188
- ].join('\n');
188
+ ];
189
+ if (result.adaptiveRuntime?.enabled) {
190
+ const stages = {
191
+ awake: 'awake',
192
+ 'provider-sleeping': 'Provider sleeping',
193
+ sleeping: 'Provider and database sleeping',
194
+ waking: 'waking',
195
+ degraded: 'degraded',
196
+ unknown: 'unknown'
197
+ };
198
+ lines.push(`Adaptive memory: ${stages[result.adaptiveRuntime.stage] ??
199
+ result.adaptiveRuntime.stage}`);
200
+ }
201
+ return lines.join('\n');
189
202
  }
190
203
 
191
204
  function providerLabel(status, labels) {
192
205
  if (status === 'not_configured') return labels.not_configured_provider;
206
+ if (status === 'sleeping') return 'sleeping on purpose';
193
207
  return labels[status] ?? status;
194
208
  }
195
209
 
@@ -49,19 +49,35 @@ export function formatSetupPreview(plan, options) {
49
49
  DEFAULT_RUNTIME_SETTINGS,
50
50
  { consolePort: options.port }
51
51
  );
52
- return [
52
+ const runtimeMode = runtimeSettings.graphRuntimeMode;
53
+ const lines = [
53
54
  'Ready to set up Fuli',
54
55
  options.personalOnly
55
56
  ? 'Storage: personal only, using local Graphiti / Neo4j'
56
57
  : 'Storage: local Graphiti / Neo4j plus a development shared-project Provider',
57
- 'Container runtime: detected automatically; an installed desktop runtime starts if needed',
58
+ runtimeMode === 'native'
59
+ ? 'Graph runtime: native processes; no Docker VM required'
60
+ : 'Graph runtime: container; Docker or Rancher Desktop starts if needed',
58
61
  `Shared services: ${options.personalOnly ? 'not connected' : 'local development Provider'}`,
59
62
  `Personal space: ${options.personalSpaceName}`,
63
+ `Neo4j memory: ${options.memoryProfile ?? 'saved profile or balanced default'}`,
64
+ `Adaptive memory: ${plan.adaptiveRuntimeSettings?.enabled ?
65
+ 'enabled; Provider and database sleep after idle time' :
66
+ 'disabled; graph services stay running'}`,
60
67
  `Management UI: ${options.noStart
61
68
  ? 'will not start'
62
69
  : `http://127.0.0.1:${runtimeSettings.ports.console}`}`,
63
70
  `Agents: ${agentSummary}`
64
- ].join('\n');
71
+ ];
72
+ if (
73
+ runtimeMode !== 'native' &&
74
+ plan.runtimeModeRecommendation?.recommendedMode === 'native'
75
+ ) {
76
+ lines.splice(3, 0,
77
+ 'Recommendation: native mode avoids shared VM memory on this low-memory Mac; ' +
78
+ 'select it with --runtime-mode native');
79
+ }
80
+ return lines.join('\n');
65
81
  }
66
82
 
67
83
  export function formatSetupResult(result, plan) {
@@ -137,6 +137,10 @@ export function formatUpdatePreview(options) {
137
137
  options.dataDir
138
138
  ? `Data: preserve ${options.dataDir}`
139
139
  : 'Data: preserve the current default data directory',
140
+ `Neo4j memory: ${options.memoryProfile ?? 'keep saved profile or use balanced by default'}`,
141
+ `Adaptive memory: ${options.adaptiveMemory === null
142
+ ? 'keep saved setting'
143
+ : options.adaptiveMemory ? 'enable' : 'disable'}`,
140
144
  `Agent integrations and Skills: ${options.skipAgents
141
145
  ? 'skip refresh as requested'
142
146
  : 'refresh through setup in the new version'}`