@holt-os/holt 0.0.2 → 0.2.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 CHANGED
@@ -6,53 +6,96 @@ 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 development (Phase 0).** The architecture and roadmap are locked and the CLI skeleton is here, but Holt is not yet functional end-to-end. Star or watch to follow along. Contributions welcome.
9
+ > **Status: early but usable.** `holt init` and `holt chat` 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, and you can switch brains mid-conversation without losing context. Memory, skills, and the knowledge graph are the next phases.
10
10
 
11
11
  ---
12
12
 
13
- ## Why Holt
14
-
15
- - 🧠 **Memory you can see.** Persistent RAG memory plus a *navigable knowledge graph* of everything you feed it. No black-box profile you have to trust.
16
- - 🔌 **Any LLM.** Claude, OpenAI, Gemini, or a local model. Swap your brain with one line of config. No vendor lock-in.
17
- - 💸 **Local executes, cloud reviews.** Run a local model for the work and let a premium cloud model review only the risky, irreversible steps. Cheap and private by design.
18
- - 🧩 **MCP plugin pantry.** Skills, channels, providers, embeddings: everything is a plugin speaking the [Model Context Protocol](https://modelcontextprotocol.io). Extend it in any language.
19
- - 📚 **Standard skills.** Compatible with the [agentskills.io](https://agentskills.io) skill format. Pull from the community catalog or publish your own.
20
- - 🖥️ **CLI-first.** Works in your terminal the moment you clone. Telegram and other channels are opt-in.
21
-
22
13
  ## Quickstart
23
14
 
24
15
  ```bash
25
- npm install -g @holt-os/holt # or: pnpm add -g @holt-os/holt
26
- holt init # pick your brain + memory (local or cloud)
27
- holt chat # start talking
16
+ npm install -g @holt-os/holt
17
+ cd ~/where-you-want-to-work
18
+ holt init # trust this folder, choose and install brains, sign in, set defaults
19
+ holt chat # start talking (or use your custom command, e.g. `ai`)
28
20
  ```
29
21
 
30
- Add skills:
22
+ ## First run
31
23
 
32
- ```bash
33
- holt skill search finance
34
- holt skill add deep-research
24
+ Holt runs in the folder you launch it from, like a per-project tool. The first time you use a folder it asks:
25
+
26
+ ```
27
+ Trust this folder?
28
+ /Users/you/where-you-want-to-work
29
+ Holt will read and write here.
30
+ Trust and continue? [y/N]
31
+ ```
32
+
33
+ Trusted folders are remembered in `~/.holt/trust.json`. Everything Holt writes for that folder (its config, and later its memory) stays in `<folder>/.holt/`.
34
+
35
+ During `holt init` you:
36
+
37
+ 1. **Trust the folder.**
38
+ 2. **Choose brains** (claude, codex, gemini). Holt installs any you pick that are missing.
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
+ 4. **Pick a default** brain and, optionally, a **launch command** (a short word like `ai` that runs `holt chat`).
41
+
42
+ ## Using it
43
+
44
+ Inside `holt chat`:
45
+
46
+ ```
47
+ /brain list your brains and see which is active
48
+ /brain gemini switch brain. your conversation context is kept
49
+ /setting configure brains and your launch command
50
+ /clear forget the conversation so far
51
+ /help show commands
52
+ /exit leave
53
+ ```
54
+
55
+ 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
+ ## Brains
58
+
59
+ A brain is an agent CLI installed and logged in on your machine. No API keys to paste.
60
+
61
+ | Brain | Command | Install |
62
+ |-------|---------|---------|
63
+ | Claude Code | `claude` | `npm i -g @anthropic-ai/claude-code` |
64
+ | Codex | `codex` | `npm i -g @openai/codex` |
65
+ | Gemini CLI | `gemini` | `npm i -g @google/gemini-cli` |
66
+
67
+ `holt init` runs these for you when you pick a brain that is missing. You can also sign in any time with `holt login <brain>`. Raw API providers are planned for a later phase.
68
+
69
+ ## Commands
70
+
71
+ ```
72
+ holt init set up (trust, brains, sign-in, defaults) for this folder
73
+ holt chat start a session
74
+ holt setting configure brains and launch command
75
+ holt login <brain> sign in to claude, codex, or gemini
76
+ holt version print version
77
+ holt help show help
35
78
  ```
36
79
 
37
80
  ## Configuration
38
81
 
39
- Copy `config.example.yml` to `config.yml` and edit. See the file for the full schema: brain/provider, memory and embeddings, output format (HTML or Markdown), orchestration, and channels.
82
+ `holt init` writes `<folder>/.holt/config.json` (default brain and enabled brains for that folder). Trusted folders live in `~/.holt/trust.json`. Edit settings with `holt setting`.
40
83
 
41
84
  ## Architecture
42
85
 
43
- Small strongly-typed **TypeScript core** (agent loop, brain router, memory orchestration, risky-action review gate, plugin dispatcher). *Everything else* is an MCP plugin: providers, embeddings, skills, channels. See [`ARCHITECTURE.md`](./ARCHITECTURE.md).
86
+ Small strongly-typed **TypeScript core** (command dispatch, brain router, transcript, and a plugin dispatcher coming with skills). Brains and, soon, skills and channels are adapters. See [`ARCHITECTURE.md`](./ARCHITECTURE.md).
44
87
 
45
88
  ## Roadmap
46
89
 
47
90
  Built in always-shippable phases toward a full-vision v1:
48
91
 
49
- 0. **Skeleton**: core loop + CLI *(in progress)*
50
- 1. **Memory**: sqlite-vec RAG + embeddings
51
- 2. **Any-LLM**: provider plugins + output toggle
52
- 3. **Skills**: agentskills.io catalog + installer
53
- 4. **Knowledge graph**: navigable memory view
54
- 5. **Orchestration**: local-executes / cloud-reviews
55
- 6. **Channels + polish**: Telegram, docs, one-command install
92
+ 0. **Skeleton and chat**: trust, init with install and sign-in, chat, brain switching with kept context *(shipped)*
93
+ 1. **Memory**: sqlite-vec store, local or cloud embeddings, recall across sessions
94
+ 2. **Any LLM directly**: raw provider brains and an HTML or Markdown output toggle
95
+ 3. **Skills**: install, search, and publish in the agentskills.io format
96
+ 4. **Knowledge graph**: a view where you can see and navigate your own memory
97
+ 5. **Orchestration**: a local model works, a cloud model reviews the risky steps
98
+ 6. **Channels and polish**: Telegram, docs, one-command setup
56
99
 
57
100
  ## Contributing
58
101
 
@@ -1,5 +1,7 @@
1
- # Holt configuration. Copy to config.yml and edit.
2
- # config.yml is git-ignored; never commit secrets or personal memory.
1
+ # Planned full configuration schema (later phases). Preview only.
2
+ # Phase 0 does NOT read this file: run `holt init`, which writes
3
+ # ~/.holt/config.json (your brains, default brain, and launch command).
4
+ # Never commit real secrets or personal memory.
3
5
 
4
6
  # ── Brain ────────────────────────────────────────────────────────────────
5
7
  # The LLM that runs the agent. Provider-agnostic: swap freely.
package/dist/cli.js CHANGED
@@ -1,7 +1,500 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/config.ts
4
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
5
+
6
+ // src/workspace.ts
7
+ import { homedir } from "os";
8
+ import { join } from "path";
9
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
10
+
11
+ // src/ui.ts
12
+ import readline from "readline";
13
+ var on = process.stdout.isTTY && !process.env.NO_COLOR;
14
+ var wrap = (code) => (s) => on ? `\x1B[${code}m${s}\x1B[0m` : s;
15
+ var c = {
16
+ dim: wrap("2"),
17
+ bold: wrap("1"),
18
+ accent: wrap("38;5;214"),
19
+ // amber
20
+ green: wrap("32"),
21
+ red: wrap("31"),
22
+ cyan: wrap("36")
23
+ };
24
+ function createReader() {
25
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
26
+ const buffer = [];
27
+ const waiters = [];
28
+ let closed = false;
29
+ rl.on("line", (line) => {
30
+ const w = waiters.shift();
31
+ if (w) w(line);
32
+ else buffer.push(line);
33
+ });
34
+ rl.on("close", () => {
35
+ closed = true;
36
+ while (waiters.length) waiters.shift()(null);
37
+ });
38
+ const ask = (q) => new Promise((resolve) => {
39
+ if (q) process.stdout.write(q);
40
+ if (buffer.length) resolve(buffer.shift());
41
+ else if (closed) resolve(null);
42
+ else waiters.push(resolve);
43
+ });
44
+ return { ask, close: () => rl.close() };
45
+ }
46
+
47
+ // src/workspace.ts
48
+ var GLOBAL_DIR = join(homedir(), ".holt");
49
+ var TRUST_PATH = join(GLOBAL_DIR, "trust.json");
50
+ function workspace() {
51
+ return process.cwd();
52
+ }
53
+ function wsHoltDir(dir = workspace()) {
54
+ return join(dir, ".holt");
55
+ }
56
+ function wsConfigPath(dir = workspace()) {
57
+ return join(wsHoltDir(dir), "config.json");
58
+ }
59
+ function readTrust() {
60
+ try {
61
+ return JSON.parse(readFileSync(TRUST_PATH, "utf8"));
62
+ } catch {
63
+ return { trusted: [] };
64
+ }
65
+ }
66
+ function isTrusted(dir = workspace()) {
67
+ return readTrust().trusted.includes(dir);
68
+ }
69
+ function trustDir(dir = workspace()) {
70
+ const t = readTrust();
71
+ if (!t.trusted.includes(dir)) t.trusted.push(dir);
72
+ mkdirSync(GLOBAL_DIR, { recursive: true });
73
+ writeFileSync(TRUST_PATH, JSON.stringify(t, null, 2) + "\n", "utf8");
74
+ }
75
+ async function ensureTrusted(ask) {
76
+ const ws = workspace();
77
+ if (isTrusted(ws)) return true;
78
+ console.log("\n" + c.accent("Trust this folder?"));
79
+ console.log(" " + ws);
80
+ console.log(c.dim(" Holt will read and write here: its config, memory, and any files you ask a brain to touch."));
81
+ const ans = (await ask(" Trust and continue? [y/N] ") ?? "").trim().toLowerCase();
82
+ if (ans === "y" || ans === "yes") {
83
+ trustDir(ws);
84
+ console.log(c.green(" Trusted.") + c.dim(" (remembered for next time)") + "\n");
85
+ return true;
86
+ }
87
+ console.log(c.dim(" Cancelled. Holt only runs in folders you trust.\n"));
88
+ return false;
89
+ }
90
+ function ensureWsDir() {
91
+ mkdirSync(wsHoltDir(), { recursive: true });
92
+ }
93
+
94
+ // src/config.ts
95
+ var BRAIN_DEFS = {
96
+ claude: { label: "Claude Code", command: "claude", args: ["-p"] },
97
+ codex: { label: "Codex (OpenAI)", command: "codex", args: ["exec"] },
98
+ gemini: { label: "Gemini CLI", command: "gemini", args: ["-p"] }
99
+ };
100
+ var BRAIN_SETUP = {
101
+ claude: { install: ["npm", "install", "-g", "@anthropic-ai/claude-code"], login: ["claude"] },
102
+ codex: { install: ["npm", "install", "-g", "@openai/codex"], login: ["codex", "login"] },
103
+ gemini: { install: ["npm", "install", "-g", "@google/gemini-cli"], login: ["gemini"] }
104
+ };
105
+ var BRAIN_IDS = ["claude", "codex", "gemini"];
106
+ function defaultConfig() {
107
+ const brains = {};
108
+ for (const id of BRAIN_IDS) {
109
+ const d = BRAIN_DEFS[id];
110
+ brains[id] = { id, label: d.label, command: d.command, args: [...d.args], enabled: false };
111
+ }
112
+ return { version: 2, defaultBrain: null, brains };
113
+ }
114
+ function loadConfig() {
115
+ const path = wsConfigPath();
116
+ if (!existsSync2(path)) return null;
117
+ try {
118
+ const cfg = JSON.parse(readFileSync2(path, "utf8"));
119
+ const base = defaultConfig();
120
+ for (const id of BRAIN_IDS) if (!cfg.brains?.[id]) cfg.brains[id] = base.brains[id];
121
+ return cfg;
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+ function saveConfig(cfg) {
127
+ ensureWsDir();
128
+ writeFileSync2(wsConfigPath(), JSON.stringify(cfg, null, 2) + "\n", "utf8");
129
+ }
130
+
131
+ // src/brains.ts
132
+ import { spawn, spawnSync } from "child_process";
133
+ function isInstalled(command) {
134
+ const finder = process.platform === "win32" ? "where" : "which";
135
+ const res = spawnSync(finder, [command], { stdio: "ignore" });
136
+ return res.status === 0;
137
+ }
138
+ function renderPrompt(history, message) {
139
+ if (history.length === 0) return message;
140
+ const lines = history.map((t) => `${t.role === "user" ? "User" : "Assistant"}: ${t.content}`);
141
+ lines.push(`User: ${message}`);
142
+ return [
143
+ "You are continuing an ongoing conversation. Below is the transcript so far.",
144
+ "Read it for context and reply only as the assistant to the final User message.",
145
+ "",
146
+ lines.join("\n\n"),
147
+ "",
148
+ "Assistant:"
149
+ ].join("\n");
150
+ }
151
+ function runBrain(brain, prompt) {
152
+ return new Promise((resolve) => {
153
+ let child;
154
+ try {
155
+ child = spawn(brain.command, [...brain.args, prompt], { stdio: ["ignore", "pipe", "pipe"] });
156
+ } catch (e) {
157
+ resolve({ ok: false, text: `Could not launch "${brain.command}": ${e.message}` });
158
+ return;
159
+ }
160
+ let out = "";
161
+ let err = "";
162
+ child.stdout.on("data", (d) => {
163
+ out += d.toString();
164
+ });
165
+ child.stderr.on("data", (d) => {
166
+ err += d.toString();
167
+ });
168
+ child.on("error", (e) => resolve({ ok: false, text: `Could not run "${brain.command}": ${e.message}` }));
169
+ child.on("close", (code) => {
170
+ const text = out.trim();
171
+ if (code === 0 && text) resolve({ ok: true, text });
172
+ else resolve({ ok: false, text: err.trim() || text || `"${brain.command}" exited with code ${code}` });
173
+ });
174
+ });
175
+ }
176
+
177
+ // src/alias.ts
178
+ import { homedir as homedir2 } from "os";
179
+ import { join as join2 } from "path";
180
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3 } from "fs";
181
+ var START = "# >>> holt launch alias >>>";
182
+ var END = "# <<< holt launch alias <<<";
183
+ function rcFile() {
184
+ const shell = process.env.SHELL || "";
185
+ if (shell.includes("zsh")) return join2(homedir2(), ".zshrc");
186
+ if (shell.includes("bash")) return join2(homedir2(), ".bashrc");
187
+ return join2(homedir2(), ".profile");
188
+ }
189
+ function installAlias(name) {
190
+ const file = rcFile();
191
+ const block = `${START}
192
+ alias ${name}="holt chat"
193
+ ${END}`;
194
+ try {
195
+ let content = existsSync3(file) ? readFileSync3(file, "utf8") : "";
196
+ const re = new RegExp(`${START}[\\s\\S]*?${END}`);
197
+ if (re.test(content)) content = content.replace(re, block);
198
+ else content = content.replace(/\n*$/, "\n") + block + "\n";
199
+ writeFileSync3(file, content, "utf8");
200
+ return { ok: true, file };
201
+ } catch (e) {
202
+ return { ok: false, file, message: `Could not write ${file}: ${e.message}` };
203
+ }
204
+ }
205
+ function currentAlias() {
206
+ const file = rcFile();
207
+ try {
208
+ const m = readFileSync3(file, "utf8").match(/alias\s+([^\s=]+)="holt chat"/);
209
+ return m ? m[1] : null;
210
+ } catch {
211
+ return null;
212
+ }
213
+ }
214
+ function removeAlias() {
215
+ const file = rcFile();
216
+ try {
217
+ if (existsSync3(file)) {
218
+ const content = readFileSync3(file, "utf8").replace(new RegExp(`\\n*${START}[\\s\\S]*?${END}\\n*`), "\n");
219
+ writeFileSync3(file, content, "utf8");
220
+ }
221
+ return { ok: true, file };
222
+ } catch (e) {
223
+ return { ok: false, file, message: e.message };
224
+ }
225
+ }
226
+
227
+ // src/install.ts
228
+ import { spawn as spawn2 } from "child_process";
229
+ function runInteractive(cmd, args) {
230
+ return new Promise((resolve) => {
231
+ let child;
232
+ try {
233
+ child = spawn2(cmd, args, { stdio: "inherit" });
234
+ } catch {
235
+ resolve(-1);
236
+ return;
237
+ }
238
+ child.on("error", () => resolve(-1));
239
+ child.on("close", (code) => resolve(code ?? -1));
240
+ });
241
+ }
242
+
243
+ // src/commands/init.ts
244
+ function parseBrains(raw, found) {
245
+ const s = raw.trim().toLowerCase();
246
+ if (s === "") return found.length ? found : [...BRAIN_IDS];
247
+ if (s === "all") return [...BRAIN_IDS];
248
+ const picked = s.split(/[\s,]+/).filter((x) => BRAIN_IDS.includes(x));
249
+ if (picked.length) return [...new Set(picked)];
250
+ return found.length ? found : ["claude"];
251
+ }
252
+ async function init() {
253
+ const { ask, close } = createReader();
254
+ if (!await ensureTrusted(ask)) {
255
+ close();
256
+ return;
257
+ }
258
+ console.log(c.accent("Holt setup") + c.dim(` (${workspace()})`) + "\n");
259
+ console.log("Looking for agent CLIs on your machine...\n");
260
+ const found = [];
261
+ for (const id of BRAIN_IDS) {
262
+ const ok = isInstalled(BRAIN_DEFS[id].command);
263
+ console.log(` ${ok ? c.green("found ") : c.dim("missing")} ${BRAIN_DEFS[id].label} (${BRAIN_DEFS[id].command})`);
264
+ if (ok) found.push(id);
265
+ }
266
+ console.log("");
267
+ const chosen = parseBrains(
268
+ await ask('Which brains do you want? claude, codex, gemini (comma-separated, or "all"): ') ?? "",
269
+ found
270
+ );
271
+ console.log(c.dim(` using: ${chosen.join(", ")}`));
272
+ const toInstall = chosen.filter((id) => !isInstalled(BRAIN_DEFS[id].command));
273
+ const loginWanted = /* @__PURE__ */ new Set();
274
+ for (const id of toInstall) {
275
+ const a = (await ask(` ${BRAIN_DEFS[id].label} is not installed. Sign in after install? [Y/n] `) ?? "").trim().toLowerCase();
276
+ if (a !== "n" && a !== "no") loginWanted.add(id);
277
+ }
278
+ const defPick = chosen.includes("claude") ? "claude" : chosen[0];
279
+ const dans = (await ask(`
280
+ Default brain? [${chosen.join("/")}] (${defPick}): `) ?? "").trim();
281
+ const defaultBrain = chosen.includes(dans) ? dans : defPick;
282
+ const aliasAns = (await ask('Launch command? Type a custom word like "ai", or press enter to keep "holt": ') ?? "").trim();
283
+ let aliasNote = "";
284
+ if (aliasAns && aliasAns !== "holt") {
285
+ if (isInstalled(aliasAns)) console.log(c.dim(` note: "${aliasAns}" already exists; the alias will shadow it in new shells.`));
286
+ const r = installAlias(aliasAns);
287
+ aliasNote = r.ok ? c.green(` alias "${aliasAns}" -> holt chat added to ${r.file} (run: source ${r.file})`) : c.red(" " + r.message);
288
+ }
289
+ close();
290
+ for (const id of toInstall) {
291
+ const s = BRAIN_SETUP[id];
292
+ console.log("\n" + c.accent(`Installing ${BRAIN_DEFS[id].label}`) + c.dim(` (${s.install.join(" ")})`));
293
+ const code = await runInteractive(s.install[0], s.install.slice(1));
294
+ console.log(code === 0 ? c.green(` ${BRAIN_DEFS[id].label} installed.`) : c.red(` Install failed (exit ${code}). Run manually: ${s.install.join(" ")}`));
295
+ }
296
+ for (const id of toInstall) {
297
+ if (!loginWanted.has(id) || !isInstalled(BRAIN_DEFS[id].command)) continue;
298
+ const s = BRAIN_SETUP[id];
299
+ console.log("\n" + c.accent(`Sign in to ${BRAIN_DEFS[id].label}`));
300
+ console.log(c.dim(` Starting "${s.login.join(" ")}". Complete sign-in, then exit that tool to return here.`));
301
+ await runInteractive(s.login[0], s.login.slice(1));
302
+ }
303
+ const cfg = loadConfig() ?? defaultConfig();
304
+ for (const id of BRAIN_IDS) cfg.brains[id].enabled = chosen.includes(id) && isInstalled(BRAIN_DEFS[id].command);
305
+ cfg.defaultBrain = cfg.brains[defaultBrain].enabled ? defaultBrain : BRAIN_IDS.find((id) => cfg.brains[id].enabled) ?? null;
306
+ saveConfig(cfg);
307
+ console.log("\n" + c.green("Saved to ./.holt/config.json"));
308
+ if (aliasNote) console.log(aliasNote);
309
+ if (cfg.defaultBrain) console.log("Start chatting: " + c.accent(aliasAns && aliasAns !== "holt" ? aliasAns : "holt chat") + "\n");
310
+ else console.log(c.dim('No brain is ready yet. Install one, then run "holt init" again.\n'));
311
+ }
312
+
313
+ // src/commands/setting.ts
314
+ function printStatus(cfg) {
315
+ console.log("\n" + c.accent("Holt settings") + c.dim(" (this folder)"));
316
+ for (const id of BRAIN_IDS) {
317
+ const b = cfg.brains[id];
318
+ const tags = [
319
+ b.enabled ? c.green("enabled") : isInstalled(b.command) ? c.dim("installed, off") : c.dim("not installed"),
320
+ cfg.defaultBrain === id ? c.accent("default") : ""
321
+ ].filter(Boolean).join(" ");
322
+ console.log(` ${id.padEnd(7)} ${b.label.padEnd(16)} ${tags}`);
323
+ }
324
+ console.log(c.dim(` launch command: ${currentAlias() || "holt (default)"}`));
325
+ console.log("\n " + c.dim("[d] default brain [t] toggle brain [a] launch command [enter] done"));
326
+ }
327
+ async function runSettings(ask) {
328
+ let cfg = loadConfig() ?? defaultConfig();
329
+ while (true) {
330
+ printStatus(cfg);
331
+ const raw = await ask(" > ");
332
+ const choice = (raw ?? "").trim().toLowerCase();
333
+ if (raw === null || choice === "" || choice === "q") break;
334
+ if (choice === "d") {
335
+ const enabled = BRAIN_IDS.filter((id) => cfg.brains[id].enabled);
336
+ if (enabled.length === 0) {
337
+ console.log(c.dim(' No enabled brains. Toggle one on first with "t".'));
338
+ continue;
339
+ }
340
+ const pick = (await ask(` default brain [${enabled.join("/")}]: `) ?? "").trim();
341
+ if (enabled.includes(pick)) {
342
+ cfg.defaultBrain = pick;
343
+ console.log(c.green(` default set to ${cfg.brains[pick].label}`));
344
+ } else console.log(c.dim(" unchanged."));
345
+ } else if (choice === "t") {
346
+ const pick = (await ask(` toggle which brain [${BRAIN_IDS.join("/")}]: `) ?? "").trim();
347
+ if (BRAIN_IDS.includes(pick)) {
348
+ if (!cfg.brains[pick].enabled && !isInstalled(cfg.brains[pick].command)) {
349
+ console.log(c.dim(` ${cfg.brains[pick].label} is not installed. Run "holt init" to install it.`));
350
+ } else {
351
+ cfg.brains[pick].enabled = !cfg.brains[pick].enabled;
352
+ if (!cfg.brains[pick].enabled && cfg.defaultBrain === pick) cfg.defaultBrain = BRAIN_IDS.find((id) => cfg.brains[id].enabled) ?? null;
353
+ if (cfg.brains[pick].enabled && !cfg.defaultBrain) cfg.defaultBrain = pick;
354
+ console.log(c.dim(` ${cfg.brains[pick].label} is now ${cfg.brains[pick].enabled ? "on" : "off"}.`));
355
+ }
356
+ } else console.log(c.dim(" unchanged."));
357
+ } else if (choice === "a") {
358
+ const name = (await ask(" launch command (blank to reset to holt): ") ?? "").trim();
359
+ if (name && name !== "holt") {
360
+ if (isInstalled(name)) console.log(c.dim(` note: "${name}" already exists; the alias will shadow it in new shells.`));
361
+ const r = installAlias(name);
362
+ console.log(r.ok ? c.green(` alias "${name}" -> holt chat added to ${r.file} (run: source ${r.file})`) : c.red(" " + r.message));
363
+ } else {
364
+ removeAlias();
365
+ console.log(c.dim(" reset to holt."));
366
+ }
367
+ } else {
368
+ console.log(c.dim(" pick d, t, a, or press enter to finish."));
369
+ }
370
+ saveConfig(cfg);
371
+ }
372
+ saveConfig(cfg);
373
+ return cfg;
374
+ }
375
+ async function setting() {
376
+ const { ask, close } = createReader();
377
+ if (!await ensureTrusted(ask)) {
378
+ close();
379
+ return;
380
+ }
381
+ await runSettings(ask);
382
+ close();
383
+ console.log("");
384
+ }
385
+
386
+ // src/commands/chat.ts
387
+ function help() {
388
+ console.log(c.dim([
389
+ " commands:",
390
+ " /brain [name] switch brain (claude, codex, gemini). context is kept.",
391
+ " /setting configure brains and your launch command",
392
+ " /clear forget the conversation so far",
393
+ " /help this list",
394
+ " /exit leave"
395
+ ].join("\n")));
396
+ }
397
+ async function chat() {
398
+ const { ask, close } = createReader();
399
+ if (!await ensureTrusted(ask)) {
400
+ close();
401
+ return;
402
+ }
403
+ let cfg = loadConfig();
404
+ if (!cfg || !cfg.defaultBrain) {
405
+ const a = (await ask(c.dim("No Holt setup in this folder. Set it up now? [Y/n] ")) ?? "").trim().toLowerCase();
406
+ close();
407
+ if (a === "n" || a === "no") {
408
+ console.log(c.dim(' Run "holt init" here when ready.\n'));
409
+ return;
410
+ }
411
+ await init();
412
+ console.log(c.dim('\nSetup done. Run "holt chat" to start talking.\n'));
413
+ return;
414
+ }
415
+ let current = cfg.defaultBrain;
416
+ const history = [];
417
+ console.log("\n" + c.accent("Holt") + c.dim(` brain: ${cfg.brains[current].label}`));
418
+ console.log(c.dim("Type a message. Commands: /brain /setting /clear /help /exit\n"));
419
+ while (true) {
420
+ const raw = await ask(c.accent("\u203A "));
421
+ if (raw === null) break;
422
+ const line = raw.trim();
423
+ if (!line) continue;
424
+ if (line.startsWith("/")) {
425
+ const parts = line.slice(1).split(/\s+/);
426
+ const cmd = (parts[0] || "").toLowerCase();
427
+ const arg = (parts[1] || "").toLowerCase();
428
+ if (cmd === "exit" || cmd === "quit" || cmd === "q") break;
429
+ if (cmd === "help" || cmd === "h") {
430
+ help();
431
+ continue;
432
+ }
433
+ if (cmd === "clear") {
434
+ history.length = 0;
435
+ console.log(c.dim(" context cleared."));
436
+ continue;
437
+ }
438
+ if (cmd === "setting" || cmd === "settings") {
439
+ cfg = await runSettings(ask);
440
+ if (!cfg.brains[current].enabled && cfg.defaultBrain) current = cfg.defaultBrain;
441
+ console.log(c.dim(` brain: ${cfg.brains[current].label}`));
442
+ continue;
443
+ }
444
+ if (cmd === "brain") {
445
+ const enabled = BRAIN_IDS.filter((id) => cfg.brains[id].enabled);
446
+ if (arg && enabled.includes(arg)) {
447
+ current = arg;
448
+ const turns = Math.floor(history.length / 2);
449
+ console.log(c.green(` switched to ${cfg.brains[current].label}. Context kept (${turns} turn${turns === 1 ? "" : "s"}).`));
450
+ } else if (arg) {
451
+ console.log(c.dim(` "${arg}" is not available. Installed: ${enabled.join(", ") || "none"}`));
452
+ } else {
453
+ console.log(c.dim(" brains: " + enabled.map((id) => id === current ? c.accent(id + " (current)") : id).join(" ")));
454
+ console.log(c.dim(" usage: /brain <name>"));
455
+ }
456
+ continue;
457
+ }
458
+ console.log(c.dim(` unknown command: /${cmd} (try /help)`));
459
+ continue;
460
+ }
461
+ const brain = cfg.brains[current];
462
+ if (!isInstalled(brain.command)) {
463
+ console.log(c.red(` ${brain.label} (${brain.command}) is not on your PATH. Use /brain to switch or /setting.`));
464
+ continue;
465
+ }
466
+ console.log(c.dim(` ${brain.label} is thinking...`));
467
+ const res = await runBrain(brain, renderPrompt(history, line));
468
+ if (res.ok) {
469
+ history.push({ role: "user", content: line });
470
+ history.push({ role: "assistant", content: res.text });
471
+ console.log("\n" + res.text + "\n");
472
+ } else {
473
+ console.log(c.red("\n " + res.text + "\n"));
474
+ }
475
+ }
476
+ close();
477
+ console.log(c.dim("\nBye.\n"));
478
+ }
479
+
480
+ // src/commands/login.ts
481
+ async function login(which) {
482
+ const id = (which || "").toLowerCase();
483
+ if (!BRAIN_IDS.includes(id)) {
484
+ console.log(c.dim(`
485
+ Usage: holt login <${BRAIN_IDS.join("|")}>
486
+ `));
487
+ return;
488
+ }
489
+ const s = BRAIN_SETUP[id];
490
+ console.log("\n" + c.accent(`Sign in to ${BRAIN_DEFS[id].label}`));
491
+ console.log(c.dim(` Starting "${s.login.join(" ")}". Complete sign-in, then exit that tool.
492
+ `));
493
+ await runInteractive(s.login[0], s.login.slice(1));
494
+ }
495
+
3
496
  // src/cli.ts
4
- var VERSION = "0.0.1";
497
+ var VERSION = "0.2.0";
5
498
  var BANNER = `
6
499
  \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
7
500
  \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
@@ -12,27 +505,24 @@ var BANNER = `
12
505
  Everything you know, kept and connected.
13
506
  `;
14
507
  var HELP = `${BANNER}
15
- Usage: holt <command> [options]
508
+ Usage: holt <command>
16
509
 
17
510
  Commands:
18
- init Set up your brain (LLM) and memory (local or cloud)
19
- chat Start an interactive session
20
- skill <cmd> Manage skills: search | add | remove | list | publish
511
+ init Trust this folder, choose and install brains, sign in, set defaults
512
+ chat Start a session. Switch brains mid-chat with /brain, context is kept
513
+ setting Configure brains and your launch command (per folder)
514
+ login <brain> Sign in to a brain: claude, codex, or gemini
21
515
  version Print the Holt version
22
516
  help Show this help
23
517
 
518
+ Holt runs in the folder you launch it from and asks to trust it first.
519
+ Brains are the agent CLIs on your machine: claude (Claude Code), codex, gemini.
520
+
24
521
  Docs: https://productsdecoded.com/holt
25
522
  Repo: https://github.com/holt-os/holt
26
-
27
- Holt is in early development (Phase 0). Most commands are not wired up yet.
28
523
  `;
29
- function notReady(cmd) {
30
- console.log(`
31
- "${cmd}" is not implemented yet. Holt is in Phase 0 (skeleton).`);
32
- console.log(" Follow progress: https://github.com/holt-os/holt\n");
33
- }
34
- function main(argv) {
35
- const [cmd, ...rest] = argv;
524
+ async function main() {
525
+ const cmd = process.argv[2];
36
526
  switch (cmd) {
37
527
  case void 0:
38
528
  case "help":
@@ -46,9 +536,17 @@ function main(argv) {
46
536
  console.log(`holt ${VERSION}`);
47
537
  break;
48
538
  case "init":
539
+ await init();
540
+ break;
49
541
  case "chat":
50
- case "skill":
51
- notReady([cmd, ...rest].join(" "));
542
+ await chat();
543
+ break;
544
+ case "setting":
545
+ case "settings":
546
+ await setting();
547
+ break;
548
+ case "login":
549
+ await login(process.argv[3]);
52
550
  break;
53
551
  default:
54
552
  console.log(`
@@ -58,4 +556,4 @@ function main(argv) {
58
556
  process.exitCode = 1;
59
557
  }
60
558
  }
61
- main(process.argv.slice(2));
559
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holt-os/holt",
3
- "version": "0.0.2",
3
+ "version": "0.2.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",