viveworker 0.2.1 → 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/README.md +55 -0
- package/package.json +16 -2
- package/scripts/a2a-cli.mjs +189 -0
- package/scripts/a2a-executor.mjs +126 -0
- package/scripts/a2a-handler.mjs +439 -0
- package/scripts/a2a-relay-client.mjs +394 -0
- package/scripts/com.viveworker.moltbook-scout.plist.sample +58 -0
- package/scripts/moltbook-api.mjs +206 -0
- package/scripts/moltbook-cli.mjs +1372 -0
- package/scripts/moltbook-scout-auto.sh +447 -0
- package/scripts/moltbook-scout-run.sh +14 -0
- package/scripts/moltbook-watcher.mjs +294 -0
- package/scripts/viveworker-bridge.mjs +814 -16
- package/scripts/viveworker-claude-hook.mjs +7 -1
- package/scripts/viveworker.mjs +314 -1
- package/web/app.css +104 -3
- package/web/app.js +445 -28
- package/web/i18n.js +90 -0
- package/web/sw.js +1 -1
package/README.md
CHANGED
|
@@ -193,6 +193,60 @@ npx viveworker doctor
|
|
|
193
193
|
- On some devices, local CA trust is still manual before HTTPS works reliably
|
|
194
194
|
- Web Push supports approvals, plans, multiple-choice questions, and completions
|
|
195
195
|
|
|
196
|
+
## Moltbook Integration
|
|
197
|
+
|
|
198
|
+
`viveworker` connects to [Moltbook](https://www.moltbook.com), a social network for AI agents. Once configured, your agent automatically maintains a presence on Moltbook — replying to other agents and sharing what it builds — with you approving everything from your phone.
|
|
199
|
+
|
|
200
|
+
### What it does
|
|
201
|
+
|
|
202
|
+
- **Incoming replies**: detects when other agents comment on your posts and notifies your phone so you can draft a reply
|
|
203
|
+
- **Draft approval on phone**: reply drafts and original post drafts appear in `Tasks` and `Timeline`, where you can approve, deny, or edit them from your phone
|
|
204
|
+
- **Auto-scout replies**: every 2 minutes, scans the Moltbook feed, scores posts against your agent's persona (0–100), batches candidates over a 30-minute window, picks the best match, drafts a reply via LLM, and proposes it for your approval
|
|
205
|
+
- **Original post drafts**: based on your daily coding activity, composes new posts in your agent's voice and proposes them at natural intervals — morning (yesterday recap), noon (morning progress), and evening (full-day summary). Up to 3 per day; deny any slot you don't want
|
|
206
|
+
|
|
207
|
+
### How it works
|
|
208
|
+
|
|
209
|
+
1. Define your agent's persona in `~/.viveworker/moltbook-persona.md` — voice, expertise, interests, topics to avoid
|
|
210
|
+
2. The system filters all content through this persona: only activities and posts that match your agent's expertise are surfaced
|
|
211
|
+
3. The Moltbook watcher pushes incoming replies and draft proposals into `Tasks` and `Timeline`
|
|
212
|
+
4. On your phone, you can approve, deny, or edit the draft body before sending
|
|
213
|
+
5. The Moltbook CLI long-polls for that decision, then posts to Moltbook and solves the verification puzzle automatically
|
|
214
|
+
|
|
215
|
+
### Setup
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
# Install the Moltbook watcher and auto-scout alongside viveworker
|
|
219
|
+
npx viveworker setup \
|
|
220
|
+
--moltbook \
|
|
221
|
+
--moltbook-api-key your-api-key \
|
|
222
|
+
--moltbook-agent-id your-agent-id \
|
|
223
|
+
--moltbook-agent-name "your-agent-name" \
|
|
224
|
+
--auto-scout
|
|
225
|
+
|
|
226
|
+
# Describe your agent's voice and expertise
|
|
227
|
+
npx viveworker moltbook persona init
|
|
228
|
+
|
|
229
|
+
# After setup, use start/stop as usual
|
|
230
|
+
npx viveworker start
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`setup --moltbook` writes `~/.viveworker/moltbook.env` and installs the Moltbook watcher.
|
|
234
|
+
`--auto-scout` installs the scheduled scout job. After that, `npx viveworker start` is your normal restart command for the main app.
|
|
235
|
+
|
|
236
|
+
Open `Settings > Moltbook` in the phone app to see the current auto-scout posting quota, current batch, and recent compose status.
|
|
237
|
+
|
|
238
|
+
### Key commands
|
|
239
|
+
|
|
240
|
+
- `npx viveworker moltbook list` — show pending comment notifications
|
|
241
|
+
- `npx viveworker moltbook poll` — manually refresh Moltbook notifications once
|
|
242
|
+
- `npx viveworker moltbook reconcile` — resolve inbox items that were already replied to elsewhere
|
|
243
|
+
- `npx viveworker moltbook scout` — manually pick a feed candidate
|
|
244
|
+
- `npx viveworker moltbook propose <postId> --text "..."` — submit a reply draft for phone approval
|
|
245
|
+
- `npx viveworker moltbook compose` — inspect today's activity for original-post material
|
|
246
|
+
- `npx viveworker moltbook compose-propose --title "..." --content "..."` — submit an original-post draft for phone approval
|
|
247
|
+
- `npx viveworker moltbook persona show` — view your agent's persona
|
|
248
|
+
- `npx viveworker setup --auto-scout-uninstall` — remove the scheduled auto-scout job
|
|
249
|
+
|
|
196
250
|
## Roadmap
|
|
197
251
|
|
|
198
252
|
Planned next steps include:
|
|
@@ -200,3 +254,4 @@ Planned next steps include:
|
|
|
200
254
|
- Windows support
|
|
201
255
|
- ✅ ~~Android support~~ (Apr 1, 2026)
|
|
202
256
|
- ✅ ~~image attachment support from mobile~~ (Mar 26, 2026)
|
|
257
|
+
- ✅ ~~Moltbook integration~~ (Apr 10, 2026)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viveworker",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Local mobile companion for Codex Desktop and Claude Desktop approvals,
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Local mobile companion for Codex Desktop and Claude Desktop — approvals, code review, Moltbook drafts, and A2A (Agent-to-Agent) task relay on your LAN.",
|
|
5
5
|
"author": "Yuta Hoshino <hoshino.lireneo@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
"approvals",
|
|
20
20
|
"questions",
|
|
21
21
|
"code-review",
|
|
22
|
+
"moltbook",
|
|
23
|
+
"ai-agents",
|
|
24
|
+
"a2a",
|
|
25
|
+
"agent-to-agent",
|
|
22
26
|
"lan",
|
|
23
27
|
"companion-app",
|
|
24
28
|
"vivecoding"
|
|
@@ -33,6 +37,16 @@
|
|
|
33
37
|
"scripts/viveworker.mjs",
|
|
34
38
|
"scripts/viveworker-bridge.mjs",
|
|
35
39
|
"scripts/viveworker-claude-hook.mjs",
|
|
40
|
+
"scripts/moltbook-api.mjs",
|
|
41
|
+
"scripts/moltbook-cli.mjs",
|
|
42
|
+
"scripts/moltbook-watcher.mjs",
|
|
43
|
+
"scripts/moltbook-scout-run.sh",
|
|
44
|
+
"scripts/moltbook-scout-auto.sh",
|
|
45
|
+
"scripts/com.viveworker.moltbook-scout.plist.sample",
|
|
46
|
+
"scripts/a2a-cli.mjs",
|
|
47
|
+
"scripts/a2a-handler.mjs",
|
|
48
|
+
"scripts/a2a-executor.mjs",
|
|
49
|
+
"scripts/a2a-relay-client.mjs",
|
|
36
50
|
"scripts/lib",
|
|
37
51
|
"web",
|
|
38
52
|
"README.md",
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* a2a-cli.mjs — CLI for viveworker A2A relay operations.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* viveworker a2a setup --user-id <id> [--relay-url <url>] [--timeout <seconds>]
|
|
6
|
+
*
|
|
7
|
+
* The `setup` command:
|
|
8
|
+
* 1. Creates a setup session on the relay
|
|
9
|
+
* 2. Opens the GitHub OAuth URL in the user's browser
|
|
10
|
+
* 3. Polls for completion (user approves in browser)
|
|
11
|
+
* 4. Writes credentials to ~/.viveworker/a2a.env
|
|
12
|
+
* 5. Bridge can then be restarted to auto-connect
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { promises as fs } from "node:fs";
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { execSync } from "node:child_process";
|
|
19
|
+
import { upsertEnvText } from "./lib/pairing.mjs";
|
|
20
|
+
|
|
21
|
+
const A2A_ENV_FILE = path.join(os.homedir(), ".viveworker", "a2a.env");
|
|
22
|
+
const DEFAULT_RELAY_URL = "https://a2a.viveworker.com";
|
|
23
|
+
const DEFAULT_TIMEOUT = 300; // 5 minutes
|
|
24
|
+
const POLL_INTERVAL = 3000; // 3 seconds
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// CLI entry point
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export async function runA2ACli(args) {
|
|
31
|
+
const cmd = args[0];
|
|
32
|
+
|
|
33
|
+
switch (cmd) {
|
|
34
|
+
case "setup":
|
|
35
|
+
return handleSetup(args.slice(1));
|
|
36
|
+
default:
|
|
37
|
+
console.log("Usage: viveworker a2a setup --user-id <id> [--relay-url <url>] [--timeout <seconds>]");
|
|
38
|
+
if (cmd && cmd !== "help" && cmd !== "--help") {
|
|
39
|
+
throw new Error(`Unknown command: ${cmd}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// setup command
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
async function handleSetup(args) {
|
|
49
|
+
const flags = parseFlags(args);
|
|
50
|
+
const userId = flags["user-id"] || flags["userId"];
|
|
51
|
+
const relayUrl = (flags["relay-url"] || flags["relayUrl"] || DEFAULT_RELAY_URL).replace(/\/$/u, "");
|
|
52
|
+
const timeout = Number(flags["timeout"]) || DEFAULT_TIMEOUT;
|
|
53
|
+
|
|
54
|
+
if (!userId) {
|
|
55
|
+
throw new Error("--user-id is required\nUsage: viveworker a2a setup --user-id <id>");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(`\n🔗 viveworker A2A Relay Setup`);
|
|
59
|
+
console.log(` Relay: ${relayUrl}`);
|
|
60
|
+
console.log(` User ID: ${userId}\n`);
|
|
61
|
+
|
|
62
|
+
// Step 1: Create setup session
|
|
63
|
+
console.log("⏳ Creating setup session...");
|
|
64
|
+
const sessionRes = await fetchJson(`${relayUrl}/auth/setup`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: { "content-type": "application/json" },
|
|
67
|
+
body: JSON.stringify({ userId }),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (sessionRes.error) {
|
|
71
|
+
throw new Error(`Setup session failed: ${sessionRes.error}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { token, authUrl } = sessionRes;
|
|
75
|
+
console.log(`✅ Session created\n`);
|
|
76
|
+
|
|
77
|
+
// Step 2: Open browser
|
|
78
|
+
console.log(`🌐 Opening browser for GitHub authorization...`);
|
|
79
|
+
console.log(` ${authUrl}\n`);
|
|
80
|
+
openBrowser(authUrl);
|
|
81
|
+
|
|
82
|
+
console.log(`⏳ Waiting for GitHub authorization (timeout: ${timeout}s)...`);
|
|
83
|
+
|
|
84
|
+
// Step 3: Poll for completion
|
|
85
|
+
const deadline = Date.now() + timeout * 1000;
|
|
86
|
+
let result = null;
|
|
87
|
+
|
|
88
|
+
while (Date.now() < deadline) {
|
|
89
|
+
await sleep(POLL_INTERVAL);
|
|
90
|
+
|
|
91
|
+
const pollRes = await fetchJson(`${relayUrl}/auth/setup/${token}`);
|
|
92
|
+
|
|
93
|
+
if (pollRes.status === "completed") {
|
|
94
|
+
result = pollRes.credentials;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (pollRes.status === "error") {
|
|
99
|
+
throw new Error(`Registration failed: ${pollRes.error}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (pollRes.error) {
|
|
103
|
+
throw new Error(`Poll error: ${pollRes.error}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Still pending — continue polling
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!result) {
|
|
110
|
+
throw new Error("Timeout waiting for GitHub authorization. Please try again.");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(`\n✅ GitHub authorization complete (@${result.githubLogin})\n`);
|
|
114
|
+
|
|
115
|
+
// Step 4: Write to a2a.env
|
|
116
|
+
console.log(`📝 Writing credentials to ${A2A_ENV_FILE}...`);
|
|
117
|
+
|
|
118
|
+
let currentEnv = "";
|
|
119
|
+
try {
|
|
120
|
+
currentEnv = await fs.readFile(A2A_ENV_FILE, "utf8");
|
|
121
|
+
} catch {
|
|
122
|
+
// File may not exist
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const updated = upsertEnvText(currentEnv, {
|
|
126
|
+
A2A_API_KEY: result.a2aApiKey,
|
|
127
|
+
A2A_RELAY_URL: result.relayUrl,
|
|
128
|
+
A2A_RELAY_USER_ID: result.userId,
|
|
129
|
+
A2A_RELAY_REGISTER_SECRET: result.registerSecret,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await fs.mkdir(path.dirname(A2A_ENV_FILE), { recursive: true, mode: 0o700 });
|
|
133
|
+
await fs.writeFile(A2A_ENV_FILE, updated, { mode: 0o600 });
|
|
134
|
+
|
|
135
|
+
console.log(`✅ Credentials saved\n`);
|
|
136
|
+
console.log(`🚀 Setup complete! Restart your viveworker bridge to connect.`);
|
|
137
|
+
console.log(` Your A2A endpoint: ${result.relayUrl}/${result.userId}\n`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Helpers
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
function parseFlags(args) {
|
|
145
|
+
const flags = {};
|
|
146
|
+
for (let i = 0; i < args.length; i++) {
|
|
147
|
+
const arg = args[i];
|
|
148
|
+
if (arg.startsWith("--")) {
|
|
149
|
+
const eqIdx = arg.indexOf("=");
|
|
150
|
+
if (eqIdx !== -1) {
|
|
151
|
+
flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
|
|
152
|
+
} else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
153
|
+
flags[arg.slice(2)] = args[++i];
|
|
154
|
+
} else {
|
|
155
|
+
flags[arg.slice(2)] = true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return flags;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function fetchJson(url, options = {}) {
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const timer = setTimeout(() => controller.abort(), 15000);
|
|
165
|
+
try {
|
|
166
|
+
const res = await fetch(url, { ...options, signal: controller.signal });
|
|
167
|
+
return await res.json();
|
|
168
|
+
} finally {
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function openBrowser(url) {
|
|
174
|
+
try {
|
|
175
|
+
if (process.platform === "darwin") {
|
|
176
|
+
execSync(`open "${url}"`, { stdio: "ignore" });
|
|
177
|
+
} else if (process.platform === "linux") {
|
|
178
|
+
execSync(`xdg-open "${url}"`, { stdio: "ignore" });
|
|
179
|
+
} else if (process.platform === "win32") {
|
|
180
|
+
execSync(`start "" "${url}"`, { stdio: "ignore" });
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// Browser open failed — URL is already printed for manual use
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function sleep(ms) {
|
|
188
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
189
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* a2a-executor.mjs — Execute approved A2A tasks via Codex CLI.
|
|
3
|
+
*
|
|
4
|
+
* After the user approves an A2A task in the PWA, this module spawns
|
|
5
|
+
* `codex exec` to perform the work, captures the output, and updates
|
|
6
|
+
* the task status to completed/failed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import { completeA2ATask, failA2ATask } from "./a2a-handler.mjs";
|
|
13
|
+
|
|
14
|
+
const EXEC_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Execute an A2A task by spawning `codex exec`.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} task - A2A task object
|
|
20
|
+
* @param {object} config - Bridge config
|
|
21
|
+
* @param {object} runtime - Bridge runtime
|
|
22
|
+
* @param {object} state - Persistent state
|
|
23
|
+
* @param {object} helpers
|
|
24
|
+
* @param {Function} helpers.recordTimelineEntry
|
|
25
|
+
* @param {Function} helpers.saveState
|
|
26
|
+
*/
|
|
27
|
+
export async function executeA2ATask(task, config, runtime, state, { recordTimelineEntry, saveState }) {
|
|
28
|
+
const instruction = task.instruction || "";
|
|
29
|
+
if (!instruction) {
|
|
30
|
+
failA2ATask(task, "No instruction provided");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(`[a2a-exec] Starting task ${task.id}: ${instruction.slice(0, 80)}`);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const result = await runCodexExec(instruction, config);
|
|
38
|
+
completeA2ATask(task, result);
|
|
39
|
+
console.log(`[a2a-exec] Task ${task.id} completed (${result.length} chars)`);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
failA2ATask(task, error.message);
|
|
42
|
+
console.error(`[a2a-exec] Task ${task.id} failed: ${error.message}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Record completion in timeline
|
|
46
|
+
try {
|
|
47
|
+
recordTimelineEntry({
|
|
48
|
+
config,
|
|
49
|
+
runtime,
|
|
50
|
+
state,
|
|
51
|
+
entry: {
|
|
52
|
+
stableId: `a2a_task_result:${task.id}`,
|
|
53
|
+
token: task.token,
|
|
54
|
+
kind: "a2a_task",
|
|
55
|
+
threadId: "a2a",
|
|
56
|
+
threadLabel: "A2A",
|
|
57
|
+
title: task.status === "completed"
|
|
58
|
+
? `A2A ✅: ${instruction.slice(0, 60)}`
|
|
59
|
+
: `A2A ❌: ${instruction.slice(0, 60)}`,
|
|
60
|
+
summary: task.statusMessage || "",
|
|
61
|
+
messageText: task.status === "completed"
|
|
62
|
+
? (task.artifacts?.[0]?.parts?.[0]?.text || "").slice(0, 500)
|
|
63
|
+
: task.statusMessage || "Failed",
|
|
64
|
+
taskStatus: task.status,
|
|
65
|
+
createdAtMs: task.updatedAtMs || Date.now(),
|
|
66
|
+
readOnly: true,
|
|
67
|
+
provider: "a2a",
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
await saveState(config.stateFile, state);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(`[a2a-exec-timeline] ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Spawn `codex exec` and capture stdout+stderr.
|
|
78
|
+
*/
|
|
79
|
+
function runCodexExec(instruction, config) {
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const codexBin = config.codexBin || "codex";
|
|
82
|
+
const args = ["exec", "--prompt", instruction, "--approval-mode", "full-auto"];
|
|
83
|
+
const cwd = config.workspaceRoot || process.cwd();
|
|
84
|
+
|
|
85
|
+
const child = spawn(codexBin, args, {
|
|
86
|
+
cwd,
|
|
87
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
88
|
+
env: { ...process.env, CODEX_HOME: config.codexHome || path.join(os.homedir(), ".codex") },
|
|
89
|
+
timeout: EXEC_TIMEOUT_MS,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
let stdout = "";
|
|
93
|
+
let stderr = "";
|
|
94
|
+
|
|
95
|
+
child.stdout.on("data", (chunk) => {
|
|
96
|
+
stdout += chunk.toString("utf8");
|
|
97
|
+
// Cap output at 100KB
|
|
98
|
+
if (stdout.length > 100_000) {
|
|
99
|
+
child.kill("SIGTERM");
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
child.stderr.on("data", (chunk) => {
|
|
103
|
+
stderr += chunk.toString("utf8");
|
|
104
|
+
if (stderr.length > 50_000) stderr = stderr.slice(-50_000);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
child.on("error", (error) => {
|
|
108
|
+
reject(new Error(`Failed to spawn codex: ${error.message}`));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
child.on("close", (code) => {
|
|
112
|
+
if (code === 0) {
|
|
113
|
+
resolve(stdout.trim() || "(completed with no output)");
|
|
114
|
+
} else {
|
|
115
|
+
const errorDetail = stderr.trim() || stdout.trim() || `exit code ${code}`;
|
|
116
|
+
reject(new Error(`codex exec failed: ${errorDetail.slice(0, 500)}`));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Timeout fallback
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
try { child.kill("SIGTERM"); } catch { /* ignore */ }
|
|
123
|
+
reject(new Error("codex exec timed out (10 minutes)"));
|
|
124
|
+
}, EXEC_TIMEOUT_MS).unref?.();
|
|
125
|
+
});
|
|
126
|
+
}
|