@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.
- package/README.md +81 -4
- package/dist/cli.js +1090 -23
- 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. **
|
|
166
|
-
6. **
|
|
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((
|
|
39
|
+
const ask = (q) => new Promise((resolve4) => {
|
|
40
40
|
if (q) process.stdout.write(q);
|
|
41
|
-
if (buffer.length)
|
|
42
|
-
else if (closed)
|
|
43
|
-
else waiters.push(
|
|
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((
|
|
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
|
-
|
|
230
|
+
resolve4({ ok: false, text: `Could not launch "${brain.command}": ${e.message}` });
|
|
231
231
|
return;
|
|
232
232
|
}
|
|
233
233
|
let out = "";
|
|
234
|
-
let
|
|
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
|
-
|
|
241
|
+
err2 += d.toString();
|
|
242
242
|
});
|
|
243
|
-
child.on("error", (e) =>
|
|
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)
|
|
247
|
-
else
|
|
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((
|
|
396
|
+
return new Promise((resolve4) => {
|
|
397
397
|
let child;
|
|
398
398
|
try {
|
|
399
399
|
child = spawn2(cmd, args, { stdio: "inherit" });
|
|
400
400
|
} catch {
|
|
401
|
-
|
|
401
|
+
resolve4(-1);
|
|
402
402
|
return;
|
|
403
403
|
}
|
|
404
|
-
child.on("error", () =>
|
|
405
|
-
child.on("close", (code) =>
|
|
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
|
|
769
|
-
console.log(` ${
|
|
770
|
-
if (
|
|
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
|
|
992
|
-
carry += decoder.decode(
|
|
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 = (
|
|
1600
|
+
const onChunk = (chunk2) => {
|
|
1601
1601
|
streamed = true;
|
|
1602
|
-
process.stdout.write(
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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.
|
|
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}"`);
|