@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.
Files changed (3) hide show
  1. package/README.md +34 -0
  2. package/dist/cli.js +318 -7
  3. 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 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
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: err.trim() || text || `"${brain.command}" exited with code ${code}` });
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 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(
@@ -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.6.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}"`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holt-os/holt",
3
- "version": "0.6.0",
3
+ "version": "0.7.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",