@bennys001/claude-code-memory 0.11.1 → 0.12.3

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 +26 -4
  2. package/dist/index.js +334 -76
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,19 +7,37 @@ MCP server that gives Claude Code persistent memory via an Obsidian-compatible k
7
7
  Requires [Bun](https://bun.sh/) and [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
8
8
 
9
9
  ```bash
10
- bunx @bennys001/claude-code-memory --init
10
+ bun install -g @bennys001/claude-code-memory && ccm --init
11
11
  ```
12
12
 
13
- This scaffolds the vault at `~/.ccm/knowledge-base/`, registers it with Obsidian (if installed), and adds the MCP server to Claude Code.
13
+ This installs the `ccm` binary globally, scaffolds the vault at `~/.ccm/knowledge-base/`, registers it with Obsidian (if installed), and adds the MCP server to Claude Code.
14
14
 
15
- ### Manual setup
15
+ ### Update
16
+
17
+ ```bash
18
+ ccm --update
19
+ ```
20
+
21
+ ### Uninstall
22
+
23
+ ```bash
24
+ ccm --uninstall
25
+ ```
26
+
27
+ Removes the MCP server registration, Obsidian vault registration, and the global binary. Prompts before deleting the vault.
28
+
29
+ ### Manual MCP setup
16
30
 
17
31
  If `--init` can't reach the `claude` CLI, register manually:
18
32
 
19
33
  ```bash
20
- claude mcp add --transport stdio --scope user ccm -- bunx @bennys001/claude-code-memory --stdio
34
+ claude mcp add --transport stdio --scope user ccm -- ccm --stdio
21
35
  ```
22
36
 
37
+ ## Real-Time Sync
38
+
39
+ The server watches the vault directory for changes while running. Edits made outside Claude — in Obsidian or your editor — are picked up automatically. New and modified `.md` files are re-parsed, deleted files are removed from the index, and new subdirectories are watched on creation. No restart required.
40
+
23
41
  ## MCP Tools
24
42
 
25
43
  ### `research`
@@ -63,6 +81,10 @@ Generates/updates a `## Knowledge Index` section in a project's `CLAUDE.md`. Inj
63
81
  |-------------|--------|----------|-------------------------------------|
64
82
  | `targetDir` | string | No | Project directory (defaults to CWD) |
65
83
 
84
+ ### `diagnostics`
85
+
86
+ Live runtime diagnostics — vault stats (entry counts by type, total tokens), file watcher activity (flushes, upserts, removes), process metrics (uptime, RSS, heap), and server version. No parameters.
87
+
66
88
  ### `fetch-page`
67
89
 
68
90
  Fetches a web page, extracts main content via Readability, converts to markdown. Returns a temp file path — read it, then `write` to vault.
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  var package_default = {
5
5
  name: "@bennys001/claude-code-memory",
6
6
  publishConfig: { access: "public" },
7
- version: "0.11.1",
7
+ version: "0.12.3",
8
8
  description: "MCP server that gives Claude Code persistent memory via an Obsidian knowledge vault",
9
9
  module: "dist/index.js",
10
10
  main: "dist/index.js",
@@ -57,7 +57,7 @@ var package_default = {
57
57
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
58
58
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
59
59
  import { homedir as homedir4 } from "os";
60
- import { join as join7 } from "path";
60
+ import { join as join8 } from "path";
61
61
 
62
62
  // src/vault/loader.ts
63
63
  import { globby } from "globby";
@@ -167,6 +167,127 @@ function filterEntries(entries, config) {
167
167
  });
168
168
  }
169
169
 
170
+ // src/vault/watcher.ts
171
+ import { watch, readdirSync } from "fs";
172
+ import { resolve, relative as relative2, join } from "path";
173
+ var {Glob } = globalThis.Bun;
174
+ function sortEntries(entries) {
175
+ entries.sort((a, b) => NOTE_TYPE_PRIORITY[a.frontmatter.type] - NOTE_TYPE_PRIORITY[b.frontmatter.type]);
176
+ }
177
+ function removeEntry(entries, filePath) {
178
+ const idx = entries.findIndex((e) => e.filePath === filePath);
179
+ if (idx === -1)
180
+ return false;
181
+ entries.splice(idx, 1);
182
+ return true;
183
+ }
184
+ async function upsertEntry(entries, filePath, vaultPath) {
185
+ removeEntry(entries, filePath);
186
+ const entry = await parseNote(filePath, vaultPath);
187
+ if (entry) {
188
+ entries.push(entry);
189
+ sortEntries(entries);
190
+ }
191
+ }
192
+ function watchVault(vaultPath, entries) {
193
+ const watchers = new Map;
194
+ const pending = new Set;
195
+ let timer = null;
196
+ const stats = { activeWatchers: 0, totalFlushes: 0, totalUpserts: 0, totalRemoves: 0 };
197
+ const flush = async () => {
198
+ stats.totalFlushes++;
199
+ const paths = [...pending];
200
+ pending.clear();
201
+ for (const rel of paths) {
202
+ const abs = resolve(vaultPath, rel);
203
+ const exists = await Bun.file(abs).exists();
204
+ if (exists) {
205
+ await upsertEntry(entries, abs, vaultPath);
206
+ stats.totalUpserts++;
207
+ console.error(`[watcher] upserted ${rel}`);
208
+ } else {
209
+ const removed = removeEntry(entries, abs);
210
+ if (removed) {
211
+ stats.totalRemoves++;
212
+ console.error(`[watcher] removed ${rel}`);
213
+ }
214
+ }
215
+ }
216
+ };
217
+ const schedule = () => {
218
+ if (timer)
219
+ clearTimeout(timer);
220
+ timer = setTimeout(flush, 300);
221
+ };
222
+ const removeDirWatcher = (dirPath) => {
223
+ const w = watchers.get(dirPath);
224
+ if (w) {
225
+ w.close();
226
+ watchers.delete(dirPath);
227
+ stats.activeWatchers--;
228
+ }
229
+ };
230
+ const addDirWatcher = (dirPath) => {
231
+ if (watchers.has(dirPath))
232
+ return;
233
+ try {
234
+ const w = watch(dirPath, (event, filename) => {
235
+ if (!filename)
236
+ return;
237
+ const abs = join(dirPath, filename);
238
+ const rel = relative2(vaultPath, abs);
239
+ if (rel.split("/").some((seg) => seg.startsWith(".")))
240
+ return;
241
+ if (filename.endsWith(".md")) {
242
+ pending.add(rel);
243
+ schedule();
244
+ return;
245
+ }
246
+ if (event === "rename") {
247
+ try {
248
+ readdirSync(abs);
249
+ addDirWatcher(abs);
250
+ const glob = new Glob("**/*.md");
251
+ for (const match of glob.scanSync({ cwd: abs })) {
252
+ const mdRel = relative2(vaultPath, join(abs, match));
253
+ if (!mdRel.split("/").some((seg) => seg.startsWith("."))) {
254
+ pending.add(mdRel);
255
+ }
256
+ }
257
+ schedule();
258
+ } catch {
259
+ removeDirWatcher(abs);
260
+ }
261
+ }
262
+ });
263
+ watchers.set(dirPath, w);
264
+ stats.activeWatchers++;
265
+ } catch (err) {
266
+ console.error(`[watcher] failed to watch ${dirPath}: ${err}`);
267
+ }
268
+ };
269
+ const walkAndWatch = (dirPath) => {
270
+ addDirWatcher(dirPath);
271
+ try {
272
+ for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
273
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
274
+ walkAndWatch(join(dirPath, entry.name));
275
+ }
276
+ }
277
+ } catch {}
278
+ };
279
+ walkAndWatch(vaultPath);
280
+ const stop = () => {
281
+ if (timer)
282
+ clearTimeout(timer);
283
+ for (const w of watchers.values())
284
+ w.close();
285
+ watchers.clear();
286
+ stats.activeWatchers = 0;
287
+ };
288
+ return { stop, stats };
289
+ }
290
+
170
291
  // src/tools/index-tool.ts
171
292
  import { z as z2 } from "zod";
172
293
 
@@ -226,6 +347,62 @@ function injectKnowledgeSection(existingContent, entries) {
226
347
  `;
227
348
  }
228
349
 
350
+ // src/update-checker.ts
351
+ var NPM_URL = "https://registry.npmjs.org/@bennys001/claude-code-memory/latest";
352
+ var CHANGELOG_URL = "https://raw.githubusercontent.com/Ben-Spn/claude-code-memory/master/CHANGELOG.md";
353
+ var latestVersion = null;
354
+ var releaseNotes = [];
355
+ async function fetchLatestVersion() {
356
+ const res = await fetch(NPM_URL);
357
+ if (!res.ok)
358
+ throw new Error(`npm registry returned ${res.status}`);
359
+ const data = await res.json();
360
+ return data.version;
361
+ }
362
+ function parseChangelog(markdown, version) {
363
+ const heading = `## ${version}`;
364
+ const start = markdown.indexOf(heading);
365
+ if (start === -1)
366
+ return [];
367
+ const afterHeading = start + heading.length;
368
+ const nextSection = markdown.indexOf(`
369
+ ## `, afterHeading);
370
+ const section = nextSection === -1 ? markdown.substring(afterHeading) : markdown.substring(afterHeading, nextSection);
371
+ return section.split(`
372
+ `).filter((l) => l.startsWith("- ")).map((l) => l.slice(2).trim());
373
+ }
374
+ async function fetchReleaseNotes(version) {
375
+ const res = await fetch(CHANGELOG_URL);
376
+ if (!res.ok)
377
+ return [];
378
+ const markdown = await res.text();
379
+ return parseChangelog(markdown, version);
380
+ }
381
+ function initUpdateCheck() {
382
+ fetchLatestVersion().then(async (v) => {
383
+ latestVersion = v;
384
+ if (v !== package_default.version) {
385
+ releaseNotes = await fetchReleaseNotes(v);
386
+ }
387
+ }).catch(() => {});
388
+ }
389
+ function getUpdateNotice() {
390
+ if (!latestVersion || latestVersion === package_default.version)
391
+ return "";
392
+ const lines = [`
393
+
394
+ ---
395
+ Update available: v${package_default.version} \u2192 v${latestVersion}`];
396
+ if (releaseNotes.length > 0) {
397
+ lines.push("What's new:");
398
+ for (const note of releaseNotes)
399
+ lines.push(` - ${note}`);
400
+ }
401
+ lines.push("Run `ccm --update` to upgrade");
402
+ return lines.join(`
403
+ `);
404
+ }
405
+
229
406
  // src/tools/index-tool.ts
230
407
  function executeIndex(args, entries) {
231
408
  let filtered = entries;
@@ -242,19 +419,19 @@ function registerIndexTool(server, entries) {
242
419
  })
243
420
  }, async (args) => {
244
421
  const result = executeIndex(args, entries);
245
- return { content: [{ type: "text", text: result }] };
422
+ return { content: [{ type: "text", text: result + getUpdateNotice() }] };
246
423
  });
247
424
  }
248
425
 
249
426
  // src/tools/read-tool.ts
250
- import { resolve, relative as relative2 } from "path";
427
+ import { resolve as resolve2, relative as relative3 } from "path";
251
428
  import { z as z3 } from "zod";
252
429
  async function executeRead(notePath, vaultPath) {
253
430
  if (notePath.startsWith("/")) {
254
431
  return { ok: false, error: "Absolute paths not allowed. Use paths relative to vault root." };
255
432
  }
256
- const resolved = resolve(vaultPath, notePath);
257
- const rel = relative2(vaultPath, resolved);
433
+ const resolved = resolve2(vaultPath, notePath);
434
+ const rel = relative3(vaultPath, resolved);
258
435
  if (rel.startsWith("..")) {
259
436
  return { ok: false, error: "Path resolves outside vault. Use paths relative to vault root." };
260
437
  }
@@ -273,7 +450,7 @@ function registerReadTool(server, vaultPath) {
273
450
  }, async ({ path }) => {
274
451
  const result = await executeRead(path, vaultPath);
275
452
  if (result.ok) {
276
- return { content: [{ type: "text", text: result.content }] };
453
+ return { content: [{ type: "text", text: result.content + getUpdateNotice() }] };
277
454
  }
278
455
  return { content: [{ type: "text", text: result.error }], isError: true };
279
456
  });
@@ -331,7 +508,7 @@ function registerSearchTool(server, entries) {
331
508
  }, async (args) => {
332
509
  const results = executeSearch(args, entries);
333
510
  if (results.length === 0) {
334
- return { content: [{ type: "text", text: "No notes match that query." }] };
511
+ return { content: [{ type: "text", text: "No notes match that query." + getUpdateNotice() }] };
335
512
  }
336
513
  const table = [
337
514
  "| T | Title | Path | ~Tok | Score |",
@@ -339,19 +516,19 @@ function registerSearchTool(server, entries) {
339
516
  ...results.map((r) => `| ${r.icon} | ${r.title} | ${r.relativePath} | ${r.tokenDisplay} | ${r.score} |`)
340
517
  ].join(`
341
518
  `);
342
- return { content: [{ type: "text", text: table }] };
519
+ return { content: [{ type: "text", text: table + getUpdateNotice() }] };
343
520
  });
344
521
  }
345
522
 
346
523
  // src/tools/sync-tool.ts
347
524
  import { z as z5 } from "zod";
348
- import { join as join3, resolve as resolve2 } from "path";
525
+ import { join as join4, resolve as resolve3 } from "path";
349
526
  import { homedir } from "os";
350
527
 
351
528
  // src/vault/config.ts
352
529
  import { parse, stringify } from "smol-toml";
353
- import { join as join2, basename as basename2, extname } from "path";
354
- var {Glob } = globalThis.Bun;
530
+ import { join as join3, basename as basename2, extname } from "path";
531
+ var {Glob: Glob2 } = globalThis.Bun;
355
532
  var EXT_TO_TAGS = {
356
533
  rs: ["rust"],
357
534
  ts: ["typescript"],
@@ -382,7 +559,7 @@ var EXT_TO_TAGS = {
382
559
  };
383
560
  var IGNORE_DIRS = ["node_modules", ".*", "target", "dist", "build"];
384
561
  async function loadProjectConfig(dir) {
385
- const configPath = join2(dir, ".context.toml");
562
+ const configPath = join3(dir, ".context.toml");
386
563
  const file = Bun.file(configPath);
387
564
  if (!await file.exists())
388
565
  return null;
@@ -410,7 +587,7 @@ function collectVaultTags(entries) {
410
587
  return tags;
411
588
  }
412
589
  async function detectTagsFromExtensions(dir, vaultTags) {
413
- const glob = new Glob(`**/*.*`);
590
+ const glob = new Glob2(`**/*.*`);
414
591
  const extensions = new Set;
415
592
  for await (const path of glob.scan({ cwd: dir, dot: false, followSymlinks: false })) {
416
593
  const skip = IGNORE_DIRS.some((d) => d.startsWith(".") ? path.startsWith(".") : path.startsWith(d + "/"));
@@ -439,7 +616,7 @@ async function createDefaultConfig(dir, allEntries) {
439
616
  config.filter = { tags: inferred };
440
617
  }
441
618
  }
442
- const configPath = join2(dir, ".context.toml");
619
+ const configPath = join3(dir, ".context.toml");
443
620
  await Bun.write(configPath, stringify(config) + `
444
621
  `);
445
622
  return config;
@@ -555,8 +732,8 @@ async function syncToFile(claudeMdPath, filtered) {
555
732
  async function executeSync(targetDir, allEntries, vaultPath, options = {}) {
556
733
  let config = await loadProjectConfig(targetDir);
557
734
  let autoCreated = false;
558
- const globalClaudeDir = options.globalClaudeDir ?? resolve2(homedir(), ".claude");
559
- const isGlobalDir = resolve2(targetDir) === resolve2(globalClaudeDir);
735
+ const globalClaudeDir = options.globalClaudeDir ?? resolve3(homedir(), ".claude");
736
+ const isGlobalDir = resolve3(targetDir) === resolve3(globalClaudeDir);
560
737
  if (!config && !isGlobalDir) {
561
738
  config = await createDefaultConfig(targetDir, allEntries);
562
739
  autoCreated = true;
@@ -570,7 +747,7 @@ async function executeSync(targetDir, allEntries, vaultPath, options = {}) {
570
747
  } else {
571
748
  filtered = filtered.filter((e) => e.frontmatter.projects.length === 0 && !hasTechTag(e));
572
749
  }
573
- const { entryCount, totalTokens, changed } = await syncToFile(join3(targetDir, "CLAUDE.md"), filtered);
750
+ const { entryCount, totalTokens, changed } = await syncToFile(join4(targetDir, "CLAUDE.md"), filtered);
574
751
  let summary = changed ? `Synced ${entryCount} entries to CLAUDE.md (${formatTokenCount(totalTokens)} total index tokens)` : `CLAUDE.md already up to date (${entryCount} entries, ${formatTokenCount(totalTokens)} total index tokens)`;
575
752
  if (autoCreated) {
576
753
  const allTags = [...new Set(allEntries.flatMap((e) => e.frontmatter.tags))].sort();
@@ -589,7 +766,7 @@ Available vault tags: [${allTags.join(", ")}] \u2014 edit .context.toml filter.t
589
766
  }
590
767
  if (!isGlobalDir) {
591
768
  const globalEntries = allEntries.filter((e) => e.frontmatter.projects.length === 0 && !hasTechTag(e));
592
- const globalResult = await syncToFile(join3(globalClaudeDir, "CLAUDE.md"), globalEntries);
769
+ const globalResult = await syncToFile(join4(globalClaudeDir, "CLAUDE.md"), globalEntries);
593
770
  summary += globalResult.changed ? `
594
771
  Synced ${globalResult.entryCount} global entries to ~/.claude/CLAUDE.md (${formatTokenCount(globalResult.totalTokens)} total index tokens)` : `
595
772
  ~/.claude/CLAUDE.md already up to date (${globalResult.entryCount} entries, ${formatTokenCount(globalResult.totalTokens)} total index tokens)`;
@@ -609,12 +786,12 @@ function registerSyncTool(server, entries, vaultPath) {
609
786
  }, async ({ targetDir }) => {
610
787
  const dir = targetDir ?? process.cwd();
611
788
  const result = await executeSync(dir, entries, vaultPath);
612
- return { content: [{ type: "text", text: result.summary }] };
789
+ return { content: [{ type: "text", text: result.summary + getUpdateNotice() }] };
613
790
  });
614
791
  }
615
792
 
616
793
  // src/tools/write-tool.ts
617
- import { resolve as resolve3, relative as relative3, dirname } from "path";
794
+ import { resolve as resolve4, relative as relative4, dirname } from "path";
618
795
  import { mkdir } from "fs/promises";
619
796
  import { z as z6 } from "zod";
620
797
 
@@ -645,7 +822,7 @@ function buildFrontmatter(args) {
645
822
  for (const t of args.tags)
646
823
  lines.push(` - ${t}`);
647
824
  }
648
- lines.push(`created: ${args.date}`, `updated: ${args.date}`, "---");
825
+ lines.push(`created: ${args.created}`, `updated: ${args.updated}`, "---");
649
826
  return lines.join(`
650
827
  `);
651
828
  }
@@ -653,20 +830,33 @@ async function executeWrite(args, entries, vaultPath) {
653
830
  if (args.path.startsWith("/")) {
654
831
  return { ok: false, error: "Absolute paths not allowed. Use paths relative to vault root." };
655
832
  }
656
- const resolved = resolve3(vaultPath, args.path);
657
- const rel = relative3(vaultPath, resolved);
833
+ const resolved = resolve4(vaultPath, args.path);
834
+ const rel = relative4(vaultPath, resolved);
658
835
  if (rel.startsWith("..")) {
659
836
  return { ok: false, error: "Path resolves outside vault. Use paths relative to vault root." };
660
837
  }
661
- if (await Bun.file(resolved).exists()) {
662
- return { ok: false, error: `File already exists: ${args.path}. Use a different path.` };
838
+ const fileExists = await Bun.file(resolved).exists();
839
+ if (fileExists && !args.overwrite) {
840
+ return { ok: false, error: `File already exists: ${args.path}. Pass overwrite: true to replace it.` };
663
841
  }
664
842
  const today = formatDate(new Date);
843
+ let createdDate = today;
844
+ if (fileExists) {
845
+ const existing = entries.find((e) => e.filePath === resolved);
846
+ if (existing) {
847
+ createdDate = formatDate(existing.frontmatter.created);
848
+ } else {
849
+ const parsed = await parseNote(resolved, vaultPath);
850
+ if (parsed)
851
+ createdDate = formatDate(parsed.frontmatter.created);
852
+ }
853
+ }
665
854
  const frontmatter = buildFrontmatter({
666
855
  type: args.type,
667
856
  tags: args.tags,
668
857
  projects: args.projects,
669
- date: today
858
+ created: createdDate,
859
+ updated: today
670
860
  });
671
861
  const content = `${frontmatter}
672
862
 
@@ -678,33 +868,38 @@ ${args.body}
678
868
  await Bun.write(resolved, content);
679
869
  const entry = await parseNote(resolved, vaultPath);
680
870
  if (entry) {
871
+ const idx = entries.findIndex((e) => e.filePath === resolved);
872
+ if (idx !== -1)
873
+ entries.splice(idx, 1);
681
874
  entries.push(entry);
682
875
  entries.sort((a, b) => NOTE_TYPE_PRIORITY[a.frontmatter.type] - NOTE_TYPE_PRIORITY[b.frontmatter.type]);
683
876
  }
684
- return { ok: true, path: rel };
877
+ return { ok: true, path: rel, updated: fileExists && !!args.overwrite };
685
878
  }
686
879
  function registerWriteTool(server, entries, vaultPath) {
687
880
  server.registerTool("write", {
688
- description: "Create a new note in the vault with structured frontmatter. " + "Rejects writes to existing paths. " + "To write a web page to the vault, first use the fetch-page tool.",
881
+ description: "Create or update a note in the vault with structured frontmatter. " + "Rejects writes to existing paths unless overwrite is true. " + "To write a web page to the vault, first use the fetch-page tool.",
689
882
  inputSchema: z6.object({
690
883
  path: z6.string().describe("Relative path within vault (e.g. gotchas/my-new-note.md)"),
691
884
  type: NoteType.describe("Note type"),
692
885
  title: z6.string().describe("Note title (becomes the H1 heading)"),
693
886
  body: z6.string().describe("Markdown body content (after the H1)"),
694
887
  tags: z6.array(z6.string()).optional().describe("Searchable tags"),
695
- projects: z6.array(z6.string()).optional().describe("Project names this note relates to")
888
+ projects: z6.array(z6.string()).optional().describe("Project names this note relates to"),
889
+ overwrite: z6.boolean().optional().describe("Set to true to overwrite an existing note")
696
890
  })
697
891
  }, async (args) => {
698
892
  const result = await executeWrite(args, entries, vaultPath);
699
893
  if (result.ok) {
700
- return { content: [{ type: "text", text: `Created note: ${result.path}` }] };
894
+ const verb = result.updated ? "Updated" : "Created";
895
+ return { content: [{ type: "text", text: `${verb} note: ${result.path}` + getUpdateNotice() }] };
701
896
  }
702
897
  return { content: [{ type: "text", text: result.error }], isError: true };
703
898
  });
704
899
  }
705
900
 
706
901
  // src/tools/fetch-page-tool.ts
707
- import { join as join4 } from "path";
902
+ import { join as join5 } from "path";
708
903
  import { mkdtemp, writeFile } from "fs/promises";
709
904
  import { tmpdir } from "os";
710
905
  import { z as z7 } from "zod";
@@ -769,8 +964,8 @@ async function executeFetchPage(url, fetcher = fetchPageAsMarkdown) {
769
964
  const msg = err instanceof Error ? err.message : String(err);
770
965
  return { ok: false, error: `Failed to fetch URL: ${msg}` };
771
966
  }
772
- const tempDir = await mkdtemp(join4(tmpdir(), "ccm-fetch-"));
773
- const tempPath = join4(tempDir, "page.md");
967
+ const tempDir = await mkdtemp(join5(tmpdir(), "ccm-fetch-"));
968
+ const tempPath = join5(tempDir, "page.md");
774
969
  await writeFile(tempPath, page.markdown);
775
970
  return {
776
971
  ok: true,
@@ -798,7 +993,7 @@ function registerFetchPageTool(server) {
798
993
  if (result.siteName)
799
994
  parts.push(`Site: ${result.siteName}`);
800
995
  return { content: [{ type: "text", text: parts.join(`
801
- `) }] };
996
+ `) + getUpdateNotice() }] };
802
997
  }
803
998
  return { content: [{ type: "text", text: result.error }], isError: true };
804
999
  });
@@ -860,9 +1055,59 @@ function registerResearchTool(server, entries, vaultPath) {
860
1055
  }, async (args) => {
861
1056
  const result = await executeResearch(args, entries, vaultPath);
862
1057
  if (result.notes.length === 0) {
863
- return { content: [{ type: "text", text: "No notes match that query." }] };
1058
+ return { content: [{ type: "text", text: "No notes match that query." + getUpdateNotice() }] };
864
1059
  }
865
- return { content: [{ type: "text", text: buildResearchOutput(result) }] };
1060
+ return { content: [{ type: "text", text: buildResearchOutput(result) + getUpdateNotice() }] };
1061
+ });
1062
+ }
1063
+
1064
+ // src/tools/diagnostics-tool.ts
1065
+ function formatUptime(seconds) {
1066
+ const h = Math.floor(seconds / 3600);
1067
+ const m = Math.floor(seconds % 3600 / 60);
1068
+ const s = Math.floor(seconds % 60);
1069
+ return `${h}h ${m}m ${s}s`;
1070
+ }
1071
+ function formatMB(bytes) {
1072
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
1073
+ }
1074
+ function executeDiagnostics(entries, watcherStats) {
1075
+ const typeCounts = { gotchas: 0, decisions: 0, patterns: 0, references: 0 };
1076
+ let totalTokens = 0;
1077
+ for (const entry of entries) {
1078
+ typeCounts[entry.frontmatter.type]++;
1079
+ totalTokens += entry.tokenCount;
1080
+ }
1081
+ const mem = process.memoryUsage();
1082
+ const lines = [
1083
+ `${C.bold}Vault${C.reset}`,
1084
+ ` Entries: ${entries.length}`,
1085
+ ` Gotchas: ${typeCounts.gotchas} Decisions: ${typeCounts.decisions} Patterns: ${typeCounts.patterns} References: ${typeCounts.references}`,
1086
+ ` Tokens: ${totalTokens}`,
1087
+ "",
1088
+ `${C.bold}Watcher${C.reset}`,
1089
+ ` Active: ${watcherStats.activeWatchers}`,
1090
+ ` Flushes: ${watcherStats.totalFlushes}`,
1091
+ ` Upserts: ${watcherStats.totalUpserts}`,
1092
+ ` Removes: ${watcherStats.totalRemoves}`,
1093
+ "",
1094
+ `${C.bold}Process${C.reset}`,
1095
+ ` Uptime: ${formatUptime(process.uptime())}`,
1096
+ ` RSS: ${formatMB(mem.rss)}`,
1097
+ ` Heap: ${formatMB(mem.heapUsed)} / ${formatMB(mem.heapTotal)}`,
1098
+ "",
1099
+ `${C.bold}Server${C.reset}`,
1100
+ ` Version: ${package_default.version}`
1101
+ ];
1102
+ return lines.join(`
1103
+ `);
1104
+ }
1105
+ function registerDiagnosticsTool(server, entries, watcherStats) {
1106
+ server.registerTool("diagnostics", {
1107
+ description: "Show live runtime diagnostics: vault stats, watcher activity, process metrics, and server version"
1108
+ }, async () => {
1109
+ const result = executeDiagnostics(entries, watcherStats);
1110
+ return { content: [{ type: "text", text: result + getUpdateNotice() }] };
866
1111
  });
867
1112
  }
868
1113
 
@@ -870,39 +1115,39 @@ function registerResearchTool(server, entries, vaultPath) {
870
1115
  import { spawn } from "child_process";
871
1116
  var SERVER_CMD = ["ccm", "--stdio"];
872
1117
  function installGlobal() {
873
- return new Promise((resolve4) => {
1118
+ return new Promise((resolve5) => {
874
1119
  let stderr = "";
875
1120
  const proc = spawn("bun", ["install", "-g", "@bennys001/claude-code-memory"], { stdio: ["ignore", "ignore", "pipe"] });
876
1121
  proc.stderr.on("data", (data) => {
877
1122
  stderr += data.toString();
878
1123
  });
879
1124
  proc.on("error", (err) => {
880
- resolve4({ success: false, error: err.message });
1125
+ resolve5({ success: false, error: err.message });
881
1126
  });
882
1127
  proc.on("close", (code) => {
883
1128
  if (code === 0) {
884
- resolve4({ success: true });
1129
+ resolve5({ success: true });
885
1130
  } else {
886
- resolve4({ success: false, error: stderr.trim() || `bun install exited with code ${code}` });
1131
+ resolve5({ success: false, error: stderr.trim() || `bun install exited with code ${code}` });
887
1132
  }
888
1133
  });
889
1134
  });
890
1135
  }
891
1136
  function uninstallGlobal() {
892
- return new Promise((resolve4) => {
1137
+ return new Promise((resolve5) => {
893
1138
  let stderr = "";
894
1139
  const proc = spawn("bun", ["remove", "-g", "@bennys001/claude-code-memory"], { stdio: ["ignore", "ignore", "pipe"] });
895
1140
  proc.stderr.on("data", (data) => {
896
1141
  stderr += data.toString();
897
1142
  });
898
1143
  proc.on("error", (err) => {
899
- resolve4({ success: false, error: err.message });
1144
+ resolve5({ success: false, error: err.message });
900
1145
  });
901
1146
  proc.on("close", (code) => {
902
1147
  if (code === 0) {
903
- resolve4({ success: true });
1148
+ resolve5({ success: true });
904
1149
  } else {
905
- resolve4({ success: false, error: stderr.trim() || `bun remove exited with code ${code}` });
1150
+ resolve5({ success: false, error: stderr.trim() || `bun remove exited with code ${code}` });
906
1151
  }
907
1152
  });
908
1153
  });
@@ -920,15 +1165,15 @@ var REGISTER_ARGS = [
920
1165
  ];
921
1166
  var MANUAL_COMMAND = `claude mcp add --transport stdio --scope user ccm -- ${SERVER_CMD.join(" ")}`;
922
1167
  function removeMcpServer() {
923
- return new Promise((resolve4) => {
1168
+ return new Promise((resolve5) => {
924
1169
  const proc = spawn("claude", ["mcp", "remove", "ccm"], { stdio: "ignore" });
925
- proc.on("error", () => resolve4());
926
- proc.on("close", () => resolve4());
1170
+ proc.on("error", () => resolve5());
1171
+ proc.on("close", () => resolve5());
927
1172
  });
928
1173
  }
929
1174
  async function registerMcpServer() {
930
1175
  await removeMcpServer();
931
- return new Promise((resolve4) => {
1176
+ return new Promise((resolve5) => {
932
1177
  let stdout = "";
933
1178
  let stderr = "";
934
1179
  const proc = spawn("claude", REGISTER_ARGS, { stdio: "pipe" });
@@ -940,13 +1185,13 @@ async function registerMcpServer() {
940
1185
  });
941
1186
  proc.on("error", (err) => {
942
1187
  if (err.code === "ENOENT") {
943
- resolve4({
1188
+ resolve5({
944
1189
  success: false,
945
1190
  error: "Claude CLI not found in PATH",
946
1191
  manualCommand: MANUAL_COMMAND
947
1192
  });
948
1193
  } else {
949
- resolve4({
1194
+ resolve5({
950
1195
  success: false,
951
1196
  error: err.message,
952
1197
  manualCommand: MANUAL_COMMAND
@@ -955,9 +1200,9 @@ async function registerMcpServer() {
955
1200
  });
956
1201
  proc.on("close", (code) => {
957
1202
  if (code === 0) {
958
- resolve4({ success: true, output: (stdout || stderr).trim() });
1203
+ resolve5({ success: true, output: (stdout || stderr).trim() });
959
1204
  } else {
960
- resolve4({
1205
+ resolve5({
961
1206
  success: false,
962
1207
  error: (stderr || stdout).trim() || `Process exited with code ${code}`,
963
1208
  manualCommand: MANUAL_COMMAND
@@ -969,7 +1214,7 @@ async function registerMcpServer() {
969
1214
 
970
1215
  // src/cli/init.ts
971
1216
  import { mkdir as mkdir2 } from "fs/promises";
972
- import { join as join6 } from "path";
1217
+ import { join as join7 } from "path";
973
1218
  import { homedir as homedir3 } from "os";
974
1219
 
975
1220
  // src/cli/seed.ts
@@ -1087,10 +1332,10 @@ function buildSeedNotes(dateStr) {
1087
1332
 
1088
1333
  // src/cli/obsidian.ts
1089
1334
  import { readFile, writeFile as writeFile2, copyFile } from "fs/promises";
1090
- import { join as join5 } from "path";
1335
+ import { join as join6 } from "path";
1091
1336
  import { randomBytes } from "crypto";
1092
1337
  import { homedir as homedir2 } from "os";
1093
- var DEFAULT_CONFIG_PATH = join5(homedir2(), ".config/obsidian/obsidian.json");
1338
+ var DEFAULT_CONFIG_PATH = join6(homedir2(), ".config/obsidian/obsidian.json");
1094
1339
  function generateVaultId() {
1095
1340
  return randomBytes(8).toString("hex");
1096
1341
  }
@@ -1142,27 +1387,27 @@ async function unregisterVaultFromObsidian(vaultPath, configPath = DEFAULT_CONFI
1142
1387
  }
1143
1388
 
1144
1389
  // src/cli/init.ts
1145
- var VAULT_PATH = join6(homedir3(), ".ccm", "knowledge-base");
1390
+ var VAULT_PATH = join7(homedir3(), ".ccm", "knowledge-base");
1146
1391
  var SUBDIRS = ["gotchas", "decisions", "patterns", "references", "_templates"];
1147
1392
  async function executeInit() {
1148
1393
  const steps = [];
1149
1394
  await mkdir2(VAULT_PATH, { recursive: true });
1150
1395
  steps.push({ name: "vault directory", status: "created", detail: VAULT_PATH });
1151
1396
  for (const sub of SUBDIRS) {
1152
- await mkdir2(join6(VAULT_PATH, sub), { recursive: true });
1397
+ await mkdir2(join7(VAULT_PATH, sub), { recursive: true });
1153
1398
  }
1154
1399
  steps.push({ name: "subdirectories", status: "created", detail: SUBDIRS.join(", ") });
1155
- const dotObsidian = join6(VAULT_PATH, ".obsidian");
1400
+ const dotObsidian = join7(VAULT_PATH, ".obsidian");
1156
1401
  await mkdir2(dotObsidian, { recursive: true });
1157
1402
  const dateStr = formatDate(new Date);
1158
1403
  const seedNotes = buildSeedNotes(dateStr);
1159
1404
  for (const note of seedNotes) {
1160
- const fullPath = join6(VAULT_PATH, note.relativePath);
1405
+ const fullPath = join7(VAULT_PATH, note.relativePath);
1161
1406
  const file = Bun.file(fullPath);
1162
1407
  if (await file.exists()) {
1163
1408
  steps.push({ name: note.relativePath, status: "skipped", detail: "already exists" });
1164
1409
  } else {
1165
- await mkdir2(join6(VAULT_PATH, note.relativePath, ".."), { recursive: true });
1410
+ await mkdir2(join7(VAULT_PATH, note.relativePath, ".."), { recursive: true });
1166
1411
  await Bun.write(fullPath, note.content);
1167
1412
  steps.push({ name: note.relativePath, status: "created", detail: "" });
1168
1413
  }
@@ -1232,7 +1477,7 @@ function formatInitSummary(result) {
1232
1477
  // src/index.ts
1233
1478
  import { rm } from "fs/promises";
1234
1479
  import { createInterface } from "readline";
1235
- var VAULT_PATH2 = join7(homedir4(), ".ccm", "knowledge-base");
1480
+ var VAULT_PATH2 = join8(homedir4(), ".ccm", "knowledge-base");
1236
1481
  function parseCliArgs() {
1237
1482
  const args = process.argv.slice(2);
1238
1483
  if (args.includes("--version") || args.includes("-v"))
@@ -1249,7 +1494,24 @@ function parseCliArgs() {
1249
1494
  return "serve";
1250
1495
  return "help";
1251
1496
  }
1252
- function printHelp() {
1497
+ async function printHelp() {
1498
+ let updateLine = "";
1499
+ try {
1500
+ const latest = await fetchLatestVersion();
1501
+ if (latest !== package_default.version) {
1502
+ const notes = await fetchReleaseNotes(latest);
1503
+ const parts = [`
1504
+ ${C.yellow}Update available:${C.reset} v${package_default.version} \u2192 ${C.green}v${latest}${C.reset}`];
1505
+ if (notes.length > 0) {
1506
+ parts.push(`${C.bold}What's new:${C.reset}`);
1507
+ for (const note of notes)
1508
+ parts.push(` ${C.dim}-${C.reset} ${note}`);
1509
+ }
1510
+ parts.push(`Run ${C.cyan}ccm --update${C.reset} to upgrade`);
1511
+ updateLine = parts.join(`
1512
+ `);
1513
+ }
1514
+ } catch {}
1253
1515
  console.log(`${C.bold}claude-code-memory${C.reset} ${C.dim}(ccm)${C.reset} \u2014 Persistent memory for Claude Code
1254
1516
 
1255
1517
  ${C.bold}Usage:${C.reset}
@@ -1262,14 +1524,7 @@ ${C.bold}First-time install:${C.reset}
1262
1524
  ${C.cyan}bun install -g @bennys001/claude-code-memory && ccm --init${C.reset}
1263
1525
 
1264
1526
  ${C.dim}Vault:${C.reset} ~/.ccm/knowledge-base/
1265
- ${C.dim}Docs:${C.reset} https://github.com/bennys001/claude-code-memory`);
1266
- }
1267
- async function fetchLatestVersion() {
1268
- const res = await fetch("https://registry.npmjs.org/@bennys001/claude-code-memory/latest");
1269
- if (!res.ok)
1270
- throw new Error(`npm registry returned ${res.status}`);
1271
- const data = await res.json();
1272
- return data.version;
1527
+ ${C.dim}Docs:${C.reset} https://github.com/bennys001/claude-code-memory${updateLine}`);
1273
1528
  }
1274
1529
  async function runUpdate() {
1275
1530
  console.log(`${C.dim}Checking for updates...${C.reset}`);
@@ -1293,10 +1548,10 @@ ${C.green}Updated to v${latest}${C.reset}`);
1293
1548
  }
1294
1549
  function promptConfirm(question) {
1295
1550
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1296
- return new Promise((resolve4) => {
1551
+ return new Promise((resolve5) => {
1297
1552
  rl.question(question, (answer) => {
1298
1553
  rl.close();
1299
- resolve4(answer.toLowerCase() === "y");
1554
+ resolve5(answer.toLowerCase() === "y");
1300
1555
  });
1301
1556
  });
1302
1557
  }
@@ -1307,7 +1562,7 @@ async function runUninstall() {
1307
1562
  console.log(` ${C.green}+${C.reset} Removed MCP server`);
1308
1563
  await unregisterVaultFromObsidian(VAULT_PATH);
1309
1564
  console.log(` ${C.green}+${C.reset} Unregistered Obsidian vault`);
1310
- const ccmDir = join7(homedir4(), ".ccm");
1565
+ const ccmDir = join8(homedir4(), ".ccm");
1311
1566
  const deleteVault = await promptConfirm(` Delete vault at ${C.dim}${VAULT_PATH}${C.reset}? ${C.dim}(y/N)${C.reset} `);
1312
1567
  if (deleteVault) {
1313
1568
  await rm(ccmDir, { recursive: true, force: true });
@@ -1332,6 +1587,8 @@ async function runInit() {
1332
1587
  }
1333
1588
  async function runServer() {
1334
1589
  const entries = await loadVault(VAULT_PATH2);
1590
+ const { stop: stopWatcher, stats: watcherStats } = watchVault(VAULT_PATH2, entries);
1591
+ initUpdateCheck();
1335
1592
  console.error(`Loaded ${entries.length} notes from ${VAULT_PATH2}`);
1336
1593
  const server = new McpServer({
1337
1594
  name: "ccm",
@@ -1344,9 +1601,11 @@ async function runServer() {
1344
1601
  registerWriteTool(server, entries, VAULT_PATH2);
1345
1602
  registerFetchPageTool(server);
1346
1603
  registerResearchTool(server, entries, VAULT_PATH2);
1604
+ registerDiagnosticsTool(server, entries, watcherStats);
1347
1605
  const transport = new StdioServerTransport;
1348
1606
  await server.connect(transport);
1349
1607
  const shutdown = async () => {
1608
+ stopWatcher();
1350
1609
  await server.close();
1351
1610
  process.exit(0);
1352
1611
  };
@@ -1358,8 +1617,7 @@ if (cli === "version") {
1358
1617
  console.log(package_default.version);
1359
1618
  process.exit(0);
1360
1619
  } else if (cli === "help") {
1361
- printHelp();
1362
- process.exit(0);
1620
+ printHelp().then(() => process.exit(0)).catch(() => process.exit(0));
1363
1621
  } else if (cli === "update") {
1364
1622
  runUpdate().catch((err) => {
1365
1623
  console.error("Fatal:", err);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bennys001/claude-code-memory",
3
3
  "publishConfig": { "access": "public" },
4
- "version": "0.11.1",
4
+ "version": "0.12.3",
5
5
  "description": "MCP server that gives Claude Code persistent memory via an Obsidian knowledge vault",
6
6
  "module": "dist/index.js",
7
7
  "main": "dist/index.js",