fuli-context 0.4.0 → 0.7.2
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.en.md +5 -0
- package/README.md +411 -173
- package/README.zh-CN.md +448 -0
- package/compose.graphiti.yml +6 -6
- package/dist/web/assets/AboutPage-DXh7Fwxt.css +1 -0
- package/dist/web/assets/AboutPage-DfnZo7Lo.js +1 -0
- package/dist/web/assets/ConnectionsPage-BloaSKmt.js +1 -0
- package/dist/web/assets/ConnectionsPage-CBHAdZXT.css +1 -0
- package/dist/web/assets/KnowledgeInspector-Bjhdu2mf.css +1 -0
- package/dist/web/assets/KnowledgeInspector-QvNpv0_k.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-DKq1jVD5.js +1 -0
- package/dist/web/assets/KnowledgePage-BWL0jT01.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-Czq6aZ7b.js +4 -0
- package/dist/web/assets/OverviewPage-5sJoO1Gh.js +1 -0
- package/dist/web/assets/{PersonalProfilePage-S8ZcG2LU.css → PersonalProfilePage-CdmEzyvZ.css} +1 -1
- package/dist/web/assets/PersonalProfilePage-tVChM9ik.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-BPFrup4V.js +1 -0
- package/dist/web/assets/PublicProjectsPage-BPJSeREN.js +1 -0
- package/dist/web/assets/ReviewPage-B7EN817i.js +1 -0
- package/dist/web/assets/SearchableSelect-ClLN0rIb.js +1 -0
- package/dist/web/assets/SettingsPage-C6ouU0sR.css +1 -0
- package/dist/web/assets/SettingsPage-Chd_B3eH.js +1 -0
- package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
- package/dist/web/assets/console-CbDsKrsO.js +1 -0
- package/dist/web/assets/fuli-logo-Bu9WBuQe.js +1 -0
- package/dist/web/assets/fuli-logo-ER1wQ8Kk.png +0 -0
- package/dist/web/assets/i18n-BQcns1PH.js +5 -0
- package/dist/web/assets/identity-B8K4VYg6.js +1 -0
- package/dist/web/assets/index-CB8Xmsc2.js +2 -0
- package/dist/web/assets/index-CZeuPh-U.css +1 -0
- package/dist/web/assets/model-eF52GgPF.js +1 -0
- package/dist/web/assets/paths-hwz-Ac1g.js +1 -0
- package/dist/web/assets/useMinimumLoadingDisplay-BpCyBeko.css +1 -0
- package/dist/web/assets/useMinimumLoadingDisplay-wCgGDmHg.js +1 -0
- package/dist/web/assets/vue-router-BsEp_XwI.js +1 -0
- package/dist/web/index.html +9 -6
- package/docs/external-knowledge-architecture.md +279 -0
- package/examples/external-knowledge/markdown-folder.mjs +235 -0
- package/graph-provider/fuli_graph/app.py +57 -1
- package/graph-provider/fuli_graph/graph_models.py +3 -0
- package/graph-provider/fuli_graph/graph_query.py +62 -16
- package/graph-provider/fuli_graph/knowledge_review.py +612 -0
- package/graph-provider/fuli_graph/knowledge_review_models.py +127 -0
- package/graph-provider/fuli_graph/knowledge_search.py +51 -4
- package/graph-provider/fuli_graph/models.py +19 -0
- package/graph-provider/fuli_graph/runtime.py +15 -0
- package/graph-provider/fuli_graph/store_knowledge.py +2 -1
- package/graph-provider/pyproject.toml +1 -1
- package/npm-shrinkwrap.json +5 -407
- package/package.json +7 -4
- package/skills/capturing-session-knowledge/SKILL.md +24 -4
- package/skills/flreview/SKILL.md +73 -0
- package/skills/flreview/agents/openai.yaml +6 -0
- package/src/agent-tools/graph-definitions.js +79 -4
- package/src/agent-tools/graph-handlers.js +12 -0
- package/src/cli/command-registry.js +3 -3
- package/src/cli/local-runtime-command.js +60 -23
- package/src/cli/local-runtime-options.js +16 -6
- package/src/cli/setup-agent-selection.js +12 -12
- package/src/cli/setup-command.js +41 -18
- package/src/cli/shell-argument.js +10 -0
- package/src/cli/uninstall-command.js +17 -15
- package/src/cli/update-command.js +38 -30
- package/src/external-knowledge/catalog.js +61 -0
- package/src/external-knowledge/conflict-policy.js +58 -0
- package/src/external-knowledge/connected-search.js +166 -0
- package/src/external-knowledge/connectors/custom.js +133 -0
- package/src/external-knowledge/connectors/feishu.js +461 -0
- package/src/external-knowledge/connectors/mcp.js +438 -0
- package/src/external-knowledge/connectors/notion.js +307 -0
- package/src/external-knowledge/connectors/retrieval-api.js +222 -0
- package/src/external-knowledge/document-mapping.js +151 -0
- package/src/external-knowledge/graph-projection.js +17 -0
- package/src/external-knowledge/index.js +8 -0
- package/src/external-knowledge/registry.js +91 -0
- package/src/external-knowledge/runtime.js +68 -0
- package/src/external-knowledge/safe-diagnostic.js +14 -0
- package/src/external-knowledge/service.js +811 -0
- package/src/graphiti/agent-knowledge-workflows.js +7 -2
- package/src/graphiti/federated-application.js +66 -41
- package/src/graphiti/knowledge-item-role.js +21 -0
- package/src/graphiti/knowledge-review.js +43 -0
- package/src/graphiti/project-profile-mapping.js +32 -0
- package/src/graphiti/provider-client.js +22 -1
- package/src/graphiti/source-marker.js +54 -3
- package/src/graphiti/task-knowledge-recall.js +250 -0
- package/src/http/api-router.js +23 -108
- package/src/http/external-knowledge-api-router.js +104 -0
- package/src/http/graph-api-router.js +12 -1
- package/src/http/request-policy.js +38 -3
- package/src/local-runtime/lifecycle.js +37 -16
- package/src/mcp/create-mcp-server.js +1 -0
- package/src/mcp/instructions.js +7 -7
- package/src/mcp/tool-annotations.js +6 -0
- package/src/mcp/tool-result.js +1 -0
- package/src/server/application-lifecycle.js +2 -11
- package/src/server/blocked-ports.js +11 -0
- package/src/server/lan-access.js +52 -0
- package/src/server.js +103 -25
- package/src/setup/agent-installation-status.js +2 -1
- package/src/setup/agents.js +2 -1
- package/src/setup/claude-code-config.js +2 -1
- package/src/setup/codex-bootstrap.js +12 -7
- package/src/setup/container-runtime.js +25 -21
- package/src/setup/graph-runtime.js +219 -58
- package/src/setup/node-runtime.js +3 -3
- package/src/setup/options.js +2 -4
- package/src/setup/paths.js +18 -3
- package/src/setup/review-skill-adapters.js +49 -0
- package/src/setup/setup.js +26 -2
- package/src/setup/uninstall.js +4 -2
- package/src/system/resource-monitor.js +317 -0
- package/src/system/runtime-settings.js +131 -0
- package/src/system/system-service.js +119 -0
- package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +0 -1
- package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +0 -1
- package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-CJCNI2Wd.css +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-PXH-TlGt.js +0 -1
- package/dist/web/assets/KnowledgePage-CNQXd15y.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +0 -4
- package/dist/web/assets/OverviewPage-ul7g7HXh.js +0 -1
- package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +0 -1
- package/dist/web/assets/PublicProjectsPage-C700sR7z.js +0 -1
- package/dist/web/assets/ReviewPage-7MDDUDgA.js +0 -1
- package/dist/web/assets/console-B_Ena2DS.js +0 -5
- package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
- package/dist/web/assets/identity-CjBwYSMX.js +0 -1
- package/dist/web/assets/index-BQRkOwPx.js +0 -2
- package/dist/web/assets/index-CpLDx3of.css +0 -1
- package/dist/web/assets/model-C-5RVIJ2.js +0 -1
- package/dist/web/assets/paths-BkULqmGI.js +0 -1
- package/dist/web/assets/vue-router-DnD3hUm7.js +0 -1
- package/src/agent/candidate-read-service.js +0 -53
- package/src/agent/context-pack-service.js +0 -83
- package/src/agent/public-read-policy.js +0 -39
- package/src/agent/public-read-service.js +0 -69
- package/src/agent/read-projection.js +0 -107
- package/src/agent/search-service.js +0 -61
- package/src/agent/space-boundary.js +0 -33
- package/src/app/bootstrap.js +0 -8
- package/src/app/create-application.js +0 -143
- package/src/app/state-service.js +0 -38
- package/src/candidates.js +0 -78
- package/src/classifier.js +0 -63
- package/src/cli/command-arguments.js +0 -14
- package/src/context-pack.js +0 -206
- package/src/extractor.js +0 -77
- package/src/fact-history.js +0 -39
- package/src/facts/fact-service.js +0 -89
- package/src/http/bounded-json-array.js +0 -19
- package/src/http/lens-route.js +0 -50
- package/src/ingestion.js +0 -161
- package/src/lens/lens-correction-service.js +0 -81
- package/src/lens/lens-fact-writer.js +0 -85
- package/src/lens/lens-input.js +0 -62
- package/src/lens/lens-query-projection.js +0 -87
- package/src/lens/lens-query-ranking.js +0 -62
- package/src/lens/lens-query.js +0 -143
- package/src/lens/lens-resource-projection.js +0 -93
- package/src/lens/lens-resource-selection.js +0 -45
- package/src/lens/lens-resource-service.js +0 -103
- package/src/lens/lens-search-query.js +0 -122
- package/src/lens/lens-service.js +0 -183
- package/src/models.js +0 -44
- package/src/observer.js +0 -83
- package/src/project-rules.js +0 -33
- package/src/publication/outbox-service.js +0 -46
- package/src/publication/publication-envelope-schema.js +0 -75
- package/src/publication/publication-envelope.js +0 -37
- package/src/publication/publication-policy.js +0 -82
- package/src/publication/publication-service.js +0 -48
- package/src/router.js +0 -58
- package/src/runtime-options.js +0 -138
- package/src/security/episode-visibility.js +0 -14
- package/src/security/fact-visibility.js +0 -15
- package/src/security/source-metadata-policy.js +0 -8
- package/src/storage/episode-evidence-preview.js +0 -100
- package/src/storage/fact-search-order.js +0 -18
- package/src/storage/file/episode-evidence.js +0 -37
- package/src/storage/file/episode-index.js +0 -59
- package/src/storage/file/fact-search.js +0 -60
- package/src/storage/file-store.js +0 -437
- package/src/storage/migrate.js +0 -128
- package/src/storage/migrations/001-initial.sql +0 -83
- package/src/storage/migrations/002-storage-integrity.sql +0 -113
- package/src/storage/record-validation.js +0 -46
- package/src/storage/snapshot-defaults.js +0 -54
- package/src/storage/sqlite/candidates-repository.js +0 -84
- package/src/storage/sqlite/episode-evidence-repository.js +0 -154
- package/src/storage/sqlite/episode-privacy-sql.js +0 -33
- package/src/storage/sqlite/episodes-repository.js +0 -52
- package/src/storage/sqlite/fact-search-repository.js +0 -162
- package/src/storage/sqlite/facts-repository.js +0 -146
- package/src/storage/sqlite/imports-repository.js +0 -39
- package/src/storage/sqlite/mapper.js +0 -84
- package/src/storage/sqlite/open-database.js +0 -15
- package/src/storage/sqlite/outbox-repository.js +0 -103
- package/src/storage/sqlite/snapshot-repository.js +0 -69
- package/src/storage/sqlite/spaces-repository.js +0 -52
- package/src/storage/sqlite/subscriptions-repository.js +0 -54
- package/src/storage/sqlite-store.js +0 -159
- package/src/storage/stable-json.js +0 -24
- package/src/storage/store-port.js +0 -20
- package/src/store.js +0 -2
|
@@ -86,7 +86,7 @@ export async function ensureContainerRuntime(options = {}, dependencies = {}) {
|
|
|
86
86
|
throw containerRuntimeError(runtime, options.platform ?? process.platform);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
onProgress(
|
|
89
|
+
onProgress(`Starting ${runtime.desktop.label}; the first launch may take a few minutes...`);
|
|
90
90
|
await launch(runtime.desktop, dependencies);
|
|
91
91
|
|
|
92
92
|
const attempts = Math.max(1, Math.ceil(maxWaitMs / pollIntervalMs));
|
|
@@ -100,9 +100,9 @@ export async function ensureContainerRuntime(options = {}, dependencies = {}) {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
throw new Error(
|
|
103
|
-
`${runtime.desktop?.label ?? runtime.label}
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
`${runtime.desktop?.label ?? runtime.label} started, but the container engine was not ready ` +
|
|
104
|
+
`within ${Math.ceil(maxWaitMs / 1000)} seconds. Open the application to inspect errors, ` +
|
|
105
|
+
`confirm that docker info succeeds, then run fl setup again.${detailSuffix(runtime.detail)}`
|
|
106
106
|
);
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -124,12 +124,14 @@ export function runDockerCompose(args, runtime = null, {
|
|
|
124
124
|
|
|
125
125
|
const detail = safeProcessDetail(result, homeDir);
|
|
126
126
|
if (/port is already allocated|address already in use|bind.+failed/i.test(detail)) {
|
|
127
|
-
throw new Error(`Fuli
|
|
127
|
+
throw new Error(`A local port required by Fuli is already in use.${detailSuffix(detail)}`);
|
|
128
128
|
}
|
|
129
129
|
if (/cannot connect|connection refused|error during connect|docker daemon|\bEOF\b/i.test(detail)) {
|
|
130
|
-
throw new Error(
|
|
130
|
+
throw new Error(
|
|
131
|
+
`The container runtime disconnected while starting the Fuli Provider.${detailSuffix(detail)}`
|
|
132
|
+
);
|
|
131
133
|
}
|
|
132
|
-
throw new Error(`Docker Compose
|
|
134
|
+
throw new Error(`Docker Compose could not start the Fuli Provider.${detailSuffix(detail)}`);
|
|
133
135
|
}
|
|
134
136
|
|
|
135
137
|
export function selectDockerEnvironment({
|
|
@@ -161,19 +163,21 @@ export function containerRuntimeError(runtime, platform = process.platform) {
|
|
|
161
163
|
}
|
|
162
164
|
if (runtime.status === 'missing-compose') {
|
|
163
165
|
return new Error(
|
|
164
|
-
|
|
166
|
+
'Docker was found, but Docker Compose v2 is missing. Install Compose v2, then run ' +
|
|
167
|
+
'fl setup again.' +
|
|
165
168
|
detailSuffix(runtime.detail)
|
|
166
169
|
);
|
|
167
170
|
}
|
|
168
171
|
if (runtime.explicitTarget) {
|
|
169
172
|
return new Error(
|
|
170
|
-
|
|
171
|
-
`docker info
|
|
173
|
+
'The container engine selected by DOCKER_HOST or DOCKER_CONTEXT is unavailable. Fix the ' +
|
|
174
|
+
`configuration, confirm that docker info succeeds, then run fl setup again.${detailSuffix(runtime.detail)}`
|
|
172
175
|
);
|
|
173
176
|
}
|
|
174
177
|
return new Error(
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
'Docker and Compose were found, but the container engine is not running. Start Docker ' +
|
|
179
|
+
'Desktop, Rancher Desktop, or the current Docker service; confirm that docker info succeeds; ' +
|
|
180
|
+
'then run fl setup again.' +
|
|
177
181
|
detailSuffix(runtime.detail)
|
|
178
182
|
);
|
|
179
183
|
}
|
|
@@ -330,17 +334,17 @@ async function launchDesktopRuntime(desktop, { spawnProcess = spawn } = {}) {
|
|
|
330
334
|
|
|
331
335
|
function missingRuntimeMessage(platform) {
|
|
332
336
|
if (platform === 'darwin') {
|
|
333
|
-
return
|
|
334
|
-
`(${RANCHER_DOWNLOAD_URL})
|
|
335
|
-
|
|
337
|
+
return 'No usable Docker Compose container runtime was detected. Install and open Rancher ' +
|
|
338
|
+
`Desktop (${RANCHER_DOWNLOAD_URL}) or Docker Desktop (${DOCKER_DOWNLOAD_URL}), then run ` +
|
|
339
|
+
'fl setup again.';
|
|
336
340
|
}
|
|
337
341
|
if (platform === 'win32') {
|
|
338
|
-
return
|
|
339
|
-
`Rancher Desktop (${RANCHER_DOWNLOAD_URL})
|
|
340
|
-
`(${DOCKER_DOWNLOAD_URL})
|
|
342
|
+
return 'No usable Docker Compose container runtime was detected. Enable WSL 2, then install ' +
|
|
343
|
+
`and open Rancher Desktop (${RANCHER_DOWNLOAD_URL}) or Docker Desktop ` +
|
|
344
|
+
`(${DOCKER_DOWNLOAD_URL}), and run fl setup again.`;
|
|
341
345
|
}
|
|
342
|
-
return
|
|
343
|
-
|
|
346
|
+
return 'No usable Docker Compose container runtime was detected. Install Docker Engine with ' +
|
|
347
|
+
`Compose v2, or Rancher Desktop (${RANCHER_DOWNLOAD_URL}), then run fl setup again.`;
|
|
344
348
|
}
|
|
345
349
|
|
|
346
350
|
function defaultDockerAvailable(env) {
|
|
@@ -372,7 +376,7 @@ function safeProcessDetail(result, homeDir) {
|
|
|
372
376
|
}
|
|
373
377
|
|
|
374
378
|
function detailSuffix(detail) {
|
|
375
|
-
return nonEmpty(detail) ? ` Docker
|
|
379
|
+
return nonEmpty(detail) ? ` Docker returned: ${detail}` : '';
|
|
376
380
|
}
|
|
377
381
|
|
|
378
382
|
function processSucceeded(result) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import {
|
|
4
4
|
chmodSync,
|
|
@@ -15,38 +15,61 @@ import {
|
|
|
15
15
|
ensureContainerRuntime,
|
|
16
16
|
runDockerCompose
|
|
17
17
|
} from './container-runtime.js';
|
|
18
|
+
import {
|
|
19
|
+
discoverLanAddresses,
|
|
20
|
+
LAN_ACCESS_USERNAME,
|
|
21
|
+
lanConsoleUrls
|
|
22
|
+
} from '../server/lan-access.js';
|
|
18
23
|
import { readJsonFile, writeJsonFileAtomic } from '../storage/json-file.js';
|
|
24
|
+
import {
|
|
25
|
+
DEFAULT_RUNTIME_SETTINGS,
|
|
26
|
+
managedProviderUrls,
|
|
27
|
+
normalizeRuntimeSettings,
|
|
28
|
+
runtimeSettingsWithOverrides,
|
|
29
|
+
writeRuntimeSettings
|
|
30
|
+
} from '../system/runtime-settings.js';
|
|
19
31
|
|
|
20
32
|
export {
|
|
21
33
|
dockerInfoIndicatesDaemon,
|
|
22
34
|
selectDockerEnvironment
|
|
23
35
|
} from './container-runtime.js';
|
|
24
36
|
|
|
25
|
-
const PERSONAL_PROVIDER_URL = 'http://127.0.0.1:8787';
|
|
26
|
-
const WORKSPACE_PROVIDER_URL = 'http://127.0.0.1:8788';
|
|
27
|
-
|
|
28
|
-
export const MANAGED_PERSONAL_PROVIDER_URL = PERSONAL_PROVIDER_URL;
|
|
29
|
-
export const MANAGED_WORKSPACE_PROVIDER_URL = WORKSPACE_PROVIDER_URL;
|
|
30
|
-
|
|
31
37
|
export async function ensureGraphRuntime(input, dependencies = {}) {
|
|
32
38
|
const deps = graphDependencies(dependencies);
|
|
39
|
+
const runtimeSettings = normalizeRuntimeSettings(input.runtimeSettings ??
|
|
40
|
+
runtimeSettingsWithOverrides(DEFAULT_RUNTIME_SETTINGS, {
|
|
41
|
+
consolePort: input.port,
|
|
42
|
+
lanAccess: input.lan === true
|
|
43
|
+
}));
|
|
44
|
+
const urls = managedProviderUrls(runtimeSettings);
|
|
45
|
+
const lan = runtimeSettings.lanAccess;
|
|
46
|
+
const lanAddresses = lan ? deps.discoverLanAddresses() : [];
|
|
47
|
+
if (lan && lanAddresses.length === 0) {
|
|
48
|
+
throw new Error('No private IPv4 LAN address is available, so LAN mode cannot start.');
|
|
49
|
+
}
|
|
33
50
|
const containerRuntime = await deps.ensureContainerRuntime({
|
|
34
51
|
env: input.env ?? process.env,
|
|
35
52
|
onProgress: input.onProgress
|
|
36
53
|
});
|
|
37
54
|
deps.ensureDirectory(input.paths.dataDir);
|
|
38
|
-
const secrets =
|
|
55
|
+
const secrets = ensureProviderEnvironment(
|
|
56
|
+
input.paths.graphEnvPath,
|
|
57
|
+
runtimeSettings,
|
|
58
|
+
deps
|
|
59
|
+
);
|
|
60
|
+
deps.writeRuntimeSettings(input.paths.runtimeSettingsPath, runtimeSettings);
|
|
39
61
|
deps.startProviders(input.paths, input.paths.graphEnvPath, {
|
|
40
62
|
personalOnly: input.personalOnly === true,
|
|
41
63
|
containerRuntime,
|
|
42
64
|
...(input.buildProviders === false ? { build: false } : {})
|
|
43
65
|
});
|
|
44
|
-
await waitForProvider(
|
|
45
|
-
if (!input.personalOnly) await waitForProvider(
|
|
66
|
+
await waitForProvider(urls.personal, deps.fetch);
|
|
67
|
+
if (!input.personalOnly) await waitForProvider(urls.workspace, deps.fetch);
|
|
46
68
|
|
|
47
69
|
let config = deps.readConfig(input.paths.graphRuntimeConfigPath);
|
|
48
70
|
if (!config) {
|
|
49
71
|
config = await bootstrapGraph({
|
|
72
|
+
urls,
|
|
50
73
|
personalSpaceName: input.personalSpaceName,
|
|
51
74
|
secrets,
|
|
52
75
|
personalOnly: input.personalOnly === true,
|
|
@@ -54,64 +77,105 @@ export async function ensureGraphRuntime(input, dependencies = {}) {
|
|
|
54
77
|
});
|
|
55
78
|
deps.writeConfig(input.paths.graphRuntimeConfigPath, config);
|
|
56
79
|
deps.secureFile(input.paths.graphRuntimeConfigPath);
|
|
80
|
+
} else {
|
|
81
|
+
const synchronized = synchronizeManagedProviderUrls(config, urls, {
|
|
82
|
+
personalOnly: input.personalOnly === true
|
|
83
|
+
});
|
|
84
|
+
if (synchronized.changed) {
|
|
85
|
+
await reconcileManagedWorkspaceSubscriptions({
|
|
86
|
+
previousConfig: config,
|
|
87
|
+
nextConfig: synchronized.config,
|
|
88
|
+
personalUrl: urls.personal,
|
|
89
|
+
fetchImpl: deps.fetch
|
|
90
|
+
});
|
|
91
|
+
config = synchronized.config;
|
|
92
|
+
deps.writeConfig(input.paths.graphRuntimeConfigPath, config);
|
|
93
|
+
deps.secureFile(input.paths.graphRuntimeConfigPath);
|
|
94
|
+
}
|
|
57
95
|
}
|
|
58
|
-
stopOwnedLegacyRuntime(input.paths, deps);
|
|
59
96
|
|
|
60
97
|
if (input.noStart) return { status: 'initialized', url: null, pid: null };
|
|
61
98
|
const existing = deps.readState(input.paths.graphRuntimeStatePath);
|
|
62
99
|
if (isGraphRuntimeState(existing) && deps.isProcessAlive(existing.pid)) {
|
|
63
100
|
const healthy = await deps.webHealth(existing.url, existing.pid, existing.version);
|
|
64
|
-
if (
|
|
65
|
-
|
|
101
|
+
if (
|
|
102
|
+
healthy &&
|
|
103
|
+
existing.port === runtimeSettings.ports.console &&
|
|
104
|
+
exposureMatches(existing, { lan, lanAddresses })
|
|
105
|
+
) {
|
|
106
|
+
return runtimeResult('running', existing);
|
|
66
107
|
}
|
|
67
108
|
if (!healthy) throw new Error('Recorded Fuli Graphiti runtime is not healthy');
|
|
68
109
|
deps.stopProcess(existing.pid);
|
|
110
|
+
if (!await deps.waitForExit(existing.pid)) {
|
|
111
|
+
throw new Error('Fuli Graphiti console did not stop before changing exposure mode');
|
|
112
|
+
}
|
|
69
113
|
}
|
|
114
|
+
const lanAccessToken = lan ? deps.createLanAccessToken() : null;
|
|
70
115
|
const child = deps.spawnWebRuntime({
|
|
71
116
|
paths: input.paths,
|
|
72
117
|
nodePath: process.execPath,
|
|
73
|
-
port:
|
|
118
|
+
port: runtimeSettings.ports.console,
|
|
119
|
+
lan,
|
|
120
|
+
lanAccessToken,
|
|
121
|
+
env: input.env ?? process.env
|
|
74
122
|
});
|
|
75
|
-
const url = `http://127.0.0.1:${
|
|
123
|
+
const url = `http://127.0.0.1:${runtimeSettings.ports.console}`;
|
|
76
124
|
if (!Number.isInteger(child.pid) || !await waitForWeb(url, child.pid, deps)) {
|
|
77
125
|
if (Number.isInteger(child.pid)) deps.stopProcess(child.pid);
|
|
78
126
|
throw new Error('Fuli Graphiti console could not start');
|
|
79
127
|
}
|
|
80
|
-
|
|
81
|
-
version:
|
|
128
|
+
const state = {
|
|
129
|
+
version: 4,
|
|
82
130
|
pid: child.pid,
|
|
83
131
|
url,
|
|
84
|
-
port:
|
|
132
|
+
port: runtimeSettings.ports.console,
|
|
133
|
+
runtimeSettings,
|
|
134
|
+
...(lan
|
|
135
|
+
? {
|
|
136
|
+
lan: true,
|
|
137
|
+
lanUrls: lanConsoleUrls(lanAddresses, runtimeSettings.ports.console)
|
|
138
|
+
}
|
|
139
|
+
: {}),
|
|
85
140
|
managedProviders: input.personalOnly
|
|
86
141
|
? ['personal']
|
|
87
142
|
: ['personal', 'development-workspace'],
|
|
88
143
|
startedAt: new Date().toISOString()
|
|
89
|
-
}
|
|
90
|
-
|
|
144
|
+
};
|
|
145
|
+
deps.writeState(input.paths.graphRuntimeStatePath, state);
|
|
146
|
+
deps.secureFile(input.paths.graphRuntimeStatePath);
|
|
147
|
+
return runtimeResult('started', state, lanAccessToken);
|
|
91
148
|
}
|
|
92
149
|
|
|
93
|
-
function
|
|
94
|
-
|
|
150
|
+
function ensureProviderEnvironment(path, settings, deps) {
|
|
151
|
+
const previous = deps.fileExists(path) ? parseEnv(deps.readText(path)) : {};
|
|
95
152
|
const values = {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
153
|
+
...previous,
|
|
154
|
+
FULI_PERSONAL_NEO4J_PASSWORD: previous.FULI_PERSONAL_NEO4J_PASSWORD ?? secret(),
|
|
155
|
+
FULI_WORKSPACE_NEO4J_PASSWORD: previous.FULI_WORKSPACE_NEO4J_PASSWORD ?? secret(),
|
|
156
|
+
FULI_PERSONAL_BOOTSTRAP_TOKEN: previous.FULI_PERSONAL_BOOTSTRAP_TOKEN ?? secret(),
|
|
157
|
+
FULI_WORKSPACE_BOOTSTRAP_TOKEN: previous.FULI_WORKSPACE_BOOTSTRAP_TOKEN ?? secret(),
|
|
158
|
+
FULI_PERSONAL_NEO4J_HTTP_PORT: String(settings.ports.personalNeo4jHttp),
|
|
159
|
+
FULI_PERSONAL_NEO4J_BOLT_PORT: String(settings.ports.personalNeo4jBolt),
|
|
160
|
+
FULI_WORKSPACE_NEO4J_HTTP_PORT: String(settings.ports.workspaceNeo4jHttp),
|
|
161
|
+
FULI_WORKSPACE_NEO4J_BOLT_PORT: String(settings.ports.workspaceNeo4jBolt),
|
|
162
|
+
FULI_PERSONAL_PROVIDER_PORT: String(settings.ports.personalProvider),
|
|
163
|
+
FULI_WORKSPACE_PROVIDER_PORT: String(settings.ports.workspaceProvider)
|
|
100
164
|
};
|
|
101
165
|
const body = Object.entries(values).map(([key, value]) => `${key}=${value}`).join('\n') + '\n';
|
|
102
|
-
deps.writeText(path, body);
|
|
166
|
+
if (!deps.fileExists(path) || deps.readText(path) !== body) deps.writeText(path, body);
|
|
103
167
|
deps.secureFile(path);
|
|
104
168
|
return values;
|
|
105
169
|
}
|
|
106
170
|
|
|
107
|
-
async function bootstrapGraph({ personalSpaceName, secrets, personalOnly, fetchImpl }) {
|
|
171
|
+
async function bootstrapGraph({ urls, personalSpaceName, secrets, personalOnly, fetchImpl }) {
|
|
108
172
|
const personalIdentity = await bootstrapProvider(
|
|
109
|
-
|
|
173
|
+
urls.personal,
|
|
110
174
|
secrets.FULI_PERSONAL_BOOTSTRAP_TOKEN,
|
|
111
175
|
personalSpaceName,
|
|
112
176
|
fetchImpl
|
|
113
177
|
);
|
|
114
|
-
const personalSpace = await findOrCreateSpace(
|
|
178
|
+
const personalSpace = await findOrCreateSpace(urls.personal, {
|
|
115
179
|
token: personalIdentity.access_token,
|
|
116
180
|
name: personalSpaceName,
|
|
117
181
|
kind: 'personal',
|
|
@@ -120,7 +184,7 @@ async function bootstrapGraph({ personalSpaceName, secrets, personalOnly, fetchI
|
|
|
120
184
|
const config = {
|
|
121
185
|
version: 1,
|
|
122
186
|
personal: {
|
|
123
|
-
providerUrl:
|
|
187
|
+
providerUrl: urls.personal,
|
|
124
188
|
accessToken: personalIdentity.access_token,
|
|
125
189
|
principalId: personalIdentity.principal_id,
|
|
126
190
|
spaceId: personalSpace.id
|
|
@@ -130,35 +194,108 @@ async function bootstrapGraph({ personalSpaceName, secrets, personalOnly, fetchI
|
|
|
130
194
|
if (personalOnly) return config;
|
|
131
195
|
|
|
132
196
|
const workspaceIdentity = await bootstrapProvider(
|
|
133
|
-
|
|
197
|
+
urls.workspace,
|
|
134
198
|
secrets.FULI_WORKSPACE_BOOTSTRAP_TOKEN,
|
|
135
199
|
personalSpaceName,
|
|
136
200
|
fetchImpl
|
|
137
201
|
);
|
|
138
|
-
const project = await findOrCreateSpace(
|
|
202
|
+
const project = await findOrCreateSpace(urls.workspace, {
|
|
139
203
|
token: workspaceIdentity.access_token,
|
|
140
204
|
name: '工作',
|
|
141
205
|
kind: 'project',
|
|
142
206
|
fetchImpl
|
|
143
207
|
});
|
|
144
|
-
await providerRequest(
|
|
208
|
+
await providerRequest(urls.personal, '/v1/subscriptions', {
|
|
145
209
|
token: personalIdentity.access_token,
|
|
146
210
|
body: {
|
|
147
211
|
personal_space_id: personalSpace.id,
|
|
148
212
|
project_id: project.id,
|
|
149
|
-
provider_url:
|
|
213
|
+
provider_url: urls.workspace,
|
|
150
214
|
project_name: project.name
|
|
151
215
|
},
|
|
152
216
|
fetchImpl
|
|
153
217
|
});
|
|
154
218
|
config.workspaces.push({
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
219
|
+
providerUrl: urls.workspace,
|
|
220
|
+
accessToken: workspaceIdentity.access_token,
|
|
221
|
+
principalId: workspaceIdentity.principal_id,
|
|
222
|
+
managedDevelopment: true
|
|
158
223
|
});
|
|
159
224
|
return config;
|
|
160
225
|
}
|
|
161
226
|
|
|
227
|
+
export function synchronizeManagedProviderUrls(config, urls, { personalOnly = true } = {}) {
|
|
228
|
+
const next = structuredClone(config);
|
|
229
|
+
let changed = next.personal?.providerUrl !== urls.personal;
|
|
230
|
+
next.personal.providerUrl = urls.personal;
|
|
231
|
+
if (!personalOnly) {
|
|
232
|
+
for (const workspace of next.workspaces ?? []) {
|
|
233
|
+
if (!isManagedWorkspace(workspace)) continue;
|
|
234
|
+
if (workspace.providerUrl !== urls.workspace || workspace.managedDevelopment !== true) {
|
|
235
|
+
changed = true;
|
|
236
|
+
}
|
|
237
|
+
workspace.providerUrl = urls.workspace;
|
|
238
|
+
workspace.managedDevelopment = true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return { config: next, changed };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function reconcileManagedWorkspaceSubscriptions({
|
|
245
|
+
previousConfig,
|
|
246
|
+
nextConfig,
|
|
247
|
+
personalUrl,
|
|
248
|
+
fetchImpl
|
|
249
|
+
}) {
|
|
250
|
+
const previous = (previousConfig.workspaces ?? []).find(isManagedWorkspace);
|
|
251
|
+
const next = (nextConfig.workspaces ?? []).find(isManagedWorkspace);
|
|
252
|
+
if (!previous || !next || previous.providerUrl === next.providerUrl) return;
|
|
253
|
+
const query = new URLSearchParams({ personal_space_id: nextConfig.personal.spaceId });
|
|
254
|
+
const response = await fetchImpl(`${personalUrl}/v1/subscriptions?${query}`, {
|
|
255
|
+
headers: { authorization: `Bearer ${nextConfig.personal.accessToken}` }
|
|
256
|
+
});
|
|
257
|
+
if (!response.ok) throw new Error('Could not reconcile managed workspace subscriptions');
|
|
258
|
+
const subscriptions = await response.json();
|
|
259
|
+
for (const subscription of subscriptions.filter(
|
|
260
|
+
(item) => item.provider_url === previous.providerUrl
|
|
261
|
+
)) {
|
|
262
|
+
const removal = new URLSearchParams({
|
|
263
|
+
personal_space_id: nextConfig.personal.spaceId,
|
|
264
|
+
provider_url: previous.providerUrl
|
|
265
|
+
});
|
|
266
|
+
const removed = await fetchImpl(
|
|
267
|
+
`${personalUrl}/v1/subscriptions/${encodeURIComponent(subscription.project_id)}?${removal}`,
|
|
268
|
+
{
|
|
269
|
+
method: 'DELETE',
|
|
270
|
+
headers: { authorization: `Bearer ${nextConfig.personal.accessToken}` }
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
if (!removed.ok) throw new Error('Could not remove the previous managed workspace port');
|
|
274
|
+
await providerRequest(personalUrl, '/v1/subscriptions', {
|
|
275
|
+
token: nextConfig.personal.accessToken,
|
|
276
|
+
body: {
|
|
277
|
+
personal_space_id: nextConfig.personal.spaceId,
|
|
278
|
+
project_id: subscription.project_id,
|
|
279
|
+
provider_url: next.providerUrl,
|
|
280
|
+
project_name: subscription.project_name
|
|
281
|
+
},
|
|
282
|
+
fetchImpl
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isManagedWorkspace(workspace) {
|
|
288
|
+
if (workspace?.managedDevelopment === true) return true;
|
|
289
|
+
try {
|
|
290
|
+
const url = new URL(workspace?.providerUrl);
|
|
291
|
+
return url.protocol === 'http:' &&
|
|
292
|
+
['127.0.0.1', 'localhost', '::1', '[::1]'].includes(url.hostname) &&
|
|
293
|
+
url.port === '8788';
|
|
294
|
+
} catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
162
299
|
async function bootstrapProvider(url, bootstrapToken, principalName, fetchImpl) {
|
|
163
300
|
const response = await fetchImpl(`${url}/v1/bootstrap`, {
|
|
164
301
|
method: 'POST',
|
|
@@ -217,7 +354,7 @@ async function waitForProvider(url, fetchImpl) {
|
|
|
217
354
|
async function waitForWeb(url, pid, deps) {
|
|
218
355
|
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
219
356
|
if (!deps.isProcessAlive(pid)) return false;
|
|
220
|
-
if (await deps.webHealth(url, pid,
|
|
357
|
+
if (await deps.webHealth(url, pid, 4)) return true;
|
|
221
358
|
await delay(100);
|
|
222
359
|
}
|
|
223
360
|
return false;
|
|
@@ -250,14 +387,19 @@ function spawnWebRuntime(input) {
|
|
|
250
387
|
mkdirSync(dirname(input.paths.logPath), { recursive: true });
|
|
251
388
|
const log = openSync(input.paths.logPath, 'a');
|
|
252
389
|
try {
|
|
253
|
-
const
|
|
390
|
+
const args = [
|
|
254
391
|
input.paths.serverPath,
|
|
255
392
|
'--runtime-config', input.paths.graphRuntimeConfigPath,
|
|
256
393
|
'--port', String(input.port)
|
|
257
|
-
]
|
|
394
|
+
];
|
|
395
|
+
if (input.lan) args.push('--lan');
|
|
396
|
+
const child = spawn(input.nodePath, args, {
|
|
258
397
|
detached: true,
|
|
259
398
|
windowsHide: true,
|
|
260
|
-
stdio: ['ignore', log, log]
|
|
399
|
+
stdio: ['ignore', log, log],
|
|
400
|
+
env: input.lan
|
|
401
|
+
? { ...input.env, FULI_LAN_ACCESS_TOKEN: input.lanAccessToken }
|
|
402
|
+
: input.env
|
|
261
403
|
});
|
|
262
404
|
child.unref();
|
|
263
405
|
return child;
|
|
@@ -266,13 +408,6 @@ function spawnWebRuntime(input) {
|
|
|
266
408
|
}
|
|
267
409
|
}
|
|
268
410
|
|
|
269
|
-
function stopOwnedLegacyRuntime(paths, deps) {
|
|
270
|
-
deps.stopLegacyService();
|
|
271
|
-
const state = deps.readState(paths.statePath);
|
|
272
|
-
if (state?.version !== 1 || state.dbPath !== paths.dbPath || !Number.isInteger(state.pid)) return;
|
|
273
|
-
if (deps.isProcessAlive(state.pid)) deps.stopProcess(state.pid);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
411
|
function graphDependencies(overrides) {
|
|
277
412
|
return {
|
|
278
413
|
fetch: globalThis.fetch,
|
|
@@ -283,6 +418,7 @@ function graphDependencies(overrides) {
|
|
|
283
418
|
ensureDirectory: (path) => mkdirSync(path, { recursive: true }),
|
|
284
419
|
readConfig: (path) => readJsonFile(path, null),
|
|
285
420
|
writeConfig: writeJsonFileAtomic,
|
|
421
|
+
writeRuntimeSettings,
|
|
286
422
|
readState: (path) => readJsonFile(path, null),
|
|
287
423
|
writeState: writeJsonFileAtomic,
|
|
288
424
|
ensureContainerRuntime,
|
|
@@ -290,8 +426,10 @@ function graphDependencies(overrides) {
|
|
|
290
426
|
spawnWebRuntime,
|
|
291
427
|
isProcessAlive,
|
|
292
428
|
stopProcess,
|
|
293
|
-
stopLegacyService,
|
|
294
429
|
webHealth: checkLocalConsoleHealth,
|
|
430
|
+
discoverLanAddresses,
|
|
431
|
+
createLanAccessToken: secret,
|
|
432
|
+
waitForExit: waitForProcessExit,
|
|
295
433
|
...overrides
|
|
296
434
|
};
|
|
297
435
|
}
|
|
@@ -319,17 +457,32 @@ export async function checkLocalConsoleHealth(url, expectedPid, stateVersion = 3
|
|
|
319
457
|
}
|
|
320
458
|
|
|
321
459
|
function isGraphRuntimeState(state) {
|
|
322
|
-
return (state?.version === 2 || state?.version === 3) &&
|
|
460
|
+
return (state?.version === 2 || state?.version === 3 || state?.version === 4) &&
|
|
323
461
|
Number.isInteger(state.pid) && typeof state.url === 'string';
|
|
324
462
|
}
|
|
325
463
|
|
|
326
|
-
function
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
464
|
+
function exposureMatches(state, { lan }) {
|
|
465
|
+
if ((state.lan === true) !== lan) return false;
|
|
466
|
+
if (!lan) return true;
|
|
467
|
+
// A repeated explicit LAN start rotates the in-memory access code.
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function runtimeResult(status, state, lanAccessToken = null) {
|
|
472
|
+
const result = { status, url: state.url, pid: state.pid };
|
|
473
|
+
if (state.lan !== true) return result;
|
|
474
|
+
if (typeof lanAccessToken !== 'string' || lanAccessToken.length < 16) {
|
|
475
|
+
throw new Error('LAN runtime started without a valid access code');
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
...result,
|
|
479
|
+
lan: true,
|
|
480
|
+
lanUrls: [...state.lanUrls],
|
|
481
|
+
lanAccess: {
|
|
482
|
+
username: LAN_ACCESS_USERNAME,
|
|
483
|
+
accessCode: lanAccessToken
|
|
484
|
+
}
|
|
485
|
+
};
|
|
333
486
|
}
|
|
334
487
|
|
|
335
488
|
function parseEnv(body) {
|
|
@@ -355,6 +508,14 @@ function stopProcess(pid) {
|
|
|
355
508
|
try { process.kill(pid, 'SIGTERM'); } catch { /* already stopped */ }
|
|
356
509
|
}
|
|
357
510
|
|
|
511
|
+
async function waitForProcessExit(pid) {
|
|
512
|
+
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
513
|
+
if (!isProcessAlive(pid)) return true;
|
|
514
|
+
await delay(100);
|
|
515
|
+
}
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
|
|
358
519
|
function delay(ms) {
|
|
359
520
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
360
521
|
}
|
|
@@ -4,14 +4,14 @@ export function assertSupportedNodeVersion(version = process.versions.node) {
|
|
|
4
4
|
const parsed = parseNodeVersion(version);
|
|
5
5
|
if (compareVersions(parsed, MINIMUM_NODE_VERSION) >= 0) return;
|
|
6
6
|
throw new Error(
|
|
7
|
-
`fl setup
|
|
8
|
-
|
|
7
|
+
`fl setup requires Node.js 24.12 or later; the current version is ${version}. ` +
|
|
8
|
+
'Upgrade Node.js, then run fl setup again.'
|
|
9
9
|
);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function parseNodeVersion(value) {
|
|
13
13
|
const match = String(value).match(/^v?(\d+)\.(\d+)\.(\d+)/);
|
|
14
|
-
if (!match) throw new Error(
|
|
14
|
+
if (!match) throw new Error(`Could not parse Node.js version: ${value}`);
|
|
15
15
|
return match.slice(1).map(Number);
|
|
16
16
|
}
|
|
17
17
|
|
package/src/setup/options.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { DEFAULT_FULI_PORT } from '../defaults.js';
|
|
2
|
-
|
|
3
1
|
const VALUE_OPTIONS = Object.freeze({
|
|
4
2
|
'--data-dir': 'dataDir',
|
|
5
3
|
'--personal-space': 'personalSpaceName',
|
|
@@ -26,8 +24,8 @@ export function parseUpdateOptions(args = []) {
|
|
|
26
24
|
function parseSetupLikeOptions(args, command) {
|
|
27
25
|
const result = {
|
|
28
26
|
dataDir: null,
|
|
29
|
-
personalSpaceName: '
|
|
30
|
-
port:
|
|
27
|
+
personalSpaceName: 'Personal',
|
|
28
|
+
port: null,
|
|
31
29
|
yes: false,
|
|
32
30
|
codexOnly: false,
|
|
33
31
|
skipAgents: false,
|
package/src/setup/paths.js
CHANGED
|
@@ -16,19 +16,34 @@ export function resolveSetupPaths({
|
|
|
16
16
|
const root = resolve(packageRoot);
|
|
17
17
|
return {
|
|
18
18
|
dataDir: resolvedDataDir,
|
|
19
|
-
dbPath: join(resolvedDataDir, 'context.db'),
|
|
20
19
|
graphEnvPath: join(resolvedDataDir, 'graph-provider.env'),
|
|
21
20
|
graphRuntimeConfigPath: join(resolvedDataDir, 'graph-runtime.json'),
|
|
22
21
|
graphRuntimeStatePath: join(resolvedDataDir, 'graph-runtime-state.json'),
|
|
22
|
+
runtimeSettingsPath: join(resolvedDataDir, 'runtime-settings.json'),
|
|
23
|
+
externalKnowledgeRegistryPath: join(
|
|
24
|
+
resolvedDataDir,
|
|
25
|
+
'external-knowledge',
|
|
26
|
+
'bindings.json'
|
|
27
|
+
),
|
|
28
|
+
externalKnowledgeConflictPolicyPath: join(
|
|
29
|
+
resolvedDataDir,
|
|
30
|
+
'external-knowledge',
|
|
31
|
+
'conflict-policies.json'
|
|
32
|
+
),
|
|
33
|
+
externalKnowledgeConnectorDir: join(
|
|
34
|
+
resolvedDataDir,
|
|
35
|
+
'external-knowledge',
|
|
36
|
+
'connectors'
|
|
37
|
+
),
|
|
23
38
|
backupDir: join(resolvedDataDir, 'backups', 'agents'),
|
|
24
39
|
logPath: join(resolvedDataDir, 'logs', 'runtime.log'),
|
|
25
|
-
statePath: join(resolvedDataDir, 'runtime.json'),
|
|
26
40
|
serverPath: join(root, 'src', 'server.js'),
|
|
27
41
|
mcpServerPath: join(root, 'src', 'mcp-server.js'),
|
|
28
42
|
graphSetupPath: join(root, 'src', 'graphiti', 'setup.js'),
|
|
29
43
|
graphComposePath: join(root, 'compose.graphiti.yml'),
|
|
30
44
|
sessionSkillPath: join(root, 'skills', 'capturing-session-knowledge'),
|
|
31
|
-
projectSkillPath: join(root, 'skills', 'grilling-project')
|
|
45
|
+
projectSkillPath: join(root, 'skills', 'grilling-project'),
|
|
46
|
+
reviewSkillPath: join(root, 'skills', 'flreview')
|
|
32
47
|
};
|
|
33
48
|
}
|
|
34
49
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const REVIEW_SKILL_NAME = 'flreview';
|
|
2
|
+
|
|
3
|
+
// Keep every host-specific difference for the review Skill here. The shared
|
|
4
|
+
// SKILL.md intentionally contains no Codex/Claude/Cursor branching.
|
|
5
|
+
const ADAPTERS = Object.freeze({
|
|
6
|
+
codex: Object.freeze({
|
|
7
|
+
root: ['.agents', 'skills'],
|
|
8
|
+
nativeInvocation: '$flreview',
|
|
9
|
+
needsCommandBridge: true
|
|
10
|
+
}),
|
|
11
|
+
'claude-code': Object.freeze({
|
|
12
|
+
root: ['.claude', 'skills'],
|
|
13
|
+
nativeInvocation: '/flreview',
|
|
14
|
+
needsCommandBridge: false
|
|
15
|
+
}),
|
|
16
|
+
cursor: Object.freeze({
|
|
17
|
+
root: ['.cursor', 'skills'],
|
|
18
|
+
nativeInvocation: '/flreview',
|
|
19
|
+
needsCommandBridge: false
|
|
20
|
+
})
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export function adaptAgentForReviewSkill(agent, { homeDir, pathApi }) {
|
|
24
|
+
const adapter = adapterFor(agent.id);
|
|
25
|
+
return {
|
|
26
|
+
...agent,
|
|
27
|
+
reviewSkillPath: pathApi.join(homeDir, ...adapter.root, REVIEW_SKILL_NAME),
|
|
28
|
+
reviewSkillTrigger: reviewSkillTrigger(agent.id)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function reviewSkillTrigger(agentId) {
|
|
33
|
+
const adapter = adapterFor(agentId);
|
|
34
|
+
return {
|
|
35
|
+
userCommand: '/flreview',
|
|
36
|
+
nativeInvocation: adapter.nativeInvocation,
|
|
37
|
+
needsCommandBridge: adapter.needsCommandBridge
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function codexReviewCommandBridge() {
|
|
42
|
+
return 'Exact `/flreview`: load installed `flreview` Skill.';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function adapterFor(agentId) {
|
|
46
|
+
const adapter = ADAPTERS[agentId];
|
|
47
|
+
if (!adapter) throw new TypeError(`Unsupported review Skill agent: ${agentId}`);
|
|
48
|
+
return adapter;
|
|
49
|
+
}
|