offhands 0.1.14 → 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.
- package/dist/daemon.mjs +71 -20
- package/package.json +4 -1
package/dist/daemon.mjs
CHANGED
|
@@ -4961,7 +4961,8 @@ var AsyncEventQueue = class {
|
|
|
4961
4961
|
var APPROVAL_MCP_PATH = join(dirname(fileURLToPath(import.meta.url)), "..", "approval-mcp.mjs");
|
|
4962
4962
|
var CLAUDE_PREALLOWED_OFFHAND_TOOLS = [
|
|
4963
4963
|
"mcp__offhand__offhand_status_update",
|
|
4964
|
-
"mcp__offhand__offhand_context"
|
|
4964
|
+
"mcp__offhand__offhand_context",
|
|
4965
|
+
"mcp__offhand__offhand_send_file"
|
|
4965
4966
|
];
|
|
4966
4967
|
function buildClaudeArgs(run, approvalUrl2) {
|
|
4967
4968
|
const args2 = ["-p", run.prompt, "--output-format", "stream-json", "--verbose", "--include-partial-messages"];
|
|
@@ -6830,6 +6831,12 @@ function extractTerms(prompt, maxTerms = 8) {
|
|
|
6830
6831
|
}
|
|
6831
6832
|
return deduped.slice().sort((a2, b2) => b2.length - a2.length).slice(0, maxTerms);
|
|
6832
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
|
+
}
|
|
6833
6840
|
var SECRET_BASENAMES = /* @__PURE__ */ new Set([".npmrc", ".netrc", "id_rsa", "id_dsa", "id_ecdsa", "id_ed25519", "credentials.json", "secrets.json"]);
|
|
6834
6841
|
var SECRET_EXTS = [".pem", ".key", ".p12", ".pfx", ".keystore", ".jks", ".env"];
|
|
6835
6842
|
var ENV_TEMPLATE_SUFFIXES = [".example", ".sample", ".template", ".dist"];
|
|
@@ -6841,16 +6848,16 @@ function isSecretLike(path) {
|
|
|
6841
6848
|
}
|
|
6842
6849
|
function parsePorcelainZ(out) {
|
|
6843
6850
|
const parts = out.split("\0");
|
|
6844
|
-
const
|
|
6851
|
+
const entries = [];
|
|
6845
6852
|
for (let i2 = 0; i2 < parts.length; i2++) {
|
|
6846
6853
|
const p2 = parts[i2];
|
|
6847
6854
|
if (p2.length < 4) continue;
|
|
6848
6855
|
const x2 = p2[0];
|
|
6849
6856
|
const y2 = p2[1];
|
|
6850
6857
|
if (x2 === "R" || x2 === "C" || y2 === "R" || y2 === "C") i2++;
|
|
6851
|
-
|
|
6858
|
+
entries.push({ path: p2.slice(3), untracked: x2 === "?" && y2 === "?" });
|
|
6852
6859
|
}
|
|
6853
|
-
return
|
|
6860
|
+
return entries;
|
|
6854
6861
|
}
|
|
6855
6862
|
function isDirectory(abs) {
|
|
6856
6863
|
try {
|
|
@@ -6875,15 +6882,20 @@ function stem(word) {
|
|
|
6875
6882
|
function pathWords(path) {
|
|
6876
6883
|
const split = (s2) => s2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean).map(stem);
|
|
6877
6884
|
const slash = path.lastIndexOf("/");
|
|
6878
|
-
const
|
|
6885
|
+
const file = path.slice(slash + 1);
|
|
6886
|
+
const base = new Set(split(file));
|
|
6879
6887
|
const all = /* @__PURE__ */ new Set([...split(path.slice(0, Math.max(slash, 0))), ...base]);
|
|
6880
|
-
|
|
6888
|
+
const nameWords = split(file.replace(/\.[^.]+$/, ""));
|
|
6889
|
+
return { all, base, last: nameWords[nameWords.length - 1] ?? "" };
|
|
6881
6890
|
}
|
|
6882
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"]);
|
|
6883
6892
|
var isNoise = (p2) => NOISE_FILES.has((p2.split("/").pop() ?? "").toLowerCase());
|
|
6884
6893
|
var MAX_MATCHED_SHOWN = 14;
|
|
6885
6894
|
var MAX_CONTENT_PROBES = 10;
|
|
6886
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;
|
|
6887
6899
|
var MAX_UNMATCHED_WITH_MATCHES = 3;
|
|
6888
6900
|
var MAX_NAME_MATCHES = 10;
|
|
6889
6901
|
var MAX_DIRTY_ONLY = 8;
|
|
@@ -6914,14 +6926,23 @@ async function buildContext(workspace, prompt, opts = {}) {
|
|
|
6914
6926
|
const { findings, retrievalOpsRun, ripgrepAvailable, gitState } = result;
|
|
6915
6927
|
if (findings.length === 0) return null;
|
|
6916
6928
|
const rank = (f2) => f2.matchScore > 0 ? f2.dirty ? 5 : 4 : f2.dirty ? 2 : 1;
|
|
6917
|
-
|
|
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));
|
|
6918
6931
|
const strongMatches = findings.filter((f2) => f2.matchScore > 0).length;
|
|
6919
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;
|
|
6920
6936
|
let unmatched = 0;
|
|
6921
6937
|
let matchedShown = 0;
|
|
6922
|
-
const shown = findings.filter(
|
|
6923
|
-
(f2
|
|
6924
|
-
|
|
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
|
+
});
|
|
6925
6946
|
const { text: block, selectedCount } = formatBlock(prompt, shown, gitState, maxChars);
|
|
6926
6947
|
if (selectedCount === 0) return null;
|
|
6927
6948
|
return {
|
|
@@ -6944,7 +6965,7 @@ async function gatherFindings(workspace, prompt) {
|
|
|
6944
6965
|
const touch = (path) => {
|
|
6945
6966
|
let f2 = byPath.get(path);
|
|
6946
6967
|
if (!f2) {
|
|
6947
|
-
f2 = { path, reasons: [], dirty: false, matchScore: 0 };
|
|
6968
|
+
f2 = { path, reasons: [], dirty: false, matchScore: 0, terms: /* @__PURE__ */ new Set(), solid: 0, untracked: false };
|
|
6948
6969
|
byPath.set(path, f2);
|
|
6949
6970
|
}
|
|
6950
6971
|
return f2;
|
|
@@ -6972,7 +6993,8 @@ async function gatherFindings(workspace, prompt) {
|
|
|
6972
6993
|
if (statusOut !== null) {
|
|
6973
6994
|
const entries = parsePorcelainZ(statusOut);
|
|
6974
6995
|
let missing = 0;
|
|
6975
|
-
|
|
6996
|
+
const untrackedSet = /* @__PURE__ */ new Set();
|
|
6997
|
+
for (const { path: p2, untracked } of entries) {
|
|
6976
6998
|
const rel = liveRel(p2);
|
|
6977
6999
|
if (rel === null) {
|
|
6978
7000
|
missing++;
|
|
@@ -6980,14 +7002,22 @@ async function gatherFindings(workspace, prompt) {
|
|
|
6980
7002
|
}
|
|
6981
7003
|
if (isSecretLike(rel)) continue;
|
|
6982
7004
|
if (rel.endsWith("/") || isDirectory(join6(gitRoot, p2))) dirtyDirs.push(rel.replace(/\/?$/, "/"));
|
|
6983
|
-
else
|
|
7005
|
+
else {
|
|
7006
|
+
dirtySet.add(rel);
|
|
7007
|
+
if (untracked) untrackedSet.add(rel);
|
|
7008
|
+
}
|
|
6984
7009
|
}
|
|
7010
|
+
const dirtyOrder = [
|
|
7011
|
+
...[...dirtySet].filter((r2) => !untrackedSet.has(r2)),
|
|
7012
|
+
...[...dirtySet].filter((r2) => untrackedSet.has(r2) && !isJunk(r2))
|
|
7013
|
+
];
|
|
6985
7014
|
let dirtyOnly = 0;
|
|
6986
|
-
for (const rel of
|
|
7015
|
+
for (const rel of dirtyOrder) {
|
|
6987
7016
|
if (dirtyOnly >= MAX_DIRTY_ONLY) break;
|
|
6988
7017
|
if (isNoise(rel)) continue;
|
|
6989
7018
|
const f2 = touch(rel);
|
|
6990
7019
|
f2.dirty = true;
|
|
7020
|
+
f2.untracked = untrackedSet.has(rel);
|
|
6991
7021
|
note(f2, "currently dirty");
|
|
6992
7022
|
dirtyOnly++;
|
|
6993
7023
|
}
|
|
@@ -7010,6 +7040,14 @@ async function gatherFindings(workspace, prompt) {
|
|
|
7010
7040
|
else if (!stems.has(stem(t2))) stems.set(stem(t2), t2);
|
|
7011
7041
|
}
|
|
7012
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));
|
|
7013
7051
|
const lowered = literals.length > 0 ? all.map((p2) => p2.toLowerCase()) : [];
|
|
7014
7052
|
const idf = /* @__PURE__ */ new Map();
|
|
7015
7053
|
const wording = /* @__PURE__ */ new Map();
|
|
@@ -7028,15 +7066,21 @@ async function gatherFindings(workspace, prompt) {
|
|
|
7028
7066
|
const scored = [];
|
|
7029
7067
|
for (let i2 = 0; i2 < all.length; i2++) {
|
|
7030
7068
|
let score = 0;
|
|
7069
|
+
let solid = 0;
|
|
7031
7070
|
const matched = [];
|
|
7032
7071
|
for (const [key, weight] of idf) {
|
|
7033
7072
|
const isLiteral = literals.includes(key);
|
|
7034
|
-
if (isLiteral ? lowered[i2].includes(key) : words[i2].base.has(key))
|
|
7035
|
-
|
|
7036
|
-
|
|
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;
|
|
7037
7081
|
matched.push(wording.get(key));
|
|
7038
7082
|
}
|
|
7039
|
-
if (score > 0) scored.push({ path: all[i2], score, matched });
|
|
7083
|
+
if (score > 0) scored.push({ path: all[i2], score, solid, matched });
|
|
7040
7084
|
}
|
|
7041
7085
|
scored.sort((a2, b2) => b2.score - a2.score);
|
|
7042
7086
|
let taken = 0;
|
|
@@ -7045,6 +7089,8 @@ async function gatherFindings(workspace, prompt) {
|
|
|
7045
7089
|
if (isSecretLike(s2.path) || !existsSync7(join6(workspace, s2.path))) continue;
|
|
7046
7090
|
const f2 = touch(s2.path);
|
|
7047
7091
|
f2.matchScore += s2.score;
|
|
7092
|
+
f2.solid += s2.solid;
|
|
7093
|
+
for (const m3 of s2.matched) f2.terms.add(stem(m3));
|
|
7048
7094
|
if (isDirty2(s2.path)) f2.dirty = true;
|
|
7049
7095
|
note(f2, `path matches ${s2.matched.map((m3) => `"${m3}"`).join(", ")}`);
|
|
7050
7096
|
taken++;
|
|
@@ -7084,6 +7130,8 @@ async function gatherFindings(workspace, prompt) {
|
|
|
7084
7130
|
if (isSecretLike(file) || !existsSync7(join6(workspace, file))) continue;
|
|
7085
7131
|
const f2 = touch(file);
|
|
7086
7132
|
f2.matchScore += score;
|
|
7133
|
+
f2.solid += score;
|
|
7134
|
+
for (const w2 of words) f2.terms.add(stem(w2));
|
|
7087
7135
|
if (isDirty2(file)) f2.dirty = true;
|
|
7088
7136
|
note(f2, `content mentions ${words.slice(0, 3).map((w2) => `"${w2}"`).join(", ")}`);
|
|
7089
7137
|
taken++;
|
|
@@ -7098,6 +7146,8 @@ async function gatherFindings(workspace, prompt) {
|
|
|
7098
7146
|
const term = terms.find((t2) => hit.text.toLowerCase().includes(t2));
|
|
7099
7147
|
const f2 = touch(file);
|
|
7100
7148
|
f2.matchScore += 1;
|
|
7149
|
+
f2.solid += 1;
|
|
7150
|
+
if (term) f2.terms.add(stem(term));
|
|
7101
7151
|
if (isDirty2(file)) f2.dirty = true;
|
|
7102
7152
|
note(f2, term ? `matched "${term}"` : "matched search terms");
|
|
7103
7153
|
}
|
|
@@ -7109,7 +7159,7 @@ async function gatherFindings(workspace, prompt) {
|
|
|
7109
7159
|
let taken = 0;
|
|
7110
7160
|
for (const p2 of recent) {
|
|
7111
7161
|
if (taken >= MAX_RECENT) break;
|
|
7112
|
-
if (isSecretLike(p2) || isNoise(p2) || !existsSync7(join6(workspace, p2))) continue;
|
|
7162
|
+
if (isSecretLike(p2) || isNoise(p2) || isJunk(p2) || !existsSync7(join6(workspace, p2))) continue;
|
|
7113
7163
|
note(touch(p2), "recently changed");
|
|
7114
7164
|
taken++;
|
|
7115
7165
|
}
|
|
@@ -43792,7 +43842,8 @@ var ApprovalBroker = class {
|
|
|
43792
43842
|
}
|
|
43793
43843
|
};
|
|
43794
43844
|
function classifyRisk(toolName, input) {
|
|
43795
|
-
|
|
43845
|
+
const bareName = toolName.replace(/^mcp__.+?__/, "");
|
|
43846
|
+
if (bareName === "offhand_send_file") return "high";
|
|
43796
43847
|
const i2 = input ?? {};
|
|
43797
43848
|
const text = [toolName, i2.command, i2.file_path, i2.path, i2.filepath].filter((x2) => typeof x2 === "string").join(" ");
|
|
43798
43849
|
return /\b(rm|del|rmdir|rd|format|mkfs|shutdown|reboot|kill|drop\s+table|truncate|git\s+push\s+--force|--hard)\b/i.test(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "offhands",
|
|
3
|
-
"version": "0.1.
|
|
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",
|
|
@@ -22,5 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"esbuild": "^0.24.0"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"offhands": "^0.1.14"
|
|
25
28
|
}
|
|
26
29
|
}
|