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