@luanpdd/kit-mcp 1.14.0 → 1.16.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.
Files changed (38) hide show
  1. package/README.md +4 -0
  2. package/kit/COMPATIBILITY.md +65 -0
  3. package/kit/agents/ai-mutation-tester.md +1 -11
  4. package/kit/agents/burn-rate-forecaster.md +1 -9
  5. package/kit/agents/cascading-failures-auditor.md +1 -9
  6. package/kit/agents/golden-signals-instrumenter.md +1 -11
  7. package/kit/agents/incident-investigator.md +1 -9
  8. package/kit/agents/legacy-characterizer.md +1 -11
  9. package/kit/agents/load-shedding-instrumenter.md +1 -9
  10. package/kit/agents/observability-coverage-auditor.md +1 -11
  11. package/kit/agents/observability-instrumenter.md +1 -11
  12. package/kit/agents/omm-auditor.md +1 -9
  13. package/kit/agents/payload-capture-instrumenter.md +1 -11
  14. package/kit/agents/postmortem-writer.md +1 -11
  15. package/kit/agents/prr-conductor.md +1 -11
  16. package/kit/agents/refactor-safety-auditor.md +1 -11
  17. package/kit/agents/release-pipeline-auditor.md +1 -9
  18. package/kit/agents/seam-finder.md +1 -9
  19. package/kit/agents/shotgun-surgery-detector.md +1 -11
  20. package/kit/agents/slo-engineer.md +1 -9
  21. package/kit/agents/storytelling-analyst.md +1 -11
  22. package/kit/agents/supabase-architect.md +1 -9
  23. package/kit/agents/supabase-auth-bootstrapper.md +1 -11
  24. package/kit/agents/supabase-edge-fn-writer.md +1 -11
  25. package/kit/agents/supabase-migration-writer.md +1 -9
  26. package/kit/agents/supabase-realtime-implementer.md +1 -9
  27. package/kit/agents/supabase-rls-writer.md +1 -9
  28. package/kit/agents/supabase-storage-implementer.md +1 -9
  29. package/kit/agents/toil-auditor.md +1 -11
  30. package/kit/file-manifest.json +251 -250
  31. package/package.json +6 -4
  32. package/src/cli/index.js +50 -17
  33. package/src/core/manifest-verify.js +5 -1
  34. package/src/core/reverse-sync.js +23 -6
  35. package/src/core/sync.js +35 -4
  36. package/src/core/ui.js +19 -1
  37. package/src/core/watch.js +29 -3
  38. package/src/mcp-server/index.js +14 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luanpdd/kit-mcp",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "description": "Generic infrastructure to ship YOUR personal kit of agents/commands/skills as an MCP server, with cross-IDE sync (Claude Code, Cursor, Codex, Gemini, Windsurf, Antigravity, Copilot, Trae).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,14 +44,16 @@
44
44
  "test": "node test/run.mjs test/unit",
45
45
  "test:integration": "node test/run.mjs test/integration",
46
46
  "test:all": "node test/run.mjs test",
47
- "prepublishOnly": "node test/run.mjs test/unit && node test/run.mjs test/integration"
47
+ "prepublishOnly": "node scripts/regen-manifest.js && node scripts/update-readme-counts.js && node test/run.mjs test/unit && node test/run.mjs test/integration"
48
48
  },
49
49
  "dependencies": {
50
- "@inquirer/prompts": "^8.4.2",
51
50
  "@modelcontextprotocol/sdk": "^1.0.0",
52
- "chokidar": "^5.0.0",
53
51
  "commander": "^14.0.3",
54
52
  "open": "^11.0.0",
55
53
  "picocolors": "^1.1.1"
54
+ },
55
+ "optionalDependencies": {
56
+ "@inquirer/prompts": "^8.4.2",
57
+ "chokidar": "^5.0.0"
56
58
  }
57
59
  }
package/src/cli/index.js CHANGED
@@ -29,11 +29,16 @@ import { listReplays, loadReplay } from '../core/replays.js';
29
29
  import { installMcp, listInstallTargets } from '../mcp-server/install.js';
30
30
  import * as render from './render.js';
31
31
  import { c, icons, spinner, progress, select, confirm } from '../core/ui.js';
32
- import { createServer } from '../ui/server.js';
33
32
  import { readLock, lockPathFor } from '../ui/lockfile.js';
34
- import { wrapProgressForUi } from '../ui/wrapper.js';
35
- import { openBrowser } from '../ui/browser.js';
36
33
  import { checkUpgrade, getLocalVersion } from './upgrade-check.js';
34
+ // PERF-16-04: ui/server.js, ui/wrapper.js, ui/browser.js are loaded LAZILY
35
+ // inside the subcommand handlers that need them. See:
36
+ // - maybeWrapForUi (gated on lockfile presence)
37
+ // - ui.start (createServer + openBrowser)
38
+ // - ui.open (openBrowser)
39
+ // This trims ~700 LOC + transitive deps off the cold-start path of non-UI
40
+ // commands like `kit kit list-agents --terse`. lockfile.js stays eager because
41
+ // readLock() is called by every withProgress() invocation and is dep-free.
37
42
  import http from 'node:http';
38
43
  import fs from 'node:fs';
39
44
  import os from 'node:os';
@@ -106,7 +111,7 @@ async function withProgress(label, total, fn, { tool, projectRoot } = {}) {
106
111
  }
107
112
 
108
113
  // Auto-wrap if a sidecar is running for this projectRoot.
109
- const wrapper = maybeWrapForUi(onProgress, { tool, projectRoot });
114
+ const wrapper = await maybeWrapForUi(onProgress, { tool, projectRoot });
110
115
  try {
111
116
  const r = await fn(wrapper);
112
117
  if (p) p.finish(label);
@@ -121,7 +126,11 @@ async function withProgress(label, total, fn, { tool, projectRoot } = {}) {
121
126
 
122
127
  // maybeWrapForUi — returns the original callback unchanged when no sidecar is up
123
128
  // or the user opted out. Otherwise returns a wrapped callback with .done/.error.
124
- function maybeWrapForUi(onProgress, { tool, projectRoot } = {}) {
129
+ //
130
+ // PERF-16-04: this is async because we lazy-load ../ui/wrapper.js only when a
131
+ // sidecar lockfile is detected. Common path (no sidecar) returns synchronously
132
+ // via passthroughWrapper without touching wrapper.js or its transitive deps.
133
+ async function maybeWrapForUi(onProgress, { tool, projectRoot } = {}) {
125
134
  const globalOpts = program.opts();
126
135
  // commander stores `--no-ui` as opts.ui === false
127
136
  if (globalOpts.ui === false || process.env.KIT_MCP_NO_UI === '1') {
@@ -131,6 +140,8 @@ function maybeWrapForUi(onProgress, { tool, projectRoot } = {}) {
131
140
  if (!readLock(root)) {
132
141
  return passthroughWrapper(onProgress);
133
142
  }
143
+ // Lazy import — only paid when a sidecar IS up for this project.
144
+ const { wrapProgressForUi } = await import('../ui/wrapper.js');
134
145
  return wrapProgressForUi(onProgress, { projectRoot: root, tool: tool ?? null });
135
146
  }
136
147
 
@@ -154,20 +165,36 @@ function slim(x) {
154
165
  return { kind: x.kind, name: x.name, description: summarize(x.description) };
155
166
  }
156
167
 
168
+ // PERF-15-01: terse variant — paridade com mcp-server slimTerse. CLI flag --terse
169
+ // controla seleção. Mantém o mesmo shape {kind, name} para programmatic consumers
170
+ // que parseiam --json output (consistência cross-surface).
171
+ function slimTerse(x) {
172
+ return { kind: x.kind, name: x.name };
173
+ }
174
+
157
175
  // --- kit ---
158
176
  const kit = program.command('kit').description('Browse the canonical kit.');
159
- kit.command('list-agents').action(async () => {
160
- const k = await withSpinner('Loading kit...', () => listKit());
161
- out(k.agents.map(slim), v => render.renderKitList(v, 'agent'));
162
- });
163
- kit.command('list-commands').action(async () => {
164
- const k = await withSpinner('Loading kit...', () => listKit());
165
- out(k.commands.map(slim), v => render.renderKitList(v, 'command'));
166
- });
167
- kit.command('list-skills').action(async () => {
168
- const k = await withSpinner('Loading kit...', () => listKit());
169
- out([...k.skills, ...k.skillsExtras].map(slim), v => render.renderKitList(v, 'skill'));
170
- });
177
+ kit.command('list-agents')
178
+ .option('--terse', 'Omit description; return only {kind, name} (PERF-15-01)')
179
+ .action(async (opts) => {
180
+ const k = await withSpinner('Loading kit...', () => listKit());
181
+ const variant = opts.terse ? slimTerse : slim;
182
+ out(k.agents.map(variant), v => render.renderKitList(v, 'agent'));
183
+ });
184
+ kit.command('list-commands')
185
+ .option('--terse', 'Omit description; return only {kind, name} (PERF-15-01)')
186
+ .action(async (opts) => {
187
+ const k = await withSpinner('Loading kit...', () => listKit());
188
+ const variant = opts.terse ? slimTerse : slim;
189
+ out(k.commands.map(variant), v => render.renderKitList(v, 'command'));
190
+ });
191
+ kit.command('list-skills')
192
+ .option('--terse', 'Omit description; return only {kind, name} (PERF-15-01)')
193
+ .action(async (opts) => {
194
+ const k = await withSpinner('Loading kit...', () => listKit());
195
+ const variant = opts.terse ? slimTerse : slim;
196
+ out([...k.skills, ...k.skillsExtras].map(variant), v => render.renderKitList(v, 'skill'));
197
+ });
171
198
  kit.command('get <kind> <name>').action(async (kind, name) => {
172
199
  const k = await listKit();
173
200
  const item = findItem(k, kind, name);
@@ -391,6 +418,8 @@ ui.command('start')
391
418
  const projectRoot = opts.projectRoot || process.cwd();
392
419
  const port = opts.port ? Number(opts.port) : undefined;
393
420
  const idleMs = opts.idleMs !== undefined ? Number(opts.idleMs) : undefined;
421
+ // PERF-16-04: lazy-load the sidecar HTTP server module only when starting it.
422
+ const { createServer } = await import('../ui/server.js');
394
423
  const srv = createServer({ projectRoot, idleMs });
395
424
  try {
396
425
  const { port: actualPort } = await srv.start({ port });
@@ -406,6 +435,8 @@ ui.command('start')
406
435
  }
407
436
  }).catch(() => { /* offline / silent */ });
408
437
  if (opts.open !== false) {
438
+ // PERF-16-04: lazy-load browser-opener (it lazy-loads `open` package itself).
439
+ const { openBrowser } = await import('../ui/browser.js');
409
440
  await openBrowser(url);
410
441
  }
411
442
  // The server's own SIGINT handler will perform shutdown + cleanup.
@@ -464,6 +495,8 @@ ui.command('open')
464
495
  const lock = readLock(projectRoot);
465
496
  if (!lock) return fail('no sidecar running — start one with `kit ui start`');
466
497
  const url = `http://127.0.0.1:${lock.port}/`;
498
+ // PERF-16-04: lazy-load browser-opener.
499
+ const { openBrowser } = await import('../ui/browser.js');
467
500
  const r = await openBrowser(url, { force: true });
468
501
  if (!r.opened) {
469
502
  process.stderr.write(`${c.yellow(icons.warn)} could not open browser (${r.reason}); copy the URL above\n`);
@@ -59,7 +59,11 @@ export async function verifyManifest(kitRoot) {
59
59
  missing.push(rel);
60
60
  continue;
61
61
  }
62
- const actual = crypto.createHash('sha256').update(buf).digest('hex');
62
+ // Normalize CRLF→LF before hashing so manifest is platform-stable.
63
+ // git checkout converts EOL on Windows but Linux CI checks out LF —
64
+ // hashing raw bytes would diverge across platforms.
65
+ const normalized = Buffer.from(buf.toString('binary').replace(/\r\n/g, '\n'), 'binary');
66
+ const actual = crypto.createHash('sha256').update(normalized).digest('hex');
63
67
  if (actual !== expected) {
64
68
  mismatches.push({ path: rel, expected: expected.slice(0, 16), actual: actual.slice(0, 16) });
65
69
  }
@@ -31,17 +31,34 @@ export async function detectReverse(targetId, opts = {}) {
31
31
 
32
32
  const candidates = [];
33
33
 
34
- // For each capability that this target supports AND that has files on disk,
35
- // walk and classify.
36
- if (target.agents) await scanCapability(candidates, 'agent', target.agents, projectRoot, kit.agents, kitRoot);
37
- if (target.commands) await scanCapability(candidates, 'command', target.commands, projectRoot, kit.commands, kitRoot);
38
- if (target.skills) await scanSkills (candidates, target.skills, projectRoot, [...kit.skills, ...kit.skillsExtras], kitRoot);
34
+ // PERF-16-03: parallelize the 5 scans. Each scan reads a distinct subdirectory
35
+ // of the IDE layout (.claude/agents, .claude/commands, .claude/skills,
36
+ // .claude/framework, .claude/hooks) — there is no I/O contention between them.
37
+ //
38
+ // Each scan continues to push into the shared `candidates` array. This is safe
39
+ // under the single-threaded JS event loop: `Array.prototype.push` is a
40
+ // synchronous operation that completes between awaits, so concurrent scans
41
+ // never produce a torn write. The trade-off is that candidate ordering is no
42
+ // longer deterministic across categories — existing reverse-sync tests use
43
+ // `.find` / `.some` / `.filter` and never index `candidates[N]`, so this is a
44
+ // safe widening of the contract.
45
+ //
46
+ // Error semantics: `Promise.all` rejects on the first rejection — identical to
47
+ // the previous sequential `await` chain (which also propagated the first error
48
+ // and aborted the rest). Fail-fast is preserved.
49
+ const pending = [];
50
+
51
+ if (target.agents) pending.push(scanCapability(candidates, 'agent', target.agents, projectRoot, kit.agents, kitRoot));
52
+ if (target.commands) pending.push(scanCapability(candidates, 'command', target.commands, projectRoot, kit.commands, kitRoot));
53
+ if (target.skills) pending.push(scanSkills (candidates, target.skills, projectRoot, [...kit.skills, ...kit.skillsExtras], kitRoot));
39
54
  for (const cap of ['framework', 'hooks']) {
40
55
  const spec = target[cap];
41
56
  if (!spec || spec.mode !== 'mirror-tree') continue;
42
- await scanMirrorTree(candidates, cap, spec, projectRoot, kitRoot);
57
+ pending.push(scanMirrorTree(candidates, cap, spec, projectRoot, kitRoot));
43
58
  }
44
59
 
60
+ await Promise.all(pending);
61
+
45
62
  return { target: targetId, projectRoot, kitRoot, candidates };
46
63
  }
47
64
 
package/src/core/sync.js CHANGED
@@ -19,6 +19,18 @@ const STUB_MARKER = '<!-- kit-mcp:reference -->';
19
19
  const MANAGED_MARKER_FILE = '.kit-mcp-managed';
20
20
  const MANAGED_MARKER_BODY = '# Managed by @luanpdd/kit-mcp — this directory is overwritten on every `kit sync install`.\n# Do not edit files here directly; edit the canonical source under kit/ and re-run sync.\n# Removing this file disables `kit sync remove` cleanup of this tree.\n';
21
21
 
22
+ // PERF-16-01: parallelize file writes in syncTo() via Promise.all batches.
23
+ // BATCH_SIZE=16 default — safe under Linux ulimit 1024 fd default and
24
+ // macOS/Windows equivalents. Configurable via env (e.g. on slow disks).
25
+ // Values outside [1, 256] fall back to 16 (defensive — env vars are strings).
26
+ function resolveBatchSize() {
27
+ const raw = process.env.KIT_MCP_SYNC_BATCH_SIZE;
28
+ if (!raw) return 16;
29
+ const n = Number.parseInt(raw, 10);
30
+ if (!Number.isFinite(n) || n < 1 || n > 256) return 16;
31
+ return n;
32
+ }
33
+
22
34
  export async function syncTo(targetId, opts = {}) {
23
35
  const target = getTarget(targetId);
24
36
  const projectRoot = path.resolve(opts.projectRoot ?? process.cwd());
@@ -100,16 +112,35 @@ export async function syncTo(targetId, opts = {}) {
100
112
  }
101
113
 
102
114
  if (!dryRun) {
103
- let i = 0;
104
- for (const op of ops) {
115
+ const BATCH_SIZE = resolveBatchSize();
116
+ let completed = 0;
117
+ const total = ops.length;
118
+
119
+ // Apply one op (mkdir + write or copy + onProgress).
120
+ // Each op is independent: ops[] is built so writes don't share parent
121
+ // directories that need ordering — mkdir({recursive:true}) is idempotent
122
+ // even when 16 ops race for the same parent dir.
123
+ const applyOp = async (op) => {
105
124
  await fs.mkdir(path.dirname(op.path), { recursive: true });
106
125
  if (op.treeCopy) {
107
126
  await fs.copyFile(op.srcAbs, op.path);
108
127
  } else {
109
128
  await fs.writeFile(op.path, op.content, 'utf8');
110
129
  }
111
- i++;
112
- onProgress({ phase: op.kind, current: i, total: ops.length, label: path.basename(op.path) });
130
+ // Counter increment is single-threaded by JS event loop semantics —
131
+ // no torn reads even with 16 ops resolving in any order.
132
+ completed += 1;
133
+ onProgress({ phase: op.kind, current: completed, total, label: path.basename(op.path) });
134
+ };
135
+
136
+ // Sequential batches — within a batch, Promise.all parallelizes writes;
137
+ // between batches, we await to bound max-in-flight at BATCH_SIZE. If any
138
+ // op in a batch rejects, Promise.all rejects on first failure (matches
139
+ // existing behavior — sync.js had no retry logic, so a single fs error
140
+ // already aborted the install).
141
+ for (let i = 0; i < ops.length; i += BATCH_SIZE) {
142
+ const slice = ops.slice(i, i + BATCH_SIZE);
143
+ await Promise.all(slice.map(applyOp));
113
144
  }
114
145
  }
115
146
 
package/src/core/ui.js CHANGED
@@ -10,7 +10,23 @@
10
10
  // - Zero hidden globals — every primitive is a plain function/class.
11
11
 
12
12
  import pc from 'picocolors';
13
- import { select as inqSelect, confirm as inqConfirm } from '@inquirer/prompts';
13
+
14
+ // PERF-16-05: @inquirer/prompts é optionalDependency. Carregamos lazy dentro
15
+ // de select()/confirm() para que (a) modo MCP server e CI não paguem o custo
16
+ // de boot, e (b) `npm install --omit=optional` produza CLI core funcional
17
+ // (apenas comandos interativos falham com mensagem descritiva).
18
+ let _inquirerModule = null;
19
+ async function loadInquirer() {
20
+ if (_inquirerModule) return _inquirerModule;
21
+ try {
22
+ _inquirerModule = await import('@inquirer/prompts');
23
+ return _inquirerModule;
24
+ } catch (err) {
25
+ throw new Error(
26
+ 'Interactive prompts require @inquirer/prompts. Install with `npm i @inquirer/prompts` or pass --yes / --no-interactive to skip the prompt.'
27
+ );
28
+ }
29
+ }
14
30
 
15
31
  // --- color helpers ---
16
32
 
@@ -127,6 +143,7 @@ export async function select(opts) {
127
143
  if (!process.stdin.isTTY) {
128
144
  throw new Error('Interactive prompt unavailable: stdin is not a TTY. Pass the value as a flag instead.');
129
145
  }
146
+ const { select: inqSelect } = await loadInquirer();
130
147
  return inqSelect(opts);
131
148
  }
132
149
 
@@ -134,6 +151,7 @@ export async function confirm(opts) {
134
151
  if (!process.stdin.isTTY) {
135
152
  throw new Error('Interactive prompt unavailable: stdin is not a TTY. Pass --yes to skip confirmation.');
136
153
  }
154
+ const { confirm: inqConfirm } = await loadInquirer();
137
155
  return inqConfirm(opts);
138
156
  }
139
157
 
package/src/core/watch.js CHANGED
@@ -11,16 +11,35 @@
11
11
 
12
12
  import path from 'node:path';
13
13
  import fs from 'node:fs/promises';
14
- import chokidar from 'chokidar';
15
14
  import { syncTo } from './sync.js';
16
15
  import { listTargets } from './registry.js';
17
- import { resolveKitRoot } from './kit.js';
16
+ import { resolveKitRoot, clearKitCache } from './kit.js';
17
+
18
+ // PERF-16-06: chokidar é optionalDependency. Carregamos lazy dentro de watchKit()
19
+ // para que (a) `kit sync install` (que NÃO usa watch) não pague o custo de boot,
20
+ // e (b) `npm install --omit=optional` produza CLI core funcional (apenas
21
+ // `kit sync watch` falha com mensagem descritiva).
22
+ let _chokidarModule = null;
23
+ async function loadChokidar() {
24
+ if (_chokidarModule) return _chokidarModule;
25
+ try {
26
+ const mod = await import('chokidar');
27
+ _chokidarModule = mod.default || mod;
28
+ return _chokidarModule;
29
+ } catch (err) {
30
+ throw new Error(
31
+ 'kit sync watch requires chokidar. Install with `npm i chokidar` or use `kit sync install <target>` for one-shot syncing instead.'
32
+ );
33
+ }
34
+ }
18
35
 
19
36
  export async function watchKit(targets, opts = {}) {
20
37
  const projectRoot = path.resolve(opts.projectRoot ?? process.cwd());
21
38
  const kitRoot = resolveKitRoot(opts.kitRoot);
22
39
  const mode = opts.mode ?? 'reference';
23
- const debounceMs = Number.isFinite(opts.debounceMs) ? opts.debounceMs : 300;
40
+ // PERF-16-02: bump default 300 500ms to coalesce IDE save-bursts (typical
41
+ // IDE auto-save fires 5-10 events in < 500ms during a single user save).
42
+ const debounceMs = Number.isFinite(opts.debounceMs) ? opts.debounceMs : 500;
24
43
  const onLog = opts.onLog ?? (() => {});
25
44
 
26
45
  if (!Array.isArray(targets) || targets.length === 0) {
@@ -33,6 +52,7 @@ export async function watchKit(targets, opts = {}) {
33
52
  onLog(`✓ initial sync → ${t} (${r.written.length} files)`);
34
53
  }
35
54
 
55
+ const chokidar = await loadChokidar();
36
56
  const watcher = chokidar.watch(kitRoot, {
37
57
  ignored: (p) => /(^|[/\\])\.[^/\\]/.test(p), // ignore dotfiles/dotdirs
38
58
  persistent: true,
@@ -46,6 +66,12 @@ export async function watchKit(targets, opts = {}) {
46
66
  if (pending) clearTimeout(pending);
47
67
  pending = setTimeout(async () => {
48
68
  pending = null;
69
+ // PERF-16-02: invalidate kitCache (TTL 30s in kit.js PERF-01) BEFORE
70
+ // re-sync — otherwise listKit() inside syncTo can return the pre-edit
71
+ // cached value if the burst happened within the TTL window. Coalescing
72
+ // the edit-burst via debounce means clearKitCache fires AT MOST ONCE
73
+ // per 500ms window, regardless of how many save events came in.
74
+ clearKitCache();
49
75
  for (const t of targets) {
50
76
  try {
51
77
  const r = await syncTo(t, { projectRoot, kitRoot, mode });
@@ -42,6 +42,7 @@ const TOOLS = [
42
42
  kind: { type: 'string', enum: ['agent', 'command', 'skill'], description: 'For action=get' },
43
43
  name: { type: 'string', description: 'For action=get' },
44
44
  query: { type: 'string', description: 'For action=search' },
45
+ terse: { type: 'boolean', description: 'For action=list-*: omit description, return only {kind, name}. Default false (PERF-15-01).' },
45
46
  },
46
47
  required: ['action'],
47
48
  },
@@ -152,10 +153,13 @@ export const PKG_VERSION = readPkgVersion();
152
153
 
153
154
  async function handleKit(args) {
154
155
  const kit = await listKit();
156
+ // PERF-15-01: terse mode skips description payload entirely. Backward-compat:
157
+ // args.terse undefined/false preserves slim()+summarize() cap-80 behavior.
158
+ const variant = args.terse === true ? slimTerse : slim;
155
159
  switch (args.action) {
156
- case 'list-agents': return kit.agents.map(slim);
157
- case 'list-commands': return kit.commands.map(slim);
158
- case 'list-skills': return [...kit.skills, ...kit.skillsExtras].map(slim);
160
+ case 'list-agents': return kit.agents.map(variant);
161
+ case 'list-commands': return kit.commands.map(variant);
162
+ case 'list-skills': return [...kit.skills, ...kit.skillsExtras].map(variant);
159
163
  case 'get': {
160
164
  const item = findItem(kit, args.kind, args.name);
161
165
  if (!item) return { error: `Not found: ${args.kind}/${args.name}` };
@@ -305,6 +309,13 @@ function slim(x) {
305
309
  return { kind: x.kind, name: x.name, description: summarize(x.description) };
306
310
  }
307
311
 
312
+ // PERF-15-01: terse variant — omits description entirely. Used when MCP client
313
+ // only needs name discovery (e.g. populating UI lists, validating slug references).
314
+ // Default action=list-* still returns description capped via slim()/summarize().
315
+ function slimTerse(x) {
316
+ return { kind: x.kind, name: x.name };
317
+ }
318
+
308
319
  // --- server bootstrap ---
309
320
 
310
321
  export async function createServer() {