@ghl-ai/aw 0.1.50 → 0.1.52
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/c4/templates/scripts/aw-c4-bootstrap.sh +5 -4
- package/cli.mjs +22 -9
- package/commands/c4.mjs +17 -9
- package/commands/doctor.mjs +2 -2
- package/commands/init.mjs +23 -5
- package/commands/integrations.mjs +254 -0
- package/commands/mcp.mjs +90 -0
- package/commands/nuke.mjs +1 -1
- package/commands/pull.mjs +3 -2
- package/commands/push.mjs +715 -29
- package/commands/startup.mjs +22 -3
- package/constants.mjs +23 -0
- package/ecc.mjs +1 -1
- package/git.mjs +6 -4
- package/integrate.mjs +94 -21
- package/integrations/context-mode.mjs +237 -57
- package/integrations.mjs +971 -0
- package/mcp.mjs +132 -16
- package/package.json +4 -3
- package/render-rules.mjs +25 -1
- package/startup.mjs +52 -8
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { accessSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { accessSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { constants as fsConstants } from 'node:fs';
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { delimiter, dirname, join } from 'node:path';
|
|
@@ -7,6 +7,14 @@ import TOML from '@iarna/toml';
|
|
|
7
7
|
|
|
8
8
|
const INTEGRATION_NAME = 'context-mode';
|
|
9
9
|
const BINARY_NAME = 'context-mode';
|
|
10
|
+
const INSTALL_STATE_RELATIVE_PATH = ['.aw', 'context-mode-install-state.json'];
|
|
11
|
+
const INSTALL_STATE_VERSION = 1;
|
|
12
|
+
const CONTEXT_MODE_MCP_SERVER = { command: BINARY_NAME };
|
|
13
|
+
const MCP_TARGETS = {
|
|
14
|
+
codex: 'codex-config-toml',
|
|
15
|
+
cursor: 'cursor-mcp-json',
|
|
16
|
+
claude: 'claude-json',
|
|
17
|
+
};
|
|
10
18
|
|
|
11
19
|
const CODEX_HOOK_COMMANDS = {
|
|
12
20
|
PreToolUse: 'context-mode hook codex pretooluse',
|
|
@@ -17,12 +25,41 @@ const CODEX_HOOK_COMMANDS = {
|
|
|
17
25
|
Stop: 'context-mode hook codex stop',
|
|
18
26
|
};
|
|
19
27
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
const CODEX_PRE_TOOL_MATCHER = [
|
|
29
|
+
'local_shell',
|
|
30
|
+
'shell',
|
|
31
|
+
'shell_command',
|
|
32
|
+
'exec_command',
|
|
33
|
+
'Bash',
|
|
34
|
+
'Shell',
|
|
35
|
+
'apply_patch',
|
|
36
|
+
'Edit',
|
|
37
|
+
'Write',
|
|
38
|
+
'grep_files',
|
|
39
|
+
'ctx_execute',
|
|
40
|
+
'ctx_execute_file',
|
|
41
|
+
'ctx_batch_execute',
|
|
42
|
+
'ctx_fetch_and_index',
|
|
43
|
+
'ctx_search',
|
|
44
|
+
'ctx_index',
|
|
45
|
+
'mcp__',
|
|
46
|
+
].join('|');
|
|
47
|
+
|
|
48
|
+
const CODEX_HOOK_MATCHERS = {
|
|
49
|
+
PreToolUse: CODEX_PRE_TOOL_MATCHER,
|
|
50
|
+
PostToolUse: '*',
|
|
51
|
+
SessionStart: 'startup|resume',
|
|
24
52
|
};
|
|
25
53
|
|
|
54
|
+
const CURSOR_HOOK_COMMANDS = [
|
|
55
|
+
{ event: 'beforeShellExecution', command: 'context-mode hook cursor pretooluse' },
|
|
56
|
+
{ event: 'beforeMCPExecution', command: 'context-mode hook cursor pretooluse' },
|
|
57
|
+
{ event: 'afterShellExecution', command: 'context-mode hook cursor posttooluse' },
|
|
58
|
+
{ event: 'afterFileEdit', command: 'context-mode hook cursor posttooluse' },
|
|
59
|
+
{ event: 'afterMCPExecution', command: 'context-mode hook cursor posttooluse' },
|
|
60
|
+
{ event: 'stop', command: 'context-mode hook cursor stop' },
|
|
61
|
+
];
|
|
62
|
+
|
|
26
63
|
function truthy(value) {
|
|
27
64
|
return ['1', 'true', 'yes', 'on'].includes(String(value || '').trim().toLowerCase());
|
|
28
65
|
}
|
|
@@ -55,6 +92,60 @@ function cloneJson(value) {
|
|
|
55
92
|
return JSON.parse(JSON.stringify(value ?? {}));
|
|
56
93
|
}
|
|
57
94
|
|
|
95
|
+
function contextModeInstallStatePath(home) {
|
|
96
|
+
return join(home, ...INSTALL_STATE_RELATIVE_PATH);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function emptyInstallState() {
|
|
100
|
+
return { version: INSTALL_STATE_VERSION, mcpServers: [] };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeInstallState(value) {
|
|
104
|
+
const state = value && typeof value === 'object' && !Array.isArray(value)
|
|
105
|
+
? value
|
|
106
|
+
: {};
|
|
107
|
+
return {
|
|
108
|
+
version: INSTALL_STATE_VERSION,
|
|
109
|
+
mcpServers: Array.isArray(state.mcpServers)
|
|
110
|
+
? unique(state.mcpServers.filter(item => typeof item === 'string')).sort()
|
|
111
|
+
: [],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readInstallState(home) {
|
|
116
|
+
const filePath = contextModeInstallStatePath(home);
|
|
117
|
+
if (!existsSync(filePath)) return emptyInstallState();
|
|
118
|
+
return normalizeInstallState(readJson(filePath, emptyInstallState()));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function writeInstallStateIfChanged(home, state, dryRun = false) {
|
|
122
|
+
const filePath = contextModeInstallStatePath(home);
|
|
123
|
+
const nextState = normalizeInstallState(state);
|
|
124
|
+
if (nextState.mcpServers.length === 0) {
|
|
125
|
+
if (!existsSync(filePath)) return false;
|
|
126
|
+
if (!dryRun) rmSync(filePath, { force: true });
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return writeJsonIfChanged(filePath, nextState, dryRun);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function ownsMcpTarget(installState, target) {
|
|
134
|
+
return installState.mcpServers.includes(target);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function markMcpTargetOwned(installState, target) {
|
|
138
|
+
installState.mcpServers = unique([...installState.mcpServers, target]).sort();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function unmarkMcpTargetOwned(installState, target) {
|
|
142
|
+
installState.mcpServers = installState.mcpServers.filter(item => item !== target);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isDesiredMcpServer(value) {
|
|
146
|
+
return JSON.stringify(value) === JSON.stringify(CONTEXT_MODE_MCP_SERVER);
|
|
147
|
+
}
|
|
148
|
+
|
|
58
149
|
function parseTomlConfig(filePath, warnings) {
|
|
59
150
|
if (!existsSync(filePath)) return {};
|
|
60
151
|
try {
|
|
@@ -102,6 +193,31 @@ function isContextModeCommand(command) {
|
|
|
102
193
|
return String(command || '').startsWith('context-mode hook ');
|
|
103
194
|
}
|
|
104
195
|
|
|
196
|
+
function hasHookCommand(entry, command) {
|
|
197
|
+
return Array.isArray(entry?.hooks)
|
|
198
|
+
&& entry.hooks.some(hook => hook?.command === command);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function hasCanonicalCodexMatcher(entry, phase) {
|
|
202
|
+
const expectedMatcher = CODEX_HOOK_MATCHERS[phase];
|
|
203
|
+
return expectedMatcher === undefined
|
|
204
|
+
? entry?.matcher === undefined
|
|
205
|
+
: entry?.matcher === expectedMatcher;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isCanonicalCodexHookEntry(entry, phase, command) {
|
|
209
|
+
return hasCanonicalCodexMatcher(entry, phase) && hasHookCommand(entry, command);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildCodexHookEntry(phase, command) {
|
|
213
|
+
const entry = {
|
|
214
|
+
hooks: [{ type: 'command', command }],
|
|
215
|
+
};
|
|
216
|
+
const matcher = CODEX_HOOK_MATCHERS[phase];
|
|
217
|
+
if (matcher !== undefined) entry.matcher = matcher;
|
|
218
|
+
return entry;
|
|
219
|
+
}
|
|
220
|
+
|
|
105
221
|
function ensureObject(parent, key) {
|
|
106
222
|
if (!parent[key] || typeof parent[key] !== 'object' || Array.isArray(parent[key])) {
|
|
107
223
|
parent[key] = {};
|
|
@@ -109,7 +225,7 @@ function ensureObject(parent, key) {
|
|
|
109
225
|
return parent[key];
|
|
110
226
|
}
|
|
111
227
|
|
|
112
|
-
function mergeJsonMcpServer(filePath, dryRun = false) {
|
|
228
|
+
function mergeJsonMcpServer(filePath, installState, target, dryRun = false) {
|
|
113
229
|
const config = readJson(filePath, {});
|
|
114
230
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
115
231
|
throw new Error(`Expected JSON object in ${filePath}`);
|
|
@@ -117,26 +233,47 @@ function mergeJsonMcpServer(filePath, dryRun = false) {
|
|
|
117
233
|
|
|
118
234
|
const mcpServers = ensureObject(config, 'mcpServers');
|
|
119
235
|
const existing = mcpServers[INTEGRATION_NAME];
|
|
120
|
-
|
|
121
|
-
|
|
236
|
+
if (existing && !isDesiredMcpServer(existing)) {
|
|
237
|
+
throw new Error('Refusing to overwrite existing user-owned context-mode MCP config');
|
|
238
|
+
}
|
|
239
|
+
if (existing) return false;
|
|
122
240
|
|
|
123
|
-
mcpServers[INTEGRATION_NAME] =
|
|
241
|
+
mcpServers[INTEGRATION_NAME] = cloneJson(CONTEXT_MODE_MCP_SERVER);
|
|
242
|
+
markMcpTargetOwned(installState, target);
|
|
124
243
|
return writeJsonIfChanged(filePath, config, dryRun);
|
|
125
244
|
}
|
|
126
245
|
|
|
127
|
-
function removeJsonMcpServer(filePath, dryRun = false) {
|
|
128
|
-
if (!
|
|
246
|
+
function removeJsonMcpServer(filePath, installState, target, warnings, dryRun = false) {
|
|
247
|
+
if (!ownsMcpTarget(installState, target)) return false;
|
|
248
|
+
if (!existsSync(filePath)) {
|
|
249
|
+
unmarkMcpTargetOwned(installState, target);
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
129
252
|
const config = readJson(filePath, {});
|
|
130
|
-
|
|
253
|
+
const existing = config?.mcpServers?.[INTEGRATION_NAME];
|
|
254
|
+
if (!existing) {
|
|
255
|
+
unmarkMcpTargetOwned(installState, target);
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
if (!isDesiredMcpServer(existing)) {
|
|
259
|
+
warnings.push(`Preserved user-modified context-mode MCP config at ${filePath}`);
|
|
260
|
+
unmarkMcpTargetOwned(installState, target);
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
131
263
|
|
|
132
264
|
delete config.mcpServers[INTEGRATION_NAME];
|
|
133
265
|
if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
|
|
266
|
+
unmarkMcpTargetOwned(installState, target);
|
|
134
267
|
return writeJsonIfChanged(filePath, config, dryRun);
|
|
135
268
|
}
|
|
136
269
|
|
|
137
|
-
function mergeCodexToml(filePath, warnings, dryRun = false) {
|
|
270
|
+
function mergeCodexToml(filePath, warnings, installState, target, dryRun = false) {
|
|
138
271
|
const config = parseTomlConfig(filePath, warnings);
|
|
139
272
|
if (config === null) return false;
|
|
273
|
+
const existing = config.mcp_servers?.[INTEGRATION_NAME];
|
|
274
|
+
if (existing && !isDesiredMcpServer(existing)) {
|
|
275
|
+
throw new Error('Refusing to overwrite existing user-owned context-mode MCP config');
|
|
276
|
+
}
|
|
140
277
|
|
|
141
278
|
const before = JSON.stringify(config);
|
|
142
279
|
config.features = config.features && typeof config.features === 'object' && !Array.isArray(config.features)
|
|
@@ -148,20 +285,37 @@ function mergeCodexToml(filePath, warnings, dryRun = false) {
|
|
|
148
285
|
config.mcp_servers = config.mcp_servers && typeof config.mcp_servers === 'object' && !Array.isArray(config.mcp_servers)
|
|
149
286
|
? config.mcp_servers
|
|
150
287
|
: {};
|
|
151
|
-
|
|
288
|
+
if (!existing) {
|
|
289
|
+
config.mcp_servers[INTEGRATION_NAME] = cloneJson(CONTEXT_MODE_MCP_SERVER);
|
|
290
|
+
markMcpTargetOwned(installState, target);
|
|
291
|
+
}
|
|
152
292
|
|
|
153
293
|
if (JSON.stringify(config) === before) return false;
|
|
154
294
|
return writeTomlIfChanged(filePath, config, dryRun);
|
|
155
295
|
}
|
|
156
296
|
|
|
157
|
-
function removeCodexToml(filePath, warnings, dryRun = false) {
|
|
158
|
-
if (!
|
|
297
|
+
function removeCodexToml(filePath, warnings, installState, target, dryRun = false) {
|
|
298
|
+
if (!ownsMcpTarget(installState, target)) return false;
|
|
299
|
+
if (!existsSync(filePath)) {
|
|
300
|
+
unmarkMcpTargetOwned(installState, target);
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
159
303
|
const config = parseTomlConfig(filePath, warnings);
|
|
160
304
|
if (config === null) return false;
|
|
161
|
-
|
|
305
|
+
const existing = config.mcp_servers?.[INTEGRATION_NAME];
|
|
306
|
+
if (!existing) {
|
|
307
|
+
unmarkMcpTargetOwned(installState, target);
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
if (!isDesiredMcpServer(existing)) {
|
|
311
|
+
warnings.push(`Preserved user-modified context-mode MCP config at ${filePath}`);
|
|
312
|
+
unmarkMcpTargetOwned(installState, target);
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
162
315
|
|
|
163
316
|
delete config.mcp_servers[INTEGRATION_NAME];
|
|
164
317
|
if (Object.keys(config.mcp_servers).length === 0) delete config.mcp_servers;
|
|
318
|
+
unmarkMcpTargetOwned(installState, target);
|
|
165
319
|
return writeTomlIfChanged(filePath, config, dryRun);
|
|
166
320
|
}
|
|
167
321
|
|
|
@@ -170,23 +324,23 @@ function ensureCodexHook(config, phase, command) {
|
|
|
170
324
|
? config.hooks
|
|
171
325
|
: {};
|
|
172
326
|
const entries = Array.isArray(config.hooks[phase]) ? config.hooks[phase] : [];
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
327
|
+
const before = JSON.stringify(entries);
|
|
328
|
+
const nextEntries = [];
|
|
177
329
|
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (
|
|
186
|
-
|
|
330
|
+
for (const entry of entries) {
|
|
331
|
+
if (!Array.isArray(entry?.hooks)) {
|
|
332
|
+
nextEntries.push(entry);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const nextHooks = entry.hooks.filter(hook => hook?.command !== command);
|
|
337
|
+
if (nextHooks.length === 0) continue;
|
|
338
|
+
nextEntries.push(nextHooks.length === entry.hooks.length ? entry : { ...entry, hooks: nextHooks });
|
|
187
339
|
}
|
|
188
|
-
|
|
189
|
-
|
|
340
|
+
|
|
341
|
+
nextEntries.push(buildCodexHookEntry(phase, command));
|
|
342
|
+
config.hooks[phase] = nextEntries;
|
|
343
|
+
return JSON.stringify(nextEntries) !== before;
|
|
190
344
|
}
|
|
191
345
|
|
|
192
346
|
function mergeCodexHooks(filePath, dryRun = false) {
|
|
@@ -229,18 +383,13 @@ function removeCodexHooks(filePath, dryRun = false) {
|
|
|
229
383
|
return writeJsonIfChanged(filePath, config, dryRun);
|
|
230
384
|
}
|
|
231
385
|
|
|
232
|
-
function ensureCursorHook(config,
|
|
386
|
+
function ensureCursorHook(config, event, command) {
|
|
233
387
|
config.hooks = config.hooks && typeof config.hooks === 'object' && !Array.isArray(config.hooks)
|
|
234
388
|
? config.hooks
|
|
235
389
|
: {};
|
|
236
|
-
const entries = Array.isArray(config.hooks[
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return false;
|
|
240
|
-
}
|
|
241
|
-
entries.push({ command });
|
|
242
|
-
config.hooks[phase] = entries;
|
|
243
|
-
return true;
|
|
390
|
+
const entries = Array.isArray(config.hooks[event]) ? config.hooks[event] : [];
|
|
391
|
+
entries.push({ command, event });
|
|
392
|
+
config.hooks[event] = entries;
|
|
244
393
|
}
|
|
245
394
|
|
|
246
395
|
function mergeCursorHooks(filePath, dryRun = false) {
|
|
@@ -252,12 +401,24 @@ function mergeCursorHooks(filePath, dryRun = false) {
|
|
|
252
401
|
let changed = false;
|
|
253
402
|
if (config.version === undefined) {
|
|
254
403
|
config.version = 1;
|
|
255
|
-
changed = true;
|
|
256
404
|
}
|
|
257
|
-
|
|
258
|
-
|
|
405
|
+
config.hooks = config.hooks && typeof config.hooks === 'object' && !Array.isArray(config.hooks)
|
|
406
|
+
? config.hooks
|
|
407
|
+
: {};
|
|
408
|
+
|
|
409
|
+
for (const phase of Object.keys(config.hooks)) {
|
|
410
|
+
if (!Array.isArray(config.hooks[phase])) continue;
|
|
411
|
+
const nextEntries = config.hooks[phase].filter(entry => !isContextModeCommand(entry?.command));
|
|
412
|
+
if (nextEntries.length > 0) config.hooks[phase] = nextEntries;
|
|
413
|
+
else delete config.hooks[phase];
|
|
259
414
|
}
|
|
260
|
-
|
|
415
|
+
|
|
416
|
+
for (const { event, command } of CURSOR_HOOK_COMMANDS) {
|
|
417
|
+
ensureCursorHook(config, event, command);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
changed = writeJsonIfChanged(filePath, config, dryRun);
|
|
421
|
+
return changed;
|
|
261
422
|
}
|
|
262
423
|
|
|
263
424
|
function runConfigMutation(label, filePath, mutate, warnings) {
|
|
@@ -313,9 +474,7 @@ function hasCodexHookCoverage(filePath) {
|
|
|
313
474
|
const config = readJson(filePath, {});
|
|
314
475
|
return Object.entries(CODEX_HOOK_COMMANDS).every(([phase, command]) =>
|
|
315
476
|
Array.isArray(config.hooks?.[phase])
|
|
316
|
-
&& config.hooks[phase].some(entry =>
|
|
317
|
-
Array.isArray(entry?.hooks) && entry.hooks.some(hook => hook?.command === command)
|
|
318
|
-
)
|
|
477
|
+
&& config.hooks[phase].some(entry => isCanonicalCodexHookEntry(entry, phase, command))
|
|
319
478
|
);
|
|
320
479
|
} catch {
|
|
321
480
|
return false;
|
|
@@ -326,9 +485,9 @@ function hasCursorHookCoverage(filePath) {
|
|
|
326
485
|
if (!existsSync(filePath)) return false;
|
|
327
486
|
try {
|
|
328
487
|
const config = readJson(filePath, {});
|
|
329
|
-
return
|
|
330
|
-
Array.isArray(config.hooks?.[
|
|
331
|
-
&& config.hooks[
|
|
488
|
+
return CURSOR_HOOK_COMMANDS.every(({ event, command }) =>
|
|
489
|
+
Array.isArray(config.hooks?.[event])
|
|
490
|
+
&& config.hooks[event].some(entry => entry?.command === command && entry?.event === event)
|
|
332
491
|
);
|
|
333
492
|
} catch {
|
|
334
493
|
return false;
|
|
@@ -384,6 +543,17 @@ export function detectContextModeBinary(options = {}) {
|
|
|
384
543
|
});
|
|
385
544
|
const versionOutput = `${versionResult.stdout || ''}${versionResult.stderr || ''}`.trim();
|
|
386
545
|
|
|
546
|
+
if (versionResult.error || versionResult.status !== 0 || versionResult.signal) {
|
|
547
|
+
return {
|
|
548
|
+
present: false,
|
|
549
|
+
path: binaryPath,
|
|
550
|
+
version: null,
|
|
551
|
+
reason: versionOutput
|
|
552
|
+
|| versionResult.error?.message
|
|
553
|
+
|| (versionResult.signal ? `--version terminated by ${versionResult.signal}` : `--version exited with status ${versionResult.status ?? 'unknown'}`),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
387
557
|
return {
|
|
388
558
|
present: true,
|
|
389
559
|
path: binaryPath,
|
|
@@ -398,6 +568,7 @@ export function ensureContextModeIntegration(home, options = {}) {
|
|
|
398
568
|
const warnings = [];
|
|
399
569
|
const changedFiles = [];
|
|
400
570
|
const configuredHarnesses = [];
|
|
571
|
+
const installState = readInstallState(home);
|
|
401
572
|
const binary = detectContextModeBinary({ env });
|
|
402
573
|
|
|
403
574
|
if (!binary.present) {
|
|
@@ -411,11 +582,11 @@ export function ensureContextModeIntegration(home, options = {}) {
|
|
|
411
582
|
}
|
|
412
583
|
|
|
413
584
|
const updates = [
|
|
414
|
-
['Codex MCP', join(home, '.codex', 'config.toml'), () => mergeCodexToml(join(home, '.codex', 'config.toml'), warnings, dryRun)],
|
|
585
|
+
['Codex MCP', join(home, '.codex', 'config.toml'), () => mergeCodexToml(join(home, '.codex', 'config.toml'), warnings, installState, MCP_TARGETS.codex, dryRun)],
|
|
415
586
|
['Codex hooks', join(home, '.codex', 'hooks.json'), () => mergeCodexHooks(join(home, '.codex', 'hooks.json'), dryRun)],
|
|
416
|
-
['Cursor MCP', join(home, '.cursor', 'mcp.json'), () => mergeJsonMcpServer(join(home, '.cursor', 'mcp.json'), dryRun)],
|
|
587
|
+
['Cursor MCP', join(home, '.cursor', 'mcp.json'), () => mergeJsonMcpServer(join(home, '.cursor', 'mcp.json'), installState, MCP_TARGETS.cursor, dryRun)],
|
|
417
588
|
['Cursor hooks', join(home, '.cursor', 'hooks.json'), () => mergeCursorHooks(join(home, '.cursor', 'hooks.json'), dryRun)],
|
|
418
|
-
['Claude MCP', join(home, '.claude.json'), () => mergeJsonMcpServer(join(home, '.claude.json'), dryRun)],
|
|
589
|
+
['Claude MCP', join(home, '.claude.json'), () => mergeJsonMcpServer(join(home, '.claude.json'), installState, MCP_TARGETS.claude, dryRun)],
|
|
419
590
|
];
|
|
420
591
|
|
|
421
592
|
for (const [label, filePath, update] of updates) {
|
|
@@ -424,6 +595,10 @@ export function ensureContextModeIntegration(home, options = {}) {
|
|
|
424
595
|
configuredHarnesses.push(label);
|
|
425
596
|
}
|
|
426
597
|
}
|
|
598
|
+
const statePath = contextModeInstallStatePath(home);
|
|
599
|
+
if (writeInstallStateIfChanged(home, installState, dryRun)) {
|
|
600
|
+
changedFiles.push(statePath);
|
|
601
|
+
}
|
|
427
602
|
|
|
428
603
|
return {
|
|
429
604
|
changedFiles: unique(changedFiles),
|
|
@@ -438,18 +613,23 @@ export function removeContextModeIntegration(home, options = {}) {
|
|
|
438
613
|
const dryRun = options.dryRun === true;
|
|
439
614
|
const warnings = [];
|
|
440
615
|
const changedFiles = [];
|
|
616
|
+
const installState = readInstallState(home);
|
|
441
617
|
|
|
442
618
|
const removals = [
|
|
443
|
-
['Codex MCP', join(home, '.codex', 'config.toml'), () => removeCodexToml(join(home, '.codex', 'config.toml'), warnings, dryRun)],
|
|
619
|
+
['Codex MCP', join(home, '.codex', 'config.toml'), () => removeCodexToml(join(home, '.codex', 'config.toml'), warnings, installState, MCP_TARGETS.codex, dryRun)],
|
|
444
620
|
['Codex hooks', join(home, '.codex', 'hooks.json'), () => removeCodexHooks(join(home, '.codex', 'hooks.json'), dryRun)],
|
|
445
|
-
['Cursor MCP', join(home, '.cursor', 'mcp.json'), () => removeJsonMcpServer(join(home, '.cursor', 'mcp.json'), dryRun)],
|
|
621
|
+
['Cursor MCP', join(home, '.cursor', 'mcp.json'), () => removeJsonMcpServer(join(home, '.cursor', 'mcp.json'), installState, MCP_TARGETS.cursor, warnings, dryRun)],
|
|
446
622
|
['Cursor hooks', join(home, '.cursor', 'hooks.json'), () => removeCursorHooks(join(home, '.cursor', 'hooks.json'), dryRun)],
|
|
447
|
-
['Claude MCP', join(home, '.claude.json'), () => removeJsonMcpServer(join(home, '.claude.json'), dryRun)],
|
|
623
|
+
['Claude MCP', join(home, '.claude.json'), () => removeJsonMcpServer(join(home, '.claude.json'), installState, MCP_TARGETS.claude, warnings, dryRun)],
|
|
448
624
|
];
|
|
449
625
|
|
|
450
626
|
for (const [label, filePath, remove] of removals) {
|
|
451
627
|
if (runConfigMutation(label, filePath, remove, warnings)) changedFiles.push(filePath);
|
|
452
628
|
}
|
|
629
|
+
const statePath = contextModeInstallStatePath(home);
|
|
630
|
+
if (writeInstallStateIfChanged(home, installState, dryRun)) {
|
|
631
|
+
changedFiles.push(statePath);
|
|
632
|
+
}
|
|
453
633
|
|
|
454
634
|
return {
|
|
455
635
|
changedFiles: unique(changedFiles),
|