@holt-os/holt 0.14.3 → 0.15.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.
Files changed (3) hide show
  1. package/README.md +6 -1
  2. package/dist/cli.js +401 -51
  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
 
@@ -556,6 +556,11 @@ holt routine named, reusable, scheduled jobs: add | run | list | show |
556
556
  holt telegram chat with Holt from your phone: telegram [setup]
557
557
  holt notify [msg] push a message to your phone over Telegram (stdin-friendly)
558
558
  holt doctor check this machine and recommend how best to run Holt here
559
+ ("holt doctor --fix" also repairs the common problems it finds:
560
+ starts Ollama, pulls the embed model, mends a damaged config)
561
+ holt tour a 2-minute guided first run: ask, teach one fact, watch it remember
562
+ holt update get the latest Holt in one word (npm or Homebrew, auto-detected)
563
+ holt reset start this folder over (typed confirmation; your files untouched)
559
564
  holt voice teach Holt your writing voice: add <file> | show | edit | clear
560
565
  holt write <what> draft content in your voice with anti-AI checks (--type, --out, --fast)
561
566
  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 = {
@@ -162,10 +169,24 @@ function ensureWsDir() {
162
169
  }
163
170
 
164
171
  // src/config.ts
172
+ var PROVIDER_WIRE = {
173
+ anthropic: "anthropic",
174
+ openai: "openai",
175
+ gemini: "gemini",
176
+ kimi: "openai",
177
+ grok: "openai"
178
+ };
179
+ var PROVIDER_BASE_URL = {
180
+ openai: "https://api.openai.com/v1",
181
+ kimi: "https://api.moonshot.ai/v1",
182
+ grok: "https://api.x.ai/v1"
183
+ };
165
184
  var WIKI_DEFAULT_LOCAL_MODEL = "qwen2.5:7b";
166
185
  var BRAIN_DEFS = {
167
186
  claude: { label: "Claude Code", command: "claude", args: ["-p"] },
168
- codex: { label: "Codex (OpenAI)", command: "codex", args: ["exec"] },
187
+ // --skip-git-repo-check: codex exec otherwise refuses to run in folders that
188
+ // are not git repositories, which is nearly every non-developer folder.
189
+ codex: { label: "Codex (OpenAI)", command: "codex", args: ["exec", "--skip-git-repo-check"] },
169
190
  gemini: { label: "Gemini CLI", command: "gemini", args: ["-p"] }
170
191
  };
171
192
  var BRAIN_SETUP = {
@@ -174,16 +195,20 @@ var BRAIN_SETUP = {
174
195
  gemini: { install: ["npm", "install", "-g", "@google/gemini-cli"], login: ["gemini"] }
175
196
  };
176
197
  var BRAIN_IDS = ["claude", "codex", "gemini"];
177
- var PROVIDERS = ["anthropic", "openai", "gemini"];
198
+ var PROVIDERS = ["anthropic", "openai", "gemini", "kimi", "grok"];
178
199
  var PROVIDER_MODEL_SUGGESTION = {
179
200
  anthropic: "claude-sonnet-4-6",
180
201
  openai: "gpt-5",
181
- gemini: "gemini-2.5-flash"
202
+ gemini: "gemini-2.5-flash",
203
+ kimi: "kimi-k2-turbo-preview",
204
+ grok: "grok-4"
182
205
  };
183
206
  var PROVIDER_ENV = {
184
207
  anthropic: "ANTHROPIC_API_KEY",
185
208
  openai: "OPENAI_API_KEY",
186
- gemini: "GEMINI_API_KEY"
209
+ gemini: "GEMINI_API_KEY",
210
+ kimi: "MOONSHOT_API_KEY",
211
+ grok: "XAI_API_KEY"
187
212
  };
188
213
  function defaultConfig() {
189
214
  const brains = {};
@@ -201,14 +226,25 @@ function defaultConfig() {
201
226
  wiki: { maintainer: "brain", localModel: WIKI_DEFAULT_LOCAL_MODEL }
202
227
  };
203
228
  }
229
+ var lastConfigRepair = null;
204
230
  function loadConfig() {
205
231
  const path = wsConfigPath();
206
232
  if (!existsSync2(path)) return null;
233
+ let raw;
234
+ try {
235
+ raw = readFileSync2(path, "utf8");
236
+ } catch {
237
+ return null;
238
+ }
207
239
  try {
208
- const cfg = JSON.parse(readFileSync2(path, "utf8"));
240
+ const cfg = JSON.parse(raw);
209
241
  const base = defaultConfig();
210
242
  const brains = cfg.brains ?? {};
211
243
  for (const id of BRAIN_IDS) if (!brains[id]) brains[id] = base.brains[id];
244
+ const cx = brains.codex;
245
+ if (cx && cx.command === "codex" && Array.isArray(cx.args) && !cx.args.includes("--skip-git-repo-check")) {
246
+ cx.args = [...cx.args, "--skip-git-repo-check"];
247
+ }
212
248
  const wikiIn = cfg.wiki ?? {};
213
249
  const wiki = {
214
250
  maintainer: wikiIn.maintainer === "local" ? "local" : "brain",
@@ -227,6 +263,18 @@ function loadConfig() {
227
263
  wiki
228
264
  };
229
265
  } catch {
266
+ const backup2 = `${path}.broken-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[:T]/g, "")}`;
267
+ try {
268
+ renameSync(path, backup2);
269
+ lastConfigRepair = backup2;
270
+ if (!isQuietMode()) {
271
+ console.error(
272
+ c.red("\n Your Holt settings file was damaged, so it was moved aside.") + c.dim(`
273
+ Saved at: ${backup2}`) + c.dim("\n Holt will set this folder up fresh; your memory is untouched.\n")
274
+ );
275
+ }
276
+ } catch {
277
+ }
230
278
  return null;
231
279
  }
232
280
  }
@@ -310,7 +358,7 @@ function renderPrompt(history, message, memory = []) {
310
358
  function looksLikeAuthError(text) {
311
359
  try {
312
360
  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;
361
+ 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
362
  return re.test(text);
315
363
  } catch {
316
364
  return false;
@@ -967,7 +1015,8 @@ function dataLines(event) {
967
1015
  }
968
1016
  return out;
969
1017
  }
970
- function extractDelta(provider, json) {
1018
+ function extractDelta(providerIn, json) {
1019
+ const provider = PROVIDER_WIRE[providerIn];
971
1020
  const j = json;
972
1021
  if (provider === "anthropic") {
973
1022
  if (j["type"] === "content_block_delta") {
@@ -987,7 +1036,8 @@ function extractDelta(provider, json) {
987
1036
  return parts.map((p) => typeof p.text === "string" ? p.text : "").join("");
988
1037
  }
989
1038
  function buildRequest(brain, key, prompt) {
990
- if (brain.provider === "anthropic") {
1039
+ const wire = PROVIDER_WIRE[brain.provider];
1040
+ if (wire === "anthropic") {
991
1041
  return {
992
1042
  url: "https://api.anthropic.com/v1/messages",
993
1043
  headers: {
@@ -1003,9 +1053,10 @@ function buildRequest(brain, key, prompt) {
1003
1053
  })
1004
1054
  };
1005
1055
  }
1006
- if (brain.provider === "openai") {
1056
+ if (wire === "openai") {
1057
+ const base = (brain.baseUrl ?? PROVIDER_BASE_URL[brain.provider] ?? "https://api.openai.com/v1").replace(/\/$/, "");
1007
1058
  return {
1008
- url: "https://api.openai.com/v1/chat/completions",
1059
+ url: `${base}/chat/completions`,
1009
1060
  headers: {
1010
1061
  "content-type": "application/json",
1011
1062
  authorization: `Bearer ${key}`
@@ -4210,6 +4261,9 @@ async function chat() {
4210
4261
  if (embedOk && stats.withEmbeddings < stats.turns) {
4211
4262
  console.log(c.dim(` ${stats.turns - stats.withEmbeddings} older moments lack embeddings. Run "holt memory embed" to upgrade them.`));
4212
4263
  }
4264
+ if (!embedOk) {
4265
+ console.log(c.dim(' Recall works, but semantic memory is off. "holt doctor --fix" turns it on.'));
4266
+ }
4213
4267
  console.log(c.dim("Type a message. Commands: /brain /memory /output /save /allow /setting /clear /help /exit"));
4214
4268
  showStatus();
4215
4269
  while (true) {
@@ -4688,6 +4742,9 @@ async function memoryCmd(sub, rest = []) {
4688
4742
  }
4689
4743
  const hits = await recall(q, "__none__", 8);
4690
4744
  console.log("");
4745
+ if (!await embeddingsAvailable()) {
4746
+ console.log(c.dim(' (memory is in keyword mode; run "holt doctor --fix" to turn semantic recall back on)'));
4747
+ }
4691
4748
  if (hits.length === 0) console.log(c.dim(" Nothing relevant found."));
4692
4749
  else for (const h of hits) {
4693
4750
  const when = new Date(h.turn.ts).toISOString().slice(0, 10);
@@ -7327,7 +7384,7 @@ function holtTools() {
7327
7384
  }
7328
7385
 
7329
7386
  // src/version.ts
7330
- var VERSION = "0.14.3";
7387
+ var VERSION = "0.15.0";
7331
7388
 
7332
7389
  // src/commands/mcp.ts
7333
7390
  function printSetup() {
@@ -7471,7 +7528,7 @@ function parseArgs2(args) {
7471
7528
  let out;
7472
7529
  let noStore = false;
7473
7530
  let noRecall = false;
7474
- let quiet = false;
7531
+ let quiet2 = false;
7475
7532
  for (let i = 0; i < args.length; i++) {
7476
7533
  const a = args[i];
7477
7534
  if (a === "--brain") {
@@ -7491,12 +7548,12 @@ function parseArgs2(args) {
7491
7548
  continue;
7492
7549
  }
7493
7550
  if (a === "--quiet") {
7494
- quiet = true;
7551
+ quiet2 = true;
7495
7552
  continue;
7496
7553
  }
7497
7554
  words2.push(a);
7498
7555
  }
7499
- return { task: words2.join(" ").trim(), brainId, out, noStore, noRecall, quiet };
7556
+ return { task: words2.join(" ").trim(), brainId, out, noStore, noRecall, quiet: quiet2 };
7500
7557
  }
7501
7558
  async function run(args) {
7502
7559
  const p = parseArgs2(args);
@@ -7986,7 +8043,7 @@ function buildRoutinePrompt(r) {
7986
8043
  return { prompt };
7987
8044
  }
7988
8045
  async function runCmd(rest) {
7989
- let quiet = false;
8046
+ let quiet2 = false;
7990
8047
  let outOverride;
7991
8048
  let notifyOverride = false;
7992
8049
  let brainOverride;
@@ -7994,7 +8051,7 @@ async function runCmd(rest) {
7994
8051
  for (let i = 0; i < rest.length; i++) {
7995
8052
  const a = rest[i];
7996
8053
  if (a === void 0) continue;
7997
- if (a === "--quiet") quiet = true;
8054
+ if (a === "--quiet") quiet2 = true;
7998
8055
  else if (a === "--notify") notifyOverride = true;
7999
8056
  else if (a === "--out") outOverride = rest[++i];
8000
8057
  else if (a === "--brain") brainOverride = rest[++i];
@@ -8035,7 +8092,7 @@ async function runCmd(rest) {
8035
8092
  let streamed = false;
8036
8093
  const result = await runTask(built.prompt, {
8037
8094
  ...brainId ? { brainId } : {},
8038
- onChunk: quiet ? void 0 : (ch) => {
8095
+ onChunk: quiet2 ? void 0 : (ch) => {
8039
8096
  streamed = true;
8040
8097
  process.stdout.write(ch);
8041
8098
  }
@@ -8046,13 +8103,13 @@ async function runCmd(rest) {
8046
8103
  process.exitCode = 1;
8047
8104
  return;
8048
8105
  }
8049
- if (!quiet && !streamed) process.stdout.write(result.text);
8050
- if (!quiet && (streamed || result.text) && !result.text.endsWith("\n")) process.stdout.write("\n");
8106
+ if (!quiet2 && !streamed) process.stdout.write(result.text);
8107
+ if (!quiet2 && (streamed || result.text) && !result.text.endsWith("\n")) process.stdout.write("\n");
8051
8108
  if (outTarget) {
8052
8109
  const target = resolve7(routine2.workspace, outTarget);
8053
8110
  try {
8054
8111
  writeFileSync19(target, result.text, "utf8");
8055
- if (!quiet) process.stderr.write(c.dim(`saved to ${target}`) + "\n");
8112
+ if (!quiet2) process.stderr.write(c.dim(`saved to ${target}`) + "\n");
8056
8113
  } catch (e) {
8057
8114
  process.stderr.write(c.red(`Could not write ${target}: ${e.message}`) + "\n");
8058
8115
  process.exitCode = 1;
@@ -8228,6 +8285,10 @@ function readStdin3() {
8228
8285
  }
8229
8286
  }
8230
8287
 
8288
+ // src/commands/doctor.ts
8289
+ import { spawn as spawn5, spawnSync as spawnSync7 } from "child_process";
8290
+ import { existsSync as existsSync20 } from "fs";
8291
+
8231
8292
  // src/specs.ts
8232
8293
  import os from "os";
8233
8294
  var BYTES_PER_GB = 1024 ** 3;
@@ -8347,10 +8408,63 @@ function good(text) {
8347
8408
  function warn(text) {
8348
8409
  console.log(" " + c.red(text));
8349
8410
  }
8350
- async function doctor() {
8411
+ async function ollamaTags() {
8412
+ const url = process.env.HOLT_OLLAMA_URL || "http://127.0.0.1:11434";
8413
+ try {
8414
+ const res = await fetch(`${url}/api/tags`, { signal: AbortSignal.timeout(1500) });
8415
+ if (!res.ok) return null;
8416
+ const data = await res.json();
8417
+ return (data.models ?? []).map((m) => String(m.name ?? ""));
8418
+ } catch {
8419
+ return null;
8420
+ }
8421
+ }
8422
+ async function fixSemanticMemory(fixed) {
8423
+ let tags = await ollamaTags();
8424
+ if (tags === null) {
8425
+ if (!isInstalled("ollama")) {
8426
+ note("Ollama is not installed, so there is nothing to start.");
8427
+ note(
8428
+ 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.'
8429
+ );
8430
+ return false;
8431
+ }
8432
+ note("Ollama is installed but not running. Starting it...");
8433
+ try {
8434
+ const child = spawn5("ollama", ["serve"], { detached: true, stdio: "ignore" });
8435
+ child.unref();
8436
+ } catch {
8437
+ warn('Could not start Ollama. Start it yourself (open the Ollama app or run "ollama serve").');
8438
+ return false;
8439
+ }
8440
+ for (let i = 0; i < 16 && tags === null; i++) {
8441
+ await new Promise((r) => setTimeout(r, 500));
8442
+ tags = await ollamaTags();
8443
+ }
8444
+ if (tags === null) {
8445
+ warn('Ollama did not come up. Open the Ollama app once, then run "holt doctor --fix" again.');
8446
+ return false;
8447
+ }
8448
+ fixed.push("Started the Ollama daemon.");
8449
+ }
8450
+ if (!tags.some((t) => t.startsWith(EMBED_MODEL))) {
8451
+ note(`The "${EMBED_MODEL}" model is missing. Downloading it (one-time, ~275 MB)...`);
8452
+ const r = spawnSync7("ollama", ["pull", EMBED_MODEL], { stdio: "inherit" });
8453
+ if (r.status !== 0) {
8454
+ warn(`Could not download ${EMBED_MODEL}. Check your internet connection and retry.`);
8455
+ return false;
8456
+ }
8457
+ fixed.push(`Downloaded the "${EMBED_MODEL}" model.`);
8458
+ }
8459
+ resetEmbedProbe();
8460
+ return embeddingsAvailable();
8461
+ }
8462
+ async function doctor(args = []) {
8463
+ const fix = args.includes("--fix");
8464
+ const fixed = [];
8351
8465
  const specs = detect();
8352
8466
  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"));
8467
+ console.log("\n" + c.bold("Holt doctor") + c.dim(" \xB7 a look at this machine and how best to run Holt on it"));
8354
8468
  header("Machine");
8355
8469
  row("platform", `${specs.platform} / ${specs.arch}`);
8356
8470
  row("cpu", `${specs.cpuModel}${specs.cpuCount ? ` (${specs.cpuCount} cores)` : ""}`);
@@ -8387,13 +8501,29 @@ async function doctor() {
8387
8501
  } catch {
8388
8502
  embedOk = false;
8389
8503
  }
8504
+ if (!embedOk && fix) {
8505
+ embedOk = await fixSemanticMemory(fixed);
8506
+ }
8390
8507
  if (embedOk) {
8391
8508
  good(`Ollama reachable and "${EMBED_MODEL}" is present. Memory recall is semantic, private, and local.`);
8509
+ if (fix && existsSync20(wsHoltDir())) {
8510
+ const missing = loadTurns().filter((t) => !Array.isArray(t.emb)).length;
8511
+ if (missing > 0) {
8512
+ note(`${missing} memories in this folder lack embeddings. Upgrading them...`);
8513
+ const r = await backfillEmbeddings((done, total) => {
8514
+ process.stdout.write(`\r embedding ${done}/${total}...`);
8515
+ });
8516
+ console.log("");
8517
+ if (r.embedded > 0) fixed.push(`Upgraded ${r.embedded} memories to semantic recall.`);
8518
+ }
8519
+ }
8392
8520
  } else {
8393
8521
  note(`Ollama with "${EMBED_MODEL}" not reachable at ${OLLAMA_URL3}.`);
8394
8522
  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}`);
8523
+ if (!fix) {
8524
+ warn('Recommendation: run "holt doctor --fix" to set this up automatically.');
8525
+ steps.push("Enable semantic memory: holt doctor --fix");
8526
+ }
8397
8527
  }
8398
8528
  header("Knowledge wiki maintainer");
8399
8529
  const rec = recommendLocalModel2(specs.totalRamGB);
@@ -8419,6 +8549,12 @@ async function doctor() {
8419
8549
  }
8420
8550
  note("Hosting tip: a low-power always-on machine (an old laptop, a Pi) is ideal for the bot and scheduled runs.");
8421
8551
  note("Heavy local models want more RAM, so keep those on your bigger machine and let the small box relay.");
8552
+ if (fix) {
8553
+ header("Repairs");
8554
+ if (lastConfigRepair) fixed.push(`Moved a damaged settings file aside (${lastConfigRepair}).`);
8555
+ if (fixed.length === 0) good("Nothing needed repair.");
8556
+ else for (const f of fixed) good(f);
8557
+ }
8422
8558
  header("Recommended next steps");
8423
8559
  if (!cfg) {
8424
8560
  steps.unshift('Set up this folder: run "holt init".');
@@ -8432,6 +8568,165 @@ async function doctor() {
8432
8568
  console.log("");
8433
8569
  }
8434
8570
 
8571
+ // src/commands/tour.ts
8572
+ function step(n, title) {
8573
+ console.log("\n" + c.accent(`Step ${n} of 3`) + c.bold(` ${title}`));
8574
+ }
8575
+ async function tour() {
8576
+ const { ask, close } = createReader();
8577
+ try {
8578
+ if (!await ensureTrusted(ask)) return;
8579
+ const cfg = loadConfig();
8580
+ if (!cfg || !cfg.defaultBrain) {
8581
+ console.log(c.dim('\n This folder is not set up yet. Run "holt" once, then come back for the tour.\n'));
8582
+ return;
8583
+ }
8584
+ console.log("\n" + c.bold("Welcome. This takes two minutes and shows you the one thing that matters."));
8585
+ step(1, "Ask about your files");
8586
+ console.log(c.dim(" Holt reads the folder it runs in. Asking it what it sees here...\n"));
8587
+ const r = await runTask(
8588
+ "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.",
8589
+ { store: false, onChunk: (t) => process.stdout.write(t) }
8590
+ );
8591
+ if (!r.ok) {
8592
+ console.log(c.dim(" (Your brain is not reachable right now, so we will skip this step. The next two work offline.)"));
8593
+ if (r.text) console.log(c.dim(` ${r.text.trim()}`));
8594
+ }
8595
+ console.log("");
8596
+ step(2, "Teach Holt one thing about you");
8597
+ console.log(c.dim(' Type one fact you want Holt to keep. Examples: "I run a bakery in Pune",'));
8598
+ console.log(c.dim(' "My big project this quarter is the Delhi launch", "I hate long emails".\n'));
8599
+ let fact = (await ask(" Your fact: ") ?? "").trim();
8600
+ if (!fact) fact = "I just finished the Holt tour.";
8601
+ const session = newSessionId();
8602
+ await saveFact(fact, session);
8603
+ console.log(c.green("\n Kept.") + c.dim(" That went into this folder\u2019s memory, on this laptop, nowhere else."));
8604
+ step(3, "Watch it remember");
8605
+ console.log(c.dim(' Asking Holt\u2019s memory: "what do you know about me?"...\n'));
8606
+ const hits = await recall("what do you know about me? who am I? my work", "__tour__", 4);
8607
+ const shown = hits.filter((h) => h.turn.content.trim().length > 0).slice(0, 3);
8608
+ if (shown.length === 0) {
8609
+ console.log(" " + c.accent("remembered") + ` ${fact}`);
8610
+ } else {
8611
+ for (const h of shown) {
8612
+ console.log(" " + c.accent("remembered") + ` ${h.turn.content.slice(0, 110).replace(/\s+/g, " ")}`);
8613
+ }
8614
+ }
8615
+ const s = memStats();
8616
+ console.log(
8617
+ "\n" + c.bold(" Here is the point: ") + `close this terminal. Come back tomorrow, or next month, and ask again.
8618
+ Still there. This folder now holds ${s.turns} memor${s.turns === 1 ? "y" : "ies"}, and every chat adds more.
8619
+ `
8620
+ );
8621
+ console.log(c.dim(' Next: run "holt" to start a real conversation, or "holt help" to see everything it does.\n'));
8622
+ } finally {
8623
+ close();
8624
+ }
8625
+ }
8626
+
8627
+ // src/commands/reset.ts
8628
+ import { existsSync as existsSync21, rmSync as rmSync9 } from "fs";
8629
+ async function reset() {
8630
+ const { ask, close } = createReader();
8631
+ try {
8632
+ const dir = wsHoltDir();
8633
+ if (!existsSync21(dir)) {
8634
+ console.log(c.dim("\n Nothing to reset: Holt has no settings or memory in this folder.\n"));
8635
+ return;
8636
+ }
8637
+ let turns = 0;
8638
+ try {
8639
+ turns = memStats().turns;
8640
+ } catch {
8641
+ }
8642
+ console.log("\n" + c.bold("Reset Holt in this folder"));
8643
+ console.log(c.dim(` Folder: ${workspace()}`));
8644
+ console.log(c.dim(` This deletes Holt's settings and all ${turns} remembered moment${turns === 1 ? "" : "s"} here.`));
8645
+ console.log(c.dim(" Your own files are not touched. Other folders are not touched."));
8646
+ const a = (await ask(`
8647
+ Type ${c.bold("reset")} to confirm, or press enter to cancel: `) ?? "").trim().toLowerCase();
8648
+ if (a !== "reset") {
8649
+ console.log(c.dim(" Cancelled. Nothing was changed.\n"));
8650
+ return;
8651
+ }
8652
+ rmSync9(dir, { recursive: true, force: true });
8653
+ console.log(c.green("\n Done. This folder is a clean slate.") + c.dim(' Run "holt" to set it up again.\n'));
8654
+ } finally {
8655
+ close();
8656
+ }
8657
+ }
8658
+
8659
+ // src/commands/update.ts
8660
+ import { spawnSync as spawnSync8 } from "child_process";
8661
+ import { realpathSync as realpathSync4 } from "fs";
8662
+ async function latestVersion() {
8663
+ try {
8664
+ const res = await fetch("https://registry.npmjs.org/@holt-os/holt/latest", {
8665
+ signal: AbortSignal.timeout(4e3)
8666
+ });
8667
+ if (!res.ok) return null;
8668
+ const data = await res.json();
8669
+ return typeof data.version === "string" ? data.version : null;
8670
+ } catch {
8671
+ return null;
8672
+ }
8673
+ }
8674
+ function isBrewInstall() {
8675
+ try {
8676
+ const p = realpathSync4(process.argv[1] ?? "");
8677
+ return p.includes("/Cellar/") || p.includes("/homebrew/") || p.includes("/linuxbrew/");
8678
+ } catch {
8679
+ return false;
8680
+ }
8681
+ }
8682
+ async function update() {
8683
+ const { ask, close } = createReader();
8684
+ try {
8685
+ console.log(c.dim(`
8686
+ You have holt ${VERSION}. Checking for a newer one...`));
8687
+ const latest = await latestVersion();
8688
+ if (!latest) {
8689
+ console.log(c.dim(" Could not reach the registry. Check your internet connection and try again.\n"));
8690
+ return;
8691
+ }
8692
+ if (latest === VERSION) {
8693
+ console.log(c.green(` You are on the latest version (${VERSION}).`) + "\n");
8694
+ return;
8695
+ }
8696
+ console.log(` Newer version available: ${c.bold(latest)}`);
8697
+ if (isBrewInstall()) {
8698
+ const a2 = (await ask(" Update now via Homebrew? [Y/n] ") ?? "").trim().toLowerCase();
8699
+ if (a2 === "n" || a2 === "no") {
8700
+ console.log(c.dim(" Later, then. The command is: brew upgrade holt\n"));
8701
+ return;
8702
+ }
8703
+ const r2 = spawnSync8("brew", ["upgrade", "holt"], { stdio: "inherit" });
8704
+ if (r2.status === 0) console.log(c.green(`
8705
+ Updated to ${latest}.`) + "\n");
8706
+ else console.log(c.dim("\n Homebrew reported a problem above. You can retry with: brew upgrade holt\n"));
8707
+ return;
8708
+ }
8709
+ const a = (await ask(" Update now? [Y/n] ") ?? "").trim().toLowerCase();
8710
+ if (a === "n" || a === "no") {
8711
+ console.log(c.dim(" Later, then. The command is: npm install -g @holt-os/holt\n"));
8712
+ return;
8713
+ }
8714
+ const r = spawnSync8("npm", ["install", "-g", "@holt-os/holt"], { stdio: "inherit" });
8715
+ if (r.status === 0) {
8716
+ console.log(c.green(`
8717
+ Updated to ${latest}.`) + "\n");
8718
+ } else {
8719
+ console.log(
8720
+ c.dim(
8721
+ "\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"
8722
+ )
8723
+ );
8724
+ }
8725
+ } finally {
8726
+ close();
8727
+ }
8728
+ }
8729
+
8435
8730
  // src/commands/write.ts
8436
8731
  import { writeFileSync as writeFileSync20 } from "fs";
8437
8732
  import { resolve as resolve8 } from "path";
@@ -8623,6 +8918,37 @@ async function write(args) {
8623
8918
  }
8624
8919
  }
8625
8920
 
8921
+ // src/crash.ts
8922
+ import { mkdirSync as mkdirSync18, appendFileSync as appendFileSync3 } from "fs";
8923
+ import { join as join20 } from "path";
8924
+ function handleCrash(err2) {
8925
+ const stamp = (/* @__PURE__ */ new Date()).toISOString();
8926
+ const detail = err2 instanceof Error ? err2.stack || err2.message : String(err2);
8927
+ let logPath2 = null;
8928
+ const entry = `[${stamp}] holt ${VERSION} argv: ${process.argv.slice(2).join(" ")}
8929
+ ${detail}
8930
+
8931
+ `;
8932
+ for (const base of [wsHoltDir(), GLOBAL_DIR]) {
8933
+ try {
8934
+ const dir = join20(base, "logs");
8935
+ mkdirSync18(dir, { recursive: true });
8936
+ const p = join20(dir, `crash-${stamp.slice(0, 10)}.log`);
8937
+ appendFileSync3(p, entry, "utf8");
8938
+ logPath2 = p;
8939
+ break;
8940
+ } catch {
8941
+ }
8942
+ }
8943
+ if (!isQuietMode()) {
8944
+ console.error(c.red("\n Something went wrong, and it is not your fault."));
8945
+ if (logPath2) console.error(c.dim(` Details saved to: ${logPath2}`));
8946
+ console.error(c.dim(" Try: holt doctor --fix (it repairs the common causes)"));
8947
+ console.error(c.dim(" Still stuck? https://github.com/holt-os/holt/issues\n"));
8948
+ }
8949
+ process.exit(1);
8950
+ }
8951
+
8626
8952
  // src/cli.ts
8627
8953
  var BANNER = `
8628
8954
  \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 +8962,53 @@ var BANNER = `
8636
8962
  var HELP = `${BANNER}
8637
8963
  Usage: holt <command>
8638
8964
 
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]
8965
+ Start here:
8966
+ (no command) Start your assistant: sets up if needed, then launches your
8967
+ brain (Claude Code, Codex, Gemini), branded as Holt
8968
+ tour A 2-minute guided first run: ask, teach it one fact, watch it remember
8969
+ doctor Health check for this machine
8970
+ ("holt doctor --fix" also repairs the common problems for you)
8971
+
8972
+ Every day:
8973
+ run <task> Do one task and remember it: holt run "draft my status update"
8651
8974
  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)
8975
+ voice Teach Holt how you write: holt voice [add <file> | show | edit | clear]
8976
+ memory What Holt knows here: holt memory [search <q> | facts | embed | clear]
8977
+ wiki A tidy, linked notebook Holt keeps from your memory: holt wiki [sync | show]
8978
+ graph See everything Holt knows as a visual map in your browser
8979
+ skill Reusable how-tos you can share: holt skill [list | show | create | add]
8980
+
8981
+ From your phone, while you sleep:
8982
+ telegram Chat with Holt from your phone: holt telegram setup
8983
+ notify [msg] Send yourself a phone message from any script (stdin-friendly)
8984
+ schedule Run a task on a timer: holt schedule [add | list | remove]
8985
+ routine Named, reusable, scheduled jobs: holt routine [add | run | list | remove]
8986
+
8987
+ Housekeeping:
8988
+ setting Change brains, API connections, and your launch word (per folder)
8660
8989
  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)
8990
+ update Get the latest Holt, in one word
8991
+ reset Start this folder over (asks first; your own files are untouched)
8992
+ hook Ambient memory inside plain Claude Code: holt hook [install | status]
8993
+ mcp Share Holt's memory with other AI apps (Claude Code, Cursor, Codex)
8994
+ chat Lightweight built-in chat, used for API brains
8995
+ init Full setup by hand (bare "holt" runs this for you when needed)
8996
+ launch Same as bare "holt": start your assistant
8997
+ version | help Print the version / show this help
8663
8998
 
8664
8999
  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.
9000
+ A "brain" is the AI doing the thinking: an agent CLI on your machine
9001
+ (claude, codex, gemini) or a direct API connection you add in settings.
8667
9002
 
8668
9003
  Docs: https://productsdecoded.com/holt
8669
9004
  Repo: https://github.com/holt-os/holt
8670
9005
  `;
8671
9006
  async function main() {
8672
9007
  const cmd = process.argv[2];
9008
+ const sub = process.argv[3];
9009
+ if (cmd === "statusline" || cmd === "mcp" || cmd === "notify" || cmd === "hook" && (sub === "inject" || sub === "capture")) {
9010
+ setQuietMode(true);
9011
+ }
8673
9012
  switch (cmd) {
8674
9013
  case void 0:
8675
9014
  await launch();
@@ -8738,7 +9077,16 @@ async function main() {
8738
9077
  await notify(process.argv.slice(3));
8739
9078
  break;
8740
9079
  case "doctor":
8741
- await doctor();
9080
+ await doctor(process.argv.slice(3));
9081
+ break;
9082
+ case "tour":
9083
+ await tour();
9084
+ break;
9085
+ case "reset":
9086
+ await reset();
9087
+ break;
9088
+ case "update":
9089
+ await update();
8742
9090
  break;
8743
9091
  case "voice":
8744
9092
  await voice(process.argv[3], process.argv.slice(4));
@@ -8754,4 +9102,6 @@ async function main() {
8754
9102
  process.exitCode = 1;
8755
9103
  }
8756
9104
  }
8757
- main();
9105
+ process.on("uncaughtException", handleCrash);
9106
+ process.on("unhandledRejection", handleCrash);
9107
+ 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.0",
4
4
  "description": "An open-source personal agent OS: any LLM, private memory you can see and walk.",
5
5
  "type": "module",
6
6
  "license": "MIT",