mcp-memory-bucket 0.2.2 → 0.3.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/README.md +47 -13
- package/dist/client/assets/{index-BxAZ48QK.js → index-DIO48C0V.js} +212 -113
- package/dist/client/index.html +1 -1
- package/dist/src/memory/repository.js +117 -0
- package/dist/src/memory/tools.js +52 -0
- package/dist/src/server.js +3 -1
- package/dist/src/shared/relocate-tool.js +32 -18
- package/dist/src/shared/relocate.js +9 -0
- package/dist/src/shared/search-tool.js +18 -0
- package/dist/src/skills/builtin/memory-bucket-authoring/SKILL.md +423 -7
- package/dist/src/skills/repository.js +129 -0
- package/dist/src/skills/tools.js +71 -0
- package/dist/src/store/db.js +21 -8
- package/dist/src/store/search.js +60 -0
- package/dist/src/store/sync.js +6 -2
- package/dist/src/web/routes.js +98 -4
- package/dist/src/web/ui-tool.js +1 -1
- package/package.json +1 -1
|
@@ -1,13 +1,389 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description:
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
name: memory-bucket-authoring
|
|
3
|
+
description: >-
|
|
4
|
+
Explains how to author and use skills and memory docs through the
|
|
5
|
+
memory-bucket MCP server's skill_*/memory_*/relocate tools — the skill.md
|
|
6
|
+
frontmatter schema (agentskills.io standard), the memory frontmatter schema,
|
|
7
|
+
and when to use each. Use whenever a memory-bucket MCP server is connected and
|
|
8
|
+
you need to create, update, or decide between a skill and a memory doc, or
|
|
9
|
+
when the user asks to save a plan/spec/session summary or to record a reusable
|
|
10
|
+
pattern.
|
|
11
|
+
tags:
|
|
12
|
+
- memory-bucket
|
|
13
|
+
- meta
|
|
14
|
+
- authoring
|
|
15
|
+
trigger_phrases:
|
|
16
|
+
- save this as a skill
|
|
17
|
+
- save this to memory
|
|
18
|
+
- remember this plan
|
|
19
|
+
- create a skill
|
|
20
|
+
- memory-bucket
|
|
6
21
|
metadata:
|
|
7
|
-
owner:
|
|
8
|
-
status:
|
|
9
|
-
|
|
22
|
+
owner: company
|
|
23
|
+
status: stable
|
|
24
|
+
extends: null
|
|
25
|
+
deprecated: false
|
|
26
|
+
body: >-
|
|
27
|
+
## Two namespaces, two different jobs
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
If a `memory-bucket` MCP server is connected, it exposes two tool
|
|
31
|
+
|
|
32
|
+
families backed by markdown+frontmatter files:
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
- **`skill_*`** — reusable, evergreen coding patterns. Found by keyword
|
|
36
|
+
search. Use for: a convention, a component pattern, an idiom you'd want
|
|
37
|
+
to reuse across many unrelated pieces of work.
|
|
38
|
+
- **`memory_*`** — point-in-time working context (plans, specs, SQL,
|
|
39
|
+
testing notes, session summaries) attached to a key (usually a ticket
|
|
40
|
+
ID, sometimes a free-form name like "Spot Chart Design"). Found by
|
|
41
|
+
exact key lookup, not search. Use for: anything tied to one specific
|
|
42
|
+
piece of work that won't be relevant once that work ships.
|
|
43
|
+
|
|
44
|
+
If you're unsure which one applies: would this be useful on a totally
|
|
45
|
+
|
|
46
|
+
different ticket next month with no connection to today's task? Skill.
|
|
47
|
+
|
|
48
|
+
Is it specific to what's happening right now? Memory.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
## Authoring a skill
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
A skill is a **folder** containing a `SKILL.md` file, per the
|
|
55
|
+
|
|
56
|
+
[agentskills.io](https://agentskills.io) open standard — the same format
|
|
57
|
+
|
|
58
|
+
Claude Code, Cursor, and other agents read directly off disk, independent
|
|
59
|
+
|
|
60
|
+
of this MCP server. `skill_create` writes exactly this shape for you; you
|
|
61
|
+
|
|
62
|
+
don't need to construct the file yourself.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
Required frontmatter:
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
name: "lit-dropdown-component" # 1-64 chars, lowercase
|
|
73
|
+
letters/numbers/hyphens only,
|
|
74
|
+
# no leading/trailing/consecutive hyphens.
|
|
75
|
+
# This becomes the containing folder's name.
|
|
76
|
+
description: "Builds a dropdown component in Lit with keyboard navigation and
|
|
77
|
+
ARIA roles. Use when the user asks for a dropdown, select, or combobox
|
|
78
|
+
component in a Lit-based frontend."
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
**`description` is the single most important field.** It's the only
|
|
86
|
+
|
|
87
|
+
thing loaded into context at discovery time (along with `name`) — an
|
|
88
|
+
|
|
89
|
+
agent scans it to decide whether the skill is relevant *before* ever
|
|
90
|
+
|
|
91
|
+
reading the body. Write it to cover both what the skill does and when to
|
|
92
|
+
|
|
93
|
+
use it, in the same sentence or two. "Helps with dropdowns" is too vague
|
|
94
|
+
|
|
95
|
+
to trigger reliably; "Builds a dropdown component in Lit with keyboard
|
|
96
|
+
|
|
97
|
+
navigation... use when the user asks for a dropdown, select, or combobox
|
|
98
|
+
|
|
99
|
+
component" gives an agent something to pattern-match against.
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
**Always write the description in third person** — "Processes X" or
|
|
103
|
+
|
|
104
|
+
"Builds Y", never "I can help you..." or "You can use this to...". It's
|
|
105
|
+
|
|
106
|
+
injected into the system prompt alongside every other skill's
|
|
107
|
+
|
|
108
|
+
description, and an inconsistent point of view degrades discovery.
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
**Naming**: prefer gerund form (`processing-pdfs`, `writing-tests`) or a
|
|
112
|
+
|
|
113
|
+
plain noun phrase (`pdf-processing`, `lit-dropdown-component`) — both are
|
|
114
|
+
|
|
115
|
+
fine, pick whichever reads more naturally for the pattern. Avoid vague or
|
|
116
|
+
|
|
117
|
+
generic names a skill list can't be scanned by: `helper`, `utils`,
|
|
118
|
+
|
|
119
|
+
`tools`, `data`, `files`.
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
Optional frontmatter this project also supports:
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
- `license`, `compatibility` — standard fields, rarely needed.
|
|
126
|
+
|
|
127
|
+
- `tags`, `trigger_phrases` — arrays of extra keywords `skill_list`'s
|
|
128
|
+
keyword search matches against, beyond `description` itself.
|
|
129
|
+
- `owner`, `status` (`stable`/`beta`/`unreviewed`), `extends` — stored
|
|
130
|
+
under `metadata` in the frontmatter (a string-keyed map the standard
|
|
131
|
+
reserves for exactly this kind of client-specific extension). `status`
|
|
132
|
+
defaults to `unreviewed` if omitted, so low-trust content doesn't read
|
|
133
|
+
as equivalent to a reviewed pattern.
|
|
134
|
+
|
|
135
|
+
Call `skill_create(name, description, body, ...)`. Pass `folder` to place
|
|
136
|
+
|
|
137
|
+
it under a subdirectory (e.g. `folder: "frontend"`) if the skill source
|
|
138
|
+
|
|
139
|
+
tree is organized that way — check `skill_list()` or ask the user if
|
|
140
|
+
|
|
141
|
+
you're not sure of the convention in this repo.
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
### Writing the body
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
The agent reading a skill is already capable — don't explain things it
|
|
148
|
+
|
|
149
|
+
already knows (what a PDF is, how a for-loop works). Before adding a
|
|
150
|
+
|
|
151
|
+
sentence, ask "does this justify its token cost?" A concise 3-line code
|
|
152
|
+
|
|
153
|
+
snippet beats a paragraph of preamble around it.
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
Match how prescriptive you are to how fragile the task is:
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
- **High freedom** (numbered steps, heuristics) — when multiple valid
|
|
160
|
+
approaches exist and judgment matters, e.g. "review this code for
|
|
161
|
+
bugs."
|
|
162
|
+
- **Medium freedom** (a template or parameterized snippet) — when a
|
|
163
|
+
preferred pattern exists but some variation is fine.
|
|
164
|
+
- **Low freedom** (an exact command, "do not modify this") — when the
|
|
165
|
+
operation is fragile or must run in an exact sequence, e.g. a
|
|
166
|
+
migration script.
|
|
167
|
+
|
|
168
|
+
Other things that reliably improve a skill:
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
- Use one term for one concept throughout (always "field", never a mix
|
|
172
|
+
of "field"/"box"/"control") — inconsistent vocabulary makes the
|
|
173
|
+
instructions harder to follow.
|
|
174
|
+
- Avoid time-sensitive claims ("before/after March 2026, use X") since
|
|
175
|
+
they silently rot; if a pattern is genuinely deprecated, name the
|
|
176
|
+
current approach first and fold the old one into a clearly-labeled
|
|
177
|
+
"legacy" aside instead of a date-gated branch.
|
|
178
|
+
- Don't enumerate every possible library/approach — give one good
|
|
179
|
+
default plus, if truly needed, a named escape hatch for the exception
|
|
180
|
+
case. "You can use pypdf, or pdfplumber, or PyMuPDF, or..." is worse
|
|
181
|
+
than just picking one.
|
|
182
|
+
- If the body instructs calling another MCP server's tool, use the fully
|
|
183
|
+
qualified `ServerName:tool_name` form so it isn't ambiguous which
|
|
184
|
+
server owns it.
|
|
185
|
+
|
|
186
|
+
### Keeping SKILL.md small (progressive disclosure)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
Keep the `SKILL.md` body itself under ~500 lines; it's loaded in full
|
|
190
|
+
|
|
191
|
+
once the skill is activated, so move anything long (detailed reference
|
|
192
|
+
|
|
193
|
+
tables, big code samples) into files the body links to, if the host
|
|
194
|
+
|
|
195
|
+
environment supports bundling extra files alongside `SKILL.md`.
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
When you do split content out:
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
- **Link only one level deep from `SKILL.md` itself.** A reference file
|
|
202
|
+
that links to another reference file risks a shallow partial read (the
|
|
203
|
+
agent may `head` it instead of reading in full) and losing information.
|
|
204
|
+
Put every reference file's link directly in `SKILL.md`, even if that
|
|
205
|
+
means `SKILL.md` links to several files.
|
|
206
|
+
- If a linked-out reference file runs past ~100 lines, put a short table
|
|
207
|
+
of contents at its top so a partial read still reveals what's there.
|
|
208
|
+
|
|
209
|
+
## Authoring a memory doc
|
|
10
210
|
|
|
211
|
+
|
|
212
|
+
Every memory doc needs a `key` (the lookup handle) and a `description`
|
|
213
|
+
|
|
214
|
+
(what distinguishes it from other docs sharing that key):
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
```yaml
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
key: "RMXS-14" # normalized on write: uppercase, hyphenated
|
|
222
|
+
|
|
223
|
+
key_type: "ticket" # "ticket" for a real ticket ID, "freeform" for
|
|
224
|
+
names like "Spot Chart Design"
|
|
225
|
+
|
|
226
|
+
doc_type: "plan" # plan | spec | sql | testing-todo | discovery |
|
|
227
|
+
session-summary | other
|
|
228
|
+
|
|
229
|
+
description: "Bulk edit plan for product boost"
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
Call `memory_create(key, key_type, doc_type, description, body, ...)`.
|
|
237
|
+
|
|
238
|
+
One key commonly accumulates several docs over time — a plan, then a
|
|
239
|
+
|
|
240
|
+
spec, then SQL from a debugging session, then a session summary — all
|
|
241
|
+
|
|
242
|
+
retrievable together via `memory_get(key)`, or narrowed with
|
|
243
|
+
|
|
244
|
+
`memory_get(key, doc_type)`.
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
**Never infer the key from environment state** (branch name, current
|
|
248
|
+
|
|
249
|
+
directory, etc.) — always get it from what the user actually said in
|
|
250
|
+
|
|
251
|
+
conversation, or ask if it's genuinely unclear.
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
### Saving a session
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
If the user asks to save the current conversation/session as memory, use
|
|
258
|
+
|
|
259
|
+
`memory_save_session(summary, key, description, tags?)` — pass a
|
|
260
|
+
|
|
261
|
+
**summary**, not a raw transcript. If `key` or `description` weren't
|
|
262
|
+
|
|
263
|
+
given, ask for both before calling it; don't guess a key from context.
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
## Finding things: search vs. list vs. get
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
- **`skill_list`/`memory_list`** — browse by metadata only (description,
|
|
270
|
+
tags, trigger phrases for skills; key prefix for memory). Fast, but
|
|
271
|
+
blind to body content.
|
|
272
|
+
- **`skill_search`/`memory_search`/`bucket_search`** — full-text search
|
|
273
|
+
over the actual markdown body (not just metadata), ranked by
|
|
274
|
+
relevance, with a highlighted snippet per hit. `query` is raw SQLite
|
|
275
|
+
FTS5 syntax: bare words AND by default, `"exact phrase"`, `prefix*`,
|
|
276
|
+
explicit `AND`/`OR`/`NOT`. **Hyphenated or punctuated terms must be
|
|
277
|
+
quoted** (`"blue-green"`, not `blue-green` — FTS5 treats a bare hyphen
|
|
278
|
+
as an operator and will error). `skill_search`/`memory_search` also
|
|
279
|
+
accept metadata filters (`status`, `owner`/`doc_type`, `tag`) combined
|
|
280
|
+
with the text query. Use `bucket_search` instead of the two separate
|
|
281
|
+
tools when you don't know (or don't care) which bucket something
|
|
282
|
+
landed in. All three return snippets, not full bodies — follow up with
|
|
283
|
+
`skill_get`/`memory_get` (or the bulk variants below) for the rest.
|
|
284
|
+
- **`skill_get`/`memory_get`** — exact-key lookup when you already know
|
|
285
|
+
the name/key.
|
|
286
|
+
|
|
287
|
+
## Bulk operations
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
Most operations have a `_bulk_`/`bulk_` variant that takes a list and
|
|
291
|
+
|
|
292
|
+
does each item independently, returning one per-item success/failure
|
|
293
|
+
|
|
294
|
+
result rather than throwing on the first failure — so one bad name/id in
|
|
295
|
+
|
|
296
|
+
a batch never blocks the rest. Prefer these over looping individual
|
|
297
|
+
|
|
298
|
+
calls whenever acting on more than one item, e.g. after a search:
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
- `skill_bulk_get`/`memory_bulk_get` — fetch full bodies for a list of
|
|
302
|
+
names/ids in one call (memory's is by **id**, not key — search results
|
|
303
|
+
return ids).
|
|
304
|
+
- `skill_bulk_create`/`memory_bulk_create` — create several docs in one
|
|
305
|
+
call, each entry shaped like the singular `*_create` args.
|
|
306
|
+
- `skill_bulk_update`/`memory_bulk_update` — apply the same change
|
|
307
|
+
(add/remove tags, flip status, etc.) across many docs at once, e.g. to
|
|
308
|
+
a batch found via search.
|
|
309
|
+
- `skill_bulk_delete`/`memory_bulk_delete` — delete several docs by
|
|
310
|
+
name/id in one call.
|
|
311
|
+
- `relocate_bulk` — same idea for `relocate`, one entry per file.
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
## Multiple roots
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
A server can be configured with more than one skill root and/or memory
|
|
318
|
+
|
|
319
|
+
root at once — e.g. a personal skills folder plus a shared company repo.
|
|
320
|
+
|
|
321
|
+
When exactly one root of a given kind is configured, everything above
|
|
322
|
+
|
|
323
|
+
works unchanged: `skill_create`/`memory_create`/`relocate` write into it
|
|
324
|
+
|
|
325
|
+
with no extra parameter needed.
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
Once **two or more** roots of a kind are configured, `skill_create`,
|
|
329
|
+
|
|
330
|
+
`memory_create`, `memory_save_session`, and `relocate`'s `overrides` gain
|
|
331
|
+
|
|
332
|
+
a required (or, for `memory_save_session`, optional) `root` parameter —
|
|
333
|
+
|
|
334
|
+
its tool description lists the valid root names. Match a root by name
|
|
335
|
+
|
|
336
|
+
from what the user said ("save this to my personal skills", "put this in
|
|
337
|
+
|
|
338
|
+
the company repo") rather than guessing; if it's unclear which root they
|
|
339
|
+
|
|
340
|
+
mean, ask. `skill_list`/`memory_list` also gain an optional `root` filter
|
|
341
|
+
|
|
342
|
+
to narrow results to one root once multiple exist.
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
Roots (for both skills and memory) are managed through the web UI
|
|
346
|
+
|
|
347
|
+
(`bucket_open_ui`) — add one via its "+ Add root" folder browser, or
|
|
348
|
+
|
|
349
|
+
remove one via the ✕ on its chip (this only unregisters it and drops its
|
|
350
|
+
|
|
351
|
+
cached rows; it never deletes files on disk). If the server has zero
|
|
352
|
+
|
|
353
|
+
roots configured, the UI opens straight into a first-run "add your first
|
|
354
|
+
|
|
355
|
+
root" screen instead of the normal search view.
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
## relocate: pulling in an existing file
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
If there's already a local markdown file that should become a skill or
|
|
362
|
+
|
|
363
|
+
memory doc — the user says something like "save this file as memory" or
|
|
364
|
+
|
|
365
|
+
"turn this into a skill" — use `relocate(path, target, overrides?)`
|
|
366
|
+
|
|
367
|
+
instead of reading the file yourself and calling `*_create`. It infers
|
|
368
|
+
|
|
369
|
+
what it can from the filename and moves (not copies, by default) the
|
|
370
|
+
|
|
371
|
+
file into place.
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
- For `target: "memory"`, it tries to infer `key`/`doc_type`/`description`
|
|
375
|
+
from filenames like `2026-08-12-pde-433-partner-configuration-v3.md`
|
|
376
|
+
(date + ticket + slug), and does **nothing** — no partial move, no
|
|
377
|
+
guess — if the filename doesn't clearly match. If that happens, ask the
|
|
378
|
+
user for the key and description, then retry with
|
|
379
|
+
`overrides.key`/`overrides.description`.
|
|
380
|
+
- For `target: "skill"`, the name can usually be inferred from the
|
|
381
|
+
filename, but `overrides.description` is required — a good description
|
|
382
|
+
needs real content a filename can't provide, so don't try to invent one
|
|
383
|
+
yourself either; ask the user what the skill does and when to use it.
|
|
384
|
+
- Safe to re-run on the same file/target: if a matching doc already
|
|
385
|
+
exists, it reports that instead of duplicating.
|
|
386
|
+
---
|
|
11
387
|
## Two namespaces, two different jobs
|
|
12
388
|
|
|
13
389
|
If a `memory-bucket` MCP server is connected, it exposes two tool
|
|
@@ -164,6 +540,46 @@ If the user asks to save the current conversation/session as memory, use
|
|
|
164
540
|
**summary**, not a raw transcript. If `key` or `description` weren't
|
|
165
541
|
given, ask for both before calling it; don't guess a key from context.
|
|
166
542
|
|
|
543
|
+
## Finding things: search vs. list vs. get
|
|
544
|
+
|
|
545
|
+
- **`skill_list`/`memory_list`** — browse by metadata only (description,
|
|
546
|
+
tags, trigger phrases for skills; key prefix for memory). Fast, but
|
|
547
|
+
blind to body content.
|
|
548
|
+
- **`skill_search`/`memory_search`/`bucket_search`** — full-text search
|
|
549
|
+
over the actual markdown body (not just metadata), ranked by
|
|
550
|
+
relevance, with a highlighted snippet per hit. `query` is raw SQLite
|
|
551
|
+
FTS5 syntax: bare words AND by default, `"exact phrase"`, `prefix*`,
|
|
552
|
+
explicit `AND`/`OR`/`NOT`. **Hyphenated or punctuated terms must be
|
|
553
|
+
quoted** (`"blue-green"`, not `blue-green` — FTS5 treats a bare hyphen
|
|
554
|
+
as an operator and will error). `skill_search`/`memory_search` also
|
|
555
|
+
accept metadata filters (`status`, `owner`/`doc_type`, `tag`) combined
|
|
556
|
+
with the text query. Use `bucket_search` instead of the two separate
|
|
557
|
+
tools when you don't know (or don't care) which bucket something
|
|
558
|
+
landed in. All three return snippets, not full bodies — follow up with
|
|
559
|
+
`skill_get`/`memory_get` (or the bulk variants below) for the rest.
|
|
560
|
+
- **`skill_get`/`memory_get`** — exact-key lookup when you already know
|
|
561
|
+
the name/key.
|
|
562
|
+
|
|
563
|
+
## Bulk operations
|
|
564
|
+
|
|
565
|
+
Most operations have a `_bulk_`/`bulk_` variant that takes a list and
|
|
566
|
+
does each item independently, returning one per-item success/failure
|
|
567
|
+
result rather than throwing on the first failure — so one bad name/id in
|
|
568
|
+
a batch never blocks the rest. Prefer these over looping individual
|
|
569
|
+
calls whenever acting on more than one item, e.g. after a search:
|
|
570
|
+
|
|
571
|
+
- `skill_bulk_get`/`memory_bulk_get` — fetch full bodies for a list of
|
|
572
|
+
names/ids in one call (memory's is by **id**, not key — search results
|
|
573
|
+
return ids).
|
|
574
|
+
- `skill_bulk_create`/`memory_bulk_create` — create several docs in one
|
|
575
|
+
call, each entry shaped like the singular `*_create` args.
|
|
576
|
+
- `skill_bulk_update`/`memory_bulk_update` — apply the same change
|
|
577
|
+
(add/remove tags, flip status, etc.) across many docs at once, e.g. to
|
|
578
|
+
a batch found via search.
|
|
579
|
+
- `skill_bulk_delete`/`memory_bulk_delete` — delete several docs by
|
|
580
|
+
name/id in one call.
|
|
581
|
+
- `relocate_bulk` — same idea for `relocate`, one entry per file.
|
|
582
|
+
|
|
167
583
|
## Multiple roots
|
|
168
584
|
|
|
169
585
|
A server can be configured with more than one skill root and/or memory
|
|
@@ -4,6 +4,7 @@ import { writeMarkdownFile } from '../store/markdown-file.js';
|
|
|
4
4
|
import { assertValidSkillName } from '../store/skill-name.js';
|
|
5
5
|
import { resolveWithinBase } from '../store/safe-path.js';
|
|
6
6
|
import { upsertFile, removeFile, scanSingleRoot, unregisterRoot, skillSyncSpec } from '../store/sync.js';
|
|
7
|
+
import { SearchQueryError } from '../store/search.js';
|
|
7
8
|
function rowToDoc(row) {
|
|
8
9
|
return {
|
|
9
10
|
name: row.id,
|
|
@@ -11,6 +12,8 @@ function rowToDoc(row) {
|
|
|
11
12
|
tags: JSON.parse(row.tags),
|
|
12
13
|
trigger_phrases: JSON.parse(row.trigger_phrases),
|
|
13
14
|
metadata: { owner: row.owner, status: row.status, extends: row.extends },
|
|
15
|
+
deprecated: !!row.deprecated,
|
|
16
|
+
created_at: row.created_at ?? undefined,
|
|
14
17
|
source_path: row.source_path,
|
|
15
18
|
root: row.root,
|
|
16
19
|
body: row.body,
|
|
@@ -94,10 +97,58 @@ export class SkillRepository {
|
|
|
94
97
|
: items;
|
|
95
98
|
return filtered.map(({ triggerPhrases: _tp, ...rest }) => rest);
|
|
96
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Full-text search over skill description/body/tags via FTS5 — `query` is
|
|
102
|
+
* raw FTS5 MATCH syntax (AND/OR/NOT, "phrases", prefix*). Ranked by bm25.
|
|
103
|
+
* Optional metadata filters (root/status/owner/tag) apply before limit/offset,
|
|
104
|
+
* so pagination stays correct even when filtering narrows the FTS hit set.
|
|
105
|
+
*/
|
|
106
|
+
search(query, opts = {}) {
|
|
107
|
+
const { root, status, owner, tag, limit = 20, offset = 0 } = opts;
|
|
108
|
+
const conditions = [];
|
|
109
|
+
const params = [query];
|
|
110
|
+
if (root) {
|
|
111
|
+
conditions.push('s.root = ?');
|
|
112
|
+
params.push(root);
|
|
113
|
+
}
|
|
114
|
+
if (status) {
|
|
115
|
+
conditions.push('s.status = ?');
|
|
116
|
+
params.push(status);
|
|
117
|
+
}
|
|
118
|
+
if (owner) {
|
|
119
|
+
conditions.push('s.owner = ?');
|
|
120
|
+
params.push(owner);
|
|
121
|
+
}
|
|
122
|
+
if (tag) {
|
|
123
|
+
conditions.push('EXISTS (SELECT 1 FROM json_each(s.tags) WHERE value = ?)');
|
|
124
|
+
params.push(tag);
|
|
125
|
+
}
|
|
126
|
+
params.push(limit, offset);
|
|
127
|
+
try {
|
|
128
|
+
const rows = this.db
|
|
129
|
+
.prepare(`SELECT s.id AS name, s.description, s.root,
|
|
130
|
+
snippet(search_index, 3, '<<', '>>', '…', 20) AS snippet,
|
|
131
|
+
-bm25(search_index) AS score
|
|
132
|
+
FROM search_index
|
|
133
|
+
JOIN skills s ON s.id = search_index.ref_id
|
|
134
|
+
WHERE search_index.ref_table = 'skills' AND search_index MATCH ? ${conditions.map((c) => `AND ${c}`).join(' ')}
|
|
135
|
+
ORDER BY bm25(search_index)
|
|
136
|
+
LIMIT ? OFFSET ?`)
|
|
137
|
+
.all(...params);
|
|
138
|
+
return rows;
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
throw new SearchQueryError(query, err);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
97
144
|
get(name) {
|
|
98
145
|
const row = this.db.prepare(`SELECT * FROM skills WHERE id = ?`).get(name);
|
|
99
146
|
return row ? rowToDoc(row) : null;
|
|
100
147
|
}
|
|
148
|
+
/** Fetches many skills by name in one call — e.g. hydrating full bodies for a batch of search() hits. Missing names are simply absent from the result, not errors. */
|
|
149
|
+
bulkGet(names) {
|
|
150
|
+
return names.map((name) => this.get(name)).filter((doc) => doc !== null);
|
|
151
|
+
}
|
|
101
152
|
/**
|
|
102
153
|
* Creates <root>/[folder/]<name>/SKILL.md — folder-per-skill, per the
|
|
103
154
|
* agentskills.io spec (`name` must equal the containing folder's name).
|
|
@@ -128,6 +179,8 @@ export class SkillRepository {
|
|
|
128
179
|
status: frontmatter.status ?? 'unreviewed',
|
|
129
180
|
extends: frontmatter.extends ?? null,
|
|
130
181
|
},
|
|
182
|
+
deprecated: false,
|
|
183
|
+
created_at: new Date().toISOString(),
|
|
131
184
|
source_path: filePath,
|
|
132
185
|
root: targetRoot.name,
|
|
133
186
|
};
|
|
@@ -135,13 +188,37 @@ export class SkillRepository {
|
|
|
135
188
|
upsertFile(this.db, this.syncSpec, filePath);
|
|
136
189
|
return { ...fm, body };
|
|
137
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Creates many skills in one call — each entry is the same shape as create()'s
|
|
193
|
+
* args. Returns per-name results so one bad entry (duplicate name, invalid
|
|
194
|
+
* name, existing directory) doesn't abort the rest of the batch.
|
|
195
|
+
*/
|
|
196
|
+
bulkCreate(entries) {
|
|
197
|
+
return entries.map((entry) => {
|
|
198
|
+
try {
|
|
199
|
+
this.create(entry.frontmatter, entry.body, entry.folder, entry.root);
|
|
200
|
+
return { name: entry.frontmatter.name, ok: true };
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
return { name: entry.frontmatter.name, ok: false, error: err.message };
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/** Name of the always-present, non-removable builtin root (roots[0]) — never user content, never deprecatable. */
|
|
208
|
+
isBuiltin(doc) {
|
|
209
|
+
return doc.root === this.roots[0]?.name;
|
|
210
|
+
}
|
|
138
211
|
update(name, frontmatter, body) {
|
|
139
212
|
const existing = this.get(name);
|
|
140
213
|
if (!existing)
|
|
141
214
|
throw new Error(`skill with name "${name}" not found`);
|
|
215
|
+
// Builtin skills (e.g. memory-bucket-authoring) are the server's own always-present
|
|
216
|
+
// documentation, not user content — deprecating them would hide guidance every session needs.
|
|
217
|
+
const deprecated = this.isBuiltin(existing) ? existing.deprecated : frontmatter?.deprecated;
|
|
142
218
|
const merged = {
|
|
143
219
|
...existing,
|
|
144
220
|
...frontmatter,
|
|
221
|
+
deprecated,
|
|
145
222
|
name: existing.name, // name is immutable post-creation (it's also the folder name)
|
|
146
223
|
metadata: {
|
|
147
224
|
...existing.metadata,
|
|
@@ -182,15 +259,67 @@ export class SkillRepository {
|
|
|
182
259
|
upsertFile(this.db, this.syncSpec, newFilePath);
|
|
183
260
|
return { ...merged, body: existing.body };
|
|
184
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Applies the same frontmatter change to many skills at once — e.g. add/remove
|
|
264
|
+
* a tag across a batch found via search(), or flip status for a group. Tags in
|
|
265
|
+
* `add_tags`/`remove_tags` are merged/subtracted per-skill; other fields (owner,
|
|
266
|
+
* status, extends) overwrite uniformly when provided. Never touches body.
|
|
267
|
+
* Returns per-name results so partial failures (e.g. an unknown name) don't
|
|
268
|
+
* abort the rest of the batch.
|
|
269
|
+
*/
|
|
270
|
+
bulkUpdate(names, changes) {
|
|
271
|
+
return names.map((name) => {
|
|
272
|
+
try {
|
|
273
|
+
const existing = this.get(name);
|
|
274
|
+
if (!existing)
|
|
275
|
+
throw new Error(`skill with name "${name}" not found`);
|
|
276
|
+
const builtin = this.isBuiltin(existing);
|
|
277
|
+
let tags = existing.tags;
|
|
278
|
+
if (changes.add_tags?.length)
|
|
279
|
+
tags = Array.from(new Set([...tags, ...changes.add_tags]));
|
|
280
|
+
if (changes.remove_tags?.length)
|
|
281
|
+
tags = tags.filter((t) => !changes.remove_tags.includes(t));
|
|
282
|
+
this.update(name, {
|
|
283
|
+
tags,
|
|
284
|
+
owner: changes.owner,
|
|
285
|
+
status: changes.status,
|
|
286
|
+
extends: changes.extends,
|
|
287
|
+
...(changes.deprecated !== undefined && !builtin ? { deprecated: changes.deprecated } : {}),
|
|
288
|
+
});
|
|
289
|
+
return { name, ok: true };
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
return { name, ok: false, error: err.message };
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
185
296
|
/** Removes the whole skill directory, including any scripts/references/assets alongside SKILL.md. */
|
|
186
297
|
delete(name) {
|
|
187
298
|
const existing = this.get(name);
|
|
188
299
|
if (!existing)
|
|
189
300
|
throw new Error(`skill with name "${name}" not found`);
|
|
301
|
+
if (this.isBuiltin(existing))
|
|
302
|
+
throw new Error(`skill "${name}" is builtin and cannot be deleted`);
|
|
190
303
|
const skillDir = path.dirname(existing.source_path);
|
|
191
304
|
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
192
305
|
removeFile(this.db, 'skills', existing.source_path);
|
|
193
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Deletes many skills by name in one call — e.g. cleaning up a batch found
|
|
309
|
+
* via search()/list(). Returns per-name results so one bad name doesn't
|
|
310
|
+
* abort the rest of the batch.
|
|
311
|
+
*/
|
|
312
|
+
bulkDelete(names) {
|
|
313
|
+
return names.map((name) => {
|
|
314
|
+
try {
|
|
315
|
+
this.delete(name);
|
|
316
|
+
return { name, ok: true };
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
return { name, ok: false, error: err.message };
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
194
323
|
}
|
|
195
324
|
function stripSourcePath(fm) {
|
|
196
325
|
const { source_path: _sp, root: _root, ...rest } = fm;
|