@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.
- package/README.md +1 -1
- package/dist/cli.js +158 -39
- 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
|
}
|
|
@@ -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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
700
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1746
|
+
mkdirSync5(skillsRoot(scope), { recursive: true });
|
|
1628
1747
|
cpSync(skillDir, dest, { recursive: true });
|
|
1629
1748
|
const nestedGit = join7(dest, ".git");
|
|
1630
|
-
if (existsSync6(nestedGit))
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|