@ghl-ai/aw 0.1.71-beta.0 → 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.
- package/c4/codexConfig.mjs +17 -9
- package/c4/templates/scripts/aw-c4-bootstrap.sh +13 -14
- package/cli.mjs +6 -2
- package/commands/c4.mjs +13 -9
- package/commands/doctor.mjs +4 -3
- package/commands/push.mjs +67 -5
- package/ecc.mjs +1 -1
- package/git.mjs +1 -2
- package/hooks/aw-usage/lib/aw-usage-telemetry.js +26 -22
- package/mcp.mjs +25 -4
- package/package.json +1 -1
- package/startup.mjs +5 -26
package/c4/codexConfig.mjs
CHANGED
|
@@ -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 +
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
#
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
145
|
-
|
|
146
|
-
if [ "${
|
|
147
|
-
echo "[aw-c4-bootstrap] only ${
|
|
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 "$
|
|
151
|
-
echo "[aw-c4-bootstrap] registry
|
|
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: ${
|
|
152
|
+
echo "[aw-c4-bootstrap] registry: ${skill_count} skills OK"
|
|
154
153
|
fi
|
|
155
154
|
}
|
|
156
|
-
|
|
155
|
+
verify_registry_skills || true
|
|
157
156
|
|
|
158
157
|
exit "$aw_c4_exit"
|
package/cli.mjs
CHANGED
|
@@ -153,8 +153,8 @@ function printHelp() {
|
|
|
153
153
|
|
|
154
154
|
sec('Upload'),
|
|
155
155
|
cmd('aw push', 'Push all modified files (creates one PR)'),
|
|
156
|
-
cmd('aw push --aw-docs-only', 'Publish generated .aw_docs companions and print share links'),
|
|
157
156
|
cmd('aw push --aw-docs-only --feature <slug>', 'Publish one .aw_docs feature folder and print share links'),
|
|
157
|
+
cmd('aw push --aw-docs-only --all', 'Publish ALL .aw_docs feature folders (explicit opt-in; otherwise scope with --feature)'),
|
|
158
158
|
cmd('aw push <path>', 'Push file, folder, or namespace to registry'),
|
|
159
159
|
cmd('aw push-rules [path]', 'Push platform rules to platform-docs'),
|
|
160
160
|
cmd('aw push --dry-run [path]', 'Preview what would be pushed'),
|
|
@@ -240,7 +240,11 @@ export async function run(argv) {
|
|
|
240
240
|
process.exit(0);
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
|
|
243
|
+
// Help must short-circuit BEFORE dispatching any command. Otherwise a
|
|
244
|
+
// command combined with --help (e.g. `aw push --help`) skips this gate and
|
|
245
|
+
// executes the command — which is how `aw push --help` triggered a real
|
|
246
|
+
// publish instead of printing help.
|
|
247
|
+
if (args['--help']) {
|
|
244
248
|
printHelp();
|
|
245
249
|
process.exit(0);
|
|
246
250
|
}
|
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
|
|
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
|
|
412
|
-
//
|
|
416
|
+
// check, so we specifically look for the platform skill namespace to confirm
|
|
417
|
+
// the registry content was actually pulled.
|
|
413
418
|
{
|
|
414
|
-
|
|
415
|
-
|
|
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 ||
|
|
419
|
-
writer.stderr('[aw-c4] FATAL: registry
|
|
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
|
|
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.
|
package/commands/doctor.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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',
|
|
@@ -1369,7 +1369,8 @@ export async function doctorCommand(args = {}) {
|
|
|
1369
1369
|
fmt.note(formatFixPlan(plan), 'Auto-Fix Plan');
|
|
1370
1370
|
|
|
1371
1371
|
if (dryRun) {
|
|
1372
|
-
|
|
1372
|
+
setDoctorExitCode(report);
|
|
1373
|
+
fmt.outro(`⟁ aw doctor auto-fix dry run complete (${report.status.toUpperCase()})`);
|
|
1373
1374
|
return;
|
|
1374
1375
|
}
|
|
1375
1376
|
|
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
|
|
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
|
}
|
|
@@ -188,6 +220,20 @@ function resolveAwDocsScope(input, featureFlag) {
|
|
|
188
220
|
return flagScope || inputScope;
|
|
189
221
|
}
|
|
190
222
|
|
|
223
|
+
// A docs-only publish with no resolved scope would sweep EVERY
|
|
224
|
+
// .aw_docs/features/** folder into the shared docs repo. Require an explicit
|
|
225
|
+
// --all opt-in so an unscoped invocation (or one that lands here accidentally)
|
|
226
|
+
// fails closed instead of mass-publishing unrelated feature folders.
|
|
227
|
+
function assertDocsOnlyScopeOrAll(scope, all) {
|
|
228
|
+
if (!scope && all !== true) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
'Refusing to publish ALL .aw_docs/features/** at once. Pass --feature <slug> ' +
|
|
231
|
+
'(or a .aw_docs/features/<slug> path) to publish one feature folder, or pass ' +
|
|
232
|
+
'--all to publish everything intentionally.'
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
191
237
|
function collectProjectAwDocs(cwd, home, scope = null) {
|
|
192
238
|
const projectRoot = getProjectRoot(cwd, home);
|
|
193
239
|
const source = join(projectRoot, AW_DOCS_DIR);
|
|
@@ -687,9 +733,14 @@ async function publishProjectAwDocs(cwd, home, dryRun, scope = null) {
|
|
|
687
733
|
const sourceRepo = await getProjectSourceRepo(projectRoot);
|
|
688
734
|
const repoSlug = repoSlugFromSource(sourceRepo, projectRoot);
|
|
689
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';
|
|
690
739
|
const docs = files.map(file => ({
|
|
691
740
|
...file,
|
|
692
|
-
publishedPath:
|
|
741
|
+
publishedPath: isRootScope
|
|
742
|
+
? `${publishConfig.dest}/${file.relPath}`
|
|
743
|
+
: `${publishConfig.dest}/${repoSlug}/${githubUsername}/${file.relPath}`,
|
|
693
744
|
}));
|
|
694
745
|
const publishedPaths = docs.map(doc => doc.publishedPath);
|
|
695
746
|
const links = docs.map(doc => ({
|
|
@@ -714,9 +765,11 @@ async function publishProjectAwDocs(cwd, home, dryRun, scope = null) {
|
|
|
714
765
|
s.start(`Publishing ${files.length} AW doc${files.length > 1 ? 's' : ''} to ${publishConfig.repo}...`);
|
|
715
766
|
try {
|
|
716
767
|
const docsRepoDir = await ensureAwDocsRepoClone(home, publishConfig);
|
|
717
|
-
const deleteTarget =
|
|
718
|
-
? join(docsRepoDir, publishConfig.dest,
|
|
719
|
-
:
|
|
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);
|
|
720
773
|
rmSync(deleteTarget, {
|
|
721
774
|
recursive: true,
|
|
722
775
|
force: true,
|
|
@@ -1284,6 +1337,7 @@ export async function pushCommand(args) {
|
|
|
1284
1337
|
if (docsOnly) {
|
|
1285
1338
|
try {
|
|
1286
1339
|
const scope = resolveAwDocsScope(input, args['--feature']);
|
|
1340
|
+
assertDocsOnlyScopeOrAll(scope, args['--all'] === true);
|
|
1287
1341
|
const result = await publishProjectAwDocs(cwd, HOME, dryRun, scope);
|
|
1288
1342
|
if (!result.hasDocs) {
|
|
1289
1343
|
fmt.cancel(scope
|
|
@@ -1608,3 +1662,11 @@ function groupBy(arr, key) {
|
|
|
1608
1662
|
}
|
|
1609
1663
|
return result;
|
|
1610
1664
|
}
|
|
1665
|
+
|
|
1666
|
+
export const __test__ = {
|
|
1667
|
+
featureScopeFromInput,
|
|
1668
|
+
awDocsFeatureScope,
|
|
1669
|
+
awDocsRootScope,
|
|
1670
|
+
resolveAwDocsScope,
|
|
1671
|
+
assertDocsOnlyScopeOrAll,
|
|
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.
|
|
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 (
|
|
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
|
|
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
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
return
|
|
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
|
-
|
|
108
|
-
return
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
const version = readPackageVersion(pkgPath);
|
|
128
|
+
if (version) {
|
|
129
|
+
_awCliVersionDetails = { version, source: 'package_json' };
|
|
130
|
+
return _awCliVersionDetails;
|
|
131
|
+
}
|
|
130
132
|
}
|
|
131
|
-
|
|
132
|
-
return
|
|
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:
|
|
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
|
|
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,
|
|
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,
|
|
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}
|
|
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
package/startup.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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()) {
|