@intutic/sync-daemon 2.1.0 → 2.3.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 (288) hide show
  1. package/README.md +1 -1
  2. package/dist/agentReporter.d.ts +24 -11
  3. package/dist/agentReporter.d.ts.map +1 -1
  4. package/dist/agentReporter.js +20 -13
  5. package/dist/agentReporter.js.map +1 -1
  6. package/dist/configReader.d.ts +45 -25
  7. package/dist/configReader.d.ts.map +1 -1
  8. package/dist/configReader.js +70 -46
  9. package/dist/configReader.js.map +1 -1
  10. package/dist/configWriter.d.ts +32 -45
  11. package/dist/configWriter.d.ts.map +1 -1
  12. package/dist/configWriter.js +52 -686
  13. package/dist/configWriter.js.map +1 -1
  14. package/dist/disconnect/harnesses.d.ts +29 -0
  15. package/dist/disconnect/harnesses.d.ts.map +1 -0
  16. package/dist/disconnect/harnesses.js +805 -0
  17. package/dist/disconnect/harnesses.js.map +1 -0
  18. package/dist/disconnect/index.d.ts +25 -0
  19. package/dist/disconnect/index.d.ts.map +1 -0
  20. package/dist/disconnect/index.js +92 -0
  21. package/dist/disconnect/index.js.map +1 -0
  22. package/dist/disconnect/mcp.d.ts +81 -0
  23. package/dist/disconnect/mcp.d.ts.map +1 -0
  24. package/dist/disconnect/mcp.js +354 -0
  25. package/dist/disconnect/mcp.js.map +1 -0
  26. package/dist/disconnect/originals.d.ts +88 -0
  27. package/dist/disconnect/originals.d.ts.map +1 -0
  28. package/dist/disconnect/originals.js +257 -0
  29. package/dist/disconnect/originals.js.map +1 -0
  30. package/dist/disconnect/plan.d.ts +175 -0
  31. package/dist/disconnect/plan.d.ts.map +1 -0
  32. package/dist/disconnect/plan.js +580 -0
  33. package/dist/disconnect/plan.js.map +1 -0
  34. package/dist/disconnect/recognise.d.ts +43 -0
  35. package/dist/disconnect/recognise.d.ts.map +1 -0
  36. package/dist/disconnect/recognise.js +108 -0
  37. package/dist/disconnect/recognise.js.map +1 -0
  38. package/dist/disconnect/tomlLines.d.ts +39 -0
  39. package/dist/disconnect/tomlLines.d.ts.map +1 -0
  40. package/dist/disconnect/tomlLines.js +105 -0
  41. package/dist/disconnect/tomlLines.js.map +1 -0
  42. package/dist/harness/aiderConfigMerger.d.ts +29 -8
  43. package/dist/harness/aiderConfigMerger.d.ts.map +1 -1
  44. package/dist/harness/aiderConfigMerger.js +63 -12
  45. package/dist/harness/aiderConfigMerger.js.map +1 -1
  46. package/dist/harness/antigravityCliHooks.d.ts +61 -0
  47. package/dist/harness/antigravityCliHooks.d.ts.map +1 -0
  48. package/dist/harness/antigravityCliHooks.js +207 -0
  49. package/dist/harness/antigravityCliHooks.js.map +1 -0
  50. package/dist/harness/antigravityHooks.d.ts +12 -2
  51. package/dist/harness/antigravityHooks.d.ts.map +1 -1
  52. package/dist/harness/antigravityHooks.js +38 -16
  53. package/dist/harness/antigravityHooks.js.map +1 -1
  54. package/dist/harness/antigravityProducts.d.ts +40 -0
  55. package/dist/harness/antigravityProducts.d.ts.map +1 -0
  56. package/dist/harness/antigravityProducts.js +109 -0
  57. package/dist/harness/antigravityProducts.js.map +1 -0
  58. package/dist/harness/claudeAgentsMd.d.ts +29 -0
  59. package/dist/harness/claudeAgentsMd.d.ts.map +1 -0
  60. package/dist/harness/claudeAgentsMd.js +80 -0
  61. package/dist/harness/claudeAgentsMd.js.map +1 -0
  62. package/dist/harness/claudeCodeHooks.d.ts +6 -6
  63. package/dist/harness/claudeCodeHooks.d.ts.map +1 -1
  64. package/dist/harness/claudeCodeHooks.js +115 -28
  65. package/dist/harness/claudeCodeHooks.js.map +1 -1
  66. package/dist/harness/claudeProjectApproval.d.ts +53 -0
  67. package/dist/harness/claudeProjectApproval.d.ts.map +1 -0
  68. package/dist/harness/claudeProjectApproval.js +159 -0
  69. package/dist/harness/claudeProjectApproval.js.map +1 -0
  70. package/dist/harness/clineHooks.d.ts +0 -1
  71. package/dist/harness/clineHooks.d.ts.map +1 -1
  72. package/dist/harness/clineHooks.js +13 -4
  73. package/dist/harness/clineHooks.js.map +1 -1
  74. package/dist/harness/codexConfigMerger.d.ts.map +1 -1
  75. package/dist/harness/codexConfigMerger.js +4 -0
  76. package/dist/harness/codexConfigMerger.js.map +1 -1
  77. package/dist/harness/codexHooks.d.ts +2 -0
  78. package/dist/harness/codexHooks.d.ts.map +1 -1
  79. package/dist/harness/codexHooks.js +15 -4
  80. package/dist/harness/codexHooks.js.map +1 -1
  81. package/dist/harness/continueConfigMerger.d.ts.map +1 -1
  82. package/dist/harness/continueConfigMerger.js +4 -0
  83. package/dist/harness/continueConfigMerger.js.map +1 -1
  84. package/dist/harness/cursorHooks.d.ts +5 -2
  85. package/dist/harness/cursorHooks.d.ts.map +1 -1
  86. package/dist/harness/cursorHooks.js +12 -3
  87. package/dist/harness/cursorHooks.js.map +1 -1
  88. package/dist/harness/dshHooks.d.ts +5 -5
  89. package/dist/harness/dshHooks.d.ts.map +1 -1
  90. package/dist/harness/dshHooks.js +12 -5
  91. package/dist/harness/dshHooks.js.map +1 -1
  92. package/dist/harness/gateArtifacts.d.ts +35 -0
  93. package/dist/harness/gateArtifacts.d.ts.map +1 -0
  94. package/dist/harness/gateArtifacts.js +88 -0
  95. package/dist/harness/gateArtifacts.js.map +1 -0
  96. package/dist/harness/gateBody.d.ts +195 -69
  97. package/dist/harness/gateBody.d.ts.map +1 -1
  98. package/dist/harness/gateBody.js +847 -182
  99. package/dist/harness/gateBody.js.map +1 -1
  100. package/dist/harness/gateSightings.d.ts +47 -0
  101. package/dist/harness/gateSightings.d.ts.map +1 -0
  102. package/dist/harness/gateSightings.js +159 -0
  103. package/dist/harness/gateSightings.js.map +1 -0
  104. package/dist/harness/githubCopilotHooks.d.ts.map +1 -1
  105. package/dist/harness/githubCopilotHooks.js +11 -10
  106. package/dist/harness/githubCopilotHooks.js.map +1 -1
  107. package/dist/harness/gooseHardener.d.ts +0 -1
  108. package/dist/harness/gooseHardener.d.ts.map +1 -1
  109. package/dist/harness/gooseHardener.js +1 -1
  110. package/dist/harness/gooseHardener.js.map +1 -1
  111. package/dist/harness/gooseHooks.d.ts +29 -1
  112. package/dist/harness/gooseHooks.d.ts.map +1 -1
  113. package/dist/harness/gooseHooks.js +107 -69
  114. package/dist/harness/gooseHooks.js.map +1 -1
  115. package/dist/harness/grokHooks.d.ts +2 -2
  116. package/dist/harness/grokHooks.d.ts.map +1 -1
  117. package/dist/harness/grokHooks.js +20 -18
  118. package/dist/harness/grokHooks.js.map +1 -1
  119. package/dist/harness/hermesHooks.d.ts +18 -8
  120. package/dist/harness/hermesHooks.d.ts.map +1 -1
  121. package/dist/harness/hermesHooks.js +55 -19
  122. package/dist/harness/hermesHooks.js.map +1 -1
  123. package/dist/harness/holdRedaction.d.ts +4 -1
  124. package/dist/harness/holdRedaction.d.ts.map +1 -1
  125. package/dist/harness/holdRedaction.js +32 -8
  126. package/dist/harness/holdRedaction.js.map +1 -1
  127. package/dist/harness/jetbrainsXmlConfig.d.ts +20 -0
  128. package/dist/harness/jetbrainsXmlConfig.d.ts.map +1 -1
  129. package/dist/harness/jetbrainsXmlConfig.js +39 -18
  130. package/dist/harness/jetbrainsXmlConfig.js.map +1 -1
  131. package/dist/harness/mcpAutoWrite.d.ts +62 -25
  132. package/dist/harness/mcpAutoWrite.d.ts.map +1 -1
  133. package/dist/harness/mcpAutoWrite.js +406 -95
  134. package/dist/harness/mcpAutoWrite.js.map +1 -1
  135. package/dist/harness/museHooks.d.ts +3 -3
  136. package/dist/harness/museHooks.d.ts.map +1 -1
  137. package/dist/harness/museHooks.js +15 -7
  138. package/dist/harness/museHooks.js.map +1 -1
  139. package/dist/harness/n8nHooks.d.ts +0 -1
  140. package/dist/harness/n8nHooks.d.ts.map +1 -1
  141. package/dist/harness/n8nHooks.js +4 -1
  142. package/dist/harness/n8nHooks.js.map +1 -1
  143. package/dist/harness/openCodeHooks.d.ts +2 -2
  144. package/dist/harness/openCodeHooks.d.ts.map +1 -1
  145. package/dist/harness/openCodeHooks.js +9 -8
  146. package/dist/harness/openCodeHooks.js.map +1 -1
  147. package/dist/harness/openWebuiHooks.d.ts +0 -1
  148. package/dist/harness/openWebuiHooks.d.ts.map +1 -1
  149. package/dist/harness/openWebuiHooks.js +11 -3
  150. package/dist/harness/openWebuiHooks.js.map +1 -1
  151. package/dist/harness/openclawHooks.d.ts +75 -18
  152. package/dist/harness/openclawHooks.d.ts.map +1 -1
  153. package/dist/harness/openclawHooks.js +214 -160
  154. package/dist/harness/openclawHooks.js.map +1 -1
  155. package/dist/harness/openhandsHooks.d.ts +37 -11
  156. package/dist/harness/openhandsHooks.d.ts.map +1 -1
  157. package/dist/harness/openhandsHooks.js +148 -97
  158. package/dist/harness/openhandsHooks.js.map +1 -1
  159. package/dist/harness/piHooks.d.ts +52 -17
  160. package/dist/harness/piHooks.d.ts.map +1 -1
  161. package/dist/harness/piHooks.js +177 -163
  162. package/dist/harness/piHooks.js.map +1 -1
  163. package/dist/harness/protectedPaths.d.ts +107 -25
  164. package/dist/harness/protectedPaths.d.ts.map +1 -1
  165. package/dist/harness/protectedPaths.js +342 -48
  166. package/dist/harness/protectedPaths.js.map +1 -1
  167. package/dist/harness/rulesSection.d.ts +62 -0
  168. package/dist/harness/rulesSection.d.ts.map +1 -0
  169. package/dist/harness/rulesSection.js +145 -0
  170. package/dist/harness/rulesSection.js.map +1 -0
  171. package/dist/harness/windsurfHooks.d.ts +18 -3
  172. package/dist/harness/windsurfHooks.d.ts.map +1 -1
  173. package/dist/harness/windsurfHooks.js +41 -8
  174. package/dist/harness/windsurfHooks.js.map +1 -1
  175. package/dist/harness/windsurfJetBrainsProxy.d.ts +1 -1
  176. package/dist/harness/windsurfJetBrainsProxy.d.ts.map +1 -1
  177. package/dist/harness/windsurfJetBrainsProxy.js +5 -1
  178. package/dist/harness/windsurfJetBrainsProxy.js.map +1 -1
  179. package/dist/index.d.ts +21 -20
  180. package/dist/index.d.ts.map +1 -1
  181. package/dist/index.js +19 -19
  182. package/dist/index.js.map +1 -1
  183. package/dist/inventory.d.ts +53 -0
  184. package/dist/inventory.d.ts.map +1 -0
  185. package/dist/inventory.js +133 -0
  186. package/dist/inventory.js.map +1 -0
  187. package/dist/lib/complianceProbes.d.ts +2 -1
  188. package/dist/lib/complianceProbes.d.ts.map +1 -1
  189. package/dist/lib/complianceProbes.js +1 -1
  190. package/dist/lib/complianceProbes.js.map +1 -1
  191. package/dist/lib/decisionsDigest.d.ts +35 -17
  192. package/dist/lib/decisionsDigest.d.ts.map +1 -1
  193. package/dist/lib/decisionsDigest.js +106 -47
  194. package/dist/lib/decisionsDigest.js.map +1 -1
  195. package/dist/lib/egressPolicy.d.ts +2 -2
  196. package/dist/lib/egressPolicy.js +2 -2
  197. package/dist/lib/mcpRegistryPy.d.ts +11 -0
  198. package/dist/lib/mcpRegistryPy.d.ts.map +1 -0
  199. package/dist/lib/mcpRegistryPy.js +173 -0
  200. package/dist/lib/mcpRegistryPy.js.map +1 -0
  201. package/dist/lib/phrasesPy.d.ts +11 -0
  202. package/dist/lib/phrasesPy.d.ts.map +1 -0
  203. package/dist/lib/phrasesPy.js +351 -0
  204. package/dist/lib/phrasesPy.js.map +1 -0
  205. package/dist/lib/policySnapshot.d.ts +63 -9
  206. package/dist/lib/policySnapshot.d.ts.map +1 -1
  207. package/dist/lib/policySnapshot.js +269 -45
  208. package/dist/lib/policySnapshot.js.map +1 -1
  209. package/dist/lib/processPoller.d.ts.map +1 -1
  210. package/dist/lib/processPoller.js +9 -2
  211. package/dist/lib/processPoller.js.map +1 -1
  212. package/dist/lib/regexLinearity.d.ts +43 -0
  213. package/dist/lib/regexLinearity.d.ts.map +1 -0
  214. package/dist/lib/regexLinearity.js +275 -0
  215. package/dist/lib/regexLinearity.js.map +1 -0
  216. package/dist/lib/runtimeEnv.d.ts +1 -3
  217. package/dist/lib/runtimeEnv.d.ts.map +1 -1
  218. package/dist/lib/runtimeEnv.js +0 -2
  219. package/dist/lib/runtimeEnv.js.map +1 -1
  220. package/dist/lib/sequencePy.d.ts +10 -0
  221. package/dist/lib/sequencePy.d.ts.map +1 -0
  222. package/dist/lib/sequencePy.js +102 -0
  223. package/dist/lib/sequencePy.js.map +1 -0
  224. package/dist/sessionReporter.d.ts +18 -12
  225. package/dist/sessionReporter.d.ts.map +1 -1
  226. package/dist/sessionReporter.js +37 -24
  227. package/dist/sessionReporter.js.map +1 -1
  228. package/dist/skillWriter.d.ts +23 -5
  229. package/dist/skillWriter.d.ts.map +1 -1
  230. package/dist/skillWriter.js +159 -16
  231. package/dist/skillWriter.js.map +1 -1
  232. package/dist/syncCycle.d.ts +142 -0
  233. package/dist/syncCycle.d.ts.map +1 -0
  234. package/dist/syncCycle.js +327 -0
  235. package/dist/syncCycle.js.map +1 -0
  236. package/dist/trajectoryMonitor.d.ts +2 -4
  237. package/dist/trajectoryMonitor.d.ts.map +1 -1
  238. package/dist/trajectoryMonitor.js +2 -5
  239. package/dist/trajectoryMonitor.js.map +1 -1
  240. package/dist/watcher/driftWatcher.d.ts +0 -1
  241. package/dist/watcher/driftWatcher.d.ts.map +1 -1
  242. package/dist/watcher/driftWatcher.js +1 -2
  243. package/dist/watcher/driftWatcher.js.map +1 -1
  244. package/dist/watcher/settingsGuard.d.ts +36 -7
  245. package/dist/watcher/settingsGuard.d.ts.map +1 -1
  246. package/dist/watcher/settingsGuard.js +397 -60
  247. package/dist/watcher/settingsGuard.js.map +1 -1
  248. package/dist/watcher/vscodeHookSettings.d.ts +30 -0
  249. package/dist/watcher/vscodeHookSettings.d.ts.map +1 -0
  250. package/dist/watcher/vscodeHookSettings.js +170 -0
  251. package/dist/watcher/vscodeHookSettings.js.map +1 -0
  252. package/dist/wsClient.d.ts +0 -1
  253. package/dist/wsClient.d.ts.map +1 -1
  254. package/dist/wsClient.js +0 -1
  255. package/dist/wsClient.js.map +1 -1
  256. package/package.json +5 -5
  257. package/dist/harness/claudeDesktopHooks.d.ts +0 -30
  258. package/dist/harness/claudeDesktopHooks.d.ts.map +0 -1
  259. package/dist/harness/claudeDesktopHooks.js +0 -254
  260. package/dist/harness/claudeDesktopHooks.js.map +0 -1
  261. package/dist/harness/continueHooks.d.ts +0 -63
  262. package/dist/harness/continueHooks.d.ts.map +0 -1
  263. package/dist/harness/continueHooks.js +0 -247
  264. package/dist/harness/continueHooks.js.map +0 -1
  265. package/dist/harness/gateKind.d.ts +0 -164
  266. package/dist/harness/gateKind.d.ts.map +0 -1
  267. package/dist/harness/gateKind.js +0 -144
  268. package/dist/harness/gateKind.js.map +0 -1
  269. package/dist/harness/rooCodeHooks.d.ts +0 -31
  270. package/dist/harness/rooCodeHooks.d.ts.map +0 -1
  271. package/dist/harness/rooCodeHooks.js +0 -286
  272. package/dist/harness/rooCodeHooks.js.map +0 -1
  273. package/dist/hashReporter.d.ts +0 -33
  274. package/dist/hashReporter.d.ts.map +0 -1
  275. package/dist/hashReporter.js +0 -69
  276. package/dist/hashReporter.js.map +0 -1
  277. package/dist/integrityStore.d.ts +0 -35
  278. package/dist/integrityStore.d.ts.map +0 -1
  279. package/dist/integrityStore.js +0 -71
  280. package/dist/integrityStore.js.map +0 -1
  281. package/dist/lib/gitWorktrees.d.ts +0 -140
  282. package/dist/lib/gitWorktrees.d.ts.map +0 -1
  283. package/dist/lib/gitWorktrees.js +0 -191
  284. package/dist/lib/gitWorktrees.js.map +0 -1
  285. package/dist/syncLoop.d.ts +0 -172
  286. package/dist/syncLoop.d.ts.map +0 -1
  287. package/dist/syncLoop.js +0 -878
  288. package/dist/syncLoop.js.map +0 -1
@@ -0,0 +1,805 @@
1
+ /**
2
+ * harnesses.ts — the reverse of every file `intutic connect` writes, harness
3
+ * by harness.
4
+ *
5
+ * Each entry names the files one harness's writers touch (its rules file,
6
+ * its gate script and hook registrations, its proxy routing, its MCP
7
+ * config) and how each is undone. The writers themselves are the source:
8
+ * `tools/cli/src/harness/*.ts` (the connect adapters),
9
+ * `services/sync-daemon/src/harness/*Hooks.ts` and `mcpAutoWrite.ts`.
10
+ *
11
+ * Workspace files are undone in every workspace in the context; user-level
12
+ * files once. A file two harnesses share (`AGENTS.md`, `.env.intutic`) is
13
+ * undone only when none of the harnesses that write it stays connected.
14
+ *
15
+ * @module
16
+ */
17
+ import * as node_path from 'node:path';
18
+ import * as node_os from 'node:os';
19
+ import * as node_fs from 'node:fs/promises';
20
+ import { isDeepStrictEqual } from 'node:util';
21
+ import { harnessesReading } from '@intutic/shared-types';
22
+ import { isSeq, parseDocument } from 'yaml';
23
+ import { parse as parseToml } from 'smol-toml';
24
+ import { antigravityMcpConfigPath, claudeDesktopConfigPath, continueConfigPath, cursorGlobalConfigPath, geminiSettingsPath, gooseConfigPath, grokUserConfigPath, museConfigPath, openCodeGlobalConfigPath, windsurfConfigPath, } from '../harness/mcpAutoWrite.js';
25
+ import { jetbrainsConfigRoot } from '../harness/windsurfJetBrainsProxy.js';
26
+ import { windsurfSettingsPath } from '../harness/windsurfHooks.js';
27
+ import { ANTIGRAVITY_CLI_GATE, ANTIGRAVITY_HOOK_NAME, antigravityHooksPath } from '../harness/antigravityCliHooks.js';
28
+ import { DECISIONS_MARKERS, removeRulesSection, rulesSectionOf, RULES_SECTION_END } from '../harness/rulesSection.js';
29
+ import { DECISIONS_FILE_HEADER } from '../lib/decisionsDigest.js';
30
+ import { parseComponentOptions, serializeComponentOptions } from '../harness/jetbrainsXmlConfig.js';
31
+ import { resolveDshHome, listDshProfileDirs } from '../harness/dshHooks.js';
32
+ import { stripOwnHeader as stripAiderHeader, AIDER_DECISIONS_FILE, AIDER_SOPS_FILE, isAiderIntuticEntry } from '../harness/aiderConfigMerger.js';
33
+ import { OPENCLAW_PLUGIN_FILE, OPENCLAW_PLUGIN_ID, OPENCLAW_PLUGIN_MARKER, openclawAgentWorkspace, parseJson5Like, } from '../harness/openclawHooks.js';
34
+ import { PI_AGENT_DIR, PI_EXTENSION_FILE, PI_EXTENSION_MARKER } from '../harness/piHooks.js';
35
+ import { unharden } from '../harness/gooseHardener.js';
36
+ import { pruneLedger, readOriginal, sha256 } from './originals.js';
37
+ import { ENV_INTUTIC_WRITERS } from '../configWriter.js';
38
+ import { allEdits, deleteFile, deletePath, getPath, isObject, jsonFormat, orderLike, pruneEmpty, readText, removeFromArray, restoreKey, restoreMatchingValues, reverseOwnedFile, reverseStructuredFile, reverseTextFile, setPath, tomlFormat, yamlFormat, } from './plan.js';
39
+ import { antigravityRemoteShape, geminiRemoteShape, unwrapClaudeState, unwrapContinueServers, unwrapOpenCodeServers, unwrapServersAt, unwrapUnmarkedServersAt, } from './mcp.js';
40
+ import { contains, isGateCommandEntry, isGateEntry, runsGate, RULES_HEADER, startsWithRulesHeader } from './recognise.js';
41
+ import { lineValue, removeEmptyTable, removeTable, restoreKeyLine, tomlSections } from './tomlLines.js';
42
+ const home = () => node_os.homedir();
43
+ const join = node_path.join;
44
+ // ─── Shared shapes ───────────────────────────────────────────────────────────
45
+ /** Deletes a file in one of Intutic's own directories (`.intutic/`, `~/.intutic/`). */
46
+ async function intuticFile(plan, file) {
47
+ if (!plan.claim(file))
48
+ return;
49
+ try {
50
+ await node_fs.access(file);
51
+ }
52
+ catch {
53
+ return;
54
+ }
55
+ plan.change(file, 'delete (Intutic file)', () => deleteFile(file));
56
+ }
57
+ /** Deletes gate scripts from a workspace's (or the home) `.intutic/hooks/`. */
58
+ async function gateScripts(plan, root, names) {
59
+ for (const name of names)
60
+ await intuticFile(plan, join(root, '.intutic', 'hooks', name));
61
+ plan.removeIfEmpty(join(root, '.intutic', 'hooks'));
62
+ }
63
+ /** A rules file Intutic writes whole, with the generated header first. */
64
+ async function rulesFile(plan, file, workspaceRoot) {
65
+ await reverseOwnedFile(plan, file, workspaceRoot, startsWithRulesHeader);
66
+ }
67
+ /** A rules file of Intutic's own that may open with the product's front matter (`alwaysApply: true`, ...). */
68
+ async function ownRulesFile(plan, file, workspaceRoot) {
69
+ await reverseOwnedFile(plan, file, workspaceRoot, contains(RULES_HEADER));
70
+ }
71
+ /**
72
+ * An instructions file the user also writes (`CLAUDE.md`, `AGENTS.md`, ...):
73
+ * the marked rules section comes out, or, in a file an earlier version wrote
74
+ * whole, the whole file is undone.
75
+ */
76
+ async function instructionsFile(plan, file, workspaceRoot) {
77
+ const text = await readText(file);
78
+ if (text !== null && (rulesSectionOf(text) !== null || rulesSectionOf(text, DECISIONS_MARKERS) !== null))
79
+ await rulesSection(plan, file, workspaceRoot);
80
+ else
81
+ await rulesFile(plan, file, workspaceRoot);
82
+ }
83
+ async function json(plan, file, workspaceRoot, edit, deleteIfEmptyWithoutRecord = false) {
84
+ await reverseStructuredFile(plan, file, workspaceRoot, jsonFormat, edit, { deleteIfEmptyWithoutRecord });
85
+ }
86
+ /** `{ hooks: { <event>: [{ matcher, hooks: [{ command }] }] } }`, the Claude-Code-style shape. */
87
+ function removeGateEntries(doc, ctx, events, script) {
88
+ return allEdits(...events.map((event) => removeFromArray(doc, ['hooks', event], isGateEntry(script), ctx.original)));
89
+ }
90
+ /**
91
+ * A key Intutic adds when it is missing (a schema version, a format version):
92
+ * removed when the original lacked it, or when Intutic created the file, so a
93
+ * file connect created can end up empty and be deleted.
94
+ */
95
+ function removeAddedKey(doc, path, ctx) {
96
+ if (getPath(doc, path) === undefined)
97
+ return false;
98
+ const createdByIntutic = ctx.record?.existed === false;
99
+ if (!createdByIntutic && (ctx.original === null || getPath(ctx.original, path) !== undefined))
100
+ return false;
101
+ return deletePath(doc, path);
102
+ }
103
+ /** Keys Intutic stamps for provenance; never the user's. */
104
+ function removeOwnKeys(doc, keys) {
105
+ return allEdits(...keys.map((k) => deletePath(doc, [k])));
106
+ }
107
+ const startsWith = (prefix) => (v) => typeof v === 'string' && v.startsWith(prefix);
108
+ /** Runs `fn` only when none of `writers` stays connected. */
109
+ async function sharedBy(ctx, writers, fn) {
110
+ if (writers.some((h) => ctx.remaining.has(h)))
111
+ return;
112
+ await fn();
113
+ }
114
+ async function forEachWorkspace(ctx, fn) {
115
+ for (const root of ctx.workspaceRoots)
116
+ await fn(root);
117
+ }
118
+ // ─── Claude Code ─────────────────────────────────────────────────────────────
119
+ const CLAUDE_GATES = ['claude-code-check.js', 'pre-tool-check.js'];
120
+ function isClaudeGateEntry(entry) {
121
+ return CLAUDE_GATES.some((script) => isGateEntry(script)(entry));
122
+ }
123
+ /**
124
+ * Puts `permissions.deny` back. The writer records the rules it added
125
+ * (`intuticDeny`); a rule the user added since stays. In the workspace file
126
+ * the writer replaced the list outright, so the original rules come back; in
127
+ * the user file it only ever added, so an original rule missing now was
128
+ * removed by the user and stays removed.
129
+ */
130
+ function restoreDeny(doc, ctx, replaced) {
131
+ const deny = getPath(doc, ['permissions', 'deny']);
132
+ if (!Array.isArray(deny))
133
+ return false;
134
+ const recorded = ctx.record?.meta?.intuticDeny;
135
+ if (!Array.isArray(recorded)) {
136
+ if (deny.length > 0)
137
+ ctx.note('permissions.deny may hold rules an earlier Intutic version added without recording them; review it');
138
+ return false;
139
+ }
140
+ const added = new Set(recorded);
141
+ const originalValue = ctx.original ? getPath(ctx.original, ['permissions', 'deny']) : undefined;
142
+ const originalDeny = Array.isArray(originalValue) ? originalValue : [];
143
+ const kept = replaced ? originalDeny : originalDeny.filter((r) => deny.includes(r));
144
+ const next = [...kept, ...deny.filter((r) => !originalDeny.includes(r) && !added.has(r))];
145
+ if (isDeepStrictEqual(next, deny))
146
+ return false;
147
+ if (next.length === 0 && originalValue === undefined) {
148
+ deletePath(doc, ['permissions', 'deny']);
149
+ pruneEmpty(doc, ['permissions'], ctx.original);
150
+ }
151
+ else {
152
+ setPath(doc, ['permissions', 'deny'], next);
153
+ }
154
+ return true;
155
+ }
156
+ function claudeSettings(plan, file, workspaceRoot, replacedDeny) {
157
+ return json(plan, file, workspaceRoot, (doc, ctx) => {
158
+ const hooks = removeFromArray(doc, ['hooks', 'PreToolUse'], isClaudeGateEntry, ctx.original);
159
+ return allEdits(hooks, (hooks || ctx.record !== null) && restoreDeny(doc, ctx, replacedDeny));
160
+ });
161
+ }
162
+ const claudeCode = async (plan, ctx) => {
163
+ await forEachWorkspace(ctx, async (root) => {
164
+ await ownRulesFile(plan, join(root, '.claude', 'rules', 'intutic-governance.md'), root);
165
+ await decisionsFile(plan, join(root, '.claude', 'rules', 'intutic-decisions.md'), root);
166
+ // Where earlier versions wrote the rules whole and appended the decisions log.
167
+ await legacyClaudeMd(plan, join(root, 'CLAUDE.md'), root);
168
+ await claudeSettings(plan, join(root, '.claude', 'settings.json'), root, true);
169
+ await gateScripts(plan, root, ['claude-code-check.js']);
170
+ await sharedBy(ctx, ['claude-code', 'cursor'], () => gateScripts(plan, root, ['pre-tool-check.js']));
171
+ });
172
+ await claudeSettings(plan, join(home(), '.claude', 'settings.json'), home(), false);
173
+ // Claude Code's own state file: only the MCP maps are Intutic's, and it is never restored whole.
174
+ await json(plan, join(home(), '.claude.json'), home(), (doc) => unwrapClaudeState(doc));
175
+ };
176
+ // ─── Claude Desktop ──────────────────────────────────────────────────────────
177
+ const claudeDesktop = async (plan) => {
178
+ await json(plan, claudeDesktopConfigPath(), home(), (doc, c) => unwrapServersAt(doc, ['mcpServers'], c.original));
179
+ };
180
+ // ─── Cursor ──────────────────────────────────────────────────────────────────
181
+ const CURSOR_EVENTS = ['beforeShellExecution', 'beforeMCPExecution', 'preToolUse'];
182
+ function cursorHooks(plan, file, workspaceRoot) {
183
+ return json(plan, file, workspaceRoot, (doc, c) => {
184
+ const removed = allEdits(...CURSOR_EVENTS.map((e) => removeFromArray(doc, ['hooks', e], isGateCommandEntry('cursor-check.js'), c.original)));
185
+ if (!removed)
186
+ return false;
187
+ // The writer adds `version: 1` when the file has none.
188
+ if (c.original !== null)
189
+ removeAddedKey(doc, ['version'], c);
190
+ else if (Object.keys(doc).length === 1 && doc.version === 1)
191
+ delete doc.version;
192
+ return true;
193
+ }, true);
194
+ }
195
+ const cursor = async (plan, ctx) => {
196
+ await forEachWorkspace(ctx, async (root) => {
197
+ await ownRulesFile(plan, join(root, '.cursor', 'rules', 'intutic-governance.mdc'), root);
198
+ await decisionsFile(plan, join(root, '.cursor', 'rules', 'intutic-decisions.mdc'), root);
199
+ await rulesFile(plan, join(root, '.cursorrules'), root);
200
+ await gateScripts(plan, root, ['cursor-check.js']);
201
+ await cursorHooks(plan, join(root, '.cursor', 'hooks.json'), root);
202
+ await json(plan, join(root, '.cursor', 'mcp.json'), root, (doc, c) => unwrapServersAt(doc, ['mcpServers'], c.original), true);
203
+ await sharedBy(ctx, ['claude-code', 'cursor'], () => gateScripts(plan, root, ['pre-tool-check.js']));
204
+ });
205
+ await cursorHooks(plan, join(home(), '.cursor', 'hooks.json'), home());
206
+ await json(plan, cursorGlobalConfigPath(), home(), (doc, c) => unwrapServersAt(doc, ['mcpServers'], c.original));
207
+ };
208
+ // ─── Windsurf ────────────────────────────────────────────────────────────────
209
+ const CASCADE_EVENTS = ['pre_run_command', 'pre_write_code', 'pre_mcp_tool_use'];
210
+ function cascadeHooks(plan, file, workspaceRoot) {
211
+ return json(plan, file, workspaceRoot, (doc, c) => allEdits(...CASCADE_EVENTS.map((e) => removeFromArray(doc, ['hooks', e], isGateCommandEntry('windsurf-check.js'), c.original))), true);
212
+ }
213
+ /**
214
+ * Desktop's `settings.json`: the two proxy keys, and the TLS check the writer
215
+ * switched off with them. Both keys naming the same loopback listener with
216
+ * the check off is the writer's signature, recognised even when connect
217
+ * predates the recorded proxy URLs; the listener is then passed on, for the
218
+ * JetBrains settings it also pointed there. Both the settings file Windsurf
219
+ * reads and `~/.codeium/windsurf/settings.json`, where earlier versions wrote
220
+ * the keys.
221
+ */
222
+ async function windsurfProxySettings(plan, ctx) {
223
+ let listener = null;
224
+ for (const file of [windsurfSettingsPath(), join(home(), '.codeium', 'windsurf', 'settings.json')]) {
225
+ listener = (await windsurfProxySettingsFile(plan, ctx, file)) ?? listener;
226
+ }
227
+ return listener;
228
+ }
229
+ async function windsurfProxySettingsFile(plan, ctx, file) {
230
+ let listener = null;
231
+ await json(plan, file, home(), (doc, c) => {
232
+ const proxy = doc['http.proxy'];
233
+ const signature = typeof proxy === 'string' && /^http:\/\/127\.0\.0\.1:\d+$/.test(proxy) && doc['codeium.proxy'] === proxy && doc['http.proxyStrictSSL'] === false;
234
+ if (signature)
235
+ listener = proxy;
236
+ const isOurs = (v) => ctx.isProxyUrl(v) || (signature && v === proxy);
237
+ const proxied = allEdits(restoreKey(doc, ['http.proxy'], c, isOurs), restoreKey(doc, ['codeium.proxy'], c, isOurs));
238
+ if (!proxied)
239
+ return false;
240
+ restoreKey(doc, ['http.proxyStrictSSL'], c, (v) => v === false);
241
+ return true;
242
+ });
243
+ return listener;
244
+ }
245
+ /** A JetBrains `options/*.xml` settings file, as one component's options. */
246
+ function xmlComponentFormat(component) {
247
+ return {
248
+ parse(raw) {
249
+ const parsed = parseComponentOptions(raw, component);
250
+ if (parsed === null)
251
+ throw new Error('not a JetBrains settings file');
252
+ return parsed;
253
+ },
254
+ toJS: (file) => structuredClone(file),
255
+ write: (_file, next) => serializeComponentOptions(next, component),
256
+ };
257
+ }
258
+ const JETBRAINS_PROXY_OPTIONS = ['USE_HTTP_PROXY', 'USE_PROXY_PAC', 'PROXY_TYPE_IS_SOCKS', 'PROXY_HOST', 'PROXY_PORT'];
259
+ /**
260
+ * The JetBrains platform proxy the writer pointed at the Intutic proxy, and
261
+ * the Windsurf plugin's "Detect proxy" toggle it switched on, in every IDE
262
+ * where it did.
263
+ */
264
+ async function jetbrainsProxy(plan, ctx, listener) {
265
+ const root = jetbrainsConfigRoot();
266
+ if (!root)
267
+ return;
268
+ let products;
269
+ try {
270
+ products = await node_fs.readdir(root);
271
+ }
272
+ catch {
273
+ return;
274
+ }
275
+ for (const product of products) {
276
+ const options = join(root, product, 'options');
277
+ let reverted = false;
278
+ await reverseStructuredFile(plan, join(options, 'proxy.settings.xml'), home(), xmlComponentFormat('HttpConfigurable'), (doc, c) => {
279
+ const host = getPath(doc, ['options', 'PROXY_HOST']);
280
+ const port = getPath(doc, ['options', 'PROXY_PORT']);
281
+ const url = `http://${host}:${port}`;
282
+ if (typeof host !== 'string' || typeof port !== 'string' || !(ctx.isProxyUrl(url) || url === listener))
283
+ return false;
284
+ for (const key of JETBRAINS_PROXY_OPTIONS) {
285
+ const was = c.original ? getPath(c.original, ['options', key]) : undefined;
286
+ if (was === undefined)
287
+ deletePath(doc, ['options', key]);
288
+ else
289
+ setPath(doc, ['options', key], was);
290
+ }
291
+ if (c.original === null)
292
+ c.note('the IDE proxy was switched off rather than restored: an earlier Intutic version set it without keeping the previous setting');
293
+ reverted = true;
294
+ return true;
295
+ }, { deleteIfEmptyWithoutRecord: true });
296
+ if (!reverted)
297
+ continue;
298
+ await reverseStructuredFile(plan, join(options, 'CodeiumSettings.xml'), home(), xmlComponentFormat('com.codeium.intellij.settings.AppSettingsState'), (doc, c) => {
299
+ if (c.original === null || getPath(doc, ['options', 'detectProxy']) !== 'true')
300
+ return false;
301
+ const was = getPath(c.original, ['options', 'detectProxy']);
302
+ if (was === 'true')
303
+ return false;
304
+ if (was === undefined)
305
+ deletePath(doc, ['options', 'detectProxy']);
306
+ else
307
+ setPath(doc, ['options', 'detectProxy'], was);
308
+ return true;
309
+ });
310
+ }
311
+ }
312
+ const windsurf = async (plan, ctx) => {
313
+ await forEachWorkspace(ctx, async (root) => {
314
+ await ownRulesFile(plan, join(root, '.windsurf', 'rules', 'intutic-governance.md'), root);
315
+ await decisionsFile(plan, join(root, '.windsurf', 'rules', 'intutic-decisions.md'), root);
316
+ await rulesFile(plan, join(root, '.windsurfrules'), root);
317
+ await gateScripts(plan, root, ['windsurf-check.js']);
318
+ await cascadeHooks(plan, join(root, '.windsurf', 'hooks.json'), root);
319
+ });
320
+ await cascadeHooks(plan, join(home(), '.codeium', 'windsurf', 'hooks.json'), home());
321
+ await cascadeHooks(plan, join(home(), '.codeium', 'hooks.json'), home());
322
+ const listener = await windsurfProxySettings(plan, ctx);
323
+ await jetbrainsProxy(plan, ctx, listener);
324
+ await json(plan, windsurfConfigPath(), home(), (doc, c) => unwrapServersAt(doc, ['mcpServers'], c.original));
325
+ };
326
+ // ─── GitHub Copilot ──────────────────────────────────────────────────────────
327
+ const OWN_HOOK_FILE = contains('Intutic governance hook');
328
+ const githubCopilot = async (plan, ctx) => {
329
+ await forEachWorkspace(ctx, async (root) => {
330
+ await instructionsFile(plan, join(root, '.github', 'copilot-instructions.md'), root);
331
+ await gateScripts(plan, root, ['github-copilot-check.js']);
332
+ await reverseOwnedFile(plan, join(root, '.github', 'hooks', 'intutic-governance.json'), root, OWN_HOOK_FILE);
333
+ });
334
+ await reverseOwnedFile(plan, join(home(), '.copilot', 'hooks', 'intutic-governance.json'), home(), OWN_HOOK_FILE);
335
+ };
336
+ // ─── AGENTS.md readers ───────────────────────────────────────────────────────
337
+ /** Every harness whose rule sets go into the workspace's `AGENTS.md`. */
338
+ const AGENTS_MD_WRITERS = harnessesReading('AGENTS.md');
339
+ /** The `AGENTS.md` section, once none of the harnesses that read it stays connected. */
340
+ async function agentsMd(plan, ctx) {
341
+ await sharedBy(ctx, AGENTS_MD_WRITERS, () => forEachWorkspace(ctx, (root) => instructionsFile(plan, join(root, 'AGENTS.md'), root)));
342
+ }
343
+ const MUSE_EVENTS = ['PreToolUse', 'PermissionRequest'];
344
+ const muse = async (plan, ctx) => {
345
+ await agentsMd(plan, ctx);
346
+ await forEachWorkspace(ctx, async (root) => {
347
+ await gateScripts(plan, root, ['muse-check.js']);
348
+ await json(plan, join(root, '.muse', 'hooks.json'), root, (doc, c) => allEdits(removeGateEntries(doc, c, MUSE_EVENTS, 'muse-check.js'), restoreKey(doc, ['description'], c, startsWith('Intutic governance hooks'))), true);
349
+ });
350
+ const museDir = node_path.dirname(museConfigPath());
351
+ await reverseOwnedFile(plan, join(museDir, 'intutic-managed-hooks.json'), home(), contains('Intutic governance hooks'));
352
+ await json(plan, museConfigPath(), home(), (doc, c) => {
353
+ const changed = allEdits(unwrapServersAt(doc, ['mcp_servers'], c.original), restoreKey(doc, ['managed_hooks_path'], c, (v) => typeof v === 'string' && v.endsWith('intutic-managed-hooks.json')));
354
+ // Both writers add `schema_version` to a file that has none.
355
+ if (changed)
356
+ removeAddedKey(doc, ['schema_version'], c);
357
+ return changed;
358
+ });
359
+ };
360
+ function grokConfig(plan, file, workspaceRoot, ctx) {
361
+ return reverseStructuredFile(plan, file, workspaceRoot, tomlFormat, (doc, c) => {
362
+ const models = isObject(doc.model) ? Object.keys(doc.model) : [];
363
+ return allEdits(unwrapServersAt(doc, ['mcp_servers'], c.original), ...models.map((name) => restoreKey(doc, ['model', name, 'base_url'], c, ctx.isProxyUrl)));
364
+ });
365
+ }
366
+ const grok = async (plan, ctx) => {
367
+ await agentsMd(plan, ctx);
368
+ await forEachWorkspace(ctx, async (root) => {
369
+ await gateScripts(plan, root, ['grok-check.js']);
370
+ await reverseOwnedFile(plan, join(root, '.grok', 'hooks', 'intutic-governance.json'), root, OWN_HOOK_FILE);
371
+ await grokConfig(plan, join(root, '.grok', 'config.toml'), root, ctx);
372
+ });
373
+ await reverseOwnedFile(plan, join(home(), '.grok', 'hooks', 'intutic-governance.json'), home(), OWN_HOOK_FILE);
374
+ await grokConfig(plan, grokUserConfigPath(), home(), ctx);
375
+ };
376
+ const OWN_PLUGIN = contains('Intutic OpenCode governance plugin.');
377
+ const opencode = async (plan, ctx) => {
378
+ await agentsMd(plan, ctx);
379
+ await forEachWorkspace(ctx, async (root) => {
380
+ const plugins = join(root, '.opencode', 'plugins');
381
+ await reverseOwnedFile(plan, join(plugins, 'intutic-governance.js'), root, OWN_PLUGIN);
382
+ await reverseOwnedFile(plan, join(plugins, 'intutic-governance', 'index.js'), root, OWN_PLUGIN);
383
+ plan.removeIfEmpty(join(plugins, 'intutic-governance'));
384
+ await json(plan, join(root, 'opencode.json'), root, (doc, c) => unwrapOpenCodeServers(doc, c.original));
385
+ });
386
+ await json(plan, openCodeGlobalConfigPath(), home(), (doc, c) => unwrapOpenCodeServers(doc, c.original));
387
+ };
388
+ // ─── Roo Code, Cline ─────────────────────────────────────────────────────────
389
+ const rooCode = async (plan, ctx) => {
390
+ await agentsMd(plan, ctx);
391
+ await forEachWorkspace(ctx, (root) => rulesFile(plan, join(root, '.roorules'), root));
392
+ };
393
+ const cline = async (plan, ctx) => {
394
+ await forEachWorkspace(ctx, async (root) => {
395
+ await rulesFile(plan, join(root, '.clinerules', 'intutic-governance.md'), root);
396
+ await decisionsFile(plan, join(root, '.clinerules', 'intutic-decisions.md'), root);
397
+ await reverseOwnedFile(plan, join(root, '.clinerules', 'hooks', 'PreToolUse'), root, contains('Intutic Cline PreToolUse governance gate.'));
398
+ await json(plan, join(root, '.cline', 'mcp.json'), root, (doc, c) => unwrapServersAt(doc, ['mcpServers'], c.original), true);
399
+ });
400
+ };
401
+ // ─── Codex and the SDK adapters (.env.intutic) ───────────────────────────────
402
+ async function envIntutic(plan, ctx) {
403
+ await sharedBy(ctx, ENV_INTUTIC_WRITERS, () => forEachWorkspace(ctx, (root) => rulesFile(plan, join(root, '.env.intutic'), root)));
404
+ }
405
+ /** The comment `codexConfigMerger.ts` writes above the key it inserts. */
406
+ const CODEX_KEY_COMMENT = "# Set by Intutic: routes Codex's built-in OpenAI provider through the Intutic proxy.";
407
+ function codexHome() {
408
+ return process.env.CODEX_HOME || join(home(), '.codex');
409
+ }
410
+ function codexConfig(plan, ctx) {
411
+ return reverseTextFile(plan, join(codexHome(), 'config.toml'), home(), parseToml, (text, c) => {
412
+ const lines = text.split('\n');
413
+ if (lines[0] === CODEX_KEY_COMMENT && ctx.isProxyUrl(lineValue(lines[1] ?? '', 'openai_base_url'))) {
414
+ // Inserted at the top, with a blank line before the user's content.
415
+ lines.splice(0, lines[2] === '' ? 3 : 2);
416
+ return lines.join('\n');
417
+ }
418
+ const originalLines = c.originalText === null ? null : c.originalText.split('\n');
419
+ if (!restoreKeyLine(lines, null, 'openai_base_url', originalLines, ctx.isProxyUrl))
420
+ return null;
421
+ if (originalLines === null)
422
+ c.note('openai_base_url was removed rather than restored: an earlier Intutic version set it without keeping the value it replaced');
423
+ return lines.join('\n');
424
+ });
425
+ }
426
+ function codexHooks(plan, file, workspaceRoot) {
427
+ return json(plan, file, workspaceRoot, (doc, c) => allEdits(removeGateEntries(doc, c, ['PreToolUse'], 'codex-check.js'), restoreKey(doc, ['description'], c, startsWith('Intutic governance PreToolUse hook'))), true);
428
+ }
429
+ const codex = async (plan, ctx) => {
430
+ await agentsMd(plan, ctx);
431
+ await envIntutic(plan, ctx);
432
+ await forEachWorkspace(ctx, async (root) => {
433
+ await gateScripts(plan, root, ['codex-check.js']);
434
+ await codexHooks(plan, join(root, '.codex', 'hooks.json'), root);
435
+ });
436
+ await codexHooks(plan, join(codexHome(), 'hooks.json'), home());
437
+ await codexConfig(plan, ctx);
438
+ };
439
+ const sdkAdapter = (plan, ctx) => envIntutic(plan, ctx);
440
+ // ─── Aider ───────────────────────────────────────────────────────────────────
441
+ /** `.aider.conf.yml` without the header the merger writes above the user's content. */
442
+ const aiderFormat = {
443
+ parse(raw) {
444
+ const stripped = stripAiderHeader(raw);
445
+ return { doc: yamlFormat.parse(stripped), hadHeader: stripped !== raw };
446
+ },
447
+ toJS: (h) => yamlFormat.toJS(h.doc),
448
+ write: (h, next, raw, original) => yamlFormat.write(h.doc, next, raw, original?.doc ?? null),
449
+ };
450
+ /** Keys the merger removes from the user's file while it is connected; put back from the original. */
451
+ const AIDER_REMOVED_KEYS = ['test-cmd', 'lint-cmd', 'auto-test', 'auto-lint', 'test_cmd', 'lint_cmd'];
452
+ function removeFromList(doc, key, isOurs, ctx) {
453
+ const value = doc[key];
454
+ if (!Array.isArray(value))
455
+ return false;
456
+ const kept = value.filter((v) => !isOurs(v));
457
+ if (kept.length === value.length)
458
+ return false;
459
+ doc[key] = kept;
460
+ pruneEmpty(doc, [key], ctx.original);
461
+ return true;
462
+ }
463
+ function aiderConfig(plan, root, ctx) {
464
+ return reverseStructuredFile(plan, join(root, '.aider.conf.yml'), root, aiderFormat, (doc, c) => {
465
+ const hadHeader = c.handle.hadHeader;
466
+ const envIsOurs = (v) => typeof v === 'string' && v.startsWith('ANTHROPIC_BASE_URL=') && ctx.isProxyUrl(v.slice('ANTHROPIC_BASE_URL='.length));
467
+ const changed = allEdits(hadHeader, restoreKey(doc, ['openai-api-base'], c, ctx.isProxyUrl), removeFromList(doc, 'set-env', envIsOurs, c), removeFromList(doc, 'read', (v) => isAiderIntuticEntry(v, root), c));
468
+ if (!changed || c.original === null)
469
+ return changed;
470
+ // The merger dropped the user's own ANTHROPIC_BASE_URL entry and the
471
+ // auto-run keys, and turned a single `set-env`/`read` value into a list:
472
+ // all of it comes back.
473
+ const original = c.original;
474
+ const userEnv = asList(original['set-env']).filter((v) => typeof v === 'string' && v.startsWith('ANTHROPIC_BASE_URL='));
475
+ if (userEnv.length > 0)
476
+ doc['set-env'] = [...asList(doc['set-env']), ...userEnv];
477
+ for (const key of ['set-env', 'read']) {
478
+ const was = original[key];
479
+ if (was !== undefined && !Array.isArray(was) && isDeepStrictEqual(doc[key], [was]))
480
+ doc[key] = was;
481
+ }
482
+ for (const key of AIDER_REMOVED_KEYS) {
483
+ if (original[key] !== undefined && doc[key] === undefined)
484
+ doc[key] = structuredClone(original[key]);
485
+ }
486
+ orderLike(doc, original);
487
+ return true;
488
+ });
489
+ }
490
+ function asList(v) {
491
+ return Array.isArray(v) ? v : v === undefined || v === null ? [] : [v];
492
+ }
493
+ const aider = async (plan, ctx) => {
494
+ await forEachWorkspace(ctx, async (root) => {
495
+ await aiderConfig(plan, root, ctx);
496
+ await intuticFile(plan, join(root, AIDER_SOPS_FILE));
497
+ await decisionsFile(plan, join(root, AIDER_DECISIONS_FILE), root);
498
+ });
499
+ };
500
+ // ─── OpenHands ───────────────────────────────────────────────────────────────
501
+ /**
502
+ * An OpenHands `config.toml`: the `[intutic]` table earlier versions
503
+ * appended (it held the rules, which OpenHands never read), and every
504
+ * `base_url` the two writers pointed at the proxy (one of them replaced the
505
+ * first `base_url` line in the file, whichever table held it).
506
+ */
507
+ function openHandsConfig(plan, file, workspaceRoot, ctx) {
508
+ return reverseTextFile(plan, file, workspaceRoot, parseToml, (text, c) => {
509
+ const lines = text.split('\n');
510
+ const originalLines = c.originalText === null ? null : c.originalText.split('\n');
511
+ let changed = removeTable(lines, 'intutic');
512
+ const tables = new Set(tomlSections(lines).map((s) => s.name));
513
+ for (const table of tables)
514
+ changed = restoreKeyLine(lines, table, 'base_url', originalLines, ctx.isProxyUrl) || changed;
515
+ if (!changed)
516
+ return null;
517
+ const originalHadLlm = originalLines !== null && tomlSections(originalLines).some((s) => s.name === 'llm');
518
+ if (!originalHadLlm)
519
+ removeEmptyTable(lines, 'llm');
520
+ if (originalLines === null)
521
+ c.note('base_url was removed rather than restored where Intutic had set it: an earlier version kept no record of the value it replaced');
522
+ // The appended table ended the file; its removal must not take the final newline with it.
523
+ const out = lines.join('\n');
524
+ return text.endsWith('\n') && !out.endsWith('\n') ? out + '\n' : out;
525
+ });
526
+ }
527
+ const openhands = async (plan, ctx) => {
528
+ await forEachWorkspace(ctx, async (root) => {
529
+ await rulesFile(plan, join(root, '.openhands', 'microagents', 'intutic-governance.md'), root);
530
+ await decisionsFile(plan, join(root, '.openhands', 'microagents', 'intutic-decisions.md'), root);
531
+ await openHandsConfig(plan, join(root, 'config.toml'), root, ctx);
532
+ await gateScripts(plan, root, ['openhands-check.sh']);
533
+ await reverseOwnedFile(plan, join(root, '.openhands', 'hooks.json'), root, OWN_HOOK_FILE);
534
+ await json(plan, join(root, '.openhands', 'mcp.json'), root, (doc, c) => unwrapServersAt(doc, ['mcpServers'], c.original), true);
535
+ });
536
+ await openHandsConfig(plan, join(home(), '.openhands', 'config.toml'), home(), ctx);
537
+ };
538
+ // ─── Antigravity and Gemini CLI ──────────────────────────────────────────────
539
+ /** Antigravity's hooks file keys hooks by name; `intutic-governance` is Intutic's when it only runs the gate. */
540
+ function isAntigravityGate(value) {
541
+ return isObject(value) && Array.isArray(value.PreToolUse) && value.PreToolUse.length > 0 &&
542
+ value.PreToolUse.every(isGateEntry(ANTIGRAVITY_CLI_GATE));
543
+ }
544
+ /**
545
+ * A user's instructions file holding Intutic's marked rules section: the
546
+ * section goes, and the original bytes come back when nothing else changed.
547
+ * Trailing line breaks are compared loosely, since the section's removal
548
+ * cannot tell them apart; a file that had no final line break gets none back.
549
+ */
550
+ function rulesSection(plan, file, workspaceRoot) {
551
+ return reverseTextFile(plan, file, workspaceRoot, (text) => text.trimEnd(), (text, c) => {
552
+ const withoutRules = removeRulesSection(text) ?? text;
553
+ const next = removeRulesSection(withoutRules, DECISIONS_MARKERS) ?? withoutRules;
554
+ if (next === text)
555
+ return null;
556
+ if (c.originalText === null || c.originalText.endsWith('\n'))
557
+ return next;
558
+ const tail = text.trimEnd();
559
+ return tail.endsWith(RULES_SECTION_END) || tail.endsWith(DECISIONS_MARKERS.end) ? next.replace(/\r?\n$/, '') : next;
560
+ });
561
+ }
562
+ /**
563
+ * A `CLAUDE.md` earlier versions wrote to: the decisions-log section they
564
+ * appended comes out, and a file they wrote whole underneath it is given
565
+ * back too. Without the section, the whole-file case is {@link rulesFile}.
566
+ */
567
+ async function legacyClaudeMd(plan, file, workspaceRoot) {
568
+ const text = await readText(file);
569
+ if (text === null || rulesSectionOf(text, DECISIONS_MARKERS) === null)
570
+ return rulesFile(plan, file, workspaceRoot);
571
+ const record = await readOriginal(file, workspaceRoot);
572
+ await reverseTextFile(plan, file, workspaceRoot, (t) => t.trimEnd(), (t, c) => {
573
+ const next = removeRulesSection(t, DECISIONS_MARKERS);
574
+ if (next === null)
575
+ return null;
576
+ if (startsWithRulesHeader(next) && record?.writtenSha256 === sha256(next))
577
+ return c.originalText ?? '';
578
+ return next;
579
+ });
580
+ }
581
+ /** The decisions log's own file next to a harness's rules file. */
582
+ async function decisionsFile(plan, file, workspaceRoot) {
583
+ await reverseOwnedFile(plan, file, workspaceRoot, contains(DECISIONS_FILE_HEADER));
584
+ }
585
+ const antigravity = async (plan, ctx) => {
586
+ await forEachWorkspace(ctx, async (root) => {
587
+ await gateScripts(plan, root, ['antigravity-check.sh', ANTIGRAVITY_CLI_GATE]);
588
+ await rulesSection(plan, join(root, 'GEMINI.md'), root);
589
+ // Where earlier versions put the rules (a key neither product reads), and
590
+ // the project's Gemini CLI MCP servers.
591
+ await json(plan, join(root, '.gemini', 'settings.json'), root, (doc, c) => allEdits(restoreKey(doc, ['customInstructions'], c, (v) => typeof v === 'string' && startsWithRulesHeader(v)), unwrapUnmarkedServersAt(doc, ['mcpServers'], c.original, geminiRemoteShape)), true);
592
+ });
593
+ await json(plan, geminiSettingsPath(), home(), (doc, c) => allEdits(removeGateEntries(doc, c, ['BeforeTool'], 'antigravity-check.sh'), unwrapUnmarkedServersAt(doc, ['mcpServers'], c.original, geminiRemoteShape)));
594
+ await json(plan, antigravityHooksPath(), home(), (doc, c) => restoreKey(doc, [ANTIGRAVITY_HOOK_NAME], c, isAntigravityGate), true);
595
+ await json(plan, antigravityMcpConfigPath(), home(), (doc, c) => unwrapUnmarkedServersAt(doc, ['mcpServers'], c.original, antigravityRemoteShape), true);
596
+ };
597
+ // ─── Continue ────────────────────────────────────────────────────────────────
598
+ function continueSettings(plan, file, workspaceRoot) {
599
+ return json(plan, file, workspaceRoot, (doc, c) => allEdits(removeGateEntries(doc, c, ['PreToolUse'], 'continue-check.js'), removeOwnKeys(doc, ['_intutic_comment', '_intutic_last_sync'])), true);
600
+ }
601
+ const continueHarness = async (plan, ctx) => {
602
+ await forEachWorkspace(ctx, async (root) => {
603
+ await ownRulesFile(plan, join(root, '.continue', 'rules', 'intutic-governance.md'), root);
604
+ await decisionsFile(plan, join(root, '.continue', 'rules', 'intutic-decisions.md'), root);
605
+ await gateScripts(plan, root, ['continue-check.js']);
606
+ await continueSettings(plan, join(root, '.continue', 'settings.json'), root);
607
+ });
608
+ const dir = join(home(), '.continue');
609
+ await continueSettings(plan, join(dir, 'settings.json'), home());
610
+ await reverseStructuredFile(plan, join(dir, 'config.yaml'), home(), yamlFormat, (doc, c) => restoreMatchingValues(doc, c, ctx.isProxyUrl));
611
+ await json(plan, continueConfigPath(), home(), (doc, c) => unwrapContinueServers(doc, c.original));
612
+ };
613
+ // ─── Goose ───────────────────────────────────────────────────────────────────
614
+ async function goosePlugin(plan) {
615
+ const plugin = join(home(), '.agents', 'plugins', 'intutic-governance');
616
+ for (const file of [join(plugin, 'plugin.json'), join(plugin, 'hooks', 'hooks.json'), join(plugin, 'scripts', 'intutic-check.sh')]) {
617
+ // Hardened read-only and immutable while connected: the flag comes off first.
618
+ await reverseOwnedFile(plan, file, home(), contains('Intutic'), { beforeApply: () => unharden(file) });
619
+ plan.removeIfEmpty(node_path.dirname(file));
620
+ }
621
+ plan.removeIfEmpty(plugin);
622
+ }
623
+ const goose = async (plan, ctx) => {
624
+ await forEachWorkspace(ctx, (root) => rulesSection(plan, join(root, '.goosehints'), root));
625
+ await goosePlugin(plan);
626
+ await reverseStructuredFile(plan, gooseConfigPath(), home(), yamlFormat, (doc, c) => allEdits(unwrapServersAt(doc, ['mcp'], c.original), restoreMatchingValues(doc, c, ctx.isProxyUrl), restoreKey(doc, ['hooks', 'pre_tool_use'], c, (v) => typeof v === 'string' && v.endsWith(join('intutic-governance', 'scripts', 'intutic-check.sh')))));
627
+ };
628
+ // ─── Hermes, Pi, OpenClaw, n8n, Open WebUI ───────────────────────────────────
629
+ /** A per-harness env snippet in a workspace's `.intutic/env/`. */
630
+ async function envSnippet(plan, ctx, name) {
631
+ await forEachWorkspace(ctx, async (root) => {
632
+ await intuticFile(plan, join(root, '.intutic', 'env', name));
633
+ plan.removeIfEmpty(join(root, '.intutic', 'env'));
634
+ });
635
+ }
636
+ const hermes = async (plan, ctx) => {
637
+ await agentsMd(plan, ctx);
638
+ await gateScripts(plan, home(), ['hermes-check.sh']);
639
+ await reverseStructuredFile(plan, join(home(), '.hermes', 'config.yaml'), home(), yamlFormat, (doc, c) => allEdits(removeFromArray(doc, ['hooks', 'pre_tool_call'], (e) => isObject(e) && runsGate(e.command, 'hermes-check.sh'), c.original),
640
+ // Where earlier versions put it.
641
+ restoreKey(doc, ['hooks', 'preToolUse', 'command'], c, (v) => runsGate(v, 'hermes-check.sh'))));
642
+ const skill = join(home(), '.hermes', 'skills', 'intutic-governance');
643
+ await reverseOwnedFile(plan, join(skill, 'SKILL.md'), home(), contains('name: intutic-governance'));
644
+ plan.removeIfEmpty(skill);
645
+ await envSnippet(plan, ctx, 'hermes.env');
646
+ };
647
+ const pi = async (plan, ctx) => {
648
+ await agentsMd(plan, ctx);
649
+ await reverseOwnedFile(plan, join(home(), PI_EXTENSION_FILE), home(), contains(PI_EXTENSION_MARKER));
650
+ const piModels = (file) => json(plan, file, home(), (doc, c) => allEdits(restoreKey(doc, ['providers', 'anthropic', 'baseUrl'], c, ctx.isProxyUrl), restoreKey(doc, ['providers', 'openai', 'baseUrl'], c, ctx.isProxyUrl)), true);
651
+ await piModels(join(home(), PI_AGENT_DIR, 'models.json'));
652
+ // Where earlier versions wrote, and Pi never read: a PreToolUse hook in
653
+ // ~/.pi/hooks.json and the routing in ~/.pi/models.json.
654
+ await gateScripts(plan, home(), ['pi-check.sh']);
655
+ await json(plan, join(home(), '.pi', 'hooks.json'), home(), (doc, c) => removeGateEntries(doc, c, ['PreToolUse'], 'pi-check.sh'), true);
656
+ await piModels(join(home(), '.pi', 'models.json'));
657
+ await envSnippet(plan, ctx, 'pi.env');
658
+ };
659
+ /** OpenClaw's config is JSON5-like; the writer re-serialises it as JSON, and so does this. */
660
+ const json5LikeFormat = { ...jsonFormat, parse: (raw) => (raw.trim() === '' ? {} : parseJson5Like(raw)) };
661
+ /** The plugin's `plugins.load.paths` entry, and its id in `plugins.allow` unless the user had listed it before connect. */
662
+ function removeOpenclawPlugin(doc, ctx) {
663
+ const originalAllow = ctx.original ? getPath(ctx.original, ['plugins', 'allow']) : undefined;
664
+ const allowedBefore = Array.isArray(originalAllow) && originalAllow.includes(OPENCLAW_PLUGIN_ID);
665
+ return allEdits(removeFromArray(doc, ['plugins', 'load', 'paths'], (v) => typeof v === 'string' && v.endsWith(OPENCLAW_PLUGIN_FILE), ctx.original), !allowedBefore && removeFromArray(doc, ['plugins', 'allow'], (v) => v === OPENCLAW_PLUGIN_ID, ctx.original));
666
+ }
667
+ /** Where earlier versions registered the gate: an internal hook entry, which never sees a tool call. */
668
+ function removeLegacyOpenclawHook(doc, ctx) {
669
+ const path = ['hooks', 'internal', 'entries', 'intutic-governance'];
670
+ const entry = getPath(doc, path);
671
+ if (!isObject(entry) || !runsGate(entry.command, 'openclaw-check.js'))
672
+ return false;
673
+ deletePath(doc, path);
674
+ pruneEmpty(doc, path.slice(0, -1), ctx.original);
675
+ return true;
676
+ }
677
+ const openclaw = async (plan, ctx) => {
678
+ // The rules section of its agent workspace's AGENTS.md.
679
+ await rulesSection(plan, join(await openclawAgentWorkspace(), 'AGENTS.md'), home());
680
+ await reverseOwnedFile(plan, join(home(), OPENCLAW_PLUGIN_FILE), home(), contains(OPENCLAW_PLUGIN_MARKER));
681
+ await gateScripts(plan, home(), ['openclaw-check.js']);
682
+ await reverseStructuredFile(plan, join(home(), '.openclaw', 'openclaw.json'), home(), json5LikeFormat, (doc, c) => allEdits(removeOpenclawPlugin(doc, c), removeLegacyOpenclawHook(doc, c)), { deleteIfEmptyWithoutRecord: true });
683
+ await envSnippet(plan, ctx, 'openclaw.env');
684
+ };
685
+ /** n8n's local files. The workflow variables connect sets over n8n's API are undone by the CLI. */
686
+ const n8n = async (plan, ctx) => {
687
+ await gateScripts(plan, home(), ['n8n-governance-hook.js']);
688
+ for (const name of ['governance-workflow.json', 'INSTALL.md'])
689
+ await intuticFile(plan, join(home(), '.intutic', 'n8n', name));
690
+ plan.removeIfEmpty(join(home(), '.intutic', 'n8n'));
691
+ await envSnippet(plan, ctx, 'n8n.env');
692
+ };
693
+ const openWebui = async (plan, ctx) => {
694
+ await reverseOwnedFile(plan, join(home(), '.open-webui', 'intutic-governance-filter.py'), home(), contains('Intutic'));
695
+ await envSnippet(plan, ctx, 'open-webui.env');
696
+ };
697
+ // ─── dsh ─────────────────────────────────────────────────────────────────────
698
+ const DSH_ROW_ID = 'intutic-governance';
699
+ const DSH_LLM_ENTRY_ID = 'llm-deepseek';
700
+ /** A dsh `cordis.patch.yml`, whose root is a list of patch rows. */
701
+ const dshPatchFormat = {
702
+ parse(raw) {
703
+ const doc = parseDocument(raw.trim() === '' ? '[]' : raw);
704
+ if (doc.errors.length > 0)
705
+ throw doc.errors[0];
706
+ if (doc.contents !== null && !isSeq(doc.contents))
707
+ throw new Error('not a patch list');
708
+ return doc;
709
+ },
710
+ toJS: (doc) => ({ rows: doc.toJS() ?? [] }),
711
+ write: (doc) => doc.toString(),
712
+ };
713
+ /** Whether a patch row is the plugin row the writer inserts (alone, or inside an `insert:` block it wrote). */
714
+ function isDshPluginRow(row) {
715
+ if (!isObject(row))
716
+ return false;
717
+ if (row.id === DSH_ROW_ID)
718
+ return true;
719
+ return Array.isArray(row.insert) && row.insert.length === 1 && isObject(row.insert[0]) && row.insert[0].id === DSH_ROW_ID;
720
+ }
721
+ function dshPatch(plan, file, dshHome, ctx) {
722
+ return reverseStructuredFile(plan, file, dshHome, dshPatchFormat, (doc, c) => {
723
+ const yaml = c.handle;
724
+ const rows = doc.rows;
725
+ const originalRows = c.original ? c.original.rows : null;
726
+ let changed = false;
727
+ // Last first, so the indexes of the rows still to visit stay put.
728
+ for (let i = rows.length - 1; i >= 0; i--) {
729
+ const row = rows[i];
730
+ if (isDshPluginRow(row)) {
731
+ yaml.deleteIn([i]);
732
+ changed = true;
733
+ continue;
734
+ }
735
+ if (!isObject(row) || row.id !== DSH_LLM_ENTRY_ID || 'insert' in row)
736
+ continue;
737
+ const baseUrl = getPath(row, ['config', 'baseURL']);
738
+ if (!ctx.isProxyUrl(baseUrl))
739
+ continue;
740
+ const originalRow = originalRows?.find((r) => isObject(r) && r.id === DSH_LLM_ENTRY_ID && !('insert' in r));
741
+ const was = originalRow ? getPath(originalRow, ['config', 'baseURL']) : undefined;
742
+ const onlyBaseUrl = isObject(row.config) && Object.keys(row.config).length === 1 && Object.keys(row).length === 2;
743
+ if (was !== undefined)
744
+ yaml.setIn([i, 'config', 'baseURL'], was);
745
+ else if (onlyBaseUrl && originalRow === undefined)
746
+ yaml.deleteIn([i]);
747
+ else
748
+ yaml.deleteIn([i, 'config', 'baseURL']);
749
+ if (originalRows === null)
750
+ c.note('the llm-deepseek baseURL was removed rather than restored: an earlier Intutic version kept no record of the value it replaced');
751
+ changed = true;
752
+ }
753
+ doc.rows = yaml.toJS() ?? [];
754
+ return changed;
755
+ });
756
+ }
757
+ const DSH_GATE_RANGE = '^2.0.0';
758
+ const dsh = async (plan, ctx) => {
759
+ await agentsMd(plan, ctx);
760
+ // dsh's files live under $DSH_HOME, and their records with them (see dshHooks.ts).
761
+ const dshHome = resolveDshHome();
762
+ for (const profile of await listDshProfileDirs(dshHome)) {
763
+ await dshPatch(plan, join(profile, 'cordis.patch.yml'), dshHome, ctx);
764
+ await json(plan, join(profile, 'package.json'), dshHome, (doc, c) => {
765
+ // Added only when the profile declared none; with no record, it may be the user's own `dsh plugin add`.
766
+ if (c.original === null || getPath(doc, ['dependencies', '@intutic/gate']) !== DSH_GATE_RANGE)
767
+ return false;
768
+ if (getPath(c.original, ['dependencies', '@intutic/gate']) !== undefined)
769
+ return false;
770
+ deletePath(doc, ['dependencies', '@intutic/gate']);
771
+ pruneEmpty(doc, ['dependencies'], c.original);
772
+ return true;
773
+ });
774
+ }
775
+ await reverseOwnedFile(plan, join(dshHome, 'INSTALL.md'), dshHome, startsWith('# Intutic governance for dsh'));
776
+ plan.quiet(join(dshHome, '.intutic', 'originals'), () => pruneLedger(dshHome));
777
+ };
778
+ // ─── Registry ────────────────────────────────────────────────────────────────
779
+ /** The reverser for each harness id; harnesses connect writes nothing for are absent. */
780
+ export const HARNESS_REVERSERS = {
781
+ 'claude-code': claudeCode,
782
+ 'claude-desktop': claudeDesktop,
783
+ cursor,
784
+ windsurf,
785
+ 'github-copilot': githubCopilot,
786
+ 'muse-code': muse,
787
+ grok,
788
+ opencode,
789
+ 'roo-code': rooCode,
790
+ cline,
791
+ codex,
792
+ aider,
793
+ openhands,
794
+ antigravity,
795
+ continue: continueHarness,
796
+ goose,
797
+ hermes,
798
+ pi,
799
+ openclaw,
800
+ n8n,
801
+ 'open-webui': openWebui,
802
+ dsh,
803
+ ...Object.fromEntries(ENV_INTUTIC_WRITERS.filter((h) => h !== 'codex').map((h) => [h, sdkAdapter])),
804
+ };
805
+ //# sourceMappingURL=harnesses.js.map