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.
Files changed (207) hide show
  1. package/README.en.md +5 -0
  2. package/README.md +411 -173
  3. package/README.zh-CN.md +448 -0
  4. package/compose.graphiti.yml +6 -6
  5. package/dist/web/assets/AboutPage-DXh7Fwxt.css +1 -0
  6. package/dist/web/assets/AboutPage-DfnZo7Lo.js +1 -0
  7. package/dist/web/assets/ConnectionsPage-BloaSKmt.js +1 -0
  8. package/dist/web/assets/ConnectionsPage-CBHAdZXT.css +1 -0
  9. package/dist/web/assets/KnowledgeInspector-Bjhdu2mf.css +1 -0
  10. package/dist/web/assets/KnowledgeInspector-QvNpv0_k.js +1 -0
  11. package/dist/web/assets/KnowledgeOrganizerPage-Cx1w1jb2.css +1 -0
  12. package/dist/web/assets/KnowledgeOrganizerPage-DKq1jVD5.js +1 -0
  13. package/dist/web/assets/KnowledgePage-BWL0jT01.js +1 -0
  14. package/dist/web/assets/KnowledgeWorkspace-Czq6aZ7b.js +4 -0
  15. package/dist/web/assets/OverviewPage-5sJoO1Gh.js +1 -0
  16. package/dist/web/assets/{PersonalProfilePage-S8ZcG2LU.css → PersonalProfilePage-CdmEzyvZ.css} +1 -1
  17. package/dist/web/assets/PersonalProfilePage-tVChM9ik.js +1 -0
  18. package/dist/web/assets/PersonalProjectsPage-BPFrup4V.js +1 -0
  19. package/dist/web/assets/PublicProjectsPage-BPJSeREN.js +1 -0
  20. package/dist/web/assets/ReviewPage-B7EN817i.js +1 -0
  21. package/dist/web/assets/SearchableSelect-ClLN0rIb.js +1 -0
  22. package/dist/web/assets/SettingsPage-C6ouU0sR.css +1 -0
  23. package/dist/web/assets/SettingsPage-Chd_B3eH.js +1 -0
  24. package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
  25. package/dist/web/assets/console-CbDsKrsO.js +1 -0
  26. package/dist/web/assets/fuli-logo-Bu9WBuQe.js +1 -0
  27. package/dist/web/assets/fuli-logo-ER1wQ8Kk.png +0 -0
  28. package/dist/web/assets/i18n-BQcns1PH.js +5 -0
  29. package/dist/web/assets/identity-B8K4VYg6.js +1 -0
  30. package/dist/web/assets/index-CB8Xmsc2.js +2 -0
  31. package/dist/web/assets/index-CZeuPh-U.css +1 -0
  32. package/dist/web/assets/model-eF52GgPF.js +1 -0
  33. package/dist/web/assets/paths-hwz-Ac1g.js +1 -0
  34. package/dist/web/assets/useMinimumLoadingDisplay-BpCyBeko.css +1 -0
  35. package/dist/web/assets/useMinimumLoadingDisplay-wCgGDmHg.js +1 -0
  36. package/dist/web/assets/vue-router-BsEp_XwI.js +1 -0
  37. package/dist/web/index.html +9 -6
  38. package/docs/external-knowledge-architecture.md +279 -0
  39. package/examples/external-knowledge/markdown-folder.mjs +235 -0
  40. package/graph-provider/fuli_graph/app.py +57 -1
  41. package/graph-provider/fuli_graph/graph_models.py +3 -0
  42. package/graph-provider/fuli_graph/graph_query.py +62 -16
  43. package/graph-provider/fuli_graph/knowledge_review.py +612 -0
  44. package/graph-provider/fuli_graph/knowledge_review_models.py +127 -0
  45. package/graph-provider/fuli_graph/knowledge_search.py +51 -4
  46. package/graph-provider/fuli_graph/models.py +19 -0
  47. package/graph-provider/fuli_graph/runtime.py +15 -0
  48. package/graph-provider/fuli_graph/store_knowledge.py +2 -1
  49. package/graph-provider/pyproject.toml +1 -1
  50. package/npm-shrinkwrap.json +5 -407
  51. package/package.json +7 -4
  52. package/skills/capturing-session-knowledge/SKILL.md +24 -4
  53. package/skills/flreview/SKILL.md +73 -0
  54. package/skills/flreview/agents/openai.yaml +6 -0
  55. package/src/agent-tools/graph-definitions.js +79 -4
  56. package/src/agent-tools/graph-handlers.js +12 -0
  57. package/src/cli/command-registry.js +3 -3
  58. package/src/cli/local-runtime-command.js +60 -23
  59. package/src/cli/local-runtime-options.js +16 -6
  60. package/src/cli/setup-agent-selection.js +12 -12
  61. package/src/cli/setup-command.js +41 -18
  62. package/src/cli/shell-argument.js +10 -0
  63. package/src/cli/uninstall-command.js +17 -15
  64. package/src/cli/update-command.js +38 -30
  65. package/src/external-knowledge/catalog.js +61 -0
  66. package/src/external-knowledge/conflict-policy.js +58 -0
  67. package/src/external-knowledge/connected-search.js +166 -0
  68. package/src/external-knowledge/connectors/custom.js +133 -0
  69. package/src/external-knowledge/connectors/feishu.js +461 -0
  70. package/src/external-knowledge/connectors/mcp.js +438 -0
  71. package/src/external-knowledge/connectors/notion.js +307 -0
  72. package/src/external-knowledge/connectors/retrieval-api.js +222 -0
  73. package/src/external-knowledge/document-mapping.js +151 -0
  74. package/src/external-knowledge/graph-projection.js +17 -0
  75. package/src/external-knowledge/index.js +8 -0
  76. package/src/external-knowledge/registry.js +91 -0
  77. package/src/external-knowledge/runtime.js +68 -0
  78. package/src/external-knowledge/safe-diagnostic.js +14 -0
  79. package/src/external-knowledge/service.js +811 -0
  80. package/src/graphiti/agent-knowledge-workflows.js +7 -2
  81. package/src/graphiti/federated-application.js +66 -41
  82. package/src/graphiti/knowledge-item-role.js +21 -0
  83. package/src/graphiti/knowledge-review.js +43 -0
  84. package/src/graphiti/project-profile-mapping.js +32 -0
  85. package/src/graphiti/provider-client.js +22 -1
  86. package/src/graphiti/source-marker.js +54 -3
  87. package/src/graphiti/task-knowledge-recall.js +250 -0
  88. package/src/http/api-router.js +23 -108
  89. package/src/http/external-knowledge-api-router.js +104 -0
  90. package/src/http/graph-api-router.js +12 -1
  91. package/src/http/request-policy.js +38 -3
  92. package/src/local-runtime/lifecycle.js +37 -16
  93. package/src/mcp/create-mcp-server.js +1 -0
  94. package/src/mcp/instructions.js +7 -7
  95. package/src/mcp/tool-annotations.js +6 -0
  96. package/src/mcp/tool-result.js +1 -0
  97. package/src/server/application-lifecycle.js +2 -11
  98. package/src/server/blocked-ports.js +11 -0
  99. package/src/server/lan-access.js +52 -0
  100. package/src/server.js +103 -25
  101. package/src/setup/agent-installation-status.js +2 -1
  102. package/src/setup/agents.js +2 -1
  103. package/src/setup/claude-code-config.js +2 -1
  104. package/src/setup/codex-bootstrap.js +12 -7
  105. package/src/setup/container-runtime.js +25 -21
  106. package/src/setup/graph-runtime.js +219 -58
  107. package/src/setup/node-runtime.js +3 -3
  108. package/src/setup/options.js +2 -4
  109. package/src/setup/paths.js +18 -3
  110. package/src/setup/review-skill-adapters.js +49 -0
  111. package/src/setup/setup.js +26 -2
  112. package/src/setup/uninstall.js +4 -2
  113. package/src/system/resource-monitor.js +317 -0
  114. package/src/system/runtime-settings.js +131 -0
  115. package/src/system/system-service.js +119 -0
  116. package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +0 -1
  117. package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +0 -1
  118. package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +0 -1
  119. package/dist/web/assets/KnowledgeOrganizerPage-CJCNI2Wd.css +0 -1
  120. package/dist/web/assets/KnowledgeOrganizerPage-PXH-TlGt.js +0 -1
  121. package/dist/web/assets/KnowledgePage-CNQXd15y.js +0 -1
  122. package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +0 -4
  123. package/dist/web/assets/OverviewPage-ul7g7HXh.js +0 -1
  124. package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +0 -1
  125. package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +0 -1
  126. package/dist/web/assets/PublicProjectsPage-C700sR7z.js +0 -1
  127. package/dist/web/assets/ReviewPage-7MDDUDgA.js +0 -1
  128. package/dist/web/assets/console-B_Ena2DS.js +0 -5
  129. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  130. package/dist/web/assets/identity-CjBwYSMX.js +0 -1
  131. package/dist/web/assets/index-BQRkOwPx.js +0 -2
  132. package/dist/web/assets/index-CpLDx3of.css +0 -1
  133. package/dist/web/assets/model-C-5RVIJ2.js +0 -1
  134. package/dist/web/assets/paths-BkULqmGI.js +0 -1
  135. package/dist/web/assets/vue-router-DnD3hUm7.js +0 -1
  136. package/src/agent/candidate-read-service.js +0 -53
  137. package/src/agent/context-pack-service.js +0 -83
  138. package/src/agent/public-read-policy.js +0 -39
  139. package/src/agent/public-read-service.js +0 -69
  140. package/src/agent/read-projection.js +0 -107
  141. package/src/agent/search-service.js +0 -61
  142. package/src/agent/space-boundary.js +0 -33
  143. package/src/app/bootstrap.js +0 -8
  144. package/src/app/create-application.js +0 -143
  145. package/src/app/state-service.js +0 -38
  146. package/src/candidates.js +0 -78
  147. package/src/classifier.js +0 -63
  148. package/src/cli/command-arguments.js +0 -14
  149. package/src/context-pack.js +0 -206
  150. package/src/extractor.js +0 -77
  151. package/src/fact-history.js +0 -39
  152. package/src/facts/fact-service.js +0 -89
  153. package/src/http/bounded-json-array.js +0 -19
  154. package/src/http/lens-route.js +0 -50
  155. package/src/ingestion.js +0 -161
  156. package/src/lens/lens-correction-service.js +0 -81
  157. package/src/lens/lens-fact-writer.js +0 -85
  158. package/src/lens/lens-input.js +0 -62
  159. package/src/lens/lens-query-projection.js +0 -87
  160. package/src/lens/lens-query-ranking.js +0 -62
  161. package/src/lens/lens-query.js +0 -143
  162. package/src/lens/lens-resource-projection.js +0 -93
  163. package/src/lens/lens-resource-selection.js +0 -45
  164. package/src/lens/lens-resource-service.js +0 -103
  165. package/src/lens/lens-search-query.js +0 -122
  166. package/src/lens/lens-service.js +0 -183
  167. package/src/models.js +0 -44
  168. package/src/observer.js +0 -83
  169. package/src/project-rules.js +0 -33
  170. package/src/publication/outbox-service.js +0 -46
  171. package/src/publication/publication-envelope-schema.js +0 -75
  172. package/src/publication/publication-envelope.js +0 -37
  173. package/src/publication/publication-policy.js +0 -82
  174. package/src/publication/publication-service.js +0 -48
  175. package/src/router.js +0 -58
  176. package/src/runtime-options.js +0 -138
  177. package/src/security/episode-visibility.js +0 -14
  178. package/src/security/fact-visibility.js +0 -15
  179. package/src/security/source-metadata-policy.js +0 -8
  180. package/src/storage/episode-evidence-preview.js +0 -100
  181. package/src/storage/fact-search-order.js +0 -18
  182. package/src/storage/file/episode-evidence.js +0 -37
  183. package/src/storage/file/episode-index.js +0 -59
  184. package/src/storage/file/fact-search.js +0 -60
  185. package/src/storage/file-store.js +0 -437
  186. package/src/storage/migrate.js +0 -128
  187. package/src/storage/migrations/001-initial.sql +0 -83
  188. package/src/storage/migrations/002-storage-integrity.sql +0 -113
  189. package/src/storage/record-validation.js +0 -46
  190. package/src/storage/snapshot-defaults.js +0 -54
  191. package/src/storage/sqlite/candidates-repository.js +0 -84
  192. package/src/storage/sqlite/episode-evidence-repository.js +0 -154
  193. package/src/storage/sqlite/episode-privacy-sql.js +0 -33
  194. package/src/storage/sqlite/episodes-repository.js +0 -52
  195. package/src/storage/sqlite/fact-search-repository.js +0 -162
  196. package/src/storage/sqlite/facts-repository.js +0 -146
  197. package/src/storage/sqlite/imports-repository.js +0 -39
  198. package/src/storage/sqlite/mapper.js +0 -84
  199. package/src/storage/sqlite/open-database.js +0 -15
  200. package/src/storage/sqlite/outbox-repository.js +0 -103
  201. package/src/storage/sqlite/snapshot-repository.js +0 -69
  202. package/src/storage/sqlite/spaces-repository.js +0 -52
  203. package/src/storage/sqlite/subscriptions-repository.js +0 -54
  204. package/src/storage/sqlite-store.js +0 -159
  205. package/src/storage/stable-json.js +0 -24
  206. package/src/storage/store-port.js +0 -20
  207. package/src/store.js +0 -2
@@ -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: options.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 });
@@ -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};