offhands 0.1.10 → 0.1.12

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 +219 -54
  2. package/package.json +2 -2
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";
@@ -6015,8 +6015,8 @@ function sleep(ms) {
6015
6015
  // ../daemon/src/session-manager.ts
6016
6016
  import { randomUUID } from "node:crypto";
6017
6017
  import { hostname, platform as platform2, tmpdir } from "node:os";
6018
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
6019
- 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";
6020
6020
  import { fileURLToPath as fileURLToPath3 } from "node:url";
6021
6021
  import { execFile as execFile4 } from "node:child_process";
6022
6022
  import { promisify as promisify4 } from "node:util";
@@ -6209,10 +6209,10 @@ function dedupeDropName(desiredName, existingNames) {
6209
6209
  const existing = new Set([...existingNames].map((n2) => n2.toLowerCase()));
6210
6210
  if (!existing.has(safe.toLowerCase())) return safe;
6211
6211
  const parsed = parse(safe);
6212
- const stem = parsed.name || "drop";
6212
+ const stem2 = parsed.name || "drop";
6213
6213
  const ext = parsed.ext;
6214
6214
  for (let i2 = 1; ; i2++) {
6215
- const candidate = `${stem}-${i2}${ext}`;
6215
+ const candidate = `${stem2}-${i2}${ext}`;
6216
6216
  if (!existing.has(candidate.toLowerCase())) return candidate;
6217
6217
  }
6218
6218
  }
@@ -6626,6 +6626,10 @@ async function probeWolCapability() {
6626
6626
  }
6627
6627
  }
6628
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
+
6629
6633
  // ../daemon/src/intelligence/ripgrep.ts
6630
6634
  import { execFile as execFile3 } from "node:child_process";
6631
6635
  import { promisify as promisify3 } from "node:util";
@@ -6758,6 +6762,62 @@ function extractTerms(prompt, maxTerms = 8) {
6758
6762
  }
6759
6763
  return deduped.slice().sort((a2, b2) => b2.length - a2.length).slice(0, maxTerms);
6760
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_UNMATCHED_WITH_MATCHES = 3;
6817
+ var MAX_NAME_MATCHES = 10;
6818
+ var MAX_DIRTY_ONLY = 8;
6819
+ var MAX_RECENT = 6;
6820
+ var MAX_LS_FILES = 6e4;
6761
6821
  async function buildContext(workspace, prompt, opts = {}) {
6762
6822
  const startedAt = Date.now();
6763
6823
  const maxChars = opts.maxChars ?? (Number(process.env.OFFHAND_INTEL_MAX_CHARS) || DEFAULT_MAX_CHARS);
@@ -6782,8 +6842,13 @@ async function buildContext(workspace, prompt, opts = {}) {
6782
6842
  }
6783
6843
  const { findings, retrievalOpsRun, ripgrepAvailable, gitState } = result;
6784
6844
  if (findings.length === 0) return null;
6785
- findings.sort((a2, b2) => b2.priority - a2.priority);
6786
- const { text: block, selectedCount } = formatBlock(prompt, findings, gitState, maxChars);
6845
+ const rank = (f2) => f2.matchScore > 0 ? f2.dirty ? 5 : 4 : f2.dirty ? 2 : 1;
6846
+ findings.sort((a2, b2) => rank(b2) - rank(a2) || b2.matchScore - a2.matchScore);
6847
+ const strongMatches = findings.filter((f2) => f2.matchScore > 0).length;
6848
+ const unmatchedCap = strongMatches >= 3 ? MAX_UNMATCHED_WITH_MATCHES : Infinity;
6849
+ let unmatched = 0;
6850
+ const shown = findings.filter((f2) => f2.matchScore > 0 || ++unmatched <= unmatchedCap);
6851
+ const { text: block, selectedCount } = formatBlock(prompt, shown, gitState, maxChars);
6787
6852
  if (selectedCount === 0) return null;
6788
6853
  return {
6789
6854
  block,
@@ -6802,41 +6867,141 @@ async function buildContext(workspace, prompt, opts = {}) {
6802
6867
  async function gatherFindings(workspace, prompt) {
6803
6868
  let retrievalOpsRun = 0;
6804
6869
  const byPath = /* @__PURE__ */ new Map();
6805
- const add = (path, reason, priority) => {
6806
- const existing = byPath.get(path);
6807
- if (existing) {
6808
- if (!existing.reasons.includes(reason)) existing.reasons.push(reason);
6809
- existing.priority = Math.max(existing.priority, priority);
6810
- } else {
6811
- byPath.set(path, { path, reasons: [reason], priority });
6870
+ const touch = (path) => {
6871
+ let f2 = byPath.get(path);
6872
+ if (!f2) {
6873
+ f2 = { path, reasons: [], dirty: false, matchScore: 0 };
6874
+ byPath.set(path, f2);
6812
6875
  }
6876
+ return f2;
6877
+ };
6878
+ const note = (f2, reason) => {
6879
+ if (!f2.reasons.includes(reason)) f2.reasons.push(reason);
6813
6880
  };
6814
- const statusOut = await git(workspace, "status", "--porcelain");
6881
+ const wsReal = realOrSame(workspace);
6882
+ const rootOut = await git(workspace, "rev-parse", "--show-toplevel");
6883
+ retrievalOpsRun++;
6884
+ const gitRoot = realOrSame(rootOut?.trim() || workspace);
6885
+ const liveRel = (rootRelative) => {
6886
+ const abs = join6(gitRoot, rootRelative);
6887
+ const rel = relative(wsReal, abs);
6888
+ if (!rel || rel.startsWith("..") || isAbsolute(rel)) return null;
6889
+ if (!existsSync7(abs)) return null;
6890
+ return rel.split(sep).join("/");
6891
+ };
6892
+ const statusOut = await git(workspace, "status", "--porcelain", "-z");
6815
6893
  retrievalOpsRun++;
6816
6894
  let gitState = null;
6895
+ const dirtySet = /* @__PURE__ */ new Set();
6896
+ const dirtyDirs = [];
6897
+ const isDirty2 = (p2) => dirtySet.has(p2) || dirtyDirs.some((d2) => p2.startsWith(d2));
6817
6898
  if (statusOut !== null) {
6818
- const dirtyFiles = statusOut.split("\n").filter((l2) => l2.length > 3).map((l2) => l2.slice(3).trim()).filter(Boolean);
6819
- for (const f2 of dirtyFiles.slice(0, 15)) add(f2, "currently dirty", 3);
6820
- gitState = dirtyFiles.length > 0 ? `${dirtyFiles.length} file(s) with uncommitted changes` : "clean working tree";
6899
+ const entries = parsePorcelainZ(statusOut);
6900
+ let missing = 0;
6901
+ for (const p2 of entries) {
6902
+ const rel = liveRel(p2);
6903
+ if (rel === null) {
6904
+ missing++;
6905
+ continue;
6906
+ }
6907
+ if (isSecretLike(rel)) continue;
6908
+ if (rel.endsWith("/") || isDirectory(join6(gitRoot, p2))) dirtyDirs.push(rel.replace(/\/?$/, "/"));
6909
+ else dirtySet.add(rel);
6910
+ }
6911
+ let dirtyOnly = 0;
6912
+ for (const rel of dirtySet) {
6913
+ if (dirtyOnly >= MAX_DIRTY_ONLY) break;
6914
+ if (isNoise(rel)) continue;
6915
+ const f2 = touch(rel);
6916
+ f2.dirty = true;
6917
+ note(f2, "currently dirty");
6918
+ dirtyOnly++;
6919
+ }
6920
+ gitState = entries.length === 0 ? "clean working tree" : `${entries.length - missing} modified/new file(s)${missing > 0 ? ` and ${missing} deleted` : ""}, uncommitted`;
6821
6921
  }
6822
6922
  const ripgrepAvailable = await detectRipgrep();
6823
6923
  retrievalOpsRun++;
6824
6924
  const terms = extractTerms(prompt);
6925
+ if (terms.length > 0) {
6926
+ const listing = await git(workspace, "ls-files", "-co", "--exclude-standard", "-z");
6927
+ retrievalOpsRun++;
6928
+ const all = (listing ?? "").split("\0").filter(Boolean);
6929
+ if (all.length > 0 && all.length <= MAX_LS_FILES) {
6930
+ const stems = /* @__PURE__ */ new Map();
6931
+ const literals = [];
6932
+ for (const t2 of terms) {
6933
+ if (/[._-]/.test(t2)) literals.push(t2);
6934
+ else if (!stems.has(stem(t2))) stems.set(stem(t2), t2);
6935
+ }
6936
+ const words = all.map(pathWords);
6937
+ const lowered = literals.length > 0 ? all.map((p2) => p2.toLowerCase()) : [];
6938
+ const idf = /* @__PURE__ */ new Map();
6939
+ const wording = /* @__PURE__ */ new Map();
6940
+ const idfOf = (key, df) => {
6941
+ if (df > 0 && df < all.length) idf.set(key, Math.log(1 + all.length / df));
6942
+ };
6943
+ for (const [s2, original] of stems) {
6944
+ wording.set(s2, original);
6945
+ idfOf(s2, words.reduce((n2, w2) => w2.all.has(s2) ? n2 + 1 : n2, 0));
6946
+ }
6947
+ for (const lit of literals) {
6948
+ wording.set(lit, lit);
6949
+ idfOf(lit, lowered.reduce((n2, p2) => p2.includes(lit) ? n2 + 1 : n2, 0));
6950
+ }
6951
+ if (idf.size > 0) {
6952
+ const scored = [];
6953
+ for (let i2 = 0; i2 < all.length; i2++) {
6954
+ let score = 0;
6955
+ const matched = [];
6956
+ for (const [key, weight] of idf) {
6957
+ const isLiteral = literals.includes(key);
6958
+ if (isLiteral ? lowered[i2].includes(key) : words[i2].base.has(key)) score += weight * 2;
6959
+ else if (!isLiteral && words[i2].all.has(key)) score += weight;
6960
+ else continue;
6961
+ matched.push(wording.get(key));
6962
+ }
6963
+ if (score > 0) scored.push({ path: all[i2], score, matched });
6964
+ }
6965
+ scored.sort((a2, b2) => b2.score - a2.score);
6966
+ let taken = 0;
6967
+ for (const s2 of scored) {
6968
+ if (taken >= MAX_NAME_MATCHES) break;
6969
+ if (isSecretLike(s2.path) || !existsSync7(join6(workspace, s2.path))) continue;
6970
+ const f2 = touch(s2.path);
6971
+ f2.matchScore += s2.score;
6972
+ if (isDirty2(s2.path)) f2.dirty = true;
6973
+ note(f2, `path matches ${s2.matched.map((m3) => `"${m3}"`).join(", ")}`);
6974
+ taken++;
6975
+ }
6976
+ }
6977
+ }
6978
+ }
6825
6979
  if (ripgrepAvailable && terms.length > 0) {
6826
6980
  const hits = await searchTerms(workspace, terms);
6827
6981
  retrievalOpsRun++;
6828
6982
  for (const hit of hits) {
6983
+ const file = hit.file.replace(/\\/g, "/").replace(/^\.\//, "");
6984
+ if (isSecretLike(file)) continue;
6829
6985
  const term = terms.find((t2) => hit.text.toLowerCase().includes(t2));
6830
- add(hit.file, term ? `matched "${term}"` : "matched search terms", 2);
6986
+ const f2 = touch(file);
6987
+ f2.matchScore += 1;
6988
+ if (isDirty2(file)) f2.dirty = true;
6989
+ note(f2, term ? `matched "${term}"` : "matched search terms");
6831
6990
  }
6832
6991
  }
6833
- const logOut = await git(workspace, "log", "-n", "15", "--name-only", "--pretty=format:");
6992
+ const logOut = await git(workspace, "-c", "core.quotepath=off", "log", "-n", "15", "--name-only", "--relative", "--pretty=format:");
6834
6993
  retrievalOpsRun++;
6835
6994
  if (logOut !== null) {
6836
- const recentFiles = [...new Set(logOut.split("\n").map((l2) => l2.trim()).filter(Boolean))];
6837
- for (const f2 of recentFiles.slice(0, 10)) add(f2, "recently changed", 1);
6995
+ const recent = [...new Set(logOut.split("\n").map((l2) => l2.trim()).filter((l2) => l2 && !l2.startsWith('"')))];
6996
+ let taken = 0;
6997
+ for (const p2 of recent) {
6998
+ if (taken >= MAX_RECENT) break;
6999
+ if (isSecretLike(p2) || isNoise(p2) || !existsSync7(join6(workspace, p2))) continue;
7000
+ note(touch(p2), "recently changed");
7001
+ taken++;
7002
+ }
6838
7003
  }
6839
- return { findings: [...byPath.values()], retrievalOpsRun, ripgrepAvailable, gitState };
7004
+ return { findings: [...byPath.values()].filter((f2) => f2.reasons.length > 0), retrievalOpsRun, ripgrepAvailable, gitState };
6840
7005
  }
6841
7006
  function formatBlock(prompt, rankedFindings, gitState, maxChars) {
6842
7007
  const header = `TASK
@@ -6866,7 +7031,7 @@ OFFHAND REPOSITORY CONTEXT
6866
7031
  // ../daemon/src/session-manager.ts
6867
7032
  function resolveDaemonVersion() {
6868
7033
  try {
6869
- const pkgPath = join6(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json");
7034
+ const pkgPath = join7(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json");
6870
7035
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
6871
7036
  return pkg.version ?? "0.0.0";
6872
7037
  } catch {
@@ -7211,7 +7376,7 @@ var SessionManager = class {
7211
7376
  case "fs-create-folder": {
7212
7377
  try {
7213
7378
  const safeName = safeDropName(msg.name);
7214
- mkdirSync2(join6(resolve3(msg.path), safeName), { recursive: false });
7379
+ mkdirSync2(join7(resolve3(msg.path), safeName), { recursive: false });
7215
7380
  } catch (e) {
7216
7381
  reply({
7217
7382
  type: "fs-response",
@@ -7352,9 +7517,9 @@ var SessionManager = class {
7352
7517
  let out = prompt + "\n\nAttached files (read them from disk):";
7353
7518
  for (const a2 of attachments) {
7354
7519
  const bytes = await this.attachmentFetcher(a2.blobId);
7355
- const dir = join6(tmpdir(), "offhand-attachments");
7520
+ const dir = join7(tmpdir(), "offhand-attachments");
7356
7521
  mkdirSync2(dir, { recursive: true });
7357
- const path = join6(dir, `${a2.blobId.slice(0, 8)}-${basename3(a2.name)}`);
7522
+ const path = join7(dir, `${a2.blobId.slice(0, 8)}-${basename3(a2.name)}`);
7358
7523
  writeFileSync2(path, bytes);
7359
7524
  out += `
7360
7525
  - ${path} (${a2.mime})`;
@@ -7574,17 +7739,17 @@ function listFolders(path) {
7574
7739
  if (!path) return { path: "", parent: null, dirs: driveRoots() };
7575
7740
  const current = resolve3(path);
7576
7741
  const dirs = safeReadDir2(current).filter((entry) => entry.isDirectory() && !skipDir(entry.name)).map((entry) => {
7577
- const full = join6(current, entry.name);
7578
- return { name: entry.name, path: full, isGit: existsSync7(join6(full, ".git")) };
7742
+ const full = join7(current, entry.name);
7743
+ return { name: entry.name, path: full, isGit: existsSync8(join7(full, ".git")) };
7579
7744
  }).sort((a2, b2) => Number(b2.isGit) - Number(a2.isGit) || a2.name.localeCompare(b2.name)).slice(0, 200);
7580
7745
  return { path: current, parent: isRoot(current) ? null : dirname3(current), dirs };
7581
7746
  }
7582
7747
  function driveRoots() {
7583
- if (process.platform !== "win32") return [{ name: "/", path: "/", isGit: existsSync7("/.git") }];
7748
+ if (process.platform !== "win32") return [{ name: "/", path: "/", isGit: existsSync8("/.git") }];
7584
7749
  const roots = [];
7585
7750
  for (let code = 67; code <= 90; code++) {
7586
7751
  const name = `${String.fromCharCode(code)}:\\`;
7587
- if (existsSync7(name)) roots.push({ name, path: name, isGit: existsSync7(join6(name, ".git")) });
7752
+ if (existsSync8(name)) roots.push({ name, path: name, isGit: existsSync8(join7(name, ".git")) });
7588
7753
  }
7589
7754
  return roots.sort((a2, b2) => a2.name.localeCompare(b2.name)).slice(0, 200);
7590
7755
  }
@@ -7618,13 +7783,13 @@ function stripTrailing(path) {
7618
7783
  import { DatabaseSync } from "node:sqlite";
7619
7784
  import { mkdirSync as mkdirSync3 } from "node:fs";
7620
7785
  import { homedir as homedir6 } from "node:os";
7621
- import { join as join7, basename as basename4 } from "node:path";
7786
+ import { join as join8, basename as basename4 } from "node:path";
7622
7787
  import { randomUUID as randomUUID2 } from "node:crypto";
7623
7788
  var Store = class {
7624
7789
  db;
7625
- constructor(dir = process.env.OFFHAND_HOME ?? join7(homedir6(), ".offhand")) {
7790
+ constructor(dir = process.env.OFFHAND_HOME ?? join8(homedir6(), ".offhand")) {
7626
7791
  mkdirSync3(dir, { recursive: true });
7627
- this.db = new DatabaseSync(join7(dir, "offhand.db"));
7792
+ this.db = new DatabaseSync(join8(dir, "offhand.db"));
7628
7793
  this.db.exec(`
7629
7794
  PRAGMA journal_mode = WAL;
7630
7795
  CREATE TABLE IF NOT EXISTS sessions (
@@ -43350,17 +43515,17 @@ var RelayClient = class {
43350
43515
  };
43351
43516
 
43352
43517
  // ../daemon/src/pairing.ts
43353
- import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
43518
+ import { existsSync as existsSync9, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
43354
43519
  import { homedir as homedir7 } from "node:os";
43355
- import { join as join8 } from "node:path";
43520
+ import { join as join9 } from "node:path";
43356
43521
  import { randomInt } from "node:crypto";
43357
- var PAIRING_DIR = process.env.OFFHAND_HOME ?? join8(homedir7(), ".offhand");
43358
- var PAIRING_PATH = join8(PAIRING_DIR, "pairing.json");
43522
+ var PAIRING_DIR = process.env.OFFHAND_HOME ?? join9(homedir7(), ".offhand");
43523
+ var PAIRING_PATH = join9(PAIRING_DIR, "pairing.json");
43359
43524
  var POLL_MS = 2e3;
43360
43525
  var POLL_TIMEOUT_MS = 10 * 60 * 1e3;
43361
43526
  async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://offhand-web.onrender.com") {
43362
43527
  await ready;
43363
- if (!forceNew && existsSync8(PAIRING_PATH)) {
43528
+ if (!forceNew && existsSync9(PAIRING_PATH)) {
43364
43529
  const f2 = JSON.parse(readFileSync4(PAIRING_PATH, "utf8"));
43365
43530
  const kp2 = { publicKey: fromB64u(f2.daemonPublicKey), secretKey: fromB64u(f2.daemonSecretKey) };
43366
43531
  const phonePk = fromB64u(f2.phonePublicKey);
@@ -43433,7 +43598,7 @@ async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://off
43433
43598
  // ../daemon/src/approvals.ts
43434
43599
  import { randomUUID as randomUUID3 } from "node:crypto";
43435
43600
  import { tmpdir as tmpdir2 } from "node:os";
43436
- import { join as join9 } from "node:path";
43601
+ import { join as join10 } from "node:path";
43437
43602
  var ApprovalBroker = class {
43438
43603
  constructor(timeoutMs) {
43439
43604
  this.timeoutMs = timeoutMs;
@@ -43457,7 +43622,7 @@ var ApprovalBroker = class {
43457
43622
  }
43458
43623
  const risk = classifyRisk(toolName, input);
43459
43624
  const filePath = input?.file_path;
43460
- if (toolName === "Read" && typeof filePath === "string" && filePath.toLowerCase().startsWith(join9(tmpdir2(), "offhand-attachments").toLowerCase())) {
43625
+ if (toolName === "Read" && typeof filePath === "string" && filePath.toLowerCase().startsWith(join10(tmpdir2(), "offhand-attachments").toLowerCase())) {
43461
43626
  return Promise.resolve({ approve: true });
43462
43627
  }
43463
43628
  if (this.policyProvider() === "trusting" && risk === "low" && toolName !== "AskUserQuestion") {
@@ -43610,27 +43775,27 @@ async function downloadArtifact(relayUrl2, sessionId, blobId, keys) {
43610
43775
 
43611
43776
  // ../daemon/src/autostart.ts
43612
43777
  import { homedir as homedir8 } from "node:os";
43613
- import { join as join10, resolve as resolve4, dirname as dirname4 } from "node:path";
43614
- import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, rmSync } from "node:fs";
43778
+ import { join as join11, resolve as resolve4, dirname as dirname4 } from "node:path";
43779
+ import { existsSync as existsSync10, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, rmSync } from "node:fs";
43615
43780
  import { fileURLToPath as fileURLToPath4 } from "node:url";
43616
43781
  import { spawnSync } from "node:child_process";
43617
43782
  import { tmpdir as tmpdir3 } from "node:os";
43618
43783
  var __dirname2 = dirname4(fileURLToPath4(import.meta.url));
43619
43784
  var SHORTCUT_NAME = "OffhandDaemon.lnk";
43620
- var OFFHAND_HOME = process.env.OFFHAND_HOME ?? join10(homedir8(), ".offhand");
43785
+ var OFFHAND_HOME = process.env.OFFHAND_HOME ?? join11(homedir8(), ".offhand");
43621
43786
  function getStartupDir() {
43622
43787
  if (process.platform !== "win32") {
43623
- return join10(homedir8(), ".config", "autostart");
43788
+ return join11(homedir8(), ".config", "autostart");
43624
43789
  }
43625
- return join10(process.env.APPDATA ?? "", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
43790
+ return join11(process.env.APPDATA ?? "", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
43626
43791
  }
43627
43792
  function getWrapperPath() {
43628
- const wrapperDir = join10(OFFHAND_HOME, "autostart");
43793
+ const wrapperDir = join11(OFFHAND_HOME, "autostart");
43629
43794
  mkdirSync5(wrapperDir, { recursive: true });
43630
- return join10(wrapperDir, "start-daemon.bat");
43795
+ return join11(wrapperDir, "start-daemon.bat");
43631
43796
  }
43632
43797
  function getShortcutPath() {
43633
- return join10(getStartupDir(), SHORTCUT_NAME);
43798
+ return join11(getStartupDir(), SHORTCUT_NAME);
43634
43799
  }
43635
43800
  function buildWrapperScript(config2, entry = process.argv[1] ?? "", execPath = process.execPath) {
43636
43801
  const wsArgs2 = config2.workspaces.map((w2) => `--workspace "${w2}"`).join(" ");
@@ -43659,7 +43824,7 @@ $Shortcut.Arguments = "/c ${targetPath}"
43659
43824
  $Shortcut.WorkingDirectory = "${resolve4(__dirname2, "..", "..")}"
43660
43825
  $Shortcut.Description = "Offhand Daemon - Auto-start on login"
43661
43826
  $Shortcut.Save()`;
43662
- const scriptPath = join10(tmpdir3(), "offhand-create-shortcut.ps1");
43827
+ const scriptPath = join11(tmpdir3(), "offhand-create-shortcut.ps1");
43663
43828
  writeFileSync4(scriptPath, psScript);
43664
43829
  const result = spawnSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath], { encoding: "utf8", shell: true });
43665
43830
  try {
@@ -43669,7 +43834,7 @@ $Shortcut.Save()`;
43669
43834
  return { success: result.status === 0, output: result.stdout + result.stderr };
43670
43835
  }
43671
43836
  function checkShortcutExists(shortcutPath) {
43672
- return existsSync9(shortcutPath);
43837
+ return existsSync10(shortcutPath);
43673
43838
  }
43674
43839
  function installAutostart(config2) {
43675
43840
  const wrapperPath = getWrapperPath();
@@ -43689,7 +43854,7 @@ function getAutostartStatus() {
43689
43854
  const shortcutPath = getShortcutPath();
43690
43855
  const installed = checkShortcutExists(shortcutPath);
43691
43856
  const wrapperPath = getWrapperPath();
43692
- const wrapperExists = existsSync9(wrapperPath);
43857
+ const wrapperExists = existsSync10(wrapperPath);
43693
43858
  let details = `Shortcut: ${shortcutPath} (${installed ? "EXISTS" : "MISSING"})`;
43694
43859
  if (wrapperExists) {
43695
43860
  details += `
@@ -43698,7 +43863,7 @@ Wrapper: ${wrapperPath} (EXISTS)`;
43698
43863
  return { installed, details };
43699
43864
  }
43700
43865
  function saveConfig(config2) {
43701
- const configPath = join10(OFFHAND_HOME, "autostart.json");
43866
+ const configPath = join11(OFFHAND_HOME, "autostart.json");
43702
43867
  writeFileSync4(configPath, JSON.stringify(config2, null, 2));
43703
43868
  }
43704
43869
  function getConfigFromStore() {
@@ -43746,7 +43911,7 @@ var devUrl = argValue("--dev-url");
43746
43911
  var store = new Store();
43747
43912
  var wsArgs = argValues("--workspace").map((w2) => resolve5(w2));
43748
43913
  for (const w2 of wsArgs) {
43749
- if (!existsSync10(w2)) {
43914
+ if (!existsSync11(w2)) {
43750
43915
  console.error(`workspace does not exist: ${w2}`);
43751
43916
  process.exit(1);
43752
43917
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offhands",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
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",
@@ -23,4 +23,4 @@
23
23
  "devDependencies": {
24
24
  "esbuild": "^0.24.0"
25
25
  }
26
- }
26
+ }