@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.
- package/README.md +1 -1
- package/dist/cli.js +135 -37
- 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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
362
|
+
writeState(null);
|
|
363
|
+
return { ok: true, kind: "none", file, immediate: true };
|
|
294
364
|
} catch (e) {
|
|
295
|
-
|
|
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
|
|
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
|
-
|
|
467
|
+
mkdirSync4(memDir(), { recursive: true });
|
|
377
468
|
appendFileSync(memPath(), JSON.stringify(t) + "\n", "utf8");
|
|
378
469
|
}
|
|
379
470
|
function clearMemory() {
|
|
380
|
-
if (existsSync4(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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
654
|
-
|
|
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 (
|
|
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 ${
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1746
|
+
mkdirSync5(skillsRoot(scope), { recursive: true });
|
|
1649
1747
|
cpSync(skillDir, dest, { recursive: true });
|
|
1650
1748
|
const nestedGit = join7(dest, ".git");
|
|
1651
|
-
if (existsSync6(nestedGit))
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|