@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.
- package/README.md +26 -4
- package/dist/index.js +334 -76
- 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
|
-
|
|
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
|
-
###
|
|
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 --
|
|
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.
|
|
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
|
|
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
|
|
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 =
|
|
257
|
-
const rel =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 ??
|
|
559
|
-
const isGlobalDir =
|
|
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(
|
|
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(
|
|
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
|
|
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.
|
|
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 =
|
|
657
|
-
const rel =
|
|
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
|
-
|
|
662
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
773
|
-
const tempPath =
|
|
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((
|
|
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
|
-
|
|
1125
|
+
resolve5({ success: false, error: err.message });
|
|
881
1126
|
});
|
|
882
1127
|
proc.on("close", (code) => {
|
|
883
1128
|
if (code === 0) {
|
|
884
|
-
|
|
1129
|
+
resolve5({ success: true });
|
|
885
1130
|
} else {
|
|
886
|
-
|
|
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((
|
|
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
|
-
|
|
1144
|
+
resolve5({ success: false, error: err.message });
|
|
900
1145
|
});
|
|
901
1146
|
proc.on("close", (code) => {
|
|
902
1147
|
if (code === 0) {
|
|
903
|
-
|
|
1148
|
+
resolve5({ success: true });
|
|
904
1149
|
} else {
|
|
905
|
-
|
|
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((
|
|
1168
|
+
return new Promise((resolve5) => {
|
|
924
1169
|
const proc = spawn("claude", ["mcp", "remove", "ccm"], { stdio: "ignore" });
|
|
925
|
-
proc.on("error", () =>
|
|
926
|
-
proc.on("close", () =>
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1203
|
+
resolve5({ success: true, output: (stdout || stderr).trim() });
|
|
959
1204
|
} else {
|
|
960
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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((
|
|
1551
|
+
return new Promise((resolve5) => {
|
|
1297
1552
|
rl.question(question, (answer) => {
|
|
1298
1553
|
rl.close();
|
|
1299
|
-
|
|
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 =
|
|
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.
|
|
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",
|