@holt-os/holt 0.5.0 → 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 +158 -39
  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
  }
@@ -471,12 +562,21 @@ function printStatus(cfg) {
471
562
  console.log("\n " + c.dim("[d] default brain [t] toggle brain [c] connect API brain [x] remove API brain [a] launch command [enter] done"));
472
563
  }
473
564
  async function connectApiBrain(ask, cfg) {
474
- const provRaw = (await ask(` provider [${PROVIDERS.join("/")}]: `) ?? "").trim().toLowerCase();
475
- if (!PROVIDERS.includes(provRaw)) {
476
- console.log(c.dim(" cancelled (unknown provider)."));
565
+ let provider = null;
566
+ for (let tries = 0; tries < 3 && !provider; tries++) {
567
+ const provRaw = (await ask(` provider [${PROVIDERS.join("/")}] (enter for ${PROVIDERS[0]}, or "skip"): `) ?? "skip").trim().toLowerCase();
568
+ if (provRaw === "skip" || provRaw === "q" || provRaw === "n" || provRaw === "no") {
569
+ console.log(c.dim(' skipped. Add one later with "holt setting" then "c".'));
570
+ return null;
571
+ }
572
+ if (provRaw === "") provider = PROVIDERS[0];
573
+ else if (PROVIDERS.includes(provRaw)) provider = provRaw;
574
+ else console.log(c.dim(` "${provRaw}" is not a provider. Type one of: ${PROVIDERS.join(", ")}`));
575
+ }
576
+ if (!provider) {
577
+ console.log(c.dim(' skipped after three tries. Add one later with "holt setting" then "c".'));
477
578
  return null;
478
579
  }
479
- const provider = provRaw;
480
580
  const suggestion = PROVIDER_MODEL_SUGGESTION[provider];
481
581
  const modelRaw = (await ask(` model (enter for ${suggestion}): `) ?? "").trim();
482
582
  const model = modelRaw || suggestion;
@@ -563,9 +663,10 @@ async function runSettings(ask) {
563
663
  } else if (choice === "a") {
564
664
  const name = (await ask(" launch command (blank to reset to holt): ") ?? "").trim();
565
665
  if (name && name !== "holt") {
566
- if (isInstalled(name)) console.log(c.dim(` note: "${name}" already exists; the alias will shadow it in new shells.`));
567
666
  const r = installAlias(name);
568
- 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));
569
670
  } else {
570
671
  removeAlias();
571
672
  console.log(c.dim(" reset to holt."));
@@ -637,10 +738,16 @@ Default brain? [${chosen.join("/")}] (${defPick}): `) ?? "").trim();
637
738
  const defaultBrain = chosen.includes(dans) ? dans : defPick;
638
739
  const aliasAns = (await ask('Launch command? Type a custom word like "ai", or press enter to keep "holt": ') ?? "").trim();
639
740
  let aliasNote = "";
741
+ let aliasNeedsSource = "";
742
+ let aliasWorked = false;
640
743
  if (aliasAns && aliasAns !== "holt") {
641
- if (isInstalled(aliasAns)) console.log(c.dim(` note: "${aliasAns}" already exists; the alias will shadow it in new shells.`));
642
744
  const r = installAlias(aliasAns);
643
- aliasNote = r.ok ? c.green(` alias "${aliasAns}" -> holt chat added to ${r.file} (run: source ${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);
644
751
  }
645
752
  let wantMemorySetup = false;
646
753
  const embedReady = await embeddingsAvailable();
@@ -696,8 +803,20 @@ Default brain? [${chosen.join("/")}] (${defPick}): `) ?? "").trim();
696
803
  saveConfig(cfg);
697
804
  console.log("\n" + c.green("Saved to ./.holt/config.json"));
698
805
  if (aliasNote) console.log(aliasNote);
699
- if (cfg.defaultBrain) console.log("Start chatting: " + c.accent(aliasAns && aliasAns !== "holt" ? aliasAns : "holt chat") + "\n");
700
- else console.log(c.dim('No brain is ready yet. Install one, then run "holt init" again.\n'));
806
+ if (cfg.defaultBrain) {
807
+ if (aliasWorked && !aliasNeedsSource) {
808
+ console.log("Start chatting: " + c.accent(aliasAns) + "\n");
809
+ } else if (aliasNeedsSource) {
810
+ console.log("\nStart chatting:");
811
+ console.log(" " + c.accent(`source ${aliasNeedsSource}`) + c.dim(" (once; new terminals will not need it)"));
812
+ console.log(" " + c.accent(aliasAns) + "\n");
813
+ console.log(c.dim(" Or right now, without sourcing: holt chat\n"));
814
+ } else {
815
+ console.log("Start chatting: " + c.accent("holt chat") + "\n");
816
+ }
817
+ } else {
818
+ console.log(c.dim('No brain is ready yet. Install one, then run "holt init" again.\n'));
819
+ }
701
820
  }
702
821
 
703
822
  // src/commands/chat.ts
@@ -1437,13 +1556,13 @@ async function memoryCmd(sub, rest = []) {
1437
1556
 
1438
1557
  // src/commands/skill.ts
1439
1558
  import {
1440
- mkdirSync as mkdirSync4,
1559
+ mkdirSync as mkdirSync5,
1441
1560
  writeFileSync as writeFileSync6,
1442
1561
  readFileSync as readFileSync6,
1443
1562
  existsSync as existsSync6,
1444
1563
  readdirSync as readdirSync2,
1445
1564
  statSync as statSync3,
1446
- rmSync as rmSync2,
1565
+ rmSync as rmSync3,
1447
1566
  cpSync,
1448
1567
  mkdtempSync
1449
1568
  } from "fs";
@@ -1565,7 +1684,7 @@ function cmdCreate(name, global) {
1565
1684
  console.log(c.dim(" " + dir + "\n"));
1566
1685
  return;
1567
1686
  }
1568
- mkdirSync4(dir, { recursive: true });
1687
+ mkdirSync5(dir, { recursive: true });
1569
1688
  writeFileSync6(join7(dir, "SKILL.md"), SKILL_TEMPLATE(clean), "utf8");
1570
1689
  console.log(c.green(`
1571
1690
  Created ${scope} skill "${clean}".`));
@@ -1624,17 +1743,17 @@ function cmdAdd(source, global) {
1624
1743
  console.log(c.dim(" " + dest + "\n"));
1625
1744
  return;
1626
1745
  }
1627
- mkdirSync4(skillsRoot(scope), { recursive: true });
1746
+ mkdirSync5(skillsRoot(scope), { recursive: true });
1628
1747
  cpSync(skillDir, dest, { recursive: true });
1629
1748
  const nestedGit = join7(dest, ".git");
1630
- if (existsSync6(nestedGit)) rmSync2(nestedGit, { recursive: true, force: true });
1749
+ if (existsSync6(nestedGit)) rmSync3(nestedGit, { recursive: true, force: true });
1631
1750
  console.log(c.green(`
1632
1751
  Installed ${scope} skill "${skillName}".`));
1633
1752
  console.log(c.dim(" " + join7(dest, "SKILL.md") + "\n"));
1634
1753
  } finally {
1635
1754
  if (tempDir && existsSync6(tempDir)) {
1636
1755
  try {
1637
- rmSync2(tempDir, { recursive: true, force: true });
1756
+ rmSync3(tempDir, { recursive: true, force: true });
1638
1757
  } catch {
1639
1758
  }
1640
1759
  }
@@ -1659,7 +1778,7 @@ async function cmdRemove(name, ask) {
1659
1778
  const a = (await ask(`
1660
1779
  Delete ${scope} skill "${clean}"? [y/N] `) ?? "").trim().toLowerCase();
1661
1780
  if (a === "y" || a === "yes") {
1662
- rmSync2(target, { recursive: true, force: true });
1781
+ rmSync3(target, { recursive: true, force: true });
1663
1782
  console.log(c.green(" Removed.\n"));
1664
1783
  } else {
1665
1784
  console.log(c.dim(" Kept.\n"));
@@ -1706,8 +1825,8 @@ async function skillCmd(sub, rest = []) {
1706
1825
  }
1707
1826
 
1708
1827
  // src/commands/graph.ts
1709
- import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "fs";
1710
- 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";
1711
1830
  import { spawn as spawn3 } from "child_process";
1712
1831
 
1713
1832
  // src/graphview.ts
@@ -2372,7 +2491,7 @@ async function graph(args = []) {
2372
2491
  edges: g.edges.length
2373
2492
  });
2374
2493
  const outPath = opts.out ? resolve2(opts.out) : join8(wsHoltDir(), "graph.html");
2375
- mkdirSync5(dirname(outPath), { recursive: true });
2494
+ mkdirSync6(dirname2(outPath), { recursive: true });
2376
2495
  writeFileSync7(outPath, html, "utf8");
2377
2496
  console.log("\n" + c.accent("Memory graph built") + c.dim(" (this folder)"));
2378
2497
  console.log(` nodes ${g.nodes.length} (${turns.length} turns, ${conceptCount} concepts)`);
@@ -2387,7 +2506,7 @@ async function graph(args = []) {
2387
2506
  }
2388
2507
 
2389
2508
  // src/cli.ts
2390
- var VERSION = "0.5.0";
2509
+ var VERSION = "0.5.2";
2391
2510
  var BANNER = `
2392
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
2393
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.0",
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",