offhands 0.1.2 → 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 +421 -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,344 @@ 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 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
+
5300
5633
  // ../daemon/src/session-manager.ts
5301
5634
  import { randomUUID } from "node:crypto";
5302
5635
  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";
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";
5305
5638
  import { fileURLToPath as fileURLToPath2 } from "node:url";
5306
5639
 
5307
5640
  // ../daemon/src/receipts.ts
@@ -5366,25 +5699,25 @@ function shouldScreenshot(touchedFiles, devUrl2) {
5366
5699
  }
5367
5700
 
5368
5701
  // ../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");
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");
5373
5706
  function listClaudeConversations(workspace, projectsDir = process.env.OFFHAND_CLAUDE_PROJECTS_DIR ?? DEFAULT_PROJECTS_DIR) {
5374
5707
  const projectDir = findProjectDir(workspace, projectsDir);
5375
5708
  if (!projectDir) return [];
5376
5709
  return listJsonlFiles(projectDir).map((path) => summarizeConversation(path, workspace)).filter((c2) => c2 !== null).sort((a2, b2) => b2.lastActiveMs - a2.lastActiveMs).slice(0, 50);
5377
5710
  }
5378
5711
  function findProjectDir(workspace, projectsDir) {
5379
- if (!existsSync3(projectsDir)) return null;
5712
+ if (!existsSync4(projectsDir)) return null;
5380
5713
  const candidates = /* @__PURE__ */ new Set([encodeWorkspace(workspace), encodeWorkspace(resolve(workspace))]);
5381
5714
  for (const candidate of candidates) {
5382
- const exact = join3(projectsDir, candidate);
5383
- if (existsSync3(exact)) return exact;
5715
+ const exact = join4(projectsDir, candidate);
5716
+ if (existsSync4(exact)) return exact;
5384
5717
  }
5385
5718
  const lower = new Set([...candidates].map((c2) => c2.toLowerCase()));
5386
5719
  for (const entry of safeReadDir(projectsDir)) {
5387
- 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);
5388
5721
  }
5389
5722
  return null;
5390
5723
  }
@@ -5392,12 +5725,12 @@ function encodeWorkspace(workspace) {
5392
5725
  return workspace.replace(/[^A-Za-z0-9]/g, "-");
5393
5726
  }
5394
5727
  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;
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;
5398
5731
  return [
5399
5732
  ...files,
5400
- ...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))
5401
5734
  ];
5402
5735
  }
5403
5736
  function summarizeConversation(path, workspace) {
@@ -5416,7 +5749,7 @@ function summarizeConversation(path, workspace) {
5416
5749
  return {
5417
5750
  conversationId: basename(path, ".jsonl"),
5418
5751
  workspace,
5419
- firstPrompt: truncate3(firstPrompt || "(empty prompt)", 120),
5752
+ firstPrompt: truncate4(firstPrompt || "(empty prompt)", 120),
5420
5753
  lastActiveMs: Math.floor(statSync(path).mtimeMs),
5421
5754
  messageCount
5422
5755
  };
@@ -5442,7 +5775,7 @@ function userMessageText(value) {
5442
5775
  function collapseText(text) {
5443
5776
  return text.replace(/\s+/g, " ").trim();
5444
5777
  }
5445
- function truncate3(text, max) {
5778
+ function truncate4(text, max) {
5446
5779
  return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
5447
5780
  }
5448
5781
  function safeReadDir(path) {
@@ -5459,7 +5792,7 @@ function isRecord(value) {
5459
5792
  // ../daemon/src/drop.ts
5460
5793
  import {
5461
5794
  copyFileSync,
5462
- existsSync as existsSync4,
5795
+ existsSync as existsSync5,
5463
5796
  mkdirSync,
5464
5797
  readdirSync as readdirSync2,
5465
5798
  readFileSync as readFileSync2,
@@ -5468,18 +5801,18 @@ import {
5468
5801
  watch,
5469
5802
  writeFileSync
5470
5803
  } 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";
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";
5474
5807
  var DROP_LOG_SESSION_ID = "drop";
5475
5808
  function offhandHome() {
5476
- return process.env.OFFHAND_HOME ?? join4(homedir4(), ".offhand");
5809
+ return process.env.OFFHAND_HOME ?? join5(homedir5(), ".offhand");
5477
5810
  }
5478
5811
  function dropOutboxDir(home = offhandHome()) {
5479
- return join4(home, "drop-outbox");
5812
+ return join5(home, "drop-outbox");
5480
5813
  }
5481
5814
  function incomingDropDir() {
5482
- return join4(homedir4(), "Downloads", "offhand");
5815
+ return join5(homedir5(), "Downloads", "offhand");
5483
5816
  }
5484
5817
  function safeDropName(input) {
5485
5818
  const base = basename2(input).replace(/[<>:"/\\|?*\x00-\x1F]/g, "_").replace(/[. ]+$/g, "").trim();
@@ -5503,13 +5836,13 @@ function queueDropFileForPhone(sourcePath, outbox = dropOutboxDir()) {
5503
5836
  const st = statSync2(source);
5504
5837
  if (!st.isFile()) throw new Error(`not a file: ${sourcePath}`);
5505
5838
  mkdirSync(outbox, { recursive: true });
5506
- const dest = join4(outbox, dedupeDropName(basename2(source), safeNames(outbox)));
5839
+ const dest = join5(outbox, dedupeDropName(basename2(source), safeNames(outbox)));
5507
5840
  copyFileSync(source, dest);
5508
5841
  return dest;
5509
5842
  }
5510
5843
  function saveIncomingDrop(name, bytes, dir = incomingDropDir()) {
5511
5844
  mkdirSync(dir, { recursive: true });
5512
- const path = join4(dir, dedupeDropName(name, safeNames(dir)));
5845
+ const path = join5(dir, dedupeDropName(name, safeNames(dir)));
5513
5846
  writeFileSync(path, bytes);
5514
5847
  return path;
5515
5848
  }
@@ -5552,7 +5885,7 @@ function startDropOutboxWatcher(outbox, sendFile) {
5552
5885
  console.error(`drop: sent but could not delete ${path}: ${e instanceof Error ? e.message : String(e)}`);
5553
5886
  }
5554
5887
  } catch (e) {
5555
- if (existsSync4(path)) {
5888
+ if (existsSync5(path)) {
5556
5889
  console.error(`drop: failed to send ${path}: ${e instanceof Error ? e.message : String(e)}`);
5557
5890
  schedule(15e3);
5558
5891
  }
@@ -5564,7 +5897,7 @@ function startDropOutboxWatcher(outbox, sendFile) {
5564
5897
  timer = null;
5565
5898
  if (closed) return;
5566
5899
  for (const entry of safeEntries(outbox)) {
5567
- if (entry.isFile()) void processFile(join4(outbox, entry.name));
5900
+ if (entry.isFile()) void processFile(join5(outbox, entry.name));
5568
5901
  }
5569
5902
  };
5570
5903
  watcher = watch(outbox, () => schedule());
@@ -5592,7 +5925,7 @@ function showDropToast(savedPath, textToClipboard) {
5592
5925
  `$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)`,
5593
5926
  `[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier(${psQuote(appId)}).Show($toast)`
5594
5927
  ].filter(Boolean);
5595
- const child = spawn4("powershell", ["-NoProfile", "-Command", lines.join("; ")], {
5928
+ const child = spawn5("powershell", ["-NoProfile", "-Command", lines.join("; ")], {
5596
5929
  windowsHide: true,
5597
5930
  stdio: ["ignore", "ignore", "pipe"]
5598
5931
  });
@@ -5650,7 +5983,7 @@ function mimeFromName(path) {
5650
5983
  // ../daemon/src/session-manager.ts
5651
5984
  function resolveDaemonVersion() {
5652
5985
  try {
5653
- const pkgPath = join5(dirname2(fileURLToPath2(import.meta.url)), "..", "package.json");
5986
+ const pkgPath = join6(dirname2(fileURLToPath2(import.meta.url)), "..", "package.json");
5654
5987
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
5655
5988
  return pkg.version ?? "0.0.0";
5656
5989
  } catch {
@@ -5748,9 +6081,9 @@ var SessionManager = class {
5748
6081
  prompt += "\n\nAttached files (read them from disk):";
5749
6082
  for (const a2 of msg.attachments) {
5750
6083
  const bytes = await this.attachmentFetcher(a2.blobId);
5751
- const dir = join5(tmpdir(), "offhand-attachments");
6084
+ const dir = join6(tmpdir(), "offhand-attachments");
5752
6085
  mkdirSync2(dir, { recursive: true });
5753
- 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)}`);
5754
6087
  writeFileSync2(path, bytes);
5755
6088
  prompt += `
5756
6089
  - ${path} (${a2.mime})`;
@@ -5988,17 +6321,17 @@ function listFolders(path) {
5988
6321
  if (!path) return { path: "", parent: null, dirs: driveRoots() };
5989
6322
  const current = resolve3(path);
5990
6323
  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")) };
6324
+ const full = join6(current, entry.name);
6325
+ return { name: entry.name, path: full, isGit: existsSync6(join6(full, ".git")) };
5993
6326
  }).sort((a2, b2) => Number(b2.isGit) - Number(a2.isGit) || a2.name.localeCompare(b2.name)).slice(0, 200);
5994
6327
  return { path: current, parent: isRoot(current) ? null : dirname2(current), dirs };
5995
6328
  }
5996
6329
  function driveRoots() {
5997
- if (process.platform !== "win32") return [{ name: "/", path: "/", isGit: existsSync5("/.git") }];
6330
+ if (process.platform !== "win32") return [{ name: "/", path: "/", isGit: existsSync6("/.git") }];
5998
6331
  const roots = [];
5999
6332
  for (let code = 67; code <= 90; code++) {
6000
6333
  const name = `${String.fromCharCode(code)}:\\`;
6001
- 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")) });
6002
6335
  }
6003
6336
  return roots.sort((a2, b2) => a2.name.localeCompare(b2.name)).slice(0, 200);
6004
6337
  }
@@ -6031,14 +6364,14 @@ function stripTrailing(path) {
6031
6364
  // ../daemon/src/store.ts
6032
6365
  import { DatabaseSync } from "node:sqlite";
6033
6366
  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";
6367
+ import { homedir as homedir6 } from "node:os";
6368
+ import { join as join7, basename as basename4 } from "node:path";
6036
6369
  import { randomUUID as randomUUID2 } from "node:crypto";
6037
6370
  var Store = class {
6038
6371
  db;
6039
- constructor(dir = process.env.OFFHAND_HOME ?? join6(homedir5(), ".offhand")) {
6372
+ constructor(dir = process.env.OFFHAND_HOME ?? join7(homedir6(), ".offhand")) {
6040
6373
  mkdirSync3(dir, { recursive: true });
6041
- this.db = new DatabaseSync(join6(dir, "offhand.db"));
6374
+ this.db = new DatabaseSync(join7(dir, "offhand.db"));
6042
6375
  this.db.exec(`
6043
6376
  PRAGMA journal_mode = WAL;
6044
6377
  CREATE TABLE IF NOT EXISTS sessions (
@@ -10280,6 +10613,11 @@ var coerce = {
10280
10613
  var NEVER = INVALID;
10281
10614
 
10282
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
+ });
10283
10621
  var RunEventSchema = external_exports.discriminatedUnion("type", [
10284
10622
  external_exports.object({ type: external_exports.literal("text"), chunk: external_exports.string() }),
10285
10623
  /** Extended-thinking content, streamed the same way as text (medium+ effort tiers). */
@@ -10294,11 +10632,7 @@ var RunEventSchema = external_exports.discriminatedUnion("type", [
10294
10632
  /** Optional content preview (edit diff / command) shown in the approval sheet. */
10295
10633
  preview: external_exports.string().optional(),
10296
10634
  /** 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()
10635
+ question: ApprovalQuestionSchema.optional()
10302
10636
  }),
10303
10637
  /** Encrypted blob reference (M5): phone fetches + decrypts locally. */
10304
10638
  external_exports.object({
@@ -10681,9 +11015,9 @@ if (typeof Module.getRandomValue === "undefined") {
10681
11015
  Module.getRandomValue = randomValuesStandard;
10682
11016
  } catch (e) {
10683
11017
  try {
10684
- crypto = null;
11018
+ crypto2 = null;
10685
11019
  randomValueNodeJS = function() {
10686
- var buf = crypto["randomBytes"](4);
11020
+ var buf = crypto2["randomBytes"](4);
10687
11021
  return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
10688
11022
  };
10689
11023
  randomValueNodeJS();
@@ -10696,7 +11030,7 @@ if (typeof Module.getRandomValue === "undefined") {
10696
11030
  var window_;
10697
11031
  var crypto_;
10698
11032
  var randomValuesStandard;
10699
- var crypto;
11033
+ var crypto2;
10700
11034
  var randomValueNodeJS;
10701
11035
  var _Module = Module;
10702
11036
  Module.ready = new Promise(function(resolve6, reject) {
@@ -37590,7 +37924,7 @@ Module.ready = new Promise(function(resolve6, reject) {
37590
37924
  try {
37591
37925
  var window_ = "object" === typeof window ? window : self;
37592
37926
  var crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto;
37593
- crypto_ = crypto_ === void 0 ? crypto : crypto_;
37927
+ crypto_ = crypto_ === void 0 ? crypto2 : crypto_;
37594
37928
  var randomValuesStandard = function() {
37595
37929
  var buf = new Uint32Array(1);
37596
37930
  crypto_.getRandomValues(buf);
@@ -37600,9 +37934,9 @@ Module.ready = new Promise(function(resolve6, reject) {
37600
37934
  Module3.getRandomValue = randomValuesStandard;
37601
37935
  } catch (e) {
37602
37936
  try {
37603
- var crypto = null;
37937
+ var crypto2 = null;
37604
37938
  var randomValueNodeJS = function() {
37605
- var buf = crypto["randomBytes"](4);
37939
+ var buf = crypto2["randomBytes"](4);
37606
37940
  return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
37607
37941
  };
37608
37942
  randomValueNodeJS();
@@ -38248,7 +38582,7 @@ Module.ready = new Promise(function(resolve6, reject) {
38248
38582
  try {
38249
38583
  var window_ = "object" === typeof window ? window : self;
38250
38584
  var crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto;
38251
- crypto_ = crypto_ === void 0 ? crypto : crypto_;
38585
+ crypto_ = crypto_ === void 0 ? crypto2 : crypto_;
38252
38586
  var randomValuesStandard = function() {
38253
38587
  var buf = new Uint32Array(1);
38254
38588
  crypto_.getRandomValues(buf);
@@ -38258,9 +38592,9 @@ Module.ready = new Promise(function(resolve6, reject) {
38258
38592
  Module2.getRandomValue = randomValuesStandard;
38259
38593
  } catch (e) {
38260
38594
  try {
38261
- var crypto = null;
38595
+ var crypto2 = null;
38262
38596
  var randomValueNodeJS = function() {
38263
- var buf = crypto["randomBytes"](4);
38597
+ var buf = crypto2["randomBytes"](4);
38264
38598
  return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
38265
38599
  };
38266
38600
  randomValueNodeJS();
@@ -41366,17 +41700,17 @@ var RelayClient = class {
41366
41700
  };
41367
41701
 
41368
41702
  // ../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";
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";
41372
41706
  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");
41707
+ var PAIRING_DIR = process.env.OFFHAND_HOME ?? join8(homedir7(), ".offhand");
41708
+ var PAIRING_PATH = join8(PAIRING_DIR, "pairing.json");
41375
41709
  var POLL_MS = 2e3;
41376
41710
  var POLL_TIMEOUT_MS = 10 * 60 * 1e3;
41377
41711
  async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://offhand-web.onrender.com") {
41378
41712
  await ready;
41379
- if (!forceNew && existsSync6(PAIRING_PATH)) {
41713
+ if (!forceNew && existsSync7(PAIRING_PATH)) {
41380
41714
  const f2 = JSON.parse(readFileSync4(PAIRING_PATH, "utf8"));
41381
41715
  const kp2 = { publicKey: fromB64u(f2.daemonPublicKey), secretKey: fromB64u(f2.daemonSecretKey) };
41382
41716
  const phonePk = fromB64u(f2.phonePublicKey);
@@ -41449,7 +41783,7 @@ async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://off
41449
41783
  // ../daemon/src/approvals.ts
41450
41784
  import { randomUUID as randomUUID3 } from "node:crypto";
41451
41785
  import { tmpdir as tmpdir2 } from "node:os";
41452
- import { join as join8 } from "node:path";
41786
+ import { join as join9 } from "node:path";
41453
41787
  var ApprovalBroker = class {
41454
41788
  constructor(timeoutMs) {
41455
41789
  this.timeoutMs = timeoutMs;
@@ -41473,7 +41807,7 @@ var ApprovalBroker = class {
41473
41807
  }
41474
41808
  const risk = classifyRisk(toolName, input);
41475
41809
  const filePath = input?.file_path;
41476
- 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())) {
41477
41811
  return Promise.resolve({ approve: true });
41478
41812
  }
41479
41813
  if (this.policyProvider() === "trusting" && risk === "low" && toolName !== "AskUserQuestion") {
@@ -41540,11 +41874,11 @@ function summariseInput(input) {
41540
41874
  if (typeof input !== "object" || input === null) return String(input ?? "");
41541
41875
  const i2 = input;
41542
41876
  const q2 = firstQuestion(i2);
41543
- if (q2) return truncate4(q2.text, 200);
41877
+ if (q2) return truncate5(q2.text, 200);
41544
41878
  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);
41879
+ if (typeof i2[key] === "string" && i2[key] !== "") return truncate5(`${key}: ${i2[key]}`, 200);
41546
41880
  }
41547
- return truncate4(JSON.stringify(input), 200);
41881
+ return truncate5(JSON.stringify(input), 200);
41548
41882
  }
41549
41883
  function firstQuestion(i2) {
41550
41884
  if (!Array.isArray(i2.questions) || i2.questions.length === 0) return void 0;
@@ -41562,7 +41896,7 @@ function buildPreview(toolName, input) {
41562
41896
  const q2 = firstQuestion(i2);
41563
41897
  if (q2) {
41564
41898
  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);
41899
+ return truncate5(lines.join("\n"), 600);
41566
41900
  }
41567
41901
  if (/^(Edit|MultiEdit)/.test(toolName) && typeof i2.new_string === "string") {
41568
41902
  const oldS = typeof i2.old_string === "string" ? i2.old_string : "";
@@ -41570,15 +41904,15 @@ function buildPreview(toolName, input) {
41570
41904
  ...oldS.split("\n").map((l2) => `- ${l2}`),
41571
41905
  ...i2.new_string.split("\n").map((l2) => `+ ${l2}`)
41572
41906
  ];
41573
- return truncate4(lines.join("\n"), 600);
41907
+ return truncate5(lines.join("\n"), 600);
41574
41908
  }
41575
41909
  if (/^Write/.test(toolName) && typeof i2.content === "string") {
41576
- 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);
41577
41911
  }
41578
- if (typeof i2.command === "string") return truncate4(`$ ${i2.command}`, 600);
41912
+ if (typeof i2.command === "string") return truncate5(`$ ${i2.command}`, 600);
41579
41913
  return void 0;
41580
41914
  }
41581
- function truncate4(s2, max) {
41915
+ function truncate5(s2, max) {
41582
41916
  return s2.length <= max ? s2 : s2.slice(0, max - 1) + "\u2026";
41583
41917
  }
41584
41918
 
@@ -41615,28 +41949,28 @@ async function downloadArtifact(relayUrl2, sessionId, blobId, keys) {
41615
41949
  }
41616
41950
 
41617
41951
  // ../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";
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";
41621
41955
  import { fileURLToPath as fileURLToPath3 } from "node:url";
41622
41956
  import { spawnSync } from "node:child_process";
41623
41957
  import { tmpdir as tmpdir3 } from "node:os";
41624
41958
  var __dirname2 = dirname3(fileURLToPath3(import.meta.url));
41625
41959
  var SHORTCUT_NAME = "OffhandDaemon.lnk";
41626
- var OFFHAND_HOME = process.env.OFFHAND_HOME ?? join9(homedir7(), ".offhand");
41960
+ var OFFHAND_HOME = process.env.OFFHAND_HOME ?? join10(homedir8(), ".offhand");
41627
41961
  function getStartupDir() {
41628
41962
  if (process.platform !== "win32") {
41629
- return join9(homedir7(), ".config", "autostart");
41963
+ return join10(homedir8(), ".config", "autostart");
41630
41964
  }
41631
- return join9(process.env.APPDATA ?? "", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
41965
+ return join10(process.env.APPDATA ?? "", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
41632
41966
  }
41633
41967
  function getWrapperPath() {
41634
- const wrapperDir = join9(OFFHAND_HOME, "autostart");
41968
+ const wrapperDir = join10(OFFHAND_HOME, "autostart");
41635
41969
  mkdirSync5(wrapperDir, { recursive: true });
41636
- return join9(wrapperDir, "start-daemon.bat");
41970
+ return join10(wrapperDir, "start-daemon.bat");
41637
41971
  }
41638
41972
  function getShortcutPath() {
41639
- return join9(getStartupDir(), SHORTCUT_NAME);
41973
+ return join10(getStartupDir(), SHORTCUT_NAME);
41640
41974
  }
41641
41975
  function buildWrapperScript(config) {
41642
41976
  const projectRoot = resolve4(__dirname2, "..", "..");
@@ -41669,7 +42003,7 @@ $Shortcut.Arguments = "/c ${targetPath}"
41669
42003
  $Shortcut.WorkingDirectory = "${resolve4(__dirname2, "..", "..")}"
41670
42004
  $Shortcut.Description = "Offhand Daemon - Auto-start on login"
41671
42005
  $Shortcut.Save()`;
41672
- const scriptPath = join9(tmpdir3(), "offhand-create-shortcut.ps1");
42006
+ const scriptPath = join10(tmpdir3(), "offhand-create-shortcut.ps1");
41673
42007
  writeFileSync4(scriptPath, psScript);
41674
42008
  const result = spawnSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath], { encoding: "utf8", shell: true });
41675
42009
  try {
@@ -41679,7 +42013,7 @@ $Shortcut.Save()`;
41679
42013
  return { success: result.status === 0, output: result.stdout + result.stderr };
41680
42014
  }
41681
42015
  function checkShortcutExists(shortcutPath) {
41682
- return existsSync7(shortcutPath);
42016
+ return existsSync8(shortcutPath);
41683
42017
  }
41684
42018
  function installAutostart(config) {
41685
42019
  const wrapperPath = getWrapperPath();
@@ -41699,7 +42033,7 @@ function getAutostartStatus() {
41699
42033
  const shortcutPath = getShortcutPath();
41700
42034
  const installed = checkShortcutExists(shortcutPath);
41701
42035
  const wrapperPath = getWrapperPath();
41702
- const wrapperExists = existsSync7(wrapperPath);
42036
+ const wrapperExists = existsSync8(wrapperPath);
41703
42037
  let details = `Shortcut: ${shortcutPath} (${installed ? "EXISTS" : "MISSING"})`;
41704
42038
  if (wrapperExists) {
41705
42039
  details += `
@@ -41708,7 +42042,7 @@ Wrapper: ${wrapperPath} (EXISTS)`;
41708
42042
  return { installed, details };
41709
42043
  }
41710
42044
  function saveConfig(config) {
41711
- const configPath = join9(OFFHAND_HOME, "autostart.json");
42045
+ const configPath = join10(OFFHAND_HOME, "autostart.json");
41712
42046
  writeFileSync4(configPath, JSON.stringify(config, null, 2));
41713
42047
  }
41714
42048
  function getConfigFromStore() {
@@ -41757,7 +42091,7 @@ var devUrl = argValue("--dev-url");
41757
42091
  var store = new Store();
41758
42092
  var wsArgs = argValues("--workspace").map((w2) => resolve5(w2));
41759
42093
  for (const w2 of wsArgs) {
41760
- if (!existsSync8(w2)) {
42094
+ if (!existsSync9(w2)) {
41761
42095
  console.error(`workspace does not exist: ${w2}`);
41762
42096
  process.exit(1);
41763
42097
  }
@@ -41769,6 +42103,7 @@ var approvalUrl = `http://127.0.0.1:${port}/approval`;
41769
42103
  var runners = [
41770
42104
  new ClaudeCodeRunner(broker, approvalUrl),
41771
42105
  new CopilotCliRunner(),
42106
+ new OpenCodeRunner(),
41772
42107
  new CodexCliRunner(),
41773
42108
  new CursorAgentRunner(),
41774
42109
  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.3",
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",