@luanpdd/kit-mcp 1.12.0 → 1.13.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 CHANGED
@@ -28,9 +28,9 @@ Inspired by [vinilana/dotcontext](https://github.com/vinilana/dotcontext) — se
28
28
  ```
29
29
  kit-mcp/
30
30
  ├── kit/ ← bundled brownfield workflow (PT-BR)
31
- │ ├── agents/ 19 agents (planner, executor, verifier, debugger,
31
+ │ ├── agents/ 47 agents (planner, executor, verifier, debugger,
32
32
  │ │ ui-auditor, codebase-mapper, …)
33
- │ ├── commands/ 60 slash-commands (/novo-marco, /planejar-fase,
33
+ │ ├── commands/ 87 slash-commands (/novo-marco, /planejar-fase,
34
34
  │ │ /executar-fase, /publicar, …)
35
35
  │ ├── framework/ workflows + templates + bin libs the agents use
36
36
  │ ├── hooks/ workflow guards, prompt guards, statusline
@@ -59,7 +59,7 @@ kit-mcp/
59
59
 
60
60
  ### About the bundled workflow
61
61
 
62
- The bundled `kit/` is an opinionated **brownfield planning workflow** in Portuguese — milestones, phases, requirements, planning, execution with atomic commits and checkpoints, retrospective auditing. Installing `@luanpdd/kit-mcp` and syncing into your IDE gives you all 60+ slash-commands, 24+ agents, plus the framework templates that they delegate into.
62
+ The bundled `kit/` is an opinionated **brownfield planning workflow** in Portuguese — milestones, phases, requirements, planning, execution with atomic commits and checkpoints, retrospective auditing. Installing `@luanpdd/kit-mcp` and syncing into your IDE gives you all 87+ slash-commands, 47+ agents, plus the framework templates that they delegate into.
63
63
 
64
64
  If that's not what you want, point `--kit-root` at your own folder and ignore everything under `kit/` — the infrastructure (registry, sync, gates, forensics, MCP server) works the same regardless of what kit you load.
65
65
 
@@ -175,8 +175,8 @@ A production engineering layer derived from *Site Reliability Engineering: How G
175
175
 
176
176
  ```bash
177
177
  # Browse what's bundled
178
- npx -y @luanpdd/kit-mcp kit list-agents # 19 agents
179
- npx -y @luanpdd/kit-mcp kit list-commands # 60 commands
178
+ npx -y @luanpdd/kit-mcp kit list-agents # 47 agents
179
+ npx -y @luanpdd/kit-mcp kit list-commands # 87 commands
180
180
  npx -y @luanpdd/kit-mcp sync targets # supported IDEs
181
181
 
182
182
  # Install into your project for Claude Code
@@ -239,9 +239,9 @@ In non-TTY mode (pipes, CI), animations degrade to linear status lines automatic
239
239
  ### `kit kit ...` — browse the kit
240
240
 
241
241
  ```bash
242
- kit kit list-agents # 19 agents (bundled workflow)
243
- kit kit list-commands # 60 commands (bundled workflow)
244
- kit kit list-skills # 1 skill (example only — bring your own)
242
+ kit kit list-agents # 47 agents (bundled workflow)
243
+ kit kit list-commands # 87 commands (bundled workflow)
244
+ kit kit list-skills # 49 skills (bundled workflow)
245
245
  kit kit get agent planner
246
246
  kit kit search "milestone" # fuzzy match across all kinds
247
247
  ```
@@ -627,9 +627,9 @@ npm run test:all # both
627
627
  Plus the original quick smokes:
628
628
 
629
629
  ```bash
630
- node bin/cli.js kit list-agents | head -5 # 19 bundled agents
630
+ node bin/cli.js kit list-agents | head -5 # 47 bundled agents
631
631
  node bin/cli.js sync targets # 8 IDEs
632
- node bin/cli.js gates list # 5 gates
632
+ node bin/cli.js gates list # 20 gates
633
633
  node bin/cli.js install dry-run claude-code --via npx
634
634
  ```
635
635
 
@@ -3,12 +3,6 @@ name: codebase-mapper
3
3
  description: Explora a codebase e escreve docs de análise estruturados. Invocado por /mapear-codebase com foco (tech, arch, quality, concerns). Reduz carga de contexto do orquestrador.
4
4
  tools: Read, Bash, Grep, Glob, Write
5
5
  color: cyan
6
- # hooks:
7
- # PostToolUse:
8
- # - matcher: "Write|Edit"
9
- # hooks:
10
- # - type: command
11
- # command: "npx eslint --fix $FILE 2>/dev/null || true"
12
6
  ---
13
7
 
14
8
  <output_style>
@@ -4,12 +4,6 @@ description: Investiga bugs usando método científico, gerencia sessões de deb
4
4
  tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch
5
5
  permissionMode: acceptEdits
6
6
  color: orange
7
- # hooks:
8
- # PostToolUse:
9
- # - matcher: "Write|Edit"
10
- # hooks:
11
- # - type: command
12
- # command: "npx eslint --fix $FILE 2>/dev/null || true"
13
7
  ---
14
8
 
15
9
  <output_style>
@@ -4,12 +4,6 @@ description: Executa planos framework com commits atômicos, tratamento de desvi
4
4
  tools: Read, Write, Edit, Bash, Grep, Glob
5
5
  permissionMode: acceptEdits
6
6
  color: yellow
7
- # hooks:
8
- # PostToolUse:
9
- # - matcher: "Write|Edit"
10
- # hooks:
11
- # - type: command
12
- # command: "npx eslint --fix $FILE 2>/dev/null || true"
13
7
  ---
14
8
 
15
9
  <output_style>
@@ -3,12 +3,6 @@ name: phase-researcher
3
3
  description: Pesquisa como implementar uma fase antes do planejamento. Produz RESEARCH.md consumido pelo planner. Invocado pelo orquestrador /planejar-fase.
4
4
  tools: Read, Write, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*
5
5
  color: cyan
6
- # hooks:
7
- # PostToolUse:
8
- # - matcher: "Write|Edit"
9
- # hooks:
10
- # - type: command
11
- # command: "npx eslint --fix $FILE 2>/dev/null || true"
12
6
  ---
13
7
 
14
8
  <output_style>
@@ -3,12 +3,6 @@ name: planner
3
3
  description: Cria planos de fase executáveis com decomposição de tarefas, análise de dependências e verificação orientada a objetivos. Acionado pelo orquestrador /planejar-fase.
4
4
  tools: Read, Write, Bash, Glob, Grep, WebFetch, mcp__context7__*
5
5
  color: green
6
- # hooks:
7
- # PostToolUse:
8
- # - matcher: "Write|Edit"
9
- # hooks:
10
- # - type: command
11
- # command: "npx eslint --fix $FILE 2>/dev/null || true"
12
6
  ---
13
7
 
14
8
  <output_style>
@@ -3,12 +3,6 @@ name: project-researcher
3
3
  description: Pesquisa ecossistema do domínio antes do roadmap. Produz arquivos em .planning/research/ consumidos pelo roadmapper. Invocado por /novo-projeto ou /novo-marco.
4
4
  tools: Read, Write, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*
5
5
  color: cyan
6
- # hooks:
7
- # PostToolUse:
8
- # - matcher: "Write|Edit"
9
- # hooks:
10
- # - type: command
11
- # command: "npx eslint --fix $FILE 2>/dev/null || true"
12
6
  ---
13
7
 
14
8
  <output_style>
@@ -3,12 +3,6 @@ name: research-synthesizer
3
3
  description: Sintetiza outputs de agentes pesquisadores paralelos em SUMMARY.md. Invocado por /novo-projeto após 4 agentes pesquisadores concluírem.
4
4
  tools: Read, Write, Bash
5
5
  color: purple
6
- # hooks:
7
- # PostToolUse:
8
- # - matcher: "Write|Edit"
9
- # hooks:
10
- # - type: command
11
- # command: "npx eslint --fix $FILE 2>/dev/null || true"
12
6
  ---
13
7
 
14
8
  <output_style>
@@ -3,12 +3,6 @@ name: roadmapper
3
3
  description: Cria roadmaps de projeto com divisão de fases, mapeamento de requisitos, derivação de critérios de sucesso e validação de cobertura. Invocado pelo orquestrador /novo-projeto.
4
4
  tools: Read, Write, Bash, Glob, Grep
5
5
  color: purple
6
- # hooks:
7
- # PostToolUse:
8
- # - matcher: "Write|Edit"
9
- # hooks:
10
- # - type: command
11
- # command: "npx eslint --fix $FILE 2>/dev/null || true"
12
6
  ---
13
7
 
14
8
  <output_style>
@@ -3,12 +3,6 @@ name: ui-auditor
3
3
  description: Auditoria visual retroativa de 6 pilares do código frontend implementado. Produz UI-REVIEW.md pontuado. Invocado pelo orquestrador /revisar-ui.
4
4
  tools: Read, Write, Bash, Grep, Glob
5
5
  color: "#F472B6"
6
- # hooks:
7
- # PostToolUse:
8
- # - matcher: "Write|Edit"
9
- # hooks:
10
- # - type: command
11
- # command: "npx eslint --fix $FILE 2>/dev/null || true"
12
6
  ---
13
7
 
14
8
  <output_style>
@@ -3,12 +3,6 @@ name: ui-researcher
3
3
  description: Produz contrato de design UI-SPEC.md para fases frontend. Lê artefatos upstream, detecta estado do sistema de design, faz apenas perguntas não respondidas. Invocado pelo orquestrador /fase-ui.
4
4
  tools: Read, Write, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*
5
5
  color: "#E879F9"
6
- # hooks:
7
- # PostToolUse:
8
- # - matcher: "Write|Edit"
9
- # hooks:
10
- # - type: command
11
- # command: "npx eslint --fix $FILE 2>/dev/null || true"
12
6
  ---
13
7
 
14
8
  <output_style>
@@ -3,12 +3,6 @@ name: verifier
3
3
  description: Verifica atingimento do objetivo da fase via análise reversa. Checa se codebase entrega o prometido, não só task completion. Cria VERIFICATION.md.
4
4
  tools: Read, Write, Bash, Grep, Glob
5
5
  color: green
6
- # hooks:
7
- # PostToolUse:
8
- # - matcher: "Write|Edit"
9
- # hooks:
10
- # - type: command
11
- # command: "npx eslint --fix $FILE 2>/dev/null || true"
12
6
  ---
13
7
 
14
8
  <output_style>
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // hook-version: 1.30.0
3
+ // SEC-13-05: flush-before-exit category = E (parent returns after spawn unref; child uses sync fs writes) — no fix needed
3
4
  // Check for framework updates in background, write result to cache
4
5
  // Called by SessionStart hook - runs once per session
5
6
 
@@ -42,6 +43,9 @@ if (!fs.existsSync(cacheDir)) {
42
43
  }
43
44
 
44
45
  // Run check in background (spawn background process, windowsHide prevents console flash)
46
+ // SEC-13-05: parent process retorna imediatamente após child.unref() — não
47
+ // há buffered I/O no parent. Child usa fs.writeFileSync (sync), sem race.
48
+ // Categoria E na taxonomia da Phase 80.
45
49
  const child = spawn(process.execPath, ['-e', `
46
50
  const fs = require('fs');
47
51
  const path = require('path');
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
- // hook-version: 1.30.0
2
+ // hook-version: 1.30.1
3
+ // SEC-13-05: flush-before-exit category = A (stdout.write + immediate exit)
4
+ // Fix applied: process.stdout.write(payload, () => process.exit(0)) on warning path.
3
5
  // Context Monitor - PostToolUse/AfterTool hook (Gemini uses AfterTool)
4
6
  // Reads context metrics from the statusline bridge file and injects
5
7
  // warnings when context usage is high. This makes the AGENT aware of
@@ -148,7 +150,12 @@ process.stdin.on('end', () => {
148
150
  }
149
151
  };
150
152
 
151
- process.stdout.write(JSON.stringify(output));
153
+ // SEC-13-05: aguardar flush do stdout antes do exit. Sem callback, em
154
+ // pipes lentos (CI/Windows/Git Bash) o JSON pode ser dropado quando o
155
+ // process termina antes do kernel drenar o buffer.
156
+ process.stdout.write(JSON.stringify(output), () => {
157
+ process.exit(0);
158
+ });
152
159
  } catch (e) {
153
160
  // Silent fail -- never block tool execution
154
161
  process.exit(0);
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
- // hook-version: 1.4.0
2
+ // hook-version: 1.4.1
3
+ // SEC-13-05: flush-before-exit category = A (stderr.write + immediate exit)
4
+ // Fix applied: process.stderr.write(summary, () => process.exit(0)) on success path.
3
5
  // kit-mcp · Post-apply Migration Hook (PostToolUse)
4
6
  //
5
7
  // Triggers automatically AFTER a successful Supabase MCP apply_migration call.
@@ -104,12 +106,18 @@ process.stdin.on('end', () => {
104
106
  }
105
107
 
106
108
  // The final advisory printed back to Claude (and to the user via stderr)
109
+ // SEC-13-05: aguardar flush do stderr antes do exit. Sem callback, o
110
+ // resumo final pode ser dropado em pipes lentos (CI/Windows). Os outros
111
+ // process.stderr.write intermediários (linhas ~71/87/93/100) NÃO precisam
112
+ // do callback porque o process continua executando após eles — o event
113
+ // loop drena o buffer naturalmente antes do próximo write.
107
114
  if (mirroredPath || stubPath) {
108
115
  const lines = ['[post-apply-migration] resumo:'];
109
116
  if (mirroredPath) lines.push(` • SQL: ${path.relative(projectRoot, mirroredPath)}`);
110
117
  if (stubPath) lines.push(` • Stub: ${path.relative(vault, stubPath)}`);
111
118
  lines.push(' → cofre Obsidian: edite o stub e commite quando puder.');
112
- process.stderr.write(lines.join('\n') + '\n');
119
+ process.stderr.write(lines.join('\n') + '\n', () => process.exit(0));
120
+ return;
113
121
  }
114
122
 
115
123
  process.exit(0);
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
- // hook-version: 1.30.0
2
+ // hook-version: 1.30.1
3
+ // SEC-13-05: flush-before-exit category = A (stdout.write + immediate exit)
4
+ // Fix applied: process.stdout.write(payload, () => process.exit(0)) on warning path.
3
5
  // framework Prompt Injection Guard — PreToolUse hook
4
6
  // Scans file content being written to .planning/ for prompt injection patterns.
5
7
  // Defense-in-depth: catches injected instructions before they enter agent context.
@@ -88,7 +90,12 @@ process.stdin.on('end', () => {
88
90
  },
89
91
  };
90
92
 
91
- process.stdout.write(JSON.stringify(output));
93
+ // SEC-13-05: aguardar flush do stdout antes do exit. Sem callback, em
94
+ // pipes lentos (CI/Windows/Git Bash) o JSON pode ser dropado quando o
95
+ // process termina antes do kernel drenar o buffer.
96
+ process.stdout.write(JSON.stringify(output), () => {
97
+ process.exit(0);
98
+ });
92
99
  } catch {
93
100
  // Silent fail — never block tool execution
94
101
  process.exit(0);
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // hook-version: 1.6.0
2
+ // hook-version: 1.6.1
3
3
  // kit-mcp · Sidecar Tool Publisher (PostToolUse)
4
4
  //
5
5
  // Publishes every Claude Code tool invocation to the kit-mcp sidecar so the
@@ -74,8 +74,7 @@ process.stdin.on('end', () => {
74
74
  payload,
75
75
  };
76
76
 
77
- publish(port, event);
78
- process.exit(0);
77
+ publish(port, event).then(() => process.exit(0));
79
78
  } catch (err) {
80
79
  process.stderr.write(`[sidecar-tool-publisher] ${err.message}\n`);
81
80
  process.exit(0);
@@ -160,23 +159,30 @@ function detectIde() {
160
159
  }
161
160
 
162
161
  function publish(port, event) {
163
- const body = JSON.stringify(event);
164
- const req = http.request({
165
- method: 'POST',
166
- host: '127.0.0.1',
167
- port,
168
- path: '/publish',
169
- agent: false,
170
- headers: {
171
- host: `127.0.0.1:${port}`,
172
- 'content-type': 'application/json',
173
- 'content-length': Buffer.byteLength(body, 'utf8'),
174
- origin: `http://127.0.0.1:${port}`,
175
- connection: 'close',
176
- },
177
- }, (res) => { res.resume(); });
178
- req.on('error', () => { /* fire-and-forget */ });
179
- req.setTimeout(800, () => { try { req.destroy(); } catch (_) { /* noop */ } });
180
- req.write(body);
181
- req.end();
162
+ return new Promise((resolve) => {
163
+ const body = JSON.stringify(event);
164
+ const req = http.request({
165
+ method: 'POST',
166
+ host: '127.0.0.1',
167
+ port,
168
+ path: '/publish',
169
+ agent: false,
170
+ headers: {
171
+ host: `127.0.0.1:${port}`,
172
+ 'content-type': 'application/json',
173
+ 'content-length': Buffer.byteLength(body, 'utf8'),
174
+ origin: `http://127.0.0.1:${port}`,
175
+ connection: 'close',
176
+ },
177
+ }, (res) => {
178
+ // Drain response body to ensure server has fully processed before resolve
179
+ res.resume();
180
+ res.on('end', resolve);
181
+ res.on('close', resolve);
182
+ });
183
+ req.on('error', () => resolve());
184
+ req.setTimeout(800, () => { try { req.destroy(); } catch (_) { /* noop */ } resolve(); });
185
+ req.write(body);
186
+ req.end();
187
+ });
182
188
  }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // hook-version: 1.30.0
3
+ // SEC-13-05: flush-before-exit category = C (no process.exit, natural termination flushes) — no fix needed
3
4
  // Claude Code Statusline - Edition
4
5
  // Shows: model | current task | directory | context usage
5
6
 
@@ -107,6 +108,11 @@ process.stdin.on('end', () => {
107
108
  }
108
109
 
109
110
  // Output
111
+ // SEC-13-05: statusline termina naturalmente após este write — Node
112
+ // garante o flush antes do process exit quando não há process.exit
113
+ // explícito. NÃO converter para process.stdout.write(x, callback) +
114
+ // process.exit() — isso introduziria um early-exit que poderia
115
+ // truncar saída em casos onde o write é maior que o buffer do pipe.
110
116
  const dirname = path.basename(dir);
111
117
  if (task) {
112
118
  process.stdout.write(`${updateNotice}\x1b[2m${model}\x1b[0m │ \x1b[1m${task}\x1b[0m │ \x1b[2m${dirname}\x1b[0m${ctx}`);
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
- // hook-version: 1.30.0
2
+ // hook-version: 1.30.1
3
+ // SEC-13-05: flush-before-exit category = A (stdout.write + immediate exit)
4
+ // Fix applied: process.stdout.write(payload, () => process.exit(0)) on warning path.
3
5
  // framework Workflow Guard — PreToolUse hook
4
6
  // Detects when Claude attempts file edits outside a framework workflow context
5
7
  // (no active / command or Task subagent) and injects an advisory warning.
@@ -86,7 +88,12 @@ process.stdin.on('end', () => {
86
88
  }
87
89
  };
88
90
 
89
- process.stdout.write(JSON.stringify(output));
91
+ // SEC-13-05: aguardar flush do stdout antes do exit. Sem callback, em
92
+ // pipes lentos (CI/Windows/Git Bash) o JSON pode ser dropado quando o
93
+ // process termina antes do kernel drenar o buffer.
94
+ process.stdout.write(JSON.stringify(output), () => {
95
+ process.exit(0);
96
+ });
90
97
  } catch (e) {
91
98
  // Silent fail — never block tool execution
92
99
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luanpdd/kit-mcp",
3
- "version": "1.12.0",
3
+ "version": "1.13.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": {
@@ -16,7 +16,6 @@
16
16
  "kit/",
17
17
  "gates/",
18
18
  "README.md",
19
- "CHANGELOG.md",
20
19
  "LICENSE"
21
20
  ],
22
21
  "keywords": [
package/src/cli/index.js CHANGED
@@ -18,7 +18,7 @@ import { fileURLToPath } from 'node:url';
18
18
  import path from 'node:path';
19
19
  import { listKit, searchKit, findItem } from '../core/kit.js';
20
20
  import { listTargets } from '../core/registry.js';
21
- import { syncTo, statusOf, removeFrom } from '../core/sync.js';
21
+ import { syncTo, statusOf, removeFrom, summarize } from '../core/sync.js';
22
22
  import { watchKit, detectExistingTargets } from '../core/watch.js';
23
23
  import { listGates, getGate, gatesForStage } from '../core/gates.js';
24
24
  import { runGate } from '../core/gate-runner.js';
@@ -148,7 +148,10 @@ function fail(msg) {
148
148
  }
149
149
 
150
150
  function slim(x) {
151
- return { kind: x.kind, name: x.name, description: x.description };
151
+ // PERF-13-01: cap description at SUMMARY_MAX_CHARS via shared summarize()
152
+ // helper from src/core/sync.js — keeps cross-surface behavior identical
153
+ // (CLI listing == MCP listing). Full text remains in each item's source file.
154
+ return { kind: x.kind, name: x.name, description: summarize(x.description) };
152
155
  }
153
156
 
154
157
  // --- kit ---
@@ -17,15 +17,55 @@ import fs from 'node:fs/promises';
17
17
 
18
18
  const REPLAY_DIR_REL = path.join('.planning', 'replays');
19
19
 
20
+ // SEC-13-02: replayId path traversal guard. The MCP forensics tool exposes
21
+ // load-replay/annotate-replay/record-replay actions; without sanitization,
22
+ // a malicious replayId like '../../../etc/passwd' would read/write files
23
+ // outside .planning/replays/.
24
+ //
25
+ // Strategy: allowlist regex (no slashes, no '..', no NUL) + post-resolve assertion
26
+ // that the final path stays inside REPLAY_DIR_REL.
27
+ const REPLAY_ID_RE = /^[A-Za-z0-9_.-]+$/;
28
+
29
+ function validateReplayId(id) {
30
+ if (typeof id !== 'string' || !id) {
31
+ throw new Error('invalid replay id: must be a non-empty string');
32
+ }
33
+ if (id === '.' || id === '..' || id.includes('..')) {
34
+ throw new Error('invalid replay id: traversal sequences not allowed');
35
+ }
36
+ if (!REPLAY_ID_RE.test(id)) {
37
+ throw new Error(`invalid replay id: only [A-Za-z0-9_.-] allowed, got ${JSON.stringify(id)}`);
38
+ }
39
+ return id;
40
+ }
41
+
42
+ function assertPathInside(filePath, baseDir) {
43
+ const resolved = path.resolve(filePath);
44
+ const base = path.resolve(baseDir);
45
+ // Ensure resolved is base or a child of base (handle trailing-sep edge case).
46
+ if (resolved !== base && !resolved.startsWith(base + path.sep)) {
47
+ throw new Error('invalid replay id: resolved path escapes replay directory');
48
+ }
49
+ return resolved;
50
+ }
51
+
20
52
  export async function recordReplay(payload, opts = {}) {
21
53
  const projectRoot = path.resolve(opts.projectRoot ?? process.cwd());
22
54
  const dir = path.join(projectRoot, REPLAY_DIR_REL);
23
55
  await fs.mkdir(dir, { recursive: true });
24
56
 
25
57
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
26
- const slug = [payload.phase, payload.plan, payload.agent].filter(Boolean).join('-') || 'unknown';
58
+ // SEC-13-02: validate each slug component independently before concat
59
+ const slugParts = [payload.phase, payload.plan, payload.agent].filter(Boolean);
60
+ for (const part of slugParts) {
61
+ validateReplayId(String(part));
62
+ }
63
+ const slug = slugParts.join('-') || 'unknown';
27
64
  const id = `${ts}-${slug}`;
65
+ // Re-validate the full id (defense in depth — ts is well-formed but cheap to check)
66
+ validateReplayId(id);
28
67
  const file = path.join(dir, `${id}.json`);
68
+ assertPathInside(file, dir);
29
69
 
30
70
  const record = { id, recorded_at: new Date().toISOString(), ...payload };
31
71
  await fs.writeFile(file, JSON.stringify(record, null, 2), 'utf8');
@@ -49,15 +89,21 @@ export async function listReplays(opts = {}) {
49
89
  }
50
90
 
51
91
  export async function loadReplay(id, opts = {}) {
92
+ validateReplayId(id);
52
93
  const projectRoot = path.resolve(opts.projectRoot ?? process.cwd());
53
- const file = path.join(projectRoot, REPLAY_DIR_REL, `${id}.json`);
94
+ const dir = path.join(projectRoot, REPLAY_DIR_REL);
95
+ const file = path.join(dir, `${id}.json`);
96
+ assertPathInside(file, dir);
54
97
  const raw = await fs.readFile(file, 'utf8');
55
98
  return JSON.parse(raw);
56
99
  }
57
100
 
58
101
  export async function annotateReplay(id, outcome, opts = {}) {
102
+ validateReplayId(id);
59
103
  const projectRoot = path.resolve(opts.projectRoot ?? process.cwd());
60
- const file = path.join(projectRoot, REPLAY_DIR_REL, `${id}.json`);
104
+ const dir = path.join(projectRoot, REPLAY_DIR_REL);
105
+ const file = path.join(dir, `${id}.json`);
106
+ assertPathInside(file, dir);
61
107
  const r = JSON.parse(await fs.readFile(file, 'utf8'));
62
108
  r.outcome = { ...(r.outcome ?? {}), ...outcome, annotated_at: new Date().toISOString() };
63
109
  await fs.writeFile(file, JSON.stringify(r, null, 2), 'utf8');
package/src/core/sync.js CHANGED
@@ -257,8 +257,10 @@ See: [\`${rel}\`](${rel})
257
257
  // own file under kit/ — duplicating them here costs tokens in every Claude
258
258
  // Code session. Cap each line at ~80 chars; users can `kit get <name>` for the
259
259
  // full description.
260
- const SUMMARY_MAX_CHARS = 80;
261
- function summarize(desc) {
260
+ // PERF-13-01: exported so slim() in src/mcp-server/index.js and src/cli/index.js
261
+ // can reuse the same cap (single source of truth — no duplicated constants).
262
+ export const SUMMARY_MAX_CHARS = 80;
263
+ export function summarize(desc) {
262
264
  if (!desc) return '';
263
265
  const flat = desc.replace(/\s+/g, ' ').trim();
264
266
  if (flat.length <= SUMMARY_MAX_CHARS) return flat;
@@ -12,9 +12,13 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
12
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
13
13
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
14
14
 
15
+ import { readFileSync } from 'node:fs';
16
+ import { fileURLToPath } from 'node:url';
17
+ import path from 'node:path';
18
+
15
19
  import { listKit, searchKit, findItem } from '../core/kit.js';
16
20
  import { listTargets } from '../core/registry.js';
17
- import { syncTo, statusOf, removeFrom } from '../core/sync.js';
21
+ import { syncTo, statusOf, removeFrom, summarize } from '../core/sync.js';
18
22
  import { detectReverse, applyReverse } from '../core/reverse-sync.js';
19
23
  import { listGates, getGate, gatesForStage } from '../core/gates.js';
20
24
  import { runGate } from '../core/gate-runner.js';
@@ -125,6 +129,23 @@ const TOOLS = [
125
129
  },
126
130
  ];
127
131
 
132
+ // DRIFT-13-03: read version from package.json at module load (NOT inside
133
+ // createServer — re-reading on every call adds zero value). Same pattern as
134
+ // bin/cli.js:43-51. Both files are 2 levels deep from repo root, so the
135
+ // '..', '..' resolution works identically. Falls back to 'unknown' if the
136
+ // package.json lookup fails (unusual install layout).
137
+ function readPkgVersion() {
138
+ try {
139
+ const here = path.dirname(fileURLToPath(import.meta.url));
140
+ const pkgPath = path.resolve(here, '..', '..', 'package.json');
141
+ return JSON.parse(readFileSync(pkgPath, 'utf8')).version;
142
+ } catch {
143
+ return 'unknown';
144
+ }
145
+ }
146
+
147
+ export const PKG_VERSION = readPkgVersion();
148
+
128
149
  // --- handlers ---
129
150
 
130
151
  async function handleKit(args) {
@@ -200,12 +221,14 @@ async function handleGates(args) {
200
221
  case 'get': return getGate(args.id);
201
222
  case 'for-stage': return gatesForStage(args.stage);
202
223
  case 'run':
203
- return withAutoSpawn(args, 'gates.run', () =>
204
- runGate(args.id, {
205
- projectRoot: args.projectRoot,
206
- yes: true, // MCP context: never prompt
207
- interactive: false, // MCP never prompts
208
- }));
224
+ // SEC-13-01: MCP transport must never execute shell — runGate spawns bash with
225
+ // arbitrary content from gates/*.md (which reverse-sync can rewrite). Even with
226
+ // {yes: true}, this skips the interactive "y/N before exec" promise. The CLI
227
+ // entry point (`kit gates run <id>` via bin/cli.js) preserves the prompt and
228
+ // remains the only path to executing gates.
229
+ return {
230
+ error: 'MCP gates.run requires interactive TTY confirmation; use `kit gates run` from CLI instead.',
231
+ };
209
232
  default: return { error: `Unknown action: ${args.action}` };
210
233
  }
211
234
  }
@@ -255,14 +278,16 @@ const HANDLERS = {
255
278
  function slim(x) {
256
279
  // absPath omitted by design — list-* tools are AI-consumed in tight context budgets.
257
280
  // Use action=get to fetch the absPath (and content) for a specific item.
258
- return { kind: x.kind, name: x.name, description: x.description };
281
+ // PERF-13-01 (TOK-02): truncate description via SUMMARY_MAX_CHARS (80) cap shared
282
+ // with src/core/sync.js — full description lives in each item's file under kit/.
283
+ return { kind: x.kind, name: x.name, description: summarize(x.description) };
259
284
  }
260
285
 
261
286
  // --- server bootstrap ---
262
287
 
263
288
  export async function createServer() {
264
289
  const server = new Server(
265
- { name: 'kit-mcp', version: '0.1.0' },
290
+ { name: 'kit-mcp', version: PKG_VERSION },
266
291
  { capabilities: { tools: {} } }
267
292
  );
268
293