mcp-memory-bucket 0.1.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/LICENSE +21 -0
- package/README.md +76 -0
- package/bin/mcp-memory-bucket.js +2 -0
- package/dist/client/assets/index-CxhInmjj.js +150 -0
- package/dist/client/index.html +16 -0
- package/dist/src/config.js +23 -0
- package/dist/src/memory/repository.js +99 -0
- package/dist/src/memory/tools.js +97 -0
- package/dist/src/server.js +86 -0
- package/dist/src/shared/relocate-tool.js +32 -0
- package/dist/src/shared/relocate.js +106 -0
- package/dist/src/skills/builtin/memory-bucket-authoring/SKILL.md +187 -0
- package/dist/src/skills/repository.js +144 -0
- package/dist/src/skills/tools.js +82 -0
- package/dist/src/store/db.js +74 -0
- package/dist/src/store/markdown-file.js +19 -0
- package/dist/src/store/safe-path.js +11 -0
- package/dist/src/store/skill-name.js +11 -0
- package/dist/src/store/slug.js +7 -0
- package/dist/src/store/sync.js +144 -0
- package/dist/src/types.js +4 -0
- package/dist/src/web/routes.js +204 -0
- package/dist/src/web/ui-tool.js +6 -0
- package/package.json +54 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import chokidar, {} from 'chokidar';
|
|
4
|
+
import { readMarkdownFile } from './markdown-file.js';
|
|
5
|
+
import { flattenTags } from './db.js';
|
|
6
|
+
const skillColumns = ['id', 'description', 'owner', 'status', 'tags', 'trigger_phrases', 'extends'];
|
|
7
|
+
const memoryColumns = ['id', 'key', 'key_type', 'description', 'doc_type', 'tags', 'status', 'related_to'];
|
|
8
|
+
export function skillSyncSpec(sources) {
|
|
9
|
+
return {
|
|
10
|
+
table: 'skills',
|
|
11
|
+
sources,
|
|
12
|
+
matchesFile: (filePath) => path.basename(filePath) === 'SKILL.md',
|
|
13
|
+
columns: skillColumns,
|
|
14
|
+
getId: (fm) => fm.name,
|
|
15
|
+
toRow: (fm) => ({
|
|
16
|
+
id: fm.name,
|
|
17
|
+
description: fm.description,
|
|
18
|
+
owner: fm.metadata?.owner ?? null,
|
|
19
|
+
status: fm.metadata?.status ?? 'unreviewed',
|
|
20
|
+
tags: JSON.stringify(fm.tags ?? []),
|
|
21
|
+
trigger_phrases: JSON.stringify(fm.trigger_phrases ?? []),
|
|
22
|
+
extends: fm.metadata?.extends ?? null,
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function memorySyncSpec(sources) {
|
|
27
|
+
return {
|
|
28
|
+
table: 'memory_docs',
|
|
29
|
+
sources,
|
|
30
|
+
matchesFile: (filePath) => filePath.endsWith('.md'),
|
|
31
|
+
columns: memoryColumns,
|
|
32
|
+
getId: (fm) => fm.id,
|
|
33
|
+
toRow: (fm) => ({
|
|
34
|
+
id: fm.id,
|
|
35
|
+
key: fm.key,
|
|
36
|
+
key_type: fm.key_type ?? 'freeform',
|
|
37
|
+
description: fm.description,
|
|
38
|
+
doc_type: fm.doc_type ?? 'other',
|
|
39
|
+
tags: JSON.stringify(fm.tags ?? []),
|
|
40
|
+
status: fm.status ?? 'active',
|
|
41
|
+
related_to: fm.related_to ?? null,
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Upserts one file's frontmatter/body into its cache table, keyed by mtime
|
|
47
|
+
* so an unchanged file is skipped. Exported so repositories can index
|
|
48
|
+
* synchronously right after their own writes — the watcher's own event for
|
|
49
|
+
* that same write becomes a harmless no-op re-check once mtime matches.
|
|
50
|
+
*/
|
|
51
|
+
export function upsertFile(db, spec, filePath) {
|
|
52
|
+
const existing = db
|
|
53
|
+
.prepare(`SELECT mtime_ms FROM ${spec.table} WHERE source_path = ?`)
|
|
54
|
+
.get(filePath);
|
|
55
|
+
const parsed = readMarkdownFile(filePath);
|
|
56
|
+
if (existing && existing.mtime_ms === parsed.mtimeMs)
|
|
57
|
+
return; // unchanged, skip reprocessing
|
|
58
|
+
const id = spec.getId(parsed.frontmatter);
|
|
59
|
+
if (!id) {
|
|
60
|
+
console.error(`[memory-bucket] skipping ${filePath}: missing required id field in frontmatter`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const row = spec.toRow(parsed.frontmatter, filePath);
|
|
64
|
+
const cols = [...spec.columns, 'source_path', 'body', 'mtime_ms'];
|
|
65
|
+
const values = [...spec.columns.map((c) => row[c]), filePath, parsed.body, parsed.mtimeMs];
|
|
66
|
+
const placeholders = cols.map(() => '?').join(', ');
|
|
67
|
+
const updateClause = cols
|
|
68
|
+
.filter((c) => c !== 'id')
|
|
69
|
+
.map((c) => `${c} = excluded.${c}`)
|
|
70
|
+
.join(', ');
|
|
71
|
+
db.prepare(`INSERT INTO ${spec.table} (${cols.join(', ')}) VALUES (${placeholders})
|
|
72
|
+
ON CONFLICT(id) DO UPDATE SET ${updateClause}`).run(...values);
|
|
73
|
+
db.prepare(`DELETE FROM search_index WHERE ref_table = ? AND ref_id = ?`).run(spec.table, id);
|
|
74
|
+
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 ?? '[]')));
|
|
75
|
+
}
|
|
76
|
+
export function removeFile(db, table, filePath) {
|
|
77
|
+
const existing = db.prepare(`SELECT id FROM ${table} WHERE source_path = ?`).get(filePath);
|
|
78
|
+
db.prepare(`DELETE FROM ${table} WHERE source_path = ?`).run(filePath);
|
|
79
|
+
if (existing) {
|
|
80
|
+
db.prepare(`DELETE FROM search_index WHERE ref_table = ? AND ref_id = ?`).run(table, existing.id);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Full scan of all configured source dirs — used once at startup before the watcher takes over. */
|
|
84
|
+
export function initialScan(db, spec) {
|
|
85
|
+
for (const dir of spec.sources) {
|
|
86
|
+
if (!fs.existsSync(dir))
|
|
87
|
+
continue;
|
|
88
|
+
for (const file of walkMarkdownFiles(dir)) {
|
|
89
|
+
if (!spec.matchesFile(file))
|
|
90
|
+
continue;
|
|
91
|
+
try {
|
|
92
|
+
upsertFile(db, spec, file);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
console.error(`[memory-bucket] failed to index ${file}:`, err);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function* walkMarkdownFiles(dir) {
|
|
101
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
102
|
+
const full = path.join(dir, entry.name);
|
|
103
|
+
if (entry.isDirectory()) {
|
|
104
|
+
yield* walkMarkdownFiles(full);
|
|
105
|
+
}
|
|
106
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
107
|
+
yield full;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function watchSources(db, spec) {
|
|
112
|
+
const watcher = chokidar.watch(spec.sources, {
|
|
113
|
+
ignoreInitial: true,
|
|
114
|
+
persistent: true,
|
|
115
|
+
depth: 10,
|
|
116
|
+
});
|
|
117
|
+
watcher
|
|
118
|
+
.on('add', (filePath) => {
|
|
119
|
+
if (!spec.matchesFile(filePath))
|
|
120
|
+
return;
|
|
121
|
+
try {
|
|
122
|
+
upsertFile(db, spec, filePath);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
console.error(`[memory-bucket] failed to index ${filePath}:`, err);
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
.on('change', (filePath) => {
|
|
129
|
+
if (!spec.matchesFile(filePath))
|
|
130
|
+
return;
|
|
131
|
+
try {
|
|
132
|
+
upsertFile(db, spec, filePath);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
console.error(`[memory-bucket] failed to reindex ${filePath}:`, err);
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
.on('unlink', (filePath) => {
|
|
139
|
+
if (!spec.matchesFile(filePath))
|
|
140
|
+
return;
|
|
141
|
+
removeFile(db, spec.table, filePath);
|
|
142
|
+
});
|
|
143
|
+
return watcher;
|
|
144
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
function asArray(v) {
|
|
3
|
+
if (v === undefined)
|
|
4
|
+
return [];
|
|
5
|
+
return Array.isArray(v) ? v.map(String) : [String(v)];
|
|
6
|
+
}
|
|
7
|
+
function tagWhereClause(tags) {
|
|
8
|
+
if (tags.length === 0)
|
|
9
|
+
return { clause: '', params: [] };
|
|
10
|
+
const clauses = tags.map(() => `EXISTS (SELECT 1 FROM json_each(tags) WHERE value = ?)`);
|
|
11
|
+
return { clause: ` AND ${clauses.join(' AND ')}`, params: tags };
|
|
12
|
+
}
|
|
13
|
+
function queryEntries(db, req) {
|
|
14
|
+
const type = req.query.type ?? 'all';
|
|
15
|
+
const tags = asArray(req.query.tag);
|
|
16
|
+
const statuses = asArray(req.query.status);
|
|
17
|
+
const owners = asArray(req.query.owner);
|
|
18
|
+
const docTypes = asArray(req.query.doc_type);
|
|
19
|
+
const keyTypes = asArray(req.query.key_type);
|
|
20
|
+
const q = req.query.q?.trim();
|
|
21
|
+
const matchedIds = q
|
|
22
|
+
? matchSearch(db, q)
|
|
23
|
+
: null;
|
|
24
|
+
if (q && matchedIds && matchedIds.skills.size === 0 && matchedIds.memory_docs.size === 0) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const results = [];
|
|
28
|
+
if (type === 'skill' || type === 'all') {
|
|
29
|
+
results.push(...queryTable(db, 'skills', { tags, statuses, owners, docTypes: [], keyTypes: [] }, matchedIds?.skills));
|
|
30
|
+
}
|
|
31
|
+
if (type === 'memory' || type === 'all') {
|
|
32
|
+
results.push(...queryTable(db, 'memory_docs', { tags, statuses, owners: [], docTypes, keyTypes }, matchedIds?.memory_docs));
|
|
33
|
+
}
|
|
34
|
+
const sort = req.query.sort ?? 'mtime_desc';
|
|
35
|
+
results.sort((a, b) => {
|
|
36
|
+
if (sort === 'mtime_asc')
|
|
37
|
+
return a.mtime_ms - b.mtime_ms;
|
|
38
|
+
if (sort === 'name_asc')
|
|
39
|
+
return a.name.localeCompare(b.name);
|
|
40
|
+
return b.mtime_ms - a.mtime_ms; // mtime_desc, default
|
|
41
|
+
});
|
|
42
|
+
return results;
|
|
43
|
+
}
|
|
44
|
+
function queryTable(db, table, filters, restrictToIds) {
|
|
45
|
+
if (restrictToIds && restrictToIds.size === 0)
|
|
46
|
+
return [];
|
|
47
|
+
const params = [];
|
|
48
|
+
let where = '1 = 1';
|
|
49
|
+
const { clause: tagClause, params: tagParams } = tagWhereClause(filters.tags);
|
|
50
|
+
where += tagClause;
|
|
51
|
+
params.push(...tagParams);
|
|
52
|
+
if (filters.statuses.length > 0) {
|
|
53
|
+
where += ` AND status IN (${filters.statuses.map(() => '?').join(', ')})`;
|
|
54
|
+
params.push(...filters.statuses);
|
|
55
|
+
}
|
|
56
|
+
if (table === 'skills' && filters.owners.length > 0) {
|
|
57
|
+
where += ` AND owner IN (${filters.owners.map(() => '?').join(', ')})`;
|
|
58
|
+
params.push(...filters.owners);
|
|
59
|
+
}
|
|
60
|
+
if (table === 'memory_docs' && filters.docTypes.length > 0) {
|
|
61
|
+
where += ` AND doc_type IN (${filters.docTypes.map(() => '?').join(', ')})`;
|
|
62
|
+
params.push(...filters.docTypes);
|
|
63
|
+
}
|
|
64
|
+
if (table === 'memory_docs' && filters.keyTypes.length > 0) {
|
|
65
|
+
where += ` AND key_type IN (${filters.keyTypes.map(() => '?').join(', ')})`;
|
|
66
|
+
params.push(...filters.keyTypes);
|
|
67
|
+
}
|
|
68
|
+
if (restrictToIds) {
|
|
69
|
+
where += ` AND id IN (${[...restrictToIds].map(() => '?').join(', ')})`;
|
|
70
|
+
params.push(...restrictToIds);
|
|
71
|
+
}
|
|
72
|
+
if (table === 'skills') {
|
|
73
|
+
const rows = db
|
|
74
|
+
.prepare(`SELECT id, description, owner, status, tags, mtime_ms FROM skills WHERE ${where}`)
|
|
75
|
+
.all(...params);
|
|
76
|
+
return rows.map((r) => ({
|
|
77
|
+
_table: 'skills',
|
|
78
|
+
id: r.id,
|
|
79
|
+
name: r.id,
|
|
80
|
+
description: r.description,
|
|
81
|
+
tags: JSON.parse(r.tags),
|
|
82
|
+
status: r.status,
|
|
83
|
+
owner: r.owner,
|
|
84
|
+
doc_type: null,
|
|
85
|
+
key_type: null,
|
|
86
|
+
mtime_ms: r.mtime_ms,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
const rows = db
|
|
90
|
+
.prepare(`SELECT id, key, description, doc_type, key_type, status, tags, mtime_ms FROM memory_docs WHERE ${where}`)
|
|
91
|
+
.all(...params);
|
|
92
|
+
return rows.map((r) => ({
|
|
93
|
+
_table: 'memory_docs',
|
|
94
|
+
id: r.id,
|
|
95
|
+
name: r.key,
|
|
96
|
+
description: r.description,
|
|
97
|
+
tags: JSON.parse(r.tags),
|
|
98
|
+
status: r.status,
|
|
99
|
+
owner: null,
|
|
100
|
+
doc_type: r.doc_type,
|
|
101
|
+
key_type: r.key_type,
|
|
102
|
+
mtime_ms: r.mtime_ms,
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
/** Runs the FTS5 query once and buckets matching ids by source table. */
|
|
106
|
+
function matchSearch(db, q) {
|
|
107
|
+
const skills = new Set();
|
|
108
|
+
const memory_docs = new Set();
|
|
109
|
+
let rows;
|
|
110
|
+
try {
|
|
111
|
+
rows = db
|
|
112
|
+
.prepare(`SELECT ref_table, ref_id FROM search_index WHERE search_index MATCH ? ORDER BY rank`)
|
|
113
|
+
.all(q);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Bad FTS5 query syntax (e.g. a bare quote) — treat as no matches rather than 500ing.
|
|
117
|
+
return { skills, memory_docs };
|
|
118
|
+
}
|
|
119
|
+
for (const row of rows) {
|
|
120
|
+
(row.ref_table === 'skills' ? skills : memory_docs).add(row.ref_id);
|
|
121
|
+
}
|
|
122
|
+
return { skills, memory_docs };
|
|
123
|
+
}
|
|
124
|
+
function buildFacets(db, type) {
|
|
125
|
+
const tags = new Set();
|
|
126
|
+
const statuses = new Set();
|
|
127
|
+
const owners = new Set();
|
|
128
|
+
const docTypes = new Set();
|
|
129
|
+
const keyTypes = new Set();
|
|
130
|
+
if (type === 'skill' || type === 'all') {
|
|
131
|
+
const rows = db.prepare(`SELECT tags, status, owner FROM skills`).all();
|
|
132
|
+
for (const r of rows) {
|
|
133
|
+
JSON.parse(r.tags).forEach((t) => tags.add(t));
|
|
134
|
+
statuses.add(r.status);
|
|
135
|
+
if (r.owner)
|
|
136
|
+
owners.add(r.owner);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (type === 'memory' || type === 'all') {
|
|
140
|
+
const rows = db.prepare(`SELECT tags, status, doc_type, key_type FROM memory_docs`).all();
|
|
141
|
+
for (const r of rows) {
|
|
142
|
+
JSON.parse(r.tags).forEach((t) => tags.add(t));
|
|
143
|
+
statuses.add(r.status);
|
|
144
|
+
docTypes.add(r.doc_type);
|
|
145
|
+
keyTypes.add(r.key_type);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
tags: [...tags].sort(),
|
|
150
|
+
statuses: [...statuses].sort(),
|
|
151
|
+
owners: [...owners].sort(),
|
|
152
|
+
doc_types: [...docTypes].sort(),
|
|
153
|
+
key_types: [...keyTypes].sort(),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function buildHealth(db) {
|
|
157
|
+
const skillIds = new Set(db.prepare(`SELECT id FROM skills`).all().map((r) => r.id));
|
|
158
|
+
const memoryIds = new Set(db.prepare(`SELECT id FROM memory_docs`).all().map((r) => r.id));
|
|
159
|
+
const skills = db.prepare(`SELECT id, extends, trigger_phrases, mtime_ms FROM skills`).all();
|
|
160
|
+
const danglingExtends = skills
|
|
161
|
+
.filter((s) => s.extends && !skillIds.has(s.extends))
|
|
162
|
+
.map((s) => ({ id: s.id, extends: s.extends }));
|
|
163
|
+
const emptyTriggerPhrases = skills
|
|
164
|
+
.filter((s) => JSON.parse(s.trigger_phrases).length === 0)
|
|
165
|
+
.map((s) => s.id);
|
|
166
|
+
const memoryDocs = db.prepare(`SELECT id, related_to, status, mtime_ms FROM memory_docs`).all();
|
|
167
|
+
const danglingRelatedTo = memoryDocs
|
|
168
|
+
.filter((m) => m.related_to && !memoryIds.has(m.related_to) && !skillIds.has(m.related_to))
|
|
169
|
+
.map((m) => ({ id: m.id, related_to: m.related_to }));
|
|
170
|
+
const staleCutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
171
|
+
const staleActiveMemoryDocs = memoryDocs
|
|
172
|
+
.filter((m) => m.status === 'active' && m.mtime_ms < staleCutoff)
|
|
173
|
+
.map((m) => m.id);
|
|
174
|
+
return { danglingExtends, danglingRelatedTo, emptyTriggerPhrases, staleActiveMemoryDocs };
|
|
175
|
+
}
|
|
176
|
+
export function buildWebRouter(db) {
|
|
177
|
+
const router = express.Router();
|
|
178
|
+
router.get('/api/entries', (req, res) => {
|
|
179
|
+
res.json(queryEntries(db, req));
|
|
180
|
+
});
|
|
181
|
+
router.get('/api/entries/:table/:id', (req, res) => {
|
|
182
|
+
const { table, id } = req.params;
|
|
183
|
+
if (table !== 'skills' && table !== 'memory_docs') {
|
|
184
|
+
res.status(400).json({ error: 'table must be "skills" or "memory_docs"' });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const row = db.prepare(`SELECT * FROM ${table} WHERE id = ?`).get(id);
|
|
188
|
+
if (!row) {
|
|
189
|
+
res.status(404).json({ error: 'not found' });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const tags = JSON.parse(row.tags);
|
|
193
|
+
const trigger_phrases = row.trigger_phrases ? JSON.parse(row.trigger_phrases) : undefined;
|
|
194
|
+
res.json({ ...row, tags, trigger_phrases });
|
|
195
|
+
});
|
|
196
|
+
router.get('/api/facets', (req, res) => {
|
|
197
|
+
const type = req.query.type ?? 'all';
|
|
198
|
+
res.json(buildFacets(db, type));
|
|
199
|
+
});
|
|
200
|
+
router.get('/api/health', (_req, res) => {
|
|
201
|
+
res.json(buildHealth(db));
|
|
202
|
+
});
|
|
203
|
+
return router;
|
|
204
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function registerUiTool(mcp, port) {
|
|
2
|
+
mcp.tool('bucket_open_ui', 'Returns the URL for the mem-bucket web viewer — a read-only browser UI for searching/filtering skills and memory docs by tag, status, owner, and fulltext. Not for editing; use the skill_*/memory_* tools for that.', {}, async () => {
|
|
3
|
+
const url = `http://localhost:${port}/`;
|
|
4
|
+
return { content: [{ type: 'text', text: url }] };
|
|
5
|
+
});
|
|
6
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-memory-bucket",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
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
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/anatolipr/avo-mcp-tools.git",
|
|
9
|
+
"directory": "packages/mcp-memory-bucket"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"bin": {
|
|
13
|
+
"mcp-memory-bucket": "./bin/mcp-memory-bucket.js"
|
|
14
|
+
},
|
|
15
|
+
"main": "dist/src/server.js",
|
|
16
|
+
"files": [
|
|
17
|
+
"bin",
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"dev": "npm-run-all --parallel dev:client dev:server",
|
|
26
|
+
"dev:client": "vite build --watch",
|
|
27
|
+
"dev:server": "tsx watch src/server.ts",
|
|
28
|
+
"build": "tsc -p tsconfig.server.json && vite build && npm run copy:builtin-skills",
|
|
29
|
+
"copy:builtin-skills": "mkdir -p dist/src/skills/builtin && cp -r src/skills/builtin/. dist/src/skills/builtin/",
|
|
30
|
+
"start": "tsx src/server.ts",
|
|
31
|
+
"start_in_folder": "tsx src/server.ts --memory-dir",
|
|
32
|
+
"typecheck": "tsc --noEmit -p tsconfig.server.json && tsc --noEmit -p tsconfig.client.json",
|
|
33
|
+
"test": "node --import tsx --test test/**/*.test.ts"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
37
|
+
"avosignals": "^1.0.16",
|
|
38
|
+
"better-sqlite3": "^11.8.0",
|
|
39
|
+
"chokidar": "^4.0.0",
|
|
40
|
+
"express": "^4.21.0",
|
|
41
|
+
"gray-matter": "^4.0.3",
|
|
42
|
+
"lit": "^3.3.3",
|
|
43
|
+
"zod": "^3.23.8"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/better-sqlite3": "^7.6.0",
|
|
47
|
+
"@types/express": "^4.17.0",
|
|
48
|
+
"@types/node": "^24.0.0",
|
|
49
|
+
"npm-run-all": "^4.1.5",
|
|
50
|
+
"tsx": "^4.19.0",
|
|
51
|
+
"typescript": "^5.7.0",
|
|
52
|
+
"vite": "^6.0.0"
|
|
53
|
+
}
|
|
54
|
+
}
|