offhands 0.1.9 → 0.1.11

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 +218 -56
  2. package/package.json +3 -3
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 existsSync10 } from "node:fs";
4780
+ import { existsSync as existsSync11 } from "node:fs";
4781
4781
 
4782
4782
  // ../daemon/src/runners/claude-code.ts
4783
4783
  import { spawn } from "node:child_process";
@@ -5520,7 +5520,8 @@ var OpenCodeRunner = class {
5520
5520
  if (!pathDir) continue;
5521
5521
  const cmdPath = join3(pathDir, "opencode.cmd");
5522
5522
  if (existsSync3(cmdPath)) {
5523
- this.resolved = [cmdPath];
5523
+ const realExe = join3(dirname2(cmdPath), "node_modules", "opencode-ai", "bin", "opencode.exe");
5524
+ this.resolved = [existsSync3(realExe) ? realExe : cmdPath];
5524
5525
  return this.resolved;
5525
5526
  }
5526
5527
  const nodeModulesBin = join3(pathDir, "node_modules", ".bin", "opencode.cmd");
@@ -5654,13 +5655,14 @@ var OpenCodeRunner = class {
5654
5655
  }
5655
5656
  }
5656
5657
  args2.push("--dir", run.workspace);
5657
- const promptArg = process.platform === "win32" ? `"${run.prompt.replace(/"/g, '\\"')}"` : run.prompt;
5658
+ const viaShell = process.platform === "win32" && /\.(cmd|bat)$/i.test(cmd[0]);
5659
+ const promptArg = viaShell ? `"${run.prompt.replace(/\r?\n/g, " ").replace(/"/g, '\\"')}"` : run.prompt;
5658
5660
  args2.push(promptArg);
5659
5661
  try {
5660
5662
  child = spawn4(cmd[0], args2, {
5661
5663
  cwd: run.workspace,
5662
5664
  stdio: ["ignore", "pipe", "pipe"],
5663
- shell: process.platform === "win32",
5665
+ shell: viaShell,
5664
5666
  // One-shot process per prompt — no shared-server cross-session risk
5665
5667
  // here, so the session id can go straight on the env unconditionally.
5666
5668
  env: { ...process.env, OPENCODE_CONFIG_CONTENT: statusOnlyMcpConfigContent(this.statusUrl, run.sessionId) }
@@ -6013,8 +6015,8 @@ function sleep(ms) {
6013
6015
  // ../daemon/src/session-manager.ts
6014
6016
  import { randomUUID } from "node:crypto";
6015
6017
  import { hostname, platform as platform2, tmpdir } from "node:os";
6016
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
6017
- import { dirname as dirname3, join as join6, basename as basename3, parse as parse2, resolve as resolve3 } from "node:path";
6018
+ import { existsSync as existsSync8, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
6019
+ import { dirname as dirname3, join as join7, basename as basename3, parse as parse2, resolve as resolve3 } from "node:path";
6018
6020
  import { fileURLToPath as fileURLToPath3 } from "node:url";
6019
6021
  import { execFile as execFile4 } from "node:child_process";
6020
6022
  import { promisify as promisify4 } from "node:util";
@@ -6207,10 +6209,10 @@ function dedupeDropName(desiredName, existingNames) {
6207
6209
  const existing = new Set([...existingNames].map((n2) => n2.toLowerCase()));
6208
6210
  if (!existing.has(safe.toLowerCase())) return safe;
6209
6211
  const parsed = parse(safe);
6210
- const stem = parsed.name || "drop";
6212
+ const stem2 = parsed.name || "drop";
6211
6213
  const ext = parsed.ext;
6212
6214
  for (let i2 = 1; ; i2++) {
6213
- const candidate = `${stem}-${i2}${ext}`;
6215
+ const candidate = `${stem2}-${i2}${ext}`;
6214
6216
  if (!existing.has(candidate.toLowerCase())) return candidate;
6215
6217
  }
6216
6218
  }
@@ -6624,6 +6626,10 @@ async function probeWolCapability() {
6624
6626
  }
6625
6627
  }
6626
6628
 
6629
+ // ../daemon/src/intelligence/context-inject.ts
6630
+ import { existsSync as existsSync7, realpathSync, statSync as statSync3 } from "node:fs";
6631
+ import { isAbsolute, join as join6, relative, sep } from "node:path";
6632
+
6627
6633
  // ../daemon/src/intelligence/ripgrep.ts
6628
6634
  import { execFile as execFile3 } from "node:child_process";
6629
6635
  import { promisify as promisify3 } from "node:util";
@@ -6756,6 +6762,61 @@ function extractTerms(prompt, maxTerms = 8) {
6756
6762
  }
6757
6763
  return deduped.slice().sort((a2, b2) => b2.length - a2.length).slice(0, maxTerms);
6758
6764
  }
6765
+ var SECRET_BASENAMES = /* @__PURE__ */ new Set([".npmrc", ".netrc", "id_rsa", "id_dsa", "id_ecdsa", "id_ed25519", "credentials.json", "secrets.json"]);
6766
+ var SECRET_EXTS = [".pem", ".key", ".p12", ".pfx", ".keystore", ".jks", ".env"];
6767
+ var ENV_TEMPLATE_SUFFIXES = [".example", ".sample", ".template", ".dist"];
6768
+ function isSecretLike(path) {
6769
+ const base = (path.split(/[\\/]/).pop() ?? "").toLowerCase();
6770
+ if (base === ".env" || base.startsWith(".env.")) return !ENV_TEMPLATE_SUFFIXES.some((s2) => base.endsWith(s2));
6771
+ if (SECRET_BASENAMES.has(base)) return true;
6772
+ return SECRET_EXTS.some((e) => base.endsWith(e));
6773
+ }
6774
+ function parsePorcelainZ(out) {
6775
+ const parts = out.split("\0");
6776
+ const paths = [];
6777
+ for (let i2 = 0; i2 < parts.length; i2++) {
6778
+ const p2 = parts[i2];
6779
+ if (p2.length < 4) continue;
6780
+ const x2 = p2[0];
6781
+ const y2 = p2[1];
6782
+ if (x2 === "R" || x2 === "C" || y2 === "R" || y2 === "C") i2++;
6783
+ paths.push(p2.slice(3));
6784
+ }
6785
+ return paths;
6786
+ }
6787
+ function isDirectory(abs) {
6788
+ try {
6789
+ return statSync3(abs).isDirectory();
6790
+ } catch {
6791
+ return false;
6792
+ }
6793
+ }
6794
+ function realOrSame(p2) {
6795
+ try {
6796
+ return realpathSync.native(p2);
6797
+ } catch {
6798
+ return p2;
6799
+ }
6800
+ }
6801
+ function stem(word) {
6802
+ let w2 = word.toLowerCase();
6803
+ if (w2.length > 4) w2 = w2.replace(/(ing|ed|es|s)$/, "");
6804
+ if (w2.length > 3) w2 = w2.replace(/e$/, "");
6805
+ return w2;
6806
+ }
6807
+ function pathWords(path) {
6808
+ const split = (s2) => s2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean).map(stem);
6809
+ const slash = path.lastIndexOf("/");
6810
+ const base = new Set(split(path.slice(slash + 1)));
6811
+ const all = /* @__PURE__ */ new Set([...split(path.slice(0, Math.max(slash, 0))), ...base]);
6812
+ return { all, base };
6813
+ }
6814
+ var NOISE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb", "bun.lock", "composer.lock", "cargo.lock", "poetry.lock"]);
6815
+ var isNoise = (p2) => NOISE_FILES.has((p2.split("/").pop() ?? "").toLowerCase());
6816
+ var MAX_NAME_MATCHES = 10;
6817
+ var MAX_DIRTY_ONLY = 8;
6818
+ var MAX_RECENT = 6;
6819
+ var MAX_LS_FILES = 6e4;
6759
6820
  async function buildContext(workspace, prompt, opts = {}) {
6760
6821
  const startedAt = Date.now();
6761
6822
  const maxChars = opts.maxChars ?? (Number(process.env.OFFHAND_INTEL_MAX_CHARS) || DEFAULT_MAX_CHARS);
@@ -6780,7 +6841,8 @@ async function buildContext(workspace, prompt, opts = {}) {
6780
6841
  }
6781
6842
  const { findings, retrievalOpsRun, ripgrepAvailable, gitState } = result;
6782
6843
  if (findings.length === 0) return null;
6783
- findings.sort((a2, b2) => b2.priority - a2.priority);
6844
+ const rank = (f2) => f2.matchScore > 0 ? f2.dirty ? 5 : 4 : f2.dirty ? 2 : 1;
6845
+ findings.sort((a2, b2) => rank(b2) - rank(a2) || b2.matchScore - a2.matchScore);
6784
6846
  const { text: block, selectedCount } = formatBlock(prompt, findings, gitState, maxChars);
6785
6847
  if (selectedCount === 0) return null;
6786
6848
  return {
@@ -6800,41 +6862,141 @@ async function buildContext(workspace, prompt, opts = {}) {
6800
6862
  async function gatherFindings(workspace, prompt) {
6801
6863
  let retrievalOpsRun = 0;
6802
6864
  const byPath = /* @__PURE__ */ new Map();
6803
- const add = (path, reason, priority) => {
6804
- const existing = byPath.get(path);
6805
- if (existing) {
6806
- if (!existing.reasons.includes(reason)) existing.reasons.push(reason);
6807
- existing.priority = Math.max(existing.priority, priority);
6808
- } else {
6809
- byPath.set(path, { path, reasons: [reason], priority });
6865
+ const touch = (path) => {
6866
+ let f2 = byPath.get(path);
6867
+ if (!f2) {
6868
+ f2 = { path, reasons: [], dirty: false, matchScore: 0 };
6869
+ byPath.set(path, f2);
6810
6870
  }
6871
+ return f2;
6872
+ };
6873
+ const note = (f2, reason) => {
6874
+ if (!f2.reasons.includes(reason)) f2.reasons.push(reason);
6811
6875
  };
6812
- const statusOut = await git(workspace, "status", "--porcelain");
6876
+ const wsReal = realOrSame(workspace);
6877
+ const rootOut = await git(workspace, "rev-parse", "--show-toplevel");
6878
+ retrievalOpsRun++;
6879
+ const gitRoot = realOrSame(rootOut?.trim() || workspace);
6880
+ const liveRel = (rootRelative) => {
6881
+ const abs = join6(gitRoot, rootRelative);
6882
+ const rel = relative(wsReal, abs);
6883
+ if (!rel || rel.startsWith("..") || isAbsolute(rel)) return null;
6884
+ if (!existsSync7(abs)) return null;
6885
+ return rel.split(sep).join("/");
6886
+ };
6887
+ const statusOut = await git(workspace, "status", "--porcelain", "-z");
6813
6888
  retrievalOpsRun++;
6814
6889
  let gitState = null;
6890
+ const dirtySet = /* @__PURE__ */ new Set();
6891
+ const dirtyDirs = [];
6892
+ const isDirty2 = (p2) => dirtySet.has(p2) || dirtyDirs.some((d2) => p2.startsWith(d2));
6815
6893
  if (statusOut !== null) {
6816
- const dirtyFiles = statusOut.split("\n").filter((l2) => l2.length > 3).map((l2) => l2.slice(3).trim()).filter(Boolean);
6817
- for (const f2 of dirtyFiles.slice(0, 15)) add(f2, "currently dirty", 3);
6818
- gitState = dirtyFiles.length > 0 ? `${dirtyFiles.length} file(s) with uncommitted changes` : "clean working tree";
6894
+ const entries = parsePorcelainZ(statusOut);
6895
+ let missing = 0;
6896
+ for (const p2 of entries) {
6897
+ const rel = liveRel(p2);
6898
+ if (rel === null) {
6899
+ missing++;
6900
+ continue;
6901
+ }
6902
+ if (isSecretLike(rel)) continue;
6903
+ if (rel.endsWith("/") || isDirectory(join6(gitRoot, p2))) dirtyDirs.push(rel.replace(/\/?$/, "/"));
6904
+ else dirtySet.add(rel);
6905
+ }
6906
+ let dirtyOnly = 0;
6907
+ for (const rel of dirtySet) {
6908
+ if (dirtyOnly >= MAX_DIRTY_ONLY) break;
6909
+ if (isNoise(rel)) continue;
6910
+ const f2 = touch(rel);
6911
+ f2.dirty = true;
6912
+ note(f2, "currently dirty");
6913
+ dirtyOnly++;
6914
+ }
6915
+ gitState = entries.length === 0 ? "clean working tree" : `${entries.length - missing} modified/new file(s)${missing > 0 ? ` and ${missing} deleted` : ""}, uncommitted`;
6819
6916
  }
6820
6917
  const ripgrepAvailable = await detectRipgrep();
6821
6918
  retrievalOpsRun++;
6822
6919
  const terms = extractTerms(prompt);
6920
+ if (terms.length > 0) {
6921
+ const listing = await git(workspace, "ls-files", "-co", "--exclude-standard", "-z");
6922
+ retrievalOpsRun++;
6923
+ const all = (listing ?? "").split("\0").filter(Boolean);
6924
+ if (all.length > 0 && all.length <= MAX_LS_FILES) {
6925
+ const stems = /* @__PURE__ */ new Map();
6926
+ const literals = [];
6927
+ for (const t2 of terms) {
6928
+ if (/[._-]/.test(t2)) literals.push(t2);
6929
+ else if (!stems.has(stem(t2))) stems.set(stem(t2), t2);
6930
+ }
6931
+ const words = all.map(pathWords);
6932
+ const lowered = literals.length > 0 ? all.map((p2) => p2.toLowerCase()) : [];
6933
+ const idf = /* @__PURE__ */ new Map();
6934
+ const wording = /* @__PURE__ */ new Map();
6935
+ const idfOf = (key, df) => {
6936
+ if (df > 0 && df < all.length) idf.set(key, Math.log(1 + all.length / df));
6937
+ };
6938
+ for (const [s2, original] of stems) {
6939
+ wording.set(s2, original);
6940
+ idfOf(s2, words.reduce((n2, w2) => w2.all.has(s2) ? n2 + 1 : n2, 0));
6941
+ }
6942
+ for (const lit of literals) {
6943
+ wording.set(lit, lit);
6944
+ idfOf(lit, lowered.reduce((n2, p2) => p2.includes(lit) ? n2 + 1 : n2, 0));
6945
+ }
6946
+ if (idf.size > 0) {
6947
+ const scored = [];
6948
+ for (let i2 = 0; i2 < all.length; i2++) {
6949
+ let score = 0;
6950
+ const matched = [];
6951
+ for (const [key, weight] of idf) {
6952
+ const isLiteral = literals.includes(key);
6953
+ if (isLiteral ? lowered[i2].includes(key) : words[i2].base.has(key)) score += weight * 2;
6954
+ else if (!isLiteral && words[i2].all.has(key)) score += weight;
6955
+ else continue;
6956
+ matched.push(wording.get(key));
6957
+ }
6958
+ if (score > 0) scored.push({ path: all[i2], score, matched });
6959
+ }
6960
+ scored.sort((a2, b2) => b2.score - a2.score);
6961
+ let taken = 0;
6962
+ for (const s2 of scored) {
6963
+ if (taken >= MAX_NAME_MATCHES) break;
6964
+ if (isSecretLike(s2.path) || !existsSync7(join6(workspace, s2.path))) continue;
6965
+ const f2 = touch(s2.path);
6966
+ f2.matchScore += s2.score;
6967
+ if (isDirty2(s2.path)) f2.dirty = true;
6968
+ note(f2, `path matches ${s2.matched.map((m3) => `"${m3}"`).join(", ")}`);
6969
+ taken++;
6970
+ }
6971
+ }
6972
+ }
6973
+ }
6823
6974
  if (ripgrepAvailable && terms.length > 0) {
6824
6975
  const hits = await searchTerms(workspace, terms);
6825
6976
  retrievalOpsRun++;
6826
6977
  for (const hit of hits) {
6978
+ const file = hit.file.replace(/\\/g, "/").replace(/^\.\//, "");
6979
+ if (isSecretLike(file)) continue;
6827
6980
  const term = terms.find((t2) => hit.text.toLowerCase().includes(t2));
6828
- add(hit.file, term ? `matched "${term}"` : "matched search terms", 2);
6981
+ const f2 = touch(file);
6982
+ f2.matchScore += 1;
6983
+ if (isDirty2(file)) f2.dirty = true;
6984
+ note(f2, term ? `matched "${term}"` : "matched search terms");
6829
6985
  }
6830
6986
  }
6831
- const logOut = await git(workspace, "log", "-n", "15", "--name-only", "--pretty=format:");
6987
+ const logOut = await git(workspace, "-c", "core.quotepath=off", "log", "-n", "15", "--name-only", "--relative", "--pretty=format:");
6832
6988
  retrievalOpsRun++;
6833
6989
  if (logOut !== null) {
6834
- const recentFiles = [...new Set(logOut.split("\n").map((l2) => l2.trim()).filter(Boolean))];
6835
- for (const f2 of recentFiles.slice(0, 10)) add(f2, "recently changed", 1);
6990
+ const recent = [...new Set(logOut.split("\n").map((l2) => l2.trim()).filter((l2) => l2 && !l2.startsWith('"')))];
6991
+ let taken = 0;
6992
+ for (const p2 of recent) {
6993
+ if (taken >= MAX_RECENT) break;
6994
+ if (isSecretLike(p2) || isNoise(p2) || !existsSync7(join6(workspace, p2))) continue;
6995
+ note(touch(p2), "recently changed");
6996
+ taken++;
6997
+ }
6836
6998
  }
6837
- return { findings: [...byPath.values()], retrievalOpsRun, ripgrepAvailable, gitState };
6999
+ return { findings: [...byPath.values()].filter((f2) => f2.reasons.length > 0), retrievalOpsRun, ripgrepAvailable, gitState };
6838
7000
  }
6839
7001
  function formatBlock(prompt, rankedFindings, gitState, maxChars) {
6840
7002
  const header = `TASK
@@ -6864,7 +7026,7 @@ OFFHAND REPOSITORY CONTEXT
6864
7026
  // ../daemon/src/session-manager.ts
6865
7027
  function resolveDaemonVersion() {
6866
7028
  try {
6867
- const pkgPath = join6(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json");
7029
+ const pkgPath = join7(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json");
6868
7030
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
6869
7031
  return pkg.version ?? "0.0.0";
6870
7032
  } catch {
@@ -7209,7 +7371,7 @@ var SessionManager = class {
7209
7371
  case "fs-create-folder": {
7210
7372
  try {
7211
7373
  const safeName = safeDropName(msg.name);
7212
- mkdirSync2(join6(resolve3(msg.path), safeName), { recursive: false });
7374
+ mkdirSync2(join7(resolve3(msg.path), safeName), { recursive: false });
7213
7375
  } catch (e) {
7214
7376
  reply({
7215
7377
  type: "fs-response",
@@ -7350,9 +7512,9 @@ var SessionManager = class {
7350
7512
  let out = prompt + "\n\nAttached files (read them from disk):";
7351
7513
  for (const a2 of attachments) {
7352
7514
  const bytes = await this.attachmentFetcher(a2.blobId);
7353
- const dir = join6(tmpdir(), "offhand-attachments");
7515
+ const dir = join7(tmpdir(), "offhand-attachments");
7354
7516
  mkdirSync2(dir, { recursive: true });
7355
- const path = join6(dir, `${a2.blobId.slice(0, 8)}-${basename3(a2.name)}`);
7517
+ const path = join7(dir, `${a2.blobId.slice(0, 8)}-${basename3(a2.name)}`);
7356
7518
  writeFileSync2(path, bytes);
7357
7519
  out += `
7358
7520
  - ${path} (${a2.mime})`;
@@ -7572,17 +7734,17 @@ function listFolders(path) {
7572
7734
  if (!path) return { path: "", parent: null, dirs: driveRoots() };
7573
7735
  const current = resolve3(path);
7574
7736
  const dirs = safeReadDir2(current).filter((entry) => entry.isDirectory() && !skipDir(entry.name)).map((entry) => {
7575
- const full = join6(current, entry.name);
7576
- return { name: entry.name, path: full, isGit: existsSync7(join6(full, ".git")) };
7737
+ const full = join7(current, entry.name);
7738
+ return { name: entry.name, path: full, isGit: existsSync8(join7(full, ".git")) };
7577
7739
  }).sort((a2, b2) => Number(b2.isGit) - Number(a2.isGit) || a2.name.localeCompare(b2.name)).slice(0, 200);
7578
7740
  return { path: current, parent: isRoot(current) ? null : dirname3(current), dirs };
7579
7741
  }
7580
7742
  function driveRoots() {
7581
- if (process.platform !== "win32") return [{ name: "/", path: "/", isGit: existsSync7("/.git") }];
7743
+ if (process.platform !== "win32") return [{ name: "/", path: "/", isGit: existsSync8("/.git") }];
7582
7744
  const roots = [];
7583
7745
  for (let code = 67; code <= 90; code++) {
7584
7746
  const name = `${String.fromCharCode(code)}:\\`;
7585
- if (existsSync7(name)) roots.push({ name, path: name, isGit: existsSync7(join6(name, ".git")) });
7747
+ if (existsSync8(name)) roots.push({ name, path: name, isGit: existsSync8(join7(name, ".git")) });
7586
7748
  }
7587
7749
  return roots.sort((a2, b2) => a2.name.localeCompare(b2.name)).slice(0, 200);
7588
7750
  }
@@ -7616,13 +7778,13 @@ function stripTrailing(path) {
7616
7778
  import { DatabaseSync } from "node:sqlite";
7617
7779
  import { mkdirSync as mkdirSync3 } from "node:fs";
7618
7780
  import { homedir as homedir6 } from "node:os";
7619
- import { join as join7, basename as basename4 } from "node:path";
7781
+ import { join as join8, basename as basename4 } from "node:path";
7620
7782
  import { randomUUID as randomUUID2 } from "node:crypto";
7621
7783
  var Store = class {
7622
7784
  db;
7623
- constructor(dir = process.env.OFFHAND_HOME ?? join7(homedir6(), ".offhand")) {
7785
+ constructor(dir = process.env.OFFHAND_HOME ?? join8(homedir6(), ".offhand")) {
7624
7786
  mkdirSync3(dir, { recursive: true });
7625
- this.db = new DatabaseSync(join7(dir, "offhand.db"));
7787
+ this.db = new DatabaseSync(join8(dir, "offhand.db"));
7626
7788
  this.db.exec(`
7627
7789
  PRAGMA journal_mode = WAL;
7628
7790
  CREATE TABLE IF NOT EXISTS sessions (
@@ -43348,17 +43510,17 @@ var RelayClient = class {
43348
43510
  };
43349
43511
 
43350
43512
  // ../daemon/src/pairing.ts
43351
- import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
43513
+ import { existsSync as existsSync9, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
43352
43514
  import { homedir as homedir7 } from "node:os";
43353
- import { join as join8 } from "node:path";
43515
+ import { join as join9 } from "node:path";
43354
43516
  import { randomInt } from "node:crypto";
43355
- var PAIRING_DIR = process.env.OFFHAND_HOME ?? join8(homedir7(), ".offhand");
43356
- var PAIRING_PATH = join8(PAIRING_DIR, "pairing.json");
43517
+ var PAIRING_DIR = process.env.OFFHAND_HOME ?? join9(homedir7(), ".offhand");
43518
+ var PAIRING_PATH = join9(PAIRING_DIR, "pairing.json");
43357
43519
  var POLL_MS = 2e3;
43358
43520
  var POLL_TIMEOUT_MS = 10 * 60 * 1e3;
43359
43521
  async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://offhand-web.onrender.com") {
43360
43522
  await ready;
43361
- if (!forceNew && existsSync8(PAIRING_PATH)) {
43523
+ if (!forceNew && existsSync9(PAIRING_PATH)) {
43362
43524
  const f2 = JSON.parse(readFileSync4(PAIRING_PATH, "utf8"));
43363
43525
  const kp2 = { publicKey: fromB64u(f2.daemonPublicKey), secretKey: fromB64u(f2.daemonSecretKey) };
43364
43526
  const phonePk = fromB64u(f2.phonePublicKey);
@@ -43431,7 +43593,7 @@ async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://off
43431
43593
  // ../daemon/src/approvals.ts
43432
43594
  import { randomUUID as randomUUID3 } from "node:crypto";
43433
43595
  import { tmpdir as tmpdir2 } from "node:os";
43434
- import { join as join9 } from "node:path";
43596
+ import { join as join10 } from "node:path";
43435
43597
  var ApprovalBroker = class {
43436
43598
  constructor(timeoutMs) {
43437
43599
  this.timeoutMs = timeoutMs;
@@ -43455,7 +43617,7 @@ var ApprovalBroker = class {
43455
43617
  }
43456
43618
  const risk = classifyRisk(toolName, input);
43457
43619
  const filePath = input?.file_path;
43458
- if (toolName === "Read" && typeof filePath === "string" && filePath.toLowerCase().startsWith(join9(tmpdir2(), "offhand-attachments").toLowerCase())) {
43620
+ if (toolName === "Read" && typeof filePath === "string" && filePath.toLowerCase().startsWith(join10(tmpdir2(), "offhand-attachments").toLowerCase())) {
43459
43621
  return Promise.resolve({ approve: true });
43460
43622
  }
43461
43623
  if (this.policyProvider() === "trusting" && risk === "low" && toolName !== "AskUserQuestion") {
@@ -43608,27 +43770,27 @@ async function downloadArtifact(relayUrl2, sessionId, blobId, keys) {
43608
43770
 
43609
43771
  // ../daemon/src/autostart.ts
43610
43772
  import { homedir as homedir8 } from "node:os";
43611
- import { join as join10, resolve as resolve4, dirname as dirname4 } from "node:path";
43612
- import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, rmSync } from "node:fs";
43773
+ import { join as join11, resolve as resolve4, dirname as dirname4 } from "node:path";
43774
+ import { existsSync as existsSync10, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, rmSync } from "node:fs";
43613
43775
  import { fileURLToPath as fileURLToPath4 } from "node:url";
43614
43776
  import { spawnSync } from "node:child_process";
43615
43777
  import { tmpdir as tmpdir3 } from "node:os";
43616
43778
  var __dirname2 = dirname4(fileURLToPath4(import.meta.url));
43617
43779
  var SHORTCUT_NAME = "OffhandDaemon.lnk";
43618
- var OFFHAND_HOME = process.env.OFFHAND_HOME ?? join10(homedir8(), ".offhand");
43780
+ var OFFHAND_HOME = process.env.OFFHAND_HOME ?? join11(homedir8(), ".offhand");
43619
43781
  function getStartupDir() {
43620
43782
  if (process.platform !== "win32") {
43621
- return join10(homedir8(), ".config", "autostart");
43783
+ return join11(homedir8(), ".config", "autostart");
43622
43784
  }
43623
- return join10(process.env.APPDATA ?? "", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
43785
+ return join11(process.env.APPDATA ?? "", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
43624
43786
  }
43625
43787
  function getWrapperPath() {
43626
- const wrapperDir = join10(OFFHAND_HOME, "autostart");
43788
+ const wrapperDir = join11(OFFHAND_HOME, "autostart");
43627
43789
  mkdirSync5(wrapperDir, { recursive: true });
43628
- return join10(wrapperDir, "start-daemon.bat");
43790
+ return join11(wrapperDir, "start-daemon.bat");
43629
43791
  }
43630
43792
  function getShortcutPath() {
43631
- return join10(getStartupDir(), SHORTCUT_NAME);
43793
+ return join11(getStartupDir(), SHORTCUT_NAME);
43632
43794
  }
43633
43795
  function buildWrapperScript(config2, entry = process.argv[1] ?? "", execPath = process.execPath) {
43634
43796
  const wsArgs2 = config2.workspaces.map((w2) => `--workspace "${w2}"`).join(" ");
@@ -43657,7 +43819,7 @@ $Shortcut.Arguments = "/c ${targetPath}"
43657
43819
  $Shortcut.WorkingDirectory = "${resolve4(__dirname2, "..", "..")}"
43658
43820
  $Shortcut.Description = "Offhand Daemon - Auto-start on login"
43659
43821
  $Shortcut.Save()`;
43660
- const scriptPath = join10(tmpdir3(), "offhand-create-shortcut.ps1");
43822
+ const scriptPath = join11(tmpdir3(), "offhand-create-shortcut.ps1");
43661
43823
  writeFileSync4(scriptPath, psScript);
43662
43824
  const result = spawnSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath], { encoding: "utf8", shell: true });
43663
43825
  try {
@@ -43667,7 +43829,7 @@ $Shortcut.Save()`;
43667
43829
  return { success: result.status === 0, output: result.stdout + result.stderr };
43668
43830
  }
43669
43831
  function checkShortcutExists(shortcutPath) {
43670
- return existsSync9(shortcutPath);
43832
+ return existsSync10(shortcutPath);
43671
43833
  }
43672
43834
  function installAutostart(config2) {
43673
43835
  const wrapperPath = getWrapperPath();
@@ -43687,7 +43849,7 @@ function getAutostartStatus() {
43687
43849
  const shortcutPath = getShortcutPath();
43688
43850
  const installed = checkShortcutExists(shortcutPath);
43689
43851
  const wrapperPath = getWrapperPath();
43690
- const wrapperExists = existsSync9(wrapperPath);
43852
+ const wrapperExists = existsSync10(wrapperPath);
43691
43853
  let details = `Shortcut: ${shortcutPath} (${installed ? "EXISTS" : "MISSING"})`;
43692
43854
  if (wrapperExists) {
43693
43855
  details += `
@@ -43696,7 +43858,7 @@ Wrapper: ${wrapperPath} (EXISTS)`;
43696
43858
  return { installed, details };
43697
43859
  }
43698
43860
  function saveConfig(config2) {
43699
- const configPath = join10(OFFHAND_HOME, "autostart.json");
43861
+ const configPath = join11(OFFHAND_HOME, "autostart.json");
43700
43862
  writeFileSync4(configPath, JSON.stringify(config2, null, 2));
43701
43863
  }
43702
43864
  function getConfigFromStore() {
@@ -43744,7 +43906,7 @@ var devUrl = argValue("--dev-url");
43744
43906
  var store = new Store();
43745
43907
  var wsArgs = argValues("--workspace").map((w2) => resolve5(w2));
43746
43908
  for (const w2 of wsArgs) {
43747
- if (!existsSync10(w2)) {
43909
+ if (!existsSync11(w2)) {
43748
43910
  console.error(`workspace does not exist: ${w2}`);
43749
43911
  process.exit(1);
43750
43912
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "offhands",
3
- "version": "0.1.9",
4
- "description": "Phone coding-agent relay. Daemon runs on your laptop, PWA on your phone. E2E encrypted.",
3
+ "version": "0.1.11",
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",
7
7
  "bin": {
@@ -23,4 +23,4 @@
23
23
  "devDependencies": {
24
24
  "esbuild": "^0.24.0"
25
25
  }
26
- }
26
+ }