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.
- package/dist/daemon.mjs +153 -40
- 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 =
|
|
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
|
-
|
|
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(/^\.\//, "");
|