@ghl-ai/aw 0.1.44-beta.5 → 0.1.44-beta.8
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/cursorRulesShim.mjs +222 -0
- package/c4/diagnostics.mjs +47 -0
- package/c4/gitAuth.mjs +17 -1
- package/c4/index.mjs +2 -0
- package/c4/proxyConfig.mjs +127 -0
- package/commands/c4.mjs +71 -12
- package/package.json +1 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* c4/cursorRulesShim.mjs — Cursor Cloud slash-command expansion shim.
|
|
3
|
+
*
|
|
4
|
+
* Why: Cursor Cloud Agent's chat UI does not pre-expand `/aw:<NAME>` slash
|
|
5
|
+
* commands the way Cursor Desktop's plugin does. The model sees the raw
|
|
6
|
+
* literal `/aw:...` and falls through to natural-language interpretation,
|
|
7
|
+
* losing the contract from the matching command file even though the file
|
|
8
|
+
* is correctly installed on disk by `commandSurface.mjs`.
|
|
9
|
+
*
|
|
10
|
+
* Workaround: write `<repoRoot>/.cursor/rules/aw-slash-expand.mdc` — a
|
|
11
|
+
* Cursor project rule that teaches the MODEL itself to read the matching
|
|
12
|
+
* command file and execute its contract verbatim. Cursor's `.mdc` rule
|
|
13
|
+
* format is honored in Agent mode (verified via Cursor docs); the rule
|
|
14
|
+
* loads on every session when `alwaysApply: true`.
|
|
15
|
+
*
|
|
16
|
+
* Per-harness behavior:
|
|
17
|
+
* - cursor-cloud → write the rule (idempotent on byte-equal content)
|
|
18
|
+
* - claude-web → skip (UI expands slash commands natively)
|
|
19
|
+
* - codex-web → skip (different routing surface — see
|
|
20
|
+
* aw-c4-codex-slash-shim future work in overview.md)
|
|
21
|
+
* - any other → skip (defensive default)
|
|
22
|
+
*
|
|
23
|
+
* Refs: .aw_docs/features/aw-c4-cursor-slash-shim/{overview,spec,tasks}.md
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
existsSync,
|
|
28
|
+
lstatSync,
|
|
29
|
+
mkdirSync,
|
|
30
|
+
readFileSync,
|
|
31
|
+
statSync,
|
|
32
|
+
unlinkSync,
|
|
33
|
+
writeFileSync,
|
|
34
|
+
} from 'node:fs';
|
|
35
|
+
import { join } from 'node:path';
|
|
36
|
+
|
|
37
|
+
const RULE_FILENAME = 'aw-slash-expand.mdc';
|
|
38
|
+
const CURSOR_RULES_SUBPATH = '.cursor/rules';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Canonical project-rule body. Exported so tests can assert the on-disk
|
|
42
|
+
* file matches byte-for-byte without re-reading from a fixture. Must NOT
|
|
43
|
+
* be mutated at runtime.
|
|
44
|
+
*
|
|
45
|
+
* The body lives here (not in a separate fixture file) so that bumping
|
|
46
|
+
* the rule wording is a single-file change that ships with `@ghl-ai/aw`
|
|
47
|
+
* via npm — no additional asset bundling needed.
|
|
48
|
+
*/
|
|
49
|
+
export const CURSOR_SLASH_SHIM_RULE = `---
|
|
50
|
+
description: Expand \`/aw:<NAME>\` user messages by reading the matching command file
|
|
51
|
+
alwaysApply: true
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
# AW slash-command expansion (Cursor Cloud workaround)
|
|
55
|
+
|
|
56
|
+
When the user message starts with \`/aw:<NAME>\` (case-sensitive; \`<NAME>\` may
|
|
57
|
+
contain hyphens — both in the namespace path and in the command name), do
|
|
58
|
+
the following BEFORE producing any other response.
|
|
59
|
+
|
|
60
|
+
## 1. Stage-command lookup (primary)
|
|
61
|
+
|
|
62
|
+
Read \`~/.cursor/commands/aw/<NAME>.md\`. If the file exists, jump to step 3
|
|
63
|
+
with its content as the resolved instructions.
|
|
64
|
+
|
|
65
|
+
## 2. Registry-command lookup (fallback, hyphen-disambiguation)
|
|
66
|
+
|
|
67
|
+
If step 1 did not resolve and \`<NAME>\` contains at least one hyphen, the
|
|
68
|
+
slash form is a flattened \`<ns_segments...>-<cmd_segments...>\` token. The
|
|
69
|
+
boundary between namespace and command is ambiguous because both can
|
|
70
|
+
contain hyphens (registry has e.g. \`pr-review\`, \`capture-learning\`,
|
|
71
|
+
\`ship-auto\`, \`draw-architecture\`).
|
|
72
|
+
|
|
73
|
+
Disambiguate by **iterating every hyphen position in \`<NAME>\` from
|
|
74
|
+
RIGHTMOST to LEFTMOST**, and use the first candidate file that exists. The
|
|
75
|
+
repo registry root is \`.aw/.aw_registry/\` (relative to the agent's CWD —
|
|
76
|
+
the repo root in Cursor Cloud).
|
|
77
|
+
|
|
78
|
+
For each split position \`i\` from rightmost to leftmost:
|
|
79
|
+
- \`prefix = <NAME>[:i]\` (everything before the hyphen at position \`i\`)
|
|
80
|
+
- \`suffix = <NAME>[i+1:]\` (everything after; may itself contain hyphens)
|
|
81
|
+
- candidate = \`.aw/.aw_registry/<prefix-with-hyphens-replaced-by-slashes>/commands/<suffix>.md\`
|
|
82
|
+
- Try \`Read\` on the candidate. If the file exists, jump to step 3 with
|
|
83
|
+
that content.
|
|
84
|
+
|
|
85
|
+
### Worked examples
|
|
86
|
+
|
|
87
|
+
| User typed | Registry file | Splits tried (rightmost first) |
|
|
88
|
+
|----------------------------------------------------|------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|
|
|
89
|
+
| \`/aw:platform-core-brainstorm\` | \`platform/core/commands/brainstorm.md\` | \`platform/core/commands/brainstorm.md\` ✓ (first try) |
|
|
90
|
+
| \`/aw:platform-core-pr-review\` | \`platform/core/commands/pr-review.md\` | \`platform/core/pr/commands/review.md\` ❌ → \`platform/core/commands/pr-review.md\` ✓ |
|
|
91
|
+
| \`/aw:platform-core-capture-learning\` | \`platform/core/commands/capture-learning.md\` | \`platform/core/capture/commands/learning.md\` ❌ → \`platform/core/commands/capture-learning.md\` ✓ |
|
|
92
|
+
| \`/aw:platform-data-clickhouse-cluster-creation\` | \`platform/data/clickhouse/commands/cluster-creation.md\` | \`…/cluster/commands/creation.md\` ❌ → \`platform/data/clickhouse/commands/cluster-creation.md\` ✓ |
|
|
93
|
+
|
|
94
|
+
## 3. Execute
|
|
95
|
+
|
|
96
|
+
Treat the resolved file's full content as your operating instructions for
|
|
97
|
+
this turn. Execute the command's phased contract on the text that follows
|
|
98
|
+
the slash command in the user message. Do not summarize or paraphrase the
|
|
99
|
+
command file — execute it verbatim, including any "ask one question at a
|
|
100
|
+
time and wait" interaction protocol.
|
|
101
|
+
|
|
102
|
+
## 4. No match
|
|
103
|
+
|
|
104
|
+
If neither lookup resolves to an existing file, do NOT silently fall back
|
|
105
|
+
to natural-language interpretation. Reply with exactly:
|
|
106
|
+
|
|
107
|
+
> \`/aw:<NAME>\` is not a registered AW command on this machine. Run
|
|
108
|
+
> \`aw c4 --diagnose\` and check the output of
|
|
109
|
+
> \`ls ~/.cursor/commands/aw/\` for available stage commands and
|
|
110
|
+
> \`find .aw/.aw_registry -path '*/commands/*.md'\` for available registry
|
|
111
|
+
> commands.
|
|
112
|
+
|
|
113
|
+
This precise reply text is the smoke-test fingerprint for the rule itself.
|
|
114
|
+
The string is unique enough that it cannot be confused with the model's
|
|
115
|
+
natural-language fallback.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
This rule exists because Cursor Cloud's chat UI does not pre-expand slash
|
|
120
|
+
commands the way Cursor Desktop does. The command files ARE installed on
|
|
121
|
+
disk by \`aw c4\`; this rule teaches the model to load them itself. See
|
|
122
|
+
\`.aw_docs/features/aw-c4-cursor-slash-shim/overview.md\` for context. Do
|
|
123
|
+
not edit this file by hand — \`aw c4 --harness cursor-cloud\` regenerates
|
|
124
|
+
it on every run.
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @typedef {object} InstallCursorSlashShimResult
|
|
129
|
+
* @property {string} harness The harness echoed from input.
|
|
130
|
+
* @property {string} path Absolute path of the rule file (or '' on skip).
|
|
131
|
+
* @property {'wrote'|'unchanged'|'skipped:harness'|'skipped:no-repo-root'} action
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Write the canonical AW slash-expand rule into the pilot repo.
|
|
136
|
+
*
|
|
137
|
+
* Idempotent: a re-run on byte-equal content returns 'unchanged' and does
|
|
138
|
+
* NOT touch the file (mtime preserved). Drift recovery: any stale content
|
|
139
|
+
* (including a symlink at the rule path) is replaced with a regular file
|
|
140
|
+
* containing the canonical template.
|
|
141
|
+
*
|
|
142
|
+
* @param {object} opts
|
|
143
|
+
* @param {string} opts.harness 'cursor-cloud' (writes) or other (skips).
|
|
144
|
+
* @param {string} opts.repoRoot Absolute path to the repo root.
|
|
145
|
+
* @returns {InstallCursorSlashShimResult}
|
|
146
|
+
*/
|
|
147
|
+
export function installCursorSlashShim(opts) {
|
|
148
|
+
if (!opts || typeof opts !== 'object') {
|
|
149
|
+
throw new Error('installCursorSlashShim: opts object is required');
|
|
150
|
+
}
|
|
151
|
+
const { harness, repoRoot } = opts;
|
|
152
|
+
|
|
153
|
+
if (harness !== 'cursor-cloud') {
|
|
154
|
+
return { harness, path: '', action: 'skipped:harness' };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!repoRoot || typeof repoRoot !== 'string' || !isExistingDir(repoRoot)) {
|
|
158
|
+
return { harness, path: '', action: 'skipped:no-repo-root' };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const rulesDir = join(repoRoot, CURSOR_RULES_SUBPATH);
|
|
162
|
+
const rulePath = join(rulesDir, RULE_FILENAME);
|
|
163
|
+
|
|
164
|
+
// Idempotency check: if a regular file with byte-identical content
|
|
165
|
+
// already exists, return 'unchanged' WITHOUT writing — preserves mtime
|
|
166
|
+
// for the test contract and avoids spurious file-watcher noise.
|
|
167
|
+
if (isRegularFileWithIdenticalContent(rulePath)) {
|
|
168
|
+
return { harness, path: rulePath, action: 'unchanged' };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Drift recovery: clear the existing entry (regular file with stale
|
|
172
|
+
// bytes, or any symlink) before writing a fresh regular file. Symlinks
|
|
173
|
+
// get unlinked rather than overwritten so we never write through to
|
|
174
|
+
// their targets.
|
|
175
|
+
if (lstatExists(rulePath)) {
|
|
176
|
+
try {
|
|
177
|
+
unlinkSync(rulePath);
|
|
178
|
+
} catch {
|
|
179
|
+
// Ignore — writeFileSync below will overwrite a regular file
|
|
180
|
+
// anyway. If it's something stranger (e.g. a directory we can't
|
|
181
|
+
// delete), the writeFileSync will throw and bubble up to safe().
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
mkdirSync(rulesDir, { recursive: true });
|
|
186
|
+
writeFileSync(rulePath, CURSOR_SLASH_SHIM_RULE, 'utf8');
|
|
187
|
+
|
|
188
|
+
return { harness, path: rulePath, action: 'wrote' };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isExistingDir(p) {
|
|
192
|
+
try {
|
|
193
|
+
return statSync(p).isDirectory();
|
|
194
|
+
} catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function lstatExists(p) {
|
|
200
|
+
try {
|
|
201
|
+
lstatSync(p);
|
|
202
|
+
return true;
|
|
203
|
+
} catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isRegularFileWithIdenticalContent(rulePath) {
|
|
209
|
+
if (!existsSync(rulePath)) return false;
|
|
210
|
+
let isSym = false;
|
|
211
|
+
try {
|
|
212
|
+
isSym = lstatSync(rulePath).isSymbolicLink();
|
|
213
|
+
} catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
if (isSym) return false; // Always replace symlinks with regular files.
|
|
217
|
+
try {
|
|
218
|
+
return readFileSync(rulePath, 'utf8') === CURSOR_SLASH_SHIM_RULE;
|
|
219
|
+
} catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
package/c4/diagnostics.mjs
CHANGED
|
@@ -112,6 +112,16 @@ export function summarizeAsOneLine(state) {
|
|
|
112
112
|
|
|
113
113
|
if (state.mcpProbe) parts.push(`mcp ${state.mcpProbe}`);
|
|
114
114
|
|
|
115
|
+
if (state.slashShim) {
|
|
116
|
+
parts.push(`slashShim ${slashShimSummaryToken(state.slashShim.action)}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Proxy state — only surfaced when actually applied. Skipping the
|
|
120
|
+
// token on no-proxy harnesses keeps the summary line short.
|
|
121
|
+
if (state.proxy && state.proxy.applied && state.proxy.source) {
|
|
122
|
+
parts.push(`proxy ${state.proxy.source}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
115
125
|
const seconds = Number(state.durationMs ?? 0) / 1000;
|
|
116
126
|
parts.push(`init ${seconds.toFixed(1)}s`);
|
|
117
127
|
parts.push(state.didInit ? 'ready' : 'skipped');
|
|
@@ -119,6 +129,18 @@ export function summarizeAsOneLine(state) {
|
|
|
119
129
|
return parts.join(DOT);
|
|
120
130
|
}
|
|
121
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Map the structured slashShim action into a 1-token line summary glyph.
|
|
134
|
+
* 'wrote' is loud (something changed). 'unchanged' is quiet ('ok'). Both
|
|
135
|
+
* skip variants collapse to 'skip' so the line stays short on Claude/Codex
|
|
136
|
+
* harnesses where slashShim is a no-op.
|
|
137
|
+
*/
|
|
138
|
+
function slashShimSummaryToken(action) {
|
|
139
|
+
if (action === 'wrote') return 'wrote';
|
|
140
|
+
if (action === 'unchanged') return 'ok';
|
|
141
|
+
return 'skip';
|
|
142
|
+
}
|
|
143
|
+
|
|
122
144
|
/* ─────────────────────────────────────────────────────────────────────────
|
|
123
145
|
* diagnoseAwRouterView
|
|
124
146
|
* ───────────────────────────────────────────────────────────────────────── */
|
|
@@ -455,6 +477,31 @@ export function dumpPostInitState(opts = {}) {
|
|
|
455
477
|
for (const skillPath of sampleRegistrySkillPaths(opts.awHome, 10)) {
|
|
456
478
|
lines.push(` ${skillPath}`);
|
|
457
479
|
}
|
|
480
|
+
lines.push('');
|
|
481
|
+
|
|
482
|
+
// Cursor slash-shim block (cursor-cloud only; opt.slashShim absent on
|
|
483
|
+
// other harnesses, where the block is intentionally suppressed).
|
|
484
|
+
if (opts.slashShim && typeof opts.slashShim === 'object') {
|
|
485
|
+
const shim = opts.slashShim;
|
|
486
|
+
lines.push('slashShim:');
|
|
487
|
+
lines.push(` harness=${shim.harness ?? '<unknown>'}`);
|
|
488
|
+
lines.push(` action=${shim.action ?? '<unknown>'}`);
|
|
489
|
+
if (shim.path) lines.push(` path=${shim.path}`);
|
|
490
|
+
lines.push('');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Proxy block (always emitted when opts.proxy is provided, so operators
|
|
494
|
+
// can confirm whether HTTPS_PROXY was honored. Codex Cloud diagnostics
|
|
495
|
+
// depends on this being present.)
|
|
496
|
+
if (opts.proxy && typeof opts.proxy === 'object') {
|
|
497
|
+
const proxy = opts.proxy;
|
|
498
|
+
lines.push('proxy:');
|
|
499
|
+
lines.push(` applied=${proxy.applied ? 'true' : 'false'}`);
|
|
500
|
+
if (proxy.source) lines.push(` source=${proxy.source}`);
|
|
501
|
+
if (proxy.proxyUrl) lines.push(` url=${proxy.proxyUrl}`);
|
|
502
|
+
lines.push('');
|
|
503
|
+
}
|
|
504
|
+
|
|
458
505
|
lines.push('─── end ───');
|
|
459
506
|
lines.push('');
|
|
460
507
|
|
package/c4/gitAuth.mjs
CHANGED
|
@@ -349,12 +349,21 @@ export async function verifyAuth(mode, token, { shell = defaultShell, fetchImpl
|
|
|
349
349
|
* c FAIL → 'rewrite-conflict' (the Cursor scenario)
|
|
350
350
|
* else → 'ok'
|
|
351
351
|
*
|
|
352
|
+
* Note: 'pat-invalid' is misnamed when `authStatus` is undefined (i.e. the
|
|
353
|
+
* REST `fetch()` errored out before getting an HTTP status). The Codex
|
|
354
|
+
* Cloud failure mode is exactly that: HTTPS_PROXY is set but Node fetch
|
|
355
|
+
* doesn't honor it, so the REST probe returns `{ ok:false, error:'fetch failed' }`
|
|
356
|
+
* with no status. Callers must distinguish the two by looking at
|
|
357
|
+
* `authStatus` and `lsRemoteWithAuthOk`.
|
|
358
|
+
*
|
|
352
359
|
* @param {string} token
|
|
353
360
|
* @param {object} [opts]
|
|
354
361
|
* @returns {Promise<{
|
|
355
362
|
* apiOk: boolean,
|
|
356
363
|
* lsRemoteWithAuthOk: boolean,
|
|
357
364
|
* lsRemoteViaRewriteOk: boolean,
|
|
365
|
+
* authStatus: number | undefined,
|
|
366
|
+
* authError: string | undefined,
|
|
358
367
|
* diagnosis: 'ok' | 'pat-invalid' | 'rewrite-conflict' | 'helper-mismatch' | 'network'
|
|
359
368
|
* }>}
|
|
360
369
|
*/
|
|
@@ -380,5 +389,12 @@ export async function preflightPlatformDocs(token, opts = {}) {
|
|
|
380
389
|
diagnosis = 'ok';
|
|
381
390
|
}
|
|
382
391
|
|
|
383
|
-
return {
|
|
392
|
+
return {
|
|
393
|
+
apiOk,
|
|
394
|
+
lsRemoteWithAuthOk,
|
|
395
|
+
lsRemoteViaRewriteOk,
|
|
396
|
+
authStatus: a.status,
|
|
397
|
+
authError: a.error,
|
|
398
|
+
diagnosis,
|
|
399
|
+
};
|
|
384
400
|
}
|
package/c4/index.mjs
CHANGED
|
@@ -50,11 +50,13 @@ export { MCP_URL_DEFAULT, registerGhlAiMcp } from './mcpServer.mjs';
|
|
|
50
50
|
export { probeMcpServer } from './mcpSmokeProbe.mjs';
|
|
51
51
|
export { ensureClaudeMarketplace } from './claudePluginRegistry.mjs';
|
|
52
52
|
export { ensureCommandSurface, diagnoseCommandResolution } from './commandSurface.mjs';
|
|
53
|
+
export { installCursorSlashShim, CURSOR_SLASH_SHIM_RULE } from './cursorRulesShim.mjs';
|
|
53
54
|
export { ensureRepoLocalClaudeSettings } from './repoLocalClaudeSettings.mjs';
|
|
54
55
|
export { copyRepoRootInstructions } from './repoRootInstructions.mjs';
|
|
55
56
|
export { ensureRepoLocalIgnore } from './repoLocalIgnore.mjs';
|
|
56
57
|
export { jsonMergeWithDedup, claudeHooksMerge } from './jsonMerge.mjs';
|
|
57
58
|
export { runPreflight } from './preflight.mjs';
|
|
59
|
+
export { configureUndiciProxy } from './proxyConfig.mjs';
|
|
58
60
|
export {
|
|
59
61
|
summarizeAsOneLine,
|
|
60
62
|
diagnoseAwRouterView,
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* c4/proxyConfig.mjs — install undici proxy dispatcher from env.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists:
|
|
5
|
+
* Codex Cloud (and other corporate-MITM cloud agents) force outbound
|
|
6
|
+
* HTTPS through a proxy via HTTPS_PROXY. Standard tools (curl, git, npm)
|
|
7
|
+
* honor that variable transparently. Node's native fetch (undici) does
|
|
8
|
+
* NOT honor proxy env vars before Node 24, so REST preflight in
|
|
9
|
+
* `aw c4` fails with `fetch failed: ENETUNREACH` even though the same
|
|
10
|
+
* PAT works fine via `git ls-remote` and `curl`.
|
|
11
|
+
*
|
|
12
|
+
* This module reads the proxy URL from env in the documented priority
|
|
13
|
+
* order and installs an undici ProxyAgent as the global dispatcher
|
|
14
|
+
* exactly once at orchestrator entry. After this runs, every fetch()
|
|
15
|
+
* call inside aw-c4 routes through the proxy.
|
|
16
|
+
*
|
|
17
|
+
* Why dynamic import:
|
|
18
|
+
* Keeps the no-proxy fast path zero-cost (no undici namespace resolution)
|
|
19
|
+
* and lets unit tests inject a synthetic { setGlobalDispatcher, ProxyAgent }
|
|
20
|
+
* pair via opts.undiciImpl without going through vi.mock module-graph
|
|
21
|
+
* plumbing.
|
|
22
|
+
*
|
|
23
|
+
* Failure isolation:
|
|
24
|
+
* If ProxyAgent construction or setGlobalDispatcher throws, we swallow
|
|
25
|
+
* it and report `applied: false`. A misconfigured proxy URL must NEVER
|
|
26
|
+
* crash `aw c4` — the orchestrator continues and surfaces the failure
|
|
27
|
+
* in diagnostics so the user can see what happened.
|
|
28
|
+
*
|
|
29
|
+
* Contract:
|
|
30
|
+
* .aw_docs/features/aw-c4-codex-proxy-fix/spec.md::§"libs/aw/c4/proxyConfig.mjs"
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const PROXY_ENV_PRIORITY = Object.freeze([
|
|
34
|
+
'HTTPS_PROXY',
|
|
35
|
+
'https_proxy',
|
|
36
|
+
'HTTP_PROXY',
|
|
37
|
+
'http_proxy',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {object} ProxyConfigResult
|
|
42
|
+
* @property {boolean} applied True iff a global dispatcher was
|
|
43
|
+
* installed successfully.
|
|
44
|
+
* @property {string|null} proxyUrl The URL the agent was constructed
|
|
45
|
+
* with, or null when no proxy env
|
|
46
|
+
* var was set.
|
|
47
|
+
* @property {string|null} source Exact env-var name the URL was
|
|
48
|
+
* resolved from, or null. Useful
|
|
49
|
+
* for diagnostics (so the user can
|
|
50
|
+
* see "we used HTTPS_PROXY, not
|
|
51
|
+
* http_proxy").
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the first non-empty proxy URL from env in priority order.
|
|
56
|
+
*
|
|
57
|
+
* @param {NodeJS.ProcessEnv} env
|
|
58
|
+
* @returns {{ url: string, source: string } | null}
|
|
59
|
+
*/
|
|
60
|
+
function resolveProxyFromEnv(env) {
|
|
61
|
+
for (const name of PROXY_ENV_PRIORITY) {
|
|
62
|
+
const value = env?.[name];
|
|
63
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
64
|
+
return { url: value, source: name };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Install an undici ProxyAgent as the global fetch dispatcher when
|
|
72
|
+
* an HTTP/HTTPS proxy env var is set.
|
|
73
|
+
*
|
|
74
|
+
* Calling this with no proxy in env is safe — it returns
|
|
75
|
+
* `{ applied: false, ... }` without touching undici. Calling it twice
|
|
76
|
+
* is also safe; the second call replaces the first dispatcher.
|
|
77
|
+
*
|
|
78
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
79
|
+
* @param {object} [opts]
|
|
80
|
+
* @param {{ setGlobalDispatcher: Function, ProxyAgent: Function }} [opts.undiciImpl]
|
|
81
|
+
* Override for tests. When omitted, undici is dynamically imported.
|
|
82
|
+
* @returns {ProxyConfigResult}
|
|
83
|
+
*/
|
|
84
|
+
export function configureUndiciProxy(env = process.env, opts = {}) {
|
|
85
|
+
const resolved = resolveProxyFromEnv(env);
|
|
86
|
+
if (!resolved) {
|
|
87
|
+
return { applied: false, proxyUrl: null, source: null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const undici = opts.undiciImpl ?? loadUndiciSync();
|
|
91
|
+
if (!undici) {
|
|
92
|
+
return { applied: false, proxyUrl: resolved.url, source: resolved.source };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const agent = new undici.ProxyAgent(resolved.url);
|
|
97
|
+
undici.setGlobalDispatcher(agent);
|
|
98
|
+
return { applied: true, proxyUrl: resolved.url, source: resolved.source };
|
|
99
|
+
} catch {
|
|
100
|
+
// ProxyAgent ctor or setGlobalDispatcher rejected the URL. The most
|
|
101
|
+
// common cause is a malformed URL ("proxy:8080" without scheme).
|
|
102
|
+
// Caller treats apply-failure same as no-proxy and continues.
|
|
103
|
+
return { applied: false, proxyUrl: resolved.url, source: resolved.source };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
import { createRequire } from 'node:module';
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Load the bundled `undici` synchronously via createRequire so we can
|
|
111
|
+
* keep `configureUndiciProxy` synchronous (orchestrator entry must not
|
|
112
|
+
* become async just to install a dispatcher).
|
|
113
|
+
*
|
|
114
|
+
* Falls back to `null` if the import is unavailable for any reason —
|
|
115
|
+
* in which case the caller treats it as a no-op.
|
|
116
|
+
*/
|
|
117
|
+
let cachedUndici;
|
|
118
|
+
function loadUndiciSync() {
|
|
119
|
+
if (cachedUndici !== undefined) return cachedUndici;
|
|
120
|
+
try {
|
|
121
|
+
const localRequire = createRequire(import.meta.url);
|
|
122
|
+
cachedUndici = localRequire('undici');
|
|
123
|
+
} catch {
|
|
124
|
+
cachedUndici = null;
|
|
125
|
+
}
|
|
126
|
+
return cachedUndici;
|
|
127
|
+
}
|
package/commands/c4.mjs
CHANGED
|
@@ -210,6 +210,21 @@ export async function c4Command(rawArgs, overrides = {}) {
|
|
|
210
210
|
|
|
211
211
|
const startTs = now();
|
|
212
212
|
|
|
213
|
+
// Step 0 — undici proxy installation.
|
|
214
|
+
// Codex Cloud (and other corporate-MITM cloud agents) force outbound
|
|
215
|
+
// HTTPS through HTTPS_PROXY. Standard tools (curl, git, npm) honor it
|
|
216
|
+
// transparently; Node's native fetch (undici) does not before Node 24.
|
|
217
|
+
// Install a global ProxyAgent dispatcher BEFORE any other step so every
|
|
218
|
+
// subsequent fetch() (preflight REST, MCP smoke probe, etc.) routes
|
|
219
|
+
// through the proxy. Defensive try/catch — proxy install failure must
|
|
220
|
+
// never crash aw-c4.
|
|
221
|
+
let proxy = { applied: false, proxyUrl: null, source: null };
|
|
222
|
+
try {
|
|
223
|
+
proxy = c4.configureUndiciProxy(env);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
writer.stderr(`[aw-c4] configureUndiciProxy failed (non-fatal): ${err?.message ?? err}\n`);
|
|
226
|
+
}
|
|
227
|
+
|
|
213
228
|
// Step 1 — harness detection (override wins).
|
|
214
229
|
const cliHarness = args['--harness'];
|
|
215
230
|
const harness = SUPPORTED_HARNESSES.includes(cliHarness) ? cliHarness : c4.detectHarness({ env }).harness;
|
|
@@ -228,6 +243,7 @@ export async function c4Command(rawArgs, overrides = {}) {
|
|
|
228
243
|
authOk: false,
|
|
229
244
|
didInit: false,
|
|
230
245
|
durationMs: now() - startTs,
|
|
246
|
+
proxy,
|
|
231
247
|
}) + '\n');
|
|
232
248
|
return exit(0);
|
|
233
249
|
}
|
|
@@ -263,6 +279,7 @@ export async function c4Command(rawArgs, overrides = {}) {
|
|
|
263
279
|
bridge: self.view ? { ok: self.view.ok } : undefined,
|
|
264
280
|
injector: self.injector ? { ok: self.injector.ok } : undefined,
|
|
265
281
|
diagnose: true,
|
|
282
|
+
proxy,
|
|
266
283
|
}) + '\n');
|
|
267
284
|
} catch (err) {
|
|
268
285
|
writer.stderr(`[aw-c4] diagnose: summary line failed: ${err?.message ?? err}\n`);
|
|
@@ -282,18 +299,39 @@ export async function c4Command(rawArgs, overrides = {}) {
|
|
|
282
299
|
// Step 5 — preflight.
|
|
283
300
|
const preflight = await c4.preflightPlatformDocs(token);
|
|
284
301
|
if (preflight.diagnosis === 'pat-invalid') {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
302
|
+
// Disambiguate two failure modes that both end up tagged 'pat-invalid':
|
|
303
|
+
// (a) Real bad PAT — REST returned a 4xx HTTP status (`authStatus`
|
|
304
|
+
// is a number). Git ls-remote also fails. Exit 0 with the
|
|
305
|
+
// actionable "refresh PAT" message.
|
|
306
|
+
// (b) Codex Cloud proxy gap — REST `fetch()` failed BEFORE getting
|
|
307
|
+
// any HTTP status (`authStatus == null`, `authError` like
|
|
308
|
+
// "fetch failed: ENETUNREACH") because Node fetch can't see
|
|
309
|
+
// HTTPS_PROXY. Git ls-remote works (libcurl honors the proxy).
|
|
310
|
+
// The PAT is fine; proceed with install and surface the real
|
|
311
|
+
// cause on stderr.
|
|
312
|
+
const restStatusKnown = typeof preflight.authStatus === 'number';
|
|
313
|
+
if (!restStatusKnown && preflight.lsRemoteWithAuthOk) {
|
|
314
|
+
const cause = preflight.authError ?? 'unknown';
|
|
315
|
+
writer.stderr(
|
|
316
|
+
`[aw-c4] api.github.com unreachable from Node fetch (${cause}); ` +
|
|
317
|
+
`git auth OK, continuing.\n`,
|
|
318
|
+
);
|
|
319
|
+
// fall through into normal install flow
|
|
320
|
+
} else {
|
|
321
|
+
writer.stderr('[aw-c4] PAT invalid (REST 4xx). Refresh GITHUB_PAT and retry.\n');
|
|
322
|
+
writer.stdout(c4.summarizeAsOneLine({
|
|
323
|
+
harness,
|
|
324
|
+
hasToken: true,
|
|
325
|
+
tokenSource,
|
|
326
|
+
authOk: false,
|
|
327
|
+
didInit: false,
|
|
328
|
+
durationMs: now() - startTs,
|
|
329
|
+
proxy,
|
|
330
|
+
}) + '\n');
|
|
331
|
+
return exit(0);
|
|
332
|
+
}
|
|
295
333
|
}
|
|
296
|
-
if (preflight.diagnosis !== 'ok') {
|
|
334
|
+
if (preflight.diagnosis !== 'ok' && preflight.diagnosis !== 'pat-invalid') {
|
|
297
335
|
writer.stderr(`[aw-c4] preflight diagnosis: ${preflight.diagnosis} (continuing)\n`);
|
|
298
336
|
}
|
|
299
337
|
|
|
@@ -306,6 +344,7 @@ export async function c4Command(rawArgs, overrides = {}) {
|
|
|
306
344
|
authOk: preflight.diagnosis === 'ok',
|
|
307
345
|
didInit: false,
|
|
308
346
|
durationMs: now() - startTs,
|
|
347
|
+
proxy,
|
|
309
348
|
}) + '\n');
|
|
310
349
|
return exit(0);
|
|
311
350
|
}
|
|
@@ -346,6 +385,17 @@ export async function c4Command(rawArgs, overrides = {}) {
|
|
|
346
385
|
// Step 12 — slash command surface.
|
|
347
386
|
safe('ensureCommandSurface', () => c4.ensureCommandSurface({ harness, home, eccHome }), writer);
|
|
348
387
|
|
|
388
|
+
// Step 12b — Cursor Cloud slash-expand rule (no-op on other harnesses).
|
|
389
|
+
// The model-side workaround for Cursor Cloud's chat UI not pre-expanding
|
|
390
|
+
// `/aw:<NAME>` slash commands. The function self-skips on other
|
|
391
|
+
// harnesses, so the orchestrator does not pre-filter; the resulting
|
|
392
|
+
// action is surfaced in the one-line summary and post-init dump.
|
|
393
|
+
const slashShim = safe(
|
|
394
|
+
'installCursorSlashShim',
|
|
395
|
+
() => c4.installCursorSlashShim({ harness, repoRoot: cwd }),
|
|
396
|
+
writer,
|
|
397
|
+
);
|
|
398
|
+
|
|
349
399
|
// Step 13 — repo-root context.
|
|
350
400
|
safe('copyRepoRootInstructions', () => c4.copyRepoRootInstructions(harness, cwd, eccHome), writer);
|
|
351
401
|
|
|
@@ -368,7 +418,14 @@ export async function c4Command(rawArgs, overrides = {}) {
|
|
|
368
418
|
}
|
|
369
419
|
|
|
370
420
|
// Step 17 — post-init dump.
|
|
371
|
-
c4.dumpPostInitState({
|
|
421
|
+
c4.dumpPostInitState({
|
|
422
|
+
harness,
|
|
423
|
+
cwd,
|
|
424
|
+
awHome,
|
|
425
|
+
configPaths: [],
|
|
426
|
+
slashShim: slashShim?.ok ? slashShim.value : null,
|
|
427
|
+
proxy,
|
|
428
|
+
});
|
|
372
429
|
|
|
373
430
|
// Step 18 — one-line summary.
|
|
374
431
|
writer.stdout(c4.summarizeAsOneLine({
|
|
@@ -381,6 +438,8 @@ export async function c4Command(rawArgs, overrides = {}) {
|
|
|
381
438
|
bridge: branch?.bridge,
|
|
382
439
|
injector: branch?.injector,
|
|
383
440
|
mcpProbe: mcpProbe?.value?.authStatus,
|
|
441
|
+
slashShim: slashShim?.ok ? slashShim.value : null,
|
|
442
|
+
proxy,
|
|
384
443
|
}) + '\n');
|
|
385
444
|
|
|
386
445
|
return exit(0);
|