offhands 0.1.3 → 0.1.5

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/daemon.mjs +369 -106
  2. package/package.json +1 -1
package/dist/daemon.mjs CHANGED
@@ -5299,6 +5299,7 @@ stderr: ${stderrTail}` : ""}`
5299
5299
 
5300
5300
  // ../daemon/src/runners/opencode-cli.ts
5301
5301
  import { spawn as spawn4 } from "node:child_process";
5302
+ import { createServer } from "node:net";
5302
5303
  import { existsSync as existsSync3 } from "node:fs";
5303
5304
  import { homedir as homedir3 } from "node:os";
5304
5305
  import { delimiter as delimiter2, join as join3 } from "node:path";
@@ -5307,102 +5308,71 @@ import { delimiter as delimiter2, join as join3 } from "node:path";
5307
5308
  function mapOpenCodeEvent(value) {
5308
5309
  if (typeof value !== "object" || value === null) return [];
5309
5310
  const v2 = value;
5310
- const type = typeof v2.type === "string" ? v2.type : "";
5311
- const data = v2.data ?? {};
5312
- if (type === "text_delta" || type === "message.delta" && data.type === "text") {
5313
- const chunk = typeof data.text === "string" ? data.text : "";
5314
- return chunk ? [{ type: "text", chunk }] : [];
5315
- }
5316
- if (type === "thinking_delta" || type === "message.delta" && data.type === "thinking") {
5317
- const chunk = typeof data.thinking === "string" ? data.thinking : "";
5318
- return chunk ? [{ type: "thinking", chunk }] : [];
5319
- }
5320
- if (type === "tool_call_start" || type === "message.delta" && data.type === "tool_use") {
5321
- const name = firstString2(data, "toolName", "name", "tool") ?? "tool";
5322
- const summary = firstString2(data, "command", "path", "file", "summary", "description", "query") ?? "";
5311
+ const part = v2.part ?? {};
5312
+ const partType = typeof part.type === "string" ? part.type : typeof v2.type === "string" ? v2.type : "";
5313
+ if (partType === "text") {
5314
+ const text = typeof part.text === "string" ? part.text : "";
5315
+ return text ? [{ type: "text", chunk: text }] : [];
5316
+ }
5317
+ if (partType === "reasoning") {
5318
+ const thinking = typeof part.text === "string" ? part.text : "";
5319
+ return thinking ? [{ type: "thinking", chunk: thinking }] : [];
5320
+ }
5321
+ if (partType === "tool") {
5322
+ const name = typeof part.tool === "string" ? part.tool : "tool";
5323
+ const state = part.state ?? {};
5324
+ const input = state.input;
5325
+ const summary = input ? firstString2(input, "command", "filePath", "path", "file", "pattern", "query", "url") ?? "" : "";
5326
+ const status = typeof state.status === "string" ? state.status : void 0;
5327
+ if (status === "error") {
5328
+ const errMsg = typeof state.error === "string" ? state.error : "tool call failed";
5329
+ return [{ type: "tool", name, summary: `${name}: ${truncate3(errMsg, 120)}` }];
5330
+ }
5323
5331
  return [{ type: "tool", name, summary: summary ? `${name}: ${truncate3(summary, 120)}` : name }];
5324
5332
  }
5325
- if (type === "tool_call_end" || type === "tool_call_result") {
5326
- return [];
5327
- }
5328
- if (type === "permission.request" || type === "permission_request") {
5329
- const id = firstString2(data, "id", "permissionId") ?? crypto.randomUUID();
5330
- const action = firstString2(data, "toolName", "action", "command") ?? "action";
5331
- const detail = firstString2(data, "detail", "description", "message") ?? "";
5332
- const risk = firstString2(data, "risk") === "high" ? "high" : "low";
5333
- const preview = firstString2(data, "preview", "diff", "command") ?? void 0;
5334
- const question = data.question ? mapQuestion(data.question) : void 0;
5335
- return [{
5336
- type: "approval",
5337
- id,
5338
- action,
5339
- detail,
5340
- risk,
5341
- preview,
5342
- question
5343
- }];
5344
- }
5345
- if (type === "result" || type === "completed" || type === "done") {
5346
- const isError = v2.is_error === true || v2.error === true || typeof data.exitCode === "number" && data.exitCode !== 0;
5347
- const text = firstString2(v2, "result", "text", "summary", "message") ?? "";
5348
- if (isError) {
5349
- return [{ type: "error", message: text || `run failed (${String(v2.subtype ?? v2.type ?? "unknown")})` }];
5333
+ if (partType === "step-start") return [];
5334
+ if (partType === "step-finish") {
5335
+ const reason = typeof part.reason === "string" ? part.reason : "stop";
5336
+ const usage = extractUsageFromPart(part);
5337
+ if (reason === "error") {
5338
+ const message = firstString2(part, "error", "message") ?? "run failed";
5339
+ const events2 = [{ type: "error", message }];
5340
+ if (usage) events2.push(usage);
5341
+ return events2;
5350
5342
  }
5351
- const events = [{ type: "done", summary: text }];
5352
- const usage = extractUsage(v2, data);
5343
+ if (reason !== "stop") return usage ? [usage] : [];
5344
+ const events = [{ type: "done", summary: "" }];
5353
5345
  if (usage) events.push(usage);
5354
5346
  return events;
5355
5347
  }
5356
- if (type === "error" || type === "failed") {
5348
+ if (!part.type && typeof v2.type === "string" && v2.type === "error") {
5357
5349
  const message = firstString2(v2, "message", "error", "detail", "result") ?? "unknown error";
5358
5350
  return [{ type: "error", message }];
5359
5351
  }
5360
- if (type === "rate_limit" || type === "usage") {
5361
- const usage = extractUsage(v2, data);
5362
- return usage ? [usage] : [];
5363
- }
5364
- if (type === "session.created" || type === "session.updated") {
5365
- return [];
5366
- }
5367
5352
  return [];
5368
5353
  }
5369
5354
  function extractOpenCodeSessionId(value) {
5370
5355
  if (typeof value !== "object" || value === null) return null;
5371
5356
  const v2 = value;
5372
- const data = v2.data ?? {};
5373
- return firstString2(v2, "sessionId", "session_id", "id") ?? firstString2(data, "sessionId", "session_id", "id") ?? null;
5374
- }
5375
- function mapQuestion(q2) {
5376
- if (typeof q2 !== "object" || q2 === null) return void 0;
5377
- const qq = q2;
5378
- const text = typeof qq.text === "string" ? qq.text : "";
5379
- const options = Array.isArray(qq.options) ? qq.options.filter((o2) => typeof o2 === "object" && o2 !== null).map((o2) => ({
5380
- label: typeof o2.label === "string" ? o2.label : "",
5381
- description: typeof o2.description === "string" ? o2.description : void 0
5382
- })) : [];
5383
- const multiSelect = typeof qq.multiSelect === "boolean" ? qq.multiSelect : void 0;
5384
- if (!text && options.length === 0) return void 0;
5385
- return { text, options, multiSelect };
5386
- }
5387
- function extractUsage(v2, data) {
5388
- const usage = v2.usage ?? data.usage;
5389
- const costUsd = typeof v2.costUsd === "number" ? v2.costUsd : typeof data.costUsd === "number" ? data.costUsd : typeof v2.total_cost_usd === "number" ? v2.total_cost_usd : void 0;
5390
- if (!usage && costUsd === void 0) return null;
5391
- const n2 = (k2) => {
5392
- const val = usage?.[k2] ?? data[k2];
5393
- return typeof val === "number" ? val : 0;
5394
- };
5395
- const contextTokens = n2("inputTokens") + n2("input_tokens") + n2("cacheReadInputTokens") + n2("cache_read_input_tokens") + n2("cacheCreationInputTokens") + n2("cache_creation_input_tokens") + n2("outputTokens") + n2("output_tokens") + n2("reasoningTokens") + n2("reasoning_tokens");
5357
+ const part = v2.part ?? {};
5358
+ return firstString2(v2, "sessionID", "sessionId", "session_id") ?? firstString2(part, "sessionID", "sessionId", "session_id") ?? null;
5359
+ }
5360
+ function extractUsageFromPart(part) {
5361
+ const tokens = part.tokens;
5362
+ const costUsd = typeof part.cost === "number" ? part.cost : void 0;
5363
+ if (!tokens && costUsd === void 0) return null;
5364
+ const n2 = (k2) => typeof tokens?.[k2] === "number" ? tokens[k2] : 0;
5365
+ const contextTokens = tokens && typeof tokens.total === "number" ? tokens.total : n2("input") + n2("output") + n2("reasoning");
5396
5366
  if (contextTokens <= 0 && costUsd === void 0) return null;
5397
5367
  return {
5398
5368
  type: "usage",
5399
5369
  contextTokens,
5400
5370
  contextWindow: 2e5,
5401
- // OpenCode doesn't expose window; use reasonable default
5402
5371
  ...costUsd !== void 0 ? { costUsd } : {}
5403
5372
  };
5404
5373
  }
5405
5374
  function firstString2(obj, ...keys) {
5375
+ if (!obj) return void 0;
5406
5376
  for (const k2 of keys) {
5407
5377
  const val = obj[k2];
5408
5378
  if (typeof val === "string" && val !== "") return val;
@@ -5414,7 +5384,11 @@ function truncate3(s2, max) {
5414
5384
  }
5415
5385
 
5416
5386
  // ../daemon/src/runners/opencode-cli.ts
5387
+ var POLL_INTERVAL_MS = 500;
5417
5388
  var OpenCodeRunner = class {
5389
+ constructor(broker2) {
5390
+ this.broker = broker2;
5391
+ }
5418
5392
  id = "opencode";
5419
5393
  name = "OpenCode";
5420
5394
  supportsApprovals = true;
@@ -5423,6 +5397,10 @@ var OpenCodeRunner = class {
5423
5397
  /** [command, ...prefixArgs] resolved once. */
5424
5398
  resolved = null;
5425
5399
  modelsCache = [];
5400
+ // Lazily-started, shared `opencode serve` instance backing startViaHttpApi.
5401
+ serverPort = null;
5402
+ serverProcess = null;
5403
+ serverStarting = null;
5426
5404
  resolveCommand() {
5427
5405
  if (this.resolved) return this.resolved;
5428
5406
  if (process.platform === "win32") {
@@ -5493,7 +5471,7 @@ var OpenCodeRunner = class {
5493
5471
  if (code === 0) {
5494
5472
  try {
5495
5473
  const lines = output.trim().split("\n");
5496
- const models = lines.map((l2) => l2.trim()).filter((l2) => l2 && !l2.startsWith("opencode/"));
5474
+ const models = lines.map((l2) => l2.trim()).filter((l2) => l2.length > 0);
5497
5475
  this.modelsCache = models;
5498
5476
  Object.defineProperty(this, "models", {
5499
5477
  value: models,
@@ -5515,6 +5493,13 @@ var OpenCodeRunner = class {
5515
5493
  return existsSync3(join3(homedir3(), ".local", "share", "opencode", "auth.json"));
5516
5494
  }
5517
5495
  start(run, callbacks) {
5496
+ const mode = run.permissionMode ?? "guarded";
5497
+ if (this.broker && (mode === "guarded" || mode === "acceptEdits")) {
5498
+ return this.startViaHttpApi(run, mode, callbacks);
5499
+ }
5500
+ return this.startViaCli(run, mode, callbacks);
5501
+ }
5502
+ startViaCli(run, mode, callbacks) {
5518
5503
  const queue = new AsyncEventQueue();
5519
5504
  const parser = new NdjsonParser();
5520
5505
  let child;
@@ -5538,10 +5523,8 @@ var OpenCodeRunner = class {
5538
5523
  const args2 = [
5539
5524
  ...cmd.slice(1),
5540
5525
  "run",
5541
- run.prompt,
5542
5526
  "--format",
5543
- "json",
5544
- "--no-color"
5527
+ "json"
5545
5528
  ];
5546
5529
  if (run.model) {
5547
5530
  args2.push("--model", run.model);
@@ -5549,12 +5532,10 @@ var OpenCodeRunner = class {
5549
5532
  if (run.resumeConversationId) {
5550
5533
  args2.push("--session", run.resumeConversationId);
5551
5534
  }
5552
- const mode = run.permissionMode ?? "guarded";
5553
- if (mode === "bypass") {
5554
- args2.push("--auto");
5555
- } else if (mode === "plan") {
5535
+ if (mode === "plan") {
5556
5536
  args2.push("--agent", "plan");
5557
5537
  } else {
5538
+ args2.push("--auto");
5558
5539
  }
5559
5540
  if (run.effort) {
5560
5541
  const variantMap = { low: "minimal", medium: "high", high: "max", max: "max" };
@@ -5570,6 +5551,8 @@ var OpenCodeRunner = class {
5570
5551
  }
5571
5552
  }
5572
5553
  args2.push("--dir", run.workspace);
5554
+ const promptArg = process.platform === "win32" ? `"${run.prompt.replace(/"/g, '\\"')}"` : run.prompt;
5555
+ args2.push(promptArg);
5573
5556
  try {
5574
5557
  child = spawn4(cmd[0], args2, {
5575
5558
  cwd: run.workspace,
@@ -5623,12 +5606,283 @@ stderr: ${stderrTail}` : ""}`
5623
5606
  });
5624
5607
  return {
5625
5608
  events: queue,
5626
- respond: (approvalId, ok, answer) => {
5609
+ // No broker (or bypass/plan, which never ask) nothing to answer.
5610
+ respond: () => {
5627
5611
  },
5628
5612
  cancel: () => child.kill()
5629
5613
  };
5630
5614
  }
5615
+ startViaHttpApi(run, mode, callbacks) {
5616
+ const queue = new AsyncEventQueue();
5617
+ const emit = (events) => {
5618
+ for (const e of events) queue.push(e);
5619
+ };
5620
+ const broker2 = this.broker;
5621
+ const directory = run.workspace;
5622
+ const encDir = encodeURIComponent(directory);
5623
+ let cancelled = false;
5624
+ let sessionId = run.resumeConversationId ?? null;
5625
+ let port2 = null;
5626
+ let detachBroker;
5627
+ const pendingQuestions = /* @__PURE__ */ new Map();
5628
+ const cleanup = () => {
5629
+ detachBroker?.();
5630
+ queue.close();
5631
+ };
5632
+ void (async () => {
5633
+ try {
5634
+ port2 = await this.ensureServer();
5635
+ } catch (e) {
5636
+ emit([{ type: "error", message: `failed to start opencode server: ${String(e)}` }]);
5637
+ cleanup();
5638
+ return;
5639
+ }
5640
+ if (cancelled) {
5641
+ cleanup();
5642
+ return;
5643
+ }
5644
+ const base = `http://127.0.0.1:${port2}`;
5645
+ detachBroker = broker2.attach((ev) => queue.push(ev));
5646
+ try {
5647
+ if (!sessionId) {
5648
+ const modelRef2 = parseModelRef(run.model);
5649
+ const body = { permission: buildPermissionRuleset(mode) };
5650
+ if (modelRef2) body.model = { id: modelRef2.modelId, providerID: modelRef2.providerId };
5651
+ const session = await httpJson(`${base}/session?directory=${encDir}`, "POST", body);
5652
+ sessionId = session.id;
5653
+ }
5654
+ callbacks?.onConversationId?.(sessionId);
5655
+ const parts = [{ type: "text", text: run.prompt }];
5656
+ for (const file of run.attachments ?? []) {
5657
+ parts.push({ type: "file", mime: "application/octet-stream", url: fileUrl(file) });
5658
+ }
5659
+ const promptBody = { parts };
5660
+ const modelRef = parseModelRef(run.model);
5661
+ if (modelRef) promptBody.model = { providerID: modelRef.providerId, modelID: modelRef.modelId };
5662
+ await httpJson(`${base}/session/${sessionId}/prompt_async?directory=${encDir}`, "POST", promptBody, true);
5663
+ await this.pollUntilDone(base, sessionId, encDir, broker2, emit, pendingQuestions, () => cancelled);
5664
+ } catch (e) {
5665
+ if (!cancelled) emit([{ type: "error", message: `opencode HTTP API error: ${String(e)}` }]);
5666
+ } finally {
5667
+ cleanup();
5668
+ }
5669
+ })();
5670
+ return {
5671
+ events: queue,
5672
+ respond: (approvalId, ok, answer) => {
5673
+ const settleQuestion = pendingQuestions.get(approvalId);
5674
+ if (settleQuestion) {
5675
+ pendingQuestions.delete(approvalId);
5676
+ settleQuestion(ok, answer);
5677
+ return;
5678
+ }
5679
+ broker2.resolve(approvalId, ok, answer);
5680
+ },
5681
+ cancel: () => {
5682
+ cancelled = true;
5683
+ if (port2 && sessionId) {
5684
+ const abortUrl = `http://127.0.0.1:${port2}/session/${sessionId}/abort?directory=${encDir}`;
5685
+ void httpJson(abortUrl, "POST", {}, true).catch(() => {
5686
+ });
5687
+ }
5688
+ }
5689
+ };
5690
+ }
5691
+ /** Poll session messages + pending permissions/questions until the turn finishes. */
5692
+ async pollUntilDone(base, sessionId, encDir, broker2, emit, pendingQuestions, isCancelled) {
5693
+ const seenPermissions = /* @__PURE__ */ new Set();
5694
+ const seenQuestions = /* @__PURE__ */ new Set();
5695
+ const emittedParts = /* @__PURE__ */ new Set();
5696
+ let sawTerminal = false;
5697
+ while (!isCancelled() && !sawTerminal) {
5698
+ await sleep(POLL_INTERVAL_MS);
5699
+ if (isCancelled()) break;
5700
+ try {
5701
+ const pending = await httpJson(`${base}/permission?directory=${encDir}`, "GET");
5702
+ for (const p2 of pending) {
5703
+ if (p2.sessionID !== sessionId || seenPermissions.has(p2.id)) continue;
5704
+ seenPermissions.add(p2.id);
5705
+ void this.answerPermission(base, encDir, p2, broker2);
5706
+ }
5707
+ } catch {
5708
+ }
5709
+ try {
5710
+ const pendingQ = await httpJson(`${base}/question?directory=${encDir}`, "GET");
5711
+ for (const q2 of pendingQ) {
5712
+ if (q2.sessionID !== sessionId || seenQuestions.has(q2.id)) continue;
5713
+ seenQuestions.add(q2.id);
5714
+ void this.answerQuestion(base, encDir, q2, emit, pendingQuestions);
5715
+ }
5716
+ } catch {
5717
+ }
5718
+ let messages;
5719
+ try {
5720
+ messages = await httpJson(`${base}/session/${sessionId}/message?directory=${encDir}`, "GET");
5721
+ } catch {
5722
+ continue;
5723
+ }
5724
+ for (const row of messages) {
5725
+ if (row.info.role !== "assistant") continue;
5726
+ for (const part of row.parts) {
5727
+ if (!part.id || emittedParts.has(part.id)) continue;
5728
+ if ((part.type === "text" || part.type === "reasoning") && !part.time?.end) continue;
5729
+ emittedParts.add(part.id);
5730
+ const events = mapOpenCodeEvent({ part });
5731
+ emit(events);
5732
+ if (events.some((e) => e.type === "done" || e.type === "error")) sawTerminal = true;
5733
+ }
5734
+ if (!sawTerminal && row.info.error) {
5735
+ sawTerminal = true;
5736
+ emit([{ type: "error", message: row.info.error.data?.message ?? "run failed" }]);
5737
+ }
5738
+ }
5739
+ }
5740
+ }
5741
+ /** Route one pending permission through the shared broker, then reply. */
5742
+ async answerPermission(base, encDir, p2, broker2) {
5743
+ const input = { ...p2.metadata };
5744
+ const verdict = await broker2.submit(p2.permission, input);
5745
+ try {
5746
+ await httpJson(`${base}/permission/${p2.id}/reply?directory=${encDir}`, "POST", {
5747
+ reply: verdict.approve ? "once" : "reject",
5748
+ ...verdict.message ? { message: verdict.message } : {}
5749
+ }, true);
5750
+ } catch {
5751
+ }
5752
+ }
5753
+ /** Surface an OpenCode "question" (its AskUserQuestion equivalent) as a
5754
+ * real approval-with-options card, wait for the phone's pick, then answer
5755
+ * through the question API's own reply/reject endpoints. */
5756
+ async answerQuestion(base, encDir, q2, emit, pendingQuestions) {
5757
+ const first = q2.questions[0];
5758
+ if (!first) return;
5759
+ const options = first.options.map((o2) => ({ label: o2.label, description: o2.description }));
5760
+ const preview = [first.question, ...options.map((o2, n2) => `${n2 + 1}. ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)].join("\n");
5761
+ emit([{
5762
+ type: "approval",
5763
+ id: q2.id,
5764
+ action: "question",
5765
+ detail: truncate4(first.question, 200),
5766
+ risk: "low",
5767
+ preview: truncate4(preview, 600),
5768
+ question: { text: first.question, options, multiSelect: first.multiple }
5769
+ }]);
5770
+ const verdict = await new Promise((resolve6) => {
5771
+ pendingQuestions.set(q2.id, (ok, answer) => resolve6({ ok, answer }));
5772
+ });
5773
+ emit([{
5774
+ type: "approval-result",
5775
+ id: q2.id,
5776
+ approve: verdict.ok,
5777
+ ...verdict.answer ? { answer: verdict.answer } : {}
5778
+ }]);
5779
+ try {
5780
+ if (verdict.ok && verdict.answer) {
5781
+ await httpJson(`${base}/question/${q2.id}/reply?directory=${encDir}`, "POST", { answers: [[verdict.answer]] }, true);
5782
+ } else {
5783
+ await httpJson(`${base}/question/${q2.id}/reject?directory=${encDir}`, "POST", {}, true);
5784
+ }
5785
+ } catch {
5786
+ }
5787
+ }
5788
+ /** Lazily start (once) the shared `opencode serve` instance used for
5789
+ * broker-driven runs, and wait for it to accept connections. */
5790
+ async ensureServer() {
5791
+ if (this.serverPort) return this.serverPort;
5792
+ if (this.serverStarting) return this.serverStarting;
5793
+ this.serverStarting = (async () => {
5794
+ const cmd = this.resolveCommand();
5795
+ if (!cmd) throw new Error("opencode CLI not found");
5796
+ const port2 = await findFreePort();
5797
+ const proc = spawn4(cmd[0], [...cmd.slice(1), "serve", "--port", String(port2), "--hostname", "127.0.0.1"], {
5798
+ stdio: ["ignore", "pipe", "pipe"],
5799
+ shell: process.platform === "win32"
5800
+ });
5801
+ proc.on("exit", () => {
5802
+ if (this.serverProcess === proc) {
5803
+ this.serverProcess = null;
5804
+ this.serverPort = null;
5805
+ }
5806
+ });
5807
+ await waitForServerReady(port2);
5808
+ this.serverProcess = proc;
5809
+ this.serverPort = port2;
5810
+ return port2;
5811
+ })();
5812
+ try {
5813
+ return await this.serverStarting;
5814
+ } finally {
5815
+ this.serverStarting = null;
5816
+ }
5817
+ }
5631
5818
  };
5819
+ function buildPermissionRuleset(mode) {
5820
+ const base = [{ permission: "*", pattern: "*", action: "allow" }];
5821
+ if (mode === "acceptEdits") {
5822
+ return [
5823
+ ...base,
5824
+ { permission: "edit", pattern: "*", action: "allow" },
5825
+ { permission: "bash", pattern: "*", action: "ask" },
5826
+ { permission: "webfetch", pattern: "*", action: "ask" }
5827
+ ];
5828
+ }
5829
+ return [
5830
+ ...base,
5831
+ { permission: "edit", pattern: "*", action: "ask" },
5832
+ { permission: "bash", pattern: "*", action: "ask" },
5833
+ { permission: "webfetch", pattern: "*", action: "ask" }
5834
+ ];
5835
+ }
5836
+ function parseModelRef(model) {
5837
+ if (!model) return null;
5838
+ const idx = model.indexOf("/");
5839
+ if (idx < 0) return null;
5840
+ return { providerId: model.slice(0, idx), modelId: model.slice(idx + 1) };
5841
+ }
5842
+ function fileUrl(path) {
5843
+ const normalised = path.replace(/\\/g, "/");
5844
+ return `file://${normalised.startsWith("/") ? "" : "/"}${normalised}`;
5845
+ }
5846
+ function truncate4(s2, max) {
5847
+ return s2.length <= max ? s2 : s2.slice(0, max - 1) + "\u2026";
5848
+ }
5849
+ async function httpJson(url, method, body, expectNoContent = false) {
5850
+ const res = await fetch(url, {
5851
+ method,
5852
+ headers: body !== void 0 ? { "content-type": "application/json" } : void 0,
5853
+ body: body !== void 0 ? JSON.stringify(body) : void 0
5854
+ });
5855
+ if (!res.ok) throw new Error(`opencode API ${method} ${url} -> ${res.status}`);
5856
+ if (expectNoContent || res.status === 204) return void 0;
5857
+ return res.json();
5858
+ }
5859
+ function findFreePort() {
5860
+ return new Promise((resolve6, reject) => {
5861
+ const srv = createServer();
5862
+ srv.unref();
5863
+ srv.on("error", reject);
5864
+ srv.listen(0, "127.0.0.1", () => {
5865
+ const address = srv.address();
5866
+ const port2 = typeof address === "object" && address ? address.port : 0;
5867
+ srv.close(() => resolve6(port2));
5868
+ });
5869
+ });
5870
+ }
5871
+ async function waitForServerReady(port2, timeoutMs = 2e4) {
5872
+ const deadline = Date.now() + timeoutMs;
5873
+ while (Date.now() < deadline) {
5874
+ try {
5875
+ const res = await fetch(`http://127.0.0.1:${port2}/doc`);
5876
+ if (res.ok) return;
5877
+ } catch {
5878
+ }
5879
+ await sleep(150);
5880
+ }
5881
+ throw new Error(`opencode serve did not become ready on port ${port2}`);
5882
+ }
5883
+ function sleep(ms) {
5884
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
5885
+ }
5632
5886
 
5633
5887
  // ../daemon/src/session-manager.ts
5634
5888
  import { randomUUID } from "node:crypto";
@@ -5749,7 +6003,7 @@ function summarizeConversation(path, workspace) {
5749
6003
  return {
5750
6004
  conversationId: basename(path, ".jsonl"),
5751
6005
  workspace,
5752
- firstPrompt: truncate4(firstPrompt || "(empty prompt)", 120),
6006
+ firstPrompt: truncate5(firstPrompt || "(empty prompt)", 120),
5753
6007
  lastActiveMs: Math.floor(statSync(path).mtimeMs),
5754
6008
  messageCount
5755
6009
  };
@@ -5775,7 +6029,7 @@ function userMessageText(value) {
5775
6029
  function collapseText(text) {
5776
6030
  return text.replace(/\s+/g, " ").trim();
5777
6031
  }
5778
- function truncate4(text, max) {
6032
+ function truncate5(text, max) {
5779
6033
  return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
5780
6034
  }
5781
6035
  function safeReadDir(path) {
@@ -6558,7 +6812,7 @@ function searchableText(msg) {
6558
6812
  }
6559
6813
 
6560
6814
  // ../daemon/src/local-server.ts
6561
- import { createServer } from "node:http";
6815
+ import { createServer as createServer2 } from "node:http";
6562
6816
 
6563
6817
  // ../node_modules/.pnpm/ws@8.21.3/node_modules/ws/wrapper.mjs
6564
6818
  var import_stream = __toESM(require_stream(), 1);
@@ -11015,9 +11269,9 @@ if (typeof Module.getRandomValue === "undefined") {
11015
11269
  Module.getRandomValue = randomValuesStandard;
11016
11270
  } catch (e) {
11017
11271
  try {
11018
- crypto2 = null;
11272
+ crypto = null;
11019
11273
  randomValueNodeJS = function() {
11020
- var buf = crypto2["randomBytes"](4);
11274
+ var buf = crypto["randomBytes"](4);
11021
11275
  return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
11022
11276
  };
11023
11277
  randomValueNodeJS();
@@ -11030,7 +11284,7 @@ if (typeof Module.getRandomValue === "undefined") {
11030
11284
  var window_;
11031
11285
  var crypto_;
11032
11286
  var randomValuesStandard;
11033
- var crypto2;
11287
+ var crypto;
11034
11288
  var randomValueNodeJS;
11035
11289
  var _Module = Module;
11036
11290
  Module.ready = new Promise(function(resolve6, reject) {
@@ -37924,7 +38178,7 @@ Module.ready = new Promise(function(resolve6, reject) {
37924
38178
  try {
37925
38179
  var window_ = "object" === typeof window ? window : self;
37926
38180
  var crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto;
37927
- crypto_ = crypto_ === void 0 ? crypto2 : crypto_;
38181
+ crypto_ = crypto_ === void 0 ? crypto : crypto_;
37928
38182
  var randomValuesStandard = function() {
37929
38183
  var buf = new Uint32Array(1);
37930
38184
  crypto_.getRandomValues(buf);
@@ -37934,9 +38188,9 @@ Module.ready = new Promise(function(resolve6, reject) {
37934
38188
  Module3.getRandomValue = randomValuesStandard;
37935
38189
  } catch (e) {
37936
38190
  try {
37937
- var crypto2 = null;
38191
+ var crypto = null;
37938
38192
  var randomValueNodeJS = function() {
37939
- var buf = crypto2["randomBytes"](4);
38193
+ var buf = crypto["randomBytes"](4);
37940
38194
  return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
37941
38195
  };
37942
38196
  randomValueNodeJS();
@@ -38582,7 +38836,7 @@ Module.ready = new Promise(function(resolve6, reject) {
38582
38836
  try {
38583
38837
  var window_ = "object" === typeof window ? window : self;
38584
38838
  var crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto;
38585
- crypto_ = crypto_ === void 0 ? crypto2 : crypto_;
38839
+ crypto_ = crypto_ === void 0 ? crypto : crypto_;
38586
38840
  var randomValuesStandard = function() {
38587
38841
  var buf = new Uint32Array(1);
38588
38842
  crypto_.getRandomValues(buf);
@@ -38592,9 +38846,9 @@ Module.ready = new Promise(function(resolve6, reject) {
38592
38846
  Module2.getRandomValue = randomValuesStandard;
38593
38847
  } catch (e) {
38594
38848
  try {
38595
- var crypto2 = null;
38849
+ var crypto = null;
38596
38850
  var randomValueNodeJS = function() {
38597
- var buf = crypto2["randomBytes"](4);
38851
+ var buf = crypto["randomBytes"](4);
38598
38852
  return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
38599
38853
  };
38600
38854
  randomValueNodeJS();
@@ -41516,7 +41770,7 @@ var LocalSessionServer = class {
41516
41770
  constructor(manager2, port2, broker2) {
41517
41771
  this.manager = manager2;
41518
41772
  this.broker = broker2;
41519
- const http = createServer((req, res) => void this.onRequest(req, res));
41773
+ const http = createServer2((req, res) => void this.onRequest(req, res));
41520
41774
  this.wss = new import_websocket_server.default({ server: http });
41521
41775
  this.wss.on("connection", (ws) => this.onConnection(ws));
41522
41776
  http.on("error", (err) => {
@@ -41865,7 +42119,7 @@ var ApprovalBroker = class {
41865
42119
  };
41866
42120
  function classifyRisk(toolName, input) {
41867
42121
  const i2 = input ?? {};
41868
- const text = [toolName, i2.command, i2.file_path, i2.path].filter((x2) => typeof x2 === "string").join(" ");
42122
+ const text = [toolName, i2.command, i2.file_path, i2.path, i2.filepath].filter((x2) => typeof x2 === "string").join(" ");
41869
42123
  return /\b(rm|del|rmdir|rd|format|mkfs|shutdown|reboot|kill|drop\s+table|truncate|git\s+push\s+--force|--hard)\b/i.test(
41870
42124
  text
41871
42125
  ) ? "high" : "low";
@@ -41874,11 +42128,11 @@ function summariseInput(input) {
41874
42128
  if (typeof input !== "object" || input === null) return String(input ?? "");
41875
42129
  const i2 = input;
41876
42130
  const q2 = firstQuestion(i2);
41877
- if (q2) return truncate5(q2.text, 200);
41878
- for (const key of ["command", "file_path", "path", "url", "pattern", "description"]) {
41879
- if (typeof i2[key] === "string" && i2[key] !== "") return truncate5(`${key}: ${i2[key]}`, 200);
42131
+ if (q2) return truncate6(q2.text, 200);
42132
+ for (const key of ["command", "file_path", "filepath", "path", "url", "pattern", "description"]) {
42133
+ if (typeof i2[key] === "string" && i2[key] !== "") return truncate6(`${key}: ${i2[key]}`, 200);
41880
42134
  }
41881
- return truncate5(JSON.stringify(input), 200);
42135
+ return truncate6(JSON.stringify(input), 200);
41882
42136
  }
41883
42137
  function firstQuestion(i2) {
41884
42138
  if (!Array.isArray(i2.questions) || i2.questions.length === 0) return void 0;
@@ -41896,7 +42150,7 @@ function buildPreview(toolName, input) {
41896
42150
  const q2 = firstQuestion(i2);
41897
42151
  if (q2) {
41898
42152
  const lines = [q2.text, ...q2.options.map((o2, n2) => `${n2 + 1}. ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)];
41899
- return truncate5(lines.join("\n"), 600);
42153
+ return truncate6(lines.join("\n"), 600);
41900
42154
  }
41901
42155
  if (/^(Edit|MultiEdit)/.test(toolName) && typeof i2.new_string === "string") {
41902
42156
  const oldS = typeof i2.old_string === "string" ? i2.old_string : "";
@@ -41904,15 +42158,24 @@ function buildPreview(toolName, input) {
41904
42158
  ...oldS.split("\n").map((l2) => `- ${l2}`),
41905
42159
  ...i2.new_string.split("\n").map((l2) => `+ ${l2}`)
41906
42160
  ];
41907
- return truncate5(lines.join("\n"), 600);
42161
+ return truncate6(lines.join("\n"), 600);
41908
42162
  }
41909
42163
  if (/^Write/.test(toolName) && typeof i2.content === "string") {
41910
- return truncate5(i2.content.split("\n").map((l2) => `+ ${l2}`).join("\n"), 600);
42164
+ return truncate6(i2.content.split("\n").map((l2) => `+ ${l2}`).join("\n"), 600);
42165
+ }
42166
+ if (typeof i2.diff === "string" && i2.diff.trim()) {
42167
+ return truncate6(formatUnifiedDiff(i2.diff), 600);
41911
42168
  }
41912
- if (typeof i2.command === "string") return truncate5(`$ ${i2.command}`, 600);
42169
+ if (typeof i2.url === "string") return truncate6(`\u2192 ${i2.url}`, 600);
42170
+ if (typeof i2.command === "string") return truncate6(`$ ${i2.command}`, 600);
41913
42171
  return void 0;
41914
42172
  }
41915
- function truncate5(s2, max) {
42173
+ function formatUnifiedDiff(diff) {
42174
+ const lines = diff.split("\n");
42175
+ const hunkStart = lines.findIndex((l2) => l2.startsWith("@@"));
42176
+ return (hunkStart >= 0 ? lines.slice(hunkStart) : lines).join("\n").trimEnd();
42177
+ }
42178
+ function truncate6(s2, max) {
41916
42179
  return s2.length <= max ? s2 : s2.slice(0, max - 1) + "\u2026";
41917
42180
  }
41918
42181
 
@@ -42103,7 +42366,7 @@ var approvalUrl = `http://127.0.0.1:${port}/approval`;
42103
42366
  var runners = [
42104
42367
  new ClaudeCodeRunner(broker, approvalUrl),
42105
42368
  new CopilotCliRunner(),
42106
- new OpenCodeRunner(),
42369
+ new OpenCodeRunner(broker),
42107
42370
  new CodexCliRunner(),
42108
42371
  new CursorAgentRunner(),
42109
42372
  new GeminiCliRunner()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offhands",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Phone → coding-agent relay. Daemon runs on your laptop, PWA on your phone. E2E encrypted.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",