@shipers-dev/multi 0.9.0 → 0.9.2
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/dist/index.js +104 -12
- package/package.json +1 -1
- package/src/acp-runner.ts +18 -4
- package/src/index.ts +67 -8
- package/.claude/settings.local.json +0 -11
package/dist/index.js
CHANGED
|
@@ -5169,6 +5169,25 @@ class RequestError extends Error {
|
|
|
5169
5169
|
// src/acp-runner.ts
|
|
5170
5170
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
5171
5171
|
import { dirname } from "path";
|
|
5172
|
+
function fmtErr(e) {
|
|
5173
|
+
if (e == null)
|
|
5174
|
+
return "unknown error";
|
|
5175
|
+
if (typeof e === "string")
|
|
5176
|
+
return e;
|
|
5177
|
+
if (e instanceof Error)
|
|
5178
|
+
return e.message;
|
|
5179
|
+
if (typeof e === "object") {
|
|
5180
|
+
const inner = e.error ?? e.cause ?? e;
|
|
5181
|
+
const msg = inner?.message ?? e.message ?? e.reason ?? e.statusText;
|
|
5182
|
+
const code = inner?.code ?? e.code ?? e.status;
|
|
5183
|
+
if (msg)
|
|
5184
|
+
return code ? `${msg} (code ${code})` : String(msg);
|
|
5185
|
+
try {
|
|
5186
|
+
return JSON.stringify(e).slice(0, 500);
|
|
5187
|
+
} catch {}
|
|
5188
|
+
}
|
|
5189
|
+
return String(e);
|
|
5190
|
+
}
|
|
5172
5191
|
async function runAcp(opts) {
|
|
5173
5192
|
const cleanEnv = {};
|
|
5174
5193
|
for (const [k, v] of Object.entries(process.env)) {
|
|
@@ -5221,7 +5240,7 @@ async function runAcp(opts) {
|
|
|
5221
5240
|
`) : content;
|
|
5222
5241
|
return { content: sliced };
|
|
5223
5242
|
} catch (e) {
|
|
5224
|
-
throw new Error(`readTextFile failed: ${
|
|
5243
|
+
throw new Error(`readTextFile failed: ${fmtErr(e)}`);
|
|
5225
5244
|
}
|
|
5226
5245
|
},
|
|
5227
5246
|
async writeTextFile(params) {
|
|
@@ -5232,7 +5251,7 @@ async function runAcp(opts) {
|
|
|
5232
5251
|
writeFileSync(params.path, params.content, "utf8");
|
|
5233
5252
|
return {};
|
|
5234
5253
|
} catch (e) {
|
|
5235
|
-
throw new Error(`writeTextFile failed: ${
|
|
5254
|
+
throw new Error(`writeTextFile failed: ${fmtErr(e)}`);
|
|
5236
5255
|
}
|
|
5237
5256
|
}
|
|
5238
5257
|
};
|
|
@@ -5259,7 +5278,7 @@ async function runAcp(opts) {
|
|
|
5259
5278
|
await conn.loadSession?.({ sessionId: activeSessionId, cwd: opts.cwd || process.cwd(), mcpServers: [] });
|
|
5260
5279
|
await opts.onEvent({ event_type: "progress", payload: { message: `ACP session ${activeSessionId.slice(0, 8)} resumed` } });
|
|
5261
5280
|
} catch (e) {
|
|
5262
|
-
await opts.onEvent({ event_type: "progress", payload: { message: `load_session failed; starting new. ${
|
|
5281
|
+
await opts.onEvent({ event_type: "progress", payload: { message: `load_session failed; starting new. ${fmtErr(e)}` } });
|
|
5263
5282
|
const { sessionId } = await conn.newSession({ cwd: opts.cwd || process.cwd(), mcpServers: [] });
|
|
5264
5283
|
activeSessionId = sessionId;
|
|
5265
5284
|
if (opts.onSession)
|
|
@@ -5286,7 +5305,7 @@ async function runAcp(opts) {
|
|
|
5286
5305
|
res = await runPrompt();
|
|
5287
5306
|
stopReason = res.stopReason;
|
|
5288
5307
|
} catch (e) {
|
|
5289
|
-
await opts.onEvent({ event_type: "error", payload: { message: `retry with fresh session failed: ${
|
|
5308
|
+
await opts.onEvent({ event_type: "error", payload: { message: `retry with fresh session failed: ${fmtErr(e)}` } });
|
|
5290
5309
|
}
|
|
5291
5310
|
}
|
|
5292
5311
|
if (chunkCount === 0) {
|
|
@@ -5689,13 +5708,14 @@ var LOG_PATH2 = join3(MULTI_DIR, "logs", "agent.log");
|
|
|
5689
5708
|
var SKILLS_DIR = join3(MULTI_DIR, "skills");
|
|
5690
5709
|
var STOP_PATH = join3(MULTI_DIR, "stop.flag");
|
|
5691
5710
|
var TASKS_DB_PATH = join3(MULTI_DIR, "tasks.db");
|
|
5692
|
-
var VERSION = "0.
|
|
5711
|
+
var VERSION = "0.9.2";
|
|
5693
5712
|
var COMMANDS = {
|
|
5694
5713
|
setup: "Register this device with a workspace",
|
|
5695
5714
|
connect: "Connect device to realtime hub and execute assigned tasks",
|
|
5696
5715
|
link: "Link this device to an agent (agent_id required)",
|
|
5697
5716
|
status: "Show current status",
|
|
5698
5717
|
stop: "Stop the running daemon",
|
|
5718
|
+
restart: "Stop and relaunch the daemon in background",
|
|
5699
5719
|
logs: "View execution logs"
|
|
5700
5720
|
};
|
|
5701
5721
|
function ensureDirs() {
|
|
@@ -5761,6 +5781,9 @@ async function main() {
|
|
|
5761
5781
|
case "stop":
|
|
5762
5782
|
await cmdStop();
|
|
5763
5783
|
break;
|
|
5784
|
+
case "restart":
|
|
5785
|
+
await cmdRestart(apiUrl);
|
|
5786
|
+
break;
|
|
5764
5787
|
case "logs":
|
|
5765
5788
|
await cmdLogs();
|
|
5766
5789
|
break;
|
|
@@ -5782,6 +5805,7 @@ Commands:
|
|
|
5782
5805
|
connect ${COMMANDS.connect}
|
|
5783
5806
|
status ${COMMANDS.status}
|
|
5784
5807
|
stop ${COMMANDS.stop}
|
|
5808
|
+
restart ${COMMANDS.restart}
|
|
5785
5809
|
logs ${COMMANDS.logs}
|
|
5786
5810
|
|
|
5787
5811
|
Options:
|
|
@@ -6159,7 +6183,7 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
|
|
|
6159
6183
|
worktreeBranch = wt.branch;
|
|
6160
6184
|
await postStream(apiUrl, issueId, "worktree_created", { path: wt.path, branch: wt.branch, reused: !wt.created });
|
|
6161
6185
|
} catch (e) {
|
|
6162
|
-
await postStream(apiUrl, issueId, "worktree_error", { message:
|
|
6186
|
+
await postStream(apiUrl, issueId, "worktree_error", { message: fmtError(e) });
|
|
6163
6187
|
}
|
|
6164
6188
|
}
|
|
6165
6189
|
log(`\u25B6 run_task ${task.key}: ${isFollowup ? "(follow-up) " : ""}${task.title}${workingDir ? ` [cwd: ${workingDir}${worktreeBranch ? ` @${worktreeBranch}` : ""}]` : ""}`);
|
|
@@ -6628,14 +6652,15 @@ ${userPart}` : userPart;
|
|
|
6628
6652
|
log(` \u2713 ${task.key} complete`);
|
|
6629
6653
|
}
|
|
6630
6654
|
} catch (e) {
|
|
6655
|
+
const msg = fmtError(e);
|
|
6631
6656
|
if (ctx?.runEntry?.stopped) {
|
|
6632
6657
|
await markStopped(apiUrl, issueId, ctx.runEntry.stopReason || "stopped");
|
|
6633
|
-
log(` \u23F9 ${task.key} stopped (${
|
|
6658
|
+
log(` \u23F9 ${task.key} stopped (${msg})`);
|
|
6634
6659
|
} else {
|
|
6635
|
-
await postStream(apiUrl, issueId, "error", { message:
|
|
6636
|
-
await postComment(`\u274C spawn error: ${
|
|
6660
|
+
await postStream(apiUrl, issueId, "error", { message: msg });
|
|
6661
|
+
await postComment(`\u274C spawn error: ${msg}`);
|
|
6637
6662
|
await apiClient.post(`${apiUrl}/api/issues/${issueId}/fail`, {});
|
|
6638
|
-
log(` \u2717 ${task.key} failed: ${
|
|
6663
|
+
log(` \u2717 ${task.key} failed: ${msg}`);
|
|
6639
6664
|
}
|
|
6640
6665
|
}
|
|
6641
6666
|
}
|
|
@@ -6738,7 +6763,11 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx) {
|
|
|
6738
6763
|
}
|
|
6739
6764
|
})();
|
|
6740
6765
|
const headers = { "x-agent-id": parentTask.agent_id };
|
|
6741
|
-
|
|
6766
|
+
if (typeof ctx.refreshLocalAgents === "function") {
|
|
6767
|
+
try {
|
|
6768
|
+
await ctx.refreshLocalAgents();
|
|
6769
|
+
} catch {}
|
|
6770
|
+
}
|
|
6742
6771
|
for (const a of actions) {
|
|
6743
6772
|
try {
|
|
6744
6773
|
if (a.type === "create") {
|
|
@@ -6793,6 +6822,25 @@ function stripMd(s) {
|
|
|
6793
6822
|
function stripSelfMention(prompt, _agentType) {
|
|
6794
6823
|
return prompt.replace(/^@[A-Za-z0-9_\-]+\s*/, "").trim();
|
|
6795
6824
|
}
|
|
6825
|
+
function fmtError(e) {
|
|
6826
|
+
if (e == null)
|
|
6827
|
+
return "unknown error";
|
|
6828
|
+
if (typeof e === "string")
|
|
6829
|
+
return e;
|
|
6830
|
+
if (e instanceof Error)
|
|
6831
|
+
return e.stack ? `${e.message}` : String(e);
|
|
6832
|
+
if (typeof e === "object") {
|
|
6833
|
+
const inner = e.error ?? e.cause ?? e;
|
|
6834
|
+
const msg = inner?.message ?? e.message ?? e.reason ?? e.statusText;
|
|
6835
|
+
const code = inner?.code ?? e.code ?? e.status;
|
|
6836
|
+
if (msg)
|
|
6837
|
+
return code ? `${msg} (code ${code})` : String(msg);
|
|
6838
|
+
try {
|
|
6839
|
+
return JSON.stringify(e).slice(0, 500);
|
|
6840
|
+
} catch {}
|
|
6841
|
+
}
|
|
6842
|
+
return String(e);
|
|
6843
|
+
}
|
|
6796
6844
|
function statusIcon(status) {
|
|
6797
6845
|
switch (status) {
|
|
6798
6846
|
case "pending":
|
|
@@ -6947,7 +6995,7 @@ Context (original task ${task.key}): ${task.title}` : base || task.title;
|
|
|
6947
6995
|
try {
|
|
6948
6996
|
proc = Bun.spawn([agent.path, ...args], { stdout: "pipe", stderr: "pipe", stdin: "ignore", cwd: task?.working_dir || undefined });
|
|
6949
6997
|
} catch (e) {
|
|
6950
|
-
yield { event_type: "error", payload: { message: `spawn failed: ${
|
|
6998
|
+
yield { event_type: "error", payload: { message: `spawn failed: ${fmtError(e)}` } };
|
|
6951
6999
|
return;
|
|
6952
7000
|
}
|
|
6953
7001
|
const queue = [];
|
|
@@ -7151,6 +7199,50 @@ async function cmdStop() {
|
|
|
7151
7199
|
console.log(`Sent SIGTERM to ${pid}`);
|
|
7152
7200
|
} catch {}
|
|
7153
7201
|
}
|
|
7202
|
+
async function cmdRestart(apiUrl) {
|
|
7203
|
+
if (existsSync3(PID_PATH)) {
|
|
7204
|
+
const pid = Number(readFileSync3(PID_PATH, "utf8").trim());
|
|
7205
|
+
if (pid && isRunning(pid)) {
|
|
7206
|
+
ensureDirs();
|
|
7207
|
+
writeFileSync3(STOP_PATH, "1");
|
|
7208
|
+
try {
|
|
7209
|
+
process.kill(pid, "SIGTERM");
|
|
7210
|
+
} catch {}
|
|
7211
|
+
console.log(`\u23F9 Stopping daemon (pid ${pid})...`);
|
|
7212
|
+
const deadline = Date.now() + 1e4;
|
|
7213
|
+
while (Date.now() < deadline && isRunning(pid))
|
|
7214
|
+
await sleep(200);
|
|
7215
|
+
if (isRunning(pid)) {
|
|
7216
|
+
try {
|
|
7217
|
+
process.kill(pid, "SIGKILL");
|
|
7218
|
+
} catch {}
|
|
7219
|
+
await sleep(300);
|
|
7220
|
+
}
|
|
7221
|
+
}
|
|
7222
|
+
try {
|
|
7223
|
+
if (existsSync3(PID_PATH))
|
|
7224
|
+
unlinkSync(PID_PATH);
|
|
7225
|
+
} catch {}
|
|
7226
|
+
}
|
|
7227
|
+
try {
|
|
7228
|
+
if (existsSync3(STOP_PATH))
|
|
7229
|
+
unlinkSync(STOP_PATH);
|
|
7230
|
+
} catch {}
|
|
7231
|
+
console.log("\uD83D\uDD04 Relaunching daemon...");
|
|
7232
|
+
ensureDirs();
|
|
7233
|
+
const args = Bun.argv.slice(1).filter((a) => a !== "-d" && a !== "--detach" && a !== "restart");
|
|
7234
|
+
if (!args.includes("connect"))
|
|
7235
|
+
args.splice(1, 0, "connect");
|
|
7236
|
+
const proc = Bun.spawn([process.execPath, ...args, "--api", apiUrl], {
|
|
7237
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
7238
|
+
env: { ...process.env, MULTI_DETACHED: "1" }
|
|
7239
|
+
});
|
|
7240
|
+
proc.unref?.();
|
|
7241
|
+
await sleep(500);
|
|
7242
|
+
console.log(`\u2705 Daemon restarted (pid ${proc.pid}).`);
|
|
7243
|
+
console.log(` Tail logs: multi-agent logs`);
|
|
7244
|
+
process.exit(0);
|
|
7245
|
+
}
|
|
7154
7246
|
async function cmdLogs() {
|
|
7155
7247
|
if (!existsSync3(LOG_PATH2)) {
|
|
7156
7248
|
console.log("No logs yet.");
|
package/package.json
CHANGED
package/src/acp-runner.ts
CHANGED
|
@@ -8,6 +8,20 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
|
8
8
|
import { dirname } from 'path';
|
|
9
9
|
import { apiClient } from './client';
|
|
10
10
|
|
|
11
|
+
function fmtErr(e: any): string {
|
|
12
|
+
if (e == null) return 'unknown error';
|
|
13
|
+
if (typeof e === 'string') return e;
|
|
14
|
+
if (e instanceof Error) return e.message;
|
|
15
|
+
if (typeof e === 'object') {
|
|
16
|
+
const inner = e.error ?? e.cause ?? e;
|
|
17
|
+
const msg = inner?.message ?? e.message ?? e.reason ?? e.statusText;
|
|
18
|
+
const code = inner?.code ?? e.code ?? e.status;
|
|
19
|
+
if (msg) return code ? `${msg} (code ${code})` : String(msg);
|
|
20
|
+
try { return JSON.stringify(e).slice(0, 500); } catch {}
|
|
21
|
+
}
|
|
22
|
+
return String(e);
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
export type AcpEvent =
|
|
12
26
|
| { event_type: 'progress'; payload: any }
|
|
13
27
|
| { event_type: 'assistant_text'; payload: { text: string } }
|
|
@@ -79,7 +93,7 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
79
93
|
: content;
|
|
80
94
|
return { content: sliced };
|
|
81
95
|
} catch (e) {
|
|
82
|
-
throw new Error(`readTextFile failed: ${
|
|
96
|
+
throw new Error(`readTextFile failed: ${fmtErr(e)}`);
|
|
83
97
|
}
|
|
84
98
|
},
|
|
85
99
|
async writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
|
|
@@ -89,7 +103,7 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
89
103
|
writeFileSync(params.path, params.content, 'utf8');
|
|
90
104
|
return {};
|
|
91
105
|
} catch (e) {
|
|
92
|
-
throw new Error(`writeTextFile failed: ${
|
|
106
|
+
throw new Error(`writeTextFile failed: ${fmtErr(e)}`);
|
|
93
107
|
}
|
|
94
108
|
},
|
|
95
109
|
};
|
|
@@ -118,7 +132,7 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
118
132
|
await conn.loadSession?.({ sessionId: activeSessionId, cwd: opts.cwd || process.cwd(), mcpServers: [] } as any);
|
|
119
133
|
await opts.onEvent({ event_type: 'progress', payload: { message: `ACP session ${activeSessionId.slice(0, 8)} resumed` } });
|
|
120
134
|
} catch (e) {
|
|
121
|
-
await opts.onEvent({ event_type: 'progress', payload: { message: `load_session failed; starting new. ${
|
|
135
|
+
await opts.onEvent({ event_type: 'progress', payload: { message: `load_session failed; starting new. ${fmtErr(e)}` } });
|
|
122
136
|
const { sessionId } = await conn.newSession({ cwd: opts.cwd || process.cwd(), mcpServers: [] } as any);
|
|
123
137
|
activeSessionId = sessionId;
|
|
124
138
|
if (opts.onSession) await opts.onSession(sessionId);
|
|
@@ -148,7 +162,7 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
|
|
|
148
162
|
res = await runPrompt();
|
|
149
163
|
stopReason = (res as any).stopReason;
|
|
150
164
|
} catch (e) {
|
|
151
|
-
await opts.onEvent({ event_type: 'error', payload: { message: `retry with fresh session failed: ${
|
|
165
|
+
await opts.onEvent({ event_type: 'error', payload: { message: `retry with fresh session failed: ${fmtErr(e)}` } });
|
|
152
166
|
}
|
|
153
167
|
}
|
|
154
168
|
|
package/src/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ const LOG_PATH = join(MULTI_DIR, 'logs', 'agent.log');
|
|
|
18
18
|
const SKILLS_DIR = join(MULTI_DIR, 'skills');
|
|
19
19
|
const STOP_PATH = join(MULTI_DIR, 'stop.flag');
|
|
20
20
|
const TASKS_DB_PATH = join(MULTI_DIR, 'tasks.db');
|
|
21
|
-
const VERSION = '0.
|
|
21
|
+
const VERSION = '0.9.2';
|
|
22
22
|
|
|
23
23
|
const COMMANDS = {
|
|
24
24
|
setup: 'Register this device with a workspace',
|
|
@@ -26,6 +26,7 @@ const COMMANDS = {
|
|
|
26
26
|
link: 'Link this device to an agent (agent_id required)',
|
|
27
27
|
status: 'Show current status',
|
|
28
28
|
stop: 'Stop the running daemon',
|
|
29
|
+
restart: 'Stop and relaunch the daemon in background',
|
|
29
30
|
logs: 'View execution logs',
|
|
30
31
|
} as const;
|
|
31
32
|
|
|
@@ -98,6 +99,9 @@ async function main() {
|
|
|
98
99
|
case 'stop':
|
|
99
100
|
await cmdStop();
|
|
100
101
|
break;
|
|
102
|
+
case 'restart':
|
|
103
|
+
await cmdRestart(apiUrl);
|
|
104
|
+
break;
|
|
101
105
|
case 'logs':
|
|
102
106
|
await cmdLogs();
|
|
103
107
|
break;
|
|
@@ -120,6 +124,7 @@ Commands:
|
|
|
120
124
|
connect ${COMMANDS.connect}
|
|
121
125
|
status ${COMMANDS.status}
|
|
122
126
|
stop ${COMMANDS.stop}
|
|
127
|
+
restart ${COMMANDS.restart}
|
|
123
128
|
logs ${COMMANDS.logs}
|
|
124
129
|
|
|
125
130
|
Options:
|
|
@@ -496,7 +501,7 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
496
501
|
worktreeBranch = wt.branch;
|
|
497
502
|
await postStream(apiUrl, issueId, 'worktree_created', { path: wt.path, branch: wt.branch, reused: !wt.created });
|
|
498
503
|
} catch (e) {
|
|
499
|
-
await postStream(apiUrl, issueId, 'worktree_error', { message:
|
|
504
|
+
await postStream(apiUrl, issueId, 'worktree_error', { message: fmtError(e) });
|
|
500
505
|
}
|
|
501
506
|
}
|
|
502
507
|
|
|
@@ -870,14 +875,15 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
|
|
|
870
875
|
} else if (hadError) { await apiClient.post(`${apiUrl}/api/issues/${issueId}/fail`, {}); log(` ✗ ${task.key} failed`); }
|
|
871
876
|
else { await apiClient.post(`${apiUrl}/api/issues/${issueId}/complete`, {}); log(` ✓ ${task.key} complete`); }
|
|
872
877
|
} catch (e) {
|
|
878
|
+
const msg = fmtError(e);
|
|
873
879
|
if (ctx?.runEntry?.stopped) {
|
|
874
880
|
await markStopped(apiUrl, issueId, ctx.runEntry.stopReason || 'stopped');
|
|
875
|
-
log(` ⏹ ${task.key} stopped (${
|
|
881
|
+
log(` ⏹ ${task.key} stopped (${msg})`);
|
|
876
882
|
} else {
|
|
877
|
-
await postStream(apiUrl, issueId, 'error', { message:
|
|
878
|
-
await postComment(`❌ spawn error: ${
|
|
883
|
+
await postStream(apiUrl, issueId, 'error', { message: msg });
|
|
884
|
+
await postComment(`❌ spawn error: ${msg}`);
|
|
879
885
|
await apiClient.post(`${apiUrl}/api/issues/${issueId}/fail`, {});
|
|
880
|
-
log(` ✗ ${task.key} failed: ${
|
|
886
|
+
log(` ✗ ${task.key} failed: ${msg}`);
|
|
881
887
|
}
|
|
882
888
|
}
|
|
883
889
|
}
|
|
@@ -989,7 +995,9 @@ async function executePlanActions(apiUrl: string, parentTask: any, actions: Plan
|
|
|
989
995
|
const headers = { 'x-agent-id': parentTask.agent_id };
|
|
990
996
|
|
|
991
997
|
// Refresh the set of agents linked to this device once per plan execution.
|
|
992
|
-
|
|
998
|
+
if (typeof ctx.refreshLocalAgents === 'function') {
|
|
999
|
+
try { await ctx.refreshLocalAgents(); } catch {}
|
|
1000
|
+
}
|
|
993
1001
|
|
|
994
1002
|
for (const a of actions) {
|
|
995
1003
|
try {
|
|
@@ -1033,6 +1041,23 @@ function stripSelfMention(prompt: string, _agentType?: string): string {
|
|
|
1033
1041
|
return prompt.replace(/^@[A-Za-z0-9_\-]+\s*/, '').trim();
|
|
1034
1042
|
}
|
|
1035
1043
|
|
|
1044
|
+
// Best-effort error → string. Plain Error, ACP error envelopes ({error:{message,code,data}}),
|
|
1045
|
+
// fetch Response-shaped rejections, and arbitrary objects all stringify to something readable
|
|
1046
|
+
// instead of "[object Object]".
|
|
1047
|
+
function fmtError(e: any): string {
|
|
1048
|
+
if (e == null) return 'unknown error';
|
|
1049
|
+
if (typeof e === 'string') return e;
|
|
1050
|
+
if (e instanceof Error) return e.stack ? `${e.message}` : String(e);
|
|
1051
|
+
if (typeof e === 'object') {
|
|
1052
|
+
const inner = e.error ?? e.cause ?? e;
|
|
1053
|
+
const msg = inner?.message ?? e.message ?? e.reason ?? e.statusText;
|
|
1054
|
+
const code = inner?.code ?? e.code ?? e.status;
|
|
1055
|
+
if (msg) return code ? `${msg} (code ${code})` : String(msg);
|
|
1056
|
+
try { return JSON.stringify(e).slice(0, 500); } catch {}
|
|
1057
|
+
}
|
|
1058
|
+
return String(e);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1036
1061
|
function statusIcon(status?: string): string {
|
|
1037
1062
|
switch (status) {
|
|
1038
1063
|
case 'pending': return '⏳';
|
|
@@ -1176,7 +1201,7 @@ function makeCliRunner(agent: { type: string; path: string }): Runner {
|
|
|
1176
1201
|
try {
|
|
1177
1202
|
proc = Bun.spawn([agent.path, ...args], { stdout: 'pipe', stderr: 'pipe', stdin: 'ignore', cwd: (task as any)?.working_dir || undefined });
|
|
1178
1203
|
} catch (e) {
|
|
1179
|
-
yield { event_type: 'error', payload: { message: `spawn failed: ${
|
|
1204
|
+
yield { event_type: 'error', payload: { message: `spawn failed: ${fmtError(e)}` } };
|
|
1180
1205
|
return;
|
|
1181
1206
|
}
|
|
1182
1207
|
|
|
@@ -1352,6 +1377,40 @@ async function cmdStop() {
|
|
|
1352
1377
|
try { process.kill(pid, 'SIGTERM'); console.log(`Sent SIGTERM to ${pid}`); } catch {}
|
|
1353
1378
|
}
|
|
1354
1379
|
|
|
1380
|
+
async function cmdRestart(apiUrl: string) {
|
|
1381
|
+
if (existsSync(PID_PATH)) {
|
|
1382
|
+
const pid = Number(readFileSync(PID_PATH, 'utf8').trim());
|
|
1383
|
+
if (pid && isRunning(pid)) {
|
|
1384
|
+
ensureDirs();
|
|
1385
|
+
writeFileSync(STOP_PATH, '1');
|
|
1386
|
+
try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
1387
|
+
console.log(`⏹ Stopping daemon (pid ${pid})...`);
|
|
1388
|
+
const deadline = Date.now() + 10_000;
|
|
1389
|
+
while (Date.now() < deadline && isRunning(pid)) await sleep(200);
|
|
1390
|
+
if (isRunning(pid)) {
|
|
1391
|
+
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
1392
|
+
await sleep(300);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
try { if (existsSync(PID_PATH)) unlinkSync(PID_PATH); } catch {}
|
|
1396
|
+
}
|
|
1397
|
+
try { if (existsSync(STOP_PATH)) unlinkSync(STOP_PATH); } catch {}
|
|
1398
|
+
console.log('🔄 Relaunching daemon...');
|
|
1399
|
+
ensureDirs();
|
|
1400
|
+
// Spawn `connect` detached (don't re-exec `restart` — would loop).
|
|
1401
|
+
const args = Bun.argv.slice(1).filter(a => a !== '-d' && a !== '--detach' && a !== 'restart');
|
|
1402
|
+
if (!args.includes('connect')) args.splice(1, 0, 'connect');
|
|
1403
|
+
const proc = Bun.spawn([process.execPath, ...args, '--api', apiUrl], {
|
|
1404
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
1405
|
+
env: { ...process.env, MULTI_DETACHED: '1' },
|
|
1406
|
+
});
|
|
1407
|
+
(proc as any).unref?.();
|
|
1408
|
+
await sleep(500);
|
|
1409
|
+
console.log(`✅ Daemon restarted (pid ${proc.pid}).`);
|
|
1410
|
+
console.log(` Tail logs: multi-agent logs`);
|
|
1411
|
+
process.exit(0);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1355
1414
|
async function cmdLogs() {
|
|
1356
1415
|
if (!existsSync(LOG_PATH)) { console.log('No logs yet.'); return; }
|
|
1357
1416
|
const content = readFileSync(LOG_PATH, 'utf8');
|