@holt-os/holt 0.0.1 → 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 +70 -29
- package/config.example.yml +4 -2
- package/dist/cli.js +515 -17
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,59 +2,100 @@
|
|
|
2
2
|
|
|
3
3
|
**Everything you know, kept and connected.**
|
|
4
4
|
|
|
5
|
-
Holt is an open-source, self-hosted personal agent OS. Clone it, pick your skills, choose your brain
|
|
5
|
+
Holt is an open-source, self-hosted personal agent OS. Clone it, pick your skills, choose your brain, and it runs on *your* machine with persistent memory you can actually see and walk.
|
|
6
6
|
|
|
7
|
-
> A *holt* is a small wood
|
|
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
|
-
>
|
|
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
|
-
##
|
|
13
|
+
## Quickstart
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
```bash
|
|
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`)
|
|
20
|
+
```
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## First run
|
|
23
23
|
|
|
24
|
-
|
|
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
25
|
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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]
|
|
30
31
|
```
|
|
31
32
|
|
|
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/`.
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
37
78
|
```
|
|
38
79
|
|
|
39
80
|
## Configuration
|
|
40
81
|
|
|
41
|
-
|
|
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`.
|
|
42
83
|
|
|
43
84
|
## Architecture
|
|
44
85
|
|
|
45
|
-
Small strongly-typed **TypeScript core** (
|
|
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).
|
|
46
87
|
|
|
47
88
|
## Roadmap
|
|
48
89
|
|
|
49
90
|
Built in always-shippable phases toward a full-vision v1:
|
|
50
91
|
|
|
51
|
-
0. **Skeleton
|
|
52
|
-
1. **Memory
|
|
53
|
-
2. **Any
|
|
54
|
-
3. **Skills
|
|
55
|
-
4. **Knowledge graph
|
|
56
|
-
5. **Orchestration
|
|
57
|
-
6. **Channels
|
|
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
|
|
58
99
|
|
|
59
100
|
## Contributing
|
|
60
101
|
|
package/config.example.yml
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
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
|
|
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>
|
|
508
|
+
Usage: holt <command>
|
|
16
509
|
|
|
17
510
|
Commands:
|
|
18
|
-
init
|
|
19
|
-
chat Start
|
|
20
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
"${cmd}" is not implemented yet \u2014 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
|
-
|
|
51
|
-
|
|
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(
|
|
559
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holt-os/holt",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "An open-source personal agent OS
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
7
7
|
"author": "Debashis Nayak",
|