@holt-os/holt 0.12.0 → 0.12.1
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 +12 -1
- package/dist/cli.js +296 -58
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,14 @@ Holt is an open-source, self-hosted personal agent OS. Clone it, pick your skill
|
|
|
6
6
|
|
|
7
7
|
> A *holt* is a small wood: a sheltered place where things are kept and grow. That's the idea. A private home for your knowledge that compounds over time.
|
|
8
8
|
|
|
9
|
-
> **Status:
|
|
9
|
+
> **Status: shipping and usable today.** Working now:
|
|
10
|
+
>
|
|
11
|
+
> - `holt init` + `holt chat` with any brain (Claude Code, Codex, Gemini, or a direct API key), and mid-conversation brain switching that keeps context.
|
|
12
|
+
> - Per-folder memory with semantic recall and distilled facts, ambient memory for Claude Code (`holt hook`), and opt-in memory shared across folders (`holt memory global`).
|
|
13
|
+
> - A self-maintaining knowledge wiki (`holt wiki`) and an interactive knowledge graph (`holt graph`) that can also ingest your code and docs into communities.
|
|
14
|
+
> - Run a task once, on a schedule, or as a named routine (`holt run` / `schedule` / `routine`), and reach it from your phone over Telegram.
|
|
15
|
+
> - Draft in your own voice with anti-AI checks (`holt write`), and size your setup with `holt doctor`.
|
|
16
|
+
> - Ten built-in skills plus a community registry (`holt skill search`), and an MCP server so Claude Code, Cursor, or Codex can read this folder's memory.
|
|
10
17
|
|
|
11
18
|
---
|
|
12
19
|
|
|
@@ -319,6 +326,8 @@ Install **merges** into any existing `hooks` config, never clobbers unrelated se
|
|
|
319
326
|
|
|
320
327
|
**Trusted-folder guard (important).** The two runtime hooks (`holt hook inject` and `holt hook capture`, invoked by Claude Code, not you) no-op **silently** unless the folder is a trusted Holt workspace with an existing `.holt/memory`. So Holt never injects your private notes into an unrelated project, and never creates memory in a folder you never set up. To make a folder ambient, run `holt init` (or `holt chat`) there once so it becomes trusted and gets its `.holt/memory`. The inject hook keeps stdout clean (only the context block is printed, since Claude Code injects it verbatim); all diagnostics go to stderr, and both hooks always exit 0 so a memory step never blocks or slows your prompt.
|
|
321
328
|
|
|
329
|
+
**Seeing what the hooks did (`~/.holt/hooks.log`).** Since the hooks stay silent, every `holt hook capture` run appends **one** line to `~/.holt/hooks.log` with the outcome or the exact skip reason (untrusted folder, no config, `extractFacts` off, no usable brain, missing or empty transcript, too few exchanges, or `saved N facts`), preceded by a line of the raw hook fields it received. Check this file first if capture seems to save nothing. `inject` uses the same log but stays quiet by default; set `HOLT_HOOK_DEBUG=1` to make it log too.
|
|
330
|
+
|
|
322
331
|
To uninstall, run `holt hook remove` (add `--project` if you installed with `--project`).
|
|
323
332
|
|
|
324
333
|
## Output format
|
|
@@ -435,6 +444,8 @@ holt voice clear # remove it
|
|
|
435
444
|
|
|
436
445
|
The interview asks you to pick a **depth** up front: `quick` (a few questions) or `detailed` (more). Questions are about **writing and communication style only**: tone, sentence length, first vs third person, emoji habits, words you love or ban, who you write for, and what you want to sound like and not sound like.
|
|
437
446
|
|
|
447
|
+
Changed your mind at the first prompt? Answer `n`, `skip`, `cancel`, or just press enter to back out cleanly. Nothing is saved and no profile is built; run `holt voice` again whenever you like. Inside the question list, an empty answer skips only that one question.
|
|
448
|
+
|
|
438
449
|
Privacy is a hard rule. The interview never asks for personal details (no name, job, location, or life story). If you happen to mention something personal in an answer, Holt stores only what you typed and never asks follow ups.
|
|
439
450
|
|
|
440
451
|
You can also feed it real writing. `holt voice add <file>` reads a file; inside the interview you can paste a sample instead. Samples are stored as a hash and length by default; an excerpt is kept only if you say yes, so Holt can match your rhythm directly.
|
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 = [];
|
|
@@ -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
|
}
|
|
@@ -1756,10 +1821,19 @@ var QUESTIONS = [
|
|
|
1756
1821
|
{ key: "avoid", q: "What do you NOT want to sound like? (e.g. corporate, salesy, robotic)", depth: "detailed" },
|
|
1757
1822
|
{ key: "signature", q: "Any signature moves? (openings you like, how you end, humor, analogies)", depth: "detailed" }
|
|
1758
1823
|
];
|
|
1824
|
+
function isSkip(a) {
|
|
1825
|
+
if (a === null) return true;
|
|
1826
|
+
const s = a.trim().toLowerCase();
|
|
1827
|
+
return s === "" || s === "n" || s === "no" || s === "skip" || s === "cancel" || s === "q" || s === "quit";
|
|
1828
|
+
}
|
|
1829
|
+
function skipNotice() {
|
|
1830
|
+
console.log(c.dim("\n Skipped. Run ") + c.accent("holt voice") + c.dim(" anytime.\n"));
|
|
1831
|
+
}
|
|
1759
1832
|
async function askDepth(ask) {
|
|
1760
1833
|
const a = await ask('Depth? "quick" (a few questions) or "detailed" (more): [quick] ');
|
|
1761
1834
|
if (a === null) return null;
|
|
1762
1835
|
const s = a.trim().toLowerCase();
|
|
1836
|
+
if (s === "n" || s === "no" || s === "skip" || s === "cancel" || s === "q" || s === "quit") return null;
|
|
1763
1837
|
return s === "detailed" || s === "d" ? "detailed" : "quick";
|
|
1764
1838
|
}
|
|
1765
1839
|
async function runQuestions(ask, depth) {
|
|
@@ -1887,13 +1961,21 @@ async function voiceInterview() {
|
|
|
1887
1961
|
const v = existing ?? emptyVoice();
|
|
1888
1962
|
console.log("\n" + c.accent("Teach Holt your writing voice"));
|
|
1889
1963
|
console.log(c.dim(" Style only. No personal questions. You can do the interview, share samples, or both.\n"));
|
|
1890
|
-
const
|
|
1891
|
-
|
|
1964
|
+
const modeRaw = await ask("Do the [i]nterview, add [s]amples, or [b]oth? [i] ");
|
|
1965
|
+
if (isSkip(modeRaw)) {
|
|
1966
|
+
close();
|
|
1967
|
+
skipNotice();
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
const mode = (modeRaw ?? "").trim().toLowerCase();
|
|
1971
|
+
const wantInterview = mode === "i" || mode === "b" || mode === "interview" || mode === "both";
|
|
1892
1972
|
const wantSamples = mode === "s" || mode === "b" || mode === "samples" || mode === "both";
|
|
1893
|
-
|
|
1973
|
+
const wantInterviewOrDefault = wantInterview || !wantSamples;
|
|
1974
|
+
if (wantInterviewOrDefault) {
|
|
1894
1975
|
const depth = await askDepth(ask);
|
|
1895
1976
|
if (depth === null) {
|
|
1896
1977
|
close();
|
|
1978
|
+
skipNotice();
|
|
1897
1979
|
return;
|
|
1898
1980
|
}
|
|
1899
1981
|
v.depth = depth;
|
|
@@ -1917,6 +1999,11 @@ async function voiceInterview() {
|
|
|
1917
1999
|
}
|
|
1918
2000
|
}
|
|
1919
2001
|
close();
|
|
2002
|
+
const gotAnswers = v.answers.some((a) => a.answer.trim());
|
|
2003
|
+
if (!gotAnswers && v.samples.length === 0) {
|
|
2004
|
+
skipNotice();
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
1920
2007
|
saveVoice(v);
|
|
1921
2008
|
console.log(c.dim("\n Building your style profile from a brain..."));
|
|
1922
2009
|
const synthed = await synthesizeVoice(v);
|
|
@@ -2218,7 +2305,10 @@ async function extractAndSaveFacts(active, cfg, history, session) {
|
|
|
2218
2305
|
try {
|
|
2219
2306
|
if (Math.floor(history.length / 2) < MIN_EXCHANGES_FOR_EXTRACTION) return 0;
|
|
2220
2307
|
const prompt = buildExtractionPrompt(history);
|
|
2221
|
-
const res = active.kind === "cli" ? await runBrain(
|
|
2308
|
+
const res = active.kind === "cli" ? await runBrain(
|
|
2309
|
+
active.commandOverride ? { ...cfg.brains[active.id], command: active.commandOverride } : cfg.brains[active.id],
|
|
2310
|
+
prompt
|
|
2311
|
+
) : await runApiBrain(active.brain, prompt);
|
|
2222
2312
|
if (!res.ok) return 0;
|
|
2223
2313
|
const facts = parseFacts(res.text);
|
|
2224
2314
|
let saved = 0;
|
|
@@ -2896,9 +2986,8 @@ function saveReply(reply, format, name) {
|
|
|
2896
2986
|
function statusLine(brainLabel, history) {
|
|
2897
2987
|
const live = Math.min(history.length, MAX_REPLAY_TURNS);
|
|
2898
2988
|
const frac = MAX_REPLAY_TURNS ? live / MAX_REPLAY_TURNS : 0;
|
|
2899
|
-
const pct = Math.round(frac * 100);
|
|
2900
2989
|
const mem = memStats().turns;
|
|
2901
|
-
return c.dim(" " + brainLabel + " ") + c.dim("[") + bar(frac) + c.dim("]") + c.dim(` ${
|
|
2990
|
+
return c.dim(" " + brainLabel + " ") + c.dim("[") + bar(frac) + c.dim("]") + c.dim(` recent ${live}/${MAX_REPLAY_TURNS} \xB7 ${mem} in memory`);
|
|
2902
2991
|
}
|
|
2903
2992
|
function help() {
|
|
2904
2993
|
console.log(c.dim([
|
|
@@ -2954,6 +3043,12 @@ async function chat() {
|
|
|
2954
3043
|
const session = newSessionId();
|
|
2955
3044
|
const history = [];
|
|
2956
3045
|
let lastReply = "";
|
|
3046
|
+
const sb = createStatusBar();
|
|
3047
|
+
const showStatus = () => {
|
|
3048
|
+
const line = statusLine(active.label, history);
|
|
3049
|
+
if (sb.active) sb.set(line);
|
|
3050
|
+
else console.log(line + "\n");
|
|
3051
|
+
};
|
|
2957
3052
|
const embedOk = await embeddingsAvailable();
|
|
2958
3053
|
const stats = memStats();
|
|
2959
3054
|
console.log("\n" + c.accent("Holt") + c.dim(` brain: ${active.label}`));
|
|
@@ -2964,7 +3059,7 @@ async function chat() {
|
|
|
2964
3059
|
console.log(c.dim(` ${stats.turns - stats.withEmbeddings} older moments lack embeddings. Run "holt memory embed" to upgrade them.`));
|
|
2965
3060
|
}
|
|
2966
3061
|
console.log(c.dim("Type a message. Commands: /brain /memory /output /save /setting /clear /help /exit"));
|
|
2967
|
-
|
|
3062
|
+
showStatus();
|
|
2968
3063
|
while (true) {
|
|
2969
3064
|
const raw = await ask(c.accent("\u203A "));
|
|
2970
3065
|
if (raw === null) break;
|
|
@@ -3107,11 +3202,12 @@ async function chat() {
|
|
|
3107
3202
|
const now = Date.now();
|
|
3108
3203
|
appendTurn({ id: randomUUID3().slice(0, 8), ts: now, session, role: "user", content: line, emb: await embed(line) ?? void 0 });
|
|
3109
3204
|
appendTurn({ id: randomUUID3().slice(0, 8), ts: now, session, role: "assistant", content: res.text, emb: await embed(res.text) ?? void 0 });
|
|
3110
|
-
|
|
3205
|
+
showStatus();
|
|
3111
3206
|
} else {
|
|
3112
3207
|
console.log(c.red("\n " + res.text + "\n"));
|
|
3113
3208
|
}
|
|
3114
3209
|
}
|
|
3210
|
+
sb.detach();
|
|
3115
3211
|
if (cfg.memory?.extractFacts !== false) {
|
|
3116
3212
|
try {
|
|
3117
3213
|
const arg = active.kind === "cli" ? { kind: "cli", id: active.id } : { kind: "api", brain: active.brain };
|
|
@@ -5853,7 +5949,7 @@ function holtTools() {
|
|
|
5853
5949
|
}
|
|
5854
5950
|
|
|
5855
5951
|
// src/version.ts
|
|
5856
|
-
var VERSION = "0.12.
|
|
5952
|
+
var VERSION = "0.12.1";
|
|
5857
5953
|
|
|
5858
5954
|
// src/commands/mcp.ts
|
|
5859
5955
|
function printSetup() {
|
|
@@ -5899,7 +5995,8 @@ async function mcp(sub, rest = []) {
|
|
|
5899
5995
|
}
|
|
5900
5996
|
|
|
5901
5997
|
// src/commands/hook.ts
|
|
5902
|
-
import { readFileSync as readFileSync15, writeFileSync as writeFileSync12, existsSync as existsSync14, mkdirSync as mkdirSync11, copyFileSync, realpathSync as realpathSync2 } from "fs";
|
|
5998
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync12, existsSync as existsSync14, mkdirSync as mkdirSync11, copyFileSync, realpathSync as realpathSync2, appendFileSync as appendFileSync2, statSync as statSync7 } from "fs";
|
|
5999
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
5903
6000
|
import { join as join15, dirname as dirname5 } from "path";
|
|
5904
6001
|
import { homedir as homedir3 } from "os";
|
|
5905
6002
|
var EVENT = {
|
|
@@ -6114,25 +6211,87 @@ function guardTrusted(real) {
|
|
|
6114
6211
|
if (!existsSync14(memDir())) return false;
|
|
6115
6212
|
return true;
|
|
6116
6213
|
}
|
|
6214
|
+
function hookLog(msg) {
|
|
6215
|
+
try {
|
|
6216
|
+
mkdirSync11(GLOBAL_DIR, { recursive: true });
|
|
6217
|
+
appendFileSync2(join15(GLOBAL_DIR, "hooks.log"), `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
6218
|
+
`, "utf8");
|
|
6219
|
+
} catch {
|
|
6220
|
+
}
|
|
6221
|
+
}
|
|
6222
|
+
function fileBytes(path) {
|
|
6223
|
+
if (!path) return 0;
|
|
6224
|
+
try {
|
|
6225
|
+
return statSync7(path).size;
|
|
6226
|
+
} catch {
|
|
6227
|
+
return 0;
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6117
6230
|
async function inject() {
|
|
6231
|
+
const debug = !!process.env.HOLT_HOOK_DEBUG;
|
|
6232
|
+
const dlog = (m) => {
|
|
6233
|
+
if (debug) hookLog(m);
|
|
6234
|
+
};
|
|
6118
6235
|
try {
|
|
6119
6236
|
const raw = await readStdin(2e3);
|
|
6120
6237
|
const data = parseHookInput(raw);
|
|
6238
|
+
dlog(
|
|
6239
|
+
`inject cwd=${data.cwd ?? "(none)"} event=${data.hook_event_name ?? "UserPromptSubmit"} hasCwd=${!!data.cwd} promptLen=${(data.prompt || "").trim().length}`
|
|
6240
|
+
);
|
|
6121
6241
|
const prompt = (data.prompt || "").trim();
|
|
6122
|
-
if (prompt.length < 3)
|
|
6242
|
+
if (prompt.length < 3) {
|
|
6243
|
+
dlog("inject -> prompt too short");
|
|
6244
|
+
return;
|
|
6245
|
+
}
|
|
6123
6246
|
const real = enterFolder(data.cwd);
|
|
6124
|
-
if (!real)
|
|
6125
|
-
|
|
6247
|
+
if (!real) {
|
|
6248
|
+
dlog("inject -> no cwd");
|
|
6249
|
+
return;
|
|
6250
|
+
}
|
|
6251
|
+
if (!guardTrusted(real)) {
|
|
6252
|
+
dlog(`inject -> untrusted or no .holt/memory (real=${real})`);
|
|
6253
|
+
return;
|
|
6254
|
+
}
|
|
6126
6255
|
const session = data.session_id || newSessionId();
|
|
6127
6256
|
const hits = await recall(prompt, session, 4);
|
|
6128
|
-
if (!hits.length)
|
|
6257
|
+
if (!hits.length) {
|
|
6258
|
+
dlog("inject -> recalled 0 notes");
|
|
6259
|
+
return;
|
|
6260
|
+
}
|
|
6129
6261
|
const lines = hits.map((h) => `- ${h.turn.content.replace(/\s+/g, " ").trim().slice(0, 500)}`);
|
|
6130
6262
|
process.stdout.write(
|
|
6131
|
-
[
|
|
6263
|
+
[
|
|
6264
|
+
"Background on this user, recalled from earlier sessions (use it directly, do not mention looking it up):",
|
|
6265
|
+
...lines,
|
|
6266
|
+
""
|
|
6267
|
+
].join("\n")
|
|
6132
6268
|
);
|
|
6269
|
+
dlog(`inject -> injected ${hits.length} note(s)`);
|
|
6133
6270
|
} catch (e) {
|
|
6134
6271
|
process.stderr.write("holt hook inject: " + (e instanceof Error ? e.message : String(e)) + "\n");
|
|
6272
|
+
dlog("inject -> error: " + (e instanceof Error ? e.message : String(e)));
|
|
6273
|
+
}
|
|
6274
|
+
}
|
|
6275
|
+
function countLines(raw) {
|
|
6276
|
+
let n = 0;
|
|
6277
|
+
for (const line of raw.split("\n")) if (line.trim()) n++;
|
|
6278
|
+
return n;
|
|
6279
|
+
}
|
|
6280
|
+
function textFromContent(content) {
|
|
6281
|
+
if (typeof content === "string") return content.trim();
|
|
6282
|
+
if (!Array.isArray(content)) return "";
|
|
6283
|
+
const parts = [];
|
|
6284
|
+
for (const b of content) {
|
|
6285
|
+
if (typeof b === "string") {
|
|
6286
|
+
parts.push(b);
|
|
6287
|
+
continue;
|
|
6288
|
+
}
|
|
6289
|
+
if (!b || typeof b !== "object") continue;
|
|
6290
|
+
const blk = b;
|
|
6291
|
+
if (blk.type === "tool_use" || blk.type === "tool_result" || blk.type === "thinking") continue;
|
|
6292
|
+
if (typeof blk.text === "string") parts.push(blk.text);
|
|
6135
6293
|
}
|
|
6294
|
+
return parts.join(" ").trim();
|
|
6136
6295
|
}
|
|
6137
6296
|
function readTranscript(path) {
|
|
6138
6297
|
const out = [];
|
|
@@ -6150,56 +6309,133 @@ function readTranscript(path) {
|
|
|
6150
6309
|
} catch {
|
|
6151
6310
|
continue;
|
|
6152
6311
|
}
|
|
6312
|
+
if (!rec || typeof rec !== "object") continue;
|
|
6153
6313
|
const d = rec;
|
|
6154
|
-
|
|
6155
|
-
const m = d.message;
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
if (
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
text = m.content.map((b) => {
|
|
6162
|
-
if (typeof b === "string") return b;
|
|
6163
|
-
if (b && typeof b.text === "string") return b.text;
|
|
6164
|
-
return "";
|
|
6165
|
-
}).join(" ").trim();
|
|
6166
|
-
}
|
|
6314
|
+
const topType = d.type === "user" || d.type === "assistant" ? d.type : void 0;
|
|
6315
|
+
const m = d.message && typeof d.message === "object" ? d.message : void 0;
|
|
6316
|
+
const msgRole = m && (m.role === "user" || m.role === "assistant") ? m.role : void 0;
|
|
6317
|
+
const role = msgRole ?? topType;
|
|
6318
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
6319
|
+
let text = m ? textFromContent(m.content) : "";
|
|
6320
|
+
if (!text) text = textFromContent(d.content);
|
|
6167
6321
|
text = text.trim();
|
|
6168
6322
|
if (text.length < 3) continue;
|
|
6169
|
-
out.push({ role
|
|
6323
|
+
out.push({ role, content: text });
|
|
6170
6324
|
}
|
|
6171
6325
|
return out;
|
|
6172
6326
|
}
|
|
6327
|
+
function resolveCommandPath(command) {
|
|
6328
|
+
if (command.startsWith("/") && existsSync14(command)) return command;
|
|
6329
|
+
try {
|
|
6330
|
+
const shell = process.env.SHELL || "/bin/sh";
|
|
6331
|
+
const res = spawnSync3(shell, ["-lc", "command -v " + command], { encoding: "utf8" });
|
|
6332
|
+
if (res.status === 0) {
|
|
6333
|
+
const p = (res.stdout || "").trim().split("\n")[0]?.trim();
|
|
6334
|
+
if (p && p.startsWith("/") && existsSync14(p)) return p;
|
|
6335
|
+
}
|
|
6336
|
+
} catch {
|
|
6337
|
+
}
|
|
6338
|
+
const home = homedir3();
|
|
6339
|
+
const dirs = [
|
|
6340
|
+
join15(home, ".local", "bin"),
|
|
6341
|
+
"/opt/homebrew/bin",
|
|
6342
|
+
"/usr/local/bin",
|
|
6343
|
+
join15(home, ".npm-global", "bin"),
|
|
6344
|
+
"/usr/bin",
|
|
6345
|
+
"/bin"
|
|
6346
|
+
];
|
|
6347
|
+
for (const dir of dirs) {
|
|
6348
|
+
const candidate = join15(dir, command);
|
|
6349
|
+
if (existsSync14(candidate)) return candidate;
|
|
6350
|
+
}
|
|
6351
|
+
return null;
|
|
6352
|
+
}
|
|
6173
6353
|
function resolveExtractionBrain(cfg) {
|
|
6174
6354
|
const id = cfg.defaultBrain;
|
|
6175
|
-
|
|
6355
|
+
const diag = { defaultBrain: id ?? "(none)", command: "(none)", whichFound: false, resolvedPath: null };
|
|
6356
|
+
if (!id) return { brain: null, diag };
|
|
6176
6357
|
if (BRAIN_IDS.includes(id)) {
|
|
6177
6358
|
const bid = id;
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
return {
|
|
6359
|
+
const command = cfg.brains[bid].command;
|
|
6360
|
+
diag.command = command;
|
|
6361
|
+
if (!cfg.brains[bid].enabled) return { brain: null, diag };
|
|
6362
|
+
const whichFound = isInstalled(command);
|
|
6363
|
+
diag.whichFound = whichFound;
|
|
6364
|
+
if (whichFound) {
|
|
6365
|
+
return { brain: { kind: "cli", id: bid }, diag };
|
|
6366
|
+
}
|
|
6367
|
+
const resolved = resolveCommandPath(command);
|
|
6368
|
+
diag.resolvedPath = resolved;
|
|
6369
|
+
if (resolved) {
|
|
6370
|
+
return { brain: { kind: "cli", id: bid, commandOverride: resolved }, diag };
|
|
6371
|
+
}
|
|
6372
|
+
return { brain: null, diag };
|
|
6181
6373
|
}
|
|
6182
6374
|
const api = findApiBrain(cfg, id);
|
|
6183
|
-
if (api && resolveApiKey(api)) return { kind: "api", brain: api };
|
|
6184
|
-
return null;
|
|
6375
|
+
if (api && resolveApiKey(api)) return { brain: { kind: "api", brain: api }, diag };
|
|
6376
|
+
return { brain: null, diag };
|
|
6185
6377
|
}
|
|
6186
6378
|
async function capture() {
|
|
6379
|
+
const cwdForLog = process.cwd();
|
|
6187
6380
|
try {
|
|
6188
6381
|
const raw = await readStdin(3e3);
|
|
6189
6382
|
const data = parseHookInput(raw);
|
|
6383
|
+
hookLog(
|
|
6384
|
+
`capture-fields event=${data.hook_event_name ?? "Stop"} hasCwd=${!!data.cwd} hasTranscriptPath=${!!data.transcript_path} transcriptBytes=${fileBytes(data.transcript_path)} stdinBytes=${raw.length}`
|
|
6385
|
+
);
|
|
6190
6386
|
const real = enterFolder(data.cwd);
|
|
6191
|
-
if (!real)
|
|
6192
|
-
|
|
6387
|
+
if (!real) {
|
|
6388
|
+
hookLog(`capture cwd=${data.cwd ?? cwdForLog} event=Stop -> no cwd`);
|
|
6389
|
+
return;
|
|
6390
|
+
}
|
|
6391
|
+
if (!guardTrusted(real)) {
|
|
6392
|
+
hookLog(`capture cwd=${real} event=Stop -> untrusted or no .holt/memory (real=${real})`);
|
|
6393
|
+
return;
|
|
6394
|
+
}
|
|
6193
6395
|
const cfg = loadConfig();
|
|
6194
|
-
if (!cfg)
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
if (
|
|
6396
|
+
if (!cfg) {
|
|
6397
|
+
hookLog(`capture cwd=${real} event=Stop -> no config`);
|
|
6398
|
+
return;
|
|
6399
|
+
}
|
|
6400
|
+
if (cfg.memory?.extractFacts === false) {
|
|
6401
|
+
hookLog(`capture cwd=${real} event=Stop -> extractFacts off`);
|
|
6402
|
+
return;
|
|
6403
|
+
}
|
|
6404
|
+
const { brain, diag } = resolveExtractionBrain(cfg);
|
|
6405
|
+
if (!brain) {
|
|
6406
|
+
hookLog(
|
|
6407
|
+
`capture cwd=${real} event=Stop -> no usable brain (defaultBrain=${diag.defaultBrain} command=${diag.command} whichFound=${diag.whichFound} resolvedPath=${diag.resolvedPath ?? "none"})`
|
|
6408
|
+
);
|
|
6409
|
+
return;
|
|
6410
|
+
}
|
|
6411
|
+
if (brain.kind === "cli" && brain.commandOverride) {
|
|
6412
|
+
hookLog(`capture cwd=${real} event=Stop -> brain resolved via absolute path (resolvedPath=${brain.commandOverride})`);
|
|
6413
|
+
}
|
|
6414
|
+
if (!data.transcript_path) {
|
|
6415
|
+
hookLog(`capture cwd=${real} event=Stop -> no transcript_path`);
|
|
6416
|
+
return;
|
|
6417
|
+
}
|
|
6199
6418
|
const history = readTranscript(data.transcript_path);
|
|
6200
|
-
if (!history.length)
|
|
6419
|
+
if (!history.length) {
|
|
6420
|
+
let bytes = 0;
|
|
6421
|
+
let lines = 0;
|
|
6422
|
+
try {
|
|
6423
|
+
const rawT = readFileSync15(data.transcript_path, "utf8");
|
|
6424
|
+
bytes = Buffer.byteLength(rawT, "utf8");
|
|
6425
|
+
lines = countLines(rawT);
|
|
6426
|
+
} catch {
|
|
6427
|
+
}
|
|
6428
|
+
hookLog(`capture cwd=${real} event=Stop -> transcript parsed 0 turns (path=${data.transcript_path} bytes=${bytes} lines=${lines})`);
|
|
6429
|
+
return;
|
|
6430
|
+
}
|
|
6431
|
+
const exchanges = Math.floor(history.length / 2);
|
|
6432
|
+
if (exchanges < MIN_EXCHANGES_FOR_EXTRACTION) {
|
|
6433
|
+
hookLog(`capture cwd=${real} event=Stop -> only ${exchanges} exchange(s) (<${MIN_EXCHANGES_FOR_EXTRACTION}, skipped)`);
|
|
6434
|
+
return;
|
|
6435
|
+
}
|
|
6201
6436
|
const session = "hook-" + (data.session_id || newSessionId());
|
|
6202
6437
|
const n = await extractAndSaveFacts(brain, cfg, history, session);
|
|
6438
|
+
hookLog(`capture cwd=${real} event=Stop -> saved ${n} fact${n === 1 ? "" : "s"}`);
|
|
6203
6439
|
if (n > 0) process.stderr.write(`holt hook capture: saved ${n} fact${n === 1 ? "" : "s"} to ${memDir()}
|
|
6204
6440
|
`);
|
|
6205
6441
|
if (cfg.wiki?.autoSync) {
|
|
@@ -6212,7 +6448,9 @@ async function capture() {
|
|
|
6212
6448
|
}
|
|
6213
6449
|
}
|
|
6214
6450
|
} catch (e) {
|
|
6215
|
-
|
|
6451
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
6452
|
+
process.stderr.write("holt hook capture: " + msg + "\n");
|
|
6453
|
+
hookLog(`capture cwd=${cwdForLog} event=Stop -> error: ${msg}`);
|
|
6216
6454
|
}
|
|
6217
6455
|
}
|
|
6218
6456
|
var USAGE2 = [
|
|
@@ -6370,7 +6608,7 @@ async function run(args) {
|
|
|
6370
6608
|
}
|
|
6371
6609
|
|
|
6372
6610
|
// src/commands/schedule.ts
|
|
6373
|
-
import { spawnSync as
|
|
6611
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
6374
6612
|
import { writeFileSync as writeFileSync15, mkdirSync as mkdirSync13, rmSync as rmSync6, existsSync as existsSync15 } from "fs";
|
|
6375
6613
|
import { dirname as dirname6 } from "path";
|
|
6376
6614
|
|
|
@@ -6379,7 +6617,7 @@ import { homedir as homedir4 } from "os";
|
|
|
6379
6617
|
import { join as join16 } from "path";
|
|
6380
6618
|
import { readFileSync as readFileSync16, writeFileSync as writeFileSync14, mkdirSync as mkdirSync12 } from "fs";
|
|
6381
6619
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
6382
|
-
import { spawnSync as
|
|
6620
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
6383
6621
|
function schedulesPath() {
|
|
6384
6622
|
return join16(GLOBAL_DIR, "schedules.json");
|
|
6385
6623
|
}
|
|
@@ -6435,7 +6673,7 @@ function shQuote(s) {
|
|
|
6435
6673
|
function resolveHoltPath() {
|
|
6436
6674
|
try {
|
|
6437
6675
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
6438
|
-
const res =
|
|
6676
|
+
const res = spawnSync4(finder, ["holt"], { encoding: "utf8" });
|
|
6439
6677
|
if (res.status === 0 && typeof res.stdout === "string") {
|
|
6440
6678
|
const first = res.stdout.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
6441
6679
|
if (first) return first;
|
|
@@ -6538,7 +6776,7 @@ function installDarwin(job, holtPath) {
|
|
|
6538
6776
|
const path = plistPath(job.id);
|
|
6539
6777
|
mkdirSync13(dirname6(path), { recursive: true });
|
|
6540
6778
|
writeFileSync15(path, buildLaunchdPlist(job, holtPath), "utf8");
|
|
6541
|
-
const res =
|
|
6779
|
+
const res = spawnSync5("launchctl", ["load", "-w", path], { encoding: "utf8" });
|
|
6542
6780
|
if (res.status === 0) {
|
|
6543
6781
|
console.log(c.green(" Loaded into launchd."));
|
|
6544
6782
|
} else {
|
|
@@ -6549,11 +6787,11 @@ function installDarwin(job, holtPath) {
|
|
|
6549
6787
|
console.log(c.dim(` plist: ${path}`));
|
|
6550
6788
|
}
|
|
6551
6789
|
function currentCrontab() {
|
|
6552
|
-
const res =
|
|
6790
|
+
const res = spawnSync5("crontab", ["-l"], { encoding: "utf8" });
|
|
6553
6791
|
return res.status === 0 && typeof res.stdout === "string" ? res.stdout : "";
|
|
6554
6792
|
}
|
|
6555
6793
|
function writeCrontab(body) {
|
|
6556
|
-
const res =
|
|
6794
|
+
const res = spawnSync5("crontab", ["-"], { input: body, encoding: "utf8" });
|
|
6557
6795
|
return res.status === 0;
|
|
6558
6796
|
}
|
|
6559
6797
|
function installLinux(job, holtPath) {
|
|
@@ -6626,7 +6864,7 @@ function listCmd2() {
|
|
|
6626
6864
|
function removeDarwin(id) {
|
|
6627
6865
|
const path = plistPath(id);
|
|
6628
6866
|
if (existsSync15(path)) {
|
|
6629
|
-
|
|
6867
|
+
spawnSync5("launchctl", ["unload", path], { encoding: "utf8" });
|
|
6630
6868
|
try {
|
|
6631
6869
|
rmSync6(path);
|
|
6632
6870
|
} catch {
|
|
@@ -6634,10 +6872,10 @@ function removeDarwin(id) {
|
|
|
6634
6872
|
}
|
|
6635
6873
|
}
|
|
6636
6874
|
function removeLinux(id) {
|
|
6637
|
-
const res =
|
|
6875
|
+
const res = spawnSync5("crontab", ["-l"], { encoding: "utf8" });
|
|
6638
6876
|
const body = res.status === 0 && typeof res.stdout === "string" ? res.stdout : "";
|
|
6639
6877
|
const next = stripCronLines(body, id);
|
|
6640
|
-
|
|
6878
|
+
spawnSync5("crontab", ["-"], { input: next, encoding: "utf8" });
|
|
6641
6879
|
}
|
|
6642
6880
|
function removeCmd(rest) {
|
|
6643
6881
|
const id = rest[0];
|
|
@@ -6687,7 +6925,7 @@ async function schedule(sub, rest = []) {
|
|
|
6687
6925
|
}
|
|
6688
6926
|
|
|
6689
6927
|
// src/commands/routine.ts
|
|
6690
|
-
import { spawnSync as
|
|
6928
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
6691
6929
|
import { writeFileSync as writeFileSync17, mkdirSync as mkdirSync15, rmSync as rmSync7, existsSync as existsSync16 } from "fs";
|
|
6692
6930
|
import { dirname as dirname7, resolve as resolve6 } from "path";
|
|
6693
6931
|
|
|
@@ -6772,7 +7010,7 @@ function installDarwin2(job, holtPath) {
|
|
|
6772
7010
|
const path = plistPath(job.id);
|
|
6773
7011
|
mkdirSync15(dirname7(path), { recursive: true });
|
|
6774
7012
|
writeFileSync17(path, buildLaunchdPlist(job, holtPath), "utf8");
|
|
6775
|
-
const res =
|
|
7013
|
+
const res = spawnSync6("launchctl", ["load", "-w", path], { encoding: "utf8" });
|
|
6776
7014
|
if (res.status === 0) {
|
|
6777
7015
|
console.log(c.green(" Loaded into launchd."));
|
|
6778
7016
|
} else {
|
|
@@ -6783,11 +7021,11 @@ function installDarwin2(job, holtPath) {
|
|
|
6783
7021
|
console.log(c.dim(` plist: ${path}`));
|
|
6784
7022
|
}
|
|
6785
7023
|
function currentCrontab2() {
|
|
6786
|
-
const res =
|
|
7024
|
+
const res = spawnSync6("crontab", ["-l"], { encoding: "utf8" });
|
|
6787
7025
|
return res.status === 0 && typeof res.stdout === "string" ? res.stdout : "";
|
|
6788
7026
|
}
|
|
6789
7027
|
function writeCrontab2(body) {
|
|
6790
|
-
const res =
|
|
7028
|
+
const res = spawnSync6("crontab", ["-"], { input: body, encoding: "utf8" });
|
|
6791
7029
|
return res.status === 0;
|
|
6792
7030
|
}
|
|
6793
7031
|
function installLinux2(job, holtPath) {
|
|
@@ -6816,7 +7054,7 @@ function installSchedule(r) {
|
|
|
6816
7054
|
function removeScheduleDarwin(id) {
|
|
6817
7055
|
const path = plistPath(id);
|
|
6818
7056
|
if (existsSync16(path)) {
|
|
6819
|
-
|
|
7057
|
+
spawnSync6("launchctl", ["unload", path], { encoding: "utf8" });
|
|
6820
7058
|
try {
|
|
6821
7059
|
rmSync7(path);
|
|
6822
7060
|
} catch {
|