@shipers-dev/multi 0.63.0 → 0.65.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 +305 -31
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -17116,29 +17116,49 @@ async function ensureWorktree(workingDir, issueKey, issueId, opts) {
17116
17116
  return { path: workingDir, branch: "", created: false };
17117
17117
  }
17118
17118
  ensureGitignoreEntry(workingDir, ".multi/");
17119
- const key = normalizeKey(issueKey);
17120
- const branch = `multi/${key}`;
17119
+ const slug = normalizeKey(opts?.worktreeName ?? issueKey);
17120
+ const branch = `multi/${slug}`;
17121
17121
  const wtDir = join5(workingDir, ".multi", "worktrees");
17122
- const wtPath = join5(wtDir, key);
17122
+ const wtPath = join5(wtDir, slug);
17123
+ let baseRef = "HEAD";
17124
+ const explicit = opts?.baseBranch || opts?.parentBranch || null;
17125
+ if (explicit) {
17126
+ const localOk = await branchExists(workingDir, explicit);
17127
+ if (localOk) {
17128
+ baseRef = explicit;
17129
+ } else {
17130
+ const fetched = await run3(workingDir, "git", ["fetch", "origin", `${explicit}:${explicit}`]);
17131
+ if (fetched.code === 0)
17132
+ baseRef = explicit;
17133
+ }
17134
+ }
17123
17135
  if (existsSync3(wtPath)) {
17136
+ if (await isDirty(wtPath)) {
17137
+ throw new WorktreeDirtyError({ path: wtPath, branch, reason: "dirty" });
17138
+ }
17139
+ if (opts?.baseBranch) {
17140
+ const mergeBase = await run3(wtPath, "git", ["merge-base", branch, opts.baseBranch]);
17141
+ if (mergeBase.code === 0 && mergeBase.stdout) {
17142
+ const forkOnBase = await run3(workingDir, "git", ["merge-base", "--is-ancestor", mergeBase.stdout, opts.baseBranch]);
17143
+ if (forkOnBase.code === 1) {
17144
+ const baseHead = await run3(workingDir, "git", ["rev-parse", "--verify", `${opts.baseBranch}^{commit}`]);
17145
+ throw new WorktreeDirtyError({
17146
+ path: wtPath,
17147
+ branch,
17148
+ reason: "base_mismatch",
17149
+ expectedBase: opts.baseBranch,
17150
+ actualBase: baseHead.code === 0 ? baseHead.stdout : mergeBase.stdout
17151
+ });
17152
+ }
17153
+ }
17154
+ }
17124
17155
  if (issueId)
17125
- setWorktreeIndexEntry(workingDir, key, issueId);
17156
+ setWorktreeIndexEntry(workingDir, slug, issueId);
17126
17157
  return { path: wtPath, branch, created: false };
17127
17158
  }
17128
17159
  try {
17129
17160
  mkdirSync3(wtDir, { recursive: true });
17130
17161
  } catch {}
17131
- let baseRef = "HEAD";
17132
- if (opts?.parentBranch) {
17133
- const localOk = await branchExists(workingDir, opts.parentBranch);
17134
- if (localOk) {
17135
- baseRef = opts.parentBranch;
17136
- } else {
17137
- const fetched = await run3(workingDir, "git", ["fetch", "origin", `${opts.parentBranch}:${opts.parentBranch}`]);
17138
- if (fetched.code === 0)
17139
- baseRef = opts.parentBranch;
17140
- }
17141
- }
17142
17162
  const exists4 = await branchExists(workingDir, branch);
17143
17163
  const args2 = exists4 ? ["worktree", "add", wtPath, branch] : ["worktree", "add", "-b", branch, wtPath, baseRef];
17144
17164
  const r = await run3(workingDir, "git", args2);
@@ -17147,7 +17167,7 @@ async function ensureWorktree(workingDir, issueKey, issueId, opts) {
17147
17167
  }
17148
17168
  await linkIgnoredFiles(workingDir, wtPath);
17149
17169
  if (issueId)
17150
- setWorktreeIndexEntry(workingDir, key, issueId);
17170
+ setWorktreeIndexEntry(workingDir, slug, issueId);
17151
17171
  return { path: wtPath, branch, created: true };
17152
17172
  }
17153
17173
  async function linkIgnoredFiles(workingDir, wtPath) {
@@ -17274,7 +17294,17 @@ async function squashMergeChild(wtPath, childBranch, childKey) {
17274
17294
  result.message = `${childKey} squash-merged (${stagedFiles.length} file${stagedFiles.length === 1 ? "" : "s"})`;
17275
17295
  return result;
17276
17296
  }
17277
- var init_worktree = () => {};
17297
+ var WorktreeDirtyError;
17298
+ var init_worktree = __esm(() => {
17299
+ WorktreeDirtyError = class WorktreeDirtyError extends Error {
17300
+ details;
17301
+ code = "E_WORKTREE_DIRTY";
17302
+ constructor(details) {
17303
+ super(`worktree dirty or base-mismatched at ${details.path} (${details.reason})`);
17304
+ this.details = details;
17305
+ }
17306
+ };
17307
+ });
17278
17308
 
17279
17309
  // ../../node_modules/zod/v4/core/core.js
17280
17310
  function $constructor(name, initializer, params) {
@@ -34022,7 +34052,8 @@ var init_streams = __esm(() => {
34022
34052
  "stopped",
34023
34053
  "queued",
34024
34054
  "worktree_created",
34025
- "worktree_error"
34055
+ "worktree_error",
34056
+ "worktree_dirty"
34026
34057
  ];
34027
34058
  StreamEventTypeSchema = exports_external.enum(STREAM_EVENT_TYPES);
34028
34059
  StreamEventInputSchema = exports_external.object({
@@ -34220,7 +34251,7 @@ function parsePlanBlocks(text) {
34220
34251
  }
34221
34252
  return { actions, errors: errors3 };
34222
34253
  }
34223
- var PLAN_SCHEMA_VERSION = 10, Priority, AssigneeType, IssueStatus, SessionRole, SkillFile, EvalPolicy, PlanActionSchema, PlanEnvelopeSchema, UiBlockSchema, UI_FENCE_RE, FENCE_RE;
34254
+ var PLAN_SCHEMA_VERSION = 11, Priority, AssigneeType, IssueStatus, SessionRole, SkillFile, EvalPolicy, PlanActionSchema, PlanEnvelopeSchema, UiBlockSchema, UI_FENCE_RE, FENCE_RE;
34224
34255
  var init_plans = __esm(() => {
34225
34256
  init_zod();
34226
34257
  Priority = exports_external.enum(["low", "medium", "high"]);
@@ -34243,6 +34274,8 @@ var init_plans = __esm(() => {
34243
34274
  priority: Priority.optional(),
34244
34275
  assignee_type: AssigneeType.optional(),
34245
34276
  assignee_id: exports_external.string().optional(),
34277
+ base_ref: exports_external.string().min(1).max(200).optional(),
34278
+ worktree: exports_external.string().min(1).max(60).regex(/^[a-z0-9][a-z0-9_\-\/]*$/i).optional(),
34246
34279
  parent_id: exports_external.string().optional(),
34247
34280
  blocked_by: exports_external.array(exports_external.string().min(1)).optional(),
34248
34281
  await_children: exports_external.boolean().optional(),
@@ -34853,9 +34886,54 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
34853
34886
  const ISSUE_BASE = tenantWsId && projectId ? `${apiUrl}/api/workspaces/${tenantWsId}/projects/${projectId}/issues/${issueId}` : null;
34854
34887
  let workingDir = baseWorkingDir;
34855
34888
  let worktreeBranch = "";
34856
- if (baseWorkingDir) {
34889
+ const inlineMode = task.inline_mode === true;
34890
+ const baseBranch = task.base_branch ?? null;
34891
+ const worktreeName = task.worktree_name ?? null;
34892
+ let inlineRestoreHead = null;
34893
+ const blockOnDirty = async (reason, extra = {}) => {
34894
+ await postStream(apiUrl, issueId, "worktree_dirty", { reason, ...extra });
34895
+ if (tenantWsId) {
34896
+ try {
34897
+ await patchIssueStatus(apiUrl, tenantWsId, issueId, "blocked");
34898
+ } catch {}
34899
+ }
34900
+ await postStream(apiUrl, issueId, "run_finished", { stopReason: "blocked", duration_ms: 0 });
34901
+ };
34902
+ if (baseWorkingDir && inlineMode) {
34903
+ try {
34904
+ const status3 = Bun.spawnSync(["git", "status", "--porcelain"], { cwd: baseWorkingDir, stdout: "pipe" });
34905
+ const dirty = (status3.stdout?.toString() || "").trim().length > 0;
34906
+ if (dirty) {
34907
+ await blockOnDirty("inline_dirty", { path: baseWorkingDir });
34908
+ return;
34909
+ }
34910
+ if (baseBranch) {
34911
+ const origSymRef = Bun.spawnSync(["git", "symbolic-ref", "--quiet", "HEAD"], { cwd: baseWorkingDir, stdout: "pipe" });
34912
+ if (origSymRef.exitCode === 0) {
34913
+ inlineRestoreHead = (origSymRef.stdout?.toString() || "").trim().replace(/^refs\/heads\//, "") || null;
34914
+ } else {
34915
+ const origSha = Bun.spawnSync(["git", "rev-parse", "HEAD"], { cwd: baseWorkingDir, stdout: "pipe" });
34916
+ inlineRestoreHead = (origSha.stdout?.toString() || "").trim() || null;
34917
+ }
34918
+ const co = Bun.spawnSync(["git", "checkout", baseBranch], { cwd: baseWorkingDir, stdout: "pipe", stderr: "pipe" });
34919
+ if (co.exitCode !== 0) {
34920
+ await postStream(apiUrl, issueId, "worktree_error", { mode: "inline", message: co.stderr?.toString() || "checkout failed" });
34921
+ await blockOnDirty("inline_checkout_failed", { baseBranch });
34922
+ return;
34923
+ }
34924
+ worktreeBranch = baseBranch;
34925
+ }
34926
+ await postStream(apiUrl, issueId, "worktree_created", { path: baseWorkingDir, branch: worktreeBranch, reused: true, inline: true });
34927
+ } catch (e) {
34928
+ await postStream(apiUrl, issueId, "worktree_error", { mode: "inline", message: fmtError(e) });
34929
+ }
34930
+ } else if (baseWorkingDir) {
34857
34931
  try {
34858
- const wt = await ensureWorktree(baseWorkingDir, task.key || issueId, issueId, { parentBranch: task.parent_branch ?? undefined });
34932
+ const wt = await ensureWorktree(baseWorkingDir, task.key || issueId, issueId, {
34933
+ baseBranch: baseBranch ?? undefined,
34934
+ parentBranch: task.parent_branch ?? undefined,
34935
+ worktreeName: worktreeName ?? undefined
34936
+ });
34859
34937
  workingDir = wt.path;
34860
34938
  worktreeBranch = wt.branch;
34861
34939
  await postStream(apiUrl, issueId, "worktree_created", { path: wt.path, branch: wt.branch, reused: !wt.created });
@@ -34867,6 +34945,10 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
34867
34945
  }
34868
34946
  }
34869
34947
  } catch (e) {
34948
+ if (e?.code === "E_WORKTREE_DIRTY") {
34949
+ await blockOnDirty("worktree_dirty", { details: e.details });
34950
+ return;
34951
+ }
34870
34952
  await postStream(apiUrl, issueId, "worktree_error", { message: fmtError(e) });
34871
34953
  }
34872
34954
  }
@@ -35110,6 +35192,10 @@ _${bits.join(" · ")}_`);
35110
35192
  const acpCapable = detected.filter((d) => d.type === "claude-code");
35111
35193
  const useAcp = preferType !== "pi" && acpCapable.length > 0 && process.env.MULTI_LEGACY !== "1";
35112
35194
  const useAcpx = !useAcp && preferType && ["pi", "codex", "openclaw"].includes(preferType) && process.env.MULTI_LEGACY !== "1";
35195
+ let issueHistoryBlock = "";
35196
+ if (tenantWsId && ISSUE_BASE) {
35197
+ issueHistoryBlock = await fetchIssueHistory(apiUrl, tenantWsId, projectId, issueId, log3);
35198
+ }
35113
35199
  try {
35114
35200
  if (useAcp) {
35115
35201
  const issueContext = `## Issue ${task.key}: ${task.title}${task.description ? `
@@ -35127,6 +35213,20 @@ ${task.description}` : ""}`;
35127
35213
  ${cleanFollowup}`;
35128
35214
  } else {
35129
35215
  userPart = stripSelfMention(issueContext, preferType);
35216
+ userPart += `
35217
+
35218
+ ---
35219
+
35220
+ You have been assigned this issue. Investigate and resolve it. Read the codebase, understand the problem, implement a fix or complete the described work, and use your tools to make progress. When finished, emit a multi-plan action to mark the issue done (or blocked if you need human input).`;
35221
+ }
35222
+ if (issueHistoryBlock) {
35223
+ userPart += `
35224
+
35225
+ ---
35226
+
35227
+ ## Issue history (prior activity)
35228
+
35229
+ ${issueHistoryBlock}`;
35130
35230
  }
35131
35231
  if (attachmentRefs.length) {
35132
35232
  const lines = attachmentRefs.map((a) => `- ${a.filename}: ${a.path}`).join(`
@@ -35145,6 +35245,7 @@ Note: if (and only if) you produce binary or large artifact outputs (screenshots
35145
35245
  Respond in the chat. Only if you produce large artifact files (screenshots, data exports, generated source code), write them under ${outDir}. Do not put your answer in a file.`;
35146
35246
  }
35147
35247
  let preamble = "";
35248
+ let preambleFailed = false;
35148
35249
  try {
35149
35250
  if (tenantWsId) {
35150
35251
  const agentRes = await apiClient.get(`${apiUrl}/api/workspaces/${tenantWsId}/agents/${task.agent_id}`);
@@ -35174,7 +35275,9 @@ ${body}
35174
35275
  }
35175
35276
  }
35176
35277
  } catch (e) {
35278
+ preambleFailed = true;
35177
35279
  log3(`preamble fetch failed: ${String(e)}`);
35280
+ await postBoundChatMessage(apiUrl, task, `⚠️ System: agent preamble (prompt + skills) fetch failed — the agent is running without its role definition. Error: ${String(e).slice(0, 200)}`);
35178
35281
  }
35179
35282
  preamble += await buildPlanningPreamble(apiUrl, task);
35180
35283
  const prompt = preamble ? `${preamble}
@@ -35255,7 +35358,10 @@ ${body}
35255
35358
  }
35256
35359
  }
35257
35360
  }
35258
- } catch {}
35361
+ } catch (e) {
35362
+ log3(`preamble fetch failed (acpx): ${String(e)}`);
35363
+ await postBoundChatMessage(apiUrl, task, `⚠️ System: agent preamble fetch failed — running without role definition. Error: ${String(e).slice(0, 200)}`);
35364
+ }
35259
35365
  preamble += await buildPlanningPreamble(apiUrl, task);
35260
35366
  const issueContext = `## Issue ${task.key}: ${task.title}${task.description ? `
35261
35367
 
@@ -35272,6 +35378,20 @@ ${task.description}` : ""}`;
35272
35378
  ${cleanFollowup}`;
35273
35379
  } else {
35274
35380
  userPart = stripSelfMention(issueContext, preferType);
35381
+ userPart += `
35382
+
35383
+ ---
35384
+
35385
+ You have been assigned this issue. Investigate and resolve it. Read the codebase, understand the problem, implement a fix or complete the described work, and use your tools to make progress. When finished, emit a multi-plan action to mark the issue done (or blocked if you need human input).`;
35386
+ }
35387
+ if (issueHistoryBlock) {
35388
+ userPart += `
35389
+
35390
+ ---
35391
+
35392
+ ## Issue history (prior activity)
35393
+
35394
+ ${issueHistoryBlock}`;
35275
35395
  }
35276
35396
  if (attachmentRefs.length) {
35277
35397
  const lines = attachmentRefs.map((a) => `- ${a.filename}: ${a.path}`).join(`
@@ -35398,6 +35518,58 @@ ${userPart}` : userPart;
35398
35518
  await apiClient.post(`${ISSUE_BASE}/fail`, {});
35399
35519
  log3(` ✗ ${task.key} failed: ${msg}`);
35400
35520
  }
35521
+ } finally {
35522
+ if (inlineMode && inlineRestoreHead && baseWorkingDir) {
35523
+ try {
35524
+ Bun.spawnSync(["git", "checkout", inlineRestoreHead], { cwd: baseWorkingDir, stdout: "pipe", stderr: "pipe" });
35525
+ } catch {}
35526
+ }
35527
+ }
35528
+ }
35529
+ async function fetchIssueHistory(apiUrl, tenantWsId, projectId, issueId, log4) {
35530
+ const MAX_HISTORY_CHARS = 6000;
35531
+ try {
35532
+ const issueBase = `${apiUrl}/api/workspaces/${tenantWsId}/projects/${projectId}/issues/${issueId}`;
35533
+ const res = await apiClient.get(`${issueBase}/activity`);
35534
+ const activities = Array.isArray(res.data?.results) ? res.data.results : Array.isArray(res.data) ? res.data : [];
35535
+ if (!activities.length)
35536
+ return "";
35537
+ const lines = [];
35538
+ const recent = activities.slice(-20).reverse();
35539
+ for (const act of recent) {
35540
+ const action = act.action || "activity";
35541
+ const actor = act.actor_name || act.actor_id || "system";
35542
+ const ts = act.created_at ? new Date(act.created_at * 1000).toISOString().slice(0, 16) : "";
35543
+ let line = `- **${actor}** ${action}`;
35544
+ if (ts)
35545
+ line += ` _${ts}_`;
35546
+ if (act.details) {
35547
+ try {
35548
+ const d = typeof act.details === "string" ? JSON.parse(act.details) : act.details;
35549
+ if (d.message)
35550
+ line += `: ${String(d.message).slice(0, 200)}`;
35551
+ else if (d.title)
35552
+ line += `: ${String(d.title).slice(0, 200)}`;
35553
+ else if (d.from && d.to)
35554
+ line += `: ${d.from} → ${d.to}`;
35555
+ else if (d.text)
35556
+ line += `: ${String(d.text).slice(0, 200)}`;
35557
+ } catch {}
35558
+ }
35559
+ lines.push(line);
35560
+ }
35561
+ const block = lines.join(`
35562
+ `);
35563
+ if (block.length > MAX_HISTORY_CHARS) {
35564
+ log4(` issue history truncated: ${block.length} → ${MAX_HISTORY_CHARS} chars`);
35565
+ return block.slice(0, MAX_HISTORY_CHARS) + `
35566
+
35567
+ _… truncated_`;
35568
+ }
35569
+ return block;
35570
+ } catch (e) {
35571
+ log4(` issue history fetch failed: ${String(e).slice(0, 200)}`);
35572
+ return "";
35401
35573
  }
35402
35574
  }
35403
35575
  async function buildPlanningPreamble(apiUrl, task, _wsId) {
@@ -35598,7 +35770,7 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx, parseErrors
35598
35770
  }
35599
35771
  }
35600
35772
  } else if (a.type === "delegate") {
35601
- const res = await apiClient.post(mutateUrl, { action: "update", id: a.id, assignee_type: "agent", assignee_id: a.assignee_id, status: "todo" }, { headers });
35773
+ const res = await apiClient.post(mutateUrl, { action: "update", id: a.id, assignee_type: "agent", assignee_id: a.assignee_id, status: "in_progress" }, { headers });
35602
35774
  if (!res.success) {
35603
35775
  lines.push(`- [err] delegate ${a.id}: ${res.error || res.status}`);
35604
35776
  results.push({ type: "delegate", status: "error", error: String(res.error || res.status), label: a.id });
@@ -37035,7 +37207,7 @@ async function executeChatPlanActions(actionsIn, parseErrors, ctx) {
37035
37207
  results.push({ type: "update", status: "ok", issue_id: res.data.id, key: res.data.key, status_to: a.status ?? null, title_to: a.title ?? null });
37036
37208
  tally(true);
37037
37209
  } else if (a.type === "delegate") {
37038
- const res = await apiClient.post(mutateUrl, { action: "update", id: a.id, assignee_type: "agent", assignee_id: a.assignee_id, status: "todo" }, { headers });
37210
+ const res = await apiClient.post(mutateUrl, { action: "update", id: a.id, assignee_type: "agent", assignee_id: a.assignee_id, status: "in_progress" }, { headers });
37039
37211
  if (!res.success) {
37040
37212
  lines.push(`- [err] delegate ${a.id}: ${res.error || res.status}`);
37041
37213
  results.push({ type: "delegate", status: "error", error: String(res.error || res.status), label: a.id });
@@ -38681,7 +38853,7 @@ import { parseArgs } from "util";
38681
38853
  // package.json
38682
38854
  var package_default = {
38683
38855
  name: "@shipers-dev/multi",
38684
- version: "0.63.0",
38856
+ version: "0.65.0",
38685
38857
  type: "module",
38686
38858
  bin: {
38687
38859
  "multi-agent": "./dist/index.js"
@@ -39762,6 +39934,66 @@ import { homedir as homedir4 } from "os";
39762
39934
  import { join as join16 } from "path";
39763
39935
  init_adapter_pidfile();
39764
39936
  init_errors();
39937
+ // package.json
39938
+ var package_default2 = {
39939
+ name: "@shipers-dev/multi",
39940
+ version: "0.65.0",
39941
+ type: "module",
39942
+ bin: {
39943
+ "multi-agent": "./dist/index.js"
39944
+ },
39945
+ files: [
39946
+ "dist"
39947
+ ],
39948
+ scripts: {
39949
+ dev: "MULTI_HOME=${MULTI_HOME:-$HOME/.multi-dev} MULTI_API=${MULTI_API:-http://127.0.0.1:8787} bun run src/index.ts connect",
39950
+ build: "bun build src/index.ts --outdir=dist --target=node --sourcemap=none --external undici --external loro-crdt && node scripts/post-build.cjs",
39951
+ prepublishOnly: "bun run build"
39952
+ },
39953
+ dependencies: {
39954
+ "@agentclientprotocol/claude-agent-acp": "^0.31.0",
39955
+ "@agentclientprotocol/sdk": "^0.20.0",
39956
+ "@effect/platform-node": "^0.103.0",
39957
+ effect: "^3.21.2",
39958
+ "loro-crdt": "^1.12.1",
39959
+ undici: "^7.0.0"
39960
+ },
39961
+ devDependencies: {
39962
+ "@multi/lib": "workspace:*"
39963
+ }
39964
+ };
39965
+
39966
+ // src/_impl/daemon-main.ts
39967
+ var CLI_VERSION = package_default2.version;
39968
+ function findBunBinary() {
39969
+ const tryPath = (p) => {
39970
+ try {
39971
+ return existsSync16(p) ? p : null;
39972
+ } catch {
39973
+ return null;
39974
+ }
39975
+ };
39976
+ if (process.execPath && process.execPath.endsWith("/bun")) {
39977
+ const hit = tryPath(process.execPath);
39978
+ if (hit)
39979
+ return hit;
39980
+ }
39981
+ const fromWhich = Bun.which?.("bun");
39982
+ if (fromWhich)
39983
+ return fromWhich;
39984
+ const candidates = [
39985
+ join16(homedir4(), ".bun", "bin", "bun"),
39986
+ "/opt/homebrew/bin/bun",
39987
+ "/usr/local/bin/bun",
39988
+ "/home/linuxbrew/.linuxbrew/bin/bun"
39989
+ ];
39990
+ for (const c of candidates) {
39991
+ const hit = tryPath(c);
39992
+ if (hit)
39993
+ return hit;
39994
+ }
39995
+ return null;
39996
+ }
39765
39997
  var MULTI_DIR5 = join16(homedir4(), ".multi");
39766
39998
  var PID_PATH2 = join16(MULTI_DIR5, "agent.pid");
39767
39999
  var PORT_PATH2 = join16(MULTI_DIR5, "agent.port");
@@ -40132,7 +40364,7 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
40132
40364
  }
40133
40365
  const route = () => {
40134
40366
  if (url2.pathname === "/health")
40135
- return Response.json({ ok: true, device_id: cfg.deviceId });
40367
+ return Response.json({ ok: true, device_id: cfg.deviceId, cli_version: CLI_VERSION });
40136
40368
  if (url2.pathname === "/files" && req.method === "GET") {
40137
40369
  if (req.headers.get("authorization") !== expectedAuth)
40138
40370
  return new Response("unauthorized", { status: 401 });
@@ -40539,6 +40771,47 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
40539
40771
  }
40540
40772
  })();
40541
40773
  }
40774
+ if (url2.pathname === "/upgrade" && req.method === "POST") {
40775
+ if (req.headers.get("authorization") !== expectedAuth)
40776
+ return new Response("unauthorized", { status: 401 });
40777
+ return (async () => {
40778
+ const bunBin = findBunBinary();
40779
+ if (!bunBin) {
40780
+ return Response.json({ error: "bun binary not found; run `bun add -g @shipers-dev/multi` manually" }, { status: 503 });
40781
+ }
40782
+ let body = {};
40783
+ try {
40784
+ body = await req.json();
40785
+ } catch {}
40786
+ const spec = body.target ? `@shipers-dev/multi@${body.target}` : "@shipers-dev/multi";
40787
+ log3(`[upgrade] spawning ${bunBin} add -g ${spec}`);
40788
+ const child = Bun.spawn([bunBin, "add", "-g", spec], {
40789
+ stdout: "pipe",
40790
+ stderr: "pipe",
40791
+ stdin: "ignore"
40792
+ });
40793
+ (async () => {
40794
+ try {
40795
+ const stdoutTxt = await new Response(child.stdout).text();
40796
+ const stderrTxt = await new Response(child.stderr).text();
40797
+ const code = await child.exited;
40798
+ if (stdoutTxt.trim())
40799
+ log3(`[upgrade] stdout: ${stdoutTxt.trim()}`);
40800
+ if (stderrTxt.trim())
40801
+ log3(`[upgrade] stderr: ${stderrTxt.trim()}`);
40802
+ if (code === 0) {
40803
+ log3("[upgrade] success; exiting so service manager respawns with new binary");
40804
+ setTimeout(() => process.exit(0), 500);
40805
+ } else {
40806
+ log3(`[upgrade] failed with exit code ${code}`);
40807
+ }
40808
+ } catch (e) {
40809
+ log3(`[upgrade] error: ${e.message}`);
40810
+ }
40811
+ })();
40812
+ return Response.json({ accepted: true, current_version: CLI_VERSION, target: spec }, { status: 202 });
40813
+ })();
40814
+ }
40542
40815
  if (url2.pathname === "/stop" && req.method === "POST") {
40543
40816
  if (req.headers.get("authorization") !== expectedAuth)
40544
40817
  return new Response("unauthorized", { status: 401 });
@@ -40654,7 +40927,8 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
40654
40927
  device_id: cfg.deviceId,
40655
40928
  workspace_id: cfg.workspaceId,
40656
40929
  api_url: apiUrl,
40657
- pid: process.pid
40930
+ pid: process.pid,
40931
+ cli_version: CLI_VERSION
40658
40932
  }, null, 2));
40659
40933
  } catch {}
40660
40934
  let tunnel;
@@ -41069,14 +41343,14 @@ var collectServiceEnv = () => {
41069
41343
  env.PATH = process.env.PATH;
41070
41344
  if (process.env.HOME)
41071
41345
  env.HOME = process.env.HOME;
41072
- if (process.env.MULTI_HOME)
41073
- env.MULTI_HOME = process.env.MULTI_HOME;
41074
- if (process.env.MULTI_API)
41075
- env.MULTI_API = process.env.MULTI_API;
41076
41346
  if (process.env.SHELL)
41077
41347
  env.SHELL = process.env.SHELL;
41078
41348
  if (process.env.LANG)
41079
41349
  env.LANG = process.env.LANG;
41350
+ for (const [k, v] of Object.entries(process.env)) {
41351
+ if (k.startsWith("MULTI_") && typeof v === "string" && v.length > 0)
41352
+ env[k] = v;
41353
+ }
41080
41354
  return env;
41081
41355
  };
41082
41356
  var ensureSupported = exports_Effect.fn("service.ensureSupported")(function* () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipers-dev/multi",
3
- "version": "0.63.0",
3
+ "version": "0.65.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "multi-agent": "./dist/index.js"