@launchchair/agent 0.4.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.
- package/AGENTS.md +248 -0
- package/LICENSE +8 -0
- package/README.md +188 -0
- package/chatgpt-runner.mjs +239 -0
- package/claude-code-runner.mjs +141 -0
- package/claude-runner.mjs +218 -0
- package/codex-runner.mjs +123 -0
- package/launchchair-cli.mjs +1013 -0
- package/local-bridge.mjs +540 -0
- package/mcp-bridge-server.mjs +1101 -0
- package/package.json +43 -0
- package/runner-command.mjs +96 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LaunchChair runner for Claude Code CLI.
|
|
5
|
+
*
|
|
6
|
+
* Reads one LaunchChair prompt from stdin, sends it to `claude -p`, and writes
|
|
7
|
+
* Claude's final response to stdout. This is separate from claude-runner.mjs,
|
|
8
|
+
* which calls the Anthropic Messages API and requires ANTHROPIC_API_KEY.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import process from "node:process";
|
|
13
|
+
|
|
14
|
+
function env(name, fallback = "") {
|
|
15
|
+
const value = process.env[name];
|
|
16
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readStdin() {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
let buffer = "";
|
|
22
|
+
process.stdin.setEncoding("utf8");
|
|
23
|
+
process.stdin.on("data", (chunk) => {
|
|
24
|
+
buffer += chunk;
|
|
25
|
+
});
|
|
26
|
+
process.stdin.on("end", () => resolve(buffer));
|
|
27
|
+
process.stdin.on("error", (error) => reject(error));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildClaudeArgs(config) {
|
|
32
|
+
const args = ["-p"];
|
|
33
|
+
|
|
34
|
+
if (config.outputFormat && config.outputFormat !== "text") {
|
|
35
|
+
args.push("--output-format", config.outputFormat);
|
|
36
|
+
}
|
|
37
|
+
if (config.model) {
|
|
38
|
+
args.push("--model", config.model);
|
|
39
|
+
}
|
|
40
|
+
if (config.allowedTools) {
|
|
41
|
+
args.push("--allowedTools", config.allowedTools);
|
|
42
|
+
}
|
|
43
|
+
if (config.permissionMode) {
|
|
44
|
+
args.push("--permission-mode", config.permissionMode);
|
|
45
|
+
}
|
|
46
|
+
if (config.appendSystemPrompt) {
|
|
47
|
+
args.push("--append-system-prompt", config.appendSystemPrompt);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return args;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function extractClaudeOutput(stdout, outputFormat) {
|
|
54
|
+
const text = String(stdout ?? "").trim();
|
|
55
|
+
if (outputFormat !== "json") {
|
|
56
|
+
return text;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const payload = JSON.parse(text);
|
|
61
|
+
if (typeof payload?.structured_output !== "undefined") {
|
|
62
|
+
return JSON.stringify(payload.structured_output, null, 2);
|
|
63
|
+
}
|
|
64
|
+
if (typeof payload?.result === "string") {
|
|
65
|
+
return payload.result.trim();
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
return text;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return text;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function runClaude(prompt, config) {
|
|
75
|
+
const args = buildClaudeArgs(config);
|
|
76
|
+
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const child = spawn(config.bin, args, {
|
|
79
|
+
cwd: config.cwd,
|
|
80
|
+
env: process.env,
|
|
81
|
+
shell: false,
|
|
82
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
let stdout = "";
|
|
86
|
+
let stderr = "";
|
|
87
|
+
|
|
88
|
+
child.stdout.on("data", (chunk) => {
|
|
89
|
+
stdout += chunk.toString();
|
|
90
|
+
});
|
|
91
|
+
child.stderr.on("data", (chunk) => {
|
|
92
|
+
stderr += chunk.toString();
|
|
93
|
+
});
|
|
94
|
+
child.on("error", (error) => reject(error));
|
|
95
|
+
child.on("close", (code) => {
|
|
96
|
+
if ((code ?? 1) !== 0) {
|
|
97
|
+
const error = new Error(`Claude Code exited with code ${String(code ?? 1)}${stderr ? `: ${stderr.trim()}` : ""}`);
|
|
98
|
+
error.code = code ?? 1;
|
|
99
|
+
error.stdout = stdout;
|
|
100
|
+
error.stderr = stderr;
|
|
101
|
+
reject(error);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
resolve({ stdout, stderr, code: code ?? 0 });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
child.stdin.write(prompt);
|
|
108
|
+
child.stdin.end();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function main() {
|
|
113
|
+
const config = {
|
|
114
|
+
bin: env("LC_AGENT_CLAUDE_CODE_BIN", "claude"),
|
|
115
|
+
cwd: env("LC_AGENT_CLAUDE_CODE_CWD", process.cwd()),
|
|
116
|
+
model: env("LC_AGENT_CLAUDE_CODE_MODEL"),
|
|
117
|
+
outputFormat: env("LC_AGENT_CLAUDE_CODE_OUTPUT_FORMAT", "text"),
|
|
118
|
+
allowedTools: env("LC_AGENT_CLAUDE_CODE_ALLOWED_TOOLS"),
|
|
119
|
+
permissionMode: env("LC_AGENT_CLAUDE_CODE_PERMISSION_MODE"),
|
|
120
|
+
appendSystemPrompt: env("LC_AGENT_CLAUDE_CODE_APPEND_SYSTEM_PROMPT"),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const prompt = String(await readStdin()).trim();
|
|
124
|
+
if (!prompt) {
|
|
125
|
+
throw new Error("Runner received an empty prompt.");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const result = await runClaude(prompt, config);
|
|
129
|
+
const output = extractClaudeOutput(result.stdout, config.outputFormat).trim();
|
|
130
|
+
if (!output) {
|
|
131
|
+
throw new Error("Claude Code completed without returning output.");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
process.stdout.write(`${output}\n`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
main().catch((error) => {
|
|
138
|
+
const message = error instanceof Error ? error.message : String(error ?? "claude-code-runner failed");
|
|
139
|
+
process.stderr.write(`${message}\n`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
|
|
5
|
+
function env(name, fallback = "") {
|
|
6
|
+
const value = process.env[name];
|
|
7
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function toInt(value, fallback) {
|
|
11
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
12
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function toFloat(value, fallback) {
|
|
16
|
+
const parsed = Number.parseFloat(String(value ?? ""));
|
|
17
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stripCodeFence(text) {
|
|
21
|
+
const trimmed = String(text ?? "").trim();
|
|
22
|
+
if (!trimmed.startsWith("```")) {
|
|
23
|
+
return trimmed;
|
|
24
|
+
}
|
|
25
|
+
const lines = trimmed.split("\n");
|
|
26
|
+
if (lines.length < 3) {
|
|
27
|
+
return trimmed;
|
|
28
|
+
}
|
|
29
|
+
const first = lines[0].trim();
|
|
30
|
+
const last = lines[lines.length - 1].trim();
|
|
31
|
+
if (!first.startsWith("```") || last !== "```") {
|
|
32
|
+
return trimmed;
|
|
33
|
+
}
|
|
34
|
+
return lines.slice(1, -1).join("\n").trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function tryJsonParse(candidate) {
|
|
38
|
+
try {
|
|
39
|
+
return { ok: true, value: JSON.parse(candidate) };
|
|
40
|
+
} catch {
|
|
41
|
+
return { ok: false, value: null };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function extractFirstJsonValue(text) {
|
|
46
|
+
const raw = String(text ?? "");
|
|
47
|
+
const direct = tryJsonParse(raw.trim());
|
|
48
|
+
if (direct.ok) {
|
|
49
|
+
return direct.value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const unfenced = stripCodeFence(raw);
|
|
53
|
+
const fromFence = tryJsonParse(unfenced);
|
|
54
|
+
if (fromFence.ok) {
|
|
55
|
+
return fromFence.value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (let start = 0; start < unfenced.length; start += 1) {
|
|
59
|
+
const open = unfenced[start];
|
|
60
|
+
if (open !== "{" && open !== "[") {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const close = open === "{" ? "}" : "]";
|
|
64
|
+
let depth = 0;
|
|
65
|
+
let inString = false;
|
|
66
|
+
let escaped = false;
|
|
67
|
+
|
|
68
|
+
for (let i = start; i < unfenced.length; i += 1) {
|
|
69
|
+
const char = unfenced[i];
|
|
70
|
+
|
|
71
|
+
if (inString) {
|
|
72
|
+
if (escaped) {
|
|
73
|
+
escaped = false;
|
|
74
|
+
} else if (char === "\\") {
|
|
75
|
+
escaped = true;
|
|
76
|
+
} else if (char === "\"") {
|
|
77
|
+
inString = false;
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (char === "\"") {
|
|
83
|
+
inString = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (char === open) {
|
|
87
|
+
depth += 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (char === close) {
|
|
91
|
+
depth -= 1;
|
|
92
|
+
if (depth === 0) {
|
|
93
|
+
const candidate = unfenced.slice(start, i + 1).trim();
|
|
94
|
+
const parsed = tryJsonParse(candidate);
|
|
95
|
+
if (parsed.ok) {
|
|
96
|
+
return parsed.value;
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readStdin() {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
let buffer = "";
|
|
110
|
+
process.stdin.setEncoding("utf8");
|
|
111
|
+
process.stdin.on("data", (chunk) => {
|
|
112
|
+
buffer += chunk;
|
|
113
|
+
});
|
|
114
|
+
process.stdin.on("end", () => resolve(buffer));
|
|
115
|
+
process.stdin.on("error", (error) => reject(error));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function extractClaudeText(payload) {
|
|
120
|
+
const content = Array.isArray(payload?.content) ? payload.content : [];
|
|
121
|
+
const merged = content
|
|
122
|
+
.map((part) => (typeof part?.text === "string" ? part.text : ""))
|
|
123
|
+
.join("\n")
|
|
124
|
+
.trim();
|
|
125
|
+
if (!merged) {
|
|
126
|
+
throw new Error("Anthropic response did not include assistant text.");
|
|
127
|
+
}
|
|
128
|
+
return merged;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function callClaude(input, config) {
|
|
132
|
+
const controller = new AbortController();
|
|
133
|
+
const timeout = setTimeout(() => controller.abort(), config.timeoutMs);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const response = await fetch(`${config.baseUrl}/messages`, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: {
|
|
139
|
+
"x-api-key": config.apiKey,
|
|
140
|
+
"anthropic-version": "2023-06-01",
|
|
141
|
+
"content-type": "application/json",
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
model: config.model,
|
|
145
|
+
temperature: config.temperature,
|
|
146
|
+
max_tokens: config.maxTokens,
|
|
147
|
+
system: "Return exactly one valid JSON value (object or array) and nothing else. No markdown, no prose.",
|
|
148
|
+
messages: [
|
|
149
|
+
{
|
|
150
|
+
role: "user",
|
|
151
|
+
content: input,
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
}),
|
|
155
|
+
signal: controller.signal,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const payload = await response.json().catch(() => ({}));
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
const detail = String(payload?.error?.message ?? payload?.error?.type ?? response.statusText ?? "Request failed");
|
|
161
|
+
throw new Error(`Anthropic request failed (${response.status}): ${detail}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return extractClaudeText(payload);
|
|
165
|
+
} finally {
|
|
166
|
+
clearTimeout(timeout);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function main() {
|
|
171
|
+
const config = {
|
|
172
|
+
apiKey: env("ANTHROPIC_API_KEY"),
|
|
173
|
+
baseUrl: env("ANTHROPIC_BASE_URL", "https://api.anthropic.com/v1").replace(/\/+$/, ""),
|
|
174
|
+
model: env("LC_AGENT_ANTHROPIC_MODEL", env("ANTHROPIC_MODEL", "claude-3-7-sonnet-latest")),
|
|
175
|
+
temperature: toFloat(process.env.LC_AGENT_ANTHROPIC_TEMPERATURE, 0),
|
|
176
|
+
maxTokens: toInt(process.env.LC_AGENT_ANTHROPIC_MAX_TOKENS, 12000),
|
|
177
|
+
timeoutMs: toInt(process.env.LC_AGENT_ANTHROPIC_TIMEOUT_MS, 180000),
|
|
178
|
+
maxAttempts: toInt(process.env.LC_AGENT_JSON_MAX_ATTEMPTS, 3),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (!config.apiKey) {
|
|
182
|
+
throw new Error("Missing ANTHROPIC_API_KEY for claude-runner.");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const prompt = String(await readStdin()).trim();
|
|
186
|
+
if (!prompt) {
|
|
187
|
+
throw new Error("Runner received an empty prompt.");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let lastRaw = "";
|
|
191
|
+
for (let attempt = 1; attempt <= config.maxAttempts; attempt += 1) {
|
|
192
|
+
const input =
|
|
193
|
+
attempt === 1
|
|
194
|
+
? prompt
|
|
195
|
+
: [
|
|
196
|
+
"Repair this to strict valid JSON only.",
|
|
197
|
+
"Do not add explanations, markdown, or code fences.",
|
|
198
|
+
"Return exactly one JSON value.",
|
|
199
|
+
"",
|
|
200
|
+
lastRaw,
|
|
201
|
+
].join("\n");
|
|
202
|
+
|
|
203
|
+
lastRaw = await callClaude(input, config);
|
|
204
|
+
const parsed = extractFirstJsonValue(lastRaw);
|
|
205
|
+
if (parsed !== null) {
|
|
206
|
+
process.stdout.write(`${JSON.stringify(parsed, null, 2)}\n`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
throw new Error("Model did not return valid JSON after retry attempts.");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
main().catch((error) => {
|
|
215
|
+
const message = error instanceof Error ? error.message : String(error ?? "claude-runner failed");
|
|
216
|
+
process.stderr.write(`${message}\n`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
});
|
package/codex-runner.mjs
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LaunchChair Codex runner.
|
|
5
|
+
*
|
|
6
|
+
* Reads a LaunchChair queued prompt from stdin, runs it through `codex exec`,
|
|
7
|
+
* and prints only Codex's final message to stdout so the Agent API can parse it.
|
|
8
|
+
*
|
|
9
|
+
* Auth:
|
|
10
|
+
* - Reuses local Codex CLI auth by default, including ChatGPT sign-in.
|
|
11
|
+
* - Does not require OPENAI_API_KEY unless the user's Codex CLI is configured that way.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
import process from "node:process";
|
|
19
|
+
|
|
20
|
+
function env(name, fallback = "") {
|
|
21
|
+
const value = process.env[name];
|
|
22
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function readStdin() {
|
|
26
|
+
const chunks = [];
|
|
27
|
+
for await (const chunk of process.stdin) {
|
|
28
|
+
chunks.push(Buffer.from(chunk));
|
|
29
|
+
}
|
|
30
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildArgs(outputPath) {
|
|
34
|
+
const cwd = env("LC_AGENT_CODEX_CWD", process.cwd());
|
|
35
|
+
const sandbox = env("LC_AGENT_CODEX_SANDBOX", "workspace-write");
|
|
36
|
+
const model = env("LC_AGENT_CODEX_MODEL");
|
|
37
|
+
const profile = env("LC_AGENT_CODEX_PROFILE");
|
|
38
|
+
const ephemeral = env("LC_AGENT_CODEX_EPHEMERAL", "1") !== "0";
|
|
39
|
+
const skipGitRepoCheck = env("LC_AGENT_CODEX_SKIP_GIT_CHECK", "0") === "1";
|
|
40
|
+
|
|
41
|
+
const args = ["exec", "--cd", cwd, "--sandbox", sandbox, "--output-last-message", outputPath];
|
|
42
|
+
if (ephemeral) {
|
|
43
|
+
args.push("--ephemeral");
|
|
44
|
+
}
|
|
45
|
+
if (skipGitRepoCheck) {
|
|
46
|
+
args.push("--skip-git-repo-check");
|
|
47
|
+
}
|
|
48
|
+
if (model) {
|
|
49
|
+
args.push("--model", model);
|
|
50
|
+
}
|
|
51
|
+
if (profile) {
|
|
52
|
+
args.push("--profile", profile);
|
|
53
|
+
}
|
|
54
|
+
args.push("-");
|
|
55
|
+
return args;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function main() {
|
|
59
|
+
const prompt = await readStdin();
|
|
60
|
+
if (!prompt) {
|
|
61
|
+
console.error("codex-runner: empty prompt.");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), "launchchair-codex-runner-"));
|
|
66
|
+
const outputPath = path.join(tempDir, "last-message.txt");
|
|
67
|
+
const codexBin = env("LC_AGENT_CODEX_BIN", "codex");
|
|
68
|
+
const args = buildArgs(outputPath);
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const result = await new Promise((resolve, reject) => {
|
|
72
|
+
const child = spawn(codexBin, args, {
|
|
73
|
+
cwd: process.cwd(),
|
|
74
|
+
shell: false,
|
|
75
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
76
|
+
env: process.env,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
let stdout = "";
|
|
80
|
+
let stderr = "";
|
|
81
|
+
child.stdout.on("data", (chunk) => {
|
|
82
|
+
stdout += chunk.toString();
|
|
83
|
+
});
|
|
84
|
+
child.stderr.on("data", (chunk) => {
|
|
85
|
+
stderr += chunk.toString();
|
|
86
|
+
});
|
|
87
|
+
child.on("error", (error) => reject(error));
|
|
88
|
+
child.on("close", (code) => {
|
|
89
|
+
resolve({
|
|
90
|
+
code: code ?? 1,
|
|
91
|
+
stdout,
|
|
92
|
+
stderr,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
child.stdin.write(prompt);
|
|
96
|
+
child.stdin.end();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (result.code !== 0) {
|
|
100
|
+
if (result.stdout.trim()) {
|
|
101
|
+
console.error(result.stdout.trim());
|
|
102
|
+
}
|
|
103
|
+
if (result.stderr.trim()) {
|
|
104
|
+
console.error(result.stderr.trim());
|
|
105
|
+
}
|
|
106
|
+
process.exit(result.code);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const finalMessage = (await readFile(outputPath, "utf8")).trim();
|
|
110
|
+
if (!finalMessage) {
|
|
111
|
+
console.error("codex-runner: Codex completed but returned an empty final message.");
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
process.stdout.write(finalMessage);
|
|
115
|
+
} finally {
|
|
116
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
main().catch((error) => {
|
|
121
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
});
|