fuli-context 0.2.0

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 (220) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +178 -0
  3. package/compose.graphiti.yml +86 -0
  4. package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +1 -0
  5. package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +1 -0
  6. package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +1 -0
  7. package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +1 -0
  8. package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +1 -0
  9. package/dist/web/assets/KnowledgePage-Khuo3cNs.js +1 -0
  10. package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +4 -0
  11. package/dist/web/assets/OverviewPage-DC4C4Fhn.js +1 -0
  12. package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +1 -0
  13. package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +1 -0
  14. package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +1 -0
  15. package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +1 -0
  16. package/dist/web/assets/ReviewPage-Dztk2jDG.js +1 -0
  17. package/dist/web/assets/console-tbvTUi6p.js +1 -0
  18. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  19. package/dist/web/assets/identity-B3s5CXhv.js +1 -0
  20. package/dist/web/assets/index-Devf7glZ.css +1 -0
  21. package/dist/web/assets/index-DwUDhQKc.js +6 -0
  22. package/dist/web/assets/model-DVT8sjy9.js +1 -0
  23. package/dist/web/assets/paths-B790iniO.js +1 -0
  24. package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +1 -0
  25. package/dist/web/assets/vue-router-BYTAoWT3.js +1 -0
  26. package/dist/web/index.html +22 -0
  27. package/graph-provider/Dockerfile +16 -0
  28. package/graph-provider/fuli_graph/__init__.py +5 -0
  29. package/graph-provider/fuli_graph/app.py +547 -0
  30. package/graph-provider/fuli_graph/auth.py +17 -0
  31. package/graph-provider/fuli_graph/collaboration_context.py +290 -0
  32. package/graph-provider/fuli_graph/config.py +34 -0
  33. package/graph-provider/fuli_graph/graph_models.py +123 -0
  34. package/graph-provider/fuli_graph/graph_projection.py +405 -0
  35. package/graph-provider/fuli_graph/graph_query.py +561 -0
  36. package/graph-provider/fuli_graph/knowledge_audit.py +765 -0
  37. package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +246 -0
  38. package/graph-provider/fuli_graph/knowledge_management.py +816 -0
  39. package/graph-provider/fuli_graph/knowledge_records.py +65 -0
  40. package/graph-provider/fuli_graph/knowledge_search.py +723 -0
  41. package/graph-provider/fuli_graph/knowledge_usage_models.py +52 -0
  42. package/graph-provider/fuli_graph/model_validation.py +58 -0
  43. package/graph-provider/fuli_graph/models.py +1179 -0
  44. package/graph-provider/fuli_graph/personal_project_access.py +29 -0
  45. package/graph-provider/fuli_graph/preference_conflicts.py +420 -0
  46. package/graph-provider/fuli_graph/project_action_models.py +142 -0
  47. package/graph-provider/fuli_graph/project_knowledge.py +707 -0
  48. package/graph-provider/fuli_graph/project_management.py +251 -0
  49. package/graph-provider/fuli_graph/provider_values.py +40 -0
  50. package/graph-provider/fuli_graph/runtime.py +350 -0
  51. package/graph-provider/fuli_graph/search_projection.py +156 -0
  52. package/graph-provider/fuli_graph/store.py +24 -0
  53. package/graph-provider/fuli_graph/store_identity.py +237 -0
  54. package/graph-provider/fuli_graph/store_knowledge.py +447 -0
  55. package/graph-provider/fuli_graph/store_projects.py +242 -0
  56. package/graph-provider/fuli_graph/store_publication.py +370 -0
  57. package/graph-provider/fuli_graph/store_records.py +134 -0
  58. package/graph-provider/pyproject.toml +35 -0
  59. package/npm-shrinkwrap.json +6045 -0
  60. package/package.json +99 -0
  61. package/skills/capturing-session-knowledge/SKILL.md +226 -0
  62. package/skills/capturing-session-knowledge/agents/openai.yaml +13 -0
  63. package/skills/grilling-project/SKILL.md +129 -0
  64. package/skills/grilling-project/agents/openai.yaml +4 -0
  65. package/skills/grilling-project/references/project-profile.md +105 -0
  66. package/src/agent/candidate-read-service.js +53 -0
  67. package/src/agent/context-pack-service.js +83 -0
  68. package/src/agent/public-read-policy.js +39 -0
  69. package/src/agent/public-read-service.js +69 -0
  70. package/src/agent/read-projection.js +107 -0
  71. package/src/agent/search-service.js +61 -0
  72. package/src/agent/space-boundary.js +33 -0
  73. package/src/agent-tools/graph-definitions.js +479 -0
  74. package/src/agent-tools/graph-handlers.js +96 -0
  75. package/src/agent-tools/schema.js +37 -0
  76. package/src/agent-tools.js +16 -0
  77. package/src/app/application-error.js +13 -0
  78. package/src/app/bootstrap.js +8 -0
  79. package/src/app/create-application.js +143 -0
  80. package/src/app/state-service.js +38 -0
  81. package/src/candidates.js +78 -0
  82. package/src/classifier.js +63 -0
  83. package/src/cli/command-arguments.js +14 -0
  84. package/src/cli/command-registry.js +78 -0
  85. package/src/cli/commands/candidates.js +17 -0
  86. package/src/cli/commands/ingestion.js +32 -0
  87. package/src/cli/commands/reads.js +50 -0
  88. package/src/cli/commands/spaces.js +20 -0
  89. package/src/cli/invocation.js +47 -0
  90. package/src/cli/local-runtime-command.js +94 -0
  91. package/src/cli/local-runtime-options.js +64 -0
  92. package/src/cli/migrate-command.js +71 -0
  93. package/src/cli/migration-destination.js +117 -0
  94. package/src/cli/setup-agent-selection.js +195 -0
  95. package/src/cli/setup-command.js +85 -0
  96. package/src/cli/uninstall-command.js +67 -0
  97. package/src/cli.js +90 -0
  98. package/src/context-pack.js +206 -0
  99. package/src/defaults.js +1 -0
  100. package/src/extractor.js +77 -0
  101. package/src/fact-history.js +39 -0
  102. package/src/facts/fact-service.js +89 -0
  103. package/src/graphiti/agent-access-policy.js +88 -0
  104. package/src/graphiti/capture-policy.js +84 -0
  105. package/src/graphiti/federated-application.js +1292 -0
  106. package/src/graphiti/personal-context.js +29 -0
  107. package/src/graphiti/project-path-context.js +159 -0
  108. package/src/graphiti/provider-client.js +288 -0
  109. package/src/graphiti/runtime-config.js +85 -0
  110. package/src/graphiti/setup.js +43 -0
  111. package/src/graphiti/source-marker.js +142 -0
  112. package/src/http/api-router.js +133 -0
  113. package/src/http/bounded-json-array.js +19 -0
  114. package/src/http/error-mapping.js +36 -0
  115. package/src/http/graph-api-router.js +257 -0
  116. package/src/http/lens-route.js +50 -0
  117. package/src/http/request-policy.js +41 -0
  118. package/src/http/response.js +29 -0
  119. package/src/http/static-handler.js +55 -0
  120. package/src/ingestion.js +161 -0
  121. package/src/lens/lens-correction-service.js +81 -0
  122. package/src/lens/lens-fact-writer.js +85 -0
  123. package/src/lens/lens-input.js +62 -0
  124. package/src/lens/lens-query-projection.js +87 -0
  125. package/src/lens/lens-query-ranking.js +62 -0
  126. package/src/lens/lens-query.js +143 -0
  127. package/src/lens/lens-resource-projection.js +93 -0
  128. package/src/lens/lens-resource-selection.js +45 -0
  129. package/src/lens/lens-resource-service.js +103 -0
  130. package/src/lens/lens-search-query.js +122 -0
  131. package/src/lens/lens-service.js +183 -0
  132. package/src/local-runtime/lifecycle.js +254 -0
  133. package/src/mcp/create-mcp-server.js +85 -0
  134. package/src/mcp/instructions.js +10 -0
  135. package/src/mcp/project-action-preview-tokens.js +72 -0
  136. package/src/mcp/runtime.js +71 -0
  137. package/src/mcp/tool-annotations.js +63 -0
  138. package/src/mcp/tool-result.js +167 -0
  139. package/src/mcp/tool-schema.js +60 -0
  140. package/src/mcp-server.js +76 -0
  141. package/src/models.js +44 -0
  142. package/src/observer.js +83 -0
  143. package/src/package-metadata.js +6 -0
  144. package/src/project-rules.js +33 -0
  145. package/src/publication/canonical-json.js +39 -0
  146. package/src/publication/outbox-service.js +46 -0
  147. package/src/publication/publication-envelope-schema.js +75 -0
  148. package/src/publication/publication-envelope.js +37 -0
  149. package/src/publication/publication-policy.js +82 -0
  150. package/src/publication/publication-service.js +48 -0
  151. package/src/router.js +58 -0
  152. package/src/runtime-options.js +139 -0
  153. package/src/security/episode-visibility.js +14 -0
  154. package/src/security/fact-visibility.js +15 -0
  155. package/src/security/sensitive-content.js +18 -0
  156. package/src/security/source-metadata-policy.js +8 -0
  157. package/src/server/application-lifecycle.js +34 -0
  158. package/src/server/listen.js +24 -0
  159. package/src/server.js +135 -0
  160. package/src/setup/agent-installation-status.js +97 -0
  161. package/src/setup/agents.js +123 -0
  162. package/src/setup/codex-bootstrap.js +118 -0
  163. package/src/setup/codex-config.js +122 -0
  164. package/src/setup/config-backup.js +22 -0
  165. package/src/setup/container-runtime.js +388 -0
  166. package/src/setup/cursor-config.js +61 -0
  167. package/src/setup/graph-runtime.js +360 -0
  168. package/src/setup/node-runtime.js +24 -0
  169. package/src/setup/options.js +91 -0
  170. package/src/setup/paths.js +47 -0
  171. package/src/setup/session-skill.js +91 -0
  172. package/src/setup/setup.js +85 -0
  173. package/src/setup/uninstall.js +90 -0
  174. package/src/storage/episode-evidence-preview.js +100 -0
  175. package/src/storage/fact-search-order.js +18 -0
  176. package/src/storage/file/episode-evidence.js +37 -0
  177. package/src/storage/file/episode-index.js +59 -0
  178. package/src/storage/file/fact-search.js +60 -0
  179. package/src/storage/file-store.js +437 -0
  180. package/src/storage/import-json.js +70 -0
  181. package/src/storage/json-file.js +41 -0
  182. package/src/storage/legacy-record-validation.js +152 -0
  183. package/src/storage/legacy-snapshot-validation.js +32 -0
  184. package/src/storage/migrate.js +128 -0
  185. package/src/storage/migrations/001-initial.sql +83 -0
  186. package/src/storage/migrations/002-storage-integrity.sql +113 -0
  187. package/src/storage/record-validation.js +46 -0
  188. package/src/storage/snapshot-defaults.js +54 -0
  189. package/src/storage/sqlite/candidates-repository.js +84 -0
  190. package/src/storage/sqlite/episode-evidence-repository.js +154 -0
  191. package/src/storage/sqlite/episode-privacy-sql.js +33 -0
  192. package/src/storage/sqlite/episodes-repository.js +52 -0
  193. package/src/storage/sqlite/fact-search-repository.js +162 -0
  194. package/src/storage/sqlite/facts-repository.js +146 -0
  195. package/src/storage/sqlite/imports-repository.js +39 -0
  196. package/src/storage/sqlite/mapper.js +84 -0
  197. package/src/storage/sqlite/open-database.js +15 -0
  198. package/src/storage/sqlite/outbox-repository.js +103 -0
  199. package/src/storage/sqlite/snapshot-repository.js +69 -0
  200. package/src/storage/sqlite/spaces-repository.js +52 -0
  201. package/src/storage/sqlite/subscriptions-repository.js +54 -0
  202. package/src/storage/sqlite-store.js +159 -0
  203. package/src/storage/stable-json.js +24 -0
  204. package/src/storage/store-port.js +20 -0
  205. package/src/store.js +2 -0
  206. package/src/workspace-protocol/array-validation.js +19 -0
  207. package/src/workspace-protocol/base64url-contract.js +27 -0
  208. package/src/workspace-protocol/constants.js +37 -0
  209. package/src/workspace-protocol/error-contract.js +24 -0
  210. package/src/workspace-protocol/fact-contract.js +110 -0
  211. package/src/workspace-protocol/governance-contract.js +47 -0
  212. package/src/workspace-protocol/index.js +53 -0
  213. package/src/workspace-protocol/json-budget.js +19 -0
  214. package/src/workspace-protocol/manifest-contract.js +186 -0
  215. package/src/workspace-protocol/publication-contract.js +212 -0
  216. package/src/workspace-protocol/query-contract.js +149 -0
  217. package/src/workspace-protocol/scalar-contract.js +192 -0
  218. package/src/workspace-protocol/sync-contract.js +184 -0
  219. package/src/workspace-protocol/text-contract.js +39 -0
  220. package/src/workspace-protocol/validation.js +66 -0
@@ -0,0 +1,123 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ import { connectCursor, disconnectCursor } from './cursor-config.js';
7
+ import { connectCodex, disconnectCodex } from './codex-config.js';
8
+
9
+ export function discoverAgents({
10
+ platform = process.platform,
11
+ env = process.env,
12
+ homeDir = homedir(),
13
+ fileExists = existsSync,
14
+ commandExists = (command) => defaultCommandExists(command, platform)
15
+ } = {}) {
16
+ const pathApi = platform === 'win32' ? path.win32 : path.posix;
17
+ const codexHome = nonEmpty(env.CODEX_HOME) ?? pathApi.join(homeDir, '.codex');
18
+ const skillName = 'capturing-session-knowledge';
19
+ const projectSkillName = 'grilling-project';
20
+ return [
21
+ {
22
+ id: 'codex',
23
+ label: 'Codex',
24
+ command: 'codex',
25
+ configPath: pathApi.join(codexHome, 'config.toml'),
26
+ globalInstructionsPath: pathApi.join(codexHome, 'AGENTS.md'),
27
+ globalInstructionsOverridePath: pathApi.join(codexHome, 'AGENTS.override.md'),
28
+ skillPath: pathApi.join(homeDir, '.agents', 'skills', skillName),
29
+ projectSkillPath: pathApi.join(homeDir, '.agents', 'skills', projectSkillName),
30
+ available: Boolean(commandExists('codex') || fileExists(pathApi.join(codexHome, 'config.toml')))
31
+ },
32
+ {
33
+ id: 'claude-code',
34
+ label: 'Claude Code',
35
+ command: 'claude',
36
+ configPath: pathApi.join(homeDir, '.claude.json'),
37
+ skillPath: pathApi.join(homeDir, '.claude', 'skills', skillName),
38
+ projectSkillPath: pathApi.join(homeDir, '.claude', 'skills', projectSkillName),
39
+ available: Boolean(commandExists('claude'))
40
+ },
41
+ {
42
+ id: 'cursor',
43
+ label: 'Cursor',
44
+ command: 'cursor',
45
+ configPath: pathApi.join(homeDir, '.cursor', 'mcp.json'),
46
+ skillPath: pathApi.join(homeDir, '.cursor', 'skills', skillName),
47
+ projectSkillPath: pathApi.join(homeDir, '.cursor', 'skills', projectSkillName),
48
+ available: Boolean(commandExists('cursor'))
49
+ }
50
+ ];
51
+ }
52
+
53
+ export function buildAgentCommands(agent, context) {
54
+ const serverArgs = [
55
+ context.mcpServerPath,
56
+ '--runtime-config',
57
+ context.runtimeConfigPath
58
+ ];
59
+ const mcpArgs = [context.nodePath, ...serverArgs];
60
+ if (agent.id === 'codex') {
61
+ return {
62
+ remove: [agent.command, ['mcp', 'remove', 'fuli']],
63
+ add: [agent.command, ['mcp', 'add', 'fuli', '--', ...mcpArgs]]
64
+ };
65
+ }
66
+ if (agent.id === 'claude-code') {
67
+ return {
68
+ remove: [agent.command, ['mcp', 'remove', '--scope', 'user', 'fuli']],
69
+ add: [agent.command, ['mcp', 'add', '--scope', 'user', 'fuli', '--', ...mcpArgs]]
70
+ };
71
+ }
72
+ throw new TypeError(`Unsupported agent: ${agent.id}`);
73
+ }
74
+
75
+ export function connectAgent(agent, context, {
76
+ runCommand = defaultRunCommand,
77
+ connectCodexConfig = connectCodex,
78
+ connectCursorConfig = connectCursor
79
+ } = {}) {
80
+ if (agent.id === 'codex') return connectCodexConfig(agent, context);
81
+ if (agent.id === 'cursor') return connectCursorConfig(agent, context);
82
+
83
+ const commands = buildAgentCommands(agent, context);
84
+ runCommand(...commands.remove);
85
+ const result = runCommand(...commands.add);
86
+ if (result?.status !== 0) {
87
+ throw new Error(`Could not connect ${agent.label} to Fuli`);
88
+ }
89
+ return { id: agent.id, label: agent.label, status: 'connected' };
90
+ }
91
+
92
+ export function disconnectAgent(agent, {
93
+ disconnectCodexConfig = disconnectCodex,
94
+ disconnectJsonConfig = disconnectCursor
95
+ } = {}) {
96
+ if (agent.id === 'codex') return disconnectCodexConfig(agent);
97
+ if (agent.id === 'cursor' || agent.id === 'claude-code') {
98
+ return disconnectJsonConfig(agent);
99
+ }
100
+ throw new TypeError(`Unsupported agent: ${agent.id}`);
101
+ }
102
+
103
+ export function defaultCommandExists(command, platform = process.platform) {
104
+ const locator = platform === 'win32' ? 'where.exe' : 'which';
105
+ const result = spawnSync(locator, [command], {
106
+ encoding: 'utf8',
107
+ windowsHide: true,
108
+ stdio: 'ignore'
109
+ });
110
+ return result.status === 0;
111
+ }
112
+
113
+ function defaultRunCommand(command, args) {
114
+ return spawnSync(command, args, {
115
+ encoding: 'utf8',
116
+ windowsHide: true,
117
+ stdio: 'pipe'
118
+ });
119
+ }
120
+
121
+ function nonEmpty(value) {
122
+ return typeof value === 'string' && value.trim() ? value : null;
123
+ }
@@ -0,0 +1,118 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ renameSync,
7
+ rmSync,
8
+ writeFileSync
9
+ } from 'node:fs';
10
+ import { basename, dirname, join } from 'node:path';
11
+
12
+ const BLOCK_START = '<!-- BEGIN FULI MANAGED INSTRUCTIONS -->';
13
+ const BLOCK_END = '<!-- END FULI MANAGED INSTRUCTIONS -->';
14
+
15
+ const GLOBAL_INSTRUCTIONS = `${BLOCK_START}
16
+ At the start of every user task, before any other tool or answer, call exactly
17
+ \`get_collaboration_preferences\` with \`projectPath\`=current working directory. Never substitute
18
+ project action or Fuli tool.
19
+ Apply all returned \`effective_preferences\` before answers or tools. For write tools, enforce
20
+ them in the actual payload; mentioning them later is not compliance.
21
+ Before using a relevant \`deferred_conflict\`, call
22
+ \`resolve_deferred_preference_conflict\`; ignore unrelated ones.
23
+ The current request wins on conflict.
24
+ ${BLOCK_END}`;
25
+
26
+ export function installCodexBootstrap(agent, _context, {
27
+ fileExists = existsSync,
28
+ readText = defaultReadText,
29
+ writeText = writeTextAtomic
30
+ } = {}) {
31
+ if (!agent?.globalInstructionsPath) return { changed: false };
32
+ const instructionsPath = activeGlobalInstructionsPath(agent, {
33
+ fileExists,
34
+ readText
35
+ });
36
+ const current = readText(instructionsPath);
37
+ const next = replaceFuliGlobalInstructions(current);
38
+ if (next === current) return { changed: false };
39
+ writeText(instructionsPath, next);
40
+ return { changed: true };
41
+ }
42
+
43
+ export function removeCodexBootstrap(agent, {
44
+ fileExists = existsSync,
45
+ readText = defaultReadText,
46
+ writeText = writeTextAtomic
47
+ } = {}) {
48
+ let changed = false;
49
+ for (const filePath of [
50
+ agent.globalInstructionsPath,
51
+ agent.globalInstructionsOverridePath
52
+ ].filter(Boolean)) {
53
+ if (!fileExists(filePath)) continue;
54
+ const current = readText(filePath);
55
+ const next = removeFuliGlobalInstructions(current);
56
+ if (next === current) continue;
57
+ writeText(filePath, next);
58
+ changed = true;
59
+ }
60
+ return { changed };
61
+ }
62
+
63
+ export function isCodexBootstrapCurrent(agent, _context, {
64
+ fileExists = existsSync,
65
+ readText = defaultReadText
66
+ } = {}) {
67
+ if (!agent?.globalInstructionsPath) return false;
68
+ try {
69
+ const instructionsPath = activeGlobalInstructionsPath(agent, {
70
+ fileExists,
71
+ readText
72
+ });
73
+ const current = readText(instructionsPath);
74
+ return replaceFuliGlobalInstructions(current) === current;
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+
80
+ export function replaceFuliGlobalInstructions(source) {
81
+ const withoutBlock = removeFuliGlobalInstructions(source).trimEnd();
82
+ return `${withoutBlock ? `${withoutBlock}\n\n` : ''}${GLOBAL_INSTRUCTIONS}\n`;
83
+ }
84
+
85
+ export function removeFuliGlobalInstructions(source) {
86
+ const start = source.indexOf(BLOCK_START);
87
+ if (start === -1) return source;
88
+ const end = source.indexOf(BLOCK_END, start);
89
+ if (end === -1) return source;
90
+ const before = source.slice(0, start).trimEnd();
91
+ const after = source.slice(end + BLOCK_END.length).trimStart();
92
+ if (before && after) return `${before}\n\n${after}`;
93
+ if (before) return `${before}\n`;
94
+ return after;
95
+ }
96
+
97
+ function activeGlobalInstructionsPath(agent, { fileExists, readText }) {
98
+ const override = agent.globalInstructionsOverridePath;
99
+ if (override && fileExists(override) && readText(override).trim()) return override;
100
+ return agent.globalInstructionsPath;
101
+ }
102
+
103
+ function defaultReadText(filePath) {
104
+ return existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
105
+ }
106
+
107
+ function writeTextAtomic(filePath, value) {
108
+ const directory = dirname(filePath);
109
+ const temporary = join(directory, `.${basename(filePath)}.${randomUUID()}.tmp`);
110
+ mkdirSync(directory, { recursive: true });
111
+ try {
112
+ writeFileSync(temporary, value, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
113
+ renameSync(temporary, filePath);
114
+ } catch (error) {
115
+ rmSync(temporary, { force: true });
116
+ throw error;
117
+ }
118
+ }
@@ -0,0 +1,122 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ renameSync,
7
+ rmSync,
8
+ writeFileSync
9
+ } from 'node:fs';
10
+ import { basename, dirname, join } from 'node:path';
11
+
12
+ import {
13
+ installCodexBootstrap,
14
+ removeCodexBootstrap
15
+ } from './codex-bootstrap.js';
16
+
17
+ export function connectCodex(agent, context, {
18
+ readConfig = readText,
19
+ writeConfig = writeTextAtomic,
20
+ installBootstrap = installCodexBootstrap
21
+ } = {}) {
22
+ const current = readConfig(agent.configPath);
23
+ const next = replaceFuliTable(current, {
24
+ command: context.nodePath,
25
+ args: [context.mcpServerPath, '--runtime-config', context.runtimeConfigPath]
26
+ });
27
+ const bootstrap = installBootstrap(agent, context);
28
+ const registrationChanged = next !== current;
29
+ if (registrationChanged) writeConfig(agent.configPath, next);
30
+ return {
31
+ id: agent.id,
32
+ label: agent.label,
33
+ status: 'connected',
34
+ newTaskRequired: registrationChanged || bootstrap.changed === true
35
+ };
36
+ }
37
+
38
+ export function disconnectCodex(agent, {
39
+ fileExists = existsSync,
40
+ readConfig = readText,
41
+ writeConfig = writeTextAtomic,
42
+ removeBootstrap = removeCodexBootstrap
43
+ } = {}) {
44
+ const bootstrap = removeBootstrap(agent);
45
+ let registrationChanged = false;
46
+ if (fileExists(agent.configPath)) {
47
+ const current = readConfig(agent.configPath);
48
+ const next = removeFuliTables(current);
49
+ if (next !== current) {
50
+ writeConfig(agent.configPath, next);
51
+ registrationChanged = true;
52
+ }
53
+ }
54
+ return {
55
+ id: agent.id,
56
+ label: agent.label,
57
+ status: registrationChanged || bootstrap.changed ? 'disconnected' : 'not_connected'
58
+ };
59
+ }
60
+
61
+ export function replaceFuliTable(source, { command, args }) {
62
+ const withoutFuli = removeFuliTables(source);
63
+ const lines = withoutFuli.replaceAll('\r\n', '\n').split('\n');
64
+ while (lines.length && !lines.at(-1).trim()) lines.pop();
65
+ const block = [
66
+ '[mcp_servers.fuli]',
67
+ `command = ${tomlString(command)}`,
68
+ `args = [${args.map(tomlString).join(', ')}]`,
69
+ 'enabled = true',
70
+ 'required = true',
71
+ 'startup_timeout_sec = 30',
72
+ 'tool_timeout_sec = 120',
73
+ 'default_tools_approval_mode = "auto"'
74
+ ];
75
+ return `${[...lines, ...(lines.length ? [''] : []), ...block].join('\n')}\n`;
76
+ }
77
+
78
+ export function removeFuliTables(source) {
79
+ const lines = source.replaceAll('\r\n', '\n').split('\n');
80
+ const kept = [];
81
+ let skipping = false;
82
+ let removed = false;
83
+
84
+ for (const line of lines) {
85
+ const header = /^\s*\[([^\]]+)]\s*(?:#.*)?$/.exec(line)?.[1]?.trim();
86
+ if (header) {
87
+ skipping = isFuliTable(header);
88
+ if (skipping) removed = true;
89
+ }
90
+ if (!skipping) kept.push(line);
91
+ }
92
+
93
+ if (!removed) return source;
94
+ while (kept.length && !kept.at(-1).trim()) kept.pop();
95
+ return kept.length ? `${kept.join('\n')}\n` : '';
96
+ }
97
+
98
+ function isFuliTable(header) {
99
+ return /^mcp_servers\.(?:fuli|"fuli"|'fuli')(?:\.|$)/.test(header);
100
+ }
101
+
102
+ function tomlString(value) {
103
+ if (typeof value !== 'string' || !value) throw new TypeError('Codex MCP values must be nonempty');
104
+ return JSON.stringify(value);
105
+ }
106
+
107
+ function readText(filePath) {
108
+ return existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
109
+ }
110
+
111
+ function writeTextAtomic(filePath, value) {
112
+ const directory = dirname(filePath);
113
+ const tempPath = join(directory, `.${basename(filePath)}.${randomUUID()}.tmp`);
114
+ mkdirSync(directory, { recursive: true });
115
+ try {
116
+ writeFileSync(tempPath, value, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
117
+ renameSync(tempPath, filePath);
118
+ } catch (error) {
119
+ rmSync(tempPath, { force: true });
120
+ throw error;
121
+ }
122
+ }
@@ -0,0 +1,22 @@
1
+ import {
2
+ copyFileSync,
3
+ existsSync,
4
+ mkdirSync
5
+ } from 'node:fs';
6
+ import { extname, join } from 'node:path';
7
+
8
+ export function backupAgentConfig(agent, {
9
+ backupDir,
10
+ now = () => new Date(),
11
+ fileExists = existsSync,
12
+ makeDirectory = mkdirSync,
13
+ copyFile = copyFileSync
14
+ }) {
15
+ if (!fileExists(agent.configPath)) return null;
16
+ makeDirectory(backupDir, { recursive: true });
17
+ const timestamp = now().toISOString().replaceAll(':', '-').replaceAll('.', '-');
18
+ const extension = extname(agent.configPath) || '.bak';
19
+ const backupPath = join(backupDir, `${agent.id}-${timestamp}${extension}`);
20
+ copyFile(agent.configPath, backupPath);
21
+ return backupPath;
22
+ }