offhands 0.1.9 → 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 +218 -56
- package/package.json +3 -3
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";
|
|
@@ -5520,7 +5520,8 @@ var OpenCodeRunner = class {
|
|
|
5520
5520
|
if (!pathDir) continue;
|
|
5521
5521
|
const cmdPath = join3(pathDir, "opencode.cmd");
|
|
5522
5522
|
if (existsSync3(cmdPath)) {
|
|
5523
|
-
|
|
5523
|
+
const realExe = join3(dirname2(cmdPath), "node_modules", "opencode-ai", "bin", "opencode.exe");
|
|
5524
|
+
this.resolved = [existsSync3(realExe) ? realExe : cmdPath];
|
|
5524
5525
|
return this.resolved;
|
|
5525
5526
|
}
|
|
5526
5527
|
const nodeModulesBin = join3(pathDir, "node_modules", ".bin", "opencode.cmd");
|
|
@@ -5654,13 +5655,14 @@ var OpenCodeRunner = class {
|
|
|
5654
5655
|
}
|
|
5655
5656
|
}
|
|
5656
5657
|
args2.push("--dir", run.workspace);
|
|
5657
|
-
const
|
|
5658
|
+
const viaShell = process.platform === "win32" && /\.(cmd|bat)$/i.test(cmd[0]);
|
|
5659
|
+
const promptArg = viaShell ? `"${run.prompt.replace(/\r?\n/g, " ").replace(/"/g, '\\"')}"` : run.prompt;
|
|
5658
5660
|
args2.push(promptArg);
|
|
5659
5661
|
try {
|
|
5660
5662
|
child = spawn4(cmd[0], args2, {
|
|
5661
5663
|
cwd: run.workspace,
|
|
5662
5664
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5663
|
-
shell:
|
|
5665
|
+
shell: viaShell,
|
|
5664
5666
|
// One-shot process per prompt — no shared-server cross-session risk
|
|
5665
5667
|
// here, so the session id can go straight on the env unconditionally.
|
|
5666
5668
|
env: { ...process.env, OPENCODE_CONFIG_CONTENT: statusOnlyMcpConfigContent(this.statusUrl, run.sessionId) }
|
|
@@ -6013,8 +6015,8 @@ function sleep(ms) {
|
|
|
6013
6015
|
// ../daemon/src/session-manager.ts
|
|
6014
6016
|
import { randomUUID } from "node:crypto";
|
|
6015
6017
|
import { hostname, platform as platform2, tmpdir } from "node:os";
|
|
6016
|
-
import { existsSync as
|
|
6017
|
-
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";
|
|
6018
6020
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
6019
6021
|
import { execFile as execFile4 } from "node:child_process";
|
|
6020
6022
|
import { promisify as promisify4 } from "node:util";
|
|
@@ -6207,10 +6209,10 @@ function dedupeDropName(desiredName, existingNames) {
|
|
|
6207
6209
|
const existing = new Set([...existingNames].map((n2) => n2.toLowerCase()));
|
|
6208
6210
|
if (!existing.has(safe.toLowerCase())) return safe;
|
|
6209
6211
|
const parsed = parse(safe);
|
|
6210
|
-
const
|
|
6212
|
+
const stem2 = parsed.name || "drop";
|
|
6211
6213
|
const ext = parsed.ext;
|
|
6212
6214
|
for (let i2 = 1; ; i2++) {
|
|
6213
|
-
const candidate = `${
|
|
6215
|
+
const candidate = `${stem2}-${i2}${ext}`;
|
|
6214
6216
|
if (!existing.has(candidate.toLowerCase())) return candidate;
|
|
6215
6217
|
}
|
|
6216
6218
|
}
|
|
@@ -6624,6 +6626,10 @@ async function probeWolCapability() {
|
|
|
6624
6626
|
}
|
|
6625
6627
|
}
|
|
6626
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
|
+
|
|
6627
6633
|
// ../daemon/src/intelligence/ripgrep.ts
|
|
6628
6634
|
import { execFile as execFile3 } from "node:child_process";
|
|
6629
6635
|
import { promisify as promisify3 } from "node:util";
|
|
@@ -6756,6 +6762,61 @@ function extractTerms(prompt, maxTerms = 8) {
|
|
|
6756
6762
|
}
|
|
6757
6763
|
return deduped.slice().sort((a2, b2) => b2.length - a2.length).slice(0, maxTerms);
|
|
6758
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;
|
|
6759
6820
|
async function buildContext(workspace, prompt, opts = {}) {
|
|
6760
6821
|
const startedAt = Date.now();
|
|
6761
6822
|
const maxChars = opts.maxChars ?? (Number(process.env.OFFHAND_INTEL_MAX_CHARS) || DEFAULT_MAX_CHARS);
|
|
@@ -6780,7 +6841,8 @@ async function buildContext(workspace, prompt, opts = {}) {
|
|
|
6780
6841
|
}
|
|
6781
6842
|
const { findings, retrievalOpsRun, ripgrepAvailable, gitState } = result;
|
|
6782
6843
|
if (findings.length === 0) return null;
|
|
6783
|
-
|
|
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);
|
|
6784
6846
|
const { text: block, selectedCount } = formatBlock(prompt, findings, gitState, maxChars);
|
|
6785
6847
|
if (selectedCount === 0) return null;
|
|
6786
6848
|
return {
|
|
@@ -6800,41 +6862,141 @@ async function buildContext(workspace, prompt, opts = {}) {
|
|
|
6800
6862
|
async function gatherFindings(workspace, prompt) {
|
|
6801
6863
|
let retrievalOpsRun = 0;
|
|
6802
6864
|
const byPath = /* @__PURE__ */ new Map();
|
|
6803
|
-
const
|
|
6804
|
-
|
|
6805
|
-
if (
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
} else {
|
|
6809
|
-
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);
|
|
6810
6870
|
}
|
|
6871
|
+
return f2;
|
|
6872
|
+
};
|
|
6873
|
+
const note = (f2, reason) => {
|
|
6874
|
+
if (!f2.reasons.includes(reason)) f2.reasons.push(reason);
|
|
6811
6875
|
};
|
|
6812
|
-
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");
|
|
6813
6888
|
retrievalOpsRun++;
|
|
6814
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));
|
|
6815
6893
|
if (statusOut !== null) {
|
|
6816
|
-
const
|
|
6817
|
-
|
|
6818
|
-
|
|
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`;
|
|
6819
6916
|
}
|
|
6820
6917
|
const ripgrepAvailable = await detectRipgrep();
|
|
6821
6918
|
retrievalOpsRun++;
|
|
6822
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
|
+
}
|
|
6823
6974
|
if (ripgrepAvailable && terms.length > 0) {
|
|
6824
6975
|
const hits = await searchTerms(workspace, terms);
|
|
6825
6976
|
retrievalOpsRun++;
|
|
6826
6977
|
for (const hit of hits) {
|
|
6978
|
+
const file = hit.file.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
6979
|
+
if (isSecretLike(file)) continue;
|
|
6827
6980
|
const term = terms.find((t2) => hit.text.toLowerCase().includes(t2));
|
|
6828
|
-
|
|
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");
|
|
6829
6985
|
}
|
|
6830
6986
|
}
|
|
6831
|
-
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:");
|
|
6832
6988
|
retrievalOpsRun++;
|
|
6833
6989
|
if (logOut !== null) {
|
|
6834
|
-
const
|
|
6835
|
-
|
|
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
|
+
}
|
|
6836
6998
|
}
|
|
6837
|
-
return { findings: [...byPath.values()], retrievalOpsRun, ripgrepAvailable, gitState };
|
|
6999
|
+
return { findings: [...byPath.values()].filter((f2) => f2.reasons.length > 0), retrievalOpsRun, ripgrepAvailable, gitState };
|
|
6838
7000
|
}
|
|
6839
7001
|
function formatBlock(prompt, rankedFindings, gitState, maxChars) {
|
|
6840
7002
|
const header = `TASK
|
|
@@ -6864,7 +7026,7 @@ OFFHAND REPOSITORY CONTEXT
|
|
|
6864
7026
|
// ../daemon/src/session-manager.ts
|
|
6865
7027
|
function resolveDaemonVersion() {
|
|
6866
7028
|
try {
|
|
6867
|
-
const pkgPath =
|
|
7029
|
+
const pkgPath = join7(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
6868
7030
|
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
6869
7031
|
return pkg.version ?? "0.0.0";
|
|
6870
7032
|
} catch {
|
|
@@ -7209,7 +7371,7 @@ var SessionManager = class {
|
|
|
7209
7371
|
case "fs-create-folder": {
|
|
7210
7372
|
try {
|
|
7211
7373
|
const safeName = safeDropName(msg.name);
|
|
7212
|
-
mkdirSync2(
|
|
7374
|
+
mkdirSync2(join7(resolve3(msg.path), safeName), { recursive: false });
|
|
7213
7375
|
} catch (e) {
|
|
7214
7376
|
reply({
|
|
7215
7377
|
type: "fs-response",
|
|
@@ -7350,9 +7512,9 @@ var SessionManager = class {
|
|
|
7350
7512
|
let out = prompt + "\n\nAttached files (read them from disk):";
|
|
7351
7513
|
for (const a2 of attachments) {
|
|
7352
7514
|
const bytes = await this.attachmentFetcher(a2.blobId);
|
|
7353
|
-
const dir =
|
|
7515
|
+
const dir = join7(tmpdir(), "offhand-attachments");
|
|
7354
7516
|
mkdirSync2(dir, { recursive: true });
|
|
7355
|
-
const path =
|
|
7517
|
+
const path = join7(dir, `${a2.blobId.slice(0, 8)}-${basename3(a2.name)}`);
|
|
7356
7518
|
writeFileSync2(path, bytes);
|
|
7357
7519
|
out += `
|
|
7358
7520
|
- ${path} (${a2.mime})`;
|
|
@@ -7572,17 +7734,17 @@ function listFolders(path) {
|
|
|
7572
7734
|
if (!path) return { path: "", parent: null, dirs: driveRoots() };
|
|
7573
7735
|
const current = resolve3(path);
|
|
7574
7736
|
const dirs = safeReadDir2(current).filter((entry) => entry.isDirectory() && !skipDir(entry.name)).map((entry) => {
|
|
7575
|
-
const full =
|
|
7576
|
-
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")) };
|
|
7577
7739
|
}).sort((a2, b2) => Number(b2.isGit) - Number(a2.isGit) || a2.name.localeCompare(b2.name)).slice(0, 200);
|
|
7578
7740
|
return { path: current, parent: isRoot(current) ? null : dirname3(current), dirs };
|
|
7579
7741
|
}
|
|
7580
7742
|
function driveRoots() {
|
|
7581
|
-
if (process.platform !== "win32") return [{ name: "/", path: "/", isGit:
|
|
7743
|
+
if (process.platform !== "win32") return [{ name: "/", path: "/", isGit: existsSync8("/.git") }];
|
|
7582
7744
|
const roots = [];
|
|
7583
7745
|
for (let code = 67; code <= 90; code++) {
|
|
7584
7746
|
const name = `${String.fromCharCode(code)}:\\`;
|
|
7585
|
-
if (
|
|
7747
|
+
if (existsSync8(name)) roots.push({ name, path: name, isGit: existsSync8(join7(name, ".git")) });
|
|
7586
7748
|
}
|
|
7587
7749
|
return roots.sort((a2, b2) => a2.name.localeCompare(b2.name)).slice(0, 200);
|
|
7588
7750
|
}
|
|
@@ -7616,13 +7778,13 @@ function stripTrailing(path) {
|
|
|
7616
7778
|
import { DatabaseSync } from "node:sqlite";
|
|
7617
7779
|
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
7618
7780
|
import { homedir as homedir6 } from "node:os";
|
|
7619
|
-
import { join as
|
|
7781
|
+
import { join as join8, basename as basename4 } from "node:path";
|
|
7620
7782
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7621
7783
|
var Store = class {
|
|
7622
7784
|
db;
|
|
7623
|
-
constructor(dir = process.env.OFFHAND_HOME ??
|
|
7785
|
+
constructor(dir = process.env.OFFHAND_HOME ?? join8(homedir6(), ".offhand")) {
|
|
7624
7786
|
mkdirSync3(dir, { recursive: true });
|
|
7625
|
-
this.db = new DatabaseSync(
|
|
7787
|
+
this.db = new DatabaseSync(join8(dir, "offhand.db"));
|
|
7626
7788
|
this.db.exec(`
|
|
7627
7789
|
PRAGMA journal_mode = WAL;
|
|
7628
7790
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -43348,17 +43510,17 @@ var RelayClient = class {
|
|
|
43348
43510
|
};
|
|
43349
43511
|
|
|
43350
43512
|
// ../daemon/src/pairing.ts
|
|
43351
|
-
import { existsSync as
|
|
43513
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
|
|
43352
43514
|
import { homedir as homedir7 } from "node:os";
|
|
43353
|
-
import { join as
|
|
43515
|
+
import { join as join9 } from "node:path";
|
|
43354
43516
|
import { randomInt } from "node:crypto";
|
|
43355
|
-
var PAIRING_DIR = process.env.OFFHAND_HOME ??
|
|
43356
|
-
var PAIRING_PATH =
|
|
43517
|
+
var PAIRING_DIR = process.env.OFFHAND_HOME ?? join9(homedir7(), ".offhand");
|
|
43518
|
+
var PAIRING_PATH = join9(PAIRING_DIR, "pairing.json");
|
|
43357
43519
|
var POLL_MS = 2e3;
|
|
43358
43520
|
var POLL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
43359
43521
|
async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://offhand-web.onrender.com") {
|
|
43360
43522
|
await ready;
|
|
43361
|
-
if (!forceNew &&
|
|
43523
|
+
if (!forceNew && existsSync9(PAIRING_PATH)) {
|
|
43362
43524
|
const f2 = JSON.parse(readFileSync4(PAIRING_PATH, "utf8"));
|
|
43363
43525
|
const kp2 = { publicKey: fromB64u(f2.daemonPublicKey), secretKey: fromB64u(f2.daemonSecretKey) };
|
|
43364
43526
|
const phonePk = fromB64u(f2.phonePublicKey);
|
|
@@ -43431,7 +43593,7 @@ async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://off
|
|
|
43431
43593
|
// ../daemon/src/approvals.ts
|
|
43432
43594
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
43433
43595
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
43434
|
-
import { join as
|
|
43596
|
+
import { join as join10 } from "node:path";
|
|
43435
43597
|
var ApprovalBroker = class {
|
|
43436
43598
|
constructor(timeoutMs) {
|
|
43437
43599
|
this.timeoutMs = timeoutMs;
|
|
@@ -43455,7 +43617,7 @@ var ApprovalBroker = class {
|
|
|
43455
43617
|
}
|
|
43456
43618
|
const risk = classifyRisk(toolName, input);
|
|
43457
43619
|
const filePath = input?.file_path;
|
|
43458
|
-
if (toolName === "Read" && typeof filePath === "string" && filePath.toLowerCase().startsWith(
|
|
43620
|
+
if (toolName === "Read" && typeof filePath === "string" && filePath.toLowerCase().startsWith(join10(tmpdir2(), "offhand-attachments").toLowerCase())) {
|
|
43459
43621
|
return Promise.resolve({ approve: true });
|
|
43460
43622
|
}
|
|
43461
43623
|
if (this.policyProvider() === "trusting" && risk === "low" && toolName !== "AskUserQuestion") {
|
|
@@ -43608,27 +43770,27 @@ async function downloadArtifact(relayUrl2, sessionId, blobId, keys) {
|
|
|
43608
43770
|
|
|
43609
43771
|
// ../daemon/src/autostart.ts
|
|
43610
43772
|
import { homedir as homedir8 } from "node:os";
|
|
43611
|
-
import { join as
|
|
43612
|
-
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";
|
|
43613
43775
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
43614
43776
|
import { spawnSync } from "node:child_process";
|
|
43615
43777
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
43616
43778
|
var __dirname2 = dirname4(fileURLToPath4(import.meta.url));
|
|
43617
43779
|
var SHORTCUT_NAME = "OffhandDaemon.lnk";
|
|
43618
|
-
var OFFHAND_HOME = process.env.OFFHAND_HOME ??
|
|
43780
|
+
var OFFHAND_HOME = process.env.OFFHAND_HOME ?? join11(homedir8(), ".offhand");
|
|
43619
43781
|
function getStartupDir() {
|
|
43620
43782
|
if (process.platform !== "win32") {
|
|
43621
|
-
return
|
|
43783
|
+
return join11(homedir8(), ".config", "autostart");
|
|
43622
43784
|
}
|
|
43623
|
-
return
|
|
43785
|
+
return join11(process.env.APPDATA ?? "", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
|
|
43624
43786
|
}
|
|
43625
43787
|
function getWrapperPath() {
|
|
43626
|
-
const wrapperDir =
|
|
43788
|
+
const wrapperDir = join11(OFFHAND_HOME, "autostart");
|
|
43627
43789
|
mkdirSync5(wrapperDir, { recursive: true });
|
|
43628
|
-
return
|
|
43790
|
+
return join11(wrapperDir, "start-daemon.bat");
|
|
43629
43791
|
}
|
|
43630
43792
|
function getShortcutPath() {
|
|
43631
|
-
return
|
|
43793
|
+
return join11(getStartupDir(), SHORTCUT_NAME);
|
|
43632
43794
|
}
|
|
43633
43795
|
function buildWrapperScript(config2, entry = process.argv[1] ?? "", execPath = process.execPath) {
|
|
43634
43796
|
const wsArgs2 = config2.workspaces.map((w2) => `--workspace "${w2}"`).join(" ");
|
|
@@ -43657,7 +43819,7 @@ $Shortcut.Arguments = "/c ${targetPath}"
|
|
|
43657
43819
|
$Shortcut.WorkingDirectory = "${resolve4(__dirname2, "..", "..")}"
|
|
43658
43820
|
$Shortcut.Description = "Offhand Daemon - Auto-start on login"
|
|
43659
43821
|
$Shortcut.Save()`;
|
|
43660
|
-
const scriptPath =
|
|
43822
|
+
const scriptPath = join11(tmpdir3(), "offhand-create-shortcut.ps1");
|
|
43661
43823
|
writeFileSync4(scriptPath, psScript);
|
|
43662
43824
|
const result = spawnSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath], { encoding: "utf8", shell: true });
|
|
43663
43825
|
try {
|
|
@@ -43667,7 +43829,7 @@ $Shortcut.Save()`;
|
|
|
43667
43829
|
return { success: result.status === 0, output: result.stdout + result.stderr };
|
|
43668
43830
|
}
|
|
43669
43831
|
function checkShortcutExists(shortcutPath) {
|
|
43670
|
-
return
|
|
43832
|
+
return existsSync10(shortcutPath);
|
|
43671
43833
|
}
|
|
43672
43834
|
function installAutostart(config2) {
|
|
43673
43835
|
const wrapperPath = getWrapperPath();
|
|
@@ -43687,7 +43849,7 @@ function getAutostartStatus() {
|
|
|
43687
43849
|
const shortcutPath = getShortcutPath();
|
|
43688
43850
|
const installed = checkShortcutExists(shortcutPath);
|
|
43689
43851
|
const wrapperPath = getWrapperPath();
|
|
43690
|
-
const wrapperExists =
|
|
43852
|
+
const wrapperExists = existsSync10(wrapperPath);
|
|
43691
43853
|
let details = `Shortcut: ${shortcutPath} (${installed ? "EXISTS" : "MISSING"})`;
|
|
43692
43854
|
if (wrapperExists) {
|
|
43693
43855
|
details += `
|
|
@@ -43696,7 +43858,7 @@ Wrapper: ${wrapperPath} (EXISTS)`;
|
|
|
43696
43858
|
return { installed, details };
|
|
43697
43859
|
}
|
|
43698
43860
|
function saveConfig(config2) {
|
|
43699
|
-
const configPath =
|
|
43861
|
+
const configPath = join11(OFFHAND_HOME, "autostart.json");
|
|
43700
43862
|
writeFileSync4(configPath, JSON.stringify(config2, null, 2));
|
|
43701
43863
|
}
|
|
43702
43864
|
function getConfigFromStore() {
|
|
@@ -43744,7 +43906,7 @@ var devUrl = argValue("--dev-url");
|
|
|
43744
43906
|
var store = new Store();
|
|
43745
43907
|
var wsArgs = argValues("--workspace").map((w2) => resolve5(w2));
|
|
43746
43908
|
for (const w2 of wsArgs) {
|
|
43747
|
-
if (!
|
|
43909
|
+
if (!existsSync11(w2)) {
|
|
43748
43910
|
console.error(`workspace does not exist: ${w2}`);
|
|
43749
43911
|
process.exit(1);
|
|
43750
43912
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "offhands",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Phone
|
|
3
|
+
"version": "0.1.11",
|
|
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",
|
|
7
7
|
"bin": {
|
|
@@ -23,4 +23,4 @@
|
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"esbuild": "^0.24.0"
|
|
25
25
|
}
|
|
26
|
-
}
|
|
26
|
+
}
|