@holt-os/holt 0.6.0 → 0.7.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 +34 -0
- package/dist/cli.js +318 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -127,6 +127,39 @@ 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.
|
|
@@ -137,6 +170,7 @@ Replies print as markdown. `/output html` (or `markdown`) switches the save form
|
|
|
137
170
|
holt init set up (trust, brains, sign-in, defaults) for this folder
|
|
138
171
|
holt chat start a session that remembers past ones
|
|
139
172
|
holt graph see your memory as an interactive knowledge graph
|
|
173
|
+
holt mcp run an MCP server so other tools use this folder's memory (holt mcp setup for config)
|
|
140
174
|
holt skill manage skills: list | show | create | add | remove
|
|
141
175
|
holt memory inspect memory: holt memory [search <query> | embed | clear]
|
|
142
176
|
holt setting configure brains, API brains, and launch command
|
package/dist/cli.js
CHANGED
|
@@ -231,20 +231,20 @@ function runBrain(brain, prompt, onChunk) {
|
|
|
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
243
|
child.on("error", (e) => resolve3({ ok: false, text: `Could not run "${brain.command}": ${e.message}` }));
|
|
244
244
|
child.on("close", (code) => {
|
|
245
245
|
const text = out.trim();
|
|
246
246
|
if (code === 0 && text) resolve3({ ok: true, text });
|
|
247
|
-
else resolve3({ ok: false, text:
|
|
247
|
+
else resolve3({ ok: false, text: err2.trim() || text || `"${brain.command}" exited with code ${code}` });
|
|
248
248
|
});
|
|
249
249
|
});
|
|
250
250
|
}
|
|
@@ -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(
|
|
@@ -2692,8 +2692,315 @@ 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((resolve3) => {
|
|
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", (chunk) => {
|
|
2828
|
+
buffer += chunk;
|
|
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(() => resolve3()).catch(() => resolve3());
|
|
2843
|
+
});
|
|
2844
|
+
stdin.on("error", (e) => {
|
|
2845
|
+
console.error(`[mcp] stdin error: ${e.message}`);
|
|
2846
|
+
chain.then(() => resolve3()).catch(() => resolve3());
|
|
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
|
+
|
|
2695
3002
|
// src/cli.ts
|
|
2696
|
-
var VERSION = "0.
|
|
3003
|
+
var VERSION = "0.7.0";
|
|
2697
3004
|
var BANNER = `
|
|
2698
3005
|
\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
3006
|
\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
|
|
@@ -2712,6 +3019,7 @@ Commands:
|
|
|
2712
3019
|
memory Inspect memory: holt memory [search <query> | embed | clear]
|
|
2713
3020
|
graph See your memory as an interactive knowledge graph in the browser
|
|
2714
3021
|
skill Manage skills: holt skill [list | show | create | add | remove]
|
|
3022
|
+
mcp Serve this folder's memory to Claude Code, Cursor, Codex (holt mcp setup)
|
|
2715
3023
|
setting Configure brains, API brains, and your launch command (per folder)
|
|
2716
3024
|
login <brain> Sign in to a brain: claude, codex, or gemini
|
|
2717
3025
|
version Print the Holt version
|
|
@@ -2761,6 +3069,9 @@ async function main() {
|
|
|
2761
3069
|
case "graph":
|
|
2762
3070
|
await graph(process.argv.slice(3));
|
|
2763
3071
|
break;
|
|
3072
|
+
case "mcp":
|
|
3073
|
+
await mcp(process.argv[3], process.argv.slice(4));
|
|
3074
|
+
break;
|
|
2764
3075
|
default:
|
|
2765
3076
|
console.log(`
|
|
2766
3077
|
Unknown command: "${cmd}"`);
|