@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.
@@ -1,67 +1,64 @@
1
- import { accessSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, 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
- import { delimiter, dirname, isAbsolute, join } from 'node:path';
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
- 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
- }
31
-
32
- function hookCommand(commandTarget, harness, phase) {
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
- function codexHookMatcherForPhase(phase) {
48
- if (phase === 'SessionStart') return 'startup|resume';
49
- if (phase === 'PreToolUse' || phase === 'PostToolUse') return '*';
50
- return undefined;
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
- 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
- }
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
- 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);
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 = codexHookMatcherForPhase(phase);
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 = codexHookMatcherForPhase(phase);
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, commandTarget = BINARY_NAME, dryRun = false) {
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 && JSON.stringify(existing) === JSON.stringify(desired)) {
352
- markMcpTargetOwned(installState, target);
353
- return false;
354
- }
239
+ if (existing) return false;
355
240
 
356
- mcpServers[INTEGRATION_NAME] = desired;
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, commandTarget = BINARY_NAME, dryRun = false) {
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
- config.mcp_servers[INTEGRATION_NAME] = mcpServerConfig(commandTarget);
404
- markMcpTargetOwned(installState, target);
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 => !isContextModeCommand(hook?.command));
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, commandTarget = BINARY_NAME, dryRun = false) {
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(codexHookCommands(commandTarget))) {
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, commandTarget = BINARY_NAME, dryRun = false) {
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 cursorHookCommands(commandTarget)) {
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, expectedCommand = null) {
452
+ function hasJsonMcpServer(filePath) {
566
453
  if (!existsSync(filePath)) return false;
567
454
  try {
568
- const server = readJson(filePath, {}).mcpServers?.[INTEGRATION_NAME];
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, expectedCommand = null) {
461
+ function hasCodexMcpServer(filePath) {
578
462
  if (!existsSync(filePath)) return false;
579
463
  try {
580
464
  const config = TOML.parse(readFileSync(filePath, 'utf8'));
581
- const server = config.mcp_servers?.[INTEGRATION_NAME];
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, commandTarget = null) {
471
+ function hasCodexHookCoverage(filePath) {
591
472
  if (!existsSync(filePath)) return false;
592
473
  try {
593
474
  const config = readJson(filePath, {});
594
- const expectedCommands = commandTarget ? codexHookCommands(commandTarget) : null;
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, commandTarget = null) {
484
+ function hasCursorHookCoverage(filePath) {
611
485
  if (!existsSync(filePath)) return false;
612
486
  try {
613
487
  const config = readJson(filePath, {});
614
- const expectedCommands = commandTarget ? cursorHookCommands(commandTarget) : null;
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?.event === event && (expectedCommands
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, 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);
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 binaryResult = ensureContextModeBinary({ ...options, env, home, dryRun });
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: warnings.length > 0 ? warnings : [`context-mode binary is required before config mutation: ${binary.reason}`],
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, 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)],
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 = resolveContextModeExecutable({ ...options, env, home });
643
+ const binary = detectContextModeBinary({ env });
867
644
  const hasAnyConfig = hasAnyContextModeConfig(home);
868
- const configured = binary.present && hasCompleteContextModeConfig(home, commandTargetForBinary(binary));
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 integrations add ${INTEGRATION_NAME}`
874
- : `context-mode not configured${binary.present ? `; binary discoverable via ${binary.source}` : '; binary not installed'}`;
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 integrations add ${INTEGRATION_NAME}\` to repair or \`aw integrations remove ${INTEGRATION_NAME}\` to clean it up.`,
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
+ }