offhands 0.1.10 → 0.1.11
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 +213 -53
- 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,61 @@ 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_NAME_MATCHES = 10;
|
|
6817
|
+
var MAX_DIRTY_ONLY = 8;
|
|
6818
|
+
var MAX_RECENT = 6;
|
|
6819
|
+
var MAX_LS_FILES = 6e4;
|
|
6761
6820
|
async function buildContext(workspace, prompt, opts = {}) {
|
|
6762
6821
|
const startedAt = Date.now();
|
|
6763
6822
|
const maxChars = opts.maxChars ?? (Number(process.env.OFFHAND_INTEL_MAX_CHARS) || DEFAULT_MAX_CHARS);
|
|
@@ -6782,7 +6841,8 @@ async function buildContext(workspace, prompt, opts = {}) {
|
|
|
6782
6841
|
}
|
|
6783
6842
|
const { findings, retrievalOpsRun, ripgrepAvailable, gitState } = result;
|
|
6784
6843
|
if (findings.length === 0) return null;
|
|
6785
|
-
|
|
6844
|
+
const rank = (f2) => f2.matchScore > 0 ? f2.dirty ? 5 : 4 : f2.dirty ? 2 : 1;
|
|
6845
|
+
findings.sort((a2, b2) => rank(b2) - rank(a2) || b2.matchScore - a2.matchScore);
|
|
6786
6846
|
const { text: block, selectedCount } = formatBlock(prompt, findings, gitState, maxChars);
|
|
6787
6847
|
if (selectedCount === 0) return null;
|
|
6788
6848
|
return {
|
|
@@ -6802,41 +6862,141 @@ async function buildContext(workspace, prompt, opts = {}) {
|
|
|
6802
6862
|
async function gatherFindings(workspace, prompt) {
|
|
6803
6863
|
let retrievalOpsRun = 0;
|
|
6804
6864
|
const byPath = /* @__PURE__ */ new Map();
|
|
6805
|
-
const
|
|
6806
|
-
|
|
6807
|
-
if (
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
} else {
|
|
6811
|
-
byPath.set(path, { path, reasons: [reason], priority });
|
|
6865
|
+
const touch = (path) => {
|
|
6866
|
+
let f2 = byPath.get(path);
|
|
6867
|
+
if (!f2) {
|
|
6868
|
+
f2 = { path, reasons: [], dirty: false, matchScore: 0 };
|
|
6869
|
+
byPath.set(path, f2);
|
|
6812
6870
|
}
|
|
6871
|
+
return f2;
|
|
6872
|
+
};
|
|
6873
|
+
const note = (f2, reason) => {
|
|
6874
|
+
if (!f2.reasons.includes(reason)) f2.reasons.push(reason);
|
|
6813
6875
|
};
|
|
6814
|
-
const
|
|
6876
|
+
const wsReal = realOrSame(workspace);
|
|
6877
|
+
const rootOut = await git(workspace, "rev-parse", "--show-toplevel");
|
|
6878
|
+
retrievalOpsRun++;
|
|
6879
|
+
const gitRoot = realOrSame(rootOut?.trim() || workspace);
|
|
6880
|
+
const liveRel = (rootRelative) => {
|
|
6881
|
+
const abs = join6(gitRoot, rootRelative);
|
|
6882
|
+
const rel = relative(wsReal, abs);
|
|
6883
|
+
if (!rel || rel.startsWith("..") || isAbsolute(rel)) return null;
|
|
6884
|
+
if (!existsSync7(abs)) return null;
|
|
6885
|
+
return rel.split(sep).join("/");
|
|
6886
|
+
};
|
|
6887
|
+
const statusOut = await git(workspace, "status", "--porcelain", "-z");
|
|
6815
6888
|
retrievalOpsRun++;
|
|
6816
6889
|
let gitState = null;
|
|
6890
|
+
const dirtySet = /* @__PURE__ */ new Set();
|
|
6891
|
+
const dirtyDirs = [];
|
|
6892
|
+
const isDirty2 = (p2) => dirtySet.has(p2) || dirtyDirs.some((d2) => p2.startsWith(d2));
|
|
6817
6893
|
if (statusOut !== null) {
|
|
6818
|
-
const
|
|
6819
|
-
|
|
6820
|
-
|
|
6894
|
+
const entries = parsePorcelainZ(statusOut);
|
|
6895
|
+
let missing = 0;
|
|
6896
|
+
for (const p2 of entries) {
|
|
6897
|
+
const rel = liveRel(p2);
|
|
6898
|
+
if (rel === null) {
|
|
6899
|
+
missing++;
|
|
6900
|
+
continue;
|
|
6901
|
+
}
|
|
6902
|
+
if (isSecretLike(rel)) continue;
|
|
6903
|
+
if (rel.endsWith("/") || isDirectory(join6(gitRoot, p2))) dirtyDirs.push(rel.replace(/\/?$/, "/"));
|
|
6904
|
+
else dirtySet.add(rel);
|
|
6905
|
+
}
|
|
6906
|
+
let dirtyOnly = 0;
|
|
6907
|
+
for (const rel of dirtySet) {
|
|
6908
|
+
if (dirtyOnly >= MAX_DIRTY_ONLY) break;
|
|
6909
|
+
if (isNoise(rel)) continue;
|
|
6910
|
+
const f2 = touch(rel);
|
|
6911
|
+
f2.dirty = true;
|
|
6912
|
+
note(f2, "currently dirty");
|
|
6913
|
+
dirtyOnly++;
|
|
6914
|
+
}
|
|
6915
|
+
gitState = entries.length === 0 ? "clean working tree" : `${entries.length - missing} modified/new file(s)${missing > 0 ? ` and ${missing} deleted` : ""}, uncommitted`;
|
|
6821
6916
|
}
|
|
6822
6917
|
const ripgrepAvailable = await detectRipgrep();
|
|
6823
6918
|
retrievalOpsRun++;
|
|
6824
6919
|
const terms = extractTerms(prompt);
|
|
6920
|
+
if (terms.length > 0) {
|
|
6921
|
+
const listing = await git(workspace, "ls-files", "-co", "--exclude-standard", "-z");
|
|
6922
|
+
retrievalOpsRun++;
|
|
6923
|
+
const all = (listing ?? "").split("\0").filter(Boolean);
|
|
6924
|
+
if (all.length > 0 && all.length <= MAX_LS_FILES) {
|
|
6925
|
+
const stems = /* @__PURE__ */ new Map();
|
|
6926
|
+
const literals = [];
|
|
6927
|
+
for (const t2 of terms) {
|
|
6928
|
+
if (/[._-]/.test(t2)) literals.push(t2);
|
|
6929
|
+
else if (!stems.has(stem(t2))) stems.set(stem(t2), t2);
|
|
6930
|
+
}
|
|
6931
|
+
const words = all.map(pathWords);
|
|
6932
|
+
const lowered = literals.length > 0 ? all.map((p2) => p2.toLowerCase()) : [];
|
|
6933
|
+
const idf = /* @__PURE__ */ new Map();
|
|
6934
|
+
const wording = /* @__PURE__ */ new Map();
|
|
6935
|
+
const idfOf = (key, df) => {
|
|
6936
|
+
if (df > 0 && df < all.length) idf.set(key, Math.log(1 + all.length / df));
|
|
6937
|
+
};
|
|
6938
|
+
for (const [s2, original] of stems) {
|
|
6939
|
+
wording.set(s2, original);
|
|
6940
|
+
idfOf(s2, words.reduce((n2, w2) => w2.all.has(s2) ? n2 + 1 : n2, 0));
|
|
6941
|
+
}
|
|
6942
|
+
for (const lit of literals) {
|
|
6943
|
+
wording.set(lit, lit);
|
|
6944
|
+
idfOf(lit, lowered.reduce((n2, p2) => p2.includes(lit) ? n2 + 1 : n2, 0));
|
|
6945
|
+
}
|
|
6946
|
+
if (idf.size > 0) {
|
|
6947
|
+
const scored = [];
|
|
6948
|
+
for (let i2 = 0; i2 < all.length; i2++) {
|
|
6949
|
+
let score = 0;
|
|
6950
|
+
const matched = [];
|
|
6951
|
+
for (const [key, weight] of idf) {
|
|
6952
|
+
const isLiteral = literals.includes(key);
|
|
6953
|
+
if (isLiteral ? lowered[i2].includes(key) : words[i2].base.has(key)) score += weight * 2;
|
|
6954
|
+
else if (!isLiteral && words[i2].all.has(key)) score += weight;
|
|
6955
|
+
else continue;
|
|
6956
|
+
matched.push(wording.get(key));
|
|
6957
|
+
}
|
|
6958
|
+
if (score > 0) scored.push({ path: all[i2], score, matched });
|
|
6959
|
+
}
|
|
6960
|
+
scored.sort((a2, b2) => b2.score - a2.score);
|
|
6961
|
+
let taken = 0;
|
|
6962
|
+
for (const s2 of scored) {
|
|
6963
|
+
if (taken >= MAX_NAME_MATCHES) break;
|
|
6964
|
+
if (isSecretLike(s2.path) || !existsSync7(join6(workspace, s2.path))) continue;
|
|
6965
|
+
const f2 = touch(s2.path);
|
|
6966
|
+
f2.matchScore += s2.score;
|
|
6967
|
+
if (isDirty2(s2.path)) f2.dirty = true;
|
|
6968
|
+
note(f2, `path matches ${s2.matched.map((m3) => `"${m3}"`).join(", ")}`);
|
|
6969
|
+
taken++;
|
|
6970
|
+
}
|
|
6971
|
+
}
|
|
6972
|
+
}
|
|
6973
|
+
}
|
|
6825
6974
|
if (ripgrepAvailable && terms.length > 0) {
|
|
6826
6975
|
const hits = await searchTerms(workspace, terms);
|
|
6827
6976
|
retrievalOpsRun++;
|
|
6828
6977
|
for (const hit of hits) {
|
|
6978
|
+
const file = hit.file.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6979
|
+
if (isSecretLike(file)) continue;
|
|
6829
6980
|
const term = terms.find((t2) => hit.text.toLowerCase().includes(t2));
|
|
6830
|
-
|
|
6981
|
+
const f2 = touch(file);
|
|
6982
|
+
f2.matchScore += 1;
|
|
6983
|
+
if (isDirty2(file)) f2.dirty = true;
|
|
6984
|
+
note(f2, term ? `matched "${term}"` : "matched search terms");
|
|
6831
6985
|
}
|
|
6832
6986
|
}
|
|
6833
|
-
const logOut = await git(workspace, "log", "-n", "15", "--name-only", "--pretty=format:");
|
|
6987
|
+
const logOut = await git(workspace, "-c", "core.quotepath=off", "log", "-n", "15", "--name-only", "--relative", "--pretty=format:");
|
|
6834
6988
|
retrievalOpsRun++;
|
|
6835
6989
|
if (logOut !== null) {
|
|
6836
|
-
const
|
|
6837
|
-
|
|
6990
|
+
const recent = [...new Set(logOut.split("\n").map((l2) => l2.trim()).filter((l2) => l2 && !l2.startsWith('"')))];
|
|
6991
|
+
let taken = 0;
|
|
6992
|
+
for (const p2 of recent) {
|
|
6993
|
+
if (taken >= MAX_RECENT) break;
|
|
6994
|
+
if (isSecretLike(p2) || isNoise(p2) || !existsSync7(join6(workspace, p2))) continue;
|
|
6995
|
+
note(touch(p2), "recently changed");
|
|
6996
|
+
taken++;
|
|
6997
|
+
}
|
|
6838
6998
|
}
|
|
6839
|
-
return { findings: [...byPath.values()], retrievalOpsRun, ripgrepAvailable, gitState };
|
|
6999
|
+
return { findings: [...byPath.values()].filter((f2) => f2.reasons.length > 0), retrievalOpsRun, ripgrepAvailable, gitState };
|
|
6840
7000
|
}
|
|
6841
7001
|
function formatBlock(prompt, rankedFindings, gitState, maxChars) {
|
|
6842
7002
|
const header = `TASK
|
|
@@ -6866,7 +7026,7 @@ OFFHAND REPOSITORY CONTEXT
|
|
|
6866
7026
|
// ../daemon/src/session-manager.ts
|
|
6867
7027
|
function resolveDaemonVersion() {
|
|
6868
7028
|
try {
|
|
6869
|
-
const pkgPath =
|
|
7029
|
+
const pkgPath = join7(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
6870
7030
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
6871
7031
|
return pkg.version ?? "0.0.0";
|
|
6872
7032
|
} catch {
|
|
@@ -7211,7 +7371,7 @@ var SessionManager = class {
|
|
|
7211
7371
|
case "fs-create-folder": {
|
|
7212
7372
|
try {
|
|
7213
7373
|
const safeName = safeDropName(msg.name);
|
|
7214
|
-
mkdirSync2(
|
|
7374
|
+
mkdirSync2(join7(resolve3(msg.path), safeName), { recursive: false });
|
|
7215
7375
|
} catch (e) {
|
|
7216
7376
|
reply({
|
|
7217
7377
|
type: "fs-response",
|
|
@@ -7352,9 +7512,9 @@ var SessionManager = class {
|
|
|
7352
7512
|
let out = prompt + "\n\nAttached files (read them from disk):";
|
|
7353
7513
|
for (const a2 of attachments) {
|
|
7354
7514
|
const bytes = await this.attachmentFetcher(a2.blobId);
|
|
7355
|
-
const dir =
|
|
7515
|
+
const dir = join7(tmpdir(), "offhand-attachments");
|
|
7356
7516
|
mkdirSync2(dir, { recursive: true });
|
|
7357
|
-
const path =
|
|
7517
|
+
const path = join7(dir, `${a2.blobId.slice(0, 8)}-${basename3(a2.name)}`);
|
|
7358
7518
|
writeFileSync2(path, bytes);
|
|
7359
7519
|
out += `
|
|
7360
7520
|
- ${path} (${a2.mime})`;
|
|
@@ -7574,17 +7734,17 @@ function listFolders(path) {
|
|
|
7574
7734
|
if (!path) return { path: "", parent: null, dirs: driveRoots() };
|
|
7575
7735
|
const current = resolve3(path);
|
|
7576
7736
|
const dirs = safeReadDir2(current).filter((entry) => entry.isDirectory() && !skipDir(entry.name)).map((entry) => {
|
|
7577
|
-
const full =
|
|
7578
|
-
return { name: entry.name, path: full, isGit:
|
|
7737
|
+
const full = join7(current, entry.name);
|
|
7738
|
+
return { name: entry.name, path: full, isGit: existsSync8(join7(full, ".git")) };
|
|
7579
7739
|
}).sort((a2, b2) => Number(b2.isGit) - Number(a2.isGit) || a2.name.localeCompare(b2.name)).slice(0, 200);
|
|
7580
7740
|
return { path: current, parent: isRoot(current) ? null : dirname3(current), dirs };
|
|
7581
7741
|
}
|
|
7582
7742
|
function driveRoots() {
|
|
7583
|
-
if (process.platform !== "win32") return [{ name: "/", path: "/", isGit:
|
|
7743
|
+
if (process.platform !== "win32") return [{ name: "/", path: "/", isGit: existsSync8("/.git") }];
|
|
7584
7744
|
const roots = [];
|
|
7585
7745
|
for (let code = 67; code <= 90; code++) {
|
|
7586
7746
|
const name = `${String.fromCharCode(code)}:\\`;
|
|
7587
|
-
if (
|
|
7747
|
+
if (existsSync8(name)) roots.push({ name, path: name, isGit: existsSync8(join7(name, ".git")) });
|
|
7588
7748
|
}
|
|
7589
7749
|
return roots.sort((a2, b2) => a2.name.localeCompare(b2.name)).slice(0, 200);
|
|
7590
7750
|
}
|
|
@@ -7618,13 +7778,13 @@ function stripTrailing(path) {
|
|
|
7618
7778
|
import { DatabaseSync } from "node:sqlite";
|
|
7619
7779
|
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
7620
7780
|
import { homedir as homedir6 } from "node:os";
|
|
7621
|
-
import { join as
|
|
7781
|
+
import { join as join8, basename as basename4 } from "node:path";
|
|
7622
7782
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7623
7783
|
var Store = class {
|
|
7624
7784
|
db;
|
|
7625
|
-
constructor(dir = process.env.OFFHAND_HOME ??
|
|
7785
|
+
constructor(dir = process.env.OFFHAND_HOME ?? join8(homedir6(), ".offhand")) {
|
|
7626
7786
|
mkdirSync3(dir, { recursive: true });
|
|
7627
|
-
this.db = new DatabaseSync(
|
|
7787
|
+
this.db = new DatabaseSync(join8(dir, "offhand.db"));
|
|
7628
7788
|
this.db.exec(`
|
|
7629
7789
|
PRAGMA journal_mode = WAL;
|
|
7630
7790
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -43350,17 +43510,17 @@ var RelayClient = class {
|
|
|
43350
43510
|
};
|
|
43351
43511
|
|
|
43352
43512
|
// ../daemon/src/pairing.ts
|
|
43353
|
-
import { existsSync as
|
|
43513
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
|
|
43354
43514
|
import { homedir as homedir7 } from "node:os";
|
|
43355
|
-
import { join as
|
|
43515
|
+
import { join as join9 } from "node:path";
|
|
43356
43516
|
import { randomInt } from "node:crypto";
|
|
43357
|
-
var PAIRING_DIR = process.env.OFFHAND_HOME ??
|
|
43358
|
-
var PAIRING_PATH =
|
|
43517
|
+
var PAIRING_DIR = process.env.OFFHAND_HOME ?? join9(homedir7(), ".offhand");
|
|
43518
|
+
var PAIRING_PATH = join9(PAIRING_DIR, "pairing.json");
|
|
43359
43519
|
var POLL_MS = 2e3;
|
|
43360
43520
|
var POLL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
43361
43521
|
async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://offhand-web.onrender.com") {
|
|
43362
43522
|
await ready;
|
|
43363
|
-
if (!forceNew &&
|
|
43523
|
+
if (!forceNew && existsSync9(PAIRING_PATH)) {
|
|
43364
43524
|
const f2 = JSON.parse(readFileSync4(PAIRING_PATH, "utf8"));
|
|
43365
43525
|
const kp2 = { publicKey: fromB64u(f2.daemonPublicKey), secretKey: fromB64u(f2.daemonSecretKey) };
|
|
43366
43526
|
const phonePk = fromB64u(f2.phonePublicKey);
|
|
@@ -43433,7 +43593,7 @@ async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://off
|
|
|
43433
43593
|
// ../daemon/src/approvals.ts
|
|
43434
43594
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
43435
43595
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
43436
|
-
import { join as
|
|
43596
|
+
import { join as join10 } from "node:path";
|
|
43437
43597
|
var ApprovalBroker = class {
|
|
43438
43598
|
constructor(timeoutMs) {
|
|
43439
43599
|
this.timeoutMs = timeoutMs;
|
|
@@ -43457,7 +43617,7 @@ var ApprovalBroker = class {
|
|
|
43457
43617
|
}
|
|
43458
43618
|
const risk = classifyRisk(toolName, input);
|
|
43459
43619
|
const filePath = input?.file_path;
|
|
43460
|
-
if (toolName === "Read" && typeof filePath === "string" && filePath.toLowerCase().startsWith(
|
|
43620
|
+
if (toolName === "Read" && typeof filePath === "string" && filePath.toLowerCase().startsWith(join10(tmpdir2(), "offhand-attachments").toLowerCase())) {
|
|
43461
43621
|
return Promise.resolve({ approve: true });
|
|
43462
43622
|
}
|
|
43463
43623
|
if (this.policyProvider() === "trusting" && risk === "low" && toolName !== "AskUserQuestion") {
|
|
@@ -43610,27 +43770,27 @@ async function downloadArtifact(relayUrl2, sessionId, blobId, keys) {
|
|
|
43610
43770
|
|
|
43611
43771
|
// ../daemon/src/autostart.ts
|
|
43612
43772
|
import { homedir as homedir8 } from "node:os";
|
|
43613
|
-
import { join as
|
|
43614
|
-
import { existsSync as
|
|
43773
|
+
import { join as join11, resolve as resolve4, dirname as dirname4 } from "node:path";
|
|
43774
|
+
import { existsSync as existsSync10, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, rmSync } from "node:fs";
|
|
43615
43775
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
43616
43776
|
import { spawnSync } from "node:child_process";
|
|
43617
43777
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
43618
43778
|
var __dirname2 = dirname4(fileURLToPath4(import.meta.url));
|
|
43619
43779
|
var SHORTCUT_NAME = "OffhandDaemon.lnk";
|
|
43620
|
-
var OFFHAND_HOME = process.env.OFFHAND_HOME ??
|
|
43780
|
+
var OFFHAND_HOME = process.env.OFFHAND_HOME ?? join11(homedir8(), ".offhand");
|
|
43621
43781
|
function getStartupDir() {
|
|
43622
43782
|
if (process.platform !== "win32") {
|
|
43623
|
-
return
|
|
43783
|
+
return join11(homedir8(), ".config", "autostart");
|
|
43624
43784
|
}
|
|
43625
|
-
return
|
|
43785
|
+
return join11(process.env.APPDATA ?? "", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
|
|
43626
43786
|
}
|
|
43627
43787
|
function getWrapperPath() {
|
|
43628
|
-
const wrapperDir =
|
|
43788
|
+
const wrapperDir = join11(OFFHAND_HOME, "autostart");
|
|
43629
43789
|
mkdirSync5(wrapperDir, { recursive: true });
|
|
43630
|
-
return
|
|
43790
|
+
return join11(wrapperDir, "start-daemon.bat");
|
|
43631
43791
|
}
|
|
43632
43792
|
function getShortcutPath() {
|
|
43633
|
-
return
|
|
43793
|
+
return join11(getStartupDir(), SHORTCUT_NAME);
|
|
43634
43794
|
}
|
|
43635
43795
|
function buildWrapperScript(config2, entry = process.argv[1] ?? "", execPath = process.execPath) {
|
|
43636
43796
|
const wsArgs2 = config2.workspaces.map((w2) => `--workspace "${w2}"`).join(" ");
|
|
@@ -43659,7 +43819,7 @@ $Shortcut.Arguments = "/c ${targetPath}"
|
|
|
43659
43819
|
$Shortcut.WorkingDirectory = "${resolve4(__dirname2, "..", "..")}"
|
|
43660
43820
|
$Shortcut.Description = "Offhand Daemon - Auto-start on login"
|
|
43661
43821
|
$Shortcut.Save()`;
|
|
43662
|
-
const scriptPath =
|
|
43822
|
+
const scriptPath = join11(tmpdir3(), "offhand-create-shortcut.ps1");
|
|
43663
43823
|
writeFileSync4(scriptPath, psScript);
|
|
43664
43824
|
const result = spawnSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath], { encoding: "utf8", shell: true });
|
|
43665
43825
|
try {
|
|
@@ -43669,7 +43829,7 @@ $Shortcut.Save()`;
|
|
|
43669
43829
|
return { success: result.status === 0, output: result.stdout + result.stderr };
|
|
43670
43830
|
}
|
|
43671
43831
|
function checkShortcutExists(shortcutPath) {
|
|
43672
|
-
return
|
|
43832
|
+
return existsSync10(shortcutPath);
|
|
43673
43833
|
}
|
|
43674
43834
|
function installAutostart(config2) {
|
|
43675
43835
|
const wrapperPath = getWrapperPath();
|
|
@@ -43689,7 +43849,7 @@ function getAutostartStatus() {
|
|
|
43689
43849
|
const shortcutPath = getShortcutPath();
|
|
43690
43850
|
const installed = checkShortcutExists(shortcutPath);
|
|
43691
43851
|
const wrapperPath = getWrapperPath();
|
|
43692
|
-
const wrapperExists =
|
|
43852
|
+
const wrapperExists = existsSync10(wrapperPath);
|
|
43693
43853
|
let details = `Shortcut: ${shortcutPath} (${installed ? "EXISTS" : "MISSING"})`;
|
|
43694
43854
|
if (wrapperExists) {
|
|
43695
43855
|
details += `
|
|
@@ -43698,7 +43858,7 @@ Wrapper: ${wrapperPath} (EXISTS)`;
|
|
|
43698
43858
|
return { installed, details };
|
|
43699
43859
|
}
|
|
43700
43860
|
function saveConfig(config2) {
|
|
43701
|
-
const configPath =
|
|
43861
|
+
const configPath = join11(OFFHAND_HOME, "autostart.json");
|
|
43702
43862
|
writeFileSync4(configPath, JSON.stringify(config2, null, 2));
|
|
43703
43863
|
}
|
|
43704
43864
|
function getConfigFromStore() {
|
|
@@ -43746,7 +43906,7 @@ var devUrl = argValue("--dev-url");
|
|
|
43746
43906
|
var store = new Store();
|
|
43747
43907
|
var wsArgs = argValues("--workspace").map((w2) => resolve5(w2));
|
|
43748
43908
|
for (const w2 of wsArgs) {
|
|
43749
|
-
if (!
|
|
43909
|
+
if (!existsSync11(w2)) {
|
|
43750
43910
|
console.error(`workspace does not exist: ${w2}`);
|
|
43751
43911
|
process.exit(1);
|
|
43752
43912
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "offhands",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
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
|
+
}
|