@sksoftofficial/mindroot 1.0.6 → 1.2.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.
@@ -0,0 +1,41 @@
1
+ <!-- mindroot:start -->
2
+ # Agent memory policy (mindroot)
3
+
4
+ Use mindroot MCP tools as persistent memory for durable facts about the user and each project. Keep it compact; it is not a scratchpad.
5
+
6
+ ## Project memory
7
+
8
+ - Resolve the project once with `search_projects` or `list_projects` and reuse its slug.
9
+ - Call `search_memories` when starting a new task or investigating a new topic, then `read_note` the relevant linked notes. Reuse notes already in context; repeat lookups only when more information or refreshed context is needed.
10
+ - Consult memory before exploring source. If relevant notes are missing or contradict reality, inspect source and correct stale notes.
11
+ - Project content tools need a project slug. A project is created automatically on first write.
12
+
13
+ ## Notes vs memories
14
+
15
+ Notes are storage; memories are the retrieval index into them. They work as a pair:
16
+
17
+ - **Memories** (`save_memory`, `delete_memory`) — short, standalone atomic facts ("where email delivery lives", "never migrate during business hours"), optionally linked via `target_path` ("note.md::Heading"). The semantic search layer: `search_memories` ranks hybrid (embeddings + keywords). No listing tool; hits carry the ids for `delete_memory`. When in doubt, save a memory.
18
+ - **Notes** (`save_note`, `update_section`, `read_note`, `delete_note`) — structured markdown docs parsed into heading-addressable sections. `read_note` without args returns full content plus the section list; with `section` ("Audit logging::API") returns only that section's body — prefer section reads when you don't need the whole note. `update_section` takes the same heading paths; run `read_note`/`list_notes` first and copy exact paths. `search_notes` is a literal case-insensitive string search over section text — a fallback for exact keywords/identifiers, not semantic search.
19
+
20
+ After writing or updating a notable note section, also save 1–3 memories pointing at it (`target_path`) so future searches surface it — one per key fact a future agent would search for. If a memory stands alone (no note worth writing), that's fine too. Dedupe by searching first, then deleting stale hits — never stack near-copies.
21
+
22
+ Notes read like compact index cards: one sentence on what the feature is, then the files, entry points, and data flow needed to work on it later — enough to answer "where is this implemented?" without reading source. Keep one feature per note under keyword-rich headings (`## Model`, `## API`, `## Gotchas`, …), plus standard cross-cutting notes: `overview.md`, `conventions.md` (patterns shared across features), `commands.md`, `gotchas.md`.
23
+
24
+ ## What to save
25
+
26
+ Facts that improve future navigation, implementation, debugging, or verification: structure and key files, config values, frameworks/services, build/test/deploy commands, conventions, recurring bugs and gotchas.
27
+
28
+ Never save: task history, reasoning trails, rejected alternatives, dated recaps, secrets, logs, or speculation.
29
+
30
+ Write bullets as standalone present-tense facts with file anchors (`models/AuditLog.jsx` defines model `AuditLog` in collection `auditlog`). Not "we decided X today".
31
+
32
+ After work that changes a durable fact, update the smallest relevant note section — plus linked memories for its key facts — automatically before your final response, no confirmation needed. Notes stay compact: merge overlapping bullets, drop stale ones.
33
+
34
+ ## Project skills
35
+
36
+ Skills store reusable procedures for recurring work in this project: notes and memories hold facts, skills hold how to do a task.
37
+
38
+ - Call `search_skills` once before a non-trivial multi-step task; skip simple edits, straightforward questions, and work already clear from context. Reuse skills already loaded; if nothing fits, proceed normally.
39
+ - After finishing a verified multi-step procedure that is not already captured, save or improve its skill automatically before responding — the reusable procedure, not a task recap, an issue-specific fix, or an unverified approach.
40
+ - Structure content as **When to use**, **Prerequisites**, **Procedure**, **Success criteria**, and **Pitfalls**; reference notes or source instead of duplicating facts, and correct skills that no longer match project behavior.
41
+ <!-- mindroot:end -->
package/README.md CHANGED
@@ -4,7 +4,7 @@ AI agents that never forget.
4
4
 
5
5
  ![Mindroot persistent AI agent memory system](https://skbilisim.com/assets/projects/mindroot.webp)
6
6
 
7
- Mindroot is a tiny Node.js service that gives your AI agents a permanent, project-scoped memory: hybrid-searched **memories** over human-editable markdown **notes**, exposed through a local HTTP service with an MCP interface, a REST API, and a built-in dashboard. Everything runs on your machine — offline embeddings, SQLite, plain markdown files you own. No cloud, no API keys, no per-query cost.
7
+ Mindroot is a tiny Node.js service that gives your AI agents project-scoped hybrid-searched **memories** over human-editable markdown **notes**, and reusable **skills** for recurring work. It exposes a local HTTP service with an MCP interface, a REST API, and a built-in dashboard. Everything runs on your machine — offline embeddings, SQLite, plain markdown files you own. No cloud, no API keys, no per-query cost.
8
8
 
9
9
  - Website: https://skbilisim.com/en/projects/mindroot
10
10
  - npm: https://www.npmjs.com/package/@sksoftofficial/mindroot
@@ -14,15 +14,17 @@ Mindroot is a tiny Node.js service that gives your AI agents a permanent, projec
14
14
  - **Notes** (Layer 2): human-editable markdown files under `~/.mindroot/notes/<project>/`, parsed into heading-addressable sections such as `Overview::API`. The unit of storage.
15
15
  - **Memories** (Layer 1): short, retrieval-optimized texts that stand alone or point into a note or section via `target_path`. The unit of retrieval.
16
16
  - **Search**: memories are the semantic index — hybrid ranking of cosine similarity (0.75) over local embeddings plus BM25 keyword search (0.25) through SQLite FTS5. Note sections use literal case-insensitive string matching. All content search is strictly project-scoped; project discovery uses its own fuzzy slug search with slug embeddings and token overlap.
17
+ - **Skills**: project-specific Markdown procedures stored in SQLite with stable names and revision history. Skill names and descriptions have their own hybrid search index; agents load the full instructions only when a match is relevant. Saving a skill indexes it automatically, without creating memories or embedding the full procedure.
17
18
 
18
19
  ## Features
19
20
 
20
21
  - **Two-layer memory** — markdown notes for depth, short memories for recall, linked via heading paths.
21
22
  - **Local embeddings** — 768-dimensional q8 embeddings from `onnx-community/embeddinggemma-300m-ONNX` via transformers.js, computed fully offline and cached under `~/.mindroot/models`.
22
23
  - **Hybrid search** — semantic + BM25 ranking over memories, literal string matching over note sections, fuzzy project-name matching across projects.
23
- - **MCP server** — JSON-RPC endpoint at `/mcp` with 12 project/memory/note tools for agents.
24
+ - **Project skills** — agents discover and apply saved procedures, then automatically capture or improve verified procedures through the skills policy below.
25
+ - **MCP server** — JSON-RPC endpoint at `/mcp` with 16 project/memory/note/skill tools for agents.
24
26
  - **REST API** — the same operations over `POST/GET/PUT/DELETE /api/*` for scripts and integrations.
25
- - **Dashboard** — web UI at `/` for browsing notes and memories, editing markdown, and searching.
27
+ - **Dashboard** — web UI at `/` for browsing notes, memories, and skills, editing Markdown, and inspecting skill revisions.
26
28
  - **CLI** — init, start/stop/restart/status, and human-readable browsing commands.
27
29
  - **Background service** — `mindroot start` daemonizes, writes a pidfile, and waits until healthy; `stop` and `restart` manage it.
28
30
  - **Self-healing reads** — note reads verify a stored content hash and silently re-index files edited externally.
@@ -57,6 +59,28 @@ The default address is `127.0.0.1:7620`, configurable in `~/.mindroot/config.jso
57
59
 
58
60
  ## Quick start (MCP)
59
61
 
62
+ For OpenCode, Claude Code, or Codex, use the one-command setup instead of manually running `init` and `start`:
63
+
64
+ ```sh
65
+ mindroot install opencode # ~/.config/opencode
66
+ mindroot install claude # ~/.claude.json + ~/.claude/CLAUDE.md
67
+ mindroot install codex # ~/.codex/config.toml + ~/.codex/AGENTS.md
68
+ ```
69
+
70
+ This initializes Mindroot (including the model cache), starts the background service if needed, and configures the client's global `mindroot` MCP entry with the local service URL and API key. It also installs the bundled `INSTRUCTIONS.md` policy into the client's global instructions file.
71
+
72
+ Rerunning refreshes the MCP entry and replaces the policy between `<!-- mindroot:start -->` and `<!-- mindroot:end -->`, rather than appending duplicates. Other MCP servers, configuration settings, comments, and instructions outside the marked blocks are preserved. Unbalanced policy markers or malformed configuration cause an error rather than an overwrite.
73
+
74
+ Default locations, honoring each client's directory override:
75
+
76
+ - OpenCode: `~/.config/opencode`, honoring `XDG_CONFIG_HOME` and `OPENCODE_CONFIG_DIR`. An existing `opencode.jsonc` takes precedence; otherwise the command updates or creates `opencode.json`.
77
+ - Claude Code: `~/.claude.json` and `~/.claude/CLAUDE.md`, honoring `CLAUDE_CONFIG_DIR` (both files then live inside that directory).
78
+ - Codex: `~/.codex/config.toml` and `~/.codex/AGENTS.md`, honoring `CODEX_HOME`. A non-empty `AGENTS.override.md` takes precedence and is updated instead; an empty override falls back to `AGENTS.md`.
79
+
80
+ Quit and restart the client afterward to load the changes. The command configures the client; it does not launch it.
81
+
82
+ ### Other MCP clients
83
+
60
84
  Point your MCP client at the service:
61
85
 
62
86
  ```json
@@ -81,10 +105,14 @@ Add this policy to your agent's instructions:
81
105
  <!-- mindroot:start -->
82
106
  # Agent memory policy (mindroot)
83
107
 
84
- Use mindroot MCP tools as persistent memory for durable project facts. It's a project index, not a scratchpad.
108
+ Use mindroot MCP tools as persistent memory for durable facts about the user and each project. Keep it compact; it is not a scratchpad.
85
109
 
86
- - EVERY task — not once per session; a single session can contain multiple unrelated tasks, so run this per task: resolve the project slug (`search_projects` with the repo name, or `list_projects`), `search_memories` with keywords from the task, then `read_note` the notes those memory hits link to. Always choose reading the facts from mindroot over exploring the codebase. Exploring source is the second option, ONLY when mindroot has no relevant notes or the notes don't match reality — then source is the truth (and correct the stale note).
87
- - Every content tool needs a project slug. A project is created automatically on first write.
110
+ ## Project memory
111
+
112
+ - Resolve the project once with `search_projects` or `list_projects` and reuse its slug.
113
+ - Call `search_memories` when starting a new task or investigating a new topic, then `read_note` the relevant linked notes. Reuse notes already in context; repeat lookups only when more information or refreshed context is needed.
114
+ - Consult memory before exploring source. If relevant notes are missing or contradict reality, inspect source and correct stale notes.
115
+ - Project content tools need a project slug. A project is created automatically on first write.
88
116
 
89
117
  ## Notes vs memories
90
118
 
@@ -106,9 +134,39 @@ Never save: task history, reasoning trails, rejected alternatives, dated recaps,
106
134
  Write bullets as standalone present-tense facts with file anchors (`models/AuditLog.jsx` defines model `AuditLog` in collection `auditlog`). Not "we decided X today".
107
135
 
108
136
  After work that changes a durable fact, update the smallest relevant note section — plus linked memories for its key facts — automatically before your final response, no confirmation needed. Notes stay compact: merge overlapping bullets, drop stale ones.
137
+
138
+ ## Project skills
139
+
140
+ Skills store reusable procedures for recurring work in this project: notes and memories hold facts, skills hold how to do a task.
141
+
142
+ - Call `search_skills` once before a non-trivial multi-step task; skip simple edits, straightforward questions, and work already clear from context. Reuse skills already loaded; if nothing fits, proceed normally.
143
+ - After finishing a verified multi-step procedure that is not already captured, save or improve its skill automatically before responding — the reusable procedure, not a task recap, an issue-specific fix, or an unverified approach.
144
+ - Structure content as **When to use**, **Prerequisites**, **Procedure**, **Success criteria**, and **Pitfalls**; reference notes or source instead of duplicating facts, and correct skills that no longer match project behavior.
109
145
  <!-- mindroot:end -->
110
146
  ```
111
147
 
148
+ ## Using skills
149
+
150
+ Automatic skill management is driven by the agent policy: the agent searches once before starting non-trivial work and captures verified procedures after finishing it. Mindroot stores and retrieves procedures; it does not watch conversations or execute skills itself. Copy the policy above (also available in `INSTRUCTIONS.md`) into your agent's instructions.
151
+
152
+ For example, an agent can create a skill with `save_skill`:
153
+
154
+ ```json
155
+ {
156
+ "project": "my-project",
157
+ "name": "add-api-route",
158
+ "description": "Use when adding an API route with the project's existing handler and error conventions.",
159
+ "content": "## When to use\nAdding an API route.\n\n## Prerequisites\nRead conventions.md::API and identify the existing router.\n\n## Procedure\n1. Follow a neighboring handler's registration and error handling.\n2. Implement the requested behavior.\n3. Update the API documentation.\n\n## Success criteria\nThe route is registered and satisfies the requested behavior; follow the task's authorized validation requirements.\n\n## Pitfalls\nDo not copy credentials or request-specific data into examples.",
160
+ "expected_revision": 0
161
+ }
162
+ ```
163
+
164
+ When a saved procedure would help with a later API task, call `search_skills` with `{ "project": "my-project", "query": "add an API endpoint" }`, then `read_skill` with a relevant returned name. Search returns only names, descriptions, revisions, timestamps, and scores.
165
+
166
+ To improve a skill, read its latest version and save under the same name with that `expected_revision`. Each changed save preserves a revision; identical saves are a no-op. A stale or concurrent save returns a conflict so the agent can reread and reconcile. Use `read_skill` with `revision` to inspect an older version; to restore it, save that content using the latest revision as `expected_revision`. Deleting a skill removes all its revisions and index entries.
167
+
168
+ Skills are available through MCP, REST, and the dashboard. The dashboard lists saved skills and supports reading, editing, viewing revision history, and deletion. CLI content commands cover notes and memories. After updating an existing installation, restart mindroot to create the skill tables and reconnect your MCP client to refresh its tool list.
169
+
112
170
  ## CLI
113
171
 
114
172
  CLI output is human-readable; agents should use MCP tools.
@@ -116,6 +174,7 @@ CLI output is human-readable; agents should use MCP tools.
116
174
  | Command | Description |
117
175
  |---|---|
118
176
  | `mindroot init` | Initialize store + model cache (idempotent) |
177
+ | `mindroot install <opencode\|claude\|codex>` | Initialize, start, and configure a coding agent globally |
119
178
  | `mindroot start [--foreground]` | Daemonize the service (or run foreground for debugging) |
120
179
  | `mindroot stop` | Stop the running service |
121
180
  | `mindroot restart` | Restart the service (stop + start) |
@@ -129,11 +188,11 @@ CLI output is human-readable; agents should use MCP tools.
129
188
 
130
189
  ## MCP tools
131
190
 
132
- All tools are prefixed `mindroot_`.
191
+ Tool names on the wire are unprefixed; MCP clients may display them with a `mindroot_` prefix.
133
192
 
134
193
  | Tool | Params | Purpose |
135
194
  |---|---|---|
136
- | `list_projects` | — | List projects with note/memory counts |
195
+ | `list_projects` | — | List projects with note/memory/skill counts |
137
196
  | `search_projects` | `query` | Fuzzy-find a project by name (semantic + token overlap) |
138
197
  | `rename_project` | `project*`, `new_slug*` | Rename a project; moves notes and preserves memories and indexes |
139
198
  | `search_memories` | `project*`, `query`, `limit?` | Hybrid search over memories (hits carry ids) |
@@ -145,6 +204,10 @@ All tools are prefixed `mindroot_`.
145
204
  | `save_note` | `project*`, `path`, `content` | Create/overwrite a note |
146
205
  | `update_section` | `project*`, `path`, `heading_path`, `content` | Replace one section body, or append the section when missing |
147
206
  | `delete_note` | `project*`, `path` | Delete a note (clears its linked memories) |
207
+ | `search_skills` | `project*`, `query`, `limit?` | Hybrid search over skill names and descriptions; returns summaries |
208
+ | `read_skill` | `project*`, `name`, `revision?` | Read current instructions or a historical revision |
209
+ | `save_skill` | `project*`, `name`, `description`, `content`, `expected_revision?` | Create/update and index a skill; preserve history; use revision 0 for create-only |
210
+ | `delete_skill` | `project*`, `name` | Delete a skill, its history, and its index |
148
211
 
149
212
  ## REST API
150
213
 
@@ -155,14 +218,16 @@ Same operations as MCP, for scripts and integrations. All `/api/*` routes (and `
155
218
  | `GET` | `/health` | Liveness probe |
156
219
  | `GET` | `/api/projects` | List projects |
157
220
  | `POST` | `/api/projects` | Create project `{ slug }` |
158
- | `GET` | `/api/projects/:slug` | Project detail with docs and memories |
221
+ | `GET` | `/api/projects/:slug` | Project detail with docs, memories, and skill summaries |
159
222
  | `DELETE` | `/api/projects/:slug` | Delete a project |
160
223
  | `GET` | `/api/projects/:slug/docs` | List notes |
161
224
  | `GET/PUT/DELETE` | `/api/projects/:slug/doc?path=` | Read (optionally `&section=`), write, or delete one note |
162
225
  | `POST` | `/api/projects/:slug/sections` | Update one section `{ path, heading_path, content }` |
163
226
  | `GET/POST` | `/api/projects/:slug/memories` | List or add memories `{ text, target_path? }` |
164
227
  | `DELETE` | `/api/projects/:slug/memories/:id` | Delete a memory |
165
- | `POST` | `/api/search` | Search `{ project, query, kind?: "memories"\|"notes", limit? }` |
228
+ | `GET` | `/api/projects/:slug/skills` | List skill summaries |
229
+ | `GET/PUT/DELETE` | `/api/projects/:slug/skills/:name` | Read (`?revision=` optional), save `{ description, content, expected_revision? }`, or delete a skill |
230
+ | `POST` | `/api/search` | Search `{ project, query, kind?: "memories"\|"notes"\|"skills", limit? }`; omit `kind` for memories + notes |
166
231
  | `POST` | `/mcp` | JSON-RPC MCP endpoint |
167
232
 
168
233
  ## Dashboard
@@ -170,8 +235,10 @@ Same operations as MCP, for scripts and integrations. All `/api/*` routes (and `
170
235
  The dashboard at `http://127.0.0.1:7620/` stores its key in localStorage and offers:
171
236
 
172
237
  - **Notes** view with a markdown editor in Write / Split / Read modes and rendered preview.
173
- - **Memories** view for browsing and adding memories with optional note links.
174
- - **Search** view across notes and memories.
238
+ - **Memories** view for browsing saved memories, following note links, and deleting entries.
239
+ - **Skills** view for browsing saved procedures, editing descriptions and Markdown in Write / Split / Read modes, inspecting older revisions, and deleting skills. Saves detect conflicting updates and preserve your draft on failure; historical revisions are read-only.
240
+
241
+ Memory creation and search are available through MCP and REST; the dashboard has no add-memory form or Search tab.
175
242
 
176
243
  ## Configuration
177
244
 
@@ -180,7 +247,7 @@ Everything lives under `~/.mindroot/`:
180
247
  - `config.json` — `port` (default 7620) and `apiKey`
181
248
  - `notes/<project>/` — your markdown notes
182
249
  - `models/` — cached ONNX embedding model
183
- - `mindroot.db` — SQLite index (WAL mode)
250
+ - `mindroot.db` — SQLite index, skill Markdown content, and skill revisions (WAL mode)
184
251
  - `mindroot.pid` / `mindroot.log` — written by `start`/`stop`
185
252
 
186
253
  Set `MINDROOT_DIR` to relocate the whole store.
package/bin/mindroot.js CHANGED
@@ -4,6 +4,7 @@ import { init } from "../src/commands/init.js";
4
4
  import { start, stop, restart, status } from "../src/commands/start.js";
5
5
  import { projects, searchCmd, saveMemoryCmd, memoriesCmd, notesCmd } from "../src/commands/human.js";
6
6
  import { dashboard } from "../src/commands/dashboard.js";
7
+ import { install } from "../src/commands/install.js";
7
8
 
8
9
  const [command, ...rest] = process.argv.slice(2);
9
10
 
@@ -11,6 +12,7 @@ const usage = `mindroot — persistent memory for AI agents
11
12
 
12
13
  Usage:
13
14
  mindroot init Initialize store and download model
15
+ mindroot install <opencode|claude|codex> Initialize, start, and configure a coding agent globally
14
16
  mindroot start [--foreground] Start the service in the background (or foreground)
15
17
  mindroot stop Stop the running service
16
18
  mindroot restart Restart the service (stop + start)
@@ -32,6 +34,8 @@ async function main() {
32
34
  return console.log(usage);
33
35
  case "init":
34
36
  return init();
37
+ case "install":
38
+ return install(rest);
35
39
  case "start":
36
40
  return start(rest);
37
41
  case "stop":
package/package.json CHANGED
@@ -1,13 +1,21 @@
1
1
  {
2
2
  "name": "@sksoftofficial/mindroot",
3
- "version": "1.0.6",
3
+ "version": "1.2.0",
4
4
  "description": "Long-term memory for AI agents: hybrid-searched memories over human-editable markdown notes. 100% local — offline ONNX embeddings, SQLite FTS5, MCP server, REST API, CLI, dashboard. No cloud, no API keys.",
5
- "homepage": "https://skbilisim.com/en/projects/mindroot",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/sksoftofficial/mindroot.git"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/sksoftofficial/mindroot/issues"
11
+ },
12
+ "homepage": "https://github.com/sksoftofficial/mindroot#readme",
6
13
  "type": "module",
7
14
  "bin": {
8
15
  "mindroot": "./bin/mindroot.js"
9
16
  },
10
17
  "files": [
18
+ "INSTRUCTIONS.md",
11
19
  "bin/",
12
20
  "src/",
13
21
  "test/"
@@ -40,7 +48,10 @@
40
48
  "author": "sksoftofficial",
41
49
  "license": "ISC",
42
50
  "dependencies": {
43
- "@huggingface/transformers": "^3.7.5"
51
+ "@decimalturn/toml-patch": "^3.0.5",
52
+ "@huggingface/transformers": "^3.7.5",
53
+ "force-graph": "1.51.4",
54
+ "jsonc-parser": "^3.3.1"
44
55
  },
45
56
  "engines": {
46
57
  "node": ">=22"
@@ -0,0 +1,210 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { parse, modify, applyEdits } from "jsonc-parser";
5
+ import { parse as parseToml, patch as patchToml } from "@decimalturn/toml-patch";
6
+ import { ensureConfig } from "../paths.js";
7
+ import { init } from "./init.js";
8
+ import { start } from "./start.js";
9
+
10
+ function readPolicy() {
11
+ return fs.readFileSync(new URL("../../INSTRUCTIONS.md", import.meta.url), "utf8").trim();
12
+ }
13
+
14
+ function syncPolicy(agents, agentsPath) {
15
+ const policy = readPolicy();
16
+ const markers = [...agents.matchAll(/<!-- mindroot:(start|end) -->/g)];
17
+ if (markers.length % 2 || markers.some((marker, i) => marker[1] !== (i % 2 ? "end" : "start"))) {
18
+ throw new Error(`unbalanced mindroot markers in ${agentsPath}; fix them before rerunning`);
19
+ }
20
+ let replaced = false;
21
+ let updated = agents.replace(/<!-- mindroot:start -->[\s\S]*?<!-- mindroot:end -->/g, () => {
22
+ if (replaced) return "";
23
+ replaced = true;
24
+ return policy;
25
+ });
26
+ if (!replaced) {
27
+ const separator = !agents || agents.endsWith("\n\n") ? "" : agents.endsWith("\n") ? "\n" : "\n\n";
28
+ updated = agents + separator + policy + "\n";
29
+ }
30
+ return updated;
31
+ }
32
+
33
+ function mcpEntry(cfg) {
34
+ return {
35
+ url: `http://127.0.0.1:${cfg.port}/mcp`,
36
+ headers: { Authorization: `Bearer ${cfg.apiKey}` },
37
+ };
38
+ }
39
+
40
+ function isObject(value) {
41
+ return value && typeof value === "object" && !Array.isArray(value);
42
+ }
43
+
44
+ function writeBoth(configPath, originalConfig, updatedConfig, agentsPath, agents, updatedAgents) {
45
+ // Validate both files before writing either.
46
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
47
+ fs.mkdirSync(path.dirname(agentsPath), { recursive: true });
48
+ if (updatedConfig !== originalConfig) fs.writeFileSync(configPath, updatedConfig, { mode: 0o600 });
49
+ if (updatedAgents !== agents) fs.writeFileSync(agentsPath, updatedAgents);
50
+ return { configPath, agentsPath };
51
+ }
52
+
53
+ export function configureOpencode(cfg, configDir) {
54
+ const xdg = process.env.XDG_CONFIG_HOME;
55
+ configDir ??= process.env.OPENCODE_CONFIG_DIR || path.join(
56
+ xdg && path.isAbsolute(xdg) ? xdg : path.join(os.homedir(), ".config"), "opencode",
57
+ );
58
+ const jsoncPath = path.join(configDir, "opencode.jsonc");
59
+ const configPath = fs.existsSync(jsoncPath) ? jsoncPath : path.join(configDir, "opencode.json");
60
+ const agentsPath = path.join(configDir, "AGENTS.md");
61
+ const originalConfig = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "{}\n";
62
+ const agents = fs.existsSync(agentsPath) ? fs.readFileSync(agentsPath, "utf8") : "";
63
+ const errors = [];
64
+ const config = parse(originalConfig, errors, { allowTrailingComma: true });
65
+ if (errors.length || !isObject(config)) {
66
+ throw new Error(`invalid OpenCode configuration: ${configPath}; fix it before rerunning`);
67
+ }
68
+ if (config.mcp !== undefined && !isObject(config.mcp)) {
69
+ throw new Error(`expected an mcp object in ${configPath}; fix it before rerunning`);
70
+ }
71
+
72
+ const indent = originalConfig.match(/\n([ \t]+)\S/)?.[1] ?? " ";
73
+ const options = { formattingOptions: {
74
+ insertSpaces: !indent.includes("\t"), tabSize: indent.length,
75
+ eol: originalConfig.includes("\r\n") ? "\r\n" : "\n",
76
+ } };
77
+ let updatedConfig = originalConfig;
78
+ if (config.$schema === undefined) {
79
+ updatedConfig = applyEdits(updatedConfig, modify(updatedConfig, ["$schema"], "https://opencode.ai/config.json", options));
80
+ }
81
+ updatedConfig = applyEdits(updatedConfig, modify(updatedConfig, ["mcp", "mindroot"], {
82
+ type: "remote",
83
+ ...mcpEntry(cfg),
84
+ enabled: true,
85
+ }, options));
86
+ const updatedAgents = syncPolicy(agents, agentsPath);
87
+
88
+ return writeBoth(configPath, originalConfig, updatedConfig, agentsPath, agents, updatedAgents);
89
+ }
90
+
91
+ export function configureClaude(cfg, configDir) {
92
+ const override = configDir ?? process.env.CLAUDE_CONFIG_DIR;
93
+ const home = os.homedir();
94
+ // Without CLAUDE_CONFIG_DIR, the config file lives outside the instructions directory.
95
+ const configPath = override ? path.join(override, ".claude.json") : path.join(home, ".claude.json");
96
+ const agentsPath = override ? path.join(override, "CLAUDE.md") : path.join(home, ".claude", "CLAUDE.md");
97
+ const originalConfig = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "{}\n";
98
+ const agents = fs.existsSync(agentsPath) ? fs.readFileSync(agentsPath, "utf8") : "";
99
+ let config;
100
+ try {
101
+ config = JSON.parse(originalConfig);
102
+ } catch {
103
+ throw new Error(`invalid Claude configuration: ${configPath}; fix it before rerunning`);
104
+ }
105
+ if (!isObject(config)) {
106
+ throw new Error(`invalid Claude configuration: ${configPath}; fix it before rerunning`);
107
+ }
108
+ if (config.mcpServers !== undefined && !isObject(config.mcpServers)) {
109
+ throw new Error(`expected an mcpServers object in ${configPath}; fix it before rerunning`);
110
+ }
111
+
112
+ const updatedConfig = JSON.stringify({
113
+ ...config,
114
+ mcpServers: {
115
+ ...config.mcpServers,
116
+ mindroot: { type: "http", ...mcpEntry(cfg) },
117
+ },
118
+ }, null, 2) + "\n";
119
+ const updatedAgents = syncPolicy(agents, agentsPath);
120
+
121
+ return writeBoth(configPath, originalConfig, updatedConfig, agentsPath, agents, updatedAgents);
122
+ }
123
+
124
+ export function configureCodex(cfg, configDir) {
125
+ configDir ??= process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
126
+ const configPath = path.join(configDir, "config.toml");
127
+ const basePath = path.join(configDir, "AGENTS.md");
128
+ const overridePath = path.join(configDir, "AGENTS.override.md");
129
+ const originalConfig = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
130
+ let config;
131
+ try {
132
+ config = parseToml(originalConfig);
133
+ } catch {
134
+ throw new Error(`invalid Codex configuration: ${configPath}; fix it before rerunning`);
135
+ }
136
+ if (config.mcp_servers !== undefined && !isObject(config.mcp_servers)) {
137
+ throw new Error(`expected an mcp_servers table in ${configPath}; fix it before rerunning`);
138
+ }
139
+ if (config.mcp_servers?.mindroot !== undefined && !isObject(config.mcp_servers.mindroot)) {
140
+ throw new Error(`expected mcp_servers.mindroot to be a table in ${configPath}; fix it before rerunning`);
141
+ }
142
+
143
+ // A non-empty AGENTS.override.md hides AGENTS.md entirely, so sync it instead.
144
+ let agentsPath = basePath;
145
+ let agents;
146
+ if (fs.existsSync(overridePath)) {
147
+ const override = fs.readFileSync(overridePath, "utf8");
148
+ if (override.trim()) {
149
+ agentsPath = overridePath;
150
+ agents = override;
151
+ }
152
+ }
153
+ agents ??= fs.existsSync(basePath) ? fs.readFileSync(basePath, "utf8") : "";
154
+
155
+ const updatedConfig = patchToml(originalConfig, {
156
+ mcp_servers: {
157
+ ...config.mcp_servers,
158
+ mindroot: {
159
+ url: `http://127.0.0.1:${cfg.port}/mcp`,
160
+ http_headers: { Authorization: `Bearer ${cfg.apiKey}` },
161
+ },
162
+ },
163
+ });
164
+ const updatedAgents = syncPolicy(agents, agentsPath);
165
+
166
+ return writeBoth(configPath, originalConfig, updatedConfig, agentsPath, agents, updatedAgents);
167
+ }
168
+
169
+ const TARGETS = {
170
+ opencode: {
171
+ label: "OpenCode",
172
+ configure: configureOpencode,
173
+ hint: "Quit and restart OpenCode to load the updated configuration and instructions.",
174
+ },
175
+ claude: {
176
+ label: "Claude Code",
177
+ configure: configureClaude,
178
+ hint: "Start a new Claude Code session to load the updated configuration and instructions.",
179
+ },
180
+ codex: {
181
+ label: "Codex",
182
+ configure: configureCodex,
183
+ hint: "Start a new Codex session to load the updated configuration and instructions.",
184
+ },
185
+ };
186
+
187
+ export async function install(argv = []) {
188
+ const target = argv.length === 1 ? TARGETS[argv[0]] : undefined;
189
+ if (!target) {
190
+ throw new Error("usage: mindroot install <opencode|claude|codex>");
191
+ }
192
+ await init();
193
+ await start();
194
+ if (process.exitCode) return;
195
+
196
+ const { cfg } = ensureConfig();
197
+ const response = await fetch(`http://127.0.0.1:${cfg.port}/api/projects`, {
198
+ headers: { Authorization: `Bearer ${cfg.apiKey}` },
199
+ signal: AbortSignal.timeout(5000),
200
+ });
201
+ if (!response.ok) {
202
+ throw new Error("mindroot is not ready with the configured API key; run mindroot restart and retry");
203
+ }
204
+ await response.body?.cancel();
205
+
206
+ const { configPath, agentsPath } = target.configure(cfg);
207
+ console.log(`${target.label} MCP configured: ${configPath}`);
208
+ console.log(`Mindroot instructions synced: ${agentsPath}`);
209
+ console.log(target.hint);
210
+ }
package/src/core.js CHANGED
@@ -16,13 +16,15 @@ import { ONNX_MODEL_ID } from "./paths.js";
16
16
  import { searchMemories as hybridSearchMemories, searchNotes as hybridSearchNotes, searchProjects } from "./search.js";
17
17
 
18
18
  export { searchProjects };
19
+ export { listSkills, readSkill, saveSkill, deleteSkill, searchSkills } from "./skills.js";
19
20
 
20
21
  export async function listProjects(db) {
21
22
  return db
22
23
  .prepare(
23
24
  `SELECT p.slug, p.created_at,
24
25
  (SELECT COUNT(*) FROM docs d WHERE d.project_id = p.id) AS docs,
25
- (SELECT COUNT(*) FROM memories m WHERE m.project_id = p.id) AS memories
26
+ (SELECT COUNT(*) FROM memories m WHERE m.project_id = p.id) AS memories,
27
+ (SELECT COUNT(*) FROM skills s WHERE s.project_id = p.id) AS skills
26
28
  FROM projects p ORDER BY p.slug`,
27
29
  )
28
30
  .all();
@@ -50,7 +52,12 @@ export async function renameProject(db, from, to) {
50
52
  if (await fs.stat(targetDir).then(() => true, () => false)) {
51
53
  throw httpError(409, `directory already exists: ${to}`);
52
54
  }
53
- await fs.rename(projectDir(from), targetDir);
55
+ // A project containing only skills or memories may have no notes directory.
56
+ const hasNotes = await fs.stat(projectDir(from)).then(() => true, (err) => {
57
+ if (err.code === "ENOENT") return false;
58
+ throw err;
59
+ });
60
+ if (hasNotes) await fs.rename(projectDir(from), targetDir);
54
61
  db.prepare("UPDATE projects SET slug = ? WHERE id = ?").run(to, project.id);
55
62
  db.prepare("DELETE FROM project_embeddings WHERE project_id = ?").run(project.id);
56
63
  return { renamed: { from, to } };