@ghl-ai/aw 0.1.71 → 0.1.72

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.
@@ -27,6 +27,7 @@ import TOML from '@iarna/toml';
27
27
 
28
28
  export const MCP_URL_DEFAULT =
29
29
  'https://services.leadconnectorhq.com/agentic-workspace/mcp';
30
+ export const CODEX_MCP_BEARER_TOKEN_ENV = 'GHL_AI_MCP_BEARER_TOKEN';
30
31
 
31
32
  const FILE_MODE = 0o600;
32
33
 
@@ -76,6 +77,12 @@ function configPathFor(home) {
76
77
  return join(home, '.codex/config.toml');
77
78
  }
78
79
 
80
+ function removeAuthorizationHeader(table) {
81
+ if (!table || typeof table !== 'object' || Array.isArray(table)) return false;
82
+ delete table.Authorization;
83
+ return Object.keys(table).length === 0;
84
+ }
85
+
79
86
  /**
80
87
  * Ensure `[features] codex_hooks = true` in ~/.codex/config.toml.
81
88
  * Preserves all other keys (round-trip via @iarna/toml).
@@ -99,11 +106,11 @@ export function ensureCodexHooksFlag(home) {
99
106
  }
100
107
 
101
108
  /**
102
- * Ensure `[mcp_servers.ghl-ai]` block with url + Bearer token + transport.
109
+ * Ensure `[mcp_servers.ghl-ai]` block with url + bearer token env var + transport.
103
110
  * URL falls back to env MCP_URL → MCP_URL_DEFAULT.
104
111
  *
105
112
  * @param {string} home
106
- * @param {string} token
113
+ * @param {string} token Validated by the caller, never persisted into Codex config.
107
114
  * @returns {{ changed: boolean }}
108
115
  */
109
116
  export function ensureCodexMcpServer(home, token) {
@@ -132,14 +139,15 @@ export function ensureCodexMcpServer(home, token) {
132
139
  // Match the canonical `aw init` reference (libs/aw/mcp.mjs::tomlMcpServerBlock)
133
140
  // which always sets startup_timeout_sec = 30 for HTTP MCP servers.
134
141
  ghlAi.startup_timeout_sec = 30;
135
- if (
136
- ghlAi.headers == null ||
137
- typeof ghlAi.headers !== 'object' ||
138
- Array.isArray(ghlAi.headers)
139
- ) {
140
- ghlAi.headers = {};
142
+ ghlAi.bearer_token_env_var = CODEX_MCP_BEARER_TOKEN_ENV;
143
+ if (removeAuthorizationHeader(ghlAi.http_headers)) delete ghlAi.http_headers;
144
+ if (removeAuthorizationHeader(ghlAi.env_http_headers)) delete ghlAi.env_http_headers;
145
+ if (ghlAi.headers && typeof ghlAi.headers === 'object' && !Array.isArray(ghlAi.headers)) {
146
+ delete ghlAi.headers.Authorization;
147
+ if (Object.keys(ghlAi.headers).length === 0) {
148
+ delete ghlAi.headers;
149
+ }
141
150
  }
142
- ghlAi.headers.Authorization = `Bearer ${token}`;
143
151
  root.mcp_servers['ghl-ai'] = ghlAi;
144
152
 
145
153
  const serialized = TOML.stringify(root);
@@ -129,30 +129,29 @@ aw_c4_exit=$?
129
129
 
130
130
  # Defense-in-depth: `aw c4` delegates registry sync to `aw init --silent`,
131
131
  # which can fail to fetch in silent mode without surfacing an error. When
132
- # that happens, the registry (~/.aw/.aw_registry) may be empty or incomplete
133
- # and only the 10 hardcoded stage commands get linked. Re-run `aw pull`
134
- # post-c4 if the command count looks low.
135
- verify_registry_commands() {
136
- local aw_cmd_dir="$HOME/.aw/.aw_registry"
137
- if [ ! -d "$aw_cmd_dir" ]; then
132
+ # that happens, the registry (~/.aw/.aw_registry) may be empty or incomplete.
133
+ # Re-run `aw pull` post-c4 if the skill surface looks too small.
134
+ verify_registry_skills() {
135
+ local aw_registry_dir="$HOME/.aw/.aw_registry"
136
+ if [ ! -d "$aw_registry_dir" ]; then
138
137
  echo "[aw-c4-bootstrap] registry dir missing — running aw init + pull"
139
138
  aw init --no-integrations --silent 2>&1 | tail -3 || true
140
139
  aw pull 2>&1 | tail -5 || true
141
140
  return
142
141
  fi
143
142
 
144
- local cmd_count
145
- cmd_count=$(find "$aw_cmd_dir" -name '*.md' -path '*/commands/*' ! -path '*/evals/*' 2>/dev/null | wc -l | tr -d ' ')
146
- if [ "${cmd_count:-0}" -lt 20 ]; then
147
- echo "[aw-c4-bootstrap] only ${cmd_count} registry commands found — running aw pull"
143
+ local skill_count
144
+ skill_count=$(find "$aw_registry_dir" -name 'SKILL.md' -path '*/skills/*' ! -path '*/evals/*' 2>/dev/null | wc -l | tr -d ' ')
145
+ if [ "${skill_count:-0}" -lt 5 ]; then
146
+ echo "[aw-c4-bootstrap] only ${skill_count} registry skills found — running aw pull"
148
147
  aw pull 2>&1 | tail -5 || true
149
148
  local new_count
150
- new_count=$(find "$aw_cmd_dir" -name '*.md' -path '*/commands/*' ! -path '*/evals/*' 2>/dev/null | wc -l | tr -d ' ')
151
- echo "[aw-c4-bootstrap] registry commands: ${cmd_count} → ${new_count}"
149
+ new_count=$(find "$aw_registry_dir" -name 'SKILL.md' -path '*/skills/*' ! -path '*/evals/*' 2>/dev/null | wc -l | tr -d ' ')
150
+ echo "[aw-c4-bootstrap] registry skills: ${skill_count} → ${new_count}"
152
151
  else
153
- echo "[aw-c4-bootstrap] registry: ${cmd_count} commands OK"
152
+ echo "[aw-c4-bootstrap] registry: ${skill_count} skills OK"
154
153
  fi
155
154
  }
156
- verify_registry_commands || true
155
+ verify_registry_skills || true
157
156
 
158
157
  exit "$aw_c4_exit"
package/commands/c4.mjs CHANGED
@@ -110,6 +110,11 @@ function safeListNamespaceDirs(dir) {
110
110
  } catch { return []; }
111
111
  }
112
112
 
113
+ function hasRegistrySkillSurface(registryDir, existsSync = fsExistsSync) {
114
+ if (existsSync(join(registryDir, 'platform', 'core', 'skills'))) return true;
115
+ return safeListNamespaceDirs(registryDir).length > 0;
116
+ }
117
+
113
118
  async function safeAsync(label, fn, writer) {
114
119
  try {
115
120
  return { ok: true, value: await fn() };
@@ -405,18 +410,17 @@ export async function c4Command(rawArgs, overrides = {}) {
405
410
  return exit(1);
406
411
  }
407
412
 
408
- // Step 9a — verify registry has namespace directories. `aw init --silent`
413
+ // Step 9a — verify registry has the skill surface. `aw init --silent`
409
414
  // can exit 0 but fail to fetch (fetchAndMerge swallows the error in silent
410
415
  // mode). Metadata-only artifacts (AW-PROTOCOL.md) pass a simple non-empty
411
- // check, so we specifically look for directories (namespaces like
412
- // "platform") to confirm the registry content was actually pulled.
416
+ // check, so we specifically look for the platform skill namespace to confirm
417
+ // the registry content was actually pulled.
413
418
  {
414
- const registryNamespaces = safeListNamespaceDirs(awRegistry);
415
- if (registryNamespaces.length === 0) {
416
- writer.stderr('[aw-c4] registry has no namespace directories after init — retrying with aw pull\n');
419
+ if (!hasRegistrySkillSurface(awRegistry, fs.existsSync)) {
420
+ writer.stderr('[aw-c4] registry has no platform skill surface after init — retrying with aw pull\n');
417
421
  const pullRes = spawnSync('aw', ['pull'], { stdio: 'pipe' });
418
- if (pullRes?.status !== 0 || safeListNamespaceDirs(awRegistry).length === 0) {
419
- writer.stderr('[aw-c4] FATAL: registry commands were not fetched\n');
422
+ if (pullRes?.status !== 0 || !hasRegistrySkillSurface(awRegistry, fs.existsSync)) {
423
+ writer.stderr('[aw-c4] FATAL: registry skills were not fetched\n');
420
424
  return exit(1);
421
425
  }
422
426
  }
@@ -439,7 +443,7 @@ export async function c4Command(rawArgs, overrides = {}) {
439
443
  writer.stdout('[aw-c4] MCP disabled; skipping registerGhlAiMcp\n');
440
444
  }
441
445
 
442
- // Step 12 — slash command surface (10 stage commands from ECC).
446
+ // Step 12 — legacy slash command surface, when provided by the installed ECC.
443
447
  safe('ensureCommandSurface', () => c4.ensureCommandSurface({ harness, home, eccHome }), writer);
444
448
 
445
449
  // Step 12a — full registry command surface.
@@ -284,7 +284,7 @@ function tomlMcpHealth(filePath) {
284
284
  return {
285
285
  present: /\[mcp_servers\.ghl-ai\]/.test(content),
286
286
  url: /\[mcp_servers\.ghl-ai\][\s\S]*?url\s*=\s*"https?:\/\/[^"]+"/.test(content),
287
- authorization: /\[mcp_servers\.ghl-ai\.headers\][\s\S]*?Authorization\s*=\s*".+"/.test(content),
287
+ authorization: /\[mcp_servers\.ghl-ai\][\s\S]*?bearer_token_env_var\s*=\s*".+"/.test(content),
288
288
  };
289
289
  }
290
290
 
@@ -891,7 +891,7 @@ function buildDoctorChecks(homeDir, cwd) {
891
891
  const codexMcp = tomlMcpHealth(codexConfigPath);
892
892
  checks.push(
893
893
  codexMcp.present && codexMcp.url && codexMcp.authorization
894
- ? makeCheck('codex-mcp', 'Codex MCP config', 'pass', 'Codex has a ghl-ai MCP server with URL and Authorization header')
894
+ ? makeCheck('codex-mcp', 'Codex MCP config', 'pass', 'Codex has a ghl-ai MCP server with URL and bearer token env var')
895
895
  : makeCheck(
896
896
  'codex-mcp',
897
897
  'Codex MCP config',
package/commands/push.mjs CHANGED
@@ -153,13 +153,45 @@ function normalizeRelPath(value) {
153
153
  .replace(/\/+$/, '');
154
154
  }
155
155
 
156
+ // Root namespaces publish at the AW docs root (aw_docs/<ns>/...), mirroring the
157
+ // local .aw_docs/<ns>/... subtree 1:1 — NO per-workspace <repo>/<user> segment
158
+ // and full nested folders. Use for shared, repo-keyed doc trees such as PR
159
+ // reviews (pr-reviews/<owner>/<repo>/pr-<n>/<run>) so they do not land under the
160
+ // reviewer's workspace namespace as one mangled flat slug.
161
+ const AW_DOCS_ROOT_NAMESPACES = ['pr-reviews'];
162
+
163
+ function awDocsRootScope(relPath) {
164
+ const value = normalizeRelPath(relPath);
165
+ const segments = value.split('/');
166
+ // A root scope's delete-then-copy target is the resolved subtree. Require the
167
+ // full pr-reviews/<repo>/pr-<number>/<run> depth so a shallow path (e.g.
168
+ // `pr-reviews` or `pr-reviews/<repo>`) can never resolve to a broad shared
169
+ // subtree and wipe unrelated review runs on the remote.
170
+ if (segments[0] !== 'pr-reviews' || segments.length < 4) {
171
+ throw new Error(
172
+ `Root-scope docs publish must target pr-reviews/<repo>/pr-<number>/<run>, got "${relPath}".`
173
+ );
174
+ }
175
+ for (const seg of segments) {
176
+ if (!seg || seg === '.' || seg === '..' || !/^[A-Za-z0-9._-]+$/.test(seg)) {
177
+ throw new Error(`Invalid AW docs path segment "${seg}" in "${relPath}". Segments may contain letters, numbers, dot, underscore, and dash only.`);
178
+ }
179
+ }
180
+ return { type: 'root', relPrefix: value };
181
+ }
182
+
156
183
  function featureScopeFromInput(input) {
157
184
  const value = normalizeRelPath(input);
158
185
  if (!value) return null;
159
186
 
187
+ const stripped = value.replace(/^\.aw_docs\//, '');
188
+ if (AW_DOCS_ROOT_NAMESPACES.includes(stripped.split('/')[0])) {
189
+ return awDocsRootScope(stripped);
190
+ }
191
+
160
192
  const match = value.match(/^(?:\.aw_docs\/)?features\/([^/]+)$/);
161
193
  if (!match) {
162
- throw new Error('Docs-only publish path must be .aw_docs/features/<feature-slug> or use --feature <feature-slug>.');
194
+ throw new Error('Docs-only publish path must be .aw_docs/features/<feature-slug>, .aw_docs/pr-reviews/<...>, or use --feature <feature-slug>.');
163
195
  }
164
196
  return awDocsFeatureScope(match[1]);
165
197
  }
@@ -701,9 +733,14 @@ async function publishProjectAwDocs(cwd, home, dryRun, scope = null) {
701
733
  const sourceRepo = await getProjectSourceRepo(projectRoot);
702
734
  const repoSlug = repoSlugFromSource(sourceRepo, projectRoot);
703
735
  const githubUsername = safePathSegment(await getGitHubUser(), 'unknown');
736
+ // Root-scoped docs mirror to the AW docs root (aw_docs/<relPath>); all other
737
+ // docs are namespaced under <repo>/<user> as before.
738
+ const isRootScope = scope?.type === 'root';
704
739
  const docs = files.map(file => ({
705
740
  ...file,
706
- publishedPath: `${publishConfig.dest}/${repoSlug}/${githubUsername}/${file.relPath}`,
741
+ publishedPath: isRootScope
742
+ ? `${publishConfig.dest}/${file.relPath}`
743
+ : `${publishConfig.dest}/${repoSlug}/${githubUsername}/${file.relPath}`,
707
744
  }));
708
745
  const publishedPaths = docs.map(doc => doc.publishedPath);
709
746
  const links = docs.map(doc => ({
@@ -728,9 +765,11 @@ async function publishProjectAwDocs(cwd, home, dryRun, scope = null) {
728
765
  s.start(`Publishing ${files.length} AW doc${files.length > 1 ? 's' : ''} to ${publishConfig.repo}...`);
729
766
  try {
730
767
  const docsRepoDir = await ensureAwDocsRepoClone(home, publishConfig);
731
- const deleteTarget = scope?.relPrefix
732
- ? join(docsRepoDir, publishConfig.dest, repoSlug, githubUsername, scope.relPrefix)
733
- : join(docsRepoDir, publishConfig.dest, repoSlug, githubUsername);
768
+ const deleteTarget = isRootScope
769
+ ? join(docsRepoDir, publishConfig.dest, scope.relPrefix)
770
+ : scope?.relPrefix
771
+ ? join(docsRepoDir, publishConfig.dest, repoSlug, githubUsername, scope.relPrefix)
772
+ : join(docsRepoDir, publishConfig.dest, repoSlug, githubUsername);
734
773
  rmSync(deleteTarget, {
735
774
  recursive: true,
736
775
  force: true,
@@ -1627,6 +1666,7 @@ function groupBy(arr, key) {
1627
1666
  export const __test__ = {
1628
1667
  featureScopeFromInput,
1629
1668
  awDocsFeatureScope,
1669
+ awDocsRootScope,
1630
1670
  resolveAwDocsScope,
1631
1671
  assertDocsOnlyScopeOrAll,
1632
1672
  };
package/ecc.mjs CHANGED
@@ -12,7 +12,7 @@ import { applyStoredStartupPreferences } from "./startup.mjs";
12
12
 
13
13
  const AW_ECC_REPO_SSH = "git@github.com:shreyansh-ghl/aw-ecc.git";
14
14
  const AW_ECC_REPO_HTTPS = "https://github.com/shreyansh-ghl/aw-ecc.git";
15
- export const AW_ECC_TAG = "v1.4.66";
15
+ export const AW_ECC_TAG = "v1.4.67-beta.0";
16
16
  const REQUIRED_ECC_FILES = [
17
17
  "package.json",
18
18
  "scripts/install-apply.js",
package/git.mjs CHANGED
@@ -390,8 +390,7 @@ export async function fetchAndMerge(awHome, { silent = true } = {}) {
390
390
  // drops bare-name patterns (e.g. "content", "CODEOWNERS") when HEAD advances.
391
391
  //
392
392
  // --autostash: AW writes into the registry working tree from external
393
- // sources (ensureAwRuntimeHook copies ~/.aw-ecc/.../session-start.sh into a
394
- // tracked path; transformCursorAwRefs rewrites /aw: → /aw- through Cursor
393
+ // sources (transformCursorAwRefs rewrites /aw: → /aw- through Cursor
395
394
  // skill directory symlinks that resolve into .aw_registry/). When those
396
395
  // versions drift, the working tree is dirty at rebase time and rebase
397
396
  // refuses to run, silently aborting the entire pull. Autostash stashes the
@@ -80,9 +80,9 @@ function isDisabled() {
80
80
  return cfg.enabled === false;
81
81
  }
82
82
 
83
- // ── AW version ───────────────────────────────────────────────────────
83
+ // ── AW CLI version detection ─────────────────────────────────────────
84
84
 
85
- let _awVersion = null;
85
+ let _awCliVersionDetails = null;
86
86
 
87
87
  function parseVersionString(raw) {
88
88
  if (!raw) return null;
@@ -90,12 +90,20 @@ function parseVersionString(raw) {
90
90
  return match ? match[1] : null;
91
91
  }
92
92
 
93
- function getAwVersion() {
94
- if (_awVersion) return _awVersion;
93
+ function readPackageVersion(pkgPath) {
94
+ try {
95
+ return parseVersionString(JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version) || null;
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+
101
+ function getAwCliVersionDetails() {
102
+ if (_awCliVersionDetails) return _awCliVersionDetails;
95
103
  const envVersion = parseVersionString(process.env.AW_VERSION);
96
104
  if (envVersion) {
97
- _awVersion = envVersion;
98
- return _awVersion;
105
+ _awCliVersionDetails = { version: envVersion, source: 'env' };
106
+ return _awCliVersionDetails;
99
107
  }
100
108
  try {
101
109
  const cliVersion = parseVersionString(execSync('aw --version', {
@@ -104,32 +112,26 @@ function getAwVersion() {
104
112
  stdio: ['ignore', 'pipe', 'ignore'],
105
113
  }));
106
114
  if (cliVersion) {
107
- _awVersion = cliVersion;
108
- return _awVersion;
115
+ _awCliVersionDetails = { version: cliVersion, source: 'aw_binary' };
116
+ return _awCliVersionDetails;
109
117
  }
110
118
  } catch { /* ignore */ }
111
119
  const candidates = [
112
120
  path.join(AW_HOME, 'node_modules', '@ghl-ai', 'aw', 'package.json'),
113
121
  ];
114
- // Derive global npm prefix from the running node binary (no shell needed)
115
- try {
116
- const nodeDir = path.dirname(process.execPath);
117
- candidates.push(path.join(nodeDir, '..', 'lib', 'node_modules', '@ghl-ai', 'aw', 'package.json'));
118
- } catch { /* ignore */ }
119
122
  try {
120
123
  const globalPrefix = execSync('npm prefix -g', { encoding: 'utf8', timeout: 3000 }).trim();
121
124
  candidates.push(path.join(globalPrefix, 'lib', 'node_modules', '@ghl-ai', 'aw', 'package.json'));
122
125
  } catch { /* ignore */ }
123
- // aw-ecc version as last-resort fallback
124
- candidates.push(path.join(os.homedir(), '.aw-ecc', 'package.json'));
125
126
  for (const pkgPath of candidates) {
126
- try {
127
- _awVersion = parseVersionString(JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version) || null;
128
- if (_awVersion) return _awVersion;
129
- } catch { /* ignore */ }
127
+ const version = readPackageVersion(pkgPath);
128
+ if (version) {
129
+ _awCliVersionDetails = { version, source: 'package_json' };
130
+ return _awCliVersionDetails;
131
+ }
130
132
  }
131
- _awVersion = null;
132
- return _awVersion;
133
+ _awCliVersionDetails = { version: null, source: 'unavailable' };
134
+ return _awCliVersionDetails;
133
135
  }
134
136
 
135
137
  // ── Harness detection ────────────────────────────────────────────────
@@ -427,6 +429,7 @@ function buildEvent(hookInput, eventType, payload) {
427
429
  const cfg = loadConfig();
428
430
  const git = getGitInfo();
429
431
  const harness = detectHarness(input);
432
+ const awCli = getAwCliVersionDetails();
430
433
 
431
434
  // Normalize session_id: Claude/Codex use session_id, Cursor uses conversation_id
432
435
  const sessionId = input.session_id
@@ -458,7 +461,7 @@ function buildEvent(hookInput, eventType, payload) {
458
461
  github_user: git.user || input.user_email || null,
459
462
  github_email: git.email || input.user_email || null,
460
463
  project_hash: computeProjectHash(cwd),
461
- aw_version: getAwVersion(),
464
+ aw_version: awCli.version,
462
465
  event: eventType,
463
466
  client_ts: new Date().toISOString(),
464
467
  payload: payload || {},
@@ -497,6 +500,7 @@ module.exports = {
497
500
  readSessionLastSlashCommand,
498
501
  readLastAssistantFromTranscript,
499
502
  resolvePromptText,
503
+ getAwCliVersionDetails,
500
504
  tryAcquireDedupe,
501
505
  isCodexInternalTaskTitlePrompt,
502
506
  isCodexInternalTaskTitleCompletion,
package/mcp.mjs CHANGED
@@ -17,6 +17,7 @@ const ENABLED_MODE = 'enabled';
17
17
  const DISABLED_MODE = 'disabled';
18
18
  const DISABLE_MCP_ENV = 'AW_DISABLE_MCP';
19
19
  const MCP_SERVER_NAME = 'ghl-ai';
20
+ const CODEX_MCP_BEARER_TOKEN_ENV = 'GHL_AI_MCP_BEARER_TOKEN';
20
21
 
21
22
  function mcpPrefsPath(homeDir = HOME) {
22
23
  return join(homeDir, '.aw', MCP_PREFS_FILENAME);
@@ -77,6 +78,18 @@ function detectPaths() {
77
78
  return { ghlMcpUrl };
78
79
  }
79
80
 
81
+ function resolveCodexBearerTokenEnvVar() {
82
+ if (process.env[CODEX_MCP_BEARER_TOKEN_ENV]) return CODEX_MCP_BEARER_TOKEN_ENV;
83
+ if (process.env.GITHUB_TOKEN) return 'GITHUB_TOKEN';
84
+ return CODEX_MCP_BEARER_TOKEN_ENV;
85
+ }
86
+
87
+ function warnIfCodexBearerEnvMissing(envVar, silent = false) {
88
+ if (silent || process.env[envVar]) return;
89
+ fmt.logWarn(`Codex MCP auth expects ${envVar} in the Codex environment.`);
90
+ fmt.logWarn(` Fix: export ${envVar}=<token> and restart Codex.`);
91
+ }
92
+
80
93
  /**
81
94
  * Resolve GitHub token for MCP auth. Zero manual steps.
82
95
  *
@@ -186,7 +199,7 @@ function findExistingClickUpToken() {
186
199
  } catch { /* corrupt file — skip */ }
187
200
  }
188
201
 
189
- // TOML config: Codex extract from [mcp_servers.ghl-ai.headers] section
202
+ // Legacy Codex TOML may still carry a ClickUp token in the old headers section.
190
203
  const codexToml = join(HOME, '.codex', 'config.toml');
191
204
  if (existsSync(codexToml)) {
192
205
  try {
@@ -356,6 +369,11 @@ export async function setupMcp(cwd, namespace, { silent = false } = {}) {
356
369
  url: mcpUrl,
357
370
  headers,
358
371
  };
372
+ const ghlAiServerCodex = {
373
+ url: mcpUrl,
374
+ bearer_token_env_var: resolveCodexBearerTokenEnvVar(),
375
+ };
376
+ warnIfCodexBearerEnvMissing(ghlAiServerCodex.bearer_token_env_var, silent);
359
377
 
360
378
  // ── Claude Code: ~/.claude.json (global) ──
361
379
  const claudeJsonPath = join(HOME, '.claude.json');
@@ -375,11 +393,11 @@ export async function setupMcp(cwd, namespace, { silent = false } = {}) {
375
393
  // survives — without this, each re-init overwrites ~/.codex/config.toml
376
394
  // from the ECC source which doesn't have the ghl-ai block.
377
395
  const codexTomlPath = join(HOME, '.codex', 'config.toml');
378
- if (mergeTomlMcpServer(codexTomlPath, MCP_SERVER_NAME, ghlAiServerLocal)) {
396
+ if (mergeTomlMcpServer(codexTomlPath, MCP_SERVER_NAME, ghlAiServerCodex)) {
379
397
  updatedFiles.push(codexTomlPath);
380
398
  }
381
399
  const eccCodexTomlPath = join(HOME, '.aw-ecc', '.codex', 'config.toml');
382
- mergeTomlMcpServer(eccCodexTomlPath, MCP_SERVER_NAME, ghlAiServerLocal);
400
+ mergeTomlMcpServer(eccCodexTomlPath, MCP_SERVER_NAME, ghlAiServerCodex);
383
401
 
384
402
  // Deduplicate
385
403
  const unique = [...new Set(updatedFiles)];
@@ -454,7 +472,7 @@ function tomlMcpHealth(filePath) {
454
472
  path: filePath,
455
473
  present: new RegExp(`\\[mcp_servers\\.${MCP_SERVER_NAME}\\]`).test(content),
456
474
  url: new RegExp(`\\[mcp_servers\\.${MCP_SERVER_NAME}\\][\\s\\S]*?url\\s*=\\s*"https?:\\/\\/[^"]+"`).test(content),
457
- authorization: new RegExp(`\\[mcp_servers\\.${MCP_SERVER_NAME}\\.headers\\][\\s\\S]*?Authorization\\s*=\\s*".+"`).test(content),
475
+ authorization: new RegExp(`\\[mcp_servers\\.${MCP_SERVER_NAME}\\][\\s\\S]*?bearer_token_env_var\\s*=\\s*".+"`).test(content),
458
476
  };
459
477
  } catch {
460
478
  // Unreadable TOML should behave like an absent optional MCP config.
@@ -510,6 +528,9 @@ function tomlMcpServerBlock(serverName, serverConfig) {
510
528
  if (serverConfig.args) {
511
529
  block += `args = [${serverConfig.args.map((a) => JSON.stringify(a)).join(', ')}]\n`;
512
530
  }
531
+ if (serverConfig.bearer_token_env_var) {
532
+ block += `bearer_token_env_var = ${JSON.stringify(serverConfig.bearer_token_env_var)}\n`;
533
+ }
513
534
  block += `startup_timeout_sec = 30\n`;
514
535
  if (serverConfig.headers && Object.keys(serverConfig.headers).length > 0) {
515
536
  block += `\n[mcp_servers.${serverName}.headers]\n`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.71",
3
+ "version": "0.1.72",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
package/startup.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { randomBytes } from 'node:crypto';
@@ -83,10 +83,6 @@ function resolveRegistryRoot(homeDir = homedir()) {
83
83
  ].find(existsSync) || null;
84
84
  }
85
85
 
86
- function awRuntimeHookSourcePath(homeDir = homedir()) {
87
- return join(homeDir, '.aw-ecc', 'skills', 'using-aw-skills', 'hooks', 'session-start.sh');
88
- }
89
-
90
86
  function readJson(filePath, fallback = {}) {
91
87
  if (!existsSync(filePath)) return fallback;
92
88
  try {
@@ -393,27 +389,10 @@ function hasCodexSessionStartScript(homeDir = homedir()) {
393
389
  }
394
390
 
395
391
  export function ensureAwRuntimeHook(homeDir = homedir()) {
396
- const sourcePath = awRuntimeHookSourcePath(homeDir);
397
- const registryRoot = resolveRegistryRoot(homeDir);
398
- if (!existsSync(sourcePath) || !registryRoot) return [];
399
-
400
- const destinationPath = join(
401
- registryRoot,
402
- 'platform',
403
- 'core',
404
- 'skills',
405
- 'using-aw-skills',
406
- 'hooks',
407
- 'session-start.sh',
408
- );
409
- const sourceContent = readFileSync(sourcePath, 'utf8');
410
- const existingContent = existsSync(destinationPath) ? readFileSync(destinationPath, 'utf8') : null;
411
- if (existingContent === sourceContent) return [];
412
-
413
- mkdirSync(dirname(destinationPath), { recursive: true });
414
- writeFileSync(destinationPath, sourceContent);
415
- try { chmodSync(destinationPath, 0o755); } catch { /* best effort */ }
416
- return [destinationPath];
392
+ resolveRegistryRoot(homeDir);
393
+ // The AW router hook is registry-owned. aw-ecc no longer ships SDLC skills
394
+ // or using-aw-skills, so init/pull must not recreate it from ~/.aw-ecc.
395
+ return [];
417
396
  }
418
397
 
419
398
  function hasCodexHooksEnabled(homeDir = homedir()) {