@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.
- package/LICENSE +21 -21
- package/README.md +168 -914
- package/gates/agent-no-recursive-dispatch.md +45 -11
- package/kit/COMANDOS.md +138 -138
- package/kit/README.md +76 -76
- package/kit/agents/advisor-researcher.md +106 -106
- package/kit/agents/assumptions-analyzer.md +107 -107
- package/kit/agents/audit-log-implementer.md +1 -1
- package/kit/agents/auditor-consistencia-isolamento.md +1 -1
- package/kit/agents/b2b-saas-architect.md +1 -1
- package/kit/agents/cascading-failures-auditor.md +1 -1
- package/kit/agents/codebase-mapper.md +768 -768
- package/kit/agents/crm-pipeline-implementer.md +1 -1
- package/kit/agents/debugger.md +813 -813
- package/kit/agents/detector-tenant-quente.md +1 -1
- package/kit/agents/evolution-go-integrator.md +1 -1
- package/kit/agents/example-reviewer.md +21 -21
- package/kit/agents/executor.md +564 -564
- package/kit/agents/integration-checker.md +200 -200
- package/kit/agents/invite-flow-implementer.md +1 -1
- package/kit/agents/legacy-characterizer.md +1 -1
- package/kit/agents/lgpd-compliance-auditor.md +1 -1
- package/kit/agents/multi-tenant-isolation-auditor.md +1 -1
- package/kit/agents/multi-tenant-rls-writer.md +1 -1
- package/kit/agents/nyquist-auditor.md +178 -178
- package/kit/agents/observability-coverage-auditor.md +1 -1
- package/kit/agents/org-onboarding-implementer.md +1 -1
- package/kit/agents/payload-capture-instrumenter.md +1 -1
- package/kit/agents/phase-researcher.md +696 -696
- package/kit/agents/plan-checker.md +272 -272
- package/kit/agents/planner.md +922 -922
- package/kit/agents/project-researcher.md +652 -652
- package/kit/agents/refactor-safety-auditor.md +1 -1
- package/kit/agents/research-synthesizer.md +245 -245
- package/kit/agents/roadmapper.md +677 -677
- package/kit/agents/seam-finder.md +1 -1
- package/kit/agents/shotgun-surgery-detector.md +1 -1
- package/kit/agents/supabase-branching-architect.md +1 -1
- package/kit/agents/supabase-cicd-pipeline-implementer.md +1 -1
- package/kit/agents/supabase-column-privileges-writer.md +1 -1
- package/kit/agents/supabase-migration-writer.md +1 -1
- package/kit/agents/supabase-rbac-implementer.md +1 -1
- package/kit/agents/supabase-rls-hardener.md +1 -1
- package/kit/agents/supabase-rls-writer.md +1 -1
- package/kit/agents/supabase-roles-implementer.md +1 -1
- package/kit/agents/super-admin-implementer.md +1 -1
- package/kit/agents/ui-auditor.md +437 -437
- package/kit/agents/ui-checker.md +302 -302
- package/kit/agents/ui-researcher.md +355 -355
- package/kit/agents/user-profiler.md +175 -175
- package/kit/agents/validador-evolucao-schema.md +1 -1
- package/kit/agents/verifier.md +728 -728
- package/kit/commands/adicionar-backlog.md +75 -75
- package/kit/commands/adicionar-fase.md +42 -42
- package/kit/commands/adicionar-tarefa.md +45 -45
- package/kit/commands/adicionar-testes.md +41 -41
- package/kit/commands/ajuda.md +21 -21
- package/kit/commands/atualizar.md +37 -37
- package/kit/commands/auditar-cascading.md +1 -1
- package/kit/commands/auditar-marco.md +179 -179
- package/kit/commands/auditar-observabilidade-cobertura.md +1 -1
- package/kit/commands/auditar-refactor.md +1 -1
- package/kit/commands/auditar-release.md +1 -1
- package/kit/commands/auditar-uat.md +23 -23
- package/kit/commands/autonomo.md +40 -40
- package/kit/commands/branch-pr.md +24 -24
- package/kit/commands/burn-rate-status.md +1 -1
- package/kit/commands/capturar-payloads.md +1 -1
- package/kit/commands/caracterizar.md +1 -1
- package/kit/commands/concluir-marco.md +247 -247
- package/kit/commands/configuracoes.md +36 -36
- package/kit/commands/dados-distribuidos.md +1 -1
- package/kit/commands/definir-perfil.md +10 -10
- package/kit/commands/depurar.md +190 -190
- package/kit/commands/detectar-duplicacao.md +1 -1
- package/kit/commands/discutir-fase.md +131 -131
- package/kit/commands/encontrar-seams.md +1 -1
- package/kit/commands/entrar-discord.md +17 -17
- package/kit/commands/estatisticas.md +18 -18
- package/kit/commands/example-greeting.md +33 -33
- package/kit/commands/executar-fase.md +58 -58
- package/kit/commands/expresso.md +56 -56
- package/kit/commands/fase-ui.md +34 -34
- package/kit/commands/fazer.md +57 -57
- package/kit/commands/fio.md +125 -125
- package/kit/commands/fluxos-trabalho.md +64 -64
- package/kit/commands/forense.md +176 -176
- package/kit/commands/gerenciador.md +38 -38
- package/kit/commands/inserir-fase.md +31 -31
- package/kit/commands/legacy.md +1 -1
- package/kit/commands/limpeza.md +17 -17
- package/kit/commands/listar-hipoteses-fase.md +45 -45
- package/kit/commands/listar-workspaces.md +18 -18
- package/kit/commands/load-shedding.md +1 -1
- package/kit/commands/mapear-codebase.md +70 -70
- package/kit/commands/multi-tenant.md +1 -1
- package/kit/commands/nota.md +33 -33
- package/kit/commands/novo-marco.md +43 -43
- package/kit/commands/novo-projeto.md +41 -41
- package/kit/commands/novo-workspace.md +43 -43
- package/kit/commands/pausar-trabalho.md +37 -37
- package/kit/commands/perfil-usuario.md +45 -45
- package/kit/commands/pesquisar-fase.md +195 -195
- package/kit/commands/planejar-fase.md +67 -67
- package/kit/commands/planejar-lacunas.md +33 -33
- package/kit/commands/plantar-ideia.md +25 -25
- package/kit/commands/progresso.md +24 -24
- package/kit/commands/proximo.md +30 -30
- package/kit/commands/publicar.md +490 -490
- package/kit/commands/rapido.md +35 -35
- package/kit/commands/reaplicar-patches.md +124 -124
- package/kit/commands/refactor-seguro.md +1 -1
- package/kit/commands/relatorio-sessao.md +19 -19
- package/kit/commands/remover-fase.md +31 -31
- package/kit/commands/remover-workspace.md +26 -26
- package/kit/commands/resumo-marco.md +50 -50
- package/kit/commands/retomar-trabalho.md +40 -40
- package/kit/commands/revisar-backlog.md +60 -60
- package/kit/commands/revisar-ui.md +32 -32
- package/kit/commands/revisar.md +37 -37
- package/kit/commands/saude.md +21 -21
- package/kit/commands/setup-notion.md +93 -93
- package/kit/commands/storytelling.md +1 -1
- package/kit/commands/supabase.md +1 -1
- package/kit/commands/sync-main.md +68 -68
- package/kit/commands/validar-fase.md +35 -35
- package/kit/commands/verificar-tarefas.md +44 -44
- package/kit/commands/verificar-trabalho.md +64 -64
- package/kit/file-manifest.json +90 -90
- package/kit/framework/bin/lib/commands.cjs +959 -959
- package/kit/framework/bin/lib/config.cjs +442 -442
- package/kit/framework/bin/lib/core.cjs +1230 -1230
- package/kit/framework/bin/lib/frontmatter.cjs +336 -336
- package/kit/framework/bin/lib/init.cjs +1442 -1442
- package/kit/framework/bin/lib/milestone.cjs +252 -252
- package/kit/framework/bin/lib/model-profiles.cjs +68 -68
- package/kit/framework/bin/lib/phase.cjs +888 -888
- package/kit/framework/bin/lib/profile-output.cjs +952 -952
- package/kit/framework/bin/lib/profile-pipeline.cjs +539 -539
- package/kit/framework/bin/lib/roadmap.cjs +329 -329
- package/kit/framework/bin/lib/security.cjs +382 -382
- package/kit/framework/bin/lib/state.cjs +1031 -1031
- package/kit/framework/bin/lib/template.cjs +222 -222
- package/kit/framework/bin/lib/uat.cjs +282 -282
- package/kit/framework/bin/lib/verify.cjs +888 -888
- package/kit/framework/bin/lib/workstream.cjs +491 -491
- package/kit/framework/bin/tools.cjs +918 -918
- package/kit/framework/commands/workstreams.md +63 -63
- package/kit/framework/references/checkpoints.md +778 -778
- package/kit/framework/references/continuation-format.md +249 -249
- package/kit/framework/references/decimal-phase-calculation.md +64 -64
- package/kit/framework/references/git-integration.md +295 -295
- package/kit/framework/references/git-planning-commit.md +38 -38
- package/kit/framework/references/model-profile-resolution.md +36 -36
- package/kit/framework/references/model-profiles.md +139 -139
- package/kit/framework/references/phase-argument-parsing.md +61 -61
- package/kit/framework/references/planning-config.md +202 -202
- package/kit/framework/references/questioning.md +162 -162
- package/kit/framework/references/tdd.md +263 -263
- package/kit/framework/references/ui-brand.md +160 -160
- package/kit/framework/references/user-profiling.md +657 -657
- package/kit/framework/references/verification-patterns.md +612 -612
- package/kit/framework/references/workstream-flag.md +58 -58
- package/kit/framework/templates/DEBUG.md +164 -164
- package/kit/framework/templates/UAT.md +265 -265
- package/kit/framework/templates/UI-SPEC.md +100 -100
- package/kit/framework/templates/VALIDATION.md +76 -76
- package/kit/framework/templates/claude-md.md +122 -122
- package/kit/framework/templates/codebase/architecture.md +185 -185
- package/kit/framework/templates/codebase/concerns.md +205 -205
- package/kit/framework/templates/codebase/conventions.md +204 -204
- package/kit/framework/templates/codebase/integrations.md +192 -192
- package/kit/framework/templates/codebase/stack.md +158 -158
- package/kit/framework/templates/codebase/structure.md +199 -199
- package/kit/framework/templates/codebase/testing.md +301 -301
- package/kit/framework/templates/config.json +44 -44
- package/kit/framework/templates/context.md +352 -352
- package/kit/framework/templates/continue-here.md +78 -78
- package/kit/framework/templates/copilot-instructions.md +7 -7
- package/kit/framework/templates/debug-subagent-prompt.md +91 -91
- package/kit/framework/templates/dev-preferences.md +20 -20
- package/kit/framework/templates/discovery.md +146 -146
- package/kit/framework/templates/discussion-log.md +63 -63
- package/kit/framework/templates/milestone-archive.md +123 -123
- package/kit/framework/templates/milestone.md +115 -115
- package/kit/framework/templates/phase-prompt.md +610 -610
- package/kit/framework/templates/planner-subagent-prompt.md +117 -117
- package/kit/framework/templates/project.md +186 -186
- package/kit/framework/templates/requirements.md +231 -231
- package/kit/framework/templates/research-project/ARCHITECTURE.md +204 -204
- package/kit/framework/templates/research-project/FEATURES.md +147 -147
- package/kit/framework/templates/research-project/PITFALLS.md +200 -200
- package/kit/framework/templates/research-project/STACK.md +120 -120
- package/kit/framework/templates/research-project/SUMMARY.md +170 -170
- package/kit/framework/templates/research.md +419 -419
- package/kit/framework/templates/retrospective.md +54 -54
- package/kit/framework/templates/roadmap.md +202 -202
- package/kit/framework/templates/state.md +176 -176
- package/kit/framework/templates/summary-complex.md +59 -59
- package/kit/framework/templates/summary-minimal.md +41 -41
- package/kit/framework/templates/summary-standard.md +48 -48
- package/kit/framework/templates/summary.md +209 -209
- package/kit/framework/templates/user-profile.md +146 -146
- package/kit/framework/templates/user-setup.md +256 -256
- package/kit/framework/templates/verification-report.md +258 -258
- package/kit/framework/workflows/add-phase.md +112 -112
- package/kit/framework/workflows/add-tests.md +351 -351
- package/kit/framework/workflows/add-todo.md +158 -158
- package/kit/framework/workflows/audit-milestone.md +340 -340
- package/kit/framework/workflows/audit-uat.md +109 -109
- package/kit/framework/workflows/autonomous.md +891 -891
- package/kit/framework/workflows/check-todos.md +177 -177
- package/kit/framework/workflows/cleanup.md +152 -152
- package/kit/framework/workflows/complete-milestone.md +696 -696
- package/kit/framework/workflows/diagnose-issues.md +231 -231
- package/kit/framework/workflows/discovery-phase.md +289 -289
- package/kit/framework/workflows/discuss-phase-assumptions.md +653 -653
- package/kit/framework/workflows/discuss-phase.md +784 -784
- package/kit/framework/workflows/do.md +104 -104
- package/kit/framework/workflows/execute-phase.md +838 -838
- package/kit/framework/workflows/execute-plan.md +510 -510
- package/kit/framework/workflows/fast.md +102 -102
- package/kit/framework/workflows/forensics.md +265 -265
- package/kit/framework/workflows/health.md +181 -181
- package/kit/framework/workflows/help.md +619 -619
- package/kit/framework/workflows/insert-phase.md +130 -130
- package/kit/framework/workflows/list-phase-assumptions.md +178 -178
- package/kit/framework/workflows/list-workspaces.md +56 -56
- package/kit/framework/workflows/manager.md +362 -362
- package/kit/framework/workflows/map-codebase.md +377 -377
- package/kit/framework/workflows/milestone-summary.md +223 -223
- package/kit/framework/workflows/new-milestone.md +486 -486
- package/kit/framework/workflows/new-project.md +1159 -1159
- package/kit/framework/workflows/new-workspace.md +237 -237
- package/kit/framework/workflows/next.md +97 -97
- package/kit/framework/workflows/node-repair.md +92 -92
- package/kit/framework/workflows/note.md +156 -156
- package/kit/framework/workflows/pause-work.md +176 -176
- package/kit/framework/workflows/plan-milestone-gaps.md +273 -273
- package/kit/framework/workflows/plan-phase.md +765 -765
- package/kit/framework/workflows/plant-seed.md +169 -169
- package/kit/framework/workflows/pr-branch.md +129 -129
- package/kit/framework/workflows/profile-user.md +450 -450
- package/kit/framework/workflows/progress.md +507 -507
- package/kit/framework/workflows/quick.md +757 -757
- package/kit/framework/workflows/remove-phase.md +155 -155
- package/kit/framework/workflows/remove-workspace.md +90 -90
- package/kit/framework/workflows/research-phase.md +82 -82
- package/kit/framework/workflows/resume-project.md +326 -326
- package/kit/framework/workflows/review.md +228 -228
- package/kit/framework/workflows/session-report.md +146 -146
- package/kit/framework/workflows/settings.md +283 -283
- package/kit/framework/workflows/ship.md +228 -228
- package/kit/framework/workflows/stats.md +60 -60
- package/kit/framework/workflows/transition.md +671 -671
- package/kit/framework/workflows/ui-phase.md +302 -302
- package/kit/framework/workflows/ui-review.md +165 -165
- package/kit/framework/workflows/update.md +323 -323
- package/kit/framework/workflows/validate-phase.md +174 -174
- package/kit/framework/workflows/verify-phase.md +252 -252
- package/kit/framework/workflows/verify-work.md +637 -637
- package/kit/hooks/check-update.js +118 -118
- package/kit/hooks/context-monitor.js +163 -163
- package/kit/hooks/prompt-guard.js +103 -103
- package/kit/hooks/statusline.js +125 -125
- package/kit/hooks/workflow-guard.js +101 -101
- package/kit/settings.json +45 -45
- package/kit/skills/ai-prompt-characterization/SKILL.md +1 -1
- package/kit/skills/armadilhas-sistemas-distribuidos/SKILL.md +1 -1
- package/kit/skills/audit-log-multi-tenant/SKILL.md +1 -1
- package/kit/skills/b2b-saas-architecture/SKILL.md +1 -1
- package/kit/skills/consistencia-leitura-replica/SKILL.md +1 -1
- package/kit/skills/crm-lead-pipeline-patterns/SKILL.md +1 -1
- package/kit/skills/escolha-modelo-consistencia/SKILL.md +1 -1
- package/kit/skills/evolucao-schema-compativel/SKILL.md +1 -1
- package/kit/skills/evolution-go-whatsapp-integration/SKILL.md +1 -1
- package/kit/skills/example-skill/SKILL.md +42 -42
- package/kit/skills/legacy-api-only-applications/SKILL.md +1 -1
- package/kit/skills/legacy-characterization-tests/SKILL.md +1 -1
- package/kit/skills/legacy-effect-analysis/SKILL.md +1 -1
- package/kit/skills/legacy-extract-class/SKILL.md +1 -1
- package/kit/skills/legacy-programming-by-difference/SKILL.md +1 -1
- package/kit/skills/legacy-seams-and-test-harness/SKILL.md +1 -1
- package/kit/skills/legacy-shotgun-surgery/SKILL.md +1 -1
- package/kit/skills/legacy-sprout-wrap-techniques/SKILL.md +1 -1
- package/kit/skills/legacy-storytelling-naked-crc/SKILL.md +1 -1
- package/kit/skills/lgpd-multi-tenant-compliance/SKILL.md +1 -1
- package/kit/skills/member-invite-flow/SKILL.md +1 -1
- package/kit/skills/member-management-react-shadcn/SKILL.md +1 -1
- package/kit/skills/multi-tenant-performance-scaling/SKILL.md +1 -1
- package/kit/skills/multi-tenant-rls-hierarchy/SKILL.md +1 -1
- package/kit/skills/org-onboarding-flow/SKILL.md +1 -1
- package/kit/skills/org-switcher-react-pattern/SKILL.md +1 -1
- package/kit/skills/permission-gate-react-pattern/SKILL.md +1 -1
- package/kit/skills/postgres-isolamento-concorrencia/SKILL.md +1 -1
- package/kit/skills/pre-refactor-characterization/SKILL.md +1 -1
- package/kit/skills/rbac-permissions-matrix-supabase/SKILL.md +1 -1
- package/kit/skills/streams-eventos-cdc/SKILL.md +1 -1
- package/kit/skills/supabase-branching-workflow/SKILL.md +1 -1
- package/kit/skills/supabase-ci-cd-github-actions/SKILL.md +1 -1
- package/kit/skills/supabase-column-level-security/SKILL.md +1 -1
- package/kit/skills/supabase-config-toml-remotes/SKILL.md +1 -1
- package/kit/skills/supabase-custom-claims-rbac/SKILL.md +1 -1
- package/kit/skills/supabase-migration-repair/SKILL.md +1 -1
- package/kit/skills/supabase-migrations/SKILL.md +1 -1
- package/kit/skills/supabase-pgtap-testing/SKILL.md +1 -1
- package/kit/skills/supabase-postgres-roles/SKILL.md +1 -1
- package/kit/skills/supabase-rls-defense-in-depth/SKILL.md +1 -1
- package/kit/skills/supabase-rls-policies/SKILL.md +1 -1
- package/kit/skills/super-admin-platform-pattern/SKILL.md +1 -1
- package/kit/skills/tenant-quente-mitigacao/SKILL.md +1 -1
- package/kit/skills/whatsapp-conversation-state-machine/SKILL.md +1 -1
- package/package.json +63 -63
- package/src/cli/index.js +378 -6
- package/src/cli/render.js +7 -0
- package/src/core/kit.js +216 -216
- package/src/core/logger.js +170 -0
- package/src/core/notify.js +60 -0
- package/src/core/reflect.js +247 -247
- package/src/core/reverse-sync.js +372 -372
- package/src/core/sync.js +418 -418
- package/src/core/watch.js +121 -121
- package/src/mcp-server/index.js +276 -10
- 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
|
+
}
|