@ghl-ai/aw 0.1.57 → 0.1.58-beta.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/cli.mjs +5 -2
- package/commands/init.mjs +13 -56
- package/commands/integration.mjs +77 -38
- package/commands/integrations.mjs +100 -84
- package/ecc.mjs +3 -14
- package/git.mjs +0 -2
- package/integrations/context-mode.mjs +89 -312
- package/integrations/index.mjs +31 -0
- package/integrations.mjs +40 -153
- package/link.mjs +69 -3
- package/package.json +3 -4
- package/update.mjs +14 -29
- package/package-manager.mjs +0 -72
|
@@ -1,67 +1,64 @@
|
|
|
1
|
-
import { accessSync, existsSync, mkdirSync, readFileSync,
|
|
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
|
-
import { delimiter, dirname,
|
|
4
|
+
import { delimiter, dirname, 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';
|
|
13
7
|
|
|
14
8
|
const INTEGRATION_NAME = 'context-mode';
|
|
15
9
|
const BINARY_NAME = 'context-mode';
|
|
16
|
-
const PACKAGE_SPEC = 'context-mode@latest';
|
|
17
10
|
const INSTALL_STATE_RELATIVE_PATH = ['.aw', 'context-mode-install-state.json'];
|
|
18
11
|
const INSTALL_STATE_VERSION = 1;
|
|
12
|
+
const CONTEXT_MODE_MCP_SERVER = { command: BINARY_NAME };
|
|
19
13
|
const MCP_TARGETS = {
|
|
20
14
|
codex: 'codex-config-toml',
|
|
21
15
|
cursor: 'cursor-mcp-json',
|
|
22
16
|
claude: 'claude-json',
|
|
23
17
|
};
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return `${shellQuote(commandTarget)} hook ${harness} ${phase}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
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
|
-
}
|
|
19
|
+
const CODEX_HOOK_COMMANDS = {
|
|
20
|
+
PreToolUse: 'context-mode hook codex pretooluse',
|
|
21
|
+
PostToolUse: 'context-mode hook codex posttooluse',
|
|
22
|
+
SessionStart: 'context-mode hook codex sessionstart',
|
|
23
|
+
PreCompact: 'context-mode hook codex precompact',
|
|
24
|
+
UserPromptSubmit: 'context-mode hook codex userpromptsubmit',
|
|
25
|
+
Stop: 'context-mode hook codex stop',
|
|
26
|
+
};
|
|
46
27
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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',
|
|
52
|
+
};
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
{ event: 'afterMCPExecution', command: postToolUse },
|
|
62
|
-
{ event: 'stop', command: hookCommand(commandTarget, 'cursor', 'stop') },
|
|
63
|
-
];
|
|
64
|
-
}
|
|
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
|
+
];
|
|
65
62
|
|
|
66
63
|
function truthy(value) {
|
|
67
64
|
return ['1', 'true', 'yes', 'on'].includes(String(value || '').trim().toLowerCase());
|
|
@@ -145,16 +142,8 @@ function unmarkMcpTargetOwned(installState, target) {
|
|
|
145
142
|
installState.mcpServers = installState.mcpServers.filter(item => item !== target);
|
|
146
143
|
}
|
|
147
144
|
|
|
148
|
-
function mcpServerConfig(commandTarget = BINARY_NAME) {
|
|
149
|
-
return { command: commandTarget };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
145
|
function isDesiredMcpServer(value) {
|
|
153
|
-
return value
|
|
154
|
-
&& typeof value === 'object'
|
|
155
|
-
&& !Array.isArray(value)
|
|
156
|
-
&& Object.keys(value).length === 1
|
|
157
|
-
&& isContextModeMcpCommand(value.command);
|
|
146
|
+
return JSON.stringify(value) === JSON.stringify(CONTEXT_MODE_MCP_SERVER);
|
|
158
147
|
}
|
|
159
148
|
|
|
160
149
|
function parseTomlConfig(filePath, warnings) {
|
|
@@ -200,108 +189,8 @@ function findExecutable(binaryName, env = process.env) {
|
|
|
200
189
|
return null;
|
|
201
190
|
}
|
|
202
191
|
|
|
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
|
-
|
|
283
192
|
function isContextModeCommand(command) {
|
|
284
|
-
|
|
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);
|
|
193
|
+
return String(command || '').startsWith('context-mode hook ');
|
|
305
194
|
}
|
|
306
195
|
|
|
307
196
|
function hasHookCommand(entry, command) {
|
|
@@ -310,7 +199,7 @@ function hasHookCommand(entry, command) {
|
|
|
310
199
|
}
|
|
311
200
|
|
|
312
201
|
function hasCanonicalCodexMatcher(entry, phase) {
|
|
313
|
-
const expectedMatcher =
|
|
202
|
+
const expectedMatcher = CODEX_HOOK_MATCHERS[phase];
|
|
314
203
|
return expectedMatcher === undefined
|
|
315
204
|
? entry?.matcher === undefined
|
|
316
205
|
: entry?.matcher === expectedMatcher;
|
|
@@ -324,7 +213,7 @@ function buildCodexHookEntry(phase, command) {
|
|
|
324
213
|
const entry = {
|
|
325
214
|
hooks: [{ type: 'command', command }],
|
|
326
215
|
};
|
|
327
|
-
const matcher =
|
|
216
|
+
const matcher = CODEX_HOOK_MATCHERS[phase];
|
|
328
217
|
if (matcher !== undefined) entry.matcher = matcher;
|
|
329
218
|
return entry;
|
|
330
219
|
}
|
|
@@ -336,7 +225,7 @@ function ensureObject(parent, key) {
|
|
|
336
225
|
return parent[key];
|
|
337
226
|
}
|
|
338
227
|
|
|
339
|
-
function mergeJsonMcpServer(filePath, installState, target,
|
|
228
|
+
function mergeJsonMcpServer(filePath, installState, target, dryRun = false) {
|
|
340
229
|
const config = readJson(filePath, {});
|
|
341
230
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
342
231
|
throw new Error(`Expected JSON object in ${filePath}`);
|
|
@@ -344,16 +233,12 @@ function mergeJsonMcpServer(filePath, installState, target, commandTarget = BINA
|
|
|
344
233
|
|
|
345
234
|
const mcpServers = ensureObject(config, 'mcpServers');
|
|
346
235
|
const existing = mcpServers[INTEGRATION_NAME];
|
|
347
|
-
const desired = mcpServerConfig(commandTarget);
|
|
348
236
|
if (existing && !isDesiredMcpServer(existing)) {
|
|
349
237
|
throw new Error('Refusing to overwrite existing user-owned context-mode MCP config');
|
|
350
238
|
}
|
|
351
|
-
if (existing
|
|
352
|
-
markMcpTargetOwned(installState, target);
|
|
353
|
-
return false;
|
|
354
|
-
}
|
|
239
|
+
if (existing) return false;
|
|
355
240
|
|
|
356
|
-
mcpServers[INTEGRATION_NAME] =
|
|
241
|
+
mcpServers[INTEGRATION_NAME] = cloneJson(CONTEXT_MODE_MCP_SERVER);
|
|
357
242
|
markMcpTargetOwned(installState, target);
|
|
358
243
|
return writeJsonIfChanged(filePath, config, dryRun);
|
|
359
244
|
}
|
|
@@ -382,7 +267,7 @@ function removeJsonMcpServer(filePath, installState, target, warnings, dryRun =
|
|
|
382
267
|
return writeJsonIfChanged(filePath, config, dryRun);
|
|
383
268
|
}
|
|
384
269
|
|
|
385
|
-
function mergeCodexToml(filePath, warnings, installState, target,
|
|
270
|
+
function mergeCodexToml(filePath, warnings, installState, target, dryRun = false) {
|
|
386
271
|
const config = parseTomlConfig(filePath, warnings);
|
|
387
272
|
if (config === null) return false;
|
|
388
273
|
const existing = config.mcp_servers?.[INTEGRATION_NAME];
|
|
@@ -400,8 +285,10 @@ function mergeCodexToml(filePath, warnings, installState, target, commandTarget
|
|
|
400
285
|
config.mcp_servers = config.mcp_servers && typeof config.mcp_servers === 'object' && !Array.isArray(config.mcp_servers)
|
|
401
286
|
? config.mcp_servers
|
|
402
287
|
: {};
|
|
403
|
-
|
|
404
|
-
|
|
288
|
+
if (!existing) {
|
|
289
|
+
config.mcp_servers[INTEGRATION_NAME] = cloneJson(CONTEXT_MODE_MCP_SERVER);
|
|
290
|
+
markMcpTargetOwned(installState, target);
|
|
291
|
+
}
|
|
405
292
|
|
|
406
293
|
if (JSON.stringify(config) === before) return false;
|
|
407
294
|
return writeTomlIfChanged(filePath, config, dryRun);
|
|
@@ -446,7 +333,7 @@ function ensureCodexHook(config, phase, command) {
|
|
|
446
333
|
continue;
|
|
447
334
|
}
|
|
448
335
|
|
|
449
|
-
const nextHooks = entry.hooks.filter(hook =>
|
|
336
|
+
const nextHooks = entry.hooks.filter(hook => hook?.command !== command);
|
|
450
337
|
if (nextHooks.length === 0) continue;
|
|
451
338
|
nextEntries.push(nextHooks.length === entry.hooks.length ? entry : { ...entry, hooks: nextHooks });
|
|
452
339
|
}
|
|
@@ -456,14 +343,14 @@ function ensureCodexHook(config, phase, command) {
|
|
|
456
343
|
return JSON.stringify(nextEntries) !== before;
|
|
457
344
|
}
|
|
458
345
|
|
|
459
|
-
function mergeCodexHooks(filePath,
|
|
346
|
+
function mergeCodexHooks(filePath, dryRun = false) {
|
|
460
347
|
const config = readJson(filePath, {});
|
|
461
348
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
462
349
|
throw new Error(`Expected JSON object in ${filePath}`);
|
|
463
350
|
}
|
|
464
351
|
|
|
465
352
|
let changed = false;
|
|
466
|
-
for (const [phase, command] of Object.entries(
|
|
353
|
+
for (const [phase, command] of Object.entries(CODEX_HOOK_COMMANDS)) {
|
|
467
354
|
changed = ensureCodexHook(config, phase, command) || changed;
|
|
468
355
|
}
|
|
469
356
|
return changed && writeJsonIfChanged(filePath, config, dryRun);
|
|
@@ -505,7 +392,7 @@ function ensureCursorHook(config, event, command) {
|
|
|
505
392
|
config.hooks[event] = entries;
|
|
506
393
|
}
|
|
507
394
|
|
|
508
|
-
function mergeCursorHooks(filePath,
|
|
395
|
+
function mergeCursorHooks(filePath, dryRun = false) {
|
|
509
396
|
const config = readJson(filePath, {});
|
|
510
397
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
511
398
|
throw new Error(`Expected JSON object in ${filePath}`);
|
|
@@ -526,7 +413,7 @@ function mergeCursorHooks(filePath, commandTarget = BINARY_NAME, dryRun = false)
|
|
|
526
413
|
else delete config.hooks[phase];
|
|
527
414
|
}
|
|
528
415
|
|
|
529
|
-
for (const { event, command } of
|
|
416
|
+
for (const { event, command } of CURSOR_HOOK_COMMANDS) {
|
|
530
417
|
ensureCursorHook(config, event, command);
|
|
531
418
|
}
|
|
532
419
|
|
|
@@ -562,61 +449,45 @@ function removeCursorHooks(filePath, dryRun = false) {
|
|
|
562
449
|
return writeJsonIfChanged(filePath, config, dryRun);
|
|
563
450
|
}
|
|
564
451
|
|
|
565
|
-
function hasJsonMcpServer(filePath
|
|
452
|
+
function hasJsonMcpServer(filePath) {
|
|
566
453
|
if (!existsSync(filePath)) return false;
|
|
567
454
|
try {
|
|
568
|
-
|
|
569
|
-
return expectedCommand
|
|
570
|
-
? isExpectedMcpServer(server, expectedCommand)
|
|
571
|
-
: isContextModeMcpCommand(server?.command);
|
|
455
|
+
return readJson(filePath, {}).mcpServers?.[INTEGRATION_NAME]?.command === BINARY_NAME;
|
|
572
456
|
} catch {
|
|
573
457
|
return false;
|
|
574
458
|
}
|
|
575
459
|
}
|
|
576
460
|
|
|
577
|
-
function hasCodexMcpServer(filePath
|
|
461
|
+
function hasCodexMcpServer(filePath) {
|
|
578
462
|
if (!existsSync(filePath)) return false;
|
|
579
463
|
try {
|
|
580
464
|
const config = TOML.parse(readFileSync(filePath, 'utf8'));
|
|
581
|
-
|
|
582
|
-
return expectedCommand
|
|
583
|
-
? isExpectedMcpServer(server, expectedCommand)
|
|
584
|
-
: isContextModeMcpCommand(server?.command);
|
|
465
|
+
return config.mcp_servers?.[INTEGRATION_NAME]?.command === BINARY_NAME;
|
|
585
466
|
} catch {
|
|
586
467
|
return false;
|
|
587
468
|
}
|
|
588
469
|
}
|
|
589
470
|
|
|
590
|
-
function hasCodexHookCoverage(filePath
|
|
471
|
+
function hasCodexHookCoverage(filePath) {
|
|
591
472
|
if (!existsSync(filePath)) return false;
|
|
592
473
|
try {
|
|
593
474
|
const config = readJson(filePath, {});
|
|
594
|
-
|
|
595
|
-
return Object.keys(codexHookCommands()).every(phase =>
|
|
475
|
+
return Object.entries(CODEX_HOOK_COMMANDS).every(([phase, command]) =>
|
|
596
476
|
Array.isArray(config.hooks?.[phase])
|
|
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
|
-
)
|
|
477
|
+
&& config.hooks[phase].some(entry => isCanonicalCodexHookEntry(entry, phase, command))
|
|
604
478
|
);
|
|
605
479
|
} catch {
|
|
606
480
|
return false;
|
|
607
481
|
}
|
|
608
482
|
}
|
|
609
483
|
|
|
610
|
-
function hasCursorHookCoverage(filePath
|
|
484
|
+
function hasCursorHookCoverage(filePath) {
|
|
611
485
|
if (!existsSync(filePath)) return false;
|
|
612
486
|
try {
|
|
613
487
|
const config = readJson(filePath, {});
|
|
614
|
-
|
|
615
|
-
return cursorHookCommands().every(({ event }) =>
|
|
488
|
+
return CURSOR_HOOK_COMMANDS.every(({ event, command }) =>
|
|
616
489
|
Array.isArray(config.hooks?.[event])
|
|
617
|
-
&& config.hooks[event].some(entry => entry?.
|
|
618
|
-
? entry?.command === expectedCommands.find(command => command.event === event)?.command
|
|
619
|
-
: isContextModeCommand(entry?.command)))
|
|
490
|
+
&& config.hooks[event].some(entry => entry?.command === command && entry?.event === event)
|
|
620
491
|
);
|
|
621
492
|
} catch {
|
|
622
493
|
return false;
|
|
@@ -631,12 +502,12 @@ function hasAnyContextModeConfig(home) {
|
|
|
631
502
|
|| hasJsonMcpServer(join(home, '.claude.json'));
|
|
632
503
|
}
|
|
633
504
|
|
|
634
|
-
function hasCompleteContextModeConfig(home
|
|
635
|
-
return hasCodexMcpServer(join(home, '.codex', 'config.toml')
|
|
636
|
-
&& hasCodexHookCoverage(join(home, '.codex', 'hooks.json')
|
|
637
|
-
&& hasJsonMcpServer(join(home, '.cursor', 'mcp.json')
|
|
638
|
-
&& hasCursorHookCoverage(join(home, '.cursor', 'hooks.json')
|
|
639
|
-
&& hasJsonMcpServer(join(home, '.claude.json')
|
|
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'));
|
|
640
511
|
}
|
|
641
512
|
|
|
642
513
|
export function isContextModeRequested(args = {}, env = process.env) {
|
|
@@ -660,10 +531,6 @@ export function detectContextModeBinary(options = {}) {
|
|
|
660
531
|
};
|
|
661
532
|
}
|
|
662
533
|
|
|
663
|
-
return describeContextModeBinary(binaryPath, env, 'path');
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function describeContextModeBinary(binaryPath, env = process.env, source = 'path') {
|
|
667
534
|
const versionEnv = {
|
|
668
535
|
...process.env,
|
|
669
536
|
...env,
|
|
@@ -690,85 +557,11 @@ function describeContextModeBinary(binaryPath, env = process.env, source = 'path
|
|
|
690
557
|
return {
|
|
691
558
|
present: true,
|
|
692
559
|
path: binaryPath,
|
|
693
|
-
source,
|
|
694
|
-
commandTarget: source === 'path' ? BINARY_NAME : binaryPath,
|
|
695
560
|
version: versionOutput || 'unknown',
|
|
696
561
|
reason: null,
|
|
697
562
|
};
|
|
698
563
|
}
|
|
699
564
|
|
|
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
|
-
|
|
772
565
|
export function ensureContextModeIntegration(home, options = {}) {
|
|
773
566
|
const env = options.env || process.env;
|
|
774
567
|
const dryRun = options.dryRun === true;
|
|
@@ -776,31 +569,24 @@ export function ensureContextModeIntegration(home, options = {}) {
|
|
|
776
569
|
const changedFiles = [];
|
|
777
570
|
const configuredHarnesses = [];
|
|
778
571
|
const installState = readInstallState(home);
|
|
779
|
-
const
|
|
780
|
-
warnings.push(...binaryResult.warnings);
|
|
781
|
-
const binary = binaryResult.binary;
|
|
572
|
+
const binary = detectContextModeBinary({ env });
|
|
782
573
|
|
|
783
574
|
if (!binary.present) {
|
|
784
575
|
return {
|
|
785
576
|
changedFiles,
|
|
786
|
-
warnings:
|
|
577
|
+
warnings: [`context-mode binary is required before config mutation: ${binary.reason}`],
|
|
787
578
|
configuredHarnesses,
|
|
788
|
-
complete: false,
|
|
789
579
|
binary,
|
|
790
580
|
dryRun,
|
|
791
|
-
installedPackage: binaryResult.installed,
|
|
792
|
-
plannedInstall: binaryResult.plannedInstall,
|
|
793
|
-
install: binaryResult.install,
|
|
794
581
|
};
|
|
795
582
|
}
|
|
796
583
|
|
|
797
|
-
const commandTarget = commandTargetForBinary(binary);
|
|
798
584
|
const updates = [
|
|
799
|
-
['Codex MCP', join(home, '.codex', 'config.toml'), () => mergeCodexToml(join(home, '.codex', 'config.toml'), warnings, installState, MCP_TARGETS.codex,
|
|
800
|
-
['Codex hooks', join(home, '.codex', 'hooks.json'), () => mergeCodexHooks(join(home, '.codex', 'hooks.json'),
|
|
801
|
-
['Cursor MCP', join(home, '.cursor', 'mcp.json'), () => mergeJsonMcpServer(join(home, '.cursor', 'mcp.json'), installState, MCP_TARGETS.cursor,
|
|
802
|
-
['Cursor hooks', join(home, '.cursor', 'hooks.json'), () => mergeCursorHooks(join(home, '.cursor', 'hooks.json'),
|
|
803
|
-
['Claude MCP', join(home, '.claude.json'), () => mergeJsonMcpServer(join(home, '.claude.json'), installState, MCP_TARGETS.claude,
|
|
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)],
|
|
804
590
|
];
|
|
805
591
|
|
|
806
592
|
for (const [label, filePath, update] of updates) {
|
|
@@ -814,21 +600,12 @@ export function ensureContextModeIntegration(home, options = {}) {
|
|
|
814
600
|
changedFiles.push(statePath);
|
|
815
601
|
}
|
|
816
602
|
|
|
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
|
-
|
|
822
603
|
return {
|
|
823
604
|
changedFiles: unique(changedFiles),
|
|
824
605
|
warnings,
|
|
825
606
|
configuredHarnesses,
|
|
826
|
-
complete,
|
|
827
607
|
binary,
|
|
828
608
|
dryRun,
|
|
829
|
-
installedPackage: binaryResult.installed,
|
|
830
|
-
plannedInstall: binaryResult.plannedInstall,
|
|
831
|
-
install: binaryResult.install,
|
|
832
609
|
};
|
|
833
610
|
}
|
|
834
611
|
|
|
@@ -863,15 +640,15 @@ export function removeContextModeIntegration(home, options = {}) {
|
|
|
863
640
|
|
|
864
641
|
export function getContextModeIntegrationSummary(home, options = {}) {
|
|
865
642
|
const env = options.env || process.env;
|
|
866
|
-
const binary =
|
|
643
|
+
const binary = detectContextModeBinary({ env });
|
|
867
644
|
const hasAnyConfig = hasAnyContextModeConfig(home);
|
|
868
|
-
const configured = binary.present && hasCompleteContextModeConfig(home
|
|
645
|
+
const configured = binary.present && hasCompleteContextModeConfig(home);
|
|
869
646
|
const state = configured ? 'configured' : hasAnyConfig ? 'broken' : 'absent';
|
|
870
647
|
const summary = configured
|
|
871
648
|
? `context-mode configured (${binary.version || 'version unknown'})`
|
|
872
649
|
: hasAnyConfig
|
|
873
|
-
? `context-mode partially configured; run aw
|
|
874
|
-
: `context-mode not configured${binary.present ?
|
|
650
|
+
? `context-mode partially configured; run aw integration add ${INTEGRATION_NAME}`
|
|
651
|
+
: `context-mode not configured${binary.present ? '' : '; binary not installed'}`;
|
|
875
652
|
|
|
876
653
|
return {
|
|
877
654
|
name: INTEGRATION_NAME,
|
|
@@ -903,7 +680,7 @@ export function getContextModeDoctorStatus(home, options = {}) {
|
|
|
903
680
|
title: 'Context Mode integration',
|
|
904
681
|
status: 'warn',
|
|
905
682
|
summary: summary.summary,
|
|
906
|
-
fix: `Run \`aw
|
|
683
|
+
fix: `Run \`aw integration add ${INTEGRATION_NAME}\` to repair or \`aw integration remove ${INTEGRATION_NAME}\` to clean it up.`,
|
|
907
684
|
}];
|
|
908
685
|
}
|
|
909
686
|
|
|
@@ -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
|
+
}
|