@holt-os/holt 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +81 -4
  2. package/dist/cli.js +1090 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -127,18 +127,95 @@ holt skill remove <name> delete a skill
127
127
 
128
128
  In chat, run one with `/skill <name> [your input]`. Available skills are also listed to the brain each turn, so it knows what it can be asked to follow.
129
129
 
130
+ ## Use Holt from your other tools (MCP)
131
+
132
+ Holt can run as an [MCP](https://modelcontextprotocol.io) server, so Claude Code, Cursor, and Codex can recall and remember from Holt's memory for the folder without leaving those tools.
133
+
134
+ ```bash
135
+ holt mcp # run the MCP server for the current folder (talks JSON-RPC over stdio)
136
+ holt mcp setup # print the config snippets for each client
137
+ ```
138
+
139
+ The server serves the memory of whatever folder it is launched in. Because MCP is non-interactive (stdin carries the protocol), it cannot prompt for trust, so it **auto-trusts the launch folder** and adds it to `~/.holt/trust.json`. Startup logging goes to stderr; stdout is reserved for the protocol.
140
+
141
+ Tools it exposes:
142
+
143
+ - `recall` (`query`, optional `k`): search this folder's memory for relevant moments and facts.
144
+ - `remember` (`content`): save a durable fact to this folder's memory.
145
+ - `list_skills`: list the skills installed for this folder.
146
+ - `get_skill` (`name`): get a skill's full instructions.
147
+ - `memory_stats`: memory statistics for this folder.
148
+
149
+ **Claude Code**
150
+
151
+ ```bash
152
+ claude mcp add holt -- holt mcp
153
+ ```
154
+
155
+ **Cursor / Codex** (JSON config):
156
+
157
+ ```json
158
+ { "mcpServers": { "holt": { "command": "holt", "args": ["mcp"] } } }
159
+ ```
160
+
161
+ Point the client at the folder whose memory you want it to use; Holt serves whatever folder the server process starts in.
162
+
130
163
  ## Output format
131
164
 
132
165
  Replies print as markdown. `/output html` (or `markdown`) switches the save format and persists it. `/save [name]` writes the last reply to the current folder: `.md`, or a small self-contained dark-theme `.html` page.
133
166
 
167
+ ## Run tasks, schedule them, reach them from your phone
168
+
169
+ Beyond interactive chat, Holt can run a task once, on a timer, or from Telegram. The selected brain does the task; your memory is recalled into it automatically.
170
+
171
+ ### holt run
172
+
173
+ Run a single task non-interactively. It recalls relevant memory, injects your skills, runs the brain once, streams the reply, and saves the exchange to this folder's memory.
174
+
175
+ ```bash
176
+ holt run "summarize the open items in this folder"
177
+ ```
178
+
179
+ Options: `--brain <id>` pick a brain, `--out <file>` also write the reply to a file, `--no-store` skip saving, `--no-recall` skip memory, `--quiet` suppress streaming. The folder must be trusted first (`holt init` or `holt chat` once); in a non-interactive context an untrusted folder exits non-zero instead of prompting.
180
+
181
+ ### holt schedule
182
+
183
+ Run a task automatically on a timer, using your OS scheduler (launchd on macOS, cron on Linux). A scheduled run behaves exactly like typing `holt run` in that folder.
184
+
185
+ ```bash
186
+ holt schedule add "summarize my inbox and flag what is urgent" 07:00 --notify
187
+ holt schedule list
188
+ holt schedule remove <id>
189
+ ```
190
+
191
+ `<HH:MM>` is a 24h daily time. `--notify` pushes the output to your Telegram. Jobs live in `~/.holt/schedules.json`; each run logs to `~/.holt/logs/<id>.log`.
192
+
193
+ ### Telegram: chat with Holt from your phone
194
+
195
+ Holt can run as a Telegram bot. Messages you send are run through your brain (with memory) and the reply comes back to your phone. Single user: only your chat id is served.
196
+
197
+ ```bash
198
+ holt telegram setup # message @BotFather for a token, paste it, auto-detect your chat id
199
+ holt telegram # run the bot (keep it running under a service manager)
200
+ holt notify "backup done" # push a one-off message
201
+ holt run "daily brief" | holt notify # pipe any output to your phone
202
+ ```
203
+
204
+ The token is stored in `~/.holt/telegram.json` (mode 600) and never printed in full.
205
+
134
206
  ## Commands
135
207
 
136
208
  ```
137
209
  holt init set up (trust, brains, sign-in, defaults) for this folder
138
210
  holt chat start a session that remembers past ones
211
+ holt run <task> run one task non-interactively (recall, brain, remember)
212
+ holt schedule run a task on a timer: add | list | remove
213
+ holt telegram chat with Holt from your phone: telegram [setup]
214
+ holt notify [msg] push a message to your phone over Telegram (stdin-friendly)
139
215
  holt graph see your memory as an interactive knowledge graph
216
+ holt mcp run an MCP server so other tools use this folder's memory (holt mcp setup)
140
217
  holt skill manage skills: list | show | create | add | remove
141
- holt memory inspect memory: holt memory [search <query> | embed | clear]
218
+ holt memory inspect memory: holt memory [search <query> | facts | embed | clear]
142
219
  holt setting configure brains, API brains, and launch command
143
220
  holt login <brain> sign in to claude, codex, or gemini
144
221
  holt version print version
@@ -147,7 +224,7 @@ holt help show help
147
224
 
148
225
  ## Configuration
149
226
 
150
- `holt init` writes `<folder>/.holt/config.json` (default brain and enabled brains for that folder). Trusted folders live in `~/.holt/trust.json`. Edit settings with `holt setting`.
227
+ `holt init` writes `<folder>/.holt/config.json` (default brain and enabled brains for that folder). Trusted folders live in `~/.holt/trust.json`. Edit settings with `holt setting`. Scheduled jobs live in `~/.holt/schedules.json`, and the Telegram token in `~/.holt/telegram.json` (mode 600).
151
228
 
152
229
  ## Architecture
153
230
 
@@ -162,8 +239,8 @@ Built in always-shippable phases toward a full-vision v1:
162
239
  2. **Any LLM directly**: API brains with your own key, HTML or Markdown output *(shipped)*
163
240
  3. **Skills**: portable SKILL.md skills, create/add/run *(shipped)*
164
241
  4. **Knowledge graph**: see and navigate your own memory with `holt graph` *(shipped)*
165
- 5. **Orchestration**: a local model works, a cloud model reviews the risky steps
166
- 6. **Channels and polish**: Telegram, docs site, skill registry publishing
242
+ 5. **Everywhere**: MCP server so other tools read Holt's memory, plus `holt run`, scheduling, and Telegram *(shipped)*
243
+ 6. **Next**: docs site, skill registry publishing, more channels
167
244
 
168
245
  ## Contributing
169
246
 
package/dist/cli.js CHANGED
@@ -36,11 +36,11 @@ function createReader() {
36
36
  closed = true;
37
37
  while (waiters.length) waiters.shift()(null);
38
38
  });
39
- const ask = (q) => new Promise((resolve3) => {
39
+ const ask = (q) => new Promise((resolve4) => {
40
40
  if (q) process.stdout.write(q);
41
- if (buffer.length) resolve3(buffer.shift());
42
- else if (closed) resolve3(null);
43
- else waiters.push(resolve3);
41
+ if (buffer.length) resolve4(buffer.shift());
42
+ else if (closed) resolve4(null);
43
+ else waiters.push(resolve4);
44
44
  });
45
45
  return { ask, close: () => rl.close() };
46
46
  }
@@ -222,29 +222,29 @@ function renderPrompt(history, message, memory = []) {
222
222
  return parts.join("\n");
223
223
  }
224
224
  function runBrain(brain, prompt, onChunk) {
225
- return new Promise((resolve3) => {
225
+ return new Promise((resolve4) => {
226
226
  let child;
227
227
  try {
228
228
  child = spawn(brain.command, [...brain.args, prompt], { stdio: ["ignore", "pipe", "pipe"] });
229
229
  } catch (e) {
230
- resolve3({ ok: false, text: `Could not launch "${brain.command}": ${e.message}` });
230
+ resolve4({ ok: false, text: `Could not launch "${brain.command}": ${e.message}` });
231
231
  return;
232
232
  }
233
233
  let out = "";
234
- let err = "";
234
+ let err2 = "";
235
235
  child.stdout.on("data", (d) => {
236
236
  const s = d.toString();
237
237
  out += s;
238
238
  if (onChunk) onChunk(s);
239
239
  });
240
240
  child.stderr.on("data", (d) => {
241
- err += d.toString();
241
+ err2 += d.toString();
242
242
  });
243
- child.on("error", (e) => resolve3({ ok: false, text: `Could not run "${brain.command}": ${e.message}` }));
243
+ child.on("error", (e) => resolve4({ ok: false, text: `Could not run "${brain.command}": ${e.message}` }));
244
244
  child.on("close", (code) => {
245
245
  const text = out.trim();
246
- if (code === 0 && text) resolve3({ ok: true, text });
247
- else resolve3({ ok: false, text: err.trim() || text || `"${brain.command}" exited with code ${code}` });
246
+ if (code === 0 && text) resolve4({ ok: true, text });
247
+ else resolve4({ ok: false, text: err2.trim() || text || `"${brain.command}" exited with code ${code}` });
248
248
  });
249
249
  });
250
250
  }
@@ -393,16 +393,16 @@ function currentAlias() {
393
393
  // src/install.ts
394
394
  import { spawn as spawn2 } from "child_process";
395
395
  function runInteractive(cmd, args) {
396
- return new Promise((resolve3) => {
396
+ return new Promise((resolve4) => {
397
397
  let child;
398
398
  try {
399
399
  child = spawn2(cmd, args, { stdio: "inherit" });
400
400
  } catch {
401
- resolve3(-1);
401
+ resolve4(-1);
402
402
  return;
403
403
  }
404
- child.on("error", () => resolve3(-1));
405
- child.on("close", (code) => resolve3(code ?? -1));
404
+ child.on("error", () => resolve4(-1));
405
+ child.on("close", (code) => resolve4(code ?? -1));
406
406
  });
407
407
  }
408
408
 
@@ -765,9 +765,9 @@ async function init() {
765
765
  console.log("Looking for agent CLIs on your machine...\n");
766
766
  const found = [];
767
767
  for (const id of BRAIN_IDS) {
768
- const ok = isInstalled(BRAIN_DEFS[id].command);
769
- console.log(` ${ok ? c.green("found ") : c.dim("missing")} ${BRAIN_DEFS[id].label} (${BRAIN_DEFS[id].command})`);
770
- if (ok) found.push(id);
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
771
  }
772
772
  console.log("");
773
773
  const chosen = parseBrains(
@@ -988,8 +988,8 @@ async function runApiBrain(brain, prompt, onChunk) {
988
988
  let carry = "";
989
989
  let text = "";
990
990
  try {
991
- for await (const chunk of res.body) {
992
- carry += decoder.decode(chunk, { stream: true });
991
+ for await (const chunk2 of res.body) {
992
+ carry += decoder.decode(chunk2, { stream: true });
993
993
  const { events, rest } = parseSSELines(carry);
994
994
  carry = rest;
995
995
  for (const ev of events) {
@@ -1597,9 +1597,9 @@ async function chat() {
1597
1597
  const base = renderPrompt(history, line, remembered);
1598
1598
  const prompt = promptOverride ?? (skillsBlock ? skillsBlock + "\n\n" + base : base);
1599
1599
  let streamed = false;
1600
- const onChunk = (chunk) => {
1600
+ const onChunk = (chunk2) => {
1601
1601
  streamed = true;
1602
- process.stdout.write(chunk);
1602
+ process.stdout.write(chunk2);
1603
1603
  };
1604
1604
  const res = active.kind === "cli" ? await runBrain(cfg.brains[active.id], prompt, onChunk) : await runApiBrain(active.brain, prompt, onChunk);
1605
1605
  if (res.ok) {
@@ -2692,8 +2692,1055 @@ async function graph(args = []) {
2692
2692
  }
2693
2693
  }
2694
2694
 
2695
+ // src/mcp/server.ts
2696
+ var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
2697
+ function isObject(v) {
2698
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2699
+ }
2700
+ function ok(id, result) {
2701
+ return { jsonrpc: "2.0", id: id ?? null, result };
2702
+ }
2703
+ function err(id, code, message) {
2704
+ return { jsonrpc: "2.0", id: id ?? null, error: { code, message } };
2705
+ }
2706
+ async function handleMcpMessage(msg, opts) {
2707
+ if (!isObject(msg)) {
2708
+ console.error("[mcp] ignoring non-object message");
2709
+ return null;
2710
+ }
2711
+ const message = msg;
2712
+ const method = typeof message.method === "string" ? message.method : void 0;
2713
+ const hasId = "id" in message && message.id !== void 0 && message.id !== null;
2714
+ const id = message.id;
2715
+ if (method === void 0) {
2716
+ if (hasId) return err(id, -32600, "Invalid Request: missing method");
2717
+ console.error("[mcp] ignoring message with no method");
2718
+ return null;
2719
+ }
2720
+ if (!hasId) {
2721
+ if (!method.startsWith("notifications/")) {
2722
+ console.error(`[mcp] ignoring notification-shaped message: ${method}`);
2723
+ }
2724
+ return null;
2725
+ }
2726
+ const params = isObject(message.params) ? message.params : {};
2727
+ try {
2728
+ switch (method) {
2729
+ case "initialize": {
2730
+ const requested = params["protocolVersion"];
2731
+ const protocolVersion = typeof requested === "string" ? requested : DEFAULT_PROTOCOL_VERSION;
2732
+ return ok(id, {
2733
+ protocolVersion,
2734
+ capabilities: { tools: {} },
2735
+ serverInfo: { name: opts.name, version: opts.version }
2736
+ });
2737
+ }
2738
+ case "ping":
2739
+ return ok(id, {});
2740
+ case "tools/list":
2741
+ return ok(id, {
2742
+ tools: opts.tools.map((t) => ({
2743
+ name: t.name,
2744
+ description: t.description,
2745
+ inputSchema: t.inputSchema
2746
+ }))
2747
+ });
2748
+ case "tools/call": {
2749
+ const name = typeof params["name"] === "string" ? params["name"] : "";
2750
+ const rawArgs = params["arguments"];
2751
+ const args = isObject(rawArgs) ? rawArgs : {};
2752
+ const tool = opts.tools.find((t) => t.name === name);
2753
+ if (!tool) {
2754
+ return ok(id, {
2755
+ content: [{ type: "text", text: "Unknown tool: " + name }],
2756
+ isError: true
2757
+ });
2758
+ }
2759
+ try {
2760
+ const text = await tool.handler(args);
2761
+ return ok(id, { content: [{ type: "text", text }] });
2762
+ } catch (e) {
2763
+ const emsg = e instanceof Error ? e.message : String(e);
2764
+ return ok(id, {
2765
+ content: [{ type: "text", text: "Tool error: " + emsg }],
2766
+ isError: true
2767
+ });
2768
+ }
2769
+ }
2770
+ default:
2771
+ return err(id, -32601, "Method not found: " + method);
2772
+ }
2773
+ } catch (e) {
2774
+ const emsg = e instanceof Error ? e.message : String(e);
2775
+ console.error(`[mcp] internal error handling ${method}: ${emsg}`);
2776
+ return err(id, -32603, "Internal error: " + emsg);
2777
+ }
2778
+ }
2779
+ function recoverId(line) {
2780
+ const num = line.match(/"id"\s*:\s*(-?\d+(?:\.\d+)?)/);
2781
+ if (num && num[1] !== void 0) {
2782
+ const n = Number(num[1]);
2783
+ if (Number.isFinite(n)) return n;
2784
+ }
2785
+ const str = line.match(/"id"\s*:\s*"((?:[^"\\]|\\.)*)"/);
2786
+ if (str && str[1] !== void 0) return str[1];
2787
+ return null;
2788
+ }
2789
+ function runMcpServer(opts) {
2790
+ return new Promise((resolve4) => {
2791
+ const stdin = process.stdin;
2792
+ let buffer = "";
2793
+ let chain = Promise.resolve();
2794
+ const send = (response) => {
2795
+ process.stdout.write(JSON.stringify(response) + "\n");
2796
+ };
2797
+ const processLine = (rawLine) => {
2798
+ const line = rawLine.trim();
2799
+ if (line === "") return;
2800
+ let parsed;
2801
+ try {
2802
+ parsed = JSON.parse(line);
2803
+ } catch {
2804
+ const id = recoverId(line);
2805
+ if (id !== null) {
2806
+ send(err(id, -32700, "Parse error"));
2807
+ } else {
2808
+ console.error("[mcp] parse error on line, no id to recover, skipping");
2809
+ }
2810
+ return;
2811
+ }
2812
+ if (Array.isArray(parsed)) {
2813
+ chain = chain.then(async () => {
2814
+ for (const entry of parsed) {
2815
+ const response = await handleMcpMessage(entry, opts);
2816
+ if (response !== null) send(response);
2817
+ }
2818
+ });
2819
+ return;
2820
+ }
2821
+ chain = chain.then(async () => {
2822
+ const response = await handleMcpMessage(parsed, opts);
2823
+ if (response !== null) send(response);
2824
+ });
2825
+ };
2826
+ stdin.setEncoding("utf8");
2827
+ stdin.on("data", (chunk2) => {
2828
+ buffer += chunk2;
2829
+ let nl = buffer.indexOf("\n");
2830
+ while (nl !== -1) {
2831
+ const line = buffer.slice(0, nl);
2832
+ buffer = buffer.slice(nl + 1);
2833
+ processLine(line);
2834
+ nl = buffer.indexOf("\n");
2835
+ }
2836
+ });
2837
+ stdin.on("end", () => {
2838
+ if (buffer.length > 0) {
2839
+ processLine(buffer);
2840
+ buffer = "";
2841
+ }
2842
+ chain.then(() => resolve4()).catch(() => resolve4());
2843
+ });
2844
+ stdin.on("error", (e) => {
2845
+ console.error(`[mcp] stdin error: ${e.message}`);
2846
+ chain.then(() => resolve4()).catch(() => resolve4());
2847
+ });
2848
+ stdin.resume();
2849
+ });
2850
+ }
2851
+
2852
+ // src/mcp/tools.ts
2853
+ var MCP_SESSION = "__mcp__";
2854
+ function holtTools() {
2855
+ return [
2856
+ {
2857
+ name: "recall",
2858
+ description: "Search Holt's memory for this folder and return the most relevant remembered moments and distilled facts.",
2859
+ inputSchema: {
2860
+ type: "object",
2861
+ properties: {
2862
+ query: { type: "string", description: "What to recall." },
2863
+ k: { type: "number", description: "Max results to return (default 5)." }
2864
+ },
2865
+ required: ["query"]
2866
+ },
2867
+ handler: async (args) => {
2868
+ try {
2869
+ const query = String(args.query || "").trim();
2870
+ if (!query) return "Provide a query to recall.";
2871
+ const k = Number(args.k) || 5;
2872
+ const hits = await recall(query, MCP_SESSION, k);
2873
+ if (hits.length === 0) return "No relevant memory found.";
2874
+ const lines = hits.map(
2875
+ (h) => `${h.score.toFixed(2)} (${h.turn.role}) ${h.turn.content.replace(/\s+/g, " ").trim()}`
2876
+ );
2877
+ return [`Recalled ${hits.length} relevant memory item(s):`, ...lines].join("\n");
2878
+ } catch (err2) {
2879
+ return `Could not recall right now: ${err2 instanceof Error ? err2.message : String(err2)}`;
2880
+ }
2881
+ }
2882
+ },
2883
+ {
2884
+ name: "remember",
2885
+ description: "Save a durable fact to Holt's memory for this folder.",
2886
+ inputSchema: {
2887
+ type: "object",
2888
+ properties: {
2889
+ content: { type: "string", description: "The fact to remember." }
2890
+ },
2891
+ required: ["content"]
2892
+ },
2893
+ handler: async (args) => {
2894
+ try {
2895
+ const content = String(args.content || "").trim();
2896
+ if (!content) return "Nothing to remember: content was empty.";
2897
+ const ok2 = await saveFact(content, MCP_SESSION);
2898
+ return ok2 ? "Remembered." : "Already known (duplicate).";
2899
+ } catch (err2) {
2900
+ return `Could not remember right now: ${err2 instanceof Error ? err2.message : String(err2)}`;
2901
+ }
2902
+ }
2903
+ },
2904
+ {
2905
+ name: "list_skills",
2906
+ description: "List the skills installed for this folder (workspace and global).",
2907
+ inputSchema: { type: "object", properties: {} },
2908
+ handler: async () => {
2909
+ try {
2910
+ const skills = listSkills();
2911
+ if (skills.length === 0) return "No skills installed.";
2912
+ const lines = skills.map((s) => `${s.name}: ${s.description || "(no description)"}`);
2913
+ return ["Installed skills:", ...lines].join("\n");
2914
+ } catch (err2) {
2915
+ return `Could not list skills right now: ${err2 instanceof Error ? err2.message : String(err2)}`;
2916
+ }
2917
+ }
2918
+ },
2919
+ {
2920
+ name: "get_skill",
2921
+ description: "Get the full instructions (body) of a skill by name.",
2922
+ inputSchema: {
2923
+ type: "object",
2924
+ properties: {
2925
+ name: { type: "string", description: "The skill name." }
2926
+ },
2927
+ required: ["name"]
2928
+ },
2929
+ handler: async (args) => {
2930
+ try {
2931
+ const name = String(args.name || "").trim();
2932
+ if (!name) return "Provide a skill name.";
2933
+ const loaded = loadSkill(name);
2934
+ if (!loaded) return `No skill named "${name}".`;
2935
+ return [`Skill: ${loaded.skill.name}`, "", loaded.body].join("\n");
2936
+ } catch (err2) {
2937
+ return `Could not load that skill right now: ${err2 instanceof Error ? err2.message : String(err2)}`;
2938
+ }
2939
+ }
2940
+ },
2941
+ {
2942
+ name: "memory_stats",
2943
+ description: "Show statistics for Holt's memory in this folder.",
2944
+ inputSchema: { type: "object", properties: {} },
2945
+ handler: async () => {
2946
+ try {
2947
+ const s = memStats();
2948
+ return [
2949
+ "Holt memory (this folder):",
2950
+ `moments ${s.turns}`,
2951
+ `facts ${s.facts}`,
2952
+ `sessions ${s.sessions}`,
2953
+ `embedded ${s.withEmbeddings} of ${s.turns}`,
2954
+ `size ${(s.bytes / 1024).toFixed(1)} KB`
2955
+ ].join("\n");
2956
+ } catch (err2) {
2957
+ return `Could not read memory stats right now: ${err2 instanceof Error ? err2.message : String(err2)}`;
2958
+ }
2959
+ }
2960
+ }
2961
+ ];
2962
+ }
2963
+
2964
+ // src/commands/mcp.ts
2965
+ function printSetup() {
2966
+ const config = JSON.stringify(
2967
+ { mcpServers: { holt: { command: "holt", args: ["mcp"] } } },
2968
+ null,
2969
+ 2
2970
+ );
2971
+ const out = [
2972
+ "Add Holt as an MCP server to your other tools.",
2973
+ "",
2974
+ "The server serves the memory of whatever folder it is launched in, and",
2975
+ "auto-trusts that folder on first run.",
2976
+ "",
2977
+ "Claude Code:",
2978
+ " claude mcp add holt -- holt mcp",
2979
+ "",
2980
+ "Cursor / Codex (JSON config):",
2981
+ config,
2982
+ ""
2983
+ ].join("\n");
2984
+ console.log(out);
2985
+ }
2986
+ async function mcp(sub, rest = []) {
2987
+ void rest;
2988
+ if (sub === "setup") {
2989
+ printSetup();
2990
+ return;
2991
+ }
2992
+ 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: "" };
3056
+ }
3057
+ }
3058
+
3059
+ // src/commands/run.ts
3060
+ var USAGE = [
3061
+ " holt run <task> [options]",
3062
+ "",
3063
+ " Run a single task against your default brain and stream the reply.",
3064
+ "",
3065
+ " options:",
3066
+ " --brain <id> use a specific brain (CLI or API brain id)",
3067
+ " --out <file> also write the reply to a file (relative to this folder)",
3068
+ " --no-store do not save this exchange to memory",
3069
+ " --no-recall do not pull memory into the prompt",
3070
+ " --quiet suppress streaming; still writes --out and sets exit code",
3071
+ "",
3072
+ ' example: holt run "summarize the open items in this folder"'
3073
+ ].join("\n");
3074
+ function parseArgs2(args) {
3075
+ const words2 = [];
3076
+ let brainId;
3077
+ let out;
3078
+ let noStore = false;
3079
+ let noRecall = false;
3080
+ let quiet = false;
3081
+ for (let i = 0; i < args.length; i++) {
3082
+ const a = args[i];
3083
+ if (a === "--brain") {
3084
+ brainId = args[++i];
3085
+ continue;
3086
+ }
3087
+ if (a === "--out") {
3088
+ out = args[++i];
3089
+ continue;
3090
+ }
3091
+ if (a === "--no-store") {
3092
+ noStore = true;
3093
+ continue;
3094
+ }
3095
+ if (a === "--no-recall") {
3096
+ noRecall = true;
3097
+ continue;
3098
+ }
3099
+ if (a === "--quiet") {
3100
+ quiet = true;
3101
+ continue;
3102
+ }
3103
+ words2.push(a);
3104
+ }
3105
+ return { task: words2.join(" ").trim(), brainId, out, noStore, noRecall, quiet };
3106
+ }
3107
+ async function run(args) {
3108
+ const p = parseArgs2(args);
3109
+ if (!p.task) {
3110
+ console.log(USAGE);
3111
+ return;
3112
+ }
3113
+ if (!isTrusted()) {
3114
+ if (process.stdin.isTTY) {
3115
+ const { ask, close } = createReader();
3116
+ const ok2 = await ensureTrusted(ask);
3117
+ close();
3118
+ if (!ok2) return;
3119
+ } else {
3120
+ process.stderr.write("This folder is not trusted. Run holt init or holt chat here once, then schedule/run.\n");
3121
+ process.exitCode = 1;
3122
+ return;
3123
+ }
3124
+ }
3125
+ let streamed = false;
3126
+ const result = await runTask(p.task, {
3127
+ brainId: p.brainId,
3128
+ recall: !p.noRecall,
3129
+ store: !p.noStore,
3130
+ onChunk: p.quiet ? void 0 : (ch) => {
3131
+ streamed = true;
3132
+ process.stdout.write(ch);
3133
+ }
3134
+ });
3135
+ if (!p.quiet && !streamed) {
3136
+ process.stdout.write(result.text);
3137
+ }
3138
+ if (!p.quiet && (streamed || result.text) && !result.text.endsWith("\n")) {
3139
+ process.stdout.write("\n");
3140
+ }
3141
+ if (!result.ok) {
3142
+ process.stderr.write(c.red(result.text) + "\n");
3143
+ process.exitCode = 1;
3144
+ return;
3145
+ }
3146
+ if (p.out) {
3147
+ const target = resolve3(process.cwd(), p.out);
3148
+ writeFileSync8(target, result.text, "utf8");
3149
+ process.stderr.write(c.dim(`saved to ${target}`) + "\n");
3150
+ }
3151
+ }
3152
+
3153
+ // src/commands/schedule.ts
3154
+ import { spawnSync as spawnSync4 } from "child_process";
3155
+ import { writeFileSync as writeFileSync10, mkdirSync as mkdirSync8, rmSync as rmSync4, existsSync as existsSync8 } from "fs";
3156
+ import { dirname as dirname3 } from "path";
3157
+
3158
+ // src/scheduler.ts
3159
+ import { homedir as homedir3 } from "os";
3160
+ import { join as join9 } from "path";
3161
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync9, mkdirSync as mkdirSync7 } from "fs";
3162
+ import { randomUUID as randomUUID4 } from "crypto";
3163
+ import { spawnSync as spawnSync3 } from "child_process";
3164
+ function schedulesPath() {
3165
+ return join9(GLOBAL_DIR, "schedules.json");
3166
+ }
3167
+ function logsDir() {
3168
+ return join9(GLOBAL_DIR, "logs");
3169
+ }
3170
+ function logPath(id) {
3171
+ return join9(logsDir(), `${id}.log`);
3172
+ }
3173
+ function plistPath(id) {
3174
+ return join9(homedir3(), "Library", "LaunchAgents", `com.holt.${id}.plist`);
3175
+ }
3176
+ function loadJobs() {
3177
+ try {
3178
+ const raw = JSON.parse(readFileSync8(schedulesPath(), "utf8"));
3179
+ return Array.isArray(raw) ? raw : [];
3180
+ } catch {
3181
+ return [];
3182
+ }
3183
+ }
3184
+ function saveJobs(jobs) {
3185
+ mkdirSync7(GLOBAL_DIR, { recursive: true });
3186
+ writeFileSync9(schedulesPath(), JSON.stringify(jobs, null, 2) + "\n", "utf8");
3187
+ }
3188
+ function addJob(job) {
3189
+ const jobs = loadJobs();
3190
+ jobs.push(job);
3191
+ saveJobs(jobs);
3192
+ return jobs;
3193
+ }
3194
+ function removeJob(id) {
3195
+ const jobs = loadJobs().filter((j) => j.id !== id);
3196
+ saveJobs(jobs);
3197
+ return jobs;
3198
+ }
3199
+ function newJobId() {
3200
+ return randomUUID4().slice(0, 8);
3201
+ }
3202
+ function parseWhen(when) {
3203
+ const m = /^([01]?\d|2[0-3]):([0-5]\d)$/.exec(when.trim());
3204
+ if (!m) {
3205
+ throw new Error(
3206
+ `Could not read time "${when}". Use 24h HH:MM for a daily run, e.g. 07:00 or 23:30.`
3207
+ );
3208
+ }
3209
+ const hour = Number(m[1]);
3210
+ const minute = Number(m[2]);
3211
+ return { hour, minute };
3212
+ }
3213
+ function shQuote(s) {
3214
+ return `'` + s.replace(/'/g, `'\\''`) + `'`;
3215
+ }
3216
+ function resolveHoltPath() {
3217
+ try {
3218
+ const finder = process.platform === "win32" ? "where" : "which";
3219
+ const res = spawnSync3(finder, ["holt"], { encoding: "utf8" });
3220
+ if (res.status === 0 && typeof res.stdout === "string") {
3221
+ const first = res.stdout.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
3222
+ if (first) return first;
3223
+ }
3224
+ } catch {
3225
+ }
3226
+ return "holt";
3227
+ }
3228
+ function buildCommand(job, holtPath) {
3229
+ const holt = shQuote(holtPath);
3230
+ const ws = shQuote(job.workspace);
3231
+ const log = shQuote(logPath(job.id));
3232
+ let cmd = `cd ${ws} && ${holt} run ${shQuote(job.task)}`;
3233
+ if (job.brain) cmd += ` --brain ${shQuote(job.brain)}`;
3234
+ cmd += ` --quiet --out ${log}`;
3235
+ if (job.notify) cmd += ` && ${holt} notify "$(cat ${log})"`;
3236
+ return cmd;
3237
+ }
3238
+ function xmlEscape(s) {
3239
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
3240
+ }
3241
+ function buildLaunchdPlist(job, holtPath) {
3242
+ const { hour, minute } = parseWhen(job.when);
3243
+ const command = buildCommand(job, holtPath);
3244
+ const log = logPath(job.id);
3245
+ const args = ["/bin/sh", "-lc", command];
3246
+ const argXml = args.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
3247
+ return `<?xml version="1.0" encoding="UTF-8"?>
3248
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3249
+ <plist version="1.0">
3250
+ <dict>
3251
+ <key>Label</key>
3252
+ <string>com.holt.${xmlEscape(job.id)}</string>
3253
+ <key>ProgramArguments</key>
3254
+ <array>
3255
+ ${argXml}
3256
+ </array>
3257
+ <key>StartCalendarInterval</key>
3258
+ <dict>
3259
+ <key>Hour</key>
3260
+ <integer>${hour}</integer>
3261
+ <key>Minute</key>
3262
+ <integer>${minute}</integer>
3263
+ </dict>
3264
+ <key>RunAtLoad</key>
3265
+ <false/>
3266
+ <key>StandardOutPath</key>
3267
+ <string>${xmlEscape(log)}</string>
3268
+ <key>StandardErrorPath</key>
3269
+ <string>${xmlEscape(log)}</string>
3270
+ </dict>
3271
+ </plist>
3272
+ `;
3273
+ }
3274
+ var CRON_MARKER = "holt:";
3275
+ function buildCronLine(job, holtPath) {
3276
+ const { hour, minute } = parseWhen(job.when);
3277
+ const command = buildCommand(job, holtPath);
3278
+ return `${minute} ${hour} * * * ${command} # ${CRON_MARKER}${job.id}`;
3279
+ }
3280
+ function stripCronLines(crontab, id) {
3281
+ const marker = `# ${CRON_MARKER}${id}`;
3282
+ const kept = crontab.split("\n").filter((line) => !line.includes(marker));
3283
+ return kept.join("\n").replace(/\n+$/, "\n");
3284
+ }
3285
+ function appendCronLine(crontab, line) {
3286
+ const base = crontab.replace(/\s*$/, "");
3287
+ return (base ? base + "\n" : "") + line + "\n";
3288
+ }
3289
+ function ensureLogsDir() {
3290
+ mkdirSync7(logsDir(), { recursive: true });
3291
+ }
3292
+
3293
+ // src/commands/schedule.ts
3294
+ function parseAdd(rest) {
3295
+ const positional = [];
3296
+ const opts = { notify: false };
3297
+ for (let i = 0; i < rest.length; i++) {
3298
+ const a = rest[i];
3299
+ if (a === "--notify") opts.notify = true;
3300
+ else if (a === "--name") opts.name = rest[++i];
3301
+ else if (a === "--brain") opts.brain = rest[++i];
3302
+ else if (a && a.startsWith("--")) {
3303
+ const eq = a.indexOf("=");
3304
+ if (eq > 2) {
3305
+ const k = a.slice(2, eq);
3306
+ const v = a.slice(eq + 1);
3307
+ if (k === "name") opts.name = v;
3308
+ else if (k === "brain") opts.brain = v;
3309
+ }
3310
+ } else if (a !== void 0) positional.push(a);
3311
+ }
3312
+ return { task: positional[0], when: positional[1], opts };
3313
+ }
3314
+ function installDarwin(job, holtPath) {
3315
+ const path = plistPath(job.id);
3316
+ mkdirSync8(dirname3(path), { recursive: true });
3317
+ writeFileSync10(path, buildLaunchdPlist(job, holtPath), "utf8");
3318
+ const res = spawnSync4("launchctl", ["load", "-w", path], { encoding: "utf8" });
3319
+ if (res.status === 0) {
3320
+ console.log(c.green(" Loaded into launchd."));
3321
+ } else {
3322
+ console.log(c.dim(" Wrote the plist, but launchctl load did not confirm."));
3323
+ console.log(c.dim(` Load it yourself with: launchctl load -w ${path}`));
3324
+ if (res.stderr && res.stderr.trim()) console.log(c.dim(` (${res.stderr.trim()})`));
3325
+ }
3326
+ console.log(c.dim(` plist: ${path}`));
3327
+ }
3328
+ function currentCrontab() {
3329
+ const res = spawnSync4("crontab", ["-l"], { encoding: "utf8" });
3330
+ return res.status === 0 && typeof res.stdout === "string" ? res.stdout : "";
3331
+ }
3332
+ function writeCrontab(body) {
3333
+ const res = spawnSync4("crontab", ["-"], { input: body, encoding: "utf8" });
3334
+ return res.status === 0;
3335
+ }
3336
+ function installLinux(job, holtPath) {
3337
+ const line = buildCronLine(job, holtPath);
3338
+ const next = appendCronLine(currentCrontab(), line);
3339
+ if (writeCrontab(next)) {
3340
+ console.log(c.green(" Added to your crontab."));
3341
+ console.log(c.dim(` line: ${line}`));
3342
+ } else {
3343
+ console.log(c.dim(" Could not update the crontab automatically. Add this line yourself:"));
3344
+ console.log(" " + line);
3345
+ }
3346
+ }
3347
+ async function addCmd(rest) {
3348
+ const { task, when, opts } = parseAdd(rest);
3349
+ if (!task || !when) {
3350
+ console.log(c.dim('\n Usage: holt schedule add "<task>" <HH:MM> [--name <n>] [--notify] [--brain <id>]\n'));
3351
+ return;
3352
+ }
3353
+ try {
3354
+ parseWhen(when);
3355
+ } catch (e) {
3356
+ console.log("\n " + c.red(e.message) + "\n");
3357
+ return;
3358
+ }
3359
+ const ws = workspace();
3360
+ const holtPath = resolveHoltPath();
3361
+ const job = {
3362
+ id: newJobId(),
3363
+ name: opts.name || task.slice(0, 40),
3364
+ task,
3365
+ when,
3366
+ workspace: ws,
3367
+ notify: opts.notify,
3368
+ ...opts.brain ? { brain: opts.brain } : {}
3369
+ };
3370
+ ensureLogsDir();
3371
+ addJob(job);
3372
+ console.log("\n" + c.accent("Scheduled."));
3373
+ console.log(` id ${job.id}`);
3374
+ console.log(` when daily at ${job.when}`);
3375
+ console.log(` workspace ${job.workspace}`);
3376
+ if (job.notify) console.log(` notify on (pushes output to Telegram)`);
3377
+ console.log(` log ${logPath(job.id)}`);
3378
+ if (process.platform === "darwin") installDarwin(job, holtPath);
3379
+ else if (process.platform === "linux") installLinux(job, holtPath);
3380
+ else {
3381
+ console.log(c.dim("\n Your OS has no built-in installer. Install this entry yourself:"));
3382
+ console.log(" " + buildCronLine(job, holtPath));
3383
+ }
3384
+ console.log("");
3385
+ }
3386
+ function listCmd() {
3387
+ const jobs = loadJobs();
3388
+ if (jobs.length === 0) {
3389
+ console.log(c.dim("\n No scheduled tasks yet. Add one with:"));
3390
+ console.log(c.dim(' holt schedule add "<task>" <HH:MM> [--notify]\n'));
3391
+ return;
3392
+ }
3393
+ console.log("\n" + c.accent("Scheduled tasks"));
3394
+ for (const j of jobs) {
3395
+ console.log(` ${c.bold(j.id)} daily ${j.when} ${c.dim(j.notify ? "(notify)" : "")}`);
3396
+ console.log(` ${j.name}`);
3397
+ console.log(c.dim(` ${j.workspace}`));
3398
+ }
3399
+ console.log("");
3400
+ }
3401
+ function removeDarwin(id) {
3402
+ const path = plistPath(id);
3403
+ if (existsSync8(path)) {
3404
+ spawnSync4("launchctl", ["unload", path], { encoding: "utf8" });
3405
+ try {
3406
+ rmSync4(path);
3407
+ } catch {
3408
+ }
3409
+ }
3410
+ }
3411
+ function removeLinux(id) {
3412
+ const res = spawnSync4("crontab", ["-l"], { encoding: "utf8" });
3413
+ const body = res.status === 0 && typeof res.stdout === "string" ? res.stdout : "";
3414
+ const next = stripCronLines(body, id);
3415
+ spawnSync4("crontab", ["-"], { input: next, encoding: "utf8" });
3416
+ }
3417
+ function removeCmd(rest) {
3418
+ const id = rest[0];
3419
+ if (!id) {
3420
+ console.log(c.dim("\n Usage: holt schedule remove <id> (see ids with: holt schedule list)\n"));
3421
+ return;
3422
+ }
3423
+ const before = loadJobs();
3424
+ const existed = before.some((j) => j.id === id);
3425
+ removeJob(id);
3426
+ if (process.platform === "darwin") removeDarwin(id);
3427
+ else if (process.platform === "linux") removeLinux(id);
3428
+ if (existed) console.log("\n" + c.green(` Removed schedule ${id}.`) + "\n");
3429
+ else console.log(c.dim(`
3430
+ No schedule with id ${id}. Nothing to remove.
3431
+ `));
3432
+ }
3433
+ function usage2() {
3434
+ console.log("\n" + c.accent("holt schedule") + c.dim(" run a task automatically on a timer"));
3435
+ console.log(c.dim('\n holt schedule add "<task>" <HH:MM> [--name <n>] [--notify] [--brain <id>]'));
3436
+ console.log(c.dim(" holt schedule list"));
3437
+ console.log(c.dim(" holt schedule remove <id>\n"));
3438
+ }
3439
+ async function schedule(sub, rest = []) {
3440
+ const action = (sub || "").toLowerCase();
3441
+ if (action === "list") {
3442
+ listCmd();
3443
+ return;
3444
+ }
3445
+ if (action !== "add" && action !== "remove" && action !== "rm") {
3446
+ usage2();
3447
+ return;
3448
+ }
3449
+ const { ask, close } = createReader();
3450
+ if (!await ensureTrusted(ask)) {
3451
+ close();
3452
+ return;
3453
+ }
3454
+ close();
3455
+ if (action === "add") await addCmd(rest);
3456
+ else removeCmd(rest);
3457
+ }
3458
+
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
+ // src/commands/notify.ts
3703
+ import { readFileSync as readFileSync10 } from "fs";
3704
+ async function notify(args) {
3705
+ const nonFlag = args.filter((a) => a !== "-" && !a.startsWith("--"));
3706
+ const wantsStdin = args.includes("-") || !process.stdin.isTTY;
3707
+ let message = "";
3708
+ if (nonFlag.length > 0 && !args.includes("-")) {
3709
+ message = nonFlag.join(" ").trim();
3710
+ } else if (wantsStdin) {
3711
+ message = readStdin().trim();
3712
+ } else if (nonFlag.length > 0) {
3713
+ message = nonFlag.join(" ").trim();
3714
+ }
3715
+ if (!message) {
3716
+ process.stderr.write('Nothing to send. Pass a message or pipe text into "holt notify".\n');
3717
+ process.exitCode = 1;
3718
+ return;
3719
+ }
3720
+ const cfg = loadTelegramConfig();
3721
+ if (!cfg) {
3722
+ process.stderr.write('Telegram is not set up. Run "holt telegram setup".\n');
3723
+ process.exitCode = 1;
3724
+ return;
3725
+ }
3726
+ const ok2 = await sendMessage(message, cfg.allowedChatId);
3727
+ if (ok2) {
3728
+ process.stderr.write("Sent to Telegram.\n");
3729
+ } else {
3730
+ process.stderr.write("Failed to send to Telegram. Check the network and your bot token.\n");
3731
+ process.exitCode = 1;
3732
+ }
3733
+ }
3734
+ function readStdin() {
3735
+ try {
3736
+ return readFileSync10(0, "utf8");
3737
+ } catch {
3738
+ return "";
3739
+ }
3740
+ }
3741
+
2695
3742
  // src/cli.ts
2696
- var VERSION = "0.6.0";
3743
+ var VERSION = "0.8.0";
2697
3744
  var BANNER = `
2698
3745
  \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
2699
3746
  \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
@@ -2709,9 +3756,14 @@ Usage: holt <command>
2709
3756
  Commands:
2710
3757
  init Trust this folder, choose and install brains, sign in, set defaults
2711
3758
  chat Start a session. It remembers past sessions in this folder
3759
+ run <task> Run one task non-interactively: recall, brain executes, remember
3760
+ schedule Fire "holt run" on a timer: holt schedule [add | list | remove]
3761
+ telegram Chat with Holt from your phone: holt telegram [setup]
3762
+ notify [msg] Push a message to your phone over Telegram (stdin-friendly)
2712
3763
  memory Inspect memory: holt memory [search <query> | embed | clear]
2713
3764
  graph See your memory as an interactive knowledge graph in the browser
2714
3765
  skill Manage skills: holt skill [list | show | create | add | remove]
3766
+ mcp Serve this folder's memory to Claude Code, Cursor, Codex (holt mcp setup)
2715
3767
  setting Configure brains, API brains, and your launch command (per folder)
2716
3768
  login <brain> Sign in to a brain: claude, codex, or gemini
2717
3769
  version Print the Holt version
@@ -2761,6 +3813,21 @@ async function main() {
2761
3813
  case "graph":
2762
3814
  await graph(process.argv.slice(3));
2763
3815
  break;
3816
+ case "mcp":
3817
+ await mcp(process.argv[3], process.argv.slice(4));
3818
+ break;
3819
+ case "run":
3820
+ await run(process.argv.slice(3));
3821
+ break;
3822
+ case "schedule":
3823
+ await schedule(process.argv[3], process.argv.slice(4));
3824
+ break;
3825
+ case "telegram":
3826
+ await telegram(process.argv[3], process.argv.slice(4));
3827
+ break;
3828
+ case "notify":
3829
+ await notify(process.argv.slice(3));
3830
+ break;
2764
3831
  default:
2765
3832
  console.log(`
2766
3833
  Unknown command: "${cmd}"`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holt-os/holt",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "An open-source personal agent OS: any LLM, private memory you can see and walk.",
5
5
  "type": "module",
6
6
  "license": "MIT",