@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.
Files changed (164) hide show
  1. package/CHANGELOG.md +338 -0
  2. package/LICENSE +21 -0
  3. package/README.md +52 -0
  4. package/dist/derived.d.ts +5 -3
  5. package/dist/derived.d.ts.map +1 -1
  6. package/dist/derived.js +11 -7
  7. package/dist/derived.js.map +1 -1
  8. package/dist/index.d.ts +4 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +3 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/providers/acp/index.d.ts +1 -1
  13. package/dist/providers/acp/index.d.ts.map +1 -1
  14. package/dist/providers/acp/index.js +5 -97
  15. package/dist/providers/acp/index.js.map +1 -1
  16. package/dist/providers/acp/session.d.ts +8 -1
  17. package/dist/providers/acp/session.d.ts.map +1 -1
  18. package/dist/providers/acp/session.js +94 -0
  19. package/dist/providers/acp/session.js.map +1 -1
  20. package/dist/providers/claude/attach.d.ts +8 -0
  21. package/dist/providers/claude/attach.d.ts.map +1 -0
  22. package/dist/providers/claude/attach.js +113 -0
  23. package/dist/providers/claude/attach.js.map +1 -0
  24. package/dist/providers/claude/goal-capability.d.ts +15 -0
  25. package/dist/providers/claude/goal-capability.d.ts.map +1 -0
  26. package/dist/providers/claude/goal-capability.js +20 -0
  27. package/dist/providers/claude/goal-capability.js.map +1 -0
  28. package/dist/providers/claude/index.d.ts.map +1 -1
  29. package/dist/providers/claude/index.js +8 -4
  30. package/dist/providers/claude/index.js.map +1 -1
  31. package/dist/providers/claude/session.d.ts +11 -9
  32. package/dist/providers/claude/session.d.ts.map +1 -1
  33. package/dist/providers/claude/session.js +29 -14
  34. package/dist/providers/claude/session.js.map +1 -1
  35. package/dist/providers/codex/attach.d.ts +9 -0
  36. package/dist/providers/codex/attach.d.ts.map +1 -0
  37. package/dist/providers/codex/attach.js +93 -0
  38. package/dist/providers/codex/attach.js.map +1 -0
  39. package/dist/providers/codex/goal-capability.d.ts +13 -0
  40. package/dist/providers/codex/goal-capability.d.ts.map +1 -0
  41. package/dist/providers/codex/goal-capability.js +18 -0
  42. package/dist/providers/codex/goal-capability.js.map +1 -0
  43. package/dist/providers/codex/index.d.ts +1 -0
  44. package/dist/providers/codex/index.d.ts.map +1 -1
  45. package/dist/providers/codex/index.js +9 -6
  46. package/dist/providers/codex/index.js.map +1 -1
  47. package/dist/providers/codex/session.d.ts +11 -7
  48. package/dist/providers/codex/session.d.ts.map +1 -1
  49. package/dist/providers/codex/session.js +24 -12
  50. package/dist/providers/codex/session.js.map +1 -1
  51. package/dist/providers/codex/transcript-normalize.d.ts +28 -0
  52. package/dist/providers/codex/transcript-normalize.d.ts.map +1 -0
  53. package/dist/providers/codex/transcript-normalize.js +191 -0
  54. package/dist/providers/codex/transcript-normalize.js.map +1 -0
  55. package/dist/providers/cursor/index.d.ts.map +1 -1
  56. package/dist/providers/cursor/index.js +2 -2
  57. package/dist/providers/cursor/index.js.map +1 -1
  58. package/dist/providers/openclaw/index.d.ts.map +1 -1
  59. package/dist/providers/openclaw/index.js +2 -2
  60. package/dist/providers/openclaw/index.js.map +1 -1
  61. package/dist/providers/opencode/index.d.ts.map +1 -1
  62. package/dist/providers/opencode/index.js +3 -5
  63. package/dist/providers/opencode/index.js.map +1 -1
  64. package/dist/providers/pi/index.d.ts.map +1 -1
  65. package/dist/providers/pi/index.js +3 -5
  66. package/dist/providers/pi/index.js.map +1 -1
  67. package/dist/providers/process/index.d.ts.map +1 -1
  68. package/dist/providers/process/index.js +2 -2
  69. package/dist/providers/process/index.js.map +1 -1
  70. package/dist/registry.d.ts +0 -1
  71. package/dist/registry.d.ts.map +1 -1
  72. package/dist/registry.js +0 -4
  73. package/dist/registry.js.map +1 -1
  74. package/dist/sessions/index.d.ts +3 -0
  75. package/dist/sessions/index.d.ts.map +1 -0
  76. package/dist/sessions/index.js +2 -0
  77. package/dist/sessions/index.js.map +1 -0
  78. package/dist/sessions/record.d.ts +43 -0
  79. package/dist/sessions/record.d.ts.map +1 -0
  80. package/dist/sessions/record.js +85 -0
  81. package/dist/sessions/record.js.map +1 -0
  82. package/dist/types.d.ts +119 -0
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/types.js.map +1 -1
  85. package/dist/utils/uuid.d.ts +7 -1
  86. package/dist/utils/uuid.d.ts.map +1 -1
  87. package/dist/utils/uuid.js +21 -1
  88. package/dist/utils/uuid.js.map +1 -1
  89. package/package.json +64 -7
  90. package/src/derived.ts +311 -0
  91. package/src/goals/controller.ts +442 -0
  92. package/src/goals/index.ts +21 -0
  93. package/src/goals/normalize.ts +173 -0
  94. package/src/goals/sentinel.ts +90 -0
  95. package/src/index.ts +270 -0
  96. package/src/providers/_shared/http-agent.ts +304 -0
  97. package/src/providers/acp/index.ts +103 -0
  98. package/src/providers/acp/parse.ts +131 -0
  99. package/src/providers/acp/session.ts +744 -0
  100. package/src/providers/claude/attach.ts +147 -0
  101. package/src/providers/claude/codec.ts +43 -0
  102. package/src/providers/claude/execute.ts +300 -0
  103. package/src/providers/claude/goal-capability.ts +21 -0
  104. package/src/providers/claude/index.ts +72 -0
  105. package/src/providers/claude/mcp.ts +82 -0
  106. package/src/providers/claude/parse.ts +824 -0
  107. package/src/providers/claude/session.ts +1192 -0
  108. package/src/providers/claude/transcript.ts +555 -0
  109. package/src/providers/codex/attach.ts +123 -0
  110. package/src/providers/codex/codec.ts +50 -0
  111. package/src/providers/codex/execute.ts +337 -0
  112. package/src/providers/codex/goal-capability.ts +19 -0
  113. package/src/providers/codex/index.ts +57 -0
  114. package/src/providers/codex/modes.ts +159 -0
  115. package/src/providers/codex/parse.ts +691 -0
  116. package/src/providers/codex/plan-mode.ts +49 -0
  117. package/src/providers/codex/session.ts +1287 -0
  118. package/src/providers/codex/transcript-normalize.ts +197 -0
  119. package/src/providers/codex/transcript.ts +487 -0
  120. package/src/providers/codex/usage-scanner.ts +178 -0
  121. package/src/providers/copilot/index.ts +19 -0
  122. package/src/providers/cursor/codec.ts +44 -0
  123. package/src/providers/cursor/execute.ts +271 -0
  124. package/src/providers/cursor/index.ts +25 -0
  125. package/src/providers/cursor/parse.ts +288 -0
  126. package/src/providers/gemini/index.ts +21 -0
  127. package/src/providers/openclaw/codec.ts +40 -0
  128. package/src/providers/openclaw/execute.ts +19 -0
  129. package/src/providers/openclaw/index.ts +29 -0
  130. package/src/providers/opencode/codec.ts +50 -0
  131. package/src/providers/opencode/event-parse.ts +141 -0
  132. package/src/providers/opencode/execute.ts +251 -0
  133. package/src/providers/opencode/http-session.ts +427 -0
  134. package/src/providers/opencode/index.ts +30 -0
  135. package/src/providers/opencode/parse.ts +203 -0
  136. package/src/providers/opencode/server.ts +0 -0
  137. package/src/providers/pi/codec.ts +44 -0
  138. package/src/providers/pi/execute.ts +297 -0
  139. package/src/providers/pi/index.ts +30 -0
  140. package/src/providers/pi/parse.ts +231 -0
  141. package/src/providers/pi/session.ts +381 -0
  142. package/src/providers/process/execute.ts +148 -0
  143. package/src/providers/process/index.ts +52 -0
  144. package/src/registry.ts +40 -0
  145. package/src/sessions/index.ts +8 -0
  146. package/src/sessions/record.ts +108 -0
  147. package/src/types.ts +1638 -0
  148. package/src/utils/ask-user-question.ts +57 -0
  149. package/src/utils/auth.ts +661 -0
  150. package/src/utils/binary.ts +179 -0
  151. package/src/utils/endpoint.ts +172 -0
  152. package/src/utils/env.ts +63 -0
  153. package/src/utils/execute-all.ts +68 -0
  154. package/src/utils/exit-plan-mode.ts +40 -0
  155. package/src/utils/instructions.ts +427 -0
  156. package/src/utils/process.ts +223 -0
  157. package/src/utils/runtime-config.ts +100 -0
  158. package/src/utils/runtime-homes.ts +49 -0
  159. package/src/utils/skill-commands.ts +493 -0
  160. package/src/utils/skills.ts +500 -0
  161. package/src/utils/template.ts +16 -0
  162. package/src/utils/tool-names.ts +51 -0
  163. package/src/utils/uuid.ts +21 -0
  164. 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
+ }
@@ -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
+ }