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.
@@ -1,13 +1,389 @@
1
1
  ---
2
- name: "memory-bucket-authoring"
3
- description: "Explains how to author and use skills and memory docs through the memory-bucket MCP server's skill_*/memory_*/relocate tools — the skill.md frontmatter schema (agentskills.io standard), the memory frontmatter schema, and when to use each. Use whenever a memory-bucket MCP server is connected and you need to create, update, or decide between a skill and a memory doc, or when the user asks to save a plan/spec/session summary or to record a reusable pattern."
4
- tags: ["memory-bucket", "meta", "authoring"]
5
- trigger_phrases: ["save this as a skill", "save this to memory", "remember this plan", "create a skill", "memory-bucket"]
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: "company"
8
- status: "stable"
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;