viveworker 0.3.0 → 0.4.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.
- package/README.md +25 -11
- package/package.json +11 -2
- package/scripts/a2a-cli.mjs +198 -0
- package/scripts/a2a-executor.mjs +126 -0
- package/scripts/a2a-handler.mjs +455 -0
- package/scripts/a2a-relay-client.mjs +394 -0
- package/scripts/moltbook-api.mjs +11 -6
- package/scripts/moltbook-cli.mjs +92 -22
- package/scripts/moltbook-scout-auto.sh +447 -0
- package/scripts/viveworker-bridge.mjs +272 -8
- package/scripts/viveworker.mjs +11 -0
- package/web/app.css +29 -2
- package/web/app.js +152 -8
- package/web/i18n.js +45 -9
- package/web/sw.js +1 -1
package/README.md
CHANGED
|
@@ -200,6 +200,7 @@ npx viveworker doctor
|
|
|
200
200
|
### What it does
|
|
201
201
|
|
|
202
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
|
|
203
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
|
|
204
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
|
|
205
206
|
|
|
@@ -207,31 +208,44 @@ npx viveworker doctor
|
|
|
207
208
|
|
|
208
209
|
1. Define your agent's persona in `~/.viveworker/moltbook-persona.md` — voice, expertise, interests, topics to avoid
|
|
209
210
|
2. The system filters all content through this persona: only activities and posts that match your agent's expertise are surfaced
|
|
210
|
-
3.
|
|
211
|
-
4. On
|
|
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
|
|
212
214
|
|
|
213
215
|
### Setup
|
|
214
216
|
|
|
215
217
|
```bash
|
|
216
|
-
#
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
223
227
|
npx viveworker moltbook persona init
|
|
224
228
|
|
|
225
|
-
#
|
|
229
|
+
# After setup, use start/stop as usual
|
|
226
230
|
npx viveworker start
|
|
227
231
|
```
|
|
228
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
|
+
|
|
229
238
|
### Key commands
|
|
230
239
|
|
|
231
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
|
|
232
243
|
- `npx viveworker moltbook scout` — manually pick a feed candidate
|
|
233
|
-
- `npx viveworker moltbook
|
|
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
|
|
234
247
|
- `npx viveworker moltbook persona show` — view your agent's persona
|
|
248
|
+
- `npx viveworker setup --auto-scout-uninstall` — remove the scheduled auto-scout job
|
|
235
249
|
|
|
236
250
|
## Roadmap
|
|
237
251
|
|
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.1",
|
|
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"
|
|
@@ -37,7 +41,12 @@
|
|
|
37
41
|
"scripts/moltbook-cli.mjs",
|
|
38
42
|
"scripts/moltbook-watcher.mjs",
|
|
39
43
|
"scripts/moltbook-scout-run.sh",
|
|
44
|
+
"scripts/moltbook-scout-auto.sh",
|
|
40
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",
|
|
41
50
|
"scripts/lib",
|
|
42
51
|
"web",
|
|
43
52
|
"README.md",
|
|
@@ -0,0 +1,198 @@
|
|
|
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> [--description <text>] [--skills <csv>] [--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
|
+
const description = flags["description"] || "";
|
|
54
|
+
const skillsRaw = flags["skills"] || "";
|
|
55
|
+
|
|
56
|
+
if (!userId) {
|
|
57
|
+
throw new Error("--user-id is required\nUsage: viveworker a2a setup --user-id <id>");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(`\n🔗 viveworker A2A Relay Setup`);
|
|
61
|
+
console.log(` Relay: ${relayUrl}`);
|
|
62
|
+
console.log(` User ID: ${userId}`);
|
|
63
|
+
if (description) console.log(` Desc: ${description}`);
|
|
64
|
+
if (skillsRaw) console.log(` Skills: ${skillsRaw}`);
|
|
65
|
+
console.log();
|
|
66
|
+
|
|
67
|
+
// Step 1: Create setup session
|
|
68
|
+
console.log("⏳ Creating setup session...");
|
|
69
|
+
const sessionRes = await fetchJson(`${relayUrl}/auth/setup`, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: { "content-type": "application/json" },
|
|
72
|
+
body: JSON.stringify({ userId }),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (sessionRes.error) {
|
|
76
|
+
throw new Error(`Setup session failed: ${sessionRes.error}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { token, authUrl } = sessionRes;
|
|
80
|
+
console.log(`✅ Session created\n`);
|
|
81
|
+
|
|
82
|
+
// Step 2: Open browser
|
|
83
|
+
console.log(`🌐 Opening browser for GitHub authorization...`);
|
|
84
|
+
console.log(` ${authUrl}\n`);
|
|
85
|
+
openBrowser(authUrl);
|
|
86
|
+
|
|
87
|
+
console.log(`⏳ Waiting for GitHub authorization (timeout: ${timeout}s)...`);
|
|
88
|
+
|
|
89
|
+
// Step 3: Poll for completion
|
|
90
|
+
const deadline = Date.now() + timeout * 1000;
|
|
91
|
+
let result = null;
|
|
92
|
+
|
|
93
|
+
while (Date.now() < deadline) {
|
|
94
|
+
await sleep(POLL_INTERVAL);
|
|
95
|
+
|
|
96
|
+
const pollRes = await fetchJson(`${relayUrl}/auth/setup/${token}`);
|
|
97
|
+
|
|
98
|
+
if (pollRes.status === "completed") {
|
|
99
|
+
result = pollRes.credentials;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (pollRes.status === "error") {
|
|
104
|
+
throw new Error(`Registration failed: ${pollRes.error}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (pollRes.error) {
|
|
108
|
+
throw new Error(`Poll error: ${pollRes.error}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Still pending — continue polling
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!result) {
|
|
115
|
+
throw new Error("Timeout waiting for GitHub authorization. Please try again.");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(`\n✅ GitHub authorization complete (@${result.githubLogin})\n`);
|
|
119
|
+
|
|
120
|
+
// Step 4: Write to a2a.env
|
|
121
|
+
console.log(`📝 Writing credentials to ${A2A_ENV_FILE}...`);
|
|
122
|
+
|
|
123
|
+
let currentEnv = "";
|
|
124
|
+
try {
|
|
125
|
+
currentEnv = await fs.readFile(A2A_ENV_FILE, "utf8");
|
|
126
|
+
} catch {
|
|
127
|
+
// File may not exist
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const envVars = {
|
|
131
|
+
A2A_API_KEY: result.a2aApiKey,
|
|
132
|
+
A2A_RELAY_URL: result.relayUrl,
|
|
133
|
+
A2A_RELAY_USER_ID: result.userId,
|
|
134
|
+
A2A_RELAY_REGISTER_SECRET: result.registerSecret,
|
|
135
|
+
};
|
|
136
|
+
if (description) envVars.A2A_DESCRIPTION = description;
|
|
137
|
+
if (skillsRaw) envVars.A2A_SKILLS = skillsRaw;
|
|
138
|
+
|
|
139
|
+
const updated = upsertEnvText(currentEnv, envVars);
|
|
140
|
+
|
|
141
|
+
await fs.mkdir(path.dirname(A2A_ENV_FILE), { recursive: true, mode: 0o700 });
|
|
142
|
+
await fs.writeFile(A2A_ENV_FILE, updated, { mode: 0o600 });
|
|
143
|
+
|
|
144
|
+
console.log(`✅ Credentials saved\n`);
|
|
145
|
+
console.log(`🚀 Setup complete! Restart your viveworker bridge to connect.`);
|
|
146
|
+
console.log(` Your A2A endpoint: ${result.relayUrl}/${result.userId}\n`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Helpers
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
function parseFlags(args) {
|
|
154
|
+
const flags = {};
|
|
155
|
+
for (let i = 0; i < args.length; i++) {
|
|
156
|
+
const arg = args[i];
|
|
157
|
+
if (arg.startsWith("--")) {
|
|
158
|
+
const eqIdx = arg.indexOf("=");
|
|
159
|
+
if (eqIdx !== -1) {
|
|
160
|
+
flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
|
|
161
|
+
} else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
162
|
+
flags[arg.slice(2)] = args[++i];
|
|
163
|
+
} else {
|
|
164
|
+
flags[arg.slice(2)] = true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return flags;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function fetchJson(url, options = {}) {
|
|
172
|
+
const controller = new AbortController();
|
|
173
|
+
const timer = setTimeout(() => controller.abort(), 15000);
|
|
174
|
+
try {
|
|
175
|
+
const res = await fetch(url, { ...options, signal: controller.signal });
|
|
176
|
+
return await res.json();
|
|
177
|
+
} finally {
|
|
178
|
+
clearTimeout(timer);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function openBrowser(url) {
|
|
183
|
+
try {
|
|
184
|
+
if (process.platform === "darwin") {
|
|
185
|
+
execSync(`open "${url}"`, { stdio: "ignore" });
|
|
186
|
+
} else if (process.platform === "linux") {
|
|
187
|
+
execSync(`xdg-open "${url}"`, { stdio: "ignore" });
|
|
188
|
+
} else if (process.platform === "win32") {
|
|
189
|
+
execSync(`start "" "${url}"`, { stdio: "ignore" });
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
// Browser open failed — URL is already printed for manual use
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function sleep(ms) {
|
|
197
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
198
|
+
}
|
|
@@ -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
|
+
}
|