@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.
- package/dist/cli.js +864 -836
- 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/
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
|
671
|
-
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
}
|
|
731
|
-
|
|
669
|
+
} catch (err2) {
|
|
670
|
+
allOk = false;
|
|
671
|
+
process.stderr.write(`[telegram] sendMessage failed: ${err2.message}
|
|
672
|
+
`);
|
|
732
673
|
}
|
|
733
|
-
saveConfig(cfg);
|
|
734
674
|
}
|
|
735
|
-
|
|
736
|
-
return cfg;
|
|
675
|
+
return allOk;
|
|
737
676
|
}
|
|
738
|
-
async function
|
|
739
|
-
const
|
|
740
|
-
if (!
|
|
741
|
-
|
|
742
|
-
|
|
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/
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
|
1145
|
-
import { join as
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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 =
|
|
1279
|
-
|
|
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
|
|
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 =
|
|
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 ?
|
|
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
|
|
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 ?
|
|
1546
|
-
const stillValid =
|
|
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 =
|
|
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:
|
|
1614
|
-
appendTurn({ id:
|
|
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
|
|
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 (!
|
|
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 =
|
|
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
|
|
1747
|
-
writeFileSync as
|
|
1748
|
-
readFileSync as
|
|
1749
|
-
existsSync as
|
|
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
|
|
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 (
|
|
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(
|
|
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) =>
|
|
1816
|
-
if (withSkill.length === 1) return
|
|
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(" " +
|
|
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 =
|
|
1868
|
-
if (
|
|
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
|
-
|
|
1875
|
-
|
|
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(" " +
|
|
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(
|
|
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 (!
|
|
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(
|
|
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 =
|
|
1927
|
-
if (
|
|
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
|
-
|
|
2259
|
+
mkdirSync6(skillsRoot(scope), { recursive: true });
|
|
1934
2260
|
cpSync(skillDir, dest, { recursive: true });
|
|
1935
|
-
const nestedGit =
|
|
1936
|
-
if (
|
|
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(" " +
|
|
2265
|
+
console.log(c.dim(" " + join8(dest, "SKILL.md") + "\n"));
|
|
1940
2266
|
} finally {
|
|
1941
|
-
if (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 =
|
|
1956
|
-
const globalDir =
|
|
1957
|
-
const target =
|
|
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
|
|
2016
|
-
import { join as
|
|
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) :
|
|
2681
|
-
|
|
2682
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3161
|
-
import { readFileSync as
|
|
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
|
|
3436
|
+
return join10(GLOBAL_DIR, "schedules.json");
|
|
3166
3437
|
}
|
|
3167
3438
|
function logsDir() {
|
|
3168
|
-
return
|
|
3439
|
+
return join10(GLOBAL_DIR, "logs");
|
|
3169
3440
|
}
|
|
3170
3441
|
function logPath(id) {
|
|
3171
|
-
return
|
|
3442
|
+
return join10(logsDir(), `${id}.log`);
|
|
3172
3443
|
}
|
|
3173
3444
|
function plistPath(id) {
|
|
3174
|
-
return
|
|
3445
|
+
return join10(homedir3(), "Library", "LaunchAgents", `com.holt.${id}.plist`);
|
|
3175
3446
|
}
|
|
3176
3447
|
function loadJobs() {
|
|
3177
3448
|
try {
|
|
3178
|
-
const raw = JSON.parse(
|
|
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
|
-
|
|
3186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3317
|
-
|
|
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 (
|
|
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.
|
|
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
|