@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.
Files changed (3) hide show
  1. package/README.md +14 -1
  2. package/dist/cli.js +424 -71
  3. 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
- const ans = (await ask(" Trust and continue? [y/N] ") ?? "").trim().toLowerCase();
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
- codex: { label: "Codex (OpenAI)", command: "codex", args: ["exec"] },
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
- const cfg = JSON.parse(readFileSync2(path, "utf8"));
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(provider, json) {
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
- if (brain.provider === "anthropic") {
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 (brain.provider === "openai") {
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: "https://api.openai.com/v1/chat/completions",
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 mode = (await ask("\n Auto-detect from a message you just sent? [Y/n] ") ?? "").trim().toLowerCase();
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 (mode === "" || mode === "y" || mode === "yes") {
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
- const a = (await ask(" Store a short excerpt so Holt can match your tone directly? [y/N] ") ?? "").trim().toLowerCase();
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
- const a = (await ask(` ${BRAIN_DEFS[id].label} is not installed. Sign in after install? [Y/n] `) ?? "").trim().toLowerCase();
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
- const apiAns = (await ask("\nAlso connect a direct API brain (raw key, no CLI needed)? [y/N] ") ?? "").trim().toLowerCase();
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
- const a = (await ask("\n" + q) ?? "").trim().toLowerCase();
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
- const tgAns = (await ask("\nChat with Holt from your phone over Telegram? [y/N] ") ?? "").trim().toLowerCase();
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 voiceAns = (await voiceReader.ask("Want Holt to learn how you write, so it can draft in your voice? [y/N] ") ?? "").trim().toLowerCase();
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 (voiceAns === "y" || voiceAns === "yes") {
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 a = (await ask(c.dim("No Holt setup in this folder. Set it up now? [Y/n] ")) ?? "").trim().toLowerCase();
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 (a === "n" || a === "no") {
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
- const ans = (await ask(c.dim(` Allow this session to access ${dir} (outside this folder)? [y/N] `)) ?? "").trim().toLowerCase();
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.14.3";
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 quiet = false;
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
- quiet = true;
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 quiet = false;
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") quiet = true;
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: quiet ? void 0 : (ch) => {
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 (!quiet && !streamed) process.stdout.write(result.text);
8050
- if (!quiet && (streamed || result.text) && !result.text.endsWith("\n")) process.stdout.write("\n");
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 (!quiet) process.stderr.write(c.dim(`saved to ${target}`) + "\n");
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 doctor() {
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(" a look at this machine and how best to run Holt on it"));
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
- warn(`Recommendation: enable local embeddings. Install Ollama, then: ollama pull ${EMBED_MODEL}`);
8396
- steps.push(`Enable semantic memory: ollama pull ${EMBED_MODEL}`);
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
- Commands:
8640
- (no command) Start your assistant: sets up if needed, then launches the real
8641
- interactive brain (Claude Code, Codex, Gemini), branded as Holt
8642
- init Trust this folder, choose and install brains, sign in, set defaults
8643
- launch Same as bare "holt": start your assistant
8644
- chat Lightweight REPL that remembers past sessions (used for API brains)
8645
- run <task> Run one task non-interactively: recall, brain executes, remember
8646
- schedule Fire "holt run" on a timer: holt schedule [add | list | remove]
8647
- routine Named, reusable, scheduled jobs: holt routine [add | run | list | remove]
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
- memory Inspect memory: holt memory [search <query> | embed | clear]
8653
- wiki Your derived knowledge wiki: holt wiki [sync | rebuild | lint | list | show]
8654
- graph See your memory as an interactive knowledge graph in the browser
8655
- skill Manage skills: holt skill [list | show | create | add | remove]
8656
- doctor Check this machine and recommend how best to run Holt here
8657
- mcp Serve this folder's memory to Claude Code, Cursor, Codex (holt mcp setup)
8658
- hook Ambient memory for Claude Code: holt hook [install | remove | status]
8659
- setting Configure brains, API brains, and your launch command (per folder)
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
- version Print the Holt version
8662
- help Show this help (bare "holt" starts your assistant, not this)
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
- Brains are agent CLIs on your machine (claude, codex, gemini) or direct
8666
- API connections you add in settings.
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
- main();
9108
+ process.on("uncaughtException", handleCrash);
9109
+ process.on("unhandledRejection", handleCrash);
9110
+ main().catch(handleCrash);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holt-os/holt",
3
- "version": "0.14.3",
3
+ "version": "0.15.1",
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",