@holt-os/holt 0.12.0 → 0.13.0
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/README.md +25 -1
- package/dist/cli.js +557 -152
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -27,6 +27,70 @@ function bar(fraction, width = 12) {
|
|
|
27
27
|
const filled = Math.round(f * width);
|
|
28
28
|
return c.green("\u2588".repeat(filled)) + c.dim("\u2591".repeat(width - filled));
|
|
29
29
|
}
|
|
30
|
+
var CSI = "\x1B[";
|
|
31
|
+
function createStatusBar() {
|
|
32
|
+
const out = process.stdout;
|
|
33
|
+
if (!out.isTTY) {
|
|
34
|
+
return { active: false, set: () => {
|
|
35
|
+
}, detach: () => {
|
|
36
|
+
} };
|
|
37
|
+
}
|
|
38
|
+
let text = "";
|
|
39
|
+
let detached = false;
|
|
40
|
+
const realWrite = out.write.bind(out);
|
|
41
|
+
const rows = () => out.rows || 24;
|
|
42
|
+
const paint = () => {
|
|
43
|
+
if (detached) return;
|
|
44
|
+
const r = rows();
|
|
45
|
+
realWrite(
|
|
46
|
+
`\x1B7${CSI}${r};1H${CSI}2K` + // clear the line
|
|
47
|
+
text + `\x1B8`
|
|
48
|
+
// restore cursor
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
const reserve = () => {
|
|
52
|
+
const r = rows();
|
|
53
|
+
realWrite(
|
|
54
|
+
`${CSI}1;${Math.max(1, r - 1)}r${CSI}${Math.max(1, r - 1)};1H`
|
|
55
|
+
// move cursor into the region
|
|
56
|
+
);
|
|
57
|
+
paint();
|
|
58
|
+
};
|
|
59
|
+
const onResize = () => {
|
|
60
|
+
if (!detached) reserve();
|
|
61
|
+
};
|
|
62
|
+
const detach = () => {
|
|
63
|
+
if (detached) return;
|
|
64
|
+
detached = true;
|
|
65
|
+
out.removeListener("resize", onResize);
|
|
66
|
+
process.removeListener("exit", detach);
|
|
67
|
+
process.removeListener("SIGINT", onSignal);
|
|
68
|
+
process.removeListener("SIGTERM", onSignal);
|
|
69
|
+
const r = rows();
|
|
70
|
+
realWrite(
|
|
71
|
+
`${CSI}r\x1B7${CSI}${r};1H${CSI}2K\x1B8${CSI}?25h`
|
|
72
|
+
// ensure cursor visible
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
const onSignal = (sig) => {
|
|
76
|
+
detach();
|
|
77
|
+
process.removeListener(sig, onSignal);
|
|
78
|
+
process.kill(process.pid, sig);
|
|
79
|
+
};
|
|
80
|
+
reserve();
|
|
81
|
+
out.on("resize", onResize);
|
|
82
|
+
process.once("exit", detach);
|
|
83
|
+
process.once("SIGINT", onSignal);
|
|
84
|
+
process.once("SIGTERM", onSignal);
|
|
85
|
+
return {
|
|
86
|
+
active: true,
|
|
87
|
+
set(next) {
|
|
88
|
+
text = next;
|
|
89
|
+
paint();
|
|
90
|
+
},
|
|
91
|
+
detach
|
|
92
|
+
};
|
|
93
|
+
}
|
|
30
94
|
function createReader() {
|
|
31
95
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
32
96
|
const buffer = [];
|
|
@@ -41,11 +105,11 @@ function createReader() {
|
|
|
41
105
|
closed = true;
|
|
42
106
|
while (waiters.length) waiters.shift()(null);
|
|
43
107
|
});
|
|
44
|
-
const ask = (q) => new Promise((
|
|
108
|
+
const ask = (q) => new Promise((resolve9) => {
|
|
45
109
|
if (q) process.stdout.write(q);
|
|
46
|
-
if (buffer.length)
|
|
47
|
-
else if (closed)
|
|
48
|
-
else waiters.push(
|
|
110
|
+
if (buffer.length) resolve9(buffer.shift());
|
|
111
|
+
else if (closed) resolve9(null);
|
|
112
|
+
else waiters.push(resolve9);
|
|
49
113
|
});
|
|
50
114
|
return { ask, close: () => rl.close() };
|
|
51
115
|
}
|
|
@@ -225,6 +289,7 @@ function renderPrompt(history, message, memory = []) {
|
|
|
225
289
|
const parts = [];
|
|
226
290
|
if (recent.length || memory.length) {
|
|
227
291
|
parts.push(
|
|
292
|
+
"You are the user's assistant, running through Holt, a tool that gives you durable memory of this user across sessions. Notes recalled from their past sessions may appear below; use them naturally and answer as their assistant.",
|
|
228
293
|
"You are continuing an ongoing conversation. Use the context below and reply only as the assistant to the final User message."
|
|
229
294
|
);
|
|
230
295
|
}
|
|
@@ -242,13 +307,13 @@ function renderPrompt(history, message, memory = []) {
|
|
|
242
307
|
parts.push("", `User: ${message}`, "", "Assistant:");
|
|
243
308
|
return parts.join("\n");
|
|
244
309
|
}
|
|
245
|
-
function runBrain(brain, prompt, onChunk) {
|
|
246
|
-
return new Promise((
|
|
310
|
+
function runBrain(brain, prompt, onChunk, extraArgs = []) {
|
|
311
|
+
return new Promise((resolve9) => {
|
|
247
312
|
let child;
|
|
248
313
|
try {
|
|
249
|
-
child = spawn(brain.command, [...brain.args, prompt], { stdio: ["ignore", "pipe", "pipe"] });
|
|
314
|
+
child = spawn(brain.command, [...brain.args, ...extraArgs, prompt], { stdio: ["ignore", "pipe", "pipe"] });
|
|
250
315
|
} catch (e) {
|
|
251
|
-
|
|
316
|
+
resolve9({ ok: false, text: `Could not launch "${brain.command}": ${e.message}` });
|
|
252
317
|
return;
|
|
253
318
|
}
|
|
254
319
|
let out = "";
|
|
@@ -261,11 +326,11 @@ function runBrain(brain, prompt, onChunk) {
|
|
|
261
326
|
child.stderr.on("data", (d) => {
|
|
262
327
|
err2 += d.toString();
|
|
263
328
|
});
|
|
264
|
-
child.on("error", (e) =>
|
|
329
|
+
child.on("error", (e) => resolve9({ ok: false, text: `Could not run "${brain.command}": ${e.message}` }));
|
|
265
330
|
child.on("close", (code) => {
|
|
266
331
|
const text = out.trim();
|
|
267
|
-
if (code === 0 && text)
|
|
268
|
-
else
|
|
332
|
+
if (code === 0 && text) resolve9({ ok: true, text });
|
|
333
|
+
else resolve9({ ok: false, text: err2.trim() || text || `"${brain.command}" exited with code ${code}` });
|
|
269
334
|
});
|
|
270
335
|
});
|
|
271
336
|
}
|
|
@@ -435,16 +500,16 @@ function currentAlias() {
|
|
|
435
500
|
// src/install.ts
|
|
436
501
|
import { spawn as spawn2 } from "child_process";
|
|
437
502
|
function runInteractive(cmd, args) {
|
|
438
|
-
return new Promise((
|
|
503
|
+
return new Promise((resolve9) => {
|
|
439
504
|
let child;
|
|
440
505
|
try {
|
|
441
506
|
child = spawn2(cmd, args, { stdio: "inherit" });
|
|
442
507
|
} catch {
|
|
443
|
-
|
|
508
|
+
resolve9(-1);
|
|
444
509
|
return;
|
|
445
510
|
}
|
|
446
|
-
child.on("error", () =>
|
|
447
|
-
child.on("close", (code) =>
|
|
511
|
+
child.on("error", () => resolve9(-1));
|
|
512
|
+
child.on("close", (code) => resolve9(code ?? -1));
|
|
448
513
|
});
|
|
449
514
|
}
|
|
450
515
|
|
|
@@ -1756,10 +1821,19 @@ var QUESTIONS = [
|
|
|
1756
1821
|
{ key: "avoid", q: "What do you NOT want to sound like? (e.g. corporate, salesy, robotic)", depth: "detailed" },
|
|
1757
1822
|
{ key: "signature", q: "Any signature moves? (openings you like, how you end, humor, analogies)", depth: "detailed" }
|
|
1758
1823
|
];
|
|
1824
|
+
function isSkip(a) {
|
|
1825
|
+
if (a === null) return true;
|
|
1826
|
+
const s = a.trim().toLowerCase();
|
|
1827
|
+
return s === "" || s === "n" || s === "no" || s === "skip" || s === "cancel" || s === "q" || s === "quit";
|
|
1828
|
+
}
|
|
1829
|
+
function skipNotice() {
|
|
1830
|
+
console.log(c.dim("\n Skipped. Run ") + c.accent("holt voice") + c.dim(" anytime.\n"));
|
|
1831
|
+
}
|
|
1759
1832
|
async function askDepth(ask) {
|
|
1760
1833
|
const a = await ask('Depth? "quick" (a few questions) or "detailed" (more): [quick] ');
|
|
1761
1834
|
if (a === null) return null;
|
|
1762
1835
|
const s = a.trim().toLowerCase();
|
|
1836
|
+
if (s === "n" || s === "no" || s === "skip" || s === "cancel" || s === "q" || s === "quit") return null;
|
|
1763
1837
|
return s === "detailed" || s === "d" ? "detailed" : "quick";
|
|
1764
1838
|
}
|
|
1765
1839
|
async function runQuestions(ask, depth) {
|
|
@@ -1887,13 +1961,21 @@ async function voiceInterview() {
|
|
|
1887
1961
|
const v = existing ?? emptyVoice();
|
|
1888
1962
|
console.log("\n" + c.accent("Teach Holt your writing voice"));
|
|
1889
1963
|
console.log(c.dim(" Style only. No personal questions. You can do the interview, share samples, or both.\n"));
|
|
1890
|
-
const
|
|
1891
|
-
|
|
1964
|
+
const modeRaw = await ask("Do the [i]nterview, add [s]amples, or [b]oth? [i] ");
|
|
1965
|
+
if (isSkip(modeRaw)) {
|
|
1966
|
+
close();
|
|
1967
|
+
skipNotice();
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
const mode = (modeRaw ?? "").trim().toLowerCase();
|
|
1971
|
+
const wantInterview = mode === "i" || mode === "b" || mode === "interview" || mode === "both";
|
|
1892
1972
|
const wantSamples = mode === "s" || mode === "b" || mode === "samples" || mode === "both";
|
|
1893
|
-
|
|
1973
|
+
const wantInterviewOrDefault = wantInterview || !wantSamples;
|
|
1974
|
+
if (wantInterviewOrDefault) {
|
|
1894
1975
|
const depth = await askDepth(ask);
|
|
1895
1976
|
if (depth === null) {
|
|
1896
1977
|
close();
|
|
1978
|
+
skipNotice();
|
|
1897
1979
|
return;
|
|
1898
1980
|
}
|
|
1899
1981
|
v.depth = depth;
|
|
@@ -1917,6 +1999,11 @@ async function voiceInterview() {
|
|
|
1917
1999
|
}
|
|
1918
2000
|
}
|
|
1919
2001
|
close();
|
|
2002
|
+
const gotAnswers = v.answers.some((a) => a.answer.trim());
|
|
2003
|
+
if (!gotAnswers && v.samples.length === 0) {
|
|
2004
|
+
skipNotice();
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
1920
2007
|
saveVoice(v);
|
|
1921
2008
|
console.log(c.dim("\n Building your style profile from a brain..."));
|
|
1922
2009
|
const synthed = await synthesizeVoice(v);
|
|
@@ -2218,7 +2305,10 @@ async function extractAndSaveFacts(active, cfg, history, session) {
|
|
|
2218
2305
|
try {
|
|
2219
2306
|
if (Math.floor(history.length / 2) < MIN_EXCHANGES_FOR_EXTRACTION) return 0;
|
|
2220
2307
|
const prompt = buildExtractionPrompt(history);
|
|
2221
|
-
const res = active.kind === "cli" ? await runBrain(
|
|
2308
|
+
const res = active.kind === "cli" ? await runBrain(
|
|
2309
|
+
active.commandOverride ? { ...cfg.brains[active.id], command: active.commandOverride } : cfg.brains[active.id],
|
|
2310
|
+
prompt
|
|
2311
|
+
) : await runApiBrain(active.brain, prompt);
|
|
2222
2312
|
if (!res.ok) return 0;
|
|
2223
2313
|
const facts = parseFacts(res.text);
|
|
2224
2314
|
let saved = 0;
|
|
@@ -2892,14 +2982,103 @@ function saveReply(reply, format, name) {
|
|
|
2892
2982
|
return path;
|
|
2893
2983
|
}
|
|
2894
2984
|
|
|
2985
|
+
// src/access.ts
|
|
2986
|
+
import { existsSync as existsSync9, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
|
|
2987
|
+
import { homedir as homedir3 } from "os";
|
|
2988
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, resolve as resolve2, sep } from "path";
|
|
2989
|
+
function expandHome(p) {
|
|
2990
|
+
if (p === "~") return homedir3();
|
|
2991
|
+
if (p.startsWith("~/")) return resolve2(homedir3(), p.slice(2));
|
|
2992
|
+
return p;
|
|
2993
|
+
}
|
|
2994
|
+
function isInside(child, parent) {
|
|
2995
|
+
if (child === parent) return true;
|
|
2996
|
+
const base = parent.endsWith(sep) ? parent : parent + sep;
|
|
2997
|
+
return child.startsWith(base);
|
|
2998
|
+
}
|
|
2999
|
+
function resolveExisting(token) {
|
|
3000
|
+
const expanded = expandHome(token);
|
|
3001
|
+
const abs = isAbsolute2(expanded) ? expanded : resolve2(process.cwd(), expanded);
|
|
3002
|
+
if (!existsSync9(abs)) return null;
|
|
3003
|
+
try {
|
|
3004
|
+
return realpathSync2(abs);
|
|
3005
|
+
} catch {
|
|
3006
|
+
return abs;
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
function containingDir(absPath) {
|
|
3010
|
+
try {
|
|
3011
|
+
return statSync4(absPath).isDirectory() ? absPath : dirname3(absPath);
|
|
3012
|
+
} catch {
|
|
3013
|
+
return dirname3(absPath);
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
function resolveGrantDir(token) {
|
|
3017
|
+
const expanded = expandHome(token);
|
|
3018
|
+
const abs = isAbsolute2(expanded) ? expanded : resolve2(process.cwd(), expanded);
|
|
3019
|
+
let real = abs;
|
|
3020
|
+
try {
|
|
3021
|
+
real = realpathSync2(abs);
|
|
3022
|
+
} catch {
|
|
3023
|
+
}
|
|
3024
|
+
return containingDir(real);
|
|
3025
|
+
}
|
|
3026
|
+
function findOutsidePaths(message, workspaceDir) {
|
|
3027
|
+
const ws = (() => {
|
|
3028
|
+
try {
|
|
3029
|
+
return realpathSync2(workspaceDir);
|
|
3030
|
+
} catch {
|
|
3031
|
+
return resolve2(workspaceDir);
|
|
3032
|
+
}
|
|
3033
|
+
})();
|
|
3034
|
+
const holt = wsHoltDir(ws);
|
|
3035
|
+
const tokens2 = message.match(/(?:~\/|\/|~(?=\s|$))[^\s]*/g) ?? [];
|
|
3036
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
3037
|
+
for (const rawToken of tokens2) {
|
|
3038
|
+
const token = rawToken.replace(/[.,;:!?)"'\]]+$/, "");
|
|
3039
|
+
if (!token) continue;
|
|
3040
|
+
const abs = resolveExisting(token);
|
|
3041
|
+
if (!abs) continue;
|
|
3042
|
+
if (isInside(abs, ws)) continue;
|
|
3043
|
+
if (isInside(abs, holt)) continue;
|
|
3044
|
+
dirs.add(containingDir(abs));
|
|
3045
|
+
}
|
|
3046
|
+
return [...dirs];
|
|
3047
|
+
}
|
|
3048
|
+
function claudeAccessArgs(grantedDirs) {
|
|
3049
|
+
const dirs = [...grantedDirs];
|
|
3050
|
+
if (dirs.length === 0) return [];
|
|
3051
|
+
const args = dirs.map((d) => `--add-dir=${d}`);
|
|
3052
|
+
args.push("--allowedTools=Read,Glob,Grep");
|
|
3053
|
+
return args;
|
|
3054
|
+
}
|
|
3055
|
+
|
|
2895
3056
|
// src/commands/chat.ts
|
|
2896
3057
|
function statusLine(brainLabel, history) {
|
|
2897
3058
|
const live = Math.min(history.length, MAX_REPLAY_TURNS);
|
|
2898
3059
|
const frac = MAX_REPLAY_TURNS ? live / MAX_REPLAY_TURNS : 0;
|
|
2899
|
-
const pct = Math.round(frac * 100);
|
|
2900
3060
|
const mem = memStats().turns;
|
|
2901
|
-
return c.dim(" " + brainLabel + " ") + c.dim("[") + bar(frac) + c.dim("]") + c.dim(` ${
|
|
3061
|
+
return c.dim(" " + brainLabel + " ") + c.dim("[") + bar(frac) + c.dim("]") + c.dim(` recent ${live}/${MAX_REPLAY_TURNS} \xB7 ${mem} in memory`);
|
|
2902
3062
|
}
|
|
3063
|
+
var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
3064
|
+
"exit",
|
|
3065
|
+
"quit",
|
|
3066
|
+
"q",
|
|
3067
|
+
"help",
|
|
3068
|
+
"h",
|
|
3069
|
+
"clear",
|
|
3070
|
+
"memory",
|
|
3071
|
+
"mem",
|
|
3072
|
+
"output",
|
|
3073
|
+
"save",
|
|
3074
|
+
"setting",
|
|
3075
|
+
"settings",
|
|
3076
|
+
"brain",
|
|
3077
|
+
"skill",
|
|
3078
|
+
"skills",
|
|
3079
|
+
"allow",
|
|
3080
|
+
"allowed"
|
|
3081
|
+
]);
|
|
2903
3082
|
function help() {
|
|
2904
3083
|
console.log(c.dim([
|
|
2905
3084
|
" commands:",
|
|
@@ -2908,6 +3087,8 @@ function help() {
|
|
|
2908
3087
|
' /skill [name] [input] run a skill, or list them. "holt skill" manages them.',
|
|
2909
3088
|
" /output [fmt] show or set output format: markdown | html",
|
|
2910
3089
|
" /save [name] save the last reply to a file in this folder",
|
|
3090
|
+
" /allow [path] let this session read a folder outside this one (session-only)",
|
|
3091
|
+
" /allowed list folders granted outside-access this session",
|
|
2911
3092
|
" /setting configure brains, API brains, and your launch command",
|
|
2912
3093
|
" /clear forget this session so far (saved memory stays)",
|
|
2913
3094
|
" /help this list",
|
|
@@ -2954,6 +3135,13 @@ async function chat() {
|
|
|
2954
3135
|
const session = newSessionId();
|
|
2955
3136
|
const history = [];
|
|
2956
3137
|
let lastReply = "";
|
|
3138
|
+
const grantedDirs = /* @__PURE__ */ new Set();
|
|
3139
|
+
const sb = createStatusBar();
|
|
3140
|
+
const showStatus = () => {
|
|
3141
|
+
const line = statusLine(active.label, history);
|
|
3142
|
+
if (sb.active) sb.set(line);
|
|
3143
|
+
else console.log(line + "\n");
|
|
3144
|
+
};
|
|
2957
3145
|
const embedOk = await embeddingsAvailable();
|
|
2958
3146
|
const stats = memStats();
|
|
2959
3147
|
console.log("\n" + c.accent("Holt") + c.dim(` brain: ${active.label}`));
|
|
@@ -2963,8 +3151,8 @@ async function chat() {
|
|
|
2963
3151
|
if (embedOk && stats.withEmbeddings < stats.turns) {
|
|
2964
3152
|
console.log(c.dim(` ${stats.turns - stats.withEmbeddings} older moments lack embeddings. Run "holt memory embed" to upgrade them.`));
|
|
2965
3153
|
}
|
|
2966
|
-
console.log(c.dim("Type a message. Commands: /brain /memory /output /save /setting /clear /help /exit"));
|
|
2967
|
-
|
|
3154
|
+
console.log(c.dim("Type a message. Commands: /brain /memory /output /save /allow /setting /clear /help /exit"));
|
|
3155
|
+
showStatus();
|
|
2968
3156
|
while (true) {
|
|
2969
3157
|
const raw = await ask(c.accent("\u203A "));
|
|
2970
3158
|
if (raw === null) break;
|
|
@@ -2979,7 +3167,7 @@ async function chat() {
|
|
|
2979
3167
|
}
|
|
2980
3168
|
promptOverride = inv.prompt;
|
|
2981
3169
|
console.log(c.dim(` running skill "${inv.skillName}"...`));
|
|
2982
|
-
} else if (line.startsWith("/")) {
|
|
3170
|
+
} else if (line.startsWith("/") && KNOWN_COMMANDS.has((line.slice(1).split(/\s+/)[0] || "").toLowerCase())) {
|
|
2983
3171
|
const parts = line.slice(1).split(/\s+/);
|
|
2984
3172
|
const cmd = (parts[0] || "").toLowerCase();
|
|
2985
3173
|
const rest = parts.slice(1).join(" ");
|
|
@@ -3074,6 +3262,24 @@ async function chat() {
|
|
|
3074
3262
|
console.log(c.dim(" skills: " + (names.join(" ") || "none") + " usage: /skill <name> [input]"));
|
|
3075
3263
|
continue;
|
|
3076
3264
|
}
|
|
3265
|
+
if (cmd === "allowed") {
|
|
3266
|
+
if (grantedDirs.size === 0) console.log(c.dim(" no folders outside this one are granted this session."));
|
|
3267
|
+
else {
|
|
3268
|
+
console.log(c.dim(" granted this session (read-only):"));
|
|
3269
|
+
for (const d of grantedDirs) console.log(c.dim(` ${d}`));
|
|
3270
|
+
}
|
|
3271
|
+
continue;
|
|
3272
|
+
}
|
|
3273
|
+
if (cmd === "allow") {
|
|
3274
|
+
if (!rest) {
|
|
3275
|
+
console.log(c.dim(" usage: /allow <path> (grants that path's folder read access for this session)"));
|
|
3276
|
+
continue;
|
|
3277
|
+
}
|
|
3278
|
+
const dir = resolveGrantDir(rest);
|
|
3279
|
+
grantedDirs.add(dir);
|
|
3280
|
+
console.log(c.green(` granted this session: ${dir}`));
|
|
3281
|
+
continue;
|
|
3282
|
+
}
|
|
3077
3283
|
console.log(c.dim(` unknown command: /${cmd} (try /help)`));
|
|
3078
3284
|
continue;
|
|
3079
3285
|
}
|
|
@@ -3085,18 +3291,39 @@ async function chat() {
|
|
|
3085
3291
|
console.log(c.red(` ${active.label} has no API key. Use /setting to add one, or /brain to switch.`));
|
|
3086
3292
|
continue;
|
|
3087
3293
|
}
|
|
3294
|
+
if (promptOverride === null) {
|
|
3295
|
+
const outside = findOutsidePaths(line, workspace());
|
|
3296
|
+
for (const dir of outside) {
|
|
3297
|
+
if (grantedDirs.has(dir)) continue;
|
|
3298
|
+
const ans = (await ask(c.dim(` Allow this session to access ${dir} (outside this folder)? [y/N] `)) ?? "").trim().toLowerCase();
|
|
3299
|
+
if (ans === "y" || ans === "yes") {
|
|
3300
|
+
grantedDirs.add(dir);
|
|
3301
|
+
console.log(c.green(` granted this session: ${dir}`));
|
|
3302
|
+
} else {
|
|
3303
|
+
console.log(c.dim(" not granted."));
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3088
3307
|
const remembered = await recall(line, session, 4);
|
|
3089
|
-
const label = remembered.length ?
|
|
3308
|
+
const label = remembered.length ? `Holt is thinking (recalled ${remembered.length} moment${remembered.length === 1 ? "" : "s"})...` : `Holt is thinking...`;
|
|
3090
3309
|
console.log(c.dim(` ${label}`) + "\n");
|
|
3091
3310
|
const skillsBlock = skillsPromptBlock();
|
|
3092
3311
|
const base = renderPrompt(history, line, remembered);
|
|
3093
3312
|
const prompt = promptOverride ?? (skillsBlock ? skillsBlock + "\n\n" + base : base);
|
|
3313
|
+
let extraArgs = [];
|
|
3314
|
+
if (grantedDirs.size > 0) {
|
|
3315
|
+
if (active.kind === "cli" && cfg.brains[active.id].command === "claude") {
|
|
3316
|
+
extraArgs = claudeAccessArgs(grantedDirs);
|
|
3317
|
+
} else {
|
|
3318
|
+
console.log(c.dim(" note: external file access is only available with a Claude Code brain; skipping granted folders this turn."));
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3094
3321
|
let streamed = false;
|
|
3095
3322
|
const onChunk = (chunk2) => {
|
|
3096
3323
|
streamed = true;
|
|
3097
3324
|
process.stdout.write(chunk2);
|
|
3098
3325
|
};
|
|
3099
|
-
const res = active.kind === "cli" ? await runBrain(cfg.brains[active.id], prompt, onChunk) : await runApiBrain(active.brain, prompt, onChunk);
|
|
3326
|
+
const res = active.kind === "cli" ? await runBrain(cfg.brains[active.id], prompt, onChunk, extraArgs) : await runApiBrain(active.brain, prompt, onChunk);
|
|
3100
3327
|
if (res.ok) {
|
|
3101
3328
|
if (!streamed) console.log(res.text);
|
|
3102
3329
|
if (!res.text.endsWith("\n")) console.log("");
|
|
@@ -3107,11 +3334,12 @@ async function chat() {
|
|
|
3107
3334
|
const now = Date.now();
|
|
3108
3335
|
appendTurn({ id: randomUUID3().slice(0, 8), ts: now, session, role: "user", content: line, emb: await embed(line) ?? void 0 });
|
|
3109
3336
|
appendTurn({ id: randomUUID3().slice(0, 8), ts: now, session, role: "assistant", content: res.text, emb: await embed(res.text) ?? void 0 });
|
|
3110
|
-
|
|
3337
|
+
showStatus();
|
|
3111
3338
|
} else {
|
|
3112
3339
|
console.log(c.red("\n " + res.text + "\n"));
|
|
3113
3340
|
}
|
|
3114
3341
|
}
|
|
3342
|
+
sb.detach();
|
|
3115
3343
|
if (cfg.memory?.extractFacts !== false) {
|
|
3116
3344
|
try {
|
|
3117
3345
|
const arg = active.kind === "cli" ? { kind: "cli", id: active.id } : { kind: "api", brain: active.brain };
|
|
@@ -3155,7 +3383,7 @@ async function login(which) {
|
|
|
3155
3383
|
}
|
|
3156
3384
|
|
|
3157
3385
|
// src/commands/memory.ts
|
|
3158
|
-
import { existsSync as
|
|
3386
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
3159
3387
|
import { basename } from "path";
|
|
3160
3388
|
async function memoryCmd(sub, rest = []) {
|
|
3161
3389
|
const { ask, close } = createReader();
|
|
@@ -3203,7 +3431,7 @@ async function memoryCmd(sub, rest = []) {
|
|
|
3203
3431
|
return;
|
|
3204
3432
|
}
|
|
3205
3433
|
if (action === "facts") {
|
|
3206
|
-
if (!
|
|
3434
|
+
if (!existsSync10(factsMdPath())) {
|
|
3207
3435
|
console.log(c.dim("\n No facts distilled yet. They form when you end a chat session.\n"));
|
|
3208
3436
|
close();
|
|
3209
3437
|
return;
|
|
@@ -3322,7 +3550,7 @@ function globalCmd(act, rest) {
|
|
|
3322
3550
|
}
|
|
3323
3551
|
|
|
3324
3552
|
// src/commands/wiki.ts
|
|
3325
|
-
import { existsSync as
|
|
3553
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
|
|
3326
3554
|
import { join as join10 } from "path";
|
|
3327
3555
|
import { spawn as spawn3 } from "child_process";
|
|
3328
3556
|
function titleToSlug(title) {
|
|
@@ -3516,7 +3744,7 @@ function listCmd() {
|
|
|
3516
3744
|
for (const slug of slugs) {
|
|
3517
3745
|
const pg = readPage(slug);
|
|
3518
3746
|
if (!pg) continue;
|
|
3519
|
-
const size =
|
|
3747
|
+
const size = existsSync11(join10(wikiDir(), slug + ".md")) ? (readFileSync11(join10(wikiDir(), slug + ".md"), "utf8").length / 1024).toFixed(1) : "0.0";
|
|
3520
3748
|
console.log(` ${c.cyan(pg.title.padEnd(28).slice(0, 28))} ${c.dim(pg.updated || " ")} ${size} KB`);
|
|
3521
3749
|
}
|
|
3522
3750
|
console.log("");
|
|
@@ -3604,8 +3832,8 @@ function setupCmd(cfg) {
|
|
|
3604
3832
|
`));
|
|
3605
3833
|
}
|
|
3606
3834
|
function openCmd() {
|
|
3607
|
-
const target =
|
|
3608
|
-
if (!
|
|
3835
|
+
const target = existsSync11(wikiIndexPath()) ? wikiIndexPath() : wikiDir();
|
|
3836
|
+
if (!existsSync11(target)) {
|
|
3609
3837
|
console.log(c.dim('\n No wiki yet. Run "holt wiki sync" first.\n'));
|
|
3610
3838
|
return;
|
|
3611
3839
|
}
|
|
@@ -3695,20 +3923,20 @@ import {
|
|
|
3695
3923
|
mkdirSync as mkdirSync9,
|
|
3696
3924
|
writeFileSync as writeFileSync10,
|
|
3697
3925
|
readFileSync as readFileSync13,
|
|
3698
|
-
existsSync as
|
|
3926
|
+
existsSync as existsSync13,
|
|
3699
3927
|
readdirSync as readdirSync3,
|
|
3700
|
-
statSync as
|
|
3928
|
+
statSync as statSync6,
|
|
3701
3929
|
rmSync as rmSync5,
|
|
3702
3930
|
cpSync,
|
|
3703
3931
|
mkdtempSync
|
|
3704
3932
|
} from "fs";
|
|
3705
|
-
import { join as join12, resolve as
|
|
3933
|
+
import { join as join12, resolve as resolve4, sep as sep2, isAbsolute as isAbsolute4 } from "path";
|
|
3706
3934
|
import { tmpdir } from "os";
|
|
3707
3935
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3708
3936
|
|
|
3709
3937
|
// src/registry.ts
|
|
3710
|
-
import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as
|
|
3711
|
-
import { join as join11, isAbsolute as
|
|
3938
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync12, mkdirSync as mkdirSync8, statSync as statSync5 } from "fs";
|
|
3939
|
+
import { join as join11, isAbsolute as isAbsolute3, resolve as resolve3 } from "path";
|
|
3712
3940
|
import { pathToFileURL, fileURLToPath as fileURLToPath2 } from "url";
|
|
3713
3941
|
var DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/holt-os/registry/main/registry.json";
|
|
3714
3942
|
var REGISTRY_REPO_URL = "https://github.com/holt-os/registry";
|
|
@@ -3753,7 +3981,7 @@ function parseRegistry(text) {
|
|
|
3753
3981
|
function readCache() {
|
|
3754
3982
|
try {
|
|
3755
3983
|
const path = registryCachePath();
|
|
3756
|
-
if (!
|
|
3984
|
+
if (!existsSync12(path)) return null;
|
|
3757
3985
|
const raw = JSON.parse(readFileSync12(path, "utf8"));
|
|
3758
3986
|
if (!raw || typeof raw.fetchedAt !== "number" || typeof raw.url !== "string") return null;
|
|
3759
3987
|
const registry = coerceRegistry(raw.registry);
|
|
@@ -3780,12 +4008,12 @@ function localPathFor(loc) {
|
|
|
3780
4008
|
}
|
|
3781
4009
|
}
|
|
3782
4010
|
if (/^https?:\/\//i.test(loc)) return null;
|
|
3783
|
-
return
|
|
4011
|
+
return isAbsolute3(loc) ? loc : resolve3(loc);
|
|
3784
4012
|
}
|
|
3785
4013
|
var UNREACHABLE_MSG = "no registry reachable; set HOLT_REGISTRY_URL or the community registry is not live yet";
|
|
3786
4014
|
function loadLocal(path) {
|
|
3787
4015
|
try {
|
|
3788
|
-
if (!
|
|
4016
|
+
if (!existsSync12(path)) {
|
|
3789
4017
|
return { registry: null, fromCache: false, error: UNREACHABLE_MSG };
|
|
3790
4018
|
}
|
|
3791
4019
|
const registry = parseRegistry(readFileSync12(path, "utf8"));
|
|
@@ -3908,21 +4136,21 @@ function splitSubpath(source) {
|
|
|
3908
4136
|
return { base: source.slice(0, i), subpath: source.slice(i + 1) };
|
|
3909
4137
|
}
|
|
3910
4138
|
function resolveSubpathDir(fetched, subpath) {
|
|
3911
|
-
if (
|
|
4139
|
+
if (isAbsolute4(subpath)) return null;
|
|
3912
4140
|
const segments = subpath.split(/[\\/]/);
|
|
3913
4141
|
if (segments.some((s) => s === "..")) return null;
|
|
3914
|
-
const target =
|
|
3915
|
-
const root =
|
|
3916
|
-
if (target !== root && !target.startsWith(root +
|
|
4142
|
+
const target = resolve4(fetched, subpath);
|
|
4143
|
+
const root = resolve4(fetched);
|
|
4144
|
+
if (target !== root && !target.startsWith(root + sep2)) return null;
|
|
3917
4145
|
return target;
|
|
3918
4146
|
}
|
|
3919
4147
|
function findSkillDir(root) {
|
|
3920
|
-
if (
|
|
4148
|
+
if (existsSync13(join12(root, "SKILL.md"))) return root;
|
|
3921
4149
|
let subs = [];
|
|
3922
4150
|
try {
|
|
3923
4151
|
subs = readdirSync3(root).filter((f) => {
|
|
3924
4152
|
try {
|
|
3925
|
-
return
|
|
4153
|
+
return statSync6(join12(root, f)).isDirectory();
|
|
3926
4154
|
} catch {
|
|
3927
4155
|
return false;
|
|
3928
4156
|
}
|
|
@@ -3930,7 +4158,7 @@ function findSkillDir(root) {
|
|
|
3930
4158
|
} catch {
|
|
3931
4159
|
return null;
|
|
3932
4160
|
}
|
|
3933
|
-
const withSkill = subs.filter((f) =>
|
|
4161
|
+
const withSkill = subs.filter((f) => existsSync13(join12(root, f, "SKILL.md")));
|
|
3934
4162
|
if (withSkill.length === 1) return join12(root, withSkill[0]);
|
|
3935
4163
|
return null;
|
|
3936
4164
|
}
|
|
@@ -3985,7 +4213,7 @@ function cmdCreate(name, global) {
|
|
|
3985
4213
|
}
|
|
3986
4214
|
const scope = global ? "global" : "workspace";
|
|
3987
4215
|
const dir = join12(skillsRoot(scope), clean);
|
|
3988
|
-
if (
|
|
4216
|
+
if (existsSync13(dir)) {
|
|
3989
4217
|
console.error(c.red(`
|
|
3990
4218
|
A ${scope} skill named "${clean}" already exists.`));
|
|
3991
4219
|
console.error(c.dim(" " + dir + "\n"));
|
|
@@ -4016,8 +4244,8 @@ function installFromSource(source, scope) {
|
|
|
4016
4244
|
}
|
|
4017
4245
|
fetched = tempDir;
|
|
4018
4246
|
} else {
|
|
4019
|
-
fetched =
|
|
4020
|
-
if (!
|
|
4247
|
+
fetched = resolve4(base);
|
|
4248
|
+
if (!existsSync13(fetched)) {
|
|
4021
4249
|
console.error(c.red(`
|
|
4022
4250
|
No such path: ${fetched}
|
|
4023
4251
|
`));
|
|
@@ -4035,7 +4263,7 @@ function installFromSource(source, scope) {
|
|
|
4035
4263
|
process.exitCode = 1;
|
|
4036
4264
|
return false;
|
|
4037
4265
|
}
|
|
4038
|
-
if (!
|
|
4266
|
+
if (!existsSync13(target) || !statSync6(target).isDirectory()) {
|
|
4039
4267
|
console.error(c.red(`
|
|
4040
4268
|
Subpath "${subpath}" does not exist in the source.
|
|
4041
4269
|
`));
|
|
@@ -4067,7 +4295,7 @@ function installFromSource(source, scope) {
|
|
|
4067
4295
|
return false;
|
|
4068
4296
|
}
|
|
4069
4297
|
const dest = join12(skillsRoot(scope), skillName);
|
|
4070
|
-
if (
|
|
4298
|
+
if (existsSync13(dest)) {
|
|
4071
4299
|
console.error(c.red(`
|
|
4072
4300
|
A ${scope} skill named "${skillName}" already exists.`));
|
|
4073
4301
|
console.error(c.dim(" " + dest + "\n"));
|
|
@@ -4077,13 +4305,13 @@ function installFromSource(source, scope) {
|
|
|
4077
4305
|
mkdirSync9(skillsRoot(scope), { recursive: true });
|
|
4078
4306
|
cpSync(skillDir, dest, { recursive: true });
|
|
4079
4307
|
const nestedGit = join12(dest, ".git");
|
|
4080
|
-
if (
|
|
4308
|
+
if (existsSync13(nestedGit)) rmSync5(nestedGit, { recursive: true, force: true });
|
|
4081
4309
|
console.log(c.green(`
|
|
4082
4310
|
Installed ${scope} skill "${skillName}".`));
|
|
4083
4311
|
console.log(c.dim(" " + join12(dest, "SKILL.md") + "\n"));
|
|
4084
4312
|
return true;
|
|
4085
4313
|
} finally {
|
|
4086
|
-
if (tempDir &&
|
|
4314
|
+
if (tempDir && existsSync13(tempDir)) {
|
|
4087
4315
|
try {
|
|
4088
4316
|
rmSync5(tempDir, { recursive: true, force: true });
|
|
4089
4317
|
} catch {
|
|
@@ -4098,7 +4326,7 @@ async function cmdAdd(source, global, refresh) {
|
|
|
4098
4326
|
}
|
|
4099
4327
|
const scope = global ? "global" : "workspace";
|
|
4100
4328
|
const { base } = splitSubpath(source);
|
|
4101
|
-
if (isGitUrl(base) ||
|
|
4329
|
+
if (isGitUrl(base) || existsSync13(resolve4(base))) {
|
|
4102
4330
|
installFromSource(source, scope);
|
|
4103
4331
|
return;
|
|
4104
4332
|
}
|
|
@@ -4181,7 +4409,7 @@ function cmdPublish(name) {
|
|
|
4181
4409
|
description = (data.description || loaded.skill.description || "").trim();
|
|
4182
4410
|
} else {
|
|
4183
4411
|
const file = join12(process.cwd(), "SKILL.md");
|
|
4184
|
-
if (!
|
|
4412
|
+
if (!existsSync13(file)) {
|
|
4185
4413
|
console.error(c.red("\n No SKILL.md in the current directory, and no skill name given."));
|
|
4186
4414
|
console.error(c.dim(" Usage: holt skill publish [<name>]\n"));
|
|
4187
4415
|
process.exitCode = 1;
|
|
@@ -4225,9 +4453,9 @@ async function cmdRemove(name, ask) {
|
|
|
4225
4453
|
}
|
|
4226
4454
|
const wsDir = join12(skillsRoot("workspace"), clean);
|
|
4227
4455
|
const globalDir = join12(skillsRoot("global"), clean);
|
|
4228
|
-
const target =
|
|
4456
|
+
const target = existsSync13(wsDir) ? wsDir : existsSync13(globalDir) ? globalDir : "";
|
|
4229
4457
|
if (!target) {
|
|
4230
|
-
if (
|
|
4458
|
+
if (existsSync13(join12(skillsRoot("builtin"), clean))) {
|
|
4231
4459
|
console.error(c.red(`
|
|
4232
4460
|
"${clean}" is a built-in skill and cannot be removed; it ships inside Holt.`));
|
|
4233
4461
|
console.error(c.dim(" Override it instead: create a workspace or global skill with the same name.\n"));
|
|
@@ -4300,8 +4528,8 @@ async function skillCmd(sub, rest = []) {
|
|
|
4300
4528
|
}
|
|
4301
4529
|
|
|
4302
4530
|
// src/commands/graph.ts
|
|
4303
|
-
import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync10, existsSync as
|
|
4304
|
-
import { join as join14, dirname as
|
|
4531
|
+
import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync10, existsSync as existsSync14 } from "fs";
|
|
4532
|
+
import { join as join14, dirname as dirname5, resolve as resolve5 } from "path";
|
|
4305
4533
|
import { spawn as spawn4 } from "child_process";
|
|
4306
4534
|
|
|
4307
4535
|
// src/communities.ts
|
|
@@ -5098,8 +5326,8 @@ function renderGraphHtml(graph2, meta) {
|
|
|
5098
5326
|
}
|
|
5099
5327
|
|
|
5100
5328
|
// src/ingest.ts
|
|
5101
|
-
import { readdirSync as readdirSync4, readFileSync as readFileSync14, statSync as
|
|
5102
|
-
import { join as join13, relative, sep as
|
|
5329
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync14, statSync as statSync7 } from "fs";
|
|
5330
|
+
import { join as join13, relative, sep as sep3, extname, dirname as dirname4, basename as basename2 } from "path";
|
|
5103
5331
|
var CODE_EXT = /* @__PURE__ */ new Set([
|
|
5104
5332
|
"js",
|
|
5105
5333
|
"jsx",
|
|
@@ -5150,7 +5378,7 @@ var MAX_FILES = 2e3;
|
|
|
5150
5378
|
var MAX_FILE_BYTES = 400 * 1024;
|
|
5151
5379
|
var MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
5152
5380
|
function toPosix(p) {
|
|
5153
|
-
return
|
|
5381
|
+
return sep3 === "/" ? p : p.split(sep3).join("/");
|
|
5154
5382
|
}
|
|
5155
5383
|
function ingestFolder(root, want = { code: true, docs: true }) {
|
|
5156
5384
|
const files = [];
|
|
@@ -5175,7 +5403,7 @@ function ingestFolder(root, want = { code: true, docs: true }) {
|
|
|
5175
5403
|
const full = join13(dir, name);
|
|
5176
5404
|
let st;
|
|
5177
5405
|
try {
|
|
5178
|
-
st =
|
|
5406
|
+
st = statSync7(full);
|
|
5179
5407
|
} catch {
|
|
5180
5408
|
skipped.unreadable++;
|
|
5181
5409
|
continue;
|
|
@@ -5339,7 +5567,7 @@ function resolvePySpec(f, spec, byId) {
|
|
|
5339
5567
|
return null;
|
|
5340
5568
|
}
|
|
5341
5569
|
function toPosixDir(id) {
|
|
5342
|
-
const d = toPosix(
|
|
5570
|
+
const d = toPosix(dirname4(id));
|
|
5343
5571
|
return d === "" || d === "." ? "." : d;
|
|
5344
5572
|
}
|
|
5345
5573
|
var MD_LINK_RE = /\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g;
|
|
@@ -5511,7 +5739,7 @@ async function graph(args = []) {
|
|
|
5511
5739
|
const ingestCode = opts.code;
|
|
5512
5740
|
const ingestDocs = opts.docs;
|
|
5513
5741
|
const turns = loadTurns();
|
|
5514
|
-
const includeWiki = opts.wiki !== void 0 ? opts.wiki :
|
|
5742
|
+
const includeWiki = opts.wiki !== void 0 ? opts.wiki : existsSync14(wikiDir());
|
|
5515
5743
|
const wikiPages = includeWiki ? wikiGraphPages() : [];
|
|
5516
5744
|
let fileNodeCount = 0;
|
|
5517
5745
|
let docNodeCount = 0;
|
|
@@ -5541,8 +5769,8 @@ async function graph(args = []) {
|
|
|
5541
5769
|
if (codeGraph) assignCommunities(g);
|
|
5542
5770
|
if (opts.report) {
|
|
5543
5771
|
const md = renderReport(g, { workspace: workspace() });
|
|
5544
|
-
const reportPath = opts.out ?
|
|
5545
|
-
mkdirSync10(
|
|
5772
|
+
const reportPath = opts.out ? resolve5(opts.out) : join14(workspace(), "GRAPH_REPORT.md");
|
|
5773
|
+
mkdirSync10(dirname5(reportPath), { recursive: true });
|
|
5546
5774
|
writeFileSync11(reportPath, md, "utf8");
|
|
5547
5775
|
const communities = new Set(g.nodes.map((n) => n.community ?? "0")).size;
|
|
5548
5776
|
console.log("\n" + c.accent("Graph report written") + c.dim(" (this folder)"));
|
|
@@ -5563,8 +5791,8 @@ async function graph(args = []) {
|
|
|
5563
5791
|
concepts: conceptCount,
|
|
5564
5792
|
edges: g.edges.length
|
|
5565
5793
|
});
|
|
5566
|
-
const outPath = opts.out ?
|
|
5567
|
-
mkdirSync10(
|
|
5794
|
+
const outPath = opts.out ? resolve5(opts.out) : join14(wsHoltDir(), "graph.html");
|
|
5795
|
+
mkdirSync10(dirname5(outPath), { recursive: true });
|
|
5568
5796
|
writeFileSync11(outPath, html, "utf8");
|
|
5569
5797
|
console.log("\n" + c.accent("Memory graph built") + c.dim(" (this folder)"));
|
|
5570
5798
|
const parts = [`${turns.length} turns`, `${conceptCount} concepts`];
|
|
@@ -5677,7 +5905,7 @@ function recoverId(line) {
|
|
|
5677
5905
|
return null;
|
|
5678
5906
|
}
|
|
5679
5907
|
function runMcpServer(opts) {
|
|
5680
|
-
return new Promise((
|
|
5908
|
+
return new Promise((resolve9) => {
|
|
5681
5909
|
const stdin = process.stdin;
|
|
5682
5910
|
let buffer = "";
|
|
5683
5911
|
let chain = Promise.resolve();
|
|
@@ -5729,11 +5957,11 @@ function runMcpServer(opts) {
|
|
|
5729
5957
|
processLine(buffer);
|
|
5730
5958
|
buffer = "";
|
|
5731
5959
|
}
|
|
5732
|
-
chain.then(() =>
|
|
5960
|
+
chain.then(() => resolve9()).catch(() => resolve9());
|
|
5733
5961
|
});
|
|
5734
5962
|
stdin.on("error", (e) => {
|
|
5735
5963
|
console.error(`[mcp] stdin error: ${e.message}`);
|
|
5736
|
-
chain.then(() =>
|
|
5964
|
+
chain.then(() => resolve9()).catch(() => resolve9());
|
|
5737
5965
|
});
|
|
5738
5966
|
stdin.resume();
|
|
5739
5967
|
});
|
|
@@ -5853,7 +6081,7 @@ function holtTools() {
|
|
|
5853
6081
|
}
|
|
5854
6082
|
|
|
5855
6083
|
// src/version.ts
|
|
5856
|
-
var VERSION = "0.
|
|
6084
|
+
var VERSION = "0.13.0";
|
|
5857
6085
|
|
|
5858
6086
|
// src/commands/mcp.ts
|
|
5859
6087
|
function printSetup() {
|
|
@@ -5899,9 +6127,10 @@ async function mcp(sub, rest = []) {
|
|
|
5899
6127
|
}
|
|
5900
6128
|
|
|
5901
6129
|
// src/commands/hook.ts
|
|
5902
|
-
import { readFileSync as readFileSync15, writeFileSync as writeFileSync12, existsSync as
|
|
5903
|
-
import {
|
|
5904
|
-
import {
|
|
6130
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync12, existsSync as existsSync15, mkdirSync as mkdirSync11, copyFileSync, realpathSync as realpathSync3, appendFileSync as appendFileSync2, statSync as statSync8 } from "fs";
|
|
6131
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
6132
|
+
import { join as join15, dirname as dirname6 } from "path";
|
|
6133
|
+
import { homedir as homedir4 } from "os";
|
|
5905
6134
|
var EVENT = {
|
|
5906
6135
|
inject: "UserPromptSubmit",
|
|
5907
6136
|
capture: "Stop"
|
|
@@ -5911,13 +6140,13 @@ var HOOK_CMD = {
|
|
|
5911
6140
|
capture: "holt hook capture"
|
|
5912
6141
|
};
|
|
5913
6142
|
function globalSettingsPath() {
|
|
5914
|
-
return join15(
|
|
6143
|
+
return join15(homedir4(), ".claude", "settings.json");
|
|
5915
6144
|
}
|
|
5916
6145
|
function projectSettingsPath() {
|
|
5917
6146
|
return join15(process.cwd(), ".claude", "settings.json");
|
|
5918
6147
|
}
|
|
5919
6148
|
function readSettings(path) {
|
|
5920
|
-
if (!
|
|
6149
|
+
if (!existsSync15(path)) return {};
|
|
5921
6150
|
try {
|
|
5922
6151
|
const parsed = JSON.parse(readFileSync15(path, "utf8"));
|
|
5923
6152
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
@@ -5926,11 +6155,11 @@ function readSettings(path) {
|
|
|
5926
6155
|
}
|
|
5927
6156
|
}
|
|
5928
6157
|
function writeSettings(path, settings) {
|
|
5929
|
-
mkdirSync11(
|
|
6158
|
+
mkdirSync11(dirname6(path), { recursive: true });
|
|
5930
6159
|
writeFileSync12(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
5931
6160
|
}
|
|
5932
6161
|
function backup(path) {
|
|
5933
|
-
if (!
|
|
6162
|
+
if (!existsSync15(path)) return null;
|
|
5934
6163
|
const dest = `${path}.holt-bak`;
|
|
5935
6164
|
try {
|
|
5936
6165
|
copyFileSync(path, dest);
|
|
@@ -6020,7 +6249,7 @@ function remove(rest) {
|
|
|
6020
6249
|
const paths = project ? [projectSettingsPath()] : [globalSettingsPath(), projectSettingsPath()];
|
|
6021
6250
|
let touchedAny = false;
|
|
6022
6251
|
for (const path of paths) {
|
|
6023
|
-
if (!
|
|
6252
|
+
if (!existsSync15(path)) continue;
|
|
6024
6253
|
let settings;
|
|
6025
6254
|
try {
|
|
6026
6255
|
settings = readSettings(path);
|
|
@@ -6050,7 +6279,7 @@ function status(rest) {
|
|
|
6050
6279
|
];
|
|
6051
6280
|
console.log(c.accent(" Holt ambient hooks"));
|
|
6052
6281
|
for (const e of entries) {
|
|
6053
|
-
if (!
|
|
6282
|
+
if (!existsSync15(e.path)) {
|
|
6054
6283
|
console.log(c.dim(` ${e.label.padEnd(8)} ${e.path} (no settings file)`));
|
|
6055
6284
|
continue;
|
|
6056
6285
|
}
|
|
@@ -6070,13 +6299,13 @@ function status(rest) {
|
|
|
6070
6299
|
console.log(c.dim(" Runtime hooks only act in trusted Holt folders that already have .holt/memory."));
|
|
6071
6300
|
}
|
|
6072
6301
|
function readStdin(timeoutMs) {
|
|
6073
|
-
return new Promise((
|
|
6302
|
+
return new Promise((resolve9) => {
|
|
6074
6303
|
let input = "";
|
|
6075
6304
|
let done = false;
|
|
6076
6305
|
const finish = () => {
|
|
6077
6306
|
if (done) return;
|
|
6078
6307
|
done = true;
|
|
6079
|
-
|
|
6308
|
+
resolve9(input);
|
|
6080
6309
|
};
|
|
6081
6310
|
try {
|
|
6082
6311
|
process.stdin.setEncoding("utf8");
|
|
@@ -6100,9 +6329,9 @@ function parseHookInput(raw) {
|
|
|
6100
6329
|
}
|
|
6101
6330
|
}
|
|
6102
6331
|
function enterFolder(cwd) {
|
|
6103
|
-
if (!cwd || !
|
|
6332
|
+
if (!cwd || !existsSync15(cwd)) return null;
|
|
6104
6333
|
try {
|
|
6105
|
-
const real =
|
|
6334
|
+
const real = realpathSync3(cwd);
|
|
6106
6335
|
process.chdir(real);
|
|
6107
6336
|
return real;
|
|
6108
6337
|
} catch {
|
|
@@ -6111,29 +6340,117 @@ function enterFolder(cwd) {
|
|
|
6111
6340
|
}
|
|
6112
6341
|
function guardTrusted(real) {
|
|
6113
6342
|
if (!isTrusted(real)) return false;
|
|
6114
|
-
if (!
|
|
6343
|
+
if (!existsSync15(memDir())) return false;
|
|
6115
6344
|
return true;
|
|
6116
6345
|
}
|
|
6346
|
+
function hookLog(msg) {
|
|
6347
|
+
try {
|
|
6348
|
+
mkdirSync11(GLOBAL_DIR, { recursive: true });
|
|
6349
|
+
appendFileSync2(join15(GLOBAL_DIR, "hooks.log"), `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
6350
|
+
`, "utf8");
|
|
6351
|
+
} catch {
|
|
6352
|
+
}
|
|
6353
|
+
}
|
|
6354
|
+
function fileBytes(path) {
|
|
6355
|
+
if (!path) return 0;
|
|
6356
|
+
try {
|
|
6357
|
+
return statSync8(path).size;
|
|
6358
|
+
} catch {
|
|
6359
|
+
return 0;
|
|
6360
|
+
}
|
|
6361
|
+
}
|
|
6362
|
+
var THROTTLE_MIN_NEW_EXCHANGES = 2;
|
|
6363
|
+
function hookStatePath() {
|
|
6364
|
+
return join15(GLOBAL_DIR, "hook-state.json");
|
|
6365
|
+
}
|
|
6366
|
+
function readHookState() {
|
|
6367
|
+
try {
|
|
6368
|
+
const parsed = JSON.parse(readFileSync15(hookStatePath(), "utf8"));
|
|
6369
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
6370
|
+
return parsed;
|
|
6371
|
+
} catch {
|
|
6372
|
+
return {};
|
|
6373
|
+
}
|
|
6374
|
+
}
|
|
6375
|
+
function lastProcessedExchanges(state, sessionId) {
|
|
6376
|
+
const rec = state[sessionId];
|
|
6377
|
+
return rec && typeof rec.lastExchanges === "number" ? rec.lastExchanges : 0;
|
|
6378
|
+
}
|
|
6379
|
+
function saveHookState(sessionId, exchanges) {
|
|
6380
|
+
try {
|
|
6381
|
+
mkdirSync11(GLOBAL_DIR, { recursive: true });
|
|
6382
|
+
const state = readHookState();
|
|
6383
|
+
state[sessionId] = { lastExchanges: exchanges };
|
|
6384
|
+
writeFileSync12(hookStatePath(), JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
6385
|
+
} catch {
|
|
6386
|
+
}
|
|
6387
|
+
}
|
|
6117
6388
|
async function inject() {
|
|
6389
|
+
const debug = !!process.env.HOLT_HOOK_DEBUG;
|
|
6390
|
+
const dlog = (m) => {
|
|
6391
|
+
if (debug) hookLog(m);
|
|
6392
|
+
};
|
|
6118
6393
|
try {
|
|
6119
6394
|
const raw = await readStdin(2e3);
|
|
6120
6395
|
const data = parseHookInput(raw);
|
|
6396
|
+
dlog(
|
|
6397
|
+
`inject cwd=${data.cwd ?? "(none)"} event=${data.hook_event_name ?? "UserPromptSubmit"} hasCwd=${!!data.cwd} promptLen=${(data.prompt || "").trim().length}`
|
|
6398
|
+
);
|
|
6121
6399
|
const prompt = (data.prompt || "").trim();
|
|
6122
|
-
if (prompt.length < 3)
|
|
6400
|
+
if (prompt.length < 3) {
|
|
6401
|
+
dlog("inject -> prompt too short");
|
|
6402
|
+
return;
|
|
6403
|
+
}
|
|
6123
6404
|
const real = enterFolder(data.cwd);
|
|
6124
|
-
if (!real)
|
|
6125
|
-
|
|
6405
|
+
if (!real) {
|
|
6406
|
+
dlog("inject -> no cwd");
|
|
6407
|
+
return;
|
|
6408
|
+
}
|
|
6409
|
+
if (!guardTrusted(real)) {
|
|
6410
|
+
dlog(`inject -> untrusted or no .holt/memory (real=${real})`);
|
|
6411
|
+
return;
|
|
6412
|
+
}
|
|
6126
6413
|
const session = data.session_id || newSessionId();
|
|
6127
6414
|
const hits = await recall(prompt, session, 4);
|
|
6128
|
-
if (!hits.length)
|
|
6415
|
+
if (!hits.length) {
|
|
6416
|
+
dlog("inject -> recalled 0 notes");
|
|
6417
|
+
return;
|
|
6418
|
+
}
|
|
6129
6419
|
const lines = hits.map((h) => `- ${h.turn.content.replace(/\s+/g, " ").trim().slice(0, 500)}`);
|
|
6130
6420
|
process.stdout.write(
|
|
6131
|
-
[
|
|
6421
|
+
[
|
|
6422
|
+
"Background on this user, recalled from earlier sessions (use it directly, do not mention looking it up):",
|
|
6423
|
+
...lines,
|
|
6424
|
+
""
|
|
6425
|
+
].join("\n")
|
|
6132
6426
|
);
|
|
6427
|
+
dlog(`inject -> injected ${hits.length} note(s)`);
|
|
6133
6428
|
} catch (e) {
|
|
6134
6429
|
process.stderr.write("holt hook inject: " + (e instanceof Error ? e.message : String(e)) + "\n");
|
|
6430
|
+
dlog("inject -> error: " + (e instanceof Error ? e.message : String(e)));
|
|
6135
6431
|
}
|
|
6136
6432
|
}
|
|
6433
|
+
function countLines(raw) {
|
|
6434
|
+
let n = 0;
|
|
6435
|
+
for (const line of raw.split("\n")) if (line.trim()) n++;
|
|
6436
|
+
return n;
|
|
6437
|
+
}
|
|
6438
|
+
function textFromContent(content) {
|
|
6439
|
+
if (typeof content === "string") return content.trim();
|
|
6440
|
+
if (!Array.isArray(content)) return "";
|
|
6441
|
+
const parts = [];
|
|
6442
|
+
for (const b of content) {
|
|
6443
|
+
if (typeof b === "string") {
|
|
6444
|
+
parts.push(b);
|
|
6445
|
+
continue;
|
|
6446
|
+
}
|
|
6447
|
+
if (!b || typeof b !== "object") continue;
|
|
6448
|
+
const blk = b;
|
|
6449
|
+
if (blk.type === "tool_use" || blk.type === "tool_result" || blk.type === "thinking") continue;
|
|
6450
|
+
if (typeof blk.text === "string") parts.push(blk.text);
|
|
6451
|
+
}
|
|
6452
|
+
return parts.join(" ").trim();
|
|
6453
|
+
}
|
|
6137
6454
|
function readTranscript(path) {
|
|
6138
6455
|
const out = [];
|
|
6139
6456
|
let raw;
|
|
@@ -6150,56 +6467,142 @@ function readTranscript(path) {
|
|
|
6150
6467
|
} catch {
|
|
6151
6468
|
continue;
|
|
6152
6469
|
}
|
|
6470
|
+
if (!rec || typeof rec !== "object") continue;
|
|
6153
6471
|
const d = rec;
|
|
6154
|
-
|
|
6155
|
-
const m = d.message;
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
if (
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
text = m.content.map((b) => {
|
|
6162
|
-
if (typeof b === "string") return b;
|
|
6163
|
-
if (b && typeof b.text === "string") return b.text;
|
|
6164
|
-
return "";
|
|
6165
|
-
}).join(" ").trim();
|
|
6166
|
-
}
|
|
6472
|
+
const topType = d.type === "user" || d.type === "assistant" ? d.type : void 0;
|
|
6473
|
+
const m = d.message && typeof d.message === "object" ? d.message : void 0;
|
|
6474
|
+
const msgRole = m && (m.role === "user" || m.role === "assistant") ? m.role : void 0;
|
|
6475
|
+
const role = msgRole ?? topType;
|
|
6476
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
6477
|
+
let text = m ? textFromContent(m.content) : "";
|
|
6478
|
+
if (!text) text = textFromContent(d.content);
|
|
6167
6479
|
text = text.trim();
|
|
6168
6480
|
if (text.length < 3) continue;
|
|
6169
|
-
out.push({ role
|
|
6481
|
+
out.push({ role, content: text });
|
|
6170
6482
|
}
|
|
6171
6483
|
return out;
|
|
6172
6484
|
}
|
|
6485
|
+
function resolveCommandPath(command) {
|
|
6486
|
+
if (command.startsWith("/") && existsSync15(command)) return command;
|
|
6487
|
+
try {
|
|
6488
|
+
const shell = process.env.SHELL || "/bin/sh";
|
|
6489
|
+
const res = spawnSync3(shell, ["-lc", "command -v " + command], { encoding: "utf8" });
|
|
6490
|
+
if (res.status === 0) {
|
|
6491
|
+
const p = (res.stdout || "").trim().split("\n")[0]?.trim();
|
|
6492
|
+
if (p && p.startsWith("/") && existsSync15(p)) return p;
|
|
6493
|
+
}
|
|
6494
|
+
} catch {
|
|
6495
|
+
}
|
|
6496
|
+
const home = homedir4();
|
|
6497
|
+
const dirs = [
|
|
6498
|
+
join15(home, ".local", "bin"),
|
|
6499
|
+
"/opt/homebrew/bin",
|
|
6500
|
+
"/usr/local/bin",
|
|
6501
|
+
join15(home, ".npm-global", "bin"),
|
|
6502
|
+
"/usr/bin",
|
|
6503
|
+
"/bin"
|
|
6504
|
+
];
|
|
6505
|
+
for (const dir of dirs) {
|
|
6506
|
+
const candidate = join15(dir, command);
|
|
6507
|
+
if (existsSync15(candidate)) return candidate;
|
|
6508
|
+
}
|
|
6509
|
+
return null;
|
|
6510
|
+
}
|
|
6173
6511
|
function resolveExtractionBrain(cfg) {
|
|
6174
6512
|
const id = cfg.defaultBrain;
|
|
6175
|
-
|
|
6513
|
+
const diag = { defaultBrain: id ?? "(none)", command: "(none)", whichFound: false, resolvedPath: null };
|
|
6514
|
+
if (!id) return { brain: null, diag };
|
|
6176
6515
|
if (BRAIN_IDS.includes(id)) {
|
|
6177
6516
|
const bid = id;
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
return {
|
|
6517
|
+
const command = cfg.brains[bid].command;
|
|
6518
|
+
diag.command = command;
|
|
6519
|
+
if (!cfg.brains[bid].enabled) return { brain: null, diag };
|
|
6520
|
+
const whichFound = isInstalled(command);
|
|
6521
|
+
diag.whichFound = whichFound;
|
|
6522
|
+
if (whichFound) {
|
|
6523
|
+
return { brain: { kind: "cli", id: bid }, diag };
|
|
6524
|
+
}
|
|
6525
|
+
const resolved = resolveCommandPath(command);
|
|
6526
|
+
diag.resolvedPath = resolved;
|
|
6527
|
+
if (resolved) {
|
|
6528
|
+
return { brain: { kind: "cli", id: bid, commandOverride: resolved }, diag };
|
|
6529
|
+
}
|
|
6530
|
+
return { brain: null, diag };
|
|
6181
6531
|
}
|
|
6182
6532
|
const api = findApiBrain(cfg, id);
|
|
6183
|
-
if (api && resolveApiKey(api)) return { kind: "api", brain: api };
|
|
6184
|
-
return null;
|
|
6533
|
+
if (api && resolveApiKey(api)) return { brain: { kind: "api", brain: api }, diag };
|
|
6534
|
+
return { brain: null, diag };
|
|
6185
6535
|
}
|
|
6186
6536
|
async function capture() {
|
|
6537
|
+
const cwdForLog = process.cwd();
|
|
6187
6538
|
try {
|
|
6188
6539
|
const raw = await readStdin(3e3);
|
|
6189
6540
|
const data = parseHookInput(raw);
|
|
6541
|
+
hookLog(
|
|
6542
|
+
`capture-fields event=${data.hook_event_name ?? "Stop"} hasCwd=${!!data.cwd} hasTranscriptPath=${!!data.transcript_path} transcriptBytes=${fileBytes(data.transcript_path)} stdinBytes=${raw.length}`
|
|
6543
|
+
);
|
|
6190
6544
|
const real = enterFolder(data.cwd);
|
|
6191
|
-
if (!real)
|
|
6192
|
-
|
|
6545
|
+
if (!real) {
|
|
6546
|
+
hookLog(`capture cwd=${data.cwd ?? cwdForLog} event=Stop -> no cwd`);
|
|
6547
|
+
return;
|
|
6548
|
+
}
|
|
6549
|
+
if (!guardTrusted(real)) {
|
|
6550
|
+
hookLog(`capture cwd=${real} event=Stop -> untrusted or no .holt/memory (real=${real})`);
|
|
6551
|
+
return;
|
|
6552
|
+
}
|
|
6193
6553
|
const cfg = loadConfig();
|
|
6194
|
-
if (!cfg)
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
if (
|
|
6554
|
+
if (!cfg) {
|
|
6555
|
+
hookLog(`capture cwd=${real} event=Stop -> no config`);
|
|
6556
|
+
return;
|
|
6557
|
+
}
|
|
6558
|
+
if (cfg.memory?.extractFacts === false) {
|
|
6559
|
+
hookLog(`capture cwd=${real} event=Stop -> extractFacts off`);
|
|
6560
|
+
return;
|
|
6561
|
+
}
|
|
6562
|
+
const { brain, diag } = resolveExtractionBrain(cfg);
|
|
6563
|
+
if (!brain) {
|
|
6564
|
+
hookLog(
|
|
6565
|
+
`capture cwd=${real} event=Stop -> no usable brain (defaultBrain=${diag.defaultBrain} command=${diag.command} whichFound=${diag.whichFound} resolvedPath=${diag.resolvedPath ?? "none"})`
|
|
6566
|
+
);
|
|
6567
|
+
return;
|
|
6568
|
+
}
|
|
6569
|
+
if (brain.kind === "cli" && brain.commandOverride) {
|
|
6570
|
+
hookLog(`capture cwd=${real} event=Stop -> brain resolved via absolute path (resolvedPath=${brain.commandOverride})`);
|
|
6571
|
+
}
|
|
6572
|
+
if (!data.transcript_path) {
|
|
6573
|
+
hookLog(`capture cwd=${real} event=Stop -> no transcript_path`);
|
|
6574
|
+
return;
|
|
6575
|
+
}
|
|
6199
6576
|
const history = readTranscript(data.transcript_path);
|
|
6200
|
-
if (!history.length)
|
|
6577
|
+
if (!history.length) {
|
|
6578
|
+
let bytes = 0;
|
|
6579
|
+
let lines = 0;
|
|
6580
|
+
try {
|
|
6581
|
+
const rawT = readFileSync15(data.transcript_path, "utf8");
|
|
6582
|
+
bytes = Buffer.byteLength(rawT, "utf8");
|
|
6583
|
+
lines = countLines(rawT);
|
|
6584
|
+
} catch {
|
|
6585
|
+
}
|
|
6586
|
+
hookLog(`capture cwd=${real} event=Stop -> transcript parsed 0 turns (path=${data.transcript_path} bytes=${bytes} lines=${lines})`);
|
|
6587
|
+
return;
|
|
6588
|
+
}
|
|
6589
|
+
const exchanges = Math.floor(history.length / 2);
|
|
6590
|
+
if (exchanges < MIN_EXCHANGES_FOR_EXTRACTION) {
|
|
6591
|
+
hookLog(`capture cwd=${real} event=Stop -> only ${exchanges} exchange(s) (<${MIN_EXCHANGES_FOR_EXTRACTION}, skipped)`);
|
|
6592
|
+
return;
|
|
6593
|
+
}
|
|
6594
|
+
const throttleId = data.session_id || null;
|
|
6595
|
+
const lastProcessed = throttleId ? lastProcessedExchanges(readHookState(), throttleId) : 0;
|
|
6596
|
+
if (throttleId && exchanges - lastProcessed < THROTTLE_MIN_NEW_EXCHANGES) {
|
|
6597
|
+
hookLog(
|
|
6598
|
+
`capture cwd=${real} event=Stop -> throttled (exchanges=${exchanges} lastProcessed=${lastProcessed}, need +${THROTTLE_MIN_NEW_EXCHANGES})`
|
|
6599
|
+
);
|
|
6600
|
+
return;
|
|
6601
|
+
}
|
|
6201
6602
|
const session = "hook-" + (data.session_id || newSessionId());
|
|
6202
6603
|
const n = await extractAndSaveFacts(brain, cfg, history, session);
|
|
6604
|
+
if (throttleId) saveHookState(throttleId, exchanges);
|
|
6605
|
+
hookLog(`capture cwd=${real} event=Stop -> saved ${n} fact${n === 1 ? "" : "s"}`);
|
|
6203
6606
|
if (n > 0) process.stderr.write(`holt hook capture: saved ${n} fact${n === 1 ? "" : "s"} to ${memDir()}
|
|
6204
6607
|
`);
|
|
6205
6608
|
if (cfg.wiki?.autoSync) {
|
|
@@ -6212,7 +6615,9 @@ async function capture() {
|
|
|
6212
6615
|
}
|
|
6213
6616
|
}
|
|
6214
6617
|
} catch (e) {
|
|
6215
|
-
|
|
6618
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
6619
|
+
process.stderr.write("holt hook capture: " + msg + "\n");
|
|
6620
|
+
hookLog(`capture cwd=${cwdForLog} event=Stop -> error: ${msg}`);
|
|
6216
6621
|
}
|
|
6217
6622
|
}
|
|
6218
6623
|
var USAGE2 = [
|
|
@@ -6269,7 +6674,7 @@ async function hook(sub, rest = []) {
|
|
|
6269
6674
|
|
|
6270
6675
|
// src/commands/run.ts
|
|
6271
6676
|
import { writeFileSync as writeFileSync13 } from "fs";
|
|
6272
|
-
import { resolve as
|
|
6677
|
+
import { resolve as resolve6 } from "path";
|
|
6273
6678
|
var USAGE3 = [
|
|
6274
6679
|
" holt run <task> [options]",
|
|
6275
6680
|
"",
|
|
@@ -6358,7 +6763,7 @@ async function run(args) {
|
|
|
6358
6763
|
process.stdout.write("\n");
|
|
6359
6764
|
}
|
|
6360
6765
|
if (p.out) {
|
|
6361
|
-
const target =
|
|
6766
|
+
const target = resolve6(process.cwd(), p.out);
|
|
6362
6767
|
try {
|
|
6363
6768
|
writeFileSync13(target, result.text, "utf8");
|
|
6364
6769
|
process.stderr.write(c.dim(`saved to ${target}`) + "\n");
|
|
@@ -6370,16 +6775,16 @@ async function run(args) {
|
|
|
6370
6775
|
}
|
|
6371
6776
|
|
|
6372
6777
|
// src/commands/schedule.ts
|
|
6373
|
-
import { spawnSync as
|
|
6374
|
-
import { writeFileSync as writeFileSync15, mkdirSync as mkdirSync13, rmSync as rmSync6, existsSync as
|
|
6375
|
-
import { dirname as
|
|
6778
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
6779
|
+
import { writeFileSync as writeFileSync15, mkdirSync as mkdirSync13, rmSync as rmSync6, existsSync as existsSync16 } from "fs";
|
|
6780
|
+
import { dirname as dirname7 } from "path";
|
|
6376
6781
|
|
|
6377
6782
|
// src/scheduler.ts
|
|
6378
|
-
import { homedir as
|
|
6783
|
+
import { homedir as homedir5 } from "os";
|
|
6379
6784
|
import { join as join16 } from "path";
|
|
6380
6785
|
import { readFileSync as readFileSync16, writeFileSync as writeFileSync14, mkdirSync as mkdirSync12 } from "fs";
|
|
6381
6786
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
6382
|
-
import { spawnSync as
|
|
6787
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
6383
6788
|
function schedulesPath() {
|
|
6384
6789
|
return join16(GLOBAL_DIR, "schedules.json");
|
|
6385
6790
|
}
|
|
@@ -6390,7 +6795,7 @@ function logPath(id) {
|
|
|
6390
6795
|
return join16(logsDir(), `${id}.log`);
|
|
6391
6796
|
}
|
|
6392
6797
|
function plistPath(id) {
|
|
6393
|
-
return join16(
|
|
6798
|
+
return join16(homedir5(), "Library", "LaunchAgents", `com.holt.${id}.plist`);
|
|
6394
6799
|
}
|
|
6395
6800
|
function loadJobs() {
|
|
6396
6801
|
try {
|
|
@@ -6435,7 +6840,7 @@ function shQuote(s) {
|
|
|
6435
6840
|
function resolveHoltPath() {
|
|
6436
6841
|
try {
|
|
6437
6842
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
6438
|
-
const res =
|
|
6843
|
+
const res = spawnSync4(finder, ["holt"], { encoding: "utf8" });
|
|
6439
6844
|
if (res.status === 0 && typeof res.stdout === "string") {
|
|
6440
6845
|
const first = res.stdout.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
6441
6846
|
if (first) return first;
|
|
@@ -6536,9 +6941,9 @@ function parseAdd(rest) {
|
|
|
6536
6941
|
}
|
|
6537
6942
|
function installDarwin(job, holtPath) {
|
|
6538
6943
|
const path = plistPath(job.id);
|
|
6539
|
-
mkdirSync13(
|
|
6944
|
+
mkdirSync13(dirname7(path), { recursive: true });
|
|
6540
6945
|
writeFileSync15(path, buildLaunchdPlist(job, holtPath), "utf8");
|
|
6541
|
-
const res =
|
|
6946
|
+
const res = spawnSync5("launchctl", ["load", "-w", path], { encoding: "utf8" });
|
|
6542
6947
|
if (res.status === 0) {
|
|
6543
6948
|
console.log(c.green(" Loaded into launchd."));
|
|
6544
6949
|
} else {
|
|
@@ -6549,11 +6954,11 @@ function installDarwin(job, holtPath) {
|
|
|
6549
6954
|
console.log(c.dim(` plist: ${path}`));
|
|
6550
6955
|
}
|
|
6551
6956
|
function currentCrontab() {
|
|
6552
|
-
const res =
|
|
6957
|
+
const res = spawnSync5("crontab", ["-l"], { encoding: "utf8" });
|
|
6553
6958
|
return res.status === 0 && typeof res.stdout === "string" ? res.stdout : "";
|
|
6554
6959
|
}
|
|
6555
6960
|
function writeCrontab(body) {
|
|
6556
|
-
const res =
|
|
6961
|
+
const res = spawnSync5("crontab", ["-"], { input: body, encoding: "utf8" });
|
|
6557
6962
|
return res.status === 0;
|
|
6558
6963
|
}
|
|
6559
6964
|
function installLinux(job, holtPath) {
|
|
@@ -6625,8 +7030,8 @@ function listCmd2() {
|
|
|
6625
7030
|
}
|
|
6626
7031
|
function removeDarwin(id) {
|
|
6627
7032
|
const path = plistPath(id);
|
|
6628
|
-
if (
|
|
6629
|
-
|
|
7033
|
+
if (existsSync16(path)) {
|
|
7034
|
+
spawnSync5("launchctl", ["unload", path], { encoding: "utf8" });
|
|
6630
7035
|
try {
|
|
6631
7036
|
rmSync6(path);
|
|
6632
7037
|
} catch {
|
|
@@ -6634,10 +7039,10 @@ function removeDarwin(id) {
|
|
|
6634
7039
|
}
|
|
6635
7040
|
}
|
|
6636
7041
|
function removeLinux(id) {
|
|
6637
|
-
const res =
|
|
7042
|
+
const res = spawnSync5("crontab", ["-l"], { encoding: "utf8" });
|
|
6638
7043
|
const body = res.status === 0 && typeof res.stdout === "string" ? res.stdout : "";
|
|
6639
7044
|
const next = stripCronLines(body, id);
|
|
6640
|
-
|
|
7045
|
+
spawnSync5("crontab", ["-"], { input: next, encoding: "utf8" });
|
|
6641
7046
|
}
|
|
6642
7047
|
function removeCmd(rest) {
|
|
6643
7048
|
const id = rest[0];
|
|
@@ -6687,9 +7092,9 @@ async function schedule(sub, rest = []) {
|
|
|
6687
7092
|
}
|
|
6688
7093
|
|
|
6689
7094
|
// src/commands/routine.ts
|
|
6690
|
-
import { spawnSync as
|
|
6691
|
-
import { writeFileSync as writeFileSync17, mkdirSync as mkdirSync15, rmSync as rmSync7, existsSync as
|
|
6692
|
-
import { dirname as
|
|
7095
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
7096
|
+
import { writeFileSync as writeFileSync17, mkdirSync as mkdirSync15, rmSync as rmSync7, existsSync as existsSync17 } from "fs";
|
|
7097
|
+
import { dirname as dirname8, resolve as resolve7 } from "path";
|
|
6693
7098
|
|
|
6694
7099
|
// src/routines.ts
|
|
6695
7100
|
import { readFileSync as readFileSync17, writeFileSync as writeFileSync16, mkdirSync as mkdirSync14 } from "fs";
|
|
@@ -6770,9 +7175,9 @@ function routineJobId(name) {
|
|
|
6770
7175
|
}
|
|
6771
7176
|
function installDarwin2(job, holtPath) {
|
|
6772
7177
|
const path = plistPath(job.id);
|
|
6773
|
-
mkdirSync15(
|
|
7178
|
+
mkdirSync15(dirname8(path), { recursive: true });
|
|
6774
7179
|
writeFileSync17(path, buildLaunchdPlist(job, holtPath), "utf8");
|
|
6775
|
-
const res =
|
|
7180
|
+
const res = spawnSync6("launchctl", ["load", "-w", path], { encoding: "utf8" });
|
|
6776
7181
|
if (res.status === 0) {
|
|
6777
7182
|
console.log(c.green(" Loaded into launchd."));
|
|
6778
7183
|
} else {
|
|
@@ -6783,11 +7188,11 @@ function installDarwin2(job, holtPath) {
|
|
|
6783
7188
|
console.log(c.dim(` plist: ${path}`));
|
|
6784
7189
|
}
|
|
6785
7190
|
function currentCrontab2() {
|
|
6786
|
-
const res =
|
|
7191
|
+
const res = spawnSync6("crontab", ["-l"], { encoding: "utf8" });
|
|
6787
7192
|
return res.status === 0 && typeof res.stdout === "string" ? res.stdout : "";
|
|
6788
7193
|
}
|
|
6789
7194
|
function writeCrontab2(body) {
|
|
6790
|
-
const res =
|
|
7195
|
+
const res = spawnSync6("crontab", ["-"], { input: body, encoding: "utf8" });
|
|
6791
7196
|
return res.status === 0;
|
|
6792
7197
|
}
|
|
6793
7198
|
function installLinux2(job, holtPath) {
|
|
@@ -6815,8 +7220,8 @@ function installSchedule(r) {
|
|
|
6815
7220
|
}
|
|
6816
7221
|
function removeScheduleDarwin(id) {
|
|
6817
7222
|
const path = plistPath(id);
|
|
6818
|
-
if (
|
|
6819
|
-
|
|
7223
|
+
if (existsSync17(path)) {
|
|
7224
|
+
spawnSync6("launchctl", ["unload", path], { encoding: "utf8" });
|
|
6820
7225
|
try {
|
|
6821
7226
|
rmSync7(path);
|
|
6822
7227
|
} catch {
|
|
@@ -7009,7 +7414,7 @@ async function runCmd(rest) {
|
|
|
7009
7414
|
if (!quiet && !streamed) process.stdout.write(result.text);
|
|
7010
7415
|
if (!quiet && (streamed || result.text) && !result.text.endsWith("\n")) process.stdout.write("\n");
|
|
7011
7416
|
if (outTarget) {
|
|
7012
|
-
const target =
|
|
7417
|
+
const target = resolve7(routine2.workspace, outTarget);
|
|
7013
7418
|
try {
|
|
7014
7419
|
writeFileSync17(target, result.text, "utf8");
|
|
7015
7420
|
if (!quiet) process.stderr.write(c.dim(`saved to ${target}`) + "\n");
|
|
@@ -7394,7 +7799,7 @@ async function doctor() {
|
|
|
7394
7799
|
|
|
7395
7800
|
// src/commands/write.ts
|
|
7396
7801
|
import { writeFileSync as writeFileSync18 } from "fs";
|
|
7397
|
-
import { resolve as
|
|
7802
|
+
import { resolve as resolve8 } from "path";
|
|
7398
7803
|
|
|
7399
7804
|
// src/antiai.ts
|
|
7400
7805
|
var EM_DASH = String.fromCharCode(8212);
|
|
@@ -7572,7 +7977,7 @@ async function write(args) {
|
|
|
7572
7977
|
draft = stripEmDash(draft).trimEnd();
|
|
7573
7978
|
process.stdout.write(draft + "\n");
|
|
7574
7979
|
if (p.out) {
|
|
7575
|
-
const target =
|
|
7980
|
+
const target = resolve8(process.cwd(), p.out);
|
|
7576
7981
|
try {
|
|
7577
7982
|
writeFileSync18(target, draft + "\n", "utf8");
|
|
7578
7983
|
process.stderr.write(c.dim(`saved to ${target}`) + "\n");
|