@holt-os/holt 0.8.2 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +14 -1
  2. package/dist/cli.js +125 -43
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -10,10 +10,23 @@ Holt is an open-source, self-hosted personal agent OS. Clone it, pick your skill
10
10
 
11
11
  ---
12
12
 
13
- ## Quickstart
13
+ ## Install
14
+
15
+ With Homebrew (installs Node for you, so you do not need npm first):
16
+
17
+ ```bash
18
+ brew install holt-os/tap/holt
19
+ ```
20
+
21
+ Or with npm, if you already have Node:
14
22
 
15
23
  ```bash
16
24
  npm install -g @holt-os/holt
25
+ ```
26
+
27
+ ## Quickstart
28
+
29
+ ```bash
17
30
  cd ~/where-you-want-to-work
18
31
  holt init # trust this folder, choose and install brains, sign in, set defaults
19
32
  holt chat # start talking (or use your custom command, e.g. `ai`)
package/dist/cli.js CHANGED
@@ -324,8 +324,29 @@ ${END}`;
324
324
  return { ok: false, kind: "none", file, immediate: false, message: `Could not write ${file}: ${e.message}` };
325
325
  }
326
326
  }
327
+ function isValidAliasName(name) {
328
+ return /^[A-Za-z_][A-Za-z0-9_-]{0,31}$/.test(name);
329
+ }
327
330
  function installAlias(name) {
331
+ if (!isValidAliasName(name)) {
332
+ return {
333
+ ok: false,
334
+ kind: "none",
335
+ file: "",
336
+ immediate: false,
337
+ message: `"${name}" is not a valid launch command. Use a single word: a letter or _ then letters, digits, _ or - (max 32).`
338
+ };
339
+ }
328
340
  removeAlias();
341
+ if (process.platform !== "win32" && isInstalled(name)) {
342
+ return {
343
+ ok: false,
344
+ kind: "none",
345
+ file: "",
346
+ immediate: false,
347
+ message: `"${name}" is already a command on your PATH. Pick another word.`
348
+ };
349
+ }
329
350
  if (process.platform !== "win32") {
330
351
  const binDir = launcherBinDir();
331
352
  if (binDir) {
@@ -480,9 +501,10 @@ function appendTurn(t) {
480
501
  }
481
502
  function clearMemory() {
482
503
  if (existsSync4(memPath())) rmSync2(memPath());
504
+ if (existsSync4(factsMdPath())) rmSync2(factsMdPath());
483
505
  }
484
506
  function normalizeFact(s) {
485
- return s.toLowerCase().replace(/\s+/g, " ").trim().replace(/[.,;:!?]+$/, "");
507
+ return (s || "").toLowerCase().replace(/\s+/g, " ").trim().replace(/[.,;:!?]+$/, "");
486
508
  }
487
509
  var FACTS_HEADER = "# Holt facts\n\nDistilled memories for this folder. Safe to edit by hand.\n";
488
510
  async function saveFact(content, session) {
@@ -550,7 +572,7 @@ function cosine(a, b) {
550
572
  }
551
573
  function tokens(s) {
552
574
  return new Set(
553
- s.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2)
575
+ (s || "").toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2)
554
576
  );
555
577
  }
556
578
  function keywordScore(q, text) {
@@ -612,7 +634,7 @@ function loadTelegramConfig() {
612
634
  if (!existsSync5(path)) return null;
613
635
  try {
614
636
  const raw = JSON.parse(readFileSync5(path, "utf8"));
615
- if (typeof raw.token !== "string" || typeof raw.allowedChatId !== "number") return null;
637
+ if (typeof raw.token !== "string" || typeof raw.allowedChatId !== "number" || raw.allowedChatId === 0) return null;
616
638
  return { token: raw.token, allowedChatId: raw.allowedChatId };
617
639
  } catch {
618
640
  return null;
@@ -674,11 +696,11 @@ async function sendMessage(text, chatId) {
674
696
  }
675
697
  return allOk;
676
698
  }
677
- async function getUpdates(offset) {
678
- const cfg = loadTelegramConfig();
679
- if (!cfg) return [];
699
+ async function getUpdates(offset, tokenOverride) {
700
+ const token = tokenOverride ?? loadTelegramConfig()?.token;
701
+ if (!token) return [];
680
702
  const timeoutSec = 30;
681
- const url = buildGetUpdatesUrl(cfg.token, offset, timeoutSec);
703
+ const url = buildGetUpdatesUrl(token, offset, timeoutSec);
682
704
  const ac = new AbortController();
683
705
  const timer = setTimeout(() => ac.abort(), (timeoutSec + 10) * 1e3);
684
706
  try {
@@ -1062,9 +1084,8 @@ async function setupTelegram(ask) {
1062
1084
  const mode = (await ask("\n Auto-detect from a message you just sent? [Y/n] ") ?? "").trim().toLowerCase();
1063
1085
  let allowedChatId = null;
1064
1086
  if (mode === "" || mode === "y" || mode === "yes") {
1065
- saveTelegramConfig({ token, allowedChatId: 0 });
1066
1087
  console.log(c.dim("\n Checking for a recent message..."));
1067
- const updates = await getUpdates(0);
1088
+ const updates = await getUpdates(0, token);
1068
1089
  allowedChatId = latestChatId(updates);
1069
1090
  if (allowedChatId === null) {
1070
1091
  console.log(c.red(" Did not find a recent message."));
@@ -1586,7 +1607,7 @@ async function extractAndSaveFacts(active, cfg, history, session) {
1586
1607
 
1587
1608
  // src/output.ts
1588
1609
  import { writeFileSync as writeFileSync6 } from "fs";
1589
- import { join as join7 } from "path";
1610
+ import { join as join7, isAbsolute } from "path";
1590
1611
  function escapeHtml(s) {
1591
1612
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1592
1613
  }
@@ -1597,8 +1618,12 @@ function renderInline(text) {
1597
1618
  return `\0${codes.length - 1}\0`;
1598
1619
  });
1599
1620
  s = escapeHtml(s);
1600
- s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, label, url) => {
1601
- const safe = url.replace(/"/g, "%22");
1621
+ s = s.replace(/\[([^\]]+)\]\((\S*?(?:\([^)\s]*\)[^)\s]*)*)\)/g, (_m, label, url) => {
1622
+ const trimmed = url.trim();
1623
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed);
1624
+ const safeScheme = !scheme || /^(https?|mailto)$/i.test(scheme[1]);
1625
+ if (!safeScheme) return label;
1626
+ const safe = trimmed.replace(/"/g, "%22");
1602
1627
  return `<a href="${safe}">${label}</a>`;
1603
1628
  });
1604
1629
  s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
@@ -1718,6 +1743,9 @@ function saveReply(reply, format, name) {
1718
1743
  const ext = format === "html" ? ".html" : ".md";
1719
1744
  let base = (name ?? "").trim();
1720
1745
  if (!base) base = `holt-reply-${timestamp()}`;
1746
+ if (isAbsolute(base) || /[\\/]/.test(base) || base === "." || base === "..") {
1747
+ throw new Error('name must be a plain file name in this folder (no "/", "\\", or "..").');
1748
+ }
1721
1749
  if (!/\.[a-z0-9]+$/i.test(base)) base += ext;
1722
1750
  const path = join7(process.cwd(), base);
1723
1751
  writeFileSync6(path, renderReply(reply, format), "utf8");
@@ -1877,15 +1905,17 @@ async function chat() {
1877
1905
  const cliEnabled = BRAIN_IDS.filter((id) => cfg.brains[id].enabled);
1878
1906
  const apiIds = cfg.apiBrains.map((a) => a.id);
1879
1907
  const all = [...cliEnabled, ...apiIds];
1880
- if (arg && all.includes(arg)) {
1881
- const next = resolveActive2(cfg, arg);
1908
+ const rawArg = parts[1] || "";
1909
+ const match = all.find((id) => id.toLowerCase() === rawArg.toLowerCase());
1910
+ if (rawArg && match) {
1911
+ const next = resolveActive2(cfg, match);
1882
1912
  if (next) {
1883
1913
  active = next;
1884
1914
  const turns = Math.floor(history.length / 2);
1885
1915
  console.log(c.green(` switched to ${active.label}. Context kept (${turns} turn${turns === 1 ? "" : "s"}).`));
1886
1916
  }
1887
- } else if (arg) {
1888
- console.log(c.dim(` "${arg}" is not available. Available: ${all.join(", ") || "none"}`));
1917
+ } else if (rawArg) {
1918
+ console.log(c.dim(` "${rawArg}" is not available. Available: ${all.join(", ") || "none"}`));
1889
1919
  } else {
1890
1920
  const cfgRef = cfg;
1891
1921
  const activeId = active.id;
@@ -1958,9 +1988,16 @@ async function chat() {
1958
1988
  async function login(which) {
1959
1989
  const id = (which || "").toLowerCase();
1960
1990
  if (!BRAIN_IDS.includes(id)) {
1961
- console.log(c.dim(`
1991
+ if (which) {
1992
+ console.error(`
1993
+ Unknown brain "${which}". Use one of: ${BRAIN_IDS.join(", ")}
1994
+ `);
1995
+ process.exitCode = 1;
1996
+ } else {
1997
+ console.log(c.dim(`
1962
1998
  Usage: holt login <${BRAIN_IDS.join("|")}>
1963
1999
  `));
2000
+ }
1964
2001
  return;
1965
2002
  }
1966
2003
  const s = BRAIN_SETUP[id];
@@ -2046,6 +2083,14 @@ async function memoryCmd(sub, rest = []) {
2046
2083
  close();
2047
2084
  return;
2048
2085
  }
2086
+ if (action && !["clear", "embed", "facts", "search"].includes(action)) {
2087
+ console.error(`
2088
+ Unknown memory subcommand: "${sub}". Use: search <query> | facts | embed | clear
2089
+ `);
2090
+ process.exitCode = 1;
2091
+ close();
2092
+ return;
2093
+ }
2049
2094
  const s = memStats();
2050
2095
  const embedOk = await embeddingsAvailable();
2051
2096
  const sessions = new Set(loadTurns().map((t) => t.session)).size;
@@ -2170,9 +2215,10 @@ function cmdShow(name) {
2170
2215
  }
2171
2216
  const loaded = loadSkill(name);
2172
2217
  if (!loaded) {
2173
- console.log(c.dim(`
2218
+ console.error(c.dim(`
2174
2219
  No skill named "${sanitizeName(name)}". Try "holt skill list".
2175
2220
  `));
2221
+ process.exitCode = 1;
2176
2222
  return;
2177
2223
  }
2178
2224
  const { skill, body } = loaded;
@@ -2192,9 +2238,10 @@ function cmdCreate(name, global) {
2192
2238
  const scope = global ? "global" : "workspace";
2193
2239
  const dir = join8(skillsRoot(scope), clean);
2194
2240
  if (existsSync8(dir)) {
2195
- console.log(c.red(`
2241
+ console.error(c.red(`
2196
2242
  A ${scope} skill named "${clean}" already exists.`));
2197
- console.log(c.dim(" " + dir + "\n"));
2243
+ console.error(c.dim(" " + dir + "\n"));
2244
+ process.exitCode = 1;
2198
2245
  return;
2199
2246
  }
2200
2247
  mkdirSync6(dir, { recursive: true });
@@ -2219,22 +2266,25 @@ function cmdAdd(source, global) {
2219
2266
  Cloning ${source} ...`));
2220
2267
  const res = spawnSync2("git", ["clone", "--depth", "1", source, tempDir], { stdio: "ignore" });
2221
2268
  if (res.status !== 0) {
2222
- console.log(c.red(" Clone failed. Check the URL and that git is installed.\n"));
2269
+ console.error(c.red(" Clone failed. Check the URL and that git is installed.\n"));
2270
+ process.exitCode = 1;
2223
2271
  return;
2224
2272
  }
2225
2273
  fetched = tempDir;
2226
2274
  } else {
2227
2275
  fetched = resolve(source);
2228
2276
  if (!existsSync8(fetched)) {
2229
- console.log(c.red(`
2277
+ console.error(c.red(`
2230
2278
  No such path: ${fetched}
2231
2279
  `));
2280
+ process.exitCode = 1;
2232
2281
  return;
2233
2282
  }
2234
2283
  }
2235
2284
  const skillDir = findSkillDir(fetched);
2236
2285
  if (!skillDir) {
2237
- console.log(c.red("\n Could not find a SKILL.md at the source root or in a single subfolder.\n"));
2286
+ console.error(c.red("\n Could not find a SKILL.md at the source root or in a single subfolder.\n"));
2287
+ process.exitCode = 1;
2238
2288
  return;
2239
2289
  }
2240
2290
  let skillName = "";
@@ -2246,14 +2296,16 @@ function cmdAdd(source, global) {
2246
2296
  }
2247
2297
  if (!skillName) skillName = sanitizeName(skillDir.split(/[\\/]/).pop() || "");
2248
2298
  if (!skillName) {
2249
- console.log(c.red("\n Could not determine a valid skill name.\n"));
2299
+ console.error(c.red("\n Could not determine a valid skill name.\n"));
2300
+ process.exitCode = 1;
2250
2301
  return;
2251
2302
  }
2252
2303
  const dest = join8(skillsRoot(scope), skillName);
2253
2304
  if (existsSync8(dest)) {
2254
- console.log(c.red(`
2305
+ console.error(c.red(`
2255
2306
  A ${scope} skill named "${skillName}" already exists.`));
2256
- console.log(c.dim(" " + dest + "\n"));
2307
+ console.error(c.dim(" " + dest + "\n"));
2308
+ process.exitCode = 1;
2257
2309
  return;
2258
2310
  }
2259
2311
  mkdirSync6(skillsRoot(scope), { recursive: true });
@@ -2282,9 +2334,10 @@ async function cmdRemove(name, ask) {
2282
2334
  const globalDir = join8(skillsRoot("global"), clean);
2283
2335
  const target = existsSync8(wsDir) ? wsDir : existsSync8(globalDir) ? globalDir : "";
2284
2336
  if (!target) {
2285
- console.log(c.dim(`
2337
+ console.error(c.dim(`
2286
2338
  No skill named "${clean}" found.
2287
2339
  `));
2340
+ process.exitCode = 1;
2288
2341
  return;
2289
2342
  }
2290
2343
  const scope = target === wsDir ? "workspace" : "global";
@@ -2330,6 +2383,7 @@ async function skillCmd(sub, rest = []) {
2330
2383
  await cmdRemove(args[0], ask);
2331
2384
  break;
2332
2385
  default:
2386
+ if (action) process.exitCode = 1;
2333
2387
  usage();
2334
2388
  }
2335
2389
  } finally {
@@ -2504,7 +2558,7 @@ function buildGraph(turns) {
2504
2558
  return { nodes, edges };
2505
2559
  }
2506
2560
  function escapeForScript(json) {
2507
- return json.replace(/<\/(script)/gi, "<\\/$1").replace(/<!--/g, "<\\!--");
2561
+ return json.replace(/<\/(script)/gi, "<\\/$1").replace(/<!--/g, "<\\u0021--");
2508
2562
  }
2509
2563
  function escapeHtml2(s) {
2510
2564
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -2617,7 +2671,14 @@ function renderGraphHtml(graph2, meta) {
2617
2671
  <script>
2618
2672
  (function () {
2619
2673
  "use strict";
2620
- var DATA = JSON.parse(document.getElementById("graph-data").textContent);
2674
+ var DATA;
2675
+ try {
2676
+ DATA = JSON.parse(document.getElementById("graph-data").textContent);
2677
+ } catch (e) {
2678
+ document.getElementById("empty").textContent = "Could not read the graph data.";
2679
+ document.getElementById("empty").style.display = "flex";
2680
+ return;
2681
+ }
2621
2682
  var nodes = DATA.nodes || [];
2622
2683
  var edges = DATA.edges || [];
2623
2684
 
@@ -3177,6 +3238,7 @@ function runMcpServer(opts) {
3177
3238
 
3178
3239
  // src/mcp/tools.ts
3179
3240
  var MCP_SESSION = "__mcp__";
3241
+ var MCP_RECALL_SESSION = "__mcp_recall__";
3180
3242
  function holtTools() {
3181
3243
  return [
3182
3244
  {
@@ -3195,7 +3257,7 @@ function holtTools() {
3195
3257
  const query = String(args.query || "").trim();
3196
3258
  if (!query) return "Provide a query to recall.";
3197
3259
  const k = Number(args.k) || 5;
3198
- const hits = await recall(query, MCP_SESSION, k);
3260
+ const hits = await recall(query, MCP_RECALL_SESSION, k);
3199
3261
  if (hits.length === 0) return "No relevant memory found.";
3200
3262
  const lines = hits.map(
3201
3263
  (h) => `${h.score.toFixed(2)} (${h.turn.role}) ${h.turn.content.replace(/\s+/g, " ").trim()}`
@@ -3287,6 +3349,9 @@ function holtTools() {
3287
3349
  ];
3288
3350
  }
3289
3351
 
3352
+ // src/version.ts
3353
+ var VERSION = "0.8.3";
3354
+
3290
3355
  // src/commands/mcp.ts
3291
3356
  function printSetup() {
3292
3357
  const config = JSON.stringify(
@@ -3315,10 +3380,15 @@ async function mcp(sub, rest = []) {
3315
3380
  printSetup();
3316
3381
  return;
3317
3382
  }
3383
+ if (sub !== void 0) {
3384
+ console.error(`Unknown mcp subcommand: "${sub}". Use "holt mcp" to serve, or "holt mcp setup".`);
3385
+ process.exitCode = 1;
3386
+ return;
3387
+ }
3318
3388
  try {
3319
3389
  if (!isTrusted()) trustDir();
3320
3390
  console.error("Holt MCP: serving memory for " + workspace());
3321
- await runMcpServer({ name: "holt", version: "0.7.0", tools: holtTools() });
3391
+ await runMcpServer({ name: "holt", version: VERSION, tools: holtTools() });
3322
3392
  } catch (err2) {
3323
3393
  console.error("Holt MCP failed: " + (err2 instanceof Error ? err2.message : String(err2)));
3324
3394
  process.exitCode = 1;
@@ -3403,21 +3473,27 @@ async function run(args) {
3403
3473
  process.stdout.write(ch);
3404
3474
  }
3405
3475
  });
3476
+ if (!result.ok) {
3477
+ if (streamed) process.stdout.write("\n");
3478
+ process.stderr.write(c.red(result.text) + "\n");
3479
+ process.exitCode = 1;
3480
+ return;
3481
+ }
3406
3482
  if (!p.quiet && !streamed) {
3407
3483
  process.stdout.write(result.text);
3408
3484
  }
3409
3485
  if (!p.quiet && (streamed || result.text) && !result.text.endsWith("\n")) {
3410
3486
  process.stdout.write("\n");
3411
3487
  }
3412
- if (!result.ok) {
3413
- process.stderr.write(c.red(result.text) + "\n");
3414
- process.exitCode = 1;
3415
- return;
3416
- }
3417
3488
  if (p.out) {
3418
3489
  const target = resolve3(process.cwd(), p.out);
3419
- writeFileSync9(target, result.text, "utf8");
3420
- process.stderr.write(c.dim(`saved to ${target}`) + "\n");
3490
+ try {
3491
+ writeFileSync9(target, result.text, "utf8");
3492
+ process.stderr.write(c.dim(`saved to ${target}`) + "\n");
3493
+ } catch (e) {
3494
+ process.stderr.write(c.red(`Could not write ${target}: ${e.message}`) + "\n");
3495
+ process.exitCode = 1;
3496
+ }
3421
3497
  }
3422
3498
  }
3423
3499
 
@@ -3619,12 +3695,14 @@ async function addCmd(rest) {
3619
3695
  const { task, when, opts } = parseAdd(rest);
3620
3696
  if (!task || !when) {
3621
3697
  console.log(c.dim('\n Usage: holt schedule add "<task>" <HH:MM> [--name <n>] [--notify] [--brain <id>]\n'));
3698
+ process.exitCode = 1;
3622
3699
  return;
3623
3700
  }
3624
3701
  try {
3625
3702
  parseWhen(when);
3626
3703
  } catch (e) {
3627
- console.log("\n " + c.red(e.message) + "\n");
3704
+ console.error("\n " + c.red(e.message) + "\n");
3705
+ process.exitCode = 1;
3628
3706
  return;
3629
3707
  }
3630
3708
  const ws = workspace();
@@ -3689,6 +3767,7 @@ function removeCmd(rest) {
3689
3767
  const id = rest[0];
3690
3768
  if (!id) {
3691
3769
  console.log(c.dim("\n Usage: holt schedule remove <id> (see ids with: holt schedule list)\n"));
3770
+ process.exitCode = 1;
3692
3771
  return;
3693
3772
  }
3694
3773
  const before = loadJobs();
@@ -3697,9 +3776,12 @@ function removeCmd(rest) {
3697
3776
  if (process.platform === "darwin") removeDarwin(id);
3698
3777
  else if (process.platform === "linux") removeLinux(id);
3699
3778
  if (existed) console.log("\n" + c.green(` Removed schedule ${id}.`) + "\n");
3700
- else console.log(c.dim(`
3779
+ else {
3780
+ console.error(c.dim(`
3701
3781
  No schedule with id ${id}. Nothing to remove.
3702
3782
  `));
3783
+ process.exitCode = 1;
3784
+ }
3703
3785
  }
3704
3786
  function usage2() {
3705
3787
  console.log("\n" + c.accent("holt schedule") + c.dim(" run a task automatically on a timer"));
@@ -3714,6 +3796,7 @@ async function schedule(sub, rest = []) {
3714
3796
  return;
3715
3797
  }
3716
3798
  if (action !== "add" && action !== "remove" && action !== "rm") {
3799
+ if (action) process.exitCode = 1;
3717
3800
  usage2();
3718
3801
  return;
3719
3802
  }
@@ -3768,7 +3851,6 @@ function readStdin() {
3768
3851
  }
3769
3852
 
3770
3853
  // src/cli.ts
3771
- var VERSION = "0.8.2";
3772
3854
  var BANNER = `
3773
3855
  \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
3774
3856
  \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
@@ -3857,9 +3939,9 @@ async function main() {
3857
3939
  await notify(process.argv.slice(3));
3858
3940
  break;
3859
3941
  default:
3860
- console.log(`
3942
+ console.error(`
3861
3943
  Unknown command: "${cmd}"`);
3862
- console.log(` Run "holt help" for usage.
3944
+ console.error(` Run "holt help" for usage.
3863
3945
  `);
3864
3946
  process.exitCode = 1;
3865
3947
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holt-os/holt",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
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",