@luanpdd/kit-mcp 1.18.0 → 1.20.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 +648 -648
- package/kit/COMANDOS.md +138 -138
- package/kit/README.md +52 -52
- package/kit/agents/advisor-researcher.md +106 -106
- package/kit/agents/assumptions-analyzer.md +107 -107
- package/kit/agents/codebase-mapper.md +768 -768
- package/kit/agents/debugger.md +772 -772
- package/kit/agents/example-reviewer.md +21 -21
- package/kit/agents/executor.md +523 -523
- package/kit/agents/integration-checker.md +200 -200
- package/kit/agents/nyquist-auditor.md +178 -178
- package/kit/agents/phase-researcher.md +696 -696
- package/kit/agents/plan-checker.md +272 -272
- package/kit/agents/planner.md +891 -891
- package/kit/agents/project-researcher.md +652 -652
- package/kit/agents/research-synthesizer.md +245 -245
- package/kit/agents/roadmapper.md +677 -677
- 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/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-marco.md +179 -179
- 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 +338 -70
- package/kit/commands/concluir-marco.md +247 -247
- package/kit/commands/configuracoes.md +36 -36
- package/kit/commands/definir-perfil.md +10 -10
- package/kit/commands/depurar.md +190 -190
- package/kit/commands/discutir-fase.md +131 -131
- 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/limpeza.md +17 -17
- package/kit/commands/listar-hipoteses-fase.md +45 -45
- package/kit/commands/listar-workspaces.md +18 -18
- package/kit/commands/mapear-codebase.md +70 -70
- 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/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/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 +3 -3
- 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/example-skill/SKILL.md +42 -42
- package/package.json +63 -59
- package/src/core/kit.js +216 -216
- package/src/core/metrics.js +135 -10
- 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 +34 -3
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
|
+
}
|
package/src/core/metrics.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// OBS-18-01 / OBS-18-02 — in-memory golden signals for kit-mcp server.
|
|
2
|
+
// OBS-19-01 / OBS-19-02 / OBS-19-03 — disk-persistent rolling snapshots.
|
|
2
3
|
//
|
|
3
4
|
// Phase 94: Eat Your Own Dog Food. The skill `four-golden-signals` says any
|
|
4
5
|
// user-facing service worth its salt instruments Latency + Traffic + Errors
|
|
@@ -7,31 +8,48 @@
|
|
|
7
8
|
// operator wants when something feels off.
|
|
8
9
|
//
|
|
9
10
|
// Scope decisions (see .planning/phases/94-golden-signals-mcp-server/94-CONTEXT.md):
|
|
10
|
-
// - Zero dependencies.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
11
|
+
// - Zero new dependencies. Phase 99 adds fs/promises + path from stdlib only —
|
|
12
|
+
// the 6-deps budget Phase 92.01 fought to maintain and Phase 93.01 enforces
|
|
13
|
+
// in CI is preserved.
|
|
14
|
+
// - In-memory primary, on-demand persistence. The Map+array core stays
|
|
15
|
+
// in-memory; persistSnapshot writes a JSON file under .planning/metrics/
|
|
16
|
+
// snapshots/ when called. No background timer, no implicit writes — the
|
|
17
|
+
// /burn-rate-status command and metrics-snapshot tool are the writers.
|
|
16
18
|
// - Bounded memory. Histograms cap at HISTOGRAM_CAP=1000 samples per tool
|
|
17
|
-
// with FIFO drop.
|
|
18
|
-
//
|
|
19
|
-
//
|
|
19
|
+
// with FIFO drop.
|
|
20
|
+
// - Bounded disk. cleanupOldSnapshots prunes files > 30 days old on every
|
|
21
|
+
// persistSnapshot call (rolling window, no separate retention job).
|
|
20
22
|
// - Snapshot is read-only. Returns a fresh plain-object copy so callers
|
|
21
23
|
// can JSON.stringify it without exposing internal Map references.
|
|
24
|
+
// - Persisted shape includes `ts` (epoch ms) inside the JSON. We do NOT
|
|
25
|
+
// parse the filename for windowing — filesystem-safe ISO encoding
|
|
26
|
+
// (`replace(/[:.]/g, '-')`) is one-way (cannot reliably round-trip back
|
|
27
|
+
// through Date.parse) and mtime is unreliable across copy/touch. The
|
|
28
|
+
// in-file ts is authoritative.
|
|
22
29
|
//
|
|
23
|
-
// API surface (
|
|
30
|
+
// API surface (5 exports + 2 async):
|
|
24
31
|
// incrementInvocation(tool, status) — counter++ keyed `${tool}:${status}`
|
|
25
32
|
// recordLatency(tool, ms) — push to histogram, FIFO at cap
|
|
26
33
|
// snapshot() — { counters, latency } plain object
|
|
27
34
|
// reset() — clear both maps; called on boot if
|
|
28
35
|
// KIT_MCP_METRICS_RESET=1
|
|
36
|
+
// persistSnapshot(rootDir) — write {ts, counters, latency} to
|
|
37
|
+
// .planning/metrics/snapshots/<ts>.json
|
|
38
|
+
// + cleanup files > 30d
|
|
39
|
+
// loadSnapshots(rootDir, windowMs) — read all snapshots whose in-file ts
|
|
40
|
+
// is within windowMs (default 30d),
|
|
41
|
+
// sorted ascending by ts
|
|
29
42
|
//
|
|
30
43
|
// Boot-time reset honors the env var by calling reset() at module load when
|
|
31
44
|
// the flag is set. This keeps the signal "fresh" for a probe in tests or for
|
|
32
45
|
// an operator who spawned the server with the flag for a clean comparison.
|
|
33
46
|
|
|
47
|
+
import fs from 'node:fs/promises';
|
|
48
|
+
import path from 'node:path';
|
|
49
|
+
|
|
34
50
|
const HISTOGRAM_CAP = 1000;
|
|
51
|
+
const DEFAULT_RETENTION_MS = 30 * 86400 * 1000; // 30 days rolling.
|
|
52
|
+
const SNAPSHOT_DIR_REL = path.join('.planning', 'metrics', 'snapshots');
|
|
35
53
|
|
|
36
54
|
const counters = new Map(); // key: `${tool}:${status}` → count (number)
|
|
37
55
|
const histograms = new Map(); // key: tool → number[] (length ≤ HISTOGRAM_CAP)
|
|
@@ -130,6 +148,112 @@ export function reset() {
|
|
|
130
148
|
histograms.clear();
|
|
131
149
|
}
|
|
132
150
|
|
|
151
|
+
/**
|
|
152
|
+
* OBS-19-01 — Persist the current snapshot to disk under
|
|
153
|
+
* `<rootDir>/.planning/metrics/snapshots/<timestamp>.json`. Runs the rolling
|
|
154
|
+
* cleanup of files older than `retentionMs` (default 30d) on every call so
|
|
155
|
+
* callers don't need a separate retention job.
|
|
156
|
+
*
|
|
157
|
+
* The on-disk shape is `{ ts: <epoch_ms>, counters, latency }`. The `ts` field
|
|
158
|
+
* inside the JSON — NOT the filename — is the authoritative timestamp for
|
|
159
|
+
* loadSnapshots windowing. The filename uses an ISO encoding with `:` and `.`
|
|
160
|
+
* replaced by `-` for filesystem safety; that encoding is one-way (cannot
|
|
161
|
+
* round-trip back through Date.parse), so we never parse it for ordering.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} [rootDir=process.cwd()] Project root. Snapshots land under
|
|
164
|
+
* `<rootDir>/.planning/metrics/snapshots/`.
|
|
165
|
+
* @param {object} [opts]
|
|
166
|
+
* @param {number} [opts.retentionMs] Override the rolling-window age in ms.
|
|
167
|
+
* Defaults to 30 days. Tests use shorter windows to drive the cleanup path.
|
|
168
|
+
* @returns {Promise<{file: string, snap: {ts: number, counters: object, latency: object}}>}
|
|
169
|
+
*/
|
|
170
|
+
export async function persistSnapshot(rootDir = process.cwd(), opts = {}) {
|
|
171
|
+
const retentionMs = Number.isFinite(opts.retentionMs) ? opts.retentionMs : DEFAULT_RETENTION_MS;
|
|
172
|
+
const dir = path.join(rootDir, SNAPSHOT_DIR_REL);
|
|
173
|
+
await fs.mkdir(dir, { recursive: true });
|
|
174
|
+
const ts = Date.now();
|
|
175
|
+
const snap = { ts, ...snapshot() };
|
|
176
|
+
// Filesystem-safe ISO encoding — Windows forbids `:` in paths and `.` is
|
|
177
|
+
// ambiguous with extension separators on shells with brace expansion.
|
|
178
|
+
const isoSafe = new Date(ts).toISOString().replace(/[:.]/g, '-');
|
|
179
|
+
const file = path.join(dir, `${isoSafe}.json`);
|
|
180
|
+
await fs.writeFile(file, JSON.stringify(snap, null, 2));
|
|
181
|
+
await cleanupOldSnapshots(dir, retentionMs);
|
|
182
|
+
return { file, snap };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* OBS-19-02 — Load all snapshots from disk whose in-file `ts` is within the
|
|
187
|
+
* sliding window. Returns the array sorted ascending by `ts` so consumers
|
|
188
|
+
* (`/burn-rate-status`) can compute first-vs-last deltas without re-sorting.
|
|
189
|
+
*
|
|
190
|
+
* Defensive against malformed JSON: a corrupt file is skipped silently rather
|
|
191
|
+
* than aborting the whole load. The 30d window is rolling from "now" — pass a
|
|
192
|
+
* smaller value to drive recent-only views (e.g. `60 * 60 * 1000` for last
|
|
193
|
+
* hour) when computing burn rate over a baseline window.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} [rootDir=process.cwd()] Project root.
|
|
196
|
+
* @param {number} [windowMs] Sliding window in ms. Defaults to 30 days.
|
|
197
|
+
* @returns {Promise<Array<{ts: number, counters: object, latency: object}>>}
|
|
198
|
+
* Empty array if the snapshots directory does not exist.
|
|
199
|
+
*/
|
|
200
|
+
export async function loadSnapshots(rootDir = process.cwd(), windowMs = DEFAULT_RETENTION_MS) {
|
|
201
|
+
const dir = path.join(rootDir, SNAPSHOT_DIR_REL);
|
|
202
|
+
const cutoff = Date.now() - windowMs;
|
|
203
|
+
let files;
|
|
204
|
+
try {
|
|
205
|
+
files = await fs.readdir(dir);
|
|
206
|
+
} catch {
|
|
207
|
+
return []; // Dir absent on first run — not an error.
|
|
208
|
+
}
|
|
209
|
+
const results = [];
|
|
210
|
+
for (const f of files) {
|
|
211
|
+
if (!f.endsWith('.json')) continue;
|
|
212
|
+
try {
|
|
213
|
+
const raw = await fs.readFile(path.join(dir, f), 'utf-8');
|
|
214
|
+
const parsed = JSON.parse(raw);
|
|
215
|
+
if (Number.isFinite(parsed?.ts) && parsed.ts >= cutoff) {
|
|
216
|
+
results.push(parsed);
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
// Corrupt file — skip silently rather than break the whole burn-rate
|
|
220
|
+
// calculation. A future phase can surface counts via a doctor probe.
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return results.sort((a, b) => a.ts - b.ts);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* OBS-19-03 — Internal helper: delete snapshot files older than `maxAgeMs`.
|
|
228
|
+
* Called from persistSnapshot on every write so retention is implicit.
|
|
229
|
+
* Uses fs.stat().mtimeMs as the age proxy; we accept the small drift versus
|
|
230
|
+
* the in-file `ts` because cleanup is best-effort eviction, not authoritative
|
|
231
|
+
* windowing (loadSnapshots reads the in-file ts).
|
|
232
|
+
*
|
|
233
|
+
* @param {string} dir Absolute path to the snapshots directory.
|
|
234
|
+
* @param {number} maxAgeMs Files with mtime older than this are unlinked.
|
|
235
|
+
* @returns {Promise<void>}
|
|
236
|
+
*/
|
|
237
|
+
async function cleanupOldSnapshots(dir, maxAgeMs) {
|
|
238
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
239
|
+
let files;
|
|
240
|
+
try {
|
|
241
|
+
files = await fs.readdir(dir);
|
|
242
|
+
} catch {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
for (const f of files) {
|
|
246
|
+
if (!f.endsWith('.json')) continue;
|
|
247
|
+
const fp = path.join(dir, f);
|
|
248
|
+
try {
|
|
249
|
+
const stat = await fs.stat(fp);
|
|
250
|
+
if (stat.mtimeMs < cutoff) await fs.unlink(fp);
|
|
251
|
+
} catch {
|
|
252
|
+
// Unlink can race with concurrent cleanup; ignore ENOENT and friends.
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
133
257
|
// Boot-time reset honors KIT_MCP_METRICS_RESET=1. We call reset() instead of
|
|
134
258
|
// merely skipping init because the maps are already empty at module load —
|
|
135
259
|
// the call is a no-op today but documents the contract for any future module
|
|
@@ -141,3 +265,4 @@ if (process.env.KIT_MCP_METRICS_RESET === '1') {
|
|
|
141
265
|
// Exported for tests only — keeps the API surface explicit while letting unit
|
|
142
266
|
// tests assert on the FIFO behavior at the boundary.
|
|
143
267
|
export const __TEST_HISTOGRAM_CAP = HISTOGRAM_CAP;
|
|
268
|
+
export const __TEST_SNAPSHOT_DIR_REL = SNAPSHOT_DIR_REL;
|