@intutic/sync-daemon 2.0.0 → 2.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 (206) hide show
  1. package/dist/agentReporter.d.ts +5 -12
  2. package/dist/agentReporter.d.ts.map +1 -1
  3. package/dist/agentReporter.js +25 -4
  4. package/dist/agentReporter.js.map +1 -1
  5. package/dist/configReader.d.ts +44 -22
  6. package/dist/configReader.d.ts.map +1 -1
  7. package/dist/configReader.js +70 -44
  8. package/dist/configReader.js.map +1 -1
  9. package/dist/configWriter.d.ts +29 -41
  10. package/dist/configWriter.d.ts.map +1 -1
  11. package/dist/configWriter.js +28 -625
  12. package/dist/configWriter.js.map +1 -1
  13. package/dist/disconnect/harnesses.d.ts +31 -0
  14. package/dist/disconnect/harnesses.d.ts.map +1 -0
  15. package/dist/disconnect/harnesses.js +728 -0
  16. package/dist/disconnect/harnesses.js.map +1 -0
  17. package/dist/disconnect/index.d.ts +25 -0
  18. package/dist/disconnect/index.d.ts.map +1 -0
  19. package/dist/disconnect/index.js +82 -0
  20. package/dist/disconnect/index.js.map +1 -0
  21. package/dist/disconnect/mcp.d.ts +60 -0
  22. package/dist/disconnect/mcp.d.ts.map +1 -0
  23. package/dist/disconnect/mcp.js +283 -0
  24. package/dist/disconnect/mcp.js.map +1 -0
  25. package/dist/disconnect/originals.d.ts +88 -0
  26. package/dist/disconnect/originals.d.ts.map +1 -0
  27. package/dist/disconnect/originals.js +257 -0
  28. package/dist/disconnect/originals.js.map +1 -0
  29. package/dist/disconnect/plan.d.ts +175 -0
  30. package/dist/disconnect/plan.d.ts.map +1 -0
  31. package/dist/disconnect/plan.js +580 -0
  32. package/dist/disconnect/plan.js.map +1 -0
  33. package/dist/disconnect/recognise.d.ts +43 -0
  34. package/dist/disconnect/recognise.d.ts.map +1 -0
  35. package/dist/disconnect/recognise.js +98 -0
  36. package/dist/disconnect/recognise.js.map +1 -0
  37. package/dist/disconnect/tomlLines.d.ts +39 -0
  38. package/dist/disconnect/tomlLines.d.ts.map +1 -0
  39. package/dist/disconnect/tomlLines.js +105 -0
  40. package/dist/disconnect/tomlLines.js.map +1 -0
  41. package/dist/harness/aiderConfigMerger.d.ts +42 -5
  42. package/dist/harness/aiderConfigMerger.d.ts.map +1 -1
  43. package/dist/harness/aiderConfigMerger.js +113 -86
  44. package/dist/harness/aiderConfigMerger.js.map +1 -1
  45. package/dist/harness/antigravityHooks.d.ts +13 -8
  46. package/dist/harness/antigravityHooks.d.ts.map +1 -1
  47. package/dist/harness/antigravityHooks.js +53 -39
  48. package/dist/harness/antigravityHooks.js.map +1 -1
  49. package/dist/harness/claudeCodeHooks.d.ts +1 -2
  50. package/dist/harness/claudeCodeHooks.d.ts.map +1 -1
  51. package/dist/harness/claudeCodeHooks.js +61 -34
  52. package/dist/harness/claudeCodeHooks.js.map +1 -1
  53. package/dist/harness/claudeDesktopHooks.d.ts +3 -0
  54. package/dist/harness/claudeDesktopHooks.d.ts.map +1 -1
  55. package/dist/harness/claudeDesktopHooks.js +3 -1
  56. package/dist/harness/claudeDesktopHooks.js.map +1 -1
  57. package/dist/harness/claudeProjectApproval.d.ts +53 -0
  58. package/dist/harness/claudeProjectApproval.d.ts.map +1 -0
  59. package/dist/harness/claudeProjectApproval.js +159 -0
  60. package/dist/harness/claudeProjectApproval.js.map +1 -0
  61. package/dist/harness/clineHooks.d.ts +39 -9
  62. package/dist/harness/clineHooks.d.ts.map +1 -1
  63. package/dist/harness/clineHooks.js +142 -60
  64. package/dist/harness/clineHooks.js.map +1 -1
  65. package/dist/harness/codexConfigMerger.d.ts +38 -0
  66. package/dist/harness/codexConfigMerger.d.ts.map +1 -0
  67. package/dist/harness/codexConfigMerger.js +105 -0
  68. package/dist/harness/codexConfigMerger.js.map +1 -0
  69. package/dist/harness/codexHooks.d.ts +4 -2
  70. package/dist/harness/codexHooks.d.ts.map +1 -1
  71. package/dist/harness/codexHooks.js +14 -11
  72. package/dist/harness/codexHooks.js.map +1 -1
  73. package/dist/harness/continueConfigMerger.d.ts +46 -0
  74. package/dist/harness/continueConfigMerger.d.ts.map +1 -0
  75. package/dist/harness/continueConfigMerger.js +94 -0
  76. package/dist/harness/continueConfigMerger.js.map +1 -0
  77. package/dist/harness/continueHooks.d.ts +15 -25
  78. package/dist/harness/continueHooks.d.ts.map +1 -1
  79. package/dist/harness/continueHooks.js +25 -119
  80. package/dist/harness/continueHooks.js.map +1 -1
  81. package/dist/harness/cursorHooks.d.ts +33 -16
  82. package/dist/harness/cursorHooks.d.ts.map +1 -1
  83. package/dist/harness/cursorHooks.js +69 -25
  84. package/dist/harness/cursorHooks.js.map +1 -1
  85. package/dist/harness/dshHooks.d.ts +59 -52
  86. package/dist/harness/dshHooks.d.ts.map +1 -1
  87. package/dist/harness/dshHooks.js +152 -147
  88. package/dist/harness/dshHooks.js.map +1 -1
  89. package/dist/harness/gateBody.d.ts +11 -2
  90. package/dist/harness/gateBody.d.ts.map +1 -1
  91. package/dist/harness/gateBody.js +33 -6
  92. package/dist/harness/gateBody.js.map +1 -1
  93. package/dist/harness/gateKind.d.ts.map +1 -1
  94. package/dist/harness/gateKind.js +1 -0
  95. package/dist/harness/gateKind.js.map +1 -1
  96. package/dist/harness/githubCopilotHooks.d.ts.map +1 -1
  97. package/dist/harness/githubCopilotHooks.js +5 -10
  98. package/dist/harness/githubCopilotHooks.js.map +1 -1
  99. package/dist/harness/gooseHooks.d.ts +15 -0
  100. package/dist/harness/gooseHooks.d.ts.map +1 -1
  101. package/dist/harness/gooseHooks.js +55 -43
  102. package/dist/harness/gooseHooks.js.map +1 -1
  103. package/dist/harness/grokHooks.d.ts.map +1 -1
  104. package/dist/harness/grokHooks.js +14 -12
  105. package/dist/harness/grokHooks.js.map +1 -1
  106. package/dist/harness/hermesHooks.d.ts +9 -0
  107. package/dist/harness/hermesHooks.d.ts.map +1 -1
  108. package/dist/harness/hermesHooks.js +39 -33
  109. package/dist/harness/hermesHooks.js.map +1 -1
  110. package/dist/harness/holdRedaction.d.ts +4 -1
  111. package/dist/harness/holdRedaction.d.ts.map +1 -1
  112. package/dist/harness/holdRedaction.js +32 -8
  113. package/dist/harness/holdRedaction.js.map +1 -1
  114. package/dist/harness/jetbrainsXmlConfig.d.ts +20 -0
  115. package/dist/harness/jetbrainsXmlConfig.d.ts.map +1 -1
  116. package/dist/harness/jetbrainsXmlConfig.js +39 -18
  117. package/dist/harness/jetbrainsXmlConfig.js.map +1 -1
  118. package/dist/harness/jsonMergeTarget.d.ts +23 -0
  119. package/dist/harness/jsonMergeTarget.d.ts.map +1 -0
  120. package/dist/harness/jsonMergeTarget.js +50 -0
  121. package/dist/harness/jsonMergeTarget.js.map +1 -0
  122. package/dist/harness/mcpAutoWrite.d.ts +64 -20
  123. package/dist/harness/mcpAutoWrite.d.ts.map +1 -1
  124. package/dist/harness/mcpAutoWrite.js +429 -85
  125. package/dist/harness/mcpAutoWrite.js.map +1 -1
  126. package/dist/harness/museHooks.d.ts.map +1 -1
  127. package/dist/harness/museHooks.js +6 -2
  128. package/dist/harness/museHooks.js.map +1 -1
  129. package/dist/harness/n8nHooks.d.ts.map +1 -1
  130. package/dist/harness/n8nHooks.js +6 -7
  131. package/dist/harness/n8nHooks.js.map +1 -1
  132. package/dist/harness/openCodeHooks.d.ts +37 -6
  133. package/dist/harness/openCodeHooks.d.ts.map +1 -1
  134. package/dist/harness/openCodeHooks.js +103 -13
  135. package/dist/harness/openCodeHooks.js.map +1 -1
  136. package/dist/harness/openWebuiHooks.d.ts.map +1 -1
  137. package/dist/harness/openWebuiHooks.js +10 -5
  138. package/dist/harness/openWebuiHooks.js.map +1 -1
  139. package/dist/harness/openclawHooks.d.ts +5 -0
  140. package/dist/harness/openclawHooks.d.ts.map +1 -1
  141. package/dist/harness/openclawHooks.js +3 -1
  142. package/dist/harness/openclawHooks.js.map +1 -1
  143. package/dist/harness/openhandsHooks.d.ts +26 -1
  144. package/dist/harness/openhandsHooks.d.ts.map +1 -1
  145. package/dist/harness/openhandsHooks.js +160 -40
  146. package/dist/harness/openhandsHooks.js.map +1 -1
  147. package/dist/harness/piHooks.d.ts +17 -0
  148. package/dist/harness/piHooks.d.ts.map +1 -1
  149. package/dist/harness/piHooks.js +31 -29
  150. package/dist/harness/piHooks.js.map +1 -1
  151. package/dist/harness/protectedPaths.d.ts +64 -6
  152. package/dist/harness/protectedPaths.d.ts.map +1 -1
  153. package/dist/harness/protectedPaths.js +161 -11
  154. package/dist/harness/protectedPaths.js.map +1 -1
  155. package/dist/harness/rooCodeHooks.d.ts.map +1 -1
  156. package/dist/harness/rooCodeHooks.js +5 -10
  157. package/dist/harness/rooCodeHooks.js.map +1 -1
  158. package/dist/harness/windsurfHooks.d.ts +21 -3
  159. package/dist/harness/windsurfHooks.d.ts.map +1 -1
  160. package/dist/harness/windsurfHooks.js +96 -44
  161. package/dist/harness/windsurfHooks.js.map +1 -1
  162. package/dist/harness/windsurfJetBrainsProxy.d.ts +10 -7
  163. package/dist/harness/windsurfJetBrainsProxy.d.ts.map +1 -1
  164. package/dist/harness/windsurfJetBrainsProxy.js +22 -7
  165. package/dist/harness/windsurfJetBrainsProxy.js.map +1 -1
  166. package/dist/index.d.ts +12 -12
  167. package/dist/index.d.ts.map +1 -1
  168. package/dist/index.js +11 -10
  169. package/dist/index.js.map +1 -1
  170. package/dist/lib/decisionsDigest.d.ts +1 -1
  171. package/dist/lib/decisionsDigest.js +1 -1
  172. package/dist/lib/policySnapshot.d.ts +37 -0
  173. package/dist/lib/policySnapshot.d.ts.map +1 -1
  174. package/dist/lib/policySnapshot.js +170 -8
  175. package/dist/lib/policySnapshot.js.map +1 -1
  176. package/dist/sessionReporter.d.ts +2 -2
  177. package/dist/sessionReporter.js +2 -2
  178. package/dist/syncCycle.d.ts +142 -0
  179. package/dist/syncCycle.d.ts.map +1 -0
  180. package/dist/syncCycle.js +315 -0
  181. package/dist/syncCycle.js.map +1 -0
  182. package/dist/trajectoryMonitor.d.ts +1 -1
  183. package/dist/trajectoryMonitor.d.ts.map +1 -1
  184. package/dist/trajectoryMonitor.js +1 -1
  185. package/dist/trajectoryMonitor.js.map +1 -1
  186. package/dist/watcher/settingsGuard.d.ts +4 -1
  187. package/dist/watcher/settingsGuard.d.ts.map +1 -1
  188. package/dist/watcher/settingsGuard.js +42 -18
  189. package/dist/watcher/settingsGuard.js.map +1 -1
  190. package/package.json +6 -6
  191. package/dist/hashReporter.d.ts +0 -33
  192. package/dist/hashReporter.d.ts.map +0 -1
  193. package/dist/hashReporter.js +0 -69
  194. package/dist/hashReporter.js.map +0 -1
  195. package/dist/integrityStore.d.ts +0 -35
  196. package/dist/integrityStore.d.ts.map +0 -1
  197. package/dist/integrityStore.js +0 -71
  198. package/dist/integrityStore.js.map +0 -1
  199. package/dist/lib/gitWorktrees.d.ts +0 -140
  200. package/dist/lib/gitWorktrees.d.ts.map +0 -1
  201. package/dist/lib/gitWorktrees.js +0 -191
  202. package/dist/lib/gitWorktrees.js.map +0 -1
  203. package/dist/syncLoop.d.ts +0 -172
  204. package/dist/syncLoop.d.ts.map +0 -1
  205. package/dist/syncLoop.js +0 -878
  206. package/dist/syncLoop.js.map +0 -1
@@ -0,0 +1,105 @@
1
+ /**
2
+ * codexConfigMerger.ts — point Codex's built-in OpenAI provider at the proxy.
3
+ *
4
+ * Codex reads its user config from `$CODEX_HOME/config.toml` (`~/.codex` by
5
+ * default). The one key this writer owns is the top-level `openai_base_url`,
6
+ * Codex's documented override for the built-in `openai` provider's base URL.
7
+ * It is user-level only, which is why this writes the user file and not a
8
+ * project `.codex/config.toml`. A `[model_providers.openai]` table cannot do
9
+ * this job: Codex reserves the built-in provider ids (`openai`, `ollama`,
10
+ * `lmstudio`) and does not let `model_providers` override them.
11
+ *
12
+ * Everything else in the file belongs to the user — MCP servers, profiles,
13
+ * model choice, comments — and is kept byte for byte: the key is replaced or
14
+ * inserted as a single line rather than by re-serialising the document, which
15
+ * would drop every comment. The result is parsed before it is written, and an
16
+ * existing file that does not parse is left alone.
17
+ *
18
+ * A file whose first line is the old adapter's header was written whole by
19
+ * that adapter (it replaced the user's config with two provider tables), so it
20
+ * holds nothing of the user's and is regenerated.
21
+ *
22
+ * @module
23
+ */
24
+ import * as fs from 'node:fs/promises';
25
+ import * as path from 'node:path';
26
+ import * as os from 'node:os';
27
+ import { parse as parseToml } from 'smol-toml';
28
+ import { createLogger } from '@intutic/logger';
29
+ import { keepOriginal } from '../disconnect/originals.js';
30
+ const log = createLogger('sync-codex-config');
31
+ /** First line of the file the previous adapter wrote over the user's config. */
32
+ const LEGACY_HEADER = '# Intutic proxy config (auto-generated)';
33
+ const KEY = 'openai_base_url';
34
+ /** Comment written above the key the first time it is inserted. */
35
+ const KEY_COMMENT = "# Set by Intutic: routes Codex's built-in OpenAI provider through the Intutic proxy.";
36
+ function parses(toml) {
37
+ try {
38
+ return parseToml(toml);
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ /**
45
+ * Set the top-level `openai_base_url` in a Codex config.toml's text.
46
+ *
47
+ * @returns the new file content, or `null` when `raw` is not valid TOML (the
48
+ * caller must leave the file alone).
49
+ */
50
+ export function setCodexOpenaiBaseUrl(raw, baseUrl) {
51
+ const source = raw.startsWith(LEGACY_HEADER) ? '' : raw;
52
+ if (parses(source) === null)
53
+ return null;
54
+ const line = `${KEY} = ${JSON.stringify(baseUrl)}`;
55
+ const lines = source.split('\n');
56
+ // Top-level keys must come before the first table header; a key of the same
57
+ // name inside a table is a different key and is not touched.
58
+ const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
59
+ const topLevelEnd = firstTable === -1 ? lines.length : firstTable;
60
+ const existing = lines.slice(0, topLevelEnd).findIndex((l) => new RegExp(`^\\s*${KEY}\\s*=`).test(l));
61
+ let next;
62
+ if (existing !== -1) {
63
+ lines[existing] = line;
64
+ next = lines.join('\n');
65
+ }
66
+ else {
67
+ next = `${KEY_COMMENT}\n${line}\n` + (source.trim() === '' ? '' : `\n${source}`);
68
+ }
69
+ const check = parses(next);
70
+ if (check === null || check[KEY] !== baseUrl)
71
+ return null;
72
+ return next;
73
+ }
74
+ /**
75
+ * Merge `openai_base_url` into the Codex user config at `configPath`.
76
+ *
77
+ * @returns `true` when the file now carries the URL, `false` when it was left
78
+ * untouched because it is not valid TOML (a warning is logged).
79
+ */
80
+ export async function mergeCodexConfig(configPath, baseUrl) {
81
+ let raw = '';
82
+ try {
83
+ raw = await fs.readFile(configPath, 'utf-8');
84
+ }
85
+ catch (err) {
86
+ if (err.code !== 'ENOENT')
87
+ throw err;
88
+ }
89
+ const next = setCodexOpenaiBaseUrl(raw, baseUrl);
90
+ if (next === null) {
91
+ log.warn({ action: 'codex_config_merge_skipped', path: configPath }, `${configPath} is not valid TOML — left untouched; fix it and the next sync will set ${KEY}`);
92
+ return false;
93
+ }
94
+ if (next === raw)
95
+ return true;
96
+ // A user-level file: its record lives under the home directory.
97
+ await keepOriginal(configPath, os.homedir());
98
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
99
+ const tmp = configPath + '.intutic-tmp';
100
+ await fs.writeFile(tmp, next, 'utf-8');
101
+ await fs.rename(tmp, configPath);
102
+ log.info({ action: 'codex_config_merged', path: configPath }, `Set ${KEY} in Codex config`);
103
+ return true;
104
+ }
105
+ //# sourceMappingURL=codexConfigMerger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codexConfigMerger.js","sourceRoot":"","sources":["../../src/harness/codexConfigMerger.ts"],"names":[],"mappings":"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"}
@@ -2,8 +2,10 @@
2
2
  * codexHooks.ts — Codex CLI PreToolUse hook injection.
3
3
  *
4
4
  * Codex CLI executes PreToolUse hooks registered in `hooks.json`:
5
- * - User-level: ~/.codex/hooks.json
6
- * - Project-level: <workspaceRoot>/.codex/hooks.json
5
+ * - User-level: $CODEX_HOME/hooks.json (~/.codex/hooks.json by default)
6
+ * - Project-level: <workspaceRoot>/.codex/hooks.json — Codex loads this one
7
+ * only once the user has trusted the project's `.codex/` layer, so the
8
+ * user-level registration is the one that always applies.
7
9
  *
8
10
  * Each PreToolUse entry carries a `matcher` regex over tool names; the hook
9
11
  * command receives JSON on stdin — {tool_name, tool_use_id, tool_input}, with
@@ -1 +1 @@
1
- {"version":3,"file":"codexHooks.d.ts","sourceRoot":"","sources":["../../src/harness/codexHooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAwLH;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,WAAW,SAAK,GACf,OAAO,CAAC,IAAI,CAAC,CAwBf"}
1
+ {"version":3,"file":"codexHooks.d.ts","sourceRoot":"","sources":["../../src/harness/codexHooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAsLH;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,WAAW,SAAK,GACf,OAAO,CAAC,IAAI,CAAC,CA2Bf"}
@@ -2,8 +2,10 @@
2
2
  * codexHooks.ts — Codex CLI PreToolUse hook injection.
3
3
  *
4
4
  * Codex CLI executes PreToolUse hooks registered in `hooks.json`:
5
- * - User-level: ~/.codex/hooks.json
6
- * - Project-level: <workspaceRoot>/.codex/hooks.json
5
+ * - User-level: $CODEX_HOME/hooks.json (~/.codex/hooks.json by default)
6
+ * - Project-level: <workspaceRoot>/.codex/hooks.json — Codex loads this one
7
+ * only once the user has trusted the project's `.codex/` layer, so the
8
+ * user-level registration is the one that always applies.
7
9
  *
8
10
  * Each PreToolUse entry carries a `matcher` regex over tool names; the hook
9
11
  * command receives JSON on stdin — {tool_name, tool_use_id, tool_input}, with
@@ -26,8 +28,10 @@ import * as fs from 'node:fs/promises';
26
28
  import * as path from 'node:path';
27
29
  import * as os from 'node:os';
28
30
  import { createLogger } from '@intutic/logger';
31
+ import { keepOriginal } from '../disconnect/originals.js';
29
32
  import { newIso } from '@intutic/id';
30
33
  import { emitJsGate, emitJsFailClosedPrelude } from './gateBody.js';
34
+ import { readJsonObjectForMerge } from './jsonMergeTarget.js';
31
35
  const log = createLogger('sync-codex-hooks');
32
36
  /**
33
37
  * The pre-tool gate script — receives Codex's PreToolUse JSON on stdin,
@@ -139,13 +143,9 @@ process.stdin.on('end', () => {
139
143
  * uses — so repeated syncs are idempotent and never stack duplicate entries.
140
144
  */
141
145
  async function mergeCodexHooksJson(configPath, hookScriptPath) {
142
- let existing = {};
143
- try {
144
- existing = JSON.parse(await fs.readFile(configPath, 'utf-8'));
145
- }
146
- catch {
147
- // File doesn't exist or is malformed — start fresh
148
- }
146
+ const existing = await readJsonObjectForMerge(configPath);
147
+ if (existing === null)
148
+ return;
149
149
  const existingHooks = existing.hooks ?? {};
150
150
  const existingPreToolUse = Array.isArray(existingHooks.PreToolUse)
151
151
  ? existingHooks.PreToolUse
@@ -215,11 +215,14 @@ export async function writeCodexHooks(workspaceRoot, proxyUrl, workspaceId = '')
215
215
  await fs.chmod(hookScriptPath, 0o755);
216
216
  // 1. Project-level: <root>/.codex/hooks.json
217
217
  const projectCodexDir = path.join(workspaceRoot, '.codex');
218
+ await keepOriginal(path.join(projectCodexDir, 'hooks.json'), workspaceRoot);
218
219
  await fs.mkdir(projectCodexDir, { recursive: true });
219
220
  await mergeCodexHooksJson(path.join(projectCodexDir, 'hooks.json'), hookScriptPath);
220
221
  log.info({ action: 'codex_hooks_written', level: 'project', path: projectCodexDir }, 'Codex project-level hooks written');
221
- // 2. User-level: ~/.codex/hooks.json
222
- const userCodexDir = path.join(os.homedir(), '.codex');
222
+ // 2. User-level: $CODEX_HOME/hooks.json — Codex keeps its user config in
223
+ // CODEX_HOME when set, ~/.codex otherwise.
224
+ const userCodexDir = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
225
+ await keepOriginal(path.join(userCodexDir, 'hooks.json'), workspaceRoot);
223
226
  await fs.mkdir(userCodexDir, { recursive: true });
224
227
  await mergeCodexHooksJson(path.join(userCodexDir, 'hooks.json'), hookScriptPath);
225
228
  log.info({ action: 'codex_hooks_written', level: 'user', path: userCodexDir }, 'Codex user-level hooks written');
@@ -1 +1 @@
1
- {"version":3,"file":"codexHooks.js","sourceRoot":"","sources":["../../src/harness/codexHooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAA;AACtC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,UAAU,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AAEnE,MAAM,GAAG,GAAG,YAAY,CAAC,kBAAkB,CAAC,CAAA;AAE5C;;;GAGG;AACH,SAAS,eAAe,CAAC,QAAgB,EAAE,aAAqB,EAAE,WAAmB;IACnF,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,mBAAmB,CAAC,CAAA;IACzF,OAAO;;;;YAIG,QAAQ;gBACJ,MAAM,EAAE;;EAEtB,uBAAuB,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;;;;;;;;;gFASc,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC;;;;;;;;;;;EAWzG,UAAU,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;;;;;;;;;;;;;;;;;;wBAkB7B,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgDpD,CAAA;AACD,CAAC;AAED;;;;;;;GAOG;AACH,KAAK,UAAU,mBAAmB,CAAC,UAAkB,EAAE,cAAsB;IAC3E,IAAI,QAAQ,GAA4B,EAAE,CAAA;IAC1C,IAAI,CAAC;QACH,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAA;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,mDAAmD;IACrD,CAAC;IAED,MAAM,aAAa,GAAI,QAAQ,CAAC,KAAiC,IAAI,EAAE,CAAA;IACvE,MAAM,kBAAkB,GAAG,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC,UAAU,CAAC;QAChE,CAAC,CAAE,aAAa,CAAC,UAAwB;QACzC,CAAC,CAAC,EAAE,CAAA;IAEN,MAAM,UAAU,GAAG,SAAS,cAAc,GAAG,CAAA;IAC7C,MAAM,QAAQ,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC,KAAc,EAAE,EAAE;QAC5D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,IAAI,CAAA;QAC5D,MAAM,KAAK,GAAK,KAAiC,CAAC,KAAmB,IAAI,EAAE,CAAA;QAC3E,OAAO,CAAC,CACN,KAAK,CAAC,MAAM,GAAG,CAAC;YAChB,KAAK,CAAC,KAAK,CACT,CAAC,CAAU,EAAE,EAAE,CACb,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAK,CAA6B,CAAC,OAAO,KAAK,UAAU,CAC/F,CACF,CAAA;IACH,CAAC,CAAC,CAAA;IAEF,mEAAmE;IACnE,4EAA4E;IAC5E,2EAA2E;IAC3E,6EAA6E;IAC7E,sEAAsE;IACtE,mEAAmE;IACnE,2EAA2E;IAC3E,2EAA2E;IAC3E,0EAA0E;IAC1E,wDAAwD;IACxD,OAAQ,QAAoC,CAAC,kBAAkB,CAAC,CAAA;IAChE,OAAQ,QAAoC,CAAC,oBAAoB,CAAC,CAAA;IAClE,MAAM,MAAM,GAAG;QACb,GAAG,QAAQ;QACX,WAAW,EACT,mFAAmF;YACnF,cAAc,MAAM,EAAE,EAAE;QAC1B,KAAK,EAAE;YACL,GAAG,aAAa;YAChB,UAAU,EAAE;gBACV,GAAG,QAAQ;gBACX;oBACE,iEAAiE;oBACjE,mEAAmE;oBACnE,kDAAkD;oBAClD,OAAO,EAAE,IAAI;oBACb,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;iBAClD;aACF;SACF;KACF,CAAA;IAED,MAAM,GAAG,GAAG,UAAU,GAAG,cAAc,CAAA;IACvC,MAAM,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAA;IACxE,MAAM,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,UAAU,CAAC,CAAA;AAClC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,aAAqB,EACrB,QAAgB,EAChB,WAAW,GAAG,EAAE;IAEhB,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,UAAU,EAAE,OAAO,CAAC,CAAA;IACnE,MAAM,EAAE,CAAC,KAAK,CAAC,aAAa,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAClD,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAEnF,wEAAwE;IACxE,+CAA+C;IAC/C,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,gBAAgB,CAAC,CAAA;IACjE,MAAM,SAAS,GAAG,cAAc,GAAG,cAAc,CAAA;IACjD,MAAM,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,eAAe,CAAC,QAAQ,EAAE,aAAa,EAAE,WAAW,CAAC,EAAE,OAAO,CAAC,CAAA;IAC7F,MAAM,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,cAAc,CAAC,CAAA;IAC1C,MAAM,EAAE,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,CAAC,CAAA;IAErC,6CAA6C;IAC7C,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAA;IAC1D,MAAM,EAAE,CAAC,KAAK,CAAC,eAAe,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACpD,MAAM,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,YAAY,CAAC,EAAE,cAAc,CAAC,CAAA;IACnF,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,qBAAqB,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,eAAe,EAAE,EAAE,mCAAmC,CAAC,CAAA;IAEzH,qCAAqC;IACrC,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAA;IACtD,MAAM,EAAE,CAAC,KAAK,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACjD,MAAM,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,EAAE,cAAc,CAAC,CAAA;IAChF,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,qBAAqB,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,gCAAgC,CAAC,CAAA;AAClH,CAAC"}
1
+ {"version":3,"file":"codexHooks.js","sourceRoot":"","sources":["../../src/harness/codexHooks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * continueConfigMerger.ts — point Continue's OpenAI and Anthropic models at
3
+ * the proxy.
4
+ *
5
+ * Continue reads its models from `~/.continue/config.yaml`. This sets
6
+ * `apiBase` on each model whose `provider` is `openai` or `anthropic` — the two
7
+ * wire protocols the proxy serves — and leaves every other model, and every
8
+ * other key, alone. Pointing an Ollama or Gemini model at the proxy would break
9
+ * it, so those are not touched.
10
+ *
11
+ * Continue joins its request path onto `apiBase` with URL resolution
12
+ * (`chat/completions` for OpenAI, `messages` for Anthropic), so the value is
13
+ * the proxy's `/v1/` with the trailing slash: without it the last path segment
14
+ * would be replaced instead of extended.
15
+ *
16
+ * The file is edited through the `yaml` library's document model, so comments,
17
+ * ordering and formatting survive. A file that does not parse, or has no
18
+ * `models` list, is left alone: inventing a model entry would give Continue a
19
+ * model with no provider or name, which it rejects.
20
+ *
21
+ * @module
22
+ */
23
+ /** Result of merging the proxy URL into Continue's config.yaml text. */
24
+ export interface ContinueMergeResult {
25
+ /** New file content, or `null` when the file must be left untouched. */
26
+ content: string | null;
27
+ /** Number of models now pointing at the proxy. */
28
+ routed: number;
29
+ /** Why the file was left untouched, when `content` is `null`. */
30
+ skipped?: 'unparseable' | 'no-models';
31
+ }
32
+ /**
33
+ * Set `apiBase` on every OpenAI/Anthropic model in a Continue config.yaml.
34
+ *
35
+ * @param raw - Current file content.
36
+ * @param apiBase - The proxy's OpenAI-style base URL (host + `/v1`).
37
+ */
38
+ export declare function setContinueApiBase(raw: string, apiBase: string): ContinueMergeResult;
39
+ /**
40
+ * Merge the proxy URL into Continue's config.yaml at `configPath`.
41
+ *
42
+ * @returns the number of models routed through the proxy; 0 when the file was
43
+ * missing, unparseable or had no OpenAI/Anthropic model (logged).
44
+ */
45
+ export declare function mergeContinueConfig(configPath: string, apiBase: string): Promise<number>;
46
+ //# sourceMappingURL=continueConfigMerger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"continueConfigMerger.d.ts","sourceRoot":"","sources":["../../src/harness/continueConfigMerger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAcH,wEAAwE;AACxE,MAAM,WAAW,mBAAmB;IAClC,wEAAwE;IACxE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,OAAO,CAAC,EAAE,aAAa,GAAG,WAAW,CAAA;CACtC;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,mBAAmB,CAkBpF;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA6B9F"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * continueConfigMerger.ts — point Continue's OpenAI and Anthropic models at
3
+ * the proxy.
4
+ *
5
+ * Continue reads its models from `~/.continue/config.yaml`. This sets
6
+ * `apiBase` on each model whose `provider` is `openai` or `anthropic` — the two
7
+ * wire protocols the proxy serves — and leaves every other model, and every
8
+ * other key, alone. Pointing an Ollama or Gemini model at the proxy would break
9
+ * it, so those are not touched.
10
+ *
11
+ * Continue joins its request path onto `apiBase` with URL resolution
12
+ * (`chat/completions` for OpenAI, `messages` for Anthropic), so the value is
13
+ * the proxy's `/v1/` with the trailing slash: without it the last path segment
14
+ * would be replaced instead of extended.
15
+ *
16
+ * The file is edited through the `yaml` library's document model, so comments,
17
+ * ordering and formatting survive. A file that does not parse, or has no
18
+ * `models` list, is left alone: inventing a model entry would give Continue a
19
+ * model with no provider or name, which it rejects.
20
+ *
21
+ * @module
22
+ */
23
+ import * as fs from 'node:fs/promises';
24
+ import * as path from 'node:path';
25
+ import * as os from 'node:os';
26
+ import { parseDocument, isMap, isSeq } from 'yaml';
27
+ import { createLogger } from '@intutic/logger';
28
+ import { keepOriginal } from '../disconnect/originals.js';
29
+ const log = createLogger('sync-continue-config');
30
+ /** Providers whose wire protocol the proxy serves. */
31
+ const PROXIED_PROVIDERS = new Set(['openai', 'anthropic']);
32
+ /**
33
+ * Set `apiBase` on every OpenAI/Anthropic model in a Continue config.yaml.
34
+ *
35
+ * @param raw - Current file content.
36
+ * @param apiBase - The proxy's OpenAI-style base URL (host + `/v1`).
37
+ */
38
+ export function setContinueApiBase(raw, apiBase) {
39
+ const value = apiBase.endsWith('/') ? apiBase : `${apiBase}/`;
40
+ const doc = parseDocument(raw);
41
+ if (doc.errors.length > 0)
42
+ return { content: null, routed: 0, skipped: 'unparseable' };
43
+ const models = isMap(doc.contents) ? doc.contents.get('models', true) : undefined;
44
+ if (!isSeq(models))
45
+ return { content: null, routed: 0, skipped: 'no-models' };
46
+ let routed = 0;
47
+ for (const model of models.items) {
48
+ if (!isMap(model))
49
+ continue;
50
+ const provider = model.get('provider');
51
+ if (typeof provider !== 'string' || !PROXIED_PROVIDERS.has(provider))
52
+ continue;
53
+ model.set('apiBase', value);
54
+ routed++;
55
+ }
56
+ if (routed === 0)
57
+ return { content: null, routed: 0, skipped: 'no-models' };
58
+ return { content: doc.toString(), routed };
59
+ }
60
+ /**
61
+ * Merge the proxy URL into Continue's config.yaml at `configPath`.
62
+ *
63
+ * @returns the number of models routed through the proxy; 0 when the file was
64
+ * missing, unparseable or had no OpenAI/Anthropic model (logged).
65
+ */
66
+ export async function mergeContinueConfig(configPath, apiBase) {
67
+ let raw;
68
+ try {
69
+ raw = await fs.readFile(configPath, 'utf-8');
70
+ }
71
+ catch (err) {
72
+ if (err.code === 'ENOENT')
73
+ return 0;
74
+ throw err;
75
+ }
76
+ const result = setContinueApiBase(raw, apiBase);
77
+ if (result.content === null) {
78
+ log.warn({ action: 'continue_config_merge_skipped', path: configPath, reason: result.skipped }, result.skipped === 'unparseable'
79
+ ? `${configPath} is not valid YAML — left untouched`
80
+ : `${configPath} has no openai or anthropic model to route — left untouched`);
81
+ return 0;
82
+ }
83
+ if (result.content !== raw) {
84
+ // A user-level file: its record lives under the home directory.
85
+ await keepOriginal(configPath, os.homedir());
86
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
87
+ const tmp = configPath + '.intutic-tmp';
88
+ await fs.writeFile(tmp, result.content, 'utf-8');
89
+ await fs.rename(tmp, configPath);
90
+ }
91
+ log.info({ action: 'continue_config_merged', path: configPath, routed: result.routed }, 'Continue models routed through the Intutic proxy');
92
+ return result.routed;
93
+ }
94
+ //# sourceMappingURL=continueConfigMerger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"continueConfigMerger.js","sourceRoot":"","sources":["../../src/harness/continueConfigMerger.ts"],"names":[],"mappings":"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"}
@@ -1,13 +1,11 @@
1
1
  /**
2
- * continueHooks.ts — Continue governance: proxy routing + CLI PreToolUse gate.
2
+ * continueHooks.ts — Continue CLI PreToolUse gate.
3
3
  *
4
- * Two mechanisms, for two different Continue surfaces:
4
+ * Proxy routing for Continue (the `apiBase` of each OpenAI/Anthropic model in
5
+ * `~/.continue/config.yaml`) is written by the CLI's Continue adapter
6
+ * (tools/cli/src/harness/continue.ts). This module writes only the gate.
5
7
  *
6
- * 1. **Proxy routing** (both surfaces): each model's `apiBase` field in
7
- * `~/.continue/config.json` is set to `proxyUrl`, so LLM egress traverses
8
- * the Intutic proxy.
9
- *
10
- * 2. **PreToolUse gate** (Continue **CLI `cn` only** — the IDE extension has
8
+ * **PreToolUse gate** (Continue **CLI `cn` only** — the IDE extension has
11
9
  * no hook system): the CLI executes PreToolUse hooks registered in
12
10
  * `~/.continue/settings.json` (user) and `<repo>/.continue/settings.json`
13
11
  * (project), with a Claude-Code-compatible stdin contract —
@@ -40,9 +38,8 @@
40
38
  * get no gate at all. Double registration is harmless: both gates evaluate
41
39
  * the same rules, and an allow/allow or block/block pair is idempotent.
42
40
  *
43
- * JSONC handling: ~/.continue/config.json allows `// comments`. We strip
44
- * single-line comments with a regex before parsing, then write back as
45
- * standard JSON with a managed-file header comment.
41
+ * The settings files are merged, never replaced: a file that is not a plain
42
+ * JSON object is left alone and reported (see jsonMergeTarget.ts).
46
43
  *
47
44
  * LLD #14 — Phase 3 cross-harness defence (Gap 3, WS-B)
48
45
  * HLD §3.14 — Three-Tier Defense Cascade (Tier 1 Native Gating)
@@ -50,24 +47,17 @@
50
47
  * @module
51
48
  */
52
49
  /**
53
- * Configure Continue: proxy routing for both surfaces, and the CLI's
54
- * PreToolUse gate.
55
- *
56
- * 1. Reads `~/.continue/config.json` (JSONC — strips // comments before parsing).
57
- * 2. Sets `apiBase = proxyUrl` on each model that doesn't already use it.
58
- * 3. Adds a `customInstructions` governance notice.
59
- * 4. Writes the merged result back atomically.
60
- * 5. Writes `.intutic/env/continue.env` with the proxy URL.
61
- * 6. Writes the gate script `.intutic/hooks/continue-check.js` and registers
62
- * it in `~/.continue/settings.json` and `<root>/.continue/settings.json`
63
- * (Continue CLI PreToolUse — see the module header for the
64
- * `.claude/settings.json` overlap).
50
+ * Write the Continue CLI gate script `.intutic/hooks/continue-check.js` and
51
+ * register it in `~/.continue/settings.json` and
52
+ * `<root>/.continue/settings.json` (see the module header for the
53
+ * `.claude/settings.json` overlap).
65
54
  *
66
- * Safe to call repeatedly — uses atomic rename (`.intutic-tmp` → final path).
55
+ * Safe to call repeatedly — uses atomic rename (`.intutic-tmp` → final path)
56
+ * and command-keyed de-duplication in both settings files.
67
57
  *
68
58
  * @param workspaceRoot - Absolute workspace root path.
69
- * @param proxyUrl - Intutic proxy URL to set as model apiBase.
70
- * @param workspaceId - Workspace ID stored in the env snippet.
59
+ * @param proxyUrl - Intutic proxy URL written into the gate header.
60
+ * @param workspaceId - Workspace ID embedded in every hook event payload.
71
61
  */
72
62
  export declare function writeContinueHooks(workspaceRoot: string, proxyUrl: string, workspaceId?: string): Promise<void>;
73
63
  //# sourceMappingURL=continueHooks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"continueHooks.d.ts","sourceRoot":"","sources":["../../src/harness/continueHooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AA2MH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,kBAAkB,CACtC,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,WAAW,SAAK,GACf,OAAO,CAAC,IAAI,CAAC,CAoHf"}
1
+ {"version":3,"file":"continueHooks.d.ts","sourceRoot":"","sources":["../../src/harness/continueHooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAkLH;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CACtC,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,WAAW,SAAK,GACf,OAAO,CAAC,IAAI,CAAC,CA+Bf"}
@@ -1,13 +1,11 @@
1
1
  /**
2
- * continueHooks.ts — Continue governance: proxy routing + CLI PreToolUse gate.
2
+ * continueHooks.ts — Continue CLI PreToolUse gate.
3
3
  *
4
- * Two mechanisms, for two different Continue surfaces:
4
+ * Proxy routing for Continue (the `apiBase` of each OpenAI/Anthropic model in
5
+ * `~/.continue/config.yaml`) is written by the CLI's Continue adapter
6
+ * (tools/cli/src/harness/continue.ts). This module writes only the gate.
5
7
  *
6
- * 1. **Proxy routing** (both surfaces): each model's `apiBase` field in
7
- * `~/.continue/config.json` is set to `proxyUrl`, so LLM egress traverses
8
- * the Intutic proxy.
9
- *
10
- * 2. **PreToolUse gate** (Continue **CLI `cn` only** — the IDE extension has
8
+ * **PreToolUse gate** (Continue **CLI `cn` only** — the IDE extension has
11
9
  * no hook system): the CLI executes PreToolUse hooks registered in
12
10
  * `~/.continue/settings.json` (user) and `<repo>/.continue/settings.json`
13
11
  * (project), with a Claude-Code-compatible stdin contract —
@@ -40,9 +38,8 @@
40
38
  * get no gate at all. Double registration is harmless: both gates evaluate
41
39
  * the same rules, and an allow/allow or block/block pair is idempotent.
42
40
  *
43
- * JSONC handling: ~/.continue/config.json allows `// comments`. We strip
44
- * single-line comments with a regex before parsing, then write back as
45
- * standard JSON with a managed-file header comment.
41
+ * The settings files are merged, never replaced: a file that is not a plain
42
+ * JSON object is left alone and reported (see jsonMergeTarget.ts).
46
43
  *
47
44
  * LLD #14 — Phase 3 cross-harness defence (Gap 3, WS-B)
48
45
  * HLD §3.14 — Three-Tier Defense Cascade (Tier 1 Native Gating)
@@ -53,28 +50,11 @@ import * as fs from 'node:fs/promises';
53
50
  import * as path from 'node:path';
54
51
  import * as os from 'node:os';
55
52
  import { createLogger } from '@intutic/logger';
53
+ import { keepOriginal } from '../disconnect/originals.js';
56
54
  import { newIso } from '@intutic/id';
57
55
  import { emitJsGate, emitJsFailClosedPrelude } from './gateBody.js';
56
+ import { readJsonObjectForMerge } from './jsonMergeTarget.js';
58
57
  const log = createLogger('sync-continue-hooks');
59
- /** Default path to the Continue.dev global config file. */
60
- const CONTINUE_CONFIG_PATH = path.join(os.homedir(), '.continue', 'config.json');
61
- /** Governance notice injected into customInstructions. */
62
- const GOVERNANCE_NOTICE = [
63
- 'This workspace is governed by Intutic SOP policies.',
64
- 'All LLM API requests are routed through the Intutic governance proxy.',
65
- 'Do not remove or override the apiBase field in this configuration.',
66
- ].join(' ');
67
- // ─── JSONC comment stripper ───────────────────────────────────────────────────
68
- /**
69
- * Strip single-line `// ...` comments from a JSONC string so it can be
70
- * parsed by JSON.parse. Block comments (`/* ... *\/`) are not stripped
71
- * because they are uncommon in Continue config files.
72
- */
73
- function stripJsoncComments(raw) {
74
- // Remove // comments that are not inside strings.
75
- // Simple approach: replace // up to end-of-line (acceptable for Continue config).
76
- return raw.replace(/\/\/[^\n]*/g, '');
77
- }
78
58
  // ─── PreToolUse gate (Continue CLI `cn`) ──────────────────────────────────────
79
59
  /**
80
60
  * The pre-tool gate script — receives the Continue CLI's Claude-Code-compatible
@@ -191,13 +171,9 @@ process.stdin.on('end', () => {
191
171
  * repeated syncs never stack duplicate entries.
192
172
  */
193
173
  async function mergeContinueSettings(settingsPath, hookScriptPath) {
194
- let existing = {};
195
- try {
196
- existing = JSON.parse(stripJsoncComments(await fs.readFile(settingsPath, 'utf-8')));
197
- }
198
- catch {
199
- // File doesn't exist or is malformed — start fresh
200
- }
174
+ const existing = await readJsonObjectForMerge(settingsPath);
175
+ if (existing === null)
176
+ return;
201
177
  const existingHooks = existing.hooks ?? {};
202
178
  const existingPreToolUse = Array.isArray(existingHooks.PreToolUse)
203
179
  ? existingHooks.PreToolUse
@@ -234,91 +210,19 @@ async function mergeContinueSettings(settingsPath, hookScriptPath) {
234
210
  }
235
211
  // ─── Main export ──────────────────────────────────────────────────────────────
236
212
  /**
237
- * Configure Continue: proxy routing for both surfaces, and the CLI's
238
- * PreToolUse gate.
213
+ * Write the Continue CLI gate script `.intutic/hooks/continue-check.js` and
214
+ * register it in `~/.continue/settings.json` and
215
+ * `<root>/.continue/settings.json` (see the module header for the
216
+ * `.claude/settings.json` overlap).
239
217
  *
240
- * 1. Reads `~/.continue/config.json` (JSONC — strips // comments before parsing).
241
- * 2. Sets `apiBase = proxyUrl` on each model that doesn't already use it.
242
- * 3. Adds a `customInstructions` governance notice.
243
- * 4. Writes the merged result back atomically.
244
- * 5. Writes `.intutic/env/continue.env` with the proxy URL.
245
- * 6. Writes the gate script `.intutic/hooks/continue-check.js` and registers
246
- * it in `~/.continue/settings.json` and `<root>/.continue/settings.json`
247
- * (Continue CLI PreToolUse — see the module header for the
248
- * `.claude/settings.json` overlap).
249
- *
250
- * Safe to call repeatedly — uses atomic rename (`.intutic-tmp` → final path).
218
+ * Safe to call repeatedly — uses atomic rename (`.intutic-tmp` → final path)
219
+ * and command-keyed de-duplication in both settings files.
251
220
  *
252
221
  * @param workspaceRoot - Absolute workspace root path.
253
- * @param proxyUrl - Intutic proxy URL to set as model apiBase.
254
- * @param workspaceId - Workspace ID stored in the env snippet.
222
+ * @param proxyUrl - Intutic proxy URL written into the gate header.
223
+ * @param workspaceId - Workspace ID embedded in every hook event payload.
255
224
  */
256
225
  export async function writeContinueHooks(workspaceRoot, proxyUrl, workspaceId = '') {
257
- // ── 1. Ensure directories ──────────────────────────────────────────────────
258
- const continueDir = path.join(os.homedir(), '.continue');
259
- const envDir = path.join(workspaceRoot, '.intutic', 'env');
260
- await Promise.all([
261
- fs.mkdir(continueDir, { recursive: true }),
262
- fs.mkdir(envDir, { recursive: true }),
263
- ]);
264
- // ── 2. Read and parse ~/.continue/config.json ─────────────────────────────
265
- let configObj = {};
266
- try {
267
- const raw = await fs.readFile(CONTINUE_CONFIG_PATH, 'utf-8');
268
- const stripped = stripJsoncComments(raw);
269
- configObj = JSON.parse(stripped);
270
- }
271
- catch {
272
- // File doesn't exist or is malformed — start with empty config
273
- log.warn({ action: 'continue_config_read_failed', path: CONTINUE_CONFIG_PATH }, 'Could not read ~/.continue/config.json — creating fresh');
274
- }
275
- // ── 3. Update model apiBase fields ────────────────────────────────────────
276
- const models = Array.isArray(configObj.models) ? configObj.models : [];
277
- let modifiedCount = 0;
278
- const updatedModels = models.map((model) => {
279
- if (typeof model !== 'object' || model === null)
280
- return model;
281
- const m = model;
282
- if (m.apiBase === proxyUrl)
283
- return m; // Already set — no-op
284
- modifiedCount++;
285
- return { ...m, apiBase: proxyUrl };
286
- });
287
- // ── 4. Build merged config ────────────────────────────────────────────────
288
- const mergedConfig = {
289
- ...configObj,
290
- // Re-attach updated models array (or keep original if empty)
291
- ...(models.length > 0 ? { models: updatedModels } : {}),
292
- // Add/overwrite customInstructions with governance notice
293
- customInstructions: GOVERNANCE_NOTICE,
294
- // Metadata fields
295
- _intutic_managed: true,
296
- _intutic_last_sync: newIso(),
297
- _intutic_workspace_id: workspaceId,
298
- };
299
- // ── 5. Write back atomically ──────────────────────────────────────────────
300
- // Prepend a managed-file warning as a comment (will be lost on next read —
301
- // that's acceptable; we re-add it on each sync).
302
- const managedHeader = `// AUTO-MANAGED by Intutic sync-daemon. Last sync: ${newIso()}\n// Do not edit apiBase fields — they are governance proxy routes.\n`;
303
- const serialized = managedHeader + JSON.stringify(mergedConfig, null, 2) + '\n';
304
- const tmpConfig = CONTINUE_CONFIG_PATH + '.intutic-tmp';
305
- await fs.writeFile(tmpConfig, serialized, 'utf-8');
306
- await fs.rename(tmpConfig, CONTINUE_CONFIG_PATH);
307
- log.info({ action: 'continue_config_written', path: CONTINUE_CONFIG_PATH, modifiedModels: modifiedCount }, 'Continue.dev config patched with Intutic proxy apiBase');
308
- // ── 6. Write .intutic/env/continue.env ────────────────────────────────────
309
- const envContent = [
310
- `# Intutic Continue.dev governance env — auto-generated ${newIso()}`,
311
- `# This file is managed by the Intutic sync-daemon. DO NOT EDIT.`,
312
- `CONTINUE_PROXY_URL=${proxyUrl}`,
313
- workspaceId ? `INTUTIC_WORKSPACE_ID=${workspaceId}` : '',
314
- `# All Continue.dev model requests are routed through CONTINUE_PROXY_URL.`,
315
- ].filter((l) => l !== '').join('\n') + '\n';
316
- const envFilePath = path.join(envDir, 'continue.env');
317
- const tmpEnv = envFilePath + '.intutic-tmp';
318
- await fs.writeFile(tmpEnv, envContent, 'utf-8');
319
- await fs.rename(tmpEnv, envFilePath);
320
- log.info({ action: 'continue_env_written', path: envFilePath }, 'Continue.dev env snippet written');
321
- // ── 7. PreToolUse gate for the Continue CLI (cn) ──────────────────────────
322
226
  const hookScriptDir = path.join(workspaceRoot, '.intutic', 'hooks');
323
227
  await fs.mkdir(hookScriptDir, { recursive: true });
324
228
  await fs.mkdir(path.join(workspaceRoot, '.intutic', 'events'), { recursive: true });
@@ -331,12 +235,14 @@ export async function writeContinueHooks(workspaceRoot, proxyUrl, workspaceId =
331
235
  await fs.chmod(hookScriptPath, 0o755);
332
236
  // Project-level: <root>/.continue/settings.json
333
237
  const projectContinueDir = path.join(workspaceRoot, '.continue');
238
+ await keepOriginal(path.join(projectContinueDir, 'settings.json'), workspaceRoot);
334
239
  await fs.mkdir(projectContinueDir, { recursive: true });
335
240
  await mergeContinueSettings(path.join(projectContinueDir, 'settings.json'), hookScriptPath);
336
- // User-level: ~/.continue/settings.json. homedir() read at call time (not the
337
- // module-scope constant above) so tests that move HOME before invoking are
338
- // honoured — see the gooseHooks note in generatedShellIntegrity.test.ts.
241
+ // User-level: ~/.continue/settings.json. homedir() read at call time so
242
+ // tests that move HOME before invoking are honoured — see the gooseHooks
243
+ // note in generatedShellIntegrity.test.ts.
339
244
  const userContinueDir = path.join(os.homedir(), '.continue');
245
+ await keepOriginal(path.join(userContinueDir, 'settings.json'), workspaceRoot);
340
246
  await fs.mkdir(userContinueDir, { recursive: true });
341
247
  await mergeContinueSettings(path.join(userContinueDir, 'settings.json'), hookScriptPath);
342
248
  log.info({ action: 'continue_gate_written', path: hookScriptPath }, 'Continue CLI PreToolUse gate written and registered (project + user settings.json)');