@holt-os/holt 0.14.0 → 0.14.2

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 +1 -1
  2. package/dist/cli.js +397 -286
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,7 +8,7 @@ Holt is an open-source, self-hosted personal agent OS. Clone it, pick your skill
8
8
 
9
9
  > **Status: shipping and usable today.** Working now:
10
10
  >
11
- > - `holt init` + `holt chat` with any brain (Claude Code, Codex, Gemini, or a direct API key), and mid-conversation brain switching that keeps context.
11
+ > - Bare `holt` launches the real interactive brain (Claude Code, Codex, or Gemini) branded as Holt, setting the folder up the first time. `holt chat` stays as a lightweight REPL for direct API brains, with mid-conversation brain switching that keeps context.
12
12
  > - Per-folder memory with semantic recall and distilled facts, ambient memory for Claude Code (`holt hook`), and opt-in memory shared across folders (`holt memory global`).
13
13
  > - A self-maintaining knowledge wiki (`holt wiki`) and an interactive knowledge graph (`holt graph`) that can also ingest your code and docs into communities.
14
14
  > - Run a task once, on a schedule, or as a named routine (`holt run` / `schedule` / `routine`), and reach it from your phone over Telegram.
package/dist/cli.js CHANGED
@@ -1737,7 +1737,7 @@ function parseStyleProfile(raw) {
1737
1737
  }
1738
1738
  if (!obj || typeof obj !== "object") return null;
1739
1739
  const o = obj;
1740
- const str = (x) => typeof x === "string" && x.trim() ? x.trim() : void 0;
1740
+ const str2 = (x) => typeof x === "string" && x.trim() ? x.trim() : void 0;
1741
1741
  const arr = (x) => {
1742
1742
  if (!Array.isArray(x)) return void 0;
1743
1743
  const out = x.filter((i) => typeof i === "string" && i.trim().length > 0).map((i) => i.trim());
@@ -1748,17 +1748,17 @@ function parseStyleProfile(raw) {
1748
1748
  return Number.isFinite(n) ? Math.max(1, Math.min(5, Math.round(n))) : void 0;
1749
1749
  };
1750
1750
  const profile = {
1751
- tone: str(o["tone"]),
1751
+ tone: str2(o["tone"]),
1752
1752
  formality: num(o["formality"]),
1753
- avgSentenceLength: str(o["avgSentenceLength"]),
1754
- person: str(o["person"]),
1755
- emoji: str(o["emoji"]),
1756
- formatting: str(o["formatting"]),
1753
+ avgSentenceLength: str2(o["avgSentenceLength"]),
1754
+ person: str2(o["person"]),
1755
+ emoji: str2(o["emoji"]),
1756
+ formatting: str2(o["formatting"]),
1757
1757
  signatureMoves: arr(o["signatureMoves"]),
1758
1758
  bannedWords: arr(o["bannedWords"]),
1759
1759
  targetAudiences: arr(o["targetAudiences"]),
1760
- soundsLike: str(o["soundsLike"]),
1761
- doesNotSoundLike: str(o["doesNotSoundLike"])
1760
+ soundsLike: str2(o["soundsLike"]),
1761
+ doesNotSoundLike: str2(o["doesNotSoundLike"])
1762
1762
  };
1763
1763
  const any = Object.values(profile).some((v) => v !== void 0);
1764
1764
  return any ? profile : null;
@@ -2220,14 +2220,154 @@ Default brain? [${chosen.join("/")}] (${defPick}): `) ?? "").trim();
2220
2220
  }
2221
2221
 
2222
2222
  // src/commands/launch.ts
2223
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync10, existsSync as existsSync11, mkdirSync as mkdirSync9, copyFileSync as copyFileSync2 } from "fs";
2224
- import { join as join11, dirname as dirname5 } from "path";
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";
2225
2226
 
2226
- // src/commands/hook.ts
2227
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, existsSync as existsSync9, mkdirSync as mkdirSync8, copyFileSync, realpathSync as realpathSync2, appendFileSync as appendFileSync2, statSync as statSync4 } from "fs";
2228
- import { spawnSync as spawnSync2 } from "child_process";
2229
- import { join as join9, dirname as dirname3 } from "path";
2227
+ // src/scheduler.ts
2230
2228
  import { homedir as homedir3 } from "os";
2229
+ import { join as join8 } from "path";
2230
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
2231
+ import { randomUUID as randomUUID3 } from "crypto";
2232
+ import { spawnSync as spawnSync2 } from "child_process";
2233
+ function schedulesPath() {
2234
+ return join8(GLOBAL_DIR, "schedules.json");
2235
+ }
2236
+ function logsDir() {
2237
+ return join8(GLOBAL_DIR, "logs");
2238
+ }
2239
+ function logPath(id) {
2240
+ return join8(logsDir(), `${id}.log`);
2241
+ }
2242
+ function plistPath(id) {
2243
+ return join8(homedir3(), "Library", "LaunchAgents", `com.holt.${id}.plist`);
2244
+ }
2245
+ function loadJobs() {
2246
+ try {
2247
+ const raw = JSON.parse(readFileSync9(schedulesPath(), "utf8"));
2248
+ return Array.isArray(raw) ? raw : [];
2249
+ } catch {
2250
+ return [];
2251
+ }
2252
+ }
2253
+ function saveJobs(jobs) {
2254
+ mkdirSync7(GLOBAL_DIR, { recursive: true });
2255
+ writeFileSync7(schedulesPath(), JSON.stringify(jobs, null, 2) + "\n", "utf8");
2256
+ }
2257
+ function addJob(job) {
2258
+ const jobs = loadJobs();
2259
+ jobs.push(job);
2260
+ saveJobs(jobs);
2261
+ return jobs;
2262
+ }
2263
+ function removeJob(id) {
2264
+ const jobs = loadJobs().filter((j) => j.id !== id);
2265
+ saveJobs(jobs);
2266
+ return jobs;
2267
+ }
2268
+ function newJobId() {
2269
+ return randomUUID3().slice(0, 8);
2270
+ }
2271
+ function parseWhen(when) {
2272
+ const m = /^([01]?\d|2[0-3]):([0-5]\d)$/.exec(when.trim());
2273
+ if (!m) {
2274
+ throw new Error(
2275
+ `Could not read time "${when}". Use 24h HH:MM for a daily run, e.g. 07:00 or 23:30.`
2276
+ );
2277
+ }
2278
+ const hour = Number(m[1]);
2279
+ const minute = Number(m[2]);
2280
+ return { hour, minute };
2281
+ }
2282
+ function shQuote(s) {
2283
+ return `'` + s.replace(/'/g, `'\\''`) + `'`;
2284
+ }
2285
+ function resolveHoltPath() {
2286
+ try {
2287
+ const finder = process.platform === "win32" ? "where" : "which";
2288
+ const res = spawnSync2(finder, ["holt"], { encoding: "utf8" });
2289
+ if (res.status === 0 && typeof res.stdout === "string") {
2290
+ const first = res.stdout.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
2291
+ if (first) return first;
2292
+ }
2293
+ } catch {
2294
+ }
2295
+ return "holt";
2296
+ }
2297
+ function buildCommand(job, holtPath) {
2298
+ const holt = shQuote(holtPath);
2299
+ const ws = shQuote(job.workspace);
2300
+ if (job.runArgs && job.runArgs.length > 0) {
2301
+ const parts = job.runArgs.map(shQuote).join(" ");
2302
+ return `cd ${ws} && ${holt} ${parts} --quiet`;
2303
+ }
2304
+ const log = shQuote(logPath(job.id));
2305
+ let cmd = `cd ${ws} && ${holt} run ${shQuote(job.task)}`;
2306
+ if (job.brain) cmd += ` --brain ${shQuote(job.brain)}`;
2307
+ cmd += ` --quiet --out ${log}`;
2308
+ if (job.notify) cmd += ` && ${holt} notify "$(cat ${log})"`;
2309
+ return cmd;
2310
+ }
2311
+ function xmlEscape(s) {
2312
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
2313
+ }
2314
+ function buildLaunchdPlist(job, holtPath) {
2315
+ const { hour, minute } = parseWhen(job.when);
2316
+ const command = buildCommand(job, holtPath);
2317
+ const log = logPath(job.id);
2318
+ const args = ["/bin/sh", "-lc", command];
2319
+ const argXml = args.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
2320
+ return `<?xml version="1.0" encoding="UTF-8"?>
2321
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2322
+ <plist version="1.0">
2323
+ <dict>
2324
+ <key>Label</key>
2325
+ <string>com.holt.${xmlEscape(job.id)}</string>
2326
+ <key>ProgramArguments</key>
2327
+ <array>
2328
+ ${argXml}
2329
+ </array>
2330
+ <key>StartCalendarInterval</key>
2331
+ <dict>
2332
+ <key>Hour</key>
2333
+ <integer>${hour}</integer>
2334
+ <key>Minute</key>
2335
+ <integer>${minute}</integer>
2336
+ </dict>
2337
+ <key>RunAtLoad</key>
2338
+ <false/>
2339
+ <key>StandardOutPath</key>
2340
+ <string>${xmlEscape(log)}</string>
2341
+ <key>StandardErrorPath</key>
2342
+ <string>${xmlEscape(log)}</string>
2343
+ </dict>
2344
+ </plist>
2345
+ `;
2346
+ }
2347
+ var CRON_MARKER = "holt:";
2348
+ function buildCronLine(job, holtPath) {
2349
+ const { hour, minute } = parseWhen(job.when);
2350
+ const command = buildCommand(job, holtPath);
2351
+ return `${minute} ${hour} * * * ${command} # ${CRON_MARKER}${job.id}`;
2352
+ }
2353
+ function stripCronLines(crontab, id) {
2354
+ const marker = `# ${CRON_MARKER}${id}`;
2355
+ const kept = crontab.split("\n").filter((line) => !line.includes(marker));
2356
+ return kept.join("\n").replace(/\n+$/, "\n");
2357
+ }
2358
+ function appendCronLine(crontab, line) {
2359
+ const base = crontab.replace(/\s*$/, "");
2360
+ return (base ? base + "\n" : "") + line + "\n";
2361
+ }
2362
+ function ensureLogsDir() {
2363
+ mkdirSync7(logsDir(), { recursive: true });
2364
+ }
2365
+
2366
+ // 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";
2368
+ import { spawnSync as spawnSync3 } from "child_process";
2369
+ import { join as join10, dirname as dirname3 } from "path";
2370
+ import { homedir as homedir4 } from "os";
2231
2371
 
2232
2372
  // src/facts.ts
2233
2373
  var MIN_EXCHANGES_FOR_EXTRACTION = 3;
@@ -2343,15 +2483,15 @@ async function extractAndSaveFacts(active, cfg, history, session) {
2343
2483
 
2344
2484
  // src/wiki.ts
2345
2485
  import {
2346
- readFileSync as readFileSync9,
2347
- writeFileSync as writeFileSync7,
2486
+ readFileSync as readFileSync10,
2487
+ writeFileSync as writeFileSync8,
2348
2488
  existsSync as existsSync8,
2349
- mkdirSync as mkdirSync7,
2489
+ mkdirSync as mkdirSync8,
2350
2490
  rmSync as rmSync4,
2351
2491
  readdirSync as readdirSync2,
2352
2492
  statSync as statSync3
2353
2493
  } from "fs";
2354
- import { join as join8 } from "path";
2494
+ import { join as join9 } from "path";
2355
2495
  import { totalmem } from "os";
2356
2496
 
2357
2497
  // src/localmodel.ts
@@ -2406,28 +2546,28 @@ function pullHint(model) {
2406
2546
 
2407
2547
  // src/wiki.ts
2408
2548
  function wikiDir() {
2409
- return join8(wsHoltDir(), "wiki");
2549
+ return join9(wsHoltDir(), "wiki");
2410
2550
  }
2411
2551
  function wikiStatePath() {
2412
- return join8(wikiDir(), ".state.json");
2552
+ return join9(wikiDir(), ".state.json");
2413
2553
  }
2414
2554
  function wikiIndexPath() {
2415
- return join8(wikiDir(), "index.md");
2555
+ return join9(wikiDir(), "index.md");
2416
2556
  }
2417
2557
  function pagePath(slug) {
2418
- return join8(wikiDir(), `${slug}.md`);
2558
+ return join9(wikiDir(), `${slug}.md`);
2419
2559
  }
2420
2560
  function loadState() {
2421
2561
  try {
2422
- const s = JSON.parse(readFileSync9(wikiStatePath(), "utf8"));
2562
+ const s = JSON.parse(readFileSync10(wikiStatePath(), "utf8"));
2423
2563
  return { lastSyncTs: typeof s.lastSyncTs === "number" ? s.lastSyncTs : 0, updatedAt: s.updatedAt ?? 0 };
2424
2564
  } catch {
2425
2565
  return { lastSyncTs: 0, updatedAt: 0 };
2426
2566
  }
2427
2567
  }
2428
2568
  function saveState(s) {
2429
- mkdirSync7(wikiDir(), { recursive: true });
2430
- writeFileSync7(wikiStatePath(), JSON.stringify(s, null, 2) + "\n", "utf8");
2569
+ mkdirSync8(wikiDir(), { recursive: true });
2570
+ writeFileSync8(wikiStatePath(), JSON.stringify(s, null, 2) + "\n", "utf8");
2431
2571
  }
2432
2572
  function saveSyncMarker(ts) {
2433
2573
  saveState({ lastSyncTs: ts, updatedAt: Date.now() });
@@ -2449,7 +2589,7 @@ function readPage(slug) {
2449
2589
  if (!existsSync8(p)) return null;
2450
2590
  let raw = "";
2451
2591
  try {
2452
- raw = readFileSync9(p, "utf8");
2592
+ raw = readFileSync10(p, "utf8");
2453
2593
  } catch {
2454
2594
  return null;
2455
2595
  }
@@ -2491,8 +2631,8 @@ function renderPage(pg) {
2491
2631
  return fm;
2492
2632
  }
2493
2633
  function writePage(pg) {
2494
- mkdirSync7(wikiDir(), { recursive: true });
2495
- writeFileSync7(pagePath(pg.slug), renderPage(pg), "utf8");
2634
+ mkdirSync8(wikiDir(), { recursive: true });
2635
+ writeFileSync8(pagePath(pg.slug), renderPage(pg), "utf8");
2496
2636
  }
2497
2637
  var WIKILINK_RE = /\[\[([^\]]+)\]\]/g;
2498
2638
  function extractLinks(body) {
@@ -2769,8 +2909,8 @@ function renderIndex(pages) {
2769
2909
  return lines.join("\n");
2770
2910
  }
2771
2911
  function writeIndex(pages) {
2772
- mkdirSync7(wikiDir(), { recursive: true });
2773
- writeFileSync7(wikiIndexPath(), renderIndex(pages), "utf8");
2912
+ mkdirSync8(wikiDir(), { recursive: true });
2913
+ writeFileSync8(wikiIndexPath(), renderIndex(pages), "utf8");
2774
2914
  }
2775
2915
  var WIKI_SESSION = "wiki";
2776
2916
  function wikiRowId(slug) {
@@ -2785,9 +2925,9 @@ async function indexPagesForRecall(pages) {
2785
2925
  if (pages.length === 0) return;
2786
2926
  const slugs = new Set(pages.map((p) => sanitizeName(p.slug).slice(0, 24)));
2787
2927
  const kept = dropWikiRows(slugs);
2788
- mkdirSync7(join8(wsHoltDir(), "memory"), { recursive: true });
2789
- writeFileSync7(
2790
- join8(wsHoltDir(), "memory", "turns.jsonl"),
2928
+ mkdirSync8(join9(wsHoltDir(), "memory"), { recursive: true });
2929
+ writeFileSync8(
2930
+ join9(wsHoltDir(), "memory", "turns.jsonl"),
2791
2931
  kept.map((t) => JSON.stringify(t)).join("\n") + (kept.length ? "\n" : ""),
2792
2932
  "utf8"
2793
2933
  );
@@ -2809,9 +2949,9 @@ ${pg.body}`.slice(0, 4e3);
2809
2949
  function clearWikiRecallRows() {
2810
2950
  try {
2811
2951
  const kept = loadTurns().filter((t) => t.session !== WIKI_SESSION);
2812
- mkdirSync7(join8(wsHoltDir(), "memory"), { recursive: true });
2813
- writeFileSync7(
2814
- join8(wsHoltDir(), "memory", "turns.jsonl"),
2952
+ mkdirSync8(join9(wsHoltDir(), "memory"), { recursive: true });
2953
+ writeFileSync8(
2954
+ join9(wsHoltDir(), "memory", "turns.jsonl"),
2815
2955
  kept.map((t) => JSON.stringify(t)).join("\n") + (kept.length ? "\n" : ""),
2816
2956
  "utf8"
2817
2957
  );
@@ -2865,23 +3005,23 @@ var HOOK_CMD = {
2865
3005
  capture: "holt hook capture"
2866
3006
  };
2867
3007
  function globalSettingsPath() {
2868
- return join9(homedir3(), ".claude", "settings.json");
3008
+ return join10(homedir4(), ".claude", "settings.json");
2869
3009
  }
2870
3010
  function projectSettingsPath() {
2871
- return join9(process.cwd(), ".claude", "settings.json");
3011
+ return join10(process.cwd(), ".claude", "settings.json");
2872
3012
  }
2873
3013
  function readSettings(path) {
2874
3014
  if (!existsSync9(path)) return {};
2875
3015
  try {
2876
- const parsed = JSON.parse(readFileSync10(path, "utf8"));
3016
+ const parsed = JSON.parse(readFileSync11(path, "utf8"));
2877
3017
  return parsed && typeof parsed === "object" ? parsed : {};
2878
3018
  } catch {
2879
3019
  throw new Error(`could not parse ${path} (not valid JSON) - leaving it untouched`);
2880
3020
  }
2881
3021
  }
2882
3022
  function writeSettings(path, settings) {
2883
- mkdirSync8(dirname3(path), { recursive: true });
2884
- writeFileSync8(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
3023
+ mkdirSync9(dirname3(path), { recursive: true });
3024
+ writeFileSync9(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
2885
3025
  }
2886
3026
  function backup(path) {
2887
3027
  if (!existsSync9(path)) return null;
@@ -3070,8 +3210,8 @@ function guardTrusted(real) {
3070
3210
  }
3071
3211
  function hookLog(msg) {
3072
3212
  try {
3073
- mkdirSync8(GLOBAL_DIR, { recursive: true });
3074
- appendFileSync2(join9(GLOBAL_DIR, "hooks.log"), `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
3213
+ mkdirSync9(GLOBAL_DIR, { recursive: true });
3214
+ appendFileSync2(join10(GLOBAL_DIR, "hooks.log"), `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
3075
3215
  `, "utf8");
3076
3216
  } catch {
3077
3217
  }
@@ -3086,11 +3226,11 @@ function fileBytes(path) {
3086
3226
  }
3087
3227
  var THROTTLE_MIN_NEW_EXCHANGES = 2;
3088
3228
  function hookStatePath() {
3089
- return join9(GLOBAL_DIR, "hook-state.json");
3229
+ return join10(GLOBAL_DIR, "hook-state.json");
3090
3230
  }
3091
3231
  function readHookState() {
3092
3232
  try {
3093
- const parsed = JSON.parse(readFileSync10(hookStatePath(), "utf8"));
3233
+ const parsed = JSON.parse(readFileSync11(hookStatePath(), "utf8"));
3094
3234
  if (!parsed || typeof parsed !== "object") return {};
3095
3235
  return parsed;
3096
3236
  } catch {
@@ -3103,10 +3243,10 @@ function lastProcessedExchanges(state, sessionId) {
3103
3243
  }
3104
3244
  function saveHookState(sessionId, exchanges) {
3105
3245
  try {
3106
- mkdirSync8(GLOBAL_DIR, { recursive: true });
3246
+ mkdirSync9(GLOBAL_DIR, { recursive: true });
3107
3247
  const state = readHookState();
3108
3248
  state[sessionId] = { lastExchanges: exchanges };
3109
- writeFileSync8(hookStatePath(), JSON.stringify(state, null, 2) + "\n", "utf8");
3249
+ writeFileSync9(hookStatePath(), JSON.stringify(state, null, 2) + "\n", "utf8");
3110
3250
  } catch {
3111
3251
  }
3112
3252
  }
@@ -3180,7 +3320,7 @@ function readTranscript(path) {
3180
3320
  const out = [];
3181
3321
  let raw;
3182
3322
  try {
3183
- raw = readFileSync10(path, "utf8");
3323
+ raw = readFileSync11(path, "utf8");
3184
3324
  } catch {
3185
3325
  return out;
3186
3326
  }
@@ -3211,24 +3351,24 @@ function resolveCommandPath(command) {
3211
3351
  if (command.startsWith("/") && existsSync9(command)) return command;
3212
3352
  try {
3213
3353
  const shell = process.env.SHELL || "/bin/sh";
3214
- const res = spawnSync2(shell, ["-lc", "command -v " + command], { encoding: "utf8" });
3354
+ const res = spawnSync3(shell, ["-lc", "command -v " + command], { encoding: "utf8" });
3215
3355
  if (res.status === 0) {
3216
3356
  const p = (res.stdout || "").trim().split("\n")[0]?.trim();
3217
3357
  if (p && p.startsWith("/") && existsSync9(p)) return p;
3218
3358
  }
3219
3359
  } catch {
3220
3360
  }
3221
- const home = homedir3();
3361
+ const home = homedir4();
3222
3362
  const dirs = [
3223
- join9(home, ".local", "bin"),
3363
+ join10(home, ".local", "bin"),
3224
3364
  "/opt/homebrew/bin",
3225
3365
  "/usr/local/bin",
3226
- join9(home, ".npm-global", "bin"),
3366
+ join10(home, ".npm-global", "bin"),
3227
3367
  "/usr/bin",
3228
3368
  "/bin"
3229
3369
  ];
3230
3370
  for (const dir of dirs) {
3231
- const candidate = join9(dir, command);
3371
+ const candidate = join10(dir, command);
3232
3372
  if (existsSync9(candidate)) return candidate;
3233
3373
  }
3234
3374
  return null;
@@ -3303,7 +3443,7 @@ async function capture() {
3303
3443
  let bytes = 0;
3304
3444
  let lines = 0;
3305
3445
  try {
3306
- const rawT = readFileSync10(data.transcript_path, "utf8");
3446
+ const rawT = readFileSync11(data.transcript_path, "utf8");
3307
3447
  bytes = Buffer.byteLength(rawT, "utf8");
3308
3448
  lines = countLines(rawT);
3309
3449
  } catch {
@@ -3398,11 +3538,11 @@ async function hook(sub, rest = []) {
3398
3538
  }
3399
3539
 
3400
3540
  // src/commands/chat.ts
3401
- import { randomUUID as randomUUID3 } from "crypto";
3541
+ import { randomUUID as randomUUID4 } from "crypto";
3402
3542
 
3403
3543
  // src/output.ts
3404
- import { writeFileSync as writeFileSync9 } from "fs";
3405
- import { join as join10, isAbsolute } from "path";
3544
+ import { writeFileSync as writeFileSync10 } from "fs";
3545
+ import { join as join11, isAbsolute } from "path";
3406
3546
  function escapeHtml(s) {
3407
3547
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3408
3548
  }
@@ -3542,8 +3682,8 @@ function saveReply(reply, format, name) {
3542
3682
  throw new Error('name must be a plain file name in this folder (no "/", "\\", or "..").');
3543
3683
  }
3544
3684
  if (!/\.[a-z0-9]+$/i.test(base)) base += ext;
3545
- const path = join10(process.cwd(), base);
3546
- writeFileSync9(path, renderReply(reply, format), "utf8");
3685
+ const path = join11(process.cwd(), base);
3686
+ writeFileSync10(path, renderReply(reply, format), "utf8");
3547
3687
  return path;
3548
3688
  }
3549
3689
 
@@ -3572,11 +3712,11 @@ async function login(which) {
3572
3712
 
3573
3713
  // src/access.ts
3574
3714
  import { existsSync as existsSync10, realpathSync as realpathSync3, statSync as statSync5 } from "fs";
3575
- import { homedir as homedir4 } from "os";
3715
+ import { homedir as homedir5 } from "os";
3576
3716
  import { dirname as dirname4, isAbsolute as isAbsolute2, resolve as resolve2, sep } from "path";
3577
3717
  function expandHome(p) {
3578
- if (p === "~") return homedir4();
3579
- if (p.startsWith("~/")) return resolve2(homedir4(), p.slice(2));
3718
+ if (p === "~") return homedir5();
3719
+ if (p.startsWith("~/")) return resolve2(homedir5(), p.slice(2));
3580
3720
  return p;
3581
3721
  }
3582
3722
  function isInside(child, parent) {
@@ -3947,8 +4087,8 @@ async function chat() {
3947
4087
  history.push({ role: "user", content: line });
3948
4088
  history.push({ role: "assistant", content: res.text });
3949
4089
  const now = Date.now();
3950
- appendTurn({ id: randomUUID3().slice(0, 8), ts: now, session, role: "user", content: line, emb: await embed(line) ?? void 0 });
3951
- appendTurn({ id: randomUUID3().slice(0, 8), ts: now, session, role: "assistant", content: res.text, emb: await embed(res.text) ?? void 0 });
4090
+ appendTurn({ id: randomUUID4().slice(0, 8), ts: now, session, role: "user", content: line, emb: await embed(line) ?? void 0 });
4091
+ appendTurn({ id: randomUUID4().slice(0, 8), ts: now, session, role: "assistant", content: res.text, emb: await embed(res.text) ?? void 0 });
3952
4092
  showStatus();
3953
4093
  } else {
3954
4094
  console.log(c.red("\n " + res.text + "\n"));
@@ -3975,15 +4115,39 @@ async function chat() {
3975
4115
  }
3976
4116
 
3977
4117
  // src/commands/launch.ts
3978
- var LAUNCH_BANNER = `
3979
- \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
3980
- \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D
3981
- \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
3982
- \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
3983
- \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551
3984
- \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D
3985
- Your assistant, with memory of you. Starting your session...
3986
- `;
4118
+ var TAGLINE = "Holt: your assistant, with memory that lasts.";
4119
+ var TIPS = [
4120
+ "Just talk. I remember what matters across our sessions.",
4121
+ "Ask me to recall something we discussed before.",
4122
+ "If I ever say I am signed out, type /login.",
4123
+ "Everything stays on your machine."
4124
+ ];
4125
+ function renderIntroBox() {
4126
+ const wordmark = "HOLT";
4127
+ const context = "Your assistant is ready. Memory is on for this folder.";
4128
+ const bullets = TIPS.map((t) => `\u2022 ${t}`);
4129
+ const content = [wordmark, TAGLINE, "", context, "", ...bullets];
4130
+ const pad = 2;
4131
+ const inner = Math.max(...content.map((l) => l.length));
4132
+ const span = inner + pad * 2;
4133
+ const top = "\u250C" + "\u2500".repeat(span) + "\u2510";
4134
+ const bottom = "\u2514" + "\u2500".repeat(span) + "\u2518";
4135
+ const row2 = (plain, colorize = (s) => s) => {
4136
+ const right = " ".repeat(inner - plain.length);
4137
+ const body = " ".repeat(pad) + colorize(plain) + right + " ".repeat(pad);
4138
+ return c.dim("\u2502") + body + c.dim("\u2502");
4139
+ };
4140
+ const lines = [];
4141
+ lines.push(c.dim(top));
4142
+ lines.push(row2(wordmark, c.accent));
4143
+ lines.push(row2(TAGLINE, c.dim));
4144
+ lines.push(row2(""));
4145
+ lines.push(row2(context, c.dim));
4146
+ lines.push(row2(""));
4147
+ for (const b of bullets) lines.push(row2(b, c.dim));
4148
+ lines.push(c.dim(bottom));
4149
+ return "\n" + lines.join("\n") + "\n";
4150
+ }
3987
4151
  var HOLT_IDENTITY = "You are Holt, the user's personal assistant. You have persistent memory of this user across sessions, provided by the Holt tool you run inside (it recalls relevant notes before each prompt and remembers new facts when the session ends). Act as their assistant. Do not mention the underlying model or that you are looking anything up; just use what you know naturally.";
3988
4152
  var INTERACTIVE_ARGS = {
3989
4153
  claude: [],
@@ -4002,13 +4166,13 @@ var CONTEXT_MARKER = "<!-- holt-managed identity -->";
4002
4166
  function brandContextFile(id) {
4003
4167
  const name = CONTEXT_FILE[id];
4004
4168
  if (!name) return null;
4005
- const path = join11(workspace(), name);
4169
+ const path = join12(workspace(), name);
4006
4170
  try {
4007
4171
  if (existsSync11(path)) {
4008
- const cur = readFileSync11(path, "utf8");
4172
+ const cur = readFileSync12(path, "utf8");
4009
4173
  if (!cur.includes(CONTEXT_MARKER)) return null;
4010
4174
  }
4011
- writeFileSync10(path, `${CONTEXT_MARKER}
4175
+ writeFileSync11(path, `${CONTEXT_MARKER}
4012
4176
  # Holt
4013
4177
 
4014
4178
  ${HOLT_IDENTITY}
@@ -4019,11 +4183,11 @@ ${HOLT_IDENTITY}
4019
4183
  }
4020
4184
  }
4021
4185
  function brandStatusLine() {
4022
- const path = join11(workspace(), ".claude", "settings.json");
4186
+ const path = join12(workspace(), ".claude", "settings.json");
4023
4187
  let settings = {};
4024
4188
  try {
4025
4189
  if (existsSync11(path)) {
4026
- const parsed = JSON.parse(readFileSync11(path, "utf8"));
4190
+ const parsed = JSON.parse(readFileSync12(path, "utf8"));
4027
4191
  if (parsed && typeof parsed === "object") settings = parsed;
4028
4192
  }
4029
4193
  } catch {
@@ -4034,10 +4198,10 @@ function brandStatusLine() {
4034
4198
  if (existsSync11(path)) copyFileSync2(path, `${path}.holt-bak`);
4035
4199
  settings.statusLine = {
4036
4200
  type: "command",
4037
- command: 'printf "Holt"'
4201
+ command: `${resolveHoltPath()} statusline`
4038
4202
  };
4039
- mkdirSync9(dirname5(path), { recursive: true });
4040
- writeFileSync10(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
4203
+ mkdirSync10(dirname5(path), { recursive: true });
4204
+ writeFileSync11(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
4041
4205
  return true;
4042
4206
  } catch {
4043
4207
  return false;
@@ -4059,10 +4223,20 @@ function resolveTarget(cfg) {
4059
4223
  async function ensureSetup() {
4060
4224
  const ws = workspace();
4061
4225
  if (!isTrusted(ws)) trustDir(ws);
4062
- mkdirSync9(memDir(), { recursive: true });
4226
+ mkdirSync10(memDir(), { recursive: true });
4227
+ if (!hooksAlreadyInstalled()) {
4228
+ try {
4229
+ await hook("install", []);
4230
+ } catch {
4231
+ }
4232
+ }
4233
+ }
4234
+ function hooksAlreadyInstalled() {
4063
4235
  try {
4064
- await hook("install", []);
4236
+ const p = join12(homedir6(), ".claude", "settings.json");
4237
+ return existsSync11(p) && readFileSync12(p, "utf8").includes("holt hook");
4065
4238
  } catch {
4239
+ return false;
4066
4240
  }
4067
4241
  }
4068
4242
  async function launch() {
@@ -4102,7 +4276,7 @@ async function launch() {
4102
4276
  );
4103
4277
  return;
4104
4278
  }
4105
- console.log(c.accent(LAUNCH_BANNER));
4279
+ console.log(renderIntroBox());
4106
4280
  const flags = [...brandingFlags(target.id)];
4107
4281
  if (flags.length === 0) {
4108
4282
  const ctx = brandContextFile(target.id);
@@ -4111,14 +4285,13 @@ async function launch() {
4111
4285
  if (target.id === "claude") brandStatusLine();
4112
4286
  const interactive = INTERACTIVE_ARGS[target.id];
4113
4287
  const args = [...interactive, ...flags];
4114
- console.log(c.dim(` Starting ${target.label} as Holt. Type "exit" in the session to leave.
4115
- `));
4288
+ console.log(c.dim(' Starting your Holt session. Type "exit" to leave.\n'));
4116
4289
  await runInteractive(cfg.brains[target.id].command, args);
4117
4290
  console.log(c.dim("\nBye. Holt kept what mattered from this session.\n"));
4118
4291
  }
4119
4292
 
4120
4293
  // src/commands/memory.ts
4121
- import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
4294
+ import { existsSync as existsSync12, readFileSync as readFileSync13 } from "fs";
4122
4295
  import { basename } from "path";
4123
4296
  async function memoryCmd(sub, rest = []) {
4124
4297
  const { ask, close } = createReader();
@@ -4171,7 +4344,7 @@ async function memoryCmd(sub, rest = []) {
4171
4344
  close();
4172
4345
  return;
4173
4346
  }
4174
- const body = readFileSync12(factsMdPath(), "utf8").trim();
4347
+ const body = readFileSync13(factsMdPath(), "utf8").trim();
4175
4348
  console.log("\n" + body + "\n");
4176
4349
  close();
4177
4350
  return;
@@ -4285,8 +4458,8 @@ function globalCmd(act, rest) {
4285
4458
  }
4286
4459
 
4287
4460
  // src/commands/wiki.ts
4288
- import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
4289
- import { join as join12 } from "path";
4461
+ import { existsSync as existsSync13, readFileSync as readFileSync14 } from "fs";
4462
+ import { join as join13 } from "path";
4290
4463
  import { spawn as spawn3 } from "child_process";
4291
4464
  function titleToSlug(title) {
4292
4465
  return proposeSlug(title);
@@ -4479,7 +4652,7 @@ function listCmd() {
4479
4652
  for (const slug of slugs) {
4480
4653
  const pg = readPage(slug);
4481
4654
  if (!pg) continue;
4482
- const size = existsSync13(join12(wikiDir(), slug + ".md")) ? (readFileSync13(join12(wikiDir(), slug + ".md"), "utf8").length / 1024).toFixed(1) : "0.0";
4655
+ const size = existsSync13(join13(wikiDir(), slug + ".md")) ? (readFileSync14(join13(wikiDir(), slug + ".md"), "utf8").length / 1024).toFixed(1) : "0.0";
4483
4656
  console.log(` ${c.cyan(pg.title.padEnd(28).slice(0, 28))} ${c.dim(pg.updated || " ")} ${size} KB`);
4484
4657
  }
4485
4658
  console.log("");
@@ -4497,7 +4670,7 @@ function showCmd(name) {
4497
4670
  `));
4498
4671
  return;
4499
4672
  }
4500
- const raw = readFileSync13(join12(wikiDir(), slug + ".md"), "utf8");
4673
+ const raw = readFileSync14(join13(wikiDir(), slug + ".md"), "utf8");
4501
4674
  console.log("\n" + raw.trim() + "\n");
4502
4675
  }
4503
4676
  async function statusCmd(cfg) {
@@ -4655,9 +4828,9 @@ async function wikiCmd(sub, rest = []) {
4655
4828
 
4656
4829
  // src/commands/skill.ts
4657
4830
  import {
4658
- mkdirSync as mkdirSync11,
4659
- writeFileSync as writeFileSync12,
4660
- readFileSync as readFileSync15,
4831
+ mkdirSync as mkdirSync12,
4832
+ writeFileSync as writeFileSync13,
4833
+ readFileSync as readFileSync16,
4661
4834
  existsSync as existsSync15,
4662
4835
  readdirSync as readdirSync3,
4663
4836
  statSync as statSync7,
@@ -4665,19 +4838,19 @@ import {
4665
4838
  cpSync,
4666
4839
  mkdtempSync
4667
4840
  } from "fs";
4668
- import { join as join14, resolve as resolve4, sep as sep2, isAbsolute as isAbsolute4 } from "path";
4841
+ import { join as join15, resolve as resolve4, sep as sep2, isAbsolute as isAbsolute4 } from "path";
4669
4842
  import { tmpdir } from "os";
4670
- import { spawnSync as spawnSync3 } from "child_process";
4843
+ import { spawnSync as spawnSync4 } from "child_process";
4671
4844
 
4672
4845
  // src/registry.ts
4673
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync11, existsSync as existsSync14, mkdirSync as mkdirSync10, statSync as statSync6 } from "fs";
4674
- import { join as join13, isAbsolute as isAbsolute3, resolve as resolve3 } from "path";
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";
4675
4848
  import { pathToFileURL, fileURLToPath as fileURLToPath2 } from "url";
4676
4849
  var DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/holt-os/registry/main/registry.json";
4677
4850
  var REGISTRY_REPO_URL = "https://github.com/holt-os/registry";
4678
4851
  var CACHE_TTL_MS = 60 * 60 * 1e3;
4679
4852
  function registryCachePath() {
4680
- return join13(GLOBAL_DIR, "registry-cache.json");
4853
+ return join14(GLOBAL_DIR, "registry-cache.json");
4681
4854
  }
4682
4855
  function registryUrl() {
4683
4856
  const env = (process.env.HOLT_REGISTRY_URL || "").trim();
@@ -4717,7 +4890,7 @@ function readCache() {
4717
4890
  try {
4718
4891
  const path = registryCachePath();
4719
4892
  if (!existsSync14(path)) return null;
4720
- const raw = JSON.parse(readFileSync14(path, "utf8"));
4893
+ const raw = JSON.parse(readFileSync15(path, "utf8"));
4721
4894
  if (!raw || typeof raw.fetchedAt !== "number" || typeof raw.url !== "string") return null;
4722
4895
  const registry = coerceRegistry(raw.registry);
4723
4896
  if (!registry) return null;
@@ -4728,9 +4901,9 @@ function readCache() {
4728
4901
  }
4729
4902
  function writeCache(url, registry) {
4730
4903
  try {
4731
- mkdirSync10(GLOBAL_DIR, { recursive: true });
4904
+ mkdirSync11(GLOBAL_DIR, { recursive: true });
4732
4905
  const body = { fetchedAt: Date.now(), url, registry };
4733
- writeFileSync11(registryCachePath(), JSON.stringify(body, null, 2) + "\n", "utf8");
4906
+ writeFileSync12(registryCachePath(), JSON.stringify(body, null, 2) + "\n", "utf8");
4734
4907
  } catch {
4735
4908
  }
4736
4909
  }
@@ -4751,7 +4924,7 @@ function loadLocal(path) {
4751
4924
  if (!existsSync14(path)) {
4752
4925
  return { registry: null, fromCache: false, error: UNREACHABLE_MSG };
4753
4926
  }
4754
- const registry = parseRegistry(readFileSync14(path, "utf8"));
4927
+ const registry = parseRegistry(readFileSync15(path, "utf8"));
4755
4928
  if (!registry) {
4756
4929
  return { registry: null, fromCache: false, error: "registry file is not valid JSON" };
4757
4930
  }
@@ -4880,12 +5053,12 @@ function resolveSubpathDir(fetched, subpath) {
4880
5053
  return target;
4881
5054
  }
4882
5055
  function findSkillDir(root) {
4883
- if (existsSync15(join14(root, "SKILL.md"))) return root;
5056
+ if (existsSync15(join15(root, "SKILL.md"))) return root;
4884
5057
  let subs = [];
4885
5058
  try {
4886
5059
  subs = readdirSync3(root).filter((f) => {
4887
5060
  try {
4888
- return statSync7(join14(root, f)).isDirectory();
5061
+ return statSync7(join15(root, f)).isDirectory();
4889
5062
  } catch {
4890
5063
  return false;
4891
5064
  }
@@ -4893,8 +5066,8 @@ function findSkillDir(root) {
4893
5066
  } catch {
4894
5067
  return null;
4895
5068
  }
4896
- const withSkill = subs.filter((f) => existsSync15(join14(root, f, "SKILL.md")));
4897
- if (withSkill.length === 1) return join14(root, withSkill[0]);
5069
+ const withSkill = subs.filter((f) => existsSync15(join15(root, f, "SKILL.md")));
5070
+ if (withSkill.length === 1) return join15(root, withSkill[0]);
4898
5071
  return null;
4899
5072
  }
4900
5073
  function cmdList() {
@@ -4935,7 +5108,7 @@ function cmdShow(name) {
4935
5108
  const { skill, body } = loaded;
4936
5109
  console.log("\n" + c.accent(skill.name) + c.dim(` (${skill.scope})`));
4937
5110
  if (skill.description) console.log(" " + skill.description);
4938
- console.log(c.dim(" " + join14(skill.dir, "SKILL.md")));
5111
+ console.log(c.dim(" " + join15(skill.dir, "SKILL.md")));
4939
5112
  console.log("");
4940
5113
  console.log(c.dim(body));
4941
5114
  console.log("");
@@ -4947,7 +5120,7 @@ function cmdCreate(name, global) {
4947
5120
  return;
4948
5121
  }
4949
5122
  const scope = global ? "global" : "workspace";
4950
- const dir = join14(skillsRoot(scope), clean);
5123
+ const dir = join15(skillsRoot(scope), clean);
4951
5124
  if (existsSync15(dir)) {
4952
5125
  console.error(c.red(`
4953
5126
  A ${scope} skill named "${clean}" already exists.`));
@@ -4955,11 +5128,11 @@ function cmdCreate(name, global) {
4955
5128
  process.exitCode = 1;
4956
5129
  return;
4957
5130
  }
4958
- mkdirSync11(dir, { recursive: true });
4959
- writeFileSync12(join14(dir, "SKILL.md"), SKILL_TEMPLATE(clean), "utf8");
5131
+ mkdirSync12(dir, { recursive: true });
5132
+ writeFileSync13(join15(dir, "SKILL.md"), SKILL_TEMPLATE(clean), "utf8");
4960
5133
  console.log(c.green(`
4961
5134
  Created ${scope} skill "${clean}".`));
4962
- console.log(c.dim(" " + join14(dir, "SKILL.md")));
5135
+ console.log(c.dim(" " + join15(dir, "SKILL.md")));
4963
5136
  console.log(c.dim(' Edit it, then run it in chat with "/skill ' + clean + '".\n'));
4964
5137
  }
4965
5138
  function installFromSource(source, scope) {
@@ -4968,10 +5141,10 @@ function installFromSource(source, scope) {
4968
5141
  let tempDir = "";
4969
5142
  try {
4970
5143
  if (isGitUrl(base)) {
4971
- tempDir = mkdtempSync(join14(tmpdir(), "holt-skill-"));
5144
+ tempDir = mkdtempSync(join15(tmpdir(), "holt-skill-"));
4972
5145
  console.log(c.dim(`
4973
5146
  Cloning ${base} ...`));
4974
- const res = spawnSync3("git", ["clone", "--depth", "1", base, tempDir], { stdio: "ignore" });
5147
+ const res = spawnSync4("git", ["clone", "--depth", "1", base, tempDir], { stdio: "ignore" });
4975
5148
  if (res.status !== 0) {
4976
5149
  console.error(c.red(" Clone failed. Check the URL and that git is installed.\n"));
4977
5150
  process.exitCode = 1;
@@ -5018,7 +5191,7 @@ function installFromSource(source, scope) {
5018
5191
  }
5019
5192
  let skillName = "";
5020
5193
  try {
5021
- const { data } = parseFrontmatter(readFileSync15(join14(skillDir, "SKILL.md"), "utf8"));
5194
+ const { data } = parseFrontmatter(readFileSync16(join15(skillDir, "SKILL.md"), "utf8"));
5022
5195
  skillName = sanitizeName(data.name || "");
5023
5196
  } catch {
5024
5197
  skillName = "";
@@ -5029,7 +5202,7 @@ function installFromSource(source, scope) {
5029
5202
  process.exitCode = 1;
5030
5203
  return false;
5031
5204
  }
5032
- const dest = join14(skillsRoot(scope), skillName);
5205
+ const dest = join15(skillsRoot(scope), skillName);
5033
5206
  if (existsSync15(dest)) {
5034
5207
  console.error(c.red(`
5035
5208
  A ${scope} skill named "${skillName}" already exists.`));
@@ -5037,13 +5210,13 @@ function installFromSource(source, scope) {
5037
5210
  process.exitCode = 1;
5038
5211
  return false;
5039
5212
  }
5040
- mkdirSync11(skillsRoot(scope), { recursive: true });
5213
+ mkdirSync12(skillsRoot(scope), { recursive: true });
5041
5214
  cpSync(skillDir, dest, { recursive: true });
5042
- const nestedGit = join14(dest, ".git");
5215
+ const nestedGit = join15(dest, ".git");
5043
5216
  if (existsSync15(nestedGit)) rmSync5(nestedGit, { recursive: true, force: true });
5044
5217
  console.log(c.green(`
5045
5218
  Installed ${scope} skill "${skillName}".`));
5046
- console.log(c.dim(" " + join14(dest, "SKILL.md") + "\n"));
5219
+ console.log(c.dim(" " + join15(dest, "SKILL.md") + "\n"));
5047
5220
  return true;
5048
5221
  } finally {
5049
5222
  if (tempDir && existsSync15(tempDir)) {
@@ -5133,7 +5306,7 @@ function cmdPublish(name) {
5133
5306
  }
5134
5307
  let raw = "";
5135
5308
  try {
5136
- raw = readFileSync15(join14(loaded.skill.dir, "SKILL.md"), "utf8");
5309
+ raw = readFileSync16(join15(loaded.skill.dir, "SKILL.md"), "utf8");
5137
5310
  } catch {
5138
5311
  console.error(c.red("\n Could not read the skill's SKILL.md.\n"));
5139
5312
  process.exitCode = 1;
@@ -5143,14 +5316,14 @@ function cmdPublish(name) {
5143
5316
  skillName = sanitizeName(data.name || loaded.skill.name);
5144
5317
  description = (data.description || loaded.skill.description || "").trim();
5145
5318
  } else {
5146
- const file = join14(process.cwd(), "SKILL.md");
5319
+ const file = join15(process.cwd(), "SKILL.md");
5147
5320
  if (!existsSync15(file)) {
5148
5321
  console.error(c.red("\n No SKILL.md in the current directory, and no skill name given."));
5149
5322
  console.error(c.dim(" Usage: holt skill publish [<name>]\n"));
5150
5323
  process.exitCode = 1;
5151
5324
  return;
5152
5325
  }
5153
- const { data } = parseFrontmatter(readFileSync15(file, "utf8"));
5326
+ const { data } = parseFrontmatter(readFileSync16(file, "utf8"));
5154
5327
  skillName = sanitizeName(data.name || process.cwd().split(/[\\/]/).pop() || "");
5155
5328
  description = (data.description || "").trim();
5156
5329
  sourceHint = process.cwd();
@@ -5186,11 +5359,11 @@ async function cmdRemove(name, ask) {
5186
5359
  console.log(c.dim("\n Usage: holt skill remove <name>\n"));
5187
5360
  return;
5188
5361
  }
5189
- const wsDir = join14(skillsRoot("workspace"), clean);
5190
- const globalDir = join14(skillsRoot("global"), clean);
5362
+ const wsDir = join15(skillsRoot("workspace"), clean);
5363
+ const globalDir = join15(skillsRoot("global"), clean);
5191
5364
  const target = existsSync15(wsDir) ? wsDir : existsSync15(globalDir) ? globalDir : "";
5192
5365
  if (!target) {
5193
- if (existsSync15(join14(skillsRoot("builtin"), clean))) {
5366
+ if (existsSync15(join15(skillsRoot("builtin"), clean))) {
5194
5367
  console.error(c.red(`
5195
5368
  "${clean}" is a built-in skill and cannot be removed; it ships inside Holt.`));
5196
5369
  console.error(c.dim(" Override it instead: create a workspace or global skill with the same name.\n"));
@@ -5263,8 +5436,8 @@ async function skillCmd(sub, rest = []) {
5263
5436
  }
5264
5437
 
5265
5438
  // src/commands/graph.ts
5266
- import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync12, existsSync as existsSync16 } from "fs";
5267
- import { join as join16, dirname as dirname7, resolve as resolve5 } from "path";
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";
5268
5441
  import { spawn as spawn4 } from "child_process";
5269
5442
 
5270
5443
  // src/communities.ts
@@ -6061,8 +6234,8 @@ function renderGraphHtml(graph2, meta) {
6061
6234
  }
6062
6235
 
6063
6236
  // src/ingest.ts
6064
- import { readdirSync as readdirSync4, readFileSync as readFileSync16, statSync as statSync8 } from "fs";
6065
- import { join as join15, relative, sep as sep3, extname, dirname as dirname6, basename as basename2 } from "path";
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";
6066
6239
  var CODE_EXT = /* @__PURE__ */ new Set([
6067
6240
  "js",
6068
6241
  "jsx",
@@ -6135,7 +6308,7 @@ function ingestFolder(root, want = { code: true, docs: true }) {
6135
6308
  skipped.hitFileCap = true;
6136
6309
  break;
6137
6310
  }
6138
- const full = join15(dir, name);
6311
+ const full = join16(dir, name);
6139
6312
  let st;
6140
6313
  try {
6141
6314
  st = statSync8(full);
@@ -6168,7 +6341,7 @@ function ingestFolder(root, want = { code: true, docs: true }) {
6168
6341
  }
6169
6342
  let content;
6170
6343
  try {
6171
- content = readFileSync16(full, "utf8");
6344
+ content = readFileSync17(full, "utf8");
6172
6345
  } catch {
6173
6346
  skipped.unreadable++;
6174
6347
  continue;
@@ -6504,9 +6677,9 @@ async function graph(args = []) {
6504
6677
  if (codeGraph) assignCommunities(g);
6505
6678
  if (opts.report) {
6506
6679
  const md = renderReport(g, { workspace: workspace() });
6507
- const reportPath = opts.out ? resolve5(opts.out) : join16(workspace(), "GRAPH_REPORT.md");
6508
- mkdirSync12(dirname7(reportPath), { recursive: true });
6509
- writeFileSync13(reportPath, md, "utf8");
6680
+ const reportPath = opts.out ? resolve5(opts.out) : join17(workspace(), "GRAPH_REPORT.md");
6681
+ mkdirSync13(dirname7(reportPath), { recursive: true });
6682
+ writeFileSync14(reportPath, md, "utf8");
6510
6683
  const communities = new Set(g.nodes.map((n) => n.community ?? "0")).size;
6511
6684
  console.log("\n" + c.accent("Graph report written") + c.dim(" (this folder)"));
6512
6685
  console.log(` nodes ${g.nodes.length} (${fileNodeCount} files, ${docNodeCount} docs)`);
@@ -6526,9 +6699,9 @@ async function graph(args = []) {
6526
6699
  concepts: conceptCount,
6527
6700
  edges: g.edges.length
6528
6701
  });
6529
- const outPath = opts.out ? resolve5(opts.out) : join16(wsHoltDir(), "graph.html");
6530
- mkdirSync12(dirname7(outPath), { recursive: true });
6531
- writeFileSync13(outPath, html, "utf8");
6702
+ const outPath = opts.out ? resolve5(opts.out) : join17(wsHoltDir(), "graph.html");
6703
+ mkdirSync13(dirname7(outPath), { recursive: true });
6704
+ writeFileSync14(outPath, html, "utf8");
6532
6705
  console.log("\n" + c.accent("Memory graph built") + c.dim(" (this folder)"));
6533
6706
  const parts = [`${turns.length} turns`, `${conceptCount} concepts`];
6534
6707
  if (wikiCount) parts.push(`${wikiCount} wiki pages`);
@@ -6635,8 +6808,8 @@ function recoverId(line) {
6635
6808
  const n = Number(num[1]);
6636
6809
  if (Number.isFinite(n)) return n;
6637
6810
  }
6638
- const str = line.match(/"id"\s*:\s*"((?:[^"\\]|\\.)*)"/);
6639
- if (str && str[1] !== void 0) return str[1];
6811
+ const str2 = line.match(/"id"\s*:\s*"((?:[^"\\]|\\.)*)"/);
6812
+ if (str2 && str2[1] !== void 0) return str2[1];
6640
6813
  return null;
6641
6814
  }
6642
6815
  function runMcpServer(opts) {
@@ -6816,7 +6989,7 @@ function holtTools() {
6816
6989
  }
6817
6990
 
6818
6991
  // src/version.ts
6819
- var VERSION = "0.14.0";
6992
+ var VERSION = "0.14.2";
6820
6993
 
6821
6994
  // src/commands/mcp.ts
6822
6995
  function printSetup() {
@@ -6861,8 +7034,84 @@ async function mcp(sub, rest = []) {
6861
7034
  }
6862
7035
  }
6863
7036
 
7037
+ // src/commands/statusline.ts
7038
+ import { basename as basename3 } from "path";
7039
+ var SEP = " \xB7 ";
7040
+ function readStdin2(timeoutMs) {
7041
+ return new Promise((resolve9) => {
7042
+ let input = "";
7043
+ let done = false;
7044
+ const finish = () => {
7045
+ if (done) return;
7046
+ done = true;
7047
+ resolve9(input);
7048
+ };
7049
+ try {
7050
+ process.stdin.setEncoding("utf8");
7051
+ process.stdin.on("data", (chunk2) => {
7052
+ input += chunk2;
7053
+ });
7054
+ process.stdin.on("end", finish);
7055
+ process.stdin.on("error", finish);
7056
+ } catch {
7057
+ finish();
7058
+ }
7059
+ setTimeout(finish, timeoutMs);
7060
+ });
7061
+ }
7062
+ function get(obj, key) {
7063
+ if (!obj || typeof obj !== "object") return void 0;
7064
+ return obj[key];
7065
+ }
7066
+ function str(v) {
7067
+ if (typeof v !== "string") return void 0;
7068
+ const t = v.trim();
7069
+ return t.length ? t : void 0;
7070
+ }
7071
+ function folderFrom(data) {
7072
+ const ws = get(data, "workspace");
7073
+ const dir = str(get(ws, "current_dir")) ?? str(get(ws, "project_dir")) ?? str(get(ws, "cwd")) ?? str(get(data, "cwd")) ?? str(get(data, "current_dir"));
7074
+ if (!dir) return void 0;
7075
+ try {
7076
+ const base = basename3(dir.replace(/[\\/]+$/, ""));
7077
+ return base.length ? base : void 0;
7078
+ } catch {
7079
+ return void 0;
7080
+ }
7081
+ }
7082
+ function modelFrom(data) {
7083
+ const model = get(data, "model");
7084
+ if (typeof model === "string") return str(model);
7085
+ return str(get(model, "display_name")) ?? str(get(model, "id")) ?? str(get(data, "model_display_name")) ?? str(get(data, "model_id"));
7086
+ }
7087
+ function renderStatusLine(data) {
7088
+ const parts = ["Holt"];
7089
+ const folder = folderFrom(data);
7090
+ if (folder) parts.push(folder);
7091
+ const model = modelFrom(data);
7092
+ if (model) parts.push(model);
7093
+ return parts.join(SEP);
7094
+ }
7095
+ function parse(raw) {
7096
+ try {
7097
+ return JSON.parse(raw || "{}");
7098
+ } catch {
7099
+ return {};
7100
+ }
7101
+ }
7102
+ async function statusline() {
7103
+ let line = "Holt";
7104
+ try {
7105
+ const raw = await readStdin2(1e3);
7106
+ line = renderStatusLine(parse(raw));
7107
+ } catch {
7108
+ line = "Holt";
7109
+ }
7110
+ process.stdout.write(line + "\n");
7111
+ }
7112
+
6864
7113
  // src/commands/run.ts
6865
- import { writeFileSync as writeFileSync14 } from "fs";
7114
+ import { writeFileSync as writeFileSync15 } from "fs";
6866
7115
  import { resolve as resolve6 } from "path";
6867
7116
  var USAGE3 = [
6868
7117
  " holt run <task> [options]",
@@ -6954,7 +7203,7 @@ async function run(args) {
6954
7203
  if (p.out) {
6955
7204
  const target = resolve6(process.cwd(), p.out);
6956
7205
  try {
6957
- writeFileSync14(target, result.text, "utf8");
7206
+ writeFileSync15(target, result.text, "utf8");
6958
7207
  process.stderr.write(c.dim(`saved to ${target}`) + "\n");
6959
7208
  } catch (e) {
6960
7209
  process.stderr.write(c.red(`Could not write ${target}: ${e.message}`) + "\n");
@@ -6967,147 +7216,6 @@ async function run(args) {
6967
7216
  import { spawnSync as spawnSync5 } from "child_process";
6968
7217
  import { writeFileSync as writeFileSync16, mkdirSync as mkdirSync14, rmSync as rmSync6, existsSync as existsSync17 } from "fs";
6969
7218
  import { dirname as dirname8 } from "path";
6970
-
6971
- // src/scheduler.ts
6972
- import { homedir as homedir5 } from "os";
6973
- import { join as join17 } from "path";
6974
- import { readFileSync as readFileSync17, writeFileSync as writeFileSync15, mkdirSync as mkdirSync13 } from "fs";
6975
- import { randomUUID as randomUUID4 } from "crypto";
6976
- import { spawnSync as spawnSync4 } from "child_process";
6977
- function schedulesPath() {
6978
- return join17(GLOBAL_DIR, "schedules.json");
6979
- }
6980
- function logsDir() {
6981
- return join17(GLOBAL_DIR, "logs");
6982
- }
6983
- function logPath(id) {
6984
- return join17(logsDir(), `${id}.log`);
6985
- }
6986
- function plistPath(id) {
6987
- return join17(homedir5(), "Library", "LaunchAgents", `com.holt.${id}.plist`);
6988
- }
6989
- function loadJobs() {
6990
- try {
6991
- const raw = JSON.parse(readFileSync17(schedulesPath(), "utf8"));
6992
- return Array.isArray(raw) ? raw : [];
6993
- } catch {
6994
- return [];
6995
- }
6996
- }
6997
- function saveJobs(jobs) {
6998
- mkdirSync13(GLOBAL_DIR, { recursive: true });
6999
- writeFileSync15(schedulesPath(), JSON.stringify(jobs, null, 2) + "\n", "utf8");
7000
- }
7001
- function addJob(job) {
7002
- const jobs = loadJobs();
7003
- jobs.push(job);
7004
- saveJobs(jobs);
7005
- return jobs;
7006
- }
7007
- function removeJob(id) {
7008
- const jobs = loadJobs().filter((j) => j.id !== id);
7009
- saveJobs(jobs);
7010
- return jobs;
7011
- }
7012
- function newJobId() {
7013
- return randomUUID4().slice(0, 8);
7014
- }
7015
- function parseWhen(when) {
7016
- const m = /^([01]?\d|2[0-3]):([0-5]\d)$/.exec(when.trim());
7017
- if (!m) {
7018
- throw new Error(
7019
- `Could not read time "${when}". Use 24h HH:MM for a daily run, e.g. 07:00 or 23:30.`
7020
- );
7021
- }
7022
- const hour = Number(m[1]);
7023
- const minute = Number(m[2]);
7024
- return { hour, minute };
7025
- }
7026
- function shQuote(s) {
7027
- return `'` + s.replace(/'/g, `'\\''`) + `'`;
7028
- }
7029
- function resolveHoltPath() {
7030
- try {
7031
- const finder = process.platform === "win32" ? "where" : "which";
7032
- const res = spawnSync4(finder, ["holt"], { encoding: "utf8" });
7033
- if (res.status === 0 && typeof res.stdout === "string") {
7034
- const first = res.stdout.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
7035
- if (first) return first;
7036
- }
7037
- } catch {
7038
- }
7039
- return "holt";
7040
- }
7041
- function buildCommand(job, holtPath) {
7042
- const holt = shQuote(holtPath);
7043
- const ws = shQuote(job.workspace);
7044
- if (job.runArgs && job.runArgs.length > 0) {
7045
- const parts = job.runArgs.map(shQuote).join(" ");
7046
- return `cd ${ws} && ${holt} ${parts} --quiet`;
7047
- }
7048
- const log = shQuote(logPath(job.id));
7049
- let cmd = `cd ${ws} && ${holt} run ${shQuote(job.task)}`;
7050
- if (job.brain) cmd += ` --brain ${shQuote(job.brain)}`;
7051
- cmd += ` --quiet --out ${log}`;
7052
- if (job.notify) cmd += ` && ${holt} notify "$(cat ${log})"`;
7053
- return cmd;
7054
- }
7055
- function xmlEscape(s) {
7056
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
7057
- }
7058
- function buildLaunchdPlist(job, holtPath) {
7059
- const { hour, minute } = parseWhen(job.when);
7060
- const command = buildCommand(job, holtPath);
7061
- const log = logPath(job.id);
7062
- const args = ["/bin/sh", "-lc", command];
7063
- const argXml = args.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
7064
- return `<?xml version="1.0" encoding="UTF-8"?>
7065
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
7066
- <plist version="1.0">
7067
- <dict>
7068
- <key>Label</key>
7069
- <string>com.holt.${xmlEscape(job.id)}</string>
7070
- <key>ProgramArguments</key>
7071
- <array>
7072
- ${argXml}
7073
- </array>
7074
- <key>StartCalendarInterval</key>
7075
- <dict>
7076
- <key>Hour</key>
7077
- <integer>${hour}</integer>
7078
- <key>Minute</key>
7079
- <integer>${minute}</integer>
7080
- </dict>
7081
- <key>RunAtLoad</key>
7082
- <false/>
7083
- <key>StandardOutPath</key>
7084
- <string>${xmlEscape(log)}</string>
7085
- <key>StandardErrorPath</key>
7086
- <string>${xmlEscape(log)}</string>
7087
- </dict>
7088
- </plist>
7089
- `;
7090
- }
7091
- var CRON_MARKER = "holt:";
7092
- function buildCronLine(job, holtPath) {
7093
- const { hour, minute } = parseWhen(job.when);
7094
- const command = buildCommand(job, holtPath);
7095
- return `${minute} ${hour} * * * ${command} # ${CRON_MARKER}${job.id}`;
7096
- }
7097
- function stripCronLines(crontab, id) {
7098
- const marker = `# ${CRON_MARKER}${id}`;
7099
- const kept = crontab.split("\n").filter((line) => !line.includes(marker));
7100
- return kept.join("\n").replace(/\n+$/, "\n");
7101
- }
7102
- function appendCronLine(crontab, line) {
7103
- const base = crontab.replace(/\s*$/, "");
7104
- return (base ? base + "\n" : "") + line + "\n";
7105
- }
7106
- function ensureLogsDir() {
7107
- mkdirSync13(logsDir(), { recursive: true });
7108
- }
7109
-
7110
- // src/commands/schedule.ts
7111
7219
  function parseAdd(rest) {
7112
7220
  const positional = [];
7113
7221
  const opts = { notify: false };
@@ -7751,7 +7859,7 @@ async function notify(args) {
7751
7859
  if (nonFlag.length > 0 && !args.includes("-")) {
7752
7860
  message = nonFlag.join(" ").trim();
7753
7861
  } else if (wantsStdin) {
7754
- message = readStdin2().trim();
7862
+ message = readStdin3().trim();
7755
7863
  } else if (nonFlag.length > 0) {
7756
7864
  message = nonFlag.join(" ").trim();
7757
7865
  }
@@ -7774,7 +7882,7 @@ async function notify(args) {
7774
7882
  process.exitCode = 1;
7775
7883
  }
7776
7884
  }
7777
- function readStdin2() {
7885
+ function readStdin3() {
7778
7886
  try {
7779
7887
  return readFileSync19(0, "utf8");
7780
7888
  } catch {
@@ -8273,6 +8381,9 @@ async function main() {
8273
8381
  case "hook":
8274
8382
  await hook(process.argv[3], process.argv.slice(4));
8275
8383
  break;
8384
+ case "statusline":
8385
+ await statusline();
8386
+ break;
8276
8387
  case "run":
8277
8388
  await run(process.argv.slice(3));
8278
8389
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holt-os/holt",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
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",