@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 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: ${String(e)}`);
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: ${String(e)}`);
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. ${String(e)}` } });
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: ${String(e)}` } });
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.8.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: String(e) });
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 (${String(e)})`);
6658
+ log(` \u23F9 ${task.key} stopped (${msg})`);
6634
6659
  } else {
6635
- await postStream(apiUrl, issueId, "error", { message: String(e) });
6636
- await postComment(`\u274C spawn error: ${String(e)}`);
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: ${String(e)}`);
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
- await ctx.refreshLocalAgents();
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: ${String(e)}` } };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipers-dev/multi",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "multi-agent": "./dist/index.js"
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: ${String(e)}`);
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: ${String(e)}`);
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. ${String(e)}` } });
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: ${String(e)}` } });
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.8.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: String(e) });
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 (${String(e)})`);
881
+ log(` ⏹ ${task.key} stopped (${msg})`);
876
882
  } else {
877
- await postStream(apiUrl, issueId, 'error', { message: String(e) });
878
- await postComment(`❌ spawn error: ${String(e)}`);
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: ${String(e)}`);
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
- await ctx.refreshLocalAgents();
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: ${String(e)}` } };
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');
@@ -1,11 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "mcp__acp__Write",
5
- "Bash(npm install:*)",
6
- "Bash(npx tsc:*)",
7
- "Bash(npx vite build:*)",
8
- "mcp__acp__Edit"
9
- ]
10
- }
11
- }