mcp-memory-bucket 0.4.3 → 0.5.1
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-DPkF4wtg.js +1486 -0
- package/dist/client/index.html +1 -1
- package/dist/src/memory/repository.js +16 -1
- package/dist/src/memory/tools.js +9 -6
- package/dist/src/server.js +1 -1
- package/dist/src/shared/relocate-tool.js +2 -1
- package/dist/src/shared/status.js +23 -0
- package/dist/src/skills/tools.js +7 -6
- package/dist/src/store/sync.js +6 -5
- package/dist/src/web/routes.js +103 -2
- package/package.json +1 -1
- package/dist/client/assets/index-DvTP9na1.js +0 -925
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-DPkF4wtg.js"></script>
|
|
31
31
|
</head>
|
|
32
32
|
<body>
|
|
33
33
|
<mem-bucket-app></mem-bucket-app>
|
|
@@ -172,7 +172,7 @@ export class MemoryRepository {
|
|
|
172
172
|
description: input.description,
|
|
173
173
|
doc_type: input.doc_type,
|
|
174
174
|
tags: input.tags ?? [],
|
|
175
|
-
status: 'active',
|
|
175
|
+
status: input.status ?? 'active',
|
|
176
176
|
related_to: input.related_to ?? null,
|
|
177
177
|
deprecated: false,
|
|
178
178
|
created_at: new Date().toISOString(),
|
|
@@ -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/memory/tools.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { statusSchema } from '../shared/status.js';
|
|
2
3
|
const MEMORY_DOC_TYPES = ['plan', 'spec', 'sql', 'testing-todo', 'discovery', 'session-summary', 'other'];
|
|
3
4
|
const MEMORY_KEY_TYPES = ['ticket', 'freeform'];
|
|
4
|
-
const
|
|
5
|
+
const MEMORY_STATUS_DEFAULTS = ['active', 'shipped', 'abandoned'];
|
|
5
6
|
const AUTHORING_SKILL_HINT = "Before your first call in a session, run skill_get(\"memory-bucket-authoring\") to learn the exact frontmatter schema and conventions — don't guess the shape.";
|
|
6
7
|
export function registerMemoryTools(mcp, repo) {
|
|
7
8
|
const roots = repo.listRoots();
|
|
@@ -26,7 +27,7 @@ export function registerMemoryTools(mcp, repo) {
|
|
|
26
27
|
mcp.tool('memory_search', 'Full-text search over memory doc description/body/tags (grep/find-like, ranked by relevance) — unlike memory_get\'s exact-key lookup, this searches by content across all keys. `query` is raw SQLite FTS5 MATCH syntax: bare words, "exact phrases", prefix* wildcards, AND/OR/NOT boolean operators; hyphenated/punctuated terms must be quoted, e.g. "blue-green". Can be combined with doc_type/status/tag filters. Returns ranked hits with a highlighted snippet, not the full body — call memory_get(key) or fetch by id for that. Paused docs are hidden by default — pass include_paused to see them.', {
|
|
27
28
|
query: z.string().describe('FTS5 match expression, e.g. `migration AND rollback` or `"blue green"`'),
|
|
28
29
|
doc_type: z.enum(MEMORY_DOC_TYPES).optional(),
|
|
29
|
-
status:
|
|
30
|
+
status: statusSchema(MEMORY_STATUS_DEFAULTS).optional(),
|
|
30
31
|
tag: z.string().optional(),
|
|
31
32
|
...(multiRoot ? { root: z.string().optional().describe(`filter to one root: ${rootNames}`) } : {}),
|
|
32
33
|
limit: z.number().int().positive().max(100).optional(),
|
|
@@ -45,7 +46,7 @@ export function registerMemoryTools(mcp, repo) {
|
|
|
45
46
|
ids: z.array(z.string()).min(1),
|
|
46
47
|
add_tags: z.array(z.string()).optional(),
|
|
47
48
|
remove_tags: z.array(z.string()).optional(),
|
|
48
|
-
status:
|
|
49
|
+
status: statusSchema(MEMORY_STATUS_DEFAULTS).optional(),
|
|
49
50
|
related_to: z.string().nullable().optional(),
|
|
50
51
|
deprecated: z.boolean().optional().describe('marks docs as deprecated (or un-deprecates when false) — independent of status'),
|
|
51
52
|
}, async ({ ids, add_tags, remove_tags, status, related_to, deprecated }) => {
|
|
@@ -59,12 +60,13 @@ export function registerMemoryTools(mcp, repo) {
|
|
|
59
60
|
description: z.string().describe('distinguishes this doc from others sharing the same key'),
|
|
60
61
|
body: z.string(),
|
|
61
62
|
tags: z.array(z.string()).optional(),
|
|
63
|
+
status: statusSchema(MEMORY_STATUS_DEFAULTS).optional().describe('defaults to "active"'),
|
|
62
64
|
related_to: z.string().optional().describe('id of a related doc, e.g. a spec linking to its plan'),
|
|
63
65
|
folder: z.string().optional().describe('optional subdirectory under the memory root'),
|
|
64
66
|
...(multiRoot ? { root: z.string().describe(`which configured memory root to write into: ${rootNames}`) } : {}),
|
|
65
|
-
}, async ({ key, key_type, doc_type, description, body, tags, related_to, folder, root }) => {
|
|
67
|
+
}, async ({ key, key_type, doc_type, description, body, tags, status, related_to, folder, root }) => {
|
|
66
68
|
try {
|
|
67
|
-
const doc = repo.create({ key, key_type, doc_type, description, body, tags, related_to, folder, root });
|
|
69
|
+
const doc = repo.create({ key, key_type, doc_type, description, body, tags, status, related_to, folder, root });
|
|
68
70
|
return { content: [{ type: 'text', text: JSON.stringify(doc, null, 2) }] };
|
|
69
71
|
}
|
|
70
72
|
catch (err) {
|
|
@@ -78,6 +80,7 @@ export function registerMemoryTools(mcp, repo) {
|
|
|
78
80
|
description: z.string().describe('distinguishes this doc from others sharing the same key'),
|
|
79
81
|
body: z.string(),
|
|
80
82
|
tags: z.array(z.string()).optional(),
|
|
83
|
+
status: statusSchema(MEMORY_STATUS_DEFAULTS).optional().describe('defaults to "active"'),
|
|
81
84
|
related_to: z.string().optional(),
|
|
82
85
|
folder: z.string().optional(),
|
|
83
86
|
...(multiRoot ? { root: z.string().describe(`which configured memory root to write into: ${rootNames}`) } : {}),
|
|
@@ -94,7 +97,7 @@ export function registerMemoryTools(mcp, repo) {
|
|
|
94
97
|
description: z.string().optional(),
|
|
95
98
|
body: z.string().optional(),
|
|
96
99
|
tags: z.array(z.string()).optional(),
|
|
97
|
-
status:
|
|
100
|
+
status: statusSchema(MEMORY_STATUS_DEFAULTS).optional(),
|
|
98
101
|
related_to: z.string().optional(),
|
|
99
102
|
deprecated: z.boolean().optional().describe('marks the doc as deprecated (or un-deprecates when false) — independent of status'),
|
|
100
103
|
}, async ({ id, body, ...frontmatterFields }) => {
|
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();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { relocate, relocateMany } from './relocate.js';
|
|
3
|
+
import { statusSchema } from './status.js';
|
|
3
4
|
const AUTHORING_SKILL_HINT = "Before your first call in a session, run skill_get(\"memory-bucket-authoring\") to learn the exact frontmatter schema and conventions — don't guess the shape.";
|
|
4
5
|
const overridesSchema = z
|
|
5
6
|
.object({
|
|
@@ -13,7 +14,7 @@ const overridesSchema = z
|
|
|
13
14
|
key_type: z.enum(['ticket', 'freeform']).optional().describe('memory target only'),
|
|
14
15
|
doc_type: z.enum(['plan', 'spec', 'sql', 'testing-todo', 'discovery', 'session-summary', 'other']).optional().describe('memory target only'),
|
|
15
16
|
tags: z.array(z.string()).optional(),
|
|
16
|
-
status:
|
|
17
|
+
status: statusSchema(['stable', 'beta', 'unreviewed', 'active', 'shipped', 'abandoned']).optional(),
|
|
17
18
|
folder: z.string().optional().describe('optional subdirectory under the target root'),
|
|
18
19
|
root: z.string().optional().describe('which configured root to write into; required if multiple roots exist for the target type'),
|
|
19
20
|
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Status is a free-form label, not a closed enum — the DB column has no CHECK
|
|
4
|
+
* constraint and the web UI's filter dropdown is populated from distinct values
|
|
5
|
+
* actually in use (see buildFacets in web/routes.ts), so any normalized string
|
|
6
|
+
* works end to end. This schema only enforces a consistent shape (lowercase,
|
|
7
|
+
* hyphenated) so casing/typo variants don't silently fragment that dropdown.
|
|
8
|
+
*/
|
|
9
|
+
const STATUS_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
10
|
+
export function normalizeStatus(status) {
|
|
11
|
+
return status.trim().toLowerCase().replace(/\s+/g, '-');
|
|
12
|
+
}
|
|
13
|
+
export function statusSchema(defaults) {
|
|
14
|
+
return z
|
|
15
|
+
.string()
|
|
16
|
+
.min(1)
|
|
17
|
+
.max(40)
|
|
18
|
+
.transform(normalizeStatus)
|
|
19
|
+
.refine((s) => STATUS_PATTERN.test(s), {
|
|
20
|
+
message: 'status must be lowercase alphanumeric with hyphens, e.g. "active" or "needs-review"',
|
|
21
|
+
})
|
|
22
|
+
.describe(`free-form status label, normalized to lowercase-hyphenated; conventional values: ${defaults.join(', ')} — but any value is accepted`);
|
|
23
|
+
}
|
package/dist/src/skills/tools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
2
|
+
import { statusSchema } from '../shared/status.js';
|
|
3
|
+
const SKILL_STATUS_DEFAULTS = ['stable', 'beta', 'unreviewed'];
|
|
3
4
|
const SKILL_NAME_DESCRIPTION = 'stable id, must be 1-64 chars, lowercase letters/numbers/hyphens only, no leading/trailing/consecutive hyphens — this becomes the skill\'s folder name (agentskills.io spec requirement)';
|
|
4
5
|
const AUTHORING_SKILL_HINT = "Before your first call in a session, run skill_get(\"memory-bucket-authoring\") to learn the exact frontmatter schema and conventions — don't guess the shape.";
|
|
5
6
|
export function registerSkillTools(mcp, repo) {
|
|
@@ -21,7 +22,7 @@ export function registerSkillTools(mcp, repo) {
|
|
|
21
22
|
});
|
|
22
23
|
mcp.tool('skill_search', 'Full-text search over skill description/body/tags (grep/find-like, ranked by relevance) — unlike skill_list\'s substring metadata filter, this searches the full markdown body. `query` is raw SQLite FTS5 MATCH syntax: bare words, "exact phrases", prefix* wildcards, AND/OR/NOT boolean operators; hyphenated/punctuated terms must be quoted, e.g. "blue-green". Can be combined with status/owner/tag filters. Returns ranked hits with a highlighted snippet, not the full body — call skill_get on a hit\'s name for that. Paused skills are hidden by default — pass include_paused to see them.', {
|
|
23
24
|
query: z.string().describe('FTS5 match expression, e.g. `deploy AND rollback` or `"blue green"`'),
|
|
24
|
-
status:
|
|
25
|
+
status: statusSchema(SKILL_STATUS_DEFAULTS).optional(),
|
|
25
26
|
owner: z.string().optional(),
|
|
26
27
|
tag: z.string().optional(),
|
|
27
28
|
limit: z.number().int().positive().max(100).optional(),
|
|
@@ -42,7 +43,7 @@ export function registerSkillTools(mcp, repo) {
|
|
|
42
43
|
add_tags: z.array(z.string()).optional(),
|
|
43
44
|
remove_tags: z.array(z.string()).optional(),
|
|
44
45
|
owner: z.string().nullable().optional(),
|
|
45
|
-
status:
|
|
46
|
+
status: statusSchema(SKILL_STATUS_DEFAULTS).optional(),
|
|
46
47
|
extends: z.string().nullable().optional(),
|
|
47
48
|
deprecated: z.boolean().optional().describe('marks skills as deprecated (or un-deprecates when false) — independent of status'),
|
|
48
49
|
}, async ({ names, add_tags, remove_tags, owner, status, extends: extendsId, deprecated }) => {
|
|
@@ -70,7 +71,7 @@ export function registerSkillTools(mcp, repo) {
|
|
|
70
71
|
license: z.string().optional(),
|
|
71
72
|
compatibility: z.string().max(500).optional().describe('only needed if the skill has specific environment requirements'),
|
|
72
73
|
owner: z.string().optional().describe('squad or "company" — stored in frontmatter.metadata, unused for resolution in V0'),
|
|
73
|
-
status:
|
|
74
|
+
status: statusSchema(SKILL_STATUS_DEFAULTS).optional().describe('defaults to "unreviewed"; stored in frontmatter.metadata'),
|
|
74
75
|
tags: z.array(z.string()).optional(),
|
|
75
76
|
trigger_phrases: z.array(z.string()).optional(),
|
|
76
77
|
extends: z.string().optional().describe('reserved for a future overlay mechanism — stored in frontmatter.metadata'),
|
|
@@ -92,7 +93,7 @@ export function registerSkillTools(mcp, repo) {
|
|
|
92
93
|
license: z.string().optional(),
|
|
93
94
|
compatibility: z.string().max(500).optional(),
|
|
94
95
|
owner: z.string().optional(),
|
|
95
|
-
status:
|
|
96
|
+
status: statusSchema(SKILL_STATUS_DEFAULTS).optional(),
|
|
96
97
|
tags: z.array(z.string()).optional(),
|
|
97
98
|
trigger_phrases: z.array(z.string()).optional(),
|
|
98
99
|
extends: z.string().optional(),
|
|
@@ -125,7 +126,7 @@ export function registerSkillTools(mcp, repo) {
|
|
|
125
126
|
license: z.string().optional(),
|
|
126
127
|
compatibility: z.string().max(500).optional(),
|
|
127
128
|
owner: z.string().optional(),
|
|
128
|
-
status:
|
|
129
|
+
status: statusSchema(SKILL_STATUS_DEFAULTS).optional(),
|
|
129
130
|
tags: z.array(z.string()).optional(),
|
|
130
131
|
trigger_phrases: z.array(z.string()).optional(),
|
|
131
132
|
extends: z.string().optional(),
|
package/dist/src/store/sync.js
CHANGED
|
@@ -18,7 +18,7 @@ export function skillSyncSpec(sources) {
|
|
|
18
18
|
matchesFile: (filePath) => path.basename(filePath) === 'SKILL.md',
|
|
19
19
|
columns: skillColumns,
|
|
20
20
|
getId: (fm) => fm.name,
|
|
21
|
-
toRow: (fm) => ({
|
|
21
|
+
toRow: (fm, _sourcePath, mtimeMs) => ({
|
|
22
22
|
id: fm.name,
|
|
23
23
|
description: fm.description,
|
|
24
24
|
owner: fm.metadata?.owner ?? null,
|
|
@@ -27,7 +27,7 @@ export function skillSyncSpec(sources) {
|
|
|
27
27
|
trigger_phrases: JSON.stringify(fm.trigger_phrases ?? []),
|
|
28
28
|
extends: fm.metadata?.extends ?? null,
|
|
29
29
|
deprecated: fm.deprecated ? 1 : 0,
|
|
30
|
-
created_at: fm.created_at ??
|
|
30
|
+
created_at: fm.created_at ?? new Date(mtimeMs).toISOString(),
|
|
31
31
|
}),
|
|
32
32
|
};
|
|
33
33
|
}
|
|
@@ -38,7 +38,7 @@ export function memorySyncSpec(sources) {
|
|
|
38
38
|
matchesFile: (filePath) => filePath.endsWith('.md'),
|
|
39
39
|
columns: memoryColumns,
|
|
40
40
|
getId: (fm) => fm.id,
|
|
41
|
-
deriveFrontmatter: (fm, filePath) => {
|
|
41
|
+
deriveFrontmatter: (fm, filePath, mtimeMs) => {
|
|
42
42
|
const basename = path.basename(filePath, '.md');
|
|
43
43
|
const fallbackId = slugify(basename) || 'untitled';
|
|
44
44
|
return {
|
|
@@ -51,6 +51,7 @@ export function memorySyncSpec(sources) {
|
|
|
51
51
|
status: fm.status ?? 'active',
|
|
52
52
|
tags: fm.tags ?? [],
|
|
53
53
|
related_to: fm.related_to ?? null,
|
|
54
|
+
created_at: fm.created_at ?? new Date(mtimeMs).toISOString(),
|
|
54
55
|
};
|
|
55
56
|
},
|
|
56
57
|
toRow: (fm) => ({
|
|
@@ -94,14 +95,14 @@ export function upsertFile(db, spec, filePath) {
|
|
|
94
95
|
if (existing && existing.mtime_ms === parsed.mtimeMs)
|
|
95
96
|
return; // unchanged, skip reprocessing
|
|
96
97
|
const frontmatter = spec.deriveFrontmatter
|
|
97
|
-
? spec.deriveFrontmatter(parsed.frontmatter, filePath)
|
|
98
|
+
? spec.deriveFrontmatter(parsed.frontmatter, filePath, parsed.mtimeMs)
|
|
98
99
|
: parsed.frontmatter;
|
|
99
100
|
const id = spec.getId(frontmatter);
|
|
100
101
|
if (!id) {
|
|
101
102
|
console.error(`[memory-bucket] skipping ${filePath}: missing required id field in frontmatter`);
|
|
102
103
|
return;
|
|
103
104
|
}
|
|
104
|
-
const row = spec.toRow(frontmatter, filePath);
|
|
105
|
+
const row = spec.toRow(frontmatter, filePath, parsed.mtimeMs);
|
|
105
106
|
const root = rootForFile(spec.sources, filePath);
|
|
106
107
|
const cols = [...spec.columns, 'source_path', 'root', 'body', 'mtime_ms'];
|
|
107
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) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-memory-bucket",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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": {
|