@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.
Files changed (3) hide show
  1. package/README.md +5 -1
  2. package/dist/cli.js +160 -75
  3. 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): if a local [Ollama](https://ollama.com) is running with an embedding model (`ollama pull nomic-embed-text`), recall matches by meaning. Asking "who owns my apartment" finds "my landlord is called Pieter". No API keys, nothing leaves your machine.
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.3.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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holt-os/holt",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "An open-source personal agent OS: any LLM, private memory you can see and walk.",
5
5
  "type": "module",
6
6
  "license": "MIT",