@holt-os/holt 0.12.1 → 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 +13 -0
- package/dist/cli.js +268 -101
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,14 +71,27 @@ Inside `holt chat`:
|
|
|
71
71
|
/memory memory stats. /memory <query> previews what recall would surface
|
|
72
72
|
/output show or set the save format: markdown | html
|
|
73
73
|
/save [name] save the last reply to a file in this folder
|
|
74
|
+
/allow <path> let this session read a folder outside this one (session-only)
|
|
75
|
+
/allowed list folders granted outside-access this session
|
|
74
76
|
/setting configure brains, API brains, and your launch command
|
|
75
77
|
/clear forget this session (saved memory stays)
|
|
76
78
|
/help show commands
|
|
77
79
|
/exit leave
|
|
78
80
|
```
|
|
79
81
|
|
|
82
|
+
A line that starts with `/` is treated as a command only when its first word is a real command. So a message like `/Users/me/resume.docx summarize this` (or a URL, or any sentence that opens with a slash) is sent to the brain as-is, never dropped as an "unknown command".
|
|
83
|
+
|
|
80
84
|
The point of `/brain`: Holt owns the transcript, so you can start a thread on one model and hand it to another mid-conversation. The new brain picks up with the full context.
|
|
81
85
|
|
|
86
|
+
### Reading files outside this folder
|
|
87
|
+
|
|
88
|
+
Holt runs the brain in the folder you launched it from, so by default the brain only sees this folder. If a message references an absolute path (`/...` or `~/...`) that exists **outside** the folder, Holt asks first: `Allow this session to access <dir> (outside this folder)? [y/N]`. Answer `y` and it grants the file's **containing directory** read access for the rest of this chat. Answer `n` and the message still goes through, the brain just does not get that folder.
|
|
89
|
+
|
|
90
|
+
- `/allow <path>` grants a folder up front with no prompt.
|
|
91
|
+
- `/allowed` lists what you have granted.
|
|
92
|
+
|
|
93
|
+
Grants are **in-memory and session-scoped**: nothing is written to disk, and they reset the next time you run `holt chat`. Access is **read-oriented** and only works with a **Claude Code** brain: Holt passes `--add-dir=<dir>` for each granted folder plus `--allowedTools=Read,Glob,Grep` so Claude Code can read (but not write) those files without an interactive permission prompt. With a Codex/Gemini or API brain, external file access is unavailable and Holt says so instead of adding flags. (If you need to tune the flags, they live in one place: `claudeAccessArgs` in `src/access.ts`.)
|
|
94
|
+
|
|
82
95
|
## Memory
|
|
83
96
|
|
|
84
97
|
Every exchange is saved to `<folder>/.holt/memory/turns.jsonl`, private and local. On each message, Holt recalls the most relevant moments from your *past* sessions in that folder and hands them to the brain, so it remembers what you told it last week.
|
package/dist/cli.js
CHANGED
|
@@ -105,11 +105,11 @@ function createReader() {
|
|
|
105
105
|
closed = true;
|
|
106
106
|
while (waiters.length) waiters.shift()(null);
|
|
107
107
|
});
|
|
108
|
-
const ask = (q) => new Promise((
|
|
108
|
+
const ask = (q) => new Promise((resolve9) => {
|
|
109
109
|
if (q) process.stdout.write(q);
|
|
110
|
-
if (buffer.length)
|
|
111
|
-
else if (closed)
|
|
112
|
-
else waiters.push(
|
|
110
|
+
if (buffer.length) resolve9(buffer.shift());
|
|
111
|
+
else if (closed) resolve9(null);
|
|
112
|
+
else waiters.push(resolve9);
|
|
113
113
|
});
|
|
114
114
|
return { ask, close: () => rl.close() };
|
|
115
115
|
}
|
|
@@ -307,13 +307,13 @@ function renderPrompt(history, message, memory = []) {
|
|
|
307
307
|
parts.push("", `User: ${message}`, "", "Assistant:");
|
|
308
308
|
return parts.join("\n");
|
|
309
309
|
}
|
|
310
|
-
function runBrain(brain, prompt, onChunk) {
|
|
311
|
-
return new Promise((
|
|
310
|
+
function runBrain(brain, prompt, onChunk, extraArgs = []) {
|
|
311
|
+
return new Promise((resolve9) => {
|
|
312
312
|
let child;
|
|
313
313
|
try {
|
|
314
|
-
child = spawn(brain.command, [...brain.args, prompt], { stdio: ["ignore", "pipe", "pipe"] });
|
|
314
|
+
child = spawn(brain.command, [...brain.args, ...extraArgs, prompt], { stdio: ["ignore", "pipe", "pipe"] });
|
|
315
315
|
} catch (e) {
|
|
316
|
-
|
|
316
|
+
resolve9({ ok: false, text: `Could not launch "${brain.command}": ${e.message}` });
|
|
317
317
|
return;
|
|
318
318
|
}
|
|
319
319
|
let out = "";
|
|
@@ -326,11 +326,11 @@ function runBrain(brain, prompt, onChunk) {
|
|
|
326
326
|
child.stderr.on("data", (d) => {
|
|
327
327
|
err2 += d.toString();
|
|
328
328
|
});
|
|
329
|
-
child.on("error", (e) =>
|
|
329
|
+
child.on("error", (e) => resolve9({ ok: false, text: `Could not run "${brain.command}": ${e.message}` }));
|
|
330
330
|
child.on("close", (code) => {
|
|
331
331
|
const text = out.trim();
|
|
332
|
-
if (code === 0 && text)
|
|
333
|
-
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}` });
|
|
334
334
|
});
|
|
335
335
|
});
|
|
336
336
|
}
|
|
@@ -500,16 +500,16 @@ function currentAlias() {
|
|
|
500
500
|
// src/install.ts
|
|
501
501
|
import { spawn as spawn2 } from "child_process";
|
|
502
502
|
function runInteractive(cmd, args) {
|
|
503
|
-
return new Promise((
|
|
503
|
+
return new Promise((resolve9) => {
|
|
504
504
|
let child;
|
|
505
505
|
try {
|
|
506
506
|
child = spawn2(cmd, args, { stdio: "inherit" });
|
|
507
507
|
} catch {
|
|
508
|
-
|
|
508
|
+
resolve9(-1);
|
|
509
509
|
return;
|
|
510
510
|
}
|
|
511
|
-
child.on("error", () =>
|
|
512
|
-
child.on("close", (code) =>
|
|
511
|
+
child.on("error", () => resolve9(-1));
|
|
512
|
+
child.on("close", (code) => resolve9(code ?? -1));
|
|
513
513
|
});
|
|
514
514
|
}
|
|
515
515
|
|
|
@@ -2982,6 +2982,77 @@ function saveReply(reply, format, name) {
|
|
|
2982
2982
|
return path;
|
|
2983
2983
|
}
|
|
2984
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
|
+
|
|
2985
3056
|
// src/commands/chat.ts
|
|
2986
3057
|
function statusLine(brainLabel, history) {
|
|
2987
3058
|
const live = Math.min(history.length, MAX_REPLAY_TURNS);
|
|
@@ -2989,6 +3060,25 @@ function statusLine(brainLabel, history) {
|
|
|
2989
3060
|
const mem = memStats().turns;
|
|
2990
3061
|
return c.dim(" " + brainLabel + " ") + c.dim("[") + bar(frac) + c.dim("]") + c.dim(` recent ${live}/${MAX_REPLAY_TURNS} \xB7 ${mem} in memory`);
|
|
2991
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
|
+
]);
|
|
2992
3082
|
function help() {
|
|
2993
3083
|
console.log(c.dim([
|
|
2994
3084
|
" commands:",
|
|
@@ -2997,6 +3087,8 @@ function help() {
|
|
|
2997
3087
|
' /skill [name] [input] run a skill, or list them. "holt skill" manages them.',
|
|
2998
3088
|
" /output [fmt] show or set output format: markdown | html",
|
|
2999
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",
|
|
3000
3092
|
" /setting configure brains, API brains, and your launch command",
|
|
3001
3093
|
" /clear forget this session so far (saved memory stays)",
|
|
3002
3094
|
" /help this list",
|
|
@@ -3043,6 +3135,7 @@ async function chat() {
|
|
|
3043
3135
|
const session = newSessionId();
|
|
3044
3136
|
const history = [];
|
|
3045
3137
|
let lastReply = "";
|
|
3138
|
+
const grantedDirs = /* @__PURE__ */ new Set();
|
|
3046
3139
|
const sb = createStatusBar();
|
|
3047
3140
|
const showStatus = () => {
|
|
3048
3141
|
const line = statusLine(active.label, history);
|
|
@@ -3058,7 +3151,7 @@ async function chat() {
|
|
|
3058
3151
|
if (embedOk && stats.withEmbeddings < stats.turns) {
|
|
3059
3152
|
console.log(c.dim(` ${stats.turns - stats.withEmbeddings} older moments lack embeddings. Run "holt memory embed" to upgrade them.`));
|
|
3060
3153
|
}
|
|
3061
|
-
console.log(c.dim("Type a message. Commands: /brain /memory /output /save /setting /clear /help /exit"));
|
|
3154
|
+
console.log(c.dim("Type a message. Commands: /brain /memory /output /save /allow /setting /clear /help /exit"));
|
|
3062
3155
|
showStatus();
|
|
3063
3156
|
while (true) {
|
|
3064
3157
|
const raw = await ask(c.accent("\u203A "));
|
|
@@ -3074,7 +3167,7 @@ async function chat() {
|
|
|
3074
3167
|
}
|
|
3075
3168
|
promptOverride = inv.prompt;
|
|
3076
3169
|
console.log(c.dim(` running skill "${inv.skillName}"...`));
|
|
3077
|
-
} else if (line.startsWith("/")) {
|
|
3170
|
+
} else if (line.startsWith("/") && KNOWN_COMMANDS.has((line.slice(1).split(/\s+/)[0] || "").toLowerCase())) {
|
|
3078
3171
|
const parts = line.slice(1).split(/\s+/);
|
|
3079
3172
|
const cmd = (parts[0] || "").toLowerCase();
|
|
3080
3173
|
const rest = parts.slice(1).join(" ");
|
|
@@ -3169,6 +3262,24 @@ async function chat() {
|
|
|
3169
3262
|
console.log(c.dim(" skills: " + (names.join(" ") || "none") + " usage: /skill <name> [input]"));
|
|
3170
3263
|
continue;
|
|
3171
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
|
+
}
|
|
3172
3283
|
console.log(c.dim(` unknown command: /${cmd} (try /help)`));
|
|
3173
3284
|
continue;
|
|
3174
3285
|
}
|
|
@@ -3180,18 +3291,39 @@ async function chat() {
|
|
|
3180
3291
|
console.log(c.red(` ${active.label} has no API key. Use /setting to add one, or /brain to switch.`));
|
|
3181
3292
|
continue;
|
|
3182
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
|
+
}
|
|
3183
3307
|
const remembered = await recall(line, session, 4);
|
|
3184
|
-
const label = remembered.length ?
|
|
3308
|
+
const label = remembered.length ? `Holt is thinking (recalled ${remembered.length} moment${remembered.length === 1 ? "" : "s"})...` : `Holt is thinking...`;
|
|
3185
3309
|
console.log(c.dim(` ${label}`) + "\n");
|
|
3186
3310
|
const skillsBlock = skillsPromptBlock();
|
|
3187
3311
|
const base = renderPrompt(history, line, remembered);
|
|
3188
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
|
+
}
|
|
3189
3321
|
let streamed = false;
|
|
3190
3322
|
const onChunk = (chunk2) => {
|
|
3191
3323
|
streamed = true;
|
|
3192
3324
|
process.stdout.write(chunk2);
|
|
3193
3325
|
};
|
|
3194
|
-
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);
|
|
3195
3327
|
if (res.ok) {
|
|
3196
3328
|
if (!streamed) console.log(res.text);
|
|
3197
3329
|
if (!res.text.endsWith("\n")) console.log("");
|
|
@@ -3251,7 +3383,7 @@ async function login(which) {
|
|
|
3251
3383
|
}
|
|
3252
3384
|
|
|
3253
3385
|
// src/commands/memory.ts
|
|
3254
|
-
import { existsSync as
|
|
3386
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
3255
3387
|
import { basename } from "path";
|
|
3256
3388
|
async function memoryCmd(sub, rest = []) {
|
|
3257
3389
|
const { ask, close } = createReader();
|
|
@@ -3299,7 +3431,7 @@ async function memoryCmd(sub, rest = []) {
|
|
|
3299
3431
|
return;
|
|
3300
3432
|
}
|
|
3301
3433
|
if (action === "facts") {
|
|
3302
|
-
if (!
|
|
3434
|
+
if (!existsSync10(factsMdPath())) {
|
|
3303
3435
|
console.log(c.dim("\n No facts distilled yet. They form when you end a chat session.\n"));
|
|
3304
3436
|
close();
|
|
3305
3437
|
return;
|
|
@@ -3418,7 +3550,7 @@ function globalCmd(act, rest) {
|
|
|
3418
3550
|
}
|
|
3419
3551
|
|
|
3420
3552
|
// src/commands/wiki.ts
|
|
3421
|
-
import { existsSync as
|
|
3553
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
|
|
3422
3554
|
import { join as join10 } from "path";
|
|
3423
3555
|
import { spawn as spawn3 } from "child_process";
|
|
3424
3556
|
function titleToSlug(title) {
|
|
@@ -3612,7 +3744,7 @@ function listCmd() {
|
|
|
3612
3744
|
for (const slug of slugs) {
|
|
3613
3745
|
const pg = readPage(slug);
|
|
3614
3746
|
if (!pg) continue;
|
|
3615
|
-
const size =
|
|
3747
|
+
const size = existsSync11(join10(wikiDir(), slug + ".md")) ? (readFileSync11(join10(wikiDir(), slug + ".md"), "utf8").length / 1024).toFixed(1) : "0.0";
|
|
3616
3748
|
console.log(` ${c.cyan(pg.title.padEnd(28).slice(0, 28))} ${c.dim(pg.updated || " ")} ${size} KB`);
|
|
3617
3749
|
}
|
|
3618
3750
|
console.log("");
|
|
@@ -3700,8 +3832,8 @@ function setupCmd(cfg) {
|
|
|
3700
3832
|
`));
|
|
3701
3833
|
}
|
|
3702
3834
|
function openCmd() {
|
|
3703
|
-
const target =
|
|
3704
|
-
if (!
|
|
3835
|
+
const target = existsSync11(wikiIndexPath()) ? wikiIndexPath() : wikiDir();
|
|
3836
|
+
if (!existsSync11(target)) {
|
|
3705
3837
|
console.log(c.dim('\n No wiki yet. Run "holt wiki sync" first.\n'));
|
|
3706
3838
|
return;
|
|
3707
3839
|
}
|
|
@@ -3791,20 +3923,20 @@ import {
|
|
|
3791
3923
|
mkdirSync as mkdirSync9,
|
|
3792
3924
|
writeFileSync as writeFileSync10,
|
|
3793
3925
|
readFileSync as readFileSync13,
|
|
3794
|
-
existsSync as
|
|
3926
|
+
existsSync as existsSync13,
|
|
3795
3927
|
readdirSync as readdirSync3,
|
|
3796
|
-
statSync as
|
|
3928
|
+
statSync as statSync6,
|
|
3797
3929
|
rmSync as rmSync5,
|
|
3798
3930
|
cpSync,
|
|
3799
3931
|
mkdtempSync
|
|
3800
3932
|
} from "fs";
|
|
3801
|
-
import { join as join12, resolve as
|
|
3933
|
+
import { join as join12, resolve as resolve4, sep as sep2, isAbsolute as isAbsolute4 } from "path";
|
|
3802
3934
|
import { tmpdir } from "os";
|
|
3803
3935
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3804
3936
|
|
|
3805
3937
|
// src/registry.ts
|
|
3806
|
-
import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as
|
|
3807
|
-
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";
|
|
3808
3940
|
import { pathToFileURL, fileURLToPath as fileURLToPath2 } from "url";
|
|
3809
3941
|
var DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/holt-os/registry/main/registry.json";
|
|
3810
3942
|
var REGISTRY_REPO_URL = "https://github.com/holt-os/registry";
|
|
@@ -3849,7 +3981,7 @@ function parseRegistry(text) {
|
|
|
3849
3981
|
function readCache() {
|
|
3850
3982
|
try {
|
|
3851
3983
|
const path = registryCachePath();
|
|
3852
|
-
if (!
|
|
3984
|
+
if (!existsSync12(path)) return null;
|
|
3853
3985
|
const raw = JSON.parse(readFileSync12(path, "utf8"));
|
|
3854
3986
|
if (!raw || typeof raw.fetchedAt !== "number" || typeof raw.url !== "string") return null;
|
|
3855
3987
|
const registry = coerceRegistry(raw.registry);
|
|
@@ -3876,12 +4008,12 @@ function localPathFor(loc) {
|
|
|
3876
4008
|
}
|
|
3877
4009
|
}
|
|
3878
4010
|
if (/^https?:\/\//i.test(loc)) return null;
|
|
3879
|
-
return
|
|
4011
|
+
return isAbsolute3(loc) ? loc : resolve3(loc);
|
|
3880
4012
|
}
|
|
3881
4013
|
var UNREACHABLE_MSG = "no registry reachable; set HOLT_REGISTRY_URL or the community registry is not live yet";
|
|
3882
4014
|
function loadLocal(path) {
|
|
3883
4015
|
try {
|
|
3884
|
-
if (!
|
|
4016
|
+
if (!existsSync12(path)) {
|
|
3885
4017
|
return { registry: null, fromCache: false, error: UNREACHABLE_MSG };
|
|
3886
4018
|
}
|
|
3887
4019
|
const registry = parseRegistry(readFileSync12(path, "utf8"));
|
|
@@ -4004,21 +4136,21 @@ function splitSubpath(source) {
|
|
|
4004
4136
|
return { base: source.slice(0, i), subpath: source.slice(i + 1) };
|
|
4005
4137
|
}
|
|
4006
4138
|
function resolveSubpathDir(fetched, subpath) {
|
|
4007
|
-
if (
|
|
4139
|
+
if (isAbsolute4(subpath)) return null;
|
|
4008
4140
|
const segments = subpath.split(/[\\/]/);
|
|
4009
4141
|
if (segments.some((s) => s === "..")) return null;
|
|
4010
|
-
const target =
|
|
4011
|
-
const root =
|
|
4012
|
-
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;
|
|
4013
4145
|
return target;
|
|
4014
4146
|
}
|
|
4015
4147
|
function findSkillDir(root) {
|
|
4016
|
-
if (
|
|
4148
|
+
if (existsSync13(join12(root, "SKILL.md"))) return root;
|
|
4017
4149
|
let subs = [];
|
|
4018
4150
|
try {
|
|
4019
4151
|
subs = readdirSync3(root).filter((f) => {
|
|
4020
4152
|
try {
|
|
4021
|
-
return
|
|
4153
|
+
return statSync6(join12(root, f)).isDirectory();
|
|
4022
4154
|
} catch {
|
|
4023
4155
|
return false;
|
|
4024
4156
|
}
|
|
@@ -4026,7 +4158,7 @@ function findSkillDir(root) {
|
|
|
4026
4158
|
} catch {
|
|
4027
4159
|
return null;
|
|
4028
4160
|
}
|
|
4029
|
-
const withSkill = subs.filter((f) =>
|
|
4161
|
+
const withSkill = subs.filter((f) => existsSync13(join12(root, f, "SKILL.md")));
|
|
4030
4162
|
if (withSkill.length === 1) return join12(root, withSkill[0]);
|
|
4031
4163
|
return null;
|
|
4032
4164
|
}
|
|
@@ -4081,7 +4213,7 @@ function cmdCreate(name, global) {
|
|
|
4081
4213
|
}
|
|
4082
4214
|
const scope = global ? "global" : "workspace";
|
|
4083
4215
|
const dir = join12(skillsRoot(scope), clean);
|
|
4084
|
-
if (
|
|
4216
|
+
if (existsSync13(dir)) {
|
|
4085
4217
|
console.error(c.red(`
|
|
4086
4218
|
A ${scope} skill named "${clean}" already exists.`));
|
|
4087
4219
|
console.error(c.dim(" " + dir + "\n"));
|
|
@@ -4112,8 +4244,8 @@ function installFromSource(source, scope) {
|
|
|
4112
4244
|
}
|
|
4113
4245
|
fetched = tempDir;
|
|
4114
4246
|
} else {
|
|
4115
|
-
fetched =
|
|
4116
|
-
if (!
|
|
4247
|
+
fetched = resolve4(base);
|
|
4248
|
+
if (!existsSync13(fetched)) {
|
|
4117
4249
|
console.error(c.red(`
|
|
4118
4250
|
No such path: ${fetched}
|
|
4119
4251
|
`));
|
|
@@ -4131,7 +4263,7 @@ function installFromSource(source, scope) {
|
|
|
4131
4263
|
process.exitCode = 1;
|
|
4132
4264
|
return false;
|
|
4133
4265
|
}
|
|
4134
|
-
if (!
|
|
4266
|
+
if (!existsSync13(target) || !statSync6(target).isDirectory()) {
|
|
4135
4267
|
console.error(c.red(`
|
|
4136
4268
|
Subpath "${subpath}" does not exist in the source.
|
|
4137
4269
|
`));
|
|
@@ -4163,7 +4295,7 @@ function installFromSource(source, scope) {
|
|
|
4163
4295
|
return false;
|
|
4164
4296
|
}
|
|
4165
4297
|
const dest = join12(skillsRoot(scope), skillName);
|
|
4166
|
-
if (
|
|
4298
|
+
if (existsSync13(dest)) {
|
|
4167
4299
|
console.error(c.red(`
|
|
4168
4300
|
A ${scope} skill named "${skillName}" already exists.`));
|
|
4169
4301
|
console.error(c.dim(" " + dest + "\n"));
|
|
@@ -4173,13 +4305,13 @@ function installFromSource(source, scope) {
|
|
|
4173
4305
|
mkdirSync9(skillsRoot(scope), { recursive: true });
|
|
4174
4306
|
cpSync(skillDir, dest, { recursive: true });
|
|
4175
4307
|
const nestedGit = join12(dest, ".git");
|
|
4176
|
-
if (
|
|
4308
|
+
if (existsSync13(nestedGit)) rmSync5(nestedGit, { recursive: true, force: true });
|
|
4177
4309
|
console.log(c.green(`
|
|
4178
4310
|
Installed ${scope} skill "${skillName}".`));
|
|
4179
4311
|
console.log(c.dim(" " + join12(dest, "SKILL.md") + "\n"));
|
|
4180
4312
|
return true;
|
|
4181
4313
|
} finally {
|
|
4182
|
-
if (tempDir &&
|
|
4314
|
+
if (tempDir && existsSync13(tempDir)) {
|
|
4183
4315
|
try {
|
|
4184
4316
|
rmSync5(tempDir, { recursive: true, force: true });
|
|
4185
4317
|
} catch {
|
|
@@ -4194,7 +4326,7 @@ async function cmdAdd(source, global, refresh) {
|
|
|
4194
4326
|
}
|
|
4195
4327
|
const scope = global ? "global" : "workspace";
|
|
4196
4328
|
const { base } = splitSubpath(source);
|
|
4197
|
-
if (isGitUrl(base) ||
|
|
4329
|
+
if (isGitUrl(base) || existsSync13(resolve4(base))) {
|
|
4198
4330
|
installFromSource(source, scope);
|
|
4199
4331
|
return;
|
|
4200
4332
|
}
|
|
@@ -4277,7 +4409,7 @@ function cmdPublish(name) {
|
|
|
4277
4409
|
description = (data.description || loaded.skill.description || "").trim();
|
|
4278
4410
|
} else {
|
|
4279
4411
|
const file = join12(process.cwd(), "SKILL.md");
|
|
4280
|
-
if (!
|
|
4412
|
+
if (!existsSync13(file)) {
|
|
4281
4413
|
console.error(c.red("\n No SKILL.md in the current directory, and no skill name given."));
|
|
4282
4414
|
console.error(c.dim(" Usage: holt skill publish [<name>]\n"));
|
|
4283
4415
|
process.exitCode = 1;
|
|
@@ -4321,9 +4453,9 @@ async function cmdRemove(name, ask) {
|
|
|
4321
4453
|
}
|
|
4322
4454
|
const wsDir = join12(skillsRoot("workspace"), clean);
|
|
4323
4455
|
const globalDir = join12(skillsRoot("global"), clean);
|
|
4324
|
-
const target =
|
|
4456
|
+
const target = existsSync13(wsDir) ? wsDir : existsSync13(globalDir) ? globalDir : "";
|
|
4325
4457
|
if (!target) {
|
|
4326
|
-
if (
|
|
4458
|
+
if (existsSync13(join12(skillsRoot("builtin"), clean))) {
|
|
4327
4459
|
console.error(c.red(`
|
|
4328
4460
|
"${clean}" is a built-in skill and cannot be removed; it ships inside Holt.`));
|
|
4329
4461
|
console.error(c.dim(" Override it instead: create a workspace or global skill with the same name.\n"));
|
|
@@ -4396,8 +4528,8 @@ async function skillCmd(sub, rest = []) {
|
|
|
4396
4528
|
}
|
|
4397
4529
|
|
|
4398
4530
|
// src/commands/graph.ts
|
|
4399
|
-
import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync10, existsSync as
|
|
4400
|
-
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";
|
|
4401
4533
|
import { spawn as spawn4 } from "child_process";
|
|
4402
4534
|
|
|
4403
4535
|
// src/communities.ts
|
|
@@ -5194,8 +5326,8 @@ function renderGraphHtml(graph2, meta) {
|
|
|
5194
5326
|
}
|
|
5195
5327
|
|
|
5196
5328
|
// src/ingest.ts
|
|
5197
|
-
import { readdirSync as readdirSync4, readFileSync as readFileSync14, statSync as
|
|
5198
|
-
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";
|
|
5199
5331
|
var CODE_EXT = /* @__PURE__ */ new Set([
|
|
5200
5332
|
"js",
|
|
5201
5333
|
"jsx",
|
|
@@ -5246,7 +5378,7 @@ var MAX_FILES = 2e3;
|
|
|
5246
5378
|
var MAX_FILE_BYTES = 400 * 1024;
|
|
5247
5379
|
var MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
5248
5380
|
function toPosix(p) {
|
|
5249
|
-
return
|
|
5381
|
+
return sep3 === "/" ? p : p.split(sep3).join("/");
|
|
5250
5382
|
}
|
|
5251
5383
|
function ingestFolder(root, want = { code: true, docs: true }) {
|
|
5252
5384
|
const files = [];
|
|
@@ -5271,7 +5403,7 @@ function ingestFolder(root, want = { code: true, docs: true }) {
|
|
|
5271
5403
|
const full = join13(dir, name);
|
|
5272
5404
|
let st;
|
|
5273
5405
|
try {
|
|
5274
|
-
st =
|
|
5406
|
+
st = statSync7(full);
|
|
5275
5407
|
} catch {
|
|
5276
5408
|
skipped.unreadable++;
|
|
5277
5409
|
continue;
|
|
@@ -5435,7 +5567,7 @@ function resolvePySpec(f, spec, byId) {
|
|
|
5435
5567
|
return null;
|
|
5436
5568
|
}
|
|
5437
5569
|
function toPosixDir(id) {
|
|
5438
|
-
const d = toPosix(
|
|
5570
|
+
const d = toPosix(dirname4(id));
|
|
5439
5571
|
return d === "" || d === "." ? "." : d;
|
|
5440
5572
|
}
|
|
5441
5573
|
var MD_LINK_RE = /\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g;
|
|
@@ -5607,7 +5739,7 @@ async function graph(args = []) {
|
|
|
5607
5739
|
const ingestCode = opts.code;
|
|
5608
5740
|
const ingestDocs = opts.docs;
|
|
5609
5741
|
const turns = loadTurns();
|
|
5610
|
-
const includeWiki = opts.wiki !== void 0 ? opts.wiki :
|
|
5742
|
+
const includeWiki = opts.wiki !== void 0 ? opts.wiki : existsSync14(wikiDir());
|
|
5611
5743
|
const wikiPages = includeWiki ? wikiGraphPages() : [];
|
|
5612
5744
|
let fileNodeCount = 0;
|
|
5613
5745
|
let docNodeCount = 0;
|
|
@@ -5637,8 +5769,8 @@ async function graph(args = []) {
|
|
|
5637
5769
|
if (codeGraph) assignCommunities(g);
|
|
5638
5770
|
if (opts.report) {
|
|
5639
5771
|
const md = renderReport(g, { workspace: workspace() });
|
|
5640
|
-
const reportPath = opts.out ?
|
|
5641
|
-
mkdirSync10(
|
|
5772
|
+
const reportPath = opts.out ? resolve5(opts.out) : join14(workspace(), "GRAPH_REPORT.md");
|
|
5773
|
+
mkdirSync10(dirname5(reportPath), { recursive: true });
|
|
5642
5774
|
writeFileSync11(reportPath, md, "utf8");
|
|
5643
5775
|
const communities = new Set(g.nodes.map((n) => n.community ?? "0")).size;
|
|
5644
5776
|
console.log("\n" + c.accent("Graph report written") + c.dim(" (this folder)"));
|
|
@@ -5659,8 +5791,8 @@ async function graph(args = []) {
|
|
|
5659
5791
|
concepts: conceptCount,
|
|
5660
5792
|
edges: g.edges.length
|
|
5661
5793
|
});
|
|
5662
|
-
const outPath = opts.out ?
|
|
5663
|
-
mkdirSync10(
|
|
5794
|
+
const outPath = opts.out ? resolve5(opts.out) : join14(wsHoltDir(), "graph.html");
|
|
5795
|
+
mkdirSync10(dirname5(outPath), { recursive: true });
|
|
5664
5796
|
writeFileSync11(outPath, html, "utf8");
|
|
5665
5797
|
console.log("\n" + c.accent("Memory graph built") + c.dim(" (this folder)"));
|
|
5666
5798
|
const parts = [`${turns.length} turns`, `${conceptCount} concepts`];
|
|
@@ -5773,7 +5905,7 @@ function recoverId(line) {
|
|
|
5773
5905
|
return null;
|
|
5774
5906
|
}
|
|
5775
5907
|
function runMcpServer(opts) {
|
|
5776
|
-
return new Promise((
|
|
5908
|
+
return new Promise((resolve9) => {
|
|
5777
5909
|
const stdin = process.stdin;
|
|
5778
5910
|
let buffer = "";
|
|
5779
5911
|
let chain = Promise.resolve();
|
|
@@ -5825,11 +5957,11 @@ function runMcpServer(opts) {
|
|
|
5825
5957
|
processLine(buffer);
|
|
5826
5958
|
buffer = "";
|
|
5827
5959
|
}
|
|
5828
|
-
chain.then(() =>
|
|
5960
|
+
chain.then(() => resolve9()).catch(() => resolve9());
|
|
5829
5961
|
});
|
|
5830
5962
|
stdin.on("error", (e) => {
|
|
5831
5963
|
console.error(`[mcp] stdin error: ${e.message}`);
|
|
5832
|
-
chain.then(() =>
|
|
5964
|
+
chain.then(() => resolve9()).catch(() => resolve9());
|
|
5833
5965
|
});
|
|
5834
5966
|
stdin.resume();
|
|
5835
5967
|
});
|
|
@@ -5949,7 +6081,7 @@ function holtTools() {
|
|
|
5949
6081
|
}
|
|
5950
6082
|
|
|
5951
6083
|
// src/version.ts
|
|
5952
|
-
var VERSION = "0.
|
|
6084
|
+
var VERSION = "0.13.0";
|
|
5953
6085
|
|
|
5954
6086
|
// src/commands/mcp.ts
|
|
5955
6087
|
function printSetup() {
|
|
@@ -5995,10 +6127,10 @@ async function mcp(sub, rest = []) {
|
|
|
5995
6127
|
}
|
|
5996
6128
|
|
|
5997
6129
|
// src/commands/hook.ts
|
|
5998
|
-
import { readFileSync as readFileSync15, writeFileSync as writeFileSync12, existsSync as
|
|
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";
|
|
5999
6131
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
6000
|
-
import { join as join15, dirname as
|
|
6001
|
-
import { homedir as
|
|
6132
|
+
import { join as join15, dirname as dirname6 } from "path";
|
|
6133
|
+
import { homedir as homedir4 } from "os";
|
|
6002
6134
|
var EVENT = {
|
|
6003
6135
|
inject: "UserPromptSubmit",
|
|
6004
6136
|
capture: "Stop"
|
|
@@ -6008,13 +6140,13 @@ var HOOK_CMD = {
|
|
|
6008
6140
|
capture: "holt hook capture"
|
|
6009
6141
|
};
|
|
6010
6142
|
function globalSettingsPath() {
|
|
6011
|
-
return join15(
|
|
6143
|
+
return join15(homedir4(), ".claude", "settings.json");
|
|
6012
6144
|
}
|
|
6013
6145
|
function projectSettingsPath() {
|
|
6014
6146
|
return join15(process.cwd(), ".claude", "settings.json");
|
|
6015
6147
|
}
|
|
6016
6148
|
function readSettings(path) {
|
|
6017
|
-
if (!
|
|
6149
|
+
if (!existsSync15(path)) return {};
|
|
6018
6150
|
try {
|
|
6019
6151
|
const parsed = JSON.parse(readFileSync15(path, "utf8"));
|
|
6020
6152
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
@@ -6023,11 +6155,11 @@ function readSettings(path) {
|
|
|
6023
6155
|
}
|
|
6024
6156
|
}
|
|
6025
6157
|
function writeSettings(path, settings) {
|
|
6026
|
-
mkdirSync11(
|
|
6158
|
+
mkdirSync11(dirname6(path), { recursive: true });
|
|
6027
6159
|
writeFileSync12(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
6028
6160
|
}
|
|
6029
6161
|
function backup(path) {
|
|
6030
|
-
if (!
|
|
6162
|
+
if (!existsSync15(path)) return null;
|
|
6031
6163
|
const dest = `${path}.holt-bak`;
|
|
6032
6164
|
try {
|
|
6033
6165
|
copyFileSync(path, dest);
|
|
@@ -6117,7 +6249,7 @@ function remove(rest) {
|
|
|
6117
6249
|
const paths = project ? [projectSettingsPath()] : [globalSettingsPath(), projectSettingsPath()];
|
|
6118
6250
|
let touchedAny = false;
|
|
6119
6251
|
for (const path of paths) {
|
|
6120
|
-
if (!
|
|
6252
|
+
if (!existsSync15(path)) continue;
|
|
6121
6253
|
let settings;
|
|
6122
6254
|
try {
|
|
6123
6255
|
settings = readSettings(path);
|
|
@@ -6147,7 +6279,7 @@ function status(rest) {
|
|
|
6147
6279
|
];
|
|
6148
6280
|
console.log(c.accent(" Holt ambient hooks"));
|
|
6149
6281
|
for (const e of entries) {
|
|
6150
|
-
if (!
|
|
6282
|
+
if (!existsSync15(e.path)) {
|
|
6151
6283
|
console.log(c.dim(` ${e.label.padEnd(8)} ${e.path} (no settings file)`));
|
|
6152
6284
|
continue;
|
|
6153
6285
|
}
|
|
@@ -6167,13 +6299,13 @@ function status(rest) {
|
|
|
6167
6299
|
console.log(c.dim(" Runtime hooks only act in trusted Holt folders that already have .holt/memory."));
|
|
6168
6300
|
}
|
|
6169
6301
|
function readStdin(timeoutMs) {
|
|
6170
|
-
return new Promise((
|
|
6302
|
+
return new Promise((resolve9) => {
|
|
6171
6303
|
let input = "";
|
|
6172
6304
|
let done = false;
|
|
6173
6305
|
const finish = () => {
|
|
6174
6306
|
if (done) return;
|
|
6175
6307
|
done = true;
|
|
6176
|
-
|
|
6308
|
+
resolve9(input);
|
|
6177
6309
|
};
|
|
6178
6310
|
try {
|
|
6179
6311
|
process.stdin.setEncoding("utf8");
|
|
@@ -6197,9 +6329,9 @@ function parseHookInput(raw) {
|
|
|
6197
6329
|
}
|
|
6198
6330
|
}
|
|
6199
6331
|
function enterFolder(cwd) {
|
|
6200
|
-
if (!cwd || !
|
|
6332
|
+
if (!cwd || !existsSync15(cwd)) return null;
|
|
6201
6333
|
try {
|
|
6202
|
-
const real =
|
|
6334
|
+
const real = realpathSync3(cwd);
|
|
6203
6335
|
process.chdir(real);
|
|
6204
6336
|
return real;
|
|
6205
6337
|
} catch {
|
|
@@ -6208,7 +6340,7 @@ function enterFolder(cwd) {
|
|
|
6208
6340
|
}
|
|
6209
6341
|
function guardTrusted(real) {
|
|
6210
6342
|
if (!isTrusted(real)) return false;
|
|
6211
|
-
if (!
|
|
6343
|
+
if (!existsSync15(memDir())) return false;
|
|
6212
6344
|
return true;
|
|
6213
6345
|
}
|
|
6214
6346
|
function hookLog(msg) {
|
|
@@ -6222,11 +6354,37 @@ function hookLog(msg) {
|
|
|
6222
6354
|
function fileBytes(path) {
|
|
6223
6355
|
if (!path) return 0;
|
|
6224
6356
|
try {
|
|
6225
|
-
return
|
|
6357
|
+
return statSync8(path).size;
|
|
6226
6358
|
} catch {
|
|
6227
6359
|
return 0;
|
|
6228
6360
|
}
|
|
6229
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
|
+
}
|
|
6230
6388
|
async function inject() {
|
|
6231
6389
|
const debug = !!process.env.HOLT_HOOK_DEBUG;
|
|
6232
6390
|
const dlog = (m) => {
|
|
@@ -6325,17 +6483,17 @@ function readTranscript(path) {
|
|
|
6325
6483
|
return out;
|
|
6326
6484
|
}
|
|
6327
6485
|
function resolveCommandPath(command) {
|
|
6328
|
-
if (command.startsWith("/") &&
|
|
6486
|
+
if (command.startsWith("/") && existsSync15(command)) return command;
|
|
6329
6487
|
try {
|
|
6330
6488
|
const shell = process.env.SHELL || "/bin/sh";
|
|
6331
6489
|
const res = spawnSync3(shell, ["-lc", "command -v " + command], { encoding: "utf8" });
|
|
6332
6490
|
if (res.status === 0) {
|
|
6333
6491
|
const p = (res.stdout || "").trim().split("\n")[0]?.trim();
|
|
6334
|
-
if (p && p.startsWith("/") &&
|
|
6492
|
+
if (p && p.startsWith("/") && existsSync15(p)) return p;
|
|
6335
6493
|
}
|
|
6336
6494
|
} catch {
|
|
6337
6495
|
}
|
|
6338
|
-
const home =
|
|
6496
|
+
const home = homedir4();
|
|
6339
6497
|
const dirs = [
|
|
6340
6498
|
join15(home, ".local", "bin"),
|
|
6341
6499
|
"/opt/homebrew/bin",
|
|
@@ -6346,7 +6504,7 @@ function resolveCommandPath(command) {
|
|
|
6346
6504
|
];
|
|
6347
6505
|
for (const dir of dirs) {
|
|
6348
6506
|
const candidate = join15(dir, command);
|
|
6349
|
-
if (
|
|
6507
|
+
if (existsSync15(candidate)) return candidate;
|
|
6350
6508
|
}
|
|
6351
6509
|
return null;
|
|
6352
6510
|
}
|
|
@@ -6433,8 +6591,17 @@ async function capture() {
|
|
|
6433
6591
|
hookLog(`capture cwd=${real} event=Stop -> only ${exchanges} exchange(s) (<${MIN_EXCHANGES_FOR_EXTRACTION}, skipped)`);
|
|
6434
6592
|
return;
|
|
6435
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
|
+
}
|
|
6436
6602
|
const session = "hook-" + (data.session_id || newSessionId());
|
|
6437
6603
|
const n = await extractAndSaveFacts(brain, cfg, history, session);
|
|
6604
|
+
if (throttleId) saveHookState(throttleId, exchanges);
|
|
6438
6605
|
hookLog(`capture cwd=${real} event=Stop -> saved ${n} fact${n === 1 ? "" : "s"}`);
|
|
6439
6606
|
if (n > 0) process.stderr.write(`holt hook capture: saved ${n} fact${n === 1 ? "" : "s"} to ${memDir()}
|
|
6440
6607
|
`);
|
|
@@ -6507,7 +6674,7 @@ async function hook(sub, rest = []) {
|
|
|
6507
6674
|
|
|
6508
6675
|
// src/commands/run.ts
|
|
6509
6676
|
import { writeFileSync as writeFileSync13 } from "fs";
|
|
6510
|
-
import { resolve as
|
|
6677
|
+
import { resolve as resolve6 } from "path";
|
|
6511
6678
|
var USAGE3 = [
|
|
6512
6679
|
" holt run <task> [options]",
|
|
6513
6680
|
"",
|
|
@@ -6596,7 +6763,7 @@ async function run(args) {
|
|
|
6596
6763
|
process.stdout.write("\n");
|
|
6597
6764
|
}
|
|
6598
6765
|
if (p.out) {
|
|
6599
|
-
const target =
|
|
6766
|
+
const target = resolve6(process.cwd(), p.out);
|
|
6600
6767
|
try {
|
|
6601
6768
|
writeFileSync13(target, result.text, "utf8");
|
|
6602
6769
|
process.stderr.write(c.dim(`saved to ${target}`) + "\n");
|
|
@@ -6609,11 +6776,11 @@ async function run(args) {
|
|
|
6609
6776
|
|
|
6610
6777
|
// src/commands/schedule.ts
|
|
6611
6778
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
6612
|
-
import { writeFileSync as writeFileSync15, mkdirSync as mkdirSync13, rmSync as rmSync6, existsSync as
|
|
6613
|
-
import { dirname as
|
|
6779
|
+
import { writeFileSync as writeFileSync15, mkdirSync as mkdirSync13, rmSync as rmSync6, existsSync as existsSync16 } from "fs";
|
|
6780
|
+
import { dirname as dirname7 } from "path";
|
|
6614
6781
|
|
|
6615
6782
|
// src/scheduler.ts
|
|
6616
|
-
import { homedir as
|
|
6783
|
+
import { homedir as homedir5 } from "os";
|
|
6617
6784
|
import { join as join16 } from "path";
|
|
6618
6785
|
import { readFileSync as readFileSync16, writeFileSync as writeFileSync14, mkdirSync as mkdirSync12 } from "fs";
|
|
6619
6786
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
@@ -6628,7 +6795,7 @@ function logPath(id) {
|
|
|
6628
6795
|
return join16(logsDir(), `${id}.log`);
|
|
6629
6796
|
}
|
|
6630
6797
|
function plistPath(id) {
|
|
6631
|
-
return join16(
|
|
6798
|
+
return join16(homedir5(), "Library", "LaunchAgents", `com.holt.${id}.plist`);
|
|
6632
6799
|
}
|
|
6633
6800
|
function loadJobs() {
|
|
6634
6801
|
try {
|
|
@@ -6774,7 +6941,7 @@ function parseAdd(rest) {
|
|
|
6774
6941
|
}
|
|
6775
6942
|
function installDarwin(job, holtPath) {
|
|
6776
6943
|
const path = plistPath(job.id);
|
|
6777
|
-
mkdirSync13(
|
|
6944
|
+
mkdirSync13(dirname7(path), { recursive: true });
|
|
6778
6945
|
writeFileSync15(path, buildLaunchdPlist(job, holtPath), "utf8");
|
|
6779
6946
|
const res = spawnSync5("launchctl", ["load", "-w", path], { encoding: "utf8" });
|
|
6780
6947
|
if (res.status === 0) {
|
|
@@ -6863,7 +7030,7 @@ function listCmd2() {
|
|
|
6863
7030
|
}
|
|
6864
7031
|
function removeDarwin(id) {
|
|
6865
7032
|
const path = plistPath(id);
|
|
6866
|
-
if (
|
|
7033
|
+
if (existsSync16(path)) {
|
|
6867
7034
|
spawnSync5("launchctl", ["unload", path], { encoding: "utf8" });
|
|
6868
7035
|
try {
|
|
6869
7036
|
rmSync6(path);
|
|
@@ -6926,8 +7093,8 @@ async function schedule(sub, rest = []) {
|
|
|
6926
7093
|
|
|
6927
7094
|
// src/commands/routine.ts
|
|
6928
7095
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
6929
|
-
import { writeFileSync as writeFileSync17, mkdirSync as mkdirSync15, rmSync as rmSync7, existsSync as
|
|
6930
|
-
import { dirname as
|
|
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";
|
|
6931
7098
|
|
|
6932
7099
|
// src/routines.ts
|
|
6933
7100
|
import { readFileSync as readFileSync17, writeFileSync as writeFileSync16, mkdirSync as mkdirSync14 } from "fs";
|
|
@@ -7008,7 +7175,7 @@ function routineJobId(name) {
|
|
|
7008
7175
|
}
|
|
7009
7176
|
function installDarwin2(job, holtPath) {
|
|
7010
7177
|
const path = plistPath(job.id);
|
|
7011
|
-
mkdirSync15(
|
|
7178
|
+
mkdirSync15(dirname8(path), { recursive: true });
|
|
7012
7179
|
writeFileSync17(path, buildLaunchdPlist(job, holtPath), "utf8");
|
|
7013
7180
|
const res = spawnSync6("launchctl", ["load", "-w", path], { encoding: "utf8" });
|
|
7014
7181
|
if (res.status === 0) {
|
|
@@ -7053,7 +7220,7 @@ function installSchedule(r) {
|
|
|
7053
7220
|
}
|
|
7054
7221
|
function removeScheduleDarwin(id) {
|
|
7055
7222
|
const path = plistPath(id);
|
|
7056
|
-
if (
|
|
7223
|
+
if (existsSync17(path)) {
|
|
7057
7224
|
spawnSync6("launchctl", ["unload", path], { encoding: "utf8" });
|
|
7058
7225
|
try {
|
|
7059
7226
|
rmSync7(path);
|
|
@@ -7247,7 +7414,7 @@ async function runCmd(rest) {
|
|
|
7247
7414
|
if (!quiet && !streamed) process.stdout.write(result.text);
|
|
7248
7415
|
if (!quiet && (streamed || result.text) && !result.text.endsWith("\n")) process.stdout.write("\n");
|
|
7249
7416
|
if (outTarget) {
|
|
7250
|
-
const target =
|
|
7417
|
+
const target = resolve7(routine2.workspace, outTarget);
|
|
7251
7418
|
try {
|
|
7252
7419
|
writeFileSync17(target, result.text, "utf8");
|
|
7253
7420
|
if (!quiet) process.stderr.write(c.dim(`saved to ${target}`) + "\n");
|
|
@@ -7632,7 +7799,7 @@ async function doctor() {
|
|
|
7632
7799
|
|
|
7633
7800
|
// src/commands/write.ts
|
|
7634
7801
|
import { writeFileSync as writeFileSync18 } from "fs";
|
|
7635
|
-
import { resolve as
|
|
7802
|
+
import { resolve as resolve8 } from "path";
|
|
7636
7803
|
|
|
7637
7804
|
// src/antiai.ts
|
|
7638
7805
|
var EM_DASH = String.fromCharCode(8212);
|
|
@@ -7810,7 +7977,7 @@ async function write(args) {
|
|
|
7810
7977
|
draft = stripEmDash(draft).trimEnd();
|
|
7811
7978
|
process.stdout.write(draft + "\n");
|
|
7812
7979
|
if (p.out) {
|
|
7813
|
-
const target =
|
|
7980
|
+
const target = resolve8(process.cwd(), p.out);
|
|
7814
7981
|
try {
|
|
7815
7982
|
writeFileSync18(target, draft + "\n", "utf8");
|
|
7816
7983
|
process.stderr.write(c.dim(`saved to ${target}`) + "\n");
|