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
package/src/setup/setup.js
CHANGED
|
@@ -4,27 +4,41 @@ import { connectAgent, discoverAgents } from './agents.js';
|
|
|
4
4
|
import { resolveSetupPaths } from './paths.js';
|
|
5
5
|
import { ensureGraphRuntime } from './graph-runtime.js';
|
|
6
6
|
import { installSessionSkill } from './session-skill.js';
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_RUNTIME_SETTINGS,
|
|
9
|
+
readRuntimeSettings,
|
|
10
|
+
runtimeSettingsWithOverrides
|
|
11
|
+
} from '../system/runtime-settings.js';
|
|
7
12
|
|
|
8
13
|
export function planLocalSetup(options, {
|
|
9
14
|
env = process.env,
|
|
10
15
|
resolvePaths = resolveSetupPaths,
|
|
11
16
|
discover = discoverAgents,
|
|
12
17
|
inspectInstallations = inspectAgentInstallations,
|
|
18
|
+
readSettings = readRuntimeSettings,
|
|
13
19
|
nodePath = process.execPath
|
|
14
20
|
} = {}) {
|
|
15
21
|
const paths = resolvePaths({ dataDir: options.dataDir, env });
|
|
22
|
+
const savedRuntimeSettings = paths.runtimeSettingsPath
|
|
23
|
+
? readSettings(paths.runtimeSettingsPath)
|
|
24
|
+
: DEFAULT_RUNTIME_SETTINGS;
|
|
25
|
+
const runtimeSettings = runtimeSettingsWithOverrides(savedRuntimeSettings, {
|
|
26
|
+
consolePort: options.port
|
|
27
|
+
});
|
|
16
28
|
const discovered = options.skipAgents ? [] : discover({ env });
|
|
17
29
|
const agents = options.codexOnly
|
|
18
30
|
? discovered.filter(({ id }) => id === 'codex')
|
|
19
31
|
: discovered;
|
|
20
32
|
return {
|
|
21
33
|
paths,
|
|
34
|
+
runtimeSettings,
|
|
22
35
|
agents: inspectInstallations(agents, {
|
|
23
36
|
nodePath,
|
|
24
37
|
mcpServerPath: paths.mcpServerPath,
|
|
25
38
|
runtimeConfigPath: paths.graphRuntimeConfigPath,
|
|
26
39
|
sessionSkillPath: paths.sessionSkillPath,
|
|
27
|
-
projectSkillPath: paths.projectSkillPath
|
|
40
|
+
projectSkillPath: paths.projectSkillPath,
|
|
41
|
+
reviewSkillPath: paths.reviewSkillPath
|
|
28
42
|
})
|
|
29
43
|
};
|
|
30
44
|
}
|
|
@@ -34,10 +48,16 @@ export async function applyLocalSetup(plan, options, dependencies = {}) {
|
|
|
34
48
|
const backupConfig = dependencies.backupConfig ?? backupAgentConfig;
|
|
35
49
|
const connect = dependencies.connect ?? connectAgent;
|
|
36
50
|
const installSkill = dependencies.installSkill ?? installSessionSkill;
|
|
51
|
+
const runtimeSettings = plan.runtimeSettings ?? runtimeSettingsWithOverrides(
|
|
52
|
+
DEFAULT_RUNTIME_SETTINGS,
|
|
53
|
+
{ consolePort: options.port }
|
|
54
|
+
);
|
|
37
55
|
const runtime = await ensureRuntime({
|
|
38
56
|
paths: plan.paths,
|
|
39
57
|
personalSpaceName: options.personalSpaceName,
|
|
40
|
-
port:
|
|
58
|
+
port: runtimeSettings.ports.console,
|
|
59
|
+
lan: runtimeSettings.lanAccess,
|
|
60
|
+
runtimeSettings,
|
|
41
61
|
personalOnly: options.personalOnly,
|
|
42
62
|
noStart: options.noStart,
|
|
43
63
|
env: dependencies.env ?? process.env,
|
|
@@ -64,6 +84,10 @@ export async function applyLocalSetup(plan, options, dependencies = {}) {
|
|
|
64
84
|
installSkill({ ...agent, skillPath: agent.projectSkillPath }, {
|
|
65
85
|
sourcePath: plan.paths.projectSkillPath,
|
|
66
86
|
backupDir: plan.paths.backupDir
|
|
87
|
+
}),
|
|
88
|
+
installSkill({ ...agent, skillPath: agent.reviewSkillPath }, {
|
|
89
|
+
sourcePath: plan.paths.reviewSkillPath,
|
|
90
|
+
backupDir: plan.paths.backupDir
|
|
67
91
|
})
|
|
68
92
|
];
|
|
69
93
|
agents.push({ ...connected, backupPath, skills });
|
package/src/setup/uninstall.js
CHANGED
|
@@ -20,7 +20,8 @@ export function planLocalUninstall(options, {
|
|
|
20
20
|
(agent.globalInstructionsOverridePath &&
|
|
21
21
|
fileExists(agent.globalInstructionsOverridePath)) ||
|
|
22
22
|
(agent.skillPath && fileExists(agent.skillPath)) ||
|
|
23
|
-
(agent.projectSkillPath && fileExists(agent.projectSkillPath))
|
|
23
|
+
(agent.projectSkillPath && fileExists(agent.projectSkillPath)) ||
|
|
24
|
+
(agent.reviewSkillPath && fileExists(agent.reviewSkillPath))
|
|
24
25
|
));
|
|
25
26
|
return { paths, agents };
|
|
26
27
|
}
|
|
@@ -60,7 +61,8 @@ export async function applyLocalUninstall(plan, dependencies = {}) {
|
|
|
60
61
|
|
|
61
62
|
for (const [skillPath, sourcePath] of [
|
|
62
63
|
[agent.skillPath, plan.paths.sessionSkillPath],
|
|
63
|
-
[agent.projectSkillPath, plan.paths.projectSkillPath]
|
|
64
|
+
[agent.projectSkillPath, plan.paths.projectSkillPath],
|
|
65
|
+
[agent.reviewSkillPath, plan.paths.reviewSkillPath]
|
|
64
66
|
]) {
|
|
65
67
|
try {
|
|
66
68
|
skills.push(removeSkill({ ...agent, skillPath }, { sourcePath }));
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readdir, lstat, statfs } from 'node:fs/promises';
|
|
3
|
+
import { freemem, totalmem } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { inspectContainerRuntime } from '../setup/container-runtime.js';
|
|
7
|
+
|
|
8
|
+
const COMPOSE_PROJECT = 'fuli-graphiti';
|
|
9
|
+
const DISK_CACHE_MS = 60_000;
|
|
10
|
+
const PROCESS_TIMEOUT_MS = 8_000;
|
|
11
|
+
const CONTAINER_LABELS = Object.freeze({
|
|
12
|
+
'personal-provider': 'Personal Provider',
|
|
13
|
+
'personal-neo4j': 'Personal Neo4j',
|
|
14
|
+
'workspace-provider': 'Workspace Provider',
|
|
15
|
+
'workspace-neo4j': 'Workspace Neo4j'
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export function createResourceMonitor({
|
|
19
|
+
dataDir,
|
|
20
|
+
packageRoot,
|
|
21
|
+
now = () => new Date(),
|
|
22
|
+
processMemory = () => process.memoryUsage(),
|
|
23
|
+
hostMemory = () => ({ totalBytes: totalmem(), freeBytes: freemem() }),
|
|
24
|
+
containerRuntime = null,
|
|
25
|
+
inspectRuntime = inspectContainerRuntime,
|
|
26
|
+
run = runCommand,
|
|
27
|
+
directorySize = allocatedDirectorySize,
|
|
28
|
+
filesystemStats = hostFilesystemStats
|
|
29
|
+
}) {
|
|
30
|
+
let resolvedRuntime = containerRuntime;
|
|
31
|
+
let diskCache = null;
|
|
32
|
+
|
|
33
|
+
async function sample() {
|
|
34
|
+
const sampledAt = now().toISOString();
|
|
35
|
+
const runtime = resolvedRuntime ??= inspectRuntime();
|
|
36
|
+
const containers = await containerMemory(runtime, run);
|
|
37
|
+
const localMemory = processMemory();
|
|
38
|
+
const host = hostMemory();
|
|
39
|
+
const disk = await diskSample({
|
|
40
|
+
runtime,
|
|
41
|
+
run,
|
|
42
|
+
dataDir,
|
|
43
|
+
packageRoot,
|
|
44
|
+
directorySize,
|
|
45
|
+
filesystemStats,
|
|
46
|
+
now,
|
|
47
|
+
cache: diskCache
|
|
48
|
+
});
|
|
49
|
+
diskCache = disk.cache;
|
|
50
|
+
|
|
51
|
+
const memoryComponents = [{
|
|
52
|
+
id: 'console',
|
|
53
|
+
label: 'Management service',
|
|
54
|
+
kind: 'process',
|
|
55
|
+
status: 'ready',
|
|
56
|
+
bytes: localMemory.rss
|
|
57
|
+
}, ...containers.components];
|
|
58
|
+
return {
|
|
59
|
+
sampledAt,
|
|
60
|
+
status: containers.complete && disk.value.complete ? 'ready' : 'partial',
|
|
61
|
+
memory: {
|
|
62
|
+
usedBytes: sumBytes(memoryComponents),
|
|
63
|
+
hostTotalBytes: host.totalBytes,
|
|
64
|
+
hostFreeBytes: host.freeBytes,
|
|
65
|
+
complete: containers.complete,
|
|
66
|
+
components: memoryComponents
|
|
67
|
+
},
|
|
68
|
+
disk: disk.value,
|
|
69
|
+
exclusions: ['browser-tab-memory', 'shared-container-vm-overhead']
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { sample };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function containerMemory(runtime, run) {
|
|
77
|
+
if (runtime?.status !== 'ready') return { complete: false, components: [] };
|
|
78
|
+
try {
|
|
79
|
+
const containers = await listManagedContainers(runtime, run);
|
|
80
|
+
if (containers.length === 0) return { complete: false, components: [] };
|
|
81
|
+
const output = await runDocker(runtime, [
|
|
82
|
+
'stats', '--no-stream', '--format', '{{.ID}}\t{{.MemUsage}}',
|
|
83
|
+
...containers.map(({ id }) => id)
|
|
84
|
+
], run);
|
|
85
|
+
const usage = new Map(parseLines(output).map((line) => {
|
|
86
|
+
const [id, value] = line.split('\t');
|
|
87
|
+
return [id, parseDockerBytes(value.split('/', 1)[0].trim())];
|
|
88
|
+
}));
|
|
89
|
+
return {
|
|
90
|
+
complete: containers.every(({ id }) => usage.has(id)),
|
|
91
|
+
components: containers.flatMap(({ id, service }) => usage.has(id) ? [{
|
|
92
|
+
id: service,
|
|
93
|
+
label: CONTAINER_LABELS[service] ?? service,
|
|
94
|
+
kind: 'container',
|
|
95
|
+
status: 'ready',
|
|
96
|
+
bytes: usage.get(id)
|
|
97
|
+
}] : [])
|
|
98
|
+
};
|
|
99
|
+
} catch {
|
|
100
|
+
return { complete: false, components: [] };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function diskSample(input) {
|
|
105
|
+
const timestamp = input.now().getTime();
|
|
106
|
+
if (input.cache && timestamp - input.cache.timestamp < DISK_CACHE_MS) {
|
|
107
|
+
return { value: input.cache.value, cache: input.cache };
|
|
108
|
+
}
|
|
109
|
+
const [applicationBytes, localDataBytes, filesystem, docker] = await Promise.all([
|
|
110
|
+
input.directorySize(input.packageRoot),
|
|
111
|
+
input.directorySize(input.dataDir),
|
|
112
|
+
input.filesystemStats(input.dataDir),
|
|
113
|
+
dockerDisk(input.runtime, input.run)
|
|
114
|
+
]);
|
|
115
|
+
const components = [
|
|
116
|
+
diskComponent('application', 'Application files', applicationBytes),
|
|
117
|
+
diskComponent('localData', 'Local runtime data', localDataBytes),
|
|
118
|
+
...docker.components
|
|
119
|
+
].filter(Boolean);
|
|
120
|
+
const value = {
|
|
121
|
+
usedBytes: sumBytes(components),
|
|
122
|
+
hostTotalBytes: filesystem.totalBytes,
|
|
123
|
+
hostFreeBytes: filesystem.freeBytes,
|
|
124
|
+
complete: docker.complete && Number.isFinite(filesystem.totalBytes) &&
|
|
125
|
+
Number.isFinite(filesystem.freeBytes),
|
|
126
|
+
measuredAt: input.now().toISOString(),
|
|
127
|
+
temporaryBytes: docker.temporaryBytes,
|
|
128
|
+
components
|
|
129
|
+
};
|
|
130
|
+
return { value, cache: { timestamp, value } };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function dockerDisk(runtime, run) {
|
|
134
|
+
if (runtime?.status !== 'ready') {
|
|
135
|
+
return { complete: false, components: [], temporaryBytes: null };
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const containers = await listManagedContainers(runtime, run);
|
|
139
|
+
if (containers.length === 0) {
|
|
140
|
+
return { complete: false, components: [], temporaryBytes: null };
|
|
141
|
+
}
|
|
142
|
+
const inspectOutput = await runDocker(runtime, [
|
|
143
|
+
'inspect', '--size', '--format', '{{.Id}}\t{{.SizeRw}}\t{{.Image}}',
|
|
144
|
+
...containers.map(({ id }) => id)
|
|
145
|
+
], run);
|
|
146
|
+
const inspected = parseLines(inspectOutput).map((line) => {
|
|
147
|
+
const [id, writable, imageId] = line.split('\t');
|
|
148
|
+
return { id, writableBytes: nonNegativeNumber(writable), imageId };
|
|
149
|
+
});
|
|
150
|
+
if (
|
|
151
|
+
inspected.length !== containers.length ||
|
|
152
|
+
inspected.some(({ id, imageId }) => !id || !imageId)
|
|
153
|
+
) {
|
|
154
|
+
throw new TypeError('Docker container disk metrics are incomplete');
|
|
155
|
+
}
|
|
156
|
+
const imageIds = [...new Set(inspected.map(({ imageId }) => imageId).filter(Boolean))];
|
|
157
|
+
const imageOutput = imageIds.length === 0 ? '' : await runDocker(runtime, [
|
|
158
|
+
'image', 'inspect', '--format', '{{.Id}}\t{{.Size}}', ...imageIds
|
|
159
|
+
], run);
|
|
160
|
+
const imageSizes = new Map(parseLines(imageOutput).map((line) => {
|
|
161
|
+
const [id, value] = line.split('\t');
|
|
162
|
+
return [id, nonNegativeNumber(value)];
|
|
163
|
+
}));
|
|
164
|
+
if (imageIds.some((id) => !imageSizes.has(id))) {
|
|
165
|
+
throw new TypeError('Docker image disk metrics are incomplete');
|
|
166
|
+
}
|
|
167
|
+
const imageBytes = imageIds.reduce((total, id) => total + imageSizes.get(id), 0);
|
|
168
|
+
const writableBytes = inspected.reduce(
|
|
169
|
+
(total, item) => total + item.writableBytes,
|
|
170
|
+
0
|
|
171
|
+
);
|
|
172
|
+
const volumeUsage = await managedVolumeUsage(runtime, containers, run);
|
|
173
|
+
return {
|
|
174
|
+
complete: true,
|
|
175
|
+
temporaryBytes: volumeUsage.temporaryBytes,
|
|
176
|
+
components: [
|
|
177
|
+
diskComponent('containerImages', 'Container images', imageBytes),
|
|
178
|
+
diskComponent('containerWritable', 'Container writable data', writableBytes),
|
|
179
|
+
diskComponent('neo4jData', 'Neo4j data', volumeUsage.dataBytes),
|
|
180
|
+
diskComponent('neo4jLogs', 'Neo4j logs', volumeUsage.logBytes)
|
|
181
|
+
].filter(Boolean)
|
|
182
|
+
};
|
|
183
|
+
} catch {
|
|
184
|
+
return { complete: false, components: [], temporaryBytes: null };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function managedVolumeUsage(runtime, containers, run) {
|
|
189
|
+
let dataBytes = 0;
|
|
190
|
+
let logBytes = 0;
|
|
191
|
+
let temporaryBytes = 0;
|
|
192
|
+
const neo4jContainers = containers.filter(({ service }) => service.endsWith('-neo4j'));
|
|
193
|
+
if (neo4jContainers.length === 0) {
|
|
194
|
+
throw new TypeError('No managed Neo4j container was available');
|
|
195
|
+
}
|
|
196
|
+
for (const container of neo4jContainers) {
|
|
197
|
+
const output = await runDocker(runtime, [
|
|
198
|
+
'exec', container.id, 'du', '-sk', '/data', '/logs', '/tmp'
|
|
199
|
+
], run);
|
|
200
|
+
const usage = new Map();
|
|
201
|
+
for (const line of parseLines(output)) {
|
|
202
|
+
const [kib, path] = line.trim().split(/\s+/, 2);
|
|
203
|
+
if (['/data', '/logs', '/tmp'].includes(path)) {
|
|
204
|
+
usage.set(path, nonNegativeNumber(kib) * 1024);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (['/data', '/logs', '/tmp'].some((path) => !usage.has(path))) {
|
|
208
|
+
throw new TypeError('Neo4j volume disk metrics are incomplete');
|
|
209
|
+
}
|
|
210
|
+
dataBytes += usage.get('/data');
|
|
211
|
+
logBytes += usage.get('/logs');
|
|
212
|
+
temporaryBytes += usage.get('/tmp');
|
|
213
|
+
}
|
|
214
|
+
return { dataBytes, logBytes, temporaryBytes };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function listManagedContainers(runtime, run) {
|
|
218
|
+
const output = await runDocker(runtime, [
|
|
219
|
+
'ps', '--filter', `label=com.docker.compose.project=${COMPOSE_PROJECT}`,
|
|
220
|
+
'--format', '{{.ID}}\t{{.Label "com.docker.compose.service"}}'
|
|
221
|
+
], run);
|
|
222
|
+
return parseLines(output).flatMap((line) => {
|
|
223
|
+
const [id, service] = line.split('\t');
|
|
224
|
+
return id && service && Object.hasOwn(CONTAINER_LABELS, service)
|
|
225
|
+
? [{ id, service }]
|
|
226
|
+
: [];
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function runDocker(runtime, args, run) {
|
|
231
|
+
return run(runtime.dockerCommand, args, { env: runtime.dockerEnvironment });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function allocatedDirectorySize(root) {
|
|
235
|
+
if (typeof root !== 'string' || !root) return 0;
|
|
236
|
+
let total = 0;
|
|
237
|
+
let entries;
|
|
238
|
+
try {
|
|
239
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
240
|
+
} catch {
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
const path = join(root, entry.name);
|
|
245
|
+
if (entry.isSymbolicLink()) continue;
|
|
246
|
+
if (entry.isDirectory()) {
|
|
247
|
+
total += await allocatedDirectorySize(path);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const stats = await lstat(path);
|
|
252
|
+
total += Number.isFinite(stats.blocks) ? stats.blocks * 512 : stats.size;
|
|
253
|
+
} catch {
|
|
254
|
+
// Files may be replaced while the snapshot is being collected.
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return total;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function hostFilesystemStats(path) {
|
|
261
|
+
try {
|
|
262
|
+
const stats = await statfs(path, { bigint: true });
|
|
263
|
+
return {
|
|
264
|
+
totalBytes: Number(stats.blocks * stats.bsize),
|
|
265
|
+
freeBytes: Number(stats.bavail * stats.bsize)
|
|
266
|
+
};
|
|
267
|
+
} catch {
|
|
268
|
+
return { totalBytes: null, freeBytes: null };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function runCommand(command, args, { env } = {}) {
|
|
273
|
+
return new Promise((resolve, reject) => {
|
|
274
|
+
execFile(command, args, {
|
|
275
|
+
encoding: 'utf8',
|
|
276
|
+
env,
|
|
277
|
+
timeout: PROCESS_TIMEOUT_MS,
|
|
278
|
+
windowsHide: true,
|
|
279
|
+
maxBuffer: 2 * 1024 * 1024
|
|
280
|
+
}, (error, stdout) => {
|
|
281
|
+
if (error) reject(error);
|
|
282
|
+
else resolve(stdout);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function parseDockerBytes(value) {
|
|
288
|
+
const match = /^([0-9]+(?:\.[0-9]+)?)\s*([kmgtp]?i?b)$/i.exec(String(value).trim());
|
|
289
|
+
if (!match) throw new TypeError(`Unsupported Docker byte value: ${value}`);
|
|
290
|
+
const unit = match[2].toLowerCase();
|
|
291
|
+
const powers = { b: 0, kb: 1, kib: 1, mb: 2, mib: 2, gb: 3, gib: 3, tb: 4, tib: 4,
|
|
292
|
+
pb: 5, pib: 5 };
|
|
293
|
+
const base = unit.includes('i') ? 1024 : 1000;
|
|
294
|
+
return Math.round(Number(match[1]) * (base ** powers[unit]));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function diskComponent(id, label, bytes) {
|
|
298
|
+
return Number.isFinite(bytes) && bytes >= 0
|
|
299
|
+
? { id, label, status: 'ready', bytes }
|
|
300
|
+
: null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function sumBytes(components) {
|
|
304
|
+
return components.reduce((total, component) => total + component.bytes, 0);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function nonNegativeNumber(value) {
|
|
308
|
+
const number = Number(value);
|
|
309
|
+
if (!Number.isFinite(number) || number < 0) {
|
|
310
|
+
throw new TypeError(`Expected a non-negative number, received: ${value}`);
|
|
311
|
+
}
|
|
312
|
+
return number;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function parseLines(value) {
|
|
316
|
+
return String(value).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
317
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { chmodSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import { isFetchBlockedPort } from '../server/blocked-ports.js';
|
|
4
|
+
import { readJsonFile, writeJsonFileAtomic } from '../storage/json-file.js';
|
|
5
|
+
|
|
6
|
+
export const RUNTIME_SETTINGS_VERSION = 1;
|
|
7
|
+
export const RESOURCE_REFRESH_OPTIONS = Object.freeze([5, 10, 30, 60]);
|
|
8
|
+
export const PORT_KEYS = Object.freeze([
|
|
9
|
+
'console',
|
|
10
|
+
'personalProvider',
|
|
11
|
+
'personalNeo4jHttp',
|
|
12
|
+
'personalNeo4jBolt',
|
|
13
|
+
'workspaceProvider',
|
|
14
|
+
'workspaceNeo4jHttp',
|
|
15
|
+
'workspaceNeo4jBolt'
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_RUNTIME_SETTINGS = deepFreeze({
|
|
19
|
+
version: RUNTIME_SETTINGS_VERSION,
|
|
20
|
+
ports: {
|
|
21
|
+
console: 2727,
|
|
22
|
+
personalProvider: 8787,
|
|
23
|
+
personalNeo4jHttp: 8060,
|
|
24
|
+
personalNeo4jBolt: 7687,
|
|
25
|
+
workspaceProvider: 8788,
|
|
26
|
+
workspaceNeo4jHttp: 7475,
|
|
27
|
+
workspaceNeo4jBolt: 7688
|
|
28
|
+
},
|
|
29
|
+
lanAccess: false,
|
|
30
|
+
resourceRefreshSeconds: 5
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export function readRuntimeSettings(path, {
|
|
34
|
+
fallback = DEFAULT_RUNTIME_SETTINGS,
|
|
35
|
+
read = readJsonFile
|
|
36
|
+
} = {}) {
|
|
37
|
+
const stored = read(path, null);
|
|
38
|
+
return normalizeRuntimeSettings(stored ?? fallback, { base: fallback });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function writeRuntimeSettings(path, input, {
|
|
42
|
+
write = writeJsonFileAtomic,
|
|
43
|
+
secure = (filePath) => chmodSync(filePath, 0o600)
|
|
44
|
+
} = {}) {
|
|
45
|
+
const settings = normalizeRuntimeSettings(input, { strict: true });
|
|
46
|
+
write(path, settings);
|
|
47
|
+
secure(path);
|
|
48
|
+
return settings;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function normalizeRuntimeSettings(input, {
|
|
52
|
+
base = DEFAULT_RUNTIME_SETTINGS,
|
|
53
|
+
strict = false
|
|
54
|
+
} = {}) {
|
|
55
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
56
|
+
throw new TypeError('Runtime settings must be an object');
|
|
57
|
+
}
|
|
58
|
+
if (input.version !== undefined && input.version !== RUNTIME_SETTINGS_VERSION) {
|
|
59
|
+
throw new TypeError('Unsupported runtime settings version');
|
|
60
|
+
}
|
|
61
|
+
const portsInput = input.ports;
|
|
62
|
+
if (strict && (!portsInput || typeof portsInput !== 'object' || Array.isArray(portsInput))) {
|
|
63
|
+
throw new TypeError('ports is required');
|
|
64
|
+
}
|
|
65
|
+
const ports = {};
|
|
66
|
+
for (const key of PORT_KEYS) {
|
|
67
|
+
const value = portsInput?.[key] ?? base.ports[key];
|
|
68
|
+
ports[key] = validPort(value, key);
|
|
69
|
+
}
|
|
70
|
+
const uniquePorts = new Set(Object.values(ports));
|
|
71
|
+
if (uniquePorts.size !== PORT_KEYS.length) {
|
|
72
|
+
throw new TypeError('Every FULI service must use a different port');
|
|
73
|
+
}
|
|
74
|
+
if (isFetchBlockedPort(ports.console)) {
|
|
75
|
+
throw new TypeError('The management UI port is blocked by web browsers');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const lanAccess = input.lanAccess ?? base.lanAccess;
|
|
79
|
+
if (typeof lanAccess !== 'boolean') throw new TypeError('lanAccess must be a boolean');
|
|
80
|
+
const resourceRefreshSeconds = input.resourceRefreshSeconds ?? base.resourceRefreshSeconds;
|
|
81
|
+
if (!RESOURCE_REFRESH_OPTIONS.includes(resourceRefreshSeconds)) {
|
|
82
|
+
throw new TypeError('resourceRefreshSeconds must be 5, 10, 30, or 60');
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
version: RUNTIME_SETTINGS_VERSION,
|
|
86
|
+
ports,
|
|
87
|
+
lanAccess,
|
|
88
|
+
resourceRefreshSeconds
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function runtimeSettingsWithOverrides(settings, {
|
|
93
|
+
consolePort = null,
|
|
94
|
+
lanAccess = null
|
|
95
|
+
} = {}) {
|
|
96
|
+
return normalizeRuntimeSettings({
|
|
97
|
+
...settings,
|
|
98
|
+
ports: {
|
|
99
|
+
...settings.ports,
|
|
100
|
+
...(consolePort === null ? {} : { console: consolePort })
|
|
101
|
+
},
|
|
102
|
+
...(lanAccess === null ? {} : { lanAccess })
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function managedProviderUrls(settings) {
|
|
107
|
+
return {
|
|
108
|
+
personal: `http://127.0.0.1:${settings.ports.personalProvider}`,
|
|
109
|
+
workspace: `http://127.0.0.1:${settings.ports.workspaceProvider}`
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function runtimeSettingsEqual(left, right) {
|
|
114
|
+
return JSON.stringify(normalizeRuntimeSettings(left)) ===
|
|
115
|
+
JSON.stringify(normalizeRuntimeSettings(right));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function validPort(value, key) {
|
|
119
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > 65535) {
|
|
120
|
+
throw new TypeError(`${key} must be an integer between 1 and 65535`);
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function deepFreeze(value) {
|
|
126
|
+
Object.freeze(value);
|
|
127
|
+
for (const child of Object.values(value)) {
|
|
128
|
+
if (child && typeof child === 'object' && !Object.isFrozen(child)) deepFreeze(child);
|
|
129
|
+
}
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readJsonFile } from '../storage/json-file.js';
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_RUNTIME_SETTINGS,
|
|
4
|
+
normalizeRuntimeSettings,
|
|
5
|
+
readRuntimeSettings,
|
|
6
|
+
runtimeSettingsEqual,
|
|
7
|
+
writeRuntimeSettings
|
|
8
|
+
} from './runtime-settings.js';
|
|
9
|
+
import { createResourceMonitor } from './resource-monitor.js';
|
|
10
|
+
|
|
11
|
+
export function createSystemService({
|
|
12
|
+
paths,
|
|
13
|
+
packageRoot,
|
|
14
|
+
activePort,
|
|
15
|
+
activeLan,
|
|
16
|
+
readJson = readJsonFile,
|
|
17
|
+
readSettings = readRuntimeSettings,
|
|
18
|
+
writeSettings = writeRuntimeSettings,
|
|
19
|
+
resourceMonitor = createResourceMonitor({
|
|
20
|
+
dataDir: paths.dataDir,
|
|
21
|
+
packageRoot
|
|
22
|
+
})
|
|
23
|
+
}) {
|
|
24
|
+
function activeSettings() {
|
|
25
|
+
const state = readJson(paths.graphRuntimeStatePath, null);
|
|
26
|
+
const graphConfig = readJson(paths.graphRuntimeConfigPath, null);
|
|
27
|
+
return settingsFromActiveRuntime({
|
|
28
|
+
state,
|
|
29
|
+
graphConfig,
|
|
30
|
+
activePort,
|
|
31
|
+
activeLan
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getSettings() {
|
|
36
|
+
const active = activeSettings();
|
|
37
|
+
const configured = readSettings(paths.runtimeSettingsPath, { fallback: active });
|
|
38
|
+
return settingsResult(configured, active);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function updateSettings(input) {
|
|
42
|
+
const configured = writeSettings(paths.runtimeSettingsPath, input);
|
|
43
|
+
return settingsResult(configured, activeSettings());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
getSettings,
|
|
48
|
+
updateSettings,
|
|
49
|
+
resources: () => resourceMonitor.sample()
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function settingsFromActiveRuntime({
|
|
54
|
+
state,
|
|
55
|
+
graphConfig,
|
|
56
|
+
activePort,
|
|
57
|
+
activeLan
|
|
58
|
+
}) {
|
|
59
|
+
let settings = DEFAULT_RUNTIME_SETTINGS;
|
|
60
|
+
if (state?.runtimeSettings) {
|
|
61
|
+
try {
|
|
62
|
+
settings = normalizeRuntimeSettings(state.runtimeSettings);
|
|
63
|
+
} catch {
|
|
64
|
+
settings = DEFAULT_RUNTIME_SETTINGS;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const ports = { ...settings.ports };
|
|
68
|
+
const consolePort = integerPort(state?.port) ?? integerPort(activePort);
|
|
69
|
+
if (consolePort !== null) ports.console = consolePort;
|
|
70
|
+
const personalProvider = urlPort(graphConfig?.personal?.providerUrl);
|
|
71
|
+
if (personalProvider !== null) ports.personalProvider = personalProvider;
|
|
72
|
+
const managedWorkspace = (graphConfig?.workspaces ?? []).find((workspace) =>
|
|
73
|
+
workspace?.managedDevelopment === true || isLoopbackProvider(workspace?.providerUrl)
|
|
74
|
+
);
|
|
75
|
+
const workspaceProvider = urlPort(managedWorkspace?.providerUrl);
|
|
76
|
+
if (workspaceProvider !== null) ports.workspaceProvider = workspaceProvider;
|
|
77
|
+
return normalizeRuntimeSettings({
|
|
78
|
+
...settings,
|
|
79
|
+
ports,
|
|
80
|
+
lanAccess: state?.lan === true || (state == null && activeLan === true)
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function settingsResult(configured, active) {
|
|
85
|
+
return {
|
|
86
|
+
configured,
|
|
87
|
+
active,
|
|
88
|
+
restartRequired: !runtimeSettingsEqual(
|
|
89
|
+
{ ...configured, resourceRefreshSeconds: active.resourceRefreshSeconds },
|
|
90
|
+
active
|
|
91
|
+
)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function integerPort(value) {
|
|
96
|
+
const port = Number(value);
|
|
97
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function urlPort(value) {
|
|
101
|
+
if (typeof value !== 'string') return null;
|
|
102
|
+
try {
|
|
103
|
+
const url = new URL(value);
|
|
104
|
+
return integerPort(url.port || (url.protocol === 'https:' ? 443 : 80));
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isLoopbackProvider(value) {
|
|
111
|
+
if (typeof value !== 'string') return false;
|
|
112
|
+
try {
|
|
113
|
+
const url = new URL(value);
|
|
114
|
+
return url.protocol === 'http:' &&
|
|
115
|
+
['127.0.0.1', 'localhost', '::1', '[::1]'].includes(url.hostname);
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{C as e,D as t,E as n,F as r,J as i,K as a,N as o,U as s,a as c,b as l,d as u,n as d,t as f,v as p,w as m,x as h,y as g}from"./console-B_Ena2DS.js";import{i as _,r as v,t as y}from"./identity-CjBwYSMX.js";var b={class:`view connections-view`},x={class:`connection-intro`},S=[`aria-label`],C=[`data-status`],w={class:`service-state`},T=[`data-status`],E={class:`service-state`},D={key:0,class:`connection-subscriptions`},O={class:`section-title`},k={class:`subscription-list`},A=[`onClick`],j={key:0,class:`empty-state`},M={type:`submit`},N=t({__name:`ConnectionsPage`,setup(t){let N=f(),P=s(``),F=l(()=>N.state?.providers?.personal?.status===`ready`),I=l(()=>N.state?.providers?.workspaces??[]),L=l(()=>I.value.filter(({status:e})=>e===`ready`).length),R=l(()=>N.state?.subscriptions??[]),z=l(()=>new Set(R.value.map(({provider_url:e,project_id:t})=>`${e}::${t}`))),B=l(()=>(N.state?.projects??[]).filter(e=>!z.value.has(H(e)))),V=l(()=>B.value.map(e=>({value:H(e),label:e.name,meta:`#${y(e.id,26)}`,search:v(e.id)})));function H(e){return`${e.providerUrl}::${e.id}`}async function U(){let e=B.value.find(e=>H(e)===P.value);if(e)try{await c(`/api/subscriptions`,{personalSpaceId:N.activePersonalSpace?.id,projectId:e.id,providerUrl:e.providerUrl,projectName:e.name}),P.value=``,N.notify(u(`pages.connections.subscribed`,{name:e.name})),await N.refresh()}catch(e){N.reportError(e)}}async function W(e){try{let t=new URLSearchParams({personalSpaceId:N.activePersonalSpace?.id??``,providerUrl:e.provider_url});await d(`/api/subscriptions/${encodeURIComponent(e.project_id)}?${t}`),N.notify(u(`pages.connections.unsubscribed`,{name:e.project_name??e.project_id})),await N.refresh()}catch(e){N.reportError(e)}}return(t,s)=>(o(),m(`section`,b,[h(`div`,x,[h(`h3`,null,i(a(u)(`pages.connections.title`)),1),h(`p`,null,i(a(u)(`pages.connections.intro`)),1)]),h(`div`,{class:`service-connection-grid`,"aria-label":a(u)(`pages.connections.statusAria`)},[h(`article`,{class:`service-connection-card`,"data-status":F.value?`ready`:`error`},[h(`header`,null,[s[2]||=h(`span`,{class:`service-connection-icon local`,"aria-hidden":`true`},[h(`span`,{class:`nav-icon nav-icon-personal-project`})],-1),h(`div`,null,[s[1]||=h(`p`,null,`LOCAL KNOWLEDGE`,-1),h(`h3`,null,i(a(u)(`pages.connections.localGraphiti`)),1)]),h(`span`,w,i(F.value?a(u)(`common.status.connected`):a(u)(`common.status.connectionError`)),1)]),h(`p`,null,i(F.value?a(u)(`pages.connections.localReadyCopy`):a(u)(`pages.connections.localErrorCopy`)),1),h(`dl`,null,[h(`div`,null,[h(`dt`,null,i(a(u)(`pages.connections.storage`)),1),s[3]||=h(`dd`,null,`Neo4j`,-1)]),h(`div`,null,[h(`dt`,null,i(a(u)(`pages.connections.purpose`)),1),h(`dd`,null,i(a(u)(`pages.connections.localPurpose`)),1)])])],8,C),h(`article`,{class:`service-connection-card`,"data-status":a(N).publicRuntimeStatus},[h(`header`,null,[s[5]||=h(`span`,{class:`service-connection-icon public`,"aria-hidden":`true`},[h(`span`,{class:`nav-icon nav-icon-public-project`})],-1),h(`div`,null,[s[4]||=h(`p`,null,`PUBLIC PROVIDER`,-1),h(`h3`,null,i(a(u)(`pages.connections.publicService`)),1)]),h(`span`,E,i(a(N).publicRuntimeStatus===`ready`?a(u)(`common.status.connected`):a(N).publicRuntimeStatus===`error`?a(u)(`common.status.connectionError`):a(u)(`common.status.notConnected`)),1)]),h(`p`,null,i(a(N).publicRuntimeStatus===`ready`?a(u)(`pages.connections.publicReadyCopy`):a(N).publicRuntimeStatus===`error`?a(u)(`pages.connections.publicErrorCopy`):a(u)(`pages.connections.publicOfflineCopy`)),1),h(`dl`,null,[h(`div`,null,[h(`dt`,null,i(a(u)(`pages.connections.currentStatus`)),1),h(`dd`,null,i(a(N).publicRuntimeStatus===`ready`?a(u)(`pages.connections.sharedServicesReady`,{count:L.value||I.value.length}):a(u)(`pages.connections.notAvailable`)),1)]),h(`div`,null,[h(`dt`,null,i(a(u)(`pages.connections.purpose`)),1),h(`dd`,null,i(a(u)(`pages.connections.publicPurpose`)),1)])])],8,T)],8,S),a(N).state?.capabilities?.subscribeProject?(o(),m(`section`,D,[h(`div`,O,[h(`h3`,null,i(a(u)(`pages.connections.subscriptionsTitle`)),1),h(`p`,null,i(a(u)(`pages.connections.subscriptionsCopy`)),1)]),h(`div`,k,[(o(!0),m(g,null,r(R.value,e=>(o(),m(`div`,{key:`${e.provider_url}:${e.project_id}`,class:`subscription-row`},[s[6]||=h(`span`,null,`↗`,-1),h(`div`,null,[h(`strong`,null,i(e.project_name??e.project_id),1),h(`span`,null,i(a(u)(`pages.connections.sharedProject`)),1)]),h(`button`,{class:`secondary-action subscription-action`,type:`button`,onClick:t=>W(e)},i(a(u)(`pages.connections.unsubscribe`)),9,A)]))),128)),R.value.length?e(``,!0):(o(),m(`div`,j,i(a(u)(`pages.connections.noSubscriptions`)),1))]),h(`form`,{class:`subscription-form`,onSubmit:p(U,[`prevent`])},[n(_,{modelValue:P.value,"onUpdate:modelValue":s[0]||=e=>P.value=e,options:V.value,label:a(u)(`pages.connections.sharedProjectLabel`),placeholder:a(u)(`pages.connections.choosePublicProject`),searchable:``,required:``},null,8,[`modelValue`,`options`,`label`,`placeholder`]),h(`button`,M,i(a(u)(`pages.connections.subscribe`)),1)],32)])):e(``,!0)]))}});export{N as default};
|