@ghl-ai/aw 0.1.50-beta.1 → 0.1.50
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 +4 -5
- package/cli.mjs +9 -21
- package/commands/c4.mjs +9 -17
- package/commands/doctor.mjs +5 -2
- package/commands/init.mjs +54 -125
- package/commands/integration.mjs +111 -0
- package/commands/nuke.mjs +3 -1
- package/commands/pull.mjs +2 -3
- package/commands/push.mjs +29 -715
- package/commands/startup.mjs +3 -22
- package/constants.mjs +0 -23
- package/ecc.mjs +1 -1
- package/git.mjs +4 -19
- package/integrate.mjs +21 -94
- package/integrations/context-mode.mjs +514 -0
- package/integrations/index.mjs +31 -0
- package/link.mjs +33 -159
- package/mcp.mjs +16 -132
- package/package.json +4 -4
- package/render-rules.mjs +1 -25
- package/startup.mjs +8 -52
- package/commands/integrations.mjs +0 -254
- package/commands/mcp.mjs +0 -90
- package/integrations.mjs +0 -971
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import { accessSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { constants as fsConstants } from 'node:fs';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { delimiter, dirname, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import TOML from '@iarna/toml';
|
|
7
|
+
|
|
8
|
+
const INTEGRATION_NAME = 'context-mode';
|
|
9
|
+
const BINARY_NAME = 'context-mode';
|
|
10
|
+
|
|
11
|
+
const CODEX_HOOK_COMMANDS = {
|
|
12
|
+
PreToolUse: 'context-mode hook codex pretooluse',
|
|
13
|
+
PostToolUse: 'context-mode hook codex posttooluse',
|
|
14
|
+
SessionStart: 'context-mode hook codex sessionstart',
|
|
15
|
+
PreCompact: 'context-mode hook codex precompact',
|
|
16
|
+
UserPromptSubmit: 'context-mode hook codex userpromptsubmit',
|
|
17
|
+
Stop: 'context-mode hook codex stop',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const CURSOR_HOOK_COMMANDS = {
|
|
21
|
+
preToolUse: 'context-mode hook cursor pretooluse',
|
|
22
|
+
postToolUse: 'context-mode hook cursor posttooluse',
|
|
23
|
+
stop: 'context-mode hook cursor stop',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function truthy(value) {
|
|
27
|
+
return ['1', 'true', 'yes', 'on'].includes(String(value || '').trim().toLowerCase());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function unique(values) {
|
|
31
|
+
return [...new Set(values)];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readJson(filePath, fallback = {}) {
|
|
35
|
+
if (!existsSync(filePath)) return fallback;
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
throw new Error(`Could not parse JSON config ${filePath}: ${err.message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeJsonIfChanged(filePath, value, dryRun = false) {
|
|
44
|
+
const next = `${JSON.stringify(value, null, 2)}\n`;
|
|
45
|
+
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
|
|
46
|
+
if (next === existing) return false;
|
|
47
|
+
if (!dryRun) {
|
|
48
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
49
|
+
writeFileSync(filePath, next);
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function cloneJson(value) {
|
|
55
|
+
return JSON.parse(JSON.stringify(value ?? {}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseTomlConfig(filePath, warnings) {
|
|
59
|
+
if (!existsSync(filePath)) return {};
|
|
60
|
+
try {
|
|
61
|
+
return TOML.parse(readFileSync(filePath, 'utf8'));
|
|
62
|
+
} catch (err) {
|
|
63
|
+
warnings.push(`Could not parse ${filePath}; leaving Codex TOML unchanged: ${err.message}`);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function writeTomlIfChanged(filePath, value, dryRun = false) {
|
|
69
|
+
const next = TOML.stringify(value);
|
|
70
|
+
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
|
|
71
|
+
if (next === existing) return false;
|
|
72
|
+
if (!dryRun) {
|
|
73
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
74
|
+
writeFileSync(filePath, next);
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function executableCandidates(binaryName) {
|
|
80
|
+
return process.platform === 'win32'
|
|
81
|
+
? [binaryName, `${binaryName}.cmd`, `${binaryName}.exe`]
|
|
82
|
+
: [binaryName];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function findExecutable(binaryName, env = process.env) {
|
|
86
|
+
const pathValue = env.PATH || '';
|
|
87
|
+
for (const dir of pathValue.split(delimiter).filter(Boolean)) {
|
|
88
|
+
for (const candidate of executableCandidates(binaryName)) {
|
|
89
|
+
const fullPath = join(dir, candidate);
|
|
90
|
+
try {
|
|
91
|
+
accessSync(fullPath, fsConstants.X_OK);
|
|
92
|
+
return fullPath;
|
|
93
|
+
} catch {
|
|
94
|
+
// Keep scanning PATH.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isContextModeCommand(command) {
|
|
102
|
+
return String(command || '').startsWith('context-mode hook ');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function ensureObject(parent, key) {
|
|
106
|
+
if (!parent[key] || typeof parent[key] !== 'object' || Array.isArray(parent[key])) {
|
|
107
|
+
parent[key] = {};
|
|
108
|
+
}
|
|
109
|
+
return parent[key];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function mergeJsonMcpServer(filePath, dryRun = false) {
|
|
113
|
+
const config = readJson(filePath, {});
|
|
114
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
115
|
+
throw new Error(`Expected JSON object in ${filePath}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const mcpServers = ensureObject(config, 'mcpServers');
|
|
119
|
+
const existing = mcpServers[INTEGRATION_NAME];
|
|
120
|
+
const desired = { command: BINARY_NAME };
|
|
121
|
+
if (JSON.stringify(existing) === JSON.stringify(desired)) return false;
|
|
122
|
+
|
|
123
|
+
mcpServers[INTEGRATION_NAME] = desired;
|
|
124
|
+
return writeJsonIfChanged(filePath, config, dryRun);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function removeJsonMcpServer(filePath, dryRun = false) {
|
|
128
|
+
if (!existsSync(filePath)) return false;
|
|
129
|
+
const config = readJson(filePath, {});
|
|
130
|
+
if (!config?.mcpServers?.[INTEGRATION_NAME]) return false;
|
|
131
|
+
|
|
132
|
+
delete config.mcpServers[INTEGRATION_NAME];
|
|
133
|
+
if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
|
|
134
|
+
return writeJsonIfChanged(filePath, config, dryRun);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function mergeCodexToml(filePath, warnings, dryRun = false) {
|
|
138
|
+
const config = parseTomlConfig(filePath, warnings);
|
|
139
|
+
if (config === null) return false;
|
|
140
|
+
|
|
141
|
+
const before = JSON.stringify(config);
|
|
142
|
+
config.features = config.features && typeof config.features === 'object' && !Array.isArray(config.features)
|
|
143
|
+
? config.features
|
|
144
|
+
: {};
|
|
145
|
+
config.features.hooks = true;
|
|
146
|
+
config.features.codex_hooks = true;
|
|
147
|
+
|
|
148
|
+
config.mcp_servers = config.mcp_servers && typeof config.mcp_servers === 'object' && !Array.isArray(config.mcp_servers)
|
|
149
|
+
? config.mcp_servers
|
|
150
|
+
: {};
|
|
151
|
+
config.mcp_servers[INTEGRATION_NAME] = { command: BINARY_NAME };
|
|
152
|
+
|
|
153
|
+
if (JSON.stringify(config) === before) return false;
|
|
154
|
+
return writeTomlIfChanged(filePath, config, dryRun);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function removeCodexToml(filePath, warnings, dryRun = false) {
|
|
158
|
+
if (!existsSync(filePath)) return false;
|
|
159
|
+
const config = parseTomlConfig(filePath, warnings);
|
|
160
|
+
if (config === null) return false;
|
|
161
|
+
if (!config.mcp_servers?.[INTEGRATION_NAME]) return false;
|
|
162
|
+
|
|
163
|
+
delete config.mcp_servers[INTEGRATION_NAME];
|
|
164
|
+
if (Object.keys(config.mcp_servers).length === 0) delete config.mcp_servers;
|
|
165
|
+
return writeTomlIfChanged(filePath, config, dryRun);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function ensureCodexHook(config, phase, command) {
|
|
169
|
+
config.hooks = config.hooks && typeof config.hooks === 'object' && !Array.isArray(config.hooks)
|
|
170
|
+
? config.hooks
|
|
171
|
+
: {};
|
|
172
|
+
const entries = Array.isArray(config.hooks[phase]) ? config.hooks[phase] : [];
|
|
173
|
+
if (entries.some(entry => Array.isArray(entry?.hooks) && entry.hooks.some(hook => hook?.command === command))) {
|
|
174
|
+
config.hooks[phase] = entries;
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const target = entries.find(entry => Array.isArray(entry?.hooks));
|
|
179
|
+
const hook = { type: 'command', command };
|
|
180
|
+
if (target) {
|
|
181
|
+
target.hooks.push(hook);
|
|
182
|
+
} else {
|
|
183
|
+
const entry = { hooks: [hook] };
|
|
184
|
+
if (phase === 'SessionStart') entry.matcher = 'startup|resume';
|
|
185
|
+
if (phase === 'PreToolUse' || phase === 'PostToolUse') entry.matcher = '*';
|
|
186
|
+
entries.push(entry);
|
|
187
|
+
}
|
|
188
|
+
config.hooks[phase] = entries;
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function mergeCodexHooks(filePath, dryRun = false) {
|
|
193
|
+
const config = readJson(filePath, {});
|
|
194
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
195
|
+
throw new Error(`Expected JSON object in ${filePath}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let changed = false;
|
|
199
|
+
for (const [phase, command] of Object.entries(CODEX_HOOK_COMMANDS)) {
|
|
200
|
+
changed = ensureCodexHook(config, phase, command) || changed;
|
|
201
|
+
}
|
|
202
|
+
return changed && writeJsonIfChanged(filePath, config, dryRun);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function removeCodexHooks(filePath, dryRun = false) {
|
|
206
|
+
if (!existsSync(filePath)) return false;
|
|
207
|
+
const config = readJson(filePath, {});
|
|
208
|
+
if (!config?.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) return false;
|
|
209
|
+
|
|
210
|
+
let changed = false;
|
|
211
|
+
for (const phase of Object.keys(config.hooks)) {
|
|
212
|
+
if (!Array.isArray(config.hooks[phase])) continue;
|
|
213
|
+
const nextEntries = config.hooks[phase]
|
|
214
|
+
.map(entry => {
|
|
215
|
+
if (!Array.isArray(entry?.hooks)) return entry;
|
|
216
|
+
const nextHooks = entry.hooks.filter(hook => !isContextModeCommand(hook?.command));
|
|
217
|
+
if (nextHooks.length !== entry.hooks.length) changed = true;
|
|
218
|
+
if (nextHooks.length === 0) return null;
|
|
219
|
+
return { ...entry, hooks: nextHooks };
|
|
220
|
+
})
|
|
221
|
+
.filter(Boolean);
|
|
222
|
+
|
|
223
|
+
if (nextEntries.length > 0) config.hooks[phase] = nextEntries;
|
|
224
|
+
else delete config.hooks[phase];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!changed) return false;
|
|
228
|
+
if (Object.keys(config.hooks).length === 0) delete config.hooks;
|
|
229
|
+
return writeJsonIfChanged(filePath, config, dryRun);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function ensureCursorHook(config, phase, command) {
|
|
233
|
+
config.hooks = config.hooks && typeof config.hooks === 'object' && !Array.isArray(config.hooks)
|
|
234
|
+
? config.hooks
|
|
235
|
+
: {};
|
|
236
|
+
const entries = Array.isArray(config.hooks[phase]) ? config.hooks[phase] : [];
|
|
237
|
+
if (entries.some(entry => entry?.command === command)) {
|
|
238
|
+
config.hooks[phase] = entries;
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
entries.push({ command });
|
|
242
|
+
config.hooks[phase] = entries;
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function mergeCursorHooks(filePath, dryRun = false) {
|
|
247
|
+
const config = readJson(filePath, {});
|
|
248
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
249
|
+
throw new Error(`Expected JSON object in ${filePath}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let changed = false;
|
|
253
|
+
if (config.version === undefined) {
|
|
254
|
+
config.version = 1;
|
|
255
|
+
changed = true;
|
|
256
|
+
}
|
|
257
|
+
for (const [phase, command] of Object.entries(CURSOR_HOOK_COMMANDS)) {
|
|
258
|
+
changed = ensureCursorHook(config, phase, command) || changed;
|
|
259
|
+
}
|
|
260
|
+
return changed && writeJsonIfChanged(filePath, config, dryRun);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function runConfigMutation(label, filePath, mutate, warnings) {
|
|
264
|
+
try {
|
|
265
|
+
return mutate();
|
|
266
|
+
} catch (err) {
|
|
267
|
+
warnings.push(`Could not update ${label} at ${filePath}: ${err.message}`);
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function removeCursorHooks(filePath, dryRun = false) {
|
|
273
|
+
if (!existsSync(filePath)) return false;
|
|
274
|
+
const config = readJson(filePath, {});
|
|
275
|
+
if (!config?.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks)) return false;
|
|
276
|
+
|
|
277
|
+
let changed = false;
|
|
278
|
+
for (const phase of Object.keys(config.hooks)) {
|
|
279
|
+
if (!Array.isArray(config.hooks[phase])) continue;
|
|
280
|
+
const nextEntries = config.hooks[phase].filter(entry => !isContextModeCommand(entry?.command));
|
|
281
|
+
if (nextEntries.length !== config.hooks[phase].length) changed = true;
|
|
282
|
+
if (nextEntries.length > 0) config.hooks[phase] = nextEntries;
|
|
283
|
+
else delete config.hooks[phase];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!changed) return false;
|
|
287
|
+
if (Object.keys(config.hooks).length === 0) delete config.hooks;
|
|
288
|
+
return writeJsonIfChanged(filePath, config, dryRun);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function hasJsonMcpServer(filePath) {
|
|
292
|
+
if (!existsSync(filePath)) return false;
|
|
293
|
+
try {
|
|
294
|
+
return readJson(filePath, {}).mcpServers?.[INTEGRATION_NAME]?.command === BINARY_NAME;
|
|
295
|
+
} catch {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function hasCodexMcpServer(filePath) {
|
|
301
|
+
if (!existsSync(filePath)) return false;
|
|
302
|
+
try {
|
|
303
|
+
const config = TOML.parse(readFileSync(filePath, 'utf8'));
|
|
304
|
+
return config.mcp_servers?.[INTEGRATION_NAME]?.command === BINARY_NAME;
|
|
305
|
+
} catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function hasCodexHookCoverage(filePath) {
|
|
311
|
+
if (!existsSync(filePath)) return false;
|
|
312
|
+
try {
|
|
313
|
+
const config = readJson(filePath, {});
|
|
314
|
+
return Object.entries(CODEX_HOOK_COMMANDS).every(([phase, command]) =>
|
|
315
|
+
Array.isArray(config.hooks?.[phase])
|
|
316
|
+
&& config.hooks[phase].some(entry =>
|
|
317
|
+
Array.isArray(entry?.hooks) && entry.hooks.some(hook => hook?.command === command)
|
|
318
|
+
)
|
|
319
|
+
);
|
|
320
|
+
} catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function hasCursorHookCoverage(filePath) {
|
|
326
|
+
if (!existsSync(filePath)) return false;
|
|
327
|
+
try {
|
|
328
|
+
const config = readJson(filePath, {});
|
|
329
|
+
return Object.entries(CURSOR_HOOK_COMMANDS).every(([phase, command]) =>
|
|
330
|
+
Array.isArray(config.hooks?.[phase])
|
|
331
|
+
&& config.hooks[phase].some(entry => entry?.command === command)
|
|
332
|
+
);
|
|
333
|
+
} catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function hasAnyContextModeConfig(home) {
|
|
339
|
+
return hasCodexMcpServer(join(home, '.codex', 'config.toml'))
|
|
340
|
+
|| hasCodexHookCoverage(join(home, '.codex', 'hooks.json'))
|
|
341
|
+
|| hasJsonMcpServer(join(home, '.cursor', 'mcp.json'))
|
|
342
|
+
|| hasCursorHookCoverage(join(home, '.cursor', 'hooks.json'))
|
|
343
|
+
|| hasJsonMcpServer(join(home, '.claude.json'));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function hasCompleteContextModeConfig(home) {
|
|
347
|
+
return hasCodexMcpServer(join(home, '.codex', 'config.toml'))
|
|
348
|
+
&& hasCodexHookCoverage(join(home, '.codex', 'hooks.json'))
|
|
349
|
+
&& hasJsonMcpServer(join(home, '.cursor', 'mcp.json'))
|
|
350
|
+
&& hasCursorHookCoverage(join(home, '.cursor', 'hooks.json'))
|
|
351
|
+
&& hasJsonMcpServer(join(home, '.claude.json'));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function isContextModeRequested(args = {}, env = process.env) {
|
|
355
|
+
if (args['--context-mode'] === true) return true;
|
|
356
|
+
if (truthy(env.AW_CONTEXT_MODE)) return true;
|
|
357
|
+
return String(env.AW_INTEGRATIONS || '')
|
|
358
|
+
.split(',')
|
|
359
|
+
.map(value => value.trim().toLowerCase())
|
|
360
|
+
.includes(INTEGRATION_NAME);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function detectContextModeBinary(options = {}) {
|
|
364
|
+
const env = options.env || options || process.env;
|
|
365
|
+
const binaryPath = findExecutable(BINARY_NAME, env);
|
|
366
|
+
if (!binaryPath) {
|
|
367
|
+
return {
|
|
368
|
+
present: false,
|
|
369
|
+
path: null,
|
|
370
|
+
version: null,
|
|
371
|
+
reason: `${BINARY_NAME} binary not found on PATH`,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const versionEnv = {
|
|
376
|
+
...process.env,
|
|
377
|
+
...env,
|
|
378
|
+
PATH: [env.PATH, process.env.PATH].filter(Boolean).join(delimiter),
|
|
379
|
+
};
|
|
380
|
+
const versionResult = spawnSync(binaryPath, ['--version'], {
|
|
381
|
+
env: versionEnv,
|
|
382
|
+
encoding: 'utf8',
|
|
383
|
+
timeout: 3000,
|
|
384
|
+
});
|
|
385
|
+
const versionOutput = `${versionResult.stdout || ''}${versionResult.stderr || ''}`.trim();
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
present: true,
|
|
389
|
+
path: binaryPath,
|
|
390
|
+
version: versionOutput || 'unknown',
|
|
391
|
+
reason: null,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function ensureContextModeIntegration(home, options = {}) {
|
|
396
|
+
const env = options.env || process.env;
|
|
397
|
+
const dryRun = options.dryRun === true;
|
|
398
|
+
const warnings = [];
|
|
399
|
+
const changedFiles = [];
|
|
400
|
+
const configuredHarnesses = [];
|
|
401
|
+
const binary = detectContextModeBinary({ env });
|
|
402
|
+
|
|
403
|
+
if (!binary.present) {
|
|
404
|
+
return {
|
|
405
|
+
changedFiles,
|
|
406
|
+
warnings: [`context-mode binary is required before config mutation: ${binary.reason}`],
|
|
407
|
+
configuredHarnesses,
|
|
408
|
+
binary,
|
|
409
|
+
dryRun,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const updates = [
|
|
414
|
+
['Codex MCP', join(home, '.codex', 'config.toml'), () => mergeCodexToml(join(home, '.codex', 'config.toml'), warnings, dryRun)],
|
|
415
|
+
['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)],
|
|
417
|
+
['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)],
|
|
419
|
+
];
|
|
420
|
+
|
|
421
|
+
for (const [label, filePath, update] of updates) {
|
|
422
|
+
if (runConfigMutation(label, filePath, update, warnings)) {
|
|
423
|
+
changedFiles.push(filePath);
|
|
424
|
+
configuredHarnesses.push(label);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
changedFiles: unique(changedFiles),
|
|
430
|
+
warnings,
|
|
431
|
+
configuredHarnesses,
|
|
432
|
+
binary,
|
|
433
|
+
dryRun,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function removeContextModeIntegration(home, options = {}) {
|
|
438
|
+
const dryRun = options.dryRun === true;
|
|
439
|
+
const warnings = [];
|
|
440
|
+
const changedFiles = [];
|
|
441
|
+
|
|
442
|
+
const removals = [
|
|
443
|
+
['Codex MCP', join(home, '.codex', 'config.toml'), () => removeCodexToml(join(home, '.codex', 'config.toml'), warnings, dryRun)],
|
|
444
|
+
['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)],
|
|
446
|
+
['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)],
|
|
448
|
+
];
|
|
449
|
+
|
|
450
|
+
for (const [label, filePath, remove] of removals) {
|
|
451
|
+
if (runConfigMutation(label, filePath, remove, warnings)) changedFiles.push(filePath);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
changedFiles: unique(changedFiles),
|
|
456
|
+
warnings,
|
|
457
|
+
dryRun,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function getContextModeIntegrationSummary(home, options = {}) {
|
|
462
|
+
const env = options.env || process.env;
|
|
463
|
+
const binary = detectContextModeBinary({ env });
|
|
464
|
+
const hasAnyConfig = hasAnyContextModeConfig(home);
|
|
465
|
+
const configured = binary.present && hasCompleteContextModeConfig(home);
|
|
466
|
+
const state = configured ? 'configured' : hasAnyConfig ? 'broken' : 'absent';
|
|
467
|
+
const summary = configured
|
|
468
|
+
? `context-mode configured (${binary.version || 'version unknown'})`
|
|
469
|
+
: hasAnyConfig
|
|
470
|
+
? `context-mode partially configured; run aw integration add ${INTEGRATION_NAME}`
|
|
471
|
+
: `context-mode not configured${binary.present ? '' : '; binary not installed'}`;
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
name: INTEGRATION_NAME,
|
|
475
|
+
installed: binary.present,
|
|
476
|
+
configured,
|
|
477
|
+
state,
|
|
478
|
+
version: binary.version,
|
|
479
|
+
path: binary.path,
|
|
480
|
+
summary,
|
|
481
|
+
binary,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function getContextModeDoctorStatus(home, options = {}) {
|
|
486
|
+
const summary = getContextModeIntegrationSummary(home, options);
|
|
487
|
+
if (summary.configured) {
|
|
488
|
+
return [{
|
|
489
|
+
id: 'context-mode',
|
|
490
|
+
title: 'Context Mode integration',
|
|
491
|
+
status: 'pass',
|
|
492
|
+
summary: summary.summary,
|
|
493
|
+
fix: null,
|
|
494
|
+
}];
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (summary.state === 'broken') {
|
|
498
|
+
return [{
|
|
499
|
+
id: 'context-mode',
|
|
500
|
+
title: 'Context Mode integration',
|
|
501
|
+
status: 'warn',
|
|
502
|
+
summary: summary.summary,
|
|
503
|
+
fix: `Run \`aw integration add ${INTEGRATION_NAME}\` to repair or \`aw integration remove ${INTEGRATION_NAME}\` to clean it up.`,
|
|
504
|
+
}];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return [{
|
|
508
|
+
id: 'context-mode',
|
|
509
|
+
title: 'Context Mode integration',
|
|
510
|
+
status: 'pass',
|
|
511
|
+
summary: summary.summary,
|
|
512
|
+
fix: null,
|
|
513
|
+
}];
|
|
514
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensureContextModeIntegration,
|
|
3
|
+
getContextModeIntegrationSummary,
|
|
4
|
+
removeContextModeIntegration,
|
|
5
|
+
} from './context-mode.mjs';
|
|
6
|
+
|
|
7
|
+
export const KNOWN_INTEGRATIONS = [
|
|
8
|
+
{
|
|
9
|
+
name: 'context-mode',
|
|
10
|
+
description: 'Local context-mode MCP server and hook integration.',
|
|
11
|
+
add: ensureContextModeIntegration,
|
|
12
|
+
remove: removeContextModeIntegration,
|
|
13
|
+
status: getContextModeIntegrationSummary,
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export function resolveIntegration(name) {
|
|
18
|
+
const normalized = String(name || '').trim().toLowerCase();
|
|
19
|
+
const integration = KNOWN_INTEGRATIONS.find(item => item.name === normalized);
|
|
20
|
+
if (integration) return integration;
|
|
21
|
+
|
|
22
|
+
const known = KNOWN_INTEGRATIONS.map(item => item.name).join(', ');
|
|
23
|
+
throw new Error(`Unknown integration: ${name || '<missing>'}. Known integrations: ${known}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function listIntegrations(home, options = {}) {
|
|
27
|
+
return KNOWN_INTEGRATIONS.map(integration => ({
|
|
28
|
+
...integration.status(home, options),
|
|
29
|
+
description: integration.description,
|
|
30
|
+
}));
|
|
31
|
+
}
|