@ghl-ai/aw 0.1.56-beta.1 → 0.1.57
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/cli.mjs +2 -5
- package/commands/init.mjs +56 -13
- package/commands/integration.mjs +38 -77
- package/commands/integrations.mjs +84 -100
- package/ecc.mjs +14 -3
- package/git.mjs +2 -0
- package/integrations/context-mode.mjs +312 -89
- package/integrations.mjs +153 -40
- package/package-manager.mjs +72 -0
- package/package.json +4 -3
- package/update.mjs +29 -14
- package/integrations/index.mjs +0 -31
|
@@ -1,64 +1,67 @@
|
|
|
1
|
-
import { accessSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { accessSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { constants as fsConstants } from 'node:fs';
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import { delimiter, dirname, join } from 'node:path';
|
|
4
|
+
import { delimiter, dirname, isAbsolute, join } from 'node:path';
|
|
5
5
|
|
|
6
6
|
import TOML from '@iarna/toml';
|
|
7
|
+
import {
|
|
8
|
+
buildGlobalPackageInstall,
|
|
9
|
+
formatCommand,
|
|
10
|
+
getNpmPrefixBin,
|
|
11
|
+
installGlobalPackage,
|
|
12
|
+
} from '../package-manager.mjs';
|
|
7
13
|
|
|
8
14
|
const INTEGRATION_NAME = 'context-mode';
|
|
9
15
|
const BINARY_NAME = 'context-mode';
|
|
16
|
+
const PACKAGE_SPEC = 'context-mode@latest';
|
|
10
17
|
const INSTALL_STATE_RELATIVE_PATH = ['.aw', 'context-mode-install-state.json'];
|
|
11
18
|
const INSTALL_STATE_VERSION = 1;
|
|
12
|
-
const CONTEXT_MODE_MCP_SERVER = { command: BINARY_NAME };
|
|
13
19
|
const MCP_TARGETS = {
|
|
14
20
|
codex: 'codex-config-toml',
|
|
15
21
|
cursor: 'cursor-mcp-json',
|
|
16
22
|
claude: 'claude-json',
|
|
17
23
|
};
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
Stop: 'context-mode hook codex stop',
|
|
26
|
-
};
|
|
25
|
+
function shellQuote(value) {
|
|
26
|
+
const text = String(value);
|
|
27
|
+
if (!isAbsolute(text)) return text;
|
|
28
|
+
if (process.platform === 'win32') return `"${text.replace(/"/g, '\\"')}"`;
|
|
29
|
+
return `'${text.replace(/'/g, `'\\''`)}'`;
|
|
30
|
+
}
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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',
|
|
52
|
-
};
|
|
32
|
+
function hookCommand(commandTarget, harness, phase) {
|
|
33
|
+
return `${shellQuote(commandTarget)} hook ${harness} ${phase}`;
|
|
34
|
+
}
|
|
53
35
|
|
|
54
|
-
|
|
55
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
36
|
+
function codexHookCommands(commandTarget = BINARY_NAME) {
|
|
37
|
+
return {
|
|
38
|
+
PreToolUse: hookCommand(commandTarget, 'codex', 'pretooluse'),
|
|
39
|
+
PostToolUse: hookCommand(commandTarget, 'codex', 'posttooluse'),
|
|
40
|
+
SessionStart: hookCommand(commandTarget, 'codex', 'sessionstart'),
|
|
41
|
+
PreCompact: hookCommand(commandTarget, 'codex', 'precompact'),
|
|
42
|
+
UserPromptSubmit: hookCommand(commandTarget, 'codex', 'userpromptsubmit'),
|
|
43
|
+
Stop: hookCommand(commandTarget, 'codex', 'stop'),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function codexHookMatcherForPhase(phase) {
|
|
48
|
+
if (phase === 'SessionStart') return 'startup|resume';
|
|
49
|
+
if (phase === 'PreToolUse' || phase === 'PostToolUse') return '*';
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function cursorHookCommands(commandTarget = BINARY_NAME) {
|
|
54
|
+
const preToolUse = hookCommand(commandTarget, 'cursor', 'pretooluse');
|
|
55
|
+
const postToolUse = hookCommand(commandTarget, 'cursor', 'posttooluse');
|
|
56
|
+
return [
|
|
57
|
+
{ event: 'beforeShellExecution', command: preToolUse },
|
|
58
|
+
{ event: 'beforeMCPExecution', command: preToolUse },
|
|
59
|
+
{ event: 'afterShellExecution', command: postToolUse },
|
|
60
|
+
{ event: 'afterFileEdit', command: postToolUse },
|
|
61
|
+
{ event: 'afterMCPExecution', command: postToolUse },
|
|
62
|
+
{ event: 'stop', command: hookCommand(commandTarget, 'cursor', 'stop') },
|
|
63
|
+
];
|
|
64
|
+
}
|
|
62
65
|
|
|
63
66
|
function truthy(value) {
|
|
64
67
|
return ['1', 'true', 'yes', 'on'].includes(String(value || '').trim().toLowerCase());
|
|
@@ -142,8 +145,16 @@ function unmarkMcpTargetOwned(installState, target) {
|
|
|
142
145
|
installState.mcpServers = installState.mcpServers.filter(item => item !== target);
|
|
143
146
|
}
|
|
144
147
|
|
|
148
|
+
function mcpServerConfig(commandTarget = BINARY_NAME) {
|
|
149
|
+
return { command: commandTarget };
|
|
150
|
+
}
|
|
151
|
+
|
|
145
152
|
function isDesiredMcpServer(value) {
|
|
146
|
-
return
|
|
153
|
+
return value
|
|
154
|
+
&& typeof value === 'object'
|
|
155
|
+
&& !Array.isArray(value)
|
|
156
|
+
&& Object.keys(value).length === 1
|
|
157
|
+
&& isContextModeMcpCommand(value.command);
|
|
147
158
|
}
|
|
148
159
|
|
|
149
160
|
function parseTomlConfig(filePath, warnings) {
|
|
@@ -189,8 +200,108 @@ function findExecutable(binaryName, env = process.env) {
|
|
|
189
200
|
return null;
|
|
190
201
|
}
|
|
191
202
|
|
|
203
|
+
function findExecutableInDirs(binaryName, dirs = []) {
|
|
204
|
+
for (const dir of unique(dirs.filter(Boolean))) {
|
|
205
|
+
for (const candidate of executableCandidates(binaryName)) {
|
|
206
|
+
const fullPath = join(dir, candidate);
|
|
207
|
+
try {
|
|
208
|
+
accessSync(fullPath, fsConstants.X_OK);
|
|
209
|
+
return fullPath;
|
|
210
|
+
} catch {
|
|
211
|
+
// Keep scanning known package-manager directories.
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function childDirs(parent) {
|
|
219
|
+
try {
|
|
220
|
+
return readdirSync(parent, { withFileTypes: true })
|
|
221
|
+
.filter(entry => entry.isDirectory())
|
|
222
|
+
.map(entry => join(parent, entry.name));
|
|
223
|
+
} catch {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function readManifestBinaryPath(home) {
|
|
229
|
+
const manifestPath = join(home, '.aw_registry', '.integration-manifest.json');
|
|
230
|
+
if (!existsSync(manifestPath)) return null;
|
|
231
|
+
try {
|
|
232
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
233
|
+
return manifest.installed?.[INTEGRATION_NAME]?.binaryPath || null;
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function knownExecutableDirs(home, env = process.env, options = {}) {
|
|
240
|
+
const dirs = [];
|
|
241
|
+
const skipSystemPrefixes = env.AW_CONTEXT_MODE_SKIP_SYSTEM_PREFIXES === '1';
|
|
242
|
+
|
|
243
|
+
const manifestPath = readManifestBinaryPath(home);
|
|
244
|
+
if (manifestPath) dirs.push(dirname(manifestPath));
|
|
245
|
+
|
|
246
|
+
if (Array.isArray(options.extraSearchDirs)) dirs.push(...options.extraSearchDirs);
|
|
247
|
+
|
|
248
|
+
const npmPrefixBin = options.npmPrefixBin ?? getNpmPrefixBin({ env });
|
|
249
|
+
if (npmPrefixBin) dirs.push(npmPrefixBin);
|
|
250
|
+
|
|
251
|
+
dirs.push(
|
|
252
|
+
join(home, '.volta', 'bin'),
|
|
253
|
+
join(home, '.local', 'share', 'pnpm'),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const voltaPackages = join(home, '.volta', 'tools', 'image', 'packages');
|
|
257
|
+
dirs.push(
|
|
258
|
+
join(voltaPackages, 'context-mode', 'bin'),
|
|
259
|
+
join(voltaPackages, 'context-mode', 'lib', 'node_modules', 'context-mode'),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
for (const nodeDir of childDirs(join(home, '.volta', 'tools', 'image', 'node'))) {
|
|
263
|
+
dirs.push(join(nodeDir, 'bin'));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const nodeDir of childDirs(join(home, '.local', 'share', 'mise', 'installs', 'node'))) {
|
|
267
|
+
dirs.push(join(nodeDir, 'bin'));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (env.PNPM_HOME) dirs.push(env.PNPM_HOME);
|
|
271
|
+
|
|
272
|
+
if (process.platform === 'darwin' && !skipSystemPrefixes) {
|
|
273
|
+
dirs.push('/opt/homebrew/bin', '/usr/local/bin');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return unique(dirs);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function commandTargetForBinary(binary) {
|
|
280
|
+
return binary.source === 'path' ? BINARY_NAME : binary.path;
|
|
281
|
+
}
|
|
282
|
+
|
|
192
283
|
function isContextModeCommand(command) {
|
|
193
|
-
|
|
284
|
+
const text = String(command || '').trim();
|
|
285
|
+
if (text.startsWith('context-mode hook ')) return true;
|
|
286
|
+
return /(^|[/\\'" ])context-mode(?:\.cmd|\.exe)?['"]? hook /.test(text);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function isContextModeMcpCommand(command) {
|
|
290
|
+
const text = String(command || '').trim();
|
|
291
|
+
if (text === BINARY_NAME) return true;
|
|
292
|
+
const commandName = text.replace(/^['"]|['"]$/g, '').split(/[/\\]/).pop().toLowerCase();
|
|
293
|
+
return commandName === BINARY_NAME
|
|
294
|
+
|| commandName === `${BINARY_NAME}.cmd`
|
|
295
|
+
|| commandName === `${BINARY_NAME}.exe`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function normalizeCommandTarget(command) {
|
|
299
|
+
return String(command || '').trim().replace(/^['"]|['"]$/g, '');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function isExpectedMcpServer(value, expectedCommand) {
|
|
303
|
+
return isDesiredMcpServer(value)
|
|
304
|
+
&& normalizeCommandTarget(value.command) === normalizeCommandTarget(expectedCommand);
|
|
194
305
|
}
|
|
195
306
|
|
|
196
307
|
function hasHookCommand(entry, command) {
|
|
@@ -199,7 +310,7 @@ function hasHookCommand(entry, command) {
|
|
|
199
310
|
}
|
|
200
311
|
|
|
201
312
|
function hasCanonicalCodexMatcher(entry, phase) {
|
|
202
|
-
const expectedMatcher =
|
|
313
|
+
const expectedMatcher = codexHookMatcherForPhase(phase);
|
|
203
314
|
return expectedMatcher === undefined
|
|
204
315
|
? entry?.matcher === undefined
|
|
205
316
|
: entry?.matcher === expectedMatcher;
|
|
@@ -213,7 +324,7 @@ function buildCodexHookEntry(phase, command) {
|
|
|
213
324
|
const entry = {
|
|
214
325
|
hooks: [{ type: 'command', command }],
|
|
215
326
|
};
|
|
216
|
-
const matcher =
|
|
327
|
+
const matcher = codexHookMatcherForPhase(phase);
|
|
217
328
|
if (matcher !== undefined) entry.matcher = matcher;
|
|
218
329
|
return entry;
|
|
219
330
|
}
|
|
@@ -225,7 +336,7 @@ function ensureObject(parent, key) {
|
|
|
225
336
|
return parent[key];
|
|
226
337
|
}
|
|
227
338
|
|
|
228
|
-
function mergeJsonMcpServer(filePath, installState, target, dryRun = false) {
|
|
339
|
+
function mergeJsonMcpServer(filePath, installState, target, commandTarget = BINARY_NAME, dryRun = false) {
|
|
229
340
|
const config = readJson(filePath, {});
|
|
230
341
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
231
342
|
throw new Error(`Expected JSON object in ${filePath}`);
|
|
@@ -233,12 +344,16 @@ function mergeJsonMcpServer(filePath, installState, target, dryRun = false) {
|
|
|
233
344
|
|
|
234
345
|
const mcpServers = ensureObject(config, 'mcpServers');
|
|
235
346
|
const existing = mcpServers[INTEGRATION_NAME];
|
|
347
|
+
const desired = mcpServerConfig(commandTarget);
|
|
236
348
|
if (existing && !isDesiredMcpServer(existing)) {
|
|
237
349
|
throw new Error('Refusing to overwrite existing user-owned context-mode MCP config');
|
|
238
350
|
}
|
|
239
|
-
if (existing)
|
|
351
|
+
if (existing && JSON.stringify(existing) === JSON.stringify(desired)) {
|
|
352
|
+
markMcpTargetOwned(installState, target);
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
240
355
|
|
|
241
|
-
mcpServers[INTEGRATION_NAME] =
|
|
356
|
+
mcpServers[INTEGRATION_NAME] = desired;
|
|
242
357
|
markMcpTargetOwned(installState, target);
|
|
243
358
|
return writeJsonIfChanged(filePath, config, dryRun);
|
|
244
359
|
}
|
|
@@ -267,7 +382,7 @@ function removeJsonMcpServer(filePath, installState, target, warnings, dryRun =
|
|
|
267
382
|
return writeJsonIfChanged(filePath, config, dryRun);
|
|
268
383
|
}
|
|
269
384
|
|
|
270
|
-
function mergeCodexToml(filePath, warnings, installState, target, dryRun = false) {
|
|
385
|
+
function mergeCodexToml(filePath, warnings, installState, target, commandTarget = BINARY_NAME, dryRun = false) {
|
|
271
386
|
const config = parseTomlConfig(filePath, warnings);
|
|
272
387
|
if (config === null) return false;
|
|
273
388
|
const existing = config.mcp_servers?.[INTEGRATION_NAME];
|
|
@@ -285,10 +400,8 @@ function mergeCodexToml(filePath, warnings, installState, target, dryRun = false
|
|
|
285
400
|
config.mcp_servers = config.mcp_servers && typeof config.mcp_servers === 'object' && !Array.isArray(config.mcp_servers)
|
|
286
401
|
? config.mcp_servers
|
|
287
402
|
: {};
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
markMcpTargetOwned(installState, target);
|
|
291
|
-
}
|
|
403
|
+
config.mcp_servers[INTEGRATION_NAME] = mcpServerConfig(commandTarget);
|
|
404
|
+
markMcpTargetOwned(installState, target);
|
|
292
405
|
|
|
293
406
|
if (JSON.stringify(config) === before) return false;
|
|
294
407
|
return writeTomlIfChanged(filePath, config, dryRun);
|
|
@@ -333,7 +446,7 @@ function ensureCodexHook(config, phase, command) {
|
|
|
333
446
|
continue;
|
|
334
447
|
}
|
|
335
448
|
|
|
336
|
-
const nextHooks = entry.hooks.filter(hook => hook?.command
|
|
449
|
+
const nextHooks = entry.hooks.filter(hook => !isContextModeCommand(hook?.command));
|
|
337
450
|
if (nextHooks.length === 0) continue;
|
|
338
451
|
nextEntries.push(nextHooks.length === entry.hooks.length ? entry : { ...entry, hooks: nextHooks });
|
|
339
452
|
}
|
|
@@ -343,14 +456,14 @@ function ensureCodexHook(config, phase, command) {
|
|
|
343
456
|
return JSON.stringify(nextEntries) !== before;
|
|
344
457
|
}
|
|
345
458
|
|
|
346
|
-
function mergeCodexHooks(filePath, dryRun = false) {
|
|
459
|
+
function mergeCodexHooks(filePath, commandTarget = BINARY_NAME, dryRun = false) {
|
|
347
460
|
const config = readJson(filePath, {});
|
|
348
461
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
349
462
|
throw new Error(`Expected JSON object in ${filePath}`);
|
|
350
463
|
}
|
|
351
464
|
|
|
352
465
|
let changed = false;
|
|
353
|
-
for (const [phase, command] of Object.entries(
|
|
466
|
+
for (const [phase, command] of Object.entries(codexHookCommands(commandTarget))) {
|
|
354
467
|
changed = ensureCodexHook(config, phase, command) || changed;
|
|
355
468
|
}
|
|
356
469
|
return changed && writeJsonIfChanged(filePath, config, dryRun);
|
|
@@ -392,7 +505,7 @@ function ensureCursorHook(config, event, command) {
|
|
|
392
505
|
config.hooks[event] = entries;
|
|
393
506
|
}
|
|
394
507
|
|
|
395
|
-
function mergeCursorHooks(filePath, dryRun = false) {
|
|
508
|
+
function mergeCursorHooks(filePath, commandTarget = BINARY_NAME, dryRun = false) {
|
|
396
509
|
const config = readJson(filePath, {});
|
|
397
510
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
398
511
|
throw new Error(`Expected JSON object in ${filePath}`);
|
|
@@ -413,7 +526,7 @@ function mergeCursorHooks(filePath, dryRun = false) {
|
|
|
413
526
|
else delete config.hooks[phase];
|
|
414
527
|
}
|
|
415
528
|
|
|
416
|
-
for (const { event, command } of
|
|
529
|
+
for (const { event, command } of cursorHookCommands(commandTarget)) {
|
|
417
530
|
ensureCursorHook(config, event, command);
|
|
418
531
|
}
|
|
419
532
|
|
|
@@ -449,45 +562,61 @@ function removeCursorHooks(filePath, dryRun = false) {
|
|
|
449
562
|
return writeJsonIfChanged(filePath, config, dryRun);
|
|
450
563
|
}
|
|
451
564
|
|
|
452
|
-
function hasJsonMcpServer(filePath) {
|
|
565
|
+
function hasJsonMcpServer(filePath, expectedCommand = null) {
|
|
453
566
|
if (!existsSync(filePath)) return false;
|
|
454
567
|
try {
|
|
455
|
-
|
|
568
|
+
const server = readJson(filePath, {}).mcpServers?.[INTEGRATION_NAME];
|
|
569
|
+
return expectedCommand
|
|
570
|
+
? isExpectedMcpServer(server, expectedCommand)
|
|
571
|
+
: isContextModeMcpCommand(server?.command);
|
|
456
572
|
} catch {
|
|
457
573
|
return false;
|
|
458
574
|
}
|
|
459
575
|
}
|
|
460
576
|
|
|
461
|
-
function hasCodexMcpServer(filePath) {
|
|
577
|
+
function hasCodexMcpServer(filePath, expectedCommand = null) {
|
|
462
578
|
if (!existsSync(filePath)) return false;
|
|
463
579
|
try {
|
|
464
580
|
const config = TOML.parse(readFileSync(filePath, 'utf8'));
|
|
465
|
-
|
|
581
|
+
const server = config.mcp_servers?.[INTEGRATION_NAME];
|
|
582
|
+
return expectedCommand
|
|
583
|
+
? isExpectedMcpServer(server, expectedCommand)
|
|
584
|
+
: isContextModeMcpCommand(server?.command);
|
|
466
585
|
} catch {
|
|
467
586
|
return false;
|
|
468
587
|
}
|
|
469
588
|
}
|
|
470
589
|
|
|
471
|
-
function hasCodexHookCoverage(filePath) {
|
|
590
|
+
function hasCodexHookCoverage(filePath, commandTarget = null) {
|
|
472
591
|
if (!existsSync(filePath)) return false;
|
|
473
592
|
try {
|
|
474
593
|
const config = readJson(filePath, {});
|
|
475
|
-
|
|
594
|
+
const expectedCommands = commandTarget ? codexHookCommands(commandTarget) : null;
|
|
595
|
+
return Object.keys(codexHookCommands()).every(phase =>
|
|
476
596
|
Array.isArray(config.hooks?.[phase])
|
|
477
|
-
&& config.hooks[phase].some(entry =>
|
|
597
|
+
&& config.hooks[phase].some(entry =>
|
|
598
|
+
hasCanonicalCodexMatcher(entry, phase)
|
|
599
|
+
&& Array.isArray(entry?.hooks)
|
|
600
|
+
&& entry.hooks.some(hook => expectedCommands
|
|
601
|
+
? hook?.command === expectedCommands[phase]
|
|
602
|
+
: isContextModeCommand(hook?.command))
|
|
603
|
+
)
|
|
478
604
|
);
|
|
479
605
|
} catch {
|
|
480
606
|
return false;
|
|
481
607
|
}
|
|
482
608
|
}
|
|
483
609
|
|
|
484
|
-
function hasCursorHookCoverage(filePath) {
|
|
610
|
+
function hasCursorHookCoverage(filePath, commandTarget = null) {
|
|
485
611
|
if (!existsSync(filePath)) return false;
|
|
486
612
|
try {
|
|
487
613
|
const config = readJson(filePath, {});
|
|
488
|
-
|
|
614
|
+
const expectedCommands = commandTarget ? cursorHookCommands(commandTarget) : null;
|
|
615
|
+
return cursorHookCommands().every(({ event }) =>
|
|
489
616
|
Array.isArray(config.hooks?.[event])
|
|
490
|
-
&& config.hooks[event].some(entry => entry?.
|
|
617
|
+
&& config.hooks[event].some(entry => entry?.event === event && (expectedCommands
|
|
618
|
+
? entry?.command === expectedCommands.find(command => command.event === event)?.command
|
|
619
|
+
: isContextModeCommand(entry?.command)))
|
|
491
620
|
);
|
|
492
621
|
} catch {
|
|
493
622
|
return false;
|
|
@@ -502,12 +631,12 @@ function hasAnyContextModeConfig(home) {
|
|
|
502
631
|
|| hasJsonMcpServer(join(home, '.claude.json'));
|
|
503
632
|
}
|
|
504
633
|
|
|
505
|
-
function hasCompleteContextModeConfig(home) {
|
|
506
|
-
return hasCodexMcpServer(join(home, '.codex', 'config.toml'))
|
|
507
|
-
&& hasCodexHookCoverage(join(home, '.codex', 'hooks.json'))
|
|
508
|
-
&& hasJsonMcpServer(join(home, '.cursor', 'mcp.json'))
|
|
509
|
-
&& hasCursorHookCoverage(join(home, '.cursor', 'hooks.json'))
|
|
510
|
-
&& hasJsonMcpServer(join(home, '.claude.json'));
|
|
634
|
+
function hasCompleteContextModeConfig(home, commandTarget = BINARY_NAME) {
|
|
635
|
+
return hasCodexMcpServer(join(home, '.codex', 'config.toml'), commandTarget)
|
|
636
|
+
&& hasCodexHookCoverage(join(home, '.codex', 'hooks.json'), commandTarget)
|
|
637
|
+
&& hasJsonMcpServer(join(home, '.cursor', 'mcp.json'), commandTarget)
|
|
638
|
+
&& hasCursorHookCoverage(join(home, '.cursor', 'hooks.json'), commandTarget)
|
|
639
|
+
&& hasJsonMcpServer(join(home, '.claude.json'), commandTarget);
|
|
511
640
|
}
|
|
512
641
|
|
|
513
642
|
export function isContextModeRequested(args = {}, env = process.env) {
|
|
@@ -531,6 +660,10 @@ export function detectContextModeBinary(options = {}) {
|
|
|
531
660
|
};
|
|
532
661
|
}
|
|
533
662
|
|
|
663
|
+
return describeContextModeBinary(binaryPath, env, 'path');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function describeContextModeBinary(binaryPath, env = process.env, source = 'path') {
|
|
534
667
|
const versionEnv = {
|
|
535
668
|
...process.env,
|
|
536
669
|
...env,
|
|
@@ -557,11 +690,85 @@ export function detectContextModeBinary(options = {}) {
|
|
|
557
690
|
return {
|
|
558
691
|
present: true,
|
|
559
692
|
path: binaryPath,
|
|
693
|
+
source,
|
|
694
|
+
commandTarget: source === 'path' ? BINARY_NAME : binaryPath,
|
|
560
695
|
version: versionOutput || 'unknown',
|
|
561
696
|
reason: null,
|
|
562
697
|
};
|
|
563
698
|
}
|
|
564
699
|
|
|
700
|
+
export function resolveContextModeExecutable(options = {}) {
|
|
701
|
+
const env = options.env || process.env;
|
|
702
|
+
const home = options.home || env.HOME || process.env.HOME;
|
|
703
|
+
const pathBinary = findExecutable(BINARY_NAME, env);
|
|
704
|
+
if (pathBinary) return describeContextModeBinary(pathBinary, env, 'path');
|
|
705
|
+
|
|
706
|
+
const knownBinary = findExecutableInDirs(BINARY_NAME, knownExecutableDirs(home, env, options));
|
|
707
|
+
if (knownBinary) return describeContextModeBinary(knownBinary, env, 'known-prefix');
|
|
708
|
+
|
|
709
|
+
return {
|
|
710
|
+
present: false,
|
|
711
|
+
path: null,
|
|
712
|
+
source: null,
|
|
713
|
+
commandTarget: null,
|
|
714
|
+
version: null,
|
|
715
|
+
reason: `${BINARY_NAME} binary not found on PATH or known package-manager prefixes`,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export function ensureContextModeBinary(options = {}) {
|
|
720
|
+
const env = options.env || process.env;
|
|
721
|
+
const dryRun = options.dryRun === true;
|
|
722
|
+
const home = options.home || env.HOME || process.env.HOME;
|
|
723
|
+
const warnings = [];
|
|
724
|
+
const resolved = resolveContextModeExecutable({ ...options, env, home });
|
|
725
|
+
if (resolved.present) {
|
|
726
|
+
return { installed: false, plannedInstall: false, binary: resolved, warnings, install: null };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const installPlan = buildGlobalPackageInstall(PACKAGE_SPEC, options.packageManager ? { packageManager: options.packageManager } : {});
|
|
730
|
+
if (dryRun) {
|
|
731
|
+
return {
|
|
732
|
+
installed: false,
|
|
733
|
+
plannedInstall: true,
|
|
734
|
+
binary: resolved,
|
|
735
|
+
warnings,
|
|
736
|
+
install: { ...installPlan, commandLine: formatCommand(installPlan) },
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (env.AW_CONTEXT_MODE_DISABLE_PACKAGE_INSTALL === '1') {
|
|
741
|
+
warnings.push(`context-mode package install disabled; planned command: ${formatCommand(installPlan)}`);
|
|
742
|
+
return {
|
|
743
|
+
installed: false,
|
|
744
|
+
plannedInstall: false,
|
|
745
|
+
binary: resolved,
|
|
746
|
+
warnings,
|
|
747
|
+
install: { ...installPlan, ok: false, commandLine: formatCommand(installPlan) },
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const installer = options.installPackage || installGlobalPackage;
|
|
752
|
+
const install = installer(PACKAGE_SPEC, {
|
|
753
|
+
env,
|
|
754
|
+
cwd: options.cwd,
|
|
755
|
+
packageManager: options.packageManager,
|
|
756
|
+
stdio: options.stdio,
|
|
757
|
+
});
|
|
758
|
+
if (!install?.ok) {
|
|
759
|
+
warnings.push(`context-mode package install failed: ${install?.error?.message || install?.stderr || install?.commandLine || 'unknown error'}`);
|
|
760
|
+
return { installed: false, plannedInstall: false, binary: resolved, warnings, install };
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const afterInstall = resolveContextModeExecutable({ ...options, env, home });
|
|
764
|
+
if (!afterInstall.present) {
|
|
765
|
+
warnings.push(`context-mode installed but binary could not be resolved: ${afterInstall.reason}`);
|
|
766
|
+
return { installed: true, plannedInstall: false, binary: afterInstall, warnings, install };
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return { installed: true, plannedInstall: false, binary: afterInstall, warnings, install };
|
|
770
|
+
}
|
|
771
|
+
|
|
565
772
|
export function ensureContextModeIntegration(home, options = {}) {
|
|
566
773
|
const env = options.env || process.env;
|
|
567
774
|
const dryRun = options.dryRun === true;
|
|
@@ -569,24 +776,31 @@ export function ensureContextModeIntegration(home, options = {}) {
|
|
|
569
776
|
const changedFiles = [];
|
|
570
777
|
const configuredHarnesses = [];
|
|
571
778
|
const installState = readInstallState(home);
|
|
572
|
-
const
|
|
779
|
+
const binaryResult = ensureContextModeBinary({ ...options, env, home, dryRun });
|
|
780
|
+
warnings.push(...binaryResult.warnings);
|
|
781
|
+
const binary = binaryResult.binary;
|
|
573
782
|
|
|
574
783
|
if (!binary.present) {
|
|
575
784
|
return {
|
|
576
785
|
changedFiles,
|
|
577
|
-
warnings: [`context-mode binary is required before config mutation: ${binary.reason}`],
|
|
786
|
+
warnings: warnings.length > 0 ? warnings : [`context-mode binary is required before config mutation: ${binary.reason}`],
|
|
578
787
|
configuredHarnesses,
|
|
788
|
+
complete: false,
|
|
579
789
|
binary,
|
|
580
790
|
dryRun,
|
|
791
|
+
installedPackage: binaryResult.installed,
|
|
792
|
+
plannedInstall: binaryResult.plannedInstall,
|
|
793
|
+
install: binaryResult.install,
|
|
581
794
|
};
|
|
582
795
|
}
|
|
583
796
|
|
|
797
|
+
const commandTarget = commandTargetForBinary(binary);
|
|
584
798
|
const updates = [
|
|
585
|
-
['Codex MCP', join(home, '.codex', 'config.toml'), () => mergeCodexToml(join(home, '.codex', 'config.toml'), warnings, installState, MCP_TARGETS.codex, dryRun)],
|
|
586
|
-
['Codex hooks', join(home, '.codex', 'hooks.json'), () => mergeCodexHooks(join(home, '.codex', 'hooks.json'), dryRun)],
|
|
587
|
-
['Cursor MCP', join(home, '.cursor', 'mcp.json'), () => mergeJsonMcpServer(join(home, '.cursor', 'mcp.json'), installState, MCP_TARGETS.cursor, dryRun)],
|
|
588
|
-
['Cursor hooks', join(home, '.cursor', 'hooks.json'), () => mergeCursorHooks(join(home, '.cursor', 'hooks.json'), dryRun)],
|
|
589
|
-
['Claude MCP', join(home, '.claude.json'), () => mergeJsonMcpServer(join(home, '.claude.json'), installState, MCP_TARGETS.claude, dryRun)],
|
|
799
|
+
['Codex MCP', join(home, '.codex', 'config.toml'), () => mergeCodexToml(join(home, '.codex', 'config.toml'), warnings, installState, MCP_TARGETS.codex, commandTarget, dryRun)],
|
|
800
|
+
['Codex hooks', join(home, '.codex', 'hooks.json'), () => mergeCodexHooks(join(home, '.codex', 'hooks.json'), commandTarget, dryRun)],
|
|
801
|
+
['Cursor MCP', join(home, '.cursor', 'mcp.json'), () => mergeJsonMcpServer(join(home, '.cursor', 'mcp.json'), installState, MCP_TARGETS.cursor, commandTarget, dryRun)],
|
|
802
|
+
['Cursor hooks', join(home, '.cursor', 'hooks.json'), () => mergeCursorHooks(join(home, '.cursor', 'hooks.json'), commandTarget, dryRun)],
|
|
803
|
+
['Claude MCP', join(home, '.claude.json'), () => mergeJsonMcpServer(join(home, '.claude.json'), installState, MCP_TARGETS.claude, commandTarget, dryRun)],
|
|
590
804
|
];
|
|
591
805
|
|
|
592
806
|
for (const [label, filePath, update] of updates) {
|
|
@@ -600,12 +814,21 @@ export function ensureContextModeIntegration(home, options = {}) {
|
|
|
600
814
|
changedFiles.push(statePath);
|
|
601
815
|
}
|
|
602
816
|
|
|
817
|
+
const complete = hasCompleteContextModeConfig(home, commandTarget);
|
|
818
|
+
if (!dryRun && !complete) {
|
|
819
|
+
warnings.push(`context-mode config is partially configured; fix the warnings above and rerun aw integrations add ${INTEGRATION_NAME}`);
|
|
820
|
+
}
|
|
821
|
+
|
|
603
822
|
return {
|
|
604
823
|
changedFiles: unique(changedFiles),
|
|
605
824
|
warnings,
|
|
606
825
|
configuredHarnesses,
|
|
826
|
+
complete,
|
|
607
827
|
binary,
|
|
608
828
|
dryRun,
|
|
829
|
+
installedPackage: binaryResult.installed,
|
|
830
|
+
plannedInstall: binaryResult.plannedInstall,
|
|
831
|
+
install: binaryResult.install,
|
|
609
832
|
};
|
|
610
833
|
}
|
|
611
834
|
|
|
@@ -640,15 +863,15 @@ export function removeContextModeIntegration(home, options = {}) {
|
|
|
640
863
|
|
|
641
864
|
export function getContextModeIntegrationSummary(home, options = {}) {
|
|
642
865
|
const env = options.env || process.env;
|
|
643
|
-
const binary =
|
|
866
|
+
const binary = resolveContextModeExecutable({ ...options, env, home });
|
|
644
867
|
const hasAnyConfig = hasAnyContextModeConfig(home);
|
|
645
|
-
const configured = binary.present && hasCompleteContextModeConfig(home);
|
|
868
|
+
const configured = binary.present && hasCompleteContextModeConfig(home, commandTargetForBinary(binary));
|
|
646
869
|
const state = configured ? 'configured' : hasAnyConfig ? 'broken' : 'absent';
|
|
647
870
|
const summary = configured
|
|
648
871
|
? `context-mode configured (${binary.version || 'version unknown'})`
|
|
649
872
|
: hasAnyConfig
|
|
650
|
-
? `context-mode partially configured; run aw
|
|
651
|
-
: `context-mode not configured${binary.present ?
|
|
873
|
+
? `context-mode partially configured; run aw integrations add ${INTEGRATION_NAME}`
|
|
874
|
+
: `context-mode not configured${binary.present ? `; binary discoverable via ${binary.source}` : '; binary not installed'}`;
|
|
652
875
|
|
|
653
876
|
return {
|
|
654
877
|
name: INTEGRATION_NAME,
|
|
@@ -680,7 +903,7 @@ export function getContextModeDoctorStatus(home, options = {}) {
|
|
|
680
903
|
title: 'Context Mode integration',
|
|
681
904
|
status: 'warn',
|
|
682
905
|
summary: summary.summary,
|
|
683
|
-
fix: `Run \`aw
|
|
906
|
+
fix: `Run \`aw integrations add ${INTEGRATION_NAME}\` to repair or \`aw integrations remove ${INTEGRATION_NAME}\` to clean it up.`,
|
|
684
907
|
}];
|
|
685
908
|
}
|
|
686
909
|
|