@sksoftofficial/mindroot 1.0.4 → 1.0.6

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 CHANGED
@@ -143,7 +143,7 @@ All tools are prefixed `mindroot_`.
143
143
  | `list_notes` | `project*` | List notes with section paths |
144
144
  | `read_note` | `project*`, `path`, `section?` | Full note content, or one section's body via `section` (hash-verified, auto-reindexed) |
145
145
  | `save_note` | `project*`, `path`, `content` | Create/overwrite a note |
146
- | `update_section` | `project*`, `path`, `heading_path`, `content` | Replace one section body, leave the rest untouched |
146
+ | `update_section` | `project*`, `path`, `heading_path`, `content` | Replace one section body, or append the section when missing |
147
147
  | `delete_note` | `project*`, `path` | Delete a note (clears its linked memories) |
148
148
 
149
149
  ## REST API
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sksoftofficial/mindroot",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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
5
  "homepage": "https://skbilisim.com/en/projects/mindroot",
6
6
  "type": "module",
package/src/core.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  reindexDoc,
11
11
  httpError,
12
12
  } from "./indexer.js";
13
- import { parseMarkdown, replaceSectionBody } from "./markdown.js";
13
+ import { appendSection, parseMarkdown, replaceSectionBody } from "./markdown.js";
14
14
  import { embedDoc } from "./embedder.js";
15
15
  import { ONNX_MODEL_ID } from "./paths.js";
16
16
  import { searchMemories as hybridSearchMemories, searchNotes as hybridSearchNotes, searchProjects } from "./search.js";
@@ -137,8 +137,9 @@ export async function updateSection(db, slug, relPath, headingPath, newContent)
137
137
  const { content } = await verifyAndLoadDoc(db, slug, relPath);
138
138
  const parsed = parseMarkdown(content);
139
139
  const sec = parsed.sections.find((s) => s.path === headingPath);
140
- if (!sec) throw httpError(404, `section not found: ${headingPath}`);
141
- const updated = replaceSectionBody(content, sec, newContent);
140
+ const updated = sec
141
+ ? replaceSectionBody(content, sec, newContent)
142
+ : appendSection(content, headingPath, newContent);
142
143
  return saveNote(db, slug, relPath, updated);
143
144
  }
144
145
 
package/src/markdown.js CHANGED
@@ -70,3 +70,9 @@ export function replaceSectionBody(raw, sec, newBody) {
70
70
  const replacement = newBody.replace(/\n+$/, "").split("\n");
71
71
  return [...lines.slice(0, sec.bodyStart), ...replacement, ...lines.slice(sec.bodyEnd)].join("\n");
72
72
  }
73
+
74
+ export function appendSection(raw, headingPath, body) {
75
+ const note = raw.replace(/\n+$/, "");
76
+ const content = body.replace(/\n+$/, "");
77
+ return `${note ? `${note}\n\n` : ""}## ${headingPath}${content ? `\n\n${content}` : ""}\n`;
78
+ }
package/src/mcp.js CHANGED
@@ -4,12 +4,12 @@ const PROTOCOL_VERSION = "2025-06-18";
4
4
  export function tools() {
5
5
  return [
6
6
  {
7
- name: "mindroot_list_projects",
7
+ name: "list_projects",
8
8
  description: "List all projects known to mindroot with their note and memory counts. Use to discover valid project slugs before calling project-scoped tools.",
9
9
  inputSchema: { type: "object", properties: {} },
10
10
  },
11
11
  {
12
- name: "mindroot_search_projects",
12
+ name: "search_projects",
13
13
  description:
14
14
  "Find projects by fuzzy name. Matches project slugs semantically and by token overlap (e.g. 'mem-agent-mcp' finds 'proper-agent-memory'). Use to discover the right project slug before calling project-scoped tools.",
15
15
  inputSchema: {
@@ -19,7 +19,7 @@ export function tools() {
19
19
  },
20
20
  },
21
21
  {
22
- name: "mindroot_rename_project",
22
+ name: "rename_project",
23
23
  description:
24
24
  "Rename a project by slug. Moves its notes directory and updates the slug; notes, memories, and search indexes are preserved under the internal project id. The fuzzy-search vector for the new slug is rebuilt automatically on the next project search.",
25
25
  inputSchema: {
@@ -32,7 +32,7 @@ export function tools() {
32
32
  },
33
33
  },
34
34
  {
35
- name: "mindroot_search_memories",
35
+ name: "search_memories",
36
36
  description:
37
37
  "Search a project's memories — short, retrieval-optimized notes about durable facts, conventions, decisions, and gotchas. This is mindroot's primary semantic search layer (hybrid vector + keyword). Use before exploring a codebase or asking the user things that may already be remembered; mindroot_search_notes is only a literal string fallback. Hits include ids usable with mindroot_delete_memory.",
38
38
  inputSchema: {
@@ -46,7 +46,7 @@ export function tools() {
46
46
  },
47
47
  },
48
48
  {
49
- name: "mindroot_search_notes",
49
+ name: "search_notes",
50
50
  description:
51
51
  "Literal case-insensitive string search over a project's note sections — no semantics. Memories are the semantic index: prefer mindroot_search_memories first and use this only as a fallback or to locate exact keywords/identifiers inside notes. Returns the note path and heading section per hit; fetch just that section with mindroot_read_note's section argument.",
52
52
  inputSchema: {
@@ -60,7 +60,7 @@ export function tools() {
60
60
  },
61
61
  },
62
62
  {
63
- name: "mindroot_save_memory",
63
+ name: "save_memory",
64
64
  description:
65
65
  "Save a memory — a short, retrieval-optimized text about a durable fact, convention, decision, or gotcha for a project. Optionally link it to a note path ('notes/foo.md') or section ('notes/foo.md::Heading::Subheading'). Write memories so they answer 'when would someone need this'.",
66
66
  inputSchema: {
@@ -74,7 +74,7 @@ export function tools() {
74
74
  },
75
75
  },
76
76
  {
77
- name: "mindroot_delete_memory",
77
+ name: "delete_memory",
78
78
  description: "Delete a memory by id. Use to dedupe or remove stale entries. Ids come from mindroot_search_memories hits.",
79
79
  inputSchema: {
80
80
  type: "object",
@@ -83,7 +83,7 @@ export function tools() {
83
83
  },
84
84
  },
85
85
  {
86
- name: "mindroot_list_notes",
86
+ name: "list_notes",
87
87
  description:
88
88
  "List all memory notes for a project with their titles and section heading paths. Use this to see a note's section layout, then read only the section you need via mindroot_read_note's section argument instead of fetching the whole note.",
89
89
  inputSchema: {
@@ -93,7 +93,7 @@ export function tools() {
93
93
  },
94
94
  },
95
95
  {
96
- name: "mindroot_read_note",
96
+ name: "read_note",
97
97
  description:
98
98
  "Read a memory note. Without `section`, returns full content plus the section list. With `section` (a heading path like 'Architecture::Storage' from mindroot_list_notes or mindroot_search_notes targets), returns only that section's body — prefer this when you don't need the whole note. Verifies file integrity first and re-indexes automatically if the file was edited externally.",
99
99
  inputSchema: {
@@ -107,7 +107,7 @@ export function tools() {
107
107
  },
108
108
  },
109
109
  {
110
- name: "mindroot_save_note",
110
+ name: "save_note",
111
111
  description:
112
112
  "Create or fully overwrite a markdown memory note. Sections are parsed from headings (# .. ######) and are individually readable (mindroot_read_note `section`) and string-searchable (mindroot_search_notes); embeddings are generated for memories only. After saving, consider saving 1-3 linked memories (mindroot_save_memory with target_path) so key facts surface in memory searches.",
113
113
  inputSchema: {
@@ -121,9 +121,9 @@ export function tools() {
121
121
  },
122
122
  },
123
123
  {
124
- name: "mindroot_update_section",
124
+ name: "update_section",
125
125
  description:
126
- "Replace the body of one heading-section inside an existing note, leaving the rest of the file untouched.",
126
+ "Replace the body of an existing heading-section, or append it to the note when missing.",
127
127
  inputSchema: {
128
128
  type: "object",
129
129
  properties: {
@@ -136,7 +136,7 @@ export function tools() {
136
136
  },
137
137
  },
138
138
  {
139
- name: "mindroot_delete_note",
139
+ name: "delete_note",
140
140
  description: "Delete a memory note and its sections index entries.",
141
141
  inputSchema: {
142
142
  type: "object",
@@ -150,29 +150,29 @@ export function tools() {
150
150
  async function callTool(db, name, args) {
151
151
  const core = await import("./core.js");
152
152
  switch (name) {
153
- case "mindroot_list_projects":
153
+ case "list_projects":
154
154
  return core.listProjects(db);
155
- case "mindroot_search_projects":
155
+ case "search_projects":
156
156
  return core.searchProjects(db, args.query);
157
- case "mindroot_rename_project":
157
+ case "rename_project":
158
158
  return core.renameProject(db, args.project, args.new_slug);
159
- case "mindroot_search_memories":
159
+ case "search_memories":
160
160
  return core.searchMemories(db, args.project, args.query, args.limit ?? 8);
161
- case "mindroot_search_notes":
161
+ case "search_notes":
162
162
  return core.searchNotes(db, args.project, args.query, args.limit ?? 8);
163
- case "mindroot_save_memory":
163
+ case "save_memory":
164
164
  return core.addMemory(db, args.project, args.text, args.target_path);
165
- case "mindroot_delete_memory":
165
+ case "delete_memory":
166
166
  return core.deleteMemory(db, args.project, Number(args.id));
167
- case "mindroot_list_notes":
167
+ case "list_notes":
168
168
  return core.listDocs(db, args.project);
169
- case "mindroot_read_note":
169
+ case "read_note":
170
170
  return core.readNote(db, args.project, args.path, args.section);
171
- case "mindroot_save_note":
171
+ case "save_note":
172
172
  return core.saveNote(db, args.project, args.path, args.content);
173
- case "mindroot_update_section":
173
+ case "update_section":
174
174
  return core.updateSection(db, args.project, args.path, args.heading_path, args.content);
175
- case "mindroot_delete_note":
175
+ case "delete_note":
176
176
  return core.deleteNote(db, args.project, args.path);
177
177
  default:
178
178
  throw new Error(`unknown tool: ${name}`);
@@ -1,6 +1,6 @@
1
1
  import { test } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { parseMarkdown, docTitle, replaceSectionBody } from "../src/markdown.js";
3
+ import { appendSection, parseMarkdown, docTitle, replaceSectionBody } from "../src/markdown.js";
4
4
 
5
5
  test("parseMarkdown builds nested heading paths and excludes H1", () => {
6
6
  const md = "# Title\n\nintro\n\n## Arch\n\ntop\n\n### Arch::Storage\n\nnested\n";
@@ -30,3 +30,8 @@ test("replaceSectionBody splices only the target section", () => {
30
30
  assert.match(out, /keep me/);
31
31
  assert.match(out, /^# T/);
32
32
  });
33
+
34
+ test("appendSection adds a missing section at the end", () => {
35
+ const md = "# T\n\n## Existing\n\nkeep me\n";
36
+ assert.equal(appendSection(md, "Added", "new body"), `${md}\n## Added\n\nnew body\n`);
37
+ });
package/test/mcp.test.js CHANGED
@@ -10,18 +10,18 @@ let db;
10
10
  let open;
11
11
 
12
12
  const EXPECTED_TOOLS = [
13
- "mindroot_list_projects",
14
- "mindroot_search_projects",
15
- "mindroot_rename_project",
16
- "mindroot_search_memories",
17
- "mindroot_search_notes",
18
- "mindroot_save_memory",
19
- "mindroot_delete_memory",
20
- "mindroot_list_notes",
21
- "mindroot_read_note",
22
- "mindroot_save_note",
23
- "mindroot_update_section",
24
- "mindroot_delete_note",
13
+ "list_projects",
14
+ "search_projects",
15
+ "rename_project",
16
+ "search_memories",
17
+ "search_notes",
18
+ "save_memory",
19
+ "delete_memory",
20
+ "list_notes",
21
+ "read_note",
22
+ "save_note",
23
+ "update_section",
24
+ "delete_note",
25
25
  ];
26
26
 
27
27
  before(async () => {
@@ -58,19 +58,19 @@ test("unknown tool returns clean error", async () => {
58
58
  });
59
59
 
60
60
  test("search without project is a clean isError", async () => {
61
- const res = await call("mindroot_search_memories", { query: "x" });
61
+ const res = await call("search_memories", { query: "x" });
62
62
  assert.equal(res.body.result.isError, true);
63
63
  assert.match(res.body.result.content[0].text, /project is required/);
64
64
  });
65
65
 
66
66
  test("invalid slug is rejected before any lookup", async () => {
67
- const res = await call("mindroot_search_memories", { project: "../evil", query: "x" });
67
+ const res = await call("search_memories", { project: "../evil", query: "x" });
68
68
  assert.equal(res.body.result.isError, true);
69
69
  assert.match(res.body.result.content[0].text, /invalid project slug/);
70
70
  });
71
71
 
72
72
  test("unknown project is a clean error", async () => {
73
- const res = await call("mindroot_search_memories", { project: "ghost", query: "x" });
73
+ const res = await call("search_memories", { project: "ghost", query: "x" });
74
74
  assert.match(res.body.result.content[0].text, /unknown project: ghost/);
75
75
  });
76
76
 
@@ -89,23 +89,43 @@ test("rename_project moves slug, dir, and embeddings", async () => {
89
89
  fs.mkdirSync(path.join(NOTES_DIR, "old-name"), { recursive: true });
90
90
  fs.writeFileSync(path.join(NOTES_DIR, "old-name", "overview.md"), "# Old\n");
91
91
 
92
- const res = await call("mindroot_rename_project", { project: "old-name", new_slug: "new-name" });
92
+ const res = await call("rename_project", { project: "old-name", new_slug: "new-name" });
93
93
  const data = JSON.parse(res.body.result.content[0].text);
94
94
  assert.deepEqual(data.renamed, { from: "old-name", to: "new-name" });
95
95
  assert.equal(db.prepare("SELECT slug FROM projects WHERE id = ?").get(pid).slug, "new-name");
96
96
  assert.ok(fs.existsSync(path.join(NOTES_DIR, "new-name", "overview.md")));
97
97
  assert.ok(!fs.existsSync(path.join(NOTES_DIR, "old-name")));
98
98
  assert.equal(db.prepare("SELECT COUNT(*) n FROM project_embeddings WHERE project_id = ?").get(pid).n, 0);
99
- const listed = JSON.parse((await call("mindroot_list_projects", {})).body.result.content[0].text);
99
+ const listed = JSON.parse((await call("list_projects", {})).body.result.content[0].text);
100
100
  const row = listed.find((p) => p.slug === "new-name");
101
101
  assert.equal(row.memories, 1);
102
102
  assert.ok(!listed.some((p) => p.slug === "old-name"));
103
103
 
104
- const dup = await call("mindroot_rename_project", { project: "new-name", new_slug: "new-name" });
104
+ const dup = await call("rename_project", { project: "new-name", new_slug: "new-name" });
105
105
  assert.equal(dup.body.result.isError, true);
106
106
  assert.match(dup.body.result.content[0].text, /already named/);
107
107
 
108
- const bad = await call("mindroot_rename_project", { project: "new-name", new_slug: "../evil" });
108
+ const bad = await call("rename_project", { project: "new-name", new_slug: "../evil" });
109
109
  assert.equal(bad.body.result.isError, true);
110
110
  assert.match(bad.body.result.content[0].text, /invalid project slug/);
111
111
  });
112
+
113
+ test("update_section appends a missing section", async () => {
114
+ await call("save_note", {
115
+ project: "section-upsert",
116
+ path: "note.md",
117
+ content: "# Note\n\n## Existing\n\nkeep me\n",
118
+ });
119
+
120
+ const res = await call("update_section", {
121
+ project: "section-upsert",
122
+ path: "note.md",
123
+ heading_path: "Added",
124
+ content: "new body",
125
+ });
126
+ assert.equal(res.body.result.isError, undefined);
127
+
128
+ const note = JSON.parse(res.body.result.content[0].text);
129
+ assert.equal(note.content, "# Note\n\n## Existing\n\nkeep me\n\n## Added\n\nnew body\n");
130
+ assert.ok(note.sections.some((section) => section.path === "Added"));
131
+ });