fuli-context 0.7.4-beta.3 → 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.
- package/README.md +77 -6
- package/README.zh-CN.md +69 -6
- package/compose.graphiti.yml +6 -6
- package/dist/web/assets/AboutPage-ByCUor-j.js +1 -0
- package/dist/web/assets/ConnectionsPage-DkdkZ40Q.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-DSe3Gwep.js +1 -0
- package/dist/web/assets/KnowledgeInspector-CYAjAb9J.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-CV4F54R_.js +1 -0
- package/dist/web/assets/KnowledgePage-vYYXipOi.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-z4u5mXOG.js +4 -0
- package/dist/web/assets/OverviewPage-BWOfeS6k.js +1 -0
- package/dist/web/assets/PersonalProfilePage-c3w4-h7a.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-B7AlLEwf.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-CgftpIPB.css +1 -0
- package/dist/web/assets/ProjectAgentsPage-DTHcL_ox.js +11 -0
- package/dist/web/assets/PublicProjectsPage-DdH0oGB3.js +1 -0
- package/dist/web/assets/ReviewPage-BYIpcIHG.js +1 -0
- package/dist/web/assets/SearchableSelect-Dz13ClQb.js +1 -0
- package/dist/web/assets/SettingsPage-2VkbXgZi.js +1 -0
- package/dist/web/assets/SettingsPage-CwZp2ka4.css +1 -0
- package/dist/web/assets/WritingTastePage-BOPQZuva.js +1 -0
- package/dist/web/assets/{console-D_2zC4lm.js → console-BlAhujsH.js} +1 -1
- package/dist/web/assets/{conversation-launcher-settings-DQmMH2j7.js → conversation-launcher-settings-DTYxiLJt.js} +1 -1
- package/dist/web/assets/i18n-Ci3heuiL.js +9 -0
- package/dist/web/assets/{identity-DmGcY3Y6.js → identity-4_-v6cB7.js} +1 -1
- package/dist/web/assets/index-BnH5mcZx.css +1 -0
- package/dist/web/assets/index-C67Q0JY2.js +2 -0
- package/dist/web/assets/{model-CVoOdRsL.js → model-yG0YqUyZ.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-CBNg4CdH.js → useApi-CROJJdhE-twhdZArA.js} +1 -1
- package/dist/web/assets/vue-router-C-wot_Di.js +1 -0
- package/dist/web/index.html +6 -6
- package/graph-provider/fuli_graph/app.py +8 -0
- package/graph-provider/fuli_graph/collaboration_context.py +69 -6
- package/graph-provider/fuli_graph/knowledge_search.py +84 -5
- package/graph-provider/fuli_graph/model_validation.py +130 -0
- package/graph-provider/fuli_graph/models.py +24 -5
- package/graph-provider/fuli_graph/project_agent_access.py +54 -0
- package/graph-provider/fuli_graph/project_agent_executor_models.py +635 -0
- package/graph-provider/fuli_graph/project_agent_models.py +278 -0
- package/graph-provider/fuli_graph/project_agent_routes.py +597 -0
- package/graph-provider/fuli_graph/project_agent_task_models.py +542 -0
- package/graph-provider/fuli_graph/runtime.py +259 -0
- package/graph-provider/fuli_graph/search_projection.py +3 -0
- package/graph-provider/fuli_graph/store.py +6 -0
- package/graph-provider/fuli_graph/store_knowledge.py +62 -9
- package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +756 -0
- package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +601 -0
- package/graph-provider/fuli_graph/store_project_agent_executors.py +1087 -0
- package/graph-provider/fuli_graph/store_project_agent_task_reads.py +458 -0
- package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +699 -0
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +409 -0
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +1144 -0
- package/graph-provider/fuli_graph/store_project_agents.py +965 -0
- package/graph-provider/pyproject.toml +2 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/capturing-session-knowledge/SKILL.md +39 -0
- package/src/adaptive-runtime/executor-pool.js +127 -0
- package/src/adaptive-runtime/graph-services.js +166 -0
- package/src/adaptive-runtime/lease-client.js +335 -0
- package/src/adaptive-runtime/runtime-broker.js +318 -0
- package/src/adaptive-runtime/settings.js +111 -0
- package/src/adaptive-runtime/state.js +74 -0
- package/src/agent-tools/graph-definitions.js +396 -0
- package/src/agent-tools/graph-handlers.js +61 -0
- package/src/agent-tools/project-agent-definitions.js +307 -0
- package/src/backup/graph-backup.js +289 -0
- package/src/backup/identity-reconciliation.js +208 -0
- package/src/backup/runtime-adapters.js +269 -0
- package/src/cli/command-registry.js +6 -2
- package/src/cli/graph-data-command.js +112 -0
- package/src/cli/local-runtime-command.js +16 -2
- package/src/cli/setup-command.js +19 -3
- package/src/cli/update-command.js +4 -0
- package/src/cli.js +6 -0
- package/src/graphiti/collaboration-preference-projection.js +29 -1
- package/src/graphiti/collaboration-preference-workflow.js +121 -0
- package/src/graphiti/federated-application.js +53 -90
- package/src/graphiti/project-agent-control-plane.js +263 -0
- package/src/graphiti/project-agent-mapping.js +1003 -0
- package/src/graphiti/project-agent-workflows.js +557 -0
- package/src/graphiti/provider-client.js +270 -1
- package/src/http/api-router.js +63 -9
- package/src/http/graph-api-router.js +278 -0
- package/src/local-runtime/lifecycle.js +46 -10
- package/src/mcp/context-resources.js +15 -6
- package/src/mcp/create-mcp-server.js +51 -16
- package/src/mcp/instructions.js +7 -5
- package/src/mcp/runtime.js +17 -5
- package/src/mcp/session-id.js +38 -1
- package/src/mcp/tool-annotations.js +69 -0
- package/src/mcp-server.js +23 -6
- package/src/native-runtime/runtime.js +717 -0
- package/src/server.js +6 -2
- package/src/setup/agent-installation-status.js +7 -1
- package/src/setup/agents.js +3 -1
- package/src/setup/claude-code-config.js +3 -1
- package/src/setup/codex-bootstrap.js +11 -10
- package/src/setup/codex-config.js +7 -1
- package/src/setup/cursor-config.js +3 -1
- package/src/setup/graph-runtime.js +158 -13
- package/src/setup/neo4j-memory-profile.js +43 -0
- package/src/setup/options.js +38 -2
- package/src/setup/paths.js +14 -0
- package/src/setup/setup.js +37 -1
- package/src/system/resource-monitor.js +181 -16
- package/src/system/runtime-settings.js +10 -1
- package/src/system/system-service.js +27 -2
- package/dist/web/assets/AboutPage-Dpf5fRWv.js +0 -1
- package/dist/web/assets/ConnectionsPage-D21aUDCP.js +0 -1
- package/dist/web/assets/KnowledgeEditDialog-u7lpfN3j.js +0 -1
- package/dist/web/assets/KnowledgeInspector-k4xnHtXd.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-Bdj7Rn84.js +0 -1
- package/dist/web/assets/KnowledgePage-UHVAbC1W.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-Lq3d-MeC.js +0 -4
- package/dist/web/assets/OverviewPage-CycAgA1V.js +0 -1
- package/dist/web/assets/PersonalProfilePage-U9oIV5FT.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-C5RpXKcA.js +0 -1
- package/dist/web/assets/PublicProjectsPage-DiWxm35E.js +0 -1
- package/dist/web/assets/ReviewPage-jxRfGSJK.js +0 -1
- package/dist/web/assets/SearchableSelect-B3-5IPup.js +0 -1
- package/dist/web/assets/SettingsPage-BqfGDOho.js +0 -1
- package/dist/web/assets/SettingsPage-CAfz4qiV.css +0 -1
- package/dist/web/assets/WritingTastePage-BJxhMqJR.js +0 -1
- package/dist/web/assets/i18n-CwqXDI1C.js +0 -5
- package/dist/web/assets/index-IUGLt4lG.js +0 -2
- package/dist/web/assets/index-X2j29j8i.css +0 -1
- package/dist/web/assets/vue-router-CWSsUUio.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,8 +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
23
|
connect-workspace --url URL --token-file FILE [--data-dir DIR]
|
|
24
24
|
update [setup options]
|
|
25
|
-
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]`);
|
|
26
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
|
-
|
|
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
|
-
]
|
|
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
|
|
package/src/cli/setup-command.js
CHANGED
|
@@ -49,19 +49,35 @@ export function formatSetupPreview(plan, options) {
|
|
|
49
49
|
DEFAULT_RUNTIME_SETTINGS,
|
|
50
50
|
{ consolePort: options.port }
|
|
51
51
|
);
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
]
|
|
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'}`
|