@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.
- package/README.md +1 -1
- package/dist/agentReporter.d.ts +24 -11
- package/dist/agentReporter.d.ts.map +1 -1
- package/dist/agentReporter.js +20 -13
- package/dist/agentReporter.js.map +1 -1
- package/dist/configReader.d.ts +45 -25
- package/dist/configReader.d.ts.map +1 -1
- package/dist/configReader.js +70 -46
- package/dist/configReader.js.map +1 -1
- package/dist/configWriter.d.ts +32 -45
- package/dist/configWriter.d.ts.map +1 -1
- package/dist/configWriter.js +52 -686
- package/dist/configWriter.js.map +1 -1
- package/dist/disconnect/harnesses.d.ts +29 -0
- package/dist/disconnect/harnesses.d.ts.map +1 -0
- package/dist/disconnect/harnesses.js +805 -0
- package/dist/disconnect/harnesses.js.map +1 -0
- package/dist/disconnect/index.d.ts +25 -0
- package/dist/disconnect/index.d.ts.map +1 -0
- package/dist/disconnect/index.js +92 -0
- package/dist/disconnect/index.js.map +1 -0
- package/dist/disconnect/mcp.d.ts +81 -0
- package/dist/disconnect/mcp.d.ts.map +1 -0
- package/dist/disconnect/mcp.js +354 -0
- package/dist/disconnect/mcp.js.map +1 -0
- package/dist/disconnect/originals.d.ts +88 -0
- package/dist/disconnect/originals.d.ts.map +1 -0
- package/dist/disconnect/originals.js +257 -0
- package/dist/disconnect/originals.js.map +1 -0
- package/dist/disconnect/plan.d.ts +175 -0
- package/dist/disconnect/plan.d.ts.map +1 -0
- package/dist/disconnect/plan.js +580 -0
- package/dist/disconnect/plan.js.map +1 -0
- package/dist/disconnect/recognise.d.ts +43 -0
- package/dist/disconnect/recognise.d.ts.map +1 -0
- package/dist/disconnect/recognise.js +108 -0
- package/dist/disconnect/recognise.js.map +1 -0
- package/dist/disconnect/tomlLines.d.ts +39 -0
- package/dist/disconnect/tomlLines.d.ts.map +1 -0
- package/dist/disconnect/tomlLines.js +105 -0
- package/dist/disconnect/tomlLines.js.map +1 -0
- package/dist/harness/aiderConfigMerger.d.ts +29 -8
- package/dist/harness/aiderConfigMerger.d.ts.map +1 -1
- package/dist/harness/aiderConfigMerger.js +63 -12
- package/dist/harness/aiderConfigMerger.js.map +1 -1
- package/dist/harness/antigravityCliHooks.d.ts +61 -0
- package/dist/harness/antigravityCliHooks.d.ts.map +1 -0
- package/dist/harness/antigravityCliHooks.js +207 -0
- package/dist/harness/antigravityCliHooks.js.map +1 -0
- package/dist/harness/antigravityHooks.d.ts +12 -2
- package/dist/harness/antigravityHooks.d.ts.map +1 -1
- package/dist/harness/antigravityHooks.js +38 -16
- package/dist/harness/antigravityHooks.js.map +1 -1
- package/dist/harness/antigravityProducts.d.ts +40 -0
- package/dist/harness/antigravityProducts.d.ts.map +1 -0
- package/dist/harness/antigravityProducts.js +109 -0
- package/dist/harness/antigravityProducts.js.map +1 -0
- package/dist/harness/claudeAgentsMd.d.ts +29 -0
- package/dist/harness/claudeAgentsMd.d.ts.map +1 -0
- package/dist/harness/claudeAgentsMd.js +80 -0
- package/dist/harness/claudeAgentsMd.js.map +1 -0
- package/dist/harness/claudeCodeHooks.d.ts +6 -6
- package/dist/harness/claudeCodeHooks.d.ts.map +1 -1
- package/dist/harness/claudeCodeHooks.js +115 -28
- package/dist/harness/claudeCodeHooks.js.map +1 -1
- package/dist/harness/claudeProjectApproval.d.ts +53 -0
- package/dist/harness/claudeProjectApproval.d.ts.map +1 -0
- package/dist/harness/claudeProjectApproval.js +159 -0
- package/dist/harness/claudeProjectApproval.js.map +1 -0
- package/dist/harness/clineHooks.d.ts +0 -1
- package/dist/harness/clineHooks.d.ts.map +1 -1
- package/dist/harness/clineHooks.js +13 -4
- package/dist/harness/clineHooks.js.map +1 -1
- package/dist/harness/codexConfigMerger.d.ts.map +1 -1
- package/dist/harness/codexConfigMerger.js +4 -0
- package/dist/harness/codexConfigMerger.js.map +1 -1
- package/dist/harness/codexHooks.d.ts +2 -0
- package/dist/harness/codexHooks.d.ts.map +1 -1
- package/dist/harness/codexHooks.js +15 -4
- package/dist/harness/codexHooks.js.map +1 -1
- package/dist/harness/continueConfigMerger.d.ts.map +1 -1
- package/dist/harness/continueConfigMerger.js +4 -0
- package/dist/harness/continueConfigMerger.js.map +1 -1
- package/dist/harness/cursorHooks.d.ts +5 -2
- package/dist/harness/cursorHooks.d.ts.map +1 -1
- package/dist/harness/cursorHooks.js +12 -3
- package/dist/harness/cursorHooks.js.map +1 -1
- package/dist/harness/dshHooks.d.ts +5 -5
- package/dist/harness/dshHooks.d.ts.map +1 -1
- package/dist/harness/dshHooks.js +12 -5
- package/dist/harness/dshHooks.js.map +1 -1
- package/dist/harness/gateArtifacts.d.ts +35 -0
- package/dist/harness/gateArtifacts.d.ts.map +1 -0
- package/dist/harness/gateArtifacts.js +88 -0
- package/dist/harness/gateArtifacts.js.map +1 -0
- package/dist/harness/gateBody.d.ts +195 -69
- package/dist/harness/gateBody.d.ts.map +1 -1
- package/dist/harness/gateBody.js +847 -182
- package/dist/harness/gateBody.js.map +1 -1
- package/dist/harness/gateSightings.d.ts +47 -0
- package/dist/harness/gateSightings.d.ts.map +1 -0
- package/dist/harness/gateSightings.js +159 -0
- package/dist/harness/gateSightings.js.map +1 -0
- package/dist/harness/githubCopilotHooks.d.ts.map +1 -1
- package/dist/harness/githubCopilotHooks.js +11 -10
- package/dist/harness/githubCopilotHooks.js.map +1 -1
- package/dist/harness/gooseHardener.d.ts +0 -1
- package/dist/harness/gooseHardener.d.ts.map +1 -1
- package/dist/harness/gooseHardener.js +1 -1
- package/dist/harness/gooseHardener.js.map +1 -1
- package/dist/harness/gooseHooks.d.ts +29 -1
- package/dist/harness/gooseHooks.d.ts.map +1 -1
- package/dist/harness/gooseHooks.js +107 -69
- package/dist/harness/gooseHooks.js.map +1 -1
- package/dist/harness/grokHooks.d.ts +2 -2
- package/dist/harness/grokHooks.d.ts.map +1 -1
- package/dist/harness/grokHooks.js +20 -18
- package/dist/harness/grokHooks.js.map +1 -1
- package/dist/harness/hermesHooks.d.ts +18 -8
- package/dist/harness/hermesHooks.d.ts.map +1 -1
- package/dist/harness/hermesHooks.js +55 -19
- package/dist/harness/hermesHooks.js.map +1 -1
- package/dist/harness/holdRedaction.d.ts +4 -1
- package/dist/harness/holdRedaction.d.ts.map +1 -1
- package/dist/harness/holdRedaction.js +32 -8
- package/dist/harness/holdRedaction.js.map +1 -1
- package/dist/harness/jetbrainsXmlConfig.d.ts +20 -0
- package/dist/harness/jetbrainsXmlConfig.d.ts.map +1 -1
- package/dist/harness/jetbrainsXmlConfig.js +39 -18
- package/dist/harness/jetbrainsXmlConfig.js.map +1 -1
- package/dist/harness/mcpAutoWrite.d.ts +62 -25
- package/dist/harness/mcpAutoWrite.d.ts.map +1 -1
- package/dist/harness/mcpAutoWrite.js +406 -95
- package/dist/harness/mcpAutoWrite.js.map +1 -1
- package/dist/harness/museHooks.d.ts +3 -3
- package/dist/harness/museHooks.d.ts.map +1 -1
- package/dist/harness/museHooks.js +15 -7
- package/dist/harness/museHooks.js.map +1 -1
- package/dist/harness/n8nHooks.d.ts +0 -1
- package/dist/harness/n8nHooks.d.ts.map +1 -1
- package/dist/harness/n8nHooks.js +4 -1
- package/dist/harness/n8nHooks.js.map +1 -1
- package/dist/harness/openCodeHooks.d.ts +2 -2
- package/dist/harness/openCodeHooks.d.ts.map +1 -1
- package/dist/harness/openCodeHooks.js +9 -8
- package/dist/harness/openCodeHooks.js.map +1 -1
- package/dist/harness/openWebuiHooks.d.ts +0 -1
- package/dist/harness/openWebuiHooks.d.ts.map +1 -1
- package/dist/harness/openWebuiHooks.js +11 -3
- package/dist/harness/openWebuiHooks.js.map +1 -1
- package/dist/harness/openclawHooks.d.ts +75 -18
- package/dist/harness/openclawHooks.d.ts.map +1 -1
- package/dist/harness/openclawHooks.js +214 -160
- package/dist/harness/openclawHooks.js.map +1 -1
- package/dist/harness/openhandsHooks.d.ts +37 -11
- package/dist/harness/openhandsHooks.d.ts.map +1 -1
- package/dist/harness/openhandsHooks.js +148 -97
- package/dist/harness/openhandsHooks.js.map +1 -1
- package/dist/harness/piHooks.d.ts +52 -17
- package/dist/harness/piHooks.d.ts.map +1 -1
- package/dist/harness/piHooks.js +177 -163
- package/dist/harness/piHooks.js.map +1 -1
- package/dist/harness/protectedPaths.d.ts +107 -25
- package/dist/harness/protectedPaths.d.ts.map +1 -1
- package/dist/harness/protectedPaths.js +342 -48
- package/dist/harness/protectedPaths.js.map +1 -1
- package/dist/harness/rulesSection.d.ts +62 -0
- package/dist/harness/rulesSection.d.ts.map +1 -0
- package/dist/harness/rulesSection.js +145 -0
- package/dist/harness/rulesSection.js.map +1 -0
- package/dist/harness/windsurfHooks.d.ts +18 -3
- package/dist/harness/windsurfHooks.d.ts.map +1 -1
- package/dist/harness/windsurfHooks.js +41 -8
- package/dist/harness/windsurfHooks.js.map +1 -1
- package/dist/harness/windsurfJetBrainsProxy.d.ts +1 -1
- package/dist/harness/windsurfJetBrainsProxy.d.ts.map +1 -1
- package/dist/harness/windsurfJetBrainsProxy.js +5 -1
- package/dist/harness/windsurfJetBrainsProxy.js.map +1 -1
- package/dist/index.d.ts +21 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -19
- package/dist/index.js.map +1 -1
- package/dist/inventory.d.ts +53 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/inventory.js +133 -0
- package/dist/inventory.js.map +1 -0
- package/dist/lib/complianceProbes.d.ts +2 -1
- package/dist/lib/complianceProbes.d.ts.map +1 -1
- package/dist/lib/complianceProbes.js +1 -1
- package/dist/lib/complianceProbes.js.map +1 -1
- package/dist/lib/decisionsDigest.d.ts +35 -17
- package/dist/lib/decisionsDigest.d.ts.map +1 -1
- package/dist/lib/decisionsDigest.js +106 -47
- package/dist/lib/decisionsDigest.js.map +1 -1
- package/dist/lib/egressPolicy.d.ts +2 -2
- package/dist/lib/egressPolicy.js +2 -2
- package/dist/lib/mcpRegistryPy.d.ts +11 -0
- package/dist/lib/mcpRegistryPy.d.ts.map +1 -0
- package/dist/lib/mcpRegistryPy.js +173 -0
- package/dist/lib/mcpRegistryPy.js.map +1 -0
- package/dist/lib/phrasesPy.d.ts +11 -0
- package/dist/lib/phrasesPy.d.ts.map +1 -0
- package/dist/lib/phrasesPy.js +351 -0
- package/dist/lib/phrasesPy.js.map +1 -0
- package/dist/lib/policySnapshot.d.ts +63 -9
- package/dist/lib/policySnapshot.d.ts.map +1 -1
- package/dist/lib/policySnapshot.js +269 -45
- package/dist/lib/policySnapshot.js.map +1 -1
- package/dist/lib/processPoller.d.ts.map +1 -1
- package/dist/lib/processPoller.js +9 -2
- package/dist/lib/processPoller.js.map +1 -1
- package/dist/lib/regexLinearity.d.ts +43 -0
- package/dist/lib/regexLinearity.d.ts.map +1 -0
- package/dist/lib/regexLinearity.js +275 -0
- package/dist/lib/regexLinearity.js.map +1 -0
- package/dist/lib/runtimeEnv.d.ts +1 -3
- package/dist/lib/runtimeEnv.d.ts.map +1 -1
- package/dist/lib/runtimeEnv.js +0 -2
- package/dist/lib/runtimeEnv.js.map +1 -1
- package/dist/lib/sequencePy.d.ts +10 -0
- package/dist/lib/sequencePy.d.ts.map +1 -0
- package/dist/lib/sequencePy.js +102 -0
- package/dist/lib/sequencePy.js.map +1 -0
- package/dist/sessionReporter.d.ts +18 -12
- package/dist/sessionReporter.d.ts.map +1 -1
- package/dist/sessionReporter.js +37 -24
- package/dist/sessionReporter.js.map +1 -1
- package/dist/skillWriter.d.ts +23 -5
- package/dist/skillWriter.d.ts.map +1 -1
- package/dist/skillWriter.js +159 -16
- package/dist/skillWriter.js.map +1 -1
- package/dist/syncCycle.d.ts +142 -0
- package/dist/syncCycle.d.ts.map +1 -0
- package/dist/syncCycle.js +327 -0
- package/dist/syncCycle.js.map +1 -0
- package/dist/trajectoryMonitor.d.ts +2 -4
- package/dist/trajectoryMonitor.d.ts.map +1 -1
- package/dist/trajectoryMonitor.js +2 -5
- package/dist/trajectoryMonitor.js.map +1 -1
- package/dist/watcher/driftWatcher.d.ts +0 -1
- package/dist/watcher/driftWatcher.d.ts.map +1 -1
- package/dist/watcher/driftWatcher.js +1 -2
- package/dist/watcher/driftWatcher.js.map +1 -1
- package/dist/watcher/settingsGuard.d.ts +36 -7
- package/dist/watcher/settingsGuard.d.ts.map +1 -1
- package/dist/watcher/settingsGuard.js +397 -60
- package/dist/watcher/settingsGuard.js.map +1 -1
- package/dist/watcher/vscodeHookSettings.d.ts +30 -0
- package/dist/watcher/vscodeHookSettings.d.ts.map +1 -0
- package/dist/watcher/vscodeHookSettings.js +170 -0
- package/dist/watcher/vscodeHookSettings.js.map +1 -0
- package/dist/wsClient.d.ts +0 -1
- package/dist/wsClient.d.ts.map +1 -1
- package/dist/wsClient.js +0 -1
- package/dist/wsClient.js.map +1 -1
- package/package.json +5 -5
- package/dist/harness/claudeDesktopHooks.d.ts +0 -30
- package/dist/harness/claudeDesktopHooks.d.ts.map +0 -1
- package/dist/harness/claudeDesktopHooks.js +0 -254
- package/dist/harness/claudeDesktopHooks.js.map +0 -1
- package/dist/harness/continueHooks.d.ts +0 -63
- package/dist/harness/continueHooks.d.ts.map +0 -1
- package/dist/harness/continueHooks.js +0 -247
- package/dist/harness/continueHooks.js.map +0 -1
- package/dist/harness/gateKind.d.ts +0 -164
- package/dist/harness/gateKind.d.ts.map +0 -1
- package/dist/harness/gateKind.js +0 -144
- package/dist/harness/gateKind.js.map +0 -1
- package/dist/harness/rooCodeHooks.d.ts +0 -31
- package/dist/harness/rooCodeHooks.d.ts.map +0 -1
- package/dist/harness/rooCodeHooks.js +0 -286
- package/dist/harness/rooCodeHooks.js.map +0 -1
- package/dist/hashReporter.d.ts +0 -33
- package/dist/hashReporter.d.ts.map +0 -1
- package/dist/hashReporter.js +0 -69
- package/dist/hashReporter.js.map +0 -1
- package/dist/integrityStore.d.ts +0 -35
- package/dist/integrityStore.d.ts.map +0 -1
- package/dist/integrityStore.js +0 -71
- package/dist/integrityStore.js.map +0 -1
- package/dist/lib/gitWorktrees.d.ts +0 -140
- package/dist/lib/gitWorktrees.d.ts.map +0 -1
- package/dist/lib/gitWorktrees.js +0 -191
- package/dist/lib/gitWorktrees.js.map +0 -1
- package/dist/syncLoop.d.ts +0 -172
- package/dist/syncLoop.d.ts.map +0 -1
- package/dist/syncLoop.js +0 -878
- 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
|
|
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
|
|
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
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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.
|
|
60
|
-
*
|
|
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 —
|
|
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`
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
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: `
|
|
79
|
-
* `
|
|
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
|
-
*
|
|
144
|
-
*
|
|
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
|
|
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`
|
|
343
|
-
*
|
|
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
|
|
384
|
-
|
|
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
|
|
474
|
-
return { name
|
|
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
|
-
// ───
|
|
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
|
-
|
|
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
|
|
914
|
-
* project
|
|
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
|
-
|
|
918
|
-
|
|
919
|
-
const
|
|
920
|
-
const servers = { ...(current.
|
|
921
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1155
|
-
*
|
|
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
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|