@holt-os/holt 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/cli.js +160 -75
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,7 @@ During `holt init` you:
|
|
|
38
38
|
2. **Choose brains** (claude, codex, gemini). Holt installs any you pick that are missing.
|
|
39
39
|
3. **Sign in.** For a newly installed brain, Holt starts that tool's own login (browser or its own prompt). Holt never stores your credentials.
|
|
40
40
|
4. **Pick a default** brain and, optionally, a **launch command** (a short word like `ai` that runs `holt chat`).
|
|
41
|
+
5. **Enable semantic memory.** If you say yes, Holt sets up a local [Ollama](https://ollama.com) with a small embed model so recall works by meaning, fully offline.
|
|
41
42
|
|
|
42
43
|
## Using it
|
|
43
44
|
|
|
@@ -61,7 +62,7 @@ Every exchange is saved to `<folder>/.holt/memory/turns.jsonl`, private and loca
|
|
|
61
62
|
|
|
62
63
|
Two recall modes, picked automatically:
|
|
63
64
|
|
|
64
|
-
- **Semantic** (best):
|
|
65
|
+
- **Semantic** (best): a local [Ollama](https://ollama.com) with an embedding model, which `holt init` offers to set up for you. Recall matches by meaning: asking "who owns my apartment" finds "my landlord is called Pieter". No API keys, nothing leaves your machine.
|
|
65
66
|
- **Keyword** (fallback): with no Ollama, recall matches by word overlap. Still useful, zero setup.
|
|
66
67
|
|
|
67
68
|
Inspect it any time:
|
|
@@ -69,9 +70,12 @@ Inspect it any time:
|
|
|
69
70
|
```bash
|
|
70
71
|
holt memory # stats for this folder
|
|
71
72
|
holt memory search <query> # find remembered moments
|
|
73
|
+
holt memory embed # embed older moments for semantic recall
|
|
72
74
|
holt memory clear # wipe this folder's memory
|
|
73
75
|
```
|
|
74
76
|
|
|
77
|
+
Turns saved before semantic memory was enabled are upgraded in one pass with `holt memory embed`.
|
|
78
|
+
|
|
75
79
|
Long conversations stay cheap: only recent turns are replayed verbatim, older context comes back through recall.
|
|
76
80
|
|
|
77
81
|
## Brains
|
package/dist/cli.js
CHANGED
|
@@ -252,81 +252,8 @@ function runInteractive(cmd, args) {
|
|
|
252
252
|
});
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
// src/commands/init.ts
|
|
256
|
-
function parseBrains(raw, found) {
|
|
257
|
-
const s = raw.trim().toLowerCase();
|
|
258
|
-
if (s === "") return found.length ? found : [...BRAIN_IDS];
|
|
259
|
-
if (s === "all") return [...BRAIN_IDS];
|
|
260
|
-
const picked = s.split(/[\s,]+/).filter((x) => BRAIN_IDS.includes(x));
|
|
261
|
-
if (picked.length) return [...new Set(picked)];
|
|
262
|
-
return found.length ? found : ["claude"];
|
|
263
|
-
}
|
|
264
|
-
async function init() {
|
|
265
|
-
const { ask, close } = createReader();
|
|
266
|
-
if (!await ensureTrusted(ask)) {
|
|
267
|
-
close();
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
console.log(c.accent("Holt setup") + c.dim(` (${workspace()})`) + "\n");
|
|
271
|
-
console.log("Looking for agent CLIs on your machine...\n");
|
|
272
|
-
const found = [];
|
|
273
|
-
for (const id of BRAIN_IDS) {
|
|
274
|
-
const ok = isInstalled(BRAIN_DEFS[id].command);
|
|
275
|
-
console.log(` ${ok ? c.green("found ") : c.dim("missing")} ${BRAIN_DEFS[id].label} (${BRAIN_DEFS[id].command})`);
|
|
276
|
-
if (ok) found.push(id);
|
|
277
|
-
}
|
|
278
|
-
console.log("");
|
|
279
|
-
const chosen = parseBrains(
|
|
280
|
-
await ask('Which brains do you want? claude, codex, gemini (comma-separated, or "all"): ') ?? "",
|
|
281
|
-
found
|
|
282
|
-
);
|
|
283
|
-
console.log(c.dim(` using: ${chosen.join(", ")}`));
|
|
284
|
-
const toInstall = chosen.filter((id) => !isInstalled(BRAIN_DEFS[id].command));
|
|
285
|
-
const loginWanted = /* @__PURE__ */ new Set();
|
|
286
|
-
for (const id of toInstall) {
|
|
287
|
-
const a = (await ask(` ${BRAIN_DEFS[id].label} is not installed. Sign in after install? [Y/n] `) ?? "").trim().toLowerCase();
|
|
288
|
-
if (a !== "n" && a !== "no") loginWanted.add(id);
|
|
289
|
-
}
|
|
290
|
-
const defPick = chosen.includes("claude") ? "claude" : chosen[0];
|
|
291
|
-
const dans = (await ask(`
|
|
292
|
-
Default brain? [${chosen.join("/")}] (${defPick}): `) ?? "").trim();
|
|
293
|
-
const defaultBrain = chosen.includes(dans) ? dans : defPick;
|
|
294
|
-
const aliasAns = (await ask('Launch command? Type a custom word like "ai", or press enter to keep "holt": ') ?? "").trim();
|
|
295
|
-
let aliasNote = "";
|
|
296
|
-
if (aliasAns && aliasAns !== "holt") {
|
|
297
|
-
if (isInstalled(aliasAns)) console.log(c.dim(` note: "${aliasAns}" already exists; the alias will shadow it in new shells.`));
|
|
298
|
-
const r = installAlias(aliasAns);
|
|
299
|
-
aliasNote = r.ok ? c.green(` alias "${aliasAns}" -> holt chat added to ${r.file} (run: source ${r.file})`) : c.red(" " + r.message);
|
|
300
|
-
}
|
|
301
|
-
close();
|
|
302
|
-
for (const id of toInstall) {
|
|
303
|
-
const s = BRAIN_SETUP[id];
|
|
304
|
-
console.log("\n" + c.accent(`Installing ${BRAIN_DEFS[id].label}`) + c.dim(` (${s.install.join(" ")})`));
|
|
305
|
-
const code = await runInteractive(s.install[0], s.install.slice(1));
|
|
306
|
-
console.log(code === 0 ? c.green(` ${BRAIN_DEFS[id].label} installed.`) : c.red(` Install failed (exit ${code}). Run manually: ${s.install.join(" ")}`));
|
|
307
|
-
}
|
|
308
|
-
for (const id of toInstall) {
|
|
309
|
-
if (!loginWanted.has(id) || !isInstalled(BRAIN_DEFS[id].command)) continue;
|
|
310
|
-
const s = BRAIN_SETUP[id];
|
|
311
|
-
console.log("\n" + c.accent(`Sign in to ${BRAIN_DEFS[id].label}`));
|
|
312
|
-
console.log(c.dim(` Starting "${s.login.join(" ")}". Complete sign-in, then exit that tool to return here.`));
|
|
313
|
-
await runInteractive(s.login[0], s.login.slice(1));
|
|
314
|
-
}
|
|
315
|
-
const cfg = loadConfig() ?? defaultConfig();
|
|
316
|
-
for (const id of BRAIN_IDS) cfg.brains[id].enabled = chosen.includes(id) && isInstalled(BRAIN_DEFS[id].command);
|
|
317
|
-
cfg.defaultBrain = cfg.brains[defaultBrain].enabled ? defaultBrain : BRAIN_IDS.find((id) => cfg.brains[id].enabled) ?? null;
|
|
318
|
-
saveConfig(cfg);
|
|
319
|
-
console.log("\n" + c.green("Saved to ./.holt/config.json"));
|
|
320
|
-
if (aliasNote) console.log(aliasNote);
|
|
321
|
-
if (cfg.defaultBrain) console.log("Start chatting: " + c.accent(aliasAns && aliasAns !== "holt" ? aliasAns : "holt chat") + "\n");
|
|
322
|
-
else console.log(c.dim('No brain is ready yet. Install one, then run "holt init" again.\n'));
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// src/commands/chat.ts
|
|
326
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
327
|
-
|
|
328
255
|
// src/memory.ts
|
|
329
|
-
import { appendFileSync, readFileSync as readFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync2, rmSync, statSync } from "fs";
|
|
256
|
+
import { appendFileSync, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync2, rmSync, statSync } from "fs";
|
|
330
257
|
import { join as join3 } from "path";
|
|
331
258
|
import { randomUUID } from "crypto";
|
|
332
259
|
var OLLAMA_URL = process.env.HOLT_OLLAMA_URL || "http://127.0.0.1:11434";
|
|
@@ -341,6 +268,9 @@ function newSessionId() {
|
|
|
341
268
|
return randomUUID().slice(0, 8);
|
|
342
269
|
}
|
|
343
270
|
var embedProbe = null;
|
|
271
|
+
function resetEmbedProbe() {
|
|
272
|
+
embedProbe = null;
|
|
273
|
+
}
|
|
344
274
|
async function embeddingsAvailable() {
|
|
345
275
|
if (embedProbe !== null) return embedProbe;
|
|
346
276
|
try {
|
|
@@ -437,6 +367,131 @@ async function recall(query, currentSession, k = 4) {
|
|
|
437
367
|
scored.sort((a, b) => b.score - a.score);
|
|
438
368
|
return scored.slice(0, k);
|
|
439
369
|
}
|
|
370
|
+
async function backfillEmbeddings(onProgress) {
|
|
371
|
+
const turns = loadTurns();
|
|
372
|
+
const missing = turns.filter((t) => !Array.isArray(t.emb));
|
|
373
|
+
if (missing.length === 0) return { embedded: 0, total: 0 };
|
|
374
|
+
let done = 0;
|
|
375
|
+
let embedded = 0;
|
|
376
|
+
for (const t of missing) {
|
|
377
|
+
const e = await embed(t.content);
|
|
378
|
+
if (e) {
|
|
379
|
+
t.emb = e;
|
|
380
|
+
embedded++;
|
|
381
|
+
}
|
|
382
|
+
done++;
|
|
383
|
+
if (onProgress) onProgress(done, missing.length);
|
|
384
|
+
}
|
|
385
|
+
mkdirSync2(memDir(), { recursive: true });
|
|
386
|
+
writeFileSync4(memPath(), turns.map((t) => JSON.stringify(t)).join("\n") + "\n", "utf8");
|
|
387
|
+
return { embedded, total: missing.length };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/commands/init.ts
|
|
391
|
+
function parseBrains(raw, found) {
|
|
392
|
+
const s = raw.trim().toLowerCase();
|
|
393
|
+
if (s === "") return found.length ? found : [...BRAIN_IDS];
|
|
394
|
+
if (s === "all") return [...BRAIN_IDS];
|
|
395
|
+
const picked = s.split(/[\s,]+/).filter((x) => BRAIN_IDS.includes(x));
|
|
396
|
+
if (picked.length) return [...new Set(picked)];
|
|
397
|
+
return found.length ? found : ["claude"];
|
|
398
|
+
}
|
|
399
|
+
async function init() {
|
|
400
|
+
const { ask, close } = createReader();
|
|
401
|
+
if (!await ensureTrusted(ask)) {
|
|
402
|
+
close();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
console.log(c.accent("Holt setup") + c.dim(` (${workspace()})`) + "\n");
|
|
406
|
+
console.log("Looking for agent CLIs on your machine...\n");
|
|
407
|
+
const found = [];
|
|
408
|
+
for (const id of BRAIN_IDS) {
|
|
409
|
+
const ok = isInstalled(BRAIN_DEFS[id].command);
|
|
410
|
+
console.log(` ${ok ? c.green("found ") : c.dim("missing")} ${BRAIN_DEFS[id].label} (${BRAIN_DEFS[id].command})`);
|
|
411
|
+
if (ok) found.push(id);
|
|
412
|
+
}
|
|
413
|
+
console.log("");
|
|
414
|
+
const chosen = parseBrains(
|
|
415
|
+
await ask('Which brains do you want? claude, codex, gemini (comma-separated, or "all"): ') ?? "",
|
|
416
|
+
found
|
|
417
|
+
);
|
|
418
|
+
console.log(c.dim(` using: ${chosen.join(", ")}`));
|
|
419
|
+
const toInstall = chosen.filter((id) => !isInstalled(BRAIN_DEFS[id].command));
|
|
420
|
+
const loginWanted = /* @__PURE__ */ new Set();
|
|
421
|
+
for (const id of toInstall) {
|
|
422
|
+
const a = (await ask(` ${BRAIN_DEFS[id].label} is not installed. Sign in after install? [Y/n] `) ?? "").trim().toLowerCase();
|
|
423
|
+
if (a !== "n" && a !== "no") loginWanted.add(id);
|
|
424
|
+
}
|
|
425
|
+
const defPick = chosen.includes("claude") ? "claude" : chosen[0];
|
|
426
|
+
const dans = (await ask(`
|
|
427
|
+
Default brain? [${chosen.join("/")}] (${defPick}): `) ?? "").trim();
|
|
428
|
+
const defaultBrain = chosen.includes(dans) ? dans : defPick;
|
|
429
|
+
const aliasAns = (await ask('Launch command? Type a custom word like "ai", or press enter to keep "holt": ') ?? "").trim();
|
|
430
|
+
let aliasNote = "";
|
|
431
|
+
if (aliasAns && aliasAns !== "holt") {
|
|
432
|
+
if (isInstalled(aliasAns)) console.log(c.dim(` note: "${aliasAns}" already exists; the alias will shadow it in new shells.`));
|
|
433
|
+
const r = installAlias(aliasAns);
|
|
434
|
+
aliasNote = r.ok ? c.green(` alias "${aliasAns}" -> holt chat added to ${r.file} (run: source ${r.file})`) : c.red(" " + r.message);
|
|
435
|
+
}
|
|
436
|
+
let wantMemorySetup = false;
|
|
437
|
+
const embedReady = await embeddingsAvailable();
|
|
438
|
+
if (embedReady) {
|
|
439
|
+
console.log(c.dim("\nSemantic memory: ready (local Ollama with " + EMBED_MODEL + " detected)."));
|
|
440
|
+
} else {
|
|
441
|
+
const ollamaHere = isInstalled("ollama");
|
|
442
|
+
const q = ollamaHere ? `Semantic memory needs a local embed model. Pull ${EMBED_MODEL} with Ollama now? [Y/n] ` : "Enable private semantic memory? Installs Ollama plus a small local embed model. Everything stays on your machine. [Y/n] ";
|
|
443
|
+
const a = (await ask("\n" + q) ?? "").trim().toLowerCase();
|
|
444
|
+
wantMemorySetup = a !== "n" && a !== "no";
|
|
445
|
+
if (!wantMemorySetup) console.log(c.dim(' Okay. Memory still works with keyword recall; run "holt init" again anytime.'));
|
|
446
|
+
}
|
|
447
|
+
close();
|
|
448
|
+
for (const id of toInstall) {
|
|
449
|
+
const s = BRAIN_SETUP[id];
|
|
450
|
+
console.log("\n" + c.accent(`Installing ${BRAIN_DEFS[id].label}`) + c.dim(` (${s.install.join(" ")})`));
|
|
451
|
+
const code = await runInteractive(s.install[0], s.install.slice(1));
|
|
452
|
+
console.log(code === 0 ? c.green(` ${BRAIN_DEFS[id].label} installed.`) : c.red(` Install failed (exit ${code}). Run manually: ${s.install.join(" ")}`));
|
|
453
|
+
}
|
|
454
|
+
for (const id of toInstall) {
|
|
455
|
+
if (!loginWanted.has(id) || !isInstalled(BRAIN_DEFS[id].command)) continue;
|
|
456
|
+
const s = BRAIN_SETUP[id];
|
|
457
|
+
console.log("\n" + c.accent(`Sign in to ${BRAIN_DEFS[id].label}`));
|
|
458
|
+
console.log(c.dim(` Starting "${s.login.join(" ")}". Complete sign-in, then exit that tool to return here.`));
|
|
459
|
+
await runInteractive(s.login[0], s.login.slice(1));
|
|
460
|
+
}
|
|
461
|
+
if (wantMemorySetup) {
|
|
462
|
+
if (!isInstalled("ollama")) {
|
|
463
|
+
if (process.platform === "darwin" && isInstalled("brew")) {
|
|
464
|
+
console.log("\n" + c.accent("Installing Ollama") + c.dim(" (brew install ollama)"));
|
|
465
|
+
const code = await runInteractive("brew", ["install", "ollama"]);
|
|
466
|
+
if (code === 0) await runInteractive("brew", ["services", "start", "ollama"]);
|
|
467
|
+
else console.log(c.red(' Install failed. Get Ollama from https://ollama.com/download and run "holt init" again.'));
|
|
468
|
+
} else {
|
|
469
|
+
console.log(c.dim('\n Get Ollama from https://ollama.com/download, then run "holt init" again to finish memory setup.'));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (isInstalled("ollama")) {
|
|
473
|
+
console.log("\n" + c.accent("Pulling embed model") + c.dim(` (ollama pull ${EMBED_MODEL})`));
|
|
474
|
+
const code = await runInteractive("ollama", ["pull", EMBED_MODEL]);
|
|
475
|
+
if (code !== 0) {
|
|
476
|
+
console.log(c.dim(' Could not pull. Start Ollama (open the app or run "ollama serve"), then run:'));
|
|
477
|
+
console.log(c.dim(` ollama pull ${EMBED_MODEL}`));
|
|
478
|
+
}
|
|
479
|
+
resetEmbedProbe();
|
|
480
|
+
if (await embeddingsAvailable()) console.log(c.green(" Semantic memory is ready. Chats in trusted folders are stored and recalled locally."));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const cfg = loadConfig() ?? defaultConfig();
|
|
484
|
+
for (const id of BRAIN_IDS) cfg.brains[id].enabled = chosen.includes(id) && isInstalled(BRAIN_DEFS[id].command);
|
|
485
|
+
cfg.defaultBrain = cfg.brains[defaultBrain].enabled ? defaultBrain : BRAIN_IDS.find((id) => cfg.brains[id].enabled) ?? null;
|
|
486
|
+
saveConfig(cfg);
|
|
487
|
+
console.log("\n" + c.green("Saved to ./.holt/config.json"));
|
|
488
|
+
if (aliasNote) console.log(aliasNote);
|
|
489
|
+
if (cfg.defaultBrain) console.log("Start chatting: " + c.accent(aliasAns && aliasAns !== "holt" ? aliasAns : "holt chat") + "\n");
|
|
490
|
+
else console.log(c.dim('No brain is ready yet. Install one, then run "holt init" again.\n'));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/commands/chat.ts
|
|
494
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
440
495
|
|
|
441
496
|
// src/commands/setting.ts
|
|
442
497
|
function printStatus(cfg) {
|
|
@@ -550,6 +605,9 @@ async function chat() {
|
|
|
550
605
|
console.log(c.dim(
|
|
551
606
|
`Memory: ${stats.turns} moments from ${stats.sessions} session${stats.sessions === 1 ? "" : "s"} in this folder (recall: ${embedOk ? "embeddings via local Ollama" : "keyword match"}).`
|
|
552
607
|
));
|
|
608
|
+
if (embedOk && stats.withEmbeddings < stats.turns) {
|
|
609
|
+
console.log(c.dim(` ${stats.turns - stats.withEmbeddings} older moments lack embeddings. Run "holt memory embed" to upgrade them.`));
|
|
610
|
+
}
|
|
553
611
|
console.log(c.dim("Type a message. Commands: /brain /memory /setting /clear /help /exit\n"));
|
|
554
612
|
while (true) {
|
|
555
613
|
const raw = await ask(c.accent("\u203A "));
|
|
@@ -676,6 +734,28 @@ async function memoryCmd(sub, rest = []) {
|
|
|
676
734
|
close();
|
|
677
735
|
return;
|
|
678
736
|
}
|
|
737
|
+
if (action === "embed") {
|
|
738
|
+
if (!await embeddingsAvailable()) {
|
|
739
|
+
console.log(c.dim(`
|
|
740
|
+
No local Ollama with ${EMBED_MODEL} reachable. Run "holt init" to set it up.
|
|
741
|
+
`));
|
|
742
|
+
close();
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const missing = loadTurns().filter((t) => !Array.isArray(t.emb)).length;
|
|
746
|
+
if (missing === 0) {
|
|
747
|
+
console.log(c.dim("\n All moments already have embeddings.\n"));
|
|
748
|
+
close();
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
console.log("");
|
|
752
|
+
const r = await backfillEmbeddings((done, total) => {
|
|
753
|
+
process.stdout.write(`\r embedding ${done}/${total}...`);
|
|
754
|
+
});
|
|
755
|
+
console.log("\n" + c.green(` Done. ${r.embedded} of ${r.total} moments embedded.`) + "\n");
|
|
756
|
+
close();
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
679
759
|
if (action === "search") {
|
|
680
760
|
const q = rest.join(" ").trim();
|
|
681
761
|
if (!q) {
|
|
@@ -703,13 +783,18 @@ async function memoryCmd(sub, rest = []) {
|
|
|
703
783
|
console.log(` embedded ${s.withEmbeddings} of ${s.turns}`);
|
|
704
784
|
console.log(` size ${(s.bytes / 1024).toFixed(1)} KB (./.holt/memory/turns.jsonl)`);
|
|
705
785
|
console.log(` recall via ${embedOk ? "embeddings (local Ollama)" : "keyword match (start Ollama with an embed model for semantic recall)"}`);
|
|
786
|
+
if (embedOk && s.withEmbeddings < s.turns) {
|
|
787
|
+
console.log(c.dim(`
|
|
788
|
+
${s.turns - s.withEmbeddings} moments lack embeddings. Run "holt memory embed" to upgrade them to semantic recall.`));
|
|
789
|
+
}
|
|
706
790
|
console.log(c.dim("\n holt memory search <query> find remembered moments"));
|
|
791
|
+
console.log(c.dim(" holt memory embed embed older moments for semantic recall"));
|
|
707
792
|
console.log(c.dim(" holt memory clear wipe this folder's memory\n"));
|
|
708
793
|
close();
|
|
709
794
|
}
|
|
710
795
|
|
|
711
796
|
// src/cli.ts
|
|
712
|
-
var VERSION = "0.
|
|
797
|
+
var VERSION = "0.4.0";
|
|
713
798
|
var BANNER = `
|
|
714
799
|
\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
715
800
|
\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D
|