@luanpdd/kit-mcp 1.27.0 → 1.29.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 (324) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +168 -914
  3. package/gates/agent-no-recursive-dispatch.md +45 -11
  4. package/kit/COMANDOS.md +138 -138
  5. package/kit/README.md +76 -76
  6. package/kit/agents/advisor-researcher.md +106 -106
  7. package/kit/agents/assumptions-analyzer.md +107 -107
  8. package/kit/agents/audit-log-implementer.md +1 -1
  9. package/kit/agents/auditor-consistencia-isolamento.md +1 -1
  10. package/kit/agents/b2b-saas-architect.md +1 -1
  11. package/kit/agents/cascading-failures-auditor.md +1 -1
  12. package/kit/agents/codebase-mapper.md +768 -768
  13. package/kit/agents/crm-pipeline-implementer.md +1 -1
  14. package/kit/agents/debugger.md +813 -813
  15. package/kit/agents/detector-tenant-quente.md +1 -1
  16. package/kit/agents/evolution-go-integrator.md +1 -1
  17. package/kit/agents/example-reviewer.md +21 -21
  18. package/kit/agents/executor.md +564 -564
  19. package/kit/agents/integration-checker.md +200 -200
  20. package/kit/agents/invite-flow-implementer.md +1 -1
  21. package/kit/agents/legacy-characterizer.md +1 -1
  22. package/kit/agents/lgpd-compliance-auditor.md +1 -1
  23. package/kit/agents/multi-tenant-isolation-auditor.md +1 -1
  24. package/kit/agents/multi-tenant-rls-writer.md +1 -1
  25. package/kit/agents/nyquist-auditor.md +178 -178
  26. package/kit/agents/observability-coverage-auditor.md +1 -1
  27. package/kit/agents/org-onboarding-implementer.md +1 -1
  28. package/kit/agents/payload-capture-instrumenter.md +1 -1
  29. package/kit/agents/phase-researcher.md +696 -696
  30. package/kit/agents/plan-checker.md +272 -272
  31. package/kit/agents/planner.md +922 -922
  32. package/kit/agents/project-researcher.md +652 -652
  33. package/kit/agents/refactor-safety-auditor.md +1 -1
  34. package/kit/agents/research-synthesizer.md +245 -245
  35. package/kit/agents/roadmapper.md +677 -677
  36. package/kit/agents/seam-finder.md +1 -1
  37. package/kit/agents/shotgun-surgery-detector.md +1 -1
  38. package/kit/agents/supabase-branching-architect.md +1 -1
  39. package/kit/agents/supabase-cicd-pipeline-implementer.md +1 -1
  40. package/kit/agents/supabase-column-privileges-writer.md +1 -1
  41. package/kit/agents/supabase-migration-writer.md +1 -1
  42. package/kit/agents/supabase-rbac-implementer.md +1 -1
  43. package/kit/agents/supabase-rls-hardener.md +1 -1
  44. package/kit/agents/supabase-rls-writer.md +1 -1
  45. package/kit/agents/supabase-roles-implementer.md +1 -1
  46. package/kit/agents/super-admin-implementer.md +1 -1
  47. package/kit/agents/ui-auditor.md +437 -437
  48. package/kit/agents/ui-checker.md +302 -302
  49. package/kit/agents/ui-researcher.md +355 -355
  50. package/kit/agents/user-profiler.md +175 -175
  51. package/kit/agents/validador-evolucao-schema.md +1 -1
  52. package/kit/agents/verifier.md +728 -728
  53. package/kit/commands/adicionar-backlog.md +75 -75
  54. package/kit/commands/adicionar-fase.md +42 -42
  55. package/kit/commands/adicionar-tarefa.md +45 -45
  56. package/kit/commands/adicionar-testes.md +41 -41
  57. package/kit/commands/ajuda.md +21 -21
  58. package/kit/commands/atualizar.md +37 -37
  59. package/kit/commands/auditar-cascading.md +1 -1
  60. package/kit/commands/auditar-marco.md +179 -179
  61. package/kit/commands/auditar-observabilidade-cobertura.md +1 -1
  62. package/kit/commands/auditar-refactor.md +1 -1
  63. package/kit/commands/auditar-release.md +1 -1
  64. package/kit/commands/auditar-uat.md +23 -23
  65. package/kit/commands/autonomo.md +40 -40
  66. package/kit/commands/branch-pr.md +24 -24
  67. package/kit/commands/burn-rate-status.md +1 -1
  68. package/kit/commands/capturar-payloads.md +1 -1
  69. package/kit/commands/caracterizar.md +1 -1
  70. package/kit/commands/concluir-marco.md +247 -247
  71. package/kit/commands/configuracoes.md +36 -36
  72. package/kit/commands/dados-distribuidos.md +1 -1
  73. package/kit/commands/definir-perfil.md +10 -10
  74. package/kit/commands/depurar.md +190 -190
  75. package/kit/commands/detectar-duplicacao.md +1 -1
  76. package/kit/commands/discutir-fase.md +131 -131
  77. package/kit/commands/encontrar-seams.md +1 -1
  78. package/kit/commands/entrar-discord.md +17 -17
  79. package/kit/commands/estatisticas.md +18 -18
  80. package/kit/commands/example-greeting.md +33 -33
  81. package/kit/commands/executar-fase.md +58 -58
  82. package/kit/commands/expresso.md +56 -56
  83. package/kit/commands/fase-ui.md +34 -34
  84. package/kit/commands/fazer.md +57 -57
  85. package/kit/commands/fio.md +125 -125
  86. package/kit/commands/fluxos-trabalho.md +64 -64
  87. package/kit/commands/forense.md +176 -176
  88. package/kit/commands/gerenciador.md +38 -38
  89. package/kit/commands/inserir-fase.md +31 -31
  90. package/kit/commands/legacy.md +1 -1
  91. package/kit/commands/limpeza.md +17 -17
  92. package/kit/commands/listar-hipoteses-fase.md +45 -45
  93. package/kit/commands/listar-workspaces.md +18 -18
  94. package/kit/commands/load-shedding.md +1 -1
  95. package/kit/commands/mapear-codebase.md +70 -70
  96. package/kit/commands/multi-tenant.md +1 -1
  97. package/kit/commands/nota.md +33 -33
  98. package/kit/commands/novo-marco.md +43 -43
  99. package/kit/commands/novo-projeto.md +41 -41
  100. package/kit/commands/novo-workspace.md +43 -43
  101. package/kit/commands/pausar-trabalho.md +37 -37
  102. package/kit/commands/perfil-usuario.md +45 -45
  103. package/kit/commands/pesquisar-fase.md +195 -195
  104. package/kit/commands/planejar-fase.md +67 -67
  105. package/kit/commands/planejar-lacunas.md +33 -33
  106. package/kit/commands/plantar-ideia.md +25 -25
  107. package/kit/commands/progresso.md +24 -24
  108. package/kit/commands/proximo.md +30 -30
  109. package/kit/commands/publicar.md +490 -490
  110. package/kit/commands/rapido.md +35 -35
  111. package/kit/commands/reaplicar-patches.md +124 -124
  112. package/kit/commands/refactor-seguro.md +1 -1
  113. package/kit/commands/relatorio-sessao.md +19 -19
  114. package/kit/commands/remover-fase.md +31 -31
  115. package/kit/commands/remover-workspace.md +26 -26
  116. package/kit/commands/resumo-marco.md +50 -50
  117. package/kit/commands/retomar-trabalho.md +40 -40
  118. package/kit/commands/revisar-backlog.md +60 -60
  119. package/kit/commands/revisar-ui.md +32 -32
  120. package/kit/commands/revisar.md +37 -37
  121. package/kit/commands/saude.md +21 -21
  122. package/kit/commands/setup-notion.md +93 -93
  123. package/kit/commands/storytelling.md +1 -1
  124. package/kit/commands/supabase.md +1 -1
  125. package/kit/commands/sync-main.md +68 -68
  126. package/kit/commands/validar-fase.md +35 -35
  127. package/kit/commands/verificar-tarefas.md +44 -44
  128. package/kit/commands/verificar-trabalho.md +64 -64
  129. package/kit/file-manifest.json +90 -90
  130. package/kit/framework/bin/lib/commands.cjs +959 -959
  131. package/kit/framework/bin/lib/config.cjs +442 -442
  132. package/kit/framework/bin/lib/core.cjs +1230 -1230
  133. package/kit/framework/bin/lib/frontmatter.cjs +336 -336
  134. package/kit/framework/bin/lib/init.cjs +1442 -1442
  135. package/kit/framework/bin/lib/milestone.cjs +252 -252
  136. package/kit/framework/bin/lib/model-profiles.cjs +68 -68
  137. package/kit/framework/bin/lib/phase.cjs +888 -888
  138. package/kit/framework/bin/lib/profile-output.cjs +952 -952
  139. package/kit/framework/bin/lib/profile-pipeline.cjs +539 -539
  140. package/kit/framework/bin/lib/roadmap.cjs +329 -329
  141. package/kit/framework/bin/lib/security.cjs +382 -382
  142. package/kit/framework/bin/lib/state.cjs +1031 -1031
  143. package/kit/framework/bin/lib/template.cjs +222 -222
  144. package/kit/framework/bin/lib/uat.cjs +282 -282
  145. package/kit/framework/bin/lib/verify.cjs +888 -888
  146. package/kit/framework/bin/lib/workstream.cjs +491 -491
  147. package/kit/framework/bin/tools.cjs +918 -918
  148. package/kit/framework/commands/workstreams.md +63 -63
  149. package/kit/framework/references/checkpoints.md +778 -778
  150. package/kit/framework/references/continuation-format.md +249 -249
  151. package/kit/framework/references/decimal-phase-calculation.md +64 -64
  152. package/kit/framework/references/git-integration.md +295 -295
  153. package/kit/framework/references/git-planning-commit.md +38 -38
  154. package/kit/framework/references/model-profile-resolution.md +36 -36
  155. package/kit/framework/references/model-profiles.md +139 -139
  156. package/kit/framework/references/phase-argument-parsing.md +61 -61
  157. package/kit/framework/references/planning-config.md +202 -202
  158. package/kit/framework/references/questioning.md +162 -162
  159. package/kit/framework/references/tdd.md +263 -263
  160. package/kit/framework/references/ui-brand.md +160 -160
  161. package/kit/framework/references/user-profiling.md +657 -657
  162. package/kit/framework/references/verification-patterns.md +612 -612
  163. package/kit/framework/references/workstream-flag.md +58 -58
  164. package/kit/framework/templates/DEBUG.md +164 -164
  165. package/kit/framework/templates/UAT.md +265 -265
  166. package/kit/framework/templates/UI-SPEC.md +100 -100
  167. package/kit/framework/templates/VALIDATION.md +76 -76
  168. package/kit/framework/templates/claude-md.md +122 -122
  169. package/kit/framework/templates/codebase/architecture.md +185 -185
  170. package/kit/framework/templates/codebase/concerns.md +205 -205
  171. package/kit/framework/templates/codebase/conventions.md +204 -204
  172. package/kit/framework/templates/codebase/integrations.md +192 -192
  173. package/kit/framework/templates/codebase/stack.md +158 -158
  174. package/kit/framework/templates/codebase/structure.md +199 -199
  175. package/kit/framework/templates/codebase/testing.md +301 -301
  176. package/kit/framework/templates/config.json +44 -44
  177. package/kit/framework/templates/context.md +352 -352
  178. package/kit/framework/templates/continue-here.md +78 -78
  179. package/kit/framework/templates/copilot-instructions.md +7 -7
  180. package/kit/framework/templates/debug-subagent-prompt.md +91 -91
  181. package/kit/framework/templates/dev-preferences.md +20 -20
  182. package/kit/framework/templates/discovery.md +146 -146
  183. package/kit/framework/templates/discussion-log.md +63 -63
  184. package/kit/framework/templates/milestone-archive.md +123 -123
  185. package/kit/framework/templates/milestone.md +115 -115
  186. package/kit/framework/templates/phase-prompt.md +610 -610
  187. package/kit/framework/templates/planner-subagent-prompt.md +117 -117
  188. package/kit/framework/templates/project.md +186 -186
  189. package/kit/framework/templates/requirements.md +231 -231
  190. package/kit/framework/templates/research-project/ARCHITECTURE.md +204 -204
  191. package/kit/framework/templates/research-project/FEATURES.md +147 -147
  192. package/kit/framework/templates/research-project/PITFALLS.md +200 -200
  193. package/kit/framework/templates/research-project/STACK.md +120 -120
  194. package/kit/framework/templates/research-project/SUMMARY.md +170 -170
  195. package/kit/framework/templates/research.md +419 -419
  196. package/kit/framework/templates/retrospective.md +54 -54
  197. package/kit/framework/templates/roadmap.md +202 -202
  198. package/kit/framework/templates/state.md +176 -176
  199. package/kit/framework/templates/summary-complex.md +59 -59
  200. package/kit/framework/templates/summary-minimal.md +41 -41
  201. package/kit/framework/templates/summary-standard.md +48 -48
  202. package/kit/framework/templates/summary.md +209 -209
  203. package/kit/framework/templates/user-profile.md +146 -146
  204. package/kit/framework/templates/user-setup.md +256 -256
  205. package/kit/framework/templates/verification-report.md +258 -258
  206. package/kit/framework/workflows/add-phase.md +112 -112
  207. package/kit/framework/workflows/add-tests.md +351 -351
  208. package/kit/framework/workflows/add-todo.md +158 -158
  209. package/kit/framework/workflows/audit-milestone.md +340 -340
  210. package/kit/framework/workflows/audit-uat.md +109 -109
  211. package/kit/framework/workflows/autonomous.md +891 -891
  212. package/kit/framework/workflows/check-todos.md +177 -177
  213. package/kit/framework/workflows/cleanup.md +152 -152
  214. package/kit/framework/workflows/complete-milestone.md +696 -696
  215. package/kit/framework/workflows/diagnose-issues.md +231 -231
  216. package/kit/framework/workflows/discovery-phase.md +289 -289
  217. package/kit/framework/workflows/discuss-phase-assumptions.md +653 -653
  218. package/kit/framework/workflows/discuss-phase.md +784 -784
  219. package/kit/framework/workflows/do.md +104 -104
  220. package/kit/framework/workflows/execute-phase.md +838 -838
  221. package/kit/framework/workflows/execute-plan.md +510 -510
  222. package/kit/framework/workflows/fast.md +102 -102
  223. package/kit/framework/workflows/forensics.md +265 -265
  224. package/kit/framework/workflows/health.md +181 -181
  225. package/kit/framework/workflows/help.md +619 -619
  226. package/kit/framework/workflows/insert-phase.md +130 -130
  227. package/kit/framework/workflows/list-phase-assumptions.md +178 -178
  228. package/kit/framework/workflows/list-workspaces.md +56 -56
  229. package/kit/framework/workflows/manager.md +362 -362
  230. package/kit/framework/workflows/map-codebase.md +377 -377
  231. package/kit/framework/workflows/milestone-summary.md +223 -223
  232. package/kit/framework/workflows/new-milestone.md +486 -486
  233. package/kit/framework/workflows/new-project.md +1159 -1159
  234. package/kit/framework/workflows/new-workspace.md +237 -237
  235. package/kit/framework/workflows/next.md +97 -97
  236. package/kit/framework/workflows/node-repair.md +92 -92
  237. package/kit/framework/workflows/note.md +156 -156
  238. package/kit/framework/workflows/pause-work.md +176 -176
  239. package/kit/framework/workflows/plan-milestone-gaps.md +273 -273
  240. package/kit/framework/workflows/plan-phase.md +765 -765
  241. package/kit/framework/workflows/plant-seed.md +169 -169
  242. package/kit/framework/workflows/pr-branch.md +129 -129
  243. package/kit/framework/workflows/profile-user.md +450 -450
  244. package/kit/framework/workflows/progress.md +507 -507
  245. package/kit/framework/workflows/quick.md +757 -757
  246. package/kit/framework/workflows/remove-phase.md +155 -155
  247. package/kit/framework/workflows/remove-workspace.md +90 -90
  248. package/kit/framework/workflows/research-phase.md +82 -82
  249. package/kit/framework/workflows/resume-project.md +326 -326
  250. package/kit/framework/workflows/review.md +228 -228
  251. package/kit/framework/workflows/session-report.md +146 -146
  252. package/kit/framework/workflows/settings.md +283 -283
  253. package/kit/framework/workflows/ship.md +228 -228
  254. package/kit/framework/workflows/stats.md +60 -60
  255. package/kit/framework/workflows/transition.md +671 -671
  256. package/kit/framework/workflows/ui-phase.md +302 -302
  257. package/kit/framework/workflows/ui-review.md +165 -165
  258. package/kit/framework/workflows/update.md +323 -323
  259. package/kit/framework/workflows/validate-phase.md +174 -174
  260. package/kit/framework/workflows/verify-phase.md +252 -252
  261. package/kit/framework/workflows/verify-work.md +637 -637
  262. package/kit/hooks/check-update.js +118 -118
  263. package/kit/hooks/context-monitor.js +163 -163
  264. package/kit/hooks/prompt-guard.js +103 -103
  265. package/kit/hooks/statusline.js +125 -125
  266. package/kit/hooks/workflow-guard.js +101 -101
  267. package/kit/settings.json +45 -45
  268. package/kit/skills/ai-prompt-characterization/SKILL.md +1 -1
  269. package/kit/skills/armadilhas-sistemas-distribuidos/SKILL.md +1 -1
  270. package/kit/skills/audit-log-multi-tenant/SKILL.md +1 -1
  271. package/kit/skills/b2b-saas-architecture/SKILL.md +1 -1
  272. package/kit/skills/consistencia-leitura-replica/SKILL.md +1 -1
  273. package/kit/skills/crm-lead-pipeline-patterns/SKILL.md +1 -1
  274. package/kit/skills/escolha-modelo-consistencia/SKILL.md +1 -1
  275. package/kit/skills/evolucao-schema-compativel/SKILL.md +1 -1
  276. package/kit/skills/evolution-go-whatsapp-integration/SKILL.md +1 -1
  277. package/kit/skills/example-skill/SKILL.md +42 -42
  278. package/kit/skills/legacy-api-only-applications/SKILL.md +1 -1
  279. package/kit/skills/legacy-characterization-tests/SKILL.md +1 -1
  280. package/kit/skills/legacy-effect-analysis/SKILL.md +1 -1
  281. package/kit/skills/legacy-extract-class/SKILL.md +1 -1
  282. package/kit/skills/legacy-programming-by-difference/SKILL.md +1 -1
  283. package/kit/skills/legacy-seams-and-test-harness/SKILL.md +1 -1
  284. package/kit/skills/legacy-shotgun-surgery/SKILL.md +1 -1
  285. package/kit/skills/legacy-sprout-wrap-techniques/SKILL.md +1 -1
  286. package/kit/skills/legacy-storytelling-naked-crc/SKILL.md +1 -1
  287. package/kit/skills/lgpd-multi-tenant-compliance/SKILL.md +1 -1
  288. package/kit/skills/member-invite-flow/SKILL.md +1 -1
  289. package/kit/skills/member-management-react-shadcn/SKILL.md +1 -1
  290. package/kit/skills/multi-tenant-performance-scaling/SKILL.md +1 -1
  291. package/kit/skills/multi-tenant-rls-hierarchy/SKILL.md +1 -1
  292. package/kit/skills/org-onboarding-flow/SKILL.md +1 -1
  293. package/kit/skills/org-switcher-react-pattern/SKILL.md +1 -1
  294. package/kit/skills/permission-gate-react-pattern/SKILL.md +1 -1
  295. package/kit/skills/postgres-isolamento-concorrencia/SKILL.md +1 -1
  296. package/kit/skills/pre-refactor-characterization/SKILL.md +1 -1
  297. package/kit/skills/rbac-permissions-matrix-supabase/SKILL.md +1 -1
  298. package/kit/skills/streams-eventos-cdc/SKILL.md +1 -1
  299. package/kit/skills/supabase-branching-workflow/SKILL.md +1 -1
  300. package/kit/skills/supabase-ci-cd-github-actions/SKILL.md +1 -1
  301. package/kit/skills/supabase-column-level-security/SKILL.md +1 -1
  302. package/kit/skills/supabase-config-toml-remotes/SKILL.md +1 -1
  303. package/kit/skills/supabase-custom-claims-rbac/SKILL.md +1 -1
  304. package/kit/skills/supabase-migration-repair/SKILL.md +1 -1
  305. package/kit/skills/supabase-migrations/SKILL.md +1 -1
  306. package/kit/skills/supabase-pgtap-testing/SKILL.md +1 -1
  307. package/kit/skills/supabase-postgres-roles/SKILL.md +1 -1
  308. package/kit/skills/supabase-rls-defense-in-depth/SKILL.md +1 -1
  309. package/kit/skills/supabase-rls-policies/SKILL.md +1 -1
  310. package/kit/skills/super-admin-platform-pattern/SKILL.md +1 -1
  311. package/kit/skills/tenant-quente-mitigacao/SKILL.md +1 -1
  312. package/kit/skills/whatsapp-conversation-state-machine/SKILL.md +1 -1
  313. package/package.json +63 -63
  314. package/src/cli/index.js +378 -6
  315. package/src/cli/render.js +7 -0
  316. package/src/core/kit.js +216 -216
  317. package/src/core/logger.js +170 -0
  318. package/src/core/notify.js +60 -0
  319. package/src/core/reflect.js +247 -247
  320. package/src/core/reverse-sync.js +372 -372
  321. package/src/core/sync.js +418 -418
  322. package/src/core/watch.js +121 -121
  323. package/src/mcp-server/index.js +276 -10
  324. package/src/mcp-server/roots.js +124 -0
package/src/core/kit.js CHANGED
@@ -1,216 +1,216 @@
1
- // Read the canonical kit/ directory and return a structured index.
2
- // Source of truth: kit/agents/*.md, kit/commands/*.md, kit/skills/*/SKILL.md
3
- //
4
- // Frontmatter is parsed loosely (no external dep) — we only need name & description.
5
-
6
- import path from 'node:path';
7
- import fs from 'node:fs/promises';
8
- import { fileURLToPath } from 'node:url';
9
-
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = path.dirname(__filename);
12
-
13
- // PERF-02: Frontmatter regexes compiled once at module load (was being recompiled
14
- // on every readMdDir / readSkillsDir entry — 60+ times per listKit call).
15
- const FRONTMATTER_SPLIT_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
16
- const FRONTMATTER_RAW_RE = /^(---\r?\n[\s\S]*?\r?\n---\r?\n?)/;
17
- const YAML_KEY_RE = /^([A-Za-z0-9_-]+):\s*(.*)$/;
18
-
19
- // Resolution order for the kit root (re-evaluated on each call so env-var
20
- // overrides set after module load — e.g. by the CLI preAction hook — work):
21
- // 1. explicit `kitRoot` opt passed by caller
22
- // 2. KIT_MCP_KIT_ROOT env var (per-session override)
23
- // 3. ./kit relative to this package (the bundled example kit)
24
- export const BUNDLED_KIT_ROOT = path.resolve(__dirname, '../../kit');
25
- export function resolveKitRoot(kitRoot) {
26
- if (kitRoot) return path.resolve(kitRoot);
27
- if (process.env.KIT_MCP_KIT_ROOT) return path.resolve(process.env.KIT_MCP_KIT_ROOT);
28
- return BUNDLED_KIT_ROOT;
29
- }
30
-
31
- // PERF-01: TTL cache for listKit output. Repeated calls within KIT_CACHE_TTL_MS
32
- // return the cached value — sync/reverse-sync/MCP list-* tools used to walk the
33
- // disk on every invocation. Trade-off: callers that edit kit/ inside the same
34
- // process may see stale data for up to 30s. Acceptable for MCP/CLI ergonomics.
35
- const KIT_CACHE_TTL_MS = 30_000;
36
- const kitCache = new Map(); // `${kitRoot}:${mode}` -> { value, ts }
37
-
38
- // PERF-S1: when sync runs in mode=reference (default), the body/content of each
39
- // kit file is never used — only frontmatter (name + description). Reading just
40
- // the first STUB_READ_BYTES is enough for any frontmatter we'd ever produce and
41
- // avoids loading 50 KB+ files (planner.md etc) from disk.
42
- const STUB_READ_BYTES = 4096;
43
-
44
- export function clearKitCache() { kitCache.clear(); }
45
-
46
- export async function listKit(kitRoot, opts = {}) {
47
- kitRoot = resolveKitRoot(kitRoot);
48
- const stubsOnly = opts.stubsOnly === true;
49
- const cacheKey = `${kitRoot}:${stubsOnly ? 'stubs' : 'full'}`;
50
- const cached = kitCache.get(cacheKey);
51
- if (cached && Date.now() - cached.ts < KIT_CACHE_TTL_MS) {
52
- return cached.value;
53
- }
54
- const [agents, commands, skills, skillsExtras] = await Promise.all([
55
- readMdDir(path.join(kitRoot, 'agents'), 'agent', { stubsOnly }),
56
- readMdDir(path.join(kitRoot, 'commands'), 'command', { stubsOnly }),
57
- readSkillsDir(path.join(kitRoot, 'skills'), { stubsOnly }),
58
- readSkillsDir(path.join(kitRoot, 'skills-extras'), { stubsOnly }).catch(() => []),
59
- ]);
60
- const value = { agents, commands, skills, skillsExtras, kitRoot, stubsOnly };
61
- kitCache.set(cacheKey, { value, ts: Date.now() });
62
- return value;
63
- }
64
-
65
- // Read just enough bytes from the head of the file to capture the frontmatter.
66
- // Returns the partial string. fs.open + fd.read avoids the OS pre-fetching the
67
- // rest of the file (which fs.readFile would force).
68
- async function readHead(absPath, n) {
69
- const fd = await fs.open(absPath, 'r');
70
- try {
71
- const buf = Buffer.alloc(n);
72
- const { bytesRead } = await fd.read(buf, 0, n, 0);
73
- return buf.subarray(0, bytesRead).toString('utf8');
74
- } finally {
75
- await fd.close();
76
- }
77
- }
78
-
79
- async function readMdDir(dir, kind, { stubsOnly = false } = {}) {
80
- let entries;
81
- try {
82
- entries = await fs.readdir(dir, { withFileTypes: true });
83
- } catch {
84
- return [];
85
- }
86
- const out = [];
87
- for (const e of entries) {
88
- if (!e.isFile() || !e.name.endsWith('.md')) continue;
89
- const absPath = path.join(dir, e.name);
90
- const raw = stubsOnly
91
- ? await readHead(absPath, STUB_READ_BYTES)
92
- : await fs.readFile(absPath, 'utf8');
93
- const { frontmatter, body } = splitFrontmatter(raw);
94
- const item = {
95
- kind,
96
- name: e.name.replace(/\.md$/, ''),
97
- absPath,
98
- frontmatter,
99
- frontmatterRaw: matchFrontmatterRaw(raw),
100
- description: frontmatter?.description ?? firstNonEmptyLine(body),
101
- };
102
- if (!stubsOnly) {
103
- item.body = body;
104
- item.content = raw;
105
- }
106
- out.push(item);
107
- }
108
- return out.sort((a, b) => a.name.localeCompare(b.name));
109
- }
110
-
111
- async function readSkillsDir(dir, { stubsOnly = false } = {}) {
112
- let entries;
113
- try {
114
- entries = await fs.readdir(dir, { withFileTypes: true });
115
- } catch {
116
- return [];
117
- }
118
- const out = [];
119
- for (const e of entries) {
120
- if (!e.isDirectory()) continue;
121
- const skillPath = path.join(dir, e.name, 'SKILL.md');
122
- let raw;
123
- try {
124
- raw = stubsOnly
125
- ? await readHead(skillPath, STUB_READ_BYTES)
126
- : await fs.readFile(skillPath, 'utf8');
127
- } catch { continue; }
128
- const { frontmatter, body } = splitFrontmatter(raw);
129
- const item = {
130
- kind: 'skill',
131
- name: e.name,
132
- absPath: skillPath,
133
- dirPath: path.join(dir, e.name),
134
- frontmatter,
135
- frontmatterRaw: matchFrontmatterRaw(raw),
136
- description: frontmatter?.description ?? firstNonEmptyLine(body),
137
- };
138
- if (!stubsOnly) {
139
- item.body = body;
140
- item.skillContent = raw;
141
- }
142
- out.push(item);
143
- }
144
- return out.sort((a, b) => a.name.localeCompare(b.name));
145
- }
146
-
147
- // --- minimal YAML-ish frontmatter parser (no deps) ---
148
- // Handles `key: value`, `key: >` multiline, but NOT nested objects/arrays.
149
- // Good enough for our SKILL.md / agent.md headers.
150
-
151
- function splitFrontmatter(raw) {
152
- const m = raw.match(FRONTMATTER_SPLIT_RE);
153
- if (!m) return { frontmatter: null, body: raw };
154
- return { frontmatter: parseLooseYaml(m[1]), body: m[2] };
155
- }
156
-
157
- function matchFrontmatterRaw(raw) {
158
- const m = raw.match(FRONTMATTER_RAW_RE);
159
- return m ? m[1] : '';
160
- }
161
-
162
- function parseLooseYaml(text) {
163
- const out = {};
164
- const lines = text.split(/\r?\n/);
165
- let i = 0;
166
- while (i < lines.length) {
167
- const line = lines[i];
168
- const m = line.match(YAML_KEY_RE);
169
- if (!m) { i++; continue; }
170
- const key = m[1];
171
- let val = m[2];
172
- if (val === '>' || val === '|') {
173
- // Multiline: collect indented lines
174
- const collected = [];
175
- i++;
176
- while (i < lines.length && /^\s+/.test(lines[i])) {
177
- collected.push(lines[i].replace(/^\s+/, ''));
178
- i++;
179
- }
180
- out[key] = collected.join(' ').trim();
181
- continue;
182
- }
183
- out[key] = val.trim().replace(/^["']|["']$/g, '');
184
- i++;
185
- }
186
- return out;
187
- }
188
-
189
- function firstNonEmptyLine(body) {
190
- for (const line of body.split(/\r?\n/)) {
191
- const t = line.trim();
192
- if (!t) continue; // blank
193
- if (t.startsWith('#')) continue; // markdown heading
194
- if (t.startsWith('<!--')) continue; // HTML comment (e.g. STUB_MARKER)
195
- return t.slice(0, 200);
196
- }
197
- return '';
198
- }
199
-
200
- // --- search helpers ---
201
-
202
- export function searchKit(kit, query) {
203
- const q = query.toLowerCase();
204
- const all = [...kit.agents, ...kit.commands, ...kit.skills, ...kit.skillsExtras];
205
- return all.filter(item =>
206
- item.name.toLowerCase().includes(q) ||
207
- (item.description ?? '').toLowerCase().includes(q)
208
- ).map(({ kind, name, description, absPath }) => ({ kind, name, description, absPath }));
209
- }
210
-
211
- export function findItem(kit, kind, name) {
212
- const buckets = { agent: kit.agents, command: kit.commands, skill: [...kit.skills, ...kit.skillsExtras] };
213
- const b = buckets[kind];
214
- if (!b) throw new Error(`Unknown kind: ${kind}`);
215
- return b.find(x => x.name === name) ?? null;
216
- }
1
+ // Read the canonical kit/ directory and return a structured index.
2
+ // Source of truth: kit/agents/*.md, kit/commands/*.md, kit/skills/*/SKILL.md
3
+ //
4
+ // Frontmatter is parsed loosely (no external dep) — we only need name & description.
5
+
6
+ import path from 'node:path';
7
+ import fs from 'node:fs/promises';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ // PERF-02: Frontmatter regexes compiled once at module load (was being recompiled
14
+ // on every readMdDir / readSkillsDir entry — 60+ times per listKit call).
15
+ const FRONTMATTER_SPLIT_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
16
+ const FRONTMATTER_RAW_RE = /^(---\r?\n[\s\S]*?\r?\n---\r?\n?)/;
17
+ const YAML_KEY_RE = /^([A-Za-z0-9_-]+):\s*(.*)$/;
18
+
19
+ // Resolution order for the kit root (re-evaluated on each call so env-var
20
+ // overrides set after module load — e.g. by the CLI preAction hook — work):
21
+ // 1. explicit `kitRoot` opt passed by caller
22
+ // 2. KIT_MCP_KIT_ROOT env var (per-session override)
23
+ // 3. ./kit relative to this package (the bundled example kit)
24
+ export const BUNDLED_KIT_ROOT = path.resolve(__dirname, '../../kit');
25
+ export function resolveKitRoot(kitRoot) {
26
+ if (kitRoot) return path.resolve(kitRoot);
27
+ if (process.env.KIT_MCP_KIT_ROOT) return path.resolve(process.env.KIT_MCP_KIT_ROOT);
28
+ return BUNDLED_KIT_ROOT;
29
+ }
30
+
31
+ // PERF-01: TTL cache for listKit output. Repeated calls within KIT_CACHE_TTL_MS
32
+ // return the cached value — sync/reverse-sync/MCP list-* tools used to walk the
33
+ // disk on every invocation. Trade-off: callers that edit kit/ inside the same
34
+ // process may see stale data for up to 30s. Acceptable for MCP/CLI ergonomics.
35
+ const KIT_CACHE_TTL_MS = 30_000;
36
+ const kitCache = new Map(); // `${kitRoot}:${mode}` -> { value, ts }
37
+
38
+ // PERF-S1: when sync runs in mode=reference (default), the body/content of each
39
+ // kit file is never used — only frontmatter (name + description). Reading just
40
+ // the first STUB_READ_BYTES is enough for any frontmatter we'd ever produce and
41
+ // avoids loading 50 KB+ files (planner.md etc) from disk.
42
+ const STUB_READ_BYTES = 4096;
43
+
44
+ export function clearKitCache() { kitCache.clear(); }
45
+
46
+ export async function listKit(kitRoot, opts = {}) {
47
+ kitRoot = resolveKitRoot(kitRoot);
48
+ const stubsOnly = opts.stubsOnly === true;
49
+ const cacheKey = `${kitRoot}:${stubsOnly ? 'stubs' : 'full'}`;
50
+ const cached = kitCache.get(cacheKey);
51
+ if (cached && Date.now() - cached.ts < KIT_CACHE_TTL_MS) {
52
+ return cached.value;
53
+ }
54
+ const [agents, commands, skills, skillsExtras] = await Promise.all([
55
+ readMdDir(path.join(kitRoot, 'agents'), 'agent', { stubsOnly }),
56
+ readMdDir(path.join(kitRoot, 'commands'), 'command', { stubsOnly }),
57
+ readSkillsDir(path.join(kitRoot, 'skills'), { stubsOnly }),
58
+ readSkillsDir(path.join(kitRoot, 'skills-extras'), { stubsOnly }).catch(() => []),
59
+ ]);
60
+ const value = { agents, commands, skills, skillsExtras, kitRoot, stubsOnly };
61
+ kitCache.set(cacheKey, { value, ts: Date.now() });
62
+ return value;
63
+ }
64
+
65
+ // Read just enough bytes from the head of the file to capture the frontmatter.
66
+ // Returns the partial string. fs.open + fd.read avoids the OS pre-fetching the
67
+ // rest of the file (which fs.readFile would force).
68
+ async function readHead(absPath, n) {
69
+ const fd = await fs.open(absPath, 'r');
70
+ try {
71
+ const buf = Buffer.alloc(n);
72
+ const { bytesRead } = await fd.read(buf, 0, n, 0);
73
+ return buf.subarray(0, bytesRead).toString('utf8');
74
+ } finally {
75
+ await fd.close();
76
+ }
77
+ }
78
+
79
+ async function readMdDir(dir, kind, { stubsOnly = false } = {}) {
80
+ let entries;
81
+ try {
82
+ entries = await fs.readdir(dir, { withFileTypes: true });
83
+ } catch {
84
+ return [];
85
+ }
86
+ const out = [];
87
+ for (const e of entries) {
88
+ if (!e.isFile() || !e.name.endsWith('.md')) continue;
89
+ const absPath = path.join(dir, e.name);
90
+ const raw = stubsOnly
91
+ ? await readHead(absPath, STUB_READ_BYTES)
92
+ : await fs.readFile(absPath, 'utf8');
93
+ const { frontmatter, body } = splitFrontmatter(raw);
94
+ const item = {
95
+ kind,
96
+ name: e.name.replace(/\.md$/, ''),
97
+ absPath,
98
+ frontmatter,
99
+ frontmatterRaw: matchFrontmatterRaw(raw),
100
+ description: frontmatter?.description ?? firstNonEmptyLine(body),
101
+ };
102
+ if (!stubsOnly) {
103
+ item.body = body;
104
+ item.content = raw;
105
+ }
106
+ out.push(item);
107
+ }
108
+ return out.sort((a, b) => a.name.localeCompare(b.name));
109
+ }
110
+
111
+ async function readSkillsDir(dir, { stubsOnly = false } = {}) {
112
+ let entries;
113
+ try {
114
+ entries = await fs.readdir(dir, { withFileTypes: true });
115
+ } catch {
116
+ return [];
117
+ }
118
+ const out = [];
119
+ for (const e of entries) {
120
+ if (!e.isDirectory()) continue;
121
+ const skillPath = path.join(dir, e.name, 'SKILL.md');
122
+ let raw;
123
+ try {
124
+ raw = stubsOnly
125
+ ? await readHead(skillPath, STUB_READ_BYTES)
126
+ : await fs.readFile(skillPath, 'utf8');
127
+ } catch { continue; }
128
+ const { frontmatter, body } = splitFrontmatter(raw);
129
+ const item = {
130
+ kind: 'skill',
131
+ name: e.name,
132
+ absPath: skillPath,
133
+ dirPath: path.join(dir, e.name),
134
+ frontmatter,
135
+ frontmatterRaw: matchFrontmatterRaw(raw),
136
+ description: frontmatter?.description ?? firstNonEmptyLine(body),
137
+ };
138
+ if (!stubsOnly) {
139
+ item.body = body;
140
+ item.skillContent = raw;
141
+ }
142
+ out.push(item);
143
+ }
144
+ return out.sort((a, b) => a.name.localeCompare(b.name));
145
+ }
146
+
147
+ // --- minimal YAML-ish frontmatter parser (no deps) ---
148
+ // Handles `key: value`, `key: >` multiline, but NOT nested objects/arrays.
149
+ // Good enough for our SKILL.md / agent.md headers.
150
+
151
+ function splitFrontmatter(raw) {
152
+ const m = raw.match(FRONTMATTER_SPLIT_RE);
153
+ if (!m) return { frontmatter: null, body: raw };
154
+ return { frontmatter: parseLooseYaml(m[1]), body: m[2] };
155
+ }
156
+
157
+ function matchFrontmatterRaw(raw) {
158
+ const m = raw.match(FRONTMATTER_RAW_RE);
159
+ return m ? m[1] : '';
160
+ }
161
+
162
+ function parseLooseYaml(text) {
163
+ const out = {};
164
+ const lines = text.split(/\r?\n/);
165
+ let i = 0;
166
+ while (i < lines.length) {
167
+ const line = lines[i];
168
+ const m = line.match(YAML_KEY_RE);
169
+ if (!m) { i++; continue; }
170
+ const key = m[1];
171
+ let val = m[2];
172
+ if (val === '>' || val === '|') {
173
+ // Multiline: collect indented lines
174
+ const collected = [];
175
+ i++;
176
+ while (i < lines.length && /^\s+/.test(lines[i])) {
177
+ collected.push(lines[i].replace(/^\s+/, ''));
178
+ i++;
179
+ }
180
+ out[key] = collected.join(' ').trim();
181
+ continue;
182
+ }
183
+ out[key] = val.trim().replace(/^["']|["']$/g, '');
184
+ i++;
185
+ }
186
+ return out;
187
+ }
188
+
189
+ function firstNonEmptyLine(body) {
190
+ for (const line of body.split(/\r?\n/)) {
191
+ const t = line.trim();
192
+ if (!t) continue; // blank
193
+ if (t.startsWith('#')) continue; // markdown heading
194
+ if (t.startsWith('<!--')) continue; // HTML comment (e.g. STUB_MARKER)
195
+ return t.slice(0, 200);
196
+ }
197
+ return '';
198
+ }
199
+
200
+ // --- search helpers ---
201
+
202
+ export function searchKit(kit, query) {
203
+ const q = query.toLowerCase();
204
+ const all = [...kit.agents, ...kit.commands, ...kit.skills, ...kit.skillsExtras];
205
+ return all.filter(item =>
206
+ item.name.toLowerCase().includes(q) ||
207
+ (item.description ?? '').toLowerCase().includes(q)
208
+ ).map(({ kind, name, description, absPath }) => ({ kind, name, description, absPath }));
209
+ }
210
+
211
+ export function findItem(kit, kind, name) {
212
+ const buckets = { agent: kit.agents, command: kit.commands, skill: [...kit.skills, ...kit.skillsExtras] };
213
+ const b = buckets[kind];
214
+ if (!b) throw new Error(`Unknown kind: ${kind}`);
215
+ return b.find(x => x.name === name) ?? null;
216
+ }
@@ -0,0 +1,170 @@
1
+ // src/core/logger.js — JSONL append-only logger with date-based rotation.
2
+ //
3
+ // Phase 158 (v1.28): every MCP tool invocation is appended to
4
+ // ~/.kit-mcp/logs/kit-mcp-YYYY-MM-DD.log
5
+ // as a single JSON line per event. Files are rotated by calendar day; on the
6
+ // first write of a new day, old files past the retention horizon are deleted.
7
+ //
8
+ // Discipline:
9
+ // - Async fire-and-forget — must never block the MCP request handler.
10
+ // - File-only — never writes to stdout (MCP spec) and stderr only on
11
+ // last-resort init failure.
12
+ // - Cross-platform — uses os.homedir(), path.join, never POSIX-only ops.
13
+ //
14
+ // Retention is read from KIT_MCP_LOG_RETENTION_DAYS (integer, default 7).
15
+ // Set to 0 to disable rotation/cleanup entirely (keep forever).
16
+
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import os from 'node:os';
20
+
21
+ const LOG_DIR = process.env.KIT_MCP_LOG_DIR
22
+ || path.join(os.homedir(), '.kit-mcp', 'logs');
23
+
24
+ function retentionDays() {
25
+ const raw = process.env.KIT_MCP_LOG_RETENTION_DAYS;
26
+ if (raw === undefined || raw === '') return 7;
27
+ const n = parseInt(raw, 10);
28
+ return Number.isFinite(n) && n >= 0 ? n : 7;
29
+ }
30
+
31
+ function todayStamp(d = new Date()) {
32
+ // YYYY-MM-DD in local time. Rotation key — must stay stable within a day.
33
+ const y = d.getFullYear();
34
+ const m = String(d.getMonth() + 1).padStart(2, '0');
35
+ const day = String(d.getDate()).padStart(2, '0');
36
+ return `${y}-${m}-${day}`;
37
+ }
38
+
39
+ export function currentLogPath() {
40
+ return path.join(LOG_DIR, `kit-mcp-${todayStamp()}.log`);
41
+ }
42
+
43
+ export function logDir() {
44
+ return LOG_DIR;
45
+ }
46
+
47
+ let initDone = false;
48
+ let initFailed = false;
49
+
50
+ function ensureDir() {
51
+ if (initDone) return !initFailed;
52
+ initDone = true;
53
+ try {
54
+ fs.mkdirSync(LOG_DIR, { recursive: true });
55
+ return true;
56
+ } catch (e) {
57
+ initFailed = true;
58
+ // Last-resort signal to operator; stderr is allowed (stdout would corrupt
59
+ // the MCP transport).
60
+ try { console.error(`[kit-mcp logger] failed to create ${LOG_DIR}: ${e.message}`); } catch { /* noop */ }
61
+ return false;
62
+ }
63
+ }
64
+
65
+ let lastRotationCheck = '';
66
+
67
+ function rotateIfNeeded() {
68
+ const today = todayStamp();
69
+ if (lastRotationCheck === today) return;
70
+ lastRotationCheck = today;
71
+
72
+ const keep = retentionDays();
73
+ if (keep === 0) return; // keep forever
74
+
75
+ try {
76
+ const entries = fs.readdirSync(LOG_DIR);
77
+ const cutoff = new Date();
78
+ cutoff.setDate(cutoff.getDate() - keep);
79
+ const cutoffStamp = todayStamp(cutoff);
80
+ for (const name of entries) {
81
+ const m = name.match(/^kit-mcp-(\d{4}-\d{2}-\d{2})\.log$/);
82
+ if (!m) continue;
83
+ if (m[1] < cutoffStamp) {
84
+ try { fs.unlinkSync(path.join(LOG_DIR, name)); } catch { /* swallow */ }
85
+ }
86
+ }
87
+ } catch { /* swallow */ }
88
+ }
89
+
90
+ // logEvent(event) — append one JSON line. Synchronous write to a file handle
91
+ // is cheap (~µs) and avoids losing events if the process exits right after.
92
+ // All fields except `ts` are caller-supplied; we add `ts` ISO 8601.
93
+ export function logEvent(event) {
94
+ if (!ensureDir()) return;
95
+ rotateIfNeeded();
96
+ const enriched = { ts: new Date().toISOString(), pid: process.pid, ...event };
97
+ try {
98
+ fs.appendFileSync(currentLogPath(), JSON.stringify(enriched) + '\n', 'utf8');
99
+ } catch { /* swallow — logging must never break the caller */ }
100
+ }
101
+
102
+ // listLogs() — returns absolute paths of available log files, most recent first.
103
+ export function listLogs() {
104
+ if (!ensureDir()) return [];
105
+ try {
106
+ return fs.readdirSync(LOG_DIR)
107
+ .filter((n) => /^kit-mcp-\d{4}-\d{2}-\d{2}\.log$/.test(n))
108
+ .sort((a, b) => b.localeCompare(a))
109
+ .map((n) => path.join(LOG_DIR, n));
110
+ } catch {
111
+ return [];
112
+ }
113
+ }
114
+
115
+ // tailLogs({lines, follow, onLine}) — reads the last N lines across files,
116
+ // optionally tailing forever (writes to onLine as new lines arrive). Returns
117
+ // a `stop()` function when follow=true; otherwise resolves after initial read.
118
+ export function tailLogs({ lines = 50, follow = false, onLine } = {}) {
119
+ if (!onLine) throw new Error('tailLogs requires onLine callback');
120
+
121
+ const files = listLogs();
122
+ // Walk from oldest-relevant forward to collect last N lines. Approximation:
123
+ // read latest file, if it has < N lines, also read the previous one, etc.
124
+ const collected = [];
125
+ for (const file of files) {
126
+ try {
127
+ const content = fs.readFileSync(file, 'utf8');
128
+ const fileLines = content.split('\n').filter(Boolean);
129
+ collected.unshift(...fileLines);
130
+ } catch { /* skip unreadable */ }
131
+ if (collected.length >= lines) break;
132
+ }
133
+ const tail = collected.slice(-lines);
134
+ for (const line of tail) onLine(line);
135
+
136
+ if (!follow) return { stop: () => {} };
137
+
138
+ // Naive follow: poll the current-day file every 250ms for new bytes.
139
+ let offset = 0;
140
+ try {
141
+ const cur = currentLogPath();
142
+ if (fs.existsSync(cur)) offset = fs.statSync(cur).size;
143
+ } catch { /* ignore */ }
144
+ let stopped = false;
145
+ const interval = setInterval(() => {
146
+ if (stopped) return;
147
+ try {
148
+ const cur = currentLogPath();
149
+ if (!fs.existsSync(cur)) return;
150
+ const stat = fs.statSync(cur);
151
+ if (stat.size <= offset) return;
152
+ const fd = fs.openSync(cur, 'r');
153
+ const buf = Buffer.alloc(stat.size - offset);
154
+ fs.readSync(fd, buf, 0, buf.length, offset);
155
+ fs.closeSync(fd);
156
+ offset = stat.size;
157
+ const text = buf.toString('utf8');
158
+ for (const line of text.split('\n')) {
159
+ if (line) onLine(line);
160
+ }
161
+ } catch { /* swallow */ }
162
+ }, 250);
163
+
164
+ return {
165
+ stop: () => {
166
+ stopped = true;
167
+ clearInterval(interval);
168
+ },
169
+ };
170
+ }
@@ -0,0 +1,60 @@
1
+ // src/core/notify.js — opt-in OS-level notification on MCP tool call.
2
+ //
3
+ // Phase 164 (v1.28). Off by default — set KIT_MCP_NOTIFY=1 to enable.
4
+ // Cross-platform best-effort: PowerShell BurntToast/MessageBox on Windows,
5
+ // osascript on macOS, notify-send on Linux. Failure is silent.
6
+ //
7
+ // Throttled: minimum 5s between notifications (configurable via
8
+ // KIT_MCP_NOTIFY_THROTTLE_MS) to prevent flood.
9
+
10
+ import { spawn } from 'node:child_process';
11
+
12
+ const DEFAULT_THROTTLE_MS = 5000;
13
+ let lastNotifyAt = 0;
14
+
15
+ function throttleMs() {
16
+ const raw = process.env.KIT_MCP_NOTIFY_THROTTLE_MS;
17
+ const n = parseInt(raw, 10);
18
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_THROTTLE_MS;
19
+ }
20
+
21
+ export function isNotifyEnabled() {
22
+ return process.env.KIT_MCP_NOTIFY === '1' || process.env.KIT_MCP_NOTIFY === 'true';
23
+ }
24
+
25
+ function spawnDetached(cmd, args) {
26
+ try {
27
+ const c = spawn(cmd, args, { detached: true, stdio: 'ignore', windowsHide: true });
28
+ c.unref();
29
+ } catch { /* swallow — notification must never break the server */ }
30
+ }
31
+
32
+ // notify({title, body}): fire a best-effort OS notification. Throttled.
33
+ export function notify({ title, body } = {}) {
34
+ if (!isNotifyEnabled()) return;
35
+ const now = Date.now();
36
+ if (now - lastNotifyAt < throttleMs()) return;
37
+ lastNotifyAt = now;
38
+
39
+ const t = (title || 'kit-mcp').replace(/"/g, "'");
40
+ const b = (body || '').replace(/"/g, "'");
41
+
42
+ if (process.platform === 'darwin') {
43
+ spawnDetached('osascript', ['-e', `display notification "${b}" with title "${t}"`]);
44
+ } else if (process.platform === 'linux') {
45
+ spawnDetached('notify-send', [t, b]);
46
+ } else if (process.platform === 'win32') {
47
+ // PowerShell toast via Windows.UI.Notifications. Falls back silently if
48
+ // BurntToast not installed — we use the simpler msg via WScript.Shell.
49
+ const ps = `Add-Type -AssemblyName System.Windows.Forms; `
50
+ + `$n = New-Object System.Windows.Forms.NotifyIcon; `
51
+ + `$n.Icon = [System.Drawing.SystemIcons]::Information; `
52
+ + `$n.BalloonTipTitle = "${t}"; `
53
+ + `$n.BalloonTipText = "${b}"; `
54
+ + `$n.Visible = $true; `
55
+ + `$n.ShowBalloonTip(3000); `
56
+ + `Start-Sleep -Seconds 4; `
57
+ + `$n.Dispose()`;
58
+ spawnDetached('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', ps]);
59
+ }
60
+ }