@ghl-ai/aw 0.1.71 → 0.1.73-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.
@@ -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);
@@ -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/init.mjs CHANGED
@@ -25,6 +25,7 @@ import * as fmt from '../fmt.mjs';
25
25
  import { chalk, setSilent } from '../fmt.mjs';
26
26
  import { linkWorkspace } from '../link.mjs';
27
27
  import { generateCommands, copyInstructions, initAwDocs, syncHomeHarnessInstructions } from '../integrate.mjs';
28
+ import { renderRules } from '../render-rules.mjs';
28
29
  import { setupMcp } from '../mcp.mjs';
29
30
  import { isContextModeRequested } from '../integrations/context-mode.mjs';
30
31
  import { applyStoredStartupPreferences, ensureAwRuntimeHook, isDefaultRoutingEnabled } from '../startup.mjs';
@@ -150,6 +151,11 @@ function syncHomeAndProjectInstructions(cwd, namespace) {
150
151
  initAwDocs(HOME);
151
152
  if (cwd !== HOME) {
152
153
  syncInstructionsAndAwDocs(cwd, namespace);
154
+ } else {
155
+ // Running from $HOME (fresh-laptop flow): render global IDE rules directly.
156
+ // The project branch above is otherwise the only renderRules call site, so
157
+ // init from $HOME used to leave ~/.claude/rules and ~/.cursor/rules empty.
158
+ renderRules(HOME, { homeDir: HOME });
153
159
  }
154
160
  }
155
161
 
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
  };
@@ -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,8 +112,8 @@ 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 = [
@@ -120,16 +128,15 @@ function getAwVersion() {
120
128
  const globalPrefix = execSync('npm prefix -g', { encoding: 'utf8', timeout: 3000 }).trim();
121
129
  candidates.push(path.join(globalPrefix, 'lib', 'node_modules', '@ghl-ai', 'aw', 'package.json'));
122
130
  } catch { /* ignore */ }
123
- // aw-ecc version as last-resort fallback
124
- candidates.push(path.join(os.homedir(), '.aw-ecc', 'package.json'));
125
131
  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 */ }
132
+ const version = readPackageVersion(pkgPath);
133
+ if (version) {
134
+ _awCliVersionDetails = { version, source: 'package_json' };
135
+ return _awCliVersionDetails;
136
+ }
130
137
  }
131
- _awVersion = null;
132
- return _awVersion;
138
+ _awCliVersionDetails = { version: null, source: 'unavailable' };
139
+ return _awCliVersionDetails;
133
140
  }
134
141
 
135
142
  // ── Harness detection ────────────────────────────────────────────────
@@ -427,6 +434,7 @@ function buildEvent(hookInput, eventType, payload) {
427
434
  const cfg = loadConfig();
428
435
  const git = getGitInfo();
429
436
  const harness = detectHarness(input);
437
+ const awCli = getAwCliVersionDetails();
430
438
 
431
439
  // Normalize session_id: Claude/Codex use session_id, Cursor uses conversation_id
432
440
  const sessionId = input.session_id
@@ -458,7 +466,7 @@ function buildEvent(hookInput, eventType, payload) {
458
466
  github_user: git.user || input.user_email || null,
459
467
  github_email: git.email || input.user_email || null,
460
468
  project_hash: computeProjectHash(cwd),
461
- aw_version: getAwVersion(),
469
+ aw_version: awCli.version,
462
470
  event: eventType,
463
471
  client_ts: new Date().toISOString(),
464
472
  payload: payload || {},
@@ -497,6 +505,7 @@ module.exports = {
497
505
  readSessionLastSlashCommand,
498
506
  readLastAssistantFromTranscript,
499
507
  resolvePromptText,
508
+ getAwCliVersionDetails,
500
509
  tryAcquireDedupe,
501
510
  isCodexInternalTaskTitlePrompt,
502
511
  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,7 +1,7 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.71",
4
- "description": "Agentic Workspace CLI pull, push & manage agents, skills and commands from the registry",
3
+ "version": "0.1.73-beta.0",
4
+ "description": "Agentic Workspace CLI \u2014 pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "aw": "bin.js"
@@ -74,4 +74,4 @@
74
74
  "devDependencies": {
75
75
  "vitest": "^4.1.2"
76
76
  }
77
- }
77
+ }