offhands 0.1.10 → 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 +213 -53
  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,61 @@ 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_NAME_MATCHES = 10;
6817
+ var MAX_DIRTY_ONLY = 8;
6818
+ var MAX_RECENT = 6;
6819
+ var MAX_LS_FILES = 6e4;
6761
6820
  async function buildContext(workspace, prompt, opts = {}) {
6762
6821
  const startedAt = Date.now();
6763
6822
  const maxChars = opts.maxChars ?? (Number(process.env.OFFHAND_INTEL_MAX_CHARS) || DEFAULT_MAX_CHARS);
@@ -6782,7 +6841,8 @@ async function buildContext(workspace, prompt, opts = {}) {
6782
6841
  }
6783
6842
  const { findings, retrievalOpsRun, ripgrepAvailable, gitState } = result;
6784
6843
  if (findings.length === 0) return null;
6785
- 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);
6786
6846
  const { text: block, selectedCount } = formatBlock(prompt, findings, gitState, maxChars);
6787
6847
  if (selectedCount === 0) return null;
6788
6848
  return {
@@ -6802,41 +6862,141 @@ async function buildContext(workspace, prompt, opts = {}) {
6802
6862
  async function gatherFindings(workspace, prompt) {
6803
6863
  let retrievalOpsRun = 0;
6804
6864
  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 });
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);
6812
6870
  }
6871
+ return f2;
6872
+ };
6873
+ const note = (f2, reason) => {
6874
+ if (!f2.reasons.includes(reason)) f2.reasons.push(reason);
6813
6875
  };
6814
- 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");
6815
6888
  retrievalOpsRun++;
6816
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));
6817
6893
  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";
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`;
6821
6916
  }
6822
6917
  const ripgrepAvailable = await detectRipgrep();
6823
6918
  retrievalOpsRun++;
6824
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
+ }
6825
6974
  if (ripgrepAvailable && terms.length > 0) {
6826
6975
  const hits = await searchTerms(workspace, terms);
6827
6976
  retrievalOpsRun++;
6828
6977
  for (const hit of hits) {
6978
+ const file = hit.file.replace(/\\/g, "/").replace(/^\.\//, "");
6979
+ if (isSecretLike(file)) continue;
6829
6980
  const term = terms.find((t2) => hit.text.toLowerCase().includes(t2));
6830
- 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");
6831
6985
  }
6832
6986
  }
6833
- 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:");
6834
6988
  retrievalOpsRun++;
6835
6989
  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);
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
+ }
6838
6998
  }
6839
- return { findings: [...byPath.values()], retrievalOpsRun, ripgrepAvailable, gitState };
6999
+ return { findings: [...byPath.values()].filter((f2) => f2.reasons.length > 0), retrievalOpsRun, ripgrepAvailable, gitState };
6840
7000
  }
6841
7001
  function formatBlock(prompt, rankedFindings, gitState, maxChars) {
6842
7002
  const header = `TASK
@@ -6866,7 +7026,7 @@ OFFHAND REPOSITORY CONTEXT
6866
7026
  // ../daemon/src/session-manager.ts
6867
7027
  function resolveDaemonVersion() {
6868
7028
  try {
6869
- const pkgPath = join6(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json");
7029
+ const pkgPath = join7(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json");
6870
7030
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
6871
7031
  return pkg.version ?? "0.0.0";
6872
7032
  } catch {
@@ -7211,7 +7371,7 @@ var SessionManager = class {
7211
7371
  case "fs-create-folder": {
7212
7372
  try {
7213
7373
  const safeName = safeDropName(msg.name);
7214
- mkdirSync2(join6(resolve3(msg.path), safeName), { recursive: false });
7374
+ mkdirSync2(join7(resolve3(msg.path), safeName), { recursive: false });
7215
7375
  } catch (e) {
7216
7376
  reply({
7217
7377
  type: "fs-response",
@@ -7352,9 +7512,9 @@ var SessionManager = class {
7352
7512
  let out = prompt + "\n\nAttached files (read them from disk):";
7353
7513
  for (const a2 of attachments) {
7354
7514
  const bytes = await this.attachmentFetcher(a2.blobId);
7355
- const dir = join6(tmpdir(), "offhand-attachments");
7515
+ const dir = join7(tmpdir(), "offhand-attachments");
7356
7516
  mkdirSync2(dir, { recursive: true });
7357
- 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)}`);
7358
7518
  writeFileSync2(path, bytes);
7359
7519
  out += `
7360
7520
  - ${path} (${a2.mime})`;
@@ -7574,17 +7734,17 @@ function listFolders(path) {
7574
7734
  if (!path) return { path: "", parent: null, dirs: driveRoots() };
7575
7735
  const current = resolve3(path);
7576
7736
  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")) };
7737
+ const full = join7(current, entry.name);
7738
+ return { name: entry.name, path: full, isGit: existsSync8(join7(full, ".git")) };
7579
7739
  }).sort((a2, b2) => Number(b2.isGit) - Number(a2.isGit) || a2.name.localeCompare(b2.name)).slice(0, 200);
7580
7740
  return { path: current, parent: isRoot(current) ? null : dirname3(current), dirs };
7581
7741
  }
7582
7742
  function driveRoots() {
7583
- if (process.platform !== "win32") return [{ name: "/", path: "/", isGit: existsSync7("/.git") }];
7743
+ if (process.platform !== "win32") return [{ name: "/", path: "/", isGit: existsSync8("/.git") }];
7584
7744
  const roots = [];
7585
7745
  for (let code = 67; code <= 90; code++) {
7586
7746
  const name = `${String.fromCharCode(code)}:\\`;
7587
- 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")) });
7588
7748
  }
7589
7749
  return roots.sort((a2, b2) => a2.name.localeCompare(b2.name)).slice(0, 200);
7590
7750
  }
@@ -7618,13 +7778,13 @@ function stripTrailing(path) {
7618
7778
  import { DatabaseSync } from "node:sqlite";
7619
7779
  import { mkdirSync as mkdirSync3 } from "node:fs";
7620
7780
  import { homedir as homedir6 } from "node:os";
7621
- import { join as join7, basename as basename4 } from "node:path";
7781
+ import { join as join8, basename as basename4 } from "node:path";
7622
7782
  import { randomUUID as randomUUID2 } from "node:crypto";
7623
7783
  var Store = class {
7624
7784
  db;
7625
- constructor(dir = process.env.OFFHAND_HOME ?? join7(homedir6(), ".offhand")) {
7785
+ constructor(dir = process.env.OFFHAND_HOME ?? join8(homedir6(), ".offhand")) {
7626
7786
  mkdirSync3(dir, { recursive: true });
7627
- this.db = new DatabaseSync(join7(dir, "offhand.db"));
7787
+ this.db = new DatabaseSync(join8(dir, "offhand.db"));
7628
7788
  this.db.exec(`
7629
7789
  PRAGMA journal_mode = WAL;
7630
7790
  CREATE TABLE IF NOT EXISTS sessions (
@@ -43350,17 +43510,17 @@ var RelayClient = class {
43350
43510
  };
43351
43511
 
43352
43512
  // ../daemon/src/pairing.ts
43353
- 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";
43354
43514
  import { homedir as homedir7 } from "node:os";
43355
- import { join as join8 } from "node:path";
43515
+ import { join as join9 } from "node:path";
43356
43516
  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");
43517
+ var PAIRING_DIR = process.env.OFFHAND_HOME ?? join9(homedir7(), ".offhand");
43518
+ var PAIRING_PATH = join9(PAIRING_DIR, "pairing.json");
43359
43519
  var POLL_MS = 2e3;
43360
43520
  var POLL_TIMEOUT_MS = 10 * 60 * 1e3;
43361
43521
  async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://offhand-web.onrender.com") {
43362
43522
  await ready;
43363
- if (!forceNew && existsSync8(PAIRING_PATH)) {
43523
+ if (!forceNew && existsSync9(PAIRING_PATH)) {
43364
43524
  const f2 = JSON.parse(readFileSync4(PAIRING_PATH, "utf8"));
43365
43525
  const kp2 = { publicKey: fromB64u(f2.daemonPublicKey), secretKey: fromB64u(f2.daemonSecretKey) };
43366
43526
  const phonePk = fromB64u(f2.phonePublicKey);
@@ -43433,7 +43593,7 @@ async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://off
43433
43593
  // ../daemon/src/approvals.ts
43434
43594
  import { randomUUID as randomUUID3 } from "node:crypto";
43435
43595
  import { tmpdir as tmpdir2 } from "node:os";
43436
- import { join as join9 } from "node:path";
43596
+ import { join as join10 } from "node:path";
43437
43597
  var ApprovalBroker = class {
43438
43598
  constructor(timeoutMs) {
43439
43599
  this.timeoutMs = timeoutMs;
@@ -43457,7 +43617,7 @@ var ApprovalBroker = class {
43457
43617
  }
43458
43618
  const risk = classifyRisk(toolName, input);
43459
43619
  const filePath = input?.file_path;
43460
- 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())) {
43461
43621
  return Promise.resolve({ approve: true });
43462
43622
  }
43463
43623
  if (this.policyProvider() === "trusting" && risk === "low" && toolName !== "AskUserQuestion") {
@@ -43610,27 +43770,27 @@ async function downloadArtifact(relayUrl2, sessionId, blobId, keys) {
43610
43770
 
43611
43771
  // ../daemon/src/autostart.ts
43612
43772
  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";
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";
43615
43775
  import { fileURLToPath as fileURLToPath4 } from "node:url";
43616
43776
  import { spawnSync } from "node:child_process";
43617
43777
  import { tmpdir as tmpdir3 } from "node:os";
43618
43778
  var __dirname2 = dirname4(fileURLToPath4(import.meta.url));
43619
43779
  var SHORTCUT_NAME = "OffhandDaemon.lnk";
43620
- var OFFHAND_HOME = process.env.OFFHAND_HOME ?? join10(homedir8(), ".offhand");
43780
+ var OFFHAND_HOME = process.env.OFFHAND_HOME ?? join11(homedir8(), ".offhand");
43621
43781
  function getStartupDir() {
43622
43782
  if (process.platform !== "win32") {
43623
- return join10(homedir8(), ".config", "autostart");
43783
+ return join11(homedir8(), ".config", "autostart");
43624
43784
  }
43625
- return join10(process.env.APPDATA ?? "", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
43785
+ return join11(process.env.APPDATA ?? "", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
43626
43786
  }
43627
43787
  function getWrapperPath() {
43628
- const wrapperDir = join10(OFFHAND_HOME, "autostart");
43788
+ const wrapperDir = join11(OFFHAND_HOME, "autostart");
43629
43789
  mkdirSync5(wrapperDir, { recursive: true });
43630
- return join10(wrapperDir, "start-daemon.bat");
43790
+ return join11(wrapperDir, "start-daemon.bat");
43631
43791
  }
43632
43792
  function getShortcutPath() {
43633
- return join10(getStartupDir(), SHORTCUT_NAME);
43793
+ return join11(getStartupDir(), SHORTCUT_NAME);
43634
43794
  }
43635
43795
  function buildWrapperScript(config2, entry = process.argv[1] ?? "", execPath = process.execPath) {
43636
43796
  const wsArgs2 = config2.workspaces.map((w2) => `--workspace "${w2}"`).join(" ");
@@ -43659,7 +43819,7 @@ $Shortcut.Arguments = "/c ${targetPath}"
43659
43819
  $Shortcut.WorkingDirectory = "${resolve4(__dirname2, "..", "..")}"
43660
43820
  $Shortcut.Description = "Offhand Daemon - Auto-start on login"
43661
43821
  $Shortcut.Save()`;
43662
- const scriptPath = join10(tmpdir3(), "offhand-create-shortcut.ps1");
43822
+ const scriptPath = join11(tmpdir3(), "offhand-create-shortcut.ps1");
43663
43823
  writeFileSync4(scriptPath, psScript);
43664
43824
  const result = spawnSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath], { encoding: "utf8", shell: true });
43665
43825
  try {
@@ -43669,7 +43829,7 @@ $Shortcut.Save()`;
43669
43829
  return { success: result.status === 0, output: result.stdout + result.stderr };
43670
43830
  }
43671
43831
  function checkShortcutExists(shortcutPath) {
43672
- return existsSync9(shortcutPath);
43832
+ return existsSync10(shortcutPath);
43673
43833
  }
43674
43834
  function installAutostart(config2) {
43675
43835
  const wrapperPath = getWrapperPath();
@@ -43689,7 +43849,7 @@ function getAutostartStatus() {
43689
43849
  const shortcutPath = getShortcutPath();
43690
43850
  const installed = checkShortcutExists(shortcutPath);
43691
43851
  const wrapperPath = getWrapperPath();
43692
- const wrapperExists = existsSync9(wrapperPath);
43852
+ const wrapperExists = existsSync10(wrapperPath);
43693
43853
  let details = `Shortcut: ${shortcutPath} (${installed ? "EXISTS" : "MISSING"})`;
43694
43854
  if (wrapperExists) {
43695
43855
  details += `
@@ -43698,7 +43858,7 @@ Wrapper: ${wrapperPath} (EXISTS)`;
43698
43858
  return { installed, details };
43699
43859
  }
43700
43860
  function saveConfig(config2) {
43701
- const configPath = join10(OFFHAND_HOME, "autostart.json");
43861
+ const configPath = join11(OFFHAND_HOME, "autostart.json");
43702
43862
  writeFileSync4(configPath, JSON.stringify(config2, null, 2));
43703
43863
  }
43704
43864
  function getConfigFromStore() {
@@ -43746,7 +43906,7 @@ var devUrl = argValue("--dev-url");
43746
43906
  var store = new Store();
43747
43907
  var wsArgs = argValues("--workspace").map((w2) => resolve5(w2));
43748
43908
  for (const w2 of wsArgs) {
43749
- if (!existsSync10(w2)) {
43909
+ if (!existsSync11(w2)) {
43750
43910
  console.error(`workspace does not exist: ${w2}`);
43751
43911
  process.exit(1);
43752
43912
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offhands",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
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
+ }