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.
@@ -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-DvTP9na1.js"></script>
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
@@ -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 MEMORY_STATUS = ['active', 'shipped', 'abandoned'];
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: z.enum(MEMORY_STATUS).optional(),
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: z.enum(MEMORY_STATUS).optional(),
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: z.enum(MEMORY_STATUS).optional(),
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 }) => {
@@ -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: z.enum(['stable', 'beta', 'unreviewed', 'active', 'shipped', 'abandoned']).optional(),
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
+ }
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
- const SKILL_STATUS = ['stable', 'beta', 'unreviewed'];
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: z.enum(SKILL_STATUS).optional(),
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: z.enum(SKILL_STATUS).optional(),
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: z.enum(SKILL_STATUS).optional().describe('defaults to "unreviewed"; stored in frontmatter.metadata'),
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: z.enum(SKILL_STATUS).optional(),
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: z.enum(SKILL_STATUS).optional(),
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(),
@@ -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 ?? null,
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];
@@ -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
- res.json({ ...row, tags, trigger_phrases });
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.4.3",
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": {