mcp-memory-bucket 0.3.0 → 0.4.2
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-BXCTjiGA.js +894 -0
- package/dist/client/index.html +23 -3
- package/dist/src/config.js +9 -0
- package/dist/src/memory/repository.js +50 -9
- package/dist/src/memory/tools.js +15 -6
- package/dist/src/server.js +4 -1
- package/dist/src/shared/bucket-root-tool.js +66 -0
- package/dist/src/shared/search-tool.js +16 -1
- package/dist/src/skills/builtin/memory-bucket-authoring/SKILL.md +13 -0
- package/dist/src/skills/repository.js +72 -14
- package/dist/src/skills/tools.js +30 -7
- package/dist/src/store/date-extract.js +49 -0
- package/dist/src/store/db.js +13 -0
- package/dist/src/store/search.js +58 -0
- package/dist/src/store/sync.js +17 -0
- package/dist/src/web/routes.js +87 -14
- package/package.json +2 -2
- package/dist/client/assets/index-DIO48C0V.js +0 -744
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const MONTHS = {
|
|
2
|
+
jan: '01', feb: '02', mar: '03', apr: '04', may: '05', jun: '06',
|
|
3
|
+
jul: '07', aug: '08', sep: '09', oct: '10', nov: '11', dec: '12',
|
|
4
|
+
};
|
|
5
|
+
const ISO_DATE_RE = /\b(\d{4})-(\d{2})-(\d{2})\b/g;
|
|
6
|
+
const WRITTEN_MONTH_RE = /\b(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\.?\s+(\d{1,2}),?\s+(\d{4})\b/gi;
|
|
7
|
+
function isValidDate(year, month, day) {
|
|
8
|
+
if (month < 1 || month > 12 || day < 1 || day > 31)
|
|
9
|
+
return false;
|
|
10
|
+
const d = new Date(Date.UTC(year, month - 1, day));
|
|
11
|
+
return d.getUTCFullYear() === year && d.getUTCMonth() === month - 1 && d.getUTCDate() === day;
|
|
12
|
+
}
|
|
13
|
+
function stripCodeBlocks(body) {
|
|
14
|
+
return body.replace(/```[\s\S]*?```/g, '');
|
|
15
|
+
}
|
|
16
|
+
/** Extracts unique ISO (YYYY-MM-DD) dates mentioned in free text — conservative by design: only unambiguous formats (ISO, written-month-with-year) are matched, code blocks and slash-dates are skipped entirely. */
|
|
17
|
+
export function extractDates(body) {
|
|
18
|
+
const text = stripCodeBlocks(body);
|
|
19
|
+
const dates = new Set();
|
|
20
|
+
for (const match of text.matchAll(ISO_DATE_RE)) {
|
|
21
|
+
const [, y, m, d] = match;
|
|
22
|
+
const year = Number(y);
|
|
23
|
+
const month = Number(m);
|
|
24
|
+
const day = Number(d);
|
|
25
|
+
if (isValidDate(year, month, day))
|
|
26
|
+
dates.add(`${y}-${m}-${d}`);
|
|
27
|
+
}
|
|
28
|
+
for (const match of text.matchAll(WRITTEN_MONTH_RE)) {
|
|
29
|
+
const [, monthName, dayStr, yearStr] = match;
|
|
30
|
+
const month = MONTHS[monthName.toLowerCase()];
|
|
31
|
+
const day = Number(dayStr);
|
|
32
|
+
const year = Number(yearStr);
|
|
33
|
+
if (month && isValidDate(year, Number(month), day)) {
|
|
34
|
+
dates.add(`${yearStr}-${month}-${String(day).padStart(2, '0')}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return Array.from(dates).sort();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Converts a UTC ISO timestamp (e.g. created_at) to the calendar date it
|
|
41
|
+
* falls on in the given IANA timezone (default: the OS timezone this
|
|
42
|
+
* process is running in). Distinct from extractDates()'s output, which is
|
|
43
|
+
* already timezone-naive text — this exists specifically so a UTC instant
|
|
44
|
+
* lands on the same calendar date a user in that timezone would call "today".
|
|
45
|
+
*/
|
|
46
|
+
export function toLocalDate(isoTimestamp, timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone) {
|
|
47
|
+
const formatter = new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
48
|
+
return formatter.format(new Date(isoTimestamp));
|
|
49
|
+
}
|
package/dist/src/store/db.js
CHANGED
|
@@ -14,6 +14,7 @@ export function openCache(dbPath) {
|
|
|
14
14
|
source_path TEXT NOT NULL UNIQUE, -- path to SKILL.md
|
|
15
15
|
root TEXT NOT NULL DEFAULT '', -- name of the configured root this file lives under
|
|
16
16
|
deprecated INTEGER NOT NULL DEFAULT 0,
|
|
17
|
+
paused INTEGER NOT NULL DEFAULT 0, -- local-only: never synced from/to SKILL.md, cache-file scoped
|
|
17
18
|
created_at TEXT,
|
|
18
19
|
body TEXT NOT NULL,
|
|
19
20
|
mtime_ms INTEGER NOT NULL
|
|
@@ -31,6 +32,7 @@ export function openCache(dbPath) {
|
|
|
31
32
|
source_path TEXT NOT NULL UNIQUE,
|
|
32
33
|
root TEXT NOT NULL DEFAULT '', -- name of the configured root this file lives under
|
|
33
34
|
deprecated INTEGER NOT NULL DEFAULT 0,
|
|
35
|
+
paused INTEGER NOT NULL DEFAULT 0, -- local-only: never synced from/to the doc's markdown file, cache-file scoped
|
|
34
36
|
created_at TEXT,
|
|
35
37
|
body TEXT NOT NULL,
|
|
36
38
|
mtime_ms INTEGER NOT NULL
|
|
@@ -46,15 +48,26 @@ export function openCache(dbPath) {
|
|
|
46
48
|
tags,
|
|
47
49
|
tokenize = 'porter unicode61'
|
|
48
50
|
);
|
|
51
|
+
|
|
52
|
+
CREATE TABLE IF NOT EXISTS doc_dates (
|
|
53
|
+
ref_table TEXT NOT NULL,
|
|
54
|
+
ref_id TEXT NOT NULL,
|
|
55
|
+
date TEXT NOT NULL
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_doc_dates_date ON doc_dates(date);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_doc_dates_ref ON doc_dates(ref_table, ref_id);
|
|
49
60
|
`);
|
|
50
61
|
ensureColumns(db, 'skills', [
|
|
51
62
|
['root', "TEXT NOT NULL DEFAULT ''"],
|
|
52
63
|
['deprecated', 'INTEGER NOT NULL DEFAULT 0'],
|
|
64
|
+
['paused', 'INTEGER NOT NULL DEFAULT 0'],
|
|
53
65
|
['created_at', 'TEXT'],
|
|
54
66
|
]);
|
|
55
67
|
ensureColumns(db, 'memory_docs', [
|
|
56
68
|
['root', "TEXT NOT NULL DEFAULT ''"],
|
|
57
69
|
['deprecated', 'INTEGER NOT NULL DEFAULT 0'],
|
|
70
|
+
['paused', 'INTEGER NOT NULL DEFAULT 0'],
|
|
58
71
|
['created_at', 'TEXT'],
|
|
59
72
|
]);
|
|
60
73
|
backfillSearchIndex(db);
|
package/dist/src/store/search.js
CHANGED
|
@@ -32,6 +32,64 @@ export function searchIndex(db, query, opts = {}) {
|
|
|
32
32
|
throw new SearchQueryError(query, err);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
const SNIPPET_CONTEXT_WORDS = 20;
|
|
36
|
+
/**
|
|
37
|
+
* Builds a `<<...>>`-marked excerpt around the first occurrence of `date` in
|
|
38
|
+
* `body`, mirroring FTS5's snippet() style for visual consistency with the
|
|
39
|
+
* other search tools. `date` doesn't always appear literally in the body —
|
|
40
|
+
* it may have matched via created_at instead — in which case there's no
|
|
41
|
+
* position to excerpt around, so the marker stands alone with no context.
|
|
42
|
+
*/
|
|
43
|
+
function buildDateSnippet(body, date) {
|
|
44
|
+
const idx = body.indexOf(date);
|
|
45
|
+
if (idx === -1)
|
|
46
|
+
return `<<${date}>> (matched via created_at, not mentioned in body)`;
|
|
47
|
+
const before = body.slice(0, idx).split(/\s+/).filter(Boolean).slice(-SNIPPET_CONTEXT_WORDS).join(' ');
|
|
48
|
+
const after = body
|
|
49
|
+
.slice(idx + date.length)
|
|
50
|
+
.split(/\s+/)
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.slice(0, SNIPPET_CONTEXT_WORDS)
|
|
53
|
+
.join(' ');
|
|
54
|
+
return `${before ? '…' + before + ' ' : ''}<<${date}>>${after ? ' ' + after + '…' : ''}`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Finds skills/memory docs whose body mentions a date, OR whose created_at
|
|
58
|
+
* falls, within [start, end] (inclusive, ISO YYYY-MM-DD) — driven by the
|
|
59
|
+
* `doc_dates` side table, populated at index time from both extractDates()
|
|
60
|
+
* on the body and the doc's created_at, not FTS5. ANY-match: a doc with
|
|
61
|
+
* multiple candidate dates matches if any falls in range; `matched_date` is
|
|
62
|
+
* the earliest match, with no priority between body-extracted and created_at.
|
|
63
|
+
*/
|
|
64
|
+
export function searchByDate(db, start, end, opts = {}) {
|
|
65
|
+
if (start > end) {
|
|
66
|
+
throw new Error(`invalid date range: start "${start}" is after end "${end}"`);
|
|
67
|
+
}
|
|
68
|
+
const { table, limit = 20, offset = 0 } = opts;
|
|
69
|
+
const params = [start, end];
|
|
70
|
+
if (table)
|
|
71
|
+
params.push(table);
|
|
72
|
+
params.push(limit, offset);
|
|
73
|
+
const rows = db
|
|
74
|
+
.prepare(`SELECT ref_table, ref_id, MIN(date) AS matched_date
|
|
75
|
+
FROM doc_dates
|
|
76
|
+
WHERE date BETWEEN ? AND ? ${table ? 'AND ref_table = ?' : ''}
|
|
77
|
+
GROUP BY ref_table, ref_id
|
|
78
|
+
ORDER BY matched_date
|
|
79
|
+
LIMIT ? OFFSET ?`)
|
|
80
|
+
.all(...params);
|
|
81
|
+
return rows.map((row) => {
|
|
82
|
+
const bodyRow = db
|
|
83
|
+
.prepare(`SELECT body FROM ${row.ref_table} WHERE id = ?`)
|
|
84
|
+
.get(row.ref_id);
|
|
85
|
+
return {
|
|
86
|
+
ref_table: row.ref_table,
|
|
87
|
+
ref_id: row.ref_id,
|
|
88
|
+
matched_date: row.matched_date,
|
|
89
|
+
snippet: bodyRow ? buildDateSnippet(bodyRow.body, row.matched_date) : '',
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
}
|
|
35
93
|
/**
|
|
36
94
|
* Full-text search across BOTH skills and memory docs in one ranked list —
|
|
37
95
|
* for the common case of "find where I put X" when the caller doesn't know
|
package/dist/src/store/sync.js
CHANGED
|
@@ -3,6 +3,10 @@ import path from 'node:path';
|
|
|
3
3
|
import chokidar, {} from 'chokidar';
|
|
4
4
|
import { readMarkdownFile } from './markdown-file.js';
|
|
5
5
|
import { flattenTags } from './db.js';
|
|
6
|
+
import { extractDates, toLocalDate } from './date-extract.js';
|
|
7
|
+
// `paused` is deliberately absent from both lists: it's a local-only cache column (see
|
|
8
|
+
// SkillRepository/MemoryRepository#setPaused) that never round-trips through frontmatter, so a
|
|
9
|
+
// file add/change/rescan must never overwrite it via the INSERT/ON CONFLICT UPDATE below.
|
|
6
10
|
const skillColumns = ['id', 'description', 'owner', 'status', 'tags', 'trigger_phrases', 'extends', 'deprecated', 'created_at'];
|
|
7
11
|
const memoryColumns = ['id', 'key', 'key_type', 'description', 'doc_type', 'tags', 'status', 'related_to', 'deprecated', 'created_at'];
|
|
8
12
|
export function skillSyncSpec(sources) {
|
|
@@ -90,12 +94,25 @@ export function upsertFile(db, spec, filePath) {
|
|
|
90
94
|
ON CONFLICT(id) DO UPDATE SET ${updateClause}`).run(...values);
|
|
91
95
|
db.prepare(`DELETE FROM search_index WHERE ref_table = ? AND ref_id = ?`).run(spec.table, id);
|
|
92
96
|
db.prepare(`INSERT INTO search_index (ref_table, ref_id, description, body, tags) VALUES (?, ?, ?, ?, ?)`).run(spec.table, id, String(row.description ?? ''), parsed.body, flattenTags(String(row.tags ?? '[]')));
|
|
97
|
+
db.prepare(`DELETE FROM doc_dates WHERE ref_table = ? AND ref_id = ?`).run(spec.table, id);
|
|
98
|
+
const dates = new Set(extractDates(parsed.body));
|
|
99
|
+
// created_at is a UTC instant; convert to the OS-local calendar date so it
|
|
100
|
+
// lines up with what a user in this timezone would call "today", matching
|
|
101
|
+
// extractDates()'s output, which is already timezone-naive local text.
|
|
102
|
+
if (row.created_at)
|
|
103
|
+
dates.add(toLocalDate(String(row.created_at)));
|
|
104
|
+
if (dates.size > 0) {
|
|
105
|
+
const insertDate = db.prepare(`INSERT INTO doc_dates (ref_table, ref_id, date) VALUES (?, ?, ?)`);
|
|
106
|
+
for (const date of dates)
|
|
107
|
+
insertDate.run(spec.table, id, date);
|
|
108
|
+
}
|
|
93
109
|
}
|
|
94
110
|
export function removeFile(db, table, filePath) {
|
|
95
111
|
const existing = db.prepare(`SELECT id FROM ${table} WHERE source_path = ?`).get(filePath);
|
|
96
112
|
db.prepare(`DELETE FROM ${table} WHERE source_path = ?`).run(filePath);
|
|
97
113
|
if (existing) {
|
|
98
114
|
db.prepare(`DELETE FROM search_index WHERE ref_table = ? AND ref_id = ?`).run(table, existing.id);
|
|
115
|
+
db.prepare(`DELETE FROM doc_dates WHERE ref_table = ? AND ref_id = ?`).run(table, existing.id);
|
|
99
116
|
}
|
|
100
117
|
}
|
|
101
118
|
/** Full scan of all configured source dirs — used once at startup before the watcher takes over. */
|
package/dist/src/web/routes.js
CHANGED
|
@@ -2,7 +2,7 @@ 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 { saveRoot, removeRoot as removeRootFromConfig } from '../config.js';
|
|
5
|
+
import { saveRoot, removeRoot as removeRootFromConfig, sanitizeRootName } from '../config.js';
|
|
6
6
|
function asArray(v) {
|
|
7
7
|
if (v === undefined)
|
|
8
8
|
return [];
|
|
@@ -25,18 +25,26 @@ function queryEntries(db, req) {
|
|
|
25
25
|
const q = req.query.q?.trim();
|
|
26
26
|
const deprecatedParam = req.query.deprecated;
|
|
27
27
|
const deprecated = deprecatedParam === '0' || deprecatedParam === '1' ? deprecatedParam : undefined;
|
|
28
|
+
const pausedParam = req.query.paused;
|
|
29
|
+
const paused = pausedParam === '0' || pausedParam === '1' ? pausedParam : undefined;
|
|
30
|
+
const dateFrom = req.query.date_from?.trim() || undefined;
|
|
31
|
+
const dateTo = req.query.date_to?.trim() || undefined;
|
|
28
32
|
const matchedIds = q
|
|
29
33
|
? matchSearch(db, q)
|
|
30
34
|
: null;
|
|
31
35
|
if (q && matchedIds && matchedIds.skills.size === 0 && matchedIds.memory_docs.size === 0) {
|
|
32
36
|
return [];
|
|
33
37
|
}
|
|
38
|
+
const dateIds = dateFrom || dateTo ? matchDateRange(db, dateFrom, dateTo) : null;
|
|
39
|
+
if (dateIds && dateIds.skills.size === 0 && dateIds.memory_docs.size === 0) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
34
42
|
const results = [];
|
|
35
43
|
if (type === 'skill' || type === 'all') {
|
|
36
|
-
results.push(...queryTable(db, 'skills', { tags, statuses, owners, docTypes: [], keyTypes: [], roots, deprecated }, matchedIds?.skills));
|
|
44
|
+
results.push(...queryTable(db, 'skills', { tags, statuses, owners, docTypes: [], keyTypes: [], roots, deprecated, paused }, intersectIds(matchedIds?.skills, dateIds?.skills)));
|
|
37
45
|
}
|
|
38
46
|
if (type === 'memory' || type === 'all') {
|
|
39
|
-
results.push(...queryTable(db, 'memory_docs', { tags, statuses, owners: [], docTypes, keyTypes, roots, deprecated }, matchedIds?.memory_docs));
|
|
47
|
+
results.push(...queryTable(db, 'memory_docs', { tags, statuses, owners: [], docTypes, keyTypes, roots, deprecated, paused }, intersectIds(matchedIds?.memory_docs, dateIds?.memory_docs)));
|
|
40
48
|
}
|
|
41
49
|
const sort = req.query.sort ?? 'mtime_desc';
|
|
42
50
|
results.sort((a, b) => {
|
|
@@ -89,13 +97,17 @@ function queryTable(db, table, filters, restrictToIds) {
|
|
|
89
97
|
where += ` AND deprecated = ?`;
|
|
90
98
|
params.push(filters.deprecated === '1' ? 1 : 0);
|
|
91
99
|
}
|
|
100
|
+
if (filters.paused !== undefined) {
|
|
101
|
+
where += ` AND paused = ?`;
|
|
102
|
+
params.push(filters.paused === '1' ? 1 : 0);
|
|
103
|
+
}
|
|
92
104
|
if (restrictToIds) {
|
|
93
105
|
where += ` AND id IN (${[...restrictToIds].map(() => '?').join(', ')})`;
|
|
94
106
|
params.push(...restrictToIds);
|
|
95
107
|
}
|
|
96
108
|
if (table === 'skills') {
|
|
97
109
|
const rows = db
|
|
98
|
-
.prepare(`SELECT id, description, owner, status, tags, root, mtime_ms, deprecated, created_at FROM skills WHERE ${where}`)
|
|
110
|
+
.prepare(`SELECT id, description, owner, status, tags, root, mtime_ms, deprecated, paused, created_at FROM skills WHERE ${where}`)
|
|
99
111
|
.all(...params);
|
|
100
112
|
return rows.map((r) => ({
|
|
101
113
|
_table: 'skills',
|
|
@@ -110,11 +122,12 @@ function queryTable(db, table, filters, restrictToIds) {
|
|
|
110
122
|
root: r.root,
|
|
111
123
|
mtime_ms: r.mtime_ms,
|
|
112
124
|
deprecated: !!r.deprecated,
|
|
125
|
+
paused: !!r.paused,
|
|
113
126
|
created_at: r.created_at,
|
|
114
127
|
}));
|
|
115
128
|
}
|
|
116
129
|
const rows = db
|
|
117
|
-
.prepare(`SELECT id, key, description, doc_type, key_type, status, tags, root, mtime_ms, deprecated, created_at FROM memory_docs WHERE ${where}`)
|
|
130
|
+
.prepare(`SELECT id, key, description, doc_type, key_type, status, tags, root, mtime_ms, deprecated, paused, created_at FROM memory_docs WHERE ${where}`)
|
|
118
131
|
.all(...params);
|
|
119
132
|
return rows.map((r) => ({
|
|
120
133
|
_table: 'memory_docs',
|
|
@@ -129,9 +142,40 @@ function queryTable(db, table, filters, restrictToIds) {
|
|
|
129
142
|
root: r.root,
|
|
130
143
|
mtime_ms: r.mtime_ms,
|
|
131
144
|
deprecated: !!r.deprecated,
|
|
145
|
+
paused: !!r.paused,
|
|
132
146
|
created_at: r.created_at,
|
|
133
147
|
}));
|
|
134
148
|
}
|
|
149
|
+
/** Combines two optional id-restriction sets (e.g. from `q` and a date range) into one, when both are present. */
|
|
150
|
+
function intersectIds(a, b) {
|
|
151
|
+
if (!a)
|
|
152
|
+
return b;
|
|
153
|
+
if (!b)
|
|
154
|
+
return a;
|
|
155
|
+
return new Set([...a].filter((id) => b.has(id)));
|
|
156
|
+
}
|
|
157
|
+
/** Queries the `doc_dates` side table for ids whose body-extracted or created_at date falls in [from, to], bucketed by source table. */
|
|
158
|
+
function matchDateRange(db, from, to) {
|
|
159
|
+
const skills = new Set();
|
|
160
|
+
const memory_docs = new Set();
|
|
161
|
+
const params = [];
|
|
162
|
+
let where = '1 = 1';
|
|
163
|
+
if (from) {
|
|
164
|
+
where += ' AND date >= ?';
|
|
165
|
+
params.push(from);
|
|
166
|
+
}
|
|
167
|
+
if (to) {
|
|
168
|
+
where += ' AND date <= ?';
|
|
169
|
+
params.push(to);
|
|
170
|
+
}
|
|
171
|
+
const rows = db
|
|
172
|
+
.prepare(`SELECT DISTINCT ref_table, ref_id FROM doc_dates WHERE ${where}`)
|
|
173
|
+
.all(...params);
|
|
174
|
+
for (const row of rows) {
|
|
175
|
+
(row.ref_table === 'skills' ? skills : memory_docs).add(row.ref_id);
|
|
176
|
+
}
|
|
177
|
+
return { skills, memory_docs };
|
|
178
|
+
}
|
|
135
179
|
/** Runs the FTS5 query once and buckets matching ids by source table. */
|
|
136
180
|
function matchSearch(db, q) {
|
|
137
181
|
const skills = new Set();
|
|
@@ -209,15 +253,6 @@ function buildHealth(db) {
|
|
|
209
253
|
.map((m) => m.id);
|
|
210
254
|
return { danglingExtends, danglingRelatedTo, emptyTriggerPhrases, staleActiveMemoryDocs };
|
|
211
255
|
}
|
|
212
|
-
/** Lowercase-hyphenate a folder-derived root name, same shape as skill names. */
|
|
213
|
-
function sanitizeRootName(raw) {
|
|
214
|
-
return raw
|
|
215
|
-
.trim()
|
|
216
|
-
.toLowerCase()
|
|
217
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
218
|
-
.replace(/^-+|-+$/g, '')
|
|
219
|
-
.slice(0, 64);
|
|
220
|
-
}
|
|
221
256
|
export function buildWebRouter(db, config, skillRepo, memoryRepo) {
|
|
222
257
|
const router = express.Router();
|
|
223
258
|
router.get('/api/entries', (req, res) => {
|
|
@@ -278,6 +313,44 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo) {
|
|
|
278
313
|
const results = table === 'skills' ? skillRepo.bulkUpdate(ids, { deprecated }) : memoryRepo.bulkUpdate(ids, { deprecated });
|
|
279
314
|
res.json({ results });
|
|
280
315
|
});
|
|
316
|
+
// `paused` is a local-only cache toggle (see SkillRepository/MemoryRepository#setPaused) — it
|
|
317
|
+
// never touches the source file, so this goes through setPaused, not update()/bulkUpdate().
|
|
318
|
+
router.patch('/api/entries/:table/:id/paused', (req, res) => {
|
|
319
|
+
const { table, id } = req.params;
|
|
320
|
+
const { paused } = req.body;
|
|
321
|
+
if (table !== 'skills' && table !== 'memory_docs') {
|
|
322
|
+
res.status(400).json({ error: 'table must be "skills" or "memory_docs"' });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (!id) {
|
|
326
|
+
res.status(400).json({ error: 'id is required' });
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (typeof paused !== 'boolean') {
|
|
330
|
+
res.status(400).json({ error: 'body must be { paused: boolean }' });
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const [result] = table === 'skills' ? skillRepo.setPaused([id], paused) : memoryRepo.setPaused([id], paused);
|
|
334
|
+
if (!result?.ok) {
|
|
335
|
+
res.status(404).json({ error: result?.error ?? 'not found' });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
res.json({ id, paused });
|
|
339
|
+
});
|
|
340
|
+
router.post('/api/entries/:table/bulk/paused', (req, res) => {
|
|
341
|
+
const { table } = req.params;
|
|
342
|
+
const { ids, paused } = req.body;
|
|
343
|
+
if (table !== 'skills' && table !== 'memory_docs') {
|
|
344
|
+
res.status(400).json({ error: 'table must be "skills" or "memory_docs"' });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (!Array.isArray(ids) || ids.length === 0 || typeof paused !== 'boolean') {
|
|
348
|
+
res.status(400).json({ error: 'body must be { ids: string[], paused: boolean }' });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const results = table === 'skills' ? skillRepo.setPaused(ids, paused) : memoryRepo.setPaused(ids, paused);
|
|
352
|
+
res.json({ results });
|
|
353
|
+
});
|
|
281
354
|
router.delete('/api/entries/:table/:id', (req, res) => {
|
|
282
355
|
const { table, id } = req.params;
|
|
283
356
|
if (table !== 'skills' && table !== 'memory_docs') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-memory-bucket",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
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": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"copy:builtin-skills": "mkdir -p dist/src/skills/builtin && cp -r src/skills/builtin/. dist/src/skills/builtin/",
|
|
30
30
|
"prepublishOnly": "npm run build",
|
|
31
31
|
"start": "tsx src/server.ts",
|
|
32
|
-
"start_in_folder": "tsx src/server.ts --memory-dir",
|
|
32
|
+
"start_in_folder": "vite build && tsx src/server.ts --memory-dir",
|
|
33
33
|
"typecheck": "tsc --noEmit -p tsconfig.server.json && tsc --noEmit -p tsconfig.client.json",
|
|
34
34
|
"test": "node --import tsx --test test/**/*.test.ts"
|
|
35
35
|
},
|