@holt-os/holt 0.8.0 → 0.8.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 (2) hide show
  1. package/dist/cli.js +864 -836
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -22,6 +22,11 @@ var c = {
22
22
  red: wrap("31"),
23
23
  cyan: wrap("36")
24
24
  };
25
+ function bar(fraction, width = 12) {
26
+ const f = Math.max(0, Math.min(1, fraction));
27
+ const filled = Math.round(f * width);
28
+ return c.green("\u2588".repeat(filled)) + c.dim("\u2591".repeat(width - filled));
29
+ }
25
30
  function createReader() {
26
31
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
27
32
  const buffer = [];
@@ -594,303 +599,123 @@ async function backfillEmbeddings(onProgress) {
594
599
  return { embedded, total: missing.length };
595
600
  }
596
601
 
597
- // src/commands/setting.ts
598
- function printStatus(cfg) {
599
- console.log("\n" + c.accent("Holt settings") + c.dim(" (this folder)"));
600
- for (const id of BRAIN_IDS) {
601
- const b = cfg.brains[id];
602
- const tags = [
603
- b.enabled ? c.green("enabled") : isInstalled(b.command) ? c.dim("installed, off") : c.dim("not installed"),
604
- cfg.defaultBrain === id ? c.accent("default") : ""
605
- ].filter(Boolean).join(" ");
606
- console.log(` ${id.padEnd(10)} ${b.label.padEnd(16)} ${tags}`);
607
- }
608
- if (cfg.apiBrains.length) {
609
- console.log(c.dim(" api brains:"));
610
- for (const a of cfg.apiBrains) {
611
- const key = resolveApiKey(a) ? c.green("key ok") : c.red("no key");
612
- const def = cfg.defaultBrain === a.id ? c.accent("default") : "";
613
- console.log(` ${a.id.padEnd(10)} ${`${a.provider}/${a.model}`.padEnd(30)} ${key} ${def}`);
614
- }
615
- }
616
- console.log(c.dim(` output format: ${cfg.outputFormat}`));
617
- console.log(c.dim(` launch command: ${currentAlias() || "holt (default)"}`));
618
- console.log("\n " + c.dim("[d] default brain [t] toggle brain [c] connect API brain [x] remove API brain [a] launch command [enter] done"));
602
+ // src/telegram.ts
603
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync5, mkdirSync as mkdirSync5 } from "fs";
604
+ import { join as join5 } from "path";
605
+ var API_BASE = "https://api.telegram.org";
606
+ var CHUNK_SIZE = 4e3;
607
+ function telegramConfigPath() {
608
+ return join5(GLOBAL_DIR, "telegram.json");
619
609
  }
620
- async function connectApiBrain(ask, cfg) {
621
- let provider = null;
622
- for (let tries = 0; tries < 3 && !provider; tries++) {
623
- const provRaw = (await ask(` provider [${PROVIDERS.join("/")}] (enter for ${PROVIDERS[0]}, or "skip"): `) ?? "skip").trim().toLowerCase();
624
- if (provRaw === "skip" || provRaw === "q" || provRaw === "n" || provRaw === "no") {
625
- console.log(c.dim(' skipped. Add one later with "holt setting" then "c".'));
626
- return null;
627
- }
628
- if (provRaw === "") provider = PROVIDERS[0];
629
- else if (PROVIDERS.includes(provRaw)) provider = provRaw;
630
- else console.log(c.dim(` "${provRaw}" is not a provider. Type one of: ${PROVIDERS.join(", ")}`));
631
- }
632
- if (!provider) {
633
- console.log(c.dim(' skipped after three tries. Add one later with "holt setting" then "c".'));
634
- return null;
635
- }
636
- const suggestion = PROVIDER_MODEL_SUGGESTION[provider];
637
- const modelRaw = (await ask(` model (enter for ${suggestion}): `) ?? "").trim();
638
- const model = modelRaw || suggestion;
639
- const idRaw = (await ask(" short name for this brain (e.g. sonnet): ") ?? "").trim();
640
- if (!idRaw) {
641
- console.log(c.dim(" cancelled (no name)."));
642
- return null;
643
- }
644
- if (isReservedBrainId(idRaw)) {
645
- console.log(c.red(` "${idRaw}" is reserved for a CLI brain. Pick another name.`));
646
- return null;
647
- }
648
- if (findApiBrain(cfg, idRaw)) {
649
- console.log(c.red(` an API brain named "${idRaw}" already exists.`));
610
+ function loadTelegramConfig() {
611
+ const path = telegramConfigPath();
612
+ if (!existsSync5(path)) return null;
613
+ try {
614
+ const raw = JSON.parse(readFileSync5(path, "utf8"));
615
+ if (typeof raw.token !== "string" || typeof raw.allowedChatId !== "number") return null;
616
+ return { token: raw.token, allowedChatId: raw.allowedChatId };
617
+ } catch {
650
618
  return null;
651
619
  }
652
- console.log(c.dim(" key: paste a raw key (stored locally, mode 600), or type the name of an env var that holds it."));
653
- const keyRaw = (await ask(" key or env var name: ") ?? "").trim();
654
- let keyEnv;
655
- if (!keyRaw) {
656
- console.log(c.dim(" no key given now. You can set the standard env var later."));
657
- } else if (/^[A-Z][A-Z0-9_]*$/.test(keyRaw)) {
658
- keyEnv = keyRaw;
659
- console.log(c.dim(` will read the key from env var ${keyEnv}.`));
660
- } else {
661
- saveCredential(provider, keyRaw);
662
- console.log(c.green(` key stored in ~/.holt/credentials.json (mode 600).`));
663
- }
664
- const brain = { id: idRaw, provider, model, ...keyEnv ? { keyEnv } : {} };
665
- cfg.apiBrains.push(brain);
666
- const resolvable = resolveApiKey(brain);
667
- console.log(c.green(` connected "${idRaw}" -> ${provider}/${model}.`) + (resolvable ? "" : c.dim(" (no key resolves yet)")));
668
- return brain;
669
620
  }
670
- function selectableIds(cfg) {
671
- return [...BRAIN_IDS.filter((id) => cfg.brains[id].enabled), ...cfg.apiBrains.map((a) => a.id)];
621
+ function saveTelegramConfig(cfg) {
622
+ mkdirSync5(GLOBAL_DIR, { recursive: true });
623
+ writeFileSync5(telegramConfigPath(), JSON.stringify(cfg, null, 2) + "\n", {
624
+ encoding: "utf8",
625
+ mode: 384
626
+ });
672
627
  }
673
- async function runSettings(ask) {
674
- let cfg = loadConfig() ?? defaultConfig();
675
- while (true) {
676
- printStatus(cfg);
677
- const raw = await ask(" > ");
678
- const choice = (raw ?? "").trim().toLowerCase();
679
- if (raw === null || choice === "" || choice === "q") break;
680
- if (choice === "d") {
681
- const options = selectableIds(cfg);
682
- if (options.length === 0) {
683
- console.log(c.dim(' No brains ready. Toggle a CLI brain on ("t") or connect an API brain ("c").'));
684
- continue;
685
- }
686
- const pick = (await ask(` default brain [${options.join("/")}]: `) ?? "").trim();
687
- if (options.includes(pick)) {
688
- cfg.defaultBrain = pick;
689
- const label = BRAIN_IDS.includes(pick) ? cfg.brains[pick].label : pick;
690
- console.log(c.green(` default set to ${label}`));
691
- } else console.log(c.dim(" unchanged."));
692
- } else if (choice === "t") {
693
- const pick = (await ask(` toggle which brain [${BRAIN_IDS.join("/")}]: `) ?? "").trim();
694
- if (BRAIN_IDS.includes(pick)) {
695
- if (!cfg.brains[pick].enabled && !isInstalled(cfg.brains[pick].command)) {
696
- console.log(c.dim(` ${cfg.brains[pick].label} is not installed. Run "holt init" to install it.`));
697
- } else {
698
- cfg.brains[pick].enabled = !cfg.brains[pick].enabled;
699
- if (!cfg.brains[pick].enabled && cfg.defaultBrain === pick) cfg.defaultBrain = selectableIds(cfg)[0] ?? null;
700
- if (cfg.brains[pick].enabled && !cfg.defaultBrain) cfg.defaultBrain = pick;
701
- console.log(c.dim(` ${cfg.brains[pick].label} is now ${cfg.brains[pick].enabled ? "on" : "off"}.`));
702
- }
703
- } else console.log(c.dim(" unchanged."));
704
- } else if (choice === "c") {
705
- await connectApiBrain(ask, cfg);
706
- if (!cfg.defaultBrain && cfg.apiBrains.length) cfg.defaultBrain = cfg.apiBrains[cfg.apiBrains.length - 1]?.id ?? null;
707
- } else if (choice === "x") {
708
- if (cfg.apiBrains.length === 0) {
709
- console.log(c.dim(" no API brains to remove."));
710
- continue;
711
- }
712
- const pick = (await ask(` remove which API brain [${cfg.apiBrains.map((a) => a.id).join("/")}]: `) ?? "").trim();
713
- const idx = cfg.apiBrains.findIndex((a) => a.id === pick);
714
- if (idx >= 0) {
715
- cfg.apiBrains.splice(idx, 1);
716
- if (cfg.defaultBrain === pick) cfg.defaultBrain = selectableIds(cfg)[0] ?? null;
717
- console.log(c.dim(` removed "${pick}". (its stored key, if any, stays in ~/.holt/credentials.json)`));
718
- } else console.log(c.dim(" unchanged."));
719
- } else if (choice === "a") {
720
- const name = (await ask(" launch command (blank to reset to holt): ") ?? "").trim();
721
- if (name && name !== "holt") {
722
- const r = installAlias(name);
723
- if (r.ok && r.immediate) console.log(c.green(` "${name}" is ready to use right now.`));
724
- else if (r.ok) console.log(c.green(` alias "${name}" added to ${r.file}. Run: source ${r.file} (new terminals will not need it).`));
725
- else console.log(c.red(" " + r.message));
726
- } else {
727
- removeAlias();
728
- console.log(c.dim(" reset to holt."));
628
+ function buildSendRequest(token, chatId, text) {
629
+ return {
630
+ url: `${API_BASE}/bot${token}/sendMessage`,
631
+ body: JSON.stringify({ chat_id: chatId, text })
632
+ };
633
+ }
634
+ function buildGetUpdatesUrl(token, offset, timeoutSec) {
635
+ const params = new URLSearchParams({
636
+ offset: String(offset),
637
+ timeout: String(timeoutSec)
638
+ });
639
+ return `${API_BASE}/bot${token}/getUpdates?${params.toString()}`;
640
+ }
641
+ function chunk(text, size = CHUNK_SIZE) {
642
+ if (text.length === 0) return [""];
643
+ const out = [];
644
+ for (let i = 0; i < text.length; i += size) out.push(text.slice(i, i + size));
645
+ return out;
646
+ }
647
+ async function sendMessage(text, chatId) {
648
+ const cfg = loadTelegramConfig();
649
+ if (!cfg) {
650
+ process.stderr.write('[telegram] not configured; run "holt telegram setup"\n');
651
+ return false;
652
+ }
653
+ const target = chatId ?? cfg.allowedChatId;
654
+ const pieces = chunk(text || "No output.");
655
+ let allOk = true;
656
+ for (const piece of pieces) {
657
+ const { url, body } = buildSendRequest(cfg.token, target, piece);
658
+ try {
659
+ const res = await fetch(url, {
660
+ method: "POST",
661
+ headers: { "content-type": "application/json" },
662
+ body
663
+ });
664
+ if (!res.ok) {
665
+ allOk = false;
666
+ process.stderr.write(`[telegram] sendMessage HTTP ${res.status}
667
+ `);
729
668
  }
730
- } else {
731
- console.log(c.dim(" pick d, t, c, x, a, or press enter to finish."));
669
+ } catch (err2) {
670
+ allOk = false;
671
+ process.stderr.write(`[telegram] sendMessage failed: ${err2.message}
672
+ `);
732
673
  }
733
- saveConfig(cfg);
734
674
  }
735
- saveConfig(cfg);
736
- return cfg;
675
+ return allOk;
737
676
  }
738
- async function setting() {
739
- const { ask, close } = createReader();
740
- if (!await ensureTrusted(ask)) {
741
- close();
742
- return;
677
+ async function getUpdates(offset) {
678
+ const cfg = loadTelegramConfig();
679
+ if (!cfg) return [];
680
+ const timeoutSec = 30;
681
+ const url = buildGetUpdatesUrl(cfg.token, offset, timeoutSec);
682
+ const ac = new AbortController();
683
+ const timer = setTimeout(() => ac.abort(), (timeoutSec + 10) * 1e3);
684
+ try {
685
+ const res = await fetch(url, { signal: ac.signal });
686
+ if (!res.ok) {
687
+ process.stderr.write(`[telegram] getUpdates HTTP ${res.status}
688
+ `);
689
+ return [];
690
+ }
691
+ const data = await res.json();
692
+ if (!data || data.ok !== true || !Array.isArray(data.result)) return [];
693
+ return data.result;
694
+ } catch (err2) {
695
+ process.stderr.write(`[telegram] getUpdates failed: ${err2.message}
696
+ `);
697
+ return [];
698
+ } finally {
699
+ clearTimeout(timer);
743
700
  }
744
- await runSettings(ask);
745
- close();
746
- console.log("");
747
701
  }
748
702
 
749
- // src/commands/init.ts
750
- function parseBrains(raw, found) {
751
- const s = raw.trim().toLowerCase();
752
- if (s === "") return found.length ? found : [...BRAIN_IDS];
753
- if (s === "all") return [...BRAIN_IDS];
754
- const picked = s.split(/[\s,]+/).filter((x) => BRAIN_IDS.includes(x));
755
- if (picked.length) return [...new Set(picked)];
756
- return found.length ? found : ["claude"];
703
+ // src/runner.ts
704
+ import { randomUUID as randomUUID2 } from "crypto";
705
+
706
+ // src/apibrain.ts
707
+ function parseSSELines(buffer) {
708
+ const normalized = buffer.replace(/\r\n/g, "\n");
709
+ const parts = normalized.split("\n\n");
710
+ const rest = parts.pop() ?? "";
711
+ const events = parts.filter((p) => p.trim() !== "");
712
+ return { events, rest };
757
713
  }
758
- async function init() {
759
- const { ask, close } = createReader();
760
- if (!await ensureTrusted(ask)) {
761
- close();
762
- return;
763
- }
764
- console.log(c.accent("Holt setup") + c.dim(` (${workspace()})`) + "\n");
765
- console.log("Looking for agent CLIs on your machine...\n");
766
- const found = [];
767
- for (const id of BRAIN_IDS) {
768
- const ok2 = isInstalled(BRAIN_DEFS[id].command);
769
- console.log(` ${ok2 ? c.green("found ") : c.dim("missing")} ${BRAIN_DEFS[id].label} (${BRAIN_DEFS[id].command})`);
770
- if (ok2) found.push(id);
771
- }
772
- console.log("");
773
- const chosen = parseBrains(
774
- await ask('Which brains do you want? claude, codex, gemini (comma-separated, or "all"): ') ?? "",
775
- found
776
- );
777
- console.log(c.dim(` using: ${chosen.join(", ")}`));
778
- const toInstall = chosen.filter((id) => !isInstalled(BRAIN_DEFS[id].command));
779
- const loginWanted = /* @__PURE__ */ new Set();
780
- for (const id of toInstall) {
781
- const a = (await ask(` ${BRAIN_DEFS[id].label} is not installed. Sign in after install? [Y/n] `) ?? "").trim().toLowerCase();
782
- if (a !== "n" && a !== "no") loginWanted.add(id);
783
- }
784
- const connectedApiBrains = [];
785
- const apiAns = (await ask("\nAlso connect a direct API brain (raw key, no CLI needed)? [y/N] ") ?? "").trim().toLowerCase();
786
- if (apiAns === "y" || apiAns === "yes") {
787
- const holder = defaultConfig();
788
- const brain = await connectApiBrain(ask, holder);
789
- if (brain) connectedApiBrains.push(brain);
790
- }
791
- const defPick = chosen.includes("claude") ? "claude" : chosen[0];
792
- const dans = (await ask(`
793
- Default brain? [${chosen.join("/")}] (${defPick}): `) ?? "").trim();
794
- const defaultBrain = chosen.includes(dans) ? dans : defPick;
795
- const aliasAns = (await ask('Launch command? Type a custom word like "ai", or press enter to keep "holt": ') ?? "").trim();
796
- let aliasNote = "";
797
- let aliasNeedsSource = "";
798
- let aliasWorked = false;
799
- if (aliasAns && aliasAns !== "holt") {
800
- const r = installAlias(aliasAns);
801
- aliasWorked = r.ok;
802
- if (r.ok && r.immediate) aliasNote = c.green(` "${aliasAns}" is ready to use right now (launcher at ${r.file}).`);
803
- else if (r.ok) {
804
- aliasNote = c.green(` alias "${aliasAns}" -> holt chat added to ${r.file}`);
805
- aliasNeedsSource = r.file;
806
- } else aliasNote = c.red(" " + r.message);
807
- }
808
- let wantMemorySetup = false;
809
- const embedReady = await embeddingsAvailable();
810
- if (embedReady) {
811
- console.log(c.dim("\nSemantic memory: ready (local Ollama with " + EMBED_MODEL + " detected)."));
812
- } else {
813
- const ollamaHere = isInstalled("ollama");
814
- const q = ollamaHere ? `Semantic memory needs a local embed model. Pull ${EMBED_MODEL} with Ollama now? [Y/n] ` : "Enable private semantic memory? Installs Ollama plus a small local embed model. Everything stays on your machine. [Y/n] ";
815
- const a = (await ask("\n" + q) ?? "").trim().toLowerCase();
816
- wantMemorySetup = a !== "n" && a !== "no";
817
- if (!wantMemorySetup) console.log(c.dim(' Okay. Memory still works with keyword recall; run "holt init" again anytime.'));
818
- }
819
- close();
820
- for (const id of toInstall) {
821
- const s = BRAIN_SETUP[id];
822
- console.log("\n" + c.accent(`Installing ${BRAIN_DEFS[id].label}`) + c.dim(` (${s.install.join(" ")})`));
823
- const code = await runInteractive(s.install[0], s.install.slice(1));
824
- console.log(code === 0 ? c.green(` ${BRAIN_DEFS[id].label} installed.`) : c.red(` Install failed (exit ${code}). Run manually: ${s.install.join(" ")}`));
825
- }
826
- for (const id of toInstall) {
827
- if (!loginWanted.has(id) || !isInstalled(BRAIN_DEFS[id].command)) continue;
828
- const s = BRAIN_SETUP[id];
829
- console.log("\n" + c.accent(`Sign in to ${BRAIN_DEFS[id].label}`));
830
- console.log(c.dim(` Starting "${s.login.join(" ")}". Complete sign-in, then exit that tool to return here.`));
831
- await runInteractive(s.login[0], s.login.slice(1));
832
- }
833
- if (wantMemorySetup) {
834
- if (!isInstalled("ollama")) {
835
- if (process.platform === "darwin" && isInstalled("brew")) {
836
- console.log("\n" + c.accent("Installing Ollama") + c.dim(" (brew install ollama)"));
837
- const code = await runInteractive("brew", ["install", "ollama"]);
838
- if (code === 0) await runInteractive("brew", ["services", "start", "ollama"]);
839
- else console.log(c.red(' Install failed. Get Ollama from https://ollama.com/download and run "holt init" again.'));
840
- } else {
841
- console.log(c.dim('\n Get Ollama from https://ollama.com/download, then run "holt init" again to finish memory setup.'));
842
- }
843
- }
844
- if (isInstalled("ollama")) {
845
- console.log("\n" + c.accent("Pulling embed model") + c.dim(` (ollama pull ${EMBED_MODEL})`));
846
- const code = await runInteractive("ollama", ["pull", EMBED_MODEL]);
847
- if (code !== 0) {
848
- console.log(c.dim(' Could not pull. Start Ollama (open the app or run "ollama serve"), then run:'));
849
- console.log(c.dim(` ollama pull ${EMBED_MODEL}`));
850
- }
851
- resetEmbedProbe();
852
- if (await embeddingsAvailable()) console.log(c.green(" Semantic memory is ready. Chats in trusted folders are stored and recalled locally."));
853
- }
854
- }
855
- const cfg = loadConfig() ?? defaultConfig();
856
- for (const id of BRAIN_IDS) cfg.brains[id].enabled = chosen.includes(id) && isInstalled(BRAIN_DEFS[id].command);
857
- for (const b of connectedApiBrains) if (!cfg.apiBrains.some((a) => a.id === b.id)) cfg.apiBrains.push(b);
858
- cfg.defaultBrain = cfg.brains[defaultBrain].enabled ? defaultBrain : BRAIN_IDS.find((id) => cfg.brains[id].enabled) ?? cfg.apiBrains[0]?.id ?? null;
859
- saveConfig(cfg);
860
- console.log("\n" + c.green("Saved to ./.holt/config.json"));
861
- if (aliasNote) console.log(aliasNote);
862
- if (cfg.defaultBrain) {
863
- if (aliasWorked && !aliasNeedsSource) {
864
- console.log("Start chatting: " + c.accent(aliasAns) + "\n");
865
- } else if (aliasNeedsSource) {
866
- console.log("\nStart chatting:");
867
- console.log(" " + c.accent(`source ${aliasNeedsSource}`) + c.dim(" (once; new terminals will not need it)"));
868
- console.log(" " + c.accent(aliasAns) + "\n");
869
- console.log(c.dim(" Or right now, without sourcing: holt chat\n"));
870
- } else {
871
- console.log("Start chatting: " + c.accent("holt chat") + "\n");
872
- }
873
- } else {
874
- console.log(c.dim('No brain is ready yet. Install one, then run "holt init" again.\n'));
875
- }
876
- }
877
-
878
- // src/commands/chat.ts
879
- import { randomUUID as randomUUID2 } from "crypto";
880
-
881
- // src/apibrain.ts
882
- function parseSSELines(buffer) {
883
- const normalized = buffer.replace(/\r\n/g, "\n");
884
- const parts = normalized.split("\n\n");
885
- const rest = parts.pop() ?? "";
886
- const events = parts.filter((p) => p.trim() !== "");
887
- return { events, rest };
888
- }
889
- function dataLines(event) {
890
- const out = [];
891
- for (const line of event.split("\n")) {
892
- const t = line.replace(/\r$/, "");
893
- if (t.startsWith("data:")) out.push(t.slice(5).replace(/^ /, ""));
714
+ function dataLines(event) {
715
+ const out = [];
716
+ for (const line of event.split("\n")) {
717
+ const t = line.replace(/\r$/, "");
718
+ if (t.startsWith("data:")) out.push(t.slice(5).replace(/^ /, ""));
894
719
  }
895
720
  return out;
896
721
  }
@@ -968,69 +793,688 @@ async function runApiBrain(brain, prompt, onChunk) {
968
793
  } catch (e) {
969
794
  return { ok: false, text: `Could not reach ${brain.provider}: ${e.message}` };
970
795
  }
971
- if (!res.ok) {
972
- let body = "";
973
- try {
974
- body = await res.text();
975
- } catch {
976
- }
977
- return { ok: false, text: `${brain.provider} error ${res.status}: ${body.slice(0, 200)}` };
796
+ if (!res.ok) {
797
+ let body = "";
798
+ try {
799
+ body = await res.text();
800
+ } catch {
801
+ }
802
+ return { ok: false, text: `${brain.provider} error ${res.status}: ${body.slice(0, 200)}` };
803
+ }
804
+ if (!res.body) {
805
+ let body = "";
806
+ try {
807
+ body = await res.text();
808
+ } catch {
809
+ }
810
+ return { ok: false, text: `${brain.provider} returned no stream. Body: ${body.slice(0, 200)}` };
811
+ }
812
+ const decoder = new TextDecoder();
813
+ let carry = "";
814
+ let text = "";
815
+ try {
816
+ for await (const chunk2 of res.body) {
817
+ carry += decoder.decode(chunk2, { stream: true });
818
+ const { events, rest } = parseSSELines(carry);
819
+ carry = rest;
820
+ for (const ev of events) {
821
+ for (const payload of dataLines(ev)) {
822
+ if (payload === "[DONE]") continue;
823
+ let json;
824
+ try {
825
+ json = JSON.parse(payload);
826
+ } catch {
827
+ continue;
828
+ }
829
+ const piece = extractDelta(brain.provider, json);
830
+ if (piece) {
831
+ text += piece;
832
+ if (onChunk) onChunk(piece);
833
+ }
834
+ }
835
+ }
836
+ }
837
+ } catch (e) {
838
+ if (text) return { ok: true, text };
839
+ return { ok: false, text: `Stream from ${brain.provider} failed: ${e.message}` };
840
+ }
841
+ if (carry.trim()) {
842
+ for (const payload of dataLines(carry)) {
843
+ if (payload === "[DONE]") continue;
844
+ try {
845
+ const piece = extractDelta(brain.provider, JSON.parse(payload));
846
+ if (piece) {
847
+ text += piece;
848
+ if (onChunk) onChunk(piece);
849
+ }
850
+ } catch {
851
+ }
852
+ }
853
+ }
854
+ const trimmed = text.trim();
855
+ if (trimmed) return { ok: true, text: trimmed };
856
+ return { ok: false, text: `${brain.provider} returned an empty reply.` };
857
+ }
858
+
859
+ // src/skills.ts
860
+ import { readFileSync as readFileSync6, readdirSync, existsSync as existsSync6, statSync as statSync2 } from "fs";
861
+ import { join as join6 } from "path";
862
+ function skillsRoot(scope) {
863
+ return scope === "workspace" ? join6(wsHoltDir(), "skills") : join6(GLOBAL_DIR, "skills");
864
+ }
865
+ function sanitizeName(name) {
866
+ return (name || "").trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
867
+ }
868
+ function parseFrontmatter(md) {
869
+ const text = md.replace(/^/, "");
870
+ const lines = text.split("\n");
871
+ let start = 0;
872
+ while (start < lines.length && lines[start].trim() === "") start++;
873
+ if (start >= lines.length || lines[start].trim() !== "---") {
874
+ return { data: {}, body: text.trim() };
875
+ }
876
+ const data = {};
877
+ let i = start + 1;
878
+ let closed = false;
879
+ for (; i < lines.length; i++) {
880
+ const line = lines[i];
881
+ if (line.trim() === "---") {
882
+ closed = true;
883
+ i++;
884
+ break;
885
+ }
886
+ const idx = line.indexOf(":");
887
+ if (idx === -1) continue;
888
+ const key = line.slice(0, idx).trim();
889
+ if (!key) continue;
890
+ let value = line.slice(idx + 1).trim();
891
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
892
+ value = value.slice(1, -1);
893
+ }
894
+ data[key.toLowerCase()] = value;
895
+ }
896
+ if (!closed) {
897
+ return { data: {}, body: text.trim() };
898
+ }
899
+ return { data, body: lines.slice(i).join("\n").trim() };
900
+ }
901
+ function readSkill(dir, folder, scope) {
902
+ const file = join6(dir, "SKILL.md");
903
+ if (!existsSync6(file)) return null;
904
+ let raw = "";
905
+ try {
906
+ raw = readFileSync6(file, "utf8");
907
+ } catch {
908
+ return null;
909
+ }
910
+ const { data, body } = parseFrontmatter(raw);
911
+ const name = sanitizeName(data.name || folder) || folder;
912
+ let description = data.description || "";
913
+ if (!description) {
914
+ const firstLine2 = body.split("\n").map((l) => l.trim()).find((l) => l.length > 0) || "";
915
+ description = firstLine2.replace(/^#+\s*/, "");
916
+ }
917
+ return { name, description, dir, scope };
918
+ }
919
+ function listScope(scope) {
920
+ const root = skillsRoot(scope);
921
+ if (!existsSync6(root)) return [];
922
+ let entries;
923
+ try {
924
+ entries = readdirSync(root);
925
+ } catch {
926
+ return [];
927
+ }
928
+ const out = [];
929
+ for (const folder of entries) {
930
+ const dir = join6(root, folder);
931
+ try {
932
+ if (!statSync2(dir).isDirectory()) continue;
933
+ } catch {
934
+ continue;
935
+ }
936
+ const skill = readSkill(dir, folder, scope);
937
+ if (skill) out.push(skill);
938
+ }
939
+ return out;
940
+ }
941
+ function listSkills() {
942
+ const byName = /* @__PURE__ */ new Map();
943
+ for (const s of listScope("global")) byName.set(s.name, s);
944
+ for (const s of listScope("workspace")) byName.set(s.name, s);
945
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
946
+ }
947
+ function loadSkill(name) {
948
+ const want = sanitizeName(name);
949
+ const skill = listSkills().find((s) => s.name === want);
950
+ if (!skill) return null;
951
+ let raw = "";
952
+ try {
953
+ raw = readFileSync6(join6(skill.dir, "SKILL.md"), "utf8");
954
+ } catch {
955
+ return null;
956
+ }
957
+ const { body } = parseFrontmatter(raw);
958
+ return { skill, body };
959
+ }
960
+ function skillsPromptBlock() {
961
+ const skills = listSkills();
962
+ if (skills.length === 0) return "";
963
+ const lines = [
964
+ `Skills available to this user (invoked with "/skill <name>"). When the user invokes a skill, follow that skill's instructions:`,
965
+ ...skills.map((s) => `- ${s.name}: ${s.description || "(no description)"}`)
966
+ ];
967
+ return lines.join("\n");
968
+ }
969
+ function resolveSkillInvocation(line) {
970
+ const trimmed = line.trim();
971
+ const m = trimmed.match(/^\/skill\s+(\S+)\s*([\s\S]*)$/i);
972
+ if (!m) return null;
973
+ const name = m[1];
974
+ const input = (m[2] || "").trim();
975
+ const loaded = loadSkill(name);
976
+ if (!loaded) return null;
977
+ const prompt = [
978
+ loaded.body,
979
+ "",
980
+ "Apply the skill above to this request:",
981
+ input || "Introduce what you can do with this skill."
982
+ ].join("\n");
983
+ return { prompt, skillName: loaded.skill.name };
984
+ }
985
+
986
+ // src/runner.ts
987
+ function resolveActive(cfg, id) {
988
+ if (BRAIN_IDS.includes(id)) {
989
+ const b = cfg.brains[id];
990
+ return { kind: "cli", id, label: b.label };
991
+ }
992
+ const api = findApiBrain(cfg, id);
993
+ if (api) return { kind: "api", id, label: `${id} (api: ${api.provider}/${api.model})`, brain: api };
994
+ return null;
995
+ }
996
+ async function runTask(task, opts = {}) {
997
+ try {
998
+ const cfg = loadConfig();
999
+ if (!cfg || !cfg.defaultBrain) {
1000
+ return { ok: false, text: "No Holt setup in this folder. Run holt init.", brainLabel: "" };
1001
+ }
1002
+ const wantId = opts.brainId ?? cfg.defaultBrain;
1003
+ const active = resolveActive(cfg, wantId);
1004
+ if (!active) {
1005
+ return { ok: false, text: `Brain "${wantId}" is not configured. Run holt setting.`, brainLabel: "" };
1006
+ }
1007
+ if (active.kind === "cli" && !isInstalled(cfg.brains[active.id].command)) {
1008
+ return {
1009
+ ok: false,
1010
+ text: `${active.label} (${cfg.brains[active.id].command}) is not on your PATH. Install it or run holt setting.`,
1011
+ brainLabel: active.label
1012
+ };
1013
+ }
1014
+ if (active.kind === "api" && !resolveApiKey(active.brain)) {
1015
+ return {
1016
+ ok: false,
1017
+ text: `${active.label} has no API key. Run holt setting to add one.`,
1018
+ brainLabel: active.label
1019
+ };
1020
+ }
1021
+ const runSession = "run-" + newSessionId();
1022
+ const recalled = opts.recall !== false ? await recall(task, runSession, 4) : [];
1023
+ const block = skillsPromptBlock();
1024
+ const base = renderPrompt([], task, recalled);
1025
+ const prompt = block ? block + "\n\n" + base : base;
1026
+ const res = active.kind === "cli" ? await runBrain(cfg.brains[active.id], prompt, opts.onChunk) : await runApiBrain(active.brain, prompt, opts.onChunk);
1027
+ if (res.ok && opts.store !== false) {
1028
+ const now = Date.now();
1029
+ appendTurn({ id: randomUUID2().slice(0, 8), ts: now, session: runSession, role: "user", content: task, emb: await embed(task) ?? void 0 });
1030
+ appendTurn({ id: randomUUID2().slice(0, 8), ts: now, session: runSession, role: "assistant", content: res.text, emb: await embed(res.text) ?? void 0 });
1031
+ }
1032
+ return { ok: res.ok, text: res.text, brainLabel: active.label };
1033
+ } catch (e) {
1034
+ return { ok: false, text: e.message, brainLabel: "" };
1035
+ }
1036
+ }
1037
+
1038
+ // src/commands/telegram.ts
1039
+ var HELP_TEXT = "Holt bot. Send me anything and I will run it through your selected brain and reply.\nCommands: /help this message.";
1040
+ async function telegram(sub, _rest = []) {
1041
+ const action = (sub || "").toLowerCase();
1042
+ if (action === "setup") {
1043
+ await setup();
1044
+ return;
1045
+ }
1046
+ await runBot();
1047
+ }
1048
+ async function setupTelegram(ask) {
1049
+ console.log("\n" + c.accent("Connect Holt to Telegram") + c.dim(" (chat with Holt from your phone)"));
1050
+ console.log(c.dim(" 1. Open Telegram and message @BotFather."));
1051
+ console.log(c.dim(" 2. Send /newbot, follow the prompts, and copy the bot token it gives you."));
1052
+ console.log(c.dim(" A token looks like 123456789:AAF... (keep it secret)."));
1053
+ const token = (await ask("\n Paste your bot token (enter to skip): ") ?? "").trim();
1054
+ if (!token) {
1055
+ console.log(c.dim(' Skipped. Set it up later with "holt telegram setup" or in "holt setting".'));
1056
+ return false;
1057
+ }
1058
+ console.log("\n" + c.dim(" Now I need your chat id (so the bot only answers you)."));
1059
+ console.log(c.dim(" Option A: open your new bot in Telegram and send it any message,"));
1060
+ console.log(c.dim(" then I will auto-detect the chat id."));
1061
+ console.log(c.dim(" Option B: paste a numeric chat id yourself."));
1062
+ const mode = (await ask("\n Auto-detect from a message you just sent? [Y/n] ") ?? "").trim().toLowerCase();
1063
+ let allowedChatId = null;
1064
+ if (mode === "" || mode === "y" || mode === "yes") {
1065
+ saveTelegramConfig({ token, allowedChatId: 0 });
1066
+ console.log(c.dim("\n Checking for a recent message..."));
1067
+ const updates = await getUpdates(0);
1068
+ allowedChatId = latestChatId(updates);
1069
+ if (allowedChatId === null) {
1070
+ console.log(c.red(" Did not find a recent message."));
1071
+ const manual = (await ask(" Paste your numeric chat id instead: ") ?? "").trim();
1072
+ allowedChatId = parseChatId(manual);
1073
+ }
1074
+ } else {
1075
+ const manual = (await ask(" Paste your numeric chat id: ") ?? "").trim();
1076
+ allowedChatId = parseChatId(manual);
1077
+ }
1078
+ if (allowedChatId === null) {
1079
+ console.log(c.red("\n No valid chat id. Nothing saved. Try again anytime."));
1080
+ return false;
1081
+ }
1082
+ const cfg = { token, allowedChatId };
1083
+ saveTelegramConfig(cfg);
1084
+ console.log(
1085
+ "\n" + c.green(" Telegram connected.") + c.dim(` token ...${token.slice(-4)} chat id ${allowedChatId} (~/.holt/telegram.json, mode 600)`)
1086
+ );
1087
+ console.log(c.dim(" Start the bot anytime with: holt telegram"));
1088
+ return true;
1089
+ }
1090
+ async function setup() {
1091
+ const { ask, close } = createReader();
1092
+ try {
1093
+ await setupTelegram(ask);
1094
+ console.log("");
1095
+ } finally {
1096
+ close();
1097
+ }
1098
+ }
1099
+ function parseChatId(s) {
1100
+ if (!/^-?\d+$/.test(s)) return null;
1101
+ const n = Number(s);
1102
+ return Number.isFinite(n) ? n : null;
1103
+ }
1104
+ function latestChatId(updates) {
1105
+ for (let i = updates.length - 1; i >= 0; i--) {
1106
+ const u = updates[i];
1107
+ if (u && u.message) return u.message.chat.id;
1108
+ }
1109
+ return null;
1110
+ }
1111
+ async function runBot() {
1112
+ const cfg = loadTelegramConfig();
1113
+ if (!cfg) {
1114
+ console.log(c.dim('\n Telegram is not set up. Run "holt telegram setup" first.\n'));
1115
+ return;
1116
+ }
1117
+ if (!isTrusted()) {
1118
+ trustDir();
1119
+ process.stderr.write(`[telegram] auto-trusted ${workspace()}
1120
+ `);
1121
+ }
1122
+ process.stderr.write("[telegram] bot started; long-polling for messages. Ctrl-C to stop.\n");
1123
+ let offset = 0;
1124
+ let busy = false;
1125
+ while (true) {
1126
+ let updates = [];
1127
+ try {
1128
+ updates = await getUpdates(offset);
1129
+ } catch (err2) {
1130
+ process.stderr.write(`[telegram] poll error: ${err2.message}
1131
+ `);
1132
+ await sleep(1e3);
1133
+ continue;
1134
+ }
1135
+ for (const u of updates) {
1136
+ if (u && typeof u.update_id === "number") offset = Math.max(offset, u.update_id + 1);
1137
+ try {
1138
+ await handleUpdate(u, cfg, () => busy, (v) => {
1139
+ busy = v;
1140
+ });
1141
+ } catch (err2) {
1142
+ process.stderr.write(`[telegram] update ${u?.update_id} failed: ${err2.message}
1143
+ `);
1144
+ }
1145
+ }
1146
+ }
1147
+ }
1148
+ async function handleUpdate(u, cfg, getBusy, setBusy) {
1149
+ const msg = u?.message;
1150
+ if (!msg || typeof msg.text !== "string") return;
1151
+ if (msg.chat.id !== cfg.allowedChatId) {
1152
+ process.stderr.write(`[telegram] ignoring message from chat ${msg.chat.id}
1153
+ `);
1154
+ return;
1155
+ }
1156
+ const text = msg.text.trim();
1157
+ if (text === "/start" || text === "/help") {
1158
+ await sendMessage(HELP_TEXT, cfg.allowedChatId);
1159
+ return;
1160
+ }
1161
+ if (getBusy()) {
1162
+ await sendMessage("Still working on the previous message. Try again in a moment.", cfg.allowedChatId);
1163
+ return;
1164
+ }
1165
+ setBusy(true);
1166
+ try {
1167
+ process.stderr.write(`[telegram] running task (${text.length} chars)
1168
+ `);
1169
+ const result = await runTask(text);
1170
+ await sendMessage(result.text || "No output.", cfg.allowedChatId);
1171
+ process.stderr.write(`[telegram] replied (brain: ${result.brainLabel})
1172
+ `);
1173
+ } catch (err2) {
1174
+ process.stderr.write(`[telegram] runTask failed: ${err2.message}
1175
+ `);
1176
+ await sendMessage("Sorry, that run failed. Check the Holt logs.", cfg.allowedChatId);
1177
+ } finally {
1178
+ setBusy(false);
1179
+ }
1180
+ }
1181
+ function sleep(ms) {
1182
+ return new Promise((r) => setTimeout(r, ms));
1183
+ }
1184
+
1185
+ // src/commands/setting.ts
1186
+ function printStatus(cfg) {
1187
+ console.log("\n" + c.accent("Holt settings") + c.dim(" (this folder)"));
1188
+ for (const id of BRAIN_IDS) {
1189
+ const b = cfg.brains[id];
1190
+ const tags = [
1191
+ b.enabled ? c.green("enabled") : isInstalled(b.command) ? c.dim("installed, off") : c.dim("not installed"),
1192
+ cfg.defaultBrain === id ? c.accent("default") : ""
1193
+ ].filter(Boolean).join(" ");
1194
+ console.log(` ${id.padEnd(10)} ${b.label.padEnd(16)} ${tags}`);
1195
+ }
1196
+ if (cfg.apiBrains.length) {
1197
+ console.log(c.dim(" api brains:"));
1198
+ for (const a of cfg.apiBrains) {
1199
+ const key = resolveApiKey(a) ? c.green("key ok") : c.red("no key");
1200
+ const def = cfg.defaultBrain === a.id ? c.accent("default") : "";
1201
+ console.log(` ${a.id.padEnd(10)} ${`${a.provider}/${a.model}`.padEnd(30)} ${key} ${def}`);
1202
+ }
1203
+ }
1204
+ console.log(c.dim(` output format: ${cfg.outputFormat}`));
1205
+ console.log(c.dim(` launch command: ${currentAlias() || "holt (default)"}`));
1206
+ const tg = loadTelegramConfig();
1207
+ console.log(c.dim(` telegram: ${tg ? `connected (chat ${tg.allowedChatId})` : "not set up"}`));
1208
+ console.log("\n " + c.dim("[d] default brain [t] toggle brain [c] connect API brain [x] remove API brain"));
1209
+ console.log(" " + c.dim("[a] launch command [m] connect Telegram [enter] done"));
1210
+ }
1211
+ async function connectApiBrain(ask, cfg) {
1212
+ let provider = null;
1213
+ for (let tries = 0; tries < 3 && !provider; tries++) {
1214
+ const provRaw = (await ask(` provider [${PROVIDERS.join("/")}] (enter for ${PROVIDERS[0]}, or "skip"): `) ?? "skip").trim().toLowerCase();
1215
+ if (provRaw === "skip" || provRaw === "q" || provRaw === "n" || provRaw === "no") {
1216
+ console.log(c.dim(' skipped. Add one later with "holt setting" then "c".'));
1217
+ return null;
1218
+ }
1219
+ if (provRaw === "") provider = PROVIDERS[0];
1220
+ else if (PROVIDERS.includes(provRaw)) provider = provRaw;
1221
+ else console.log(c.dim(` "${provRaw}" is not a provider. Type one of: ${PROVIDERS.join(", ")}`));
1222
+ }
1223
+ if (!provider) {
1224
+ console.log(c.dim(' skipped after three tries. Add one later with "holt setting" then "c".'));
1225
+ return null;
1226
+ }
1227
+ const suggestion = PROVIDER_MODEL_SUGGESTION[provider];
1228
+ const modelRaw = (await ask(` model (enter for ${suggestion}): `) ?? "").trim();
1229
+ const model = modelRaw || suggestion;
1230
+ const idRaw = (await ask(" short name for this brain (e.g. sonnet): ") ?? "").trim();
1231
+ if (!idRaw) {
1232
+ console.log(c.dim(" cancelled (no name)."));
1233
+ return null;
1234
+ }
1235
+ if (isReservedBrainId(idRaw)) {
1236
+ console.log(c.red(` "${idRaw}" is reserved for a CLI brain. Pick another name.`));
1237
+ return null;
1238
+ }
1239
+ if (findApiBrain(cfg, idRaw)) {
1240
+ console.log(c.red(` an API brain named "${idRaw}" already exists.`));
1241
+ return null;
1242
+ }
1243
+ console.log(c.dim(" key: paste a raw key (stored locally, mode 600), or type the name of an env var that holds it."));
1244
+ const keyRaw = (await ask(" key or env var name: ") ?? "").trim();
1245
+ let keyEnv;
1246
+ if (!keyRaw) {
1247
+ console.log(c.dim(" no key given now. You can set the standard env var later."));
1248
+ } else if (/^[A-Z][A-Z0-9_]*$/.test(keyRaw)) {
1249
+ keyEnv = keyRaw;
1250
+ console.log(c.dim(` will read the key from env var ${keyEnv}.`));
1251
+ } else {
1252
+ saveCredential(provider, keyRaw);
1253
+ console.log(c.green(` key stored in ~/.holt/credentials.json (mode 600).`));
1254
+ }
1255
+ const brain = { id: idRaw, provider, model, ...keyEnv ? { keyEnv } : {} };
1256
+ cfg.apiBrains.push(brain);
1257
+ const resolvable = resolveApiKey(brain);
1258
+ console.log(c.green(` connected "${idRaw}" -> ${provider}/${model}.`) + (resolvable ? "" : c.dim(" (no key resolves yet)")));
1259
+ return brain;
1260
+ }
1261
+ function selectableIds(cfg) {
1262
+ return [...BRAIN_IDS.filter((id) => cfg.brains[id].enabled), ...cfg.apiBrains.map((a) => a.id)];
1263
+ }
1264
+ async function runSettings(ask) {
1265
+ let cfg = loadConfig() ?? defaultConfig();
1266
+ while (true) {
1267
+ printStatus(cfg);
1268
+ const raw = await ask(" > ");
1269
+ const choice = (raw ?? "").trim().toLowerCase();
1270
+ if (raw === null || choice === "" || choice === "q") break;
1271
+ if (choice === "d") {
1272
+ const options = selectableIds(cfg);
1273
+ if (options.length === 0) {
1274
+ console.log(c.dim(' No brains ready. Toggle a CLI brain on ("t") or connect an API brain ("c").'));
1275
+ continue;
1276
+ }
1277
+ const pick = (await ask(` default brain [${options.join("/")}]: `) ?? "").trim();
1278
+ if (options.includes(pick)) {
1279
+ cfg.defaultBrain = pick;
1280
+ const label = BRAIN_IDS.includes(pick) ? cfg.brains[pick].label : pick;
1281
+ console.log(c.green(` default set to ${label}`));
1282
+ } else console.log(c.dim(" unchanged."));
1283
+ } else if (choice === "t") {
1284
+ const pick = (await ask(` toggle which brain [${BRAIN_IDS.join("/")}]: `) ?? "").trim();
1285
+ if (BRAIN_IDS.includes(pick)) {
1286
+ if (!cfg.brains[pick].enabled && !isInstalled(cfg.brains[pick].command)) {
1287
+ console.log(c.dim(` ${cfg.brains[pick].label} is not installed. Run "holt init" to install it.`));
1288
+ } else {
1289
+ cfg.brains[pick].enabled = !cfg.brains[pick].enabled;
1290
+ if (!cfg.brains[pick].enabled && cfg.defaultBrain === pick) cfg.defaultBrain = selectableIds(cfg)[0] ?? null;
1291
+ if (cfg.brains[pick].enabled && !cfg.defaultBrain) cfg.defaultBrain = pick;
1292
+ console.log(c.dim(` ${cfg.brains[pick].label} is now ${cfg.brains[pick].enabled ? "on" : "off"}.`));
1293
+ }
1294
+ } else console.log(c.dim(" unchanged."));
1295
+ } else if (choice === "c") {
1296
+ await connectApiBrain(ask, cfg);
1297
+ if (!cfg.defaultBrain && cfg.apiBrains.length) cfg.defaultBrain = cfg.apiBrains[cfg.apiBrains.length - 1]?.id ?? null;
1298
+ } else if (choice === "x") {
1299
+ if (cfg.apiBrains.length === 0) {
1300
+ console.log(c.dim(" no API brains to remove."));
1301
+ continue;
1302
+ }
1303
+ const pick = (await ask(` remove which API brain [${cfg.apiBrains.map((a) => a.id).join("/")}]: `) ?? "").trim();
1304
+ const idx = cfg.apiBrains.findIndex((a) => a.id === pick);
1305
+ if (idx >= 0) {
1306
+ cfg.apiBrains.splice(idx, 1);
1307
+ if (cfg.defaultBrain === pick) cfg.defaultBrain = selectableIds(cfg)[0] ?? null;
1308
+ console.log(c.dim(` removed "${pick}". (its stored key, if any, stays in ~/.holt/credentials.json)`));
1309
+ } else console.log(c.dim(" unchanged."));
1310
+ } else if (choice === "a") {
1311
+ const name = (await ask(" launch command (blank to reset to holt): ") ?? "").trim();
1312
+ if (name && name !== "holt") {
1313
+ const r = installAlias(name);
1314
+ if (r.ok && r.immediate) console.log(c.green(` "${name}" is ready to use right now.`));
1315
+ else if (r.ok) console.log(c.green(` alias "${name}" added to ${r.file}. Run: source ${r.file} (new terminals will not need it).`));
1316
+ else console.log(c.red(" " + r.message));
1317
+ } else {
1318
+ removeAlias();
1319
+ console.log(c.dim(" reset to holt."));
1320
+ }
1321
+ } else if (choice === "m") {
1322
+ await setupTelegram(ask);
1323
+ } else {
1324
+ console.log(c.dim(" pick d, t, c, x, a, m, or press enter to finish."));
1325
+ }
1326
+ saveConfig(cfg);
1327
+ }
1328
+ saveConfig(cfg);
1329
+ return cfg;
1330
+ }
1331
+ async function setting() {
1332
+ const { ask, close } = createReader();
1333
+ if (!await ensureTrusted(ask)) {
1334
+ close();
1335
+ return;
1336
+ }
1337
+ await runSettings(ask);
1338
+ close();
1339
+ console.log("");
1340
+ }
1341
+
1342
+ // src/commands/init.ts
1343
+ function parseBrains(raw, found) {
1344
+ const s = raw.trim().toLowerCase();
1345
+ if (s === "") return found.length ? found : [...BRAIN_IDS];
1346
+ if (s === "all") return [...BRAIN_IDS];
1347
+ const picked = s.split(/[\s,]+/).filter((x) => BRAIN_IDS.includes(x));
1348
+ if (picked.length) return [...new Set(picked)];
1349
+ return found.length ? found : ["claude"];
1350
+ }
1351
+ async function init() {
1352
+ const { ask, close } = createReader();
1353
+ if (!await ensureTrusted(ask)) {
1354
+ close();
1355
+ return;
1356
+ }
1357
+ console.log(c.accent("Holt setup") + c.dim(` (${workspace()})`) + "\n");
1358
+ console.log("Looking for agent CLIs on your machine...\n");
1359
+ const found = [];
1360
+ for (const id of BRAIN_IDS) {
1361
+ const ok2 = isInstalled(BRAIN_DEFS[id].command);
1362
+ console.log(` ${ok2 ? c.green("found ") : c.dim("missing")} ${BRAIN_DEFS[id].label} (${BRAIN_DEFS[id].command})`);
1363
+ if (ok2) found.push(id);
1364
+ }
1365
+ console.log("");
1366
+ const chosen = parseBrains(
1367
+ await ask('Which brains do you want? claude, codex, gemini (comma-separated, or "all"): ') ?? "",
1368
+ found
1369
+ );
1370
+ console.log(c.dim(` using: ${chosen.join(", ")}`));
1371
+ const toInstall = chosen.filter((id) => !isInstalled(BRAIN_DEFS[id].command));
1372
+ const loginWanted = /* @__PURE__ */ new Set();
1373
+ for (const id of toInstall) {
1374
+ const a = (await ask(` ${BRAIN_DEFS[id].label} is not installed. Sign in after install? [Y/n] `) ?? "").trim().toLowerCase();
1375
+ if (a !== "n" && a !== "no") loginWanted.add(id);
1376
+ }
1377
+ const connectedApiBrains = [];
1378
+ const apiAns = (await ask("\nAlso connect a direct API brain (raw key, no CLI needed)? [y/N] ") ?? "").trim().toLowerCase();
1379
+ if (apiAns === "y" || apiAns === "yes") {
1380
+ const holder = defaultConfig();
1381
+ const brain = await connectApiBrain(ask, holder);
1382
+ if (brain) connectedApiBrains.push(brain);
1383
+ }
1384
+ const defPick = chosen.includes("claude") ? "claude" : chosen[0];
1385
+ const dans = (await ask(`
1386
+ Default brain? [${chosen.join("/")}] (${defPick}): `) ?? "").trim();
1387
+ const defaultBrain = chosen.includes(dans) ? dans : defPick;
1388
+ const aliasAns = (await ask('Launch command? Type a custom word like "ai", or press enter to keep "holt": ') ?? "").trim();
1389
+ let aliasNote = "";
1390
+ let aliasNeedsSource = "";
1391
+ let aliasWorked = false;
1392
+ if (aliasAns && aliasAns !== "holt") {
1393
+ const r = installAlias(aliasAns);
1394
+ aliasWorked = r.ok;
1395
+ if (r.ok && r.immediate) aliasNote = c.green(` "${aliasAns}" is ready to use right now (launcher at ${r.file}).`);
1396
+ else if (r.ok) {
1397
+ aliasNote = c.green(` alias "${aliasAns}" -> holt chat added to ${r.file}`);
1398
+ aliasNeedsSource = r.file;
1399
+ } else aliasNote = c.red(" " + r.message);
1400
+ }
1401
+ let wantMemorySetup = false;
1402
+ const embedReady = await embeddingsAvailable();
1403
+ if (embedReady) {
1404
+ console.log(c.dim("\nSemantic memory: ready (local Ollama with " + EMBED_MODEL + " detected)."));
1405
+ } else {
1406
+ const ollamaHere = isInstalled("ollama");
1407
+ const q = ollamaHere ? `Semantic memory needs a local embed model. Pull ${EMBED_MODEL} with Ollama now? [Y/n] ` : "Enable private semantic memory? Installs Ollama plus a small local embed model. Everything stays on your machine. [Y/n] ";
1408
+ const a = (await ask("\n" + q) ?? "").trim().toLowerCase();
1409
+ wantMemorySetup = a !== "n" && a !== "no";
1410
+ if (!wantMemorySetup) console.log(c.dim(' Okay. Memory still works with keyword recall; run "holt init" again anytime.'));
1411
+ }
1412
+ if (!loadTelegramConfig()) {
1413
+ const tgAns = (await ask("\nChat with Holt from your phone over Telegram? [y/N] ") ?? "").trim().toLowerCase();
1414
+ if (tgAns === "y" || tgAns === "yes") await setupTelegram(ask);
978
1415
  }
979
- if (!res.body) {
980
- let body = "";
981
- try {
982
- body = await res.text();
983
- } catch {
984
- }
985
- return { ok: false, text: `${brain.provider} returned no stream. Body: ${body.slice(0, 200)}` };
1416
+ close();
1417
+ for (const id of toInstall) {
1418
+ const s = BRAIN_SETUP[id];
1419
+ console.log("\n" + c.accent(`Installing ${BRAIN_DEFS[id].label}`) + c.dim(` (${s.install.join(" ")})`));
1420
+ const code = await runInteractive(s.install[0], s.install.slice(1));
1421
+ console.log(code === 0 ? c.green(` ${BRAIN_DEFS[id].label} installed.`) : c.red(` Install failed (exit ${code}). Run manually: ${s.install.join(" ")}`));
986
1422
  }
987
- const decoder = new TextDecoder();
988
- let carry = "";
989
- let text = "";
990
- try {
991
- for await (const chunk2 of res.body) {
992
- carry += decoder.decode(chunk2, { stream: true });
993
- const { events, rest } = parseSSELines(carry);
994
- carry = rest;
995
- for (const ev of events) {
996
- for (const payload of dataLines(ev)) {
997
- if (payload === "[DONE]") continue;
998
- let json;
999
- try {
1000
- json = JSON.parse(payload);
1001
- } catch {
1002
- continue;
1003
- }
1004
- const piece = extractDelta(brain.provider, json);
1005
- if (piece) {
1006
- text += piece;
1007
- if (onChunk) onChunk(piece);
1008
- }
1009
- }
1423
+ for (const id of toInstall) {
1424
+ if (!loginWanted.has(id) || !isInstalled(BRAIN_DEFS[id].command)) continue;
1425
+ const s = BRAIN_SETUP[id];
1426
+ console.log("\n" + c.accent(`Sign in to ${BRAIN_DEFS[id].label}`));
1427
+ console.log(c.dim(` Starting "${s.login.join(" ")}". Complete sign-in, then exit that tool to return here.`));
1428
+ await runInteractive(s.login[0], s.login.slice(1));
1429
+ }
1430
+ if (wantMemorySetup) {
1431
+ if (!isInstalled("ollama")) {
1432
+ if (process.platform === "darwin" && isInstalled("brew")) {
1433
+ console.log("\n" + c.accent("Installing Ollama") + c.dim(" (brew install ollama)"));
1434
+ const code = await runInteractive("brew", ["install", "ollama"]);
1435
+ if (code === 0) await runInteractive("brew", ["services", "start", "ollama"]);
1436
+ else console.log(c.red(' Install failed. Get Ollama from https://ollama.com/download and run "holt init" again.'));
1437
+ } else {
1438
+ console.log(c.dim('\n Get Ollama from https://ollama.com/download, then run "holt init" again to finish memory setup.'));
1010
1439
  }
1011
1440
  }
1012
- } catch (e) {
1013
- if (text) return { ok: true, text };
1014
- return { ok: false, text: `Stream from ${brain.provider} failed: ${e.message}` };
1015
- }
1016
- if (carry.trim()) {
1017
- for (const payload of dataLines(carry)) {
1018
- if (payload === "[DONE]") continue;
1019
- try {
1020
- const piece = extractDelta(brain.provider, JSON.parse(payload));
1021
- if (piece) {
1022
- text += piece;
1023
- if (onChunk) onChunk(piece);
1024
- }
1025
- } catch {
1441
+ if (isInstalled("ollama")) {
1442
+ console.log("\n" + c.accent("Pulling embed model") + c.dim(` (ollama pull ${EMBED_MODEL})`));
1443
+ const code = await runInteractive("ollama", ["pull", EMBED_MODEL]);
1444
+ if (code !== 0) {
1445
+ console.log(c.dim(' Could not pull. Start Ollama (open the app or run "ollama serve"), then run:'));
1446
+ console.log(c.dim(` ollama pull ${EMBED_MODEL}`));
1026
1447
  }
1448
+ resetEmbedProbe();
1449
+ if (await embeddingsAvailable()) console.log(c.green(" Semantic memory is ready. Chats in trusted folders are stored and recalled locally."));
1027
1450
  }
1028
1451
  }
1029
- const trimmed = text.trim();
1030
- if (trimmed) return { ok: true, text: trimmed };
1031
- return { ok: false, text: `${brain.provider} returned an empty reply.` };
1452
+ const cfg = loadConfig() ?? defaultConfig();
1453
+ for (const id of BRAIN_IDS) cfg.brains[id].enabled = chosen.includes(id) && isInstalled(BRAIN_DEFS[id].command);
1454
+ for (const b of connectedApiBrains) if (!cfg.apiBrains.some((a) => a.id === b.id)) cfg.apiBrains.push(b);
1455
+ cfg.defaultBrain = cfg.brains[defaultBrain].enabled ? defaultBrain : BRAIN_IDS.find((id) => cfg.brains[id].enabled) ?? cfg.apiBrains[0]?.id ?? null;
1456
+ saveConfig(cfg);
1457
+ console.log("\n" + c.green("Saved to ./.holt/config.json"));
1458
+ if (aliasNote) console.log(aliasNote);
1459
+ if (cfg.defaultBrain) {
1460
+ if (aliasWorked && !aliasNeedsSource) {
1461
+ console.log("Start chatting: " + c.accent(aliasAns) + "\n");
1462
+ } else if (aliasNeedsSource) {
1463
+ console.log("\nStart chatting:");
1464
+ console.log(" " + c.accent(`source ${aliasNeedsSource}`) + c.dim(" (once; new terminals will not need it)"));
1465
+ console.log(" " + c.accent(aliasAns) + "\n");
1466
+ console.log(c.dim(" Or right now, without sourcing: holt chat\n"));
1467
+ } else {
1468
+ console.log("Start chatting: " + c.accent("holt chat") + "\n");
1469
+ }
1470
+ } else {
1471
+ console.log(c.dim('No brain is ready yet. Install one, then run "holt init" again.\n'));
1472
+ }
1032
1473
  }
1033
1474
 
1475
+ // src/commands/chat.ts
1476
+ import { randomUUID as randomUUID3 } from "crypto";
1477
+
1034
1478
  // src/facts.ts
1035
1479
  var MIN_EXCHANGES_FOR_EXTRACTION = 3;
1036
1480
  var MAX_TRANSCRIPT_TURNS = 20;
@@ -1141,8 +1585,8 @@ async function extractAndSaveFacts(active, cfg, history, session) {
1141
1585
  }
1142
1586
 
1143
1587
  // src/output.ts
1144
- import { writeFileSync as writeFileSync5 } from "fs";
1145
- import { join as join5 } from "path";
1588
+ import { writeFileSync as writeFileSync6 } from "fs";
1589
+ import { join as join7 } from "path";
1146
1590
  function escapeHtml(s) {
1147
1591
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1148
1592
  }
@@ -1275,139 +1719,19 @@ function saveReply(reply, format, name) {
1275
1719
  let base = (name ?? "").trim();
1276
1720
  if (!base) base = `holt-reply-${timestamp()}`;
1277
1721
  if (!/\.[a-z0-9]+$/i.test(base)) base += ext;
1278
- const path = join5(process.cwd(), base);
1279
- writeFileSync5(path, renderReply(reply, format), "utf8");
1722
+ const path = join7(process.cwd(), base);
1723
+ writeFileSync6(path, renderReply(reply, format), "utf8");
1280
1724
  return path;
1281
1725
  }
1282
1726
 
1283
- // src/skills.ts
1284
- import { readFileSync as readFileSync5, readdirSync, existsSync as existsSync5, statSync as statSync2 } from "fs";
1285
- import { join as join6 } from "path";
1286
- function skillsRoot(scope) {
1287
- return scope === "workspace" ? join6(wsHoltDir(), "skills") : join6(GLOBAL_DIR, "skills");
1288
- }
1289
- function sanitizeName(name) {
1290
- return (name || "").trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
1291
- }
1292
- function parseFrontmatter(md) {
1293
- const text = md.replace(/^/, "");
1294
- const lines = text.split("\n");
1295
- let start = 0;
1296
- while (start < lines.length && lines[start].trim() === "") start++;
1297
- if (start >= lines.length || lines[start].trim() !== "---") {
1298
- return { data: {}, body: text.trim() };
1299
- }
1300
- const data = {};
1301
- let i = start + 1;
1302
- let closed = false;
1303
- for (; i < lines.length; i++) {
1304
- const line = lines[i];
1305
- if (line.trim() === "---") {
1306
- closed = true;
1307
- i++;
1308
- break;
1309
- }
1310
- const idx = line.indexOf(":");
1311
- if (idx === -1) continue;
1312
- const key = line.slice(0, idx).trim();
1313
- if (!key) continue;
1314
- let value = line.slice(idx + 1).trim();
1315
- if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
1316
- value = value.slice(1, -1);
1317
- }
1318
- data[key.toLowerCase()] = value;
1319
- }
1320
- if (!closed) {
1321
- return { data: {}, body: text.trim() };
1322
- }
1323
- return { data, body: lines.slice(i).join("\n").trim() };
1324
- }
1325
- function readSkill(dir, folder, scope) {
1326
- const file = join6(dir, "SKILL.md");
1327
- if (!existsSync5(file)) return null;
1328
- let raw = "";
1329
- try {
1330
- raw = readFileSync5(file, "utf8");
1331
- } catch {
1332
- return null;
1333
- }
1334
- const { data, body } = parseFrontmatter(raw);
1335
- const name = sanitizeName(data.name || folder) || folder;
1336
- let description = data.description || "";
1337
- if (!description) {
1338
- const firstLine2 = body.split("\n").map((l) => l.trim()).find((l) => l.length > 0) || "";
1339
- description = firstLine2.replace(/^#+\s*/, "");
1340
- }
1341
- return { name, description, dir, scope };
1342
- }
1343
- function listScope(scope) {
1344
- const root = skillsRoot(scope);
1345
- if (!existsSync5(root)) return [];
1346
- let entries;
1347
- try {
1348
- entries = readdirSync(root);
1349
- } catch {
1350
- return [];
1351
- }
1352
- const out = [];
1353
- for (const folder of entries) {
1354
- const dir = join6(root, folder);
1355
- try {
1356
- if (!statSync2(dir).isDirectory()) continue;
1357
- } catch {
1358
- continue;
1359
- }
1360
- const skill = readSkill(dir, folder, scope);
1361
- if (skill) out.push(skill);
1362
- }
1363
- return out;
1364
- }
1365
- function listSkills() {
1366
- const byName = /* @__PURE__ */ new Map();
1367
- for (const s of listScope("global")) byName.set(s.name, s);
1368
- for (const s of listScope("workspace")) byName.set(s.name, s);
1369
- return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
1370
- }
1371
- function loadSkill(name) {
1372
- const want = sanitizeName(name);
1373
- const skill = listSkills().find((s) => s.name === want);
1374
- if (!skill) return null;
1375
- let raw = "";
1376
- try {
1377
- raw = readFileSync5(join6(skill.dir, "SKILL.md"), "utf8");
1378
- } catch {
1379
- return null;
1380
- }
1381
- const { body } = parseFrontmatter(raw);
1382
- return { skill, body };
1383
- }
1384
- function skillsPromptBlock() {
1385
- const skills = listSkills();
1386
- if (skills.length === 0) return "";
1387
- const lines = [
1388
- `Skills available to this user (invoked with "/skill <name>"). When the user invokes a skill, follow that skill's instructions:`,
1389
- ...skills.map((s) => `- ${s.name}: ${s.description || "(no description)"}`)
1390
- ];
1391
- return lines.join("\n");
1392
- }
1393
- function resolveSkillInvocation(line) {
1394
- const trimmed = line.trim();
1395
- const m = trimmed.match(/^\/skill\s+(\S+)\s*([\s\S]*)$/i);
1396
- if (!m) return null;
1397
- const name = m[1];
1398
- const input = (m[2] || "").trim();
1399
- const loaded = loadSkill(name);
1400
- if (!loaded) return null;
1401
- const prompt = [
1402
- loaded.body,
1403
- "",
1404
- "Apply the skill above to this request:",
1405
- input || "Introduce what you can do with this skill."
1406
- ].join("\n");
1407
- return { prompt, skillName: loaded.skill.name };
1408
- }
1409
-
1410
1727
  // src/commands/chat.ts
1728
+ function statusLine(brainLabel, history) {
1729
+ const live = Math.min(history.length, MAX_REPLAY_TURNS);
1730
+ const frac = MAX_REPLAY_TURNS ? live / MAX_REPLAY_TURNS : 0;
1731
+ const pct = Math.round(frac * 100);
1732
+ const mem = memStats().turns;
1733
+ return c.dim(" " + brainLabel + " ") + c.dim("[") + bar(frac) + c.dim("]") + c.dim(` ${pct}% context \xB7 ${mem} in memory`);
1734
+ }
1411
1735
  function help() {
1412
1736
  console.log(c.dim([
1413
1737
  " commands:",
@@ -1422,7 +1746,7 @@ function help() {
1422
1746
  " /exit leave"
1423
1747
  ].join("\n")));
1424
1748
  }
1425
- function resolveActive(cfg, id) {
1749
+ function resolveActive2(cfg, id) {
1426
1750
  if (BRAIN_IDS.includes(id)) {
1427
1751
  const b = cfg.brains[id];
1428
1752
  return { kind: "cli", id, label: b.label };
@@ -1449,10 +1773,10 @@ async function chat() {
1449
1773
  console.log(c.dim('\nSetup done. Run "holt chat" to start talking.\n'));
1450
1774
  return;
1451
1775
  }
1452
- let active = resolveActive(cfg, cfg.defaultBrain);
1776
+ let active = resolveActive2(cfg, cfg.defaultBrain);
1453
1777
  if (!active) {
1454
1778
  const fallback = BRAIN_IDS.find((id) => cfg.brains[id].enabled) ?? cfg.apiBrains[0]?.id ?? null;
1455
- active = fallback ? resolveActive(cfg, fallback) : null;
1779
+ active = fallback ? resolveActive2(cfg, fallback) : null;
1456
1780
  if (!active) {
1457
1781
  close();
1458
1782
  console.log(c.dim('\nNo brain is ready. Run "holt setting".\n'));
@@ -1471,7 +1795,8 @@ async function chat() {
1471
1795
  if (embedOk && stats.withEmbeddings < stats.turns) {
1472
1796
  console.log(c.dim(` ${stats.turns - stats.withEmbeddings} older moments lack embeddings. Run "holt memory embed" to upgrade them.`));
1473
1797
  }
1474
- console.log(c.dim("Type a message. Commands: /brain /memory /output /save /setting /clear /help /exit\n"));
1798
+ console.log(c.dim("Type a message. Commands: /brain /memory /output /save /setting /clear /help /exit"));
1799
+ console.log(statusLine(active.label, history) + "\n");
1475
1800
  while (true) {
1476
1801
  const raw = await ask(c.accent("\u203A "));
1477
1802
  if (raw === null) break;
@@ -1542,8 +1867,8 @@ async function chat() {
1542
1867
  }
1543
1868
  if (cmd === "setting" || cmd === "settings") {
1544
1869
  cfg = await runSettings(ask);
1545
- const next = cfg.defaultBrain ? resolveActive(cfg, cfg.defaultBrain) : null;
1546
- const stillValid = resolveActive(cfg, active.id);
1870
+ const next = cfg.defaultBrain ? resolveActive2(cfg, cfg.defaultBrain) : null;
1871
+ const stillValid = resolveActive2(cfg, active.id);
1547
1872
  active = stillValid ?? next ?? active;
1548
1873
  console.log(c.dim(` brain: ${active.label}`));
1549
1874
  continue;
@@ -1553,7 +1878,7 @@ async function chat() {
1553
1878
  const apiIds = cfg.apiBrains.map((a) => a.id);
1554
1879
  const all = [...cliEnabled, ...apiIds];
1555
1880
  if (arg && all.includes(arg)) {
1556
- const next = resolveActive(cfg, arg);
1881
+ const next = resolveActive2(cfg, arg);
1557
1882
  if (next) {
1558
1883
  active = next;
1559
1884
  const turns = Math.floor(history.length / 2);
@@ -1610,8 +1935,9 @@ async function chat() {
1610
1935
  history.push({ role: "user", content: line });
1611
1936
  history.push({ role: "assistant", content: res.text });
1612
1937
  const now = Date.now();
1613
- appendTurn({ id: randomUUID2().slice(0, 8), ts: now, session, role: "user", content: line, emb: await embed(line) ?? void 0 });
1614
- appendTurn({ id: randomUUID2().slice(0, 8), ts: now, session, role: "assistant", content: res.text, emb: await embed(res.text) ?? void 0 });
1938
+ appendTurn({ id: randomUUID3().slice(0, 8), ts: now, session, role: "user", content: line, emb: await embed(line) ?? void 0 });
1939
+ appendTurn({ id: randomUUID3().slice(0, 8), ts: now, session, role: "assistant", content: res.text, emb: await embed(res.text) ?? void 0 });
1940
+ console.log(statusLine(active.label, history) + "\n");
1615
1941
  } else {
1616
1942
  console.log(c.red("\n " + res.text + "\n"));
1617
1943
  }
@@ -1645,7 +1971,7 @@ async function login(which) {
1645
1971
  }
1646
1972
 
1647
1973
  // src/commands/memory.ts
1648
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
1974
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
1649
1975
  async function memoryCmd(sub, rest = []) {
1650
1976
  const { ask, close } = createReader();
1651
1977
  if (!await ensureTrusted(ask)) {
@@ -1692,12 +2018,12 @@ async function memoryCmd(sub, rest = []) {
1692
2018
  return;
1693
2019
  }
1694
2020
  if (action === "facts") {
1695
- if (!existsSync6(factsMdPath())) {
2021
+ if (!existsSync7(factsMdPath())) {
1696
2022
  console.log(c.dim("\n No facts distilled yet. They form when you end a chat session.\n"));
1697
2023
  close();
1698
2024
  return;
1699
2025
  }
1700
- const body = readFileSync6(factsMdPath(), "utf8").trim();
2026
+ const body = readFileSync7(factsMdPath(), "utf8").trim();
1701
2027
  console.log("\n" + body + "\n");
1702
2028
  close();
1703
2029
  return;
@@ -1743,17 +2069,17 @@ async function memoryCmd(sub, rest = []) {
1743
2069
 
1744
2070
  // src/commands/skill.ts
1745
2071
  import {
1746
- mkdirSync as mkdirSync5,
1747
- writeFileSync as writeFileSync6,
1748
- readFileSync as readFileSync7,
1749
- existsSync as existsSync7,
2072
+ mkdirSync as mkdirSync6,
2073
+ writeFileSync as writeFileSync7,
2074
+ readFileSync as readFileSync8,
2075
+ existsSync as existsSync8,
1750
2076
  readdirSync as readdirSync2,
1751
2077
  statSync as statSync3,
1752
2078
  rmSync as rmSync3,
1753
2079
  cpSync,
1754
2080
  mkdtempSync
1755
2081
  } from "fs";
1756
- import { join as join7, resolve } from "path";
2082
+ import { join as join8, resolve } from "path";
1757
2083
  import { tmpdir } from "os";
1758
2084
  import { spawnSync as spawnSync2 } from "child_process";
1759
2085
  function usage() {
@@ -1799,12 +2125,12 @@ function isGitUrl(s) {
1799
2125
  return s.includes("://") || s.endsWith(".git") || s.startsWith("git@");
1800
2126
  }
1801
2127
  function findSkillDir(root) {
1802
- if (existsSync7(join7(root, "SKILL.md"))) return root;
2128
+ if (existsSync8(join8(root, "SKILL.md"))) return root;
1803
2129
  let subs = [];
1804
2130
  try {
1805
2131
  subs = readdirSync2(root).filter((f) => {
1806
2132
  try {
1807
- return statSync3(join7(root, f)).isDirectory();
2133
+ return statSync3(join8(root, f)).isDirectory();
1808
2134
  } catch {
1809
2135
  return false;
1810
2136
  }
@@ -1812,8 +2138,8 @@ function findSkillDir(root) {
1812
2138
  } catch {
1813
2139
  return null;
1814
2140
  }
1815
- const withSkill = subs.filter((f) => existsSync7(join7(root, f, "SKILL.md")));
1816
- if (withSkill.length === 1) return join7(root, withSkill[0]);
2141
+ const withSkill = subs.filter((f) => existsSync8(join8(root, f, "SKILL.md")));
2142
+ if (withSkill.length === 1) return join8(root, withSkill[0]);
1817
2143
  return null;
1818
2144
  }
1819
2145
  function cmdList() {
@@ -1852,7 +2178,7 @@ function cmdShow(name) {
1852
2178
  const { skill, body } = loaded;
1853
2179
  console.log("\n" + c.accent(skill.name) + c.dim(` (${skill.scope})`));
1854
2180
  if (skill.description) console.log(" " + skill.description);
1855
- console.log(c.dim(" " + join7(skill.dir, "SKILL.md")));
2181
+ console.log(c.dim(" " + join8(skill.dir, "SKILL.md")));
1856
2182
  console.log("");
1857
2183
  console.log(c.dim(body));
1858
2184
  console.log("");
@@ -1864,18 +2190,18 @@ function cmdCreate(name, global) {
1864
2190
  return;
1865
2191
  }
1866
2192
  const scope = global ? "global" : "workspace";
1867
- const dir = join7(skillsRoot(scope), clean);
1868
- if (existsSync7(dir)) {
2193
+ const dir = join8(skillsRoot(scope), clean);
2194
+ if (existsSync8(dir)) {
1869
2195
  console.log(c.red(`
1870
2196
  A ${scope} skill named "${clean}" already exists.`));
1871
2197
  console.log(c.dim(" " + dir + "\n"));
1872
2198
  return;
1873
2199
  }
1874
- mkdirSync5(dir, { recursive: true });
1875
- writeFileSync6(join7(dir, "SKILL.md"), SKILL_TEMPLATE(clean), "utf8");
2200
+ mkdirSync6(dir, { recursive: true });
2201
+ writeFileSync7(join8(dir, "SKILL.md"), SKILL_TEMPLATE(clean), "utf8");
1876
2202
  console.log(c.green(`
1877
2203
  Created ${scope} skill "${clean}".`));
1878
- console.log(c.dim(" " + join7(dir, "SKILL.md")));
2204
+ console.log(c.dim(" " + join8(dir, "SKILL.md")));
1879
2205
  console.log(c.dim(' Edit it, then run it in chat with "/skill ' + clean + '".\n'));
1880
2206
  }
1881
2207
  function cmdAdd(source, global) {
@@ -1888,7 +2214,7 @@ function cmdAdd(source, global) {
1888
2214
  let tempDir = "";
1889
2215
  try {
1890
2216
  if (isGitUrl(source)) {
1891
- tempDir = mkdtempSync(join7(tmpdir(), "holt-skill-"));
2217
+ tempDir = mkdtempSync(join8(tmpdir(), "holt-skill-"));
1892
2218
  console.log(c.dim(`
1893
2219
  Cloning ${source} ...`));
1894
2220
  const res = spawnSync2("git", ["clone", "--depth", "1", source, tempDir], { stdio: "ignore" });
@@ -1899,7 +2225,7 @@ function cmdAdd(source, global) {
1899
2225
  fetched = tempDir;
1900
2226
  } else {
1901
2227
  fetched = resolve(source);
1902
- if (!existsSync7(fetched)) {
2228
+ if (!existsSync8(fetched)) {
1903
2229
  console.log(c.red(`
1904
2230
  No such path: ${fetched}
1905
2231
  `));
@@ -1913,7 +2239,7 @@ function cmdAdd(source, global) {
1913
2239
  }
1914
2240
  let skillName = "";
1915
2241
  try {
1916
- const { data } = parseFrontmatter(readFileSync7(join7(skillDir, "SKILL.md"), "utf8"));
2242
+ const { data } = parseFrontmatter(readFileSync8(join8(skillDir, "SKILL.md"), "utf8"));
1917
2243
  skillName = sanitizeName(data.name || "");
1918
2244
  } catch {
1919
2245
  skillName = "";
@@ -1923,22 +2249,22 @@ function cmdAdd(source, global) {
1923
2249
  console.log(c.red("\n Could not determine a valid skill name.\n"));
1924
2250
  return;
1925
2251
  }
1926
- const dest = join7(skillsRoot(scope), skillName);
1927
- if (existsSync7(dest)) {
2252
+ const dest = join8(skillsRoot(scope), skillName);
2253
+ if (existsSync8(dest)) {
1928
2254
  console.log(c.red(`
1929
2255
  A ${scope} skill named "${skillName}" already exists.`));
1930
2256
  console.log(c.dim(" " + dest + "\n"));
1931
2257
  return;
1932
2258
  }
1933
- mkdirSync5(skillsRoot(scope), { recursive: true });
2259
+ mkdirSync6(skillsRoot(scope), { recursive: true });
1934
2260
  cpSync(skillDir, dest, { recursive: true });
1935
- const nestedGit = join7(dest, ".git");
1936
- if (existsSync7(nestedGit)) rmSync3(nestedGit, { recursive: true, force: true });
2261
+ const nestedGit = join8(dest, ".git");
2262
+ if (existsSync8(nestedGit)) rmSync3(nestedGit, { recursive: true, force: true });
1937
2263
  console.log(c.green(`
1938
2264
  Installed ${scope} skill "${skillName}".`));
1939
- console.log(c.dim(" " + join7(dest, "SKILL.md") + "\n"));
2265
+ console.log(c.dim(" " + join8(dest, "SKILL.md") + "\n"));
1940
2266
  } finally {
1941
- if (tempDir && existsSync7(tempDir)) {
2267
+ if (tempDir && existsSync8(tempDir)) {
1942
2268
  try {
1943
2269
  rmSync3(tempDir, { recursive: true, force: true });
1944
2270
  } catch {
@@ -1952,9 +2278,9 @@ async function cmdRemove(name, ask) {
1952
2278
  console.log(c.dim("\n Usage: holt skill remove <name>\n"));
1953
2279
  return;
1954
2280
  }
1955
- const wsDir = join7(skillsRoot("workspace"), clean);
1956
- const globalDir = join7(skillsRoot("global"), clean);
1957
- const target = existsSync7(wsDir) ? wsDir : existsSync7(globalDir) ? globalDir : "";
2281
+ const wsDir = join8(skillsRoot("workspace"), clean);
2282
+ const globalDir = join8(skillsRoot("global"), clean);
2283
+ const target = existsSync8(wsDir) ? wsDir : existsSync8(globalDir) ? globalDir : "";
1958
2284
  if (!target) {
1959
2285
  console.log(c.dim(`
1960
2286
  No skill named "${clean}" found.
@@ -2012,8 +2338,8 @@ async function skillCmd(sub, rest = []) {
2012
2338
  }
2013
2339
 
2014
2340
  // src/commands/graph.ts
2015
- import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6 } from "fs";
2016
- import { join as join8, dirname as dirname2, resolve as resolve2 } from "path";
2341
+ import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync7 } from "fs";
2342
+ import { join as join9, dirname as dirname2, resolve as resolve2 } from "path";
2017
2343
  import { spawn as spawn3 } from "child_process";
2018
2344
 
2019
2345
  // src/graphview.ts
@@ -2677,9 +3003,9 @@ async function graph(args = []) {
2677
3003
  concepts: conceptCount,
2678
3004
  edges: g.edges.length
2679
3005
  });
2680
- const outPath = opts.out ? resolve2(opts.out) : join8(wsHoltDir(), "graph.html");
2681
- mkdirSync6(dirname2(outPath), { recursive: true });
2682
- writeFileSync7(outPath, html, "utf8");
3006
+ const outPath = opts.out ? resolve2(opts.out) : join9(wsHoltDir(), "graph.html");
3007
+ mkdirSync7(dirname2(outPath), { recursive: true });
3008
+ writeFileSync8(outPath, html, "utf8");
2683
3009
  console.log("\n" + c.accent("Memory graph built") + c.dim(" (this folder)"));
2684
3010
  console.log(` nodes ${g.nodes.length} (${turns.length} turns, ${conceptCount} concepts)`);
2685
3011
  console.log(` edges ${g.edges.length}`);
@@ -2990,73 +3316,18 @@ async function mcp(sub, rest = []) {
2990
3316
  return;
2991
3317
  }
2992
3318
  try {
2993
- if (!isTrusted()) trustDir();
2994
- console.error("Holt MCP: serving memory for " + workspace());
2995
- await runMcpServer({ name: "holt", version: "0.7.0", tools: holtTools() });
2996
- } catch (err2) {
2997
- console.error("Holt MCP failed: " + (err2 instanceof Error ? err2.message : String(err2)));
2998
- process.exitCode = 1;
2999
- }
3000
- }
3001
-
3002
- // src/commands/run.ts
3003
- import { writeFileSync as writeFileSync8 } from "fs";
3004
- import { resolve as resolve3 } from "path";
3005
-
3006
- // src/runner.ts
3007
- import { randomUUID as randomUUID3 } from "crypto";
3008
- function resolveActive2(cfg, id) {
3009
- if (BRAIN_IDS.includes(id)) {
3010
- const b = cfg.brains[id];
3011
- return { kind: "cli", id, label: b.label };
3012
- }
3013
- const api = findApiBrain(cfg, id);
3014
- if (api) return { kind: "api", id, label: `${id} (api: ${api.provider}/${api.model})`, brain: api };
3015
- return null;
3016
- }
3017
- async function runTask(task, opts = {}) {
3018
- try {
3019
- const cfg = loadConfig();
3020
- if (!cfg || !cfg.defaultBrain) {
3021
- return { ok: false, text: "No Holt setup in this folder. Run holt init.", brainLabel: "" };
3022
- }
3023
- const wantId = opts.brainId ?? cfg.defaultBrain;
3024
- const active = resolveActive2(cfg, wantId);
3025
- if (!active) {
3026
- return { ok: false, text: `Brain "${wantId}" is not configured. Run holt setting.`, brainLabel: "" };
3027
- }
3028
- if (active.kind === "cli" && !isInstalled(cfg.brains[active.id].command)) {
3029
- return {
3030
- ok: false,
3031
- text: `${active.label} (${cfg.brains[active.id].command}) is not on your PATH. Install it or run holt setting.`,
3032
- brainLabel: active.label
3033
- };
3034
- }
3035
- if (active.kind === "api" && !resolveApiKey(active.brain)) {
3036
- return {
3037
- ok: false,
3038
- text: `${active.label} has no API key. Run holt setting to add one.`,
3039
- brainLabel: active.label
3040
- };
3041
- }
3042
- const runSession = "run-" + newSessionId();
3043
- const recalled = opts.recall !== false ? await recall(task, runSession, 4) : [];
3044
- const block = skillsPromptBlock();
3045
- const base = renderPrompt([], task, recalled);
3046
- const prompt = block ? block + "\n\n" + base : base;
3047
- const res = active.kind === "cli" ? await runBrain(cfg.brains[active.id], prompt, opts.onChunk) : await runApiBrain(active.brain, prompt, opts.onChunk);
3048
- if (res.ok && opts.store !== false) {
3049
- const now = Date.now();
3050
- appendTurn({ id: randomUUID3().slice(0, 8), ts: now, session: runSession, role: "user", content: task, emb: await embed(task) ?? void 0 });
3051
- appendTurn({ id: randomUUID3().slice(0, 8), ts: now, session: runSession, role: "assistant", content: res.text, emb: await embed(res.text) ?? void 0 });
3052
- }
3053
- return { ok: res.ok, text: res.text, brainLabel: active.label };
3054
- } catch (e) {
3055
- return { ok: false, text: e.message, brainLabel: "" };
3319
+ if (!isTrusted()) trustDir();
3320
+ console.error("Holt MCP: serving memory for " + workspace());
3321
+ await runMcpServer({ name: "holt", version: "0.7.0", tools: holtTools() });
3322
+ } catch (err2) {
3323
+ console.error("Holt MCP failed: " + (err2 instanceof Error ? err2.message : String(err2)));
3324
+ process.exitCode = 1;
3056
3325
  }
3057
3326
  }
3058
3327
 
3059
3328
  // src/commands/run.ts
3329
+ import { writeFileSync as writeFileSync9 } from "fs";
3330
+ import { resolve as resolve3 } from "path";
3060
3331
  var USAGE = [
3061
3332
  " holt run <task> [options]",
3062
3333
  "",
@@ -3145,45 +3416,45 @@ async function run(args) {
3145
3416
  }
3146
3417
  if (p.out) {
3147
3418
  const target = resolve3(process.cwd(), p.out);
3148
- writeFileSync8(target, result.text, "utf8");
3419
+ writeFileSync9(target, result.text, "utf8");
3149
3420
  process.stderr.write(c.dim(`saved to ${target}`) + "\n");
3150
3421
  }
3151
3422
  }
3152
3423
 
3153
3424
  // src/commands/schedule.ts
3154
3425
  import { spawnSync as spawnSync4 } from "child_process";
3155
- import { writeFileSync as writeFileSync10, mkdirSync as mkdirSync8, rmSync as rmSync4, existsSync as existsSync8 } from "fs";
3426
+ import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync9, rmSync as rmSync4, existsSync as existsSync9 } from "fs";
3156
3427
  import { dirname as dirname3 } from "path";
3157
3428
 
3158
3429
  // src/scheduler.ts
3159
3430
  import { homedir as homedir3 } from "os";
3160
- import { join as join9 } from "path";
3161
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync9, mkdirSync as mkdirSync7 } from "fs";
3431
+ import { join as join10 } from "path";
3432
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync10, mkdirSync as mkdirSync8 } from "fs";
3162
3433
  import { randomUUID as randomUUID4 } from "crypto";
3163
3434
  import { spawnSync as spawnSync3 } from "child_process";
3164
3435
  function schedulesPath() {
3165
- return join9(GLOBAL_DIR, "schedules.json");
3436
+ return join10(GLOBAL_DIR, "schedules.json");
3166
3437
  }
3167
3438
  function logsDir() {
3168
- return join9(GLOBAL_DIR, "logs");
3439
+ return join10(GLOBAL_DIR, "logs");
3169
3440
  }
3170
3441
  function logPath(id) {
3171
- return join9(logsDir(), `${id}.log`);
3442
+ return join10(logsDir(), `${id}.log`);
3172
3443
  }
3173
3444
  function plistPath(id) {
3174
- return join9(homedir3(), "Library", "LaunchAgents", `com.holt.${id}.plist`);
3445
+ return join10(homedir3(), "Library", "LaunchAgents", `com.holt.${id}.plist`);
3175
3446
  }
3176
3447
  function loadJobs() {
3177
3448
  try {
3178
- const raw = JSON.parse(readFileSync8(schedulesPath(), "utf8"));
3449
+ const raw = JSON.parse(readFileSync9(schedulesPath(), "utf8"));
3179
3450
  return Array.isArray(raw) ? raw : [];
3180
3451
  } catch {
3181
3452
  return [];
3182
3453
  }
3183
3454
  }
3184
3455
  function saveJobs(jobs) {
3185
- mkdirSync7(GLOBAL_DIR, { recursive: true });
3186
- writeFileSync9(schedulesPath(), JSON.stringify(jobs, null, 2) + "\n", "utf8");
3456
+ mkdirSync8(GLOBAL_DIR, { recursive: true });
3457
+ writeFileSync10(schedulesPath(), JSON.stringify(jobs, null, 2) + "\n", "utf8");
3187
3458
  }
3188
3459
  function addJob(job) {
3189
3460
  const jobs = loadJobs();
@@ -3287,7 +3558,7 @@ function appendCronLine(crontab, line) {
3287
3558
  return (base ? base + "\n" : "") + line + "\n";
3288
3559
  }
3289
3560
  function ensureLogsDir() {
3290
- mkdirSync7(logsDir(), { recursive: true });
3561
+ mkdirSync8(logsDir(), { recursive: true });
3291
3562
  }
3292
3563
 
3293
3564
  // src/commands/schedule.ts
@@ -3313,8 +3584,8 @@ function parseAdd(rest) {
3313
3584
  }
3314
3585
  function installDarwin(job, holtPath) {
3315
3586
  const path = plistPath(job.id);
3316
- mkdirSync8(dirname3(path), { recursive: true });
3317
- writeFileSync10(path, buildLaunchdPlist(job, holtPath), "utf8");
3587
+ mkdirSync9(dirname3(path), { recursive: true });
3588
+ writeFileSync11(path, buildLaunchdPlist(job, holtPath), "utf8");
3318
3589
  const res = spawnSync4("launchctl", ["load", "-w", path], { encoding: "utf8" });
3319
3590
  if (res.status === 0) {
3320
3591
  console.log(c.green(" Loaded into launchd."));
@@ -3400,7 +3671,7 @@ function listCmd() {
3400
3671
  }
3401
3672
  function removeDarwin(id) {
3402
3673
  const path = plistPath(id);
3403
- if (existsSync8(path)) {
3674
+ if (existsSync9(path)) {
3404
3675
  spawnSync4("launchctl", ["unload", path], { encoding: "utf8" });
3405
3676
  try {
3406
3677
  rmSync4(path);
@@ -3456,249 +3727,6 @@ async function schedule(sub, rest = []) {
3456
3727
  else removeCmd(rest);
3457
3728
  }
3458
3729
 
3459
- // src/telegram.ts
3460
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync11, existsSync as existsSync9, mkdirSync as mkdirSync9 } from "fs";
3461
- import { join as join10 } from "path";
3462
- var API_BASE = "https://api.telegram.org";
3463
- var CHUNK_SIZE = 4e3;
3464
- function telegramConfigPath() {
3465
- return join10(GLOBAL_DIR, "telegram.json");
3466
- }
3467
- function loadTelegramConfig() {
3468
- const path = telegramConfigPath();
3469
- if (!existsSync9(path)) return null;
3470
- try {
3471
- const raw = JSON.parse(readFileSync9(path, "utf8"));
3472
- if (typeof raw.token !== "string" || typeof raw.allowedChatId !== "number") return null;
3473
- return { token: raw.token, allowedChatId: raw.allowedChatId };
3474
- } catch {
3475
- return null;
3476
- }
3477
- }
3478
- function saveTelegramConfig(cfg) {
3479
- mkdirSync9(GLOBAL_DIR, { recursive: true });
3480
- writeFileSync11(telegramConfigPath(), JSON.stringify(cfg, null, 2) + "\n", {
3481
- encoding: "utf8",
3482
- mode: 384
3483
- });
3484
- }
3485
- function buildSendRequest(token, chatId, text) {
3486
- return {
3487
- url: `${API_BASE}/bot${token}/sendMessage`,
3488
- body: JSON.stringify({ chat_id: chatId, text })
3489
- };
3490
- }
3491
- function buildGetUpdatesUrl(token, offset, timeoutSec) {
3492
- const params = new URLSearchParams({
3493
- offset: String(offset),
3494
- timeout: String(timeoutSec)
3495
- });
3496
- return `${API_BASE}/bot${token}/getUpdates?${params.toString()}`;
3497
- }
3498
- function chunk(text, size = CHUNK_SIZE) {
3499
- if (text.length === 0) return [""];
3500
- const out = [];
3501
- for (let i = 0; i < text.length; i += size) out.push(text.slice(i, i + size));
3502
- return out;
3503
- }
3504
- async function sendMessage(text, chatId) {
3505
- const cfg = loadTelegramConfig();
3506
- if (!cfg) {
3507
- process.stderr.write('[telegram] not configured; run "holt telegram setup"\n');
3508
- return false;
3509
- }
3510
- const target = chatId ?? cfg.allowedChatId;
3511
- const pieces = chunk(text || "No output.");
3512
- let allOk = true;
3513
- for (const piece of pieces) {
3514
- const { url, body } = buildSendRequest(cfg.token, target, piece);
3515
- try {
3516
- const res = await fetch(url, {
3517
- method: "POST",
3518
- headers: { "content-type": "application/json" },
3519
- body
3520
- });
3521
- if (!res.ok) {
3522
- allOk = false;
3523
- process.stderr.write(`[telegram] sendMessage HTTP ${res.status}
3524
- `);
3525
- }
3526
- } catch (err2) {
3527
- allOk = false;
3528
- process.stderr.write(`[telegram] sendMessage failed: ${err2.message}
3529
- `);
3530
- }
3531
- }
3532
- return allOk;
3533
- }
3534
- async function getUpdates(offset) {
3535
- const cfg = loadTelegramConfig();
3536
- if (!cfg) return [];
3537
- const timeoutSec = 30;
3538
- const url = buildGetUpdatesUrl(cfg.token, offset, timeoutSec);
3539
- const ac = new AbortController();
3540
- const timer = setTimeout(() => ac.abort(), (timeoutSec + 10) * 1e3);
3541
- try {
3542
- const res = await fetch(url, { signal: ac.signal });
3543
- if (!res.ok) {
3544
- process.stderr.write(`[telegram] getUpdates HTTP ${res.status}
3545
- `);
3546
- return [];
3547
- }
3548
- const data = await res.json();
3549
- if (!data || data.ok !== true || !Array.isArray(data.result)) return [];
3550
- return data.result;
3551
- } catch (err2) {
3552
- process.stderr.write(`[telegram] getUpdates failed: ${err2.message}
3553
- `);
3554
- return [];
3555
- } finally {
3556
- clearTimeout(timer);
3557
- }
3558
- }
3559
-
3560
- // src/commands/telegram.ts
3561
- var HELP_TEXT = "Holt bot. Send me anything and I will run it through your selected brain and reply.\nCommands: /help this message.";
3562
- async function telegram(sub, _rest = []) {
3563
- const action = (sub || "").toLowerCase();
3564
- if (action === "setup") {
3565
- await setup();
3566
- return;
3567
- }
3568
- await runBot();
3569
- }
3570
- async function setup() {
3571
- const { ask, close } = createReader();
3572
- try {
3573
- console.log("\n" + c.accent("Connect Holt to Telegram"));
3574
- console.log(c.dim(" 1. Open Telegram and message @BotFather."));
3575
- console.log(c.dim(" 2. Send /newbot, follow the prompts, and copy the bot token it gives you."));
3576
- console.log(c.dim(" A token looks like 123456789:AAF... (keep it secret)."));
3577
- const token = (await ask("\n Paste your bot token: ") ?? "").trim();
3578
- if (!token) {
3579
- console.log(c.red(" No token entered. Cancelled.\n"));
3580
- return;
3581
- }
3582
- console.log("\n" + c.dim(" Now I need your chat id (so I only answer you)."));
3583
- console.log(c.dim(" Option A: open your new bot in Telegram and send it any message,"));
3584
- console.log(c.dim(" then I will auto-detect the chat id."));
3585
- console.log(c.dim(" Option B: paste a numeric chat id yourself."));
3586
- const mode = (await ask("\n Auto-detect from a message you just sent? [Y/n] ") ?? "").trim().toLowerCase();
3587
- let allowedChatId = null;
3588
- if (mode === "" || mode === "y" || mode === "yes") {
3589
- saveTelegramConfig({ token, allowedChatId: 0 });
3590
- console.log(c.dim("\n Checking for a recent message..."));
3591
- const updates = await getUpdates(0);
3592
- allowedChatId = latestChatId(updates);
3593
- if (allowedChatId === null) {
3594
- console.log(c.red(" Did not find a recent message."));
3595
- const manual = (await ask(" Paste your numeric chat id instead: ") ?? "").trim();
3596
- allowedChatId = parseChatId(manual);
3597
- }
3598
- } else {
3599
- const manual = (await ask(" Paste your numeric chat id: ") ?? "").trim();
3600
- allowedChatId = parseChatId(manual);
3601
- }
3602
- if (allowedChatId === null) {
3603
- console.log(c.red('\n No valid chat id. Nothing saved. Run "holt telegram setup" again.\n'));
3604
- return;
3605
- }
3606
- const cfg = { token, allowedChatId };
3607
- saveTelegramConfig(cfg);
3608
- console.log(
3609
- "\n" + c.green(" Saved.") + c.dim(` token ...${token.slice(-4)} chat id ${allowedChatId} (~/.holt/telegram.json, mode 600)`)
3610
- );
3611
- console.log(c.dim(" Start the bot with: holt telegram\n"));
3612
- } finally {
3613
- close();
3614
- }
3615
- }
3616
- function parseChatId(s) {
3617
- if (!/^-?\d+$/.test(s)) return null;
3618
- const n = Number(s);
3619
- return Number.isFinite(n) ? n : null;
3620
- }
3621
- function latestChatId(updates) {
3622
- for (let i = updates.length - 1; i >= 0; i--) {
3623
- const u = updates[i];
3624
- if (u && u.message) return u.message.chat.id;
3625
- }
3626
- return null;
3627
- }
3628
- async function runBot() {
3629
- const cfg = loadTelegramConfig();
3630
- if (!cfg) {
3631
- console.log(c.dim('\n Telegram is not set up. Run "holt telegram setup" first.\n'));
3632
- return;
3633
- }
3634
- if (!isTrusted()) {
3635
- trustDir();
3636
- process.stderr.write(`[telegram] auto-trusted ${workspace()}
3637
- `);
3638
- }
3639
- process.stderr.write("[telegram] bot started; long-polling for messages. Ctrl-C to stop.\n");
3640
- let offset = 0;
3641
- let busy = false;
3642
- while (true) {
3643
- let updates = [];
3644
- try {
3645
- updates = await getUpdates(offset);
3646
- } catch (err2) {
3647
- process.stderr.write(`[telegram] poll error: ${err2.message}
3648
- `);
3649
- await sleep(1e3);
3650
- continue;
3651
- }
3652
- for (const u of updates) {
3653
- if (u && typeof u.update_id === "number") offset = Math.max(offset, u.update_id + 1);
3654
- try {
3655
- await handleUpdate(u, cfg, () => busy, (v) => {
3656
- busy = v;
3657
- });
3658
- } catch (err2) {
3659
- process.stderr.write(`[telegram] update ${u?.update_id} failed: ${err2.message}
3660
- `);
3661
- }
3662
- }
3663
- }
3664
- }
3665
- async function handleUpdate(u, cfg, getBusy, setBusy) {
3666
- const msg = u?.message;
3667
- if (!msg || typeof msg.text !== "string") return;
3668
- if (msg.chat.id !== cfg.allowedChatId) {
3669
- process.stderr.write(`[telegram] ignoring message from chat ${msg.chat.id}
3670
- `);
3671
- return;
3672
- }
3673
- const text = msg.text.trim();
3674
- if (text === "/start" || text === "/help") {
3675
- await sendMessage(HELP_TEXT, cfg.allowedChatId);
3676
- return;
3677
- }
3678
- if (getBusy()) {
3679
- await sendMessage("Still working on the previous message. Try again in a moment.", cfg.allowedChatId);
3680
- return;
3681
- }
3682
- setBusy(true);
3683
- try {
3684
- process.stderr.write(`[telegram] running task (${text.length} chars)
3685
- `);
3686
- const result = await runTask(text);
3687
- await sendMessage(result.text || "No output.", cfg.allowedChatId);
3688
- process.stderr.write(`[telegram] replied (brain: ${result.brainLabel})
3689
- `);
3690
- } catch (err2) {
3691
- process.stderr.write(`[telegram] runTask failed: ${err2.message}
3692
- `);
3693
- await sendMessage("Sorry, that run failed. Check the Holt logs.", cfg.allowedChatId);
3694
- } finally {
3695
- setBusy(false);
3696
- }
3697
- }
3698
- function sleep(ms) {
3699
- return new Promise((r) => setTimeout(r, ms));
3700
- }
3701
-
3702
3730
  // src/commands/notify.ts
3703
3731
  import { readFileSync as readFileSync10 } from "fs";
3704
3732
  async function notify(args) {
@@ -3740,7 +3768,7 @@ function readStdin() {
3740
3768
  }
3741
3769
 
3742
3770
  // src/cli.ts
3743
- var VERSION = "0.8.0";
3771
+ var VERSION = "0.8.2";
3744
3772
  var BANNER = `
3745
3773
  \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
3746
3774
  \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