offhands 0.1.4 → 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 +350 -86
  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,79 +5308,61 @@ 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
5311
  const part = v2.part ?? {};
5312
- const partType = typeof part.type === "string" ? part.type : "";
5313
- if (type === "text") {
5312
+ const partType = typeof part.type === "string" ? part.type : typeof v2.type === "string" ? v2.type : "";
5313
+ if (partType === "text") {
5314
5314
  const text = typeof part.text === "string" ? part.text : "";
5315
5315
  return text ? [{ type: "text", chunk: text }] : [];
5316
5316
  }
5317
- if (type === "thinking" || partType === "thinking") {
5318
- const thinking = typeof part.thinking === "string" ? part.thinking : typeof part.text === "string" ? part.text : "";
5317
+ if (partType === "reasoning") {
5318
+ const thinking = typeof part.text === "string" ? part.text : "";
5319
5319
  return thinking ? [{ type: "thinking", chunk: thinking }] : [];
5320
5320
  }
5321
- if (type === "tool_call" || partType === "tool_call") {
5322
- const name = firstString2(part, "toolName", "name", "tool") ?? "tool";
5323
- const input = part.input;
5324
- const summary = input ? firstString2(input, "command", "path", "file", "summary", "description", "query") ?? "" : "";
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
+ }
5325
5331
  return [{ type: "tool", name, summary: summary ? `${name}: ${truncate3(summary, 120)}` : name }];
5326
5332
  }
5327
- if (type === "step_start" || partType === "step-start") {
5328
- return [];
5329
- }
5330
- if (type === "step_finish" || partType === "step-finish") {
5333
+ if (partType === "step-start") return [];
5334
+ if (partType === "step-finish") {
5331
5335
  const reason = typeof part.reason === "string" ? part.reason : "stop";
5332
- const isError = reason === "error" || reason === "failed";
5333
5336
  const usage = extractUsageFromPart(part);
5334
- if (isError) {
5335
- const message = typeof part.error === "string" ? part.error : "run failed";
5337
+ if (reason === "error") {
5338
+ const message = firstString2(part, "error", "message") ?? "run failed";
5336
5339
  const events2 = [{ type: "error", message }];
5337
5340
  if (usage) events2.push(usage);
5338
5341
  return events2;
5339
5342
  }
5343
+ if (reason !== "stop") return usage ? [usage] : [];
5340
5344
  const events = [{ type: "done", summary: "" }];
5341
5345
  if (usage) events.push(usage);
5342
5346
  return events;
5343
5347
  }
5344
- if (type === "permission_request" || type === "permission.request") {
5345
- const partData = part.data;
5346
- const id = firstString2(partData, "id", "permissionId", "id") ?? crypto.randomUUID();
5347
- const action = firstString2(partData, "toolName", "action", "command") ?? "action";
5348
- const detail = firstString2(partData, "detail", "description", "message") ?? "";
5349
- const risk = firstString2(partData, "risk") === "high" ? "high" : "low";
5350
- const preview = firstString2(partData, "preview", "diff", "command") ?? void 0;
5351
- const question = partData?.question ? mapQuestion(partData.question) : void 0;
5352
- return [{
5353
- type: "approval",
5354
- id,
5355
- action,
5356
- detail,
5357
- risk,
5358
- preview,
5359
- question
5360
- }];
5361
- }
5362
- if (type === "error") {
5363
- const message = firstString2(v2, "message", "error", "detail", "result") ?? firstString2(part, "message", "error") ?? "unknown error";
5348
+ if (!part.type && typeof v2.type === "string" && v2.type === "error") {
5349
+ const message = firstString2(v2, "message", "error", "detail", "result") ?? "unknown error";
5364
5350
  return [{ type: "error", message }];
5365
5351
  }
5366
- if (type === "session_created" || type === "session.updated" || type === "session.created") {
5367
- return [];
5368
- }
5369
5352
  return [];
5370
5353
  }
5371
5354
  function extractOpenCodeSessionId(value) {
5372
5355
  if (typeof value !== "object" || value === null) return null;
5373
5356
  const v2 = value;
5374
5357
  const part = v2.part ?? {};
5375
- return firstString2(v2, "sessionId", "session_id", "sessionID", "id") ?? firstString2(part, "sessionId", "session_id", "sessionID", "id") ?? null;
5358
+ return firstString2(v2, "sessionID", "sessionId", "session_id") ?? firstString2(part, "sessionID", "sessionId", "session_id") ?? null;
5376
5359
  }
5377
5360
  function extractUsageFromPart(part) {
5378
5361
  const tokens = part.tokens;
5379
- const costUsd = typeof part.cost === "number" ? part.cost : typeof part.costUsd === "number" ? part.costUsd : void 0;
5362
+ const costUsd = typeof part.cost === "number" ? part.cost : void 0;
5380
5363
  if (!tokens && costUsd === void 0) return null;
5381
5364
  const n2 = (k2) => typeof tokens?.[k2] === "number" ? tokens[k2] : 0;
5382
- const contextTokens = n2("total") + n2("input") + n2("inputTokens") + n2("input_tokens") + n2("cacheReadInputTokens") + n2("cache_read_input_tokens") + n2("cacheCreationInputTokens") + n2("cache_creation_input_tokens") + n2("output") + n2("outputTokens") + n2("output_tokens") + n2("reasoning") + n2("reasoningTokens") + n2("reasoning_tokens");
5365
+ const contextTokens = tokens && typeof tokens.total === "number" ? tokens.total : n2("input") + n2("output") + n2("reasoning");
5383
5366
  if (contextTokens <= 0 && costUsd === void 0) return null;
5384
5367
  return {
5385
5368
  type: "usage",
@@ -5388,19 +5371,8 @@ function extractUsageFromPart(part) {
5388
5371
  ...costUsd !== void 0 ? { costUsd } : {}
5389
5372
  };
5390
5373
  }
5391
- function mapQuestion(q2) {
5392
- if (typeof q2 !== "object" || q2 === null) return void 0;
5393
- const qq = q2;
5394
- const text = typeof qq.text === "string" ? qq.text : "";
5395
- const options = Array.isArray(qq.options) ? qq.options.filter((o2) => typeof o2 === "object" && o2 !== null).map((o2) => ({
5396
- label: typeof o2.label === "string" ? o2.label : "",
5397
- description: typeof o2.description === "string" ? o2.description : void 0
5398
- })) : [];
5399
- const multiSelect = typeof qq.multiSelect === "boolean" ? qq.multiSelect : void 0;
5400
- if (!text && options.length === 0) return void 0;
5401
- return { text, options, multiSelect };
5402
- }
5403
5374
  function firstString2(obj, ...keys) {
5375
+ if (!obj) return void 0;
5404
5376
  for (const k2 of keys) {
5405
5377
  const val = obj[k2];
5406
5378
  if (typeof val === "string" && val !== "") return val;
@@ -5412,7 +5384,11 @@ function truncate3(s2, max) {
5412
5384
  }
5413
5385
 
5414
5386
  // ../daemon/src/runners/opencode-cli.ts
5387
+ var POLL_INTERVAL_MS = 500;
5415
5388
  var OpenCodeRunner = class {
5389
+ constructor(broker2) {
5390
+ this.broker = broker2;
5391
+ }
5416
5392
  id = "opencode";
5417
5393
  name = "OpenCode";
5418
5394
  supportsApprovals = true;
@@ -5421,6 +5397,10 @@ var OpenCodeRunner = class {
5421
5397
  /** [command, ...prefixArgs] resolved once. */
5422
5398
  resolved = null;
5423
5399
  modelsCache = [];
5400
+ // Lazily-started, shared `opencode serve` instance backing startViaHttpApi.
5401
+ serverPort = null;
5402
+ serverProcess = null;
5403
+ serverStarting = null;
5424
5404
  resolveCommand() {
5425
5405
  if (this.resolved) return this.resolved;
5426
5406
  if (process.platform === "win32") {
@@ -5491,7 +5471,7 @@ var OpenCodeRunner = class {
5491
5471
  if (code === 0) {
5492
5472
  try {
5493
5473
  const lines = output.trim().split("\n");
5494
- 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);
5495
5475
  this.modelsCache = models;
5496
5476
  Object.defineProperty(this, "models", {
5497
5477
  value: models,
@@ -5513,6 +5493,13 @@ var OpenCodeRunner = class {
5513
5493
  return existsSync3(join3(homedir3(), ".local", "share", "opencode", "auth.json"));
5514
5494
  }
5515
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) {
5516
5503
  const queue = new AsyncEventQueue();
5517
5504
  const parser = new NdjsonParser();
5518
5505
  let child;
@@ -5537,8 +5524,7 @@ var OpenCodeRunner = class {
5537
5524
  ...cmd.slice(1),
5538
5525
  "run",
5539
5526
  "--format",
5540
- "json",
5541
- "--no-color"
5527
+ "json"
5542
5528
  ];
5543
5529
  if (run.model) {
5544
5530
  args2.push("--model", run.model);
@@ -5546,12 +5532,10 @@ var OpenCodeRunner = class {
5546
5532
  if (run.resumeConversationId) {
5547
5533
  args2.push("--session", run.resumeConversationId);
5548
5534
  }
5549
- const mode = run.permissionMode ?? "guarded";
5550
- if (mode === "bypass") {
5551
- args2.push("--auto");
5552
- } else if (mode === "plan") {
5535
+ if (mode === "plan") {
5553
5536
  args2.push("--agent", "plan");
5554
5537
  } else {
5538
+ args2.push("--auto");
5555
5539
  }
5556
5540
  if (run.effort) {
5557
5541
  const variantMap = { low: "minimal", medium: "high", high: "max", max: "max" };
@@ -5622,12 +5606,283 @@ stderr: ${stderrTail}` : ""}`
5622
5606
  });
5623
5607
  return {
5624
5608
  events: queue,
5625
- respond: (approvalId, ok, answer) => {
5609
+ // No broker (or bypass/plan, which never ask) nothing to answer.
5610
+ respond: () => {
5626
5611
  },
5627
5612
  cancel: () => child.kill()
5628
5613
  };
5629
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
+ }
5630
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
+ }
5631
5886
 
5632
5887
  // ../daemon/src/session-manager.ts
5633
5888
  import { randomUUID } from "node:crypto";
@@ -5748,7 +6003,7 @@ function summarizeConversation(path, workspace) {
5748
6003
  return {
5749
6004
  conversationId: basename(path, ".jsonl"),
5750
6005
  workspace,
5751
- firstPrompt: truncate4(firstPrompt || "(empty prompt)", 120),
6006
+ firstPrompt: truncate5(firstPrompt || "(empty prompt)", 120),
5752
6007
  lastActiveMs: Math.floor(statSync(path).mtimeMs),
5753
6008
  messageCount
5754
6009
  };
@@ -5774,7 +6029,7 @@ function userMessageText(value) {
5774
6029
  function collapseText(text) {
5775
6030
  return text.replace(/\s+/g, " ").trim();
5776
6031
  }
5777
- function truncate4(text, max) {
6032
+ function truncate5(text, max) {
5778
6033
  return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
5779
6034
  }
5780
6035
  function safeReadDir(path) {
@@ -6557,7 +6812,7 @@ function searchableText(msg) {
6557
6812
  }
6558
6813
 
6559
6814
  // ../daemon/src/local-server.ts
6560
- import { createServer } from "node:http";
6815
+ import { createServer as createServer2 } from "node:http";
6561
6816
 
6562
6817
  // ../node_modules/.pnpm/ws@8.21.3/node_modules/ws/wrapper.mjs
6563
6818
  var import_stream = __toESM(require_stream(), 1);
@@ -11014,9 +11269,9 @@ if (typeof Module.getRandomValue === "undefined") {
11014
11269
  Module.getRandomValue = randomValuesStandard;
11015
11270
  } catch (e) {
11016
11271
  try {
11017
- crypto2 = null;
11272
+ crypto = null;
11018
11273
  randomValueNodeJS = function() {
11019
- var buf = crypto2["randomBytes"](4);
11274
+ var buf = crypto["randomBytes"](4);
11020
11275
  return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
11021
11276
  };
11022
11277
  randomValueNodeJS();
@@ -11029,7 +11284,7 @@ if (typeof Module.getRandomValue === "undefined") {
11029
11284
  var window_;
11030
11285
  var crypto_;
11031
11286
  var randomValuesStandard;
11032
- var crypto2;
11287
+ var crypto;
11033
11288
  var randomValueNodeJS;
11034
11289
  var _Module = Module;
11035
11290
  Module.ready = new Promise(function(resolve6, reject) {
@@ -37923,7 +38178,7 @@ Module.ready = new Promise(function(resolve6, reject) {
37923
38178
  try {
37924
38179
  var window_ = "object" === typeof window ? window : self;
37925
38180
  var crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto;
37926
- crypto_ = crypto_ === void 0 ? crypto2 : crypto_;
38181
+ crypto_ = crypto_ === void 0 ? crypto : crypto_;
37927
38182
  var randomValuesStandard = function() {
37928
38183
  var buf = new Uint32Array(1);
37929
38184
  crypto_.getRandomValues(buf);
@@ -37933,9 +38188,9 @@ Module.ready = new Promise(function(resolve6, reject) {
37933
38188
  Module3.getRandomValue = randomValuesStandard;
37934
38189
  } catch (e) {
37935
38190
  try {
37936
- var crypto2 = null;
38191
+ var crypto = null;
37937
38192
  var randomValueNodeJS = function() {
37938
- var buf = crypto2["randomBytes"](4);
38193
+ var buf = crypto["randomBytes"](4);
37939
38194
  return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
37940
38195
  };
37941
38196
  randomValueNodeJS();
@@ -38581,7 +38836,7 @@ Module.ready = new Promise(function(resolve6, reject) {
38581
38836
  try {
38582
38837
  var window_ = "object" === typeof window ? window : self;
38583
38838
  var crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto;
38584
- crypto_ = crypto_ === void 0 ? crypto2 : crypto_;
38839
+ crypto_ = crypto_ === void 0 ? crypto : crypto_;
38585
38840
  var randomValuesStandard = function() {
38586
38841
  var buf = new Uint32Array(1);
38587
38842
  crypto_.getRandomValues(buf);
@@ -38591,9 +38846,9 @@ Module.ready = new Promise(function(resolve6, reject) {
38591
38846
  Module2.getRandomValue = randomValuesStandard;
38592
38847
  } catch (e) {
38593
38848
  try {
38594
- var crypto2 = null;
38849
+ var crypto = null;
38595
38850
  var randomValueNodeJS = function() {
38596
- var buf = crypto2["randomBytes"](4);
38851
+ var buf = crypto["randomBytes"](4);
38597
38852
  return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
38598
38853
  };
38599
38854
  randomValueNodeJS();
@@ -41515,7 +41770,7 @@ var LocalSessionServer = class {
41515
41770
  constructor(manager2, port2, broker2) {
41516
41771
  this.manager = manager2;
41517
41772
  this.broker = broker2;
41518
- const http = createServer((req, res) => void this.onRequest(req, res));
41773
+ const http = createServer2((req, res) => void this.onRequest(req, res));
41519
41774
  this.wss = new import_websocket_server.default({ server: http });
41520
41775
  this.wss.on("connection", (ws) => this.onConnection(ws));
41521
41776
  http.on("error", (err) => {
@@ -41864,7 +42119,7 @@ var ApprovalBroker = class {
41864
42119
  };
41865
42120
  function classifyRisk(toolName, input) {
41866
42121
  const i2 = input ?? {};
41867
- 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(" ");
41868
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(
41869
42124
  text
41870
42125
  ) ? "high" : "low";
@@ -41873,11 +42128,11 @@ function summariseInput(input) {
41873
42128
  if (typeof input !== "object" || input === null) return String(input ?? "");
41874
42129
  const i2 = input;
41875
42130
  const q2 = firstQuestion(i2);
41876
- if (q2) return truncate5(q2.text, 200);
41877
- for (const key of ["command", "file_path", "path", "url", "pattern", "description"]) {
41878
- 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);
41879
42134
  }
41880
- return truncate5(JSON.stringify(input), 200);
42135
+ return truncate6(JSON.stringify(input), 200);
41881
42136
  }
41882
42137
  function firstQuestion(i2) {
41883
42138
  if (!Array.isArray(i2.questions) || i2.questions.length === 0) return void 0;
@@ -41895,7 +42150,7 @@ function buildPreview(toolName, input) {
41895
42150
  const q2 = firstQuestion(i2);
41896
42151
  if (q2) {
41897
42152
  const lines = [q2.text, ...q2.options.map((o2, n2) => `${n2 + 1}. ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)];
41898
- return truncate5(lines.join("\n"), 600);
42153
+ return truncate6(lines.join("\n"), 600);
41899
42154
  }
41900
42155
  if (/^(Edit|MultiEdit)/.test(toolName) && typeof i2.new_string === "string") {
41901
42156
  const oldS = typeof i2.old_string === "string" ? i2.old_string : "";
@@ -41903,15 +42158,24 @@ function buildPreview(toolName, input) {
41903
42158
  ...oldS.split("\n").map((l2) => `- ${l2}`),
41904
42159
  ...i2.new_string.split("\n").map((l2) => `+ ${l2}`)
41905
42160
  ];
41906
- return truncate5(lines.join("\n"), 600);
42161
+ return truncate6(lines.join("\n"), 600);
41907
42162
  }
41908
42163
  if (/^Write/.test(toolName) && typeof i2.content === "string") {
41909
- 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);
41910
42168
  }
41911
- 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);
41912
42171
  return void 0;
41913
42172
  }
41914
- 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) {
41915
42179
  return s2.length <= max ? s2 : s2.slice(0, max - 1) + "\u2026";
41916
42180
  }
41917
42181
 
@@ -42102,7 +42366,7 @@ var approvalUrl = `http://127.0.0.1:${port}/approval`;
42102
42366
  var runners = [
42103
42367
  new ClaudeCodeRunner(broker, approvalUrl),
42104
42368
  new CopilotCliRunner(),
42105
- new OpenCodeRunner(),
42369
+ new OpenCodeRunner(broker),
42106
42370
  new CodexCliRunner(),
42107
42371
  new CursorAgentRunner(),
42108
42372
  new GeminiCliRunner()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offhands",
3
- "version": "0.1.4",
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",