@intutic/sync-daemon 1.10.0 → 2.0.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 +2 -2
- package/dist/agentReporter.d.ts +205 -0
- package/dist/agentReporter.d.ts.map +1 -0
- package/dist/agentReporter.js +394 -0
- package/dist/agentReporter.js.map +1 -0
- package/dist/configReader.d.ts +39 -2
- package/dist/configReader.d.ts.map +1 -1
- package/dist/configReader.js +56 -2
- package/dist/configReader.js.map +1 -1
- package/dist/configWriter.d.ts +51 -1
- package/dist/configWriter.d.ts.map +1 -1
- package/dist/configWriter.js +490 -63
- package/dist/configWriter.js.map +1 -1
- package/dist/harness/antigravityHooks.d.ts.map +1 -1
- package/dist/harness/antigravityHooks.js +33 -55
- package/dist/harness/antigravityHooks.js.map +1 -1
- package/dist/harness/claudeCodeHooks.d.ts +47 -13
- package/dist/harness/claudeCodeHooks.d.ts.map +1 -1
- package/dist/harness/claudeCodeHooks.js +316 -54
- package/dist/harness/claudeCodeHooks.js.map +1 -1
- package/dist/harness/claudeDesktopHooks.d.ts.map +1 -1
- package/dist/harness/claudeDesktopHooks.js +35 -48
- package/dist/harness/claudeDesktopHooks.js.map +1 -1
- package/dist/harness/clineHooks.d.ts.map +1 -1
- package/dist/harness/clineHooks.js +42 -34
- package/dist/harness/clineHooks.js.map +1 -1
- package/dist/harness/codexHooks.d.ts +36 -0
- package/dist/harness/codexHooks.d.ts.map +1 -0
- package/dist/harness/codexHooks.js +227 -0
- package/dist/harness/codexHooks.js.map +1 -0
- package/dist/harness/continueHooks.d.ts +45 -5
- package/dist/harness/continueHooks.d.ts.map +1 -1
- package/dist/harness/continueHooks.js +225 -5
- package/dist/harness/continueHooks.js.map +1 -1
- package/dist/harness/cursorHooks.d.ts +31 -0
- package/dist/harness/cursorHooks.d.ts.map +1 -1
- package/dist/harness/cursorHooks.js +78 -49
- package/dist/harness/cursorHooks.js.map +1 -1
- package/dist/harness/dshHooks.d.ts +198 -0
- package/dist/harness/dshHooks.d.ts.map +1 -0
- package/dist/harness/dshHooks.js +569 -0
- package/dist/harness/dshHooks.js.map +1 -0
- package/dist/harness/gateBody.d.ts +422 -0
- package/dist/harness/gateBody.d.ts.map +1 -0
- package/dist/harness/gateBody.js +1816 -0
- package/dist/harness/gateBody.js.map +1 -0
- package/dist/harness/gateKind.d.ts +164 -0
- package/dist/harness/gateKind.d.ts.map +1 -0
- package/dist/harness/gateKind.js +143 -0
- package/dist/harness/gateKind.js.map +1 -0
- package/dist/harness/githubCopilotHooks.d.ts +54 -0
- package/dist/harness/githubCopilotHooks.d.ts.map +1 -0
- package/dist/harness/githubCopilotHooks.js +218 -0
- package/dist/harness/githubCopilotHooks.js.map +1 -0
- package/dist/harness/gooseHooks.d.ts.map +1 -1
- package/dist/harness/gooseHooks.js +67 -37
- package/dist/harness/gooseHooks.js.map +1 -1
- package/dist/harness/grokHooks.d.ts +77 -0
- package/dist/harness/grokHooks.d.ts.map +1 -0
- package/dist/harness/grokHooks.js +350 -0
- package/dist/harness/grokHooks.js.map +1 -0
- package/dist/harness/hermesHooks.d.ts.map +1 -1
- package/dist/harness/hermesHooks.js +26 -55
- package/dist/harness/hermesHooks.js.map +1 -1
- package/dist/harness/holdRedaction.d.ts +69 -0
- package/dist/harness/holdRedaction.d.ts.map +1 -0
- package/dist/harness/holdRedaction.js +211 -0
- package/dist/harness/holdRedaction.js.map +1 -0
- package/dist/harness/jetbrainsXmlConfig.d.ts +51 -0
- package/dist/harness/jetbrainsXmlConfig.d.ts.map +1 -0
- package/dist/harness/jetbrainsXmlConfig.js +151 -0
- package/dist/harness/jetbrainsXmlConfig.js.map +1 -0
- package/dist/harness/mcpAutoWrite.d.ts +100 -3
- package/dist/harness/mcpAutoWrite.d.ts.map +1 -1
- package/dist/harness/mcpAutoWrite.js +712 -90
- package/dist/harness/mcpAutoWrite.js.map +1 -1
- package/dist/harness/museHooks.d.ts +53 -0
- package/dist/harness/museHooks.d.ts.map +1 -0
- package/dist/harness/museHooks.js +273 -0
- package/dist/harness/museHooks.js.map +1 -0
- package/dist/harness/n8nHooks.d.ts +25 -9
- package/dist/harness/n8nHooks.d.ts.map +1 -1
- package/dist/harness/n8nHooks.js +174 -9
- package/dist/harness/n8nHooks.js.map +1 -1
- package/dist/harness/openCodeHooks.d.ts +93 -0
- package/dist/harness/openCodeHooks.d.ts.map +1 -0
- package/dist/harness/openCodeHooks.js +248 -0
- package/dist/harness/openCodeHooks.js.map +1 -0
- package/dist/harness/openWebuiHooks.d.ts.map +1 -1
- package/dist/harness/openWebuiHooks.js +56 -11
- package/dist/harness/openWebuiHooks.js.map +1 -1
- package/dist/harness/openclawHooks.d.ts.map +1 -1
- package/dist/harness/openclawHooks.js +30 -54
- package/dist/harness/openclawHooks.js.map +1 -1
- package/dist/harness/openhandsHooks.d.ts.map +1 -1
- package/dist/harness/openhandsHooks.js +35 -41
- package/dist/harness/openhandsHooks.js.map +1 -1
- package/dist/harness/piHooks.d.ts.map +1 -1
- package/dist/harness/piHooks.js +31 -58
- package/dist/harness/piHooks.js.map +1 -1
- package/dist/harness/protectedPaths.d.ts +353 -0
- package/dist/harness/protectedPaths.d.ts.map +1 -0
- package/dist/harness/protectedPaths.js +805 -0
- package/dist/harness/protectedPaths.js.map +1 -0
- package/dist/harness/rooCodeHooks.d.ts.map +1 -1
- package/dist/harness/rooCodeHooks.js +34 -48
- package/dist/harness/rooCodeHooks.js.map +1 -1
- package/dist/harness/windsurfHooks.d.ts +72 -0
- package/dist/harness/windsurfHooks.d.ts.map +1 -1
- package/dist/harness/windsurfHooks.js +172 -58
- package/dist/harness/windsurfHooks.js.map +1 -1
- package/dist/harness/windsurfJetBrainsProxy.d.ts +78 -0
- package/dist/harness/windsurfJetBrainsProxy.d.ts.map +1 -0
- package/dist/harness/windsurfJetBrainsProxy.js +159 -0
- package/dist/harness/windsurfJetBrainsProxy.js.map +1 -0
- package/dist/hashReporter.js +1 -1
- package/dist/hashReporter.js.map +1 -1
- package/dist/index.d.ts +12 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -6
- package/dist/index.js.map +1 -1
- package/dist/integrityStore.d.ts +0 -8
- package/dist/integrityStore.d.ts.map +1 -1
- package/dist/integrityStore.js +6 -29
- package/dist/integrityStore.js.map +1 -1
- package/dist/lib/approvedBypasses.d.ts +76 -0
- package/dist/lib/approvedBypasses.d.ts.map +1 -0
- package/dist/lib/approvedBypasses.js +140 -0
- package/dist/lib/approvedBypasses.js.map +1 -0
- package/dist/lib/complianceProbes.d.ts.map +1 -1
- package/dist/lib/complianceProbes.js +5 -0
- package/dist/lib/complianceProbes.js.map +1 -1
- package/dist/lib/decisionsDigest.d.ts +99 -0
- package/dist/lib/decisionsDigest.d.ts.map +1 -0
- package/dist/lib/decisionsDigest.js +199 -0
- package/dist/lib/decisionsDigest.js.map +1 -0
- package/dist/lib/egressPolicy.d.ts +69 -0
- package/dist/lib/egressPolicy.d.ts.map +1 -0
- package/dist/lib/egressPolicy.js +126 -0
- package/dist/lib/egressPolicy.js.map +1 -0
- package/dist/lib/gitWorktrees.d.ts +140 -0
- package/dist/lib/gitWorktrees.d.ts.map +1 -0
- package/dist/lib/gitWorktrees.js +191 -0
- package/dist/lib/gitWorktrees.js.map +1 -0
- package/dist/lib/policySnapshot.d.ts +224 -0
- package/dist/lib/policySnapshot.d.ts.map +1 -0
- package/dist/lib/policySnapshot.js +637 -0
- package/dist/lib/policySnapshot.js.map +1 -0
- package/dist/lib/processPoller.d.ts +44 -0
- package/dist/lib/processPoller.d.ts.map +1 -1
- package/dist/lib/processPoller.js +176 -1
- package/dist/lib/processPoller.js.map +1 -1
- package/dist/lib/runtimeEnv.d.ts +8 -0
- package/dist/lib/runtimeEnv.d.ts.map +1 -1
- package/dist/lib/runtimeEnv.js +3 -0
- package/dist/lib/runtimeEnv.js.map +1 -1
- package/dist/localTrajectoryHeuristics.d.ts +51 -0
- package/dist/localTrajectoryHeuristics.d.ts.map +1 -0
- package/dist/localTrajectoryHeuristics.js +90 -0
- package/dist/localTrajectoryHeuristics.js.map +1 -0
- package/dist/sessionReporter.d.ts +70 -0
- package/dist/sessionReporter.d.ts.map +1 -0
- package/dist/sessionReporter.js +140 -0
- package/dist/sessionReporter.js.map +1 -0
- package/dist/skillWriter.d.ts +1 -1
- package/dist/skillWriter.d.ts.map +1 -1
- package/dist/skillWriter.js +5 -2
- package/dist/skillWriter.js.map +1 -1
- package/dist/syncLoop.d.ts +105 -3
- package/dist/syncLoop.d.ts.map +1 -1
- package/dist/syncLoop.js +397 -65
- package/dist/syncLoop.js.map +1 -1
- package/dist/trajectoryMonitor.d.ts +2 -0
- package/dist/trajectoryMonitor.d.ts.map +1 -1
- package/dist/trajectoryMonitor.js +57 -0
- package/dist/trajectoryMonitor.js.map +1 -1
- package/dist/watcher/driftWatcher.d.ts.map +1 -1
- package/dist/watcher/driftWatcher.js +12 -3
- package/dist/watcher/driftWatcher.js.map +1 -1
- package/dist/watcher/settingsGuard.d.ts +23 -0
- package/dist/watcher/settingsGuard.d.ts.map +1 -1
- package/dist/watcher/settingsGuard.js +202 -17
- package/dist/watcher/settingsGuard.js.map +1 -1
- package/package.json +37 -6
- package/dist/brainIndexer.d.ts +0 -51
- package/dist/brainIndexer.d.ts.map +0 -1
- package/dist/brainIndexer.js +0 -148
- package/dist/brainIndexer.js.map +0 -1
- package/dist/harness/cursorHooksJson.d.ts +0 -29
- package/dist/harness/cursorHooksJson.d.ts.map +0 -1
- package/dist/harness/cursorHooksJson.js +0 -130
- package/dist/harness/cursorHooksJson.js.map +0 -1
- package/dist/harness/gooseHooksWriter.d.ts +0 -27
- package/dist/harness/gooseHooksWriter.d.ts.map +0 -1
- package/dist/harness/gooseHooksWriter.js +0 -147
- package/dist/harness/gooseHooksWriter.js.map +0 -1
- package/dist/harness/openhandsHooksWriter.d.ts +0 -23
- package/dist/harness/openhandsHooksWriter.d.ts.map +0 -1
- package/dist/harness/openhandsHooksWriter.js +0 -146
- package/dist/harness/openhandsHooksWriter.js.map +0 -1
|
@@ -12,11 +12,72 @@
|
|
|
12
12
|
* - Continue: ~/.continue/config.json (mcpServers section)
|
|
13
13
|
* - Goose: ~/.config/goose/config.yaml (mcp section)
|
|
14
14
|
* - OpenHands: <workspaceRoot>/.openhands/mcp.json
|
|
15
|
+
* - Muse Code: ~/.config/muse/settings.json (`mcp_servers` section)
|
|
16
|
+
* - Grok Build: ~/.grok/config.toml + <workspaceRoot>/.grok/config.toml
|
|
17
|
+
* ([mcp_servers.*] tables)
|
|
18
|
+
*
|
|
19
|
+
* That is 12 config paths across 10 `HarnessType` values (Cursor and Grok
|
|
20
|
+
* Build each own two paths — global/project for Cursor, user/project for
|
|
21
|
+
* Grok Build). `discoverMcpServers` below reads all twelve read-only, for
|
|
22
|
+
* reporting; the injectors above are the only thing that writes.
|
|
23
|
+
*
|
|
24
|
+
* # Grok Build's compat-path overlap — dedup, not a bug
|
|
25
|
+
*
|
|
26
|
+
* Grok Build ALSO natively reads `.cursor/mcp.json` (a compatibility
|
|
27
|
+
* feature) — which `injectCursor`/`discoverJsonObjectHarness` below ALREADY
|
|
28
|
+
* wrap/discover under `harness: 'cursor'`, independently of anything this
|
|
29
|
+
* file does for Grok Build specifically. A server declared in BOTH
|
|
30
|
+
* `.cursor/mcp.json` and Grok Build's own `[mcp_servers.*]` table therefore
|
|
31
|
+
* produces TWO rows in `discoverMcpServers`' output — one tagged `cursor`,
|
|
32
|
+
* one tagged `grok` — not one row silently merged or dropped. That is
|
|
33
|
+
* consistent with how every other harness pair already behaves here (this
|
|
34
|
+
* file reports per CONFIG FILE, never cross-harness-deduplicated), and it is
|
|
35
|
+
* the correct behaviour, not a defect to fix: the two configs are genuinely
|
|
36
|
+
* separate on disk, a user (or a future sync) could point them at different
|
|
37
|
+
* servers, and a naive same-name merge would hide that a `.cursor/mcp.json`
|
|
38
|
+
* entry is unwrapped while the `[mcp_servers.*]` entry with the same name is
|
|
39
|
+
* wrapped (or vice versa). See `grokHooks.test.ts`'s dedup test for the pin:
|
|
40
|
+
* the same server name in both files must total exactly two DISTINCT rows,
|
|
41
|
+
* never one collapsed row and never three.
|
|
42
|
+
*
|
|
43
|
+
* (Grok Build is also documented to read a bare project-root `.mcp.json` —
|
|
44
|
+
* a convention this file does not wrap or discover for ANY harness today,
|
|
45
|
+
* Grok Build included; that is a pre-existing gap in this file predating
|
|
46
|
+
* this harness, out of scope for this phase, not something newly introduced
|
|
47
|
+
* here.)
|
|
48
|
+
*
|
|
49
|
+
* Muse's `mcp_servers` map carries both `stdio` and `streamable_http` server
|
|
50
|
+
* entries, but its ON-DISK shape (`command`/`args`/`env` for stdio,
|
|
51
|
+
* `url`/`headers` for a remote transport) was ASSUMED to match the same
|
|
52
|
+
* `command`-or-`url` shape every other JSON-map harness here uses — `wrapAllServers`
|
|
53
|
+
* / `wrapWithProxy` branch on exactly that, so a `streamable_http` entry is
|
|
54
|
+
* wrapped through the SAME remote-bridge path a `url`-keyed Claude Desktop/
|
|
55
|
+
* Windsurf entry gets, without new code. See TD-362 for why this is recorded
|
|
56
|
+
* as an assumption rather than a confirmation.
|
|
15
57
|
*
|
|
16
58
|
* Proxy-wrapping convention:
|
|
17
|
-
* Each existing MCP server entry is rewritten so that the governance
|
|
18
|
-
* binary is the command, and the original server command is passed
|
|
19
|
-
*
|
|
59
|
+
* Each existing stdio MCP server entry is rewritten so that the governance
|
|
60
|
+
* proxy binary is the command, and the original server command is passed
|
|
61
|
+
* after `--`, tagged with `--server-name <name>` so the proxy can attribute
|
|
62
|
+
* traffic to the server it fronts. A `__intutic_wrapped: true` flag is added
|
|
63
|
+
* to prevent double-wrapping. Remote (HTTP/SSE, `url`-keyed) entries are now
|
|
64
|
+
* ALSO wrapped — TD-354's stdio→HTTP bridge phase closed the gap this
|
|
65
|
+
* comment used to describe as "left untouched": the entry is rewritten to
|
|
66
|
+
* spawn the SAME proxy binary in bridge mode (`--remote-url`/
|
|
67
|
+
* `--remote-transport`, headers riding in `INTUTIC_REMOTE_HEADERS` env, not
|
|
68
|
+
* argv — `ps` visibility). `__intutic_original` preserves the entry's
|
|
69
|
+
* pre-wrap `url`/`type`/`headers` so `classifyEntry` can report the TRUE
|
|
70
|
+
* transport of what got wrapped, and so a wrapped entry could be unwrapped
|
|
71
|
+
* later. See TECH_DEBT.md TD-354 for the historical decline this phase
|
|
72
|
+
* supersedes.
|
|
73
|
+
*
|
|
74
|
+
* Continuous invariant, not one-shot: `injectMcpServer` is called once from
|
|
75
|
+
* `intutic connect` (tools/cli) AND once per sync-loop iteration
|
|
76
|
+
* (services/sync-daemon/src/syncLoop.ts), so a server a user adds to a harness
|
|
77
|
+
* config after their first `connect` still gets wrapped on the next sync
|
|
78
|
+
* cycle rather than staying invisible to governance forever. Running this
|
|
79
|
+
* every ~30s only works because `writeJsonFile` below is write-if-changed —
|
|
80
|
+
* an already-wrapped, unchanged config produces zero bytes written.
|
|
20
81
|
*
|
|
21
82
|
* LLD #14 — mcpAutoWrite.ts
|
|
22
83
|
* HLD §3.14 — GUI Harness Interception (MCP registration + Universal MCP Governance)
|
|
@@ -27,7 +88,10 @@ import * as node_fs from 'node:fs/promises';
|
|
|
27
88
|
import * as node_path from 'node:path';
|
|
28
89
|
import * as node_os from 'node:os';
|
|
29
90
|
import { existsSync } from 'node:fs';
|
|
91
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
30
92
|
import { createLogger } from '@intutic/logger';
|
|
93
|
+
import { parseDocument, isMap } from 'yaml';
|
|
94
|
+
import { parse as parseToml, stringify as stringifyToml } from 'smol-toml';
|
|
31
95
|
const log = createLogger('sync-mcp-autowrite');
|
|
32
96
|
// ─── Proxy Binary Resolution ─────────────────────────────────────────────────
|
|
33
97
|
/**
|
|
@@ -50,31 +114,138 @@ function resolveProxyBin(workspaceRoot) {
|
|
|
50
114
|
const pkgPath = node_path.join(workspaceRoot, 'packages', 'mcp-proxy', 'dist', 'index.js');
|
|
51
115
|
return pkgPath;
|
|
52
116
|
}
|
|
117
|
+
// ─── Config Path Resolution ───────────────────────────────────────────────────
|
|
118
|
+
//
|
|
119
|
+
// Shared by both the injectors (which write) and discoverMcpServers (which
|
|
120
|
+
// only reads), so the two can never independently drift about where a
|
|
121
|
+
// harness keeps its config.
|
|
122
|
+
function claudeCodeConfigPath() {
|
|
123
|
+
return node_path.join(node_os.homedir(), '.claude', 'mcp.json');
|
|
124
|
+
}
|
|
125
|
+
function claudeDesktopConfigPath() {
|
|
126
|
+
const home = node_os.homedir();
|
|
127
|
+
if (process.platform === 'darwin') {
|
|
128
|
+
return node_path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
129
|
+
}
|
|
130
|
+
else if (process.platform === 'win32') {
|
|
131
|
+
return node_path.join(process.env['APPDATA'] ?? '', 'Claude', 'claude_desktop_config.json');
|
|
132
|
+
}
|
|
133
|
+
return node_path.join(home, '.config', 'Claude', 'claude_desktop_config.json');
|
|
134
|
+
}
|
|
135
|
+
function cursorGlobalConfigPath() {
|
|
136
|
+
const home = node_os.homedir();
|
|
137
|
+
if (process.platform === 'darwin') {
|
|
138
|
+
return node_path.join(home, 'Library', 'Application Support', 'Cursor', 'User', 'globalSettings.json');
|
|
139
|
+
}
|
|
140
|
+
else if (process.platform === 'win32') {
|
|
141
|
+
return node_path.join(process.env['APPDATA'] ?? '', 'Cursor', 'User', 'globalSettings.json');
|
|
142
|
+
}
|
|
143
|
+
return node_path.join(home, '.config', 'Cursor', 'User', 'globalSettings.json');
|
|
144
|
+
}
|
|
145
|
+
function cursorProjectConfigPath(workspaceRoot) {
|
|
146
|
+
return node_path.join(workspaceRoot, '.cursor', 'mcp.json');
|
|
147
|
+
}
|
|
148
|
+
function clineConfigPath(workspaceRoot) {
|
|
149
|
+
return node_path.join(workspaceRoot, '.cline', 'mcp.json');
|
|
150
|
+
}
|
|
151
|
+
function windsurfConfigPath() {
|
|
152
|
+
return node_path.join(node_os.homedir(), '.codeium', 'windsurf', 'mcp_config.json');
|
|
153
|
+
}
|
|
154
|
+
function continueConfigPath() {
|
|
155
|
+
return node_path.join(node_os.homedir(), '.continue', 'config.json');
|
|
156
|
+
}
|
|
157
|
+
function gooseConfigPath() {
|
|
158
|
+
return node_path.join(node_os.homedir(), '.config', 'goose', 'config.yaml');
|
|
159
|
+
}
|
|
160
|
+
function openHandsConfigPath(workspaceRoot) {
|
|
161
|
+
return node_path.join(workspaceRoot, '.openhands', 'mcp.json');
|
|
162
|
+
}
|
|
163
|
+
function museConfigPath() {
|
|
164
|
+
return node_path.join(node_os.homedir(), '.config', 'muse', 'settings.json');
|
|
165
|
+
}
|
|
166
|
+
function grokUserConfigPath() {
|
|
167
|
+
return node_path.join(node_os.homedir(), '.grok', 'config.toml');
|
|
168
|
+
}
|
|
169
|
+
function grokProjectConfigPath(workspaceRoot) {
|
|
170
|
+
return node_path.join(workspaceRoot, '.grok', 'config.toml');
|
|
171
|
+
}
|
|
53
172
|
// ─── Proxy Wrapping ───────────────────────────────────────────────────────────
|
|
54
173
|
/**
|
|
55
174
|
* Wrap a single MCP server entry with the governance proxy.
|
|
56
|
-
*
|
|
175
|
+
*
|
|
176
|
+
* Returns the entry unmodified if it's already wrapped, or if its shape is
|
|
177
|
+
* neither a stdio entry (`command`) nor a remote entry (`url`) — an unknown
|
|
178
|
+
* shape has nothing safe to rewrite it into.
|
|
179
|
+
*
|
|
180
|
+
* Two wrap conventions, chosen by the entry's pre-wrap shape:
|
|
181
|
+
* - stdio (`command`/`args`): the proxy binary becomes the command, the
|
|
182
|
+
* original command is passed after `--`, tagged `--server-name`.
|
|
183
|
+
* - remote (`url`, HTTP/SSE — TD-354, now superseded): the proxy binary is
|
|
184
|
+
* invoked in bridge mode (`--remote-url`/`--remote-transport` instead of
|
|
185
|
+
* `--`), also tagged `--server-name`. Any `headers` on the original entry
|
|
186
|
+
* ride into the wrapped entry's env as `INTUTIC_REMOTE_HEADERS` (a JSON
|
|
187
|
+
* object string) — never argv, for the same `ps`-visibility reasoning
|
|
188
|
+
* `packages/mcp-proxy/src/config.ts` documents for `--remote-url` itself.
|
|
189
|
+
* Both conventions end up with the SAME top-level shape (`command: 'node'`,
|
|
190
|
+
* `args: […]`) since the harness only knows how to spawn a stdio process —
|
|
191
|
+
* `__intutic_original` is what lets `classifyEntry` tell them apart again.
|
|
57
192
|
*/
|
|
58
|
-
function wrapWithProxy(entry, workspaceId, workspaceRoot) {
|
|
193
|
+
function wrapWithProxy(entry, workspaceId, workspaceRoot, serverName) {
|
|
59
194
|
if (entry.__intutic_wrapped)
|
|
60
195
|
return entry;
|
|
61
196
|
const proxyBin = resolveProxyBin(workspaceRoot);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
197
|
+
if (typeof entry.command === 'string') {
|
|
198
|
+
const originalArgs = entry.args ?? [];
|
|
199
|
+
return {
|
|
200
|
+
command: 'node',
|
|
201
|
+
args: [
|
|
202
|
+
proxyBin,
|
|
203
|
+
'--workspace-id', workspaceId,
|
|
204
|
+
'--server-name', serverName,
|
|
205
|
+
'--',
|
|
206
|
+
entry.command,
|
|
207
|
+
...originalArgs,
|
|
208
|
+
],
|
|
209
|
+
env: {
|
|
210
|
+
...(entry.env ?? {}),
|
|
211
|
+
INTUTIC_WORKSPACE_ID: workspaceId,
|
|
212
|
+
},
|
|
213
|
+
__intutic_wrapped: true,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (typeof entry.url === 'string') {
|
|
217
|
+
const remoteTransport = entry.type?.toLowerCase() === 'sse' ? 'sse' : 'http';
|
|
218
|
+
const hasHeaders = entry.headers !== undefined && Object.keys(entry.headers).length > 0;
|
|
219
|
+
const original = { url: entry.url };
|
|
220
|
+
if (entry.type !== undefined)
|
|
221
|
+
original.type = entry.type;
|
|
222
|
+
if (entry.headers !== undefined)
|
|
223
|
+
original.headers = entry.headers;
|
|
224
|
+
const env = {
|
|
73
225
|
...(entry.env ?? {}),
|
|
74
226
|
INTUTIC_WORKSPACE_ID: workspaceId,
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
227
|
+
};
|
|
228
|
+
if (hasHeaders) {
|
|
229
|
+
env['INTUTIC_REMOTE_HEADERS'] = JSON.stringify(entry.headers);
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
command: 'node',
|
|
233
|
+
args: [
|
|
234
|
+
proxyBin,
|
|
235
|
+
'--workspace-id', workspaceId,
|
|
236
|
+
'--server-name', serverName,
|
|
237
|
+
'--remote-url', entry.url,
|
|
238
|
+
'--remote-transport', remoteTransport,
|
|
239
|
+
],
|
|
240
|
+
env,
|
|
241
|
+
__intutic_wrapped: true,
|
|
242
|
+
__intutic_original: original,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
// Neither a stdio command nor a remote url — an unrecognised shape has
|
|
246
|
+
// nothing safe to rewrite it into. Leave it alone rather than building an
|
|
247
|
+
// args array around `undefined`.
|
|
248
|
+
return entry;
|
|
78
249
|
}
|
|
79
250
|
/**
|
|
80
251
|
* Wrap all servers in an mcpServers map, preserving the `intutic` entry as-is.
|
|
@@ -89,7 +260,7 @@ function wrapAllServers(servers, workspaceId, workspaceRoot) {
|
|
|
89
260
|
result[name] = entry;
|
|
90
261
|
}
|
|
91
262
|
else {
|
|
92
|
-
result[name] = wrapWithProxy(entry, workspaceId, workspaceRoot);
|
|
263
|
+
result[name] = wrapWithProxy(entry, workspaceId, workspaceRoot, name);
|
|
93
264
|
}
|
|
94
265
|
}
|
|
95
266
|
return result;
|
|
@@ -120,14 +291,39 @@ async function readJsonFile(filePath, fallback) {
|
|
|
120
291
|
return fallback;
|
|
121
292
|
}
|
|
122
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Write JSON to disk, but only if the content actually changed.
|
|
296
|
+
*
|
|
297
|
+
* `injectMcpServer` now runs every sync-loop iteration (~every 30s, see
|
|
298
|
+
* syncLoop.ts) instead of only once at `connect` time — re-running the same
|
|
299
|
+
* wrap against an already-wrapped, unchanged config must not touch the file,
|
|
300
|
+
* or every cycle would churn the file's mtime and fire a spurious inotify /
|
|
301
|
+
* FSEvents event for every harness config on every developer machine.
|
|
302
|
+
*
|
|
303
|
+
* Compares the parsed object structurally (`isDeepStrictEqual`), not the
|
|
304
|
+
* serialized string — a naive string compare would report a false "changed"
|
|
305
|
+
* on nothing more than JSON.stringify key-ordering differences between two
|
|
306
|
+
* semantically identical objects.
|
|
307
|
+
*/
|
|
123
308
|
async function writeJsonFile(filePath, data) {
|
|
309
|
+
let existing;
|
|
310
|
+
let hasExisting;
|
|
311
|
+
try {
|
|
312
|
+
existing = JSON.parse(await node_fs.readFile(filePath, 'utf-8'));
|
|
313
|
+
hasExisting = true;
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
hasExisting = false;
|
|
317
|
+
}
|
|
318
|
+
if (hasExisting && isDeepStrictEqual(existing, data)) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
124
321
|
await node_fs.mkdir(node_path.dirname(filePath), { recursive: true });
|
|
125
322
|
await node_fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
126
323
|
}
|
|
127
324
|
// ─── Target: Claude Code ─────────────────────────────────────────────────────
|
|
128
325
|
async function injectClaudeCode(workspaceId, workspaceRoot) {
|
|
129
|
-
const
|
|
130
|
-
const configPath = node_path.join(home, '.claude', 'mcp.json');
|
|
326
|
+
const configPath = claudeCodeConfigPath();
|
|
131
327
|
const current = await readJsonFile(configPath, {});
|
|
132
328
|
current.mcpServers = wrapAllServers({ intutic: buildIntuticMcpEntry(workspaceRoot), ...(current.mcpServers ?? {}) }, workspaceId, workspaceRoot);
|
|
133
329
|
await writeJsonFile(configPath, current);
|
|
@@ -135,17 +331,7 @@ async function injectClaudeCode(workspaceId, workspaceRoot) {
|
|
|
135
331
|
}
|
|
136
332
|
// ─── Target: Claude Desktop ───────────────────────────────────────────────────
|
|
137
333
|
async function injectClaudeDesktop(workspaceId, workspaceRoot) {
|
|
138
|
-
const
|
|
139
|
-
let configPath;
|
|
140
|
-
if (process.platform === 'darwin') {
|
|
141
|
-
configPath = node_path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
142
|
-
}
|
|
143
|
-
else if (process.platform === 'win32') {
|
|
144
|
-
configPath = node_path.join(process.env['APPDATA'] ?? '', 'Claude', 'claude_desktop_config.json');
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
configPath = node_path.join(home, '.config', 'Claude', 'claude_desktop_config.json');
|
|
148
|
-
}
|
|
334
|
+
const configPath = claudeDesktopConfigPath();
|
|
149
335
|
// Skip if Claude Desktop directory doesn't exist (not installed)
|
|
150
336
|
try {
|
|
151
337
|
await node_fs.access(node_path.dirname(configPath));
|
|
@@ -161,18 +347,8 @@ async function injectClaudeDesktop(workspaceId, workspaceRoot) {
|
|
|
161
347
|
}
|
|
162
348
|
// ─── Target: Cursor (global + project) ───────────────────────────────────────
|
|
163
349
|
async function injectCursor(workspaceId, workspaceRoot) {
|
|
164
|
-
const home = node_os.homedir();
|
|
165
350
|
// Global settings
|
|
166
|
-
|
|
167
|
-
if (process.platform === 'darwin') {
|
|
168
|
-
globalPath = node_path.join(home, 'Library', 'Application Support', 'Cursor', 'User', 'globalSettings.json');
|
|
169
|
-
}
|
|
170
|
-
else if (process.platform === 'win32') {
|
|
171
|
-
globalPath = node_path.join(process.env['APPDATA'] ?? '', 'Cursor', 'User', 'globalSettings.json');
|
|
172
|
-
}
|
|
173
|
-
else {
|
|
174
|
-
globalPath = node_path.join(home, '.config', 'Cursor', 'User', 'globalSettings.json');
|
|
175
|
-
}
|
|
351
|
+
const globalPath = cursorGlobalConfigPath();
|
|
176
352
|
try {
|
|
177
353
|
await node_fs.access(node_path.dirname(globalPath));
|
|
178
354
|
const current = await readJsonFile(globalPath, {});
|
|
@@ -184,7 +360,7 @@ async function injectCursor(workspaceId, workspaceRoot) {
|
|
|
184
360
|
log.debug({ action: 'cursor_global_skip' }, 'Cursor not installed — skipping global settings');
|
|
185
361
|
}
|
|
186
362
|
// Project-level .cursor/mcp.json
|
|
187
|
-
const projectPath =
|
|
363
|
+
const projectPath = cursorProjectConfigPath(workspaceRoot);
|
|
188
364
|
try {
|
|
189
365
|
const current = await readJsonFile(projectPath, {});
|
|
190
366
|
current.mcpServers = wrapAllServers({ intutic: buildIntuticMcpEntry(workspaceRoot), ...(current.mcpServers ?? {}) }, workspaceId, workspaceRoot);
|
|
@@ -197,7 +373,7 @@ async function injectCursor(workspaceId, workspaceRoot) {
|
|
|
197
373
|
}
|
|
198
374
|
// ─── Target: Cline ────────────────────────────────────────────────────────────
|
|
199
375
|
async function injectCline(workspaceId, workspaceRoot) {
|
|
200
|
-
const configPath =
|
|
376
|
+
const configPath = clineConfigPath(workspaceRoot);
|
|
201
377
|
try {
|
|
202
378
|
const current = await readJsonFile(configPath, {});
|
|
203
379
|
current.mcpServers = wrapAllServers({ intutic: buildIntuticMcpEntry(workspaceRoot), ...(current.mcpServers ?? {}) }, workspaceId, workspaceRoot);
|
|
@@ -210,8 +386,7 @@ async function injectCline(workspaceId, workspaceRoot) {
|
|
|
210
386
|
}
|
|
211
387
|
// ─── Target: Windsurf ─────────────────────────────────────────────────────────
|
|
212
388
|
async function injectWindsurf(workspaceId, workspaceRoot) {
|
|
213
|
-
const
|
|
214
|
-
const configPath = node_path.join(home, '.codeium', 'windsurf', 'mcp_config.json');
|
|
389
|
+
const configPath = windsurfConfigPath();
|
|
215
390
|
try {
|
|
216
391
|
await node_fs.access(node_path.dirname(configPath));
|
|
217
392
|
const current = await readJsonFile(configPath, {});
|
|
@@ -224,8 +399,7 @@ async function injectWindsurf(workspaceId, workspaceRoot) {
|
|
|
224
399
|
}
|
|
225
400
|
}
|
|
226
401
|
async function injectContinue(workspaceId, workspaceRoot) {
|
|
227
|
-
const
|
|
228
|
-
const configPath = node_path.join(home, '.continue', 'config.json');
|
|
402
|
+
const configPath = continueConfigPath();
|
|
229
403
|
try {
|
|
230
404
|
await node_fs.access(node_path.dirname(configPath));
|
|
231
405
|
const current = await readJsonFile(configPath, {});
|
|
@@ -235,23 +409,12 @@ async function injectContinue(workspaceId, workspaceRoot) {
|
|
|
235
409
|
}
|
|
236
410
|
// Remove any existing intutic entry and add the wrapped one
|
|
237
411
|
current.mcpServers = current.mcpServers.filter((s) => s.name !== 'intutic');
|
|
238
|
-
// Wrap existing non-intutic servers
|
|
412
|
+
// Wrap existing non-intutic servers — routed through the same
|
|
413
|
+
// wrapWithProxy used by every other harness, so the stdio-only guard and
|
|
414
|
+
// --server-name threading stay in one place instead of two.
|
|
239
415
|
current.mcpServers = current.mcpServers.map((s) => {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
name: s.name,
|
|
244
|
-
command: 'node',
|
|
245
|
-
args: [
|
|
246
|
-
resolveProxyBin(workspaceRoot),
|
|
247
|
-
'--workspace-id', workspaceId,
|
|
248
|
-
'--',
|
|
249
|
-
s.command,
|
|
250
|
-
...(s.args ?? []),
|
|
251
|
-
],
|
|
252
|
-
env: { ...(s.env ?? {}), INTUTIC_WORKSPACE_ID: workspaceId },
|
|
253
|
-
__intutic_wrapped: true,
|
|
254
|
-
};
|
|
416
|
+
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);
|
|
417
|
+
return { name: s.name, ...wrapped };
|
|
255
418
|
});
|
|
256
419
|
// Add Intutic MCP server
|
|
257
420
|
const entry = buildIntuticMcpEntry(workspaceRoot);
|
|
@@ -264,40 +427,132 @@ async function injectContinue(workspaceId, workspaceRoot) {
|
|
|
264
427
|
}
|
|
265
428
|
}
|
|
266
429
|
// ─── Target: Goose ────────────────────────────────────────────────────────────
|
|
430
|
+
/**
|
|
431
|
+
* The pre-YAML-dep fallback: append-only string injection of an `intutic:`
|
|
432
|
+
* MCP block, never touching (or even parsing) anything else in the file.
|
|
433
|
+
* This is now ONLY the fallback path (see `injectGoose`) for a config.yaml
|
|
434
|
+
* that does not parse as YAML at all — a parser that cannot safely represent
|
|
435
|
+
* the file cannot safely round-trip it either, so falling back to "never
|
|
436
|
+
* touch anything but the block we control" is the safe degrade, not a
|
|
437
|
+
* regression. Never rewraps a pre-existing remote MCP server entry (it never
|
|
438
|
+
* looks at `mcp:` beyond the literal string `intutic:`), which is why
|
|
439
|
+
* `discoverGoose` cannot claim more than it can actually verify when THIS
|
|
440
|
+
* path was the one that ran.
|
|
441
|
+
*/
|
|
442
|
+
async function injectGooseAppendOnly(configPath, existingYaml, workspaceId, proxyBin) {
|
|
443
|
+
let text = existingYaml;
|
|
444
|
+
const intuticBlock = [
|
|
445
|
+
'mcp:',
|
|
446
|
+
' intutic:',
|
|
447
|
+
` command: node`,
|
|
448
|
+
` args: [${JSON.stringify(proxyBin)}, "--workspace-id", ${JSON.stringify(workspaceId)}]`,
|
|
449
|
+
].join('\n');
|
|
450
|
+
if (!text.includes('intutic:')) {
|
|
451
|
+
text = text.trimEnd() + '\n\n' + intuticBlock + '\n';
|
|
452
|
+
}
|
|
453
|
+
if (text === existingYaml) {
|
|
454
|
+
// Already present and unchanged — write-if-changed applies here too
|
|
455
|
+
// (see writeJsonFile's doc comment for why: this runs every sync cycle now).
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
await node_fs.mkdir(node_path.dirname(configPath), { recursive: true });
|
|
459
|
+
await node_fs.writeFile(configPath, text, 'utf-8');
|
|
460
|
+
log.info({ action: 'goose_mcp_injected', mode: 'append_only_fallback' }, 'Goose config.yaml updated (append-only fallback)');
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Structurally edits `~/.config/goose/config.yaml`'s `mcp:` block via the
|
|
464
|
+
* `yaml` package's `parseDocument` — unlike every other harness here (plain
|
|
465
|
+
* `JSON.parse`/`JSON.stringify`), this is a user's hand-maintained YAML file,
|
|
466
|
+
* and `parseDocument` preserves comments and formatting for everything this
|
|
467
|
+
* function does NOT touch (`Document#setIn` replaces only the specific
|
|
468
|
+
* `['mcp', <name>]` paths whose wrapped shape actually changed).
|
|
469
|
+
*
|
|
470
|
+
* Applies the SAME `wrapWithProxy` convention every other harness uses —
|
|
471
|
+
* stdio entries wrapped with `--`, remote (`url`-keyed) entries wrapped with
|
|
472
|
+
* `--remote-url`/`--remote-transport` — so a remote MCP server declared in
|
|
473
|
+
* Goose's config gets the identical bridge coverage a remote entry in
|
|
474
|
+
* `~/.claude/mcp.json` would get, not a second, divergent convention.
|
|
475
|
+
*
|
|
476
|
+
* Falls back to `injectGooseAppendOnly` (append-only text injection, the
|
|
477
|
+
* pre-existing behaviour) when the file does not parse as YAML at all —
|
|
478
|
+
* logged as `goose_yaml_unparseable` so the fallback is diagnosable rather
|
|
479
|
+
* than silently degrading coverage. A parser that cannot safely represent a
|
|
480
|
+
* malformed file cannot safely round-trip it either; corrupting a user's
|
|
481
|
+
* hand-maintained config is a worse failure than leaving non-`intutic`
|
|
482
|
+
* entries in that file unwrapped for one more sync cycle.
|
|
483
|
+
*/
|
|
267
484
|
async function injectGoose(workspaceId, workspaceRoot) {
|
|
268
|
-
const
|
|
269
|
-
const configPath = node_path.join(home, '.config', 'goose', 'config.yaml');
|
|
485
|
+
const configPath = gooseConfigPath();
|
|
270
486
|
try {
|
|
271
487
|
await node_fs.access(node_path.dirname(configPath));
|
|
272
|
-
let yaml = '';
|
|
273
|
-
try {
|
|
274
|
-
yaml = await node_fs.readFile(configPath, 'utf-8');
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
yaml = '';
|
|
278
|
-
}
|
|
279
|
-
// Inject MCP server block if not present (simple string injection — avoids yaml dep)
|
|
280
|
-
const proxyBin = resolveProxyBin(workspaceRoot);
|
|
281
|
-
const intuticBlock = [
|
|
282
|
-
'mcp:',
|
|
283
|
-
' intutic:',
|
|
284
|
-
` command: node`,
|
|
285
|
-
` args: [${JSON.stringify(proxyBin)}, "--workspace-id", ${JSON.stringify(workspaceId)}]`,
|
|
286
|
-
].join('\n');
|
|
287
|
-
if (!yaml.includes('intutic:')) {
|
|
288
|
-
yaml = yaml.trimEnd() + '\n\n' + intuticBlock + '\n';
|
|
289
|
-
}
|
|
290
|
-
await node_fs.mkdir(node_path.dirname(configPath), { recursive: true });
|
|
291
|
-
await node_fs.writeFile(configPath, yaml, 'utf-8');
|
|
292
|
-
log.info({ action: 'goose_mcp_injected' }, 'Goose config.yaml updated');
|
|
293
488
|
}
|
|
294
489
|
catch {
|
|
295
490
|
log.debug({ action: 'goose_skip' }, 'Goose not installed — skipping');
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
let existingYaml;
|
|
494
|
+
try {
|
|
495
|
+
existingYaml = await node_fs.readFile(configPath, 'utf-8');
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
existingYaml = '';
|
|
499
|
+
}
|
|
500
|
+
const proxyBin = resolveProxyBin(workspaceRoot);
|
|
501
|
+
let doc;
|
|
502
|
+
try {
|
|
503
|
+
doc = parseDocument(existingYaml);
|
|
504
|
+
if (doc.errors.length > 0) {
|
|
505
|
+
throw doc.errors[0];
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
log.warn({ action: 'goose_yaml_unparseable', err: err.message }, 'Goose config.yaml did not parse as YAML — falling back to append-only text injection');
|
|
510
|
+
await injectGooseAppendOnly(configPath, existingYaml, workspaceId, proxyBin);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const intuticEntry = {
|
|
514
|
+
command: 'node',
|
|
515
|
+
args: [proxyBin, '--workspace-id', workspaceId],
|
|
516
|
+
};
|
|
517
|
+
let changed = false;
|
|
518
|
+
const mcpNode = doc.get('mcp');
|
|
519
|
+
const mcpIsMap = isMap(mcpNode);
|
|
520
|
+
const servers = mcpIsMap
|
|
521
|
+
? (doc.toJS().mcp ?? {})
|
|
522
|
+
: {};
|
|
523
|
+
if (mcpNode !== undefined && !mcpIsMap) {
|
|
524
|
+
// `mcp:` exists but isn't a mapping (e.g. `mcp: null`, `mcp: []`,
|
|
525
|
+
// `mcp: "x"`) — nothing safe to preserve there. Replace with a fresh
|
|
526
|
+
// empty map so the `setIn` calls below have a collection to write into
|
|
527
|
+
// (`Document#setIn` throws on a non-collection intermediate path).
|
|
528
|
+
doc.set('mcp', {});
|
|
529
|
+
changed = true;
|
|
530
|
+
}
|
|
531
|
+
if (!isDeepStrictEqual(servers['intutic'], intuticEntry)) {
|
|
532
|
+
doc.setIn(['mcp', 'intutic'], intuticEntry);
|
|
533
|
+
changed = true;
|
|
296
534
|
}
|
|
535
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
536
|
+
if (name === 'intutic')
|
|
537
|
+
continue;
|
|
538
|
+
const wrapped = wrapWithProxy(entry, workspaceId, workspaceRoot, name);
|
|
539
|
+
if (!isDeepStrictEqual(wrapped, entry)) {
|
|
540
|
+
doc.setIn(['mcp', name], wrapped);
|
|
541
|
+
changed = true;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (!changed) {
|
|
545
|
+
// Already present and unchanged — write-if-changed applies here too
|
|
546
|
+
// (see writeJsonFile's doc comment for why: this runs every sync cycle now).
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
await node_fs.mkdir(node_path.dirname(configPath), { recursive: true });
|
|
550
|
+
await node_fs.writeFile(configPath, doc.toString(), 'utf-8');
|
|
551
|
+
log.info({ action: 'goose_mcp_injected', mode: 'yaml' }, 'Goose config.yaml updated (structural YAML edit)');
|
|
297
552
|
}
|
|
298
553
|
// ─── Target: OpenHands ────────────────────────────────────────────────────────
|
|
299
554
|
async function injectOpenHands(workspaceId, workspaceRoot) {
|
|
300
|
-
const configPath =
|
|
555
|
+
const configPath = openHandsConfigPath(workspaceRoot);
|
|
301
556
|
try {
|
|
302
557
|
const current = await readJsonFile(configPath, {});
|
|
303
558
|
current.mcpServers = wrapAllServers({ intutic: buildIntuticMcpEntry(workspaceRoot), ...(current.mcpServers ?? {}) }, workspaceId, workspaceRoot);
|
|
@@ -308,12 +563,377 @@ async function injectOpenHands(workspaceId, workspaceRoot) {
|
|
|
308
563
|
log.debug({ action: 'openhands_mcp_skip', err: err.message }, 'OpenHands config not found — skipping');
|
|
309
564
|
}
|
|
310
565
|
}
|
|
566
|
+
// ─── Target: Muse Code ────────────────────────────────────────────────────────
|
|
567
|
+
/**
|
|
568
|
+
* `~/.config/muse/settings.json` carries `schema_version` (documented as a
|
|
569
|
+
* mandatory field — defaulted to `1` if this is genuinely the first thing to
|
|
570
|
+
* ever write the file), `mcp_servers` (this function's target), and
|
|
571
|
+
* `managed_hooks_path` (written by `museHooks.ts`). Read-modify-write, same as
|
|
572
|
+
* every other JSON-object harness here — `mergeMuseSettingsJson` in
|
|
573
|
+
* `museHooks.ts` performs the identical pattern for the hooks half of this
|
|
574
|
+
* same file, sequentially, the same way `gooseHooks.ts`'s `mergeGooseConfig`
|
|
575
|
+
* and `injectGoose` both write `config.yaml` without coordinating.
|
|
576
|
+
*/
|
|
577
|
+
async function injectMuse(workspaceId, workspaceRoot) {
|
|
578
|
+
const configPath = museConfigPath();
|
|
579
|
+
try {
|
|
580
|
+
await node_fs.access(node_path.dirname(configPath));
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
log.debug({ action: 'muse_skip' }, 'Muse Code not installed — skipping');
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const current = await readJsonFile(configPath, {});
|
|
587
|
+
current.mcp_servers = wrapAllServers({ intutic: buildIntuticMcpEntry(workspaceRoot), ...(current.mcp_servers ?? {}) }, workspaceId, workspaceRoot);
|
|
588
|
+
if (current.schema_version === undefined)
|
|
589
|
+
current.schema_version = 1;
|
|
590
|
+
await writeJsonFile(configPath, current);
|
|
591
|
+
log.info({ action: 'muse_mcp_injected' }, 'Muse Code ~/.config/muse/settings.json mcp_servers updated');
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* The pre-parser fallback for a `config.toml` that does not parse as TOML at
|
|
595
|
+
* all — append an `[mcp_servers.intutic]` block only, mirroring
|
|
596
|
+
* `injectGooseAppendOnly`'s exact reasoning: a parser that cannot safely
|
|
597
|
+
* represent a malformed file cannot safely round-trip it either, so this
|
|
598
|
+
* never touches (or even parses) anything else already in the file. Never
|
|
599
|
+
* rewraps a pre-existing `[mcp_servers.*]` entry when this path runs — same
|
|
600
|
+
* honest limit `injectGooseAppendOnly`/`discoverGooseLineScanFallback`
|
|
601
|
+
* document for their own fallback.
|
|
602
|
+
*/
|
|
603
|
+
async function injectGrokConfigAppendOnly(configPath, existingToml, workspaceId, proxyBin) {
|
|
604
|
+
let text = existingToml;
|
|
605
|
+
if (!text.includes('[mcp_servers.intutic]') && !text.includes('[mcp_servers."intutic"]')) {
|
|
606
|
+
const block = [
|
|
607
|
+
'[mcp_servers.intutic]',
|
|
608
|
+
'command = "node"',
|
|
609
|
+
`args = [${JSON.stringify(proxyBin)}, "--workspace-id", ${JSON.stringify(workspaceId)}]`,
|
|
610
|
+
].join('\n');
|
|
611
|
+
text = text.trimEnd() + '\n\n' + block + '\n';
|
|
612
|
+
}
|
|
613
|
+
if (text === existingToml)
|
|
614
|
+
return;
|
|
615
|
+
await node_fs.mkdir(node_path.dirname(configPath), { recursive: true });
|
|
616
|
+
await node_fs.writeFile(configPath, text, 'utf-8');
|
|
617
|
+
log.info({ action: 'grok_mcp_injected', mode: 'append_only_fallback' }, 'Grok config.toml updated (append-only fallback)');
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Structurally edits `config.toml`'s `[mcp_servers.*]` tables via `smol-toml`
|
|
621
|
+
* — the TOML counterpart of `injectGoose`'s YAML edit above, applying the
|
|
622
|
+
* SAME `wrapWithProxy` convention every other harness uses (stdio entries
|
|
623
|
+
* wrapped with `--`, remote/`url`-keyed entries wrapped with
|
|
624
|
+
* `--remote-url`/`--remote-transport`) so a remote MCP server declared for
|
|
625
|
+
* Grok Build gets identical bridge coverage to one in `~/.claude/mcp.json`,
|
|
626
|
+
* not a second, divergent convention.
|
|
627
|
+
*
|
|
628
|
+
* Unlike `injectGoose`'s `Document#setIn` (which preserves comments/
|
|
629
|
+
* formatting for untouched YAML), `smol-toml` round-trips through a plain
|
|
630
|
+
* object — no TOML-parsing dependency preserving comments existed anywhere
|
|
631
|
+
* in this monorepo to reuse (checked; see grokHooks.ts's module doc for the
|
|
632
|
+
* same finding), so a table this function DOES touch loses any inline
|
|
633
|
+
* comments it carried. Tables it does not touch (`[model.*]`, anything
|
|
634
|
+
* else) are written back byte-for-byte equal in content, just
|
|
635
|
+
* re-serialized. Falls back to `injectGrokConfigAppendOnly` when the file
|
|
636
|
+
* does not parse as TOML at all.
|
|
637
|
+
*
|
|
638
|
+
* @param configPath - Absolute path to the `config.toml` to edit (project or
|
|
639
|
+
* user level — this function is level-agnostic; the caller supplies both).
|
|
640
|
+
*/
|
|
641
|
+
async function injectGrokConfig(configPath, workspaceId, workspaceRoot) {
|
|
642
|
+
let existingToml = '';
|
|
643
|
+
try {
|
|
644
|
+
existingToml = await node_fs.readFile(configPath, 'utf-8');
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
// Deliberate fail-open: no config.toml yet, or unreadable — falls
|
|
648
|
+
// through with '' so a fresh file is written below.
|
|
649
|
+
}
|
|
650
|
+
const proxyBin = resolveProxyBin(workspaceRoot);
|
|
651
|
+
let doc;
|
|
652
|
+
try {
|
|
653
|
+
doc = existingToml.trim() ? parseToml(existingToml) : {};
|
|
654
|
+
}
|
|
655
|
+
catch (err) {
|
|
656
|
+
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');
|
|
657
|
+
await injectGrokConfigAppendOnly(configPath, existingToml, workspaceId, proxyBin);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const servers = doc.mcp_servers && typeof doc.mcp_servers === 'object' ? doc.mcp_servers : {};
|
|
661
|
+
const intuticEntry = buildIntuticMcpEntry(workspaceRoot);
|
|
662
|
+
let changed = false;
|
|
663
|
+
if (!isDeepStrictEqual(servers['intutic'], intuticEntry)) {
|
|
664
|
+
servers['intutic'] = intuticEntry;
|
|
665
|
+
changed = true;
|
|
666
|
+
}
|
|
667
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
668
|
+
if (name === 'intutic')
|
|
669
|
+
continue;
|
|
670
|
+
const wrapped = wrapWithProxy(entry, workspaceId, workspaceRoot, name);
|
|
671
|
+
if (!isDeepStrictEqual(wrapped, entry)) {
|
|
672
|
+
servers[name] = wrapped;
|
|
673
|
+
changed = true;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (!changed) {
|
|
677
|
+
// write-if-changed, same reasoning as writeJsonFile/injectGoose: this
|
|
678
|
+
// runs every sync cycle, so a no-op merge must not churn the file's mtime.
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
doc.mcp_servers = servers;
|
|
682
|
+
await node_fs.mkdir(node_path.dirname(configPath), { recursive: true });
|
|
683
|
+
const tmp = configPath + '.intutic-tmp';
|
|
684
|
+
await node_fs.writeFile(tmp, stringifyToml(doc), 'utf-8');
|
|
685
|
+
await node_fs.rename(tmp, configPath);
|
|
686
|
+
log.info({ action: 'grok_mcp_injected', path: configPath, mode: 'toml' }, 'Grok config.toml updated (structural TOML edit)');
|
|
687
|
+
}
|
|
688
|
+
async function injectGrok(workspaceId, workspaceRoot) {
|
|
689
|
+
await injectGrokConfig(grokProjectConfigPath(workspaceRoot), workspaceId, workspaceRoot);
|
|
690
|
+
await injectGrokConfig(grokUserConfigPath(), workspaceId, workspaceRoot);
|
|
691
|
+
}
|
|
692
|
+
// ─── Discovery (read-only — writes nothing) ───────────────────────────────────
|
|
693
|
+
/** Classify a raw server entry's transport + wrapped status, tolerant of any shape. */
|
|
694
|
+
function classifyEntry(entry) {
|
|
695
|
+
if (!entry || typeof entry !== 'object')
|
|
696
|
+
return { transport: 'unknown', wrapped: false };
|
|
697
|
+
const e = entry;
|
|
698
|
+
const wrapped = e.__intutic_wrapped === true;
|
|
699
|
+
// A wrapped remote entry's TOP-LEVEL shape is stdio (`command: 'node'`,
|
|
700
|
+
// `args: […]`) — that IS the bridge, not a lie, but it would misreport
|
|
701
|
+
// every wrapped remote server as though wrapping made it stdio if checked
|
|
702
|
+
// first. `__intutic_original` (see `wrapWithProxy`) records what was
|
|
703
|
+
// actually wrapped, so this checks it BEFORE falling through to the
|
|
704
|
+
// generic `command`/`url` shape check below — visibility must stay honest.
|
|
705
|
+
const original = e.__intutic_original;
|
|
706
|
+
if (wrapped && original !== null && typeof original === 'object') {
|
|
707
|
+
const o = original;
|
|
708
|
+
if (typeof o.url === 'string') {
|
|
709
|
+
const type = typeof o.type === 'string' ? o.type.toLowerCase() : '';
|
|
710
|
+
return { transport: type === 'sse' ? 'sse' : 'http', wrapped: true };
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (typeof e.command === 'string')
|
|
714
|
+
return { transport: 'stdio', wrapped };
|
|
715
|
+
if (typeof e.url === 'string') {
|
|
716
|
+
const type = typeof e.type === 'string' ? e.type.toLowerCase() : '';
|
|
717
|
+
return { transport: type === 'sse' ? 'sse' : 'http', wrapped };
|
|
718
|
+
}
|
|
719
|
+
return { transport: 'unknown', wrapped };
|
|
720
|
+
}
|
|
721
|
+
/** Read a `{ mcpServers: { [name]: entry } }` shaped config file (the shape
|
|
722
|
+
* shared by Claude Code, Claude Desktop, both Cursor configs, Cline,
|
|
723
|
+
* Windsurf and OpenHands) without writing anything back. */
|
|
724
|
+
async function discoverJsonObjectHarness(harness, filePath) {
|
|
725
|
+
if (!existsSync(filePath))
|
|
726
|
+
return [];
|
|
727
|
+
const current = await readJsonFile(filePath, {});
|
|
728
|
+
const out = [];
|
|
729
|
+
for (const [name, entry] of Object.entries(current.mcpServers ?? {})) {
|
|
730
|
+
const { transport, wrapped } = classifyEntry(entry);
|
|
731
|
+
out.push({ server: name, harness, transport, wrapped });
|
|
732
|
+
}
|
|
733
|
+
return out;
|
|
734
|
+
}
|
|
735
|
+
/** Muse Code's `~/.config/muse/settings.json` keeps servers under `mcp_servers`,
|
|
736
|
+
* not `mcpServers` — otherwise the same JSON-map shape `discoverJsonObjectHarness`
|
|
737
|
+
* reads, so this is that function with one key renamed rather than a new format. */
|
|
738
|
+
async function discoverMuse() {
|
|
739
|
+
const filePath = museConfigPath();
|
|
740
|
+
if (!existsSync(filePath))
|
|
741
|
+
return [];
|
|
742
|
+
const current = await readJsonFile(filePath, {});
|
|
743
|
+
const out = [];
|
|
744
|
+
for (const [name, entry] of Object.entries(current.mcp_servers ?? {})) {
|
|
745
|
+
const { transport, wrapped } = classifyEntry(entry);
|
|
746
|
+
out.push({ server: name, harness: 'muse-code', transport, wrapped });
|
|
747
|
+
}
|
|
748
|
+
return out;
|
|
749
|
+
}
|
|
750
|
+
/** Continue's `~/.continue/config.json` keeps `mcpServers` as an array, not a map. */
|
|
751
|
+
async function discoverContinue() {
|
|
752
|
+
const filePath = continueConfigPath();
|
|
753
|
+
if (!existsSync(filePath))
|
|
754
|
+
return [];
|
|
755
|
+
const current = await readJsonFile(filePath, {});
|
|
756
|
+
const out = [];
|
|
757
|
+
for (const s of current.mcpServers ?? []) {
|
|
758
|
+
const { transport, wrapped } = classifyEntry(s);
|
|
759
|
+
out.push({ server: s.name, harness: 'continue', transport, wrapped });
|
|
760
|
+
}
|
|
761
|
+
return out;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Goose's `~/.config/goose/config.yaml` is YAML. `injectGoose` now
|
|
765
|
+
* structurally parses and edits it (via the `yaml` package), and — per
|
|
766
|
+
* `wrapWithProxy` — DOES rewrap a pre-existing, non-`intutic` server entry
|
|
767
|
+
* (both stdio and remote), so a blanket "nothing discovered here is ever
|
|
768
|
+
* `wrapped`" claim is no longer true. Discovery mirrors the writer: a real
|
|
769
|
+
* YAML parse via `classifyEntry` (the SAME classifier every JSON-shaped
|
|
770
|
+
* harness below uses), reporting each entry's true transport and wrapped
|
|
771
|
+
* status honestly, exactly like every other harness in this file.
|
|
772
|
+
*
|
|
773
|
+
* Falls back to the old line-scan (2-space-indented `<name>:` keys under a
|
|
774
|
+
* top-level `mcp:` block) when the file does not parse as YAML — matching
|
|
775
|
+
* `injectGoose`'s own fallback to append-only injection for the same
|
|
776
|
+
* malformed-file case. The line-scan fallback still cannot see a server
|
|
777
|
+
* nested any other way (anchors, flow style, deeper indentation) and still
|
|
778
|
+
* never reports one `wrapped`, because it never inspects a value, only a key
|
|
779
|
+
* — an honest limit of a best-effort fallback over a file real parsing
|
|
780
|
+
* already gave up on.
|
|
781
|
+
*/
|
|
782
|
+
async function discoverGoose() {
|
|
783
|
+
const filePath = gooseConfigPath();
|
|
784
|
+
let rawYaml;
|
|
785
|
+
try {
|
|
786
|
+
rawYaml = await node_fs.readFile(filePath, 'utf-8');
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
return [];
|
|
790
|
+
}
|
|
791
|
+
try {
|
|
792
|
+
const doc = parseDocument(rawYaml);
|
|
793
|
+
if (doc.errors.length > 0)
|
|
794
|
+
throw doc.errors[0];
|
|
795
|
+
const mcpNode = doc.get('mcp');
|
|
796
|
+
if (!isMap(mcpNode))
|
|
797
|
+
return [];
|
|
798
|
+
// Not filtering out 'intutic' here — `discoverMcpServers` below does that
|
|
799
|
+
// once, for every harness alike (see its own doc comment), the same
|
|
800
|
+
// convention `discoverJsonObjectHarness`/`discoverContinue` already rely on.
|
|
801
|
+
const servers = doc.toJS().mcp ?? {};
|
|
802
|
+
const out = [];
|
|
803
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
804
|
+
const { transport, wrapped } = classifyEntry(entry);
|
|
805
|
+
out.push({ server: name, harness: 'goose', transport, wrapped });
|
|
806
|
+
}
|
|
807
|
+
return out;
|
|
808
|
+
}
|
|
809
|
+
catch {
|
|
810
|
+
return discoverGooseLineScanFallback(rawYaml);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
/** Best-effort fallback for a `config.yaml` that does not parse as YAML —
|
|
814
|
+
* see `discoverGoose`'s doc comment for why this exists and what it cannot see. */
|
|
815
|
+
function discoverGooseLineScanFallback(rawYaml) {
|
|
816
|
+
const out = [];
|
|
817
|
+
let inMcpBlock = false;
|
|
818
|
+
for (const line of rawYaml.split('\n')) {
|
|
819
|
+
if (/^mcp:\s*$/.test(line)) {
|
|
820
|
+
inMcpBlock = true;
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (!inMcpBlock)
|
|
824
|
+
continue;
|
|
825
|
+
if (/^\S/.test(line)) {
|
|
826
|
+
// Dedented back to column 0 — the mcp: block ended.
|
|
827
|
+
inMcpBlock = false;
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
const m = line.match(/^ {2}([A-Za-z0-9_-]+):\s*$/);
|
|
831
|
+
if (m) {
|
|
832
|
+
out.push({ server: m[1], harness: 'goose', transport: 'stdio', wrapped: false });
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return out;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Grok Build's `config.toml`, discovered read-only. Mirrors `injectGrokConfig`:
|
|
839
|
+
* a real structural TOML parse via `classifyEntry` (the SAME classifier every
|
|
840
|
+
* other harness below uses), so a wrapped `[mcp_servers.*]` entry reports its
|
|
841
|
+
* true pre-wrap transport honestly. Falls back to a best-effort line-scan
|
|
842
|
+
* (top-level keys under `[mcp_servers.` — never reports one `wrapped`, the
|
|
843
|
+
* same honest limit `discoverGooseLineScanFallback` documents for its own
|
|
844
|
+
* fallback) when the file does not parse as TOML at all.
|
|
845
|
+
*/
|
|
846
|
+
async function discoverGrokConfig(configPath) {
|
|
847
|
+
let rawToml;
|
|
848
|
+
try {
|
|
849
|
+
rawToml = await node_fs.readFile(configPath, 'utf-8');
|
|
850
|
+
}
|
|
851
|
+
catch {
|
|
852
|
+
return [];
|
|
853
|
+
}
|
|
854
|
+
try {
|
|
855
|
+
const doc = parseToml(rawToml);
|
|
856
|
+
const servers = doc.mcp_servers && typeof doc.mcp_servers === 'object' ? doc.mcp_servers : {};
|
|
857
|
+
const out = [];
|
|
858
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
859
|
+
const { transport, wrapped } = classifyEntry(entry);
|
|
860
|
+
out.push({ server: name, harness: 'grok', transport, wrapped });
|
|
861
|
+
}
|
|
862
|
+
return out;
|
|
863
|
+
}
|
|
864
|
+
catch {
|
|
865
|
+
return discoverGrokLineScanFallback(rawToml);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
/** Best-effort fallback for a `config.toml` that does not parse as TOML —
|
|
869
|
+
* see `discoverGrokConfig`'s doc comment for why this exists and what it
|
|
870
|
+
* cannot see. */
|
|
871
|
+
function discoverGrokLineScanFallback(rawToml) {
|
|
872
|
+
const out = [];
|
|
873
|
+
for (const line of rawToml.split('\n')) {
|
|
874
|
+
const m = line.match(/^\s*\[mcp_servers\.(?:"([^"]+)"|'([^']+)'|([A-Za-z0-9_-]+))\]\s*$/);
|
|
875
|
+
if (m) {
|
|
876
|
+
out.push({ server: (m[1] ?? m[2] ?? m[3]), harness: 'grok', transport: 'stdio', wrapped: false });
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return out;
|
|
880
|
+
}
|
|
881
|
+
async function discoverGrok(workspaceRoot) {
|
|
882
|
+
const [project, user] = await Promise.all([
|
|
883
|
+
discoverGrokConfig(grokProjectConfigPath(workspaceRoot)),
|
|
884
|
+
discoverGrokConfig(grokUserConfigPath()),
|
|
885
|
+
]);
|
|
886
|
+
return [...project, ...user];
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Discover every MCP server declared in any harness config this daemon knows
|
|
890
|
+
* how to parse — the same 12 config paths / 10 harnesses `injectMcpServer`
|
|
891
|
+
* wraps — without writing anything. Used for reporting (agentReporter's
|
|
892
|
+
* `mcp_tools` facet) so visibility does not silently lag behind whatever
|
|
893
|
+
* `injectMcpServer` was last run against.
|
|
894
|
+
*
|
|
895
|
+
* The `intutic` entry itself is excluded from the result: `wrapAllServers`
|
|
896
|
+
* deliberately never wraps it (wrapping our own governance server through
|
|
897
|
+
* itself is circular), so it would always report `wrapped: false` and drag
|
|
898
|
+
* down a wrapped-ratio reading of a workspace that is, in fact, fully covered.
|
|
899
|
+
*
|
|
900
|
+
* A server declared in BOTH `.cursor/mcp.json` (Grok Build's compat read
|
|
901
|
+
* path, already discovered under `harness: 'cursor'` below) AND Grok
|
|
902
|
+
* Build's own `[mcp_servers.*]` table legitimately produces two rows here —
|
|
903
|
+
* one per harness config file — not one merged row. See this module's
|
|
904
|
+
* top-of-file doc comment ("Grok Build's compat-path overlap — dedup, not a
|
|
905
|
+
* bug") for why that is correct, not a double-count to fix.
|
|
906
|
+
*/
|
|
907
|
+
export async function discoverMcpServers(workspaceRoot) {
|
|
908
|
+
const results = await Promise.all([
|
|
909
|
+
discoverJsonObjectHarness('claude-code', claudeCodeConfigPath()),
|
|
910
|
+
discoverJsonObjectHarness('claude-desktop', claudeDesktopConfigPath()),
|
|
911
|
+
discoverJsonObjectHarness('cursor', cursorGlobalConfigPath()),
|
|
912
|
+
discoverJsonObjectHarness('cursor', cursorProjectConfigPath(workspaceRoot)),
|
|
913
|
+
discoverJsonObjectHarness('cline', clineConfigPath(workspaceRoot)),
|
|
914
|
+
discoverJsonObjectHarness('windsurf', windsurfConfigPath()),
|
|
915
|
+
discoverJsonObjectHarness('openhands', openHandsConfigPath(workspaceRoot)),
|
|
916
|
+
discoverContinue(),
|
|
917
|
+
discoverGoose(),
|
|
918
|
+
discoverMuse(),
|
|
919
|
+
discoverGrok(workspaceRoot),
|
|
920
|
+
]);
|
|
921
|
+
return results.flat().filter((s) => s.server !== 'intutic');
|
|
922
|
+
}
|
|
311
923
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
312
924
|
/**
|
|
313
925
|
* Injects and proxy-wraps the Intutic MCP server configuration into all supported harnesses.
|
|
314
926
|
*
|
|
315
927
|
* Non-fatal: a failure in one harness does not prevent other harnesses from being updated.
|
|
316
928
|
*
|
|
929
|
+
* Called from two places by design: once from `intutic connect` (tools/cli)
|
|
930
|
+
* for immediate effect, and once per sync-loop iteration
|
|
931
|
+
* (services/sync-daemon/src/syncLoop.ts) so it is a continuous invariant
|
|
932
|
+
* rather than a one-shot — a server a user adds after their first `connect`
|
|
933
|
+
* still gets wrapped on the next cycle. Safe to call every cycle because
|
|
934
|
+
* `writeJsonFile` is write-if-changed: an already-wrapped, unchanged config
|
|
935
|
+
* writes zero bytes.
|
|
936
|
+
*
|
|
317
937
|
* @param workspaceId - The workspace ID for policy lookups and event attribution.
|
|
318
938
|
* @param workspaceRoot - Absolute path to the project workspace root.
|
|
319
939
|
*/
|
|
@@ -328,6 +948,8 @@ export async function injectMcpServer(workspaceRoot, workspaceId = 'unknown') {
|
|
|
328
948
|
injectContinue(workspaceId, workspaceRoot).catch((err) => log.error({ err: err.message, target: 'continue' }, 'MCP injection failed')),
|
|
329
949
|
injectGoose(workspaceId, workspaceRoot).catch((err) => log.error({ err: err.message, target: 'goose' }, 'MCP injection failed')),
|
|
330
950
|
injectOpenHands(workspaceId, workspaceRoot).catch((err) => log.error({ err: err.message, target: 'openhands' }, 'MCP injection failed')),
|
|
951
|
+
injectMuse(workspaceId, workspaceRoot).catch((err) => log.error({ err: err.message, target: 'muse-code' }, 'MCP injection failed')),
|
|
952
|
+
injectGrok(workspaceId, workspaceRoot).catch((err) => log.error({ err: err.message, target: 'grok' }, 'MCP injection failed')),
|
|
331
953
|
]);
|
|
332
954
|
log.info({ action: 'mcp_inject_complete', workspaceRoot }, 'MCP server injection complete');
|
|
333
955
|
}
|