@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
@@ -4,7 +4,10 @@
4
4
  *
5
5
  * Injects and proxy-wraps MCP server entries in:
6
6
  * - Claude Code: ~/.claude.json (user-scope `mcpServers`, and the
7
- * local-scope `projects[<workspaceRoot>].mcpServers`)
7
+ * local-scope `projects[<workspaceRoot>].mcpServers`,
8
+ * where the approved servers of the project's
9
+ * `.mcp.json` also get wrapped shadows — the shared
10
+ * `.mcp.json` itself is only read)
8
11
  * - Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json
9
12
  * - Cursor (global): ~/Library/Application Support/Cursor/User/globalSettings.json
10
13
  * - Cursor (project): <workspaceRoot>/.cursor/mcp.json
@@ -17,13 +20,18 @@
17
20
  * - Grok Build: ~/.grok/config.toml + <workspaceRoot>/.grok/config.toml
18
21
  * ([mcp_servers.*] tables)
19
22
  * - OpenCode: ~/.config/opencode/opencode.json + <workspaceRoot>/opencode.json
20
- * (`mcp` block — TD-487; see `injectOpenCode`)
23
+ * (`mcp` block; see `injectOpenCode`)
24
+ * - Gemini CLI: ~/.gemini/settings.json + <workspaceRoot>/.gemini/settings.json
25
+ * (`mcpServers`)
26
+ * - Antigravity: ~/.gemini/config/mcp_config.json (`mcpServers`)
27
+ * (both under the `antigravity` harness; see `injectAntigravity`)
21
28
  *
22
- * That is 14 config paths across 11 `HarnessType` values (Cursor, Grok
29
+ * That is 17 config paths across 12 `HarnessType` values (Cursor, Grok
23
30
  * Build and OpenCode each own two paths — global/project for Cursor and
24
- * OpenCode, user/project for Grok Build). `discoverMcpServers` below reads
25
- * all fourteen read-only, for reporting; the injectors above are the only
26
- * thing that writes.
31
+ * OpenCode, user/project for Grok Build — and the `antigravity` harness
32
+ * three, for its two products). `discoverMcpServers` below reads all
33
+ * seventeen read-only, for reporting, plus Claude Code's project
34
+ * `.mcp.json`; the injectors above are the only thing that writes.
27
35
  *
28
36
  * # Grok Build's compat-path overlap — dedup, not a bug
29
37
  *
@@ -56,8 +64,9 @@
56
64
  * `command`-or-`url` shape every other JSON-map harness here uses — `wrapAllServers`
57
65
  * / `wrapWithProxy` branch on exactly that, so a `streamable_http` entry is
58
66
  * wrapped through the SAME remote-bridge path a `url`-keyed Claude Desktop/
59
- * Windsurf entry gets, without new code. See TD-362 for why this is recorded
60
- * as an assumption rather than a confirmation.
67
+ * Windsurf entry gets, without new code. This is recorded as an assumption
68
+ * rather than a confirmation because the `muse` binary could not be installed
69
+ * to check it (see museHooks.ts).
61
70
  *
62
71
  * Proxy-wrapping convention:
63
72
  * Each existing stdio MCP server entry is rewritten so that the governance
@@ -65,25 +74,22 @@
65
74
  * after `--`, tagged with `--server-name <name>` so the proxy can attribute
66
75
  * traffic to the server it fronts. A `__intutic_wrapped: true` flag is added
67
76
  * to prevent double-wrapping. Remote (HTTP/SSE, `url`-keyed) entries are now
68
- * ALSO wrapped — TD-354's stdio→HTTP bridge phase closed the gap this
77
+ * ALSO wrapped — the stdio→HTTP bridge phase closed the gap this
69
78
  * comment used to describe as "left untouched": the entry is rewritten to
70
79
  * spawn the SAME proxy binary in bridge mode (`--remote-url`/
71
80
  * `--remote-transport`, headers riding in `INTUTIC_REMOTE_HEADERS` env, not
72
- * argv — `ps` visibility). `__intutic_original` preserves the entry's
73
- * pre-wrap `url`/`type`/`headers` so `classifyEntry` can report the TRUE
74
- * transport of what got wrapped, and so a wrapped entry could be unwrapped
75
- * later. See TECH_DEBT.md TD-354 for the historical decline this phase
76
- * supersedes.
81
+ * argv — `ps` visibility). `__intutic_original` keeps the whole pre-wrap
82
+ * entry, stdio or remote, so `classifyEntry` can report the TRUE transport
83
+ * of what got wrapped and `intutic disconnect` can put the entry back
84
+ * exactly.
77
85
  *
78
- * Continuous invariant, not one-shot: `injectMcpServer` is called once from
79
- * `intutic connect` (tools/cli) AND once per sync-loop iteration
80
- * (services/sync-daemon/src/syncLoop.ts), so a server a user adds to a harness
86
+ * Continuous invariant, not one-shot: `intutic connect` (tools/cli) calls
87
+ * `injectMcpServer` on every sync cycle, so a server a user adds to a harness
81
88
  * config after their first `connect` still gets wrapped on the next sync
82
89
  * cycle rather than staying invisible to governance forever. Running this
83
90
  * every ~30s only works because `writeJsonFile` below is write-if-changed —
84
91
  * an already-wrapped, unchanged config produces zero bytes written.
85
92
  *
86
- * LLD #14 — mcpAutoWrite.ts
87
93
  * HLD §3.14 — GUI Harness Interception (MCP registration + Universal MCP Governance)
88
94
  *
89
95
  * @module
@@ -96,8 +102,12 @@ import { createRequire } from 'node:module';
96
102
  import { isDeepStrictEqual } from 'node:util';
97
103
  import { createLogger } from '@intutic/logger';
98
104
  import { readJsonObjectForMerge } from './jsonMergeTarget.js';
105
+ import { projectServerApproval } from './claudeProjectApproval.js';
106
+ import { keepOriginal } from '../disconnect/originals.js';
99
107
  import { parseDocument, isMap } from 'yaml';
108
+ import { GEMINI_CLI_GATE_ID, sanitizeMcpEndpoint } from '@intutic/shared-types';
100
109
  import { parse as parseToml, stringify as stringifyToml } from 'smol-toml';
110
+ import { antigravityGateIdentities } from './antigravityProducts.js';
101
111
  const log = createLogger('sync-mcp-autowrite');
102
112
  // ─── Proxy Binary Resolution ─────────────────────────────────────────────────
103
113
  /** Resolves packages relative to this module — i.e. from wherever the
@@ -139,13 +149,14 @@ export function resolveProxyBin(workspaceRoot) {
139
149
  // Shared by both the injectors (which write) and discoverMcpServers (which
140
150
  // only reads), so the two can never independently drift about where a
141
151
  // harness keeps its config.
142
- /** Claude Code keeps user- and local-scope MCP servers in `~/.claude.json`
143
- * (project-scope servers live in the repo's `.mcp.json`, which is shared
144
- * with the team and is not rewritten here). */
152
+ /** Claude Code keeps user- and local-scope MCP servers in `~/.claude.json`.
153
+ * Project-scope servers live in the repo's `.mcp.json`, which is shared with
154
+ * the team and never rewritten here; they are governed through local-scope
155
+ * shadows instead (see {@link shadowProjectServers}). */
145
156
  function claudeCodeConfigPath() {
146
157
  return node_path.join(node_os.homedir(), '.claude.json');
147
158
  }
148
- function claudeDesktopConfigPath() {
159
+ export function claudeDesktopConfigPath() {
149
160
  const home = node_os.homedir();
150
161
  if (process.platform === 'darwin') {
151
162
  return node_path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
@@ -155,7 +166,7 @@ function claudeDesktopConfigPath() {
155
166
  }
156
167
  return node_path.join(home, '.config', 'Claude', 'claude_desktop_config.json');
157
168
  }
158
- function cursorGlobalConfigPath() {
169
+ export function cursorGlobalConfigPath() {
159
170
  const home = node_os.homedir();
160
171
  if (process.platform === 'darwin') {
161
172
  return node_path.join(home, 'Library', 'Application Support', 'Cursor', 'User', 'globalSettings.json');
@@ -171,22 +182,22 @@ function cursorProjectConfigPath(workspaceRoot) {
171
182
  function clineConfigPath(workspaceRoot) {
172
183
  return node_path.join(workspaceRoot, '.cline', 'mcp.json');
173
184
  }
174
- function windsurfConfigPath() {
185
+ export function windsurfConfigPath() {
175
186
  return node_path.join(node_os.homedir(), '.codeium', 'windsurf', 'mcp_config.json');
176
187
  }
177
- function continueConfigPath() {
188
+ export function continueConfigPath() {
178
189
  return node_path.join(node_os.homedir(), '.continue', 'config.json');
179
190
  }
180
- function gooseConfigPath() {
191
+ export function gooseConfigPath() {
181
192
  return node_path.join(node_os.homedir(), '.config', 'goose', 'config.yaml');
182
193
  }
183
194
  function openHandsConfigPath(workspaceRoot) {
184
195
  return node_path.join(workspaceRoot, '.openhands', 'mcp.json');
185
196
  }
186
- function museConfigPath() {
197
+ export function museConfigPath() {
187
198
  return node_path.join(node_os.homedir(), '.config', 'muse', 'settings.json');
188
199
  }
189
- function grokUserConfigPath() {
200
+ export function grokUserConfigPath() {
190
201
  return node_path.join(node_os.homedir(), '.grok', 'config.toml');
191
202
  }
192
203
  function grokProjectConfigPath(workspaceRoot) {
@@ -195,13 +206,24 @@ function grokProjectConfigPath(workspaceRoot) {
195
206
  /** OpenCode's global config: `$XDG_CONFIG_HOME/opencode/opencode.json`, which
196
207
  * is `~/.config/opencode/opencode.json` unless XDG_CONFIG_HOME is set (OpenCode
197
208
  * resolves it through xdg-basedir). Only the `.json` spelling — see `injectOpenCode`. */
198
- function openCodeGlobalConfigPath() {
209
+ export function openCodeGlobalConfigPath() {
199
210
  const base = process.env['XDG_CONFIG_HOME'] || node_path.join(node_os.homedir(), '.config');
200
211
  return node_path.join(base, 'opencode', 'opencode.json');
201
212
  }
202
213
  function openCodeProjectConfigPath(workspaceRoot) {
203
214
  return node_path.join(workspaceRoot, 'opencode.json');
204
215
  }
216
+ /** Gemini CLI's user settings, which also hold its `BeforeTool` gate (antigravityHooks.ts). */
217
+ export function geminiSettingsPath() {
218
+ return node_path.join(node_os.homedir(), '.gemini', 'settings.json');
219
+ }
220
+ function geminiProjectSettingsPath(workspaceRoot) {
221
+ return node_path.join(workspaceRoot, '.gemini', 'settings.json');
222
+ }
223
+ /** Google Antigravity's global MCP servers, next to its `hooks.json` (antigravityCliHooks.ts). */
224
+ export function antigravityMcpConfigPath() {
225
+ return node_path.join(node_os.homedir(), '.gemini', 'config', 'mcp_config.json');
226
+ }
205
227
  // ─── Proxy Wrapping ───────────────────────────────────────────────────────────
206
228
  /**
207
229
  * Wrap a single MCP server entry with the governance proxy.
@@ -213,7 +235,7 @@ function openCodeProjectConfigPath(workspaceRoot) {
213
235
  * Two wrap conventions, chosen by the entry's pre-wrap shape:
214
236
  * - stdio (`command`/`args`): the proxy binary becomes the command, the
215
237
  * original command is passed after `--`, tagged `--server-name`.
216
- * - remote (`url`, HTTP/SSE — TD-354, now superseded): the proxy binary is
238
+ * - remote (`url`, HTTP/SSE): the proxy binary is
217
239
  * invoked in bridge mode (`--remote-url`/`--remote-transport` instead of
218
240
  * `--`), also tagged `--server-name`. Any `headers` on the original entry
219
241
  * ride into the wrapped entry's env as `INTUTIC_REMOTE_HEADERS` (a JSON
@@ -227,6 +249,7 @@ function wrapWithProxy(entry, workspaceId, workspaceRoot, serverName) {
227
249
  if (entry.__intutic_wrapped)
228
250
  return entry;
229
251
  const proxyBin = resolveProxyBin(workspaceRoot);
252
+ const original = structuredClone(entry);
230
253
  if (typeof entry.command === 'string') {
231
254
  const originalArgs = entry.args ?? [];
232
255
  return {
@@ -244,16 +267,12 @@ function wrapWithProxy(entry, workspaceId, workspaceRoot, serverName) {
244
267
  INTUTIC_WORKSPACE_ID: workspaceId,
245
268
  },
246
269
  __intutic_wrapped: true,
270
+ __intutic_original: original,
247
271
  };
248
272
  }
249
273
  if (typeof entry.url === 'string') {
250
274
  const remoteTransport = entry.type?.toLowerCase() === 'sse' ? 'sse' : 'http';
251
275
  const hasHeaders = entry.headers !== undefined && Object.keys(entry.headers).length > 0;
252
- const original = { url: entry.url };
253
- if (entry.type !== undefined)
254
- original.type = entry.type;
255
- if (entry.headers !== undefined)
256
- original.headers = entry.headers;
257
276
  const env = {
258
277
  ...(entry.env ?? {}),
259
278
  INTUTIC_WORKSPACE_ID: workspaceId,
@@ -339,8 +358,8 @@ async function readJsonForWrite(filePath) {
339
358
  /**
340
359
  * Write JSON to disk, but only if the content actually changed.
341
360
  *
342
- * `injectMcpServer` now runs every sync-loop iteration (~every 30s, see
343
- * syncLoop.ts) instead of only once at `connect` time — re-running the same
361
+ * `injectMcpServer` runs every sync cycle (~every 30s, from `intutic
362
+ * connect`) instead of only once at startup — re-running the same
344
363
  * wrap against an already-wrapped, unchanged config must not touch the file,
345
364
  * or every cycle would churn the file's mtime and fire a spurious inotify /
346
365
  * FSEvents event for every harness config on every developer machine.
@@ -349,8 +368,12 @@ async function readJsonForWrite(filePath) {
349
368
  * serialized string — a naive string compare would report a false "changed"
350
369
  * on nothing more than JSON.stringify key-ordering differences between two
351
370
  * semantically identical objects.
371
+ *
372
+ * With `workspaceRoot`, the file's original is kept for `intutic disconnect`
373
+ * before the first write. `~/.claude.json` goes without: it is Claude Code's
374
+ * live state, never restored whole.
352
375
  */
353
- async function writeJsonFile(filePath, data) {
376
+ async function writeJsonFile(filePath, data, workspaceRoot) {
354
377
  let existing;
355
378
  let hasExisting;
356
379
  try {
@@ -363,6 +386,8 @@ async function writeJsonFile(filePath, data) {
363
386
  if (hasExisting && isDeepStrictEqual(existing, data)) {
364
387
  return;
365
388
  }
389
+ if (workspaceRoot !== undefined)
390
+ await keepOriginal(filePath, workspaceRoot);
366
391
  await node_fs.mkdir(node_path.dirname(filePath), { recursive: true });
367
392
  await node_fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
368
393
  }
@@ -378,14 +403,97 @@ async function injectClaudeCode(workspaceId, workspaceRoot) {
378
403
  const current = await readJsonForWrite(configPath);
379
404
  // User scope: available in every project.
380
405
  current.mcpServers = wrapAllServers({ intutic: buildIntuticMcpEntry(workspaceRoot), ...(current.mcpServers ?? {}) }, workspaceId, workspaceRoot);
381
- // Local scope: servers added with `claude mcp add` in this project.
406
+ // Local scope: servers added with `claude mcp add` in this project, then
407
+ // the shadows that govern its approved `.mcp.json` servers. A project Claude
408
+ // Code has never opened has no entry, and nothing approved to shadow.
382
409
  const project = current.projects?.[workspaceRoot];
383
- if (project?.mcpServers && Object.keys(project.mcpServers).length > 0) {
384
- project.mcpServers = wrapAllServers(project.mcpServers, workspaceId, workspaceRoot);
410
+ if (project) {
411
+ const local = wrapAllServers(project.mcpServers ?? {}, workspaceId, workspaceRoot);
412
+ const withShadows = shadowProjectServers(local, await readProjectScopeServers(workspaceRoot), await projectServerApproval(workspaceRoot, current), workspaceId, workspaceRoot);
413
+ if (project.mcpServers !== undefined || Object.keys(withShadows).length > 0)
414
+ project.mcpServers = withShadows;
385
415
  }
386
416
  await writeJsonFile(configPath, current);
387
417
  log.info({ action: 'claude_code_mcp_injected' }, 'Claude Code ~/.claude.json MCP servers updated');
388
418
  }
419
+ /** Where Claude Code keeps a project's shared, committed servers. */
420
+ function claudeCodeProjectMcpPath(workspaceRoot) {
421
+ return node_path.join(workspaceRoot, '.mcp.json');
422
+ }
423
+ /** The project's `.mcp.json` servers; empty when the file is missing or not a JSON object. */
424
+ async function readProjectScopeServers(workspaceRoot) {
425
+ const parsed = await readJsonFile(claudeCodeProjectMcpPath(workspaceRoot), {});
426
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
427
+ return {};
428
+ const servers = parsed.mcpServers;
429
+ return servers !== null && typeof servers === 'object' && !Array.isArray(servers) ? servers : {};
430
+ }
431
+ /**
432
+ * Why an approved project server cannot get a governed local-scope copy, or
433
+ * null when it can. Exported for discovery's reporting through
434
+ * {@link discoverMcpServers}.
435
+ *
436
+ * Expansion itself is not a reason: Claude Code expands `${VAR}` the same way
437
+ * at local scope as in `.mcp.json`, in `command`, `args`, `env`, `url` and
438
+ * `headers` (verified with Claude Code 2.1.233 — see the MCP governance
439
+ * guide), so a copy keeps references verbatim and writes no resolved value.
440
+ * A remote server is the exception. Its copy is a stdio entry that hands the
441
+ * url to the proxy as an argument and the headers through the environment,
442
+ * where Claude Code expands every variable; in a remote server's own `url`
443
+ * and `headers` it reads credential variables (its own API keys, cloud and
444
+ * proxy credentials) as empty, by a list it does not publish in full. A copy
445
+ * could therefore send a credential the original never would.
446
+ */
447
+ function shadowBlocker(entry) {
448
+ if (entry === null || typeof entry !== 'object')
449
+ return 'an entry the proxy cannot front';
450
+ if (entry.__intutic_wrapped)
451
+ return 'the entry in .mcp.json is already wrapped';
452
+ if (typeof entry.command === 'string')
453
+ return null;
454
+ if (typeof entry.url === 'string') {
455
+ const refs = [entry.url, ...Object.values(entry.headers ?? {})].some((v) => typeof v === 'string' && v.includes('${'));
456
+ return refs
457
+ ? 'its url or headers reference environment variables, which Claude Code expands with a credential filter a wrapped copy cannot keep'
458
+ : null;
459
+ }
460
+ return 'an entry the proxy cannot front';
461
+ }
462
+ /**
463
+ * Governs a project's `.mcp.json` servers without touching the file.
464
+ *
465
+ * `.mcp.json` is committed and shared with the team: wrapping it in place
466
+ * would write this machine's proxy path and workspace id into everyone's
467
+ * checkout. Claude Code gives a local-scope server precedence over a
468
+ * project-scope one of the same name, so each approved project server gets a
469
+ * wrapped copy at local scope, in this project's `~/.claude.json` entry,
470
+ * marked `__intutic_shadow_of: 'project'`. The rules:
471
+ *
472
+ * - A server the user added at local scope keeps its name; it is wrapped by
473
+ * the local-scope pass already and never replaced by a shadow.
474
+ * - A shadow follows its source: rewritten when the project entry changes,
475
+ * removed when the project entry, or its approval, goes away. Deleting the
476
+ * marked entries returns the project to exactly what `.mcp.json` says.
477
+ * - An entry whose shape the proxy cannot wrap is left to Claude Code as is.
478
+ * - Re-running on unchanged input produces the same map, so the
479
+ * write-if-changed in `writeJsonFile` writes nothing.
480
+ */
481
+ function shadowProjectServers(local, projectServers, approval, workspaceId, workspaceRoot) {
482
+ const result = {};
483
+ for (const [name, entry] of Object.entries(local)) {
484
+ if (entry.__intutic_shadow_of === 'project')
485
+ continue; // rebuilt below from the current source
486
+ result[name] = entry;
487
+ }
488
+ for (const [name, entry] of Object.entries(projectServers)) {
489
+ if (name === 'intutic' || name in result || !approval.approved(name))
490
+ continue;
491
+ if (shadowBlocker(entry) !== null)
492
+ continue;
493
+ result[name] = { ...wrapWithProxy(entry, workspaceId, workspaceRoot, name), __intutic_shadow_of: 'project' };
494
+ }
495
+ return result;
496
+ }
389
497
  // ─── Target: Claude Desktop ───────────────────────────────────────────────────
390
498
  async function injectClaudeDesktop(workspaceId, workspaceRoot) {
391
499
  const configPath = claudeDesktopConfigPath();
@@ -399,7 +507,7 @@ async function injectClaudeDesktop(workspaceId, workspaceRoot) {
399
507
  }
400
508
  const current = await readJsonForWrite(configPath);
401
509
  current.mcpServers = wrapAllServers({ intutic: buildIntuticMcpEntry(workspaceRoot), ...(current.mcpServers ?? {}) }, workspaceId, workspaceRoot);
402
- await writeJsonFile(configPath, current);
510
+ await writeJsonFile(configPath, current, workspaceRoot);
403
511
  log.info({ action: 'claude_desktop_mcp_injected' }, 'Claude Desktop config updated');
404
512
  }
405
513
  // ─── Target: Cursor (global + project) ───────────────────────────────────────
@@ -410,7 +518,7 @@ async function injectCursor(workspaceId, workspaceRoot) {
410
518
  await node_fs.access(node_path.dirname(globalPath));
411
519
  const current = await readJsonForWrite(globalPath);
412
520
  current.mcpServers = wrapAllServers({ intutic: buildIntuticMcpEntry(workspaceRoot), ...(current.mcpServers ?? {}) }, workspaceId, workspaceRoot);
413
- await writeJsonFile(globalPath, current);
521
+ await writeJsonFile(globalPath, current, workspaceRoot);
414
522
  log.info({ action: 'cursor_global_mcp_injected' }, 'Cursor globalSettings.json updated');
415
523
  }
416
524
  catch {
@@ -421,7 +529,7 @@ async function injectCursor(workspaceId, workspaceRoot) {
421
529
  try {
422
530
  const current = await readJsonForWrite(projectPath);
423
531
  current.mcpServers = wrapAllServers({ intutic: buildIntuticMcpEntry(workspaceRoot), ...(current.mcpServers ?? {}) }, workspaceId, workspaceRoot);
424
- await writeJsonFile(projectPath, current);
532
+ await writeJsonFile(projectPath, current, workspaceRoot);
425
533
  log.info({ action: 'cursor_project_mcp_injected' }, 'Cursor .cursor/mcp.json updated');
426
534
  }
427
535
  catch (err) {
@@ -434,7 +542,7 @@ async function injectCline(workspaceId, workspaceRoot) {
434
542
  try {
435
543
  const current = await readJsonForWrite(configPath);
436
544
  current.mcpServers = wrapAllServers({ intutic: buildIntuticMcpEntry(workspaceRoot), ...(current.mcpServers ?? {}) }, workspaceId, workspaceRoot);
437
- await writeJsonFile(configPath, current);
545
+ await writeJsonFile(configPath, current, workspaceRoot);
438
546
  log.info({ action: 'cline_mcp_injected' }, 'Cline .cline/mcp.json updated');
439
547
  }
440
548
  catch (err) {
@@ -448,7 +556,7 @@ async function injectWindsurf(workspaceId, workspaceRoot) {
448
556
  await node_fs.access(node_path.dirname(configPath));
449
557
  const current = await readJsonForWrite(configPath);
450
558
  current.mcpServers = wrapAllServers({ intutic: buildIntuticMcpEntry(workspaceRoot), ...(current.mcpServers ?? {}) }, workspaceId, workspaceRoot);
451
- await writeJsonFile(configPath, current);
559
+ await writeJsonFile(configPath, current, workspaceRoot);
452
560
  log.info({ action: 'windsurf_mcp_injected' }, 'Windsurf mcp_config.json updated');
453
561
  }
454
562
  catch {
@@ -469,14 +577,16 @@ async function injectContinue(workspaceId, workspaceRoot) {
469
577
  // Wrap existing non-intutic servers — routed through the same
470
578
  // wrapWithProxy used by every other harness, so the stdio-only guard and
471
579
  // --server-name threading stay in one place instead of two.
580
+ // The whole entry goes in, so the wrap's `__intutic_original` holds every
581
+ // key `intutic disconnect` has to put back.
472
582
  current.mcpServers = current.mcpServers.map((s) => {
473
- const wrapped = wrapWithProxy({ command: s.command, args: s.args, env: s.env, url: s.url, type: s.type, __intutic_wrapped: s.__intutic_wrapped }, workspaceId, workspaceRoot, s.name);
474
- return { name: s.name, ...wrapped };
583
+ const { name, ...entry } = s;
584
+ return { name, ...wrapWithProxy(entry, workspaceId, workspaceRoot, name) };
475
585
  });
476
586
  // Add Intutic MCP server
477
587
  const entry = buildIntuticMcpEntry(workspaceRoot);
478
588
  current.mcpServers.unshift({ name: 'intutic', ...entry });
479
- await writeJsonFile(configPath, current);
589
+ await writeJsonFile(configPath, current, workspaceRoot);
480
590
  log.info({ action: 'continue_mcp_injected' }, 'Continue ~/.continue/config.json updated');
481
591
  }
482
592
  catch {
@@ -512,6 +622,7 @@ async function injectGooseAppendOnly(configPath, existingYaml, workspaceId, prox
512
622
  // (see writeJsonFile's doc comment for why: this runs every sync cycle now).
513
623
  return;
514
624
  }
625
+ await keepOriginal(configPath, node_os.homedir());
515
626
  await node_fs.mkdir(node_path.dirname(configPath), { recursive: true });
516
627
  await node_fs.writeFile(configPath, text, 'utf-8');
517
628
  log.info({ action: 'goose_mcp_injected', mode: 'append_only_fallback' }, 'Goose config.yaml updated (append-only fallback)');
@@ -603,6 +714,7 @@ async function injectGoose(workspaceId, workspaceRoot) {
603
714
  // (see writeJsonFile's doc comment for why: this runs every sync cycle now).
604
715
  return;
605
716
  }
717
+ await keepOriginal(configPath, workspaceRoot);
606
718
  await node_fs.mkdir(node_path.dirname(configPath), { recursive: true });
607
719
  await node_fs.writeFile(configPath, doc.toString(), 'utf-8');
608
720
  log.info({ action: 'goose_mcp_injected', mode: 'yaml' }, 'Goose config.yaml updated (structural YAML edit)');
@@ -613,7 +725,7 @@ async function injectOpenHands(workspaceId, workspaceRoot) {
613
725
  try {
614
726
  const current = await readJsonForWrite(configPath);
615
727
  current.mcpServers = wrapAllServers({ intutic: buildIntuticMcpEntry(workspaceRoot), ...(current.mcpServers ?? {}) }, workspaceId, workspaceRoot);
616
- await writeJsonFile(configPath, current);
728
+ await writeJsonFile(configPath, current, workspaceRoot);
617
729
  log.info({ action: 'openhands_mcp_injected' }, 'OpenHands .openhands/mcp.json updated');
618
730
  }
619
731
  catch (err) {
@@ -644,7 +756,7 @@ async function injectMuse(workspaceId, workspaceRoot) {
644
756
  current.mcp_servers = wrapAllServers({ intutic: buildIntuticMcpEntry(workspaceRoot), ...(current.mcp_servers ?? {}) }, workspaceId, workspaceRoot);
645
757
  if (current.schema_version === undefined)
646
758
  current.schema_version = 1;
647
- await writeJsonFile(configPath, current);
759
+ await writeJsonFile(configPath, current, workspaceRoot);
648
760
  log.info({ action: 'muse_mcp_injected' }, 'Muse Code ~/.config/muse/settings.json mcp_servers updated');
649
761
  }
650
762
  /**
@@ -657,7 +769,7 @@ async function injectMuse(workspaceId, workspaceRoot) {
657
769
  * honest limit `injectGooseAppendOnly`/`discoverGooseLineScanFallback`
658
770
  * document for their own fallback.
659
771
  */
660
- async function injectGrokConfigAppendOnly(configPath, existingToml, workspaceId, proxyBin) {
772
+ async function injectGrokConfigAppendOnly(configPath, existingToml, workspaceId, proxyBin, workspaceRoot) {
661
773
  let text = existingToml;
662
774
  if (!text.includes('[mcp_servers.intutic]') && !text.includes('[mcp_servers."intutic"]')) {
663
775
  const block = [
@@ -669,6 +781,7 @@ async function injectGrokConfigAppendOnly(configPath, existingToml, workspaceId,
669
781
  }
670
782
  if (text === existingToml)
671
783
  return;
784
+ await keepOriginal(configPath, workspaceRoot);
672
785
  await node_fs.mkdir(node_path.dirname(configPath), { recursive: true });
673
786
  await node_fs.writeFile(configPath, text, 'utf-8');
674
787
  log.info({ action: 'grok_mcp_injected', mode: 'append_only_fallback' }, 'Grok config.toml updated (append-only fallback)');
@@ -711,7 +824,7 @@ async function injectGrokConfig(configPath, workspaceId, workspaceRoot) {
711
824
  }
712
825
  catch (err) {
713
826
  log.warn({ action: 'grok_toml_unparseable', path: configPath, err: err.message }, 'Grok config.toml did not parse as TOML — falling back to append-only text injection');
714
- await injectGrokConfigAppendOnly(configPath, existingToml, workspaceId, proxyBin);
827
+ await injectGrokConfigAppendOnly(configPath, existingToml, workspaceId, proxyBin, workspaceRoot);
715
828
  return;
716
829
  }
717
830
  const servers = doc.mcp_servers && typeof doc.mcp_servers === 'object' ? doc.mcp_servers : {};
@@ -736,6 +849,7 @@ async function injectGrokConfig(configPath, workspaceId, workspaceRoot) {
736
849
  return;
737
850
  }
738
851
  doc.mcp_servers = servers;
852
+ await keepOriginal(configPath, workspaceRoot);
739
853
  await node_fs.mkdir(node_path.dirname(configPath), { recursive: true });
740
854
  const tmp = configPath + '.intutic-tmp';
741
855
  await node_fs.writeFile(tmp, stringifyToml(doc), 'utf-8');
@@ -747,7 +861,7 @@ async function injectGrok(workspaceId, workspaceRoot) {
747
861
  await injectGrokConfig(grokUserConfigPath(), workspaceId, workspaceRoot);
748
862
  }
749
863
  /** The proxy binary path `resolveProxyBin` returns, in either of its two forms. */
750
- const PROXY_BIN_PATTERN = /(?:^|[\\/])(?:@intutic[\\/]mcp-governance-proxy|packages[\\/]mcp-proxy)[\\/]dist[\\/]index\.js$/;
864
+ export const PROXY_BIN_PATTERN = /(?:^|[\\/])(?:@intutic[\\/]mcp-governance-proxy|packages[\\/]mcp-proxy)[\\/]dist[\\/]index\.js$/;
751
865
  /**
752
866
  * Whether an OpenCode entry is already fronted by the governance proxy, read
753
867
  * from the command SHAPE — `["node", <proxy bin>, "--workspace-id", …]` —
@@ -858,6 +972,8 @@ async function injectOpenCodeConfig(configPath, workspaceId, workspaceRoot) {
858
972
  return;
859
973
  config['mcp'] = next;
860
974
  const indent = raw.match(/^([ \t]+)\S/m)?.[1] ?? 2;
975
+ // OpenCode entries cannot carry a marker, so disconnect needs the file as it was.
976
+ await keepOriginal(configPath, workspaceRoot);
861
977
  const tmp = configPath + '.intutic-tmp';
862
978
  await node_fs.writeFile(tmp, JSON.stringify(config, null, indent) + '\n', 'utf-8');
863
979
  await node_fs.rename(tmp, configPath);
@@ -867,7 +983,143 @@ async function injectOpenCode(workspaceId, workspaceRoot) {
867
983
  await injectOpenCodeConfig(openCodeProjectConfigPath(workspaceRoot), workspaceId, workspaceRoot);
868
984
  await injectOpenCodeConfig(openCodeGlobalConfigPath(), workspaceId, workspaceRoot);
869
985
  }
870
- // ─── Discovery (read-only — writes nothing) ───────────────────────────────────
986
+ // ─── Target: Gemini CLI and Google Antigravity ──────────────────────────────
987
+ /**
988
+ * Whether an entry is already fronted by the governance proxy, read from its
989
+ * argv (`node <proxy bin> --workspace-id …`). Gemini CLI and Antigravity
990
+ * entries carry no `__intutic_wrapped` marker: Gemini CLI's settings schema
991
+ * declares `mcpServers` entries with `additionalProperties: false` and warns
992
+ * about any other key on every start, and Antigravity documents a fixed set
993
+ * of keys too. So, as for OpenCode, the shape is the marker, and `intutic
994
+ * disconnect` rebuilds the original from the argv or the kept file.
995
+ */
996
+ function isProxyFronted(entry) {
997
+ const args = entry.args;
998
+ return (entry.command === 'node' &&
999
+ Array.isArray(args) &&
1000
+ typeof args[0] === 'string' &&
1001
+ PROXY_BIN_PATTERN.test(args[0]) &&
1002
+ args.includes('--workspace-id'));
1003
+ }
1004
+ /**
1005
+ * Gemini CLI: `url`, with `type` `sse` or `http` (with no `type`, Gemini CLI
1006
+ * tries streamable HTTP first, which is what the bridge speaks), or
1007
+ * `httpUrl` for streamable HTTP. A server Gemini CLI authenticates itself
1008
+ * (`oauth`, or a Google credential provider in `authProviderType`) stays as
1009
+ * it is: the bridge forwards static headers only, so behind it the server
1010
+ * would stop authenticating. A websocket server (`tcp`) has no bridge.
1011
+ */
1012
+ const GEMINI_FORMAT = {
1013
+ harness: 'gemini-cli',
1014
+ remote: (e) => typeof e.httpUrl === 'string'
1015
+ ? { url: e.httpUrl, transport: 'http' }
1016
+ : typeof e.url === 'string'
1017
+ ? { url: e.url, transport: e.type === 'sse' ? 'sse' : 'http' }
1018
+ : null,
1019
+ blocker: (e) => typeof e.command === 'string'
1020
+ ? null
1021
+ : e.oauth !== undefined || e.authProviderType !== undefined
1022
+ ? 'Gemini CLI authenticates this server itself (OAuth or a Google credential provider), which the proxy cannot do for it'
1023
+ : typeof e.tcp === 'string'
1024
+ ? 'a websocket (tcp) server, which the proxy cannot bridge'
1025
+ : null,
1026
+ remoteKeys: ['url', 'httpUrl', 'type', 'headers'],
1027
+ };
1028
+ /** Antigravity: a remote server is `serverUrl`, which Antigravity documents as SSE. */
1029
+ const ANTIGRAVITY_FORMAT = {
1030
+ harness: 'antigravity',
1031
+ remote: (e) => (typeof e.serverUrl === 'string' ? { url: e.serverUrl, transport: 'sse' } : null),
1032
+ blocker: () => null,
1033
+ remoteKeys: ['serverUrl', 'headers'],
1034
+ };
1035
+ function stringMap(v) {
1036
+ if (!v || typeof v !== 'object' || Array.isArray(v))
1037
+ return undefined;
1038
+ const out = {};
1039
+ for (const [k, x] of Object.entries(v))
1040
+ if (typeof x === 'string')
1041
+ out[k] = x;
1042
+ return out;
1043
+ }
1044
+ /**
1045
+ * Wrap one Gemini CLI or Antigravity server. The proxy argv is built by
1046
+ * `wrapWithProxy`, like every harness's, and only its `command`/`args`/`env`
1047
+ * are kept. The entry's other keys stay: Gemini CLI's `cwd` (where the real
1048
+ * command runs), `timeout`, `trust`, `description`, `includeTools` and
1049
+ * `excludeTools` mean the same in front of the proxy, which passes the
1050
+ * server's tools through under their own names.
1051
+ */
1052
+ function wrapUnmarkedEntry(entry, format, workspaceId, workspaceRoot, serverName) {
1053
+ if (isProxyFronted(entry) || format.blocker(entry) !== null)
1054
+ return entry;
1055
+ const { command, args, env, ...rest } = entry;
1056
+ let wrapped;
1057
+ if (typeof command === 'string') {
1058
+ const argv = Array.isArray(args) ? args.filter((a) => typeof a === 'string') : [];
1059
+ wrapped = wrapWithProxy({ command, args: argv, env: stringMap(env) }, workspaceId, workspaceRoot, serverName);
1060
+ }
1061
+ else {
1062
+ const remote = format.remote(entry);
1063
+ if (remote === null)
1064
+ return entry;
1065
+ const headers = stringMap(entry.headers);
1066
+ wrapped = wrapWithProxy({ url: remote.url, type: remote.transport, env: stringMap(env), ...(headers ? { headers } : {}) }, workspaceId, workspaceRoot, serverName);
1067
+ for (const key of format.remoteKeys)
1068
+ delete rest[key];
1069
+ }
1070
+ return { ...rest, command: wrapped.command, args: wrapped.args, env: wrapped.env };
1071
+ }
1072
+ /**
1073
+ * Wrap the `mcpServers` map of one Gemini CLI or Antigravity file, with the
1074
+ * `intutic` server added when `addIntutic`. A file that is missing is created
1075
+ * only when `addIntutic` (the product's user-level file, on a machine that has
1076
+ * the product); a project file is only ever edited.
1077
+ */
1078
+ async function injectUnmarkedConfig(configPath, format, workspaceId, workspaceRoot, addIntutic) {
1079
+ if (!addIntutic && !existsSync(configPath))
1080
+ return;
1081
+ const current = await readJsonForWrite(configPath);
1082
+ const existing = current.mcpServers && typeof current.mcpServers === 'object' && !Array.isArray(current.mcpServers)
1083
+ ? current.mcpServers
1084
+ : {};
1085
+ const servers = addIntutic ? { intutic: buildIntuticMcpEntry(workspaceRoot), ...existing } : existing;
1086
+ if (Object.keys(servers).length === 0)
1087
+ return;
1088
+ const next = {};
1089
+ for (const [name, entry] of Object.entries(servers)) {
1090
+ next[name] =
1091
+ name === 'intutic' || !entry || typeof entry !== 'object' || Array.isArray(entry)
1092
+ ? entry
1093
+ : wrapUnmarkedEntry(entry, format, workspaceId, workspaceRoot, name);
1094
+ }
1095
+ current.mcpServers = next;
1096
+ await writeJsonFile(configPath, current, workspaceRoot);
1097
+ log.info({ action: 'gemini_mcp_injected', harness: format.harness, path: configPath }, 'MCP servers wrapped');
1098
+ }
1099
+ /**
1100
+ * The `antigravity` harness's two products, each where it is installed
1101
+ * (`antigravityProducts.ts`): Gemini CLI's `mcpServers` in its user
1102
+ * settings, and in the project's `.gemini/settings.json` when there is one
1103
+ * (wrapped there, with no second `intutic` server); Antigravity's in
1104
+ * `~/.gemini/config/mcp_config.json`. Servers an Antigravity plugin brings
1105
+ * (`plugins/<name>/mcp_config.json`) belong to the plugin and are not
1106
+ * rewritten.
1107
+ */
1108
+ async function injectAntigravity(workspaceId, workspaceRoot) {
1109
+ const products = await antigravityGateIdentities(workspaceRoot);
1110
+ if (products.includes(GEMINI_CLI_GATE_ID)) {
1111
+ await injectUnmarkedConfig(geminiSettingsPath(), GEMINI_FORMAT, workspaceId, workspaceRoot, true);
1112
+ await injectUnmarkedConfig(geminiProjectSettingsPath(workspaceRoot), GEMINI_FORMAT, workspaceId, workspaceRoot, false);
1113
+ }
1114
+ if (products.includes('antigravity')) {
1115
+ await injectUnmarkedConfig(antigravityMcpConfigPath(), ANTIGRAVITY_FORMAT, workspaceId, workspaceRoot, true);
1116
+ }
1117
+ }
1118
+ /** `endpoint` only when the URL survives sanitising, so the field is never present and empty. */
1119
+ function withEndpoint(found, url) {
1120
+ const endpoint = sanitizeMcpEndpoint(url);
1121
+ return endpoint ? { ...found, endpoint } : found;
1122
+ }
871
1123
  /** Classify a raw server entry's transport + wrapped status, tolerant of any shape. */
872
1124
  function classifyEntry(entry) {
873
1125
  if (!entry || typeof entry !== 'object')
@@ -885,14 +1137,14 @@ function classifyEntry(entry) {
885
1137
  const o = original;
886
1138
  if (typeof o.url === 'string') {
887
1139
  const type = typeof o.type === 'string' ? o.type.toLowerCase() : '';
888
- return { transport: type === 'sse' ? 'sse' : 'http', wrapped: true };
1140
+ return withEndpoint({ transport: type === 'sse' ? 'sse' : 'http', wrapped: true }, o.url);
889
1141
  }
890
1142
  }
891
1143
  if (typeof e.command === 'string')
892
1144
  return { transport: 'stdio', wrapped };
893
1145
  if (typeof e.url === 'string') {
894
1146
  const type = typeof e.type === 'string' ? e.type.toLowerCase() : '';
895
- return { transport: type === 'sse' ? 'sse' : 'http', wrapped };
1147
+ return withEndpoint({ transport: type === 'sse' ? 'sse' : 'http', wrapped }, e.url);
896
1148
  }
897
1149
  return { transport: 'unknown', wrapped };
898
1150
  }
@@ -905,20 +1157,35 @@ async function discoverJsonObjectHarness(harness, filePath) {
905
1157
  const current = await readJsonFile(filePath, {});
906
1158
  const out = [];
907
1159
  for (const [name, entry] of Object.entries(current.mcpServers ?? {})) {
908
- const { transport, wrapped } = classifyEntry(entry);
909
- out.push({ server: name, harness, transport, wrapped });
1160
+ out.push({ server: name, harness, ...classifyEntry(entry) });
910
1161
  }
911
1162
  return out;
912
1163
  }
913
- /** Claude Code's `~/.claude.json`: the user-scope `mcpServers` map plus this
914
- * project's local-scope one under `projects[<workspaceRoot>]`. */
1164
+ /** Claude Code's servers as Claude Code resolves them: user scope
1165
+ * (`~/.claude.json`), project scope (the repo's `.mcp.json`) and local scope
1166
+ * (this project's entry in `~/.claude.json`), each narrower scope winning a
1167
+ * name — so a project server governed by a local-scope shadow reports as
1168
+ * wrapped. */
915
1169
  async function discoverClaudeCode(workspaceRoot) {
916
1170
  const filePath = claudeCodeConfigPath();
917
- if (!existsSync(filePath))
918
- return [];
919
- const current = await readJsonFile(filePath, {});
920
- const servers = { ...(current.projects?.[workspaceRoot]?.mcpServers ?? {}), ...(current.mcpServers ?? {}) };
921
- return Object.entries(servers).map(([name, entry]) => ({ server: name, harness: 'claude-code', ...classifyEntry(entry) }));
1171
+ const current = existsSync(filePath) ? await readJsonFile(filePath, {}) : {};
1172
+ const projectServers = await readProjectScopeServers(workspaceRoot);
1173
+ const local = current.projects?.[workspaceRoot]?.mcpServers ?? {};
1174
+ const servers = { ...(current.mcpServers ?? {}), ...projectServers, ...local };
1175
+ const approval = Object.keys(projectServers).length > 0 ? await projectServerApproval(workspaceRoot, current) : null;
1176
+ return Object.entries(servers).map(([name, entry]) => {
1177
+ const found = { server: name, harness: 'claude-code', ...classifyEntry(entry) };
1178
+ // A project server Claude Code will start, with no copy of ours in front of it.
1179
+ if (approval && name in projectServers && !(name in local) && approval.approved(name)) {
1180
+ const reason = shadowBlocker(projectServers[name]);
1181
+ if (reason)
1182
+ found.ungovernedReason = `Claude Code project server not governed: ${reason}`;
1183
+ }
1184
+ if (approval?.blockedReason && name in projectServers && !(name in local)) {
1185
+ found.ungovernedReason = `Claude Code project server not governed: ${approval.blockedReason}`;
1186
+ }
1187
+ return found;
1188
+ });
922
1189
  }
923
1190
  /** Muse Code's `~/.config/muse/settings.json` keeps servers under `mcp_servers`,
924
1191
  * not `mcpServers` — otherwise the same JSON-map shape `discoverJsonObjectHarness`
@@ -930,8 +1197,7 @@ async function discoverMuse() {
930
1197
  const current = await readJsonFile(filePath, {});
931
1198
  const out = [];
932
1199
  for (const [name, entry] of Object.entries(current.mcp_servers ?? {})) {
933
- const { transport, wrapped } = classifyEntry(entry);
934
- out.push({ server: name, harness: 'muse-code', transport, wrapped });
1200
+ out.push({ server: name, harness: 'muse-code', ...classifyEntry(entry) });
935
1201
  }
936
1202
  return out;
937
1203
  }
@@ -943,8 +1209,7 @@ async function discoverContinue() {
943
1209
  const current = await readJsonFile(filePath, {});
944
1210
  const out = [];
945
1211
  for (const s of current.mcpServers ?? []) {
946
- const { transport, wrapped } = classifyEntry(s);
947
- out.push({ server: s.name, harness: 'continue', transport, wrapped });
1212
+ out.push({ server: s.name, harness: 'continue', ...classifyEntry(s) });
948
1213
  }
949
1214
  return out;
950
1215
  }
@@ -989,8 +1254,7 @@ async function discoverGoose() {
989
1254
  const servers = doc.toJS().mcp ?? {};
990
1255
  const out = [];
991
1256
  for (const [name, entry] of Object.entries(servers)) {
992
- const { transport, wrapped } = classifyEntry(entry);
993
- out.push({ server: name, harness: 'goose', transport, wrapped });
1257
+ out.push({ server: name, harness: 'goose', ...classifyEntry(entry) });
994
1258
  }
995
1259
  return out;
996
1260
  }
@@ -1044,8 +1308,7 @@ async function discoverGrokConfig(configPath) {
1044
1308
  const servers = doc.mcp_servers && typeof doc.mcp_servers === 'object' ? doc.mcp_servers : {};
1045
1309
  const out = [];
1046
1310
  for (const [name, entry] of Object.entries(servers)) {
1047
- const { transport, wrapped } = classifyEntry(entry);
1048
- out.push({ server: name, harness: 'grok', transport, wrapped });
1311
+ out.push({ server: name, harness: 'grok', ...classifyEntry(entry) });
1049
1312
  }
1050
1313
  return out;
1051
1314
  }
@@ -1087,12 +1350,14 @@ function classifyOpenCodeEntry(entry) {
1087
1350
  if (!argv.includes('--remote-url'))
1088
1351
  return { transport: 'stdio', wrapped };
1089
1352
  const t = argv[argv.indexOf('--remote-transport') + 1];
1090
- return { transport: t === 'sse' ? 'sse' : 'http', wrapped };
1353
+ const url = argv[argv.indexOf('--remote-url') + 1];
1354
+ const found = { transport: t === 'sse' ? 'sse' : 'http', wrapped };
1355
+ return typeof url === 'string' ? withEndpoint(found, url) : found;
1091
1356
  }
1092
1357
  if (e.type === 'local' && Array.isArray(e.command))
1093
1358
  return { transport: 'stdio', wrapped };
1094
1359
  if (e.type === 'remote' && typeof e.url === 'string')
1095
- return { transport: 'http', wrapped };
1360
+ return withEndpoint({ transport: 'http', wrapped }, e.url);
1096
1361
  return { transport: 'unknown', wrapped };
1097
1362
  }
1098
1363
  async function discoverOpenCodeConfig(configPath) {
@@ -1109,9 +1374,49 @@ async function discoverOpenCode(workspaceRoot) {
1109
1374
  ]);
1110
1375
  return [...project, ...global];
1111
1376
  }
1377
+ /** Classify a Gemini CLI or Antigravity entry. A wrapped one's true
1378
+ * transport is read back from its argv, since these entries carry no
1379
+ * `__intutic_original`. */
1380
+ function classifyUnmarkedEntry(entry, format) {
1381
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
1382
+ return { transport: 'unknown', wrapped: false };
1383
+ const e = entry;
1384
+ if (isProxyFronted(e)) {
1385
+ const argv = e.args;
1386
+ if (!argv.includes('--remote-url'))
1387
+ return { transport: 'stdio', wrapped: true };
1388
+ const t = argv[argv.indexOf('--remote-transport') + 1];
1389
+ const url = argv[argv.indexOf('--remote-url') + 1];
1390
+ const found = { transport: t === 'sse' ? 'sse' : 'http', wrapped: true };
1391
+ return typeof url === 'string' ? withEndpoint(found, url) : found;
1392
+ }
1393
+ const blocker = format.blocker(e);
1394
+ const remote = format.remote(e);
1395
+ const found = typeof e.command === 'string'
1396
+ ? { transport: 'stdio', wrapped: false }
1397
+ : remote
1398
+ ? withEndpoint({ transport: remote.transport, wrapped: false }, remote.url)
1399
+ : { transport: 'unknown', wrapped: false };
1400
+ return blocker ? { ...found, ungovernedReason: blocker } : found;
1401
+ }
1402
+ async function discoverUnmarkedConfig(configPath, format) {
1403
+ if (!existsSync(configPath))
1404
+ return [];
1405
+ const current = await readJsonFile(configPath, {});
1406
+ const servers = current.mcpServers && typeof current.mcpServers === 'object' ? current.mcpServers : {};
1407
+ return Object.entries(servers).map(([name, entry]) => ({ server: name, harness: format.harness, ...classifyUnmarkedEntry(entry, format) }));
1408
+ }
1409
+ async function discoverAntigravity(workspaceRoot) {
1410
+ const results = await Promise.all([
1411
+ discoverUnmarkedConfig(geminiSettingsPath(), GEMINI_FORMAT),
1412
+ discoverUnmarkedConfig(geminiProjectSettingsPath(workspaceRoot), GEMINI_FORMAT),
1413
+ discoverUnmarkedConfig(antigravityMcpConfigPath(), ANTIGRAVITY_FORMAT),
1414
+ ]);
1415
+ return results.flat();
1416
+ }
1112
1417
  /**
1113
1418
  * Discover every MCP server declared in any harness config this daemon knows
1114
- * how to parse — the same 14 config paths / 11 harnesses `injectMcpServer`
1419
+ * how to parse — the same 17 config paths / 12 harnesses `injectMcpServer`
1115
1420
  * wraps — without writing anything. Used for reporting (agentReporter's
1116
1421
  * `mcp_tools` facet) so visibility does not silently lag behind whatever
1117
1422
  * `injectMcpServer` was last run against.
@@ -1142,6 +1447,7 @@ export async function discoverMcpServers(workspaceRoot) {
1142
1447
  discoverMuse(),
1143
1448
  discoverGrok(workspaceRoot),
1144
1449
  discoverOpenCode(workspaceRoot),
1450
+ discoverAntigravity(workspaceRoot),
1145
1451
  ]);
1146
1452
  return results.flat().filter((s) => s.server !== 'intutic');
1147
1453
  }
@@ -1151,32 +1457,37 @@ export async function discoverMcpServers(workspaceRoot) {
1151
1457
  *
1152
1458
  * Non-fatal: a failure in one harness does not prevent other harnesses from being updated.
1153
1459
  *
1154
- * Called from two places by design: once from `intutic connect` (tools/cli)
1155
- * for immediate effect, and once per sync-loop iteration
1156
- * (services/sync-daemon/src/syncLoop.ts) so it is a continuous invariant
1157
- * rather than a one-shot — a server a user adds after their first `connect`
1460
+ * Called by `intutic connect` (tools/cli) on every sync cycle, so it is a
1461
+ * continuous invariant rather than a one-shot — a server a user adds after their first `connect`
1158
1462
  * still gets wrapped on the next cycle. Safe to call every cycle because
1159
1463
  * `writeJsonFile` is write-if-changed: an already-wrapped, unchanged config
1160
1464
  * writes zero bytes.
1161
1465
  *
1162
1466
  * @param workspaceId - The workspace ID for policy lookups and event attribution.
1163
1467
  * @param workspaceRoot - Absolute path to the project workspace root.
1468
+ * @param options.skip - Harness ids to leave alone: the ones `intutic
1469
+ * disconnect --harness` took out.
1164
1470
  */
1165
- export async function injectMcpServer(workspaceRoot, workspaceId = 'unknown') {
1471
+ export async function injectMcpServer(workspaceRoot, workspaceId = 'unknown', options = {}) {
1166
1472
  log.info({ action: 'mcp_inject_start', workspaceRoot, workspaceId }, 'Starting MCP server injection');
1167
- await Promise.allSettled([
1168
- injectClaudeCode(workspaceId, workspaceRoot).catch((err) => log.error({ err: err.message, target: 'claude-code' }, 'MCP injection failed')),
1169
- injectClaudeDesktop(workspaceId, workspaceRoot).catch((err) => log.error({ err: err.message, target: 'claude-desktop' }, 'MCP injection failed')),
1170
- injectCursor(workspaceId, workspaceRoot).catch((err) => log.error({ err: err.message, target: 'cursor' }, 'MCP injection failed')),
1171
- injectCline(workspaceId, workspaceRoot).catch((err) => log.error({ err: err.message, target: 'cline' }, 'MCP injection failed')),
1172
- injectWindsurf(workspaceId, workspaceRoot).catch((err) => log.error({ err: err.message, target: 'windsurf' }, 'MCP injection failed')),
1173
- injectContinue(workspaceId, workspaceRoot).catch((err) => log.error({ err: err.message, target: 'continue' }, 'MCP injection failed')),
1174
- injectGoose(workspaceId, workspaceRoot).catch((err) => log.error({ err: err.message, target: 'goose' }, 'MCP injection failed')),
1175
- injectOpenHands(workspaceId, workspaceRoot).catch((err) => log.error({ err: err.message, target: 'openhands' }, 'MCP injection failed')),
1176
- injectMuse(workspaceId, workspaceRoot).catch((err) => log.error({ err: err.message, target: 'muse-code' }, 'MCP injection failed')),
1177
- injectGrok(workspaceId, workspaceRoot).catch((err) => log.error({ err: err.message, target: 'grok' }, 'MCP injection failed')),
1178
- injectOpenCode(workspaceId, workspaceRoot).catch((err) => log.error({ err: err.message, target: 'opencode' }, 'MCP injection failed')),
1179
- ]);
1473
+ const targets = [
1474
+ ['claude-code', injectClaudeCode],
1475
+ ['claude-desktop', injectClaudeDesktop],
1476
+ ['cursor', injectCursor],
1477
+ ['cline', injectCline],
1478
+ ['windsurf', injectWindsurf],
1479
+ ['continue', injectContinue],
1480
+ ['goose', injectGoose],
1481
+ ['openhands', injectOpenHands],
1482
+ ['muse-code', injectMuse],
1483
+ ['grok', injectGrok],
1484
+ ['opencode', injectOpenCode],
1485
+ ['antigravity', injectAntigravity],
1486
+ ];
1487
+ const skip = new Set(options.skip ?? []);
1488
+ await Promise.allSettled(targets
1489
+ .filter(([target]) => !skip.has(target))
1490
+ .map(([target, inject]) => inject(workspaceId, workspaceRoot).catch((err) => log.error({ err: err.message, target }, 'MCP injection failed'))));
1180
1491
  log.info({ action: 'mcp_inject_complete', workspaceRoot }, 'MCP server injection complete');
1181
1492
  }
1182
1493
  //# sourceMappingURL=mcpAutoWrite.js.map