offhands 0.1.1 → 0.1.3

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 +659 -103
  2. package/package.json +26 -28
package/dist/daemon.mjs CHANGED
@@ -4776,8 +4776,8 @@ var require_main = __commonJS({
4776
4776
  });
4777
4777
 
4778
4778
  // ../daemon/src/index.ts
4779
- import { resolve as resolve4 } from "node:path";
4780
- import { existsSync as existsSync7 } from "node:fs";
4779
+ import { resolve as resolve5 } from "node:path";
4780
+ import { existsSync as existsSync9 } from "node:fs";
4781
4781
 
4782
4782
  // ../daemon/src/runners/claude-code.ts
4783
4783
  import { spawn } from "node:child_process";
@@ -4830,8 +4830,31 @@ function mapClaudeEvent(value) {
4830
4830
  if (delta?.type === "text_delta" && typeof delta.text === "string" && delta.text !== "") {
4831
4831
  return [{ type: "text", chunk: delta.text }];
4832
4832
  }
4833
+ if (delta?.type === "thinking_delta" && typeof delta.thinking === "string" && delta.thinking !== "") {
4834
+ return [{ type: "thinking", chunk: delta.thinking }];
4835
+ }
4833
4836
  return [];
4834
4837
  }
4838
+ // Account-level plan usage, pushed by the CLI independent of any run's
4839
+ // result. Real numbers only — omit rather than guess if the shape drifts.
4840
+ case "rate_limit_event": {
4841
+ const info = v2.rate_limit_info;
4842
+ const windows = info?.unifiedWindows;
4843
+ const fiveHour = windows?.five_hour;
4844
+ const sevenDay = windows?.seven_day;
4845
+ if (typeof fiveHour?.utilization !== "number" || typeof fiveHour?.resetsAt !== "number" || typeof sevenDay?.utilization !== "number" || typeof sevenDay?.resetsAt !== "number") {
4846
+ return [];
4847
+ }
4848
+ return [
4849
+ {
4850
+ type: "rate-limit",
4851
+ fiveHourUtilization: fiveHour.utilization,
4852
+ fiveHourResetsAtMs: fiveHour.resetsAt * 1e3,
4853
+ sevenDayUtilization: sevenDay.utilization,
4854
+ sevenDayResetsAtMs: sevenDay.resetsAt * 1e3
4855
+ }
4856
+ ];
4857
+ }
4835
4858
  case "assistant": {
4836
4859
  const message = v2.message;
4837
4860
  const content = Array.isArray(message?.content) ? message.content : [];
@@ -4874,11 +4897,11 @@ function contextUsage(usage, costUsd) {
4874
4897
  function summariseToolInput(name, input) {
4875
4898
  if (typeof input !== "object" || input === null) return name;
4876
4899
  const i2 = input;
4877
- const firstString2 = (...keys) => {
4900
+ const firstString3 = (...keys) => {
4878
4901
  for (const k2 of keys) if (typeof i2[k2] === "string" && i2[k2] !== "") return i2[k2];
4879
4902
  return void 0;
4880
4903
  };
4881
- const hint = firstString2("file_path", "path", "command", "pattern", "query", "url", "description") ?? "";
4904
+ const hint = firstString3("file_path", "path", "command", "pattern", "query", "url", "description") ?? "";
4882
4905
  return hint ? `${name}: ${truncate(hint, 120)}` : name;
4883
4906
  }
4884
4907
  function truncate(s2, max) {
@@ -4903,7 +4926,7 @@ var AsyncEventQueue = class {
4903
4926
  for (; ; ) {
4904
4927
  while (this.buffer.length > 0) yield this.buffer.shift();
4905
4928
  if (this.closed) return;
4906
- await new Promise((resolve5) => this.waiter = resolve5);
4929
+ await new Promise((resolve6) => this.waiter = resolve6);
4907
4930
  this.waiter = null;
4908
4931
  }
4909
4932
  }
@@ -4931,10 +4954,10 @@ var ClaudeCodeRunner = class {
4931
4954
  return "claude";
4932
4955
  }
4933
4956
  async detect() {
4934
- return new Promise((resolve5) => {
4957
+ return new Promise((resolve6) => {
4935
4958
  const p2 = spawn(this.resolveBin(), ["--version"], { stdio: "ignore" });
4936
- p2.on("error", () => resolve5(false));
4937
- p2.on("exit", (code) => resolve5(code === 0));
4959
+ p2.on("error", () => resolve6(false));
4960
+ p2.on("exit", (code) => resolve6(code === 0));
4938
4961
  });
4939
4962
  }
4940
4963
  start(run, callbacks) {
@@ -5052,10 +5075,10 @@ function extractSessionId(value) {
5052
5075
  // ../daemon/src/runners/stubs.ts
5053
5076
  import { spawn as spawn2 } from "node:child_process";
5054
5077
  function commandExists(bin) {
5055
- return new Promise((resolve5) => {
5078
+ return new Promise((resolve6) => {
5056
5079
  const p2 = spawn2(bin, ["--version"], { stdio: "ignore", shell: process.platform === "win32" });
5057
- p2.on("error", () => resolve5(false));
5058
- p2.on("exit", (code) => resolve5(code === 0));
5080
+ p2.on("error", () => resolve6(false));
5081
+ p2.on("exit", (code) => resolve6(code === 0));
5059
5082
  });
5060
5083
  }
5061
5084
  function notImplemented(id) {
@@ -5177,10 +5200,10 @@ var CopilotCliRunner = class {
5177
5200
  async detect() {
5178
5201
  const cmd = this.resolveCommand();
5179
5202
  if (!cmd) return false;
5180
- return new Promise((resolve5) => {
5203
+ return new Promise((resolve6) => {
5181
5204
  const p2 = spawn3(cmd[0], [...cmd.slice(1), "--version"], { stdio: "ignore" });
5182
- p2.on("error", () => resolve5(false));
5183
- p2.on("exit", (code) => resolve5(code === 0));
5205
+ p2.on("error", () => resolve6(false));
5206
+ p2.on("exit", (code) => resolve6(code === 0));
5184
5207
  });
5185
5208
  }
5186
5209
  loggedIn() {
@@ -5274,11 +5297,345 @@ stderr: ${stderrTail}` : ""}`
5274
5297
  }
5275
5298
  };
5276
5299
 
5300
+ // ../daemon/src/runners/opencode-cli.ts
5301
+ import { spawn as spawn4 } from "node:child_process";
5302
+ import { existsSync as existsSync3 } from "node:fs";
5303
+ import { homedir as homedir3 } from "node:os";
5304
+ import { delimiter as delimiter2, join as join3 } from "node:path";
5305
+
5306
+ // ../daemon/src/runners/opencode-map.ts
5307
+ function mapOpenCodeEvent(value) {
5308
+ if (typeof value !== "object" || value === null) return [];
5309
+ 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") ?? "";
5323
+ return [{ type: "tool", name, summary: summary ? `${name}: ${truncate3(summary, 120)}` : name }];
5324
+ }
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")})` }];
5350
+ }
5351
+ const events = [{ type: "done", summary: text }];
5352
+ const usage = extractUsage(v2, data);
5353
+ if (usage) events.push(usage);
5354
+ return events;
5355
+ }
5356
+ if (type === "error" || type === "failed") {
5357
+ const message = firstString2(v2, "message", "error", "detail", "result") ?? "unknown error";
5358
+ return [{ type: "error", message }];
5359
+ }
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
+ return [];
5368
+ }
5369
+ function extractOpenCodeSessionId(value) {
5370
+ if (typeof value !== "object" || value === null) return null;
5371
+ 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");
5396
+ if (contextTokens <= 0 && costUsd === void 0) return null;
5397
+ return {
5398
+ type: "usage",
5399
+ contextTokens,
5400
+ contextWindow: 2e5,
5401
+ // OpenCode doesn't expose window; use reasonable default
5402
+ ...costUsd !== void 0 ? { costUsd } : {}
5403
+ };
5404
+ }
5405
+ function firstString2(obj, ...keys) {
5406
+ for (const k2 of keys) {
5407
+ const val = obj[k2];
5408
+ if (typeof val === "string" && val !== "") return val;
5409
+ }
5410
+ return void 0;
5411
+ }
5412
+ function truncate3(s2, max) {
5413
+ return s2.length <= max ? s2 : s2.slice(0, max - 1) + "\u2026";
5414
+ }
5415
+
5416
+ // ../daemon/src/runners/opencode-cli.ts
5417
+ var OpenCodeRunner = class {
5418
+ id = "opencode";
5419
+ name = "OpenCode";
5420
+ supportsApprovals = true;
5421
+ models = [];
5422
+ // populated dynamically via `opencode models`
5423
+ /** [command, ...prefixArgs] resolved once. */
5424
+ resolved = null;
5425
+ modelsCache = [];
5426
+ resolveCommand() {
5427
+ if (this.resolved) return this.resolved;
5428
+ if (process.platform === "win32") {
5429
+ const pathDirs = (process.env.PATH ?? "").split(delimiter2);
5430
+ const npmGlobalPaths = [
5431
+ process.env.APPDATA ? join3(process.env.APPDATA, "npm") : null,
5432
+ process.env.LOCALAPPDATA ? join3(process.env.LOCALAPPDATA, "npm") : null,
5433
+ "C:\\nvm4w\\nodejs",
5434
+ "C:\\Program Files\\nodejs",
5435
+ "C:\\Program Files (x86)\\nodejs"
5436
+ ].filter((p2) => p2 !== null && existsSync3(p2));
5437
+ const allDirs = [...pathDirs, ...npmGlobalPaths];
5438
+ for (const pathDir of allDirs) {
5439
+ if (!pathDir) continue;
5440
+ const cmdPath = join3(pathDir, "opencode.cmd");
5441
+ if (existsSync3(cmdPath)) {
5442
+ this.resolved = [cmdPath];
5443
+ return this.resolved;
5444
+ }
5445
+ const nodeModulesBin = join3(pathDir, "node_modules", ".bin", "opencode.cmd");
5446
+ if (existsSync3(nodeModulesBin)) {
5447
+ this.resolved = [nodeModulesBin];
5448
+ return this.resolved;
5449
+ }
5450
+ for (const bin of ["bun", "pnpm", "yarn"]) {
5451
+ const binPath = join3(pathDir, bin + ".cmd");
5452
+ if (existsSync3(binPath)) {
5453
+ this.resolved = [binPath, "exec", "opencode"];
5454
+ return this.resolved;
5455
+ }
5456
+ }
5457
+ }
5458
+ const native = join3(homedir3(), "AppData", "Local", "opencode", "opencode.exe");
5459
+ if (existsSync3(native)) {
5460
+ this.resolved = [native];
5461
+ return this.resolved;
5462
+ }
5463
+ return null;
5464
+ }
5465
+ this.resolved = ["opencode"];
5466
+ return this.resolved;
5467
+ }
5468
+ async detect() {
5469
+ const cmd = this.resolveCommand();
5470
+ if (!cmd) return false;
5471
+ await this.fetchModels();
5472
+ return new Promise((resolve6) => {
5473
+ const p2 = spawn4(cmd[0], [...cmd.slice(1), "--version"], { stdio: "ignore", shell: process.platform === "win32" });
5474
+ p2.on("error", () => resolve6(false));
5475
+ p2.on("exit", (code) => resolve6(code === 0));
5476
+ });
5477
+ }
5478
+ async fetchModels() {
5479
+ if (this.modelsCache.length > 0) return this.modelsCache;
5480
+ const cmd = this.resolveCommand();
5481
+ if (!cmd) return [];
5482
+ return new Promise((resolve6) => {
5483
+ const p2 = spawn4(cmd[0], [...cmd.slice(1), "models"], {
5484
+ stdio: ["ignore", "pipe", "ignore"],
5485
+ shell: process.platform === "win32"
5486
+ });
5487
+ let output = "";
5488
+ p2.stdout.setEncoding("utf8");
5489
+ p2.stdout.on("data", (chunk) => {
5490
+ output += chunk;
5491
+ });
5492
+ p2.on("close", (code) => {
5493
+ if (code === 0) {
5494
+ try {
5495
+ const lines = output.trim().split("\n");
5496
+ const models = lines.map((l2) => l2.trim()).filter((l2) => l2 && !l2.startsWith("opencode/"));
5497
+ this.modelsCache = models;
5498
+ Object.defineProperty(this, "models", {
5499
+ value: models,
5500
+ writable: false,
5501
+ configurable: true
5502
+ });
5503
+ resolve6(models);
5504
+ } catch {
5505
+ resolve6([]);
5506
+ }
5507
+ } else {
5508
+ resolve6([]);
5509
+ }
5510
+ });
5511
+ p2.on("error", () => resolve6([]));
5512
+ });
5513
+ }
5514
+ loggedIn() {
5515
+ return existsSync3(join3(homedir3(), ".local", "share", "opencode", "auth.json"));
5516
+ }
5517
+ start(run, callbacks) {
5518
+ const queue = new AsyncEventQueue();
5519
+ const parser = new NdjsonParser();
5520
+ let child;
5521
+ let stderrTail = "";
5522
+ let sawTerminal = false;
5523
+ let reportedConversation = false;
5524
+ const emit = (events) => {
5525
+ for (const e of events) {
5526
+ if (e.type === "done" || e.type === "error") sawTerminal = true;
5527
+ queue.push(e);
5528
+ }
5529
+ };
5530
+ const cmd = this.resolveCommand();
5531
+ if (!cmd) {
5532
+ queue.push({ type: "error", message: "opencode CLI not found" });
5533
+ queue.close();
5534
+ return { events: queue, respond: () => {
5535
+ }, cancel: () => {
5536
+ } };
5537
+ }
5538
+ const args2 = [
5539
+ ...cmd.slice(1),
5540
+ "run",
5541
+ run.prompt,
5542
+ "--format",
5543
+ "json",
5544
+ "--no-color"
5545
+ ];
5546
+ if (run.model) {
5547
+ args2.push("--model", run.model);
5548
+ }
5549
+ if (run.resumeConversationId) {
5550
+ args2.push("--session", run.resumeConversationId);
5551
+ }
5552
+ const mode = run.permissionMode ?? "guarded";
5553
+ if (mode === "bypass") {
5554
+ args2.push("--auto");
5555
+ } else if (mode === "plan") {
5556
+ args2.push("--agent", "plan");
5557
+ } else {
5558
+ }
5559
+ if (run.effort) {
5560
+ const variantMap = { low: "minimal", medium: "high", high: "max", max: "max" };
5561
+ const variant = variantMap[run.effort];
5562
+ if (variant) {
5563
+ args2.push("--variant", variant);
5564
+ args2.push("--thinking");
5565
+ }
5566
+ }
5567
+ if (run.attachments?.length) {
5568
+ for (const file of run.attachments) {
5569
+ args2.push("--file", file);
5570
+ }
5571
+ }
5572
+ args2.push("--dir", run.workspace);
5573
+ try {
5574
+ child = spawn4(cmd[0], args2, {
5575
+ cwd: run.workspace,
5576
+ stdio: ["ignore", "pipe", "pipe"],
5577
+ shell: process.platform === "win32"
5578
+ });
5579
+ } catch (e) {
5580
+ queue.push({ type: "error", message: `failed to spawn opencode: ${String(e)}` });
5581
+ queue.close();
5582
+ return { events: queue, respond: () => {
5583
+ }, cancel: () => {
5584
+ } };
5585
+ }
5586
+ child.stdout.setEncoding("utf8");
5587
+ child.stdout.on("data", (chunk) => {
5588
+ for (const result of parser.push(chunk)) {
5589
+ if (result.ok) {
5590
+ if (!reportedConversation) {
5591
+ const id = extractOpenCodeSessionId(result.value);
5592
+ if (id) {
5593
+ reportedConversation = true;
5594
+ callbacks?.onConversationId?.(id);
5595
+ }
5596
+ }
5597
+ emit(mapOpenCodeEvent(result.value));
5598
+ }
5599
+ }
5600
+ });
5601
+ child.stderr.setEncoding("utf8");
5602
+ child.stderr.on("data", (chunk) => {
5603
+ stderrTail = (stderrTail + chunk).slice(-2e3);
5604
+ });
5605
+ child.on("error", (err) => {
5606
+ emit([{ type: "error", message: `opencode process error: ${err.message}` }]);
5607
+ queue.close();
5608
+ });
5609
+ child.on("close", (code) => {
5610
+ for (const result of parser.flush()) {
5611
+ if (result.ok) emit(mapOpenCodeEvent(result.value));
5612
+ }
5613
+ if (!sawTerminal) {
5614
+ emit([
5615
+ {
5616
+ type: "error",
5617
+ message: `opencode exited with code ${code ?? "unknown"} before a result${stderrTail ? `
5618
+ stderr: ${stderrTail}` : ""}`
5619
+ }
5620
+ ]);
5621
+ }
5622
+ queue.close();
5623
+ });
5624
+ return {
5625
+ events: queue,
5626
+ respond: (approvalId, ok, answer) => {
5627
+ },
5628
+ cancel: () => child.kill()
5629
+ };
5630
+ }
5631
+ };
5632
+
5277
5633
  // ../daemon/src/session-manager.ts
5278
5634
  import { randomUUID } from "node:crypto";
5279
5635
  import { hostname, platform, tmpdir } from "node:os";
5280
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readdirSync as readdirSync3, writeFileSync as writeFileSync2 } from "node:fs";
5281
- import { dirname as dirname2, join as join5, basename as basename3, parse as parse2, resolve as resolve3 } from "node:path";
5636
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
5637
+ import { dirname as dirname2, join as join6, basename as basename3, parse as parse2, resolve as resolve3 } from "node:path";
5638
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
5282
5639
 
5283
5640
  // ../daemon/src/receipts.ts
5284
5641
  import { execFile } from "node:child_process";
@@ -5342,25 +5699,25 @@ function shouldScreenshot(touchedFiles, devUrl2) {
5342
5699
  }
5343
5700
 
5344
5701
  // ../daemon/src/claude-history.ts
5345
- import { existsSync as existsSync3, readdirSync, readFileSync, statSync } from "node:fs";
5346
- import { homedir as homedir3 } from "node:os";
5347
- import { basename, join as join3, resolve } from "node:path";
5348
- var DEFAULT_PROJECTS_DIR = join3(homedir3(), ".claude", "projects");
5702
+ import { existsSync as existsSync4, readdirSync, readFileSync, statSync } from "node:fs";
5703
+ import { homedir as homedir4 } from "node:os";
5704
+ import { basename, join as join4, resolve } from "node:path";
5705
+ var DEFAULT_PROJECTS_DIR = join4(homedir4(), ".claude", "projects");
5349
5706
  function listClaudeConversations(workspace, projectsDir = process.env.OFFHAND_CLAUDE_PROJECTS_DIR ?? DEFAULT_PROJECTS_DIR) {
5350
5707
  const projectDir = findProjectDir(workspace, projectsDir);
5351
5708
  if (!projectDir) return [];
5352
5709
  return listJsonlFiles(projectDir).map((path) => summarizeConversation(path, workspace)).filter((c2) => c2 !== null).sort((a2, b2) => b2.lastActiveMs - a2.lastActiveMs).slice(0, 50);
5353
5710
  }
5354
5711
  function findProjectDir(workspace, projectsDir) {
5355
- if (!existsSync3(projectsDir)) return null;
5712
+ if (!existsSync4(projectsDir)) return null;
5356
5713
  const candidates = /* @__PURE__ */ new Set([encodeWorkspace(workspace), encodeWorkspace(resolve(workspace))]);
5357
5714
  for (const candidate of candidates) {
5358
- const exact = join3(projectsDir, candidate);
5359
- if (existsSync3(exact)) return exact;
5715
+ const exact = join4(projectsDir, candidate);
5716
+ if (existsSync4(exact)) return exact;
5360
5717
  }
5361
5718
  const lower = new Set([...candidates].map((c2) => c2.toLowerCase()));
5362
5719
  for (const entry of safeReadDir(projectsDir)) {
5363
- if (entry.isDirectory() && lower.has(entry.name.toLowerCase())) return join3(projectsDir, entry.name);
5720
+ if (entry.isDirectory() && lower.has(entry.name.toLowerCase())) return join4(projectsDir, entry.name);
5364
5721
  }
5365
5722
  return null;
5366
5723
  }
@@ -5368,12 +5725,12 @@ function encodeWorkspace(workspace) {
5368
5725
  return workspace.replace(/[^A-Za-z0-9]/g, "-");
5369
5726
  }
5370
5727
  function listJsonlFiles(projectDir) {
5371
- const files = safeReadDir(projectDir).filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => join3(projectDir, entry.name));
5372
- const sessionsDir = join3(projectDir, "sessions");
5373
- if (!existsSync3(sessionsDir)) return files;
5728
+ const files = safeReadDir(projectDir).filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => join4(projectDir, entry.name));
5729
+ const sessionsDir = join4(projectDir, "sessions");
5730
+ if (!existsSync4(sessionsDir)) return files;
5374
5731
  return [
5375
5732
  ...files,
5376
- ...safeReadDir(sessionsDir).filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => join3(sessionsDir, entry.name))
5733
+ ...safeReadDir(sessionsDir).filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => join4(sessionsDir, entry.name))
5377
5734
  ];
5378
5735
  }
5379
5736
  function summarizeConversation(path, workspace) {
@@ -5392,7 +5749,7 @@ function summarizeConversation(path, workspace) {
5392
5749
  return {
5393
5750
  conversationId: basename(path, ".jsonl"),
5394
5751
  workspace,
5395
- firstPrompt: truncate3(firstPrompt || "(empty prompt)", 120),
5752
+ firstPrompt: truncate4(firstPrompt || "(empty prompt)", 120),
5396
5753
  lastActiveMs: Math.floor(statSync(path).mtimeMs),
5397
5754
  messageCount
5398
5755
  };
@@ -5418,7 +5775,7 @@ function userMessageText(value) {
5418
5775
  function collapseText(text) {
5419
5776
  return text.replace(/\s+/g, " ").trim();
5420
5777
  }
5421
- function truncate3(text, max) {
5778
+ function truncate4(text, max) {
5422
5779
  return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
5423
5780
  }
5424
5781
  function safeReadDir(path) {
@@ -5435,7 +5792,7 @@ function isRecord(value) {
5435
5792
  // ../daemon/src/drop.ts
5436
5793
  import {
5437
5794
  copyFileSync,
5438
- existsSync as existsSync4,
5795
+ existsSync as existsSync5,
5439
5796
  mkdirSync,
5440
5797
  readdirSync as readdirSync2,
5441
5798
  readFileSync as readFileSync2,
@@ -5444,18 +5801,18 @@ import {
5444
5801
  watch,
5445
5802
  writeFileSync
5446
5803
  } from "node:fs";
5447
- import { homedir as homedir4 } from "node:os";
5448
- import { basename as basename2, join as join4, parse, resolve as resolve2 } from "node:path";
5449
- import { spawn as spawn4 } from "node:child_process";
5804
+ import { homedir as homedir5 } from "node:os";
5805
+ import { basename as basename2, join as join5, parse, resolve as resolve2 } from "node:path";
5806
+ import { spawn as spawn5 } from "node:child_process";
5450
5807
  var DROP_LOG_SESSION_ID = "drop";
5451
5808
  function offhandHome() {
5452
- return process.env.OFFHAND_HOME ?? join4(homedir4(), ".offhand");
5809
+ return process.env.OFFHAND_HOME ?? join5(homedir5(), ".offhand");
5453
5810
  }
5454
5811
  function dropOutboxDir(home = offhandHome()) {
5455
- return join4(home, "drop-outbox");
5812
+ return join5(home, "drop-outbox");
5456
5813
  }
5457
5814
  function incomingDropDir() {
5458
- return join4(homedir4(), "Downloads", "offhand");
5815
+ return join5(homedir5(), "Downloads", "offhand");
5459
5816
  }
5460
5817
  function safeDropName(input) {
5461
5818
  const base = basename2(input).replace(/[<>:"/\\|?*\x00-\x1F]/g, "_").replace(/[. ]+$/g, "").trim();
@@ -5479,13 +5836,13 @@ function queueDropFileForPhone(sourcePath, outbox = dropOutboxDir()) {
5479
5836
  const st = statSync2(source);
5480
5837
  if (!st.isFile()) throw new Error(`not a file: ${sourcePath}`);
5481
5838
  mkdirSync(outbox, { recursive: true });
5482
- const dest = join4(outbox, dedupeDropName(basename2(source), safeNames(outbox)));
5839
+ const dest = join5(outbox, dedupeDropName(basename2(source), safeNames(outbox)));
5483
5840
  copyFileSync(source, dest);
5484
5841
  return dest;
5485
5842
  }
5486
5843
  function saveIncomingDrop(name, bytes, dir = incomingDropDir()) {
5487
5844
  mkdirSync(dir, { recursive: true });
5488
- const path = join4(dir, dedupeDropName(name, safeNames(dir)));
5845
+ const path = join5(dir, dedupeDropName(name, safeNames(dir)));
5489
5846
  writeFileSync(path, bytes);
5490
5847
  return path;
5491
5848
  }
@@ -5528,7 +5885,7 @@ function startDropOutboxWatcher(outbox, sendFile) {
5528
5885
  console.error(`drop: sent but could not delete ${path}: ${e instanceof Error ? e.message : String(e)}`);
5529
5886
  }
5530
5887
  } catch (e) {
5531
- if (existsSync4(path)) {
5888
+ if (existsSync5(path)) {
5532
5889
  console.error(`drop: failed to send ${path}: ${e instanceof Error ? e.message : String(e)}`);
5533
5890
  schedule(15e3);
5534
5891
  }
@@ -5540,7 +5897,7 @@ function startDropOutboxWatcher(outbox, sendFile) {
5540
5897
  timer = null;
5541
5898
  if (closed) return;
5542
5899
  for (const entry of safeEntries(outbox)) {
5543
- if (entry.isFile()) void processFile(join4(outbox, entry.name));
5900
+ if (entry.isFile()) void processFile(join5(outbox, entry.name));
5544
5901
  }
5545
5902
  };
5546
5903
  watcher = watch(outbox, () => schedule());
@@ -5568,7 +5925,7 @@ function showDropToast(savedPath, textToClipboard) {
5568
5925
  `$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)`,
5569
5926
  `[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier(${psQuote(appId)}).Show($toast)`
5570
5927
  ].filter(Boolean);
5571
- const child = spawn4("powershell", ["-NoProfile", "-Command", lines.join("; ")], {
5928
+ const child = spawn5("powershell", ["-NoProfile", "-Command", lines.join("; ")], {
5572
5929
  windowsHide: true,
5573
5930
  stdio: ["ignore", "ignore", "pipe"]
5574
5931
  });
@@ -5624,7 +5981,16 @@ function mimeFromName(path) {
5624
5981
  }
5625
5982
 
5626
5983
  // ../daemon/src/session-manager.ts
5627
- var DAEMON_VERSION = "0.1.0";
5984
+ function resolveDaemonVersion() {
5985
+ try {
5986
+ const pkgPath = join6(dirname2(fileURLToPath2(import.meta.url)), "..", "package.json");
5987
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
5988
+ return pkg.version ?? "0.0.0";
5989
+ } catch {
5990
+ return "0.0.0";
5991
+ }
5992
+ }
5993
+ var DAEMON_VERSION = resolveDaemonVersion();
5628
5994
  var SessionManager = class {
5629
5995
  constructor(store2, runners2) {
5630
5996
  this.store = store2;
@@ -5715,9 +6081,9 @@ var SessionManager = class {
5715
6081
  prompt += "\n\nAttached files (read them from disk):";
5716
6082
  for (const a2 of msg.attachments) {
5717
6083
  const bytes = await this.attachmentFetcher(a2.blobId);
5718
- const dir = join5(tmpdir(), "offhand-attachments");
6084
+ const dir = join6(tmpdir(), "offhand-attachments");
5719
6085
  mkdirSync2(dir, { recursive: true });
5720
- const path = join5(dir, `${a2.blobId.slice(0, 8)}-${basename3(a2.name)}`);
6086
+ const path = join6(dir, `${a2.blobId.slice(0, 8)}-${basename3(a2.name)}`);
5721
6087
  writeFileSync2(path, bytes);
5722
6088
  prompt += `
5723
6089
  - ${path} (${a2.mime})`;
@@ -5955,17 +6321,17 @@ function listFolders(path) {
5955
6321
  if (!path) return { path: "", parent: null, dirs: driveRoots() };
5956
6322
  const current = resolve3(path);
5957
6323
  const dirs = safeReadDir2(current).filter((entry) => entry.isDirectory() && !skipDir(entry.name)).map((entry) => {
5958
- const full = join5(current, entry.name);
5959
- return { name: entry.name, path: full, isGit: existsSync5(join5(full, ".git")) };
6324
+ const full = join6(current, entry.name);
6325
+ return { name: entry.name, path: full, isGit: existsSync6(join6(full, ".git")) };
5960
6326
  }).sort((a2, b2) => Number(b2.isGit) - Number(a2.isGit) || a2.name.localeCompare(b2.name)).slice(0, 200);
5961
6327
  return { path: current, parent: isRoot(current) ? null : dirname2(current), dirs };
5962
6328
  }
5963
6329
  function driveRoots() {
5964
- if (process.platform !== "win32") return [{ name: "/", path: "/", isGit: existsSync5("/.git") }];
6330
+ if (process.platform !== "win32") return [{ name: "/", path: "/", isGit: existsSync6("/.git") }];
5965
6331
  const roots = [];
5966
6332
  for (let code = 67; code <= 90; code++) {
5967
6333
  const name = `${String.fromCharCode(code)}:\\`;
5968
- if (existsSync5(name)) roots.push({ name, path: name, isGit: existsSync5(join5(name, ".git")) });
6334
+ if (existsSync6(name)) roots.push({ name, path: name, isGit: existsSync6(join6(name, ".git")) });
5969
6335
  }
5970
6336
  return roots.sort((a2, b2) => a2.name.localeCompare(b2.name)).slice(0, 200);
5971
6337
  }
@@ -5998,14 +6364,14 @@ function stripTrailing(path) {
5998
6364
  // ../daemon/src/store.ts
5999
6365
  import { DatabaseSync } from "node:sqlite";
6000
6366
  import { mkdirSync as mkdirSync3 } from "node:fs";
6001
- import { homedir as homedir5 } from "node:os";
6002
- import { join as join6, basename as basename4 } from "node:path";
6367
+ import { homedir as homedir6 } from "node:os";
6368
+ import { join as join7, basename as basename4 } from "node:path";
6003
6369
  import { randomUUID as randomUUID2 } from "node:crypto";
6004
6370
  var Store = class {
6005
6371
  db;
6006
- constructor(dir = process.env.OFFHAND_HOME ?? join6(homedir5(), ".offhand")) {
6372
+ constructor(dir = process.env.OFFHAND_HOME ?? join7(homedir6(), ".offhand")) {
6007
6373
  mkdirSync3(dir, { recursive: true });
6008
- this.db = new DatabaseSync(join6(dir, "offhand.db"));
6374
+ this.db = new DatabaseSync(join7(dir, "offhand.db"));
6009
6375
  this.db.exec(`
6010
6376
  PRAGMA journal_mode = WAL;
6011
6377
  CREATE TABLE IF NOT EXISTS sessions (
@@ -10247,8 +10613,15 @@ var coerce = {
10247
10613
  var NEVER = INVALID;
10248
10614
 
10249
10615
  // ../shared/src/run-events.ts
10616
+ var ApprovalQuestionSchema = external_exports.object({
10617
+ text: external_exports.string(),
10618
+ options: external_exports.array(external_exports.object({ label: external_exports.string(), description: external_exports.string().optional() })),
10619
+ multiSelect: external_exports.boolean().optional()
10620
+ });
10250
10621
  var RunEventSchema = external_exports.discriminatedUnion("type", [
10251
10622
  external_exports.object({ type: external_exports.literal("text"), chunk: external_exports.string() }),
10623
+ /** Extended-thinking content, streamed the same way as text (medium+ effort tiers). */
10624
+ external_exports.object({ type: external_exports.literal("thinking"), chunk: external_exports.string() }),
10252
10625
  external_exports.object({ type: external_exports.literal("tool"), name: external_exports.string(), summary: external_exports.string() }),
10253
10626
  external_exports.object({
10254
10627
  type: external_exports.literal("approval"),
@@ -10259,11 +10632,7 @@ var RunEventSchema = external_exports.discriminatedUnion("type", [
10259
10632
  /** Optional content preview (edit diff / command) shown in the approval sheet. */
10260
10633
  preview: external_exports.string().optional(),
10261
10634
  /** AskUserQuestion payload: render the actual question with options. */
10262
- question: external_exports.object({
10263
- text: external_exports.string(),
10264
- options: external_exports.array(external_exports.object({ label: external_exports.string(), description: external_exports.string().optional() })),
10265
- multiSelect: external_exports.boolean().optional()
10266
- }).optional()
10635
+ question: ApprovalQuestionSchema.optional()
10267
10636
  }),
10268
10637
  /** Encrypted blob reference (M5): phone fetches + decrypts locally. */
10269
10638
  external_exports.object({
@@ -10290,7 +10659,15 @@ var RunEventSchema = external_exports.discriminatedUnion("type", [
10290
10659
  /** API-equivalent cost of the run (informational for subscription plans). */
10291
10660
  costUsd: external_exports.number().optional()
10292
10661
  }),
10293
- external_exports.object({ type: external_exports.literal("error"), message: external_exports.string() })
10662
+ external_exports.object({ type: external_exports.literal("error"), message: external_exports.string() }),
10663
+ /** Account-level plan usage (from the CLI's own rate_limit_event) — real numbers only. */
10664
+ external_exports.object({
10665
+ type: external_exports.literal("rate-limit"),
10666
+ fiveHourUtilization: external_exports.number(),
10667
+ fiveHourResetsAtMs: external_exports.number(),
10668
+ sevenDayUtilization: external_exports.number(),
10669
+ sevenDayResetsAtMs: external_exports.number()
10670
+ })
10294
10671
  ]);
10295
10672
  var PermissionModeSchema = external_exports.enum(["guarded", "plan", "acceptEdits", "bypass"]);
10296
10673
  var EffortSchema = external_exports.enum(["low", "medium", "high", "max"]);
@@ -10550,10 +10927,19 @@ var RelayFrameSchema = external_exports.discriminatedUnion("kind", [
10550
10927
  }),
10551
10928
  /**
10552
10929
  * daemon → relay: ask the relay to web-push the session's phones. Carries
10553
- * ONLY an opaque approval id — never content (push bodies transit
10554
- * Apple/Google; 02-architecture.md).
10930
+ * ONLY opaque ids — never content (push bodies transit Apple/Google;
10931
+ * 02-architecture.md). `id` is the approval id (approval only); `sessionId`
10932
+ * is the offhand chat session the notification concerns (approval/done/
10933
+ * error) — lets the phone deep-link straight to the right session.
10555
10934
  */
10556
- external_exports.object({ kind: external_exports.literal("notify"), notice: external_exports.enum(["approval", "drop"]), id: external_exports.string().optional() }),
10935
+ external_exports.object({
10936
+ kind: external_exports.literal("notify"),
10937
+ notice: external_exports.enum(["approval", "drop", "done", "error", "progress"]),
10938
+ id: external_exports.string().optional(),
10939
+ sessionId: external_exports.string().optional(),
10940
+ /** 'progress' only — a bare action count, never the action itself (no content in push). */
10941
+ count: external_exports.number().int().nonnegative().optional()
10942
+ }),
10557
10943
  /** relay → daemon: verdict delivered via push-notification action button. */
10558
10944
  external_exports.object({ kind: external_exports.literal("verdict"), approvalId: external_exports.string(), approve: external_exports.boolean() }),
10559
10945
  external_exports.object({ kind: external_exports.literal("ping") }),
@@ -10629,9 +11015,9 @@ if (typeof Module.getRandomValue === "undefined") {
10629
11015
  Module.getRandomValue = randomValuesStandard;
10630
11016
  } catch (e) {
10631
11017
  try {
10632
- crypto = null;
11018
+ crypto2 = null;
10633
11019
  randomValueNodeJS = function() {
10634
- var buf = crypto["randomBytes"](4);
11020
+ var buf = crypto2["randomBytes"](4);
10635
11021
  return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
10636
11022
  };
10637
11023
  randomValueNodeJS();
@@ -10644,10 +11030,10 @@ if (typeof Module.getRandomValue === "undefined") {
10644
11030
  var window_;
10645
11031
  var crypto_;
10646
11032
  var randomValuesStandard;
10647
- var crypto;
11033
+ var crypto2;
10648
11034
  var randomValueNodeJS;
10649
11035
  var _Module = Module;
10650
- Module.ready = new Promise(function(resolve5, reject) {
11036
+ Module.ready = new Promise(function(resolve6, reject) {
10651
11037
  var Module2 = _Module;
10652
11038
  Module2.onAbort = reject;
10653
11039
  Module2.print = function(what) {
@@ -10659,13 +11045,13 @@ Module.ready = new Promise(function(resolve5, reject) {
10659
11045
  Module2.onRuntimeInitialized = function() {
10660
11046
  try {
10661
11047
  Module2._crypto_secretbox_keybytes();
10662
- resolve5();
11048
+ resolve6();
10663
11049
  } catch (err2) {
10664
11050
  reject(err2);
10665
11051
  }
10666
11052
  };
10667
11053
  Module2.useBackupModule = function() {
10668
- return new Promise(function(resolve6, reject2) {
11054
+ return new Promise(function(resolve7, reject2) {
10669
11055
  var Module3 = {};
10670
11056
  Module3.onAbort = reject2;
10671
11057
  Module3.getRandomValue = _Module.getRandomValue;
@@ -10678,7 +11064,7 @@ Module.ready = new Promise(function(resolve5, reject) {
10678
11064
  Object.keys(Module3).forEach(function(k2) {
10679
11065
  _Module[k2] = Module3[k2];
10680
11066
  });
10681
- resolve6();
11067
+ resolve7();
10682
11068
  };
10683
11069
  var Module3 = typeof Module3 != "undefined" ? Module3 : {};
10684
11070
  var ENVIRONMENT_IS_WEB2 = !!globalThis.window;
@@ -10738,13 +11124,13 @@ Module.ready = new Promise(function(resolve5, reject) {
10738
11124
  }
10739
11125
  readAsync2 = async (url) => {
10740
11126
  if (isFileURI2(url)) {
10741
- return new Promise((resolve7, reject3) => {
11127
+ return new Promise((resolve8, reject3) => {
10742
11128
  var xhr = new XMLHttpRequest();
10743
11129
  xhr.open("GET", url, true);
10744
11130
  xhr.responseType = "arraybuffer";
10745
11131
  xhr.onload = () => {
10746
11132
  if (xhr.status == 200 || xhr.status == 0 && xhr.response) {
10747
- resolve7(xhr.response);
11133
+ resolve8(xhr.response);
10748
11134
  return;
10749
11135
  }
10750
11136
  reject3(xhr.status);
@@ -37356,9 +37742,9 @@ Module.ready = new Promise(function(resolve5, reject) {
37356
37742
  }
37357
37743
  var info = getWasmImports2();
37358
37744
  if (Module3["instantiateWasm"]) {
37359
- return new Promise((resolve7, reject3) => {
37745
+ return new Promise((resolve8, reject3) => {
37360
37746
  Module3["instantiateWasm"](info, (inst, mod) => {
37361
- resolve7(receiveInstance(inst, mod));
37747
+ resolve8(receiveInstance(inst, mod));
37362
37748
  });
37363
37749
  });
37364
37750
  }
@@ -37538,7 +37924,7 @@ Module.ready = new Promise(function(resolve5, reject) {
37538
37924
  try {
37539
37925
  var window_ = "object" === typeof window ? window : self;
37540
37926
  var crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto;
37541
- crypto_ = crypto_ === void 0 ? crypto : crypto_;
37927
+ crypto_ = crypto_ === void 0 ? crypto2 : crypto_;
37542
37928
  var randomValuesStandard = function() {
37543
37929
  var buf = new Uint32Array(1);
37544
37930
  crypto_.getRandomValues(buf);
@@ -37548,9 +37934,9 @@ Module.ready = new Promise(function(resolve5, reject) {
37548
37934
  Module3.getRandomValue = randomValuesStandard;
37549
37935
  } catch (e) {
37550
37936
  try {
37551
- var crypto = null;
37937
+ var crypto2 = null;
37552
37938
  var randomValueNodeJS = function() {
37553
- var buf = crypto["randomBytes"](4);
37939
+ var buf = crypto2["randomBytes"](4);
37554
37940
  return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
37555
37941
  };
37556
37942
  randomValueNodeJS();
@@ -37859,13 +38245,13 @@ Module.ready = new Promise(function(resolve5, reject) {
37859
38245
  }
37860
38246
  readAsync = async (url) => {
37861
38247
  if (isFileURI(url)) {
37862
- return new Promise((resolve6, reject2) => {
38248
+ return new Promise((resolve7, reject2) => {
37863
38249
  var xhr = new XMLHttpRequest();
37864
38250
  xhr.open("GET", url, true);
37865
38251
  xhr.responseType = "arraybuffer";
37866
38252
  xhr.onload = () => {
37867
38253
  if (xhr.status == 200 || xhr.status == 0 && xhr.response) {
37868
- resolve6(xhr.response);
38254
+ resolve7(xhr.response);
37869
38255
  return;
37870
38256
  }
37871
38257
  reject2(xhr.status);
@@ -37979,9 +38365,9 @@ Module.ready = new Promise(function(resolve5, reject) {
37979
38365
  }
37980
38366
  var info = getWasmImports();
37981
38367
  if (Module2["instantiateWasm"]) {
37982
- return new Promise((resolve6, reject2) => {
38368
+ return new Promise((resolve7, reject2) => {
37983
38369
  Module2["instantiateWasm"](info, (inst, mod) => {
37984
- resolve6(receiveInstance(inst, mod));
38370
+ resolve7(receiveInstance(inst, mod));
37985
38371
  });
37986
38372
  });
37987
38373
  }
@@ -38196,7 +38582,7 @@ Module.ready = new Promise(function(resolve5, reject) {
38196
38582
  try {
38197
38583
  var window_ = "object" === typeof window ? window : self;
38198
38584
  var crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto;
38199
- crypto_ = crypto_ === void 0 ? crypto : crypto_;
38585
+ crypto_ = crypto_ === void 0 ? crypto2 : crypto_;
38200
38586
  var randomValuesStandard = function() {
38201
38587
  var buf = new Uint32Array(1);
38202
38588
  crypto_.getRandomValues(buf);
@@ -38206,9 +38592,9 @@ Module.ready = new Promise(function(resolve5, reject) {
38206
38592
  Module2.getRandomValue = randomValuesStandard;
38207
38593
  } catch (e) {
38208
38594
  try {
38209
- var crypto = null;
38595
+ var crypto2 = null;
38210
38596
  var randomValueNodeJS = function() {
38211
- var buf = crypto["randomBytes"](4);
38597
+ var buf = crypto2["randomBytes"](4);
38212
38598
  return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
38213
38599
  };
38214
38600
  randomValueNodeJS();
@@ -41133,6 +41519,18 @@ var LocalSessionServer = class {
41133
41519
  const http = createServer((req, res) => void this.onRequest(req, res));
41134
41520
  this.wss = new import_websocket_server.default({ server: http });
41135
41521
  this.wss.on("connection", (ws) => this.onConnection(ws));
41522
+ http.on("error", (err) => {
41523
+ if (err.code === "EADDRINUSE") {
41524
+ console.error(
41525
+ `
41526
+ Another offhands daemon is already running (port ${port2} is taken).
41527
+ Run only one \u2014 or start this one on a different port: offhands --port ${port2 + 1}
41528
+ `
41529
+ );
41530
+ process.exit(1);
41531
+ }
41532
+ throw err;
41533
+ });
41136
41534
  http.listen(port2, "127.0.0.1");
41137
41535
  }
41138
41536
  wss;
@@ -41186,6 +41584,8 @@ var RelayClient = class {
41186
41584
  heartbeat = null;
41187
41585
  detach = null;
41188
41586
  stopped = false;
41587
+ /** sessionId → tool-call count for the in-flight run (opaque count only, never the action). */
41588
+ toolCounts = /* @__PURE__ */ new Map();
41189
41589
  start() {
41190
41590
  this.connect();
41191
41591
  }
@@ -41206,15 +41606,41 @@ var RelayClient = class {
41206
41606
  if (ws.readyState === wrapper_default.OPEN) {
41207
41607
  const envelope = seal(msg, this.keys.tx);
41208
41608
  ws.send(JSON.stringify({ kind: "peer", payload: envelope }));
41609
+ if (msg.type === "run-started") {
41610
+ this.toolCounts.set(msg.sessionId, 0);
41611
+ }
41612
+ if (msg.type === "run-event" && msg.event.type === "tool") {
41613
+ const count = (this.toolCounts.get(msg.sessionId) ?? 0) + 1;
41614
+ this.toolCounts.set(msg.sessionId, count);
41615
+ ws.send(
41616
+ JSON.stringify({
41617
+ kind: "notify",
41618
+ notice: "progress",
41619
+ sessionId: msg.sessionId,
41620
+ count
41621
+ })
41622
+ );
41623
+ }
41209
41624
  if (msg.type === "run-event" && msg.event.type === "approval") {
41210
41625
  ws.send(
41211
41626
  JSON.stringify({
41212
41627
  kind: "notify",
41213
41628
  notice: "approval",
41214
- id: msg.event.id
41629
+ id: msg.event.id,
41630
+ sessionId: msg.sessionId
41215
41631
  })
41216
41632
  );
41217
41633
  }
41634
+ if (msg.type === "run-event" && msg.event.type === "done") {
41635
+ ws.send(
41636
+ JSON.stringify({ kind: "notify", notice: "done", sessionId: msg.sessionId })
41637
+ );
41638
+ }
41639
+ if (msg.type === "run-event" && msg.event.type === "error") {
41640
+ ws.send(
41641
+ JSON.stringify({ kind: "notify", notice: "error", sessionId: msg.sessionId })
41642
+ );
41643
+ }
41218
41644
  if (msg.type === "drop" && msg.direction === "to-phone") {
41219
41645
  ws.send(JSON.stringify({ kind: "notify", notice: "drop" }));
41220
41646
  }
@@ -41274,18 +41700,18 @@ var RelayClient = class {
41274
41700
  };
41275
41701
 
41276
41702
  // ../daemon/src/pairing.ts
41277
- import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
41278
- import { homedir as homedir6 } from "node:os";
41279
- import { join as join7 } from "node:path";
41703
+ import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
41704
+ import { homedir as homedir7 } from "node:os";
41705
+ import { join as join8 } from "node:path";
41280
41706
  import { randomInt } from "node:crypto";
41281
- var PAIRING_DIR = process.env.OFFHAND_HOME ?? join7(homedir6(), ".offhand");
41282
- var PAIRING_PATH = join7(PAIRING_DIR, "pairing.json");
41707
+ var PAIRING_DIR = process.env.OFFHAND_HOME ?? join8(homedir7(), ".offhand");
41708
+ var PAIRING_PATH = join8(PAIRING_DIR, "pairing.json");
41283
41709
  var POLL_MS = 2e3;
41284
41710
  var POLL_TIMEOUT_MS = 10 * 60 * 1e3;
41285
41711
  async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://offhand-web.onrender.com") {
41286
41712
  await ready;
41287
- if (!forceNew && existsSync6(PAIRING_PATH)) {
41288
- const f2 = JSON.parse(readFileSync3(PAIRING_PATH, "utf8"));
41713
+ if (!forceNew && existsSync7(PAIRING_PATH)) {
41714
+ const f2 = JSON.parse(readFileSync4(PAIRING_PATH, "utf8"));
41289
41715
  const kp2 = { publicKey: fromB64u(f2.daemonPublicKey), secretKey: fromB64u(f2.daemonSecretKey) };
41290
41716
  const phonePk = fromB64u(f2.phonePublicKey);
41291
41717
  return {
@@ -41357,7 +41783,7 @@ async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://off
41357
41783
  // ../daemon/src/approvals.ts
41358
41784
  import { randomUUID as randomUUID3 } from "node:crypto";
41359
41785
  import { tmpdir as tmpdir2 } from "node:os";
41360
- import { join as join8 } from "node:path";
41786
+ import { join as join9 } from "node:path";
41361
41787
  var ApprovalBroker = class {
41362
41788
  constructor(timeoutMs) {
41363
41789
  this.timeoutMs = timeoutMs;
@@ -41381,7 +41807,7 @@ var ApprovalBroker = class {
41381
41807
  }
41382
41808
  const risk = classifyRisk(toolName, input);
41383
41809
  const filePath = input?.file_path;
41384
- if (toolName === "Read" && typeof filePath === "string" && filePath.toLowerCase().startsWith(join8(tmpdir2(), "offhand-attachments").toLowerCase())) {
41810
+ if (toolName === "Read" && typeof filePath === "string" && filePath.toLowerCase().startsWith(join9(tmpdir2(), "offhand-attachments").toLowerCase())) {
41385
41811
  return Promise.resolve({ approve: true });
41386
41812
  }
41387
41813
  if (this.policyProvider() === "trusting" && risk === "low" && toolName !== "AskUserQuestion") {
@@ -41448,11 +41874,11 @@ function summariseInput(input) {
41448
41874
  if (typeof input !== "object" || input === null) return String(input ?? "");
41449
41875
  const i2 = input;
41450
41876
  const q2 = firstQuestion(i2);
41451
- if (q2) return truncate4(q2.text, 200);
41877
+ if (q2) return truncate5(q2.text, 200);
41452
41878
  for (const key of ["command", "file_path", "path", "url", "pattern", "description"]) {
41453
- if (typeof i2[key] === "string" && i2[key] !== "") return truncate4(`${key}: ${i2[key]}`, 200);
41879
+ if (typeof i2[key] === "string" && i2[key] !== "") return truncate5(`${key}: ${i2[key]}`, 200);
41454
41880
  }
41455
- return truncate4(JSON.stringify(input), 200);
41881
+ return truncate5(JSON.stringify(input), 200);
41456
41882
  }
41457
41883
  function firstQuestion(i2) {
41458
41884
  if (!Array.isArray(i2.questions) || i2.questions.length === 0) return void 0;
@@ -41470,7 +41896,7 @@ function buildPreview(toolName, input) {
41470
41896
  const q2 = firstQuestion(i2);
41471
41897
  if (q2) {
41472
41898
  const lines = [q2.text, ...q2.options.map((o2, n2) => `${n2 + 1}. ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)];
41473
- return truncate4(lines.join("\n"), 600);
41899
+ return truncate5(lines.join("\n"), 600);
41474
41900
  }
41475
41901
  if (/^(Edit|MultiEdit)/.test(toolName) && typeof i2.new_string === "string") {
41476
41902
  const oldS = typeof i2.old_string === "string" ? i2.old_string : "";
@@ -41478,15 +41904,15 @@ function buildPreview(toolName, input) {
41478
41904
  ...oldS.split("\n").map((l2) => `- ${l2}`),
41479
41905
  ...i2.new_string.split("\n").map((l2) => `+ ${l2}`)
41480
41906
  ];
41481
- return truncate4(lines.join("\n"), 600);
41907
+ return truncate5(lines.join("\n"), 600);
41482
41908
  }
41483
41909
  if (/^Write/.test(toolName) && typeof i2.content === "string") {
41484
- return truncate4(i2.content.split("\n").map((l2) => `+ ${l2}`).join("\n"), 600);
41910
+ return truncate5(i2.content.split("\n").map((l2) => `+ ${l2}`).join("\n"), 600);
41485
41911
  }
41486
- if (typeof i2.command === "string") return truncate4(`$ ${i2.command}`, 600);
41912
+ if (typeof i2.command === "string") return truncate5(`$ ${i2.command}`, 600);
41487
41913
  return void 0;
41488
41914
  }
41489
- function truncate4(s2, max) {
41915
+ function truncate5(s2, max) {
41490
41916
  return s2.length <= max ? s2 : s2.slice(0, max - 1) + "\u2026";
41491
41917
  }
41492
41918
 
@@ -41522,6 +41948,115 @@ async function downloadArtifact(relayUrl2, sessionId, blobId, keys) {
41522
41948
  return openBytes(new Uint8Array(await res.arrayBuffer()), keys.rx);
41523
41949
  }
41524
41950
 
41951
+ // ../daemon/src/autostart.ts
41952
+ import { homedir as homedir8 } from "node:os";
41953
+ import { join as join10, resolve as resolve4, dirname as dirname3 } from "node:path";
41954
+ import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, rmSync } from "node:fs";
41955
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
41956
+ import { spawnSync } from "node:child_process";
41957
+ import { tmpdir as tmpdir3 } from "node:os";
41958
+ var __dirname2 = dirname3(fileURLToPath3(import.meta.url));
41959
+ var SHORTCUT_NAME = "OffhandDaemon.lnk";
41960
+ var OFFHAND_HOME = process.env.OFFHAND_HOME ?? join10(homedir8(), ".offhand");
41961
+ function getStartupDir() {
41962
+ if (process.platform !== "win32") {
41963
+ return join10(homedir8(), ".config", "autostart");
41964
+ }
41965
+ return join10(process.env.APPDATA ?? "", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
41966
+ }
41967
+ function getWrapperPath() {
41968
+ const wrapperDir = join10(OFFHAND_HOME, "autostart");
41969
+ mkdirSync5(wrapperDir, { recursive: true });
41970
+ return join10(wrapperDir, "start-daemon.bat");
41971
+ }
41972
+ function getShortcutPath() {
41973
+ return join10(getStartupDir(), SHORTCUT_NAME);
41974
+ }
41975
+ function buildWrapperScript(config) {
41976
+ const projectRoot = resolve4(__dirname2, "..", "..");
41977
+ const wsArgs2 = config.workspaces.map((w2) => `--workspace "${w2}"`).join(" ");
41978
+ const relayArg = config.relayUrl ? `--relay "${config.relayUrl}"` : "";
41979
+ const devUrlArg = config.devUrl ? `--dev-url "${config.devUrl}"` : "";
41980
+ const portArg = `--port ${config.port}`;
41981
+ const approvalArg = `--approval-timeout ${config.approvalTimeout ?? 300}`;
41982
+ const webUrlArg = `--web-url "${config.webUrl ?? "https://offhand-web.onrender.com"}"`;
41983
+ const parts = [
41984
+ "pnpm --filter @offhand/daemon dev",
41985
+ wsArgs2,
41986
+ relayArg,
41987
+ devUrlArg,
41988
+ portArg,
41989
+ approvalArg,
41990
+ webUrlArg
41991
+ ].filter(Boolean);
41992
+ const cmd = parts.join(" ");
41993
+ return `@echo off
41994
+ cd /d "${projectRoot}"
41995
+ ${cmd}
41996
+ `;
41997
+ }
41998
+ function createShortcut(targetPath, shortcutPath) {
41999
+ const psScript = `$WshShell = New-Object -ComObject WScript.Shell
42000
+ $Shortcut = $WshShell.CreateShortcut("${shortcutPath}")
42001
+ $Shortcut.TargetPath = "cmd.exe"
42002
+ $Shortcut.Arguments = "/c ${targetPath}"
42003
+ $Shortcut.WorkingDirectory = "${resolve4(__dirname2, "..", "..")}"
42004
+ $Shortcut.Description = "Offhand Daemon - Auto-start on login"
42005
+ $Shortcut.Save()`;
42006
+ const scriptPath = join10(tmpdir3(), "offhand-create-shortcut.ps1");
42007
+ writeFileSync4(scriptPath, psScript);
42008
+ const result = spawnSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath], { encoding: "utf8", shell: true });
42009
+ try {
42010
+ rmSync(scriptPath);
42011
+ } catch {
42012
+ }
42013
+ return { success: result.status === 0, output: result.stdout + result.stderr };
42014
+ }
42015
+ function checkShortcutExists(shortcutPath) {
42016
+ return existsSync8(shortcutPath);
42017
+ }
42018
+ function installAutostart(config) {
42019
+ const wrapperPath = getWrapperPath();
42020
+ const script = buildWrapperScript(config);
42021
+ writeFileSync4(wrapperPath, script);
42022
+ const shortcutPath = getShortcutPath();
42023
+ const result = createShortcut(wrapperPath, shortcutPath);
42024
+ if (result.success) {
42025
+ console.log(`[autostart] Created shortcut at: ${shortcutPath}`);
42026
+ console.log(`[autostart] Wrapper script: ${wrapperPath}`);
42027
+ } else {
42028
+ console.error(`[autostart] Failed to create shortcut: ${result.output}`);
42029
+ }
42030
+ return result;
42031
+ }
42032
+ function getAutostartStatus() {
42033
+ const shortcutPath = getShortcutPath();
42034
+ const installed = checkShortcutExists(shortcutPath);
42035
+ const wrapperPath = getWrapperPath();
42036
+ const wrapperExists = existsSync8(wrapperPath);
42037
+ let details = `Shortcut: ${shortcutPath} (${installed ? "EXISTS" : "MISSING"})`;
42038
+ if (wrapperExists) {
42039
+ details += `
42040
+ Wrapper: ${wrapperPath} (EXISTS)`;
42041
+ }
42042
+ return { installed, details };
42043
+ }
42044
+ function saveConfig(config) {
42045
+ const configPath = join10(OFFHAND_HOME, "autostart.json");
42046
+ writeFileSync4(configPath, JSON.stringify(config, null, 2));
42047
+ }
42048
+ function getConfigFromStore() {
42049
+ const workspaces = ["C:\\Users\\udbha\\dev\\offhand"];
42050
+ return {
42051
+ workspaces,
42052
+ port: 4317,
42053
+ relayUrl: void 0,
42054
+ devUrl: void 0,
42055
+ approvalTimeout: 300,
42056
+ webUrl: "https://offhand-web.onrender.com"
42057
+ };
42058
+ }
42059
+
41525
42060
  // ../daemon/src/index.ts
41526
42061
  var args = process.argv.slice(2);
41527
42062
  if (args[0] === "drop") {
@@ -41554,9 +42089,9 @@ var approvalTimeoutMs = Number(argValue("--approval-timeout") ?? 300) * 1e3;
41554
42089
  var webUrl = argValue("--web-url") ?? "https://offhand-web.onrender.com";
41555
42090
  var devUrl = argValue("--dev-url");
41556
42091
  var store = new Store();
41557
- var wsArgs = argValues("--workspace").map((w2) => resolve4(w2));
42092
+ var wsArgs = argValues("--workspace").map((w2) => resolve5(w2));
41558
42093
  for (const w2 of wsArgs) {
41559
- if (!existsSync7(w2)) {
42094
+ if (!existsSync9(w2)) {
41560
42095
  console.error(`workspace does not exist: ${w2}`);
41561
42096
  process.exit(1);
41562
42097
  }
@@ -41568,6 +42103,7 @@ var approvalUrl = `http://127.0.0.1:${port}/approval`;
41568
42103
  var runners = [
41569
42104
  new ClaudeCodeRunner(broker, approvalUrl),
41570
42105
  new CopilotCliRunner(),
42106
+ new OpenCodeRunner(),
41571
42107
  new CodexCliRunner(),
41572
42108
  new CursorAgentRunner(),
41573
42109
  new GeminiCliRunner()
@@ -41588,6 +42124,26 @@ if (m2.type === "manifest") {
41588
42124
  new LocalSessionServer(manager, port, broker);
41589
42125
  console.log(` local : ws://127.0.0.1:${port}`);
41590
42126
  console.log(` approvals : timeout ${approvalTimeoutMs / 1e3}s then auto-deny`);
42127
+ var autostartStatus = getAutostartStatus();
42128
+ if (!autostartStatus.installed) {
42129
+ console.log(` autostart : installing...`);
42130
+ const config = getConfigFromStore();
42131
+ config.workspaces = store.listWorkspaces().map((w2) => w2.path);
42132
+ config.port = port;
42133
+ config.relayUrl = relayUrl;
42134
+ config.devUrl = devUrl;
42135
+ config.approvalTimeout = approvalTimeoutMs / 1e3;
42136
+ config.webUrl = webUrl;
42137
+ saveConfig(config);
42138
+ const { success, output } = installAutostart(config);
42139
+ if (success) {
42140
+ console.log(` autostart : installed (runs on login)`);
42141
+ } else {
42142
+ console.warn(` autostart : failed to install: ${output}`);
42143
+ }
42144
+ } else {
42145
+ console.log(` autostart : already installed`);
42146
+ }
41591
42147
  if (relayUrl) {
41592
42148
  const pairing = await ensurePairing(relayUrl, forceRepair, webUrl);
41593
42149
  manager.capture = captureScreenshot;
package/package.json CHANGED
@@ -1,28 +1,26 @@
1
- {
2
- "name": "offhands",
3
- "version": "0.1.1",
4
- "description": "Reserved.",
5
- "license": "UNLICENSED",
6
- "type": "module",
7
- "bin": {
8
- "offhands": "bin/offhands.mjs"
9
- },
10
- "files": [
11
- "bin",
12
- "dist",
13
- "approval-mcp.mjs",
14
- "README.md"
15
- ],
16
- "engines": {
17
- "node": ">=22.5.0"
18
- },
19
- "scripts": {
20
- "build": "node build.mjs",
21
- "typecheck": "node --version",
22
- "test": "node --version",
23
- "prepublishOnly": "node build.mjs"
24
- },
25
- "devDependencies": {
26
- "esbuild": "^0.24.0"
27
- }
28
- }
1
+ {
2
+ "name": "offhands",
3
+ "version": "0.1.3",
4
+ "description": "Phone → coding-agent relay. Daemon runs on your laptop, PWA on your phone. E2E encrypted.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "bin": {
8
+ "offhands": "bin/offhands.mjs"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "dist",
13
+ "approval-mcp.mjs",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=22.5.0"
18
+ },
19
+ "scripts": {
20
+ "typecheck": "node --version",
21
+ "test": "node --version"
22
+ },
23
+ "devDependencies": {
24
+ "esbuild": "^0.24.0"
25
+ }
26
+ }