offhands 0.1.10 → 0.1.12
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 +219 -54
- package/package.json +2 -2
package/dist/daemon.mjs
CHANGED
|
@@ -4777,7 +4777,7 @@ var require_main = __commonJS({
|
|
|
4777
4777
|
|
|
4778
4778
|
// ../daemon/src/index.ts
|
|
4779
4779
|
import { resolve as resolve5 } from "node:path";
|
|
4780
|
-
import { existsSync as
|
|
4780
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
4781
4781
|
|
|
4782
4782
|
// ../daemon/src/runners/claude-code.ts
|
|
4783
4783
|
import { spawn } from "node:child_process";
|
|
@@ -6015,8 +6015,8 @@ function sleep(ms) {
|
|
|
6015
6015
|
// ../daemon/src/session-manager.ts
|
|
6016
6016
|
import { randomUUID } from "node:crypto";
|
|
6017
6017
|
import { hostname, platform as platform2, tmpdir } from "node:os";
|
|
6018
|
-
import { existsSync as
|
|
6019
|
-
import { dirname as dirname3, join as
|
|
6018
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
6019
|
+
import { dirname as dirname3, join as join7, basename as basename3, parse as parse2, resolve as resolve3 } from "node:path";
|
|
6020
6020
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
6021
6021
|
import { execFile as execFile4 } from "node:child_process";
|
|
6022
6022
|
import { promisify as promisify4 } from "node:util";
|
|
@@ -6209,10 +6209,10 @@ function dedupeDropName(desiredName, existingNames) {
|
|
|
6209
6209
|
const existing = new Set([...existingNames].map((n2) => n2.toLowerCase()));
|
|
6210
6210
|
if (!existing.has(safe.toLowerCase())) return safe;
|
|
6211
6211
|
const parsed = parse(safe);
|
|
6212
|
-
const
|
|
6212
|
+
const stem2 = parsed.name || "drop";
|
|
6213
6213
|
const ext = parsed.ext;
|
|
6214
6214
|
for (let i2 = 1; ; i2++) {
|
|
6215
|
-
const candidate = `${
|
|
6215
|
+
const candidate = `${stem2}-${i2}${ext}`;
|
|
6216
6216
|
if (!existing.has(candidate.toLowerCase())) return candidate;
|
|
6217
6217
|
}
|
|
6218
6218
|
}
|
|
@@ -6626,6 +6626,10 @@ async function probeWolCapability() {
|
|
|
6626
6626
|
}
|
|
6627
6627
|
}
|
|
6628
6628
|
|
|
6629
|
+
// ../daemon/src/intelligence/context-inject.ts
|
|
6630
|
+
import { existsSync as existsSync7, realpathSync, statSync as statSync3 } from "node:fs";
|
|
6631
|
+
import { isAbsolute, join as join6, relative, sep } from "node:path";
|
|
6632
|
+
|
|
6629
6633
|
// ../daemon/src/intelligence/ripgrep.ts
|
|
6630
6634
|
import { execFile as execFile3 } from "node:child_process";
|
|
6631
6635
|
import { promisify as promisify3 } from "node:util";
|
|
@@ -6758,6 +6762,62 @@ function extractTerms(prompt, maxTerms = 8) {
|
|
|
6758
6762
|
}
|
|
6759
6763
|
return deduped.slice().sort((a2, b2) => b2.length - a2.length).slice(0, maxTerms);
|
|
6760
6764
|
}
|
|
6765
|
+
var SECRET_BASENAMES = /* @__PURE__ */ new Set([".npmrc", ".netrc", "id_rsa", "id_dsa", "id_ecdsa", "id_ed25519", "credentials.json", "secrets.json"]);
|
|
6766
|
+
var SECRET_EXTS = [".pem", ".key", ".p12", ".pfx", ".keystore", ".jks", ".env"];
|
|
6767
|
+
var ENV_TEMPLATE_SUFFIXES = [".example", ".sample", ".template", ".dist"];
|
|
6768
|
+
function isSecretLike(path) {
|
|
6769
|
+
const base = (path.split(/[\\/]/).pop() ?? "").toLowerCase();
|
|
6770
|
+
if (base === ".env" || base.startsWith(".env.")) return !ENV_TEMPLATE_SUFFIXES.some((s2) => base.endsWith(s2));
|
|
6771
|
+
if (SECRET_BASENAMES.has(base)) return true;
|
|
6772
|
+
return SECRET_EXTS.some((e) => base.endsWith(e));
|
|
6773
|
+
}
|
|
6774
|
+
function parsePorcelainZ(out) {
|
|
6775
|
+
const parts = out.split("\0");
|
|
6776
|
+
const paths = [];
|
|
6777
|
+
for (let i2 = 0; i2 < parts.length; i2++) {
|
|
6778
|
+
const p2 = parts[i2];
|
|
6779
|
+
if (p2.length < 4) continue;
|
|
6780
|
+
const x2 = p2[0];
|
|
6781
|
+
const y2 = p2[1];
|
|
6782
|
+
if (x2 === "R" || x2 === "C" || y2 === "R" || y2 === "C") i2++;
|
|
6783
|
+
paths.push(p2.slice(3));
|
|
6784
|
+
}
|
|
6785
|
+
return paths;
|
|
6786
|
+
}
|
|
6787
|
+
function isDirectory(abs) {
|
|
6788
|
+
try {
|
|
6789
|
+
return statSync3(abs).isDirectory();
|
|
6790
|
+
} catch {
|
|
6791
|
+
return false;
|
|
6792
|
+
}
|
|
6793
|
+
}
|
|
6794
|
+
function realOrSame(p2) {
|
|
6795
|
+
try {
|
|
6796
|
+
return realpathSync.native(p2);
|
|
6797
|
+
} catch {
|
|
6798
|
+
return p2;
|
|
6799
|
+
}
|
|
6800
|
+
}
|
|
6801
|
+
function stem(word) {
|
|
6802
|
+
let w2 = word.toLowerCase();
|
|
6803
|
+
if (w2.length > 4) w2 = w2.replace(/(ing|ed|es|s)$/, "");
|
|
6804
|
+
if (w2.length > 3) w2 = w2.replace(/e$/, "");
|
|
6805
|
+
return w2;
|
|
6806
|
+
}
|
|
6807
|
+
function pathWords(path) {
|
|
6808
|
+
const split = (s2) => s2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean).map(stem);
|
|
6809
|
+
const slash = path.lastIndexOf("/");
|
|
6810
|
+
const base = new Set(split(path.slice(slash + 1)));
|
|
6811
|
+
const all = /* @__PURE__ */ new Set([...split(path.slice(0, Math.max(slash, 0))), ...base]);
|
|
6812
|
+
return { all, base };
|
|
6813
|
+
}
|
|
6814
|
+
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
|
+
var isNoise = (p2) => NOISE_FILES.has((p2.split("/").pop() ?? "").toLowerCase());
|
|
6816
|
+
var MAX_UNMATCHED_WITH_MATCHES = 3;
|
|
6817
|
+
var MAX_NAME_MATCHES = 10;
|
|
6818
|
+
var MAX_DIRTY_ONLY = 8;
|
|
6819
|
+
var MAX_RECENT = 6;
|
|
6820
|
+
var MAX_LS_FILES = 6e4;
|
|
6761
6821
|
async function buildContext(workspace, prompt, opts = {}) {
|
|
6762
6822
|
const startedAt = Date.now();
|
|
6763
6823
|
const maxChars = opts.maxChars ?? (Number(process.env.OFFHAND_INTEL_MAX_CHARS) || DEFAULT_MAX_CHARS);
|
|
@@ -6782,8 +6842,13 @@ async function buildContext(workspace, prompt, opts = {}) {
|
|
|
6782
6842
|
}
|
|
6783
6843
|
const { findings, retrievalOpsRun, ripgrepAvailable, gitState } = result;
|
|
6784
6844
|
if (findings.length === 0) return null;
|
|
6785
|
-
|
|
6786
|
-
|
|
6845
|
+
const rank = (f2) => f2.matchScore > 0 ? f2.dirty ? 5 : 4 : f2.dirty ? 2 : 1;
|
|
6846
|
+
findings.sort((a2, b2) => rank(b2) - rank(a2) || b2.matchScore - a2.matchScore);
|
|
6847
|
+
const strongMatches = findings.filter((f2) => f2.matchScore > 0).length;
|
|
6848
|
+
const unmatchedCap = strongMatches >= 3 ? MAX_UNMATCHED_WITH_MATCHES : Infinity;
|
|
6849
|
+
let unmatched = 0;
|
|
6850
|
+
const shown = findings.filter((f2) => f2.matchScore > 0 || ++unmatched <= unmatchedCap);
|
|
6851
|
+
const { text: block, selectedCount } = formatBlock(prompt, shown, gitState, maxChars);
|
|
6787
6852
|
if (selectedCount === 0) return null;
|
|
6788
6853
|
return {
|
|
6789
6854
|
block,
|
|
@@ -6802,41 +6867,141 @@ async function buildContext(workspace, prompt, opts = {}) {
|
|
|
6802
6867
|
async function gatherFindings(workspace, prompt) {
|
|
6803
6868
|
let retrievalOpsRun = 0;
|
|
6804
6869
|
const byPath = /* @__PURE__ */ new Map();
|
|
6805
|
-
const
|
|
6806
|
-
|
|
6807
|
-
if (
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
} else {
|
|
6811
|
-
byPath.set(path, { path, reasons: [reason], priority });
|
|
6870
|
+
const touch = (path) => {
|
|
6871
|
+
let f2 = byPath.get(path);
|
|
6872
|
+
if (!f2) {
|
|
6873
|
+
f2 = { path, reasons: [], dirty: false, matchScore: 0 };
|
|
6874
|
+
byPath.set(path, f2);
|
|
6812
6875
|
}
|
|
6876
|
+
return f2;
|
|
6877
|
+
};
|
|
6878
|
+
const note = (f2, reason) => {
|
|
6879
|
+
if (!f2.reasons.includes(reason)) f2.reasons.push(reason);
|
|
6813
6880
|
};
|
|
6814
|
-
const
|
|
6881
|
+
const wsReal = realOrSame(workspace);
|
|
6882
|
+
const rootOut = await git(workspace, "rev-parse", "--show-toplevel");
|
|
6883
|
+
retrievalOpsRun++;
|
|
6884
|
+
const gitRoot = realOrSame(rootOut?.trim() || workspace);
|
|
6885
|
+
const liveRel = (rootRelative) => {
|
|
6886
|
+
const abs = join6(gitRoot, rootRelative);
|
|
6887
|
+
const rel = relative(wsReal, abs);
|
|
6888
|
+
if (!rel || rel.startsWith("..") || isAbsolute(rel)) return null;
|
|
6889
|
+
if (!existsSync7(abs)) return null;
|
|
6890
|
+
return rel.split(sep).join("/");
|
|
6891
|
+
};
|
|
6892
|
+
const statusOut = await git(workspace, "status", "--porcelain", "-z");
|
|
6815
6893
|
retrievalOpsRun++;
|
|
6816
6894
|
let gitState = null;
|
|
6895
|
+
const dirtySet = /* @__PURE__ */ new Set();
|
|
6896
|
+
const dirtyDirs = [];
|
|
6897
|
+
const isDirty2 = (p2) => dirtySet.has(p2) || dirtyDirs.some((d2) => p2.startsWith(d2));
|
|
6817
6898
|
if (statusOut !== null) {
|
|
6818
|
-
const
|
|
6819
|
-
|
|
6820
|
-
|
|
6899
|
+
const entries = parsePorcelainZ(statusOut);
|
|
6900
|
+
let missing = 0;
|
|
6901
|
+
for (const p2 of entries) {
|
|
6902
|
+
const rel = liveRel(p2);
|
|
6903
|
+
if (rel === null) {
|
|
6904
|
+
missing++;
|
|
6905
|
+
continue;
|
|
6906
|
+
}
|
|
6907
|
+
if (isSecretLike(rel)) continue;
|
|
6908
|
+
if (rel.endsWith("/") || isDirectory(join6(gitRoot, p2))) dirtyDirs.push(rel.replace(/\/?$/, "/"));
|
|
6909
|
+
else dirtySet.add(rel);
|
|
6910
|
+
}
|
|
6911
|
+
let dirtyOnly = 0;
|
|
6912
|
+
for (const rel of dirtySet) {
|
|
6913
|
+
if (dirtyOnly >= MAX_DIRTY_ONLY) break;
|
|
6914
|
+
if (isNoise(rel)) continue;
|
|
6915
|
+
const f2 = touch(rel);
|
|
6916
|
+
f2.dirty = true;
|
|
6917
|
+
note(f2, "currently dirty");
|
|
6918
|
+
dirtyOnly++;
|
|
6919
|
+
}
|
|
6920
|
+
gitState = entries.length === 0 ? "clean working tree" : `${entries.length - missing} modified/new file(s)${missing > 0 ? ` and ${missing} deleted` : ""}, uncommitted`;
|
|
6821
6921
|
}
|
|
6822
6922
|
const ripgrepAvailable = await detectRipgrep();
|
|
6823
6923
|
retrievalOpsRun++;
|
|
6824
6924
|
const terms = extractTerms(prompt);
|
|
6925
|
+
if (terms.length > 0) {
|
|
6926
|
+
const listing = await git(workspace, "ls-files", "-co", "--exclude-standard", "-z");
|
|
6927
|
+
retrievalOpsRun++;
|
|
6928
|
+
const all = (listing ?? "").split("\0").filter(Boolean);
|
|
6929
|
+
if (all.length > 0 && all.length <= MAX_LS_FILES) {
|
|
6930
|
+
const stems = /* @__PURE__ */ new Map();
|
|
6931
|
+
const literals = [];
|
|
6932
|
+
for (const t2 of terms) {
|
|
6933
|
+
if (/[._-]/.test(t2)) literals.push(t2);
|
|
6934
|
+
else if (!stems.has(stem(t2))) stems.set(stem(t2), t2);
|
|
6935
|
+
}
|
|
6936
|
+
const words = all.map(pathWords);
|
|
6937
|
+
const lowered = literals.length > 0 ? all.map((p2) => p2.toLowerCase()) : [];
|
|
6938
|
+
const idf = /* @__PURE__ */ new Map();
|
|
6939
|
+
const wording = /* @__PURE__ */ new Map();
|
|
6940
|
+
const idfOf = (key, df) => {
|
|
6941
|
+
if (df > 0 && df < all.length) idf.set(key, Math.log(1 + all.length / df));
|
|
6942
|
+
};
|
|
6943
|
+
for (const [s2, original] of stems) {
|
|
6944
|
+
wording.set(s2, original);
|
|
6945
|
+
idfOf(s2, words.reduce((n2, w2) => w2.all.has(s2) ? n2 + 1 : n2, 0));
|
|
6946
|
+
}
|
|
6947
|
+
for (const lit of literals) {
|
|
6948
|
+
wording.set(lit, lit);
|
|
6949
|
+
idfOf(lit, lowered.reduce((n2, p2) => p2.includes(lit) ? n2 + 1 : n2, 0));
|
|
6950
|
+
}
|
|
6951
|
+
if (idf.size > 0) {
|
|
6952
|
+
const scored = [];
|
|
6953
|
+
for (let i2 = 0; i2 < all.length; i2++) {
|
|
6954
|
+
let score = 0;
|
|
6955
|
+
const matched = [];
|
|
6956
|
+
for (const [key, weight] of idf) {
|
|
6957
|
+
const isLiteral = literals.includes(key);
|
|
6958
|
+
if (isLiteral ? lowered[i2].includes(key) : words[i2].base.has(key)) score += weight * 2;
|
|
6959
|
+
else if (!isLiteral && words[i2].all.has(key)) score += weight;
|
|
6960
|
+
else continue;
|
|
6961
|
+
matched.push(wording.get(key));
|
|
6962
|
+
}
|
|
6963
|
+
if (score > 0) scored.push({ path: all[i2], score, matched });
|
|
6964
|
+
}
|
|
6965
|
+
scored.sort((a2, b2) => b2.score - a2.score);
|
|
6966
|
+
let taken = 0;
|
|
6967
|
+
for (const s2 of scored) {
|
|
6968
|
+
if (taken >= MAX_NAME_MATCHES) break;
|
|
6969
|
+
if (isSecretLike(s2.path) || !existsSync7(join6(workspace, s2.path))) continue;
|
|
6970
|
+
const f2 = touch(s2.path);
|
|
6971
|
+
f2.matchScore += s2.score;
|
|
6972
|
+
if (isDirty2(s2.path)) f2.dirty = true;
|
|
6973
|
+
note(f2, `path matches ${s2.matched.map((m3) => `"${m3}"`).join(", ")}`);
|
|
6974
|
+
taken++;
|
|
6975
|
+
}
|
|
6976
|
+
}
|
|
6977
|
+
}
|
|
6978
|
+
}
|
|
6825
6979
|
if (ripgrepAvailable && terms.length > 0) {
|
|
6826
6980
|
const hits = await searchTerms(workspace, terms);
|
|
6827
6981
|
retrievalOpsRun++;
|
|
6828
6982
|
for (const hit of hits) {
|
|
6983
|
+
const file = hit.file.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6984
|
+
if (isSecretLike(file)) continue;
|
|
6829
6985
|
const term = terms.find((t2) => hit.text.toLowerCase().includes(t2));
|
|
6830
|
-
|
|
6986
|
+
const f2 = touch(file);
|
|
6987
|
+
f2.matchScore += 1;
|
|
6988
|
+
if (isDirty2(file)) f2.dirty = true;
|
|
6989
|
+
note(f2, term ? `matched "${term}"` : "matched search terms");
|
|
6831
6990
|
}
|
|
6832
6991
|
}
|
|
6833
|
-
const logOut = await git(workspace, "log", "-n", "15", "--name-only", "--pretty=format:");
|
|
6992
|
+
const logOut = await git(workspace, "-c", "core.quotepath=off", "log", "-n", "15", "--name-only", "--relative", "--pretty=format:");
|
|
6834
6993
|
retrievalOpsRun++;
|
|
6835
6994
|
if (logOut !== null) {
|
|
6836
|
-
const
|
|
6837
|
-
|
|
6995
|
+
const recent = [...new Set(logOut.split("\n").map((l2) => l2.trim()).filter((l2) => l2 && !l2.startsWith('"')))];
|
|
6996
|
+
let taken = 0;
|
|
6997
|
+
for (const p2 of recent) {
|
|
6998
|
+
if (taken >= MAX_RECENT) break;
|
|
6999
|
+
if (isSecretLike(p2) || isNoise(p2) || !existsSync7(join6(workspace, p2))) continue;
|
|
7000
|
+
note(touch(p2), "recently changed");
|
|
7001
|
+
taken++;
|
|
7002
|
+
}
|
|
6838
7003
|
}
|
|
6839
|
-
return { findings: [...byPath.values()], retrievalOpsRun, ripgrepAvailable, gitState };
|
|
7004
|
+
return { findings: [...byPath.values()].filter((f2) => f2.reasons.length > 0), retrievalOpsRun, ripgrepAvailable, gitState };
|
|
6840
7005
|
}
|
|
6841
7006
|
function formatBlock(prompt, rankedFindings, gitState, maxChars) {
|
|
6842
7007
|
const header = `TASK
|
|
@@ -6866,7 +7031,7 @@ OFFHAND REPOSITORY CONTEXT
|
|
|
6866
7031
|
// ../daemon/src/session-manager.ts
|
|
6867
7032
|
function resolveDaemonVersion() {
|
|
6868
7033
|
try {
|
|
6869
|
-
const pkgPath =
|
|
7034
|
+
const pkgPath = join7(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
6870
7035
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
6871
7036
|
return pkg.version ?? "0.0.0";
|
|
6872
7037
|
} catch {
|
|
@@ -7211,7 +7376,7 @@ var SessionManager = class {
|
|
|
7211
7376
|
case "fs-create-folder": {
|
|
7212
7377
|
try {
|
|
7213
7378
|
const safeName = safeDropName(msg.name);
|
|
7214
|
-
mkdirSync2(
|
|
7379
|
+
mkdirSync2(join7(resolve3(msg.path), safeName), { recursive: false });
|
|
7215
7380
|
} catch (e) {
|
|
7216
7381
|
reply({
|
|
7217
7382
|
type: "fs-response",
|
|
@@ -7352,9 +7517,9 @@ var SessionManager = class {
|
|
|
7352
7517
|
let out = prompt + "\n\nAttached files (read them from disk):";
|
|
7353
7518
|
for (const a2 of attachments) {
|
|
7354
7519
|
const bytes = await this.attachmentFetcher(a2.blobId);
|
|
7355
|
-
const dir =
|
|
7520
|
+
const dir = join7(tmpdir(), "offhand-attachments");
|
|
7356
7521
|
mkdirSync2(dir, { recursive: true });
|
|
7357
|
-
const path =
|
|
7522
|
+
const path = join7(dir, `${a2.blobId.slice(0, 8)}-${basename3(a2.name)}`);
|
|
7358
7523
|
writeFileSync2(path, bytes);
|
|
7359
7524
|
out += `
|
|
7360
7525
|
- ${path} (${a2.mime})`;
|
|
@@ -7574,17 +7739,17 @@ function listFolders(path) {
|
|
|
7574
7739
|
if (!path) return { path: "", parent: null, dirs: driveRoots() };
|
|
7575
7740
|
const current = resolve3(path);
|
|
7576
7741
|
const dirs = safeReadDir2(current).filter((entry) => entry.isDirectory() && !skipDir(entry.name)).map((entry) => {
|
|
7577
|
-
const full =
|
|
7578
|
-
return { name: entry.name, path: full, isGit:
|
|
7742
|
+
const full = join7(current, entry.name);
|
|
7743
|
+
return { name: entry.name, path: full, isGit: existsSync8(join7(full, ".git")) };
|
|
7579
7744
|
}).sort((a2, b2) => Number(b2.isGit) - Number(a2.isGit) || a2.name.localeCompare(b2.name)).slice(0, 200);
|
|
7580
7745
|
return { path: current, parent: isRoot(current) ? null : dirname3(current), dirs };
|
|
7581
7746
|
}
|
|
7582
7747
|
function driveRoots() {
|
|
7583
|
-
if (process.platform !== "win32") return [{ name: "/", path: "/", isGit:
|
|
7748
|
+
if (process.platform !== "win32") return [{ name: "/", path: "/", isGit: existsSync8("/.git") }];
|
|
7584
7749
|
const roots = [];
|
|
7585
7750
|
for (let code = 67; code <= 90; code++) {
|
|
7586
7751
|
const name = `${String.fromCharCode(code)}:\\`;
|
|
7587
|
-
if (
|
|
7752
|
+
if (existsSync8(name)) roots.push({ name, path: name, isGit: existsSync8(join7(name, ".git")) });
|
|
7588
7753
|
}
|
|
7589
7754
|
return roots.sort((a2, b2) => a2.name.localeCompare(b2.name)).slice(0, 200);
|
|
7590
7755
|
}
|
|
@@ -7618,13 +7783,13 @@ function stripTrailing(path) {
|
|
|
7618
7783
|
import { DatabaseSync } from "node:sqlite";
|
|
7619
7784
|
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
7620
7785
|
import { homedir as homedir6 } from "node:os";
|
|
7621
|
-
import { join as
|
|
7786
|
+
import { join as join8, basename as basename4 } from "node:path";
|
|
7622
7787
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7623
7788
|
var Store = class {
|
|
7624
7789
|
db;
|
|
7625
|
-
constructor(dir = process.env.OFFHAND_HOME ??
|
|
7790
|
+
constructor(dir = process.env.OFFHAND_HOME ?? join8(homedir6(), ".offhand")) {
|
|
7626
7791
|
mkdirSync3(dir, { recursive: true });
|
|
7627
|
-
this.db = new DatabaseSync(
|
|
7792
|
+
this.db = new DatabaseSync(join8(dir, "offhand.db"));
|
|
7628
7793
|
this.db.exec(`
|
|
7629
7794
|
PRAGMA journal_mode = WAL;
|
|
7630
7795
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -43350,17 +43515,17 @@ var RelayClient = class {
|
|
|
43350
43515
|
};
|
|
43351
43516
|
|
|
43352
43517
|
// ../daemon/src/pairing.ts
|
|
43353
|
-
import { existsSync as
|
|
43518
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
|
|
43354
43519
|
import { homedir as homedir7 } from "node:os";
|
|
43355
|
-
import { join as
|
|
43520
|
+
import { join as join9 } from "node:path";
|
|
43356
43521
|
import { randomInt } from "node:crypto";
|
|
43357
|
-
var PAIRING_DIR = process.env.OFFHAND_HOME ??
|
|
43358
|
-
var PAIRING_PATH =
|
|
43522
|
+
var PAIRING_DIR = process.env.OFFHAND_HOME ?? join9(homedir7(), ".offhand");
|
|
43523
|
+
var PAIRING_PATH = join9(PAIRING_DIR, "pairing.json");
|
|
43359
43524
|
var POLL_MS = 2e3;
|
|
43360
43525
|
var POLL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
43361
43526
|
async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://offhand-web.onrender.com") {
|
|
43362
43527
|
await ready;
|
|
43363
|
-
if (!forceNew &&
|
|
43528
|
+
if (!forceNew && existsSync9(PAIRING_PATH)) {
|
|
43364
43529
|
const f2 = JSON.parse(readFileSync4(PAIRING_PATH, "utf8"));
|
|
43365
43530
|
const kp2 = { publicKey: fromB64u(f2.daemonPublicKey), secretKey: fromB64u(f2.daemonSecretKey) };
|
|
43366
43531
|
const phonePk = fromB64u(f2.phonePublicKey);
|
|
@@ -43433,7 +43598,7 @@ async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://off
|
|
|
43433
43598
|
// ../daemon/src/approvals.ts
|
|
43434
43599
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
43435
43600
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
43436
|
-
import { join as
|
|
43601
|
+
import { join as join10 } from "node:path";
|
|
43437
43602
|
var ApprovalBroker = class {
|
|
43438
43603
|
constructor(timeoutMs) {
|
|
43439
43604
|
this.timeoutMs = timeoutMs;
|
|
@@ -43457,7 +43622,7 @@ var ApprovalBroker = class {
|
|
|
43457
43622
|
}
|
|
43458
43623
|
const risk = classifyRisk(toolName, input);
|
|
43459
43624
|
const filePath = input?.file_path;
|
|
43460
|
-
if (toolName === "Read" && typeof filePath === "string" && filePath.toLowerCase().startsWith(
|
|
43625
|
+
if (toolName === "Read" && typeof filePath === "string" && filePath.toLowerCase().startsWith(join10(tmpdir2(), "offhand-attachments").toLowerCase())) {
|
|
43461
43626
|
return Promise.resolve({ approve: true });
|
|
43462
43627
|
}
|
|
43463
43628
|
if (this.policyProvider() === "trusting" && risk === "low" && toolName !== "AskUserQuestion") {
|
|
@@ -43610,27 +43775,27 @@ async function downloadArtifact(relayUrl2, sessionId, blobId, keys) {
|
|
|
43610
43775
|
|
|
43611
43776
|
// ../daemon/src/autostart.ts
|
|
43612
43777
|
import { homedir as homedir8 } from "node:os";
|
|
43613
|
-
import { join as
|
|
43614
|
-
import { existsSync as
|
|
43778
|
+
import { join as join11, resolve as resolve4, dirname as dirname4 } from "node:path";
|
|
43779
|
+
import { existsSync as existsSync10, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, rmSync } from "node:fs";
|
|
43615
43780
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
43616
43781
|
import { spawnSync } from "node:child_process";
|
|
43617
43782
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
43618
43783
|
var __dirname2 = dirname4(fileURLToPath4(import.meta.url));
|
|
43619
43784
|
var SHORTCUT_NAME = "OffhandDaemon.lnk";
|
|
43620
|
-
var OFFHAND_HOME = process.env.OFFHAND_HOME ??
|
|
43785
|
+
var OFFHAND_HOME = process.env.OFFHAND_HOME ?? join11(homedir8(), ".offhand");
|
|
43621
43786
|
function getStartupDir() {
|
|
43622
43787
|
if (process.platform !== "win32") {
|
|
43623
|
-
return
|
|
43788
|
+
return join11(homedir8(), ".config", "autostart");
|
|
43624
43789
|
}
|
|
43625
|
-
return
|
|
43790
|
+
return join11(process.env.APPDATA ?? "", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
|
|
43626
43791
|
}
|
|
43627
43792
|
function getWrapperPath() {
|
|
43628
|
-
const wrapperDir =
|
|
43793
|
+
const wrapperDir = join11(OFFHAND_HOME, "autostart");
|
|
43629
43794
|
mkdirSync5(wrapperDir, { recursive: true });
|
|
43630
|
-
return
|
|
43795
|
+
return join11(wrapperDir, "start-daemon.bat");
|
|
43631
43796
|
}
|
|
43632
43797
|
function getShortcutPath() {
|
|
43633
|
-
return
|
|
43798
|
+
return join11(getStartupDir(), SHORTCUT_NAME);
|
|
43634
43799
|
}
|
|
43635
43800
|
function buildWrapperScript(config2, entry = process.argv[1] ?? "", execPath = process.execPath) {
|
|
43636
43801
|
const wsArgs2 = config2.workspaces.map((w2) => `--workspace "${w2}"`).join(" ");
|
|
@@ -43659,7 +43824,7 @@ $Shortcut.Arguments = "/c ${targetPath}"
|
|
|
43659
43824
|
$Shortcut.WorkingDirectory = "${resolve4(__dirname2, "..", "..")}"
|
|
43660
43825
|
$Shortcut.Description = "Offhand Daemon - Auto-start on login"
|
|
43661
43826
|
$Shortcut.Save()`;
|
|
43662
|
-
const scriptPath =
|
|
43827
|
+
const scriptPath = join11(tmpdir3(), "offhand-create-shortcut.ps1");
|
|
43663
43828
|
writeFileSync4(scriptPath, psScript);
|
|
43664
43829
|
const result = spawnSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath], { encoding: "utf8", shell: true });
|
|
43665
43830
|
try {
|
|
@@ -43669,7 +43834,7 @@ $Shortcut.Save()`;
|
|
|
43669
43834
|
return { success: result.status === 0, output: result.stdout + result.stderr };
|
|
43670
43835
|
}
|
|
43671
43836
|
function checkShortcutExists(shortcutPath) {
|
|
43672
|
-
return
|
|
43837
|
+
return existsSync10(shortcutPath);
|
|
43673
43838
|
}
|
|
43674
43839
|
function installAutostart(config2) {
|
|
43675
43840
|
const wrapperPath = getWrapperPath();
|
|
@@ -43689,7 +43854,7 @@ function getAutostartStatus() {
|
|
|
43689
43854
|
const shortcutPath = getShortcutPath();
|
|
43690
43855
|
const installed = checkShortcutExists(shortcutPath);
|
|
43691
43856
|
const wrapperPath = getWrapperPath();
|
|
43692
|
-
const wrapperExists =
|
|
43857
|
+
const wrapperExists = existsSync10(wrapperPath);
|
|
43693
43858
|
let details = `Shortcut: ${shortcutPath} (${installed ? "EXISTS" : "MISSING"})`;
|
|
43694
43859
|
if (wrapperExists) {
|
|
43695
43860
|
details += `
|
|
@@ -43698,7 +43863,7 @@ Wrapper: ${wrapperPath} (EXISTS)`;
|
|
|
43698
43863
|
return { installed, details };
|
|
43699
43864
|
}
|
|
43700
43865
|
function saveConfig(config2) {
|
|
43701
|
-
const configPath =
|
|
43866
|
+
const configPath = join11(OFFHAND_HOME, "autostart.json");
|
|
43702
43867
|
writeFileSync4(configPath, JSON.stringify(config2, null, 2));
|
|
43703
43868
|
}
|
|
43704
43869
|
function getConfigFromStore() {
|
|
@@ -43746,7 +43911,7 @@ var devUrl = argValue("--dev-url");
|
|
|
43746
43911
|
var store = new Store();
|
|
43747
43912
|
var wsArgs = argValues("--workspace").map((w2) => resolve5(w2));
|
|
43748
43913
|
for (const w2 of wsArgs) {
|
|
43749
|
-
if (!
|
|
43914
|
+
if (!existsSync11(w2)) {
|
|
43750
43915
|
console.error(`workspace does not exist: ${w2}`);
|
|
43751
43916
|
process.exit(1);
|
|
43752
43917
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "offhands",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
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",
|
|
@@ -23,4 +23,4 @@
|
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"esbuild": "^0.24.0"
|
|
25
25
|
}
|
|
26
|
-
}
|
|
26
|
+
}
|