@ghl-ai/aw 0.1.44-beta.0 → 0.1.44-beta.10
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/claudePluginRegistry.mjs +142 -0
- package/c4/codexConfig.mjs +148 -0
- package/c4/codexPromptInjector.mjs +187 -0
- package/c4/commandSurface.mjs +286 -0
- package/c4/cursorRulesShim.mjs +222 -0
- package/c4/detect.mjs +62 -0
- package/c4/diagnostics.mjs +583 -0
- package/c4/eccRegistryBridge.mjs +184 -0
- package/c4/ghCli.mjs +94 -0
- package/c4/gitAuth.mjs +400 -0
- package/c4/index.mjs +74 -0
- package/c4/initRepo.mjs +342 -0
- package/c4/jsonMerge.mjs +251 -0
- package/c4/mcpServer.mjs +160 -0
- package/c4/mcpSmokeProbe.mjs +201 -0
- package/c4/preflight.mjs +254 -0
- package/c4/proxyConfig.mjs +127 -0
- package/c4/repoLocalClaudeSettings.mjs +54 -0
- package/c4/repoLocalIgnore.mjs +157 -0
- package/c4/repoRootInstructions.mjs +166 -0
- package/c4/secrets.mjs +55 -0
- package/c4/slimRouter.mjs +483 -0
- package/c4/templates/claude/scripts/claude-web-bootstrap.sh +8 -0
- package/c4/templates/codex/scripts/codex-web-bootstrap.sh +8 -0
- package/c4/templates/cursor/environment.json +5 -0
- package/c4/templates/manifest.json +29 -0
- package/c4/templates/scripts/aw-c4-bootstrap.sh +100 -0
- package/cli.mjs +9 -0
- package/commands/c4.mjs +446 -0
- package/commands/init-repo.mjs +83 -0
- package/ecc.mjs +7 -25
- package/integrate.mjs +6 -6
- package/mcp.mjs +2 -23
- package/package.json +5 -3
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* c4/claudePluginRegistry.mjs — Claude marketplace + plugin enable + Skill
|
|
3
|
+
* permission registration.
|
|
4
|
+
*
|
|
5
|
+
* Three settings.json mutations driven by `ensureClaudeMarketplace`:
|
|
6
|
+
*
|
|
7
|
+
* 1. extraKnownMarketplaces["aw-marketplace"] = {
|
|
8
|
+
* source: { source: "directory", path: <eccDir> } // NESTED, easy to get wrong
|
|
9
|
+
* }
|
|
10
|
+
* Pilot empirically established the nested source.source shape; a flat
|
|
11
|
+
* `source: "directory"` does not register the marketplace.
|
|
12
|
+
*
|
|
13
|
+
* 2. enabledPlugins["aw@aw-marketplace"] = true
|
|
14
|
+
* Activates the plugin (without this Claude knows about the marketplace
|
|
15
|
+
* but does not surface its commands/skills).
|
|
16
|
+
*
|
|
17
|
+
* 3. permissions.allow includes "Skill"
|
|
18
|
+
* Without this Claude refuses to dispatch the Skill tool. Append-only
|
|
19
|
+
* to preserve user-authored entries; idempotent (no duplicates, no
|
|
20
|
+
* reordering).
|
|
21
|
+
*
|
|
22
|
+
* Atomic write + 0600 mode. Returns { changed: false } when every mutation
|
|
23
|
+
* is a no-op.
|
|
24
|
+
*
|
|
25
|
+
* Contract: spec.md::§"c4/claudePluginRegistry.mjs", tasks.md::3.4b.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
existsSync,
|
|
30
|
+
readFileSync,
|
|
31
|
+
writeFileSync,
|
|
32
|
+
renameSync,
|
|
33
|
+
chmodSync,
|
|
34
|
+
mkdirSync,
|
|
35
|
+
unlinkSync,
|
|
36
|
+
} from 'node:fs';
|
|
37
|
+
import { dirname, basename, join } from 'node:path';
|
|
38
|
+
|
|
39
|
+
const FILE_MODE = 0o600;
|
|
40
|
+
const MARKETPLACE_NAME = 'aw-marketplace';
|
|
41
|
+
const PLUGIN_KEY = 'aw@aw-marketplace';
|
|
42
|
+
const SKILL_PERMISSION = 'Skill';
|
|
43
|
+
|
|
44
|
+
function ensureDir(d) {
|
|
45
|
+
mkdirSync(d, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readJsonOrEmpty(filePath) {
|
|
49
|
+
if (!existsSync(filePath)) return {};
|
|
50
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
51
|
+
if (raw.trim() === '') return {};
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(raw);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
throw new Error(`Failed to parse JSON at ${filePath}: ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function atomicWriteIfChanged(filePath, serialized) {
|
|
60
|
+
if (existsSync(filePath)) {
|
|
61
|
+
const prev = readFileSync(filePath, 'utf8');
|
|
62
|
+
if (prev === serialized) return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ensureDir(dirname(filePath));
|
|
66
|
+
|
|
67
|
+
const tmp = join(
|
|
68
|
+
dirname(filePath),
|
|
69
|
+
`.${basename(filePath)}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
writeFileSync(tmp, serialized, { mode: FILE_MODE });
|
|
74
|
+
renameSync(tmp, filePath);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (existsSync(tmp)) {
|
|
77
|
+
try { unlinkSync(tmp); } catch { /* best-effort */ }
|
|
78
|
+
}
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try { chmodSync(filePath, FILE_MODE); } catch { /* best-effort */ }
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isPlainObject(v) {
|
|
87
|
+
return v != null && typeof v === 'object' && !Array.isArray(v);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function ensureMarketplace(root, eccDir) {
|
|
91
|
+
if (!isPlainObject(root.extraKnownMarketplaces)) root.extraKnownMarketplaces = {};
|
|
92
|
+
// Nested source.source schema — pilot-verified.
|
|
93
|
+
root.extraKnownMarketplaces[MARKETPLACE_NAME] = {
|
|
94
|
+
...(isPlainObject(root.extraKnownMarketplaces[MARKETPLACE_NAME])
|
|
95
|
+
? root.extraKnownMarketplaces[MARKETPLACE_NAME]
|
|
96
|
+
: {}),
|
|
97
|
+
source: { source: 'directory', path: eccDir },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function ensurePluginEnabled(root) {
|
|
102
|
+
if (!isPlainObject(root.enabledPlugins)) root.enabledPlugins = {};
|
|
103
|
+
root.enabledPlugins[PLUGIN_KEY] = true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ensureSkillPermission(root) {
|
|
107
|
+
if (!isPlainObject(root.permissions)) root.permissions = {};
|
|
108
|
+
const existing = Array.isArray(root.permissions.allow) ? root.permissions.allow : [];
|
|
109
|
+
if (existing.includes(SKILL_PERMISSION)) {
|
|
110
|
+
root.permissions.allow = existing;
|
|
111
|
+
} else {
|
|
112
|
+
root.permissions.allow = [...existing, SKILL_PERMISSION];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Ensure Claude is wired up to surface ECC's `aw:<stage>` skills via plugin
|
|
118
|
+
* marketplace + Skill permission.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} home User home (e.g. tmp dir in tests).
|
|
121
|
+
* @param {string} eccDir Path to the ECC plugin directory (~/.aw-ecc).
|
|
122
|
+
* @returns {{ changed: boolean }}
|
|
123
|
+
*/
|
|
124
|
+
export function ensureClaudeMarketplace(home, eccDir) {
|
|
125
|
+
if (!home || typeof home !== 'string') {
|
|
126
|
+
throw new Error('ensureClaudeMarketplace: home is required');
|
|
127
|
+
}
|
|
128
|
+
if (!eccDir || typeof eccDir !== 'string') {
|
|
129
|
+
throw new Error('ensureClaudeMarketplace: eccDir is required');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const settingsPath = join(home, '.claude/settings.json');
|
|
133
|
+
const root = readJsonOrEmpty(settingsPath);
|
|
134
|
+
|
|
135
|
+
ensureMarketplace(root, eccDir);
|
|
136
|
+
ensurePluginEnabled(root);
|
|
137
|
+
ensureSkillPermission(root);
|
|
138
|
+
|
|
139
|
+
const serialized = JSON.stringify(root, null, 2) + '\n';
|
|
140
|
+
const changed = atomicWriteIfChanged(settingsPath, serialized);
|
|
141
|
+
return { changed };
|
|
142
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* c4/codexConfig.mjs — Codex-only ~/.codex/config.toml editor.
|
|
3
|
+
*
|
|
4
|
+
* Codex hooks silently no-op without `[features] codex_hooks = true` in this
|
|
5
|
+
* file (validated in pilot). The MCP block lives in the same file as a
|
|
6
|
+
* `[mcp_servers.<name>]` table with optional `[mcp_servers.<name>.headers]`
|
|
7
|
+
* sub-table.
|
|
8
|
+
*
|
|
9
|
+
* We use @iarna/toml's parse + stringify for full round-trip fidelity.
|
|
10
|
+
* Atomic write (tmp file + rename, chmod 0600) keeps Codex from reading a
|
|
11
|
+
* partial file mid-write.
|
|
12
|
+
*
|
|
13
|
+
* Contract: spec.md::§"c4/codexConfig.mjs", tasks.md::3.3.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
existsSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
renameSync,
|
|
21
|
+
chmodSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
unlinkSync,
|
|
24
|
+
} from 'node:fs';
|
|
25
|
+
import { dirname, basename, join } from 'node:path';
|
|
26
|
+
import TOML from '@iarna/toml';
|
|
27
|
+
|
|
28
|
+
export const MCP_URL_DEFAULT =
|
|
29
|
+
'https://services.leadconnectorhq.com/agentic-workspace/mcp';
|
|
30
|
+
|
|
31
|
+
const FILE_MODE = 0o600;
|
|
32
|
+
|
|
33
|
+
function ensureDir(d) {
|
|
34
|
+
mkdirSync(d, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readTomlOrEmpty(filePath) {
|
|
38
|
+
if (!existsSync(filePath)) return {};
|
|
39
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
40
|
+
if (raw.trim() === '') return {};
|
|
41
|
+
try {
|
|
42
|
+
return TOML.parse(raw);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
throw new Error(`Failed to parse TOML at ${filePath}: ${err.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function atomicWriteIfChanged(filePath, serialized) {
|
|
49
|
+
if (existsSync(filePath)) {
|
|
50
|
+
const prev = readFileSync(filePath, 'utf8');
|
|
51
|
+
if (prev === serialized) return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ensureDir(dirname(filePath));
|
|
55
|
+
|
|
56
|
+
const tmp = join(
|
|
57
|
+
dirname(filePath),
|
|
58
|
+
`.${basename(filePath)}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
writeFileSync(tmp, serialized, { mode: FILE_MODE });
|
|
63
|
+
renameSync(tmp, filePath);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (existsSync(tmp)) {
|
|
66
|
+
try { unlinkSync(tmp); } catch { /* best-effort */ }
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try { chmodSync(filePath, FILE_MODE); } catch { /* best-effort */ }
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function configPathFor(home) {
|
|
76
|
+
return join(home, '.codex/config.toml');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Ensure `[features] codex_hooks = true` in ~/.codex/config.toml.
|
|
81
|
+
* Preserves all other keys (round-trip via @iarna/toml).
|
|
82
|
+
*
|
|
83
|
+
* @param {string} home
|
|
84
|
+
* @returns {{ changed: boolean }}
|
|
85
|
+
*/
|
|
86
|
+
export function ensureCodexHooksFlag(home) {
|
|
87
|
+
if (!home || typeof home !== 'string') {
|
|
88
|
+
throw new Error('ensureCodexHooksFlag: home is required');
|
|
89
|
+
}
|
|
90
|
+
const filePath = configPathFor(home);
|
|
91
|
+
const root = readTomlOrEmpty(filePath);
|
|
92
|
+
if (root.features == null || typeof root.features !== 'object' || Array.isArray(root.features)) {
|
|
93
|
+
root.features = {};
|
|
94
|
+
}
|
|
95
|
+
root.features.codex_hooks = true;
|
|
96
|
+
const serialized = TOML.stringify(root);
|
|
97
|
+
const changed = atomicWriteIfChanged(filePath, serialized);
|
|
98
|
+
return { changed };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Ensure `[mcp_servers.ghl-ai]` block with url + Bearer token + transport.
|
|
103
|
+
* URL falls back to env MCP_URL → MCP_URL_DEFAULT.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} home
|
|
106
|
+
* @param {string} token
|
|
107
|
+
* @returns {{ changed: boolean }}
|
|
108
|
+
*/
|
|
109
|
+
export function ensureCodexMcpServer(home, token) {
|
|
110
|
+
if (!home || typeof home !== 'string') {
|
|
111
|
+
throw new Error('ensureCodexMcpServer: home is required');
|
|
112
|
+
}
|
|
113
|
+
if (!token || typeof token !== 'string') {
|
|
114
|
+
throw new Error('ensureCodexMcpServer: token is required');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const url = process.env.MCP_URL || MCP_URL_DEFAULT;
|
|
118
|
+
const filePath = configPathFor(home);
|
|
119
|
+
const root = readTomlOrEmpty(filePath);
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
root.mcp_servers == null ||
|
|
123
|
+
typeof root.mcp_servers !== 'object' ||
|
|
124
|
+
Array.isArray(root.mcp_servers)
|
|
125
|
+
) {
|
|
126
|
+
root.mcp_servers = {};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const ghlAi = root.mcp_servers['ghl-ai'] ?? {};
|
|
130
|
+
ghlAi.url = url;
|
|
131
|
+
ghlAi.transport = 'http';
|
|
132
|
+
// Match the canonical `aw init` reference (libs/aw/mcp.mjs::tomlMcpServerBlock)
|
|
133
|
+
// which always sets startup_timeout_sec = 30 for HTTP MCP servers.
|
|
134
|
+
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 = {};
|
|
141
|
+
}
|
|
142
|
+
ghlAi.headers.Authorization = `Bearer ${token}`;
|
|
143
|
+
root.mcp_servers['ghl-ai'] = ghlAi;
|
|
144
|
+
|
|
145
|
+
const serialized = TOML.stringify(root);
|
|
146
|
+
const changed = atomicWriteIfChanged(filePath, serialized);
|
|
147
|
+
return { changed };
|
|
148
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* c4/codexPromptInjector.mjs — Bug B (Codex non-persistence) workaround.
|
|
3
|
+
*
|
|
4
|
+
* Codex Web does not persist `additionalContext` across turns; the slim card
|
|
5
|
+
* approach used for Claude/Cursor is insufficient. Instead, we wrap ECC's
|
|
6
|
+
* existing `~/.codex/hooks/aw-user-prompt-submit.sh` so that EVERY
|
|
7
|
+
* UserPromptSubmit emits the full using-aw-skills SKILL.md body wrapped in
|
|
8
|
+
* `<EXTREMELY_IMPORTANT>` framing.
|
|
9
|
+
*
|
|
10
|
+
* The wrapper:
|
|
11
|
+
* 1. Reads stdin (Codex's UserPromptSubmit payload).
|
|
12
|
+
* 2. Calls the original hook (renamed to `<hookPath>.upstream`) with the
|
|
13
|
+
* same stdin to capture its output.
|
|
14
|
+
* 3. If env AW_PROMPT_ROUTER_INJECT=0, passes the upstream output through
|
|
15
|
+
* verbatim (escape hatch for debugging Bug B itself).
|
|
16
|
+
* 4. Otherwise reads the router skill body, builds the merged envelope:
|
|
17
|
+
* { hookSpecificOutput: { hookEventName: "UserPromptSubmit",
|
|
18
|
+
* additionalContext: "<EXTREMELY_IMPORTANT>" + skill +
|
|
19
|
+
* "</EXTREMELY_IMPORTANT>\n\n" + upstream_additionalContext } }
|
|
20
|
+
* and prints it to stdout.
|
|
21
|
+
*
|
|
22
|
+
* Idempotency: the wrapper carries a `# aw-c4 router injector v1` header
|
|
23
|
+
* marker; reinstalls detect this and no-op (the upstream backup is NOT
|
|
24
|
+
* overwritten on the second run, which would otherwise destroy the original
|
|
25
|
+
* hook).
|
|
26
|
+
*
|
|
27
|
+
* Source-of-truth path: `aw-ecc/scripts/lib/codex-aw-hook-files.js` lists
|
|
28
|
+
* `aw-user-prompt-submit.sh` as one of the files materialized into
|
|
29
|
+
* `~/.codex/hooks/` during `aw init`. NOT in the registry path.
|
|
30
|
+
*
|
|
31
|
+
* Contract: spec.md::§"c4/codexPromptInjector.mjs", tasks.md::3.2.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
existsSync,
|
|
36
|
+
readFileSync,
|
|
37
|
+
writeFileSync,
|
|
38
|
+
renameSync,
|
|
39
|
+
chmodSync,
|
|
40
|
+
mkdirSync,
|
|
41
|
+
} from 'node:fs';
|
|
42
|
+
import { dirname, join } from 'node:path';
|
|
43
|
+
import { homedir } from 'node:os';
|
|
44
|
+
|
|
45
|
+
const WRAPPER_HEADER_MARKER = '# aw-c4 router injector v1';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @returns {string} Default hook path: ${os.homedir()}/.codex/hooks/aw-user-prompt-submit.sh
|
|
49
|
+
*/
|
|
50
|
+
export function defaultHookPath() {
|
|
51
|
+
return join(homedir(), '.codex/hooks/aw-user-prompt-submit.sh');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Render the wrapper script that re-emits the router skill on every prompt.
|
|
56
|
+
*
|
|
57
|
+
* Implementation note: the wrapper is pure bash + python3 (which Codex Web
|
|
58
|
+
* always has available). It uses `python3 -c` because envelope merging
|
|
59
|
+
* requires reading-then-mutating a JSON object — a non-trivial jq call.
|
|
60
|
+
* Embedding the python is significantly less brittle than depending on jq
|
|
61
|
+
* existing in every Codex Web image.
|
|
62
|
+
*
|
|
63
|
+
* @param {object} opts
|
|
64
|
+
* @param {string} opts.routerSkillPath Absolute path to using-aw-skills/SKILL.md
|
|
65
|
+
* @param {string} opts.escapeEnvVar Env var name (e.g. AW_PROMPT_ROUTER_INJECT)
|
|
66
|
+
* @returns {string} Full bash wrapper script.
|
|
67
|
+
*/
|
|
68
|
+
function buildWrapperScript({ routerSkillPath, escapeEnvVar }) {
|
|
69
|
+
return `#!/usr/bin/env bash
|
|
70
|
+
${WRAPPER_HEADER_MARKER}
|
|
71
|
+
set -euo pipefail
|
|
72
|
+
|
|
73
|
+
# Re-emit using-aw-skills SKILL.md on every UserPromptSubmit so Codex sees
|
|
74
|
+
# the router every turn (Bug B workaround).
|
|
75
|
+
|
|
76
|
+
UPSTREAM="$0.upstream"
|
|
77
|
+
ROUTER_SKILL_PATH="${routerSkillPath}"
|
|
78
|
+
ESCAPE_VAR="${escapeEnvVar}"
|
|
79
|
+
|
|
80
|
+
# Capture stdin once — both the upstream call and any direct emit need it.
|
|
81
|
+
PAYLOAD="$(cat)"
|
|
82
|
+
|
|
83
|
+
# Run upstream (if it exists) with the same stdin; capture stdout.
|
|
84
|
+
UPSTREAM_OUT=""
|
|
85
|
+
if [ -x "$UPSTREAM" ]; then
|
|
86
|
+
UPSTREAM_OUT="$(printf '%s' "$PAYLOAD" | "$UPSTREAM" || true)"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# Escape: pass upstream through verbatim (or empty {} if no upstream).
|
|
90
|
+
escape_value="\${!ESCAPE_VAR:-}"
|
|
91
|
+
if [ "$escape_value" = "0" ]; then
|
|
92
|
+
if [ -n "$UPSTREAM_OUT" ]; then
|
|
93
|
+
printf '%s' "$UPSTREAM_OUT"
|
|
94
|
+
else
|
|
95
|
+
printf '%s' '{}'
|
|
96
|
+
fi
|
|
97
|
+
exit 0
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
export ROUTER_SKILL_PATH UPSTREAM_OUT
|
|
101
|
+
|
|
102
|
+
python3 -c '
|
|
103
|
+
import json, os, sys
|
|
104
|
+
skill_path = os.environ["ROUTER_SKILL_PATH"]
|
|
105
|
+
try:
|
|
106
|
+
skill = open(skill_path).read()
|
|
107
|
+
except OSError:
|
|
108
|
+
skill = ""
|
|
109
|
+
|
|
110
|
+
upstream_raw = os.environ.get("UPSTREAM_OUT", "").strip()
|
|
111
|
+
upstream_ctx = ""
|
|
112
|
+
if upstream_raw:
|
|
113
|
+
try:
|
|
114
|
+
u = json.loads(upstream_raw)
|
|
115
|
+
upstream_ctx = (u.get("hookSpecificOutput") or {}).get("additionalContext", "") or ""
|
|
116
|
+
except Exception:
|
|
117
|
+
upstream_ctx = ""
|
|
118
|
+
|
|
119
|
+
framed = "<EXTREMELY_IMPORTANT>\\n" + skill + "\\n</EXTREMELY_IMPORTANT>\\n\\n" + upstream_ctx
|
|
120
|
+
out = {
|
|
121
|
+
"hookSpecificOutput": {
|
|
122
|
+
"hookEventName": "UserPromptSubmit",
|
|
123
|
+
"additionalContext": framed,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
print(json.dumps(out))
|
|
127
|
+
'
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isExistingWrapper(filePath) {
|
|
132
|
+
if (!existsSync(filePath)) return false;
|
|
133
|
+
try {
|
|
134
|
+
const head = readFileSync(filePath, 'utf8').slice(0, 256);
|
|
135
|
+
return head.includes(WRAPPER_HEADER_MARKER);
|
|
136
|
+
} catch {
|
|
137
|
+
// Read failure (e.g. permission denied or transient FS error): treat as
|
|
138
|
+
// "no wrapper present" so the caller falls through to the install path.
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function ensureDir(d) {
|
|
144
|
+
mkdirSync(d, { recursive: true });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Install (or re-install) the prompt-injector wrapper.
|
|
149
|
+
*
|
|
150
|
+
* @param {object} opts
|
|
151
|
+
* @param {string} [opts.hookPath] Default: defaultHookPath().
|
|
152
|
+
* @param {string} opts.routerSkillPath Absolute path to using-aw-skills SKILL.md.
|
|
153
|
+
* @param {string} [opts.escapeEnvVar] Default: 'AW_PROMPT_ROUTER_INJECT'.
|
|
154
|
+
* @returns {{ wrapperPath: string, backupPath: string }}
|
|
155
|
+
*/
|
|
156
|
+
export function installCodexPromptInjector(opts = {}) {
|
|
157
|
+
const hookPath = opts.hookPath ?? defaultHookPath();
|
|
158
|
+
const routerSkillPath = opts.routerSkillPath;
|
|
159
|
+
const escapeEnvVar = opts.escapeEnvVar ?? 'AW_PROMPT_ROUTER_INJECT';
|
|
160
|
+
|
|
161
|
+
if (!routerSkillPath || typeof routerSkillPath !== 'string') {
|
|
162
|
+
throw new Error('installCodexPromptInjector: routerSkillPath is required');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const backupPath = `${hookPath}.upstream`;
|
|
166
|
+
|
|
167
|
+
ensureDir(dirname(hookPath));
|
|
168
|
+
|
|
169
|
+
if (isExistingWrapper(hookPath)) {
|
|
170
|
+
// Idempotent no-op: leave wrapper + backup unchanged.
|
|
171
|
+
return { wrapperPath: hookPath, backupPath };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// First-run install: if the original hook exists and isn't already the
|
|
175
|
+
// wrapper, move it to .upstream. If neither exists, the wrapper still
|
|
176
|
+
// installs and the .upstream branch is just absent (wrapper handles it).
|
|
177
|
+
if (existsSync(hookPath) && !existsSync(backupPath)) {
|
|
178
|
+
renameSync(hookPath, backupPath);
|
|
179
|
+
try { chmodSync(backupPath, 0o755); } catch { /* best-effort */ }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const script = buildWrapperScript({ routerSkillPath, escapeEnvVar });
|
|
183
|
+
writeFileSync(hookPath, script, { mode: 0o755 });
|
|
184
|
+
try { chmodSync(hookPath, 0o755); } catch { /* best-effort */ }
|
|
185
|
+
|
|
186
|
+
return { wrapperPath: hookPath, backupPath };
|
|
187
|
+
}
|