@holt-os/holt 0.2.0 → 0.3.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 +25 -4
- package/dist/cli.js +246 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ 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: early but usable.** `holt init
|
|
9
|
+
> **Status: early but usable, and it remembers now.** `holt init`, `holt chat`, and persistent memory work today. A "brain" is an agent CLI (Claude Code, Codex, or Gemini). Holt can install a missing one for you and hand off to its sign-in, you can switch brains mid-conversation without losing context, and every session adds to a private memory in that folder that future sessions recall from. Skills and the knowledge graph view are the next phases.
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -46,14 +46,34 @@ Inside `holt chat`:
|
|
|
46
46
|
```
|
|
47
47
|
/brain list your brains and see which is active
|
|
48
48
|
/brain gemini switch brain. your conversation context is kept
|
|
49
|
+
/memory memory stats. /memory <query> previews what recall would surface
|
|
49
50
|
/setting configure brains and your launch command
|
|
50
|
-
/clear forget
|
|
51
|
+
/clear forget this session (saved memory stays)
|
|
51
52
|
/help show commands
|
|
52
53
|
/exit leave
|
|
53
54
|
```
|
|
54
55
|
|
|
55
56
|
The point of `/brain`: Holt owns the transcript, so you can start a thread on one model and hand it to another mid-conversation. The new brain picks up with the full context.
|
|
56
57
|
|
|
58
|
+
## Memory
|
|
59
|
+
|
|
60
|
+
Every exchange is saved to `<folder>/.holt/memory/turns.jsonl`, private and local. On each message, Holt recalls the most relevant moments from your *past* sessions in that folder and hands them to the brain, so it remembers what you told it last week.
|
|
61
|
+
|
|
62
|
+
Two recall modes, picked automatically:
|
|
63
|
+
|
|
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
|
+
- **Keyword** (fallback): with no Ollama, recall matches by word overlap. Still useful, zero setup.
|
|
66
|
+
|
|
67
|
+
Inspect it any time:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
holt memory # stats for this folder
|
|
71
|
+
holt memory search <query> # find remembered moments
|
|
72
|
+
holt memory clear # wipe this folder's memory
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Long conversations stay cheap: only recent turns are replayed verbatim, older context comes back through recall.
|
|
76
|
+
|
|
57
77
|
## Brains
|
|
58
78
|
|
|
59
79
|
A brain is an agent CLI installed and logged in on your machine. No API keys to paste.
|
|
@@ -70,7 +90,8 @@ A brain is an agent CLI installed and logged in on your machine. No API keys to
|
|
|
70
90
|
|
|
71
91
|
```
|
|
72
92
|
holt init set up (trust, brains, sign-in, defaults) for this folder
|
|
73
|
-
holt chat start a session
|
|
93
|
+
holt chat start a session that remembers past ones
|
|
94
|
+
holt memory inspect memory: holt memory [search <query> | clear]
|
|
74
95
|
holt setting configure brains and launch command
|
|
75
96
|
holt login <brain> sign in to claude, codex, or gemini
|
|
76
97
|
holt version print version
|
|
@@ -90,7 +111,7 @@ Small strongly-typed **TypeScript core** (command dispatch, brain router, transc
|
|
|
90
111
|
Built in always-shippable phases toward a full-vision v1:
|
|
91
112
|
|
|
92
113
|
0. **Skeleton and chat**: trust, init with install and sign-in, chat, brain switching with kept context *(shipped)*
|
|
93
|
-
1. **Memory**:
|
|
114
|
+
1. **Memory**: per-folder store, semantic recall via local embeddings with keyword fallback, streaming replies *(shipped)*
|
|
94
115
|
2. **Any LLM directly**: raw provider brains and an HTML or Markdown output toggle
|
|
95
116
|
3. **Skills**: install, search, and publish in the agentskills.io format
|
|
96
117
|
4. **Knowledge graph**: a view where you can see and navigate your own memory
|
package/dist/cli.js
CHANGED
|
@@ -130,25 +130,35 @@ function saveConfig(cfg) {
|
|
|
130
130
|
|
|
131
131
|
// src/brains.ts
|
|
132
132
|
import { spawn, spawnSync } from "child_process";
|
|
133
|
+
var MAX_REPLAY_TURNS = 12;
|
|
133
134
|
function isInstalled(command) {
|
|
134
135
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
135
136
|
const res = spawnSync(finder, [command], { stdio: "ignore" });
|
|
136
137
|
return res.status === 0;
|
|
137
138
|
}
|
|
138
|
-
function renderPrompt(history, message) {
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
139
|
+
function renderPrompt(history, message, memory = []) {
|
|
140
|
+
const recent = history.slice(-MAX_REPLAY_TURNS);
|
|
141
|
+
const parts = [];
|
|
142
|
+
if (recent.length || memory.length) {
|
|
143
|
+
parts.push(
|
|
144
|
+
"You are continuing an ongoing conversation. Use the context below and reply only as the assistant to the final User message."
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (memory.length) {
|
|
148
|
+
parts.push(
|
|
149
|
+
"",
|
|
150
|
+
"Relevant notes from this user's earlier sessions:",
|
|
151
|
+
...memory.map((m) => `- (${m.turn.role}) ${m.turn.content.slice(0, 500)}`)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (recent.length) {
|
|
155
|
+
parts.push("", "Transcript so far:", ...recent.map((t) => `${t.role === "user" ? "User" : "Assistant"}: ${t.content}`));
|
|
156
|
+
}
|
|
157
|
+
if (parts.length === 0) return message;
|
|
158
|
+
parts.push("", `User: ${message}`, "", "Assistant:");
|
|
159
|
+
return parts.join("\n");
|
|
160
|
+
}
|
|
161
|
+
function runBrain(brain, prompt, onChunk) {
|
|
152
162
|
return new Promise((resolve) => {
|
|
153
163
|
let child;
|
|
154
164
|
try {
|
|
@@ -160,7 +170,9 @@ function runBrain(brain, prompt) {
|
|
|
160
170
|
let out = "";
|
|
161
171
|
let err = "";
|
|
162
172
|
child.stdout.on("data", (d) => {
|
|
163
|
-
|
|
173
|
+
const s = d.toString();
|
|
174
|
+
out += s;
|
|
175
|
+
if (onChunk) onChunk(s);
|
|
164
176
|
});
|
|
165
177
|
child.stderr.on("data", (d) => {
|
|
166
178
|
err += d.toString();
|
|
@@ -310,6 +322,122 @@ Default brain? [${chosen.join("/")}] (${defPick}): `) ?? "").trim();
|
|
|
310
322
|
else console.log(c.dim('No brain is ready yet. Install one, then run "holt init" again.\n'));
|
|
311
323
|
}
|
|
312
324
|
|
|
325
|
+
// src/commands/chat.ts
|
|
326
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
327
|
+
|
|
328
|
+
// src/memory.ts
|
|
329
|
+
import { appendFileSync, readFileSync as readFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync2, rmSync, statSync } from "fs";
|
|
330
|
+
import { join as join3 } from "path";
|
|
331
|
+
import { randomUUID } from "crypto";
|
|
332
|
+
var OLLAMA_URL = process.env.HOLT_OLLAMA_URL || "http://127.0.0.1:11434";
|
|
333
|
+
var EMBED_MODEL = process.env.HOLT_EMBED_MODEL || "nomic-embed-text";
|
|
334
|
+
function memDir() {
|
|
335
|
+
return join3(wsHoltDir(), "memory");
|
|
336
|
+
}
|
|
337
|
+
function memPath() {
|
|
338
|
+
return join3(memDir(), "turns.jsonl");
|
|
339
|
+
}
|
|
340
|
+
function newSessionId() {
|
|
341
|
+
return randomUUID().slice(0, 8);
|
|
342
|
+
}
|
|
343
|
+
var embedProbe = null;
|
|
344
|
+
async function embeddingsAvailable() {
|
|
345
|
+
if (embedProbe !== null) return embedProbe;
|
|
346
|
+
try {
|
|
347
|
+
const res = await fetch(`${OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(1200) });
|
|
348
|
+
if (!res.ok) return embedProbe = false;
|
|
349
|
+
const data = await res.json();
|
|
350
|
+
embedProbe = !!data.models?.some((m) => (m.name || "").startsWith(EMBED_MODEL));
|
|
351
|
+
} catch {
|
|
352
|
+
embedProbe = false;
|
|
353
|
+
}
|
|
354
|
+
return embedProbe;
|
|
355
|
+
}
|
|
356
|
+
async function embed(text) {
|
|
357
|
+
if (!await embeddingsAvailable()) return null;
|
|
358
|
+
try {
|
|
359
|
+
const res = await fetch(`${OLLAMA_URL}/api/embeddings`, {
|
|
360
|
+
method: "POST",
|
|
361
|
+
headers: { "content-type": "application/json" },
|
|
362
|
+
body: JSON.stringify({ model: EMBED_MODEL, prompt: text.slice(0, 4e3) }),
|
|
363
|
+
signal: AbortSignal.timeout(1e4)
|
|
364
|
+
});
|
|
365
|
+
if (!res.ok) return null;
|
|
366
|
+
const data = await res.json();
|
|
367
|
+
if (!Array.isArray(data.embedding)) return null;
|
|
368
|
+
return data.embedding.map((x) => Math.round(x * 1e4) / 1e4);
|
|
369
|
+
} catch {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function loadTurns() {
|
|
374
|
+
if (!existsSync4(memPath())) return [];
|
|
375
|
+
const out = [];
|
|
376
|
+
for (const line of readFileSync4(memPath(), "utf8").split("\n")) {
|
|
377
|
+
if (!line.trim()) continue;
|
|
378
|
+
try {
|
|
379
|
+
out.push(JSON.parse(line));
|
|
380
|
+
} catch {
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
385
|
+
function appendTurn(t) {
|
|
386
|
+
mkdirSync2(memDir(), { recursive: true });
|
|
387
|
+
appendFileSync(memPath(), JSON.stringify(t) + "\n", "utf8");
|
|
388
|
+
}
|
|
389
|
+
function clearMemory() {
|
|
390
|
+
if (existsSync4(memPath())) rmSync(memPath());
|
|
391
|
+
}
|
|
392
|
+
function memStats() {
|
|
393
|
+
const turns = loadTurns();
|
|
394
|
+
const sessions = new Set(turns.map((t) => t.session)).size;
|
|
395
|
+
const withEmbeddings = turns.filter((t) => Array.isArray(t.emb)).length;
|
|
396
|
+
const bytes = existsSync4(memPath()) ? statSync(memPath()).size : 0;
|
|
397
|
+
return { turns: turns.length, sessions, withEmbeddings, bytes };
|
|
398
|
+
}
|
|
399
|
+
function cosine(a, b) {
|
|
400
|
+
let dot = 0;
|
|
401
|
+
let na = 0;
|
|
402
|
+
let nb = 0;
|
|
403
|
+
const n = Math.min(a.length, b.length);
|
|
404
|
+
for (let i = 0; i < n; i++) {
|
|
405
|
+
const x = a[i];
|
|
406
|
+
const y = b[i];
|
|
407
|
+
dot += x * y;
|
|
408
|
+
na += x * x;
|
|
409
|
+
nb += y * y;
|
|
410
|
+
}
|
|
411
|
+
return na && nb ? dot / (Math.sqrt(na) * Math.sqrt(nb)) : 0;
|
|
412
|
+
}
|
|
413
|
+
function tokens(s) {
|
|
414
|
+
return new Set(
|
|
415
|
+
s.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2)
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
function keywordScore(q, text) {
|
|
419
|
+
if (q.size === 0) return 0;
|
|
420
|
+
const t = tokens(text);
|
|
421
|
+
let hit = 0;
|
|
422
|
+
for (const w of q) if (t.has(w)) hit++;
|
|
423
|
+
return hit / q.size;
|
|
424
|
+
}
|
|
425
|
+
async function recall(query, currentSession, k = 4) {
|
|
426
|
+
const past = loadTurns().filter((t) => t.session !== currentSession);
|
|
427
|
+
if (past.length === 0) return [];
|
|
428
|
+
const qEmb = await embed(query);
|
|
429
|
+
const qTok = tokens(query);
|
|
430
|
+
const scored = [];
|
|
431
|
+
for (const turn of past) {
|
|
432
|
+
let score = 0;
|
|
433
|
+
if (qEmb && Array.isArray(turn.emb)) score = cosine(qEmb, turn.emb);
|
|
434
|
+
else score = keywordScore(qTok, turn.content);
|
|
435
|
+
if (score > (qEmb && Array.isArray(turn.emb) ? 0.35 : 0.15)) scored.push({ turn, score });
|
|
436
|
+
}
|
|
437
|
+
scored.sort((a, b) => b.score - a.score);
|
|
438
|
+
return scored.slice(0, k);
|
|
439
|
+
}
|
|
440
|
+
|
|
313
441
|
// src/commands/setting.ts
|
|
314
442
|
function printStatus(cfg) {
|
|
315
443
|
console.log("\n" + c.accent("Holt settings") + c.dim(" (this folder)"));
|
|
@@ -387,11 +515,12 @@ async function setting() {
|
|
|
387
515
|
function help() {
|
|
388
516
|
console.log(c.dim([
|
|
389
517
|
" commands:",
|
|
390
|
-
" /brain [name]
|
|
391
|
-
" /
|
|
392
|
-
" /
|
|
393
|
-
" /
|
|
394
|
-
" /
|
|
518
|
+
" /brain [name] switch brain (claude, codex, gemini). context is kept.",
|
|
519
|
+
" /memory [query] memory stats, or preview what a query would recall",
|
|
520
|
+
" /setting configure brains and your launch command",
|
|
521
|
+
" /clear forget this session so far (saved memory stays)",
|
|
522
|
+
" /help this list",
|
|
523
|
+
" /exit leave"
|
|
395
524
|
].join("\n")));
|
|
396
525
|
}
|
|
397
526
|
async function chat() {
|
|
@@ -413,9 +542,15 @@ async function chat() {
|
|
|
413
542
|
return;
|
|
414
543
|
}
|
|
415
544
|
let current = cfg.defaultBrain;
|
|
545
|
+
const session = newSessionId();
|
|
416
546
|
const history = [];
|
|
547
|
+
const embedOk = await embeddingsAvailable();
|
|
548
|
+
const stats = memStats();
|
|
417
549
|
console.log("\n" + c.accent("Holt") + c.dim(` brain: ${cfg.brains[current].label}`));
|
|
418
|
-
console.log(c.dim(
|
|
550
|
+
console.log(c.dim(
|
|
551
|
+
`Memory: ${stats.turns} moments from ${stats.sessions} session${stats.sessions === 1 ? "" : "s"} in this folder (recall: ${embedOk ? "embeddings via local Ollama" : "keyword match"}).`
|
|
552
|
+
));
|
|
553
|
+
console.log(c.dim("Type a message. Commands: /brain /memory /setting /clear /help /exit\n"));
|
|
419
554
|
while (true) {
|
|
420
555
|
const raw = await ask(c.accent("\u203A "));
|
|
421
556
|
if (raw === null) break;
|
|
@@ -424,6 +559,7 @@ async function chat() {
|
|
|
424
559
|
if (line.startsWith("/")) {
|
|
425
560
|
const parts = line.slice(1).split(/\s+/);
|
|
426
561
|
const cmd = (parts[0] || "").toLowerCase();
|
|
562
|
+
const rest = parts.slice(1).join(" ");
|
|
427
563
|
const arg = (parts[1] || "").toLowerCase();
|
|
428
564
|
if (cmd === "exit" || cmd === "quit" || cmd === "q") break;
|
|
429
565
|
if (cmd === "help" || cmd === "h") {
|
|
@@ -432,7 +568,19 @@ async function chat() {
|
|
|
432
568
|
}
|
|
433
569
|
if (cmd === "clear") {
|
|
434
570
|
history.length = 0;
|
|
435
|
-
console.log(c.dim(" context cleared."));
|
|
571
|
+
console.log(c.dim(" session context cleared. Saved memory is untouched."));
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
if (cmd === "memory" || cmd === "mem") {
|
|
575
|
+
if (rest) {
|
|
576
|
+
const hits = await recall(rest, session, 5);
|
|
577
|
+
if (hits.length === 0) console.log(c.dim(" nothing relevant in memory for that."));
|
|
578
|
+
else for (const h of hits) console.log(c.dim(` ${h.score.toFixed(2)} (${h.turn.role}) ${h.turn.content.slice(0, 110).replace(/\s+/g, " ")}`));
|
|
579
|
+
} else {
|
|
580
|
+
const s = memStats();
|
|
581
|
+
console.log(c.dim(` ${s.turns} moments, ${s.sessions} sessions, ${s.withEmbeddings} embedded, ${(s.bytes / 1024).toFixed(1)} KB in ./.holt/memory/`));
|
|
582
|
+
console.log(c.dim(' usage: /memory <query> to preview recall, or "holt memory clear" to wipe.'));
|
|
583
|
+
}
|
|
436
584
|
continue;
|
|
437
585
|
}
|
|
438
586
|
if (cmd === "setting" || cmd === "settings") {
|
|
@@ -463,12 +611,23 @@ async function chat() {
|
|
|
463
611
|
console.log(c.red(` ${brain.label} (${brain.command}) is not on your PATH. Use /brain to switch or /setting.`));
|
|
464
612
|
continue;
|
|
465
613
|
}
|
|
466
|
-
|
|
467
|
-
const
|
|
614
|
+
const remembered = await recall(line, session, 4);
|
|
615
|
+
const label = remembered.length ? `${brain.label} is thinking (recalled ${remembered.length} moment${remembered.length === 1 ? "" : "s"})...` : `${brain.label} is thinking...`;
|
|
616
|
+
console.log(c.dim(` ${label}`) + "\n");
|
|
617
|
+
let streamed = false;
|
|
618
|
+
const res = await runBrain(brain, renderPrompt(history, line, remembered), (chunk) => {
|
|
619
|
+
streamed = true;
|
|
620
|
+
process.stdout.write(chunk);
|
|
621
|
+
});
|
|
468
622
|
if (res.ok) {
|
|
623
|
+
if (!streamed) console.log(res.text);
|
|
624
|
+
if (!res.text.endsWith("\n")) console.log("");
|
|
625
|
+
console.log("");
|
|
469
626
|
history.push({ role: "user", content: line });
|
|
470
627
|
history.push({ role: "assistant", content: res.text });
|
|
471
|
-
|
|
628
|
+
const now = Date.now();
|
|
629
|
+
appendTurn({ id: randomUUID2().slice(0, 8), ts: now, session, role: "user", content: line, emb: await embed(line) ?? void 0 });
|
|
630
|
+
appendTurn({ id: randomUUID2().slice(0, 8), ts: now, session, role: "assistant", content: res.text, emb: await embed(res.text) ?? void 0 });
|
|
472
631
|
} else {
|
|
473
632
|
console.log(c.red("\n " + res.text + "\n"));
|
|
474
633
|
}
|
|
@@ -493,8 +652,64 @@ async function login(which) {
|
|
|
493
652
|
await runInteractive(s.login[0], s.login.slice(1));
|
|
494
653
|
}
|
|
495
654
|
|
|
655
|
+
// src/commands/memory.ts
|
|
656
|
+
async function memoryCmd(sub, rest = []) {
|
|
657
|
+
const { ask, close } = createReader();
|
|
658
|
+
if (!await ensureTrusted(ask)) {
|
|
659
|
+
close();
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const action = (sub || "").toLowerCase();
|
|
663
|
+
if (action === "clear") {
|
|
664
|
+
const s2 = memStats();
|
|
665
|
+
if (s2.turns === 0) {
|
|
666
|
+
console.log(c.dim("\n Memory is already empty.\n"));
|
|
667
|
+
close();
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const a = (await ask(`
|
|
671
|
+
Delete all ${s2.turns} remembered moments in this folder? [y/N] `) ?? "").trim().toLowerCase();
|
|
672
|
+
if (a === "y" || a === "yes") {
|
|
673
|
+
clearMemory();
|
|
674
|
+
console.log(c.green(" Memory cleared.\n"));
|
|
675
|
+
} else console.log(c.dim(" Kept.\n"));
|
|
676
|
+
close();
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (action === "search") {
|
|
680
|
+
const q = rest.join(" ").trim();
|
|
681
|
+
if (!q) {
|
|
682
|
+
console.log(c.dim("\n Usage: holt memory search <query>\n"));
|
|
683
|
+
close();
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const hits = await recall(q, "__none__", 8);
|
|
687
|
+
console.log("");
|
|
688
|
+
if (hits.length === 0) console.log(c.dim(" Nothing relevant found."));
|
|
689
|
+
else for (const h of hits) {
|
|
690
|
+
const when = new Date(h.turn.ts).toISOString().slice(0, 10);
|
|
691
|
+
console.log(` ${c.accent(h.score.toFixed(2))} ${c.dim(when)} (${h.turn.role}) ${h.turn.content.slice(0, 100).replace(/\s+/g, " ")}`);
|
|
692
|
+
}
|
|
693
|
+
console.log("");
|
|
694
|
+
close();
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const s = memStats();
|
|
698
|
+
const embedOk = await embeddingsAvailable();
|
|
699
|
+
const sessions = new Set(loadTurns().map((t) => t.session)).size;
|
|
700
|
+
console.log("\n" + c.accent("Holt memory") + c.dim(" (this folder)"));
|
|
701
|
+
console.log(` moments ${s.turns}`);
|
|
702
|
+
console.log(` sessions ${sessions}`);
|
|
703
|
+
console.log(` embedded ${s.withEmbeddings} of ${s.turns}`);
|
|
704
|
+
console.log(` size ${(s.bytes / 1024).toFixed(1)} KB (./.holt/memory/turns.jsonl)`);
|
|
705
|
+
console.log(` recall via ${embedOk ? "embeddings (local Ollama)" : "keyword match (start Ollama with an embed model for semantic recall)"}`);
|
|
706
|
+
console.log(c.dim("\n holt memory search <query> find remembered moments"));
|
|
707
|
+
console.log(c.dim(" holt memory clear wipe this folder's memory\n"));
|
|
708
|
+
close();
|
|
709
|
+
}
|
|
710
|
+
|
|
496
711
|
// src/cli.ts
|
|
497
|
-
var VERSION = "0.
|
|
712
|
+
var VERSION = "0.3.0";
|
|
498
713
|
var BANNER = `
|
|
499
714
|
\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
|
|
500
715
|
\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
|
|
@@ -509,7 +724,8 @@ Usage: holt <command>
|
|
|
509
724
|
|
|
510
725
|
Commands:
|
|
511
726
|
init Trust this folder, choose and install brains, sign in, set defaults
|
|
512
|
-
chat Start a session.
|
|
727
|
+
chat Start a session. It remembers past sessions in this folder
|
|
728
|
+
memory Inspect memory: holt memory [search <query> | clear]
|
|
513
729
|
setting Configure brains and your launch command (per folder)
|
|
514
730
|
login <brain> Sign in to a brain: claude, codex, or gemini
|
|
515
731
|
version Print the Holt version
|
|
@@ -548,6 +764,9 @@ async function main() {
|
|
|
548
764
|
case "login":
|
|
549
765
|
await login(process.argv[3]);
|
|
550
766
|
break;
|
|
767
|
+
case "memory":
|
|
768
|
+
await memoryCmd(process.argv[3], process.argv.slice(4));
|
|
769
|
+
break;
|
|
551
770
|
default:
|
|
552
771
|
console.log(`
|
|
553
772
|
Unknown command: "${cmd}"`);
|