mcp-simple-memory 0.2.0 → 0.3.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 CHANGED
@@ -149,6 +149,56 @@ mem_search({ query: "authentication problem", tag: "bug" })
149
149
  3. **Auto mode**: Keyword first. If < 3 results, falls back to vector search.
150
150
  4. **Tags**: Lightweight categorization. Normalized to lowercase.
151
151
 
152
+ ## Import Existing Knowledge
153
+
154
+ Already have notes, docs, or session logs? Import them directly:
155
+
156
+ ```bash
157
+ # Import a single file
158
+ npx mcp-simple-memory import CLAUDE.md --project my-app --tags setup
159
+
160
+ # Import all .md files in a directory
161
+ npx mcp-simple-memory import ./docs --tags documentation
162
+
163
+ # Preview what would be imported (no changes)
164
+ npx mcp-simple-memory import ./notes --dry-run
165
+
166
+ # With semantic search (auto-generates embeddings during import)
167
+ GEMINI_API_KEY=your-key npx mcp-simple-memory import memory.md
168
+ ```
169
+
170
+ The importer splits markdown files by headings (`#`, `##`, `###`) into individual memories. Each section becomes a searchable memory entry.
171
+
172
+ ### Options
173
+
174
+ | Flag | Description |
175
+ |------|-------------|
176
+ | `--project <name>` | Set project name (default: filename) |
177
+ | `--tags <t1,t2>` | Add tags to all imported memories |
178
+ | `--dry-run` | Preview without saving |
179
+
180
+ ## Database Stats
181
+
182
+ Check what's in your memory:
183
+
184
+ ```bash
185
+ npx mcp-simple-memory stats
186
+ ```
187
+
188
+ Output:
189
+ ```
190
+ mcp-simple-memory stats
191
+ DB: ~/.mcp-simple-memory/memory.db
192
+
193
+ Memories: 42
194
+ Embeddings: 42/42 (100%)
195
+ Tags: 15 unique
196
+
197
+ Projects:
198
+ my-app: 30
199
+ default: 12
200
+ ```
201
+
152
202
  ## Data Storage
153
203
 
154
204
  All data stays local:
package/dist/cli.d.ts CHANGED
@@ -3,7 +3,10 @@
3
3
  * CLI for mcp-simple-memory
4
4
  *
5
5
  * Usage:
6
- * npx mcp-simple-memory init — Add to .mcp.json
7
- * npx mcp-simple-memory serve — Start MCP server (used by Claude Code)
6
+ * npx mcp-simple-memory init — Add to .mcp.json
7
+ * npx mcp-simple-memory serve — Start MCP server (used by Claude Code)
8
+ * npx mcp-simple-memory import <file> — Import .md file as memories
9
+ * npx mcp-simple-memory import <dir> — Import all .md files in directory
10
+ * npx mcp-simple-memory stats — Show memory database stats
8
11
  */
9
12
  export {};
package/dist/cli.js CHANGED
@@ -3,28 +3,50 @@
3
3
  * CLI for mcp-simple-memory
4
4
  *
5
5
  * Usage:
6
- * npx mcp-simple-memory init — Add to .mcp.json
7
- * npx mcp-simple-memory serve — Start MCP server (used by Claude Code)
6
+ * npx mcp-simple-memory init — Add to .mcp.json
7
+ * npx mcp-simple-memory serve — Start MCP server (used by Claude Code)
8
+ * npx mcp-simple-memory import <file> — Import .md file as memories
9
+ * npx mcp-simple-memory import <dir> — Import all .md files in directory
10
+ * npx mcp-simple-memory stats — Show memory database stats
8
11
  */
9
- import { readFileSync, writeFileSync, existsSync } from "fs";
10
- import { join } from "path";
12
+ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, mkdirSync } from "fs";
13
+ import { join, basename, extname, resolve } from "path";
14
+ import { homedir } from "os";
15
+ import initSqlJs from "sql.js";
16
+ // Config - must be before command dispatch
17
+ const DATA_DIR = process.env.MCP_MEMORY_DIR || join(homedir(), ".mcp-simple-memory");
18
+ const DB_PATH = join(DATA_DIR, "memory.db");
19
+ const GEMINI_API_KEY = process.env.GEMINI_API_KEY || "";
20
+ const GEMINI_MODEL = "gemini-embedding-001";
11
21
  const args = process.argv.slice(2);
12
22
  const command = args[0] || "serve";
13
23
  if (command === "init") {
14
24
  init();
15
25
  }
16
26
  else if (command === "serve") {
17
- // Import and run the server
18
27
  await import("./index.js");
19
28
  }
29
+ else if (command === "import") {
30
+ await importMemories(args[1], args.slice(2));
31
+ }
32
+ else if (command === "stats") {
33
+ await showStats();
34
+ }
20
35
  else if (command === "help" || command === "--help" || command === "-h") {
21
36
  console.log(`
22
37
  mcp-simple-memory — Persistent memory for Claude Code
23
38
 
24
39
  Commands:
25
- init Add mcp-simple-memory to .mcp.json in current directory
26
- serve Start the MCP server (default, used by Claude Code)
27
- help Show this help
40
+ init Add mcp-simple-memory to .mcp.json in current directory
41
+ serve Start the MCP server (default, used by Claude Code)
42
+ import <file|dir> Import .md files as memories
43
+ stats Show database statistics
44
+ help Show this help
45
+
46
+ Import options:
47
+ --project <name> Set project name (default: filename without extension)
48
+ --tags <t1,t2> Add tags to all imported memories
49
+ --dry-run Show what would be imported without saving
28
50
 
29
51
  Environment variables:
30
52
  GEMINI_API_KEY Enable semantic search (optional, free tier works)
@@ -35,6 +57,100 @@ else {
35
57
  console.error(`Unknown command: ${command}. Run 'mcp-simple-memory help' for usage.`);
36
58
  process.exit(1);
37
59
  }
60
+ // ---- Database helpers (shared with index.ts) --------------------------------
61
+ async function openDb() {
62
+ if (!existsSync(DATA_DIR))
63
+ mkdirSync(DATA_DIR, { recursive: true });
64
+ const SQL = await initSqlJs();
65
+ let db;
66
+ if (existsSync(DB_PATH)) {
67
+ db = new SQL.Database(readFileSync(DB_PATH));
68
+ }
69
+ else {
70
+ db = new SQL.Database();
71
+ }
72
+ db.run(`CREATE TABLE IF NOT EXISTS memories (
73
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
74
+ title TEXT, content TEXT NOT NULL,
75
+ type TEXT DEFAULT 'memory', project TEXT DEFAULT 'default',
76
+ created_at INTEGER NOT NULL, created_iso TEXT NOT NULL,
77
+ updated_at INTEGER, updated_iso TEXT
78
+ )`);
79
+ db.run(`CREATE TABLE IF NOT EXISTS embeddings (
80
+ memory_id INTEGER PRIMARY KEY, vector BLOB NOT NULL
81
+ )`);
82
+ db.run(`CREATE TABLE IF NOT EXISTS tags (
83
+ memory_id INTEGER NOT NULL, tag TEXT NOT NULL,
84
+ PRIMARY KEY (memory_id, tag)
85
+ )`);
86
+ return db;
87
+ }
88
+ function persistDb(db) {
89
+ writeFileSync(DB_PATH, Buffer.from(db.export()));
90
+ }
91
+ function queryAll(db, sql, params = []) {
92
+ const stmt = db.prepare(sql);
93
+ if (params.length)
94
+ stmt.bind(params);
95
+ const rows = [];
96
+ while (stmt.step())
97
+ rows.push(stmt.getAsObject());
98
+ stmt.free();
99
+ return rows;
100
+ }
101
+ async function getEmbedding(text) {
102
+ if (!GEMINI_API_KEY)
103
+ return null;
104
+ try {
105
+ const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:embedContent?key=${GEMINI_API_KEY}`, {
106
+ method: "POST",
107
+ headers: { "Content-Type": "application/json" },
108
+ body: JSON.stringify({
109
+ model: `models/${GEMINI_MODEL}`,
110
+ content: { parts: [{ text }] },
111
+ }),
112
+ });
113
+ if (!res.ok)
114
+ return null;
115
+ const data = (await res.json());
116
+ return new Float32Array(data.embedding.values);
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
122
+ function chunkMarkdown(text) {
123
+ const lines = text.split("\n");
124
+ const chunks = [];
125
+ let currentTitle = "";
126
+ let currentContent = [];
127
+ let currentLevel = 0;
128
+ for (const line of lines) {
129
+ const headingMatch = line.match(/^(#{1,3})\s+(.+)/);
130
+ if (headingMatch) {
131
+ // Save previous chunk
132
+ if (currentContent.length > 0 || currentTitle) {
133
+ const content = currentContent.join("\n").trim();
134
+ if (content.length > 10) {
135
+ chunks.push({ title: currentTitle, content, level: currentLevel });
136
+ }
137
+ }
138
+ currentTitle = headingMatch[2].trim();
139
+ currentLevel = headingMatch[1].length;
140
+ currentContent = [];
141
+ }
142
+ else {
143
+ currentContent.push(line);
144
+ }
145
+ }
146
+ // Last chunk
147
+ const content = currentContent.join("\n").trim();
148
+ if (content.length > 10) {
149
+ chunks.push({ title: currentTitle, content, level: currentLevel });
150
+ }
151
+ return chunks;
152
+ }
153
+ // ---- Commands ---------------------------------------------------------------
38
154
  function init() {
39
155
  const mcpPath = join(process.cwd(), ".mcp.json");
40
156
  let config = {};
@@ -68,3 +184,140 @@ function init() {
68
184
  console.log("Example with semantic search:");
69
185
  console.log(' "env": { "GEMINI_API_KEY": "your-key-here" }');
70
186
  }
187
+ async function importMemories(target, flags) {
188
+ if (!target) {
189
+ console.error("Usage: mcp-simple-memory import <file.md|directory> [--project name] [--tags t1,t2] [--dry-run]");
190
+ process.exit(1);
191
+ }
192
+ // Parse flags
193
+ let project;
194
+ let extraTags = [];
195
+ let dryRun = false;
196
+ for (let i = 0; i < flags.length; i++) {
197
+ if (flags[i] === "--project" && flags[i + 1]) {
198
+ project = flags[++i];
199
+ }
200
+ else if (flags[i] === "--tags" && flags[i + 1]) {
201
+ extraTags = flags[++i].split(",").map((t) => t.trim().toLowerCase()).filter(Boolean);
202
+ }
203
+ else if (flags[i] === "--dry-run") {
204
+ dryRun = true;
205
+ }
206
+ }
207
+ const resolvedTarget = resolve(target);
208
+ // Collect files
209
+ let files = [];
210
+ if (!existsSync(resolvedTarget)) {
211
+ console.error(`Not found: ${resolvedTarget}`);
212
+ process.exit(1);
213
+ }
214
+ if (statSync(resolvedTarget).isDirectory()) {
215
+ files = readdirSync(resolvedTarget)
216
+ .filter((f) => extname(f).toLowerCase() === ".md")
217
+ .map((f) => join(resolvedTarget, f))
218
+ .sort();
219
+ }
220
+ else {
221
+ files = [resolvedTarget];
222
+ }
223
+ if (!files.length) {
224
+ console.error("No .md files found.");
225
+ process.exit(1);
226
+ }
227
+ console.log(`Found ${files.length} file(s) to import.\n`);
228
+ const db = await openDb();
229
+ let totalChunks = 0;
230
+ let totalEmbedded = 0;
231
+ for (const file of files) {
232
+ const name = basename(file, ".md");
233
+ const proj = project || name;
234
+ const raw = readFileSync(file, "utf-8");
235
+ const chunks = chunkMarkdown(raw);
236
+ console.log(`${name}.md → ${chunks.length} chunks (project: ${proj})`);
237
+ for (const chunk of chunks) {
238
+ const tags = [...extraTags];
239
+ // Auto-tag from heading level
240
+ if (chunk.level === 1)
241
+ tags.push("section");
242
+ if (chunk.level === 2)
243
+ tags.push("subsection");
244
+ if (dryRun) {
245
+ const preview = chunk.content.substring(0, 60).replace(/\n/g, " ");
246
+ console.log(` [dry-run] "${chunk.title}" (${chunk.content.length} chars) ${preview}...`);
247
+ totalChunks++;
248
+ continue;
249
+ }
250
+ const now = Date.now();
251
+ db.run(`INSERT INTO memories (title, content, type, project, created_at, created_iso) VALUES (?, ?, ?, ?, ?, ?)`, [chunk.title || name, chunk.content, "memory", proj, now, new Date(now).toISOString()]);
252
+ const id = queryAll(db, `SELECT last_insert_rowid() as id`)[0]?.id ?? 0;
253
+ // Tags
254
+ for (const tag of tags) {
255
+ if (tag)
256
+ db.run(`INSERT OR IGNORE INTO tags (memory_id, tag) VALUES (?, ?)`, [id, tag.toLowerCase()]);
257
+ }
258
+ // Embedding
259
+ if (GEMINI_API_KEY) {
260
+ const vec = await getEmbedding(`${chunk.title}\n${chunk.content}`);
261
+ if (vec) {
262
+ db.run(`INSERT OR REPLACE INTO embeddings (memory_id, vector) VALUES (?, ?)`, [
263
+ id,
264
+ new Uint8Array(vec.buffer),
265
+ ]);
266
+ totalEmbedded++;
267
+ }
268
+ // Rate limit
269
+ await new Promise((r) => setTimeout(r, 100));
270
+ }
271
+ const tagStr = tags.length ? ` [${tags.join(", ")}]` : "";
272
+ console.log(` #${id} "${chunk.title}" (${chunk.content.length} chars)${tagStr}`);
273
+ totalChunks++;
274
+ }
275
+ }
276
+ if (!dryRun) {
277
+ persistDb(db);
278
+ }
279
+ db.close();
280
+ console.log(`\n${dryRun ? "[DRY RUN] " : ""}Imported ${totalChunks} memories from ${files.length} file(s).`);
281
+ if (totalEmbedded > 0) {
282
+ console.log(`Generated ${totalEmbedded} embeddings.`);
283
+ }
284
+ else if (!GEMINI_API_KEY && !dryRun) {
285
+ console.log("Tip: Set GEMINI_API_KEY to auto-generate embeddings during import.");
286
+ }
287
+ }
288
+ async function showStats() {
289
+ if (!existsSync(DB_PATH)) {
290
+ console.log("No database found. Run 'mcp-simple-memory init' first.");
291
+ return;
292
+ }
293
+ const db = await openDb();
294
+ const memCount = queryAll(db, `SELECT COUNT(*) as c FROM memories`)[0]?.c ?? 0;
295
+ const embCount = queryAll(db, `SELECT COUNT(*) as c FROM embeddings`)[0]?.c ?? 0;
296
+ const tagCount = queryAll(db, `SELECT COUNT(DISTINCT tag) as c FROM tags`)[0]?.c ?? 0;
297
+ const projects = queryAll(db, `SELECT project, COUNT(*) as c FROM memories GROUP BY project ORDER BY c DESC`);
298
+ const types = queryAll(db, `SELECT type, COUNT(*) as c FROM memories GROUP BY type ORDER BY c DESC`);
299
+ const topTags = queryAll(db, `SELECT tag, COUNT(*) as c FROM tags GROUP BY tag ORDER BY c DESC LIMIT 10`);
300
+ console.log(`mcp-simple-memory stats`);
301
+ console.log(` DB: ${DB_PATH}\n`);
302
+ console.log(` Memories: ${memCount}`);
303
+ console.log(` Embeddings: ${embCount}/${memCount} (${memCount ? Math.round(embCount / memCount * 100) : 0}%)`);
304
+ console.log(` Tags: ${tagCount} unique\n`);
305
+ if (projects.length) {
306
+ console.log(` Projects:`);
307
+ for (const p of projects)
308
+ console.log(` ${p.project}: ${p.c}`);
309
+ console.log("");
310
+ }
311
+ if (types.length) {
312
+ console.log(` Types:`);
313
+ for (const t of types)
314
+ console.log(` ${t.type}: ${t.c}`);
315
+ console.log("");
316
+ }
317
+ if (topTags.length) {
318
+ console.log(` Top tags:`);
319
+ for (const t of topTags)
320
+ console.log(` ${t.tag}: ${t.c}`);
321
+ }
322
+ db.close();
323
+ }
package/dist/index.js CHANGED
@@ -217,7 +217,7 @@ async function handleSave(args) {
217
217
  if (GEMINI_API_KEY) {
218
218
  getEmbedding(`${title}\n${content}`).then((vec) => {
219
219
  if (vec) {
220
- runSql(`INSERT OR REPLACE INTO embeddings (memory_id, vector) VALUES (?, ?)`, [id, vec.buffer]);
220
+ runSql(`INSERT OR REPLACE INTO embeddings (memory_id, vector) VALUES (?, ?)`, [id, new Uint8Array(vec.buffer)]);
221
221
  persist();
222
222
  }
223
223
  });
@@ -425,7 +425,7 @@ async function handleUpdate(args) {
425
425
  const newTitle = args.title || existing[0].title;
426
426
  getEmbedding(`${newTitle}\n${newContent}`).then((vec) => {
427
427
  if (vec) {
428
- runSql(`INSERT OR REPLACE INTO embeddings (memory_id, vector) VALUES (?, ?)`, [id, vec.buffer]);
428
+ runSql(`INSERT OR REPLACE INTO embeddings (memory_id, vector) VALUES (?, ?)`, [id, new Uint8Array(vec.buffer)]);
429
429
  persist();
430
430
  }
431
431
  });
@@ -462,7 +462,7 @@ async function handleTags(args) {
462
462
  return ok(`# Tags (${rows.length})\n\n${lines.join("\n")}`);
463
463
  }
464
464
  // ---- MCP Server -----------------------------------------------------------
465
- const server = new Server({ name: "mcp-simple-memory", version: "0.2.0" }, { capabilities: { tools: {} } });
465
+ const server = new Server({ name: "mcp-simple-memory", version: "0.3.0" }, { capabilities: { tools: {} } });
466
466
  const tools = [
467
467
  {
468
468
  name: "mem_save",
@@ -620,7 +620,7 @@ async function main() {
620
620
  await initDb();
621
621
  const transport = new StdioServerTransport();
622
622
  await server.connect(transport);
623
- console.error(`[mcp-simple-memory] v0.2.0 | DB: ${DB_PATH} | Embeddings: ${GEMINI_API_KEY ? "ON" : "OFF"}`);
623
+ console.error(`[mcp-simple-memory] v0.3.0 | DB: ${DB_PATH} | Embeddings: ${GEMINI_API_KEY ? "ON" : "OFF"}`);
624
624
  }
625
625
  main().catch((e) => {
626
626
  console.error(`[mcp-simple-memory] Fatal: ${e}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-simple-memory",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Persistent memory for Claude Code via MCP. SQLite + optional semantic search. Zero native deps. Works on Windows/Mac/Linux.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",