@agentex/agent 0.0.24 → 0.0.26
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/CHANGELOG.md +338 -0
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/derived.d.ts +5 -3
- package/dist/derived.d.ts.map +1 -1
- package/dist/derived.js +11 -7
- package/dist/derived.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/acp/index.d.ts +1 -1
- package/dist/providers/acp/index.d.ts.map +1 -1
- package/dist/providers/acp/index.js +5 -97
- package/dist/providers/acp/index.js.map +1 -1
- package/dist/providers/acp/session.d.ts +8 -1
- package/dist/providers/acp/session.d.ts.map +1 -1
- package/dist/providers/acp/session.js +94 -0
- package/dist/providers/acp/session.js.map +1 -1
- package/dist/providers/claude/attach.d.ts +8 -0
- package/dist/providers/claude/attach.d.ts.map +1 -0
- package/dist/providers/claude/attach.js +113 -0
- package/dist/providers/claude/attach.js.map +1 -0
- package/dist/providers/claude/goal-capability.d.ts +15 -0
- package/dist/providers/claude/goal-capability.d.ts.map +1 -0
- package/dist/providers/claude/goal-capability.js +20 -0
- package/dist/providers/claude/goal-capability.js.map +1 -0
- package/dist/providers/claude/index.d.ts.map +1 -1
- package/dist/providers/claude/index.js +8 -4
- package/dist/providers/claude/index.js.map +1 -1
- package/dist/providers/claude/session.d.ts +11 -9
- package/dist/providers/claude/session.d.ts.map +1 -1
- package/dist/providers/claude/session.js +29 -14
- package/dist/providers/claude/session.js.map +1 -1
- package/dist/providers/codex/attach.d.ts +9 -0
- package/dist/providers/codex/attach.d.ts.map +1 -0
- package/dist/providers/codex/attach.js +93 -0
- package/dist/providers/codex/attach.js.map +1 -0
- package/dist/providers/codex/goal-capability.d.ts +13 -0
- package/dist/providers/codex/goal-capability.d.ts.map +1 -0
- package/dist/providers/codex/goal-capability.js +18 -0
- package/dist/providers/codex/goal-capability.js.map +1 -0
- package/dist/providers/codex/index.d.ts +1 -0
- package/dist/providers/codex/index.d.ts.map +1 -1
- package/dist/providers/codex/index.js +9 -6
- package/dist/providers/codex/index.js.map +1 -1
- package/dist/providers/codex/session.d.ts +11 -7
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +24 -12
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/providers/codex/transcript-normalize.d.ts +28 -0
- package/dist/providers/codex/transcript-normalize.d.ts.map +1 -0
- package/dist/providers/codex/transcript-normalize.js +191 -0
- package/dist/providers/codex/transcript-normalize.js.map +1 -0
- package/dist/providers/cursor/index.d.ts.map +1 -1
- package/dist/providers/cursor/index.js +2 -2
- package/dist/providers/cursor/index.js.map +1 -1
- package/dist/providers/openclaw/index.d.ts.map +1 -1
- package/dist/providers/openclaw/index.js +2 -2
- package/dist/providers/openclaw/index.js.map +1 -1
- package/dist/providers/opencode/index.d.ts.map +1 -1
- package/dist/providers/opencode/index.js +3 -5
- package/dist/providers/opencode/index.js.map +1 -1
- package/dist/providers/pi/index.d.ts.map +1 -1
- package/dist/providers/pi/index.js +3 -5
- package/dist/providers/pi/index.js.map +1 -1
- package/dist/providers/process/index.d.ts.map +1 -1
- package/dist/providers/process/index.js +2 -2
- package/dist/providers/process/index.js.map +1 -1
- package/dist/registry.d.ts +0 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +0 -4
- package/dist/registry.js.map +1 -1
- package/dist/sessions/index.d.ts +3 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/sessions/record.d.ts +43 -0
- package/dist/sessions/record.d.ts.map +1 -0
- package/dist/sessions/record.js +85 -0
- package/dist/sessions/record.js.map +1 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/uuid.d.ts +7 -1
- package/dist/utils/uuid.d.ts.map +1 -1
- package/dist/utils/uuid.js +21 -1
- package/dist/utils/uuid.js.map +1 -1
- package/package.json +64 -7
- package/src/derived.ts +311 -0
- package/src/goals/controller.ts +442 -0
- package/src/goals/index.ts +21 -0
- package/src/goals/normalize.ts +173 -0
- package/src/goals/sentinel.ts +90 -0
- package/src/index.ts +270 -0
- package/src/providers/_shared/http-agent.ts +304 -0
- package/src/providers/acp/index.ts +103 -0
- package/src/providers/acp/parse.ts +131 -0
- package/src/providers/acp/session.ts +744 -0
- package/src/providers/claude/attach.ts +147 -0
- package/src/providers/claude/codec.ts +43 -0
- package/src/providers/claude/execute.ts +300 -0
- package/src/providers/claude/goal-capability.ts +21 -0
- package/src/providers/claude/index.ts +72 -0
- package/src/providers/claude/mcp.ts +82 -0
- package/src/providers/claude/parse.ts +824 -0
- package/src/providers/claude/session.ts +1192 -0
- package/src/providers/claude/transcript.ts +555 -0
- package/src/providers/codex/attach.ts +123 -0
- package/src/providers/codex/codec.ts +50 -0
- package/src/providers/codex/execute.ts +337 -0
- package/src/providers/codex/goal-capability.ts +19 -0
- package/src/providers/codex/index.ts +57 -0
- package/src/providers/codex/modes.ts +159 -0
- package/src/providers/codex/parse.ts +691 -0
- package/src/providers/codex/plan-mode.ts +49 -0
- package/src/providers/codex/session.ts +1287 -0
- package/src/providers/codex/transcript-normalize.ts +197 -0
- package/src/providers/codex/transcript.ts +487 -0
- package/src/providers/codex/usage-scanner.ts +178 -0
- package/src/providers/copilot/index.ts +19 -0
- package/src/providers/cursor/codec.ts +44 -0
- package/src/providers/cursor/execute.ts +271 -0
- package/src/providers/cursor/index.ts +25 -0
- package/src/providers/cursor/parse.ts +288 -0
- package/src/providers/gemini/index.ts +21 -0
- package/src/providers/openclaw/codec.ts +40 -0
- package/src/providers/openclaw/execute.ts +19 -0
- package/src/providers/openclaw/index.ts +29 -0
- package/src/providers/opencode/codec.ts +50 -0
- package/src/providers/opencode/event-parse.ts +141 -0
- package/src/providers/opencode/execute.ts +251 -0
- package/src/providers/opencode/http-session.ts +427 -0
- package/src/providers/opencode/index.ts +30 -0
- package/src/providers/opencode/parse.ts +203 -0
- package/src/providers/opencode/server.ts +0 -0
- package/src/providers/pi/codec.ts +44 -0
- package/src/providers/pi/execute.ts +297 -0
- package/src/providers/pi/index.ts +30 -0
- package/src/providers/pi/parse.ts +231 -0
- package/src/providers/pi/session.ts +381 -0
- package/src/providers/process/execute.ts +148 -0
- package/src/providers/process/index.ts +52 -0
- package/src/registry.ts +40 -0
- package/src/sessions/index.ts +8 -0
- package/src/sessions/record.ts +108 -0
- package/src/types.ts +1638 -0
- package/src/utils/ask-user-question.ts +57 -0
- package/src/utils/auth.ts +661 -0
- package/src/utils/binary.ts +179 -0
- package/src/utils/endpoint.ts +172 -0
- package/src/utils/env.ts +63 -0
- package/src/utils/execute-all.ts +68 -0
- package/src/utils/exit-plan-mode.ts +40 -0
- package/src/utils/instructions.ts +427 -0
- package/src/utils/process.ts +223 -0
- package/src/utils/runtime-config.ts +100 -0
- package/src/utils/runtime-homes.ts +49 -0
- package/src/utils/skill-commands.ts +493 -0
- package/src/utils/skills.ts +500 -0
- package/src/utils/template.ts +16 -0
- package/src/utils/tool-names.ts +51 -0
- package/src/utils/uuid.ts +21 -0
- package/src/utils/workspace.ts +156 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
export interface ResolvedBinary {
|
|
7
|
+
bin: string;
|
|
8
|
+
prefixArgs: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const cache = new Map<string, ResolvedBinary>();
|
|
12
|
+
|
|
13
|
+
const COMMON_PATHS_UNIX: Record<string, string[]> = {
|
|
14
|
+
claude: [
|
|
15
|
+
path.join(os.homedir(), ".local", "bin", "claude"),
|
|
16
|
+
"/usr/local/bin/claude",
|
|
17
|
+
path.join(os.homedir(), ".npm-global", "bin", "claude"),
|
|
18
|
+
],
|
|
19
|
+
codex: [
|
|
20
|
+
path.join(os.homedir(), ".local", "bin", "codex"),
|
|
21
|
+
"/usr/local/bin/codex",
|
|
22
|
+
path.join(os.homedir(), ".npm-global", "bin", "codex"),
|
|
23
|
+
],
|
|
24
|
+
gemini: [
|
|
25
|
+
path.join(os.homedir(), ".local", "bin", "gemini"),
|
|
26
|
+
"/usr/local/bin/gemini",
|
|
27
|
+
path.join(os.homedir(), ".npm-global", "bin", "gemini"),
|
|
28
|
+
],
|
|
29
|
+
agent: [
|
|
30
|
+
path.join(os.homedir(), ".local", "bin", "agent"),
|
|
31
|
+
"/usr/local/bin/agent",
|
|
32
|
+
path.join(os.homedir(), ".npm-global", "bin", "agent"),
|
|
33
|
+
],
|
|
34
|
+
opencode: [
|
|
35
|
+
path.join(os.homedir(), ".local", "bin", "opencode"),
|
|
36
|
+
"/usr/local/bin/opencode",
|
|
37
|
+
path.join(os.homedir(), ".npm-global", "bin", "opencode"),
|
|
38
|
+
],
|
|
39
|
+
pi: [
|
|
40
|
+
path.join(os.homedir(), ".local", "bin", "pi"),
|
|
41
|
+
"/usr/local/bin/pi",
|
|
42
|
+
path.join(os.homedir(), ".npm-global", "bin", "pi"),
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function getCommonPathsWindows(name: string): string[] {
|
|
47
|
+
const appData = process.env["APPDATA"] ?? "";
|
|
48
|
+
const localAppData = process.env["LOCALAPPDATA"] ?? "";
|
|
49
|
+
const userProfile = process.env["USERPROFILE"] ?? "";
|
|
50
|
+
return [
|
|
51
|
+
path.join(appData, "npm", `${name}.cmd`),
|
|
52
|
+
path.join(localAppData, "pnpm", `${name}.cmd`),
|
|
53
|
+
path.join(userProfile, ".local", "bin", `${name}.exe`),
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function fileExists(p: string): Promise<boolean> {
|
|
58
|
+
try {
|
|
59
|
+
await fs.access(p);
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function runWhich(name: string): Promise<string | null> {
|
|
67
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const child = execFile(cmd, [name], { timeout: 5000 }, (err, stdout) => {
|
|
70
|
+
if (err) {
|
|
71
|
+
resolve(null);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const result = stdout.trim().split("\n")[0]?.trim();
|
|
75
|
+
resolve(result ?? null);
|
|
76
|
+
});
|
|
77
|
+
child.on("error", () => resolve(null));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function resolveWindowsCmdShim(cmdPath: string): Promise<ResolvedBinary | null> {
|
|
82
|
+
try {
|
|
83
|
+
const content = await fs.readFile(cmdPath, "utf-8");
|
|
84
|
+
// Match patterns like: "%dp0%\node_modules\...\.js" or "%~dp0\...\entry.js"
|
|
85
|
+
const match = content.match(/%(?:~dp0|dp0)[\\\/]([^\s"]+\.js)/i);
|
|
86
|
+
if (match?.[1]) {
|
|
87
|
+
const dir = path.dirname(cmdPath);
|
|
88
|
+
const jsPath = path.resolve(dir, match[1]);
|
|
89
|
+
if (await fileExists(jsPath)) {
|
|
90
|
+
return { bin: "node", prefixArgs: [jsPath] };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// Not a .cmd shim or can't read
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function findBinary(name: string, configOverride?: string): Promise<ResolvedBinary> {
|
|
100
|
+
// 1. Config override
|
|
101
|
+
if (configOverride) {
|
|
102
|
+
if (await fileExists(configOverride)) {
|
|
103
|
+
return { bin: configOverride, prefixArgs: [] };
|
|
104
|
+
}
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Configured binary path "${configOverride}" does not exist. ` +
|
|
107
|
+
`Verify the path is correct.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check cache
|
|
112
|
+
const cached = cache.get(name);
|
|
113
|
+
if (cached) return cached;
|
|
114
|
+
|
|
115
|
+
// 2. Platform-specific common paths
|
|
116
|
+
const isWin = process.platform === "win32";
|
|
117
|
+
const commonPaths = isWin
|
|
118
|
+
? getCommonPathsWindows(name)
|
|
119
|
+
: (COMMON_PATHS_UNIX[name] ?? []);
|
|
120
|
+
|
|
121
|
+
for (const p of commonPaths) {
|
|
122
|
+
if (await fileExists(p)) {
|
|
123
|
+
if (isWin && p.endsWith(".cmd")) {
|
|
124
|
+
const resolved = await resolveWindowsCmdShim(p);
|
|
125
|
+
if (resolved) {
|
|
126
|
+
cache.set(name, resolved);
|
|
127
|
+
return resolved;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const result: ResolvedBinary = { bin: p, prefixArgs: [] };
|
|
131
|
+
cache.set(name, result);
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 3. PATH via which/where
|
|
137
|
+
const whichResult = await runWhich(name);
|
|
138
|
+
if (whichResult) {
|
|
139
|
+
if (isWin && whichResult.endsWith(".cmd")) {
|
|
140
|
+
const resolved = await resolveWindowsCmdShim(whichResult);
|
|
141
|
+
if (resolved) {
|
|
142
|
+
cache.set(name, resolved);
|
|
143
|
+
return resolved;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const result: ResolvedBinary = { bin: whichResult, prefixArgs: [] };
|
|
147
|
+
cache.set(name, result);
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const installHints: Record<string, string> = {
|
|
152
|
+
claude: "Install: npm install -g @anthropic-ai/claude-code",
|
|
153
|
+
codex: "Install: npm install -g @openai/codex",
|
|
154
|
+
gemini: "Install: npm install -g @anthropic-ai/claude-code (Gemini CLI)",
|
|
155
|
+
agent: "Install the Cursor CLI agent",
|
|
156
|
+
opencode: "Install: npm install -g opencode-ai",
|
|
157
|
+
pi: "Install: npm install -g @mariozechner/pi-coding-agent",
|
|
158
|
+
};
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Could not find "${name}" binary. Searched common install paths and PATH. ` +
|
|
161
|
+
(installHints[name] ?? `Ensure "${name}" is installed and on your PATH.`)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function ensureCommandResolvable(command: string): Promise<ResolvedBinary> {
|
|
166
|
+
// If it's a path (contains separator), check existence directly
|
|
167
|
+
if (command.includes(path.sep) || command.includes("/")) {
|
|
168
|
+
if (await fileExists(command)) {
|
|
169
|
+
return { bin: command, prefixArgs: [] };
|
|
170
|
+
}
|
|
171
|
+
throw new Error(`Command not found: "${command}"`);
|
|
172
|
+
}
|
|
173
|
+
// Otherwise treat as a binary name
|
|
174
|
+
return findBinary(command);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function clearBinaryCache(): void {
|
|
178
|
+
cache.clear();
|
|
179
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { ProviderEndpointConfig } from "../types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Result of translating a `ProviderEndpointConfig` into what a specific
|
|
5
|
+
* provider's spawned CLI understands. Since there is no shared wire format
|
|
6
|
+
* across CLIs, each provider gets its own translation:
|
|
7
|
+
* - `env` is merged into the child process environment.
|
|
8
|
+
* - `args` is appended to the child argv (codex `-c` config overrides).
|
|
9
|
+
* - `unset` names env vars to REMOVE from the child env after merging. Env
|
|
10
|
+
* alone can't express deletion, and ambient provider credentials seeded by
|
|
11
|
+
* `buildEnv` (e.g. a real `ANTHROPIC_API_KEY` from the host `process.env`)
|
|
12
|
+
* must not leak to a third-party `baseUrl`.
|
|
13
|
+
*
|
|
14
|
+
* Apply order at every call site: `Object.assign(env, tx.env)` then
|
|
15
|
+
* `for (const k of tx.unset) delete env[k]`.
|
|
16
|
+
*/
|
|
17
|
+
export interface EndpointTranslation {
|
|
18
|
+
env: Record<string, string>;
|
|
19
|
+
args: string[];
|
|
20
|
+
unset: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Fresh empty translation. Not a shared singleton — callers may mutate. */
|
|
24
|
+
function empty(): EndpointTranslation {
|
|
25
|
+
return { env: {}, args: [], unset: [] };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Stable id of the Codex `model_provider` synthesized for a custom endpoint.
|
|
30
|
+
* Exposed so advanced Codex knobs can be set via `config.extraArgs` against a
|
|
31
|
+
* known name, e.g. `-c model_providers.custom.query_params.api-version="..."`.
|
|
32
|
+
*/
|
|
33
|
+
export const CODEX_CUSTOM_PROVIDER_ID = "custom";
|
|
34
|
+
|
|
35
|
+
/** Env var Codex reads the custom-endpoint credential from (its `env_key`). */
|
|
36
|
+
export const CODEX_CUSTOM_KEY_ENV = "CODEX_CUSTOM_API_KEY";
|
|
37
|
+
|
|
38
|
+
/** Prefix for the per-header env vars Codex reads via `env_http_headers`. */
|
|
39
|
+
export const CODEX_CUSTOM_HEADER_ENV_PREFIX = "CODEX_CUSTOM_HEADER_";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Ambient Anthropic alternate-routing env (Bedrock/Vertex/Foundry) that must be
|
|
43
|
+
* cleared when a custom `baseUrl` is set — otherwise Claude Code could be steered
|
|
44
|
+
* to one of those instead of the endpoint. General AWS/cloud creds are left
|
|
45
|
+
* intact (the agent may legitimately need them for tool calls).
|
|
46
|
+
*/
|
|
47
|
+
const CLAUDE_ALT_ROUTING_ENV = [
|
|
48
|
+
"ANTHROPIC_BEDROCK_BASE_URL",
|
|
49
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
50
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
51
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Translate a custom-endpoint config for a provider. Returns `env`/`args`/`unset`
|
|
56
|
+
* to apply at spawn time. Providers without a custom-endpoint mechanism return
|
|
57
|
+
* an empty translation (the config is silently ignored, like `allowedTools`
|
|
58
|
+
* on providers without argv tool filtering).
|
|
59
|
+
*/
|
|
60
|
+
export function translateEndpoint(
|
|
61
|
+
providerType: string,
|
|
62
|
+
endpoint: ProviderEndpointConfig | undefined,
|
|
63
|
+
): EndpointTranslation {
|
|
64
|
+
if (!endpoint) return empty();
|
|
65
|
+
switch (providerType) {
|
|
66
|
+
case "claude":
|
|
67
|
+
return translateClaudeEndpoint(endpoint);
|
|
68
|
+
case "codex":
|
|
69
|
+
return translateCodexEndpoint(endpoint);
|
|
70
|
+
default:
|
|
71
|
+
return empty();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Claude Code routes to any Anthropic Messages-API-compatible endpoint purely
|
|
77
|
+
* through env vars, so the translation is env-only (no argv).
|
|
78
|
+
*
|
|
79
|
+
* Credential hygiene: `buildEnv` seeds `ANTHROPIC_API_KEY` from the host's
|
|
80
|
+
* `process.env`. When we point at a custom `baseUrl`, only the auth declared
|
|
81
|
+
* here may reach that third party — so we `unset` any ambient Anthropic
|
|
82
|
+
* credential we didn't choose. Without a custom `baseUrl` (still real
|
|
83
|
+
* Anthropic) we only enforce the "exactly one auth" invariant.
|
|
84
|
+
*/
|
|
85
|
+
function translateClaudeEndpoint(e: ProviderEndpointConfig): EndpointTranslation {
|
|
86
|
+
const env: Record<string, string> = {};
|
|
87
|
+
const unset: string[] = [];
|
|
88
|
+
if (e.baseUrl) env["ANTHROPIC_BASE_URL"] = e.baseUrl;
|
|
89
|
+
// authToken (Authorization: Bearer) wins over apiKey (x-api-key) if both are
|
|
90
|
+
// set. Whichever is NOT chosen is removed from the child env, ambient or not.
|
|
91
|
+
if (e.authToken) {
|
|
92
|
+
env["ANTHROPIC_AUTH_TOKEN"] = e.authToken;
|
|
93
|
+
unset.push("ANTHROPIC_API_KEY");
|
|
94
|
+
} else if (e.apiKey) {
|
|
95
|
+
env["ANTHROPIC_API_KEY"] = e.apiKey;
|
|
96
|
+
unset.push("ANTHROPIC_AUTH_TOKEN");
|
|
97
|
+
} else if (e.baseUrl) {
|
|
98
|
+
// Custom endpoint with no declared auth: don't forward ambient Anthropic
|
|
99
|
+
// credentials to it. The caller must pass auth explicitly via the endpoint.
|
|
100
|
+
unset.push("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN");
|
|
101
|
+
}
|
|
102
|
+
// A custom baseUrl is authoritative — clear ambient alternate-routing config
|
|
103
|
+
// (Bedrock/Vertex/Foundry) so Claude Code targets the endpoint, not those.
|
|
104
|
+
if (e.baseUrl) unset.push(...CLAUDE_ALT_ROUTING_ENV);
|
|
105
|
+
if (e.headers && Object.keys(e.headers).length > 0) {
|
|
106
|
+
// ANTHROPIC_CUSTOM_HEADERS is newline-separated `Name: Value` pairs.
|
|
107
|
+
env["ANTHROPIC_CUSTOM_HEADERS"] = Object.entries(e.headers)
|
|
108
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
109
|
+
.join("\n");
|
|
110
|
+
}
|
|
111
|
+
if (e.modelMap) {
|
|
112
|
+
// Tier aliases → concrete ids on the endpoint. Lets alias callers
|
|
113
|
+
// (`model: "sonnet"`) resolve correctly against a non-Anthropic endpoint.
|
|
114
|
+
if (e.modelMap.opus) env["ANTHROPIC_DEFAULT_OPUS_MODEL"] = e.modelMap.opus;
|
|
115
|
+
if (e.modelMap.sonnet) env["ANTHROPIC_DEFAULT_SONNET_MODEL"] = e.modelMap.sonnet;
|
|
116
|
+
if (e.modelMap.haiku) env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = e.modelMap.haiku;
|
|
117
|
+
if (e.modelMap.fable) env["ANTHROPIC_DEFAULT_FABLE_MODEL"] = e.modelMap.fable;
|
|
118
|
+
}
|
|
119
|
+
return { env, args: [], unset };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Codex has no base-URL env var — custom endpoints are a `[model_providers.<id>]`
|
|
124
|
+
* config block (base_url, wire_api, env_key) selected by `model_provider`. We
|
|
125
|
+
* synthesize that block via `-c` overrides (the same mechanism already used for
|
|
126
|
+
* `model_reasoning_effort`) and inject the key into env under `env_key`.
|
|
127
|
+
*
|
|
128
|
+
* `wire_api` is `"responses"`: Codex removed the Chat Completions ("chat") wire
|
|
129
|
+
* protocol in Feb 2026, so a custom provider must speak the OpenAI Responses
|
|
130
|
+
* API (directly or via a translating gateway). `modelMap` is ignored — Codex
|
|
131
|
+
* has no tier aliases, so pass a concrete `config.model`. Requires `baseUrl`;
|
|
132
|
+
* without it there is no provider to define, so the translation is empty.
|
|
133
|
+
*
|
|
134
|
+
* Codex needs no `unset`: ambient `OPENAI_API_KEY` is the DEFAULT provider's
|
|
135
|
+
* `env_key`, and we select the synthesized `custom` provider whose `env_key`
|
|
136
|
+
* is `CODEX_CUSTOM_KEY_ENV`, so the ambient key never reaches the endpoint.
|
|
137
|
+
*/
|
|
138
|
+
function translateCodexEndpoint(e: ProviderEndpointConfig): EndpointTranslation {
|
|
139
|
+
if (!e.baseUrl) return empty();
|
|
140
|
+
const env: Record<string, string> = {};
|
|
141
|
+
const args: string[] = [];
|
|
142
|
+
const id = CODEX_CUSTOM_PROVIDER_ID;
|
|
143
|
+
const set = (key: string, value: string) => {
|
|
144
|
+
// Match the existing `-c key=<json-value>` convention (see codex effort).
|
|
145
|
+
args.push("-c", `${key}=${JSON.stringify(value)}`);
|
|
146
|
+
};
|
|
147
|
+
set("model_provider", id);
|
|
148
|
+
set(`model_providers.${id}.name`, "Custom");
|
|
149
|
+
set(`model_providers.${id}.base_url`, e.baseUrl);
|
|
150
|
+
set(`model_providers.${id}.wire_api`, "responses");
|
|
151
|
+
const key = e.authToken ?? e.apiKey;
|
|
152
|
+
if (key) {
|
|
153
|
+
env[CODEX_CUSTOM_KEY_ENV] = key;
|
|
154
|
+
set(`model_providers.${id}.env_key`, CODEX_CUSTOM_KEY_ENV);
|
|
155
|
+
}
|
|
156
|
+
if (e.headers) {
|
|
157
|
+
// Route header VALUES through env via `env_http_headers` (header → env-var
|
|
158
|
+
// name), never argv: argv is world-readable via `ps` and a header can carry
|
|
159
|
+
// a secret (Authorization, X-API-Key). Mirrors why the claude provider
|
|
160
|
+
// stages MCP headers to a 0600 file instead of the command line. Header
|
|
161
|
+
// names are assumed TOML bare keys (letters/digits/`-`/`_`), which real HTTP
|
|
162
|
+
// header tokens are — a name with a `.` would nest as a sub-table.
|
|
163
|
+
let i = 0;
|
|
164
|
+
for (const [name, value] of Object.entries(e.headers)) {
|
|
165
|
+
const headerEnv = `${CODEX_CUSTOM_HEADER_ENV_PREFIX}${i++}`;
|
|
166
|
+
env[headerEnv] = value;
|
|
167
|
+
set(`model_providers.${id}.env_http_headers.${name}`, headerEnv);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// modelMap intentionally ignored — Codex has no tier aliases.
|
|
171
|
+
return { env, args, unset: [] };
|
|
172
|
+
}
|
package/src/utils/env.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
|
|
3
|
+
const BASE_ALLOW_LIST = [
|
|
4
|
+
"PATH", "HOME", "TMPDIR", "TEMP", "TMP", "USER", "LOGNAME", "LANG", "LC_ALL",
|
|
5
|
+
// Windows essentials
|
|
6
|
+
"SystemRoot", "WINDIR", "COMSPEC", "PATHEXT", "APPDATA", "LOCALAPPDATA", "USERPROFILE",
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
/** Auth env vars that providers need for API-key auth and billing detection. */
|
|
10
|
+
const AUTH_ALLOW_LIST = [
|
|
11
|
+
// Claude / Anthropic
|
|
12
|
+
"ANTHROPIC_API_KEY", "ANTHROPIC_BEDROCK_BASE_URL",
|
|
13
|
+
// AWS (Bedrock)
|
|
14
|
+
"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_REGION", "AWS_PROFILE",
|
|
15
|
+
// Codex / OpenAI
|
|
16
|
+
"OPENAI_API_KEY",
|
|
17
|
+
// Gemini / Google
|
|
18
|
+
"GEMINI_API_KEY", "GOOGLE_API_KEY",
|
|
19
|
+
// Cursor
|
|
20
|
+
"CURSOR_API_KEY",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// HEADER covers custom-endpoint header carriers — `ANTHROPIC_CUSTOM_HEADERS` and
|
|
24
|
+
// the generated `CODEX_CUSTOM_HEADER_*` vars — whose values can hold secret
|
|
25
|
+
// header tokens (Authorization, X-API-Key). Over-redacting a non-secret header
|
|
26
|
+
// in logs is the safe direction.
|
|
27
|
+
const SENSITIVE_PATTERNS = /KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH|HEADER/i;
|
|
28
|
+
|
|
29
|
+
const ESSENTIAL_PATHS_UNIX = ["/usr/local/bin", "/usr/bin", "/bin"];
|
|
30
|
+
|
|
31
|
+
export function buildEnv(callerEnv?: Record<string, string>): Record<string, string> {
|
|
32
|
+
const env: Record<string, string> = {};
|
|
33
|
+
for (const key of BASE_ALLOW_LIST) {
|
|
34
|
+
const val = process.env[key];
|
|
35
|
+
if (val !== undefined) env[key] = val;
|
|
36
|
+
}
|
|
37
|
+
for (const key of AUTH_ALLOW_LIST) {
|
|
38
|
+
const val = process.env[key];
|
|
39
|
+
if (val !== undefined) env[key] = val;
|
|
40
|
+
}
|
|
41
|
+
if (callerEnv) Object.assign(env, callerEnv);
|
|
42
|
+
return env;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function redactEnvForLogs(env: Record<string, string>): Record<string, string> {
|
|
46
|
+
const redacted: Record<string, string> = {};
|
|
47
|
+
for (const [key, value] of Object.entries(env)) {
|
|
48
|
+
redacted[key] = SENSITIVE_PATTERNS.test(key) ? "[REDACTED]" : value;
|
|
49
|
+
}
|
|
50
|
+
return redacted;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function ensurePathInEnv(env: Record<string, string>): void {
|
|
54
|
+
const isWin = process.platform === "win32";
|
|
55
|
+
const sep = isWin ? ";" : ":";
|
|
56
|
+
const essentials = isWin ? [] : ESSENTIAL_PATHS_UNIX;
|
|
57
|
+
const current = env["PATH"] ?? env["Path"] ?? "";
|
|
58
|
+
const parts = current.split(sep);
|
|
59
|
+
for (const p of essentials) {
|
|
60
|
+
if (!parts.includes(p)) parts.push(p);
|
|
61
|
+
}
|
|
62
|
+
env["PATH"] = parts.join(sep);
|
|
63
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ExecutionContext, ExecutionResult, ProviderModule } from "../types.js";
|
|
2
|
+
import { getProvider } from "../registry.js";
|
|
3
|
+
|
|
4
|
+
export interface ExecuteAllOptions {
|
|
5
|
+
/** Abort all remaining executions if any one fails. Default: false. */
|
|
6
|
+
cancelOnFailure?: boolean;
|
|
7
|
+
/** External signal to cancel all executions. */
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ExecuteAllTask {
|
|
12
|
+
provider: string | ProviderModule;
|
|
13
|
+
ctx: ExecutionContext;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Run multiple agent executions concurrently with shared cancellation.
|
|
18
|
+
*
|
|
19
|
+
* Returns results in the same order as the input tasks. If `cancelOnFailure`
|
|
20
|
+
* is true, remaining tasks are aborted as soon as any task returns a
|
|
21
|
+
* non-completed status.
|
|
22
|
+
*/
|
|
23
|
+
export async function executeAll(
|
|
24
|
+
tasks: ExecuteAllTask[],
|
|
25
|
+
options?: ExecuteAllOptions,
|
|
26
|
+
): Promise<ExecutionResult[]> {
|
|
27
|
+
if (tasks.length === 0) return [];
|
|
28
|
+
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
|
|
31
|
+
// Chain external signal
|
|
32
|
+
if (options?.signal) {
|
|
33
|
+
if (options.signal.aborted) {
|
|
34
|
+
controller.abort(options.signal.reason);
|
|
35
|
+
} else {
|
|
36
|
+
const onAbort = () => controller.abort(options.signal!.reason);
|
|
37
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
38
|
+
// Clean up listener when our controller aborts (avoid leak)
|
|
39
|
+
controller.signal.addEventListener("abort", () => {
|
|
40
|
+
options.signal!.removeEventListener("abort", onAbort);
|
|
41
|
+
}, { once: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const promises = tasks.map(async (task): Promise<ExecutionResult> => {
|
|
46
|
+
const provider = typeof task.provider === "string"
|
|
47
|
+
? getProvider(task.provider)
|
|
48
|
+
: task.provider;
|
|
49
|
+
|
|
50
|
+
// Merge signals: task's own signal + shared controller signal
|
|
51
|
+
const taskSignal = task.ctx.signal
|
|
52
|
+
? AbortSignal.any([task.ctx.signal, controller.signal])
|
|
53
|
+
: controller.signal;
|
|
54
|
+
|
|
55
|
+
const result = await provider.execute({
|
|
56
|
+
...task.ctx,
|
|
57
|
+
signal: taskSignal,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (options?.cancelOnFailure && result.status !== "completed") {
|
|
61
|
+
controller.abort("cancelOnFailure");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return result;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return Promise.all(promises);
|
|
68
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { UserInputRequest } from "../types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Structured payload for an `ExitPlanMode` permission request.
|
|
5
|
+
*
|
|
6
|
+
* The agent calls this when it has finished planning and wants the user to
|
|
7
|
+
* approve the plan. The host receives the request via `onUserInputRequest`
|
|
8
|
+
* and decides whether to exit plan mode (allow → the agent continues in the
|
|
9
|
+
* permission mode the user picked) or stay in plan mode (deny).
|
|
10
|
+
*/
|
|
11
|
+
export interface ExitPlanModeRequest {
|
|
12
|
+
plan: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse an `ExitPlanMode` permission request.
|
|
17
|
+
*
|
|
18
|
+
* Returns the structured plan payload if the request is for `ExitPlanMode`,
|
|
19
|
+
* or `null` if it's a different tool. The plan text only flows through the
|
|
20
|
+
* live SDK control_request — it is not persisted to the saved transcript —
|
|
21
|
+
* so callers should capture it here when they want to surface it to the user.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* onUserInputRequest: async (req) => {
|
|
26
|
+
* const plan = parseExitPlanMode(req);
|
|
27
|
+
* if (plan) {
|
|
28
|
+
* const approved = await showPlanApprovalUI(plan.plan);
|
|
29
|
+
* return { allow: approved };
|
|
30
|
+
* }
|
|
31
|
+
* return { allow: true };
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function parseExitPlanMode(req: UserInputRequest): ExitPlanModeRequest | null {
|
|
36
|
+
if (req.toolName !== "ExitPlanMode") return null;
|
|
37
|
+
const plan = (req.input as Record<string, unknown>)["plan"];
|
|
38
|
+
if (typeof plan !== "string") return null;
|
|
39
|
+
return { plan };
|
|
40
|
+
}
|