axexec 1.0.0

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 (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +196 -0
  3. package/bin/axexec +17 -0
  4. package/dist/agents/claude-code/adapter.d.ts +6 -0
  5. package/dist/agents/claude-code/adapter.js +71 -0
  6. package/dist/agents/claude-code/parse-event.d.ts +16 -0
  7. package/dist/agents/claude-code/parse-event.js +185 -0
  8. package/dist/agents/claude-code/types.d.ts +180 -0
  9. package/dist/agents/claude-code/types.js +112 -0
  10. package/dist/agents/codex/adapter.d.ts +6 -0
  11. package/dist/agents/codex/adapter.js +62 -0
  12. package/dist/agents/codex/item-parsers.d.ts +17 -0
  13. package/dist/agents/codex/item-parsers.js +126 -0
  14. package/dist/agents/codex/parse-event.d.ts +38 -0
  15. package/dist/agents/codex/parse-event.js +141 -0
  16. package/dist/agents/codex/types.d.ts +407 -0
  17. package/dist/agents/codex/types.js +188 -0
  18. package/dist/agents/copilot/adapter.d.ts +11 -0
  19. package/dist/agents/copilot/adapter.js +81 -0
  20. package/dist/agents/copilot/parse-event.d.ts +20 -0
  21. package/dist/agents/copilot/parse-event.js +137 -0
  22. package/dist/agents/copilot/stream-session.d.ts +10 -0
  23. package/dist/agents/copilot/stream-session.js +120 -0
  24. package/dist/agents/copilot/tail-file.d.ts +11 -0
  25. package/dist/agents/copilot/tail-file.js +52 -0
  26. package/dist/agents/copilot/transform-event.d.ts +19 -0
  27. package/dist/agents/copilot/transform-event.js +100 -0
  28. package/dist/agents/copilot/types.d.ts +320 -0
  29. package/dist/agents/copilot/types.js +220 -0
  30. package/dist/agents/copilot/watch-session.d.ts +26 -0
  31. package/dist/agents/copilot/watch-session.js +186 -0
  32. package/dist/agents/gemini/adapter.d.ts +6 -0
  33. package/dist/agents/gemini/adapter.js +78 -0
  34. package/dist/agents/gemini/parse-event.d.ts +18 -0
  35. package/dist/agents/gemini/parse-event.js +144 -0
  36. package/dist/agents/gemini/types.d.ts +162 -0
  37. package/dist/agents/gemini/types.js +103 -0
  38. package/dist/agents/opencode/adapter.d.ts +16 -0
  39. package/dist/agents/opencode/adapter.js +142 -0
  40. package/dist/agents/opencode/cleanup-session.d.ts +17 -0
  41. package/dist/agents/opencode/cleanup-session.js +41 -0
  42. package/dist/agents/opencode/create-session-start-event.d.ts +18 -0
  43. package/dist/agents/opencode/create-session-start-event.js +24 -0
  44. package/dist/agents/opencode/detect-empty-session.d.ts +25 -0
  45. package/dist/agents/opencode/detect-empty-session.js +42 -0
  46. package/dist/agents/opencode/map-error-to-event.d.ts +10 -0
  47. package/dist/agents/opencode/map-error-to-event.js +55 -0
  48. package/dist/agents/opencode/parse-message-part.d.ts +24 -0
  49. package/dist/agents/opencode/parse-message-part.js +112 -0
  50. package/dist/agents/opencode/parse-sse-event.d.ts +23 -0
  51. package/dist/agents/opencode/parse-sse-event.js +125 -0
  52. package/dist/agents/opencode/process-sse-events.d.ts +24 -0
  53. package/dist/agents/opencode/process-sse-events.js +66 -0
  54. package/dist/agents/opencode/server-types.d.ts +509 -0
  55. package/dist/agents/opencode/server-types.js +293 -0
  56. package/dist/agents/opencode/session-api.d.ts +56 -0
  57. package/dist/agents/opencode/session-api.js +151 -0
  58. package/dist/agents/opencode/spawn-server.d.ts +29 -0
  59. package/dist/agents/opencode/spawn-server.js +95 -0
  60. package/dist/agents/opencode/sse-client.d.ts +33 -0
  61. package/dist/agents/opencode/sse-client.js +145 -0
  62. package/dist/agents/registry.d.ts +15 -0
  63. package/dist/agents/registry.js +24 -0
  64. package/dist/cli.d.ts +15 -0
  65. package/dist/cli.js +119 -0
  66. package/dist/credentials/get-credential-environment.d.ts +13 -0
  67. package/dist/credentials/get-credential-environment.js +46 -0
  68. package/dist/credentials/install-credentials.d.ts +27 -0
  69. package/dist/credentials/install-credentials.js +102 -0
  70. package/dist/credentials/save-json-file.d.ts +11 -0
  71. package/dist/credentials/save-json-file.js +21 -0
  72. package/dist/credentials/types.d.ts +24 -0
  73. package/dist/credentials/types.js +4 -0
  74. package/dist/credentials/write-agent-credentials.d.ts +36 -0
  75. package/dist/credentials/write-agent-credentials.js +91 -0
  76. package/dist/determine-session-success.d.ts +15 -0
  77. package/dist/determine-session-success.js +25 -0
  78. package/dist/format-event-tsv.d.ts +23 -0
  79. package/dist/format-event-tsv.js +136 -0
  80. package/dist/parse-credentials.d.ts +13 -0
  81. package/dist/parse-credentials.js +63 -0
  82. package/dist/parse-iso-timestamp.d.ts +21 -0
  83. package/dist/parse-iso-timestamp.js +37 -0
  84. package/dist/resolve-binary.d.ts +29 -0
  85. package/dist/resolve-binary.js +46 -0
  86. package/dist/resolve-output-mode.d.ts +39 -0
  87. package/dist/resolve-output-mode.js +39 -0
  88. package/dist/run-agent.d.ts +32 -0
  89. package/dist/run-agent.js +146 -0
  90. package/dist/stream-agent.d.ts +20 -0
  91. package/dist/stream-agent.js +207 -0
  92. package/dist/types/adapter.d.ts +101 -0
  93. package/dist/types/adapter.js +14 -0
  94. package/dist/types/events.d.ts +82 -0
  95. package/dist/types/events.js +13 -0
  96. package/dist/types/options.d.ts +41 -0
  97. package/dist/types/options.js +4 -0
  98. package/dist/validate-agent.d.ts +20 -0
  99. package/dist/validate-agent.js +50 -0
  100. package/dist/write-event.d.ts +23 -0
  101. package/dist/write-event.js +43 -0
  102. package/package.json +79 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * SSE client for OpenCode server mode.
3
+ *
4
+ * Connects to the OpenCode server's /event endpoint and streams
5
+ * parsed SSE events.
6
+ */
7
+ import { OpenCodeSSEEvent as OpenCodeSSEEventSchema } from "./server-types.js";
8
+ /** Error when SSE connection fails */
9
+ class SSEConnectionError extends Error {
10
+ statusCode;
11
+ constructor(message, statusCode) {
12
+ super(message);
13
+ this.name = "SSEConnectionError";
14
+ this.statusCode = statusCode;
15
+ }
16
+ }
17
+ /**
18
+ * Connects to OpenCode server's SSE endpoint and yields events.
19
+ *
20
+ * The generator will yield events until:
21
+ * - The connection is closed by the server
22
+ * - The abort signal is triggered
23
+ * - An error occurs
24
+ */
25
+ async function* streamSSE(options) {
26
+ const url = `${options.baseUrl}/event`;
27
+ const response = await fetch(url, {
28
+ headers: {
29
+ Accept: "text/event-stream",
30
+ "Cache-Control": "no-cache",
31
+ "x-opencode-directory": options.directory,
32
+ },
33
+ signal: options.signal,
34
+ });
35
+ if (!response.ok) {
36
+ throw new SSEConnectionError(`SSE connection failed: ${response.status} ${response.statusText}`, response.status);
37
+ }
38
+ if (!response.body) {
39
+ throw new SSEConnectionError("SSE response has no body");
40
+ }
41
+ const reader = response.body.getReader();
42
+ const decoder = new TextDecoder();
43
+ let buffer = "";
44
+ // Expose reader for manual cancellation
45
+ options.onReader?.(reader);
46
+ // Create a single abort promise that rejects when abort signal fires
47
+ // This is more efficient than creating a new promise on each iteration
48
+ const abortPromise = new Promise((_, reject) => {
49
+ if (options.signal?.aborted) {
50
+ reject(new Error("Aborted"));
51
+ return;
52
+ }
53
+ options.signal?.addEventListener("abort", () => {
54
+ reject(new Error("Aborted"));
55
+ }, { once: true });
56
+ });
57
+ try {
58
+ for (;;) {
59
+ // Check abort signal before reading
60
+ if (options.signal?.aborted) {
61
+ break;
62
+ }
63
+ // Race the read against the abort signal
64
+ let result;
65
+ try {
66
+ result = await Promise.race([reader.read(), abortPromise]);
67
+ }
68
+ catch {
69
+ // Aborted
70
+ break;
71
+ }
72
+ if (result.done) {
73
+ break;
74
+ }
75
+ buffer += decoder.decode(result.value, { stream: true });
76
+ // Normalize CRLF to LF (SSE spec allows both line ending styles)
77
+ buffer = buffer.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
78
+ // Process complete SSE messages (separated by double newline)
79
+ const messages = buffer.split("\n\n");
80
+ buffer = messages.pop() ?? ""; // Keep incomplete message in buffer
81
+ for (const message of messages) {
82
+ // Check abort signal before yielding
83
+ if (options.signal?.aborted) {
84
+ return;
85
+ }
86
+ const event = parseSSEMessage(message);
87
+ if (event) {
88
+ yield event;
89
+ }
90
+ }
91
+ }
92
+ // Process any remaining buffer (only if not aborted)
93
+ if (!options.signal?.aborted && buffer.trim()) {
94
+ const event = parseSSEMessage(buffer);
95
+ if (event) {
96
+ yield event;
97
+ }
98
+ }
99
+ }
100
+ finally {
101
+ // Cancel any pending read
102
+ reader.cancel().catch(() => { });
103
+ reader.releaseLock();
104
+ }
105
+ }
106
+ /**
107
+ * Parses a single SSE message into an OpenCode event.
108
+ *
109
+ * SSE format:
110
+ * ```
111
+ * data: {"type": "...", "properties": {...}}
112
+ * ```
113
+ */
114
+ function parseSSEMessage(message) {
115
+ const lines = message.split("\n");
116
+ const dataLines = [];
117
+ for (const line of lines) {
118
+ if (line.startsWith("data: ")) {
119
+ dataLines.push(line.slice(6));
120
+ }
121
+ else if (line.startsWith("data:")) {
122
+ dataLines.push(line.slice(5));
123
+ }
124
+ // Ignore other SSE fields (event:, id:, retry:)
125
+ }
126
+ // Per SSE spec, multiple data lines are joined with newlines
127
+ const data = dataLines.join("\n");
128
+ if (!data) {
129
+ return undefined;
130
+ }
131
+ try {
132
+ const json = JSON.parse(data);
133
+ const result = OpenCodeSSEEventSchema.safeParse(json);
134
+ if (result.success) {
135
+ return result.data;
136
+ }
137
+ // Unknown event type - log for debugging but don't fail
138
+ return undefined;
139
+ }
140
+ catch {
141
+ // Invalid JSON - skip
142
+ return undefined;
143
+ }
144
+ }
145
+ export { SSEConnectionError, streamSSE };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Adapter registry for agent lookup and registration.
3
+ *
4
+ * Adapters self-register on import, allowing new agents to be added
5
+ * without modifying core code.
6
+ */
7
+ import type { AgentAdapter, AgentInfo } from "../types/adapter.js";
8
+ import type { AgentCli } from "../types/events.js";
9
+ /** Registers an adapter in the registry */
10
+ declare function registerAdapter(adapter: AgentAdapter): void;
11
+ /** Retrieves an adapter by ID, or undefined if not found */
12
+ declare function getAdapter(id: AgentCli): AgentAdapter | undefined;
13
+ /** Returns metadata for all registered adapters */
14
+ declare function listAdapters(): AgentInfo[];
15
+ export { getAdapter, listAdapters, registerAdapter };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Adapter registry for agent lookup and registration.
3
+ *
4
+ * Adapters self-register on import, allowing new agents to be added
5
+ * without modifying core code.
6
+ */
7
+ const adapters = new Map();
8
+ /** Registers an adapter in the registry */
9
+ function registerAdapter(adapter) {
10
+ const { id } = adapter.info();
11
+ if (adapters.has(id)) {
12
+ throw new Error(`Adapter already registered: ${id}`);
13
+ }
14
+ adapters.set(id, adapter);
15
+ }
16
+ /** Retrieves an adapter by ID, or undefined if not found */
17
+ function getAdapter(id) {
18
+ return adapters.get(id);
19
+ }
20
+ /** Returns metadata for all registered adapters */
21
+ function listAdapters() {
22
+ return [...adapters.values()].map((adapter) => adapter.info());
23
+ }
24
+ export { getAdapter, listAdapters, registerAdapter };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * axexec CLI - Unified agent execution with isolation.
4
+ *
5
+ * Executes AI coding agents with complete environment isolation:
6
+ * - Credentials written to temp directory
7
+ * - Config/permissions written to temp directory
8
+ * - Agent pointed at temp directory via env vars
9
+ * - Normalized event stream output
10
+ */
11
+ import "./agents/claude-code/adapter.js";
12
+ import "./agents/codex/adapter.js";
13
+ import "./agents/gemini/adapter.js";
14
+ import "./agents/opencode/adapter.js";
15
+ import "./agents/copilot/adapter.js";
package/dist/cli.js ADDED
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * axexec CLI - Unified agent execution with isolation.
4
+ *
5
+ * Executes AI coding agents with complete environment isolation:
6
+ * - Credentials written to temp directory
7
+ * - Config/permissions written to temp directory
8
+ * - Agent pointed at temp directory via env vars
9
+ * - Normalized event stream output
10
+ */
11
+ import { Command } from "@commander-js/extra-typings";
12
+ import packageJson from "../package.json" with { type: "json" };
13
+ // Import all adapters to trigger self-registration
14
+ import "./agents/claude-code/adapter.js";
15
+ import "./agents/codex/adapter.js";
16
+ import "./agents/gemini/adapter.js";
17
+ import "./agents/opencode/adapter.js";
18
+ import "./agents/copilot/adapter.js";
19
+ import { listAdapters } from "./agents/registry.js";
20
+ import { runAgent } from "./run-agent.js";
21
+ async function readStdin() {
22
+ let data = "";
23
+ for await (const chunk of process.stdin) {
24
+ data += typeof chunk === "string" ? chunk : chunk.toString("utf8");
25
+ }
26
+ return data.trimEnd();
27
+ }
28
+ const program = new Command()
29
+ .name(packageJson.name)
30
+ .description(packageJson.description)
31
+ .version(packageJson.version, "-V, --version")
32
+ .showHelpAfterError("(add --help for additional information)")
33
+ .showSuggestionAfterError()
34
+ .option("--list-agents", "List available agents")
35
+ .option("-a, --agent <id>", "Agent to use (required)")
36
+ .option("-p, --prompt <text>", "Prompt text")
37
+ .option("-m, --model <model>", "Model to use")
38
+ .option("--allow <perms>", "Permission rules to allow (comma-separated)")
39
+ .option("--deny <perms>", "Permission rules to deny (comma-separated)")
40
+ .option("-f, --format <fmt>", "Output format: jsonl, tsv (default: tsv, truncated on TTY)")
41
+ .option("--raw-log <file>", "Write raw agent output to file")
42
+ .option("--debug", "Enable debug mode")
43
+ .option("--verbose", "Show agent stderr output")
44
+ .option("--preserve-github-sha", "Preserve GITHUB_SHA env var (Gemini)")
45
+ .argument("[prompt]", "Prompt text (alternative to -p)")
46
+ .addHelpText("after", `
47
+ Requirements:
48
+ Install the agent CLIs you plan to use: claude, codex, gemini, opencode, copilot.
49
+ Override paths: AXEXEC_CLAUDE_PATH, AXEXEC_CODEX_PATH, AXEXEC_GEMINI_PATH, AXEXEC_OPENCODE_PATH, AXEXEC_COPILOT_PATH
50
+
51
+ Examples:
52
+ axexec -a claude "Refactor auth flow"
53
+ axexec -a claude -f jsonl "Audit deps" | jq 'select(.type=="tool.call")'
54
+ axexec --list-agents | tail -n +2 | cut -f1
55
+ `)
56
+ .action(async (positionalPrompt, options) => {
57
+ // Handle --list-agents
58
+ if (options.listAgents) {
59
+ const agents = listAdapters();
60
+ process.stdout.write("ID\tNAME\tPACKAGE\n");
61
+ for (const agent of agents) {
62
+ process.stdout.write(`${agent.id}\t${agent.name}\t${agent.package}\n`);
63
+ }
64
+ return;
65
+ }
66
+ // Get prompt from flag, positional argument, or stdin (in priority order)
67
+ // Only read stdin if no prompt was provided via CLI arguments to avoid
68
+ // blocking in CI environments with open but unused stdin.
69
+ const needsStdinPrompt = !options.prompt && !positionalPrompt;
70
+ const stdinPrompt = needsStdinPrompt && !process.stdin.isTTY ? await readStdin() : undefined;
71
+ const prompt = (options.prompt ??
72
+ positionalPrompt ??
73
+ stdinPrompt)?.trimEnd();
74
+ if (!prompt) {
75
+ process.stderr.write("Error: prompt is required\n");
76
+ process.stderr.write("Usage: axexec --agent <id> <prompt>\n");
77
+ process.stderr.write("Try 'axexec --help' for more information.\n");
78
+ process.exitCode = 2;
79
+ return;
80
+ }
81
+ if (!options.agent) {
82
+ process.stderr.write("Error: --agent is required\n");
83
+ process.stderr.write("Usage: axexec --agent <id> <prompt>\n");
84
+ process.stderr.write("Try 'axexec --help' for more information.\n");
85
+ process.exitCode = 2;
86
+ return;
87
+ }
88
+ // Validate format option
89
+ let format;
90
+ if (options.format) {
91
+ if (options.format === "jsonl" || options.format === "tsv") {
92
+ format = options.format;
93
+ }
94
+ else {
95
+ process.stderr.write(`Error: Invalid format '${options.format}'\n`);
96
+ process.stderr.write("Valid formats: jsonl, tsv\n");
97
+ process.exitCode = 2;
98
+ return;
99
+ }
100
+ }
101
+ // Run agent
102
+ const result = await runAgent(options.agent, {
103
+ prompt,
104
+ model: options.model,
105
+ allow: options.allow,
106
+ deny: options.deny,
107
+ format,
108
+ rawLog: options.rawLog,
109
+ debug: options.debug,
110
+ verbose: options.verbose,
111
+ preserveGithubSha: options.preserveGithubSha,
112
+ });
113
+ // Set exit code based on success
114
+ if (!result.success) {
115
+ process.exitCode = 1;
116
+ }
117
+ });
118
+ // Parse CLI arguments
119
+ await program.parseAsync();
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Environment variable helpers for credential-based auth.
3
+ */
4
+ import type { AgentCli } from "axshared";
5
+ import type { RawCredentials } from "./types.js";
6
+ /**
7
+ * Gets additional environment variables for credential-based auth.
8
+ *
9
+ * Some agents use environment variables for API keys in addition to
10
+ * or instead of file-based credentials.
11
+ */
12
+ declare function getCredentialEnvironment(agentId: AgentCli, credentials: RawCredentials): Record<string, string>;
13
+ export { getCredentialEnvironment };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Environment variable helpers for credential-based auth.
3
+ */
4
+ /**
5
+ * Gets additional environment variables for credential-based auth.
6
+ *
7
+ * Some agents use environment variables for API keys in addition to
8
+ * or instead of file-based credentials.
9
+ */
10
+ function getCredentialEnvironment(agentId, credentials) {
11
+ const environment = {};
12
+ switch (agentId) {
13
+ case "claude": {
14
+ if (credentials.apiKey) {
15
+ environment["ANTHROPIC_API_KEY"] = credentials.apiKey;
16
+ }
17
+ break;
18
+ }
19
+ case "codex": {
20
+ if (credentials.apiKey) {
21
+ environment["OPENAI_API_KEY"] = credentials.apiKey;
22
+ }
23
+ break;
24
+ }
25
+ case "gemini": {
26
+ if (credentials.apiKey) {
27
+ environment["GEMINI_API_KEY"] = credentials.apiKey;
28
+ }
29
+ break;
30
+ }
31
+ case "opencode": {
32
+ if (credentials.apiKey) {
33
+ environment["ANTHROPIC_API_KEY"] = credentials.apiKey;
34
+ }
35
+ break;
36
+ }
37
+ case "copilot": {
38
+ if (credentials.githubToken) {
39
+ environment["GITHUB_TOKEN"] = credentials.githubToken;
40
+ }
41
+ break;
42
+ }
43
+ }
44
+ return environment;
45
+ }
46
+ export { getCredentialEnvironment };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Credential installation for isolated agent execution.
3
+ *
4
+ * Writes raw credentials to a temporary directory in agent-specific formats,
5
+ * then returns the environment variables needed to point the agent at that
6
+ * directory. This ensures complete isolation - agents never discover or use
7
+ * locally installed credentials.
8
+ */
9
+ import { type AgentCli } from "axshared";
10
+ import type { InstallResult, RawCredentials } from "./types.js";
11
+ export type { RawCredentials } from "./types.js";
12
+ /**
13
+ * Installs credentials to a temporary directory and returns isolation env vars.
14
+ *
15
+ * This function:
16
+ * 1. Creates a temp directory structure appropriate for the agent
17
+ * 2. Writes credential files in the agent-specific format
18
+ * 3. Returns environment variables that point the agent at the temp directory
19
+ *
20
+ * The caller is responsible for cleaning up the temp directory after use.
21
+ */
22
+ declare function installCredentials(agentId: AgentCli, credentials: RawCredentials): Promise<InstallResult>;
23
+ /**
24
+ * Cleanup for use in finally blocks.
25
+ */
26
+ declare function cleanupCredentials(baseDirectory: string): Promise<void>;
27
+ export { cleanupCredentials, installCredentials };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Credential installation for isolated agent execution.
3
+ *
4
+ * Writes raw credentials to a temporary directory in agent-specific formats,
5
+ * then returns the environment variables needed to point the agent at that
6
+ * directory. This ensures complete isolation - agents never discover or use
7
+ * locally installed credentials.
8
+ */
9
+ import { mkdirSync } from "node:fs";
10
+ import { mkdtemp, rm } from "node:fs/promises";
11
+ import { tmpdir } from "node:os";
12
+ import path from "node:path";
13
+ import { buildAgentRuntimeEnvironment, getAgentConfigSubdirectory, } from "axshared";
14
+ import { getCredentialEnvironment } from "./get-credential-environment.js";
15
+ import { installClaudeCredentials, installCodexCredentials, installGeminiCredentials, installOpenCodeCredentials, } from "./write-agent-credentials.js";
16
+ /**
17
+ * Creates a temporary directory structure for an agent.
18
+ */
19
+ async function createTemporaryDirectories(agentId) {
20
+ const baseDirectory = await mkdtemp(path.join(tmpdir(), `axexec-${agentId}-`));
21
+ const subdirectory = getAgentConfigSubdirectory(agentId);
22
+ let configDirectory;
23
+ let dataDirectory;
24
+ if (subdirectory) {
25
+ configDirectory = path.join(baseDirectory, subdirectory);
26
+ dataDirectory = configDirectory;
27
+ mkdirSync(configDirectory, { recursive: true, mode: 0o700 });
28
+ }
29
+ else {
30
+ configDirectory = baseDirectory;
31
+ dataDirectory = baseDirectory;
32
+ }
33
+ // OpenCode needs separate data directory
34
+ if (agentId === "opencode") {
35
+ dataDirectory = path.join(baseDirectory, "opencode");
36
+ if (dataDirectory !== configDirectory) {
37
+ mkdirSync(dataDirectory, { recursive: true, mode: 0o700 });
38
+ }
39
+ }
40
+ return { baseDirectory, configDirectory, dataDirectory };
41
+ }
42
+ /**
43
+ * Installs agent-specific credentials to the appropriate directory.
44
+ */
45
+ function installAgentCredentials(agentId, configDirectory, dataDirectory, credentials) {
46
+ switch (agentId) {
47
+ case "claude": {
48
+ installClaudeCredentials(configDirectory, credentials);
49
+ break;
50
+ }
51
+ case "codex": {
52
+ installCodexCredentials(configDirectory, credentials);
53
+ break;
54
+ }
55
+ case "gemini": {
56
+ installGeminiCredentials(configDirectory, credentials);
57
+ break;
58
+ }
59
+ case "opencode": {
60
+ installOpenCodeCredentials(dataDirectory, credentials);
61
+ break;
62
+ }
63
+ case "copilot": {
64
+ // Copilot uses GITHUB_TOKEN env var, not file-based credentials
65
+ break;
66
+ }
67
+ }
68
+ }
69
+ /**
70
+ * Installs credentials to a temporary directory and returns isolation env vars.
71
+ *
72
+ * This function:
73
+ * 1. Creates a temp directory structure appropriate for the agent
74
+ * 2. Writes credential files in the agent-specific format
75
+ * 3. Returns environment variables that point the agent at the temp directory
76
+ *
77
+ * The caller is responsible for cleaning up the temp directory after use.
78
+ */
79
+ async function installCredentials(agentId, credentials) {
80
+ const { baseDirectory, configDirectory, dataDirectory } = await createTemporaryDirectories(agentId);
81
+ installAgentCredentials(agentId, configDirectory, dataDirectory, credentials);
82
+ const runtimeEnvironment = buildAgentRuntimeEnvironment(agentId, configDirectory, dataDirectory);
83
+ const credentialEnvironment = getCredentialEnvironment(agentId, credentials);
84
+ return {
85
+ env: { ...runtimeEnvironment, ...credentialEnvironment },
86
+ configDirectory,
87
+ dataDirectory,
88
+ baseDirectory,
89
+ };
90
+ }
91
+ /**
92
+ * Cleanup for use in finally blocks.
93
+ */
94
+ async function cleanupCredentials(baseDirectory) {
95
+ try {
96
+ await rm(baseDirectory, { recursive: true, force: true });
97
+ }
98
+ catch {
99
+ // Ignore cleanup errors - best effort
100
+ }
101
+ }
102
+ export { cleanupCredentials, installCredentials };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Atomic JSON file writing utility.
3
+ */
4
+ /**
5
+ * Saves JSON data to a file atomically.
6
+ *
7
+ * Creates parent directories if needed, writes to a temp file first,
8
+ * then atomically renames to the target path.
9
+ */
10
+ declare function saveJsonFile(filePath: string, data: unknown, mode?: number): void;
11
+ export { saveJsonFile };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Atomic JSON file writing utility.
3
+ */
4
+ import { existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
5
+ import path from "node:path";
6
+ /**
7
+ * Saves JSON data to a file atomically.
8
+ *
9
+ * Creates parent directories if needed, writes to a temp file first,
10
+ * then atomically renames to the target path.
11
+ */
12
+ function saveJsonFile(filePath, data, mode = 0o600) {
13
+ const directory = path.dirname(filePath);
14
+ if (!existsSync(directory)) {
15
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
16
+ }
17
+ const temporaryFile = `${filePath}.tmp.${Date.now()}`;
18
+ writeFileSync(temporaryFile, JSON.stringify(data, undefined, 2), { mode });
19
+ renameSync(temporaryFile, filePath);
20
+ }
21
+ export { saveJsonFile };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Credential types for axexec.
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
+ /** Result of credential installation */
14
+ interface InstallResult {
15
+ /** Environment variables to pass to the agent subprocess */
16
+ env: Record<string, string>;
17
+ /** Configuration directory path (for cleanup) */
18
+ configDirectory: string;
19
+ /** Data directory path (for cleanup, may equal configDirectory) */
20
+ dataDirectory: string;
21
+ /** Base temporary directory (for cleanup) */
22
+ baseDirectory: string;
23
+ }
24
+ export type { InstallResult, RawCredentials };
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Credential types for axexec.
3
+ */
4
+ export {};
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Per-agent credential file writers.
3
+ *
4
+ * Each agent has its own credential file format. These functions
5
+ * write credentials in the agent-specific format.
6
+ */
7
+ import type { RawCredentials } from "./types.js";
8
+ /**
9
+ * Installs Claude credentials to a config directory.
10
+ *
11
+ * File: .credentials.json
12
+ * Format: { claudeAiOauth: { ... } }
13
+ */
14
+ declare function installClaudeCredentials(configDirectory: string, credentials: RawCredentials): void;
15
+ /**
16
+ * Installs Codex credentials to a config directory.
17
+ *
18
+ * File: auth.json
19
+ * Format: { OPENAI_API_KEY: "..." } or { tokens: { ... }, last_refresh: "..." }
20
+ */
21
+ declare function installCodexCredentials(configDirectory: string, credentials: RawCredentials): void;
22
+ /**
23
+ * Installs Gemini credentials to a config directory.
24
+ *
25
+ * File: oauth_creds.json
26
+ * Format: { access_token: "...", refresh_token: "...", ... }
27
+ */
28
+ declare function installGeminiCredentials(configDirectory: string, credentials: RawCredentials): void;
29
+ /**
30
+ * Installs OpenCode credentials to a data directory.
31
+ *
32
+ * File: auth.json
33
+ * Format: { anthropic: { type: "oauth", ... }, openai: { type: "api", ... } }
34
+ */
35
+ declare function installOpenCodeCredentials(dataDirectory: string, credentials: RawCredentials): void;
36
+ export { installClaudeCredentials, installCodexCredentials, installGeminiCredentials, installOpenCodeCredentials, };