@luanpdd/kit-mcp 0.5.0 → 1.0.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/CHANGELOG.md CHANGED
@@ -6,6 +6,51 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.0.0] - 2026-05-03
10
+
11
+ **First stable release.** kit-mcp now commits to backwards compatibility on the surfaces listed under "Stable API" below; breaking changes there require a 2.0.0 bump.
12
+
13
+ ### Added — Phase 1: Tooling debt
14
+ - `.github/dependabot.yml` — weekly grouped npm + github-actions updates.
15
+ - GitHub Release object created for v0.5.0 (was stuck on v0.2.0 "cleanup" as Latest).
16
+ - `.github/workflows/publish.yml` now creates a GitHub Release object automatically on every `v*` tag push, with notes extracted from this CHANGELOG. Closes the gap permanently.
17
+
18
+ ### Fixed — Phase 2: Slash-command parser
19
+ - `src/core/sync.js` — `renderReference` reorders the stub body so the first non-blank line is the H1 + description blockquote, not the `<!-- kit-mcp:reference -->` marker. Strict downstream parsers (notably Claude Desktop's skill listing) now surface the real description.
20
+ - `src/core/kit.js` — `firstNonEmptyLine` skips lines starting with `<!--` as a defensive fallback when the canonical has no frontmatter description.
21
+ - `kit/commands/*` — 8 commands (`adicionar-backlog`, `adicionar-fase`, `adicionar-tarefa`, `concluir-marco`, `definir-perfil`, `depurar`, `fio`, `inserir-fase`) had unquoted angle-bracket `argument-hint` values that strict YAML parsers misinterpreted as flow-style flags. Now consistently quoted.
22
+
23
+ ### Added — Phase 3: Reverse-sync for mirror-tree caps
24
+ - `detectReverse` now walks `.claude/framework/` and `.claude/hooks/` and reports any byte-for-byte difference vs `kit/<source>/<rel>`. The `.kit-mcp-managed` marker is automatically excluded from candidates.
25
+ - `applyReverse` adds `applyMirrorTreeOne` for `framework`/`hooks` candidates: `skip`, `overwrite`, `merge` (degenerates to overwrite — no frontmatter to preserve), `rename` (writes to `kit/<source>/<rel>.from-<tag>.<ext>` preserving the original).
26
+ - `--only framework/<rel>` / `--only hooks/<file>` filters narrow apply to one file.
27
+ - README "kit reverse-sync" section updated with the new examples.
28
+
29
+ ### Added — Phase 4: Test infrastructure
30
+ - `node:test`-based runner — zero dependencies. `test/run.mjs` walks for `*.test.js` files (works on Node 20+ where `--test` glob support is partial).
31
+ - 37 unit tests across `kit`, `sync`, `reverse-sync`, `gates`, `gate-runner`, `registry`.
32
+ - 5 integration tests spawning `bin/cli.js` end-to-end (incl. MCP server boot smoke).
33
+ - `test/fixtures/sample-kit/` minimal fixture (1 of each kind + framework template + hook + frontmatter-less command for fallback test).
34
+ - CI runs `npm test` + `npm run test:integration` before existing smoke + MCP boot, on Ubuntu / macOS / Windows × Node 20 / 22 (6/6 combinations).
35
+ - `package.json` scripts: `test`, `test:integration`, `test:all`.
36
+
37
+ ### Stable API (commitments locked at 1.0.0)
38
+
39
+ The following surfaces are covered by SemVer — breaking changes require a 2.0.0 release:
40
+
41
+ - **`src/core/registry.js` TARGETS table format.** Adding capabilities, IDEs, or new modes is non-breaking. Renaming or removing existing capability keys (`rules`, `agents`, `commands`, `skills`, `framework`, `hooks`, `mcpConfig`) is breaking.
42
+ - **MCP tool action signatures.** Tool names (`kit`, `sync`, `reverse-sync`, `gates`, `forensics`, `install`) and their action-dispatch contracts are stable. New actions are non-breaking; renaming or removing existing actions is breaking.
43
+ - **CLI subcommand surface.** Top-level commands (`kit`, `sync`, `reverse-sync`, `gates`, `forensics`, `install`) and their action sub-commands are stable. New flags are non-breaking; renaming or removing existing ones is breaking.
44
+ - **`src/core/*.js` named exports.** Functions consumed programmatically (`listKit`, `searchKit`, `findItem`, `resolveKitRoot`, `BUNDLED_KIT_ROOT`, `syncTo`, `statusOf`, `removeFrom`, `detectReverse`, `applyReverse`, `listGates`, `getGate`, `gatesForStage`, `runGate`, `listTargets`, `getTarget`, `TARGETS`) keep their signatures. Adding new exports is non-breaking; signature changes are breaking.
45
+ - **Stub format.** Files written by sync `--mode reference` keep the `<!-- kit-mcp:reference -->` marker somewhere in the body so `sync remove` and `reverse-sync detect` continue to identify them. Position within the body may change; presence is the contract.
46
+ - **`.kit-mcp-managed` marker semantics.** Mirror-tree directories (`framework/`, `hooks/`) are managed only when the marker is present at the root. Without it, `sync remove` never deletes the tree.
47
+
48
+ ### Migration
49
+
50
+ No code changes required for users on 0.5.0 — `npm install @luanpdd/kit-mcp@latest` brings in 1.0.0 with the same behavior plus the parser fixes, reverse-sync expansions, and test coverage.
51
+
52
+ If you were on 0.4.0 (deprecated) or earlier, upgrade to skip the import-time crash and missing-framework regression entirely.
53
+
9
54
  ## [0.5.0] - 2026-05-03
10
55
 
11
56
  ### Added
@@ -174,7 +219,8 @@ npx -y @luanpdd/kit-mcp sync install claude-code --project-root .
174
219
  - CLI mirror of all MCP tools.
175
220
  - `install` command that registers kit-mcp into an IDE's MCP config (JSON for Claude/Cursor/Gemini/Windsurf, TOML for Codex).
176
221
 
177
- [Unreleased]: https://github.com/luanpdd/kit-mcp/compare/v0.5.0...HEAD
222
+ [Unreleased]: https://github.com/luanpdd/kit-mcp/compare/v1.0.0...HEAD
223
+ [1.0.0]: https://github.com/luanpdd/kit-mcp/compare/v0.5.0...v1.0.0
178
224
  [0.5.0]: https://github.com/luanpdd/kit-mcp/compare/v0.4.1...v0.5.0
179
225
  [0.4.1]: https://github.com/luanpdd/kit-mcp/compare/v0.4.0...v0.4.1
180
226
  [0.4.0]: https://github.com/luanpdd/kit-mcp/compare/v0.3.0...v0.4.0
package/README.md CHANGED
@@ -194,16 +194,19 @@ kit install write claude-code --scope user --via global # assumes `npm ins
194
194
 
195
195
  ### `kit reverse-sync ...` — bring IDE edits back to the canonical kit
196
196
 
197
- If you edited an agent/command/skill **directly inside the IDE's folder** (`.claude/agents/foo.md`, `.cursor/agents/bar.md`, …) instead of in your kit, this brings those edits back so the canonical absorbs them.
197
+ If you edited an agent/command/skill/framework/hook **directly inside the IDE's folder** (`.claude/agents/foo.md`, `.claude/framework/workflows/bar.md`, `.claude/hooks/baz.js`, …) instead of in your kit, this brings those edits back so the canonical absorbs them.
198
198
 
199
199
  ```bash
200
200
  kit reverse-sync detect claude-code --project-root .
201
201
  kit reverse-sync apply claude-code --project-root . --strategy merge --dry-run
202
202
  kit reverse-sync apply claude-code --project-root . --strategy merge
203
203
  kit reverse-sync apply claude-code --project-root . --strategy overwrite --only agent/foo
204
+ kit reverse-sync apply claude-code --project-root . --strategy overwrite --only framework/workflows/new-milestone.md
204
205
  ```
205
206
 
206
- **Strategies:** `skip` (list-only), `merge` (canonical frontmatter + edited body), `overwrite`, `rename` (preserve both as `-from-{ide}.md`).
207
+ **Strategies:** `skip` (list-only), `merge` (canonical frontmatter + edited body — for agents/commands/skills), `overwrite`, `rename` (preserve both as `-from-{ide}.md`).
208
+
209
+ **Mirror-tree caps (`framework`, `hooks`):** files have no frontmatter, so `merge` degenerates to `overwrite` (with a note). The `.kit-mcp-managed` marker is automatically excluded from candidates. Filter individual files with `--only framework/<rel>` or `--only hooks/<file>`.
207
210
 
208
211
  ### `kit gates ...` — reusable workflow gates
209
212
 
@@ -469,16 +472,27 @@ PRs welcome.
469
472
 
470
473
  ---
471
474
 
472
- ## Smoke tests
475
+ ## Tests
476
+
477
+ Built on `node:test` (zero dependencies). Two suites:
478
+
479
+ ```bash
480
+ npm test # unit — kit parser, sync (all modes), reverse-sync, gates, registry
481
+ npm run test:integration # integration — spawns bin/cli.js end-to-end on a temp project
482
+ npm run test:all # both
483
+ ```
484
+
485
+ Plus the original quick smokes:
473
486
 
474
487
  ```bash
475
488
  node bin/cli.js kit list-agents | head -5 # 19 bundled agents
476
489
  node bin/cli.js sync targets # 8 IDEs
477
490
  node bin/cli.js gates list # 5 gates
478
491
  node bin/cli.js install dry-run claude-code --via npx
479
- node bin/mcp.js < /dev/null & sleep 1; kill %1 # MCP server boots and waits on stdio
480
492
  ```
481
493
 
494
+ CI runs unit + integration + smoke + MCP boot on Ubuntu / macOS / Windows × Node 20 / 22 on every push and PR.
495
+
482
496
  ---
483
497
 
484
498
  ## License
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: adicionar-backlog
3
3
  description: Adiciona uma ideia ao estacionamento de backlog (numeração 999.x)
4
- argument-hint: <description>
4
+ argument-hint: "<description>"
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Write
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: adicionar-fase
3
3
  description: Adiciona fase ao final do milestone atual no roadmap
4
- argument-hint: <description>
4
+ argument-hint: "<description>"
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Write
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: adicionar-tarefa
3
3
  description: Captura ideia ou tarefa como todo a partir do contexto da conversa atual
4
- argument-hint: [descrição opcional]
4
+ argument-hint: "[descrição opcional]"
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Write
@@ -2,7 +2,7 @@
2
2
  type: prompt
3
3
  name: concluir-marco
4
4
  description: Arquiva milestone concluído e prepara para próxima versão
5
- argument-hint: <version>
5
+ argument-hint: "<version>"
6
6
  allowed-tools:
7
7
  - Read
8
8
  - Write
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: definir-perfil
3
3
  description: Altera o perfil de modelo para os agentes framework (quality/balanced/budget/inherit)
4
- argument-hint: <perfil (quality|balanced|budget|inherit)>
4
+ argument-hint: "<perfil (quality|balanced|budget|inherit)>"
5
5
  model: haiku
6
6
  allowed-tools:
7
7
  - Bash
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: depurar
3
3
  description: Depuração sistemática com estado persistente entre resets de contexto
4
- argument-hint: [descrição do problema]
4
+ argument-hint: "[descrição do problema]"
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Bash
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: fio
3
3
  description: Gerencia threads de contexto persistentes para trabalho entre sessões
4
- argument-hint: [nome | descrição]
4
+ argument-hint: "[nome | descrição]"
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Write
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: inserir-fase
3
3
  description: Insere trabalho urgente como fase decimal (ex: 72.1) entre fases existentes
4
- argument-hint: <after> <description>
4
+ argument-hint: "<after> <description>"
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Write
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luanpdd/kit-mcp",
3
- "version": "0.5.0",
3
+ "version": "1.0.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": {
@@ -41,7 +41,10 @@
41
41
  "scripts": {
42
42
  "start": "node bin/mcp.js",
43
43
  "cli": "node bin/cli.js",
44
- "smoke": "node bin/cli.js kit list-agents | head -5"
44
+ "smoke": "node bin/cli.js kit list-agents | head -5",
45
+ "test": "node test/run.mjs test/unit",
46
+ "test:integration": "node test/run.mjs test/integration",
47
+ "test:all": "node test/run.mjs test"
45
48
  },
46
49
  "dependencies": {
47
50
  "@modelcontextprotocol/sdk": "^1.0.0",
package/src/core/kit.js CHANGED
@@ -135,7 +135,10 @@ function parseLooseYaml(text) {
135
135
  function firstNonEmptyLine(body) {
136
136
  for (const line of body.split(/\r?\n/)) {
137
137
  const t = line.trim();
138
- if (t && !t.startsWith('#')) return t.slice(0, 200);
138
+ if (!t) continue; // blank
139
+ if (t.startsWith('#')) continue; // markdown heading
140
+ if (t.startsWith('<!--')) continue; // HTML comment (e.g. STUB_MARKER)
141
+ return t.slice(0, 200);
139
142
  }
140
143
  return '';
141
144
  }
@@ -35,6 +35,11 @@ export async function detectReverse(targetId, opts = {}) {
35
35
  if (target.agents) await scanCapability(candidates, 'agent', target.agents, projectRoot, kit.agents, kitRoot);
36
36
  if (target.commands) await scanCapability(candidates, 'command', target.commands, projectRoot, kit.commands, kitRoot);
37
37
  if (target.skills) await scanSkills (candidates, target.skills, projectRoot, [...kit.skills, ...kit.skillsExtras], kitRoot);
38
+ for (const cap of ['framework', 'hooks']) {
39
+ const spec = target[cap];
40
+ if (!spec || spec.mode !== 'mirror-tree') continue;
41
+ await scanMirrorTree(candidates, cap, spec, projectRoot, kitRoot);
42
+ }
38
43
 
39
44
  return { target: targetId, projectRoot, kitRoot, candidates };
40
45
  }
@@ -117,6 +122,51 @@ async function scanSkills(candidates, capCfg, projectRoot, kitSkills, kitRoot) {
117
122
  }
118
123
  }
119
124
 
125
+ async function scanMirrorTree(candidates, cap, spec, projectRoot, kitRoot) {
126
+ const dstRoot = path.join(projectRoot, spec.path);
127
+ const srcRoot = path.join(kitRoot, spec.source);
128
+ const files = await walkRel(dstRoot);
129
+ for (const rel of files) {
130
+ if (rel === '.kit-mcp-managed' || path.basename(rel) === '.kit-mcp-managed') continue;
131
+ const dstPath = path.join(dstRoot, rel);
132
+ const srcPath = path.join(srcRoot, rel);
133
+ let dstBuf, srcBuf;
134
+ try { dstBuf = await fs.readFile(dstPath); } catch { continue; }
135
+ try { srcBuf = await fs.readFile(srcPath); } catch { srcBuf = null; }
136
+ if (!srcBuf) {
137
+ candidates.push({
138
+ kind: cap, name: rel, target: spec.path, destPath: dstPath, kitPath: srcPath,
139
+ reason: 'new-in-ide',
140
+ diffSummary: `+${dstBuf.length} bytes (no kit source)`,
141
+ });
142
+ continue;
143
+ }
144
+ if (dstBuf.equals(srcBuf)) continue;
145
+ candidates.push({
146
+ kind: cap, name: rel, target: spec.path, destPath: dstPath, kitPath: srcPath,
147
+ reason: 'modified-in-ide',
148
+ diffSummary: `${dstBuf.length} bytes vs ${srcBuf.length} canonical (${dstBuf.length - srcBuf.length >= 0 ? '+' : ''}${dstBuf.length - srcBuf.length})`,
149
+ });
150
+ }
151
+ }
152
+
153
+ async function walkRel(root) {
154
+ const out = [];
155
+ async function visit(current, prefix) {
156
+ let entries;
157
+ try { entries = await fs.readdir(current, { withFileTypes: true }); }
158
+ catch { return; }
159
+ for (const e of entries) {
160
+ const abs = path.join(current, e.name);
161
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
162
+ if (e.isDirectory()) await visit(abs, rel);
163
+ else if (e.isFile()) out.push(rel);
164
+ }
165
+ }
166
+ await visit(root, '');
167
+ return out;
168
+ }
169
+
120
170
  // --- apply ---
121
171
 
122
172
  export async function applyReverse(targetId, opts = {}) {
@@ -139,6 +189,13 @@ export async function applyReverse(targetId, opts = {}) {
139
189
 
140
190
  async function applyOne(c, strategy, opts) {
141
191
  const dryRun = !!opts.dryRun;
192
+ const isMirrorTree = c.kind === 'framework' || c.kind === 'hooks';
193
+
194
+ // Mirror-tree files don't have stub boilerplate — copy bytes verbatim.
195
+ if (isMirrorTree) {
196
+ return applyMirrorTreeOne(c, strategy, dryRun);
197
+ }
198
+
142
199
  const destContent = await fs.readFile(c.destPath, 'utf8');
143
200
  const stripped = stripStubBoilerplate(destContent);
144
201
 
@@ -147,7 +204,6 @@ async function applyOne(c, strategy, opts) {
147
204
  return 'skipped';
148
205
 
149
206
  case 'overwrite': {
150
- // Replace canonical with the destination content (stripped of stub boilerplate)
151
207
  if (!dryRun) {
152
208
  await fs.mkdir(path.dirname(c.kitPath), { recursive: true });
153
209
  await fs.writeFile(c.kitPath, stripped, 'utf8');
@@ -156,8 +212,6 @@ async function applyOne(c, strategy, opts) {
156
212
  }
157
213
 
158
214
  case 'merge': {
159
- // Frontmatter from canonical (if present), body from destination.
160
- // If canonical doesn't exist (new-in-ide), this degenerates to overwrite.
161
215
  let merged = stripped;
162
216
  if (c.reason === 'modified-in-ide') {
163
217
  try {
@@ -173,7 +227,6 @@ async function applyOne(c, strategy, opts) {
173
227
  }
174
228
 
175
229
  case 'rename': {
176
- // Write next to canonical with a -from-<targetfolder>.md suffix
177
230
  const base = c.kitPath.replace(/\.md$/, '');
178
231
  const tag = path.basename(path.dirname(path.dirname(c.destPath))).replace(/^\./, '');
179
232
  const out = `${base}-from-${tag || 'ide'}.md`;
@@ -189,6 +242,42 @@ async function applyOne(c, strategy, opts) {
189
242
  }
190
243
  }
191
244
 
245
+ async function applyMirrorTreeOne(c, strategy, dryRun) {
246
+ switch (strategy) {
247
+ case 'skip':
248
+ return 'skipped';
249
+
250
+ case 'overwrite':
251
+ case 'merge': {
252
+ // For framework/hooks files there's no frontmatter to preserve,
253
+ // so 'merge' degenerates to overwrite. Returning a verb that
254
+ // signals the degradation.
255
+ const verb = strategy === 'merge' ? 'merged (overwrite, no frontmatter)' : 'overwritten';
256
+ if (!dryRun) {
257
+ await fs.mkdir(path.dirname(c.kitPath), { recursive: true });
258
+ await fs.copyFile(c.destPath, c.kitPath);
259
+ }
260
+ return dryRun ? `${strategy} (dry-run)` : verb;
261
+ }
262
+
263
+ case 'rename': {
264
+ // Write to kit/<source>/<rel>.from-<tag> preserving extension after the tag.
265
+ const ext = path.extname(c.kitPath);
266
+ const stem = c.kitPath.slice(0, c.kitPath.length - ext.length);
267
+ const tag = path.basename(path.dirname(path.dirname(c.destPath))).replace(/^\./, '') || 'ide';
268
+ const out = `${stem}.from-${tag}${ext}`;
269
+ if (!dryRun) {
270
+ await fs.mkdir(path.dirname(out), { recursive: true });
271
+ await fs.copyFile(c.destPath, out);
272
+ }
273
+ return dryRun ? `rename → ${out} (dry-run)` : `renamed → ${out}`;
274
+ }
275
+
276
+ default:
277
+ return `unknown strategy: ${strategy}`;
278
+ }
279
+ }
280
+
192
281
  // --- helpers ---
193
282
 
194
283
  function isCleanStub(content) {
package/src/core/sync.js CHANGED
@@ -196,15 +196,19 @@ function renderReference(item, kitRoot, outPath, isSkill) {
196
196
  ? item.frontmatterRaw
197
197
  : synthFrontmatter(item);
198
198
 
199
- const desc = item.description ? `\n> ${item.description}\n` : '';
200
- // Blank line between frontmatter and the stub marker so YAML parsers don't choke.
201
- return `${fm}\n${STUB_MARKER}
199
+ // Body must NOT start with the STUB_MARKER comment — IDE listings (e.g. Claude Desktop)
200
+ // that take the first non-blank body line as the visible description would surface
201
+ // "<!-- kit-mcp:reference -->" instead of the real description. So we open with the
202
+ // H1 + description blockquote, and tuck the marker at the end as a trailing comment.
203
+ const descLine = item.description ? `\n> ${item.description}\n` : '';
204
+ return `${fm}
202
205
  # ${item.name}
203
-
206
+ ${descLine}
204
207
  > Canonical source: [\`${rel}\`](${rel})
205
- ${desc}
206
- > Generated by kit-mcp at ${new Date().toISOString()}.
207
208
  > Edit the source file in the kit, not this stub.
209
+ > Generated by kit-mcp at ${new Date().toISOString()}.
210
+
211
+ ${STUB_MARKER}
208
212
  `;
209
213
  }
210
214