@ghl-ai/aw 0.1.51 → 0.1.53

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.
@@ -12,12 +12,13 @@
12
12
  # - .codex/scripts/codex-web-bootstrap.sh (passes --harness codex-web)
13
13
  #
14
14
  # Override knobs (env):
15
- # AW_PACKAGE npm spec to install. Defaults to @ghl-ai/aw@latest. Override
16
- # with @ghl-ai/aw@beta to opt into pre-release builds, or pin to
17
- # @ghl-ai/aw@0.1.x for reproducible CI runs.
15
+ # AW_PACKAGE npm spec to install. Defaults to @ghl-ai/aw@latest.
16
+ # Override to pin a specific @ghl-ai/aw@0.1.x for reproducible
17
+ # CI runs.
18
18
  set -Eeuo pipefail
19
19
 
20
- : "${GITHUB_PAT:?ERROR: Set GITHUB_PAT in your harness secrets UI before running aw c4}"
20
+ # GitHub auth is resolved inside `aw c4` so GITHUB_PAT, GITHUB_TOKEN, and
21
+ # no-token diagnostics stay centralized in the CLI preflight.
21
22
 
22
23
  # Ensure npm is on PATH. Cursor Cloud's install shell is non-interactive — nvm
23
24
  # is not auto-sourced, and Node may not be pre-installed at all. Walk common
package/cli.mjs CHANGED
@@ -21,13 +21,14 @@ const COMMANDS = {
21
21
  doctor: () => import('./commands/doctor.mjs').then(m => m.doctorCommand),
22
22
  routing: () => import('./commands/startup.mjs').then(m => m.routingCommand),
23
23
  startup: () => import('./commands/startup.mjs').then(m => m.startupCommand),
24
+ mcp: () => import('./commands/mcp.mjs').then(m => m.mcpCommand),
24
25
  search: () => import('./commands/search.mjs').then(m => m.searchCommand),
25
26
  link: () => import('./commands/link-project.mjs').then(m => m.linkProjectCommand),
26
27
  nuke: () => import('./commands/nuke.mjs').then(m => m.nukeCommand),
27
28
  daemon: () => import('./commands/daemon.mjs').then(m => m.daemonCommand),
28
- integration: () => import('./commands/integration.mjs').then(m => m.integrationCommand),
29
- integrations: () => import('./commands/integration.mjs').then(m => m.integrationCommand),
30
29
  telemetry: () => import('./commands/telemetry.mjs').then(m => m.telemetryCommand),
30
+ integration: () => import('./commands/integration.mjs').then(m => m.integrationCommand),
31
+ integrations: () => import('./commands/integrations.mjs').then(m => m.integrationsCommand),
31
32
  'slack-sim': () => import('./commands/slack-sim.mjs').then(m => m.slackSimCommand),
32
33
  c4: () => import('./commands/c4.mjs').then(m => m.c4Command),
33
34
  'init-repo': () => import('./commands/init-repo.mjs').then(m => m.initRepoCommand),
@@ -87,8 +88,9 @@ function printHelp() {
87
88
  const sec = (title) => `\n ${chalk.bold.underline(title)}`;
88
89
  const help = [
89
90
  sec('Setup'),
90
- cmd('aw init', 'Initialize workspace (platform/ only)'),
91
+ cmd('aw init', 'Initialize workspace (auto-installs suggested integrations)'),
91
92
  cmd('aw init --namespace <team/sub-team>', 'Add a team namespace (optional)'),
93
+ cmd('aw init --no-integrations', 'Skip integration setup (Codex, Caveman, Graphify, etc)'),
92
94
  ` ${chalk.dim('Teams: platform, revex, mobile, commerce, leadgen, crm, marketplace, ai')}`,
93
95
  ` ${chalk.dim('Example: aw init --namespace revex/courses')}`,
94
96
  cmd('aw init-repo', 'Scaffold cloud-bootstrap files (idempotent, --dry-run/--force/--diff)'),
@@ -114,12 +116,18 @@ function printHelp() {
114
116
  cmd('aw doctor', 'Run a health check for routing, MCP, plugin, and AW ECC surfaces'),
115
117
  cmd('aw link', 'Link current project as a git worktree (wires IDE symlinks)'),
116
118
  cmd('aw routing status', 'Show global AW session-routing mode for Claude/Cursor/Codex'),
117
- cmd('aw routing disable', 'Disable automatic AW session routing globally'),
118
- cmd('aw routing enable', 'Re-enable automatic AW session routing globally'),
119
- cmd('aw integration list', 'List addable third-party integrations'),
120
- cmd('aw integration add <name>', 'Add a specific integration, e.g. context-mode'),
121
- cmd('aw integration status <name>', 'Show integration health and configured surfaces'),
122
- cmd('aw integration remove <name>', 'Remove a specific integration'),
119
+ cmd('aw routing disable', 'Disable default AW router/rules injection globally'),
120
+ cmd('aw routing enable', 'Re-enable default AW router/rules injection globally'),
121
+ cmd('aw mcp status', 'Show global AW MCP mode for Claude/Cursor/Codex'),
122
+ cmd('aw mcp disable', 'Remove AW-managed MCP server across Claude/Cursor/Codex'),
123
+ cmd('aw mcp enable', 'Restore AW-managed MCP server across Claude/Cursor/Codex'),
124
+ cmd('aw integrations', 'Manage third-party integrations (Codex, Caveman, etc)'),
125
+ cmd('aw integrations add <key>', 'Install a specific tool (e.g. codex, caveman)'),
126
+ cmd('aw integrations remove <key>', 'Remove a tool'),
127
+ cmd('aw integrations bundle <name>', 'Install a preset bundle'),
128
+ cmd('aw integration add <name>', 'Configure an installed local integration, e.g. context-mode'),
129
+ cmd('aw integration status <name>', 'Show local integration health'),
130
+ cmd('aw integration remove <name>', 'Remove AW-managed local integration entries'),
123
131
  cmd('aw drop <path>', 'Stop syncing or delete local content'),
124
132
  cmd('aw nuke', 'Remove entire .aw_registry/ & start fresh'),
125
133
  cmd('aw daemon install', 'Auto-pull on a schedule (macOS launchd / Linux cron)'),
package/commands/c4.mjs CHANGED
@@ -24,6 +24,7 @@ import { spawnSync as nodeSpawnSync } from 'node:child_process';
24
24
  import { existsSync as fsExistsSync } from 'node:fs';
25
25
  import { join } from 'node:path';
26
26
  import * as c4Default from '../c4/index.mjs';
27
+ import { isMcpEnabled } from '../mcp.mjs';
27
28
 
28
29
  /* ─────────────────────────────────────────────────────────────────────────
29
30
  * Constants — referenced by self-tests.
@@ -349,10 +350,10 @@ export async function c4Command(rawArgs, overrides = {}) {
349
350
  return exit(0);
350
351
  }
351
352
 
352
- // Step 6 — npm install -g @ghl-ai/aw.
353
- const npmRes = spawnSync('npm', ['install', '-g', '@ghl-ai/aw'], { stdio: 'pipe' });
353
+ // Step 6 — npm install -g @ghl-ai/aw@latest.
354
+ const npmRes = spawnSync('npm', ['install', '-g', '@ghl-ai/aw@latest'], { stdio: 'pipe' });
354
355
  if (npmRes && npmRes.status !== 0) {
355
- writer.stderr('[aw-c4] npm install -g @ghl-ai/aw failed (non-fatal); using existing aw if present\n');
356
+ writer.stderr('[aw-c4] npm install -g @ghl-ai/aw@latest failed (non-fatal); using existing aw if present\n');
356
357
  }
357
358
 
358
359
  // Step 7 — aw init --silent.
@@ -380,7 +381,12 @@ export async function c4Command(rawArgs, overrides = {}) {
380
381
  }
381
382
 
382
383
  // Step 11 — MCP register.
383
- safe('registerGhlAiMcp', () => c4.registerGhlAiMcp(harness, home, token), writer);
384
+ const mcpEnabled = isMcpEnabled(home, env);
385
+ if (mcpEnabled) {
386
+ safe('registerGhlAiMcp', () => c4.registerGhlAiMcp(harness, home, token), writer);
387
+ } else {
388
+ writer.stdout('[aw-c4] MCP disabled; skipping registerGhlAiMcp\n');
389
+ }
384
390
 
385
391
  // Step 12 — slash command surface.
386
392
  safe('ensureCommandSurface', () => c4.ensureCommandSurface({ harness, home, eccHome }), writer);
@@ -403,11 +409,13 @@ export async function c4Command(rawArgs, overrides = {}) {
403
409
  safe('ensureRepoLocalIgnore', () => c4.ensureRepoLocalIgnore({ cwd, harness }), writer);
404
410
 
405
411
  // Step 15 — MCP smoke probe (best-effort).
406
- const mcpProbe = await safeAsync(
407
- 'probeMcpServer',
408
- () => c4.probeMcpServer({ url: c4.MCP_URL_DEFAULT, token }),
409
- writer,
410
- );
412
+ const mcpProbe = mcpEnabled
413
+ ? await safeAsync(
414
+ 'probeMcpServer',
415
+ () => c4.probeMcpServer({ url: c4.MCP_URL_DEFAULT, token }),
416
+ writer,
417
+ )
418
+ : null;
411
419
 
412
420
  // Step 16 — self-tests.
413
421
  const self = runSelfTests({ harness, c4, home, awHome, eccHome });
@@ -210,7 +210,7 @@ function directoryContainsGeneratedRuleFiles(dirPath, extension) {
210
210
  try {
211
211
  return readdirSync(dirPath, { withFileTypes: true })
212
212
  .filter(entry => entry.isFile() && entry.name.endsWith(extension))
213
- .some(entry => readText(join(dirPath, entry.name)).startsWith(GENERATED_RULE_HEADER));
213
+ .some(entry => readText(join(dirPath, entry.name)).includes(GENERATED_RULE_HEADER));
214
214
  } catch {
215
215
  return false;
216
216
  }
@@ -501,7 +501,7 @@ function listGeneratedRuleFiles(dirPath, extension) {
501
501
  return readdirSync(dirPath, { withFileTypes: true })
502
502
  .filter(entry => entry.isFile() && entry.name.endsWith(extension))
503
503
  .map(entry => join(dirPath, entry.name))
504
- .filter(filePath => readText(filePath).startsWith(GENERATED_RULE_HEADER));
504
+ .filter(filePath => readText(filePath).includes(GENERATED_RULE_HEADER));
505
505
  } catch {
506
506
  return [];
507
507
  }
package/commands/init.mjs CHANGED
@@ -35,6 +35,7 @@ import { loadConfig as ensureTelemetryConfig } from '../telemetry.mjs';
35
35
  import { installAwEcc, AW_ECC_TAG } from '../ecc.mjs';
36
36
  import { removeWorkspaceHookDefaults } from '../codex.mjs';
37
37
  import { readHookManifest, pruneStaleHooks, writeHookManifest } from '../hook-cleanup.mjs';
38
+ import { promptAndInstall, autoInstallIntegrations } from '../integrations.mjs';
38
39
  import {
39
40
  initPersistentClone,
40
41
  isValidClone,
@@ -48,7 +49,7 @@ import {
48
49
  syncWorktreeSparseCheckout,
49
50
  findNearestWorktree,
50
51
  } from '../git.mjs';
51
- import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL, RULES_SOURCE_DIR, RULES_RUNTIME_DIR } from '../constants.mjs';
52
+ import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR, RULES_RUNTIME_DIR } from '../constants.mjs';
52
53
  import { syncFileTree } from '../file-tree.mjs';
53
54
 
54
55
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -136,8 +137,8 @@ function scaffoldNamespace(awHome, folderName) {
136
137
 
137
138
  // ── Ensure ~/.aw/.git/info/exclude has the whitelist block ─────────────
138
139
  //
139
- // Strategy: only .aw_registry/, .aw_rules/, content/ are tracked — everything
140
- // else at the top level of ~/.aw/ is local-only (telemetry/, hooks/, logs,
140
+ // Strategy: only .aw_registry/, .aw_rules/, content/, and .aw_docs/ are
141
+ // tracked — everything else at the top level of ~/.aw/ is local-only (telemetry/, hooks/, logs,
141
142
  // .DS_Store, etc.). We write to .git/info/exclude (not tracked .gitignore)
142
143
  // so upstream pulls never conflict.
143
144
 
@@ -151,6 +152,7 @@ const AW_MANAGED_BLOCK = [
151
152
  '!/.aw_registry',
152
153
  '!/.aw_rules',
153
154
  '!/content',
155
+ '!/.aw_docs',
154
156
  '',
155
157
  '# Nested local state within whitelisted dirs',
156
158
  '/.aw_registry/.sync-config.json',
@@ -239,6 +241,7 @@ export async function initCommand(args) {
239
241
  let namespace = args['--namespace'] || null;
240
242
  let user = args['--user'] || '';
241
243
  const silent = args['--silent'] === true;
244
+ const skipIntegrations = args['--no-integrations'] === true;
242
245
 
243
246
  // In silent mode, suppress ALL fmt output and show a single spinner.
244
247
  // setSilent(true) makes every fmt.* call a no-op — internal functions
@@ -350,7 +353,7 @@ export async function initCommand(args) {
350
353
  const isNewSubTeam = folderName && cfg && !cfg.include.includes(folderName);
351
354
  if (isNewSubTeam) {
352
355
  if (!silent) fmt.logStep(`Adding sub-team ${chalk.cyan(folderName)}...`);
353
- const newSparsePaths = [`.aw_registry/${folderName}`, 'content', RULES_SOURCE_DIR];
356
+ const newSparsePaths = [`.aw_registry/${folderName}`, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR];
354
357
  addToSparseCheckout(AW_HOME, newSparsePaths);
355
358
  config.addPattern(GLOBAL_AW_DIR, folderName);
356
359
  scaffoldNamespace(AW_HOME, folderName);
@@ -442,6 +445,12 @@ export async function initCommand(args) {
442
445
  // Write hook manifest after all hook installation is complete
443
446
  try { writeHookManifest({ eccVersion: AW_ECC_TAG, awVersion: VERSION }); } catch { /* best effort */ }
444
447
 
448
+ // Auto-install suggested integrations (Codex, Caveman, Graphify, etc) - unless --no-integrations
449
+ let installedIntegrations = [];
450
+ if (!silent && !skipIntegrations && !isNewSubTeam) {
451
+ installedIntegrations = await autoInstallIntegrations(freshCfg?.namespace || team, { silent });
452
+ }
453
+
445
454
  if (silent) {
446
455
  if (silentSpinner) { silentSpinner.stop('Done'); setSilent(false); }
447
456
  autoUpdate(await args._updateCheck);
@@ -455,6 +464,7 @@ export async function initCommand(args) {
455
464
  ? ` ${chalk.green('✓')} Removed ${removedLegacyStartupFiles.length} legacy repo startup file${removedLegacyStartupFiles.length > 1 ? 's' : ''}`
456
465
  : null,
457
466
  cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Project linked` : null,
467
+ installedIntegrations.length > 0 ? ` ${chalk.green('✓')} Integrations: ${installedIntegrations.join(', ')}` : null,
458
468
  ].filter(Boolean).join('\n'));
459
469
  }
460
470
  return;
@@ -483,7 +493,7 @@ export async function initCommand(args) {
483
493
  }
484
494
 
485
495
  // Determine sparse paths
486
- const sparsePaths = [`.aw_registry/platform`, `content`, RULES_SOURCE_DIR, `.aw_registry/AW-PROTOCOL.md`, `CODEOWNERS`];
496
+ const sparsePaths = [`.aw_registry/platform`, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR, `.aw_registry/AW-PROTOCOL.md`, `CODEOWNERS`];
487
497
  if (folderName) {
488
498
  sparsePaths.push(`.aw_registry/${folderName}`);
489
499
  }
@@ -603,6 +613,12 @@ export async function initCommand(args) {
603
613
  // Ensure telemetry config exists (generates machine_id on first run)
604
614
  ensureTelemetryConfig();
605
615
 
616
+ // Auto-install suggested integrations (Codex, Caveman, Graphify, etc) - unless --no-integrations
617
+ let installedIntegrations = [];
618
+ if (!silent && !skipIntegrations) {
619
+ installedIntegrations = await autoInstallIntegrations(team, { silent });
620
+ }
621
+
606
622
  // Offer to update if a newer version is available
607
623
  if (!silent) await promptUpdate(await args._updateCheck);
608
624
 
@@ -622,10 +638,12 @@ export async function initCommand(args) {
622
638
  hooksInstalled ? ` ${chalk.green('✓')} Git hooks: auto-sync on pull/clone (core.hooksPath)` : null,
623
639
  ` ${chalk.green('✓')} IDE task: auto-sync on workspace open`,
624
640
  cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Linked in current project` : null,
641
+ installedIntegrations.length > 0 ? ` ${chalk.green('✓')} Integrations: ${installedIntegrations.join(', ')}` : null,
625
642
  '',
626
643
  ` ${chalk.dim('Existing repos:')} ${chalk.bold('cd <project> && aw link')}`,
627
644
  ` ${chalk.dim('New clones:')} auto-linked via git hook`,
628
645
  ` ${chalk.dim('Update:')} ${chalk.bold('aw init')} ${chalk.dim('(or auto on pull/IDE open)')}`,
646
+ ` ${chalk.dim('Integrations:')} ${chalk.bold('aw integrations')} ${chalk.dim('(manage Codex, Caveman, etc)')}`,
629
647
  ` ${chalk.dim('Uninstall:')} ${chalk.bold('aw nuke')}`,
630
648
  ].filter(Boolean).join('\n'));
631
649
  }
@@ -0,0 +1,268 @@
1
+ // commands/integrations.mjs — CLI command: aw integrations add/remove/list/bundle
2
+
3
+ import * as p from '@clack/prompts';
4
+ import { homedir } from 'node:os';
5
+ import * as fmt from '../fmt.mjs';
6
+ import { chalk } from '../fmt.mjs';
7
+ import { listIntegrations as listLocalIntegrations } from '../integrations/index.mjs';
8
+ import {
9
+ INTEGRATIONS,
10
+ BUNDLES,
11
+ installIntegration,
12
+ removeIntegration,
13
+ getInstalledList,
14
+ } from '../integrations.mjs';
15
+
16
+ export async function integrationsCommand(args) {
17
+ const subcommand = args._positional[0];
18
+
19
+ switch (subcommand) {
20
+ case 'add':
21
+ return cmdAdd(args);
22
+ case 'remove':
23
+ return cmdRemove(args);
24
+ case 'bundle':
25
+ return cmdBundle(args);
26
+ case 'list':
27
+ case undefined:
28
+ return cmdList();
29
+ default:
30
+ fmt.cancel(`Unknown subcommand: ${subcommand}`);
31
+ }
32
+ }
33
+
34
+ // ────────────────────────────────────────────────────────────────────────────────
35
+ // aw integrations list
36
+ // ────────────────────────────────────────────────────────────────────────────────
37
+
38
+ async function cmdList() {
39
+ fmt.banner('Integrations', {
40
+ icon: '🔗',
41
+ subtitle: ' Available tools and MCP servers',
42
+ });
43
+
44
+ const installed = getInstalledList();
45
+ const localIntegrations = listLocalIntegrations(homedir(), { env: process.env });
46
+
47
+ // Group by type
48
+ const plugins = Object.entries(INTEGRATIONS).filter(
49
+ ([, i]) => i.type === 'plugin'
50
+ );
51
+ const remoteRcps = Object.entries(INTEGRATIONS).filter(
52
+ ([, i]) => i.type === 'remote-mcp'
53
+ );
54
+ const universalInstallers = Object.entries(INTEGRATIONS).filter(
55
+ ([, i]) => i.type === 'universal-installer'
56
+ );
57
+ const pythonClis = Object.entries(INTEGRATIONS).filter(
58
+ ([, i]) => i.type === 'python-cli'
59
+ );
60
+
61
+ const typeIcon = (type) =>
62
+ type === 'plugin' ? '🔌' : type === 'remote-mcp' ? '🌐' : type === 'universal-installer' ? '🪨' : '⚙️';
63
+
64
+ // Installed section
65
+ if (installed.length > 0) {
66
+ fmt.logMessage(`\n${chalk.bold.underline('Installed')}`);
67
+ for (const key of installed) {
68
+ const integration = INTEGRATIONS[key];
69
+ if (!integration) continue;
70
+ fmt.logSuccess(` ${typeIcon(integration.type)} ${integration.label}`);
71
+ }
72
+ }
73
+
74
+ // Local Integrations
75
+ if (localIntegrations.length > 0) {
76
+ fmt.logMessage(`\n${chalk.bold.underline('Local Integrations')}`);
77
+ for (const integration of localIntegrations) {
78
+ fmt.logMessage(
79
+ ` ⚙️ ${integration.name.padEnd(25)} — ${integration.summary}`
80
+ );
81
+ }
82
+ }
83
+
84
+ // Available Plugins
85
+ fmt.logMessage(`\n${chalk.bold.underline('Available Plugins')}`);
86
+ for (const [key, integration] of plugins) {
87
+ if (!installed.includes(key)) {
88
+ fmt.logMessage(
89
+ ` 🔌 ${integration.label.padEnd(25)} — ${integration.description}`
90
+ );
91
+ }
92
+ }
93
+
94
+ // Available Remote MCPs
95
+ fmt.logMessage(`\n${chalk.bold.underline('Available Remote MCPs')}`);
96
+ for (const [key, integration] of remoteRcps) {
97
+ if (!installed.includes(key)) {
98
+ fmt.logMessage(
99
+ ` 🌐 ${integration.label.padEnd(25)} — ${integration.description}`
100
+ );
101
+ }
102
+ }
103
+
104
+ // Universal Installers
105
+ if (universalInstallers.length > 0) {
106
+ fmt.logMessage(`\n${chalk.bold.underline('Universal Tools')}`);
107
+ for (const [key, integration] of universalInstallers) {
108
+ if (!installed.includes(key)) {
109
+ fmt.logMessage(
110
+ ` 🪨 ${integration.label.padEnd(25)} — ${integration.description}`
111
+ );
112
+ }
113
+ }
114
+ }
115
+
116
+ // Python CLIs
117
+ if (pythonClis.length > 0) {
118
+ fmt.logMessage(`\n${chalk.bold.underline('Available Python Tools')}`);
119
+ for (const [key, integration] of pythonClis) {
120
+ if (!installed.includes(key)) {
121
+ fmt.logMessage(
122
+ ` 🐍 ${integration.label.padEnd(25)} — ${integration.description}`
123
+ );
124
+ }
125
+ }
126
+ }
127
+
128
+ // Bundles
129
+ fmt.logMessage(`\n${chalk.bold.underline('Bundles')}`);
130
+ for (const [bundleKey, bundle] of Object.entries(BUNDLES)) {
131
+ fmt.logMessage(
132
+ ` 📦 ${bundle.label.padEnd(25)} — ${bundle.description}`
133
+ );
134
+ fmt.logMessage(
135
+ ` Includes: ${bundle.includes.map((k) => INTEGRATIONS[k].label).join(', ')}`
136
+ );
137
+ }
138
+
139
+ fmt.logMessage(`\n${chalk.dim('Commands:')}`);
140
+ fmt.logMessage(` aw integrations add <key> Install a specific tool`);
141
+ fmt.logMessage(` aw integrations remove <key> Remove a tool`);
142
+ fmt.logMessage(` aw integrations bundle <name> Install a preset bundle`);
143
+ fmt.logMessage(` aw integration add <name> Configure a local integration`);
144
+ }
145
+
146
+ // ────────────────────────────────────────────────────────────────────────────────
147
+ // aw integrations add <key>
148
+ // ────────────────────────────────────────────────────────────────────────────────
149
+
150
+ async function cmdAdd(args) {
151
+ const key = args._positional[1];
152
+
153
+ if (!key) {
154
+ fmt.cancel('Usage: aw integrations add <key>');
155
+ }
156
+
157
+ if (!INTEGRATIONS[key]) {
158
+ // Suggest similar keys
159
+ const available = Object.keys(INTEGRATIONS);
160
+ fmt.cancel(
161
+ [
162
+ `Unknown integration: ${chalk.red(key)}`,
163
+ '',
164
+ `Available: ${available.join(', ')}`,
165
+ ].join('\n')
166
+ );
167
+ }
168
+
169
+ const integration = INTEGRATIONS[key];
170
+ fmt.intro(`Installing ${integration.label}`);
171
+
172
+ const success = await installIntegration(key, { silent: false });
173
+
174
+ if (success) {
175
+ fmt.outro(`✓ ${integration.label} installed successfully`);
176
+ } else {
177
+ fmt.cancel(`Failed to install ${integration.label}`);
178
+ }
179
+ }
180
+
181
+ // ────────────────────────────────────────────────────────────────────────────────
182
+ // aw integrations remove <key>
183
+ // ────────────────────────────────────────────────────────────────────────────────
184
+
185
+ async function cmdRemove(args) {
186
+ const key = args._positional[1];
187
+
188
+ if (!key) {
189
+ fmt.cancel('Usage: aw integrations remove <key>');
190
+ }
191
+
192
+ if (!INTEGRATIONS[key]) {
193
+ const available = Object.keys(INTEGRATIONS);
194
+ fmt.cancel(
195
+ [
196
+ `Unknown integration: ${chalk.red(key)}`,
197
+ '',
198
+ `Available: ${available.join(', ')}`,
199
+ ].join('\n')
200
+ );
201
+ }
202
+
203
+ const integration = INTEGRATIONS[key];
204
+ fmt.intro(`Removing ${integration.label}`);
205
+
206
+ const success = await removeIntegration(key, { silent: false });
207
+
208
+ if (success) {
209
+ fmt.outro(`✓ ${integration.label} removed successfully`);
210
+ } else {
211
+ fmt.cancel(`Failed to remove ${integration.label}`);
212
+ }
213
+ }
214
+
215
+ // ────────────────────────────────────────────────────────────────────────────────
216
+ // aw integrations bundle <bundleName>
217
+ // ────────────────────────────────────────────────────────────────────────────────
218
+
219
+ async function cmdBundle(args) {
220
+ const bundleName = args._positional[1];
221
+
222
+ if (!bundleName) {
223
+ fmt.cancel('Usage: aw integrations bundle <name>');
224
+ }
225
+
226
+ if (!BUNDLES[bundleName]) {
227
+ const available = Object.keys(BUNDLES);
228
+ fmt.cancel(
229
+ [
230
+ `Unknown bundle: ${chalk.red(bundleName)}`,
231
+ '',
232
+ `Available: ${available.join(', ')}`,
233
+ ].join('\n')
234
+ );
235
+ }
236
+
237
+ const bundle = BUNDLES[bundleName];
238
+ fmt.intro(`Installing bundle: ${bundle.label}`);
239
+
240
+ fmt.logMessage(`${bundle.description}\n`);
241
+ fmt.logMessage(`Includes:`);
242
+ for (const key of bundle.includes) {
243
+ const integration = INTEGRATIONS[key];
244
+ fmt.logMessage(` • ${integration.label}`);
245
+ }
246
+
247
+ const confirm = await p.default.confirm({
248
+ message: `Continue installing ${bundle.includes.length} tool(s)?`,
249
+ initialValue: true,
250
+ });
251
+
252
+ if (p.default.isCancel(confirm) || !confirm) {
253
+ fmt.cancel('Cancelled');
254
+ }
255
+
256
+ fmt.logMessage('');
257
+
258
+ // Install all
259
+ let successCount = 0;
260
+ for (const key of bundle.includes) {
261
+ const success = await installIntegration(key, { silent: false });
262
+ if (success) successCount++;
263
+ }
264
+
265
+ fmt.outro(
266
+ `✓ Bundle installation complete (${successCount}/${bundle.includes.length} installed)`
267
+ );
268
+ }
@@ -0,0 +1,90 @@
1
+ import { homedir } from 'node:os';
2
+
3
+ import * as fmt from '../fmt.mjs';
4
+ import { chalk } from '../fmt.mjs';
5
+ import {
6
+ getMcpStatus,
7
+ removeMcpConfig,
8
+ saveMcpPreferences,
9
+ setupMcp,
10
+ } from '../mcp.mjs';
11
+
12
+ const HOME = homedir();
13
+
14
+ function isTruthyEnv(value) {
15
+ return ['1', 'true', 'yes', 'on'].includes(String(value || '').trim().toLowerCase());
16
+ }
17
+
18
+ export async function mcpCommand(args) {
19
+ const action = String(args._positional?.[0] || 'status').toLowerCase();
20
+
21
+ if (!['status', 'enable', 'disable'].includes(action)) {
22
+ fmt.cancel(`Unknown MCP action: ${action}. Use: aw mcp status|enable|disable`);
23
+ return;
24
+ }
25
+
26
+ fmt.intro(`aw mcp ${action}`);
27
+
28
+ if (action === 'status') {
29
+ return renderStatus();
30
+ }
31
+
32
+ if (action === 'disable') {
33
+ saveMcpPreferences('disabled', HOME);
34
+ const removed = removeMcpConfig();
35
+ const status = getMcpStatus(HOME);
36
+ fmt.outro([
37
+ '⟁ MCP disabled',
38
+ '',
39
+ ` ${chalk.green('✓')} Preference saved: ${chalk.dim(status.preferencesPath.replace(`${HOME}/`, '~/'))}`,
40
+ ` ${chalk.green('✓')} Removed AW-managed MCP server from ${removed} config file${removed === 1 ? '' : 's'}`,
41
+ ].join('\n'));
42
+ return;
43
+ }
44
+
45
+ saveMcpPreferences('enabled', HOME);
46
+ const silent = !!args['--silent'] || isTruthyEnv(process.env.CI);
47
+ const updatedFiles = await setupMcp(HOME, null, { silent });
48
+ const status = getMcpStatus(HOME);
49
+ if (status.effectiveMode === 'disabled') {
50
+ fmt.outro([
51
+ '⟁ MCP preference enabled',
52
+ '',
53
+ ` ${chalk.green('✓')} Preference saved: ${chalk.dim(status.preferencesPath.replace(`${HOME}/`, '~/'))}`,
54
+ ` ${chalk.yellow('!')} ${status.envDisableMcpName}=1 override is still active`,
55
+ ].join('\n'));
56
+ return;
57
+ }
58
+ fmt.outro([
59
+ '⟁ MCP enabled',
60
+ '',
61
+ ` ${chalk.green('✓')} Preference saved: ${chalk.dim(status.preferencesPath.replace(`${HOME}/`, '~/'))}`,
62
+ updatedFiles.length > 0
63
+ ? ` ${chalk.green('✓')} Updated ${updatedFiles.length} MCP config file${updatedFiles.length === 1 ? '' : 's'}`
64
+ : ` ${chalk.dim('Note:')} MCP config files already up to date`,
65
+ ].join('\n'));
66
+ }
67
+
68
+ function renderStatus() {
69
+ const status = getMcpStatus(HOME);
70
+ const modeLabel = status.envDisableMcp
71
+ ? `${status.effectiveMode} (${status.envDisableMcpName}=1 override; saved preference: ${status.mode})`
72
+ : status.mode;
73
+
74
+ fmt.note([
75
+ `${chalk.dim('mode:')} ${modeLabel}`,
76
+ `${chalk.dim('prefs:')} ${status.preferencesPath.replace(`${HOME}/`, '~/')}`,
77
+ `${chalk.dim('claude MCP:')} ${formatHealth(status.claude)}`,
78
+ `${chalk.dim('cursor MCP:')} ${formatHealth(status.cursor)}`,
79
+ `${chalk.dim('codex MCP:')} ${formatHealth(status.codex)}`,
80
+ `${chalk.dim('aw-ecc Codex MCP source:')} ${formatHealth(status.eccCodex)}`,
81
+ ].join('\n'), 'MCP');
82
+
83
+ fmt.outro('⟁ MCP status complete');
84
+ }
85
+
86
+ function formatHealth(health) {
87
+ if (!health.present) return 'disabled';
88
+ if (health.url && health.authorization) return 'enabled';
89
+ return `incomplete (url=${health.url}, authorization=${health.authorization})`;
90
+ }
package/commands/nuke.mjs CHANGED
@@ -366,7 +366,7 @@ export async function nukeCommand(args) {
366
366
  '',
367
367
  ` ${chalk.green('✓')} Generated files cleaned`,
368
368
  ` ${chalk.green('✓')} IDE symlinks cleaned`,
369
- ` ${chalk.green('✓')} MCP config removed (ghl-ai and context-mode)`,
369
+ ` ${chalk.green('✓')} MCP config removed (ghl-ai and AW-managed context-mode entries)`,
370
370
  ` ${chalk.green('✓')} aw-ecc engine removed`,
371
371
  ` ${chalk.green('✓')} Project worktrees removed`,
372
372
  ` ${chalk.green('✓')} Project symlinks cleaned`,
package/commands/pull.mjs CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  REGISTRY_DIR,
28
28
  REGISTRY_URL,
29
29
  DOCS_SOURCE_DIR,
30
+ AW_DOCS_DIR,
30
31
  RULES_SOURCE_DIR,
31
32
  RULES_RUNTIME_DIR,
32
33
  } from '../constants.mjs';
@@ -92,7 +93,7 @@ export async function pullCommand(args) {
92
93
  // Ensure platform pulls also fetch docs and rules on older installs that
93
94
  // pre-date the new sparse-checkout paths.
94
95
  if (input === 'platform') {
95
- addToSparseCheckout(AW_HOME, [`.aw_registry/platform`, DOCS_SOURCE_DIR, RULES_SOURCE_DIR]);
96
+ addToSparseCheckout(AW_HOME, [`.aw_registry/platform`, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR]);
96
97
  if (!cfg.include.includes('platform')) {
97
98
  config.addPattern(GLOBAL_AW_DIR, 'platform');
98
99
  }
@@ -110,7 +111,7 @@ export async function pullCommand(args) {
110
111
  const label = input.split('/').pop();
111
112
  if (!cfg.include.includes(input)) {
112
113
  log.logStep(`Adding ${chalk.cyan(label)} to sparse checkout...`);
113
- addToSparseCheckout(AW_HOME, [sparsePath, DOCS_SOURCE_DIR]);
114
+ addToSparseCheckout(AW_HOME, [sparsePath, DOCS_SOURCE_DIR, AW_DOCS_DIR]);
114
115
  config.addPattern(GLOBAL_AW_DIR, input);
115
116
  addedInput = input;
116
117
  addedSparsePath = sparsePath;