axexec 1.0.0 → 1.1.1

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 (37) hide show
  1. package/README.md +21 -0
  2. package/dist/agents/claude-code/types.d.ts +2 -2
  3. package/dist/agents/codex/adapter.js +8 -13
  4. package/dist/agents/gemini/types.d.ts +4 -4
  5. package/dist/build-agent-environment.d.ts +2 -0
  6. package/dist/build-agent-environment.js +19 -0
  7. package/dist/build-execution-metadata.d.ts +10 -0
  8. package/dist/build-execution-metadata.js +27 -0
  9. package/dist/build-permissions-config.d.ts +24 -0
  10. package/dist/build-permissions-config.js +48 -0
  11. package/dist/credentials/credentials.d.ts +39 -0
  12. package/dist/credentials/credentials.js +73 -0
  13. package/dist/credentials/get-credential-environment.d.ts +7 -5
  14. package/dist/credentials/get-credential-environment.js +44 -14
  15. package/dist/credentials/get-environment-trimmed.d.ts +5 -0
  16. package/dist/credentials/get-environment-trimmed.js +8 -0
  17. package/dist/credentials/install-credentials.d.ts +7 -6
  18. package/dist/credentials/install-credentials.js +26 -16
  19. package/dist/credentials/resolve-string-field.d.ts +7 -0
  20. package/dist/credentials/resolve-string-field.js +21 -0
  21. package/dist/credentials/types.d.ts +1 -10
  22. package/dist/credentials/write-agent-credentials.d.ts +28 -6
  23. package/dist/credentials/write-agent-credentials.js +88 -43
  24. package/dist/execute-agent.d.ts +12 -0
  25. package/dist/execute-agent.js +68 -0
  26. package/dist/index.d.ts +16 -0
  27. package/dist/index.js +16 -0
  28. package/dist/parse-credentials.d.ts +17 -3
  29. package/dist/parse-credentials.js +146 -27
  30. package/dist/resolve-run-diagnostics.d.ts +9 -0
  31. package/dist/resolve-run-diagnostics.js +35 -0
  32. package/dist/run-agent.d.ts +4 -20
  33. package/dist/run-agent.js +67 -109
  34. package/dist/stream-agent.js +5 -15
  35. package/dist/types/run-result.d.ts +81 -0
  36. package/dist/types/run-result.js +4 -0
  37. package/package.json +10 -2
@@ -1,15 +1,6 @@
1
1
  /**
2
2
  * Credential types for axexec.
3
3
  */
4
- /** Raw credentials that can be installed for any agent */
5
- interface RawCredentials {
6
- /** OAuth credentials (access token, refresh token, etc.) */
7
- oauth?: Record<string, unknown>;
8
- /** API key for direct API access */
9
- apiKey?: string;
10
- /** GitHub token for Copilot */
11
- githubToken?: string;
12
- }
13
4
  /** Result of credential installation */
14
5
  interface InstallResult {
15
6
  /** Environment variables to pass to the agent subprocess */
@@ -21,4 +12,4 @@ interface InstallResult {
21
12
  /** Base temporary directory (for cleanup) */
22
13
  baseDirectory: string;
23
14
  }
24
- export type { InstallResult, RawCredentials };
15
+ export type { InstallResult };
@@ -2,35 +2,57 @@
2
2
  * Per-agent credential file writers.
3
3
  *
4
4
  * Each agent has its own credential file format. These functions
5
- * write credentials in the agent-specific format.
5
+ * write credentials in the agent-specific format, extracting what
6
+ * they need from the canonical Credentials type.
6
7
  */
7
- import type { RawCredentials } from "./types.js";
8
+ import type { Credentials } from "./credentials.js";
9
+ type WarningWriter = (message: string) => void;
8
10
  /**
9
11
  * Installs Claude credentials to a config directory.
10
12
  *
11
13
  * File: .credentials.json
12
14
  * Format: { claudeAiOauth: { ... } }
15
+ *
16
+ * Handles:
17
+ * - oauth-credentials: Full OAuth data written to file
18
+ * - oauth-token: Not written (env var only)
19
+ * - api-key: Not written (env var only)
13
20
  */
14
- declare function installClaudeCredentials(configDirectory: string, credentials: RawCredentials): void;
21
+ declare function installClaudeCredentials(configDirectory: string, credentials: Credentials): void;
15
22
  /**
16
23
  * Installs Codex credentials to a config directory.
17
24
  *
18
25
  * File: auth.json
19
26
  * Format: { OPENAI_API_KEY: "..." } or { tokens: { ... }, last_refresh: "..." }
27
+ *
28
+ * Handles:
29
+ * - api-key: Written as { OPENAI_API_KEY: "..." }
30
+ * - oauth-credentials: Written as { tokens: { ... }, last_refresh: "..." }
20
31
  */
21
- declare function installCodexCredentials(configDirectory: string, credentials: RawCredentials): void;
32
+ declare function installCodexCredentials(configDirectory: string, credentials: Credentials, warn?: WarningWriter): void;
22
33
  /**
23
34
  * Installs Gemini credentials to a config directory.
24
35
  *
25
36
  * File: oauth_creds.json
26
37
  * Format: { access_token: "...", refresh_token: "...", ... }
38
+ *
39
+ * Handles:
40
+ * - oauth-credentials: Full OAuth data written to file
41
+ * - api-key: Not written (env var only)
27
42
  */
28
- declare function installGeminiCredentials(configDirectory: string, credentials: RawCredentials): void;
43
+ declare function installGeminiCredentials(configDirectory: string, credentials: Credentials): void;
29
44
  /**
30
45
  * Installs OpenCode credentials to a data directory.
31
46
  *
32
47
  * File: auth.json
33
48
  * Format: { anthropic: { type: "oauth", ... }, openai: { type: "api", ... } }
49
+ *
50
+ * OpenCode is a multi-provider agent. The Credentials.provider field
51
+ * specifies which provider (anthropic, openai, etc.) the credential is for.
52
+ *
53
+ * Handles:
54
+ * - oauth-credentials: Written as { [provider]: { type: "oauth", ...data } }
55
+ * - api-key: Written as { [provider]: { type: "api", key: "..." } }
34
56
  */
35
- declare function installOpenCodeCredentials(dataDirectory: string, credentials: RawCredentials): void;
57
+ declare function installOpenCodeCredentials(dataDirectory: string, credentials: Credentials, warn?: WarningWriter): void;
36
58
  export { installClaudeCredentials, installCodexCredentials, installGeminiCredentials, installOpenCodeCredentials, };
@@ -2,45 +2,74 @@
2
2
  * Per-agent credential file writers.
3
3
  *
4
4
  * Each agent has its own credential file format. These functions
5
- * write credentials in the agent-specific format.
5
+ * write credentials in the agent-specific format, extracting what
6
+ * they need from the canonical Credentials type.
6
7
  */
7
8
  import path from "node:path";
9
+ import { resolveStringField } from "./resolve-string-field.js";
8
10
  import { saveJsonFile } from "./save-json-file.js";
11
+ function normalizeOpenCodeProvider(provider) {
12
+ if (!provider)
13
+ return "anthropic";
14
+ return provider === "gemini" ? "google" : provider;
15
+ }
16
+ function defaultWarningWriter(message) {
17
+ const formatted = message.endsWith("\n") ? message : `${message}\n`;
18
+ process.stderr.write(formatted);
19
+ }
9
20
  /**
10
21
  * Installs Claude credentials to a config directory.
11
22
  *
12
23
  * File: .credentials.json
13
24
  * Format: { claudeAiOauth: { ... } }
25
+ *
26
+ * Handles:
27
+ * - oauth-credentials: Full OAuth data written to file
28
+ * - oauth-token: Not written (env var only)
29
+ * - api-key: Not written (env var only)
14
30
  */
15
31
  function installClaudeCredentials(configDirectory, credentials) {
16
- if (!credentials.oauth && !credentials.apiKey) {
32
+ // Only oauth-credentials need file installation
33
+ if (credentials.type !== "oauth-credentials") {
17
34
  return;
18
35
  }
19
36
  const credentialsPath = path.join(configDirectory, ".credentials.json");
20
- if (credentials.oauth) {
21
- saveJsonFile(credentialsPath, { claudeAiOauth: credentials.oauth });
22
- }
23
- // API key is passed via environment variable, not file
37
+ saveJsonFile(credentialsPath, { claudeAiOauth: credentials.data });
24
38
  }
25
39
  /**
26
40
  * Installs Codex credentials to a config directory.
27
41
  *
28
42
  * File: auth.json
29
43
  * Format: { OPENAI_API_KEY: "..." } or { tokens: { ... }, last_refresh: "..." }
44
+ *
45
+ * Handles:
46
+ * - api-key: Written as { OPENAI_API_KEY: "..." }
47
+ * - oauth-credentials: Written as { tokens: { ... }, last_refresh: "..." }
30
48
  */
31
- function installCodexCredentials(configDirectory, credentials) {
32
- if (!credentials.oauth && !credentials.apiKey) {
33
- return;
34
- }
49
+ function installCodexCredentials(configDirectory, credentials, warn = defaultWarningWriter) {
35
50
  const authPath = path.join(configDirectory, "auth.json");
36
- if (credentials.apiKey) {
37
- saveJsonFile(authPath, { OPENAI_API_KEY: credentials.apiKey });
38
- }
39
- else if (credentials.oauth) {
40
- saveJsonFile(authPath, {
41
- tokens: credentials.oauth,
42
- last_refresh: new Date().toISOString(),
43
- });
51
+ switch (credentials.type) {
52
+ case "api-key": {
53
+ const apiKey = resolveStringField(credentials.data, "apiKey", "key");
54
+ if (!apiKey) {
55
+ warn("Warning: installCodexCredentials: no API key found in credentials.data. auth.json was not written.");
56
+ break;
57
+ }
58
+ saveJsonFile(authPath, { OPENAI_API_KEY: apiKey });
59
+ break;
60
+ }
61
+ case "oauth-credentials": {
62
+ saveJsonFile(authPath, {
63
+ tokens: credentials.data,
64
+ last_refresh: new Date().toISOString(),
65
+ });
66
+ break;
67
+ }
68
+ case "oauth-token": {
69
+ // OAuth tokens are not applicable for Codex (uses API key only)
70
+ warn("Warning: installCodexCredentials: oauth-token credentials are not applicable for Codex. No authentication was configured.");
71
+ break;
72
+ }
44
73
  }
45
74
  }
46
75
  /**
@@ -48,43 +77,59 @@ function installCodexCredentials(configDirectory, credentials) {
48
77
  *
49
78
  * File: oauth_creds.json
50
79
  * Format: { access_token: "...", refresh_token: "...", ... }
80
+ *
81
+ * Handles:
82
+ * - oauth-credentials: Full OAuth data written to file
83
+ * - api-key: Not written (env var only)
51
84
  */
52
85
  function installGeminiCredentials(configDirectory, credentials) {
53
- if (!credentials.oauth && !credentials.apiKey) {
86
+ // Only oauth-credentials need file installation
87
+ if (credentials.type !== "oauth-credentials") {
54
88
  return;
55
89
  }
56
- if (credentials.oauth) {
57
- const credentialsPath = path.join(configDirectory, "oauth_creds.json");
58
- saveJsonFile(credentialsPath, credentials.oauth);
59
- }
60
- // API key is passed via environment variable, not file
90
+ const credentialsPath = path.join(configDirectory, "oauth_creds.json");
91
+ saveJsonFile(credentialsPath, credentials.data);
61
92
  }
62
93
  /**
63
94
  * Installs OpenCode credentials to a data directory.
64
95
  *
65
96
  * File: auth.json
66
97
  * Format: { anthropic: { type: "oauth", ... }, openai: { type: "api", ... } }
98
+ *
99
+ * OpenCode is a multi-provider agent. The Credentials.provider field
100
+ * specifies which provider (anthropic, openai, etc.) the credential is for.
101
+ *
102
+ * Handles:
103
+ * - oauth-credentials: Written as { [provider]: { type: "oauth", ...data } }
104
+ * - api-key: Written as { [provider]: { type: "api", key: "..." } }
67
105
  */
68
- function installOpenCodeCredentials(dataDirectory, credentials) {
69
- if (!credentials.oauth && !credentials.apiKey) {
70
- return;
71
- }
106
+ function installOpenCodeCredentials(dataDirectory, credentials, warn = defaultWarningWriter) {
72
107
  const authPath = path.join(dataDirectory, "auth.json");
73
- const authData = {};
74
- if (credentials.oauth) {
75
- authData.anthropic = {
76
- type: "oauth",
77
- ...credentials.oauth,
78
- };
79
- }
80
- if (credentials.apiKey) {
81
- authData.anthropic = {
82
- type: "api",
83
- key: credentials.apiKey,
84
- };
85
- }
86
- if (Object.keys(authData).length > 0) {
87
- saveJsonFile(authPath, authData);
108
+ const provider = normalizeOpenCodeProvider(credentials.provider);
109
+ switch (credentials.type) {
110
+ case "oauth-credentials": {
111
+ saveJsonFile(authPath, {
112
+ [provider]: {
113
+ type: "oauth",
114
+ ...credentials.data,
115
+ },
116
+ });
117
+ break;
118
+ }
119
+ case "api-key": {
120
+ const apiKey = resolveStringField(credentials.data, "apiKey", "key");
121
+ if (!apiKey) {
122
+ warn(`Warning: installOpenCodeCredentials: no API key found in credentials.data for provider "${provider}". auth.json was not written.`);
123
+ break;
124
+ }
125
+ saveJsonFile(authPath, {
126
+ [provider]: {
127
+ type: "api",
128
+ key: apiKey,
129
+ },
130
+ });
131
+ break;
132
+ }
88
133
  }
89
134
  }
90
135
  // Note: Copilot uses environment variables (GITHUB_TOKEN), not file-based credentials.
@@ -0,0 +1,12 @@
1
+ import type { AgentAdapter } from "./types/adapter.js";
2
+ import type { AgentCli } from "./types/events.js";
3
+ import type { RunAgentOptions, RunResult } from "./types/run-result.js";
4
+ import type { Credentials } from "./credentials/credentials.js";
5
+ import type { installCredentials } from "./credentials/install-credentials.js";
6
+ import type { DiagnosticWriter } from "./resolve-run-diagnostics.js";
7
+ type CredentialInstallResult = Awaited<ReturnType<typeof installCredentials>>;
8
+ /**
9
+ * Executes agent and streams events.
10
+ */
11
+ declare function executeAgent(agentId: AgentCli, adapter: AgentAdapter, options: RunAgentOptions, configEnvironment: Record<string, string>, credentialResult: CredentialInstallResult | undefined, credentials: Credentials | undefined, preserveConfigDirectory: boolean, emitError: DiagnosticWriter): Promise<RunResult>;
12
+ export { executeAgent };
@@ -0,0 +1,68 @@
1
+ import { isStreamableAdapter } from "./types/adapter.js";
2
+ import { cleanupCredentials } from "./credentials/install-credentials.js";
3
+ import { streamAgent } from "./stream-agent.js";
4
+ import { resolveOutputMode } from "./resolve-output-mode.js";
5
+ import { createEventWriter } from "./write-event.js";
6
+ import { DependencyError } from "./resolve-binary.js";
7
+ import { buildExecutionMetadata } from "./build-execution-metadata.js";
8
+ /**
9
+ * Executes agent and streams events.
10
+ */
11
+ async function executeAgent(agentId, adapter, options, configEnvironment, credentialResult, credentials, preserveConfigDirectory, emitError) {
12
+ const runOptions = {
13
+ prompt: options.prompt,
14
+ verbose: options.verbose ?? false,
15
+ model: options.model,
16
+ rawLogPath: options.rawLog,
17
+ debug: options.debug,
18
+ configEnv: configEnvironment,
19
+ preserveGithubSha: options.preserveGithubSha,
20
+ };
21
+ const outputMode = resolveOutputMode(options.format, process.stdout.isTTY);
22
+ const writeEvent = createEventWriter(outputMode);
23
+ const events = [];
24
+ // Build execution metadata (same for success and error cases)
25
+ const execution = buildExecutionMetadata(agentId, options, credentialResult, credentials, preserveConfigDirectory);
26
+ try {
27
+ const eventStream = isStreamableAdapter(adapter)
28
+ ? adapter.streamSession(runOptions)
29
+ : streamAgent(adapter, runOptions);
30
+ for await (const event of eventStream) {
31
+ events.push(event);
32
+ writeEvent(event);
33
+ }
34
+ const success = adapter.isSuccess(events);
35
+ return { events, success, execution };
36
+ }
37
+ catch (error) {
38
+ if (error instanceof DependencyError) {
39
+ emitError(error.message);
40
+ const errorEvent = {
41
+ type: "session.error",
42
+ code: "DEPENDENCY_ERROR",
43
+ message: error.message,
44
+ timestamp: Date.now(),
45
+ };
46
+ events.push(errorEvent);
47
+ writeEvent(errorEvent);
48
+ return { events, success: false, execution };
49
+ }
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ const errorEvent = {
52
+ type: "session.error",
53
+ code: "SPAWN_ERROR",
54
+ message,
55
+ timestamp: Date.now(),
56
+ };
57
+ events.push(errorEvent);
58
+ writeEvent(errorEvent);
59
+ return { events, success: false, execution };
60
+ }
61
+ finally {
62
+ // Clean up temp directory unless caller wants to preserve it
63
+ if (credentialResult && !preserveConfigDirectory) {
64
+ await cleanupCredentials(credentialResult.baseDirectory);
65
+ }
66
+ }
67
+ }
68
+ export { executeAgent };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * axexec - Unified execution engine for AI coding agents.
3
+ *
4
+ * Programmatic API for running agents with credential isolation,
5
+ * config generation, and normalized event streaming.
6
+ */
7
+ import "./agents/claude-code/adapter.js";
8
+ import "./agents/codex/adapter.js";
9
+ import "./agents/gemini/adapter.js";
10
+ import "./agents/opencode/adapter.js";
11
+ import "./agents/copilot/adapter.js";
12
+ export { runAgent } from "./run-agent.js";
13
+ export { cleanupCredentials } from "./credentials/install-credentials.js";
14
+ export type { ExecutionCredentials, ExecutionDirectories, ExecutionMetadata, RunAgentDiagnostics, RunAgentOptions, RunResult, } from "./types/run-result.js";
15
+ export type { AxexecEvent, AgentCli } from "./types/events.js";
16
+ export type { Credentials } from "./credentials/credentials.js";
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * axexec - Unified execution engine for AI coding agents.
3
+ *
4
+ * Programmatic API for running agents with credential isolation,
5
+ * config generation, and normalized event streaming.
6
+ */
7
+ // Import all adapters to trigger self-registration
8
+ import "./agents/claude-code/adapter.js";
9
+ import "./agents/codex/adapter.js";
10
+ import "./agents/gemini/adapter.js";
11
+ import "./agents/opencode/adapter.js";
12
+ import "./agents/copilot/adapter.js";
13
+ // Main entry point
14
+ export { runAgent } from "./run-agent.js";
15
+ // Cleanup utility for use with preserveConfigDirectory option
16
+ export { cleanupCredentials } from "./credentials/install-credentials.js";
@@ -1,13 +1,27 @@
1
1
  /**
2
2
  * Credential parsing from environment variables.
3
+ *
4
+ * Used by axexec CLI to read credentials from environment when
5
+ * not provided via the programmatic API.
3
6
  */
4
- import type { RawCredentials } from "./credentials/install-credentials.js";
7
+ import type { AgentCli } from "axshared";
8
+ import { type Credentials } from "./credentials/credentials.js";
9
+ type ParseCredentialsResult = {
10
+ ok: true;
11
+ credentials: Credentials | undefined;
12
+ } | {
13
+ ok: false;
14
+ message: string;
15
+ exitCode: number;
16
+ };
5
17
  /**
6
18
  * Parses credentials from environment variables.
7
19
  *
8
20
  * Environment variables checked (in order):
9
- * - AX_<AGENT>_CREDENTIALS: JSON with credentials
21
+ * - AX_<AGENT>_CREDENTIALS: JSON with full Credentials object
10
22
  * - Standard env vars: ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.
23
+ *
24
+ * Returns ok with undefined credentials if none are found.
11
25
  */
12
- declare function parseCredentialsFromEnvironment(agentId: string): RawCredentials;
26
+ declare function parseCredentialsFromEnvironment(agentId: AgentCli, model?: string): ParseCredentialsResult;
13
27
  export { parseCredentialsFromEnvironment };
@@ -1,63 +1,182 @@
1
1
  /**
2
2
  * Credential parsing from environment variables.
3
+ *
4
+ * Used by axexec CLI to read credentials from environment when
5
+ * not provided via the programmatic API.
3
6
  */
7
+ import { parseCredentials, } from "./credentials/credentials.js";
8
+ import { getEnvironmentTrimmed } from "./credentials/get-environment-trimmed.js";
4
9
  /**
5
10
  * Parses credentials from environment variables.
6
11
  *
7
12
  * Environment variables checked (in order):
8
- * - AX_<AGENT>_CREDENTIALS: JSON with credentials
13
+ * - AX_<AGENT>_CREDENTIALS: JSON with full Credentials object
9
14
  * - Standard env vars: ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.
15
+ *
16
+ * Returns ok with undefined credentials if none are found.
10
17
  */
11
- function parseCredentialsFromEnvironment(agentId) {
12
- const credentials = {};
13
- // Check for axexec credentials env var
18
+ function parseCredentialsFromEnvironment(agentId, model) {
19
+ // Check for axexec credentials env var (full Credentials JSON)
14
20
  const credEnvironmentVariable = `AX_${agentId.toUpperCase()}_CREDENTIALS`;
15
21
  const credEnvironmentValue = process.env[credEnvironmentVariable];
16
22
  if (credEnvironmentValue) {
17
23
  try {
18
24
  const parsed = JSON.parse(credEnvironmentValue);
19
- return parsed;
25
+ const parseResult = parseCredentials(parsed);
26
+ if (parseResult.ok) {
27
+ if (parseResult.credentials.agent !== agentId) {
28
+ return {
29
+ ok: false,
30
+ exitCode: 2,
31
+ message: `Error: ${credEnvironmentVariable} agent mismatch. ` +
32
+ `Expected ${agentId}, got ${parseResult.credentials.agent}.`,
33
+ };
34
+ }
35
+ return { ok: true, credentials: parseResult.credentials };
36
+ }
37
+ return {
38
+ ok: false,
39
+ exitCode: 2,
40
+ message: `Error: Invalid credentials format in ${credEnvironmentVariable}: ` +
41
+ parseResult.error,
42
+ };
20
43
  }
21
- catch {
22
- process.stderr.write(`Warning: Failed to parse ${credEnvironmentVariable}\n`);
44
+ catch (error) {
45
+ const message = error instanceof Error ? error.message : String(error);
46
+ return {
47
+ ok: false,
48
+ exitCode: 2,
49
+ message: `Error: Failed to parse ${credEnvironmentVariable}: ${message}`,
50
+ };
23
51
  }
24
52
  }
25
- // Fall back to standard env vars
53
+ // Fall back to standard API key env vars
54
+ return parseApiKeyFromEnvironment(agentId, model);
55
+ }
56
+ /**
57
+ * Parses API key from standard environment variables.
58
+ */
59
+ function parseApiKeyFromEnvironment(agentId, model) {
60
+ let apiKey;
26
61
  switch (agentId) {
27
62
  case "claude": {
28
- if (process.env["ANTHROPIC_API_KEY"]) {
29
- credentials.apiKey = process.env["ANTHROPIC_API_KEY"];
63
+ // Check for OAuth token first (higher priority)
64
+ const oauthToken = getEnvironmentTrimmed("CLAUDE_CODE_OAUTH_TOKEN");
65
+ if (oauthToken) {
66
+ return {
67
+ ok: true,
68
+ credentials: {
69
+ agent: agentId,
70
+ type: "oauth-token",
71
+ data: { oauthToken },
72
+ },
73
+ };
30
74
  }
75
+ apiKey = getEnvironmentTrimmed("ANTHROPIC_API_KEY");
31
76
  break;
32
77
  }
33
78
  case "codex": {
34
- if (process.env["OPENAI_API_KEY"]) {
35
- credentials.apiKey = process.env["OPENAI_API_KEY"];
36
- }
79
+ apiKey = getEnvironmentTrimmed("OPENAI_API_KEY");
37
80
  break;
38
81
  }
39
82
  case "gemini": {
40
- if (process.env["GEMINI_API_KEY"]) {
41
- credentials.apiKey = process.env["GEMINI_API_KEY"];
42
- }
83
+ apiKey = getEnvironmentTrimmed("GEMINI_API_KEY");
43
84
  break;
44
85
  }
45
86
  case "opencode": {
46
- if (process.env["ANTHROPIC_API_KEY"]) {
47
- credentials.apiKey = process.env["ANTHROPIC_API_KEY"];
48
- }
49
- break;
87
+ // OpenCode can use multiple providers
88
+ return parseOpenCodeApiKeyFromEnvironment(model);
50
89
  }
51
90
  case "copilot": {
52
- const token = process.env["GITHUB_TOKEN"] ??
53
- process.env["GH_TOKEN"] ??
54
- process.env["COPILOT_GITHUB_TOKEN"];
55
- if (token) {
56
- credentials.githubToken = token;
57
- }
91
+ apiKey =
92
+ getEnvironmentTrimmed("COPILOT_GITHUB_TOKEN") ??
93
+ getEnvironmentTrimmed("GH_TOKEN") ??
94
+ getEnvironmentTrimmed("GITHUB_TOKEN");
58
95
  break;
59
96
  }
60
97
  }
61
- return credentials;
98
+ if (!apiKey) {
99
+ return { ok: true, credentials: undefined };
100
+ }
101
+ return {
102
+ ok: true,
103
+ credentials: {
104
+ agent: agentId,
105
+ type: "api-key",
106
+ data: { apiKey },
107
+ },
108
+ };
109
+ }
110
+ function normalizeOpenCodeProvider(provider) {
111
+ if (!provider)
112
+ return undefined;
113
+ return provider === "gemini" ? "google" : provider;
114
+ }
115
+ function parseOpenCodeApiKeyFromEnvironment(model) {
116
+ const providers = [
117
+ {
118
+ provider: "anthropic",
119
+ envVar: "ANTHROPIC_API_KEY",
120
+ value: getEnvironmentTrimmed("ANTHROPIC_API_KEY"),
121
+ },
122
+ {
123
+ provider: "openai",
124
+ envVar: "OPENAI_API_KEY",
125
+ value: getEnvironmentTrimmed("OPENAI_API_KEY"),
126
+ },
127
+ {
128
+ provider: "google",
129
+ envVar: "GEMINI_API_KEY",
130
+ value: getEnvironmentTrimmed("GEMINI_API_KEY"),
131
+ },
132
+ ];
133
+ const available = providers.filter((entry) => entry.value !== undefined);
134
+ const modelProvider = normalizeOpenCodeProvider(model?.split("/")[0]);
135
+ if (modelProvider &&
136
+ ["anthropic", "openai", "google"].includes(modelProvider)) {
137
+ const match = providers.find((entry) => entry.provider === modelProvider);
138
+ if (match?.value) {
139
+ return {
140
+ ok: true,
141
+ credentials: {
142
+ agent: "opencode",
143
+ type: "api-key",
144
+ provider: modelProvider,
145
+ data: { apiKey: match.value },
146
+ },
147
+ };
148
+ }
149
+ const requiredEnvironmentVariable = match?.envVar ?? "AX_OPENCODE_CREDENTIALS";
150
+ return {
151
+ ok: false,
152
+ exitCode: 2,
153
+ message: `Error: ${requiredEnvironmentVariable} is required for OpenCode ` +
154
+ `model provider "${modelProvider}".`,
155
+ };
156
+ }
157
+ if (available.length === 0) {
158
+ return { ok: true, credentials: undefined };
159
+ }
160
+ if (available.length === 1) {
161
+ const match = available[0];
162
+ if (!match)
163
+ return { ok: true, credentials: undefined };
164
+ return {
165
+ ok: true,
166
+ credentials: {
167
+ agent: "opencode",
168
+ type: "api-key",
169
+ provider: match.provider,
170
+ data: { apiKey: match.value },
171
+ },
172
+ };
173
+ }
174
+ const environmentList = available.map((entry) => entry.envVar).join(", ");
175
+ return {
176
+ ok: false,
177
+ exitCode: 2,
178
+ message: "Error: Multiple OpenCode API keys detected. " +
179
+ `Set AX_OPENCODE_CREDENTIALS to disambiguate (${environmentList}).`,
180
+ };
62
181
  }
63
182
  export { parseCredentialsFromEnvironment };
@@ -0,0 +1,9 @@
1
+ import type { RunAgentOptions } from "./types/run-result.js";
2
+ type DiagnosticWriter = (message: string) => void;
3
+ declare function resolveDiagnostics(options: RunAgentOptions): {
4
+ error: DiagnosticWriter;
5
+ warn: DiagnosticWriter;
6
+ };
7
+ declare function resolveExitCodeSetter(options: RunAgentOptions): (code: number) => void;
8
+ export type { DiagnosticWriter };
9
+ export { resolveDiagnostics, resolveExitCodeSetter };
@@ -0,0 +1,35 @@
1
+ function formatDiagnosticMessage(message) {
2
+ return message.endsWith("\n") ? message : `${message}\n`;
3
+ }
4
+ function resolveDiagnostics(options) {
5
+ if (options.diagnostics) {
6
+ const diagnostics = options.diagnostics;
7
+ return {
8
+ error: (message) => {
9
+ diagnostics.error(formatDiagnosticMessage(message));
10
+ },
11
+ warn: (message) => {
12
+ diagnostics.warn(formatDiagnosticMessage(message));
13
+ },
14
+ };
15
+ }
16
+ return {
17
+ error: (message) => {
18
+ process.stderr.write(formatDiagnosticMessage(message));
19
+ },
20
+ warn: (message) => {
21
+ process.stderr.write(formatDiagnosticMessage(message));
22
+ },
23
+ };
24
+ }
25
+ function noopExitCodeSetter() { }
26
+ function setProcessExitCode(code) {
27
+ process.exitCode = code;
28
+ }
29
+ function resolveExitCodeSetter(options) {
30
+ if (options.setExitCode === false) {
31
+ return noopExitCodeSetter;
32
+ }
33
+ return setProcessExitCode;
34
+ }
35
+ export { resolveDiagnostics, resolveExitCodeSetter };