@shipers-dev/multi 0.12.1 → 0.14.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.
Files changed (2) hide show
  1. package/dist/index.js +147 -68
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -15575,7 +15575,26 @@ class RequestError extends Error {
15575
15575
 
15576
15576
  // src/acp-runner.ts
15577
15577
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
15578
- import { dirname } from "path";
15578
+ import { dirname, join } from "path";
15579
+ function ensureBypassPermissions(cwd) {
15580
+ try {
15581
+ const dir = join(cwd, ".claude");
15582
+ if (!existsSync(dir))
15583
+ mkdirSync(dir, { recursive: true });
15584
+ const path = join(dir, "settings.local.json");
15585
+ let json2 = {};
15586
+ if (existsSync(path)) {
15587
+ try {
15588
+ json2 = JSON.parse(readFileSync(path, "utf8"));
15589
+ } catch {
15590
+ json2 = {};
15591
+ }
15592
+ }
15593
+ json2.permissions = { ...json2.permissions || {}, defaultMode: "bypassPermissions" };
15594
+ writeFileSync(path, JSON.stringify(json2, null, 2) + `
15595
+ `, "utf8");
15596
+ } catch {}
15597
+ }
15579
15598
  function fmtErr(e) {
15580
15599
  if (e == null)
15581
15600
  return "unknown error";
@@ -15605,11 +15624,13 @@ async function runAcp(opts) {
15605
15624
  cleanEnv[k] = v;
15606
15625
  }
15607
15626
  const argv = Array.isArray(opts.adapterBin) ? opts.adapterBin : [opts.adapterBin];
15608
- const permMode = process.env.MULTI_CLAUDE_SAFE === "1" ? "default" : "bypassPermissions";
15627
+ const cwd = opts.cwd || process.cwd();
15628
+ if (process.env.MULTI_CLAUDE_SAFE !== "1")
15629
+ ensureBypassPermissions(cwd);
15609
15630
  const child = Bun.spawn(argv, {
15610
15631
  stdio: ["pipe", "pipe", "inherit"],
15611
- cwd: opts.cwd || process.cwd(),
15612
- env: { ...cleanEnv, ACP_PERMISSION_MODE: permMode }
15632
+ cwd,
15633
+ env: cleanEnv
15613
15634
  });
15614
15635
  try {
15615
15636
  opts.onSpawn?.(child);
@@ -15845,9 +15866,9 @@ function extractText(content) {
15845
15866
 
15846
15867
  // src/acpx-runner.ts
15847
15868
  import { appendFileSync } from "fs";
15848
- import { join } from "path";
15869
+ import { join as join2 } from "path";
15849
15870
  var HOME = process.env.HOME || process.env.USERPROFILE || ".";
15850
- var LOG_PATH = join(HOME, ".multi", "logs", "agent.log");
15871
+ var LOG_PATH = join2(HOME, ".multi", "logs", "agent.log");
15851
15872
  function dlog(msg) {
15852
15873
  try {
15853
15874
  appendFileSync(LOG_PATH, `[${new Date().toISOString()}] ${msg}
@@ -16053,7 +16074,7 @@ var StreamEventInputSchema = exports_external.object({
16053
16074
  // src/worktree.ts
16054
16075
  import { spawn } from "child_process";
16055
16076
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
16056
- import { join as join2 } from "path";
16077
+ import { join as join3 } from "path";
16057
16078
  async function run(cwd, cmd, args) {
16058
16079
  return await new Promise((resolve) => {
16059
16080
  const p = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
@@ -16080,7 +16101,7 @@ async function branchExists(dir, branch) {
16080
16101
  return r.code === 0;
16081
16102
  }
16082
16103
  function ensureGitignoreEntry(workingDir, entry) {
16083
- const gip = join2(workingDir, ".gitignore");
16104
+ const gip = join3(workingDir, ".gitignore");
16084
16105
  let body = "";
16085
16106
  try {
16086
16107
  body = existsSync2(gip) ? readFileSync2(gip, "utf8") : "";
@@ -16109,8 +16130,8 @@ async function ensureWorktree(workingDir, issueKey) {
16109
16130
  ensureGitignoreEntry(workingDir, ".multi/");
16110
16131
  const key = normalizeKey(issueKey);
16111
16132
  const branch = `multi/${key}`;
16112
- const wtDir = join2(workingDir, ".multi", "worktrees");
16113
- const wtPath = join2(wtDir, key);
16133
+ const wtDir = join3(workingDir, ".multi", "worktrees");
16134
+ const wtPath = join3(wtDir, key);
16114
16135
  if (existsSync2(wtPath)) {
16115
16136
  return { path: wtPath, branch, created: false };
16116
16137
  }
@@ -16128,15 +16149,15 @@ async function ensureWorktree(workingDir, issueKey) {
16128
16149
 
16129
16150
  // src/materializer.ts
16130
16151
  import { mkdirSync as mkdirSync3, existsSync as existsSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync3, rmSync, symlinkSync, lstatSync } from "fs";
16131
- import { join as join3, dirname as dirname2 } from "path";
16152
+ import { join as join4, dirname as dirname2 } from "path";
16132
16153
  var HOME2 = process.env.HOME || process.env.USERPROFILE || ".";
16133
- var MULTI_DIR = join3(HOME2, ".multi");
16134
- var MULTI_SKILLS = join3(MULTI_DIR, "skills");
16135
- var MULTI_AGENTS = join3(MULTI_DIR, "agents");
16136
- var STATE_PATH = join3(MULTI_DIR, "materialized.json");
16137
- var CLAUDE_DIR = join3(HOME2, ".claude");
16138
- var CLAUDE_SKILLS = join3(CLAUDE_DIR, "skills");
16139
- var CLAUDE_AGENTS = join3(CLAUDE_DIR, "agents");
16154
+ var MULTI_DIR = join4(HOME2, ".multi");
16155
+ var MULTI_SKILLS = join4(MULTI_DIR, "skills");
16156
+ var MULTI_AGENTS = join4(MULTI_DIR, "agents");
16157
+ var STATE_PATH = join4(MULTI_DIR, "materialized.json");
16158
+ var CLAUDE_DIR = join4(HOME2, ".claude");
16159
+ var CLAUDE_SKILLS = join4(CLAUDE_DIR, "skills");
16160
+ var CLAUDE_AGENTS = join4(CLAUDE_DIR, "agents");
16140
16161
  var MARKER = ".multi-managed";
16141
16162
  function slugify2(s) {
16142
16163
  return s.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "unnamed";
@@ -16161,7 +16182,7 @@ function safeRmManaged(path) {
16161
16182
  rmSync(path);
16162
16183
  return;
16163
16184
  }
16164
- if (st.isDirectory() && existsSync3(join3(path, MARKER))) {
16185
+ if (st.isDirectory() && existsSync3(join4(path, MARKER))) {
16165
16186
  rmSync(path, { recursive: true, force: true });
16166
16187
  return;
16167
16188
  }
@@ -16173,24 +16194,24 @@ function safeRmManaged(path) {
16173
16194
  } catch {}
16174
16195
  }
16175
16196
  function writeManagedSkill(slug, skill) {
16176
- const dir = join3(MULTI_SKILLS, slug);
16197
+ const dir = join4(MULTI_SKILLS, slug);
16177
16198
  mkdirSync3(dir, { recursive: true });
16178
- writeFileSync3(join3(dir, MARKER), `skill_id=${skill.id}
16199
+ writeFileSync3(join4(dir, MARKER), `skill_id=${skill.id}
16179
16200
  revision=${Date.now()}
16180
16201
  `);
16181
16202
  if (skill.body)
16182
- writeFileSync3(join3(dir, "SKILL.md"), skill.body);
16203
+ writeFileSync3(join4(dir, "SKILL.md"), skill.body);
16183
16204
  for (const f of skill.files || []) {
16184
16205
  if (f.path.includes("..") || f.path.startsWith("/"))
16185
16206
  continue;
16186
16207
  if (f.path === MARKER)
16187
16208
  continue;
16188
- const out = join3(dir, f.path);
16209
+ const out = join4(dir, f.path);
16189
16210
  mkdirSync3(dirname2(out), { recursive: true });
16190
16211
  writeFileSync3(out, f.content);
16191
16212
  }
16192
16213
  mkdirSync3(CLAUDE_SKILLS, { recursive: true });
16193
- const link = join3(CLAUDE_SKILLS, slug);
16214
+ const link = join4(CLAUDE_SKILLS, slug);
16194
16215
  safeRmManaged(link);
16195
16216
  if (!existsSync3(link)) {
16196
16217
  try {
@@ -16202,7 +16223,7 @@ function writeManagedAgent(slug, agent, skillsForAgent) {
16202
16223
  if (agent.type !== "claude-code")
16203
16224
  return;
16204
16225
  mkdirSync3(CLAUDE_AGENTS, { recursive: true });
16205
- const out = join3(CLAUDE_AGENTS, `${slug}.md`);
16226
+ const out = join4(CLAUDE_AGENTS, `${slug}.md`);
16206
16227
  safeRmManaged(out);
16207
16228
  const tools = agent.allowed_tools ? `
16208
16229
  tools: ${agent.allowed_tools}` : "";
@@ -16248,13 +16269,13 @@ async function materializeBundle(apiUrl, deviceId, log) {
16248
16269
  }
16249
16270
  for (const old of prev.skill_slugs) {
16250
16271
  if (!newSkillSlugs.includes(old)) {
16251
- safeRmManaged(join3(MULTI_SKILLS, old));
16252
- safeRmManaged(join3(CLAUDE_SKILLS, old));
16272
+ safeRmManaged(join4(MULTI_SKILLS, old));
16273
+ safeRmManaged(join4(CLAUDE_SKILLS, old));
16253
16274
  }
16254
16275
  }
16255
16276
  for (const old of prev.agent_slugs) {
16256
16277
  if (!newAgentSlugs.includes(old)) {
16257
- safeRmManaged(join3(CLAUDE_AGENTS, `${old}.md`));
16278
+ safeRmManaged(join4(CLAUDE_AGENTS, `${old}.md`));
16258
16279
  }
16259
16280
  }
16260
16281
  saveState({ revision: bundle.revision, skill_slugs: newSkillSlugs, agent_slugs: newAgentSlugs });
@@ -16268,11 +16289,11 @@ function lastMaterializedRevision() {
16268
16289
  // src/index.ts
16269
16290
  import { parseArgs } from "util";
16270
16291
  import { mkdirSync as mkdirSync4, existsSync as existsSync4, writeFileSync as writeFileSync4, readFileSync as readFileSync4, appendFileSync as appendFileSync2, unlinkSync, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
16271
- import { join as join4, dirname as dirname3 } from "path";
16292
+ import { join as join5, dirname as dirname3 } from "path";
16272
16293
  // package.json
16273
16294
  var package_default = {
16274
16295
  name: "@shipers-dev/multi",
16275
- version: "0.12.1",
16296
+ version: "0.14.0",
16276
16297
  type: "module",
16277
16298
  bin: {
16278
16299
  "multi-agent": "./dist/index.js"
@@ -16295,14 +16316,14 @@ var package_default = {
16295
16316
 
16296
16317
  // src/index.ts
16297
16318
  var HOME3 = process.env.HOME || process.env.USERPROFILE || ".";
16298
- var MULTI_DIR2 = join4(HOME3, ".multi");
16299
- var CONFIG_PATH = join4(MULTI_DIR2, "config.json");
16300
- var PID_PATH = join4(MULTI_DIR2, "agent.pid");
16301
- var PORT_PATH = join4(MULTI_DIR2, "agent.port");
16302
- var LOG_PATH2 = join4(MULTI_DIR2, "logs", "agent.log");
16303
- var SKILLS_DIR = join4(MULTI_DIR2, "skills");
16304
- var STOP_PATH = join4(MULTI_DIR2, "stop.flag");
16305
- var TASKS_DB_PATH = join4(MULTI_DIR2, "tasks.db");
16319
+ var MULTI_DIR2 = join5(HOME3, ".multi");
16320
+ var CONFIG_PATH = join5(MULTI_DIR2, "config.json");
16321
+ var PID_PATH = join5(MULTI_DIR2, "agent.pid");
16322
+ var PORT_PATH = join5(MULTI_DIR2, "agent.port");
16323
+ var LOG_PATH2 = join5(MULTI_DIR2, "logs", "agent.log");
16324
+ var SKILLS_DIR = join5(MULTI_DIR2, "skills");
16325
+ var STOP_PATH = join5(MULTI_DIR2, "stop.flag");
16326
+ var TASKS_DB_PATH = join5(MULTI_DIR2, "tasks.db");
16306
16327
  var VERSION = package_default.version;
16307
16328
  var COMMANDS = {
16308
16329
  setup: "Register this device with a workspace",
@@ -16315,7 +16336,7 @@ var COMMANDS = {
16315
16336
  reset: "Reset acpx session for an issue (--issue <id>)"
16316
16337
  };
16317
16338
  function ensureDirs() {
16318
- for (const d of [MULTI_DIR2, join4(MULTI_DIR2, "logs"), SKILLS_DIR]) {
16339
+ for (const d of [MULTI_DIR2, join5(MULTI_DIR2, "logs"), SKILLS_DIR]) {
16319
16340
  if (!existsSync4(d))
16320
16341
  mkdirSync4(d, { recursive: true });
16321
16342
  }
@@ -16715,7 +16736,7 @@ async function cmdConnect(apiUrl, config2) {
16715
16736
  try {
16716
16737
  writeFileSync4(PORT_PATH, String(port));
16717
16738
  } catch {}
16718
- let tunnel = await startTunnel(port);
16739
+ let tunnel = await startTunnel(port, log);
16719
16740
  if (!tunnel) {
16720
16741
  log("\u274C cloudflared did not emit a tunnel URL \u2014 is `cloudflared` installed? (`brew install cloudflared`)");
16721
16742
  try {
@@ -16785,7 +16806,7 @@ async function cmdConnect(apiUrl, config2) {
16785
16806
  old?.child.kill();
16786
16807
  } catch {}
16787
16808
  for (let attempt = 1;alive; attempt++) {
16788
- const next = await startTunnel(port);
16809
+ const next = await startTunnel(port, log);
16789
16810
  if (next) {
16790
16811
  tunnel = next;
16791
16812
  log(`\u2601\uFE0F Tunnel up: ${tunnel.url}`);
@@ -16854,21 +16875,77 @@ async function cmdConnect(apiUrl, config2) {
16854
16875
  }
16855
16876
  }
16856
16877
  }
16857
- async function startTunnel(port) {
16878
+ async function startTunnel(port, log2 = () => {}) {
16879
+ const named = process.env.MULTI_TUNNEL_NAME?.trim();
16880
+ const hostname3 = process.env.MULTI_TUNNEL_HOSTNAME?.trim();
16881
+ if (named) {
16882
+ if (!hostname3) {
16883
+ log2("\u274C MULTI_TUNNEL_NAME set but MULTI_TUNNEL_HOSTNAME missing \u2014 set the public hostname routed to this tunnel");
16884
+ return null;
16885
+ }
16886
+ const args = ["tunnel", "--no-autoupdate", "--url", `http://127.0.0.1:${port}`, "run", named];
16887
+ const child2 = Bun.spawn(["cloudflared", ...args], { stdout: "pipe", stderr: "pipe", stdin: "ignore" });
16888
+ const ok = await waitNamedTunnelReady(child2.stderr);
16889
+ if (!ok.ready) {
16890
+ try {
16891
+ child2.kill();
16892
+ } catch {}
16893
+ if (ok.tail) {
16894
+ const lines = ok.tail.split(`
16895
+ `).filter(Boolean).slice(-6);
16896
+ for (const l of lines)
16897
+ log2(` cloudflared: ${l}`);
16898
+ }
16899
+ return null;
16900
+ }
16901
+ const url3 = hostname3.startsWith("http") ? hostname3.replace(/\/+$/, "") : `https://${hostname3}`;
16902
+ return { child: child2, url: url3 };
16903
+ }
16858
16904
  const child = Bun.spawn(["cloudflared", "tunnel", "--no-autoupdate", "--url", `http://127.0.0.1:${port}`], {
16859
16905
  stdout: "pipe",
16860
16906
  stderr: "pipe",
16861
16907
  stdin: "ignore"
16862
16908
  });
16863
- const url2 = await parseTunnelUrl(child.stderr);
16909
+ const { url: url2, tail } = await parseTunnelUrl(child.stderr);
16864
16910
  if (!url2) {
16865
16911
  try {
16866
16912
  child.kill();
16867
16913
  } catch {}
16914
+ if (tail) {
16915
+ const lines = tail.split(`
16916
+ `).filter(Boolean).slice(-6);
16917
+ for (const l of lines)
16918
+ log2(` cloudflared: ${l}`);
16919
+ }
16868
16920
  return null;
16869
16921
  }
16870
16922
  return { child, url: url2 };
16871
16923
  }
16924
+ async function waitNamedTunnelReady(stream2) {
16925
+ const reader = stream2.getReader();
16926
+ const dec = new TextDecoder;
16927
+ const deadline = Date.now() + 30000;
16928
+ let buf = "";
16929
+ while (Date.now() < deadline) {
16930
+ const { value, done } = await reader.read();
16931
+ if (done)
16932
+ break;
16933
+ buf += dec.decode(value, { stream: true });
16934
+ if (/Registered tunnel connection|Connection [a-z0-9-]+ registered/i.test(buf)) {
16935
+ (async () => {
16936
+ try {
16937
+ while (true) {
16938
+ const { done: done2 } = await reader.read();
16939
+ if (done2)
16940
+ break;
16941
+ }
16942
+ } catch {}
16943
+ })();
16944
+ return { ready: true, tail: buf };
16945
+ }
16946
+ }
16947
+ return { ready: false, tail: buf };
16948
+ }
16872
16949
  async function probeTunnel(url2) {
16873
16950
  try {
16874
16951
  const ctrl = new AbortController;
@@ -16934,10 +17011,10 @@ async function parseTunnelUrl(stream2) {
16934
17011
  }
16935
17012
  } catch {}
16936
17013
  })();
16937
- return m[1];
17014
+ return { url: m[1], tail: buf };
16938
17015
  }
16939
17016
  }
16940
- return null;
17017
+ return { url: null, tail: buf };
16941
17018
  }
16942
17019
  async function markStopped(apiUrl, issueId, reason) {
16943
17020
  try {
@@ -16974,13 +17051,13 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
16974
17051
  await postStream(apiUrl, issueId, "progress", { message: `Device ${deviceId} picked up ${isFollowup ? "follow-up" : "task"}` });
16975
17052
  let attachmentRefs = [];
16976
17053
  if (task.from_comment_id) {
16977
- const baseDir = workingDir || join4(MULTI_DIR2, "tmp", issueId);
16978
- const inDir = join4(baseDir, ".multi-in", task.from_comment_id);
17054
+ const baseDir = workingDir || join5(MULTI_DIR2, "tmp", issueId);
17055
+ const inDir = join5(baseDir, ".multi-in", task.from_comment_id);
16979
17056
  attachmentRefs = await downloadCommentAttachments(apiUrl, task.from_comment_id, inDir);
16980
17057
  if (attachmentRefs.length)
16981
17058
  log(` fetched ${attachmentRefs.length} attachment(s) \u2192 ${inDir}`);
16982
17059
  }
16983
- const outDir = join4(workingDir || join4(MULTI_DIR2, "tmp", issueId), ".multi-out");
17060
+ const outDir = join5(workingDir || join5(MULTI_DIR2, "tmp", issueId), ".multi-out");
16984
17061
  let liveCommentId;
16985
17062
  let liveBody = "";
16986
17063
  let hadError = false;
@@ -17706,23 +17783,25 @@ async function resolveAcpAdapter(agentType, detectedPath) {
17706
17783
  if (agentType === "pi" && detectedPath && existsSync4(detectedPath)) {
17707
17784
  return [detectedPath, "--mode", "rpc"];
17708
17785
  }
17709
- const adapterName = "claude-agent-acp";
17710
- const candidates = [
17711
- join4(HOME3, ".bun", "install", "global", "node_modules", ".bin", adapterName)
17712
- ];
17713
- try {
17714
- const here = new URL(import.meta.url).pathname;
17715
- let dir = here;
17716
- for (let i = 0;i < 8; i++) {
17717
- dir = dirname3(dir);
17718
- const bin = join4(dir, "node_modules", ".bin", adapterName);
17719
- if (existsSync4(bin))
17720
- return [bin];
17721
- }
17722
- } catch {}
17723
- for (const c of candidates)
17724
- if (existsSync4(c))
17725
- return [c];
17786
+ const override = process.env.MULTI_ACP_ADAPTER?.trim();
17787
+ const adapterNames = override ? [override] : ["claude-code-acp", "claude-agent-acp"];
17788
+ for (const name of adapterNames) {
17789
+ if (name.startsWith("/") && existsSync4(name))
17790
+ return [name];
17791
+ try {
17792
+ const here = new URL(import.meta.url).pathname;
17793
+ let dir = here;
17794
+ for (let i = 0;i < 8; i++) {
17795
+ dir = dirname3(dir);
17796
+ const bin = join5(dir, "node_modules", ".bin", name);
17797
+ if (existsSync4(bin))
17798
+ return [bin];
17799
+ }
17800
+ } catch {}
17801
+ const global = join5(HOME3, ".bun", "install", "global", "node_modules", ".bin", name);
17802
+ if (existsSync4(global))
17803
+ return [global];
17804
+ }
17726
17805
  return null;
17727
17806
  }
17728
17807
  async function ackDispatch(apiUrl, dispatchId, secret) {
@@ -17773,7 +17852,7 @@ async function postStream(apiUrl, issueId, event_type, payload) {
17773
17852
  try {
17774
17853
  ensureDirs();
17775
17854
  const date5 = new Date().toISOString().slice(0, 10);
17776
- const path = join4(MULTI_DIR2, "logs", `events-${date5}.ndjson`);
17855
+ const path = join5(MULTI_DIR2, "logs", `events-${date5}.ndjson`);
17777
17856
  appendFileSync2(path, JSON.stringify({ ts: Date.now(), issue_id: issueId, event_type, payload }) + `
17778
17857
  `);
17779
17858
  } catch {}
@@ -17796,7 +17875,7 @@ async function downloadCommentAttachments(apiUrl, commentId, destDir) {
17796
17875
  continue;
17797
17876
  const buf = new Uint8Array(await res.arrayBuffer());
17798
17877
  const safe = it.filename.replace(/[^A-Za-z0-9._-]/g, "_");
17799
- const p = join4(destDir, safe);
17878
+ const p = join5(destDir, safe);
17800
17879
  writeFileSync4(p, buf);
17801
17880
  out.push({ filename: it.filename, path: p });
17802
17881
  }
@@ -17817,7 +17896,7 @@ async function uploadOutputDir(apiUrl, commentId, dir) {
17817
17896
  if (depth > 3)
17818
17897
  return;
17819
17898
  for (const name of readdirSync2(d)) {
17820
- const p = join4(d, name);
17899
+ const p = join5(d, name);
17821
17900
  try {
17822
17901
  const st = statSync2(p);
17823
17902
  if (st.isDirectory())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipers-dev/multi",
3
- "version": "0.12.1",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "multi-agent": "./dist/index.js"