@luanpdd/kit-mcp 0.4.1 → 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,67 @@ 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
+
54
+ ## [0.5.0] - 2026-05-03
55
+
56
+ ### Added
57
+ - **Mirror-tree sync for `framework` and `hooks`.** `kit/framework/` (124 files: workflows, templates, references, libs) and `kit/hooks/` (5 files) are now projected into `.claude/framework/` and `.claude/hooks/` on every `sync install claude-code`. Without this, the bundled slash-commands like `/novo-marco` were broken-by-design — they referenced `@./.claude/framework/workflows/new-milestone.md` and similar paths that never existed in the destination project. Now they resolve correctly end-to-end.
58
+ - New `mode: 'mirror-tree'` capability spec in `src/core/registry.js`. Each mirror-tree entry has a `source` (relative path inside `kit/`) and a `path` (destination path in the target project).
59
+ - A `.kit-mcp-managed` marker file is written at the root of each managed tree so `kit sync remove` can recursively clean up the directory **only** when the marker is present. Trees you authored yourself (without the marker) are never touched.
60
+ - CI smoke test asserts `.claude/framework/workflows/new-milestone.md`, `.claude/framework/templates/project.md`, and `.claude/hooks/workflow-guard.js` are projected, and that `sync remove` cleans them up.
61
+ - New CI safety test: `sync remove` against a `.claude/framework/` directory with no marker preserves user content.
62
+
63
+ ### Changed
64
+ - `statusOf` now reports `framework` and `hooks` capability paths.
65
+ - README capability matrix gained two columns (`framework`, `hooks`) and a paragraph explaining the mirror-tree semantics.
66
+
67
+ ### Migration
68
+ No action needed — `npx -y @luanpdd/kit-mcp@latest sync install claude-code --project-root .` projects the new directories automatically. If you had a manually-created `.claude/framework/` or `.claude/hooks/`, kit-mcp will overwrite individual files but won't delete user files; `sync remove` continues to leave them alone.
69
+
9
70
  ## [0.4.1] - 2026-05-03
10
71
 
11
72
  ### Fixed
@@ -158,7 +219,9 @@ npx -y @luanpdd/kit-mcp sync install claude-code --project-root .
158
219
  - CLI mirror of all MCP tools.
159
220
  - `install` command that registers kit-mcp into an IDE's MCP config (JSON for Claude/Cursor/Gemini/Windsurf, TOML for Codex).
160
221
 
161
- [Unreleased]: https://github.com/luanpdd/kit-mcp/compare/v0.4.1...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
224
+ [0.5.0]: https://github.com/luanpdd/kit-mcp/compare/v0.4.1...v0.5.0
162
225
  [0.4.1]: https://github.com/luanpdd/kit-mcp/compare/v0.4.0...v0.4.1
163
226
  [0.4.0]: https://github.com/luanpdd/kit-mcp/compare/v0.3.0...v0.4.0
164
227
  [0.3.0]: https://github.com/luanpdd/kit-mcp/compare/v0.2.1...v0.3.0
package/README.md CHANGED
@@ -159,19 +159,21 @@ kit sync watch --all # watch + auto-detect every IDE al
159
159
 
160
160
  **Per-IDE projection** — what each target receives:
161
161
 
162
- | IDE | rules → | agents → | commands → | skills → |
163
- |---|---|---|---|---|
164
- | Claude Code | `CLAUDE.md` | `.claude/agents/*.md` | `.claude/commands/*.md` | `.claude/skills/*/` |
165
- | Cursor | `.cursor/rules/*.mdc` | `.cursor/agents/*.md` | — | — |
166
- | Codex | `AGENTS.md` | — | — | `.codex/skills/*/` |
167
- | Gemini CLI | `GEMINI.md` | — | — | `.gemini/skills/*/` |
168
- | Copilot | `.github/copilot-instructions.md` | `.github/agents/*.agent` | — | `.github/skills/*/` |
169
- | Windsurf | `.windsurf/rules/*.md` | `.windsurf/agents/*.md` | — | `.windsurf/skills/*/` |
170
- | Antigravity | `.agents/rules/*.md` | `.agents/agents/*.md` | — | `.agents/workflows/*/` |
171
- | Trae | `.trae/rules/*.md` | `.trae/agents/*.md` | — | — |
162
+ | IDE | rules → | agents → | commands → | skills → | framework → | hooks → |
163
+ |---|---|---|---|---|---|---|
164
+ | Claude Code | `CLAUDE.md` | `.claude/agents/*.md` | `.claude/commands/*.md` | `.claude/skills/*/` | `.claude/framework/**` | `.claude/hooks/**` |
165
+ | Cursor | `.cursor/rules/*.mdc` | `.cursor/agents/*.md` | — | — | — | — |
166
+ | Codex | `AGENTS.md` | — | — | `.codex/skills/*/` | — | — |
167
+ | Gemini CLI | `GEMINI.md` | — | — | `.gemini/skills/*/` | — | — |
168
+ | Copilot | `.github/copilot-instructions.md` | `.github/agents/*.agent` | — | `.github/skills/*/` | — | — |
169
+ | Windsurf | `.windsurf/rules/*.md` | `.windsurf/agents/*.md` | — | `.windsurf/skills/*/` | — | — |
170
+ | Antigravity | `.agents/rules/*.md` | `.agents/agents/*.md` | — | `.agents/workflows/*/` | — | — |
171
+ | Trae | `.trae/rules/*.md` | `.trae/agents/*.md` | — | — | — | — |
172
172
 
173
173
  A capability marked `—` is not supported by that IDE. Adding a new IDE = one entry in [`src/core/registry.js`](src/core/registry.js).
174
174
 
175
+ **About `framework` and `hooks`:** these are *mirror-tree* capabilities — the entire `kit/framework/` and `kit/hooks/` subtrees are copied verbatim into `.claude/framework/` and `.claude/hooks/`. They're needed by the bundled workflow because slash-commands like `/novo-marco` reference framework files via paths like `@./.claude/framework/workflows/new-milestone.md`. A `.kit-mcp-managed` marker is written at the root of each managed tree so `kit sync remove` can clean up safely without touching directories you authored yourself.
176
+
175
177
  ### `kit install ...` — register kit-mcp into an IDE's MCP config
176
178
 
177
179
  ```bash
@@ -192,16 +194,19 @@ kit install write claude-code --scope user --via global # assumes `npm ins
192
194
 
193
195
  ### `kit reverse-sync ...` — bring IDE edits back to the canonical kit
194
196
 
195
- 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.
196
198
 
197
199
  ```bash
198
200
  kit reverse-sync detect claude-code --project-root .
199
201
  kit reverse-sync apply claude-code --project-root . --strategy merge --dry-run
200
202
  kit reverse-sync apply claude-code --project-root . --strategy merge
201
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
202
205
  ```
203
206
 
204
- **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>`.
205
210
 
206
211
  ### `kit gates ...` — reusable workflow gates
207
212
 
@@ -467,16 +472,27 @@ PRs welcome.
467
472
 
468
473
  ---
469
474
 
470
- ## 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:
471
486
 
472
487
  ```bash
473
488
  node bin/cli.js kit list-agents | head -5 # 19 bundled agents
474
489
  node bin/cli.js sync targets # 8 IDEs
475
490
  node bin/cli.js gates list # 5 gates
476
491
  node bin/cli.js install dry-run claude-code --via npx
477
- node bin/mcp.js < /dev/null & sleep 1; kill %1 # MCP server boots and waits on stdio
478
492
  ```
479
493
 
494
+ CI runs unit + integration + smoke + MCP boot on Ubuntu / macOS / Windows × Node 20 / 22 on every push and PR.
495
+
480
496
  ---
481
497
 
482
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.4.1",
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
  }
@@ -20,6 +20,8 @@ export const TARGETS = {
20
20
  agents: { path: '.claude/agents/', mode: 'multi', extension: '.md' },
21
21
  commands: { path: '.claude/commands/', mode: 'multi', extension: '.md' },
22
22
  skills: { path: '.claude/skills/', mode: 'multi-dir' },
23
+ framework:{ path: '.claude/framework/', mode: 'mirror-tree', source: 'framework' },
24
+ hooks: { path: '.claude/hooks/', mode: 'mirror-tree', source: 'hooks' },
23
25
  mcpConfig:{ path: '.mcp.json', strategy: 'merge-mcpServers-json',
24
26
  userPath: '~/.claude.json', userKey: 'mcpServers' },
25
27
  },
@@ -92,11 +94,13 @@ export function listTargets() {
92
94
  id,
93
95
  label: t.label,
94
96
  capabilities: {
95
- rules: !!t.rules,
96
- agents: !!t.agents,
97
- commands: !!t.commands,
98
- skills: !!t.skills,
99
- mcpConfig:!!t.mcpConfig,
97
+ rules: !!t.rules,
98
+ agents: !!t.agents,
99
+ commands: !!t.commands,
100
+ skills: !!t.skills,
101
+ framework: !!t.framework,
102
+ hooks: !!t.hooks,
103
+ mcpConfig: !!t.mcpConfig,
100
104
  },
101
105
  }));
102
106
  }
@@ -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
@@ -15,6 +15,8 @@ import { getTarget } from './registry.js';
15
15
  import { listKit, resolveKitRoot } from './kit.js';
16
16
 
17
17
  const STUB_MARKER = '<!-- kit-mcp:reference -->';
18
+ const MANAGED_MARKER_FILE = '.kit-mcp-managed';
19
+ 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';
18
20
 
19
21
  export async function syncTo(targetId, opts = {}) {
20
22
  const target = getTarget(targetId);
@@ -62,21 +64,62 @@ export async function syncTo(targetId, opts = {}) {
62
64
  }
63
65
  }
64
66
 
67
+ // Mirror-tree capabilities (framework, hooks) — copy a whole subtree of kit/<source>
68
+ // into target.<cap>.path, preserving relative structure. Dropped a marker file at the
69
+ // root so `kit sync remove` can clean up the tree safely.
70
+ for (const cap of ['framework', 'hooks']) {
71
+ const spec = target[cap];
72
+ if (!spec || spec.mode !== 'mirror-tree') continue;
73
+ const srcRoot = path.join(kitRoot, spec.source);
74
+ const dstRoot = path.join(projectRoot, spec.path);
75
+ const files = await walkTree(srcRoot);
76
+ if (files.length === 0) continue;
77
+ ops.push({ path: path.join(dstRoot, MANAGED_MARKER_FILE), content: MANAGED_MARKER_BODY, kind: cap });
78
+ for (const f of files) {
79
+ const dst = path.join(dstRoot, f.rel);
80
+ ops.push({ path: dst, srcAbs: f.abs, kind: cap, treeCopy: true });
81
+ }
82
+ }
83
+
65
84
  if (!dryRun) {
66
85
  for (const op of ops) {
67
86
  await fs.mkdir(path.dirname(op.path), { recursive: true });
68
- await fs.writeFile(op.path, op.content, 'utf8');
87
+ if (op.treeCopy) {
88
+ await fs.copyFile(op.srcAbs, op.path);
89
+ } else {
90
+ await fs.writeFile(op.path, op.content, 'utf8');
91
+ }
69
92
  }
70
93
  }
71
94
 
72
95
  return { target: targetId, mode, projectRoot, kitRoot, written: ops.map(o => o.path), dryRun };
73
96
  }
74
97
 
98
+ async function walkTree(dir) {
99
+ const out = [];
100
+ async function visit(current, relPrefix) {
101
+ let entries;
102
+ try { entries = await fs.readdir(current, { withFileTypes: true }); }
103
+ catch { return; }
104
+ for (const e of entries) {
105
+ const abs = path.join(current, e.name);
106
+ const rel = relPrefix ? `${relPrefix}/${e.name}` : e.name;
107
+ if (e.isDirectory()) {
108
+ await visit(abs, rel);
109
+ } else if (e.isFile()) {
110
+ out.push({ abs, rel });
111
+ }
112
+ }
113
+ }
114
+ await visit(dir, '');
115
+ return out;
116
+ }
117
+
75
118
  export async function statusOf(targetId, opts = {}) {
76
119
  const target = getTarget(targetId);
77
120
  const projectRoot = path.resolve(opts.projectRoot ?? process.cwd());
78
121
  const checks = [];
79
- for (const cap of ['rules', 'agents', 'commands', 'skills']) {
122
+ for (const cap of ['rules', 'agents', 'commands', 'skills', 'framework', 'hooks']) {
80
123
  if (!target[cap]) continue;
81
124
  const probe = path.join(projectRoot, target[cap].path);
82
125
  let exists = false;
@@ -105,6 +148,18 @@ export async function removeFrom(targetId, opts = {}) {
105
148
  }
106
149
  } catch {}
107
150
  }
151
+ // Mirror-tree capabilities: only remove if our marker is present (we manage the whole subtree).
152
+ for (const cap of ['framework', 'hooks']) {
153
+ const spec = target[cap];
154
+ if (!spec || spec.mode !== 'mirror-tree') continue;
155
+ const dir = path.join(projectRoot, spec.path);
156
+ const marker = path.join(dir, MANAGED_MARKER_FILE);
157
+ try {
158
+ await fs.access(marker);
159
+ await fs.rm(dir, { recursive: true, force: true });
160
+ removed.push(dir);
161
+ } catch {}
162
+ }
108
163
  return { target: targetId, projectRoot, removed };
109
164
  }
110
165
 
@@ -141,15 +196,19 @@ function renderReference(item, kitRoot, outPath, isSkill) {
141
196
  ? item.frontmatterRaw
142
197
  : synthFrontmatter(item);
143
198
 
144
- const desc = item.description ? `\n> ${item.description}\n` : '';
145
- // Blank line between frontmatter and the stub marker so YAML parsers don't choke.
146
- 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}
147
205
  # ${item.name}
148
-
206
+ ${descLine}
149
207
  > Canonical source: [\`${rel}\`](${rel})
150
- ${desc}
151
- > Generated by kit-mcp at ${new Date().toISOString()}.
152
208
  > Edit the source file in the kit, not this stub.
209
+ > Generated by kit-mcp at ${new Date().toISOString()}.
210
+
211
+ ${STUB_MARKER}
153
212
  `;
154
213
  }
155
214