offhands 0.1.2 → 0.1.4

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 +420 -86
  2. package/package.json +1 -1
package/dist/daemon.mjs CHANGED
@@ -4777,7 +4777,7 @@ var require_main = __commonJS({
4777
4777
 
4778
4778
  // ../daemon/src/index.ts
4779
4779
  import { resolve as resolve5 } from "node:path";
4780
- import { existsSync as existsSync8 } from "node:fs";
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";
@@ -4897,11 +4897,11 @@ function contextUsage(usage, costUsd) {
4897
4897
  function summariseToolInput(name, input) {
4898
4898
  if (typeof input !== "object" || input === null) return name;
4899
4899
  const i2 = input;
4900
- const firstString2 = (...keys) => {
4900
+ const firstString3 = (...keys) => {
4901
4901
  for (const k2 of keys) if (typeof i2[k2] === "string" && i2[k2] !== "") return i2[k2];
4902
4902
  return void 0;
4903
4903
  };
4904
- const hint = firstString2("file_path", "path", "command", "pattern", "query", "url", "description") ?? "";
4904
+ const hint = firstString3("file_path", "path", "command", "pattern", "query", "url", "description") ?? "";
4905
4905
  return hint ? `${name}: ${truncate(hint, 120)}` : name;
4906
4906
  }
4907
4907
  function truncate(s2, max) {
@@ -5297,11 +5297,343 @@ stderr: ${stderrTail}` : ""}`
5297
5297
  }
5298
5298
  };
5299
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 part = v2.part ?? {};
5312
+ const partType = typeof part.type === "string" ? part.type : "";
5313
+ if (type === "text") {
5314
+ const text = typeof part.text === "string" ? part.text : "";
5315
+ return text ? [{ type: "text", chunk: text }] : [];
5316
+ }
5317
+ if (type === "thinking" || partType === "thinking") {
5318
+ const thinking = typeof part.thinking === "string" ? part.thinking : typeof part.text === "string" ? part.text : "";
5319
+ return thinking ? [{ type: "thinking", chunk: thinking }] : [];
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") ?? "" : "";
5325
+ return [{ type: "tool", name, summary: summary ? `${name}: ${truncate3(summary, 120)}` : name }];
5326
+ }
5327
+ if (type === "step_start" || partType === "step-start") {
5328
+ return [];
5329
+ }
5330
+ if (type === "step_finish" || partType === "step-finish") {
5331
+ const reason = typeof part.reason === "string" ? part.reason : "stop";
5332
+ const isError = reason === "error" || reason === "failed";
5333
+ const usage = extractUsageFromPart(part);
5334
+ if (isError) {
5335
+ const message = typeof part.error === "string" ? part.error : "run failed";
5336
+ const events2 = [{ type: "error", message }];
5337
+ if (usage) events2.push(usage);
5338
+ return events2;
5339
+ }
5340
+ const events = [{ type: "done", summary: "" }];
5341
+ if (usage) events.push(usage);
5342
+ return events;
5343
+ }
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";
5364
+ return [{ type: "error", message }];
5365
+ }
5366
+ if (type === "session_created" || type === "session.updated" || type === "session.created") {
5367
+ return [];
5368
+ }
5369
+ return [];
5370
+ }
5371
+ function extractOpenCodeSessionId(value) {
5372
+ if (typeof value !== "object" || value === null) return null;
5373
+ const v2 = value;
5374
+ const part = v2.part ?? {};
5375
+ return firstString2(v2, "sessionId", "session_id", "sessionID", "id") ?? firstString2(part, "sessionId", "session_id", "sessionID", "id") ?? null;
5376
+ }
5377
+ function extractUsageFromPart(part) {
5378
+ const tokens = part.tokens;
5379
+ const costUsd = typeof part.cost === "number" ? part.cost : typeof part.costUsd === "number" ? part.costUsd : void 0;
5380
+ if (!tokens && costUsd === void 0) return null;
5381
+ 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");
5383
+ if (contextTokens <= 0 && costUsd === void 0) return null;
5384
+ return {
5385
+ type: "usage",
5386
+ contextTokens,
5387
+ contextWindow: 2e5,
5388
+ ...costUsd !== void 0 ? { costUsd } : {}
5389
+ };
5390
+ }
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
+ function firstString2(obj, ...keys) {
5404
+ for (const k2 of keys) {
5405
+ const val = obj[k2];
5406
+ if (typeof val === "string" && val !== "") return val;
5407
+ }
5408
+ return void 0;
5409
+ }
5410
+ function truncate3(s2, max) {
5411
+ return s2.length <= max ? s2 : s2.slice(0, max - 1) + "\u2026";
5412
+ }
5413
+
5414
+ // ../daemon/src/runners/opencode-cli.ts
5415
+ var OpenCodeRunner = class {
5416
+ id = "opencode";
5417
+ name = "OpenCode";
5418
+ supportsApprovals = true;
5419
+ models = [];
5420
+ // populated dynamically via `opencode models`
5421
+ /** [command, ...prefixArgs] resolved once. */
5422
+ resolved = null;
5423
+ modelsCache = [];
5424
+ resolveCommand() {
5425
+ if (this.resolved) return this.resolved;
5426
+ if (process.platform === "win32") {
5427
+ const pathDirs = (process.env.PATH ?? "").split(delimiter2);
5428
+ const npmGlobalPaths = [
5429
+ process.env.APPDATA ? join3(process.env.APPDATA, "npm") : null,
5430
+ process.env.LOCALAPPDATA ? join3(process.env.LOCALAPPDATA, "npm") : null,
5431
+ "C:\\nvm4w\\nodejs",
5432
+ "C:\\Program Files\\nodejs",
5433
+ "C:\\Program Files (x86)\\nodejs"
5434
+ ].filter((p2) => p2 !== null && existsSync3(p2));
5435
+ const allDirs = [...pathDirs, ...npmGlobalPaths];
5436
+ for (const pathDir of allDirs) {
5437
+ if (!pathDir) continue;
5438
+ const cmdPath = join3(pathDir, "opencode.cmd");
5439
+ if (existsSync3(cmdPath)) {
5440
+ this.resolved = [cmdPath];
5441
+ return this.resolved;
5442
+ }
5443
+ const nodeModulesBin = join3(pathDir, "node_modules", ".bin", "opencode.cmd");
5444
+ if (existsSync3(nodeModulesBin)) {
5445
+ this.resolved = [nodeModulesBin];
5446
+ return this.resolved;
5447
+ }
5448
+ for (const bin of ["bun", "pnpm", "yarn"]) {
5449
+ const binPath = join3(pathDir, bin + ".cmd");
5450
+ if (existsSync3(binPath)) {
5451
+ this.resolved = [binPath, "exec", "opencode"];
5452
+ return this.resolved;
5453
+ }
5454
+ }
5455
+ }
5456
+ const native = join3(homedir3(), "AppData", "Local", "opencode", "opencode.exe");
5457
+ if (existsSync3(native)) {
5458
+ this.resolved = [native];
5459
+ return this.resolved;
5460
+ }
5461
+ return null;
5462
+ }
5463
+ this.resolved = ["opencode"];
5464
+ return this.resolved;
5465
+ }
5466
+ async detect() {
5467
+ const cmd = this.resolveCommand();
5468
+ if (!cmd) return false;
5469
+ await this.fetchModels();
5470
+ return new Promise((resolve6) => {
5471
+ const p2 = spawn4(cmd[0], [...cmd.slice(1), "--version"], { stdio: "ignore", shell: process.platform === "win32" });
5472
+ p2.on("error", () => resolve6(false));
5473
+ p2.on("exit", (code) => resolve6(code === 0));
5474
+ });
5475
+ }
5476
+ async fetchModels() {
5477
+ if (this.modelsCache.length > 0) return this.modelsCache;
5478
+ const cmd = this.resolveCommand();
5479
+ if (!cmd) return [];
5480
+ return new Promise((resolve6) => {
5481
+ const p2 = spawn4(cmd[0], [...cmd.slice(1), "models"], {
5482
+ stdio: ["ignore", "pipe", "ignore"],
5483
+ shell: process.platform === "win32"
5484
+ });
5485
+ let output = "";
5486
+ p2.stdout.setEncoding("utf8");
5487
+ p2.stdout.on("data", (chunk) => {
5488
+ output += chunk;
5489
+ });
5490
+ p2.on("close", (code) => {
5491
+ if (code === 0) {
5492
+ try {
5493
+ const lines = output.trim().split("\n");
5494
+ const models = lines.map((l2) => l2.trim()).filter((l2) => l2 && !l2.startsWith("opencode/"));
5495
+ this.modelsCache = models;
5496
+ Object.defineProperty(this, "models", {
5497
+ value: models,
5498
+ writable: false,
5499
+ configurable: true
5500
+ });
5501
+ resolve6(models);
5502
+ } catch {
5503
+ resolve6([]);
5504
+ }
5505
+ } else {
5506
+ resolve6([]);
5507
+ }
5508
+ });
5509
+ p2.on("error", () => resolve6([]));
5510
+ });
5511
+ }
5512
+ loggedIn() {
5513
+ return existsSync3(join3(homedir3(), ".local", "share", "opencode", "auth.json"));
5514
+ }
5515
+ start(run, callbacks) {
5516
+ const queue = new AsyncEventQueue();
5517
+ const parser = new NdjsonParser();
5518
+ let child;
5519
+ let stderrTail = "";
5520
+ let sawTerminal = false;
5521
+ let reportedConversation = false;
5522
+ const emit = (events) => {
5523
+ for (const e of events) {
5524
+ if (e.type === "done" || e.type === "error") sawTerminal = true;
5525
+ queue.push(e);
5526
+ }
5527
+ };
5528
+ const cmd = this.resolveCommand();
5529
+ if (!cmd) {
5530
+ queue.push({ type: "error", message: "opencode CLI not found" });
5531
+ queue.close();
5532
+ return { events: queue, respond: () => {
5533
+ }, cancel: () => {
5534
+ } };
5535
+ }
5536
+ const args2 = [
5537
+ ...cmd.slice(1),
5538
+ "run",
5539
+ "--format",
5540
+ "json",
5541
+ "--no-color"
5542
+ ];
5543
+ if (run.model) {
5544
+ args2.push("--model", run.model);
5545
+ }
5546
+ if (run.resumeConversationId) {
5547
+ args2.push("--session", run.resumeConversationId);
5548
+ }
5549
+ const mode = run.permissionMode ?? "guarded";
5550
+ if (mode === "bypass") {
5551
+ args2.push("--auto");
5552
+ } else if (mode === "plan") {
5553
+ args2.push("--agent", "plan");
5554
+ } else {
5555
+ }
5556
+ if (run.effort) {
5557
+ const variantMap = { low: "minimal", medium: "high", high: "max", max: "max" };
5558
+ const variant = variantMap[run.effort];
5559
+ if (variant) {
5560
+ args2.push("--variant", variant);
5561
+ args2.push("--thinking");
5562
+ }
5563
+ }
5564
+ if (run.attachments?.length) {
5565
+ for (const file of run.attachments) {
5566
+ args2.push("--file", file);
5567
+ }
5568
+ }
5569
+ args2.push("--dir", run.workspace);
5570
+ const promptArg = process.platform === "win32" ? `"${run.prompt.replace(/"/g, '\\"')}"` : run.prompt;
5571
+ args2.push(promptArg);
5572
+ try {
5573
+ child = spawn4(cmd[0], args2, {
5574
+ cwd: run.workspace,
5575
+ stdio: ["ignore", "pipe", "pipe"],
5576
+ shell: process.platform === "win32"
5577
+ });
5578
+ } catch (e) {
5579
+ queue.push({ type: "error", message: `failed to spawn opencode: ${String(e)}` });
5580
+ queue.close();
5581
+ return { events: queue, respond: () => {
5582
+ }, cancel: () => {
5583
+ } };
5584
+ }
5585
+ child.stdout.setEncoding("utf8");
5586
+ child.stdout.on("data", (chunk) => {
5587
+ for (const result of parser.push(chunk)) {
5588
+ if (result.ok) {
5589
+ if (!reportedConversation) {
5590
+ const id = extractOpenCodeSessionId(result.value);
5591
+ if (id) {
5592
+ reportedConversation = true;
5593
+ callbacks?.onConversationId?.(id);
5594
+ }
5595
+ }
5596
+ emit(mapOpenCodeEvent(result.value));
5597
+ }
5598
+ }
5599
+ });
5600
+ child.stderr.setEncoding("utf8");
5601
+ child.stderr.on("data", (chunk) => {
5602
+ stderrTail = (stderrTail + chunk).slice(-2e3);
5603
+ });
5604
+ child.on("error", (err) => {
5605
+ emit([{ type: "error", message: `opencode process error: ${err.message}` }]);
5606
+ queue.close();
5607
+ });
5608
+ child.on("close", (code) => {
5609
+ for (const result of parser.flush()) {
5610
+ if (result.ok) emit(mapOpenCodeEvent(result.value));
5611
+ }
5612
+ if (!sawTerminal) {
5613
+ emit([
5614
+ {
5615
+ type: "error",
5616
+ message: `opencode exited with code ${code ?? "unknown"} before a result${stderrTail ? `
5617
+ stderr: ${stderrTail}` : ""}`
5618
+ }
5619
+ ]);
5620
+ }
5621
+ queue.close();
5622
+ });
5623
+ return {
5624
+ events: queue,
5625
+ respond: (approvalId, ok, answer) => {
5626
+ },
5627
+ cancel: () => child.kill()
5628
+ };
5629
+ }
5630
+ };
5631
+
5300
5632
  // ../daemon/src/session-manager.ts
5301
5633
  import { randomUUID } from "node:crypto";
5302
5634
  import { hostname, platform, tmpdir } from "node:os";
5303
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
5304
- import { dirname as dirname2, join as join5, basename as basename3, parse as parse2, resolve as resolve3 } from "node:path";
5635
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
5636
+ import { dirname as dirname2, join as join6, basename as basename3, parse as parse2, resolve as resolve3 } from "node:path";
5305
5637
  import { fileURLToPath as fileURLToPath2 } from "node:url";
5306
5638
 
5307
5639
  // ../daemon/src/receipts.ts
@@ -5366,25 +5698,25 @@ function shouldScreenshot(touchedFiles, devUrl2) {
5366
5698
  }
5367
5699
 
5368
5700
  // ../daemon/src/claude-history.ts
5369
- import { existsSync as existsSync3, readdirSync, readFileSync, statSync } from "node:fs";
5370
- import { homedir as homedir3 } from "node:os";
5371
- import { basename, join as join3, resolve } from "node:path";
5372
- var DEFAULT_PROJECTS_DIR = join3(homedir3(), ".claude", "projects");
5701
+ import { existsSync as existsSync4, readdirSync, readFileSync, statSync } from "node:fs";
5702
+ import { homedir as homedir4 } from "node:os";
5703
+ import { basename, join as join4, resolve } from "node:path";
5704
+ var DEFAULT_PROJECTS_DIR = join4(homedir4(), ".claude", "projects");
5373
5705
  function listClaudeConversations(workspace, projectsDir = process.env.OFFHAND_CLAUDE_PROJECTS_DIR ?? DEFAULT_PROJECTS_DIR) {
5374
5706
  const projectDir = findProjectDir(workspace, projectsDir);
5375
5707
  if (!projectDir) return [];
5376
5708
  return listJsonlFiles(projectDir).map((path) => summarizeConversation(path, workspace)).filter((c2) => c2 !== null).sort((a2, b2) => b2.lastActiveMs - a2.lastActiveMs).slice(0, 50);
5377
5709
  }
5378
5710
  function findProjectDir(workspace, projectsDir) {
5379
- if (!existsSync3(projectsDir)) return null;
5711
+ if (!existsSync4(projectsDir)) return null;
5380
5712
  const candidates = /* @__PURE__ */ new Set([encodeWorkspace(workspace), encodeWorkspace(resolve(workspace))]);
5381
5713
  for (const candidate of candidates) {
5382
- const exact = join3(projectsDir, candidate);
5383
- if (existsSync3(exact)) return exact;
5714
+ const exact = join4(projectsDir, candidate);
5715
+ if (existsSync4(exact)) return exact;
5384
5716
  }
5385
5717
  const lower = new Set([...candidates].map((c2) => c2.toLowerCase()));
5386
5718
  for (const entry of safeReadDir(projectsDir)) {
5387
- if (entry.isDirectory() && lower.has(entry.name.toLowerCase())) return join3(projectsDir, entry.name);
5719
+ if (entry.isDirectory() && lower.has(entry.name.toLowerCase())) return join4(projectsDir, entry.name);
5388
5720
  }
5389
5721
  return null;
5390
5722
  }
@@ -5392,12 +5724,12 @@ function encodeWorkspace(workspace) {
5392
5724
  return workspace.replace(/[^A-Za-z0-9]/g, "-");
5393
5725
  }
5394
5726
  function listJsonlFiles(projectDir) {
5395
- const files = safeReadDir(projectDir).filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => join3(projectDir, entry.name));
5396
- const sessionsDir = join3(projectDir, "sessions");
5397
- if (!existsSync3(sessionsDir)) return files;
5727
+ const files = safeReadDir(projectDir).filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => join4(projectDir, entry.name));
5728
+ const sessionsDir = join4(projectDir, "sessions");
5729
+ if (!existsSync4(sessionsDir)) return files;
5398
5730
  return [
5399
5731
  ...files,
5400
- ...safeReadDir(sessionsDir).filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => join3(sessionsDir, entry.name))
5732
+ ...safeReadDir(sessionsDir).filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => join4(sessionsDir, entry.name))
5401
5733
  ];
5402
5734
  }
5403
5735
  function summarizeConversation(path, workspace) {
@@ -5416,7 +5748,7 @@ function summarizeConversation(path, workspace) {
5416
5748
  return {
5417
5749
  conversationId: basename(path, ".jsonl"),
5418
5750
  workspace,
5419
- firstPrompt: truncate3(firstPrompt || "(empty prompt)", 120),
5751
+ firstPrompt: truncate4(firstPrompt || "(empty prompt)", 120),
5420
5752
  lastActiveMs: Math.floor(statSync(path).mtimeMs),
5421
5753
  messageCount
5422
5754
  };
@@ -5442,7 +5774,7 @@ function userMessageText(value) {
5442
5774
  function collapseText(text) {
5443
5775
  return text.replace(/\s+/g, " ").trim();
5444
5776
  }
5445
- function truncate3(text, max) {
5777
+ function truncate4(text, max) {
5446
5778
  return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
5447
5779
  }
5448
5780
  function safeReadDir(path) {
@@ -5459,7 +5791,7 @@ function isRecord(value) {
5459
5791
  // ../daemon/src/drop.ts
5460
5792
  import {
5461
5793
  copyFileSync,
5462
- existsSync as existsSync4,
5794
+ existsSync as existsSync5,
5463
5795
  mkdirSync,
5464
5796
  readdirSync as readdirSync2,
5465
5797
  readFileSync as readFileSync2,
@@ -5468,18 +5800,18 @@ import {
5468
5800
  watch,
5469
5801
  writeFileSync
5470
5802
  } from "node:fs";
5471
- import { homedir as homedir4 } from "node:os";
5472
- import { basename as basename2, join as join4, parse, resolve as resolve2 } from "node:path";
5473
- import { spawn as spawn4 } from "node:child_process";
5803
+ import { homedir as homedir5 } from "node:os";
5804
+ import { basename as basename2, join as join5, parse, resolve as resolve2 } from "node:path";
5805
+ import { spawn as spawn5 } from "node:child_process";
5474
5806
  var DROP_LOG_SESSION_ID = "drop";
5475
5807
  function offhandHome() {
5476
- return process.env.OFFHAND_HOME ?? join4(homedir4(), ".offhand");
5808
+ return process.env.OFFHAND_HOME ?? join5(homedir5(), ".offhand");
5477
5809
  }
5478
5810
  function dropOutboxDir(home = offhandHome()) {
5479
- return join4(home, "drop-outbox");
5811
+ return join5(home, "drop-outbox");
5480
5812
  }
5481
5813
  function incomingDropDir() {
5482
- return join4(homedir4(), "Downloads", "offhand");
5814
+ return join5(homedir5(), "Downloads", "offhand");
5483
5815
  }
5484
5816
  function safeDropName(input) {
5485
5817
  const base = basename2(input).replace(/[<>:"/\\|?*\x00-\x1F]/g, "_").replace(/[. ]+$/g, "").trim();
@@ -5503,13 +5835,13 @@ function queueDropFileForPhone(sourcePath, outbox = dropOutboxDir()) {
5503
5835
  const st = statSync2(source);
5504
5836
  if (!st.isFile()) throw new Error(`not a file: ${sourcePath}`);
5505
5837
  mkdirSync(outbox, { recursive: true });
5506
- const dest = join4(outbox, dedupeDropName(basename2(source), safeNames(outbox)));
5838
+ const dest = join5(outbox, dedupeDropName(basename2(source), safeNames(outbox)));
5507
5839
  copyFileSync(source, dest);
5508
5840
  return dest;
5509
5841
  }
5510
5842
  function saveIncomingDrop(name, bytes, dir = incomingDropDir()) {
5511
5843
  mkdirSync(dir, { recursive: true });
5512
- const path = join4(dir, dedupeDropName(name, safeNames(dir)));
5844
+ const path = join5(dir, dedupeDropName(name, safeNames(dir)));
5513
5845
  writeFileSync(path, bytes);
5514
5846
  return path;
5515
5847
  }
@@ -5552,7 +5884,7 @@ function startDropOutboxWatcher(outbox, sendFile) {
5552
5884
  console.error(`drop: sent but could not delete ${path}: ${e instanceof Error ? e.message : String(e)}`);
5553
5885
  }
5554
5886
  } catch (e) {
5555
- if (existsSync4(path)) {
5887
+ if (existsSync5(path)) {
5556
5888
  console.error(`drop: failed to send ${path}: ${e instanceof Error ? e.message : String(e)}`);
5557
5889
  schedule(15e3);
5558
5890
  }
@@ -5564,7 +5896,7 @@ function startDropOutboxWatcher(outbox, sendFile) {
5564
5896
  timer = null;
5565
5897
  if (closed) return;
5566
5898
  for (const entry of safeEntries(outbox)) {
5567
- if (entry.isFile()) void processFile(join4(outbox, entry.name));
5899
+ if (entry.isFile()) void processFile(join5(outbox, entry.name));
5568
5900
  }
5569
5901
  };
5570
5902
  watcher = watch(outbox, () => schedule());
@@ -5592,7 +5924,7 @@ function showDropToast(savedPath, textToClipboard) {
5592
5924
  `$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)`,
5593
5925
  `[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier(${psQuote(appId)}).Show($toast)`
5594
5926
  ].filter(Boolean);
5595
- const child = spawn4("powershell", ["-NoProfile", "-Command", lines.join("; ")], {
5927
+ const child = spawn5("powershell", ["-NoProfile", "-Command", lines.join("; ")], {
5596
5928
  windowsHide: true,
5597
5929
  stdio: ["ignore", "ignore", "pipe"]
5598
5930
  });
@@ -5650,7 +5982,7 @@ function mimeFromName(path) {
5650
5982
  // ../daemon/src/session-manager.ts
5651
5983
  function resolveDaemonVersion() {
5652
5984
  try {
5653
- const pkgPath = join5(dirname2(fileURLToPath2(import.meta.url)), "..", "package.json");
5985
+ const pkgPath = join6(dirname2(fileURLToPath2(import.meta.url)), "..", "package.json");
5654
5986
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
5655
5987
  return pkg.version ?? "0.0.0";
5656
5988
  } catch {
@@ -5748,9 +6080,9 @@ var SessionManager = class {
5748
6080
  prompt += "\n\nAttached files (read them from disk):";
5749
6081
  for (const a2 of msg.attachments) {
5750
6082
  const bytes = await this.attachmentFetcher(a2.blobId);
5751
- const dir = join5(tmpdir(), "offhand-attachments");
6083
+ const dir = join6(tmpdir(), "offhand-attachments");
5752
6084
  mkdirSync2(dir, { recursive: true });
5753
- const path = join5(dir, `${a2.blobId.slice(0, 8)}-${basename3(a2.name)}`);
6085
+ const path = join6(dir, `${a2.blobId.slice(0, 8)}-${basename3(a2.name)}`);
5754
6086
  writeFileSync2(path, bytes);
5755
6087
  prompt += `
5756
6088
  - ${path} (${a2.mime})`;
@@ -5988,17 +6320,17 @@ function listFolders(path) {
5988
6320
  if (!path) return { path: "", parent: null, dirs: driveRoots() };
5989
6321
  const current = resolve3(path);
5990
6322
  const dirs = safeReadDir2(current).filter((entry) => entry.isDirectory() && !skipDir(entry.name)).map((entry) => {
5991
- const full = join5(current, entry.name);
5992
- return { name: entry.name, path: full, isGit: existsSync5(join5(full, ".git")) };
6323
+ const full = join6(current, entry.name);
6324
+ return { name: entry.name, path: full, isGit: existsSync6(join6(full, ".git")) };
5993
6325
  }).sort((a2, b2) => Number(b2.isGit) - Number(a2.isGit) || a2.name.localeCompare(b2.name)).slice(0, 200);
5994
6326
  return { path: current, parent: isRoot(current) ? null : dirname2(current), dirs };
5995
6327
  }
5996
6328
  function driveRoots() {
5997
- if (process.platform !== "win32") return [{ name: "/", path: "/", isGit: existsSync5("/.git") }];
6329
+ if (process.platform !== "win32") return [{ name: "/", path: "/", isGit: existsSync6("/.git") }];
5998
6330
  const roots = [];
5999
6331
  for (let code = 67; code <= 90; code++) {
6000
6332
  const name = `${String.fromCharCode(code)}:\\`;
6001
- if (existsSync5(name)) roots.push({ name, path: name, isGit: existsSync5(join5(name, ".git")) });
6333
+ if (existsSync6(name)) roots.push({ name, path: name, isGit: existsSync6(join6(name, ".git")) });
6002
6334
  }
6003
6335
  return roots.sort((a2, b2) => a2.name.localeCompare(b2.name)).slice(0, 200);
6004
6336
  }
@@ -6031,14 +6363,14 @@ function stripTrailing(path) {
6031
6363
  // ../daemon/src/store.ts
6032
6364
  import { DatabaseSync } from "node:sqlite";
6033
6365
  import { mkdirSync as mkdirSync3 } from "node:fs";
6034
- import { homedir as homedir5 } from "node:os";
6035
- import { join as join6, basename as basename4 } from "node:path";
6366
+ import { homedir as homedir6 } from "node:os";
6367
+ import { join as join7, basename as basename4 } from "node:path";
6036
6368
  import { randomUUID as randomUUID2 } from "node:crypto";
6037
6369
  var Store = class {
6038
6370
  db;
6039
- constructor(dir = process.env.OFFHAND_HOME ?? join6(homedir5(), ".offhand")) {
6371
+ constructor(dir = process.env.OFFHAND_HOME ?? join7(homedir6(), ".offhand")) {
6040
6372
  mkdirSync3(dir, { recursive: true });
6041
- this.db = new DatabaseSync(join6(dir, "offhand.db"));
6373
+ this.db = new DatabaseSync(join7(dir, "offhand.db"));
6042
6374
  this.db.exec(`
6043
6375
  PRAGMA journal_mode = WAL;
6044
6376
  CREATE TABLE IF NOT EXISTS sessions (
@@ -10280,6 +10612,11 @@ var coerce = {
10280
10612
  var NEVER = INVALID;
10281
10613
 
10282
10614
  // ../shared/src/run-events.ts
10615
+ var ApprovalQuestionSchema = external_exports.object({
10616
+ text: external_exports.string(),
10617
+ options: external_exports.array(external_exports.object({ label: external_exports.string(), description: external_exports.string().optional() })),
10618
+ multiSelect: external_exports.boolean().optional()
10619
+ });
10283
10620
  var RunEventSchema = external_exports.discriminatedUnion("type", [
10284
10621
  external_exports.object({ type: external_exports.literal("text"), chunk: external_exports.string() }),
10285
10622
  /** Extended-thinking content, streamed the same way as text (medium+ effort tiers). */
@@ -10294,11 +10631,7 @@ var RunEventSchema = external_exports.discriminatedUnion("type", [
10294
10631
  /** Optional content preview (edit diff / command) shown in the approval sheet. */
10295
10632
  preview: external_exports.string().optional(),
10296
10633
  /** AskUserQuestion payload: render the actual question with options. */
10297
- question: external_exports.object({
10298
- text: external_exports.string(),
10299
- options: external_exports.array(external_exports.object({ label: external_exports.string(), description: external_exports.string().optional() })),
10300
- multiSelect: external_exports.boolean().optional()
10301
- }).optional()
10634
+ question: ApprovalQuestionSchema.optional()
10302
10635
  }),
10303
10636
  /** Encrypted blob reference (M5): phone fetches + decrypts locally. */
10304
10637
  external_exports.object({
@@ -10681,9 +11014,9 @@ if (typeof Module.getRandomValue === "undefined") {
10681
11014
  Module.getRandomValue = randomValuesStandard;
10682
11015
  } catch (e) {
10683
11016
  try {
10684
- crypto = null;
11017
+ crypto2 = null;
10685
11018
  randomValueNodeJS = function() {
10686
- var buf = crypto["randomBytes"](4);
11019
+ var buf = crypto2["randomBytes"](4);
10687
11020
  return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
10688
11021
  };
10689
11022
  randomValueNodeJS();
@@ -10696,7 +11029,7 @@ if (typeof Module.getRandomValue === "undefined") {
10696
11029
  var window_;
10697
11030
  var crypto_;
10698
11031
  var randomValuesStandard;
10699
- var crypto;
11032
+ var crypto2;
10700
11033
  var randomValueNodeJS;
10701
11034
  var _Module = Module;
10702
11035
  Module.ready = new Promise(function(resolve6, reject) {
@@ -37590,7 +37923,7 @@ Module.ready = new Promise(function(resolve6, reject) {
37590
37923
  try {
37591
37924
  var window_ = "object" === typeof window ? window : self;
37592
37925
  var crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto;
37593
- crypto_ = crypto_ === void 0 ? crypto : crypto_;
37926
+ crypto_ = crypto_ === void 0 ? crypto2 : crypto_;
37594
37927
  var randomValuesStandard = function() {
37595
37928
  var buf = new Uint32Array(1);
37596
37929
  crypto_.getRandomValues(buf);
@@ -37600,9 +37933,9 @@ Module.ready = new Promise(function(resolve6, reject) {
37600
37933
  Module3.getRandomValue = randomValuesStandard;
37601
37934
  } catch (e) {
37602
37935
  try {
37603
- var crypto = null;
37936
+ var crypto2 = null;
37604
37937
  var randomValueNodeJS = function() {
37605
- var buf = crypto["randomBytes"](4);
37938
+ var buf = crypto2["randomBytes"](4);
37606
37939
  return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
37607
37940
  };
37608
37941
  randomValueNodeJS();
@@ -38248,7 +38581,7 @@ Module.ready = new Promise(function(resolve6, reject) {
38248
38581
  try {
38249
38582
  var window_ = "object" === typeof window ? window : self;
38250
38583
  var crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto;
38251
- crypto_ = crypto_ === void 0 ? crypto : crypto_;
38584
+ crypto_ = crypto_ === void 0 ? crypto2 : crypto_;
38252
38585
  var randomValuesStandard = function() {
38253
38586
  var buf = new Uint32Array(1);
38254
38587
  crypto_.getRandomValues(buf);
@@ -38258,9 +38591,9 @@ Module.ready = new Promise(function(resolve6, reject) {
38258
38591
  Module2.getRandomValue = randomValuesStandard;
38259
38592
  } catch (e) {
38260
38593
  try {
38261
- var crypto = null;
38594
+ var crypto2 = null;
38262
38595
  var randomValueNodeJS = function() {
38263
- var buf = crypto["randomBytes"](4);
38596
+ var buf = crypto2["randomBytes"](4);
38264
38597
  return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
38265
38598
  };
38266
38599
  randomValueNodeJS();
@@ -41366,17 +41699,17 @@ var RelayClient = class {
41366
41699
  };
41367
41700
 
41368
41701
  // ../daemon/src/pairing.ts
41369
- import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
41370
- import { homedir as homedir6 } from "node:os";
41371
- import { join as join7 } from "node:path";
41702
+ import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
41703
+ import { homedir as homedir7 } from "node:os";
41704
+ import { join as join8 } from "node:path";
41372
41705
  import { randomInt } from "node:crypto";
41373
- var PAIRING_DIR = process.env.OFFHAND_HOME ?? join7(homedir6(), ".offhand");
41374
- var PAIRING_PATH = join7(PAIRING_DIR, "pairing.json");
41706
+ var PAIRING_DIR = process.env.OFFHAND_HOME ?? join8(homedir7(), ".offhand");
41707
+ var PAIRING_PATH = join8(PAIRING_DIR, "pairing.json");
41375
41708
  var POLL_MS = 2e3;
41376
41709
  var POLL_TIMEOUT_MS = 10 * 60 * 1e3;
41377
41710
  async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://offhand-web.onrender.com") {
41378
41711
  await ready;
41379
- if (!forceNew && existsSync6(PAIRING_PATH)) {
41712
+ if (!forceNew && existsSync7(PAIRING_PATH)) {
41380
41713
  const f2 = JSON.parse(readFileSync4(PAIRING_PATH, "utf8"));
41381
41714
  const kp2 = { publicKey: fromB64u(f2.daemonPublicKey), secretKey: fromB64u(f2.daemonSecretKey) };
41382
41715
  const phonePk = fromB64u(f2.phonePublicKey);
@@ -41449,7 +41782,7 @@ async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://off
41449
41782
  // ../daemon/src/approvals.ts
41450
41783
  import { randomUUID as randomUUID3 } from "node:crypto";
41451
41784
  import { tmpdir as tmpdir2 } from "node:os";
41452
- import { join as join8 } from "node:path";
41785
+ import { join as join9 } from "node:path";
41453
41786
  var ApprovalBroker = class {
41454
41787
  constructor(timeoutMs) {
41455
41788
  this.timeoutMs = timeoutMs;
@@ -41473,7 +41806,7 @@ var ApprovalBroker = class {
41473
41806
  }
41474
41807
  const risk = classifyRisk(toolName, input);
41475
41808
  const filePath = input?.file_path;
41476
- if (toolName === "Read" && typeof filePath === "string" && filePath.toLowerCase().startsWith(join8(tmpdir2(), "offhand-attachments").toLowerCase())) {
41809
+ if (toolName === "Read" && typeof filePath === "string" && filePath.toLowerCase().startsWith(join9(tmpdir2(), "offhand-attachments").toLowerCase())) {
41477
41810
  return Promise.resolve({ approve: true });
41478
41811
  }
41479
41812
  if (this.policyProvider() === "trusting" && risk === "low" && toolName !== "AskUserQuestion") {
@@ -41540,11 +41873,11 @@ function summariseInput(input) {
41540
41873
  if (typeof input !== "object" || input === null) return String(input ?? "");
41541
41874
  const i2 = input;
41542
41875
  const q2 = firstQuestion(i2);
41543
- if (q2) return truncate4(q2.text, 200);
41876
+ if (q2) return truncate5(q2.text, 200);
41544
41877
  for (const key of ["command", "file_path", "path", "url", "pattern", "description"]) {
41545
- if (typeof i2[key] === "string" && i2[key] !== "") return truncate4(`${key}: ${i2[key]}`, 200);
41878
+ if (typeof i2[key] === "string" && i2[key] !== "") return truncate5(`${key}: ${i2[key]}`, 200);
41546
41879
  }
41547
- return truncate4(JSON.stringify(input), 200);
41880
+ return truncate5(JSON.stringify(input), 200);
41548
41881
  }
41549
41882
  function firstQuestion(i2) {
41550
41883
  if (!Array.isArray(i2.questions) || i2.questions.length === 0) return void 0;
@@ -41562,7 +41895,7 @@ function buildPreview(toolName, input) {
41562
41895
  const q2 = firstQuestion(i2);
41563
41896
  if (q2) {
41564
41897
  const lines = [q2.text, ...q2.options.map((o2, n2) => `${n2 + 1}. ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)];
41565
- return truncate4(lines.join("\n"), 600);
41898
+ return truncate5(lines.join("\n"), 600);
41566
41899
  }
41567
41900
  if (/^(Edit|MultiEdit)/.test(toolName) && typeof i2.new_string === "string") {
41568
41901
  const oldS = typeof i2.old_string === "string" ? i2.old_string : "";
@@ -41570,15 +41903,15 @@ function buildPreview(toolName, input) {
41570
41903
  ...oldS.split("\n").map((l2) => `- ${l2}`),
41571
41904
  ...i2.new_string.split("\n").map((l2) => `+ ${l2}`)
41572
41905
  ];
41573
- return truncate4(lines.join("\n"), 600);
41906
+ return truncate5(lines.join("\n"), 600);
41574
41907
  }
41575
41908
  if (/^Write/.test(toolName) && typeof i2.content === "string") {
41576
- return truncate4(i2.content.split("\n").map((l2) => `+ ${l2}`).join("\n"), 600);
41909
+ return truncate5(i2.content.split("\n").map((l2) => `+ ${l2}`).join("\n"), 600);
41577
41910
  }
41578
- if (typeof i2.command === "string") return truncate4(`$ ${i2.command}`, 600);
41911
+ if (typeof i2.command === "string") return truncate5(`$ ${i2.command}`, 600);
41579
41912
  return void 0;
41580
41913
  }
41581
- function truncate4(s2, max) {
41914
+ function truncate5(s2, max) {
41582
41915
  return s2.length <= max ? s2 : s2.slice(0, max - 1) + "\u2026";
41583
41916
  }
41584
41917
 
@@ -41615,28 +41948,28 @@ async function downloadArtifact(relayUrl2, sessionId, blobId, keys) {
41615
41948
  }
41616
41949
 
41617
41950
  // ../daemon/src/autostart.ts
41618
- import { homedir as homedir7 } from "node:os";
41619
- import { join as join9, resolve as resolve4, dirname as dirname3 } from "node:path";
41620
- import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, rmSync } from "node:fs";
41951
+ import { homedir as homedir8 } from "node:os";
41952
+ import { join as join10, resolve as resolve4, dirname as dirname3 } from "node:path";
41953
+ import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, rmSync } from "node:fs";
41621
41954
  import { fileURLToPath as fileURLToPath3 } from "node:url";
41622
41955
  import { spawnSync } from "node:child_process";
41623
41956
  import { tmpdir as tmpdir3 } from "node:os";
41624
41957
  var __dirname2 = dirname3(fileURLToPath3(import.meta.url));
41625
41958
  var SHORTCUT_NAME = "OffhandDaemon.lnk";
41626
- var OFFHAND_HOME = process.env.OFFHAND_HOME ?? join9(homedir7(), ".offhand");
41959
+ var OFFHAND_HOME = process.env.OFFHAND_HOME ?? join10(homedir8(), ".offhand");
41627
41960
  function getStartupDir() {
41628
41961
  if (process.platform !== "win32") {
41629
- return join9(homedir7(), ".config", "autostart");
41962
+ return join10(homedir8(), ".config", "autostart");
41630
41963
  }
41631
- return join9(process.env.APPDATA ?? "", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
41964
+ return join10(process.env.APPDATA ?? "", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
41632
41965
  }
41633
41966
  function getWrapperPath() {
41634
- const wrapperDir = join9(OFFHAND_HOME, "autostart");
41967
+ const wrapperDir = join10(OFFHAND_HOME, "autostart");
41635
41968
  mkdirSync5(wrapperDir, { recursive: true });
41636
- return join9(wrapperDir, "start-daemon.bat");
41969
+ return join10(wrapperDir, "start-daemon.bat");
41637
41970
  }
41638
41971
  function getShortcutPath() {
41639
- return join9(getStartupDir(), SHORTCUT_NAME);
41972
+ return join10(getStartupDir(), SHORTCUT_NAME);
41640
41973
  }
41641
41974
  function buildWrapperScript(config) {
41642
41975
  const projectRoot = resolve4(__dirname2, "..", "..");
@@ -41669,7 +42002,7 @@ $Shortcut.Arguments = "/c ${targetPath}"
41669
42002
  $Shortcut.WorkingDirectory = "${resolve4(__dirname2, "..", "..")}"
41670
42003
  $Shortcut.Description = "Offhand Daemon - Auto-start on login"
41671
42004
  $Shortcut.Save()`;
41672
- const scriptPath = join9(tmpdir3(), "offhand-create-shortcut.ps1");
42005
+ const scriptPath = join10(tmpdir3(), "offhand-create-shortcut.ps1");
41673
42006
  writeFileSync4(scriptPath, psScript);
41674
42007
  const result = spawnSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath], { encoding: "utf8", shell: true });
41675
42008
  try {
@@ -41679,7 +42012,7 @@ $Shortcut.Save()`;
41679
42012
  return { success: result.status === 0, output: result.stdout + result.stderr };
41680
42013
  }
41681
42014
  function checkShortcutExists(shortcutPath) {
41682
- return existsSync7(shortcutPath);
42015
+ return existsSync8(shortcutPath);
41683
42016
  }
41684
42017
  function installAutostart(config) {
41685
42018
  const wrapperPath = getWrapperPath();
@@ -41699,7 +42032,7 @@ function getAutostartStatus() {
41699
42032
  const shortcutPath = getShortcutPath();
41700
42033
  const installed = checkShortcutExists(shortcutPath);
41701
42034
  const wrapperPath = getWrapperPath();
41702
- const wrapperExists = existsSync7(wrapperPath);
42035
+ const wrapperExists = existsSync8(wrapperPath);
41703
42036
  let details = `Shortcut: ${shortcutPath} (${installed ? "EXISTS" : "MISSING"})`;
41704
42037
  if (wrapperExists) {
41705
42038
  details += `
@@ -41708,7 +42041,7 @@ Wrapper: ${wrapperPath} (EXISTS)`;
41708
42041
  return { installed, details };
41709
42042
  }
41710
42043
  function saveConfig(config) {
41711
- const configPath = join9(OFFHAND_HOME, "autostart.json");
42044
+ const configPath = join10(OFFHAND_HOME, "autostart.json");
41712
42045
  writeFileSync4(configPath, JSON.stringify(config, null, 2));
41713
42046
  }
41714
42047
  function getConfigFromStore() {
@@ -41757,7 +42090,7 @@ var devUrl = argValue("--dev-url");
41757
42090
  var store = new Store();
41758
42091
  var wsArgs = argValues("--workspace").map((w2) => resolve5(w2));
41759
42092
  for (const w2 of wsArgs) {
41760
- if (!existsSync8(w2)) {
42093
+ if (!existsSync9(w2)) {
41761
42094
  console.error(`workspace does not exist: ${w2}`);
41762
42095
  process.exit(1);
41763
42096
  }
@@ -41769,6 +42102,7 @@ var approvalUrl = `http://127.0.0.1:${port}/approval`;
41769
42102
  var runners = [
41770
42103
  new ClaudeCodeRunner(broker, approvalUrl),
41771
42104
  new CopilotCliRunner(),
42105
+ new OpenCodeRunner(),
41772
42106
  new CodexCliRunner(),
41773
42107
  new CursorAgentRunner(),
41774
42108
  new GeminiCliRunner()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offhands",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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",