offhands 0.1.12 → 0.1.14

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 +153 -40
  2. package/package.json +1 -1
package/dist/daemon.mjs CHANGED
@@ -4959,6 +4959,51 @@ var AsyncEventQueue = class {
4959
4959
 
4960
4960
  // ../daemon/src/runners/claude-code.ts
4961
4961
  var APPROVAL_MCP_PATH = join(dirname(fileURLToPath(import.meta.url)), "..", "approval-mcp.mjs");
4962
+ var CLAUDE_PREALLOWED_OFFHAND_TOOLS = [
4963
+ "mcp__offhand__offhand_status_update",
4964
+ "mcp__offhand__offhand_context"
4965
+ ];
4966
+ function buildClaudeArgs(run, approvalUrl2) {
4967
+ const args2 = ["-p", run.prompt, "--output-format", "stream-json", "--verbose", "--include-partial-messages"];
4968
+ if (run.model) args2.push("--model", run.model);
4969
+ if (run.resumeConversationId) args2.push("--resume", run.resumeConversationId);
4970
+ const mode = run.permissionMode ?? "guarded";
4971
+ if (mode === "bypass") {
4972
+ args2.push("--permission-mode", "bypassPermissions");
4973
+ } else if (approvalUrl2) {
4974
+ if (mode === "plan") args2.push("--permission-mode", "plan");
4975
+ else if (mode === "acceptEdits") args2.push("--permission-mode", "acceptEdits");
4976
+ const mcpConfig = {
4977
+ mcpServers: {
4978
+ offhand: {
4979
+ command: process.execPath,
4980
+ args: [APPROVAL_MCP_PATH],
4981
+ // OFFHAND_STATUS_URL is derived from approvalUrl rather than a
4982
+ // second constructor param — same daemon, same port, one fewer
4983
+ // thing to keep in sync as approvalUrl already carries the
4984
+ // host:port everything else here needs.
4985
+ env: {
4986
+ OFFHAND_APPROVAL_URL: approvalUrl2,
4987
+ OFFHAND_STATUS_URL: approvalUrl2.replace(/\/approval$/, "/status"),
4988
+ OFFHAND_SESSION_ID: run.sessionId
4989
+ }
4990
+ }
4991
+ }
4992
+ };
4993
+ args2.push(
4994
+ "--mcp-config",
4995
+ JSON.stringify(mcpConfig),
4996
+ "--strict-mcp-config",
4997
+ "--permission-prompt-tool",
4998
+ "mcp__offhand__approval_prompt",
4999
+ "--allowedTools",
5000
+ CLAUDE_PREALLOWED_OFFHAND_TOOLS.join(",")
5001
+ );
5002
+ } else {
5003
+ args2.push("--permission-mode", "acceptEdits");
5004
+ }
5005
+ return args2;
5006
+ }
4962
5007
  var ClaudeCodeRunner = class {
4963
5008
  constructor(broker2, approvalUrl2) {
4964
5009
  this.broker = broker2;
@@ -5012,42 +5057,7 @@ var ClaudeCodeRunner = class {
5012
5057
  }
5013
5058
  };
5014
5059
  const detachBroker = this.broker?.attach((ev) => queue.push(ev));
5015
- const args2 = ["-p", run.prompt, "--output-format", "stream-json", "--verbose", "--include-partial-messages"];
5016
- if (run.model) args2.push("--model", run.model);
5017
- if (run.resumeConversationId) args2.push("--resume", run.resumeConversationId);
5018
- const mode = run.permissionMode ?? "guarded";
5019
- if (mode === "bypass") {
5020
- args2.push("--permission-mode", "bypassPermissions");
5021
- } else if (this.broker && this.approvalUrl) {
5022
- if (mode === "plan") args2.push("--permission-mode", "plan");
5023
- else if (mode === "acceptEdits") args2.push("--permission-mode", "acceptEdits");
5024
- const mcpConfig = {
5025
- mcpServers: {
5026
- offhand: {
5027
- command: process.execPath,
5028
- args: [APPROVAL_MCP_PATH],
5029
- // OFFHAND_STATUS_URL is derived from approvalUrl rather than a
5030
- // second constructor param — same daemon, same port, one fewer
5031
- // thing to keep in sync as this.approvalUrl already carries the
5032
- // host:port everything else here needs.
5033
- env: {
5034
- OFFHAND_APPROVAL_URL: this.approvalUrl,
5035
- OFFHAND_STATUS_URL: this.approvalUrl.replace(/\/approval$/, "/status"),
5036
- OFFHAND_SESSION_ID: run.sessionId
5037
- }
5038
- }
5039
- }
5040
- };
5041
- args2.push(
5042
- "--mcp-config",
5043
- JSON.stringify(mcpConfig),
5044
- "--strict-mcp-config",
5045
- "--permission-prompt-tool",
5046
- "mcp__offhand__approval_prompt"
5047
- );
5048
- } else {
5049
- args2.push("--permission-mode", "acceptEdits");
5050
- }
5060
+ const args2 = buildClaudeArgs(run, this.broker && this.approvalUrl ? this.approvalUrl : void 0);
5051
5061
  const thinking = { low: 0, medium: 8e3, high: 16e3, max: 31999 };
5052
5062
  const env = run.effort !== void 0 ? { ...process.env, MAX_THINKING_TOKENS: String(thinking[run.effort]) } : process.env;
5053
5063
  try {
@@ -6749,8 +6759,66 @@ var STOPWORDS = /* @__PURE__ */ new Set([
6749
6759
  "into",
6750
6760
  "if",
6751
6761
  "then",
6752
- "than"
6762
+ "than",
6763
+ // Instruction-shaped filler that says how to answer, not what to look at —
6764
+ // by length alone these outranked the task's real nouns ("requirements",
6765
+ // "mirroring" pushed "closing" out of the term list).
6766
+ "requirements",
6767
+ "requirement",
6768
+ "whatever",
6769
+ "existing",
6770
+ "which",
6771
+ "tell",
6772
+ "bullets",
6773
+ "bullet",
6774
+ "fewer",
6775
+ "five",
6776
+ "only",
6777
+ "why",
6778
+ "any",
6779
+ "following",
6780
+ "something",
6781
+ "thing",
6782
+ "things",
6783
+ "just",
6784
+ "about",
6785
+ "there",
6786
+ "their",
6787
+ "them",
6788
+ "they",
6789
+ "these",
6790
+ "those",
6791
+ "your",
6792
+ "back",
6793
+ "let",
6794
+ "lets",
6795
+ "read",
6796
+ "write",
6797
+ "show",
6798
+ "give",
6799
+ "explain",
6800
+ "describe",
6801
+ "files",
6802
+ "file",
6803
+ // Generic action verbs: in code these are everywhere (createX, deleteY), so
6804
+ // "do not create, edit, or delete any files" pulled in guildCreate.ts and
6805
+ // guildDelete.ts. The nouns in a prompt carry the signal; the verb rarely does.
6806
+ "create",
6807
+ "creates",
6808
+ "edit",
6809
+ "edits",
6810
+ "delete",
6811
+ "deletes",
6812
+ "change",
6813
+ "changes",
6814
+ "modify",
6815
+ "update",
6816
+ "updates",
6817
+ "implement",
6818
+ "implementation"
6753
6819
  ]);
6820
+ var MAX_TERMS = 24;
6821
+ var RIPGREP_TERMS = 8;
6754
6822
  function extractTerms(prompt, maxTerms = 8) {
6755
6823
  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
6824
  const seen = /* @__PURE__ */ new Set();
@@ -6813,6 +6881,9 @@ function pathWords(path) {
6813
6881
  }
6814
6882
  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
6883
  var isNoise = (p2) => NOISE_FILES.has((p2.split("/").pop() ?? "").toLowerCase());
6884
+ var MAX_MATCHED_SHOWN = 14;
6885
+ var MAX_CONTENT_PROBES = 10;
6886
+ var MAX_CONTENT_MATCHES = 6;
6816
6887
  var MAX_UNMATCHED_WITH_MATCHES = 3;
6817
6888
  var MAX_NAME_MATCHES = 10;
6818
6889
  var MAX_DIRTY_ONLY = 8;
@@ -6847,7 +6918,10 @@ async function buildContext(workspace, prompt, opts = {}) {
6847
6918
  const strongMatches = findings.filter((f2) => f2.matchScore > 0).length;
6848
6919
  const unmatchedCap = strongMatches >= 3 ? MAX_UNMATCHED_WITH_MATCHES : Infinity;
6849
6920
  let unmatched = 0;
6850
- const shown = findings.filter((f2) => f2.matchScore > 0 || ++unmatched <= unmatchedCap);
6921
+ let matchedShown = 0;
6922
+ const shown = findings.filter(
6923
+ (f2) => f2.matchScore > 0 ? ++matchedShown <= MAX_MATCHED_SHOWN : ++unmatched <= unmatchedCap
6924
+ );
6851
6925
  const { text: block, selectedCount } = formatBlock(prompt, shown, gitState, maxChars);
6852
6926
  if (selectedCount === 0) return null;
6853
6927
  return {
@@ -6921,11 +6995,13 @@ async function gatherFindings(workspace, prompt) {
6921
6995
  }
6922
6996
  const ripgrepAvailable = await detectRipgrep();
6923
6997
  retrievalOpsRun++;
6924
- const terms = extractTerms(prompt);
6998
+ const terms = extractTerms(prompt, MAX_TERMS);
6999
+ let listedFileCount = 0;
6925
7000
  if (terms.length > 0) {
6926
7001
  const listing = await git(workspace, "ls-files", "-co", "--exclude-standard", "-z");
6927
7002
  retrievalOpsRun++;
6928
7003
  const all = (listing ?? "").split("\0").filter(Boolean);
7004
+ listedFileCount = all.length;
6929
7005
  if (all.length > 0 && all.length <= MAX_LS_FILES) {
6930
7006
  const stems = /* @__PURE__ */ new Map();
6931
7007
  const literals = [];
@@ -6976,8 +7052,45 @@ async function gatherFindings(workspace, prompt) {
6976
7052
  }
6977
7053
  }
6978
7054
  }
7055
+ if (!ripgrepAvailable && terms.length > 0) {
7056
+ const probes = /* @__PURE__ */ new Map();
7057
+ for (const t2 of terms) {
7058
+ const s2 = stem(t2);
7059
+ if (s2.length >= 4 && !probes.has(s2)) probes.set(s2, t2);
7060
+ if (probes.size >= MAX_CONTENT_PROBES) break;
7061
+ }
7062
+ const results = await Promise.all(
7063
+ [...probes].map(async ([s2, wording]) => {
7064
+ const out = await git(workspace, "grep", "-I", "-i", "-F", "-l", "--untracked", "-e", s2);
7065
+ retrievalOpsRun++;
7066
+ return { wording, files: (out ?? "").split("\n").map((l2) => l2.trim()).filter(Boolean) };
7067
+ })
7068
+ );
7069
+ const corpus = Math.max(listedFileCount, 1);
7070
+ const contentScore = /* @__PURE__ */ new Map();
7071
+ for (const { wording, files } of results) {
7072
+ if (files.length === 0 || files.length > corpus / 2) continue;
7073
+ const weight = 0.5 * Math.log(1 + corpus / files.length);
7074
+ for (const file of files.slice(0, 300)) {
7075
+ const entry = contentScore.get(file) ?? { score: 0, words: [] };
7076
+ entry.score += weight;
7077
+ entry.words.push(wording);
7078
+ contentScore.set(file, entry);
7079
+ }
7080
+ }
7081
+ let taken = 0;
7082
+ for (const [file, { score, words }] of [...contentScore].sort((a2, b2) => b2[1].score - a2[1].score)) {
7083
+ if (taken >= MAX_CONTENT_MATCHES) break;
7084
+ if (isSecretLike(file) || !existsSync7(join6(workspace, file))) continue;
7085
+ const f2 = touch(file);
7086
+ f2.matchScore += score;
7087
+ if (isDirty2(file)) f2.dirty = true;
7088
+ note(f2, `content mentions ${words.slice(0, 3).map((w2) => `"${w2}"`).join(", ")}`);
7089
+ taken++;
7090
+ }
7091
+ }
6979
7092
  if (ripgrepAvailable && terms.length > 0) {
6980
- const hits = await searchTerms(workspace, terms);
7093
+ const hits = await searchTerms(workspace, terms.slice(0, RIPGREP_TERMS));
6981
7094
  retrievalOpsRun++;
6982
7095
  for (const hit of hits) {
6983
7096
  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.14",
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",