mcp-memory-bucket 0.4.2 → 0.5.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/dist/client/assets/index-DSHtiYCt.js +1329 -0
- package/dist/client/index.html +1 -1
- package/dist/src/memory/repository.js +15 -0
- package/dist/src/server.js +1 -1
- package/dist/src/store/sync.js +25 -4
- package/dist/src/web/routes.js +130 -2
- package/package.json +1 -1
- package/dist/client/assets/index-BXCTjiGA.js +0 -894
package/dist/client/index.html
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
:root[data-theme='dark'] { color-scheme: dark; }
|
|
28
28
|
body { font-family: system-ui, sans-serif; margin: 0; background: var(--bg); color: var(--fg); }
|
|
29
29
|
</style>
|
|
30
|
-
<script type="module" crossorigin src="/assets/index-
|
|
30
|
+
<script type="module" crossorigin src="/assets/index-DSHtiYCt.js"></script>
|
|
31
31
|
</head>
|
|
32
32
|
<body>
|
|
33
33
|
<mem-bucket-app></mem-bucket-app>
|
|
@@ -278,6 +278,21 @@ export class MemoryRepository {
|
|
|
278
278
|
fs.unlinkSync(existing.source_path);
|
|
279
279
|
removeFile(this.db, 'memory_docs', existing.source_path);
|
|
280
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Strips the doc's frontmatter entirely, leaving a bare markdown file with just the body — the
|
|
283
|
+
* inverse of memory_create, for turning a managed memory doc back into a plain dropped-in file.
|
|
284
|
+
* The file stays at the same path, but loses its `key` (so it drops out of key-based lookup) and
|
|
285
|
+
* its `id` — the upsertFile call below immediately re-derives a fresh one from the filename via
|
|
286
|
+
* deriveFrontmatter's fallback, so the caller should treat this doc as gone under its old id
|
|
287
|
+
* once this returns (look it up by the new filename-derived id/key instead).
|
|
288
|
+
*/
|
|
289
|
+
stripFrontmatter(id) {
|
|
290
|
+
const existing = this.get(id);
|
|
291
|
+
if (!existing)
|
|
292
|
+
throw new Error(`memory doc with id "${id}" not found`);
|
|
293
|
+
writeMarkdownFile(existing.source_path, {}, existing.body);
|
|
294
|
+
upsertFile(this.db, this.syncSpec, existing.source_path);
|
|
295
|
+
}
|
|
281
296
|
/**
|
|
282
297
|
* Deletes many memory docs by id in one call — e.g. cleaning up a batch of
|
|
283
298
|
* abandoned docs found via search(). Returns per-id results so one bad id
|
package/dist/src/server.js
CHANGED
|
@@ -62,7 +62,7 @@ function buildMcpServer() {
|
|
|
62
62
|
}
|
|
63
63
|
const app = express();
|
|
64
64
|
app.use(express.json());
|
|
65
|
-
app.use(buildWebRouter(db, config, skillRepo, memoryRepo));
|
|
65
|
+
app.use(buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, memorySpec));
|
|
66
66
|
app.use(express.static(path.join(packageRoot, 'dist', 'client')));
|
|
67
67
|
app.post('/mcp', async (req, res) => {
|
|
68
68
|
const server = buildMcpServer();
|
package/dist/src/store/sync.js
CHANGED
|
@@ -4,6 +4,8 @@ import chokidar, {} from 'chokidar';
|
|
|
4
4
|
import { readMarkdownFile } from './markdown-file.js';
|
|
5
5
|
import { flattenTags } from './db.js';
|
|
6
6
|
import { extractDates, toLocalDate } from './date-extract.js';
|
|
7
|
+
import { slugify } from './slug.js';
|
|
8
|
+
import { normalizeKey } from '../types.js';
|
|
7
9
|
// `paused` is deliberately absent from both lists: it's a local-only cache column (see
|
|
8
10
|
// SkillRepository/MemoryRepository#setPaused) that never round-trips through frontmatter, so a
|
|
9
11
|
// file add/change/rescan must never overwrite it via the INSERT/ON CONFLICT UPDATE below.
|
|
@@ -16,7 +18,7 @@ export function skillSyncSpec(sources) {
|
|
|
16
18
|
matchesFile: (filePath) => path.basename(filePath) === 'SKILL.md',
|
|
17
19
|
columns: skillColumns,
|
|
18
20
|
getId: (fm) => fm.name,
|
|
19
|
-
toRow: (fm) => ({
|
|
21
|
+
toRow: (fm, _sourcePath, mtimeMs) => ({
|
|
20
22
|
id: fm.name,
|
|
21
23
|
description: fm.description,
|
|
22
24
|
owner: fm.metadata?.owner ?? null,
|
|
@@ -25,7 +27,7 @@ export function skillSyncSpec(sources) {
|
|
|
25
27
|
trigger_phrases: JSON.stringify(fm.trigger_phrases ?? []),
|
|
26
28
|
extends: fm.metadata?.extends ?? null,
|
|
27
29
|
deprecated: fm.deprecated ? 1 : 0,
|
|
28
|
-
created_at: fm.created_at ??
|
|
30
|
+
created_at: fm.created_at ?? new Date(mtimeMs).toISOString(),
|
|
29
31
|
}),
|
|
30
32
|
};
|
|
31
33
|
}
|
|
@@ -36,6 +38,22 @@ export function memorySyncSpec(sources) {
|
|
|
36
38
|
matchesFile: (filePath) => filePath.endsWith('.md'),
|
|
37
39
|
columns: memoryColumns,
|
|
38
40
|
getId: (fm) => fm.id,
|
|
41
|
+
deriveFrontmatter: (fm, filePath, mtimeMs) => {
|
|
42
|
+
const basename = path.basename(filePath, '.md');
|
|
43
|
+
const fallbackId = slugify(basename) || 'untitled';
|
|
44
|
+
return {
|
|
45
|
+
...fm,
|
|
46
|
+
id: fm.id ?? fallbackId,
|
|
47
|
+
key: fm.key ?? normalizeKey(fallbackId),
|
|
48
|
+
key_type: fm.key_type ?? 'freeform',
|
|
49
|
+
description: fm.description ?? basename,
|
|
50
|
+
doc_type: fm.doc_type ?? 'other',
|
|
51
|
+
status: fm.status ?? 'active',
|
|
52
|
+
tags: fm.tags ?? [],
|
|
53
|
+
related_to: fm.related_to ?? null,
|
|
54
|
+
created_at: fm.created_at ?? new Date(mtimeMs).toISOString(),
|
|
55
|
+
};
|
|
56
|
+
},
|
|
39
57
|
toRow: (fm) => ({
|
|
40
58
|
id: fm.id,
|
|
41
59
|
key: fm.key,
|
|
@@ -76,12 +94,15 @@ export function upsertFile(db, spec, filePath) {
|
|
|
76
94
|
const parsed = readMarkdownFile(filePath);
|
|
77
95
|
if (existing && existing.mtime_ms === parsed.mtimeMs)
|
|
78
96
|
return; // unchanged, skip reprocessing
|
|
79
|
-
const
|
|
97
|
+
const frontmatter = spec.deriveFrontmatter
|
|
98
|
+
? spec.deriveFrontmatter(parsed.frontmatter, filePath, parsed.mtimeMs)
|
|
99
|
+
: parsed.frontmatter;
|
|
100
|
+
const id = spec.getId(frontmatter);
|
|
80
101
|
if (!id) {
|
|
81
102
|
console.error(`[memory-bucket] skipping ${filePath}: missing required id field in frontmatter`);
|
|
82
103
|
return;
|
|
83
104
|
}
|
|
84
|
-
const row = spec.toRow(
|
|
105
|
+
const row = spec.toRow(frontmatter, filePath, parsed.mtimeMs);
|
|
85
106
|
const root = rootForFile(spec.sources, filePath);
|
|
86
107
|
const cols = [...spec.columns, 'source_path', 'root', 'body', 'mtime_ms'];
|
|
87
108
|
const values = [...spec.columns.map((c) => row[c]), filePath, root, parsed.body, parsed.mtimeMs];
|
package/dist/src/web/routes.js
CHANGED
|
@@ -2,7 +2,9 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import express from 'express';
|
|
5
|
+
import matter from 'gray-matter';
|
|
5
6
|
import { saveRoot, removeRoot as removeRootFromConfig, sanitizeRootName } from '../config.js';
|
|
7
|
+
import { initialScan } from '../store/sync.js';
|
|
6
8
|
function asArray(v) {
|
|
7
9
|
if (v === undefined)
|
|
8
10
|
return [];
|
|
@@ -253,8 +255,21 @@ function buildHealth(db) {
|
|
|
253
255
|
.map((m) => m.id);
|
|
254
256
|
return { danglingExtends, danglingRelatedTo, emptyTriggerPhrases, staleActiveMemoryDocs };
|
|
255
257
|
}
|
|
256
|
-
export function buildWebRouter(db, config, skillRepo, memoryRepo) {
|
|
258
|
+
export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, memorySpec) {
|
|
257
259
|
const router = express.Router();
|
|
260
|
+
router.post('/api/rebuild-cache', (_req, res) => {
|
|
261
|
+
try {
|
|
262
|
+
db.exec(`DELETE FROM skills; DELETE FROM memory_docs; DELETE FROM search_index; DELETE FROM doc_dates;`);
|
|
263
|
+
initialScan(db, skillSpec);
|
|
264
|
+
initialScan(db, memorySpec);
|
|
265
|
+
const skillCount = db.prepare(`SELECT COUNT(*) AS n FROM skills`).get().n;
|
|
266
|
+
const memoryCount = db.prepare(`SELECT COUNT(*) AS n FROM memory_docs`).get().n;
|
|
267
|
+
res.json({ skillCount, memoryCount });
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
res.status(500).json({ error: err.message });
|
|
271
|
+
}
|
|
272
|
+
});
|
|
258
273
|
router.get('/api/entries', (req, res) => {
|
|
259
274
|
res.json(queryEntries(db, req));
|
|
260
275
|
});
|
|
@@ -271,7 +286,26 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo) {
|
|
|
271
286
|
}
|
|
272
287
|
const tags = JSON.parse(row.tags);
|
|
273
288
|
const trigger_phrases = row.trigger_phrases ? JSON.parse(row.trigger_phrases) : undefined;
|
|
274
|
-
|
|
289
|
+
// A memory doc's cache row always looks fully populated even for a bare file (deriveFrontmatter's
|
|
290
|
+
// fallback backfills id/key/etc — see sync.ts), so whether it actually has an authored
|
|
291
|
+
// frontmatter block has to be checked on disk. This drives "Add frontmatter" (memory only —
|
|
292
|
+
// write real values in for the first time) vs "Edit"/"Delete frontmatter". Skills have no such
|
|
293
|
+
// fallback, so a skills row reaching this route always has real frontmatter; this stays false
|
|
294
|
+
// only in the memory_docs case in practice.
|
|
295
|
+
//
|
|
296
|
+
// `raw_file` is the true on-disk content (frontmatter block + body) for the Raw view — distinct
|
|
297
|
+
// from the cache's `body` column, which is always frontmatter-stripped (see readMarkdownFile).
|
|
298
|
+
let has_frontmatter = true;
|
|
299
|
+
let raw_file;
|
|
300
|
+
try {
|
|
301
|
+
raw_file = fs.readFileSync(row.source_path, 'utf-8');
|
|
302
|
+
has_frontmatter = Object.keys(matter(raw_file).data).length > 0;
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
// file unreadable/missing — treat as having frontmatter so the UI doesn't offer to "add"
|
|
306
|
+
// one for a doc it can't actually reach; the existing edit/delete-doc paths will 404 instead.
|
|
307
|
+
}
|
|
308
|
+
res.json({ ...row, tags, trigger_phrases, has_frontmatter, raw_file });
|
|
275
309
|
});
|
|
276
310
|
router.patch('/api/entries/:table/:id/deprecated', (req, res) => {
|
|
277
311
|
const { table, id } = req.params;
|
|
@@ -313,6 +347,73 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo) {
|
|
|
313
347
|
const results = table === 'skills' ? skillRepo.bulkUpdate(ids, { deprecated }) : memoryRepo.bulkUpdate(ids, { deprecated });
|
|
314
348
|
res.json({ results });
|
|
315
349
|
});
|
|
350
|
+
// General frontmatter edit — covers both "edit an existing doc's fields" and "add frontmatter
|
|
351
|
+
// to a bare file" (a bare memory doc already has a derived id/key from deriveFrontmatter, so
|
|
352
|
+
// "add" is just this same call supplying real values). Memory `key` is editable here (update()
|
|
353
|
+
// normalizes it). Skill `name` is not — SkillRepository.update() rejects it outright since
|
|
354
|
+
// renaming requires a real folder move (see the rename route below).
|
|
355
|
+
router.patch('/api/entries/:table/:id', (req, res) => {
|
|
356
|
+
const { table, id } = req.params;
|
|
357
|
+
const { frontmatter } = req.body;
|
|
358
|
+
if (table !== 'skills' && table !== 'memory_docs') {
|
|
359
|
+
res.status(400).json({ error: 'table must be "skills" or "memory_docs"' });
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (!id) {
|
|
363
|
+
res.status(400).json({ error: 'id is required' });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (!frontmatter || typeof frontmatter !== 'object') {
|
|
367
|
+
res.status(400).json({ error: 'body must be { frontmatter: object }' });
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (table === 'skills' && 'name' in frontmatter) {
|
|
371
|
+
res.status(400).json({ error: 'name cannot be changed via this route — use rename' });
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
const updated = table === 'skills' ? skillRepo.update(id, frontmatter) : memoryRepo.update(id, frontmatter);
|
|
376
|
+
res.json(updated);
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
res.status(404).json({ error: err.message });
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
router.post('/api/entries/skills/:name/rename', (req, res) => {
|
|
383
|
+
const { name } = req.params;
|
|
384
|
+
const { new_name } = req.body;
|
|
385
|
+
if (!name) {
|
|
386
|
+
res.status(400).json({ error: 'name is required' });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (!new_name) {
|
|
390
|
+
res.status(400).json({ error: 'body must be { new_name: string }' });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
const renamed = skillRepo.rename(name, new_name);
|
|
395
|
+
res.json(renamed);
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
res.status(400).json({ error: err.message });
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
// Memory docs only — skill frontmatter (name/description) is required by the agentskills.io
|
|
402
|
+
// spec, so stripping it would produce a non-conformant SKILL.md no compliant agent can load.
|
|
403
|
+
router.delete('/api/entries/memory_docs/:id/frontmatter', (req, res) => {
|
|
404
|
+
const { id } = req.params;
|
|
405
|
+
if (!id) {
|
|
406
|
+
res.status(400).json({ error: 'id is required' });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
memoryRepo.stripFrontmatter(id);
|
|
411
|
+
res.json({ id, frontmatterRemoved: true });
|
|
412
|
+
}
|
|
413
|
+
catch (err) {
|
|
414
|
+
res.status(404).json({ error: err.message });
|
|
415
|
+
}
|
|
416
|
+
});
|
|
316
417
|
// `paused` is a local-only cache toggle (see SkillRepository/MemoryRepository#setPaused) — it
|
|
317
418
|
// never touches the source file, so this goes through setPaused, not update()/bulkUpdate().
|
|
318
419
|
router.patch('/api/entries/:table/:id/paused', (req, res) => {
|
|
@@ -479,5 +580,32 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo) {
|
|
|
479
580
|
const parent = path.dirname(dirPath) !== dirPath ? path.dirname(dirPath) : null;
|
|
480
581
|
res.json({ path: dirPath, parent, entries });
|
|
481
582
|
});
|
|
583
|
+
router.post('/api/fs/mkdir', (req, res) => {
|
|
584
|
+
const { path: parentPath, name } = req.body;
|
|
585
|
+
if (!parentPath || !path.isAbsolute(parentPath)) {
|
|
586
|
+
res.status(400).json({ error: 'path must be an absolute directory path' });
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (!name || name.trim() !== name || name.includes('/') || name === '.' || name === '..') {
|
|
590
|
+
res.status(400).json({ error: 'invalid folder name' });
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (!fs.existsSync(parentPath) || !fs.statSync(parentPath).isDirectory()) {
|
|
594
|
+
res.status(400).json({ error: `not a directory: ${parentPath}` });
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const newDirPath = path.join(parentPath, name);
|
|
598
|
+
if (fs.existsSync(newDirPath)) {
|
|
599
|
+
res.status(409).json({ error: `already exists: ${newDirPath}` });
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
try {
|
|
603
|
+
fs.mkdirSync(newDirPath);
|
|
604
|
+
res.json({ path: newDirPath, name });
|
|
605
|
+
}
|
|
606
|
+
catch (err) {
|
|
607
|
+
res.status(500).json({ error: err.message });
|
|
608
|
+
}
|
|
609
|
+
});
|
|
482
610
|
return router;
|
|
483
611
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-memory-bucket",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server exposing skill_* (reusable coding patterns) and memory_* (point-in-time working context) tools over a markdown+frontmatter source, cached into SQLite at runtime.",
|
|
6
6
|
"repository": {
|