offhands 0.1.12 → 0.1.13

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 +107 -4
  2. package/package.json +1 -1
package/dist/daemon.mjs CHANGED
@@ -6749,8 +6749,66 @@ var STOPWORDS = /* @__PURE__ */ new Set([
6749
6749
  "into",
6750
6750
  "if",
6751
6751
  "then",
6752
- "than"
6752
+ "than",
6753
+ // Instruction-shaped filler that says how to answer, not what to look at —
6754
+ // by length alone these outranked the task's real nouns ("requirements",
6755
+ // "mirroring" pushed "closing" out of the term list).
6756
+ "requirements",
6757
+ "requirement",
6758
+ "whatever",
6759
+ "existing",
6760
+ "which",
6761
+ "tell",
6762
+ "bullets",
6763
+ "bullet",
6764
+ "fewer",
6765
+ "five",
6766
+ "only",
6767
+ "why",
6768
+ "any",
6769
+ "following",
6770
+ "something",
6771
+ "thing",
6772
+ "things",
6773
+ "just",
6774
+ "about",
6775
+ "there",
6776
+ "their",
6777
+ "them",
6778
+ "they",
6779
+ "these",
6780
+ "those",
6781
+ "your",
6782
+ "back",
6783
+ "let",
6784
+ "lets",
6785
+ "read",
6786
+ "write",
6787
+ "show",
6788
+ "give",
6789
+ "explain",
6790
+ "describe",
6791
+ "files",
6792
+ "file",
6793
+ // Generic action verbs: in code these are everywhere (createX, deleteY), so
6794
+ // "do not create, edit, or delete any files" pulled in guildCreate.ts and
6795
+ // guildDelete.ts. The nouns in a prompt carry the signal; the verb rarely does.
6796
+ "create",
6797
+ "creates",
6798
+ "edit",
6799
+ "edits",
6800
+ "delete",
6801
+ "deletes",
6802
+ "change",
6803
+ "changes",
6804
+ "modify",
6805
+ "update",
6806
+ "updates",
6807
+ "implement",
6808
+ "implementation"
6753
6809
  ]);
6810
+ var MAX_TERMS = 24;
6811
+ var RIPGREP_TERMS = 8;
6754
6812
  function extractTerms(prompt, maxTerms = 8) {
6755
6813
  const raw = prompt.toLowerCase().split(/[^a-z0-9._-]+/).map((t2) => t2.replace(/^[._-]+|[._-]+$/g, "")).filter((t2) => t2.length >= 3 && !STOPWORDS.has(t2) && !/^\d+$/.test(t2));
6756
6814
  const seen = /* @__PURE__ */ new Set();
@@ -6813,6 +6871,9 @@ function pathWords(path) {
6813
6871
  }
6814
6872
  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
6873
  var isNoise = (p2) => NOISE_FILES.has((p2.split("/").pop() ?? "").toLowerCase());
6874
+ var MAX_MATCHED_SHOWN = 14;
6875
+ var MAX_CONTENT_PROBES = 10;
6876
+ var MAX_CONTENT_MATCHES = 6;
6816
6877
  var MAX_UNMATCHED_WITH_MATCHES = 3;
6817
6878
  var MAX_NAME_MATCHES = 10;
6818
6879
  var MAX_DIRTY_ONLY = 8;
@@ -6847,7 +6908,10 @@ async function buildContext(workspace, prompt, opts = {}) {
6847
6908
  const strongMatches = findings.filter((f2) => f2.matchScore > 0).length;
6848
6909
  const unmatchedCap = strongMatches >= 3 ? MAX_UNMATCHED_WITH_MATCHES : Infinity;
6849
6910
  let unmatched = 0;
6850
- const shown = findings.filter((f2) => f2.matchScore > 0 || ++unmatched <= unmatchedCap);
6911
+ let matchedShown = 0;
6912
+ const shown = findings.filter(
6913
+ (f2) => f2.matchScore > 0 ? ++matchedShown <= MAX_MATCHED_SHOWN : ++unmatched <= unmatchedCap
6914
+ );
6851
6915
  const { text: block, selectedCount } = formatBlock(prompt, shown, gitState, maxChars);
6852
6916
  if (selectedCount === 0) return null;
6853
6917
  return {
@@ -6921,11 +6985,13 @@ async function gatherFindings(workspace, prompt) {
6921
6985
  }
6922
6986
  const ripgrepAvailable = await detectRipgrep();
6923
6987
  retrievalOpsRun++;
6924
- const terms = extractTerms(prompt);
6988
+ const terms = extractTerms(prompt, MAX_TERMS);
6989
+ let listedFileCount = 0;
6925
6990
  if (terms.length > 0) {
6926
6991
  const listing = await git(workspace, "ls-files", "-co", "--exclude-standard", "-z");
6927
6992
  retrievalOpsRun++;
6928
6993
  const all = (listing ?? "").split("\0").filter(Boolean);
6994
+ listedFileCount = all.length;
6929
6995
  if (all.length > 0 && all.length <= MAX_LS_FILES) {
6930
6996
  const stems = /* @__PURE__ */ new Map();
6931
6997
  const literals = [];
@@ -6976,8 +7042,45 @@ async function gatherFindings(workspace, prompt) {
6976
7042
  }
6977
7043
  }
6978
7044
  }
7045
+ if (!ripgrepAvailable && terms.length > 0) {
7046
+ const probes = /* @__PURE__ */ new Map();
7047
+ for (const t2 of terms) {
7048
+ const s2 = stem(t2);
7049
+ if (s2.length >= 4 && !probes.has(s2)) probes.set(s2, t2);
7050
+ if (probes.size >= MAX_CONTENT_PROBES) break;
7051
+ }
7052
+ const results = await Promise.all(
7053
+ [...probes].map(async ([s2, wording]) => {
7054
+ const out = await git(workspace, "grep", "-I", "-i", "-F", "-l", "--untracked", "-e", s2);
7055
+ retrievalOpsRun++;
7056
+ return { wording, files: (out ?? "").split("\n").map((l2) => l2.trim()).filter(Boolean) };
7057
+ })
7058
+ );
7059
+ const corpus = Math.max(listedFileCount, 1);
7060
+ const contentScore = /* @__PURE__ */ new Map();
7061
+ for (const { wording, files } of results) {
7062
+ if (files.length === 0 || files.length > corpus / 2) continue;
7063
+ const weight = 0.5 * Math.log(1 + corpus / files.length);
7064
+ for (const file of files.slice(0, 300)) {
7065
+ const entry = contentScore.get(file) ?? { score: 0, words: [] };
7066
+ entry.score += weight;
7067
+ entry.words.push(wording);
7068
+ contentScore.set(file, entry);
7069
+ }
7070
+ }
7071
+ let taken = 0;
7072
+ for (const [file, { score, words }] of [...contentScore].sort((a2, b2) => b2[1].score - a2[1].score)) {
7073
+ if (taken >= MAX_CONTENT_MATCHES) break;
7074
+ if (isSecretLike(file) || !existsSync7(join6(workspace, file))) continue;
7075
+ const f2 = touch(file);
7076
+ f2.matchScore += score;
7077
+ if (isDirty2(file)) f2.dirty = true;
7078
+ note(f2, `content mentions ${words.slice(0, 3).map((w2) => `"${w2}"`).join(", ")}`);
7079
+ taken++;
7080
+ }
7081
+ }
6979
7082
  if (ripgrepAvailable && terms.length > 0) {
6980
- const hits = await searchTerms(workspace, terms);
7083
+ const hits = await searchTerms(workspace, terms.slice(0, RIPGREP_TERMS));
6981
7084
  retrievalOpsRun++;
6982
7085
  for (const hit of hits) {
6983
7086
  const file = hit.file.replace(/\\/g, "/").replace(/^\.\//, "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offhands",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
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",