@holt-os/holt 0.5.1 → 0.5.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 +135 -37
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -37,7 +37,7 @@ During `holt init` you:
37
37
  1. **Trust the folder.**
38
38
  2. **Choose brains** (claude, codex, gemini). Holt installs any you pick that are missing.
39
39
  3. **Sign in.** For a newly installed brain, Holt starts that tool's own login (browser or its own prompt). Holt never stores your credentials.
40
- 4. **Pick a default** brain and, optionally, a **launch command** (a short word like `ai` that runs `holt chat`).
40
+ 4. **Pick a default** brain and, optionally, a **launch command**: a short word like `ai` that starts `holt chat`. Holt installs it as a tiny launcher next to its own binary, so it works immediately in the same terminal, no sourcing or restart needed.
41
41
  5. **Enable semantic memory.** If you say yes, Holt sets up a local [Ollama](https://ollama.com) with a small embed model so recall works by meaning, fully offline.
42
42
 
43
43
  ## Using it
package/dist/cli.js CHANGED
@@ -248,17 +248,58 @@ function runBrain(brain, prompt, onChunk) {
248
248
 
249
249
  // src/alias.ts
250
250
  import { homedir as homedir2 } from "os";
251
- import { join as join3 } from "path";
252
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3 } from "fs";
251
+ import { join as join3, dirname, delimiter } from "path";
252
+ import {
253
+ readFileSync as readFileSync3,
254
+ writeFileSync as writeFileSync3,
255
+ existsSync as existsSync3,
256
+ mkdirSync as mkdirSync3,
257
+ rmSync,
258
+ chmodSync,
259
+ realpathSync
260
+ } from "fs";
253
261
  var START = "# >>> holt launch alias >>>";
254
262
  var END = "# <<< holt launch alias <<<";
263
+ var SHIM_MARKER = "# holt launcher";
264
+ var GLOBAL_DIR2 = join3(homedir2(), ".holt");
265
+ var LAUNCHER_STATE = join3(GLOBAL_DIR2, "launcher.json");
255
266
  function rcFile() {
256
267
  const shell = process.env.SHELL || "";
257
268
  if (shell.includes("zsh")) return join3(homedir2(), ".zshrc");
258
269
  if (shell.includes("bash")) return join3(homedir2(), ".bashrc");
259
270
  return join3(homedir2(), ".profile");
260
271
  }
261
- function installAlias(name) {
272
+ function readState() {
273
+ try {
274
+ return JSON.parse(readFileSync3(LAUNCHER_STATE, "utf8"));
275
+ } catch {
276
+ return null;
277
+ }
278
+ }
279
+ function writeState(state) {
280
+ mkdirSync3(GLOBAL_DIR2, { recursive: true });
281
+ if (state) writeFileSync3(LAUNCHER_STATE, JSON.stringify(state, null, 2) + "\n", "utf8");
282
+ else if (existsSync3(LAUNCHER_STATE)) rmSync(LAUNCHER_STATE);
283
+ }
284
+ function pathDirs() {
285
+ return (process.env.PATH || "").split(delimiter).filter(Boolean);
286
+ }
287
+ function launcherBinDir() {
288
+ const dirs = pathDirs();
289
+ const candidates = [];
290
+ const invoked = process.argv[1];
291
+ if (invoked) candidates.push(dirname(invoked));
292
+ candidates.push(dirname(process.execPath));
293
+ for (const dir of candidates) {
294
+ if (!dirs.includes(dir)) continue;
295
+ try {
296
+ if (dirs.includes(dir) || dirs.includes(realpathSync(dir))) return dir;
297
+ } catch {
298
+ }
299
+ }
300
+ return null;
301
+ }
302
+ function installRcAlias(name) {
262
303
  const file = rcFile();
263
304
  const block = `${START}
264
305
  alias ${name}="holt chat"
@@ -269,30 +310,80 @@ ${END}`;
269
310
  if (re.test(content)) content = content.replace(re, block);
270
311
  else content = content.replace(/\n*$/, "\n") + block + "\n";
271
312
  writeFileSync3(file, content, "utf8");
272
- return { ok: true, file };
313
+ writeState({ name, kind: "rc", file });
314
+ return { ok: true, kind: "rc", file, immediate: false };
273
315
  } catch (e) {
274
- return { ok: false, file, message: `Could not write ${file}: ${e.message}` };
316
+ return { ok: false, kind: "none", file, immediate: false, message: `Could not write ${file}: ${e.message}` };
275
317
  }
276
318
  }
277
- function currentAlias() {
278
- const file = rcFile();
279
- try {
280
- const m = readFileSync3(file, "utf8").match(/alias\s+([^\s=]+)="holt chat"/);
281
- return m ? m[1] : null;
282
- } catch {
283
- return null;
319
+ function installAlias(name) {
320
+ removeAlias();
321
+ if (process.platform !== "win32") {
322
+ const binDir = launcherBinDir();
323
+ if (binDir) {
324
+ const file = join3(binDir, name);
325
+ if (existsSync3(file)) {
326
+ try {
327
+ if (!readFileSync3(file, "utf8").includes(SHIM_MARKER)) {
328
+ return { ok: false, kind: "none", file, immediate: false, message: `"${name}" already exists at ${file}. Pick another word.` };
329
+ }
330
+ } catch {
331
+ return { ok: false, kind: "none", file, immediate: false, message: `"${name}" already exists at ${file}. Pick another word.` };
332
+ }
333
+ }
334
+ try {
335
+ writeFileSync3(file, `#!/bin/sh
336
+ ${SHIM_MARKER}
337
+ exec holt chat "$@"
338
+ `, "utf8");
339
+ chmodSync(file, 493);
340
+ writeState({ name, kind: "bin", file });
341
+ return { ok: true, kind: "bin", file, immediate: true };
342
+ } catch {
343
+ }
344
+ }
284
345
  }
346
+ return installRcAlias(name);
285
347
  }
286
348
  function removeAlias() {
349
+ const state = readState();
350
+ if (state?.kind === "bin" && existsSync3(state.file)) {
351
+ try {
352
+ if (readFileSync3(state.file, "utf8").includes(SHIM_MARKER)) rmSync(state.file);
353
+ } catch {
354
+ }
355
+ }
287
356
  const file = rcFile();
288
357
  try {
289
358
  if (existsSync3(file)) {
290
359
  const content = readFileSync3(file, "utf8").replace(new RegExp(`\\n*${START}[\\s\\S]*?${END}\\n*`), "\n");
291
360
  writeFileSync3(file, content, "utf8");
292
361
  }
293
- return { ok: true, file };
362
+ writeState(null);
363
+ return { ok: true, kind: "none", file, immediate: true };
294
364
  } catch (e) {
295
- return { ok: false, file, message: e.message };
365
+ writeState(null);
366
+ return { ok: false, kind: "none", file, immediate: true, message: e.message };
367
+ }
368
+ }
369
+ function currentAlias() {
370
+ const state = readState();
371
+ if (state) {
372
+ if (state.kind === "bin") {
373
+ try {
374
+ if (existsSync3(state.file) && readFileSync3(state.file, "utf8").includes(SHIM_MARKER)) return state.name;
375
+ } catch {
376
+ return null;
377
+ }
378
+ return null;
379
+ }
380
+ return state.name;
381
+ }
382
+ try {
383
+ const m = readFileSync3(rcFile(), "utf8").match(/alias\s+([^\s=]+)="holt chat"/);
384
+ return m ? m[1] : null;
385
+ } catch {
386
+ return null;
296
387
  }
297
388
  }
298
389
 
@@ -313,7 +404,7 @@ function runInteractive(cmd, args) {
313
404
  }
314
405
 
315
406
  // src/memory.ts
316
- import { appendFileSync, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync3, rmSync, statSync } from "fs";
407
+ import { appendFileSync, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync4, rmSync as rmSync2, statSync } from "fs";
317
408
  import { join as join4 } from "path";
318
409
  import { randomUUID } from "crypto";
319
410
  var OLLAMA_URL = process.env.HOLT_OLLAMA_URL || "http://127.0.0.1:11434";
@@ -373,11 +464,11 @@ function loadTurns() {
373
464
  return out;
374
465
  }
375
466
  function appendTurn(t) {
376
- mkdirSync3(memDir(), { recursive: true });
467
+ mkdirSync4(memDir(), { recursive: true });
377
468
  appendFileSync(memPath(), JSON.stringify(t) + "\n", "utf8");
378
469
  }
379
470
  function clearMemory() {
380
- if (existsSync4(memPath())) rmSync(memPath());
471
+ if (existsSync4(memPath())) rmSync2(memPath());
381
472
  }
382
473
  function memStats() {
383
474
  const turns = loadTurns();
@@ -442,7 +533,7 @@ async function backfillEmbeddings(onProgress) {
442
533
  done++;
443
534
  if (onProgress) onProgress(done, missing.length);
444
535
  }
445
- mkdirSync3(memDir(), { recursive: true });
536
+ mkdirSync4(memDir(), { recursive: true });
446
537
  writeFileSync4(memPath(), turns.map((t) => JSON.stringify(t)).join("\n") + "\n", "utf8");
447
538
  return { embedded, total: missing.length };
448
539
  }
@@ -572,9 +663,10 @@ async function runSettings(ask) {
572
663
  } else if (choice === "a") {
573
664
  const name = (await ask(" launch command (blank to reset to holt): ") ?? "").trim();
574
665
  if (name && name !== "holt") {
575
- if (isInstalled(name)) console.log(c.dim(` note: "${name}" already exists; the alias will shadow it in new shells.`));
576
666
  const r = installAlias(name);
577
- console.log(r.ok ? c.green(` alias "${name}" -> holt chat added to ${r.file} (run: source ${r.file})`) : c.red(" " + r.message));
667
+ if (r.ok && r.immediate) console.log(c.green(` "${name}" is ready to use right now.`));
668
+ else if (r.ok) console.log(c.green(` alias "${name}" added to ${r.file}. Run: source ${r.file} (new terminals will not need it).`));
669
+ else console.log(c.red(" " + r.message));
578
670
  } else {
579
671
  removeAlias();
580
672
  console.log(c.dim(" reset to holt."));
@@ -646,12 +738,16 @@ Default brain? [${chosen.join("/")}] (${defPick}): `) ?? "").trim();
646
738
  const defaultBrain = chosen.includes(dans) ? dans : defPick;
647
739
  const aliasAns = (await ask('Launch command? Type a custom word like "ai", or press enter to keep "holt": ') ?? "").trim();
648
740
  let aliasNote = "";
649
- let aliasRcFile = "";
741
+ let aliasNeedsSource = "";
742
+ let aliasWorked = false;
650
743
  if (aliasAns && aliasAns !== "holt") {
651
- if (isInstalled(aliasAns)) console.log(c.dim(` note: "${aliasAns}" already exists; the alias will shadow it in new shells.`));
652
744
  const r = installAlias(aliasAns);
653
- if (r.ok) aliasRcFile = r.file;
654
- aliasNote = r.ok ? c.green(` alias "${aliasAns}" -> holt chat added to ${r.file}`) : c.red(" " + r.message);
745
+ aliasWorked = r.ok;
746
+ if (r.ok && r.immediate) aliasNote = c.green(` "${aliasAns}" is ready to use right now (launcher at ${r.file}).`);
747
+ else if (r.ok) {
748
+ aliasNote = c.green(` alias "${aliasAns}" -> holt chat added to ${r.file}`);
749
+ aliasNeedsSource = r.file;
750
+ } else aliasNote = c.red(" " + r.message);
655
751
  }
656
752
  let wantMemorySetup = false;
657
753
  const embedReady = await embeddingsAvailable();
@@ -708,9 +804,11 @@ Default brain? [${chosen.join("/")}] (${defPick}): `) ?? "").trim();
708
804
  console.log("\n" + c.green("Saved to ./.holt/config.json"));
709
805
  if (aliasNote) console.log(aliasNote);
710
806
  if (cfg.defaultBrain) {
711
- if (aliasRcFile) {
807
+ if (aliasWorked && !aliasNeedsSource) {
808
+ console.log("Start chatting: " + c.accent(aliasAns) + "\n");
809
+ } else if (aliasNeedsSource) {
712
810
  console.log("\nStart chatting:");
713
- console.log(" " + c.accent(`source ${aliasRcFile}`) + c.dim(" (once; new terminals will not need it)"));
811
+ console.log(" " + c.accent(`source ${aliasNeedsSource}`) + c.dim(" (once; new terminals will not need it)"));
714
812
  console.log(" " + c.accent(aliasAns) + "\n");
715
813
  console.log(c.dim(" Or right now, without sourcing: holt chat\n"));
716
814
  } else {
@@ -1458,13 +1556,13 @@ async function memoryCmd(sub, rest = []) {
1458
1556
 
1459
1557
  // src/commands/skill.ts
1460
1558
  import {
1461
- mkdirSync as mkdirSync4,
1559
+ mkdirSync as mkdirSync5,
1462
1560
  writeFileSync as writeFileSync6,
1463
1561
  readFileSync as readFileSync6,
1464
1562
  existsSync as existsSync6,
1465
1563
  readdirSync as readdirSync2,
1466
1564
  statSync as statSync3,
1467
- rmSync as rmSync2,
1565
+ rmSync as rmSync3,
1468
1566
  cpSync,
1469
1567
  mkdtempSync
1470
1568
  } from "fs";
@@ -1586,7 +1684,7 @@ function cmdCreate(name, global) {
1586
1684
  console.log(c.dim(" " + dir + "\n"));
1587
1685
  return;
1588
1686
  }
1589
- mkdirSync4(dir, { recursive: true });
1687
+ mkdirSync5(dir, { recursive: true });
1590
1688
  writeFileSync6(join7(dir, "SKILL.md"), SKILL_TEMPLATE(clean), "utf8");
1591
1689
  console.log(c.green(`
1592
1690
  Created ${scope} skill "${clean}".`));
@@ -1645,17 +1743,17 @@ function cmdAdd(source, global) {
1645
1743
  console.log(c.dim(" " + dest + "\n"));
1646
1744
  return;
1647
1745
  }
1648
- mkdirSync4(skillsRoot(scope), { recursive: true });
1746
+ mkdirSync5(skillsRoot(scope), { recursive: true });
1649
1747
  cpSync(skillDir, dest, { recursive: true });
1650
1748
  const nestedGit = join7(dest, ".git");
1651
- if (existsSync6(nestedGit)) rmSync2(nestedGit, { recursive: true, force: true });
1749
+ if (existsSync6(nestedGit)) rmSync3(nestedGit, { recursive: true, force: true });
1652
1750
  console.log(c.green(`
1653
1751
  Installed ${scope} skill "${skillName}".`));
1654
1752
  console.log(c.dim(" " + join7(dest, "SKILL.md") + "\n"));
1655
1753
  } finally {
1656
1754
  if (tempDir && existsSync6(tempDir)) {
1657
1755
  try {
1658
- rmSync2(tempDir, { recursive: true, force: true });
1756
+ rmSync3(tempDir, { recursive: true, force: true });
1659
1757
  } catch {
1660
1758
  }
1661
1759
  }
@@ -1680,7 +1778,7 @@ async function cmdRemove(name, ask) {
1680
1778
  const a = (await ask(`
1681
1779
  Delete ${scope} skill "${clean}"? [y/N] `) ?? "").trim().toLowerCase();
1682
1780
  if (a === "y" || a === "yes") {
1683
- rmSync2(target, { recursive: true, force: true });
1781
+ rmSync3(target, { recursive: true, force: true });
1684
1782
  console.log(c.green(" Removed.\n"));
1685
1783
  } else {
1686
1784
  console.log(c.dim(" Kept.\n"));
@@ -1727,8 +1825,8 @@ async function skillCmd(sub, rest = []) {
1727
1825
  }
1728
1826
 
1729
1827
  // src/commands/graph.ts
1730
- import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "fs";
1731
- import { join as join8, dirname, resolve as resolve2 } from "path";
1828
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6 } from "fs";
1829
+ import { join as join8, dirname as dirname2, resolve as resolve2 } from "path";
1732
1830
  import { spawn as spawn3 } from "child_process";
1733
1831
 
1734
1832
  // src/graphview.ts
@@ -2393,7 +2491,7 @@ async function graph(args = []) {
2393
2491
  edges: g.edges.length
2394
2492
  });
2395
2493
  const outPath = opts.out ? resolve2(opts.out) : join8(wsHoltDir(), "graph.html");
2396
- mkdirSync5(dirname(outPath), { recursive: true });
2494
+ mkdirSync6(dirname2(outPath), { recursive: true });
2397
2495
  writeFileSync7(outPath, html, "utf8");
2398
2496
  console.log("\n" + c.accent("Memory graph built") + c.dim(" (this folder)"));
2399
2497
  console.log(` nodes ${g.nodes.length} (${turns.length} turns, ${conceptCount} concepts)`);
@@ -2408,7 +2506,7 @@ async function graph(args = []) {
2408
2506
  }
2409
2507
 
2410
2508
  // src/cli.ts
2411
- var VERSION = "0.5.1";
2509
+ var VERSION = "0.5.2";
2412
2510
  var BANNER = `
2413
2511
  \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
2414
2512
  \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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holt-os/holt",
3
- "version": "0.5.1",
3
+ "version": "0.5.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",