@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.
Files changed (3) hide show
  1. package/README.md +25 -1
  2. package/dist/cli.js +557 -152
  3. 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((resolve8) => {
108
+ const ask = (q) => new Promise((resolve9) => {
45
109
  if (q) process.stdout.write(q);
46
- if (buffer.length) resolve8(buffer.shift());
47
- else if (closed) resolve8(null);
48
- else waiters.push(resolve8);
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((resolve8) => {
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
- resolve8({ ok: false, text: `Could not launch "${brain.command}": ${e.message}` });
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) => resolve8({ ok: false, text: `Could not run "${brain.command}": ${e.message}` }));
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) resolve8({ ok: true, text });
268
- else resolve8({ ok: false, text: err2.trim() || text || `"${brain.command}" exited with code ${code}` });
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((resolve8) => {
503
+ return new Promise((resolve9) => {
439
504
  let child;
440
505
  try {
441
506
  child = spawn2(cmd, args, { stdio: "inherit" });
442
507
  } catch {
443
- resolve8(-1);
508
+ resolve9(-1);
444
509
  return;
445
510
  }
446
- child.on("error", () => resolve8(-1));
447
- child.on("close", (code) => resolve8(code ?? -1));
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 mode = (await ask("Do the [i]nterview, add [s]amples, or [b]oth? [i] ") ?? "").trim().toLowerCase();
1891
- const wantInterview = mode === "" || mode === "i" || mode === "b" || mode === "interview" || mode === "both";
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
- if (wantInterview) {
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(cfg.brains[active.id], prompt) : await runApiBrain(active.brain, prompt);
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(` ${pct}% context \xB7 ${mem} in memory`);
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
- console.log(statusLine(active.label, history) + "\n");
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 ? `${active.label} is thinking (recalled ${remembered.length} moment${remembered.length === 1 ? "" : "s"})...` : `${active.label} is thinking...`;
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
- console.log(statusLine(active.label, history) + "\n");
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 existsSync9, readFileSync as readFileSync10 } from "fs";
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 (!existsSync9(factsMdPath())) {
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 existsSync10, readFileSync as readFileSync11 } from "fs";
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 = existsSync10(join10(wikiDir(), slug + ".md")) ? (readFileSync11(join10(wikiDir(), slug + ".md"), "utf8").length / 1024).toFixed(1) : "0.0";
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 = existsSync10(wikiIndexPath()) ? wikiIndexPath() : wikiDir();
3608
- if (!existsSync10(target)) {
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 existsSync12,
3926
+ existsSync as existsSync13,
3699
3927
  readdirSync as readdirSync3,
3700
- statSync as statSync5,
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 resolve3, sep, isAbsolute as isAbsolute3 } from "path";
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 existsSync11, mkdirSync as mkdirSync8, statSync as statSync4 } from "fs";
3711
- import { join as join11, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
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 (!existsSync11(path)) return null;
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 isAbsolute2(loc) ? loc : resolve2(loc);
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 (!existsSync11(path)) {
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 (isAbsolute3(subpath)) return null;
4139
+ if (isAbsolute4(subpath)) return null;
3912
4140
  const segments = subpath.split(/[\\/]/);
3913
4141
  if (segments.some((s) => s === "..")) return null;
3914
- const target = resolve3(fetched, subpath);
3915
- const root = resolve3(fetched);
3916
- if (target !== root && !target.startsWith(root + sep)) return null;
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 (existsSync12(join12(root, "SKILL.md"))) return root;
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 statSync5(join12(root, f)).isDirectory();
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) => existsSync12(join12(root, f, "SKILL.md")));
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 (existsSync12(dir)) {
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 = resolve3(base);
4020
- if (!existsSync12(fetched)) {
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 (!existsSync12(target) || !statSync5(target).isDirectory()) {
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 (existsSync12(dest)) {
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 (existsSync12(nestedGit)) rmSync5(nestedGit, { recursive: true, force: true });
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 && existsSync12(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) || existsSync12(resolve3(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 (!existsSync12(file)) {
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 = existsSync12(wsDir) ? wsDir : existsSync12(globalDir) ? globalDir : "";
4456
+ const target = existsSync13(wsDir) ? wsDir : existsSync13(globalDir) ? globalDir : "";
4229
4457
  if (!target) {
4230
- if (existsSync12(join12(skillsRoot("builtin"), clean))) {
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 existsSync13 } from "fs";
4304
- import { join as join14, dirname as dirname4, resolve as resolve4 } from "path";
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 statSync6 } from "fs";
5102
- import { join as join13, relative, sep as sep2, extname, dirname as dirname3, basename as basename2 } from "path";
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 sep2 === "/" ? p : p.split(sep2).join("/");
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 = statSync6(full);
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(dirname3(id));
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 : existsSync13(wikiDir());
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 ? resolve4(opts.out) : join14(workspace(), "GRAPH_REPORT.md");
5545
- mkdirSync10(dirname4(reportPath), { recursive: true });
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 ? resolve4(opts.out) : join14(wsHoltDir(), "graph.html");
5567
- mkdirSync10(dirname4(outPath), { recursive: true });
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((resolve8) => {
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(() => resolve8()).catch(() => resolve8());
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(() => resolve8()).catch(() => resolve8());
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.12.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 existsSync14, mkdirSync as mkdirSync11, copyFileSync, realpathSync as realpathSync2 } from "fs";
5903
- import { join as join15, dirname as dirname5 } from "path";
5904
- import { homedir as homedir3 } from "os";
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(homedir3(), ".claude", "settings.json");
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 (!existsSync14(path)) return {};
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(dirname5(path), { recursive: true });
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 (!existsSync14(path)) return null;
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 (!existsSync14(path)) continue;
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 (!existsSync14(e.path)) {
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((resolve8) => {
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
- resolve8(input);
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 || !existsSync14(cwd)) return null;
6332
+ if (!cwd || !existsSync15(cwd)) return null;
6104
6333
  try {
6105
- const real = realpathSync2(cwd);
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 (!existsSync14(memDir())) return false;
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) return;
6400
+ if (prompt.length < 3) {
6401
+ dlog("inject -> prompt too short");
6402
+ return;
6403
+ }
6123
6404
  const real = enterFolder(data.cwd);
6124
- if (!real) return;
6125
- if (!guardTrusted(real)) return;
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) return;
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
- ["[Holt memory - relevant notes from earlier sessions in this folder]", ...lines, ""].join("\n")
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
- if (d.type !== "user" && d.type !== "assistant") continue;
6155
- const m = d.message;
6156
- if (!m || m.role !== "user" && m.role !== "assistant") continue;
6157
- let text = "";
6158
- if (typeof m.content === "string") {
6159
- text = m.content;
6160
- } else if (Array.isArray(m.content)) {
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: m.role, content: text });
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
- if (!id) return null;
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
- if (!cfg.brains[bid].enabled) return null;
6179
- if (!isInstalled(cfg.brains[bid].command)) return null;
6180
- return { kind: "cli", id: bid };
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) return;
6192
- if (!guardTrusted(real)) return;
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) return;
6195
- if (cfg.memory?.extractFacts === false) return;
6196
- const brain = resolveExtractionBrain(cfg);
6197
- if (!brain) return;
6198
- if (!data.transcript_path) return;
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) return;
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
- process.stderr.write("holt hook capture: " + (e instanceof Error ? e.message : String(e)) + "\n");
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 resolve5 } from "path";
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 = resolve5(process.cwd(), p.out);
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 spawnSync4 } from "child_process";
6374
- import { writeFileSync as writeFileSync15, mkdirSync as mkdirSync13, rmSync as rmSync6, existsSync as existsSync15 } from "fs";
6375
- import { dirname as dirname6 } from "path";
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 homedir4 } from "os";
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 spawnSync3 } from "child_process";
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(homedir4(), "Library", "LaunchAgents", `com.holt.${id}.plist`);
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 = spawnSync3(finder, ["holt"], { encoding: "utf8" });
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(dirname6(path), { recursive: true });
6944
+ mkdirSync13(dirname7(path), { recursive: true });
6540
6945
  writeFileSync15(path, buildLaunchdPlist(job, holtPath), "utf8");
6541
- const res = spawnSync4("launchctl", ["load", "-w", path], { encoding: "utf8" });
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 = spawnSync4("crontab", ["-l"], { encoding: "utf8" });
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 = spawnSync4("crontab", ["-"], { input: body, encoding: "utf8" });
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 (existsSync15(path)) {
6629
- spawnSync4("launchctl", ["unload", path], { encoding: "utf8" });
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 = spawnSync4("crontab", ["-l"], { encoding: "utf8" });
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
- spawnSync4("crontab", ["-"], { input: next, encoding: "utf8" });
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 spawnSync5 } from "child_process";
6691
- import { writeFileSync as writeFileSync17, mkdirSync as mkdirSync15, rmSync as rmSync7, existsSync as existsSync16 } from "fs";
6692
- import { dirname as dirname7, resolve as resolve6 } from "path";
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(dirname7(path), { recursive: true });
7178
+ mkdirSync15(dirname8(path), { recursive: true });
6774
7179
  writeFileSync17(path, buildLaunchdPlist(job, holtPath), "utf8");
6775
- const res = spawnSync5("launchctl", ["load", "-w", path], { encoding: "utf8" });
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 = spawnSync5("crontab", ["-l"], { encoding: "utf8" });
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 = spawnSync5("crontab", ["-"], { input: body, encoding: "utf8" });
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 (existsSync16(path)) {
6819
- spawnSync5("launchctl", ["unload", path], { encoding: "utf8" });
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 = resolve6(routine2.workspace, outTarget);
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 resolve7 } from "path";
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 = resolve7(process.cwd(), p.out);
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");