@luanpdd/kit-mcp 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/kit/agents/advisor-researcher.md +1 -14
- package/kit/agents/assumptions-analyzer.md +1 -14
- package/kit/agents/codebase-mapper.md +1 -14
- package/kit/agents/debugger.md +1 -19
- package/kit/agents/executor.md +1 -18
- package/kit/agents/integration-checker.md +1 -16
- package/kit/agents/nyquist-auditor.md +1 -16
- package/kit/agents/phase-researcher.md +1 -14
- package/kit/agents/plan-checker.md +1 -16
- package/kit/agents/planner.md +1 -16
- package/kit/agents/project-researcher.md +1 -14
- package/kit/agents/research-synthesizer.md +1 -9
- package/kit/agents/roadmapper.md +1 -14
- package/kit/agents/ui-auditor.md +1 -16
- package/kit/agents/ui-checker.md +1 -16
- package/kit/agents/ui-researcher.md +1 -14
- package/kit/agents/user-profiler.md +1 -9
- package/kit/agents/verifier.md +1 -16
- package/kit/commands/expresso.md +9 -0
- package/kit/commands/fazer.md +17 -4
- package/kit/commands/proximo.md +7 -0
- package/kit/commands/rapido.md +6 -0
- package/kit/framework/references/output-style.md +22 -0
- package/kit/framework/workflows/discuss-phase.md +47 -331
- package/kit/framework/workflows/help.md +14 -1
- package/kit/framework/workflows/new-project.md +16 -107
- package/kit/framework/workflows/plan-phase.md +28 -147
- package/package.json +1 -1
- package/src/cli/index.js +179 -0
- package/src/cli/upgrade-check.js +135 -0
- package/src/core/gates.js +15 -1
- package/src/core/kit.js +55 -22
- package/src/core/sync.js +3 -1
|
@@ -234,43 +234,15 @@ Se "Executar discuss-phase primeiro":
|
|
|
234
234
|
|
|
235
235
|
## 5. Tratar Pesquisa
|
|
236
236
|
|
|
237
|
-
**Pular se:**
|
|
237
|
+
**Pular se:** `--gaps`, `--skip-research`, ou `--reviews`.
|
|
238
238
|
|
|
239
|
-
**Se `has_research`
|
|
239
|
+
**Se `has_research` true e sem `--research`:** usar existente, ir pra passo 6.
|
|
240
240
|
|
|
241
|
-
**Se
|
|
241
|
+
**Se ausente OR `--research`:** sem flag explícita e sem `--auto`, perguntar (AskUserQuestion ou lista numerada se TEXT_MODE):
|
|
242
|
+
- "Pesquisar primeiro (Recomendado)" — investiga domínio/padrões/deps antes de planejar. Melhor pra features novas, integrações novas, mudanças arquiteturais.
|
|
243
|
+
- "Pular pesquisa" — planeja direto do contexto. Melhor pra bug fix, refactor simples, tarefas bem compreendidas.
|
|
242
244
|
|
|
243
|
-
|
|
244
|
-
Perguntar ao usuário se deseja pesquisar, com uma recomendação contextual baseada na fase:
|
|
245
|
-
|
|
246
|
-
Se `TEXT_MODE` for true, apresentar como lista numerada de texto simples:
|
|
247
|
-
```
|
|
248
|
-
Pesquisar antes de planejar a Fase {X}: {phase_name}?
|
|
249
|
-
|
|
250
|
-
1. Pesquisar primeiro (Recomendado) — Investigar domínio, padrões e dependências antes do planejamento. Melhor para novas funcionalidades, integrações desconhecidas ou mudanças arquiteturais.
|
|
251
|
-
2. Pular pesquisa — Planejar diretamente a partir do contexto e requisitos. Melhor para correções de bugs, refatorações simples ou tarefas bem compreendidas.
|
|
252
|
-
|
|
253
|
-
Digite o número:
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
Caso contrário usar AskUserQuestion:
|
|
257
|
-
```
|
|
258
|
-
AskUserQuestion([
|
|
259
|
-
{
|
|
260
|
-
question: "Pesquisar antes de planejar a Fase {X}: {phase_name}?",
|
|
261
|
-
header: "Pesquisa",
|
|
262
|
-
multiSelect: false,
|
|
263
|
-
options: [
|
|
264
|
-
{ label: "Pesquisar primeiro (Recomendado)", description: "Investigar domínio, padrões e dependências antes do planejamento. Melhor para novas funcionalidades, integrações desconhecidas ou mudanças arquiteturais." },
|
|
265
|
-
{ label: "Pular pesquisa", description: "Planejar diretamente a partir do contexto e requisitos. Melhor para correções de bugs, refatorações simples ou tarefas bem compreendidas." }
|
|
266
|
-
]
|
|
267
|
-
}
|
|
268
|
-
])
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
Se o usuário selecionar "Pular pesquisa": pular para o passo 6.
|
|
272
|
-
|
|
273
|
-
**Se `--auto` e `research_enabled` for false:** Pular pesquisa silenciosamente (preserva comportamento automatizado).
|
|
245
|
+
Se "Pular": passo 6. Se `--auto` e `research_enabled=false`: pular silenciosamente.
|
|
274
246
|
|
|
275
247
|
Exibir banner:
|
|
276
248
|
```
|
|
@@ -365,51 +337,16 @@ test -f "${PHASE_DIR}/${PADDED_PHASE}-VALIDATION.md" && echo "VALIDATION_CREATED
|
|
|
365
337
|
> Pular se `workflow.ui_phase` for explicitamente `false` E `workflow.ui_safety_gate` for explicitamente `false` em `.planning/config.json`. Se as chaves estiverem ausentes, tratar como habilitado.
|
|
366
338
|
|
|
367
339
|
```bash
|
|
368
|
-
UI_PHASE_CFG
|
|
369
|
-
UI_GATE_CFG=$(node "./.claude/framework/bin/tools.cjs" config-get workflow.ui_safety_gate 2>/dev/null || echo "true")
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
**Se ambos forem `false`:** Pular para o passo 6.
|
|
373
|
-
|
|
374
|
-
Verificar se a fase tem indicadores de frontend:
|
|
375
|
-
|
|
376
|
-
```bash
|
|
377
|
-
PHASE_SECTION=$(node "./.claude/framework/bin/tools.cjs" roadmap get-phase "${PHASE}" 2>/dev/null)
|
|
378
|
-
echo "$PHASE_SECTION" | grep -iE "UI|interface|frontend|component|layout|page|screen|view|form|dashboard|widget" > /dev/null 2>&1
|
|
379
|
-
HAS_UI=$?
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
**Se `HAS_UI` for 0 (indicadores de frontend encontrados):**
|
|
383
|
-
|
|
384
|
-
Verificar UI-SPEC existente:
|
|
385
|
-
```bash
|
|
386
|
-
UI_SPEC_FILE=$(ls "${PHASE_DIR}"/*-UI-SPEC.md 2>/dev/null | head -1)
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
**Se UI-SPEC.md encontrado:** Definir `UI_SPEC_PATH=$UI_SPEC_FILE`. Exibir: `Usando contrato de design de UI: ${UI_SPEC_PATH}`
|
|
390
|
-
|
|
391
|
-
**Se UI-SPEC.md ausente E `UI_GATE_CFG` for `true`:**
|
|
392
|
-
|
|
393
|
-
Se `TEXT_MODE` for true, apresentar como lista numerada de texto simples:
|
|
394
|
-
```
|
|
395
|
-
A Fase {N} tem indicadores de frontend mas sem UI-SPEC.md. Gerar um contrato de design antes do planejamento?
|
|
340
|
+
`UI_PHASE_CFG` / `UI_GATE_CFG` (default true). Se ambos false, pular pra passo 6.
|
|
396
341
|
|
|
397
|
-
|
|
398
|
-
2. Continuar sem UI-SPEC
|
|
399
|
-
3. Não é uma fase de frontend
|
|
342
|
+
**Detecção:** grep `-iE "UI|interface|frontend|component|layout|page|screen|view|form|dashboard|widget"` na descrição da fase. Se sem match, pular silenciosamente.
|
|
400
343
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
-
|
|
406
|
-
-
|
|
407
|
-
- options:
|
|
408
|
-
- "Gerar UI-SPEC primeiro" → Exibir: "Execute `/fase-ui {N} ${WS}` então re-execute `/planejar-fase {N} ${WS}`". Sair do workflow.
|
|
409
|
-
- "Continuar sem UI-SPEC" → Continuar para o passo 6.
|
|
410
|
-
- "Não é uma fase de frontend" → Continuar para o passo 6.
|
|
411
|
-
|
|
412
|
-
**Se `HAS_UI` for 1 (sem indicadores de frontend):** Pular silenciosamente para o passo 6.
|
|
344
|
+
**Se match encontrado:**
|
|
345
|
+
- UI-SPEC.md existe → usar (`UI_SPEC_PATH`); exibir confirmação
|
|
346
|
+
- UI-SPEC.md ausente E `UI_GATE_CFG=true` → AskUserQuestion (ou lista numerada se TEXT_MODE):
|
|
347
|
+
- "Gerar UI-SPEC primeiro" → exibir `/fase-ui {N} ${WS}` e sair do workflow
|
|
348
|
+
- "Continuar sem UI-SPEC" → passo 6
|
|
349
|
+
- "Não é frontend" → passo 6
|
|
413
350
|
|
|
414
351
|
## 6. Verificar Planos Existentes
|
|
415
352
|
|
|
@@ -511,28 +448,13 @@ Output consumed by /execute-phase. Plans need:
|
|
|
511
448
|
|
|
512
449
|
Every task MUST include these fields — they are NOT optional:
|
|
513
450
|
|
|
514
|
-
1. **`<read_first>`** —
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
- NEVER use subjective language ("looks correct", "properly configured", "consistent with")
|
|
522
|
-
- ALWAYS include exact strings, patterns, values, or command outputs that must be present
|
|
523
|
-
- Examples:
|
|
524
|
-
- Code: `auth.py contains def verify_token(` / `test_auth.py exits 0`
|
|
525
|
-
- Config: `.env.example contains DATABASE_URL=` / `Dockerfile contains HEALTHCHECK`
|
|
526
|
-
- Docs: `README.md contains '## Installation'` / `API.md lists all endpoints`
|
|
527
|
-
- Infra: `deploy.yml has rollback step` / `docker-compose.yml has healthcheck for db`
|
|
528
|
-
|
|
529
|
-
3. **`<action>`** — Must include CONCRETE values, not references. Rules:
|
|
530
|
-
- NEVER say "align X with Y", "match X to Y", "update to be consistent" without specifying the exact target state
|
|
531
|
-
- ALWAYS include the actual values: config keys, function signatures, SQL statements, class names, import paths, env vars, etc.
|
|
532
|
-
- If CONTEXT.md has a comparison table or expected values, copy them into the action verbatim
|
|
533
|
-
- The executor should be able to complete the task from the action text alone, without needing to read CONTEXT.md or reference files (read_first is for verification, not discovery)
|
|
534
|
-
|
|
535
|
-
**Why this matters:** Executor agents work from the plan text. Vague instructions like "update the config to match production" produce shallow one-line changes. Concrete instructions like "add DATABASE_URL=postgresql://... , set POOL_SIZE=20, add REDIS_URL=redis://..." produce complete work. The cost of verbose plans is far less than the cost of re-doing shallow execution.
|
|
451
|
+
1. **`<read_first>`** — files o executor DEVE ler antes de tocar em qualquer coisa: o arquivo sendo modificado, "source of truth" do CONTEXT.md, qualquer arquivo cujas convenções/tipos/assinaturas precisem ser replicados.
|
|
452
|
+
|
|
453
|
+
2. **`<acceptance_criteria>`** — condições verificáveis com grep/file read/test command/CLI output. NUNCA linguagem subjetiva ("looks correct"); SEMPRE strings/patterns exatos. Ex: `auth.py contains "def verify_token("`, `test_auth.py exits 0`, `.env.example contains "DATABASE_URL="`.
|
|
454
|
+
|
|
455
|
+
3. **`<action>`** — valores CONCRETOS, nunca referências. NUNCA "align X with Y"; SEMPRE valores reais (config keys, function signatures, SQL, imports, env vars). Se CONTEXT.md tem tabela de comparação, copie no `<action>` literal. Executor deve completar só com texto do action.
|
|
456
|
+
|
|
457
|
+
**Por quê:** instruções vagas ("update config to match production") geram one-line changes; instruções concretas ("add DATABASE_URL=..., POOL_SIZE=20, REDIS_URL=...") geram trabalho completo. Custo de plano verboso é ínfimo vs custo de redo de execução shallow.
|
|
536
458
|
</deep_work_rules>
|
|
537
459
|
|
|
538
460
|
<quality_gate>
|
|
@@ -725,58 +647,17 @@ Rotear para `<offer_next>` OU `auto_advance` dependendo de flags/config.
|
|
|
725
647
|
|
|
726
648
|
Verificar gatilho de avanço automático:
|
|
727
649
|
|
|
728
|
-
|
|
729
|
-
2. **Sincronizar flag de cadeia com intenção** — se o usuário invocou manualmente (sem `--auto`), limpar a flag de cadeia efêmera de qualquer cadeia `--auto` anterior interrompida. Isso NÃO toca em `workflow.auto_advance` (preferência persistente do usuário):
|
|
730
|
-
```bash
|
|
731
|
-
if [[ ! "$ARGUMENTS" =~ --auto ]]; then
|
|
732
|
-
node "./.claude/framework/bin/tools.cjs" config-set workflow._auto_chain_active false 2>/dev/null
|
|
733
|
-
fi
|
|
734
|
-
```
|
|
735
|
-
3. Ler tanto a flag de cadeia quanto a preferência do usuário:
|
|
736
|
-
```bash
|
|
737
|
-
AUTO_CHAIN=$(node "./.claude/framework/bin/tools.cjs" config-get workflow._auto_chain_active 2>/dev/null || echo "false")
|
|
738
|
-
AUTO_CFG=$(node "./.claude/framework/bin/tools.cjs" config-get workflow.auto_advance 2>/dev/null || echo "false")
|
|
739
|
-
```
|
|
740
|
-
|
|
741
|
-
**Se flag `--auto` presente OU `AUTO_CHAIN` for true OU `AUTO_CFG` for true:**
|
|
742
|
-
|
|
743
|
-
Exibir banner:
|
|
744
|
-
```
|
|
745
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
746
|
-
framework ► AVANÇANDO AUTOMATICAMENTE PARA EXECUÇÃO
|
|
747
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
748
|
-
|
|
749
|
-
Planos prontos. Iniciando execute-phase...
|
|
750
|
-
```
|
|
751
|
-
|
|
752
|
-
Iniciar execute-phase usando a ferramenta Skill para evitar sessões Task aninhadas (que causam freezes de runtime devido ao aninhamento profundo de agentes):
|
|
753
|
-
```
|
|
754
|
-
Skill(skill="framework:executar-fase", args="${PHASE} --auto --no-transition ${WS}")
|
|
755
|
-
```
|
|
756
|
-
|
|
757
|
-
A flag `--no-transition` diz ao execute-phase para retornar status após verificação em vez de encadear mais. Isso mantém a cadeia de avanço automático plana — cada fase roda no mesmo nível de aninhamento em vez de criar agentes Task mais profundos.
|
|
650
|
+
**Detecção:** flag `--auto` em $ARGUMENTS, OR `workflow._auto_chain_active=true`, OR `workflow.auto_advance=true`.
|
|
758
651
|
|
|
759
|
-
**
|
|
760
|
-
- **FASE CONCLUÍDA** → Exibir resumo final:
|
|
761
|
-
```
|
|
762
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
763
|
-
framework ► FASE ${PHASE} CONCLUÍDA ✓
|
|
764
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
652
|
+
**Sync de cadeia:** se invocação manual (sem `--auto`), zere `workflow._auto_chain_active` (não toque `workflow.auto_advance`).
|
|
765
653
|
|
|
766
|
-
|
|
654
|
+
**Quando ativo:** dispare `Skill(skill="framework:executar-fase", args="${PHASE} --auto --no-transition ${WS}")`. A flag `--no-transition` diz pra execute-phase retornar status após verificação (não encadear), mantendo cadeia plana.
|
|
767
655
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
-
|
|
771
|
-
```
|
|
772
|
-
Avanço automático parado: Execução precisa de revisão.
|
|
773
|
-
|
|
774
|
-
Revisar a saída acima e continuar manualmente:
|
|
775
|
-
/executar-fase ${PHASE} ${WS}
|
|
776
|
-
```
|
|
656
|
+
**Roteamento de retorno:**
|
|
657
|
+
- `FASE CONCLUÍDA` → próximo: `/discutir-fase ${NEXT_PHASE} --auto ${WS}` (após `/clear`)
|
|
658
|
+
- `LACUNAS ENCONTRADAS` / `VERIFICAÇÃO FALHOU` → parar cadeia. Continuar: `/executar-fase ${PHASE} ${WS}`
|
|
777
659
|
|
|
778
|
-
**
|
|
779
|
-
Rotear para `<offer_next>` (comportamento existente).
|
|
660
|
+
**Quando inativo:** rotear para `<offer_next>`.
|
|
780
661
|
|
|
781
662
|
</process>
|
|
782
663
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luanpdd/kit-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Generic infrastructure to ship YOUR personal kit of agents/commands/skills as an MCP server, with cross-IDE sync (Claude Code, Cursor, Codex, Gemini, Windsurf, Antigravity, Copilot, Trae).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/cli/index.js
CHANGED
|
@@ -33,7 +33,10 @@ import { createServer } from '../ui/server.js';
|
|
|
33
33
|
import { readLock, lockPathFor } from '../ui/lockfile.js';
|
|
34
34
|
import { wrapProgressForUi } from '../ui/wrapper.js';
|
|
35
35
|
import { openBrowser } from '../ui/browser.js';
|
|
36
|
+
import { checkUpgrade, getLocalVersion } from './upgrade-check.js';
|
|
36
37
|
import http from 'node:http';
|
|
38
|
+
import fs from 'node:fs';
|
|
39
|
+
import os from 'node:os';
|
|
37
40
|
|
|
38
41
|
// Read package.json version at boot so `--version` is always accurate. Falls
|
|
39
42
|
// back to a string if the file lookup fails (e.g. unusual install layout).
|
|
@@ -391,6 +394,14 @@ ui.command('start')
|
|
|
391
394
|
const url = `http://127.0.0.1:${actualPort}/`;
|
|
392
395
|
process.stderr.write(`${c.cyan(icons.info)} kit-mcp ui listening on ${url}\n`);
|
|
393
396
|
process.stderr.write(`${c.dim(` project: ${projectRoot}`)}\n`);
|
|
397
|
+
// U4: non-blocking upgrade check. Warns if local install is behind npm latest.
|
|
398
|
+
// Cached for 24h via ~/.kit-mcp/version-check.json so we don't hit npm on every start.
|
|
399
|
+
checkUpgrade().then((info) => {
|
|
400
|
+
if (info?.behind) {
|
|
401
|
+
process.stderr.write(`${c.yellow(icons.warn)} kit-mcp v${info.local} → v${info.latest} disponível\n`);
|
|
402
|
+
process.stderr.write(`${c.dim(' atualize com: npm i -g @luanpdd/kit-mcp@latest')}\n`);
|
|
403
|
+
}
|
|
404
|
+
}).catch(() => { /* offline / silent */ });
|
|
394
405
|
if (opts.open !== false) {
|
|
395
406
|
await openBrowser(url);
|
|
396
407
|
}
|
|
@@ -457,6 +468,174 @@ ui.command('open')
|
|
|
457
468
|
}
|
|
458
469
|
});
|
|
459
470
|
|
|
471
|
+
// --- doctor (DX diagnostic) ---
|
|
472
|
+
program.command('doctor')
|
|
473
|
+
.description('Diagnose kit-mcp setup: version, sidecar, hook, settings.json, lockfile, .planning/.')
|
|
474
|
+
.option('--project-root <path>', 'Project to diagnose (default: cwd)')
|
|
475
|
+
.action(async (opts) => {
|
|
476
|
+
const projectRoot = opts.projectRoot || process.cwd();
|
|
477
|
+
const checks = await runDoctorChecks(projectRoot);
|
|
478
|
+
const failed = checks.filter(c => c.status === 'fail').length;
|
|
479
|
+
const warned = checks.filter(c => c.status === 'warn').length;
|
|
480
|
+
|
|
481
|
+
if (program.opts().json) {
|
|
482
|
+
out({ checks, failed, warned }, () => '');
|
|
483
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
process.stdout.write(`\n${c.bold('kit-mcp doctor')} — ${projectRoot}\n\n`);
|
|
487
|
+
for (const check of checks) {
|
|
488
|
+
const sym = check.status === 'pass' ? c.green(icons.check)
|
|
489
|
+
: check.status === 'warn' ? c.yellow(icons.warn)
|
|
490
|
+
: c.red(icons.cross);
|
|
491
|
+
process.stdout.write(`${sym} ${c.bold(check.label)}\n`);
|
|
492
|
+
if (check.detail) process.stdout.write(` ${c.dim(check.detail)}\n`);
|
|
493
|
+
if (check.fix) process.stdout.write(` ${c.cyan('fix:')} ${check.fix}\n`);
|
|
494
|
+
}
|
|
495
|
+
process.stdout.write('\n');
|
|
496
|
+
if (failed > 0) {
|
|
497
|
+
process.stdout.write(`${c.red(icons.cross)} ${failed} check(s) failed\n`);
|
|
498
|
+
process.exit(1);
|
|
499
|
+
} else if (warned > 0) {
|
|
500
|
+
process.stdout.write(`${c.yellow(icons.warn)} ${warned} warning(s) — kit-mcp is functional\n`);
|
|
501
|
+
} else {
|
|
502
|
+
process.stdout.write(`${c.green(icons.check)} all checks passed\n`);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
async function runDoctorChecks(projectRoot) {
|
|
507
|
+
const checks = [];
|
|
508
|
+
|
|
509
|
+
// 1. Version + upgrade availability
|
|
510
|
+
const upgrade = await checkUpgrade();
|
|
511
|
+
if (!upgrade) {
|
|
512
|
+
checks.push({ label: 'version', status: 'fail',
|
|
513
|
+
detail: 'could not read local package.json',
|
|
514
|
+
fix: 'reinstall via `npm i -g @luanpdd/kit-mcp@latest`' });
|
|
515
|
+
} else if (upgrade.latest === null) {
|
|
516
|
+
checks.push({ label: 'version', status: 'warn',
|
|
517
|
+
detail: `local v${upgrade.local} (offline — could not check npm)` });
|
|
518
|
+
} else if (upgrade.behind) {
|
|
519
|
+
checks.push({ label: 'version', status: 'warn',
|
|
520
|
+
detail: `local v${upgrade.local}, latest v${upgrade.latest}`,
|
|
521
|
+
fix: 'npm i -g @luanpdd/kit-mcp@latest' });
|
|
522
|
+
} else {
|
|
523
|
+
checks.push({ label: 'version', status: 'pass',
|
|
524
|
+
detail: `v${upgrade.local} (latest)` });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// 2. Sidecar lockfile + healthz
|
|
528
|
+
const lock = readLock(projectRoot);
|
|
529
|
+
if (!lock) {
|
|
530
|
+
checks.push({ label: 'sidecar', status: 'warn',
|
|
531
|
+
detail: 'not running for this project',
|
|
532
|
+
fix: 'kit ui start (or omit if you don\'t need the live viewer)' });
|
|
533
|
+
} else {
|
|
534
|
+
try {
|
|
535
|
+
await getHealthz(lock.port);
|
|
536
|
+
checks.push({ label: 'sidecar', status: 'pass',
|
|
537
|
+
detail: `running on port ${lock.port} (pid ${lock.pid})` });
|
|
538
|
+
} catch (err) {
|
|
539
|
+
checks.push({ label: 'sidecar', status: 'fail',
|
|
540
|
+
detail: `lockfile says port ${lock.port} but unreachable: ${err.message}`,
|
|
541
|
+
fix: 'kit ui stop && kit ui start' });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// 3. ~/.claude/settings.json — exists + valid JSON + hooks present?
|
|
546
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
547
|
+
let settings = null;
|
|
548
|
+
try {
|
|
549
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
550
|
+
checks.push({ label: 'settings.json', status: 'pass',
|
|
551
|
+
detail: settingsPath });
|
|
552
|
+
} catch (err) {
|
|
553
|
+
if (err.code === 'ENOENT') {
|
|
554
|
+
checks.push({ label: 'settings.json', status: 'warn',
|
|
555
|
+
detail: 'not found (expected for fresh Claude Code)',
|
|
556
|
+
fix: 'will be created automatically by Claude Code' });
|
|
557
|
+
} else {
|
|
558
|
+
checks.push({ label: 'settings.json', status: 'fail',
|
|
559
|
+
detail: `invalid JSON at ${settingsPath}: ${err.message}`,
|
|
560
|
+
fix: 'edit the file or restore from .claude/settings.json.bak' });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// 4. Hook installed?
|
|
565
|
+
if (settings) {
|
|
566
|
+
const hooks = settings.hooks?.PostToolUse;
|
|
567
|
+
const hasHook = Array.isArray(hooks) && hooks.some((h) =>
|
|
568
|
+
Array.isArray(h.hooks) && h.hooks.some((cmd) =>
|
|
569
|
+
typeof cmd.command === 'string' && cmd.command.includes('sidecar-tool-publisher')));
|
|
570
|
+
if (hasHook) {
|
|
571
|
+
checks.push({ label: 'observability hook', status: 'pass',
|
|
572
|
+
detail: 'sidecar-tool-publisher registered as PostToolUse' });
|
|
573
|
+
} else {
|
|
574
|
+
checks.push({ label: 'observability hook', status: 'warn',
|
|
575
|
+
detail: 'sidecar-tool-publisher not registered',
|
|
576
|
+
fix: 'see kit/hooks/sidecar-tool-publisher.js for installation snippet' });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// 5. Bundled kit dirs exist
|
|
581
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
582
|
+
const kitRoot = path.resolve(here, '..', '..', 'kit');
|
|
583
|
+
const expected = ['agents', 'commands', 'skills'];
|
|
584
|
+
const missing = expected.filter((d) => {
|
|
585
|
+
try { return !fs.statSync(path.join(kitRoot, d)).isDirectory(); }
|
|
586
|
+
catch { return true; }
|
|
587
|
+
});
|
|
588
|
+
if (missing.length === 0) {
|
|
589
|
+
checks.push({ label: 'bundled kit', status: 'pass',
|
|
590
|
+
detail: `agents/, commands/, skills/ found in ${kitRoot}` });
|
|
591
|
+
} else {
|
|
592
|
+
checks.push({ label: 'bundled kit', status: 'fail',
|
|
593
|
+
detail: `missing: ${missing.join(', ')} in ${kitRoot}`,
|
|
594
|
+
fix: 'reinstall via `npm i -g @luanpdd/kit-mcp@latest`' });
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// 6. .planning/ in projectRoot — only warn if absent (not all projects use the framework)
|
|
598
|
+
const planningDir = path.join(projectRoot, '.planning');
|
|
599
|
+
if (fs.existsSync(planningDir)) {
|
|
600
|
+
const stateOk = fs.existsSync(path.join(planningDir, 'STATE.md'));
|
|
601
|
+
const roadmapOk = fs.existsSync(path.join(planningDir, 'ROADMAP.md'));
|
|
602
|
+
if (stateOk && roadmapOk) {
|
|
603
|
+
checks.push({ label: '.planning/', status: 'pass',
|
|
604
|
+
detail: 'STATE.md + ROADMAP.md present' });
|
|
605
|
+
} else {
|
|
606
|
+
checks.push({ label: '.planning/', status: 'warn',
|
|
607
|
+
detail: `present but missing ${[!stateOk && 'STATE.md', !roadmapOk && 'ROADMAP.md'].filter(Boolean).join(', ')}`,
|
|
608
|
+
fix: 'run `kit saude` to repair, or `/novo-marco` if mid-cycle' });
|
|
609
|
+
}
|
|
610
|
+
} else {
|
|
611
|
+
checks.push({ label: '.planning/', status: 'warn',
|
|
612
|
+
detail: 'no framework state in this project',
|
|
613
|
+
fix: 'run `/novo-projeto` to bootstrap, or skip if not using the framework' });
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// 7. Stale lockfile cleanup hint
|
|
617
|
+
try {
|
|
618
|
+
const tmpdir = os.tmpdir();
|
|
619
|
+
const orphans = fs.readdirSync(tmpdir).filter(n => /^kit-mcp-ui-[0-9a-f]{16}\.lock$/.test(n));
|
|
620
|
+
const stale = [];
|
|
621
|
+
for (const name of orphans) {
|
|
622
|
+
try {
|
|
623
|
+
const lock = JSON.parse(fs.readFileSync(path.join(tmpdir, name), 'utf8'));
|
|
624
|
+
try { process.kill(lock.pid, 0); } catch (err) {
|
|
625
|
+
if (err.code === 'ESRCH') stale.push(name);
|
|
626
|
+
}
|
|
627
|
+
} catch { /* skip unreadable */ }
|
|
628
|
+
}
|
|
629
|
+
if (stale.length > 0) {
|
|
630
|
+
checks.push({ label: 'orphan lockfiles', status: 'warn',
|
|
631
|
+
detail: `${stale.length} stale lockfile(s) in ${tmpdir}`,
|
|
632
|
+
fix: stale.map(n => `rm "${path.join(tmpdir, n)}"`).join(' && ') });
|
|
633
|
+
}
|
|
634
|
+
} catch { /* tmpdir scan is best-effort */ }
|
|
635
|
+
|
|
636
|
+
return checks;
|
|
637
|
+
}
|
|
638
|
+
|
|
460
639
|
// Helpers for kit ui (live in cli/ — stdout/console allowed here)
|
|
461
640
|
async function postShutdown(port) {
|
|
462
641
|
return new Promise((resolve, reject) => {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// upgrade-check.js — non-blocking check for newer kit-mcp on npm.
|
|
2
|
+
//
|
|
3
|
+
// Both `kit doctor` (U1) and `kit ui start` (U4) call this. Result is cached
|
|
4
|
+
// to ~/.kit-mcp/version-check.json for 24h so we don't hit the npm registry on
|
|
5
|
+
// every boot. Falls back gracefully when offline or when the request fails.
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import https from 'node:https';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
const PACKAGE_NAME = '@luanpdd/kit-mcp';
|
|
17
|
+
const CHECK_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
18
|
+
const REQUEST_TIMEOUT_MS = 1500;
|
|
19
|
+
|
|
20
|
+
function cacheFile() {
|
|
21
|
+
return path.join(os.homedir(), '.kit-mcp', 'version-check.json');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function readCache() {
|
|
25
|
+
try {
|
|
26
|
+
const raw = await fs.readFile(cacheFile(), 'utf8');
|
|
27
|
+
const obj = JSON.parse(raw);
|
|
28
|
+
if (typeof obj.checkedAt !== 'number' || typeof obj.latest !== 'string') return null;
|
|
29
|
+
if (Date.now() - obj.checkedAt > CHECK_TTL_MS) return null;
|
|
30
|
+
return obj;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function writeCache(obj) {
|
|
37
|
+
try {
|
|
38
|
+
await fs.mkdir(path.dirname(cacheFile()), { recursive: true });
|
|
39
|
+
await fs.writeFile(cacheFile(), JSON.stringify(obj), 'utf8');
|
|
40
|
+
} catch {
|
|
41
|
+
/* cache failures are silent — not critical */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fetchLatest() {
|
|
46
|
+
// Use the registry's package endpoint. Falls back to gracefully on any error.
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
const req = https.request({
|
|
49
|
+
method: 'GET',
|
|
50
|
+
hostname: 'registry.npmjs.org',
|
|
51
|
+
path: `/${encodeURIComponent(PACKAGE_NAME)}/latest`,
|
|
52
|
+
headers: { 'accept': 'application/json' },
|
|
53
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
54
|
+
}, (res) => {
|
|
55
|
+
if (res.statusCode !== 200) { res.resume(); resolve(null); return; }
|
|
56
|
+
let body = '';
|
|
57
|
+
res.setEncoding('utf8');
|
|
58
|
+
res.on('data', (c) => { body += c; });
|
|
59
|
+
res.on('end', () => {
|
|
60
|
+
try {
|
|
61
|
+
const j = JSON.parse(body);
|
|
62
|
+
resolve(typeof j.version === 'string' ? j.version : null);
|
|
63
|
+
} catch {
|
|
64
|
+
resolve(null);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
req.on('error', () => resolve(null));
|
|
69
|
+
req.on('timeout', () => { try { req.destroy(); } catch { /* noop */ } resolve(null); });
|
|
70
|
+
req.end();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function getLocalVersion() {
|
|
75
|
+
// Read package.json from the kit-mcp install root (parent of src/).
|
|
76
|
+
try {
|
|
77
|
+
const pkgPath = path.resolve(__dirname, '../../package.json');
|
|
78
|
+
const raw = await fs.readFile(pkgPath, 'utf8');
|
|
79
|
+
const j = JSON.parse(raw);
|
|
80
|
+
return typeof j.version === 'string' ? j.version : null;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Compare semver-like x.y.z lexically by component. Returns -1/0/1.
|
|
87
|
+
// Missing components default to 0 ("1.5" === "1.5.0").
|
|
88
|
+
function compareVersions(a, b) {
|
|
89
|
+
const parse = (v) => {
|
|
90
|
+
const parts = v.split('.').map((n) => Number.parseInt(n, 10) || 0);
|
|
91
|
+
while (parts.length < 3) parts.push(0);
|
|
92
|
+
return parts;
|
|
93
|
+
};
|
|
94
|
+
const [a1, a2, a3] = parse(a);
|
|
95
|
+
const [b1, b2, b3] = parse(b);
|
|
96
|
+
if (a1 !== b1) return a1 < b1 ? -1 : 1;
|
|
97
|
+
if (a2 !== b2) return a2 < b2 ? -1 : 1;
|
|
98
|
+
if (a3 !== b3) return a3 < b3 ? -1 : 1;
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// checkUpgrade({ force }): returns { local, latest, behind, source } or null on failure.
|
|
103
|
+
// force=true bypasses the 24h cache.
|
|
104
|
+
export async function checkUpgrade({ force = false } = {}) {
|
|
105
|
+
const local = await getLocalVersion();
|
|
106
|
+
if (!local) return null;
|
|
107
|
+
|
|
108
|
+
if (!force) {
|
|
109
|
+
const cached = await readCache();
|
|
110
|
+
if (cached?.latest) {
|
|
111
|
+
return {
|
|
112
|
+
local,
|
|
113
|
+
latest: cached.latest,
|
|
114
|
+
behind: compareVersions(local, cached.latest) < 0,
|
|
115
|
+
source: 'cache',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const latest = await fetchLatest();
|
|
121
|
+
if (!latest) {
|
|
122
|
+
// Network failed; surface what we have.
|
|
123
|
+
return { local, latest: null, behind: false, source: 'offline' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await writeCache({ checkedAt: Date.now(), latest });
|
|
127
|
+
return {
|
|
128
|
+
local,
|
|
129
|
+
latest,
|
|
130
|
+
behind: compareVersions(local, latest) < 0,
|
|
131
|
+
source: 'network',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const __test = { compareVersions, PACKAGE_NAME, CHECK_TTL_MS };
|
package/src/core/gates.js
CHANGED
|
@@ -22,7 +22,19 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
22
22
|
const __dirname = path.dirname(__filename);
|
|
23
23
|
export const DEFAULT_GATES_ROOT = path.resolve(__dirname, '../../gates');
|
|
24
24
|
|
|
25
|
+
// P2: TTL cache for listGates (mirrors PERF-01 in kit.js). Gates change rarely;
|
|
26
|
+
// inside a single Claude Code session we may call listGates → getGate → gatesForStage
|
|
27
|
+
// in sequence — without cache, that's 3 full directory walks of the gates dir.
|
|
28
|
+
const GATES_CACHE_TTL_MS = 30_000;
|
|
29
|
+
const gatesCache = new Map(); // gatesRoot -> { value, ts }
|
|
30
|
+
|
|
31
|
+
export function clearGatesCache() { gatesCache.clear(); }
|
|
32
|
+
|
|
25
33
|
export async function listGates(gatesRoot = DEFAULT_GATES_ROOT) {
|
|
34
|
+
const cached = gatesCache.get(gatesRoot);
|
|
35
|
+
if (cached && Date.now() - cached.ts < GATES_CACHE_TTL_MS) {
|
|
36
|
+
return cached.value;
|
|
37
|
+
}
|
|
26
38
|
let entries;
|
|
27
39
|
try { entries = await fs.readdir(gatesRoot, { withFileTypes: true }); }
|
|
28
40
|
catch { return []; }
|
|
@@ -40,7 +52,9 @@ export async function listGates(gatesRoot = DEFAULT_GATES_ROOT) {
|
|
|
40
52
|
absPath: abs,
|
|
41
53
|
});
|
|
42
54
|
}
|
|
43
|
-
|
|
55
|
+
const value = out.sort((a, b) => a.id.localeCompare(b.id));
|
|
56
|
+
gatesCache.set(gatesRoot, { value, ts: Date.now() });
|
|
57
|
+
return value;
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
export async function getGate(id, gatesRoot = DEFAULT_GATES_ROOT) {
|