@shipers-dev/multi 0.9.3 → 0.9.4

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
@@ -5700,15 +5700,35 @@ async function ensureWorktree(workingDir, issueKey) {
5700
5700
  import { parseArgs } from "util";
5701
5701
  import { mkdirSync as mkdirSync3, existsSync as existsSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync3, appendFileSync as appendFileSync2, unlinkSync, readdirSync, statSync } from "fs";
5702
5702
  import { join as join3, dirname as dirname2 } from "path";
5703
+ // package.json
5704
+ var package_default = {
5705
+ name: "@shipers-dev/multi",
5706
+ version: "0.9.4",
5707
+ type: "module",
5708
+ bin: {
5709
+ "multi-agent": "./dist/index.js"
5710
+ },
5711
+ scripts: {
5712
+ dev: "bun run src/index.ts",
5713
+ build: "bun build src/index.ts --outdir=dist --target=bun"
5714
+ },
5715
+ dependencies: {
5716
+ "@zed-industries/agent-client-protocol": "^0.4.5",
5717
+ "@zed-industries/claude-code-acp": "^0.16.2"
5718
+ }
5719
+ };
5720
+
5721
+ // src/index.ts
5703
5722
  var HOME2 = process.env.HOME || process.env.USERPROFILE || ".";
5704
5723
  var MULTI_DIR = join3(HOME2, ".multi");
5705
5724
  var CONFIG_PATH = join3(MULTI_DIR, "config.json");
5706
5725
  var PID_PATH = join3(MULTI_DIR, "agent.pid");
5726
+ var PORT_PATH = join3(MULTI_DIR, "agent.port");
5707
5727
  var LOG_PATH2 = join3(MULTI_DIR, "logs", "agent.log");
5708
5728
  var SKILLS_DIR = join3(MULTI_DIR, "skills");
5709
5729
  var STOP_PATH = join3(MULTI_DIR, "stop.flag");
5710
5730
  var TASKS_DB_PATH = join3(MULTI_DIR, "tasks.db");
5711
- var VERSION = "0.9.2";
5731
+ var VERSION = package_default.version;
5712
5732
  var COMMANDS = {
5713
5733
  setup: "Register this device with a workspace",
5714
5734
  connect: "Connect device to realtime hub and execute assigned tasks",
@@ -5716,7 +5736,8 @@ var COMMANDS = {
5716
5736
  status: "Show current status",
5717
5737
  stop: "Stop the running daemon",
5718
5738
  restart: "Stop and relaunch the daemon in background",
5719
- logs: "View execution logs"
5739
+ logs: "View execution logs",
5740
+ reset: "Reset acpx session for an issue (--issue <id>)"
5720
5741
  };
5721
5742
  function ensureDirs() {
5722
5743
  for (const d of [MULTI_DIR, join3(MULTI_DIR, "logs"), SKILLS_DIR]) {
@@ -5746,7 +5767,8 @@ async function main() {
5746
5767
  name: { type: "string" },
5747
5768
  workspace: { type: "string" },
5748
5769
  agent: { type: "string" },
5749
- api: { type: "string" }
5770
+ api: { type: "string" },
5771
+ issue: { type: "string" }
5750
5772
  },
5751
5773
  allowPositionals: true,
5752
5774
  strict: false
@@ -5787,6 +5809,9 @@ async function main() {
5787
5809
  case "logs":
5788
5810
  await cmdLogs();
5789
5811
  break;
5812
+ case "reset":
5813
+ await cmdReset(args.values.issue);
5814
+ break;
5790
5815
  default:
5791
5816
  console.error(`Unknown command: ${command}`);
5792
5817
  printHelp();
@@ -5807,6 +5832,7 @@ Commands:
5807
5832
  stop ${COMMANDS.stop}
5808
5833
  restart ${COMMANDS.restart}
5809
5834
  logs ${COMMANDS.logs}
5835
+ reset ${COMMANDS.reset}
5810
5836
 
5811
5837
  Options:
5812
5838
  --name <name> Device name
@@ -6033,6 +6059,52 @@ async function cmdConnect(apiUrl, config) {
6033
6059
  }
6034
6060
  })();
6035
6061
  }
6062
+ if (url.pathname === "/reset_session" && req.method === "POST") {
6063
+ if (req.headers.get("authorization") !== expectedAuth)
6064
+ return new Response("unauthorized", { status: 401 });
6065
+ return (async () => {
6066
+ try {
6067
+ const { issue_id, agent_type } = await req.json();
6068
+ if (!issue_id)
6069
+ return Response.json({ error: "issue_id required" }, { status: 400 });
6070
+ const entries = Array.from(running.values()).filter((e) => e.issueId === issue_id);
6071
+ for (const entry of entries) {
6072
+ entry.stopped = true;
6073
+ entry.stopReason = "session reset";
6074
+ try {
6075
+ entry.child?.kill("SIGTERM");
6076
+ } catch {}
6077
+ setTimeout(() => {
6078
+ try {
6079
+ entry.child?.kill("SIGKILL");
6080
+ } catch {}
6081
+ }, 3000);
6082
+ }
6083
+ const acpxTypes = agent_type ? [agent_type] : ["pi", "codex", "openclaw"];
6084
+ const sessionName = `issue-${issue_id}`;
6085
+ const cwd = process.env.HOME || process.cwd();
6086
+ const results = [];
6087
+ for (const t of acpxTypes) {
6088
+ try {
6089
+ const rm = Bun.spawn(["acpx", "--ttl", "0", "--cwd", cwd, t, "sessions", "close", sessionName], {
6090
+ stdout: "pipe",
6091
+ stderr: "pipe",
6092
+ stdin: "ignore"
6093
+ });
6094
+ const err = await new Response(rm.stderr).text();
6095
+ const exit = await rm.exited;
6096
+ results.push({ agent: t, exit, stderr: err.slice(0, 200) });
6097
+ } catch (e) {
6098
+ results.push({ agent: t, exit: -1, stderr: String(e).slice(0, 200) });
6099
+ }
6100
+ }
6101
+ log(`\uD83D\uDD04 reset_session ${issue_id} (killed=${entries.length}) ${results.map((r) => `${r.agent}:${r.exit}`).join(" ")}`);
6102
+ return Response.json({ ok: true, killed: entries.length, sessions: results });
6103
+ } catch (e) {
6104
+ return Response.json({ error: String(e) }, { status: 400 });
6105
+ }
6106
+ })();
6107
+ }
6036
6108
  if (url.pathname === "/stop" && req.method === "POST") {
6037
6109
  if (req.headers.get("authorization") !== expectedAuth)
6038
6110
  return new Response("unauthorized", { status: 401 });
@@ -6069,6 +6141,9 @@ async function cmdConnect(apiUrl, config) {
6069
6141
  }
6070
6142
  });
6071
6143
  log(`\uD83C\uDF10 Local server: http://127.0.0.1:${port}`);
6144
+ try {
6145
+ writeFileSync3(PORT_PATH, String(port));
6146
+ } catch {}
6072
6147
  const cf = Bun.spawn(["cloudflared", "tunnel", "--no-autoupdate", "--url", `http://127.0.0.1:${port}`], {
6073
6148
  stdout: "pipe",
6074
6149
  stderr: "pipe",
@@ -6106,6 +6181,8 @@ async function cmdConnect(apiUrl, config) {
6106
6181
  unlinkSync(PID_PATH);
6107
6182
  if (existsSync3(STOP_PATH))
6108
6183
  unlinkSync(STOP_PATH);
6184
+ if (existsSync3(PORT_PATH))
6185
+ unlinkSync(PORT_PATH);
6109
6186
  db.close();
6110
6187
  log("\uD83D\uDC4B Disconnected");
6111
6188
  process.exit(0);
@@ -7228,6 +7305,33 @@ async function cmdStop() {
7228
7305
  console.log(`Sent SIGTERM to ${pid}`);
7229
7306
  } catch {}
7230
7307
  }
7308
+ async function cmdReset(issueId) {
7309
+ if (!issueId) {
7310
+ console.error("Usage: multi-agent reset --issue <issue_id>");
7311
+ process.exit(2);
7312
+ }
7313
+ if (!existsSync3(PORT_PATH)) {
7314
+ console.error("Daemon not running (no port file).");
7315
+ process.exit(1);
7316
+ }
7317
+ const port = Number(readFileSync3(PORT_PATH, "utf8").trim());
7318
+ const config = loadConfig();
7319
+ if (!config.dispatchSecret) {
7320
+ console.error("No dispatchSecret in config \u2014 run `multi-agent setup` first.");
7321
+ process.exit(1);
7322
+ }
7323
+ const res = await fetch(`http://127.0.0.1:${port}/reset_session`, {
7324
+ method: "POST",
7325
+ headers: { "content-type": "application/json", authorization: `Bearer ${config.dispatchSecret}` },
7326
+ body: JSON.stringify({ issue_id: issueId })
7327
+ });
7328
+ const body = await res.text();
7329
+ if (!res.ok) {
7330
+ console.error(`Reset failed (${res.status}): ${body}`);
7331
+ process.exit(1);
7332
+ }
7333
+ console.log(body);
7334
+ }
7231
7335
  async function cmdRestart(apiUrl) {
7232
7336
  if (existsSync3(PID_PATH)) {
7233
7337
  const pid = Number(readFileSync3(PID_PATH, "utf8").trim());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipers-dev/multi",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "multi-agent": "./dist/index.js"
package/src/index.ts CHANGED
@@ -9,16 +9,18 @@ import { ensureWorktree } from './worktree';
9
9
  import { parseArgs } from 'util';
10
10
  import { mkdirSync, existsSync, writeFileSync, readFileSync, appendFileSync, unlinkSync, readdirSync, statSync } from 'fs';
11
11
  import { join, dirname } from 'path';
12
+ import pkg from '../package.json' with { type: 'json' };
12
13
 
13
14
  const HOME = process.env.HOME || process.env.USERPROFILE || '.';
14
15
  const MULTI_DIR = join(HOME, '.multi');
15
16
  const CONFIG_PATH = join(MULTI_DIR, 'config.json');
16
17
  const PID_PATH = join(MULTI_DIR, 'agent.pid');
18
+ const PORT_PATH = join(MULTI_DIR, 'agent.port');
17
19
  const LOG_PATH = join(MULTI_DIR, 'logs', 'agent.log');
18
20
  const SKILLS_DIR = join(MULTI_DIR, 'skills');
19
21
  const STOP_PATH = join(MULTI_DIR, 'stop.flag');
20
22
  const TASKS_DB_PATH = join(MULTI_DIR, 'tasks.db');
21
- const VERSION = '0.9.2';
23
+ const VERSION = (pkg as { version: string }).version;
22
24
 
23
25
  const COMMANDS = {
24
26
  setup: 'Register this device with a workspace',
@@ -28,6 +30,7 @@ const COMMANDS = {
28
30
  stop: 'Stop the running daemon',
29
31
  restart: 'Stop and relaunch the daemon in background',
30
32
  logs: 'View execution logs',
33
+ reset: 'Reset acpx session for an issue (--issue <id>)',
31
34
  } as const;
32
35
 
33
36
  type Command = keyof typeof COMMANDS;
@@ -62,6 +65,7 @@ async function main() {
62
65
  workspace: { type: 'string' },
63
66
  agent: { type: 'string' },
64
67
  api: { type: 'string' },
68
+ issue: { type: 'string' },
65
69
  },
66
70
  allowPositionals: true,
67
71
  strict: false,
@@ -105,6 +109,9 @@ async function main() {
105
109
  case 'logs':
106
110
  await cmdLogs();
107
111
  break;
112
+ case 'reset':
113
+ await cmdReset(args.values.issue);
114
+ break;
108
115
  default:
109
116
  console.error(`Unknown command: ${command}`);
110
117
  printHelp();
@@ -126,6 +133,7 @@ Commands:
126
133
  stop ${COMMANDS.stop}
127
134
  restart ${COMMANDS.restart}
128
135
  logs ${COMMANDS.logs}
136
+ reset ${COMMANDS.reset}
129
137
 
130
138
  Options:
131
139
  --name <name> Device name
@@ -354,6 +362,46 @@ async function cmdConnect(apiUrl: string, config: Config) {
354
362
  }
355
363
  })();
356
364
  }
365
+ if (url.pathname === '/reset_session' && req.method === 'POST') {
366
+ if (req.headers.get('authorization') !== expectedAuth) return new Response('unauthorized', { status: 401 });
367
+ return (async () => {
368
+ try {
369
+ const { issue_id, agent_type } = await req.json() as { issue_id: string; agent_type?: string };
370
+ if (!issue_id) return Response.json({ error: 'issue_id required' }, { status: 400 });
371
+
372
+ // Kill any running acpx child for this issue.
373
+ const entries = Array.from(running.values()).filter((e) => e.issueId === issue_id);
374
+ for (const entry of entries) {
375
+ entry.stopped = true;
376
+ entry.stopReason = 'session reset';
377
+ try { entry.child?.kill('SIGTERM'); } catch {}
378
+ setTimeout(() => { try { entry.child?.kill('SIGKILL'); } catch {} }, 3000);
379
+ }
380
+
381
+ // Close acpx session so next dispatch creates fresh. Only known acpx types.
382
+ const acpxTypes = agent_type ? [agent_type] : ['pi', 'codex', 'openclaw'];
383
+ const sessionName = `issue-${issue_id}`;
384
+ const cwd = process.env.HOME || process.cwd();
385
+ const results: { agent: string; exit: number; stderr: string }[] = [];
386
+ for (const t of acpxTypes) {
387
+ try {
388
+ const rm = Bun.spawn(['acpx', '--ttl', '0', '--cwd', cwd, t, 'sessions', 'close', sessionName], {
389
+ stdout: 'pipe', stderr: 'pipe', stdin: 'ignore',
390
+ });
391
+ const err = await new Response(rm.stderr as any).text();
392
+ const exit = await rm.exited;
393
+ results.push({ agent: t, exit, stderr: err.slice(0, 200) });
394
+ } catch (e) {
395
+ results.push({ agent: t, exit: -1, stderr: String(e).slice(0, 200) });
396
+ }
397
+ }
398
+ log(`🔄 reset_session ${issue_id} (killed=${entries.length}) ${results.map(r => `${r.agent}:${r.exit}`).join(' ')}`);
399
+ return Response.json({ ok: true, killed: entries.length, sessions: results });
400
+ } catch (e) {
401
+ return Response.json({ error: String(e) }, { status: 400 });
402
+ }
403
+ })();
404
+ }
357
405
  if (url.pathname === '/stop' && req.method === 'POST') {
358
406
  if (req.headers.get('authorization') !== expectedAuth) return new Response('unauthorized', { status: 401 });
359
407
  return (async () => {
@@ -382,6 +430,7 @@ async function cmdConnect(apiUrl: string, config: Config) {
382
430
  },
383
431
  });
384
432
  log(`🌐 Local server: http://127.0.0.1:${port}`);
433
+ try { writeFileSync(PORT_PATH, String(port)); } catch {}
385
434
 
386
435
  // Spawn cloudflared quick tunnel
387
436
  const cf = Bun.spawn(['cloudflared', 'tunnel', '--no-autoupdate', '--url', `http://127.0.0.1:${port}`], {
@@ -409,6 +458,7 @@ async function cmdConnect(apiUrl: string, config: Config) {
409
458
  try { await apiClient.post(`${apiUrl}/api/devices/${config.deviceId}/heartbeat`, { status: 'offline', tunnel_url: null }); } catch {}
410
459
  if (existsSync(PID_PATH)) unlinkSync(PID_PATH);
411
460
  if (existsSync(STOP_PATH)) unlinkSync(STOP_PATH);
461
+ if (existsSync(PORT_PATH)) unlinkSync(PORT_PATH);
412
462
  db.close();
413
463
  log('👋 Disconnected');
414
464
  process.exit(0);
@@ -1406,6 +1456,22 @@ async function cmdStop() {
1406
1456
  try { process.kill(pid, 'SIGTERM'); console.log(`Sent SIGTERM to ${pid}`); } catch {}
1407
1457
  }
1408
1458
 
1459
+ async function cmdReset(issueId?: string) {
1460
+ if (!issueId) { console.error('Usage: multi-agent reset --issue <issue_id>'); process.exit(2); }
1461
+ if (!existsSync(PORT_PATH)) { console.error('Daemon not running (no port file).'); process.exit(1); }
1462
+ const port = Number(readFileSync(PORT_PATH, 'utf8').trim());
1463
+ const config = loadConfig();
1464
+ if (!config.dispatchSecret) { console.error('No dispatchSecret in config — run `multi-agent setup` first.'); process.exit(1); }
1465
+ const res = await fetch(`http://127.0.0.1:${port}/reset_session`, {
1466
+ method: 'POST',
1467
+ headers: { 'content-type': 'application/json', 'authorization': `Bearer ${config.dispatchSecret}` },
1468
+ body: JSON.stringify({ issue_id: issueId }),
1469
+ });
1470
+ const body = await res.text();
1471
+ if (!res.ok) { console.error(`Reset failed (${res.status}): ${body}`); process.exit(1); }
1472
+ console.log(body);
1473
+ }
1474
+
1409
1475
  async function cmdRestart(apiUrl: string) {
1410
1476
  if (existsSync(PID_PATH)) {
1411
1477
  const pid = Number(readFileSync(PID_PATH, 'utf8').trim());