@holt-os/holt 0.14.3 → 0.15.1
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 +14 -1
- package/dist/cli.js +424 -71
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -195,7 +195,7 @@ A brain is an agent CLI installed and logged in on your machine. No API keys to
|
|
|
195
195
|
|
|
196
196
|
### Direct API brains
|
|
197
197
|
|
|
198
|
-
A brain can also be a direct provider connection: no CLI install, your own key. Run `holt setting` and pick `[c] connect API brain`, or say yes to the API-brain question in `holt init`. Choose a provider (anthropic, openai, gemini), a model (suggestions offered, type anything), and a short name. For the key, paste a raw key (stored in `~/.holt/credentials.json`, mode 600) or give the name of an env var that holds it. Resolution order: the brain's `keyEnv`, the credentials file, then the standard env var (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`). API brains stream and share memory exactly like CLI brains; switch with `/brain <name>`.
|
|
198
|
+
A brain can also be a direct provider connection: no CLI install, your own key. Run `holt setting` and pick `[c] connect API brain`, or say yes to the API-brain question in `holt init`. Choose a provider (anthropic, openai, gemini, kimi, grok), a model (suggestions offered, type anything), and a short name. Kimi (Moonshot) and Grok (xAI) speak the OpenAI wire format at their own endpoints; any other OpenAI-compatible endpoint (a gateway, a self-hosted server) works too via an optional `baseUrl` on the brain in `.holt/config.json`. For the key, paste a raw key (stored in `~/.holt/credentials.json`, mode 600) or give the name of an env var that holds it. Resolution order: the brain's `keyEnv`, the credentials file, then the standard env var (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `MOONSHOT_API_KEY`, `XAI_API_KEY`). API brains stream and share memory exactly like CLI brains; switch with `/brain <name>`.
|
|
199
199
|
|
|
200
200
|
## See your memory: `holt graph`
|
|
201
201
|
|
|
@@ -494,6 +494,14 @@ It reports, section by section:
|
|
|
494
494
|
- **Always-on / Telegram**: whether a Telegram bot is set up, and a note on hosting the bot on a low-power always-on machine while keeping heavy local models on a bigger one.
|
|
495
495
|
- **Recommended next steps**: a short checklist built from whatever gaps it found.
|
|
496
496
|
|
|
497
|
+
And when you would rather not do the steps yourself:
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
holt doctor --fix
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
`--fix` repairs the common problems in one go: starts Ollama when it is installed but not running, downloads the missing embed model, upgrades any memories saved while embeddings were off, and reports a damaged-config repair if one happened. It ends with a "Repairs" section listing exactly what it did (or "Nothing needed repair"). Every Holt error message points here, so the answer to "something is off" is always the same one command.
|
|
504
|
+
|
|
497
505
|
It is read-only advice: it changes nothing, needs no trust, and always exits cleanly even when a probe fails.
|
|
498
506
|
|
|
499
507
|
The RAM-to-model table lives in `src/specs.ts` (`LOCAL_MODEL_RECS` / `recommendLocalModel`), the single source of truth the wiki maintainer reads too.
|
|
@@ -556,6 +564,11 @@ holt routine named, reusable, scheduled jobs: add | run | list | show |
|
|
|
556
564
|
holt telegram chat with Holt from your phone: telegram [setup]
|
|
557
565
|
holt notify [msg] push a message to your phone over Telegram (stdin-friendly)
|
|
558
566
|
holt doctor check this machine and recommend how best to run Holt here
|
|
567
|
+
("holt doctor --fix" also repairs the common problems it finds:
|
|
568
|
+
starts Ollama, pulls the embed model, mends a damaged config)
|
|
569
|
+
holt tour a 2-minute guided first run: ask, teach one fact, watch it remember
|
|
570
|
+
holt update get the latest Holt in one word (npm or Homebrew, auto-detected)
|
|
571
|
+
holt reset start this folder over (typed confirmation; your files untouched)
|
|
559
572
|
holt voice teach Holt your writing voice: add <file> | show | edit | clear
|
|
560
573
|
holt write <what> draft content in your voice with anti-AI checks (--type, --out, --fast)
|
|
561
574
|
holt graph see your memory as an interactive knowledge graph
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/config.ts
|
|
4
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
4
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, renameSync } from "fs";
|
|
5
5
|
import { join as join2 } from "path";
|
|
6
6
|
|
|
7
7
|
// src/workspace.ts
|
|
@@ -11,6 +11,13 @@ import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
|
11
11
|
|
|
12
12
|
// src/ui.ts
|
|
13
13
|
import readline from "readline";
|
|
14
|
+
var quiet = false;
|
|
15
|
+
function setQuietMode(q) {
|
|
16
|
+
quiet = q;
|
|
17
|
+
}
|
|
18
|
+
function isQuietMode() {
|
|
19
|
+
return quiet;
|
|
20
|
+
}
|
|
14
21
|
var on = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
15
22
|
var wrap = (code) => (s) => on ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
16
23
|
var c = {
|
|
@@ -91,6 +98,18 @@ function createStatusBar() {
|
|
|
91
98
|
detach
|
|
92
99
|
};
|
|
93
100
|
}
|
|
101
|
+
async function askYesNo(ask, question, def) {
|
|
102
|
+
for (let tries = 0; tries < 3; tries++) {
|
|
103
|
+
const raw = await ask(question);
|
|
104
|
+
if (raw === null) return def;
|
|
105
|
+
const a = raw.trim().toLowerCase();
|
|
106
|
+
if (a === "") return def;
|
|
107
|
+
if (a === "y" || a === "yes") return true;
|
|
108
|
+
if (a === "n" || a === "no") return false;
|
|
109
|
+
console.log(c.dim(` Just "y" or "n" please (enter = ${def ? "yes" : "no"}). You typed "${raw.trim()}".`));
|
|
110
|
+
}
|
|
111
|
+
return def;
|
|
112
|
+
}
|
|
94
113
|
function createReader() {
|
|
95
114
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
96
115
|
const buffer = [];
|
|
@@ -148,8 +167,7 @@ async function ensureTrusted(ask) {
|
|
|
148
167
|
console.log("\n" + c.accent("Trust this folder?"));
|
|
149
168
|
console.log(" " + ws);
|
|
150
169
|
console.log(c.dim(" Holt will read and write here: its config, memory, and any files you ask a brain to touch."));
|
|
151
|
-
|
|
152
|
-
if (ans === "y" || ans === "yes") {
|
|
170
|
+
if (await askYesNo(ask, " Trust and continue? [y/N] ", false)) {
|
|
153
171
|
trustDir(ws);
|
|
154
172
|
console.log(c.green(" Trusted.") + c.dim(" (remembered for next time)") + "\n");
|
|
155
173
|
return true;
|
|
@@ -162,10 +180,24 @@ function ensureWsDir() {
|
|
|
162
180
|
}
|
|
163
181
|
|
|
164
182
|
// src/config.ts
|
|
183
|
+
var PROVIDER_WIRE = {
|
|
184
|
+
anthropic: "anthropic",
|
|
185
|
+
openai: "openai",
|
|
186
|
+
gemini: "gemini",
|
|
187
|
+
kimi: "openai",
|
|
188
|
+
grok: "openai"
|
|
189
|
+
};
|
|
190
|
+
var PROVIDER_BASE_URL = {
|
|
191
|
+
openai: "https://api.openai.com/v1",
|
|
192
|
+
kimi: "https://api.moonshot.ai/v1",
|
|
193
|
+
grok: "https://api.x.ai/v1"
|
|
194
|
+
};
|
|
165
195
|
var WIKI_DEFAULT_LOCAL_MODEL = "qwen2.5:7b";
|
|
166
196
|
var BRAIN_DEFS = {
|
|
167
197
|
claude: { label: "Claude Code", command: "claude", args: ["-p"] },
|
|
168
|
-
|
|
198
|
+
// --skip-git-repo-check: codex exec otherwise refuses to run in folders that
|
|
199
|
+
// are not git repositories, which is nearly every non-developer folder.
|
|
200
|
+
codex: { label: "Codex (OpenAI)", command: "codex", args: ["exec", "--skip-git-repo-check"] },
|
|
169
201
|
gemini: { label: "Gemini CLI", command: "gemini", args: ["-p"] }
|
|
170
202
|
};
|
|
171
203
|
var BRAIN_SETUP = {
|
|
@@ -174,16 +206,20 @@ var BRAIN_SETUP = {
|
|
|
174
206
|
gemini: { install: ["npm", "install", "-g", "@google/gemini-cli"], login: ["gemini"] }
|
|
175
207
|
};
|
|
176
208
|
var BRAIN_IDS = ["claude", "codex", "gemini"];
|
|
177
|
-
var PROVIDERS = ["anthropic", "openai", "gemini"];
|
|
209
|
+
var PROVIDERS = ["anthropic", "openai", "gemini", "kimi", "grok"];
|
|
178
210
|
var PROVIDER_MODEL_SUGGESTION = {
|
|
179
211
|
anthropic: "claude-sonnet-4-6",
|
|
180
212
|
openai: "gpt-5",
|
|
181
|
-
gemini: "gemini-2.5-flash"
|
|
213
|
+
gemini: "gemini-2.5-flash",
|
|
214
|
+
kimi: "kimi-k2-turbo-preview",
|
|
215
|
+
grok: "grok-4"
|
|
182
216
|
};
|
|
183
217
|
var PROVIDER_ENV = {
|
|
184
218
|
anthropic: "ANTHROPIC_API_KEY",
|
|
185
219
|
openai: "OPENAI_API_KEY",
|
|
186
|
-
gemini: "GEMINI_API_KEY"
|
|
220
|
+
gemini: "GEMINI_API_KEY",
|
|
221
|
+
kimi: "MOONSHOT_API_KEY",
|
|
222
|
+
grok: "XAI_API_KEY"
|
|
187
223
|
};
|
|
188
224
|
function defaultConfig() {
|
|
189
225
|
const brains = {};
|
|
@@ -201,14 +237,25 @@ function defaultConfig() {
|
|
|
201
237
|
wiki: { maintainer: "brain", localModel: WIKI_DEFAULT_LOCAL_MODEL }
|
|
202
238
|
};
|
|
203
239
|
}
|
|
240
|
+
var lastConfigRepair = null;
|
|
204
241
|
function loadConfig() {
|
|
205
242
|
const path = wsConfigPath();
|
|
206
243
|
if (!existsSync2(path)) return null;
|
|
244
|
+
let raw;
|
|
207
245
|
try {
|
|
208
|
-
|
|
246
|
+
raw = readFileSync2(path, "utf8");
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const cfg = JSON.parse(raw);
|
|
209
252
|
const base = defaultConfig();
|
|
210
253
|
const brains = cfg.brains ?? {};
|
|
211
254
|
for (const id of BRAIN_IDS) if (!brains[id]) brains[id] = base.brains[id];
|
|
255
|
+
const cx = brains.codex;
|
|
256
|
+
if (cx && cx.command === "codex" && Array.isArray(cx.args) && !cx.args.includes("--skip-git-repo-check")) {
|
|
257
|
+
cx.args = [...cx.args, "--skip-git-repo-check"];
|
|
258
|
+
}
|
|
212
259
|
const wikiIn = cfg.wiki ?? {};
|
|
213
260
|
const wiki = {
|
|
214
261
|
maintainer: wikiIn.maintainer === "local" ? "local" : "brain",
|
|
@@ -227,6 +274,18 @@ function loadConfig() {
|
|
|
227
274
|
wiki
|
|
228
275
|
};
|
|
229
276
|
} catch {
|
|
277
|
+
const backup2 = `${path}.broken-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[:T]/g, "")}`;
|
|
278
|
+
try {
|
|
279
|
+
renameSync(path, backup2);
|
|
280
|
+
lastConfigRepair = backup2;
|
|
281
|
+
if (!isQuietMode()) {
|
|
282
|
+
console.error(
|
|
283
|
+
c.red("\n Your Holt settings file was damaged, so it was moved aside.") + c.dim(`
|
|
284
|
+
Saved at: ${backup2}`) + c.dim("\n Holt will set this folder up fresh; your memory is untouched.\n")
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
}
|
|
230
289
|
return null;
|
|
231
290
|
}
|
|
232
291
|
}
|
|
@@ -310,7 +369,7 @@ function renderPrompt(history, message, memory = []) {
|
|
|
310
369
|
function looksLikeAuthError(text) {
|
|
311
370
|
try {
|
|
312
371
|
if (!text) return false;
|
|
313
|
-
const re = /run\s+\/login|not\s+logged\s+in|invalid\s+api\s+key|please\s+(?:log|sign)\s+in|authentication\s+(?:failed|required)|unauthorized|session\s+(?:has\s+)?expired|no\s+api\s+key|login\s+required|credit\s+balance\s+is\s+too\s+low/i;
|
|
372
|
+
const re = /run\s+\/login|not\s+logged\s+in|invalid\s+api\s+key|please\s+(?:log|sign)\s+in|authentication\s+(?:failed|required)|unauthorized|session\s+(?:has\s+)?expired|no\s+api\s+key|login\s+required|credit\s+balance\s+is\s+too\s+low|set\s+an\s+auth\s+method|GEMINI_API_KEY/i;
|
|
314
373
|
return re.test(text);
|
|
315
374
|
} catch {
|
|
316
375
|
return false;
|
|
@@ -967,7 +1026,8 @@ function dataLines(event) {
|
|
|
967
1026
|
}
|
|
968
1027
|
return out;
|
|
969
1028
|
}
|
|
970
|
-
function extractDelta(
|
|
1029
|
+
function extractDelta(providerIn, json) {
|
|
1030
|
+
const provider = PROVIDER_WIRE[providerIn];
|
|
971
1031
|
const j = json;
|
|
972
1032
|
if (provider === "anthropic") {
|
|
973
1033
|
if (j["type"] === "content_block_delta") {
|
|
@@ -987,7 +1047,8 @@ function extractDelta(provider, json) {
|
|
|
987
1047
|
return parts.map((p) => typeof p.text === "string" ? p.text : "").join("");
|
|
988
1048
|
}
|
|
989
1049
|
function buildRequest(brain, key, prompt) {
|
|
990
|
-
|
|
1050
|
+
const wire = PROVIDER_WIRE[brain.provider];
|
|
1051
|
+
if (wire === "anthropic") {
|
|
991
1052
|
return {
|
|
992
1053
|
url: "https://api.anthropic.com/v1/messages",
|
|
993
1054
|
headers: {
|
|
@@ -1003,9 +1064,10 @@ function buildRequest(brain, key, prompt) {
|
|
|
1003
1064
|
})
|
|
1004
1065
|
};
|
|
1005
1066
|
}
|
|
1006
|
-
if (
|
|
1067
|
+
if (wire === "openai") {
|
|
1068
|
+
const base = (brain.baseUrl ?? PROVIDER_BASE_URL[brain.provider] ?? "https://api.openai.com/v1").replace(/\/$/, "");
|
|
1007
1069
|
return {
|
|
1008
|
-
url:
|
|
1070
|
+
url: `${base}/chat/completions`,
|
|
1009
1071
|
headers: {
|
|
1010
1072
|
"content-type": "application/json",
|
|
1011
1073
|
authorization: `Bearer ${key}`
|
|
@@ -1318,9 +1380,9 @@ async function setupTelegram(ask) {
|
|
|
1318
1380
|
console.log(c.dim(" Option A: open your new bot in Telegram and send it any message,"));
|
|
1319
1381
|
console.log(c.dim(" then I will auto-detect the chat id."));
|
|
1320
1382
|
console.log(c.dim(" Option B: paste a numeric chat id yourself."));
|
|
1321
|
-
const
|
|
1383
|
+
const autoDetect = await askYesNo(ask, "\n Auto-detect from a message you just sent? [Y/n] ", true);
|
|
1322
1384
|
let allowedChatId = null;
|
|
1323
|
-
if (
|
|
1385
|
+
if (autoDetect) {
|
|
1324
1386
|
console.log(c.dim("\n Checking for a recent message..."));
|
|
1325
1387
|
const updates = await getUpdates(0, token);
|
|
1326
1388
|
allowedChatId = latestChatId(updates);
|
|
@@ -1861,8 +1923,7 @@ async function runQuestions(ask, depth) {
|
|
|
1861
1923
|
return out;
|
|
1862
1924
|
}
|
|
1863
1925
|
async function askKeepExcerpt(ask) {
|
|
1864
|
-
|
|
1865
|
-
return a === "y" || a === "yes";
|
|
1926
|
+
return askYesNo(ask, " Store a short excerpt so Holt can match your tone directly? [y/N] ", false);
|
|
1866
1927
|
}
|
|
1867
1928
|
async function addFileSample(ask, file, v) {
|
|
1868
1929
|
const path = resolve(process.cwd(), file);
|
|
@@ -2110,12 +2171,10 @@ async function init() {
|
|
|
2110
2171
|
const toInstall = chosen.filter((id) => !isInstalled(BRAIN_DEFS[id].command));
|
|
2111
2172
|
const loginWanted = /* @__PURE__ */ new Set();
|
|
2112
2173
|
for (const id of toInstall) {
|
|
2113
|
-
|
|
2114
|
-
if (a !== "n" && a !== "no") loginWanted.add(id);
|
|
2174
|
+
if (await askYesNo(ask, ` ${BRAIN_DEFS[id].label} is not installed. Sign in after install? [Y/n] `, true)) loginWanted.add(id);
|
|
2115
2175
|
}
|
|
2116
2176
|
const connectedApiBrains = [];
|
|
2117
|
-
|
|
2118
|
-
if (apiAns === "y" || apiAns === "yes") {
|
|
2177
|
+
if (await askYesNo(ask, "\nAlso connect a direct API brain (raw key, no CLI needed)? [y/N] ", false)) {
|
|
2119
2178
|
const holder = defaultConfig();
|
|
2120
2179
|
const brain = await connectApiBrain(ask, holder);
|
|
2121
2180
|
if (brain) connectedApiBrains.push(brain);
|
|
@@ -2144,13 +2203,11 @@ Default brain? [${chosen.join("/")}] (${defPick}): `) ?? "").trim();
|
|
|
2144
2203
|
} else {
|
|
2145
2204
|
const ollamaHere = isInstalled("ollama");
|
|
2146
2205
|
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] ";
|
|
2147
|
-
|
|
2148
|
-
wantMemorySetup = a !== "n" && a !== "no";
|
|
2206
|
+
wantMemorySetup = await askYesNo(ask, "\n" + q, true);
|
|
2149
2207
|
if (!wantMemorySetup) console.log(c.dim(' Okay. Memory still works with keyword recall; run "holt init" again anytime.'));
|
|
2150
2208
|
}
|
|
2151
2209
|
if (!loadTelegramConfig()) {
|
|
2152
|
-
|
|
2153
|
-
if (tgAns === "y" || tgAns === "yes") await setupTelegram(ask);
|
|
2210
|
+
if (await askYesNo(ask, "\nChat with Holt from your phone over Telegram? [y/N] ", false)) await setupTelegram(ask);
|
|
2154
2211
|
}
|
|
2155
2212
|
close();
|
|
2156
2213
|
for (const id of toInstall) {
|
|
@@ -2210,9 +2267,9 @@ Default brain? [${chosen.join("/")}] (${defPick}): `) ?? "").trim();
|
|
|
2210
2267
|
console.log(c.dim('No brain is ready yet. Install one, then run "holt init" again.\n'));
|
|
2211
2268
|
}
|
|
2212
2269
|
const voiceReader = createReader();
|
|
2213
|
-
const
|
|
2270
|
+
const voiceYes = await askYesNo(voiceReader.ask, "Want Holt to learn how you write, so it can draft in your voice? [y/N] ", false);
|
|
2214
2271
|
voiceReader.close();
|
|
2215
|
-
if (
|
|
2272
|
+
if (voiceYes) {
|
|
2216
2273
|
await voiceInterview();
|
|
2217
2274
|
} else {
|
|
2218
2275
|
console.log(c.dim(' Skipped. Run "holt voice" anytime to set up your writing voice.\n'));
|
|
@@ -4171,9 +4228,9 @@ async function chat() {
|
|
|
4171
4228
|
}
|
|
4172
4229
|
let cfg = loadConfig();
|
|
4173
4230
|
if (!cfg || !cfg.defaultBrain) {
|
|
4174
|
-
const
|
|
4231
|
+
const setupYes = await askYesNo(ask, c.dim("No Holt setup in this folder. Set it up now? [Y/n] "), true);
|
|
4175
4232
|
close();
|
|
4176
|
-
if (
|
|
4233
|
+
if (!setupYes) {
|
|
4177
4234
|
console.log(c.dim(' Run "holt init" here when ready.\n'));
|
|
4178
4235
|
return;
|
|
4179
4236
|
}
|
|
@@ -4210,6 +4267,9 @@ async function chat() {
|
|
|
4210
4267
|
if (embedOk && stats.withEmbeddings < stats.turns) {
|
|
4211
4268
|
console.log(c.dim(` ${stats.turns - stats.withEmbeddings} older moments lack embeddings. Run "holt memory embed" to upgrade them.`));
|
|
4212
4269
|
}
|
|
4270
|
+
if (!embedOk) {
|
|
4271
|
+
console.log(c.dim(' Recall works, but semantic memory is off. "holt doctor --fix" turns it on.'));
|
|
4272
|
+
}
|
|
4213
4273
|
console.log(c.dim("Type a message. Commands: /brain /memory /output /save /allow /setting /clear /help /exit"));
|
|
4214
4274
|
showStatus();
|
|
4215
4275
|
while (true) {
|
|
@@ -4372,8 +4432,7 @@ async function chat() {
|
|
|
4372
4432
|
const outside = findOutsidePaths(line, workspace());
|
|
4373
4433
|
for (const dir of outside) {
|
|
4374
4434
|
if (grantedDirs.has(dir)) continue;
|
|
4375
|
-
|
|
4376
|
-
if (ans === "y" || ans === "yes") {
|
|
4435
|
+
if (await askYesNo(ask, c.dim(` Allow this session to access ${dir} (outside this folder)? [y/N] `), false)) {
|
|
4377
4436
|
grantedDirs.add(dir);
|
|
4378
4437
|
console.log(c.green(` granted this session: ${dir}`));
|
|
4379
4438
|
} else {
|
|
@@ -4688,6 +4747,9 @@ async function memoryCmd(sub, rest = []) {
|
|
|
4688
4747
|
}
|
|
4689
4748
|
const hits = await recall(q, "__none__", 8);
|
|
4690
4749
|
console.log("");
|
|
4750
|
+
if (!await embeddingsAvailable()) {
|
|
4751
|
+
console.log(c.dim(' (memory is in keyword mode; run "holt doctor --fix" to turn semantic recall back on)'));
|
|
4752
|
+
}
|
|
4691
4753
|
if (hits.length === 0) console.log(c.dim(" Nothing relevant found."));
|
|
4692
4754
|
else for (const h of hits) {
|
|
4693
4755
|
const when = new Date(h.turn.ts).toISOString().slice(0, 10);
|
|
@@ -7327,7 +7389,7 @@ function holtTools() {
|
|
|
7327
7389
|
}
|
|
7328
7390
|
|
|
7329
7391
|
// src/version.ts
|
|
7330
|
-
var VERSION = "0.
|
|
7392
|
+
var VERSION = "0.15.1";
|
|
7331
7393
|
|
|
7332
7394
|
// src/commands/mcp.ts
|
|
7333
7395
|
function printSetup() {
|
|
@@ -7471,7 +7533,7 @@ function parseArgs2(args) {
|
|
|
7471
7533
|
let out;
|
|
7472
7534
|
let noStore = false;
|
|
7473
7535
|
let noRecall = false;
|
|
7474
|
-
let
|
|
7536
|
+
let quiet2 = false;
|
|
7475
7537
|
for (let i = 0; i < args.length; i++) {
|
|
7476
7538
|
const a = args[i];
|
|
7477
7539
|
if (a === "--brain") {
|
|
@@ -7491,12 +7553,12 @@ function parseArgs2(args) {
|
|
|
7491
7553
|
continue;
|
|
7492
7554
|
}
|
|
7493
7555
|
if (a === "--quiet") {
|
|
7494
|
-
|
|
7556
|
+
quiet2 = true;
|
|
7495
7557
|
continue;
|
|
7496
7558
|
}
|
|
7497
7559
|
words2.push(a);
|
|
7498
7560
|
}
|
|
7499
|
-
return { task: words2.join(" ").trim(), brainId, out, noStore, noRecall, quiet };
|
|
7561
|
+
return { task: words2.join(" ").trim(), brainId, out, noStore, noRecall, quiet: quiet2 };
|
|
7500
7562
|
}
|
|
7501
7563
|
async function run(args) {
|
|
7502
7564
|
const p = parseArgs2(args);
|
|
@@ -7986,7 +8048,7 @@ function buildRoutinePrompt(r) {
|
|
|
7986
8048
|
return { prompt };
|
|
7987
8049
|
}
|
|
7988
8050
|
async function runCmd(rest) {
|
|
7989
|
-
let
|
|
8051
|
+
let quiet2 = false;
|
|
7990
8052
|
let outOverride;
|
|
7991
8053
|
let notifyOverride = false;
|
|
7992
8054
|
let brainOverride;
|
|
@@ -7994,7 +8056,7 @@ async function runCmd(rest) {
|
|
|
7994
8056
|
for (let i = 0; i < rest.length; i++) {
|
|
7995
8057
|
const a = rest[i];
|
|
7996
8058
|
if (a === void 0) continue;
|
|
7997
|
-
if (a === "--quiet")
|
|
8059
|
+
if (a === "--quiet") quiet2 = true;
|
|
7998
8060
|
else if (a === "--notify") notifyOverride = true;
|
|
7999
8061
|
else if (a === "--out") outOverride = rest[++i];
|
|
8000
8062
|
else if (a === "--brain") brainOverride = rest[++i];
|
|
@@ -8035,7 +8097,7 @@ async function runCmd(rest) {
|
|
|
8035
8097
|
let streamed = false;
|
|
8036
8098
|
const result = await runTask(built.prompt, {
|
|
8037
8099
|
...brainId ? { brainId } : {},
|
|
8038
|
-
onChunk:
|
|
8100
|
+
onChunk: quiet2 ? void 0 : (ch) => {
|
|
8039
8101
|
streamed = true;
|
|
8040
8102
|
process.stdout.write(ch);
|
|
8041
8103
|
}
|
|
@@ -8046,13 +8108,13 @@ async function runCmd(rest) {
|
|
|
8046
8108
|
process.exitCode = 1;
|
|
8047
8109
|
return;
|
|
8048
8110
|
}
|
|
8049
|
-
if (!
|
|
8050
|
-
if (!
|
|
8111
|
+
if (!quiet2 && !streamed) process.stdout.write(result.text);
|
|
8112
|
+
if (!quiet2 && (streamed || result.text) && !result.text.endsWith("\n")) process.stdout.write("\n");
|
|
8051
8113
|
if (outTarget) {
|
|
8052
8114
|
const target = resolve7(routine2.workspace, outTarget);
|
|
8053
8115
|
try {
|
|
8054
8116
|
writeFileSync19(target, result.text, "utf8");
|
|
8055
|
-
if (!
|
|
8117
|
+
if (!quiet2) process.stderr.write(c.dim(`saved to ${target}`) + "\n");
|
|
8056
8118
|
} catch (e) {
|
|
8057
8119
|
process.stderr.write(c.red(`Could not write ${target}: ${e.message}`) + "\n");
|
|
8058
8120
|
process.exitCode = 1;
|
|
@@ -8228,6 +8290,10 @@ function readStdin3() {
|
|
|
8228
8290
|
}
|
|
8229
8291
|
}
|
|
8230
8292
|
|
|
8293
|
+
// src/commands/doctor.ts
|
|
8294
|
+
import { spawn as spawn5, spawnSync as spawnSync7 } from "child_process";
|
|
8295
|
+
import { existsSync as existsSync20 } from "fs";
|
|
8296
|
+
|
|
8231
8297
|
// src/specs.ts
|
|
8232
8298
|
import os from "os";
|
|
8233
8299
|
var BYTES_PER_GB = 1024 ** 3;
|
|
@@ -8347,10 +8413,63 @@ function good(text) {
|
|
|
8347
8413
|
function warn(text) {
|
|
8348
8414
|
console.log(" " + c.red(text));
|
|
8349
8415
|
}
|
|
8350
|
-
async function
|
|
8416
|
+
async function ollamaTags() {
|
|
8417
|
+
const url = process.env.HOLT_OLLAMA_URL || "http://127.0.0.1:11434";
|
|
8418
|
+
try {
|
|
8419
|
+
const res = await fetch(`${url}/api/tags`, { signal: AbortSignal.timeout(1500) });
|
|
8420
|
+
if (!res.ok) return null;
|
|
8421
|
+
const data = await res.json();
|
|
8422
|
+
return (data.models ?? []).map((m) => String(m.name ?? ""));
|
|
8423
|
+
} catch {
|
|
8424
|
+
return null;
|
|
8425
|
+
}
|
|
8426
|
+
}
|
|
8427
|
+
async function fixSemanticMemory(fixed) {
|
|
8428
|
+
let tags = await ollamaTags();
|
|
8429
|
+
if (tags === null) {
|
|
8430
|
+
if (!isInstalled("ollama")) {
|
|
8431
|
+
note("Ollama is not installed, so there is nothing to start.");
|
|
8432
|
+
note(
|
|
8433
|
+
process.platform === "darwin" ? 'Install it with: brew install ollama (then run "holt doctor --fix" again)' : 'Install it from https://ollama.com/download, then run "holt doctor --fix" again.'
|
|
8434
|
+
);
|
|
8435
|
+
return false;
|
|
8436
|
+
}
|
|
8437
|
+
note("Ollama is installed but not running. Starting it...");
|
|
8438
|
+
try {
|
|
8439
|
+
const child = spawn5("ollama", ["serve"], { detached: true, stdio: "ignore" });
|
|
8440
|
+
child.unref();
|
|
8441
|
+
} catch {
|
|
8442
|
+
warn('Could not start Ollama. Start it yourself (open the Ollama app or run "ollama serve").');
|
|
8443
|
+
return false;
|
|
8444
|
+
}
|
|
8445
|
+
for (let i = 0; i < 16 && tags === null; i++) {
|
|
8446
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
8447
|
+
tags = await ollamaTags();
|
|
8448
|
+
}
|
|
8449
|
+
if (tags === null) {
|
|
8450
|
+
warn('Ollama did not come up. Open the Ollama app once, then run "holt doctor --fix" again.');
|
|
8451
|
+
return false;
|
|
8452
|
+
}
|
|
8453
|
+
fixed.push("Started the Ollama daemon.");
|
|
8454
|
+
}
|
|
8455
|
+
if (!tags.some((t) => t.startsWith(EMBED_MODEL))) {
|
|
8456
|
+
note(`The "${EMBED_MODEL}" model is missing. Downloading it (one-time, ~275 MB)...`);
|
|
8457
|
+
const r = spawnSync7("ollama", ["pull", EMBED_MODEL], { stdio: "inherit" });
|
|
8458
|
+
if (r.status !== 0) {
|
|
8459
|
+
warn(`Could not download ${EMBED_MODEL}. Check your internet connection and retry.`);
|
|
8460
|
+
return false;
|
|
8461
|
+
}
|
|
8462
|
+
fixed.push(`Downloaded the "${EMBED_MODEL}" model.`);
|
|
8463
|
+
}
|
|
8464
|
+
resetEmbedProbe();
|
|
8465
|
+
return embeddingsAvailable();
|
|
8466
|
+
}
|
|
8467
|
+
async function doctor(args = []) {
|
|
8468
|
+
const fix = args.includes("--fix");
|
|
8469
|
+
const fixed = [];
|
|
8351
8470
|
const specs = detect();
|
|
8352
8471
|
const steps = [];
|
|
8353
|
-
console.log("\n" + c.bold("Holt doctor") + c.dim("
|
|
8472
|
+
console.log("\n" + c.bold("Holt doctor") + c.dim(" \xB7 a look at this machine and how best to run Holt on it"));
|
|
8354
8473
|
header("Machine");
|
|
8355
8474
|
row("platform", `${specs.platform} / ${specs.arch}`);
|
|
8356
8475
|
row("cpu", `${specs.cpuModel}${specs.cpuCount ? ` (${specs.cpuCount} cores)` : ""}`);
|
|
@@ -8387,13 +8506,29 @@ async function doctor() {
|
|
|
8387
8506
|
} catch {
|
|
8388
8507
|
embedOk = false;
|
|
8389
8508
|
}
|
|
8509
|
+
if (!embedOk && fix) {
|
|
8510
|
+
embedOk = await fixSemanticMemory(fixed);
|
|
8511
|
+
}
|
|
8390
8512
|
if (embedOk) {
|
|
8391
8513
|
good(`Ollama reachable and "${EMBED_MODEL}" is present. Memory recall is semantic, private, and local.`);
|
|
8514
|
+
if (fix && existsSync20(wsHoltDir())) {
|
|
8515
|
+
const missing = loadTurns().filter((t) => !Array.isArray(t.emb)).length;
|
|
8516
|
+
if (missing > 0) {
|
|
8517
|
+
note(`${missing} memories in this folder lack embeddings. Upgrading them...`);
|
|
8518
|
+
const r = await backfillEmbeddings((done, total) => {
|
|
8519
|
+
process.stdout.write(`\r embedding ${done}/${total}...`);
|
|
8520
|
+
});
|
|
8521
|
+
console.log("");
|
|
8522
|
+
if (r.embedded > 0) fixed.push(`Upgraded ${r.embedded} memories to semantic recall.`);
|
|
8523
|
+
}
|
|
8524
|
+
}
|
|
8392
8525
|
} else {
|
|
8393
8526
|
note(`Ollama with "${EMBED_MODEL}" not reachable at ${OLLAMA_URL3}.`);
|
|
8394
8527
|
note("Without it, Holt still remembers, but recall falls back to keyword match.");
|
|
8395
|
-
|
|
8396
|
-
|
|
8528
|
+
if (!fix) {
|
|
8529
|
+
warn('Recommendation: run "holt doctor --fix" to set this up automatically.');
|
|
8530
|
+
steps.push("Enable semantic memory: holt doctor --fix");
|
|
8531
|
+
}
|
|
8397
8532
|
}
|
|
8398
8533
|
header("Knowledge wiki maintainer");
|
|
8399
8534
|
const rec = recommendLocalModel2(specs.totalRamGB);
|
|
@@ -8419,6 +8554,12 @@ async function doctor() {
|
|
|
8419
8554
|
}
|
|
8420
8555
|
note("Hosting tip: a low-power always-on machine (an old laptop, a Pi) is ideal for the bot and scheduled runs.");
|
|
8421
8556
|
note("Heavy local models want more RAM, so keep those on your bigger machine and let the small box relay.");
|
|
8557
|
+
if (fix) {
|
|
8558
|
+
header("Repairs");
|
|
8559
|
+
if (lastConfigRepair) fixed.push(`Moved a damaged settings file aside (${lastConfigRepair}).`);
|
|
8560
|
+
if (fixed.length === 0) good("Nothing needed repair.");
|
|
8561
|
+
else for (const f of fixed) good(f);
|
|
8562
|
+
}
|
|
8422
8563
|
header("Recommended next steps");
|
|
8423
8564
|
if (!cfg) {
|
|
8424
8565
|
steps.unshift('Set up this folder: run "holt init".');
|
|
@@ -8432,6 +8573,163 @@ async function doctor() {
|
|
|
8432
8573
|
console.log("");
|
|
8433
8574
|
}
|
|
8434
8575
|
|
|
8576
|
+
// src/commands/tour.ts
|
|
8577
|
+
function step(n, title) {
|
|
8578
|
+
console.log("\n" + c.accent(`Step ${n} of 3`) + c.bold(` ${title}`));
|
|
8579
|
+
}
|
|
8580
|
+
async function tour() {
|
|
8581
|
+
const { ask, close } = createReader();
|
|
8582
|
+
try {
|
|
8583
|
+
if (!await ensureTrusted(ask)) return;
|
|
8584
|
+
const cfg = loadConfig();
|
|
8585
|
+
if (!cfg || !cfg.defaultBrain) {
|
|
8586
|
+
console.log(c.dim('\n This folder is not set up yet. Run "holt" once, then come back for the tour.\n'));
|
|
8587
|
+
return;
|
|
8588
|
+
}
|
|
8589
|
+
console.log("\n" + c.bold("Welcome. This takes two minutes and shows you the one thing that matters."));
|
|
8590
|
+
step(1, "Ask about your files");
|
|
8591
|
+
console.log(c.dim(" Holt reads the folder it runs in. Asking it what it sees here...\n"));
|
|
8592
|
+
const r = await runTask(
|
|
8593
|
+
"Look at the files in this folder and describe, in two short plain sentences, what this folder seems to be about. If it is empty or nearly empty, say that cheerfully.",
|
|
8594
|
+
{ store: false, onChunk: (t) => process.stdout.write(t) }
|
|
8595
|
+
);
|
|
8596
|
+
if (!r.ok) {
|
|
8597
|
+
console.log(c.dim(" (Your brain is not reachable right now, so we will skip this step. The next two work offline.)"));
|
|
8598
|
+
if (r.text) console.log(c.dim(` ${r.text.trim()}`));
|
|
8599
|
+
}
|
|
8600
|
+
console.log("");
|
|
8601
|
+
step(2, "Teach Holt one thing about you");
|
|
8602
|
+
console.log(c.dim(' Type one fact you want Holt to keep. Examples: "I run a bakery in Pune",'));
|
|
8603
|
+
console.log(c.dim(' "My big project this quarter is the Delhi launch", "I hate long emails".\n'));
|
|
8604
|
+
let fact = (await ask(" Your fact: ") ?? "").trim();
|
|
8605
|
+
if (!fact) fact = "I just finished the Holt tour.";
|
|
8606
|
+
const session = newSessionId();
|
|
8607
|
+
await saveFact(fact, session);
|
|
8608
|
+
console.log(c.green("\n Kept.") + c.dim(" That went into this folder\u2019s memory, on this laptop, nowhere else."));
|
|
8609
|
+
step(3, "Watch it remember");
|
|
8610
|
+
console.log(c.dim(' Asking Holt\u2019s memory: "what do you know about me?"...\n'));
|
|
8611
|
+
const hits = await recall("what do you know about me? who am I? my work", "__tour__", 4);
|
|
8612
|
+
const shown = hits.filter((h) => h.turn.content.trim().length > 0).slice(0, 3);
|
|
8613
|
+
if (shown.length === 0) {
|
|
8614
|
+
console.log(" " + c.accent("remembered") + ` ${fact}`);
|
|
8615
|
+
} else {
|
|
8616
|
+
for (const h of shown) {
|
|
8617
|
+
console.log(" " + c.accent("remembered") + ` ${h.turn.content.slice(0, 110).replace(/\s+/g, " ")}`);
|
|
8618
|
+
}
|
|
8619
|
+
}
|
|
8620
|
+
const s = memStats();
|
|
8621
|
+
console.log(
|
|
8622
|
+
"\n" + c.bold(" Here is the point: ") + `close this terminal. Come back tomorrow, or next month, and ask again.
|
|
8623
|
+
Still there. This folder now holds ${s.turns} memor${s.turns === 1 ? "y" : "ies"}, and every chat adds more.
|
|
8624
|
+
`
|
|
8625
|
+
);
|
|
8626
|
+
console.log(c.dim(' Next: run "holt" to start a real conversation, or "holt help" to see everything it does.\n'));
|
|
8627
|
+
} finally {
|
|
8628
|
+
close();
|
|
8629
|
+
}
|
|
8630
|
+
}
|
|
8631
|
+
|
|
8632
|
+
// src/commands/reset.ts
|
|
8633
|
+
import { existsSync as existsSync21, rmSync as rmSync9 } from "fs";
|
|
8634
|
+
async function reset() {
|
|
8635
|
+
const { ask, close } = createReader();
|
|
8636
|
+
try {
|
|
8637
|
+
const dir = wsHoltDir();
|
|
8638
|
+
if (!existsSync21(dir)) {
|
|
8639
|
+
console.log(c.dim("\n Nothing to reset: Holt has no settings or memory in this folder.\n"));
|
|
8640
|
+
return;
|
|
8641
|
+
}
|
|
8642
|
+
let turns = 0;
|
|
8643
|
+
try {
|
|
8644
|
+
turns = memStats().turns;
|
|
8645
|
+
} catch {
|
|
8646
|
+
}
|
|
8647
|
+
console.log("\n" + c.bold("Reset Holt in this folder"));
|
|
8648
|
+
console.log(c.dim(` Folder: ${workspace()}`));
|
|
8649
|
+
console.log(c.dim(` This deletes Holt's settings and all ${turns} remembered moment${turns === 1 ? "" : "s"} here.`));
|
|
8650
|
+
console.log(c.dim(" Your own files are not touched. Other folders are not touched."));
|
|
8651
|
+
const a = (await ask(`
|
|
8652
|
+
Type ${c.bold("reset")} to confirm, or press enter to cancel: `) ?? "").trim().toLowerCase();
|
|
8653
|
+
if (a !== "reset") {
|
|
8654
|
+
console.log(c.dim(" Cancelled. Nothing was changed.\n"));
|
|
8655
|
+
return;
|
|
8656
|
+
}
|
|
8657
|
+
rmSync9(dir, { recursive: true, force: true });
|
|
8658
|
+
console.log(c.green("\n Done. This folder is a clean slate.") + c.dim(' Run "holt" to set it up again.\n'));
|
|
8659
|
+
} finally {
|
|
8660
|
+
close();
|
|
8661
|
+
}
|
|
8662
|
+
}
|
|
8663
|
+
|
|
8664
|
+
// src/commands/update.ts
|
|
8665
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
8666
|
+
import { realpathSync as realpathSync4 } from "fs";
|
|
8667
|
+
async function latestVersion() {
|
|
8668
|
+
try {
|
|
8669
|
+
const res = await fetch("https://registry.npmjs.org/@holt-os/holt/latest", {
|
|
8670
|
+
signal: AbortSignal.timeout(4e3)
|
|
8671
|
+
});
|
|
8672
|
+
if (!res.ok) return null;
|
|
8673
|
+
const data = await res.json();
|
|
8674
|
+
return typeof data.version === "string" ? data.version : null;
|
|
8675
|
+
} catch {
|
|
8676
|
+
return null;
|
|
8677
|
+
}
|
|
8678
|
+
}
|
|
8679
|
+
function isBrewInstall() {
|
|
8680
|
+
try {
|
|
8681
|
+
const p = realpathSync4(process.argv[1] ?? "");
|
|
8682
|
+
return p.includes("/Cellar/") || p.includes("/homebrew/") || p.includes("/linuxbrew/");
|
|
8683
|
+
} catch {
|
|
8684
|
+
return false;
|
|
8685
|
+
}
|
|
8686
|
+
}
|
|
8687
|
+
async function update() {
|
|
8688
|
+
const { ask, close } = createReader();
|
|
8689
|
+
try {
|
|
8690
|
+
console.log(c.dim(`
|
|
8691
|
+
You have holt ${VERSION}. Checking for a newer one...`));
|
|
8692
|
+
const latest = await latestVersion();
|
|
8693
|
+
if (!latest) {
|
|
8694
|
+
console.log(c.dim(" Could not reach the registry. Check your internet connection and try again.\n"));
|
|
8695
|
+
return;
|
|
8696
|
+
}
|
|
8697
|
+
if (latest === VERSION) {
|
|
8698
|
+
console.log(c.green(` You are on the latest version (${VERSION}).`) + "\n");
|
|
8699
|
+
return;
|
|
8700
|
+
}
|
|
8701
|
+
console.log(` Newer version available: ${c.bold(latest)}`);
|
|
8702
|
+
if (isBrewInstall()) {
|
|
8703
|
+
if (!await askYesNo(ask, " Update now via Homebrew? [Y/n] ", true)) {
|
|
8704
|
+
console.log(c.dim(" Later, then. The command is: brew upgrade holt\n"));
|
|
8705
|
+
return;
|
|
8706
|
+
}
|
|
8707
|
+
const r2 = spawnSync8("brew", ["upgrade", "holt"], { stdio: "inherit" });
|
|
8708
|
+
if (r2.status === 0) console.log(c.green(`
|
|
8709
|
+
Updated to ${latest}.`) + "\n");
|
|
8710
|
+
else console.log(c.dim("\n Homebrew reported a problem above. You can retry with: brew upgrade holt\n"));
|
|
8711
|
+
return;
|
|
8712
|
+
}
|
|
8713
|
+
if (!await askYesNo(ask, " Update now? [Y/n] ", true)) {
|
|
8714
|
+
console.log(c.dim(" Later, then. The command is: npm install -g @holt-os/holt\n"));
|
|
8715
|
+
return;
|
|
8716
|
+
}
|
|
8717
|
+
const r = spawnSync8("npm", ["install", "-g", "@holt-os/holt"], { stdio: "inherit" });
|
|
8718
|
+
if (r.status === 0) {
|
|
8719
|
+
console.log(c.green(`
|
|
8720
|
+
Updated to ${latest}.`) + "\n");
|
|
8721
|
+
} else {
|
|
8722
|
+
console.log(
|
|
8723
|
+
c.dim(
|
|
8724
|
+
"\n The install hit a permission problem. Two easy ways out:\n 1. Run it once with sudo: sudo npm install -g @holt-os/holt\n 2. Or install Node with nvm (https://nvm.sh) so no sudo is ever needed.\n"
|
|
8725
|
+
)
|
|
8726
|
+
);
|
|
8727
|
+
}
|
|
8728
|
+
} finally {
|
|
8729
|
+
close();
|
|
8730
|
+
}
|
|
8731
|
+
}
|
|
8732
|
+
|
|
8435
8733
|
// src/commands/write.ts
|
|
8436
8734
|
import { writeFileSync as writeFileSync20 } from "fs";
|
|
8437
8735
|
import { resolve as resolve8 } from "path";
|
|
@@ -8623,6 +8921,37 @@ async function write(args) {
|
|
|
8623
8921
|
}
|
|
8624
8922
|
}
|
|
8625
8923
|
|
|
8924
|
+
// src/crash.ts
|
|
8925
|
+
import { mkdirSync as mkdirSync18, appendFileSync as appendFileSync3 } from "fs";
|
|
8926
|
+
import { join as join20 } from "path";
|
|
8927
|
+
function handleCrash(err2) {
|
|
8928
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
8929
|
+
const detail = err2 instanceof Error ? err2.stack || err2.message : String(err2);
|
|
8930
|
+
let logPath2 = null;
|
|
8931
|
+
const entry = `[${stamp}] holt ${VERSION} argv: ${process.argv.slice(2).join(" ")}
|
|
8932
|
+
${detail}
|
|
8933
|
+
|
|
8934
|
+
`;
|
|
8935
|
+
for (const base of [wsHoltDir(), GLOBAL_DIR]) {
|
|
8936
|
+
try {
|
|
8937
|
+
const dir = join20(base, "logs");
|
|
8938
|
+
mkdirSync18(dir, { recursive: true });
|
|
8939
|
+
const p = join20(dir, `crash-${stamp.slice(0, 10)}.log`);
|
|
8940
|
+
appendFileSync3(p, entry, "utf8");
|
|
8941
|
+
logPath2 = p;
|
|
8942
|
+
break;
|
|
8943
|
+
} catch {
|
|
8944
|
+
}
|
|
8945
|
+
}
|
|
8946
|
+
if (!isQuietMode()) {
|
|
8947
|
+
console.error(c.red("\n Something went wrong, and it is not your fault."));
|
|
8948
|
+
if (logPath2) console.error(c.dim(` Details saved to: ${logPath2}`));
|
|
8949
|
+
console.error(c.dim(" Try: holt doctor --fix (it repairs the common causes)"));
|
|
8950
|
+
console.error(c.dim(" Still stuck? https://github.com/holt-os/holt/issues\n"));
|
|
8951
|
+
}
|
|
8952
|
+
process.exit(1);
|
|
8953
|
+
}
|
|
8954
|
+
|
|
8626
8955
|
// src/cli.ts
|
|
8627
8956
|
var BANNER = `
|
|
8628
8957
|
\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
|
|
@@ -8636,40 +8965,53 @@ var BANNER = `
|
|
|
8636
8965
|
var HELP = `${BANNER}
|
|
8637
8966
|
Usage: holt <command>
|
|
8638
8967
|
|
|
8639
|
-
|
|
8640
|
-
(no command) Start your assistant: sets up if needed, then launches
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
telegram Chat with Holt from your phone: holt telegram [setup]
|
|
8649
|
-
notify [msg] Push a message to your phone over Telegram (stdin-friendly)
|
|
8650
|
-
voice Teach Holt how you write: holt voice [add <file> | show | edit | clear]
|
|
8968
|
+
Start here:
|
|
8969
|
+
(no command) Start your assistant: sets up if needed, then launches your
|
|
8970
|
+
brain (Claude Code, Codex, Gemini), branded as Holt
|
|
8971
|
+
tour A 2-minute guided first run: ask, teach it one fact, watch it remember
|
|
8972
|
+
doctor Health check for this machine
|
|
8973
|
+
("holt doctor --fix" also repairs the common problems for you)
|
|
8974
|
+
|
|
8975
|
+
Every day:
|
|
8976
|
+
run <task> Do one task and remember it: holt run "draft my status update"
|
|
8651
8977
|
write <what> Draft content in your voice, with anti-AI checks: holt write "..." [--type]
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8978
|
+
voice Teach Holt how you write: holt voice [add <file> | show | edit | clear]
|
|
8979
|
+
memory What Holt knows here: holt memory [search <q> | facts | embed | clear]
|
|
8980
|
+
wiki A tidy, linked notebook Holt keeps from your memory: holt wiki [sync | show]
|
|
8981
|
+
graph See everything Holt knows as a visual map in your browser
|
|
8982
|
+
skill Reusable how-tos you can share: holt skill [list | show | create | add]
|
|
8983
|
+
|
|
8984
|
+
From your phone, while you sleep:
|
|
8985
|
+
telegram Chat with Holt from your phone: holt telegram setup
|
|
8986
|
+
notify [msg] Send yourself a phone message from any script (stdin-friendly)
|
|
8987
|
+
schedule Run a task on a timer: holt schedule [add | list | remove]
|
|
8988
|
+
routine Named, reusable, scheduled jobs: holt routine [add | run | list | remove]
|
|
8989
|
+
|
|
8990
|
+
Housekeeping:
|
|
8991
|
+
setting Change brains, API connections, and your launch word (per folder)
|
|
8660
8992
|
login <brain> Sign in to a brain: claude, codex, or gemini
|
|
8661
|
-
|
|
8662
|
-
|
|
8993
|
+
update Get the latest Holt, in one word
|
|
8994
|
+
reset Start this folder over (asks first; your own files are untouched)
|
|
8995
|
+
hook Ambient memory inside plain Claude Code: holt hook [install | status]
|
|
8996
|
+
mcp Share Holt's memory with other AI apps (Claude Code, Cursor, Codex)
|
|
8997
|
+
chat Lightweight built-in chat, used for API brains
|
|
8998
|
+
init Full setup by hand (bare "holt" runs this for you when needed)
|
|
8999
|
+
launch Same as bare "holt": start your assistant
|
|
9000
|
+
version | help Print the version / show this help
|
|
8663
9001
|
|
|
8664
9002
|
Holt runs in the folder you launch it from and asks to trust it first.
|
|
8665
|
-
|
|
8666
|
-
API
|
|
9003
|
+
A "brain" is the AI doing the thinking: an agent CLI on your machine
|
|
9004
|
+
(claude, codex, gemini) or a direct API connection you add in settings.
|
|
8667
9005
|
|
|
8668
9006
|
Docs: https://productsdecoded.com/holt
|
|
8669
9007
|
Repo: https://github.com/holt-os/holt
|
|
8670
9008
|
`;
|
|
8671
9009
|
async function main() {
|
|
8672
9010
|
const cmd = process.argv[2];
|
|
9011
|
+
const sub = process.argv[3];
|
|
9012
|
+
if (cmd === "statusline" || cmd === "mcp" || cmd === "notify" || cmd === "hook" && (sub === "inject" || sub === "capture")) {
|
|
9013
|
+
setQuietMode(true);
|
|
9014
|
+
}
|
|
8673
9015
|
switch (cmd) {
|
|
8674
9016
|
case void 0:
|
|
8675
9017
|
await launch();
|
|
@@ -8738,7 +9080,16 @@ async function main() {
|
|
|
8738
9080
|
await notify(process.argv.slice(3));
|
|
8739
9081
|
break;
|
|
8740
9082
|
case "doctor":
|
|
8741
|
-
await doctor();
|
|
9083
|
+
await doctor(process.argv.slice(3));
|
|
9084
|
+
break;
|
|
9085
|
+
case "tour":
|
|
9086
|
+
await tour();
|
|
9087
|
+
break;
|
|
9088
|
+
case "reset":
|
|
9089
|
+
await reset();
|
|
9090
|
+
break;
|
|
9091
|
+
case "update":
|
|
9092
|
+
await update();
|
|
8742
9093
|
break;
|
|
8743
9094
|
case "voice":
|
|
8744
9095
|
await voice(process.argv[3], process.argv.slice(4));
|
|
@@ -8754,4 +9105,6 @@ async function main() {
|
|
|
8754
9105
|
process.exitCode = 1;
|
|
8755
9106
|
}
|
|
8756
9107
|
}
|
|
8757
|
-
|
|
9108
|
+
process.on("uncaughtException", handleCrash);
|
|
9109
|
+
process.on("unhandledRejection", handleCrash);
|
|
9110
|
+
main().catch(handleCrash);
|