@holt-os/holt 0.14.2 → 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 +57 -14
  2. package/dist/cli.js +932 -244
  3. package/package.json +2 -1
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;
207
234
  try {
208
- const cfg = JSON.parse(readFileSync2(path, "utf8"));
235
+ raw = readFileSync2(path, "utf8");
236
+ } catch {
237
+ return null;
238
+ }
239
+ try {
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}`
@@ -2220,9 +2271,9 @@ Default brain? [${chosen.join("/")}] (${defPick}): `) ?? "").trim();
2220
2271
  }
2221
2272
 
2222
2273
  // src/commands/launch.ts
2223
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync11, existsSync as existsSync11, mkdirSync as mkdirSync10, copyFileSync as copyFileSync2 } from "fs";
2224
- import { homedir as homedir6 } from "os";
2225
- import { join as join12, dirname as dirname5 } from "path";
2274
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync12, existsSync as existsSync12, mkdirSync as mkdirSync11, copyFileSync as copyFileSync2 } from "fs";
2275
+ import { homedir as homedir7 } from "os";
2276
+ import { join as join13, dirname as dirname6 } from "path";
2226
2277
 
2227
2278
  // src/scheduler.ts
2228
2279
  import { homedir as homedir3 } from "os";
@@ -2363,11 +2414,340 @@ function ensureLogsDir() {
2363
2414
  mkdirSync7(logsDir(), { recursive: true });
2364
2415
  }
2365
2416
 
2417
+ // src/skillcompiler.ts
2418
+ import {
2419
+ readFileSync as readFileSync10,
2420
+ writeFileSync as writeFileSync8,
2421
+ existsSync as existsSync8,
2422
+ mkdirSync as mkdirSync8,
2423
+ readdirSync as readdirSync2,
2424
+ statSync as statSync3,
2425
+ rmSync as rmSync4,
2426
+ cpSync
2427
+ } from "fs";
2428
+ import { join as join9, dirname as dirname3 } from "path";
2429
+ import { homedir as homedir4 } from "os";
2430
+ import { createHash as createHash2 } from "crypto";
2431
+ var MARKER = "holt-managed skill";
2432
+ var CODEX_PREFIX = "holt-";
2433
+ function manifestPath(ws = workspace()) {
2434
+ return join9(wsHoltDir(ws), "skill-commands.json");
2435
+ }
2436
+ function emptyManifest() {
2437
+ return { version: 1, brains: {} };
2438
+ }
2439
+ function readManifest(ws = workspace()) {
2440
+ const p = manifestPath(ws);
2441
+ if (!existsSync8(p)) return emptyManifest();
2442
+ try {
2443
+ const parsed = JSON.parse(readFileSync10(p, "utf8"));
2444
+ const brains = parsed.brains && typeof parsed.brains === "object" ? parsed.brains : {};
2445
+ return { version: 1, brains };
2446
+ } catch {
2447
+ return emptyManifest();
2448
+ }
2449
+ }
2450
+ function writeManifest(m, ws = workspace()) {
2451
+ mkdirSync8(wsHoltDir(ws), { recursive: true });
2452
+ writeFileSync8(manifestPath(ws), JSON.stringify(m, null, 2) + "\n", "utf8");
2453
+ }
2454
+ function hashString(s) {
2455
+ return createHash2("sha256").update(s).digest("hex");
2456
+ }
2457
+ function hashDir(dir) {
2458
+ const h = createHash2("sha256");
2459
+ const walk = (rel) => {
2460
+ const abs = join9(dir, rel);
2461
+ let entries;
2462
+ try {
2463
+ entries = readdirSync2(abs).sort();
2464
+ } catch {
2465
+ return;
2466
+ }
2467
+ for (const name of entries) {
2468
+ const childRel = rel ? join9(rel, name) : name;
2469
+ const childAbs = join9(dir, childRel);
2470
+ let st;
2471
+ try {
2472
+ st = statSync3(childAbs);
2473
+ } catch {
2474
+ continue;
2475
+ }
2476
+ if (st.isDirectory()) {
2477
+ h.update("D:" + childRel + "\n");
2478
+ walk(childRel);
2479
+ } else {
2480
+ h.update("F:" + childRel + "\n");
2481
+ try {
2482
+ h.update(readFileSync10(childAbs));
2483
+ } catch {
2484
+ }
2485
+ }
2486
+ }
2487
+ };
2488
+ walk("");
2489
+ return h.digest("hex");
2490
+ }
2491
+ function readSkillSource(skill) {
2492
+ const file = join9(skill.dir, "SKILL.md");
2493
+ let raw = "";
2494
+ try {
2495
+ raw = readFileSync10(file, "utf8");
2496
+ } catch {
2497
+ return null;
2498
+ }
2499
+ const { data, body } = parseFrontmatter(raw);
2500
+ const description = (data.description || skill.description || "").trim();
2501
+ return { name: skill.name, description, body: body.trim() };
2502
+ }
2503
+ function tomlBasicString(s) {
2504
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, " ").trim();
2505
+ }
2506
+ function tomlMultiline(s) {
2507
+ return s.replace(/\\/g, "\\\\").replace(/"""/g, '\\"\\"\\"');
2508
+ }
2509
+ function renderGeminiToml(src) {
2510
+ const instructions = src.body || src.description;
2511
+ if (!instructions.trim()) return null;
2512
+ const prompt = [
2513
+ instructions,
2514
+ "",
2515
+ "Apply the skill above to this request:",
2516
+ "{{args}}"
2517
+ ].join("\n");
2518
+ const lines = [
2519
+ `# ${MARKER}: ${src.name}`,
2520
+ "# Generated by Holt from a SKILL.md. Edit the skill, not this file: Holt",
2521
+ '# will overwrite it on the next launch. Delete via "holt skill remove".'
2522
+ ];
2523
+ if (src.description) lines.push(`description = "${tomlBasicString(src.description)}"`);
2524
+ lines.push('prompt = """');
2525
+ lines.push(tomlMultiline(prompt));
2526
+ lines.push('"""');
2527
+ return lines.join("\n") + "\n";
2528
+ }
2529
+ function renderCodexPrompt(src) {
2530
+ const instructions = src.body || src.description;
2531
+ if (!instructions.trim()) return null;
2532
+ const front = ["---"];
2533
+ if (src.description) front.push(`description: ${src.description.replace(/\n/g, " ").trim()}`);
2534
+ front.push("argument-hint: [request]");
2535
+ front.push("---");
2536
+ const parts = [
2537
+ front.join("\n"),
2538
+ "",
2539
+ `<!-- ${MARKER}: ${src.name} (generated by Holt from a SKILL.md; do not edit) -->`,
2540
+ "",
2541
+ instructions,
2542
+ "",
2543
+ "Apply the skill above to this request:",
2544
+ "",
2545
+ "$ARGUMENTS",
2546
+ ""
2547
+ ];
2548
+ return parts.join("\n");
2549
+ }
2550
+ function claudeSkillsDir(ws) {
2551
+ return join9(ws, ".claude", "skills");
2552
+ }
2553
+ function geminiCommandsDir(ws) {
2554
+ return join9(ws, ".gemini", "commands");
2555
+ }
2556
+ function codexPromptsDir() {
2557
+ return join9(homedir4(), ".codex", "prompts");
2558
+ }
2559
+ function fileIsOurs(path) {
2560
+ try {
2561
+ return readFileSync10(path, "utf8").includes(MARKER);
2562
+ } catch {
2563
+ return false;
2564
+ }
2565
+ }
2566
+ function emptyResult() {
2567
+ return { written: [], skippedExisting: [], skippedUnexpressible: [], removed: [] };
2568
+ }
2569
+ function priorEntry(m, brain, path) {
2570
+ return (m.brains[brain] || []).find((e) => e.path === path);
2571
+ }
2572
+ function compileForBrain(brain, ws = workspace()) {
2573
+ const res = emptyResult();
2574
+ const manifest = readManifest(ws);
2575
+ const prior = manifest.brains[brain] || [];
2576
+ const nextEntries = [];
2577
+ let skills;
2578
+ try {
2579
+ skills = listSkills();
2580
+ } catch {
2581
+ return res;
2582
+ }
2583
+ const enabledNames = new Set(skills.map((s) => s.name));
2584
+ for (const skill of skills) {
2585
+ try {
2586
+ if (brain === "claude") {
2587
+ compileClaude(skill, ws, manifest, res, nextEntries);
2588
+ } else if (brain === "gemini") {
2589
+ compileGemini(skill, ws, manifest, res, nextEntries);
2590
+ } else {
2591
+ compileCodex(skill, manifest, res, nextEntries);
2592
+ }
2593
+ } catch {
2594
+ res.skippedUnexpressible.push(skill.name);
2595
+ }
2596
+ }
2597
+ for (const e of prior) {
2598
+ if (enabledNames.has(e.skill) && nextEntries.some((n) => n.path === e.path)) continue;
2599
+ if (removeArtifact(e.path)) res.removed.push(e.path);
2600
+ }
2601
+ manifest.brains[brain] = nextEntries;
2602
+ try {
2603
+ writeManifest(manifest, ws);
2604
+ } catch {
2605
+ }
2606
+ return res;
2607
+ }
2608
+ function compileClaude(skill, ws, manifest, res, next) {
2609
+ const dest = join9(claudeSkillsDir(ws), skill.name);
2610
+ const known = priorEntry(manifest, "claude", dest);
2611
+ if (existsSync8(dest) && !known) {
2612
+ res.skippedExisting.push(dest);
2613
+ return;
2614
+ }
2615
+ const srcHash = hashDir(skill.dir);
2616
+ if (known && known.hash === srcHash && existsSync8(dest)) {
2617
+ next.push(known);
2618
+ return;
2619
+ }
2620
+ if (existsSync8(dest)) rmSync4(dest, { recursive: true, force: true });
2621
+ mkdirSync8(dirname3(dest), { recursive: true });
2622
+ cpSync(skill.dir, dest, { recursive: true });
2623
+ next.push({ skill: skill.name, path: dest, hash: hashDir(dest) });
2624
+ res.written.push(dest);
2625
+ }
2626
+ function compileGemini(skill, ws, manifest, res, next) {
2627
+ const src = readSkillSource(skill);
2628
+ if (!src) {
2629
+ res.skippedUnexpressible.push(skill.name);
2630
+ return;
2631
+ }
2632
+ const content = renderGeminiToml(src);
2633
+ if (content === null) {
2634
+ res.skippedUnexpressible.push(skill.name);
2635
+ return;
2636
+ }
2637
+ const dest = join9(geminiCommandsDir(ws), `${skill.name}.toml`);
2638
+ writeTextArtifact("gemini", skill.name, dest, content, manifest, res, next);
2639
+ }
2640
+ function compileCodex(skill, manifest, res, next) {
2641
+ const src = readSkillSource(skill);
2642
+ if (!src) {
2643
+ res.skippedUnexpressible.push(skill.name);
2644
+ return;
2645
+ }
2646
+ const content = renderCodexPrompt(src);
2647
+ if (content === null) {
2648
+ res.skippedUnexpressible.push(skill.name);
2649
+ return;
2650
+ }
2651
+ const dest = join9(codexPromptsDir(), `${CODEX_PREFIX}${skill.name}.md`);
2652
+ writeTextArtifact("codex", skill.name, dest, content, manifest, res, next);
2653
+ }
2654
+ function writeTextArtifact(brain, skillName, dest, content, manifest, res, next) {
2655
+ const known = priorEntry(manifest, brain, dest);
2656
+ if (existsSync8(dest) && !known && !fileIsOurs(dest)) {
2657
+ res.skippedExisting.push(dest);
2658
+ return;
2659
+ }
2660
+ const hash = hashString(content);
2661
+ if (existsSync8(dest)) {
2662
+ let cur = "";
2663
+ try {
2664
+ cur = readFileSync10(dest, "utf8");
2665
+ } catch {
2666
+ cur = "";
2667
+ }
2668
+ if (hashString(cur) === hash) {
2669
+ next.push({ skill: skillName, path: dest, hash });
2670
+ return;
2671
+ }
2672
+ }
2673
+ mkdirSync8(dirname3(dest), { recursive: true });
2674
+ writeFileSync8(dest, content, "utf8");
2675
+ next.push({ skill: skillName, path: dest, hash });
2676
+ res.written.push(dest);
2677
+ }
2678
+ function removeArtifact(path) {
2679
+ try {
2680
+ if (!existsSync8(path)) return false;
2681
+ rmSync4(path, { recursive: true, force: true });
2682
+ return true;
2683
+ } catch {
2684
+ return false;
2685
+ }
2686
+ }
2687
+ function syncSkillCommands(brain, ws = workspace()) {
2688
+ let res;
2689
+ try {
2690
+ res = compileForBrain(brain, ws);
2691
+ } catch {
2692
+ return;
2693
+ }
2694
+ reportQuiet(brain, res);
2695
+ }
2696
+ function syncAllBrains(enabled, ws = workspace()) {
2697
+ for (const brain of enabled) {
2698
+ try {
2699
+ const res = compileForBrain(brain, ws);
2700
+ reportQuiet(brain, res);
2701
+ } catch {
2702
+ }
2703
+ }
2704
+ }
2705
+ function removeSkillArtifacts(skillName, ws = workspace()) {
2706
+ const manifest = readManifest(ws);
2707
+ let removed = 0;
2708
+ for (const brain of Object.keys(manifest.brains)) {
2709
+ const entries = manifest.brains[brain] || [];
2710
+ const keep = [];
2711
+ for (const e of entries) {
2712
+ if (e.skill === skillName) {
2713
+ if (removeArtifact(e.path)) removed++;
2714
+ } else {
2715
+ keep.push(e);
2716
+ }
2717
+ }
2718
+ manifest.brains[brain] = keep;
2719
+ }
2720
+ try {
2721
+ writeManifest(manifest, ws);
2722
+ } catch {
2723
+ }
2724
+ return removed;
2725
+ }
2726
+ var BRAIN_LABEL = {
2727
+ claude: "Claude Code",
2728
+ gemini: "Gemini CLI",
2729
+ codex: "Codex"
2730
+ };
2731
+ function reportQuiet(brain, res) {
2732
+ const changed = res.written.length + res.removed.length;
2733
+ if (changed === 0 && res.skippedExisting.length === 0) return;
2734
+ const label = BRAIN_LABEL[brain];
2735
+ const parts = [];
2736
+ if (res.written.length) parts.push(`${res.written.length} synced`);
2737
+ if (res.removed.length) parts.push(`${res.removed.length} removed`);
2738
+ if (parts.length) {
2739
+ console.log(c.dim(` Skills as ${label} commands: ${parts.join(", ")}.`));
2740
+ }
2741
+ for (const p of res.skippedExisting) {
2742
+ console.log(c.dim(` Skipped ${p} (already exists and not written by Holt).`));
2743
+ }
2744
+ }
2745
+
2366
2746
  // src/commands/hook.ts
2367
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync9, copyFileSync, realpathSync as realpathSync2, appendFileSync as appendFileSync2, statSync as statSync4 } from "fs";
2747
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync10, existsSync as existsSync10, mkdirSync as mkdirSync10, copyFileSync, realpathSync as realpathSync2, appendFileSync as appendFileSync2, statSync as statSync5 } from "fs";
2368
2748
  import { spawnSync as spawnSync3 } from "child_process";
2369
- import { join as join10, dirname as dirname3 } from "path";
2370
- import { homedir as homedir4 } from "os";
2749
+ import { join as join11, dirname as dirname4 } from "path";
2750
+ import { homedir as homedir5 } from "os";
2371
2751
 
2372
2752
  // src/facts.ts
2373
2753
  var MIN_EXCHANGES_FOR_EXTRACTION = 3;
@@ -2483,15 +2863,15 @@ async function extractAndSaveFacts(active, cfg, history, session) {
2483
2863
 
2484
2864
  // src/wiki.ts
2485
2865
  import {
2486
- readFileSync as readFileSync10,
2487
- writeFileSync as writeFileSync8,
2488
- existsSync as existsSync8,
2489
- mkdirSync as mkdirSync8,
2490
- rmSync as rmSync4,
2491
- readdirSync as readdirSync2,
2492
- statSync as statSync3
2866
+ readFileSync as readFileSync11,
2867
+ writeFileSync as writeFileSync9,
2868
+ existsSync as existsSync9,
2869
+ mkdirSync as mkdirSync9,
2870
+ rmSync as rmSync5,
2871
+ readdirSync as readdirSync3,
2872
+ statSync as statSync4
2493
2873
  } from "fs";
2494
- import { join as join9 } from "path";
2874
+ import { join as join10 } from "path";
2495
2875
  import { totalmem } from "os";
2496
2876
 
2497
2877
  // src/localmodel.ts
@@ -2546,28 +2926,28 @@ function pullHint(model) {
2546
2926
 
2547
2927
  // src/wiki.ts
2548
2928
  function wikiDir() {
2549
- return join9(wsHoltDir(), "wiki");
2929
+ return join10(wsHoltDir(), "wiki");
2550
2930
  }
2551
2931
  function wikiStatePath() {
2552
- return join9(wikiDir(), ".state.json");
2932
+ return join10(wikiDir(), ".state.json");
2553
2933
  }
2554
2934
  function wikiIndexPath() {
2555
- return join9(wikiDir(), "index.md");
2935
+ return join10(wikiDir(), "index.md");
2556
2936
  }
2557
2937
  function pagePath(slug) {
2558
- return join9(wikiDir(), `${slug}.md`);
2938
+ return join10(wikiDir(), `${slug}.md`);
2559
2939
  }
2560
2940
  function loadState() {
2561
2941
  try {
2562
- const s = JSON.parse(readFileSync10(wikiStatePath(), "utf8"));
2942
+ const s = JSON.parse(readFileSync11(wikiStatePath(), "utf8"));
2563
2943
  return { lastSyncTs: typeof s.lastSyncTs === "number" ? s.lastSyncTs : 0, updatedAt: s.updatedAt ?? 0 };
2564
2944
  } catch {
2565
2945
  return { lastSyncTs: 0, updatedAt: 0 };
2566
2946
  }
2567
2947
  }
2568
2948
  function saveState(s) {
2569
- mkdirSync8(wikiDir(), { recursive: true });
2570
- writeFileSync8(wikiStatePath(), JSON.stringify(s, null, 2) + "\n", "utf8");
2949
+ mkdirSync9(wikiDir(), { recursive: true });
2950
+ writeFileSync9(wikiStatePath(), JSON.stringify(s, null, 2) + "\n", "utf8");
2571
2951
  }
2572
2952
  function saveSyncMarker(ts) {
2573
2953
  saveState({ lastSyncTs: ts, updatedAt: Date.now() });
@@ -2577,19 +2957,19 @@ function isPageFile(name) {
2577
2957
  }
2578
2958
  function listPageSlugs() {
2579
2959
  const dir = wikiDir();
2580
- if (!existsSync8(dir)) return [];
2960
+ if (!existsSync9(dir)) return [];
2581
2961
  try {
2582
- return readdirSync2(dir).filter(isPageFile).map((f) => f.slice(0, -3)).sort();
2962
+ return readdirSync3(dir).filter(isPageFile).map((f) => f.slice(0, -3)).sort();
2583
2963
  } catch {
2584
2964
  return [];
2585
2965
  }
2586
2966
  }
2587
2967
  function readPage(slug) {
2588
2968
  const p = pagePath(slug);
2589
- if (!existsSync8(p)) return null;
2969
+ if (!existsSync9(p)) return null;
2590
2970
  let raw = "";
2591
2971
  try {
2592
- raw = readFileSync10(p, "utf8");
2972
+ raw = readFileSync11(p, "utf8");
2593
2973
  } catch {
2594
2974
  return null;
2595
2975
  }
@@ -2631,8 +3011,8 @@ function renderPage(pg) {
2631
3011
  return fm;
2632
3012
  }
2633
3013
  function writePage(pg) {
2634
- mkdirSync8(wikiDir(), { recursive: true });
2635
- writeFileSync8(pagePath(pg.slug), renderPage(pg), "utf8");
3014
+ mkdirSync9(wikiDir(), { recursive: true });
3015
+ writeFileSync9(pagePath(pg.slug), renderPage(pg), "utf8");
2636
3016
  }
2637
3017
  var WIKILINK_RE = /\[\[([^\]]+)\]\]/g;
2638
3018
  function extractLinks(body) {
@@ -2909,8 +3289,8 @@ function renderIndex(pages) {
2909
3289
  return lines.join("\n");
2910
3290
  }
2911
3291
  function writeIndex(pages) {
2912
- mkdirSync8(wikiDir(), { recursive: true });
2913
- writeFileSync8(wikiIndexPath(), renderIndex(pages), "utf8");
3292
+ mkdirSync9(wikiDir(), { recursive: true });
3293
+ writeFileSync9(wikiIndexPath(), renderIndex(pages), "utf8");
2914
3294
  }
2915
3295
  var WIKI_SESSION = "wiki";
2916
3296
  function wikiRowId(slug) {
@@ -2925,9 +3305,9 @@ async function indexPagesForRecall(pages) {
2925
3305
  if (pages.length === 0) return;
2926
3306
  const slugs = new Set(pages.map((p) => sanitizeName(p.slug).slice(0, 24)));
2927
3307
  const kept = dropWikiRows(slugs);
2928
- mkdirSync8(join9(wsHoltDir(), "memory"), { recursive: true });
2929
- writeFileSync8(
2930
- join9(wsHoltDir(), "memory", "turns.jsonl"),
3308
+ mkdirSync9(join10(wsHoltDir(), "memory"), { recursive: true });
3309
+ writeFileSync9(
3310
+ join10(wsHoltDir(), "memory", "turns.jsonl"),
2931
3311
  kept.map((t) => JSON.stringify(t)).join("\n") + (kept.length ? "\n" : ""),
2932
3312
  "utf8"
2933
3313
  );
@@ -2949,9 +3329,9 @@ ${pg.body}`.slice(0, 4e3);
2949
3329
  function clearWikiRecallRows() {
2950
3330
  try {
2951
3331
  const kept = loadTurns().filter((t) => t.session !== WIKI_SESSION);
2952
- mkdirSync8(join9(wsHoltDir(), "memory"), { recursive: true });
2953
- writeFileSync8(
2954
- join9(wsHoltDir(), "memory", "turns.jsonl"),
3332
+ mkdirSync9(join10(wsHoltDir(), "memory"), { recursive: true });
3333
+ writeFileSync9(
3334
+ join10(wsHoltDir(), "memory", "turns.jsonl"),
2955
3335
  kept.map((t) => JSON.stringify(t)).join("\n") + (kept.length ? "\n" : ""),
2956
3336
  "utf8"
2957
3337
  );
@@ -2959,14 +3339,14 @@ function clearWikiRecallRows() {
2959
3339
  }
2960
3340
  }
2961
3341
  function wipeWiki() {
2962
- if (existsSync8(wikiDir())) rmSync4(wikiDir(), { recursive: true, force: true });
3342
+ if (existsSync9(wikiDir())) rmSync5(wikiDir(), { recursive: true, force: true });
2963
3343
  }
2964
3344
  function wikiStats() {
2965
3345
  const slugs = listPageSlugs();
2966
3346
  let bytes = 0;
2967
3347
  for (const slug of slugs) {
2968
3348
  try {
2969
- bytes += statSync3(pagePath(slug)).size;
3349
+ bytes += statSync4(pagePath(slug)).size;
2970
3350
  } catch {
2971
3351
  }
2972
3352
  }
@@ -3005,26 +3385,26 @@ var HOOK_CMD = {
3005
3385
  capture: "holt hook capture"
3006
3386
  };
3007
3387
  function globalSettingsPath() {
3008
- return join10(homedir4(), ".claude", "settings.json");
3388
+ return join11(homedir5(), ".claude", "settings.json");
3009
3389
  }
3010
3390
  function projectSettingsPath() {
3011
- return join10(process.cwd(), ".claude", "settings.json");
3391
+ return join11(process.cwd(), ".claude", "settings.json");
3012
3392
  }
3013
3393
  function readSettings(path) {
3014
- if (!existsSync9(path)) return {};
3394
+ if (!existsSync10(path)) return {};
3015
3395
  try {
3016
- const parsed = JSON.parse(readFileSync11(path, "utf8"));
3396
+ const parsed = JSON.parse(readFileSync12(path, "utf8"));
3017
3397
  return parsed && typeof parsed === "object" ? parsed : {};
3018
3398
  } catch {
3019
3399
  throw new Error(`could not parse ${path} (not valid JSON) - leaving it untouched`);
3020
3400
  }
3021
3401
  }
3022
3402
  function writeSettings(path, settings) {
3023
- mkdirSync9(dirname3(path), { recursive: true });
3024
- writeFileSync9(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
3403
+ mkdirSync10(dirname4(path), { recursive: true });
3404
+ writeFileSync10(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
3025
3405
  }
3026
3406
  function backup(path) {
3027
- if (!existsSync9(path)) return null;
3407
+ if (!existsSync10(path)) return null;
3028
3408
  const dest = `${path}.holt-bak`;
3029
3409
  try {
3030
3410
  copyFileSync(path, dest);
@@ -3114,7 +3494,7 @@ function remove(rest) {
3114
3494
  const paths = project ? [projectSettingsPath()] : [globalSettingsPath(), projectSettingsPath()];
3115
3495
  let touchedAny = false;
3116
3496
  for (const path of paths) {
3117
- if (!existsSync9(path)) continue;
3497
+ if (!existsSync10(path)) continue;
3118
3498
  let settings;
3119
3499
  try {
3120
3500
  settings = readSettings(path);
@@ -3144,7 +3524,7 @@ function status(rest) {
3144
3524
  ];
3145
3525
  console.log(c.accent(" Holt ambient hooks"));
3146
3526
  for (const e of entries) {
3147
- if (!existsSync9(e.path)) {
3527
+ if (!existsSync10(e.path)) {
3148
3528
  console.log(c.dim(` ${e.label.padEnd(8)} ${e.path} (no settings file)`));
3149
3529
  continue;
3150
3530
  }
@@ -3194,7 +3574,7 @@ function parseHookInput(raw) {
3194
3574
  }
3195
3575
  }
3196
3576
  function enterFolder(cwd) {
3197
- if (!cwd || !existsSync9(cwd)) return null;
3577
+ if (!cwd || !existsSync10(cwd)) return null;
3198
3578
  try {
3199
3579
  const real = realpathSync2(cwd);
3200
3580
  process.chdir(real);
@@ -3205,13 +3585,13 @@ function enterFolder(cwd) {
3205
3585
  }
3206
3586
  function guardTrusted(real) {
3207
3587
  if (!isTrusted(real)) return false;
3208
- if (!existsSync9(memDir())) return false;
3588
+ if (!existsSync10(memDir())) return false;
3209
3589
  return true;
3210
3590
  }
3211
3591
  function hookLog(msg) {
3212
3592
  try {
3213
- mkdirSync9(GLOBAL_DIR, { recursive: true });
3214
- appendFileSync2(join10(GLOBAL_DIR, "hooks.log"), `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
3593
+ mkdirSync10(GLOBAL_DIR, { recursive: true });
3594
+ appendFileSync2(join11(GLOBAL_DIR, "hooks.log"), `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
3215
3595
  `, "utf8");
3216
3596
  } catch {
3217
3597
  }
@@ -3219,18 +3599,18 @@ function hookLog(msg) {
3219
3599
  function fileBytes(path) {
3220
3600
  if (!path) return 0;
3221
3601
  try {
3222
- return statSync4(path).size;
3602
+ return statSync5(path).size;
3223
3603
  } catch {
3224
3604
  return 0;
3225
3605
  }
3226
3606
  }
3227
3607
  var THROTTLE_MIN_NEW_EXCHANGES = 2;
3228
3608
  function hookStatePath() {
3229
- return join10(GLOBAL_DIR, "hook-state.json");
3609
+ return join11(GLOBAL_DIR, "hook-state.json");
3230
3610
  }
3231
3611
  function readHookState() {
3232
3612
  try {
3233
- const parsed = JSON.parse(readFileSync11(hookStatePath(), "utf8"));
3613
+ const parsed = JSON.parse(readFileSync12(hookStatePath(), "utf8"));
3234
3614
  if (!parsed || typeof parsed !== "object") return {};
3235
3615
  return parsed;
3236
3616
  } catch {
@@ -3243,10 +3623,10 @@ function lastProcessedExchanges(state, sessionId) {
3243
3623
  }
3244
3624
  function saveHookState(sessionId, exchanges) {
3245
3625
  try {
3246
- mkdirSync9(GLOBAL_DIR, { recursive: true });
3626
+ mkdirSync10(GLOBAL_DIR, { recursive: true });
3247
3627
  const state = readHookState();
3248
3628
  state[sessionId] = { lastExchanges: exchanges };
3249
- writeFileSync9(hookStatePath(), JSON.stringify(state, null, 2) + "\n", "utf8");
3629
+ writeFileSync10(hookStatePath(), JSON.stringify(state, null, 2) + "\n", "utf8");
3250
3630
  } catch {
3251
3631
  }
3252
3632
  }
@@ -3320,7 +3700,7 @@ function readTranscript(path) {
3320
3700
  const out = [];
3321
3701
  let raw;
3322
3702
  try {
3323
- raw = readFileSync11(path, "utf8");
3703
+ raw = readFileSync12(path, "utf8");
3324
3704
  } catch {
3325
3705
  return out;
3326
3706
  }
@@ -3348,28 +3728,28 @@ function readTranscript(path) {
3348
3728
  return out;
3349
3729
  }
3350
3730
  function resolveCommandPath(command) {
3351
- if (command.startsWith("/") && existsSync9(command)) return command;
3731
+ if (command.startsWith("/") && existsSync10(command)) return command;
3352
3732
  try {
3353
3733
  const shell = process.env.SHELL || "/bin/sh";
3354
3734
  const res = spawnSync3(shell, ["-lc", "command -v " + command], { encoding: "utf8" });
3355
3735
  if (res.status === 0) {
3356
3736
  const p = (res.stdout || "").trim().split("\n")[0]?.trim();
3357
- if (p && p.startsWith("/") && existsSync9(p)) return p;
3737
+ if (p && p.startsWith("/") && existsSync10(p)) return p;
3358
3738
  }
3359
3739
  } catch {
3360
3740
  }
3361
- const home = homedir4();
3741
+ const home = homedir5();
3362
3742
  const dirs = [
3363
- join10(home, ".local", "bin"),
3743
+ join11(home, ".local", "bin"),
3364
3744
  "/opt/homebrew/bin",
3365
3745
  "/usr/local/bin",
3366
- join10(home, ".npm-global", "bin"),
3746
+ join11(home, ".npm-global", "bin"),
3367
3747
  "/usr/bin",
3368
3748
  "/bin"
3369
3749
  ];
3370
3750
  for (const dir of dirs) {
3371
- const candidate = join10(dir, command);
3372
- if (existsSync9(candidate)) return candidate;
3751
+ const candidate = join11(dir, command);
3752
+ if (existsSync10(candidate)) return candidate;
3373
3753
  }
3374
3754
  return null;
3375
3755
  }
@@ -3443,7 +3823,7 @@ async function capture() {
3443
3823
  let bytes = 0;
3444
3824
  let lines = 0;
3445
3825
  try {
3446
- const rawT = readFileSync11(data.transcript_path, "utf8");
3826
+ const rawT = readFileSync12(data.transcript_path, "utf8");
3447
3827
  bytes = Buffer.byteLength(rawT, "utf8");
3448
3828
  lines = countLines(rawT);
3449
3829
  } catch {
@@ -3541,8 +3921,8 @@ async function hook(sub, rest = []) {
3541
3921
  import { randomUUID as randomUUID4 } from "crypto";
3542
3922
 
3543
3923
  // src/output.ts
3544
- import { writeFileSync as writeFileSync10 } from "fs";
3545
- import { join as join11, isAbsolute } from "path";
3924
+ import { writeFileSync as writeFileSync11 } from "fs";
3925
+ import { join as join12, isAbsolute } from "path";
3546
3926
  function escapeHtml(s) {
3547
3927
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3548
3928
  }
@@ -3682,8 +4062,8 @@ function saveReply(reply, format, name) {
3682
4062
  throw new Error('name must be a plain file name in this folder (no "/", "\\", or "..").');
3683
4063
  }
3684
4064
  if (!/\.[a-z0-9]+$/i.test(base)) base += ext;
3685
- const path = join11(process.cwd(), base);
3686
- writeFileSync10(path, renderReply(reply, format), "utf8");
4065
+ const path = join12(process.cwd(), base);
4066
+ writeFileSync11(path, renderReply(reply, format), "utf8");
3687
4067
  return path;
3688
4068
  }
3689
4069
 
@@ -3711,12 +4091,12 @@ async function login(which) {
3711
4091
  }
3712
4092
 
3713
4093
  // src/access.ts
3714
- import { existsSync as existsSync10, realpathSync as realpathSync3, statSync as statSync5 } from "fs";
3715
- import { homedir as homedir5 } from "os";
3716
- import { dirname as dirname4, isAbsolute as isAbsolute2, resolve as resolve2, sep } from "path";
4094
+ import { existsSync as existsSync11, realpathSync as realpathSync3, statSync as statSync6 } from "fs";
4095
+ import { homedir as homedir6 } from "os";
4096
+ import { dirname as dirname5, isAbsolute as isAbsolute2, resolve as resolve2, sep } from "path";
3717
4097
  function expandHome(p) {
3718
- if (p === "~") return homedir5();
3719
- if (p.startsWith("~/")) return resolve2(homedir5(), p.slice(2));
4098
+ if (p === "~") return homedir6();
4099
+ if (p.startsWith("~/")) return resolve2(homedir6(), p.slice(2));
3720
4100
  return p;
3721
4101
  }
3722
4102
  function isInside(child, parent) {
@@ -3727,7 +4107,7 @@ function isInside(child, parent) {
3727
4107
  function resolveExisting(token) {
3728
4108
  const expanded = expandHome(token);
3729
4109
  const abs = isAbsolute2(expanded) ? expanded : resolve2(process.cwd(), expanded);
3730
- if (!existsSync10(abs)) return null;
4110
+ if (!existsSync11(abs)) return null;
3731
4111
  try {
3732
4112
  return realpathSync3(abs);
3733
4113
  } catch {
@@ -3736,9 +4116,9 @@ function resolveExisting(token) {
3736
4116
  }
3737
4117
  function containingDir(absPath) {
3738
4118
  try {
3739
- return statSync5(absPath).isDirectory() ? absPath : dirname4(absPath);
4119
+ return statSync6(absPath).isDirectory() ? absPath : dirname5(absPath);
3740
4120
  } catch {
3741
- return dirname4(absPath);
4121
+ return dirname5(absPath);
3742
4122
  }
3743
4123
  }
3744
4124
  function resolveGrantDir(token) {
@@ -3881,6 +4261,9 @@ async function chat() {
3881
4261
  if (embedOk && stats.withEmbeddings < stats.turns) {
3882
4262
  console.log(c.dim(` ${stats.turns - stats.withEmbeddings} older moments lack embeddings. Run "holt memory embed" to upgrade them.`));
3883
4263
  }
4264
+ if (!embedOk) {
4265
+ console.log(c.dim(' Recall works, but semantic memory is off. "holt doctor --fix" turns it on.'));
4266
+ }
3884
4267
  console.log(c.dim("Type a message. Commands: /brain /memory /output /save /allow /setting /clear /help /exit"));
3885
4268
  showStatus();
3886
4269
  while (true) {
@@ -4166,13 +4549,13 @@ var CONTEXT_MARKER = "<!-- holt-managed identity -->";
4166
4549
  function brandContextFile(id) {
4167
4550
  const name = CONTEXT_FILE[id];
4168
4551
  if (!name) return null;
4169
- const path = join12(workspace(), name);
4552
+ const path = join13(workspace(), name);
4170
4553
  try {
4171
- if (existsSync11(path)) {
4172
- const cur = readFileSync12(path, "utf8");
4554
+ if (existsSync12(path)) {
4555
+ const cur = readFileSync13(path, "utf8");
4173
4556
  if (!cur.includes(CONTEXT_MARKER)) return null;
4174
4557
  }
4175
- writeFileSync11(path, `${CONTEXT_MARKER}
4558
+ writeFileSync12(path, `${CONTEXT_MARKER}
4176
4559
  # Holt
4177
4560
 
4178
4561
  ${HOLT_IDENTITY}
@@ -4183,11 +4566,11 @@ ${HOLT_IDENTITY}
4183
4566
  }
4184
4567
  }
4185
4568
  function brandStatusLine() {
4186
- const path = join12(workspace(), ".claude", "settings.json");
4569
+ const path = join13(workspace(), ".claude", "settings.json");
4187
4570
  let settings = {};
4188
4571
  try {
4189
- if (existsSync11(path)) {
4190
- const parsed = JSON.parse(readFileSync12(path, "utf8"));
4572
+ if (existsSync12(path)) {
4573
+ const parsed = JSON.parse(readFileSync13(path, "utf8"));
4191
4574
  if (parsed && typeof parsed === "object") settings = parsed;
4192
4575
  }
4193
4576
  } catch {
@@ -4195,13 +4578,13 @@ function brandStatusLine() {
4195
4578
  }
4196
4579
  if (settings.statusLine) return false;
4197
4580
  try {
4198
- if (existsSync11(path)) copyFileSync2(path, `${path}.holt-bak`);
4581
+ if (existsSync12(path)) copyFileSync2(path, `${path}.holt-bak`);
4199
4582
  settings.statusLine = {
4200
4583
  type: "command",
4201
4584
  command: `${resolveHoltPath()} statusline`
4202
4585
  };
4203
- mkdirSync10(dirname5(path), { recursive: true });
4204
- writeFileSync11(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
4586
+ mkdirSync11(dirname6(path), { recursive: true });
4587
+ writeFileSync12(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
4205
4588
  return true;
4206
4589
  } catch {
4207
4590
  return false;
@@ -4223,7 +4606,7 @@ function resolveTarget(cfg) {
4223
4606
  async function ensureSetup() {
4224
4607
  const ws = workspace();
4225
4608
  if (!isTrusted(ws)) trustDir(ws);
4226
- mkdirSync10(memDir(), { recursive: true });
4609
+ mkdirSync11(memDir(), { recursive: true });
4227
4610
  if (!hooksAlreadyInstalled()) {
4228
4611
  try {
4229
4612
  await hook("install", []);
@@ -4233,8 +4616,8 @@ async function ensureSetup() {
4233
4616
  }
4234
4617
  function hooksAlreadyInstalled() {
4235
4618
  try {
4236
- const p = join12(homedir6(), ".claude", "settings.json");
4237
- return existsSync11(p) && readFileSync12(p, "utf8").includes("holt hook");
4619
+ const p = join13(homedir7(), ".claude", "settings.json");
4620
+ return existsSync12(p) && readFileSync13(p, "utf8").includes("holt hook");
4238
4621
  } catch {
4239
4622
  return false;
4240
4623
  }
@@ -4283,6 +4666,7 @@ async function launch() {
4283
4666
  if (ctx) console.log(c.dim(` Holt identity: ${ctx}`));
4284
4667
  }
4285
4668
  if (target.id === "claude") brandStatusLine();
4669
+ syncSkillCommands(target.id);
4286
4670
  const interactive = INTERACTIVE_ARGS[target.id];
4287
4671
  const args = [...interactive, ...flags];
4288
4672
  console.log(c.dim(' Starting your Holt session. Type "exit" to leave.\n'));
@@ -4291,7 +4675,7 @@ async function launch() {
4291
4675
  }
4292
4676
 
4293
4677
  // src/commands/memory.ts
4294
- import { existsSync as existsSync12, readFileSync as readFileSync13 } from "fs";
4678
+ import { existsSync as existsSync13, readFileSync as readFileSync14 } from "fs";
4295
4679
  import { basename } from "path";
4296
4680
  async function memoryCmd(sub, rest = []) {
4297
4681
  const { ask, close } = createReader();
@@ -4339,12 +4723,12 @@ async function memoryCmd(sub, rest = []) {
4339
4723
  return;
4340
4724
  }
4341
4725
  if (action === "facts") {
4342
- if (!existsSync12(factsMdPath())) {
4726
+ if (!existsSync13(factsMdPath())) {
4343
4727
  console.log(c.dim("\n No facts distilled yet. They form when you end a chat session.\n"));
4344
4728
  close();
4345
4729
  return;
4346
4730
  }
4347
- const body = readFileSync13(factsMdPath(), "utf8").trim();
4731
+ const body = readFileSync14(factsMdPath(), "utf8").trim();
4348
4732
  console.log("\n" + body + "\n");
4349
4733
  close();
4350
4734
  return;
@@ -4358,6 +4742,9 @@ async function memoryCmd(sub, rest = []) {
4358
4742
  }
4359
4743
  const hits = await recall(q, "__none__", 8);
4360
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
+ }
4361
4748
  if (hits.length === 0) console.log(c.dim(" Nothing relevant found."));
4362
4749
  else for (const h of hits) {
4363
4750
  const when = new Date(h.turn.ts).toISOString().slice(0, 10);
@@ -4458,8 +4845,8 @@ function globalCmd(act, rest) {
4458
4845
  }
4459
4846
 
4460
4847
  // src/commands/wiki.ts
4461
- import { existsSync as existsSync13, readFileSync as readFileSync14 } from "fs";
4462
- import { join as join13 } from "path";
4848
+ import { existsSync as existsSync14, readFileSync as readFileSync15 } from "fs";
4849
+ import { join as join14 } from "path";
4463
4850
  import { spawn as spawn3 } from "child_process";
4464
4851
  function titleToSlug(title) {
4465
4852
  return proposeSlug(title);
@@ -4652,7 +5039,7 @@ function listCmd() {
4652
5039
  for (const slug of slugs) {
4653
5040
  const pg = readPage(slug);
4654
5041
  if (!pg) continue;
4655
- const size = existsSync13(join13(wikiDir(), slug + ".md")) ? (readFileSync14(join13(wikiDir(), slug + ".md"), "utf8").length / 1024).toFixed(1) : "0.0";
5042
+ const size = existsSync14(join14(wikiDir(), slug + ".md")) ? (readFileSync15(join14(wikiDir(), slug + ".md"), "utf8").length / 1024).toFixed(1) : "0.0";
4656
5043
  console.log(` ${c.cyan(pg.title.padEnd(28).slice(0, 28))} ${c.dim(pg.updated || " ")} ${size} KB`);
4657
5044
  }
4658
5045
  console.log("");
@@ -4670,7 +5057,7 @@ function showCmd(name) {
4670
5057
  `));
4671
5058
  return;
4672
5059
  }
4673
- const raw = readFileSync14(join13(wikiDir(), slug + ".md"), "utf8");
5060
+ const raw = readFileSync15(join14(wikiDir(), slug + ".md"), "utf8");
4674
5061
  console.log("\n" + raw.trim() + "\n");
4675
5062
  }
4676
5063
  async function statusCmd(cfg) {
@@ -4740,8 +5127,8 @@ function setupCmd(cfg) {
4740
5127
  `));
4741
5128
  }
4742
5129
  function openCmd() {
4743
- const target = existsSync13(wikiIndexPath()) ? wikiIndexPath() : wikiDir();
4744
- if (!existsSync13(target)) {
5130
+ const target = existsSync14(wikiIndexPath()) ? wikiIndexPath() : wikiDir();
5131
+ if (!existsSync14(target)) {
4745
5132
  console.log(c.dim('\n No wiki yet. Run "holt wiki sync" first.\n'));
4746
5133
  return;
4747
5134
  }
@@ -4828,29 +5215,29 @@ async function wikiCmd(sub, rest = []) {
4828
5215
 
4829
5216
  // src/commands/skill.ts
4830
5217
  import {
4831
- mkdirSync as mkdirSync12,
4832
- writeFileSync as writeFileSync13,
4833
- readFileSync as readFileSync16,
4834
- existsSync as existsSync15,
4835
- readdirSync as readdirSync3,
4836
- statSync as statSync7,
4837
- rmSync as rmSync5,
4838
- cpSync,
5218
+ mkdirSync as mkdirSync13,
5219
+ writeFileSync as writeFileSync14,
5220
+ readFileSync as readFileSync17,
5221
+ existsSync as existsSync16,
5222
+ readdirSync as readdirSync4,
5223
+ statSync as statSync8,
5224
+ rmSync as rmSync6,
5225
+ cpSync as cpSync2,
4839
5226
  mkdtempSync
4840
5227
  } from "fs";
4841
- import { join as join15, resolve as resolve4, sep as sep2, isAbsolute as isAbsolute4 } from "path";
5228
+ import { join as join16, resolve as resolve4, sep as sep2, isAbsolute as isAbsolute4 } from "path";
4842
5229
  import { tmpdir } from "os";
4843
5230
  import { spawnSync as spawnSync4 } from "child_process";
4844
5231
 
4845
5232
  // src/registry.ts
4846
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync12, existsSync as existsSync14, mkdirSync as mkdirSync11, statSync as statSync6 } from "fs";
4847
- import { join as join14, isAbsolute as isAbsolute3, resolve as resolve3 } from "path";
5233
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync13, existsSync as existsSync15, mkdirSync as mkdirSync12, statSync as statSync7 } from "fs";
5234
+ import { join as join15, isAbsolute as isAbsolute3, resolve as resolve3 } from "path";
4848
5235
  import { pathToFileURL, fileURLToPath as fileURLToPath2 } from "url";
4849
5236
  var DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/holt-os/registry/main/registry.json";
4850
5237
  var REGISTRY_REPO_URL = "https://github.com/holt-os/registry";
4851
5238
  var CACHE_TTL_MS = 60 * 60 * 1e3;
4852
5239
  function registryCachePath() {
4853
- return join14(GLOBAL_DIR, "registry-cache.json");
5240
+ return join15(GLOBAL_DIR, "registry-cache.json");
4854
5241
  }
4855
5242
  function registryUrl() {
4856
5243
  const env = (process.env.HOLT_REGISTRY_URL || "").trim();
@@ -4889,8 +5276,8 @@ function parseRegistry(text) {
4889
5276
  function readCache() {
4890
5277
  try {
4891
5278
  const path = registryCachePath();
4892
- if (!existsSync14(path)) return null;
4893
- const raw = JSON.parse(readFileSync15(path, "utf8"));
5279
+ if (!existsSync15(path)) return null;
5280
+ const raw = JSON.parse(readFileSync16(path, "utf8"));
4894
5281
  if (!raw || typeof raw.fetchedAt !== "number" || typeof raw.url !== "string") return null;
4895
5282
  const registry = coerceRegistry(raw.registry);
4896
5283
  if (!registry) return null;
@@ -4901,9 +5288,9 @@ function readCache() {
4901
5288
  }
4902
5289
  function writeCache(url, registry) {
4903
5290
  try {
4904
- mkdirSync11(GLOBAL_DIR, { recursive: true });
5291
+ mkdirSync12(GLOBAL_DIR, { recursive: true });
4905
5292
  const body = { fetchedAt: Date.now(), url, registry };
4906
- writeFileSync12(registryCachePath(), JSON.stringify(body, null, 2) + "\n", "utf8");
5293
+ writeFileSync13(registryCachePath(), JSON.stringify(body, null, 2) + "\n", "utf8");
4907
5294
  } catch {
4908
5295
  }
4909
5296
  }
@@ -4921,10 +5308,10 @@ function localPathFor(loc) {
4921
5308
  var UNREACHABLE_MSG = "no registry reachable; set HOLT_REGISTRY_URL or the community registry is not live yet";
4922
5309
  function loadLocal(path) {
4923
5310
  try {
4924
- if (!existsSync14(path)) {
5311
+ if (!existsSync15(path)) {
4925
5312
  return { registry: null, fromCache: false, error: UNREACHABLE_MSG };
4926
5313
  }
4927
- const registry = parseRegistry(readFileSync15(path, "utf8"));
5314
+ const registry = parseRegistry(readFileSync16(path, "utf8"));
4928
5315
  if (!registry) {
4929
5316
  return { registry: null, fromCache: false, error: "registry file is not valid JSON" };
4930
5317
  }
@@ -4993,6 +5380,12 @@ function buildPublishEntry(input) {
4993
5380
  }
4994
5381
 
4995
5382
  // src/commands/skill.ts
5383
+ function enabledBrains() {
5384
+ const cfg = loadConfig();
5385
+ if (!cfg) return [...BRAIN_IDS];
5386
+ const on2 = BRAIN_IDS.filter((b) => cfg.brains[b].enabled);
5387
+ return on2.length ? on2 : [...BRAIN_IDS];
5388
+ }
4996
5389
  function usage() {
4997
5390
  console.log(c.dim([
4998
5391
  "",
@@ -5053,12 +5446,12 @@ function resolveSubpathDir(fetched, subpath) {
5053
5446
  return target;
5054
5447
  }
5055
5448
  function findSkillDir(root) {
5056
- if (existsSync15(join15(root, "SKILL.md"))) return root;
5449
+ if (existsSync16(join16(root, "SKILL.md"))) return root;
5057
5450
  let subs = [];
5058
5451
  try {
5059
- subs = readdirSync3(root).filter((f) => {
5452
+ subs = readdirSync4(root).filter((f) => {
5060
5453
  try {
5061
- return statSync7(join15(root, f)).isDirectory();
5454
+ return statSync8(join16(root, f)).isDirectory();
5062
5455
  } catch {
5063
5456
  return false;
5064
5457
  }
@@ -5066,8 +5459,8 @@ function findSkillDir(root) {
5066
5459
  } catch {
5067
5460
  return null;
5068
5461
  }
5069
- const withSkill = subs.filter((f) => existsSync15(join15(root, f, "SKILL.md")));
5070
- if (withSkill.length === 1) return join15(root, withSkill[0]);
5462
+ const withSkill = subs.filter((f) => existsSync16(join16(root, f, "SKILL.md")));
5463
+ if (withSkill.length === 1) return join16(root, withSkill[0]);
5071
5464
  return null;
5072
5465
  }
5073
5466
  function cmdList() {
@@ -5108,7 +5501,7 @@ function cmdShow(name) {
5108
5501
  const { skill, body } = loaded;
5109
5502
  console.log("\n" + c.accent(skill.name) + c.dim(` (${skill.scope})`));
5110
5503
  if (skill.description) console.log(" " + skill.description);
5111
- console.log(c.dim(" " + join15(skill.dir, "SKILL.md")));
5504
+ console.log(c.dim(" " + join16(skill.dir, "SKILL.md")));
5112
5505
  console.log("");
5113
5506
  console.log(c.dim(body));
5114
5507
  console.log("");
@@ -5120,19 +5513,19 @@ function cmdCreate(name, global) {
5120
5513
  return;
5121
5514
  }
5122
5515
  const scope = global ? "global" : "workspace";
5123
- const dir = join15(skillsRoot(scope), clean);
5124
- if (existsSync15(dir)) {
5516
+ const dir = join16(skillsRoot(scope), clean);
5517
+ if (existsSync16(dir)) {
5125
5518
  console.error(c.red(`
5126
5519
  A ${scope} skill named "${clean}" already exists.`));
5127
5520
  console.error(c.dim(" " + dir + "\n"));
5128
5521
  process.exitCode = 1;
5129
5522
  return;
5130
5523
  }
5131
- mkdirSync12(dir, { recursive: true });
5132
- writeFileSync13(join15(dir, "SKILL.md"), SKILL_TEMPLATE(clean), "utf8");
5524
+ mkdirSync13(dir, { recursive: true });
5525
+ writeFileSync14(join16(dir, "SKILL.md"), SKILL_TEMPLATE(clean), "utf8");
5133
5526
  console.log(c.green(`
5134
5527
  Created ${scope} skill "${clean}".`));
5135
- console.log(c.dim(" " + join15(dir, "SKILL.md")));
5528
+ console.log(c.dim(" " + join16(dir, "SKILL.md")));
5136
5529
  console.log(c.dim(' Edit it, then run it in chat with "/skill ' + clean + '".\n'));
5137
5530
  }
5138
5531
  function installFromSource(source, scope) {
@@ -5141,7 +5534,7 @@ function installFromSource(source, scope) {
5141
5534
  let tempDir = "";
5142
5535
  try {
5143
5536
  if (isGitUrl(base)) {
5144
- tempDir = mkdtempSync(join15(tmpdir(), "holt-skill-"));
5537
+ tempDir = mkdtempSync(join16(tmpdir(), "holt-skill-"));
5145
5538
  console.log(c.dim(`
5146
5539
  Cloning ${base} ...`));
5147
5540
  const res = spawnSync4("git", ["clone", "--depth", "1", base, tempDir], { stdio: "ignore" });
@@ -5153,7 +5546,7 @@ function installFromSource(source, scope) {
5153
5546
  fetched = tempDir;
5154
5547
  } else {
5155
5548
  fetched = resolve4(base);
5156
- if (!existsSync15(fetched)) {
5549
+ if (!existsSync16(fetched)) {
5157
5550
  console.error(c.red(`
5158
5551
  No such path: ${fetched}
5159
5552
  `));
@@ -5171,7 +5564,7 @@ function installFromSource(source, scope) {
5171
5564
  process.exitCode = 1;
5172
5565
  return false;
5173
5566
  }
5174
- if (!existsSync15(target) || !statSync7(target).isDirectory()) {
5567
+ if (!existsSync16(target) || !statSync8(target).isDirectory()) {
5175
5568
  console.error(c.red(`
5176
5569
  Subpath "${subpath}" does not exist in the source.
5177
5570
  `));
@@ -5191,7 +5584,7 @@ function installFromSource(source, scope) {
5191
5584
  }
5192
5585
  let skillName = "";
5193
5586
  try {
5194
- const { data } = parseFrontmatter(readFileSync16(join15(skillDir, "SKILL.md"), "utf8"));
5587
+ const { data } = parseFrontmatter(readFileSync17(join16(skillDir, "SKILL.md"), "utf8"));
5195
5588
  skillName = sanitizeName(data.name || "");
5196
5589
  } catch {
5197
5590
  skillName = "";
@@ -5202,26 +5595,26 @@ function installFromSource(source, scope) {
5202
5595
  process.exitCode = 1;
5203
5596
  return false;
5204
5597
  }
5205
- const dest = join15(skillsRoot(scope), skillName);
5206
- if (existsSync15(dest)) {
5598
+ const dest = join16(skillsRoot(scope), skillName);
5599
+ if (existsSync16(dest)) {
5207
5600
  console.error(c.red(`
5208
5601
  A ${scope} skill named "${skillName}" already exists.`));
5209
5602
  console.error(c.dim(" " + dest + "\n"));
5210
5603
  process.exitCode = 1;
5211
5604
  return false;
5212
5605
  }
5213
- mkdirSync12(skillsRoot(scope), { recursive: true });
5214
- cpSync(skillDir, dest, { recursive: true });
5215
- const nestedGit = join15(dest, ".git");
5216
- if (existsSync15(nestedGit)) rmSync5(nestedGit, { recursive: true, force: true });
5606
+ mkdirSync13(skillsRoot(scope), { recursive: true });
5607
+ cpSync2(skillDir, dest, { recursive: true });
5608
+ const nestedGit = join16(dest, ".git");
5609
+ if (existsSync16(nestedGit)) rmSync6(nestedGit, { recursive: true, force: true });
5217
5610
  console.log(c.green(`
5218
5611
  Installed ${scope} skill "${skillName}".`));
5219
- console.log(c.dim(" " + join15(dest, "SKILL.md") + "\n"));
5612
+ console.log(c.dim(" " + join16(dest, "SKILL.md") + "\n"));
5220
5613
  return true;
5221
5614
  } finally {
5222
- if (tempDir && existsSync15(tempDir)) {
5615
+ if (tempDir && existsSync16(tempDir)) {
5223
5616
  try {
5224
- rmSync5(tempDir, { recursive: true, force: true });
5617
+ rmSync6(tempDir, { recursive: true, force: true });
5225
5618
  } catch {
5226
5619
  }
5227
5620
  }
@@ -5234,8 +5627,8 @@ async function cmdAdd(source, global, refresh) {
5234
5627
  }
5235
5628
  const scope = global ? "global" : "workspace";
5236
5629
  const { base } = splitSubpath(source);
5237
- if (isGitUrl(base) || existsSync15(resolve4(base))) {
5238
- installFromSource(source, scope);
5630
+ if (isGitUrl(base) || existsSync16(resolve4(base))) {
5631
+ if (installFromSource(source, scope)) syncAllBrains(enabledBrains());
5239
5632
  return;
5240
5633
  }
5241
5634
  const { registry, error } = await loadRegistry({ refresh });
@@ -5256,7 +5649,7 @@ async function cmdAdd(source, global, refresh) {
5256
5649
  }
5257
5650
  console.log(c.dim(`
5258
5651
  Resolved "${hit.name}" -> ${hit.source}`));
5259
- installFromSource(hit.source, scope);
5652
+ if (installFromSource(hit.source, scope)) syncAllBrains(enabledBrains());
5260
5653
  }
5261
5654
  async function cmdSearch(query, refresh) {
5262
5655
  const { registry, error, fromCache } = await loadRegistry({ refresh });
@@ -5306,7 +5699,7 @@ function cmdPublish(name) {
5306
5699
  }
5307
5700
  let raw = "";
5308
5701
  try {
5309
- raw = readFileSync16(join15(loaded.skill.dir, "SKILL.md"), "utf8");
5702
+ raw = readFileSync17(join16(loaded.skill.dir, "SKILL.md"), "utf8");
5310
5703
  } catch {
5311
5704
  console.error(c.red("\n Could not read the skill's SKILL.md.\n"));
5312
5705
  process.exitCode = 1;
@@ -5316,14 +5709,14 @@ function cmdPublish(name) {
5316
5709
  skillName = sanitizeName(data.name || loaded.skill.name);
5317
5710
  description = (data.description || loaded.skill.description || "").trim();
5318
5711
  } else {
5319
- const file = join15(process.cwd(), "SKILL.md");
5320
- if (!existsSync15(file)) {
5712
+ const file = join16(process.cwd(), "SKILL.md");
5713
+ if (!existsSync16(file)) {
5321
5714
  console.error(c.red("\n No SKILL.md in the current directory, and no skill name given."));
5322
5715
  console.error(c.dim(" Usage: holt skill publish [<name>]\n"));
5323
5716
  process.exitCode = 1;
5324
5717
  return;
5325
5718
  }
5326
- const { data } = parseFrontmatter(readFileSync16(file, "utf8"));
5719
+ const { data } = parseFrontmatter(readFileSync17(file, "utf8"));
5327
5720
  skillName = sanitizeName(data.name || process.cwd().split(/[\\/]/).pop() || "");
5328
5721
  description = (data.description || "").trim();
5329
5722
  sourceHint = process.cwd();
@@ -5359,11 +5752,11 @@ async function cmdRemove(name, ask) {
5359
5752
  console.log(c.dim("\n Usage: holt skill remove <name>\n"));
5360
5753
  return;
5361
5754
  }
5362
- const wsDir = join15(skillsRoot("workspace"), clean);
5363
- const globalDir = join15(skillsRoot("global"), clean);
5364
- const target = existsSync15(wsDir) ? wsDir : existsSync15(globalDir) ? globalDir : "";
5755
+ const wsDir = join16(skillsRoot("workspace"), clean);
5756
+ const globalDir = join16(skillsRoot("global"), clean);
5757
+ const target = existsSync16(wsDir) ? wsDir : existsSync16(globalDir) ? globalDir : "";
5365
5758
  if (!target) {
5366
- if (existsSync15(join15(skillsRoot("builtin"), clean))) {
5759
+ if (existsSync16(join16(skillsRoot("builtin"), clean))) {
5367
5760
  console.error(c.red(`
5368
5761
  "${clean}" is a built-in skill and cannot be removed; it ships inside Holt.`));
5369
5762
  console.error(c.dim(" Override it instead: create a workspace or global skill with the same name.\n"));
@@ -5380,7 +5773,9 @@ async function cmdRemove(name, ask) {
5380
5773
  const a = (await ask(`
5381
5774
  Delete ${scope} skill "${clean}"? [y/N] `) ?? "").trim().toLowerCase();
5382
5775
  if (a === "y" || a === "yes") {
5383
- rmSync5(target, { recursive: true, force: true });
5776
+ rmSync6(target, { recursive: true, force: true });
5777
+ removeSkillArtifacts(clean);
5778
+ syncAllBrains(enabledBrains());
5384
5779
  console.log(c.green(" Removed.\n"));
5385
5780
  } else {
5386
5781
  console.log(c.dim(" Kept.\n"));
@@ -5436,8 +5831,8 @@ async function skillCmd(sub, rest = []) {
5436
5831
  }
5437
5832
 
5438
5833
  // src/commands/graph.ts
5439
- import { writeFileSync as writeFileSync14, mkdirSync as mkdirSync13, existsSync as existsSync16 } from "fs";
5440
- import { join as join17, dirname as dirname7, resolve as resolve5 } from "path";
5834
+ import { writeFileSync as writeFileSync15, mkdirSync as mkdirSync14, existsSync as existsSync17 } from "fs";
5835
+ import { join as join18, dirname as dirname8, resolve as resolve5 } from "path";
5441
5836
  import { spawn as spawn4 } from "child_process";
5442
5837
 
5443
5838
  // src/communities.ts
@@ -6234,8 +6629,8 @@ function renderGraphHtml(graph2, meta) {
6234
6629
  }
6235
6630
 
6236
6631
  // src/ingest.ts
6237
- import { readdirSync as readdirSync4, readFileSync as readFileSync17, statSync as statSync8 } from "fs";
6238
- import { join as join16, relative, sep as sep3, extname, dirname as dirname6, basename as basename2 } from "path";
6632
+ import { readdirSync as readdirSync5, readFileSync as readFileSync18, statSync as statSync9 } from "fs";
6633
+ import { join as join17, relative, sep as sep3, extname, dirname as dirname7, basename as basename2 } from "path";
6239
6634
  var CODE_EXT = /* @__PURE__ */ new Set([
6240
6635
  "js",
6241
6636
  "jsx",
@@ -6297,7 +6692,7 @@ function ingestFolder(root, want = { code: true, docs: true }) {
6297
6692
  const dir = stack.pop();
6298
6693
  let entries;
6299
6694
  try {
6300
- entries = readdirSync4(dir).sort((a, b) => a.localeCompare(b));
6695
+ entries = readdirSync5(dir).sort((a, b) => a.localeCompare(b));
6301
6696
  } catch {
6302
6697
  skipped.unreadable++;
6303
6698
  continue;
@@ -6308,10 +6703,10 @@ function ingestFolder(root, want = { code: true, docs: true }) {
6308
6703
  skipped.hitFileCap = true;
6309
6704
  break;
6310
6705
  }
6311
- const full = join16(dir, name);
6706
+ const full = join17(dir, name);
6312
6707
  let st;
6313
6708
  try {
6314
- st = statSync8(full);
6709
+ st = statSync9(full);
6315
6710
  } catch {
6316
6711
  skipped.unreadable++;
6317
6712
  continue;
@@ -6341,7 +6736,7 @@ function ingestFolder(root, want = { code: true, docs: true }) {
6341
6736
  }
6342
6737
  let content;
6343
6738
  try {
6344
- content = readFileSync17(full, "utf8");
6739
+ content = readFileSync18(full, "utf8");
6345
6740
  } catch {
6346
6741
  skipped.unreadable++;
6347
6742
  continue;
@@ -6475,7 +6870,7 @@ function resolvePySpec(f, spec, byId) {
6475
6870
  return null;
6476
6871
  }
6477
6872
  function toPosixDir(id) {
6478
- const d = toPosix(dirname6(id));
6873
+ const d = toPosix(dirname7(id));
6479
6874
  return d === "" || d === "." ? "." : d;
6480
6875
  }
6481
6876
  var MD_LINK_RE = /\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g;
@@ -6647,7 +7042,7 @@ async function graph(args = []) {
6647
7042
  const ingestCode = opts.code;
6648
7043
  const ingestDocs = opts.docs;
6649
7044
  const turns = loadTurns();
6650
- const includeWiki = opts.wiki !== void 0 ? opts.wiki : existsSync16(wikiDir());
7045
+ const includeWiki = opts.wiki !== void 0 ? opts.wiki : existsSync17(wikiDir());
6651
7046
  const wikiPages = includeWiki ? wikiGraphPages() : [];
6652
7047
  let fileNodeCount = 0;
6653
7048
  let docNodeCount = 0;
@@ -6677,9 +7072,9 @@ async function graph(args = []) {
6677
7072
  if (codeGraph) assignCommunities(g);
6678
7073
  if (opts.report) {
6679
7074
  const md = renderReport(g, { workspace: workspace() });
6680
- const reportPath = opts.out ? resolve5(opts.out) : join17(workspace(), "GRAPH_REPORT.md");
6681
- mkdirSync13(dirname7(reportPath), { recursive: true });
6682
- writeFileSync14(reportPath, md, "utf8");
7075
+ const reportPath = opts.out ? resolve5(opts.out) : join18(workspace(), "GRAPH_REPORT.md");
7076
+ mkdirSync14(dirname8(reportPath), { recursive: true });
7077
+ writeFileSync15(reportPath, md, "utf8");
6683
7078
  const communities = new Set(g.nodes.map((n) => n.community ?? "0")).size;
6684
7079
  console.log("\n" + c.accent("Graph report written") + c.dim(" (this folder)"));
6685
7080
  console.log(` nodes ${g.nodes.length} (${fileNodeCount} files, ${docNodeCount} docs)`);
@@ -6699,9 +7094,9 @@ async function graph(args = []) {
6699
7094
  concepts: conceptCount,
6700
7095
  edges: g.edges.length
6701
7096
  });
6702
- const outPath = opts.out ? resolve5(opts.out) : join17(wsHoltDir(), "graph.html");
6703
- mkdirSync13(dirname7(outPath), { recursive: true });
6704
- writeFileSync14(outPath, html, "utf8");
7097
+ const outPath = opts.out ? resolve5(opts.out) : join18(wsHoltDir(), "graph.html");
7098
+ mkdirSync14(dirname8(outPath), { recursive: true });
7099
+ writeFileSync15(outPath, html, "utf8");
6705
7100
  console.log("\n" + c.accent("Memory graph built") + c.dim(" (this folder)"));
6706
7101
  const parts = [`${turns.length} turns`, `${conceptCount} concepts`];
6707
7102
  if (wikiCount) parts.push(`${wikiCount} wiki pages`);
@@ -6989,7 +7384,7 @@ function holtTools() {
6989
7384
  }
6990
7385
 
6991
7386
  // src/version.ts
6992
- var VERSION = "0.14.2";
7387
+ var VERSION = "0.15.0";
6993
7388
 
6994
7389
  // src/commands/mcp.ts
6995
7390
  function printSetup() {
@@ -7111,7 +7506,7 @@ async function statusline() {
7111
7506
  }
7112
7507
 
7113
7508
  // src/commands/run.ts
7114
- import { writeFileSync as writeFileSync15 } from "fs";
7509
+ import { writeFileSync as writeFileSync16 } from "fs";
7115
7510
  import { resolve as resolve6 } from "path";
7116
7511
  var USAGE3 = [
7117
7512
  " holt run <task> [options]",
@@ -7133,7 +7528,7 @@ function parseArgs2(args) {
7133
7528
  let out;
7134
7529
  let noStore = false;
7135
7530
  let noRecall = false;
7136
- let quiet = false;
7531
+ let quiet2 = false;
7137
7532
  for (let i = 0; i < args.length; i++) {
7138
7533
  const a = args[i];
7139
7534
  if (a === "--brain") {
@@ -7153,12 +7548,12 @@ function parseArgs2(args) {
7153
7548
  continue;
7154
7549
  }
7155
7550
  if (a === "--quiet") {
7156
- quiet = true;
7551
+ quiet2 = true;
7157
7552
  continue;
7158
7553
  }
7159
7554
  words2.push(a);
7160
7555
  }
7161
- return { task: words2.join(" ").trim(), brainId, out, noStore, noRecall, quiet };
7556
+ return { task: words2.join(" ").trim(), brainId, out, noStore, noRecall, quiet: quiet2 };
7162
7557
  }
7163
7558
  async function run(args) {
7164
7559
  const p = parseArgs2(args);
@@ -7203,7 +7598,7 @@ async function run(args) {
7203
7598
  if (p.out) {
7204
7599
  const target = resolve6(process.cwd(), p.out);
7205
7600
  try {
7206
- writeFileSync15(target, result.text, "utf8");
7601
+ writeFileSync16(target, result.text, "utf8");
7207
7602
  process.stderr.write(c.dim(`saved to ${target}`) + "\n");
7208
7603
  } catch (e) {
7209
7604
  process.stderr.write(c.red(`Could not write ${target}: ${e.message}`) + "\n");
@@ -7214,8 +7609,8 @@ async function run(args) {
7214
7609
 
7215
7610
  // src/commands/schedule.ts
7216
7611
  import { spawnSync as spawnSync5 } from "child_process";
7217
- import { writeFileSync as writeFileSync16, mkdirSync as mkdirSync14, rmSync as rmSync6, existsSync as existsSync17 } from "fs";
7218
- import { dirname as dirname8 } from "path";
7612
+ import { writeFileSync as writeFileSync17, mkdirSync as mkdirSync15, rmSync as rmSync7, existsSync as existsSync18 } from "fs";
7613
+ import { dirname as dirname9 } from "path";
7219
7614
  function parseAdd(rest) {
7220
7615
  const positional = [];
7221
7616
  const opts = { notify: false };
@@ -7238,8 +7633,8 @@ function parseAdd(rest) {
7238
7633
  }
7239
7634
  function installDarwin(job, holtPath) {
7240
7635
  const path = plistPath(job.id);
7241
- mkdirSync14(dirname8(path), { recursive: true });
7242
- writeFileSync16(path, buildLaunchdPlist(job, holtPath), "utf8");
7636
+ mkdirSync15(dirname9(path), { recursive: true });
7637
+ writeFileSync17(path, buildLaunchdPlist(job, holtPath), "utf8");
7243
7638
  const res = spawnSync5("launchctl", ["load", "-w", path], { encoding: "utf8" });
7244
7639
  if (res.status === 0) {
7245
7640
  console.log(c.green(" Loaded into launchd."));
@@ -7327,10 +7722,10 @@ function listCmd2() {
7327
7722
  }
7328
7723
  function removeDarwin(id) {
7329
7724
  const path = plistPath(id);
7330
- if (existsSync17(path)) {
7725
+ if (existsSync18(path)) {
7331
7726
  spawnSync5("launchctl", ["unload", path], { encoding: "utf8" });
7332
7727
  try {
7333
- rmSync6(path);
7728
+ rmSync7(path);
7334
7729
  } catch {
7335
7730
  }
7336
7731
  }
@@ -7390,29 +7785,29 @@ async function schedule(sub, rest = []) {
7390
7785
 
7391
7786
  // src/commands/routine.ts
7392
7787
  import { spawnSync as spawnSync6 } from "child_process";
7393
- import { writeFileSync as writeFileSync18, mkdirSync as mkdirSync16, rmSync as rmSync7, existsSync as existsSync18 } from "fs";
7394
- import { dirname as dirname9, resolve as resolve7 } from "path";
7788
+ import { writeFileSync as writeFileSync19, mkdirSync as mkdirSync17, rmSync as rmSync8, existsSync as existsSync19 } from "fs";
7789
+ import { dirname as dirname10, resolve as resolve7 } from "path";
7395
7790
 
7396
7791
  // src/routines.ts
7397
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync17, mkdirSync as mkdirSync15 } from "fs";
7398
- import { join as join18 } from "path";
7792
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync18, mkdirSync as mkdirSync16 } from "fs";
7793
+ import { join as join19 } from "path";
7399
7794
  function sanitizeRoutineName(name) {
7400
7795
  return (name || "").trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
7401
7796
  }
7402
7797
  function routinesPath() {
7403
- return join18(GLOBAL_DIR, "routines.json");
7798
+ return join19(GLOBAL_DIR, "routines.json");
7404
7799
  }
7405
7800
  function loadRoutines() {
7406
7801
  try {
7407
- const raw = JSON.parse(readFileSync18(routinesPath(), "utf8"));
7802
+ const raw = JSON.parse(readFileSync19(routinesPath(), "utf8"));
7408
7803
  return Array.isArray(raw) ? raw : [];
7409
7804
  } catch {
7410
7805
  return [];
7411
7806
  }
7412
7807
  }
7413
7808
  function saveRoutines(routines) {
7414
- mkdirSync15(GLOBAL_DIR, { recursive: true });
7415
- writeFileSync17(routinesPath(), JSON.stringify(routines, null, 2) + "\n", "utf8");
7809
+ mkdirSync16(GLOBAL_DIR, { recursive: true });
7810
+ writeFileSync18(routinesPath(), JSON.stringify(routines, null, 2) + "\n", "utf8");
7416
7811
  }
7417
7812
  function findRoutine(name) {
7418
7813
  const want = sanitizeRoutineName(name);
@@ -7472,8 +7867,8 @@ function routineJobId(name) {
7472
7867
  }
7473
7868
  function installDarwin2(job, holtPath) {
7474
7869
  const path = plistPath(job.id);
7475
- mkdirSync16(dirname9(path), { recursive: true });
7476
- writeFileSync18(path, buildLaunchdPlist(job, holtPath), "utf8");
7870
+ mkdirSync17(dirname10(path), { recursive: true });
7871
+ writeFileSync19(path, buildLaunchdPlist(job, holtPath), "utf8");
7477
7872
  const res = spawnSync6("launchctl", ["load", "-w", path], { encoding: "utf8" });
7478
7873
  if (res.status === 0) {
7479
7874
  console.log(c.green(" Loaded into launchd."));
@@ -7517,10 +7912,10 @@ function installSchedule(r) {
7517
7912
  }
7518
7913
  function removeScheduleDarwin(id) {
7519
7914
  const path = plistPath(id);
7520
- if (existsSync18(path)) {
7915
+ if (existsSync19(path)) {
7521
7916
  spawnSync6("launchctl", ["unload", path], { encoding: "utf8" });
7522
7917
  try {
7523
- rmSync7(path);
7918
+ rmSync8(path);
7524
7919
  } catch {
7525
7920
  }
7526
7921
  }
@@ -7648,7 +8043,7 @@ function buildRoutinePrompt(r) {
7648
8043
  return { prompt };
7649
8044
  }
7650
8045
  async function runCmd(rest) {
7651
- let quiet = false;
8046
+ let quiet2 = false;
7652
8047
  let outOverride;
7653
8048
  let notifyOverride = false;
7654
8049
  let brainOverride;
@@ -7656,7 +8051,7 @@ async function runCmd(rest) {
7656
8051
  for (let i = 0; i < rest.length; i++) {
7657
8052
  const a = rest[i];
7658
8053
  if (a === void 0) continue;
7659
- if (a === "--quiet") quiet = true;
8054
+ if (a === "--quiet") quiet2 = true;
7660
8055
  else if (a === "--notify") notifyOverride = true;
7661
8056
  else if (a === "--out") outOverride = rest[++i];
7662
8057
  else if (a === "--brain") brainOverride = rest[++i];
@@ -7697,7 +8092,7 @@ async function runCmd(rest) {
7697
8092
  let streamed = false;
7698
8093
  const result = await runTask(built.prompt, {
7699
8094
  ...brainId ? { brainId } : {},
7700
- onChunk: quiet ? void 0 : (ch) => {
8095
+ onChunk: quiet2 ? void 0 : (ch) => {
7701
8096
  streamed = true;
7702
8097
  process.stdout.write(ch);
7703
8098
  }
@@ -7708,13 +8103,13 @@ async function runCmd(rest) {
7708
8103
  process.exitCode = 1;
7709
8104
  return;
7710
8105
  }
7711
- if (!quiet && !streamed) process.stdout.write(result.text);
7712
- 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");
7713
8108
  if (outTarget) {
7714
8109
  const target = resolve7(routine2.workspace, outTarget);
7715
8110
  try {
7716
- writeFileSync18(target, result.text, "utf8");
7717
- if (!quiet) process.stderr.write(c.dim(`saved to ${target}`) + "\n");
8111
+ writeFileSync19(target, result.text, "utf8");
8112
+ if (!quiet2) process.stderr.write(c.dim(`saved to ${target}`) + "\n");
7718
8113
  } catch (e) {
7719
8114
  process.stderr.write(c.red(`Could not write ${target}: ${e.message}`) + "\n");
7720
8115
  process.exitCode = 1;
@@ -7851,7 +8246,7 @@ async function routine(sub, rest = []) {
7851
8246
  }
7852
8247
 
7853
8248
  // src/commands/notify.ts
7854
- import { readFileSync as readFileSync19 } from "fs";
8249
+ import { readFileSync as readFileSync20 } from "fs";
7855
8250
  async function notify(args) {
7856
8251
  const nonFlag = args.filter((a) => a !== "-" && !a.startsWith("--"));
7857
8252
  const wantsStdin = args.includes("-") || !process.stdin.isTTY;
@@ -7884,12 +8279,16 @@ async function notify(args) {
7884
8279
  }
7885
8280
  function readStdin3() {
7886
8281
  try {
7887
- return readFileSync19(0, "utf8");
8282
+ return readFileSync20(0, "utf8");
7888
8283
  } catch {
7889
8284
  return "";
7890
8285
  }
7891
8286
  }
7892
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
+
7893
8292
  // src/specs.ts
7894
8293
  import os from "os";
7895
8294
  var BYTES_PER_GB = 1024 ** 3;
@@ -8009,10 +8408,63 @@ function good(text) {
8009
8408
  function warn(text) {
8010
8409
  console.log(" " + c.red(text));
8011
8410
  }
8012
- 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 = [];
8013
8465
  const specs = detect();
8014
8466
  const steps = [];
8015
- 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"));
8016
8468
  header("Machine");
8017
8469
  row("platform", `${specs.platform} / ${specs.arch}`);
8018
8470
  row("cpu", `${specs.cpuModel}${specs.cpuCount ? ` (${specs.cpuCount} cores)` : ""}`);
@@ -8049,13 +8501,29 @@ async function doctor() {
8049
8501
  } catch {
8050
8502
  embedOk = false;
8051
8503
  }
8504
+ if (!embedOk && fix) {
8505
+ embedOk = await fixSemanticMemory(fixed);
8506
+ }
8052
8507
  if (embedOk) {
8053
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
+ }
8054
8520
  } else {
8055
8521
  note(`Ollama with "${EMBED_MODEL}" not reachable at ${OLLAMA_URL3}.`);
8056
8522
  note("Without it, Holt still remembers, but recall falls back to keyword match.");
8057
- warn(`Recommendation: enable local embeddings. Install Ollama, then: ollama pull ${EMBED_MODEL}`);
8058
- 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
+ }
8059
8527
  }
8060
8528
  header("Knowledge wiki maintainer");
8061
8529
  const rec = recommendLocalModel2(specs.totalRamGB);
@@ -8081,6 +8549,12 @@ async function doctor() {
8081
8549
  }
8082
8550
  note("Hosting tip: a low-power always-on machine (an old laptop, a Pi) is ideal for the bot and scheduled runs.");
8083
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
+ }
8084
8558
  header("Recommended next steps");
8085
8559
  if (!cfg) {
8086
8560
  steps.unshift('Set up this folder: run "holt init".');
@@ -8094,8 +8568,167 @@ async function doctor() {
8094
8568
  console.log("");
8095
8569
  }
8096
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
+
8097
8730
  // src/commands/write.ts
8098
- import { writeFileSync as writeFileSync19 } from "fs";
8731
+ import { writeFileSync as writeFileSync20 } from "fs";
8099
8732
  import { resolve as resolve8 } from "path";
8100
8733
 
8101
8734
  // src/antiai.ts
@@ -8276,7 +8909,7 @@ async function write(args) {
8276
8909
  if (p.out) {
8277
8910
  const target = resolve8(process.cwd(), p.out);
8278
8911
  try {
8279
- writeFileSync19(target, draft + "\n", "utf8");
8912
+ writeFileSync20(target, draft + "\n", "utf8");
8280
8913
  process.stderr.write(c.dim(`saved to ${target}`) + "\n");
8281
8914
  } catch (e) {
8282
8915
  process.stderr.write(c.red(`Could not write ${target}: ${e.message}`) + "\n");
@@ -8285,6 +8918,37 @@ async function write(args) {
8285
8918
  }
8286
8919
  }
8287
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
+
8288
8952
  // src/cli.ts
8289
8953
  var BANNER = `
8290
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
@@ -8298,40 +8962,53 @@ var BANNER = `
8298
8962
  var HELP = `${BANNER}
8299
8963
  Usage: holt <command>
8300
8964
 
8301
- Commands:
8302
- (no command) Start your assistant: sets up if needed, then launches the real
8303
- interactive brain (Claude Code, Codex, Gemini), branded as Holt
8304
- init Trust this folder, choose and install brains, sign in, set defaults
8305
- launch Same as bare "holt": start your assistant
8306
- chat Lightweight REPL that remembers past sessions (used for API brains)
8307
- run <task> Run one task non-interactively: recall, brain executes, remember
8308
- schedule Fire "holt run" on a timer: holt schedule [add | list | remove]
8309
- routine Named, reusable, scheduled jobs: holt routine [add | run | list | remove]
8310
- telegram Chat with Holt from your phone: holt telegram [setup]
8311
- notify [msg] Push a message to your phone over Telegram (stdin-friendly)
8312
- 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"
8313
8974
  write <what> Draft content in your voice, with anti-AI checks: holt write "..." [--type]
8314
- memory Inspect memory: holt memory [search <query> | embed | clear]
8315
- wiki Your derived knowledge wiki: holt wiki [sync | rebuild | lint | list | show]
8316
- graph See your memory as an interactive knowledge graph in the browser
8317
- skill Manage skills: holt skill [list | show | create | add | remove]
8318
- doctor Check this machine and recommend how best to run Holt here
8319
- mcp Serve this folder's memory to Claude Code, Cursor, Codex (holt mcp setup)
8320
- hook Ambient memory for Claude Code: holt hook [install | remove | status]
8321
- 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)
8322
8989
  login <brain> Sign in to a brain: claude, codex, or gemini
8323
- version Print the Holt version
8324
- 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
8325
8998
 
8326
8999
  Holt runs in the folder you launch it from and asks to trust it first.
8327
- Brains are agent CLIs on your machine (claude, codex, gemini) or direct
8328
- 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.
8329
9002
 
8330
9003
  Docs: https://productsdecoded.com/holt
8331
9004
  Repo: https://github.com/holt-os/holt
8332
9005
  `;
8333
9006
  async function main() {
8334
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
+ }
8335
9012
  switch (cmd) {
8336
9013
  case void 0:
8337
9014
  await launch();
@@ -8400,7 +9077,16 @@ async function main() {
8400
9077
  await notify(process.argv.slice(3));
8401
9078
  break;
8402
9079
  case "doctor":
8403
- 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();
8404
9090
  break;
8405
9091
  case "voice":
8406
9092
  await voice(process.argv[3], process.argv.slice(4));
@@ -8416,4 +9102,6 @@ async function main() {
8416
9102
  process.exitCode = 1;
8417
9103
  }
8418
9104
  }
8419
- main();
9105
+ process.on("uncaughtException", handleCrash);
9106
+ process.on("unhandledRejection", handleCrash);
9107
+ main().catch(handleCrash);