offhands 0.1.15 → 0.1.16

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 +67 -18
  2. package/package.json +1 -1
package/dist/daemon.mjs CHANGED
@@ -6831,6 +6831,12 @@ function extractTerms(prompt, maxTerms = 8) {
6831
6831
  }
6832
6832
  return deduped.slice().sort((a2, b2) => b2.length - a2.length).slice(0, maxTerms);
6833
6833
  }
6834
+ var JUNK_EXTS = [".stackdump", ".log", ".tmp", ".bak", ".orig", ".rej", ".swp", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".mp4", ".mov", ".zip", ".tgz"];
6835
+ var JUNK_BASENAMES = /* @__PURE__ */ new Set([".ds_store", "thumbs.db"]);
6836
+ function isJunk(path) {
6837
+ const base = (path.split(/[\\/]/).pop() ?? "").toLowerCase();
6838
+ return JUNK_BASENAMES.has(base) || base.endsWith("~") || JUNK_EXTS.some((e) => base.endsWith(e));
6839
+ }
6834
6840
  var SECRET_BASENAMES = /* @__PURE__ */ new Set([".npmrc", ".netrc", "id_rsa", "id_dsa", "id_ecdsa", "id_ed25519", "credentials.json", "secrets.json"]);
6835
6841
  var SECRET_EXTS = [".pem", ".key", ".p12", ".pfx", ".keystore", ".jks", ".env"];
6836
6842
  var ENV_TEMPLATE_SUFFIXES = [".example", ".sample", ".template", ".dist"];
@@ -6842,16 +6848,16 @@ function isSecretLike(path) {
6842
6848
  }
6843
6849
  function parsePorcelainZ(out) {
6844
6850
  const parts = out.split("\0");
6845
- const paths = [];
6851
+ const entries = [];
6846
6852
  for (let i2 = 0; i2 < parts.length; i2++) {
6847
6853
  const p2 = parts[i2];
6848
6854
  if (p2.length < 4) continue;
6849
6855
  const x2 = p2[0];
6850
6856
  const y2 = p2[1];
6851
6857
  if (x2 === "R" || x2 === "C" || y2 === "R" || y2 === "C") i2++;
6852
- paths.push(p2.slice(3));
6858
+ entries.push({ path: p2.slice(3), untracked: x2 === "?" && y2 === "?" });
6853
6859
  }
6854
- return paths;
6860
+ return entries;
6855
6861
  }
6856
6862
  function isDirectory(abs) {
6857
6863
  try {
@@ -6876,15 +6882,20 @@ function stem(word) {
6876
6882
  function pathWords(path) {
6877
6883
  const split = (s2) => s2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean).map(stem);
6878
6884
  const slash = path.lastIndexOf("/");
6879
- const base = new Set(split(path.slice(slash + 1)));
6885
+ const file = path.slice(slash + 1);
6886
+ const base = new Set(split(file));
6880
6887
  const all = /* @__PURE__ */ new Set([...split(path.slice(0, Math.max(slash, 0))), ...base]);
6881
- return { all, base };
6888
+ const nameWords = split(file.replace(/\.[^.]+$/, ""));
6889
+ return { all, base, last: nameWords[nameWords.length - 1] ?? "" };
6882
6890
  }
6883
6891
  var NOISE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb", "bun.lock", "composer.lock", "cargo.lock", "poetry.lock"]);
6884
6892
  var isNoise = (p2) => NOISE_FILES.has((p2.split("/").pop() ?? "").toLowerCase());
6885
6893
  var MAX_MATCHED_SHOWN = 14;
6886
6894
  var MAX_CONTENT_PROBES = 10;
6887
6895
  var MAX_CONTENT_MATCHES = 6;
6896
+ var COVERAGE_BONUS = 0.3;
6897
+ var ROLE_WORD_FACTOR = 0.4;
6898
+ var MIN_SOLID_TO_DROP_WEAK = 5;
6888
6899
  var MAX_UNMATCHED_WITH_MATCHES = 3;
6889
6900
  var MAX_NAME_MATCHES = 10;
6890
6901
  var MAX_DIRTY_ONLY = 8;
@@ -6915,14 +6926,23 @@ async function buildContext(workspace, prompt, opts = {}) {
6915
6926
  const { findings, retrievalOpsRun, ripgrepAvailable, gitState } = result;
6916
6927
  if (findings.length === 0) return null;
6917
6928
  const rank = (f2) => f2.matchScore > 0 ? f2.dirty ? 5 : 4 : f2.dirty ? 2 : 1;
6918
- findings.sort((a2, b2) => rank(b2) - rank(a2) || b2.matchScore - a2.matchScore);
6929
+ const strength = (f2) => f2.matchScore * (1 + COVERAGE_BONUS * Math.max(f2.terms.size - 1, 0));
6930
+ findings.sort((a2, b2) => rank(b2) - rank(a2) || strength(b2) - strength(a2));
6919
6931
  const strongMatches = findings.filter((f2) => f2.matchScore > 0).length;
6920
6932
  const unmatchedCap = strongMatches >= 3 ? MAX_UNMATCHED_WITH_MATCHES : Infinity;
6933
+ const solidCount = findings.filter((f2) => f2.solid > 0).length;
6934
+ const droppingWeak = solidCount >= MIN_SOLID_TO_DROP_WEAK;
6935
+ const droppingUntracked = strongMatches >= 3;
6921
6936
  let unmatched = 0;
6922
6937
  let matchedShown = 0;
6923
- const shown = findings.filter(
6924
- (f2) => f2.matchScore > 0 ? ++matchedShown <= MAX_MATCHED_SHOWN : ++unmatched <= unmatchedCap
6925
- );
6938
+ const shown = findings.filter((f2) => {
6939
+ if (f2.matchScore > 0) {
6940
+ if (droppingWeak && f2.solid === 0) return false;
6941
+ return ++matchedShown <= MAX_MATCHED_SHOWN;
6942
+ }
6943
+ if (droppingUntracked && f2.untracked) return false;
6944
+ return ++unmatched <= unmatchedCap;
6945
+ });
6926
6946
  const { text: block, selectedCount } = formatBlock(prompt, shown, gitState, maxChars);
6927
6947
  if (selectedCount === 0) return null;
6928
6948
  return {
@@ -6945,7 +6965,7 @@ async function gatherFindings(workspace, prompt) {
6945
6965
  const touch = (path) => {
6946
6966
  let f2 = byPath.get(path);
6947
6967
  if (!f2) {
6948
- f2 = { path, reasons: [], dirty: false, matchScore: 0 };
6968
+ f2 = { path, reasons: [], dirty: false, matchScore: 0, terms: /* @__PURE__ */ new Set(), solid: 0, untracked: false };
6949
6969
  byPath.set(path, f2);
6950
6970
  }
6951
6971
  return f2;
@@ -6973,7 +6993,8 @@ async function gatherFindings(workspace, prompt) {
6973
6993
  if (statusOut !== null) {
6974
6994
  const entries = parsePorcelainZ(statusOut);
6975
6995
  let missing = 0;
6976
- for (const p2 of entries) {
6996
+ const untrackedSet = /* @__PURE__ */ new Set();
6997
+ for (const { path: p2, untracked } of entries) {
6977
6998
  const rel = liveRel(p2);
6978
6999
  if (rel === null) {
6979
7000
  missing++;
@@ -6981,14 +7002,22 @@ async function gatherFindings(workspace, prompt) {
6981
7002
  }
6982
7003
  if (isSecretLike(rel)) continue;
6983
7004
  if (rel.endsWith("/") || isDirectory(join6(gitRoot, p2))) dirtyDirs.push(rel.replace(/\/?$/, "/"));
6984
- else dirtySet.add(rel);
7005
+ else {
7006
+ dirtySet.add(rel);
7007
+ if (untracked) untrackedSet.add(rel);
7008
+ }
6985
7009
  }
7010
+ const dirtyOrder = [
7011
+ ...[...dirtySet].filter((r2) => !untrackedSet.has(r2)),
7012
+ ...[...dirtySet].filter((r2) => untrackedSet.has(r2) && !isJunk(r2))
7013
+ ];
6986
7014
  let dirtyOnly = 0;
6987
- for (const rel of dirtySet) {
7015
+ for (const rel of dirtyOrder) {
6988
7016
  if (dirtyOnly >= MAX_DIRTY_ONLY) break;
6989
7017
  if (isNoise(rel)) continue;
6990
7018
  const f2 = touch(rel);
6991
7019
  f2.dirty = true;
7020
+ f2.untracked = untrackedSet.has(rel);
6992
7021
  note(f2, "currently dirty");
6993
7022
  dirtyOnly++;
6994
7023
  }
@@ -7011,6 +7040,14 @@ async function gatherFindings(workspace, prompt) {
7011
7040
  else if (!stems.has(stem(t2))) stems.set(stem(t2), t2);
7012
7041
  }
7013
7042
  const words = all.map(pathWords);
7043
+ const lastByDir = /* @__PURE__ */ new Map();
7044
+ words.forEach((w2, i2) => {
7045
+ if (!w2.last) return;
7046
+ const key = `${all[i2].slice(0, all[i2].lastIndexOf("/") + 1)}\0${w2.last}`;
7047
+ lastByDir.set(key, (lastByDir.get(key) ?? 0) + 1);
7048
+ });
7049
+ const roleWords = /* @__PURE__ */ new Set();
7050
+ for (const [key, n2] of lastByDir) if (n2 >= 2) roleWords.add(key.slice(key.indexOf("\0") + 1));
7014
7051
  const lowered = literals.length > 0 ? all.map((p2) => p2.toLowerCase()) : [];
7015
7052
  const idf = /* @__PURE__ */ new Map();
7016
7053
  const wording = /* @__PURE__ */ new Map();
@@ -7029,15 +7066,21 @@ async function gatherFindings(workspace, prompt) {
7029
7066
  const scored = [];
7030
7067
  for (let i2 = 0; i2 < all.length; i2++) {
7031
7068
  let score = 0;
7069
+ let solid = 0;
7032
7070
  const matched = [];
7033
7071
  for (const [key, weight] of idf) {
7034
7072
  const isLiteral = literals.includes(key);
7035
- if (isLiteral ? lowered[i2].includes(key) : words[i2].base.has(key)) score += weight * 2;
7036
- else if (!isLiteral && words[i2].all.has(key)) score += weight;
7037
- else continue;
7073
+ if (isLiteral ? lowered[i2].includes(key) : words[i2].base.has(key)) {
7074
+ const roleOnly = !isLiteral && roleWords.has(key) && words[i2].last === key;
7075
+ score += weight * 2 * (roleOnly ? ROLE_WORD_FACTOR : 1);
7076
+ if (!roleOnly) solid += weight * 2;
7077
+ } else if (!isLiteral && words[i2].all.has(key)) {
7078
+ score += weight;
7079
+ solid += weight;
7080
+ } else continue;
7038
7081
  matched.push(wording.get(key));
7039
7082
  }
7040
- if (score > 0) scored.push({ path: all[i2], score, matched });
7083
+ if (score > 0) scored.push({ path: all[i2], score, solid, matched });
7041
7084
  }
7042
7085
  scored.sort((a2, b2) => b2.score - a2.score);
7043
7086
  let taken = 0;
@@ -7046,6 +7089,8 @@ async function gatherFindings(workspace, prompt) {
7046
7089
  if (isSecretLike(s2.path) || !existsSync7(join6(workspace, s2.path))) continue;
7047
7090
  const f2 = touch(s2.path);
7048
7091
  f2.matchScore += s2.score;
7092
+ f2.solid += s2.solid;
7093
+ for (const m3 of s2.matched) f2.terms.add(stem(m3));
7049
7094
  if (isDirty2(s2.path)) f2.dirty = true;
7050
7095
  note(f2, `path matches ${s2.matched.map((m3) => `"${m3}"`).join(", ")}`);
7051
7096
  taken++;
@@ -7085,6 +7130,8 @@ async function gatherFindings(workspace, prompt) {
7085
7130
  if (isSecretLike(file) || !existsSync7(join6(workspace, file))) continue;
7086
7131
  const f2 = touch(file);
7087
7132
  f2.matchScore += score;
7133
+ f2.solid += score;
7134
+ for (const w2 of words) f2.terms.add(stem(w2));
7088
7135
  if (isDirty2(file)) f2.dirty = true;
7089
7136
  note(f2, `content mentions ${words.slice(0, 3).map((w2) => `"${w2}"`).join(", ")}`);
7090
7137
  taken++;
@@ -7099,6 +7146,8 @@ async function gatherFindings(workspace, prompt) {
7099
7146
  const term = terms.find((t2) => hit.text.toLowerCase().includes(t2));
7100
7147
  const f2 = touch(file);
7101
7148
  f2.matchScore += 1;
7149
+ f2.solid += 1;
7150
+ if (term) f2.terms.add(stem(term));
7102
7151
  if (isDirty2(file)) f2.dirty = true;
7103
7152
  note(f2, term ? `matched "${term}"` : "matched search terms");
7104
7153
  }
@@ -7110,7 +7159,7 @@ async function gatherFindings(workspace, prompt) {
7110
7159
  let taken = 0;
7111
7160
  for (const p2 of recent) {
7112
7161
  if (taken >= MAX_RECENT) break;
7113
- if (isSecretLike(p2) || isNoise(p2) || !existsSync7(join6(workspace, p2))) continue;
7162
+ if (isSecretLike(p2) || isNoise(p2) || isJunk(p2) || !existsSync7(join6(workspace, p2))) continue;
7114
7163
  note(touch(p2), "recently changed");
7115
7164
  taken++;
7116
7165
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offhands",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
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",