@luanpdd/kit-mcp 1.1.0 → 1.2.3
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 +180 -0
- package/README.md +32 -0
- package/bin/ui.js +74 -0
- package/package.json +2 -1
- package/src/cli/index.js +211 -10
- package/src/mcp-server/index.js +53 -6
- package/src/ui/auto-spawn.js +108 -0
- package/src/ui/browser.js +78 -0
- package/src/ui/client.js +115 -0
- package/src/ui/events.js +65 -0
- package/src/ui/lockfile.js +147 -0
- package/src/ui/port.js +67 -0
- package/src/ui/server.js +432 -0
- package/src/ui/static/index.html +1022 -0
- package/src/ui/wrapper.js +119 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,186 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.2.3] - 2026-05-04
|
|
10
|
+
|
|
11
|
+
UI inteira agora fala português e usa termos que fazem sentido pra quem não conhece o código por dentro. Os tipos de evento técnicos viraram nomes legíveis, os caminhos absolutos viraram descrições do tipo "agente planner" / "comando novo-marco" / "skill limpeza", e o status badge da conexão agora lê "CONECTADO/CONECTANDO/DESCONECTADO".
|
|
12
|
+
|
|
13
|
+
### Adicionado — humanização de labels
|
|
14
|
+
|
|
15
|
+
- `EVENT_TYPE_LABEL` mapeia `run.start → Iniciado`, `run.end → Finalizado`, `progress → Em andamento`, `tool_invocation → Comando`, `milestone → Marco`, `error → Erro`, `shutdown → Desligado`. O `data-type` raw continua na markup (CSS de cor + filtros funcionam igual), apenas o texto exibido muda.
|
|
16
|
+
- `TOOL_LABEL` mapeia ids técnicos pra descrições amigáveis: `sync.install → Sincronizando kit`, `reverse-sync.apply → Importando edições do IDE`, `gates.run → Executando gate`, `sync.watch → Vigiando kit (watch)`, `sidecar → Servidor sidecar`.
|
|
17
|
+
- `STATUS_LABEL` traduz `running → em execução`, `done → concluído`, `error → erro`. Usado nos badges dos cards de active runs e no label de fade-out.
|
|
18
|
+
- `CONN_LABEL` traduz `CONNECTING → CONECTANDO`, `OPEN → CONECTADO`, `CLOSED → DESCONECTADO`.
|
|
19
|
+
- `humanizePath()` reconhece padrões comuns e devolve descrições amigáveis:
|
|
20
|
+
- `.claude/agents/planner.md` → `agente planner`
|
|
21
|
+
- `kit/commands/novo-marco.md` → `comando novo-marco`
|
|
22
|
+
- `kit/skills/limpeza/SKILL.md` → `skill limpeza`
|
|
23
|
+
- `.claude/framework/...` → `framework`
|
|
24
|
+
- `CLAUDE.md` → `manifesto CLAUDE.md`
|
|
25
|
+
- `humanizeLabel()` reconhece o padrão "verbo + caminho" comum em payloads de progresso e traduz: `writing .claude/agents/planner.md` → `criando agente planner`, `merging kit/commands/foo.md` → `mesclando comando foo`. Verbos suportados: reading, writing, projecting, merging, copying, deleting, creating, updating, syncing, applying, fetching.
|
|
26
|
+
|
|
27
|
+
### Adicionado — copy PT-BR
|
|
28
|
+
|
|
29
|
+
Toda a interface está em português:
|
|
30
|
+
- Placeholder do search: `filtrar por nome ou conteúdo…`
|
|
31
|
+
- Botões: `⏸ pausar` / `▶ retomar`, `↧ rolagem auto`, `limpar tela`
|
|
32
|
+
- Header active runs: `Em execução agora`
|
|
33
|
+
- Footer: `eventos: N`, `pausado: N em fila`, `fonte: ao vivo`
|
|
34
|
+
- Header port: `porta NNNN`
|
|
35
|
+
- Estados de timing: `há 2m 15s` (substitui `started 2m 15s`)
|
|
36
|
+
- Status do card: `em execução` / `concluído` / `erro`
|
|
37
|
+
- Fim de run: `concluído com sucesso` / `falhou` (em vez de `done` / `failed`)
|
|
38
|
+
- Início de run: `iniciando…` (em vez de `starting…`)
|
|
39
|
+
|
|
40
|
+
### Por que
|
|
41
|
+
|
|
42
|
+
A v1.2.2 tinha layout bonito mas linguagem técnica — `RUN.START`, `writing .claude/agents/example-reviewer.md`, `RUNNING`. Pra quem usa o sidecar pra primeira vez ou não conhece a arquitetura interna do kit, esses labels eram opacos. Agora o painel diz "Sincronizando kit · em execução · criando agente example-reviewer · 34/100 · 12s" em vez de jogar paths absolutos e enums no usuário.
|
|
43
|
+
|
|
44
|
+
### Sem mudanças de API
|
|
45
|
+
|
|
46
|
+
Pure UI patch, ainda. Stable API v1.0+ preservada. Sem deps novas. `data-type` no DOM continua raw (filtros e CSS não quebram). Apenas `src/ui/static/index.html` mudou.
|
|
47
|
+
|
|
48
|
+
## [1.2.2] - 2026-05-04
|
|
49
|
+
|
|
50
|
+
UX upgrade da sidecar: o feed cronológico continua, mas agora a janela mostra TAMBÉM um painel "Active runs" no topo com cards visuais pra cada execução em andamento — barra de progresso animada, percentual grande, label do passo atual, runId truncado e tempo decorrido tickando ao vivo.
|
|
51
|
+
|
|
52
|
+
### Adicionado
|
|
53
|
+
|
|
54
|
+
- **Active runs panel** em `src/ui/static/index.html` — uma `<section id="active-runs">` antes do log de eventos. Para cada `runId` ativo, renderiza um card com:
|
|
55
|
+
- Nome do tool (de `payload.tool` no `run.start`) com badge de status (running/done/error)
|
|
56
|
+
- Percentual grande (22px tabular-nums) à direita
|
|
57
|
+
- Barra de progresso 8px com transição suave + shimmer animado em estado `running`
|
|
58
|
+
- Label do passo atual (último `payload.label` ou `current/total` derivado)
|
|
59
|
+
- Footer com tempo decorrido (tick a cada 1s) + runId truncado + current/total
|
|
60
|
+
- Estado consolidado via `Map<runId, ActiveRun>`, atualizado por `run.start` (cria), `progress` (incrementa), `tool_invocation` (refina título), `run.end` (marca done, fade-out em 4s), `error` (marca erro, fade-out em 8s).
|
|
61
|
+
- Cards são **atualizados in-place** via `dataset.runid` matching — preserva a transição CSS da barra em vez de recriar o DOM a cada update.
|
|
62
|
+
- Painel some quando 0 runs ativos; aparece automaticamente quando o primeiro `run.start` chega.
|
|
63
|
+
|
|
64
|
+
### Por que
|
|
65
|
+
|
|
66
|
+
A v1.2.0 mostrava progresso, mas misturado no feed cronológico — pra saber "tô em quanto" você precisava scanar o último `progress`. O painel novo mostra o estado atual sem rolagem, com afordância visual: barra cresce, percentual sobe, shimmer anda. Quando termina, o card vira verde e some 4s depois (vermelho 8s pra erro). Pareceu "log do servidor", agora parece "process viewer".
|
|
67
|
+
|
|
68
|
+
### Sem mudanças de API
|
|
69
|
+
|
|
70
|
+
Pure UI patch. Stable API v1.0+ preservada. Sem deps novas. Apenas `src/ui/static/index.html` (~120 LOC adicionados — CSS dos cards + lógica do `upsertActiveRun` + tick interval).
|
|
71
|
+
|
|
72
|
+
## [1.2.1] - 2026-05-04
|
|
73
|
+
|
|
74
|
+
Cosmetic + UX patches descobertos durante o smoke da v1.2.0. Sem mudanças de comportamento de API.
|
|
75
|
+
|
|
76
|
+
### Corrigido
|
|
77
|
+
|
|
78
|
+
- **`eventLabel()` agora lê `payload.name`.** Eventos `milestone` que usavam `payload.name` (sem `label`) renderizavam como texto cru "milestone" em vez do nome real. Adicionado fallback `name` na cadeia de helpers no `src/ui/static/index.html`.
|
|
79
|
+
- **SSE reconecta quando o tab volta a ficar visível.** Chrome (e outros browsers Chromium) throttla timers em background tabs, podendo suspender o retry interno do `EventSource` e deixar a conexão presa em `CLOSED` mesmo depois do `kit ui` voltar. Adicionado listener `visibilitychange` que faz `hydrateFromState() → connect()` quando o tab volta a `visible` e o status atual é `CLOSED`. Re-hidrata o ring buffer pra mostrar eventos que chegaram durante o gap.
|
|
80
|
+
|
|
81
|
+
### Sem mudanças de API
|
|
82
|
+
|
|
83
|
+
`v1.2.0 → v1.2.1` é puro patch:
|
|
84
|
+
- Stable API v1.0+ preservada
|
|
85
|
+
- Sem deps novas (deps em 6/6)
|
|
86
|
+
- Sem mudança em `src/core/`, `src/cli/`, `src/mcp-server/`, ou em qualquer schema MCP/CLI
|
|
87
|
+
- Apenas `src/ui/static/index.html` recebeu ~10 LOC
|
|
88
|
+
|
|
89
|
+
## [1.2.0] - 2026-05-04
|
|
90
|
+
|
|
91
|
+
**GUI sidecar de acompanhamento.** Janela web localhost paralela mostra ao vivo (via Server-Sent Events) o que kit-mcp está fazendo enquanto sua IDE chama tools — `sync install`, `reverse-sync apply`, `gates run`. Sidecar é totalmente opt-in: quem não invoca `kit ui` continua com a experiência v1.1 idêntica.
|
|
92
|
+
|
|
93
|
+
### Adicionado — Phase 11: Lock arquitetural
|
|
94
|
+
- ADR consolidado em `.planning/decisions.md` (porta 7100-7199, lockfile em `os.tmpdir()` keyed por sha1(projectRoot), idle 30min default, sem auth no v1.2 com mitigação compensatória)
|
|
95
|
+
- Threat model em `docs/sidecar-security.md`
|
|
96
|
+
- 2 audit gates novos no CI: stdout discipline em `src/ui/` (proíbe `console.log`/`process.stdout.write`) e dep budget (≤ baseline+1)
|
|
97
|
+
|
|
98
|
+
### Adicionado — Phase 12: Fundações
|
|
99
|
+
- `src/ui/events.js` — schema de evento, validador puro, `makeEvent`, `newRunId`
|
|
100
|
+
- `src/ui/port.js` — `findFreePort` na faixa 7100-7199 com retry-loop
|
|
101
|
+
- `src/ui/lockfile.js` — `acquireLock` atômico via `O_EXCL`, `probeStale` via `process.kill(pid, 0)` + healthz HTTP
|
|
102
|
+
|
|
103
|
+
### Adicionado — Phase 13: Servidor HTTP + SSE
|
|
104
|
+
- `src/ui/server.js` — http.Server nativo, bind 127.0.0.1 literal, 5 rotas (`/`, `/events` SSE, `/healthz`, `/state`, `/publish`, `/shutdown`)
|
|
105
|
+
- Heartbeat `: ping\n\n` cada 15s; reconnect auto via EventSource native + `retry: 3000`
|
|
106
|
+
- Ring buffer in-memory de 200 eventos (FIFO; sem persistência em disco)
|
|
107
|
+
- Cap de 32 conexões SSE; cleanup quádruplo (req+res × close+error)
|
|
108
|
+
- Idle shutdown 30min default (`--idle-ms 0` desabilita)
|
|
109
|
+
- Encerramento gracioso em SIGINT/SIGTERM com active sockets destruídos
|
|
110
|
+
- Validação de `Host` header (mitiga DNS rebinding) e `Origin` em endpoints non-GET
|
|
111
|
+
- `bin/ui.js` entry detached
|
|
112
|
+
|
|
113
|
+
### Adicionado — Phase 14: UI estática single-file
|
|
114
|
+
- `src/ui/static/index.html` (~470 LOC) — vanilla DOM + EventSource, sem build step
|
|
115
|
+
- Lista cronológica + auto-scroll + `<details>` expand
|
|
116
|
+
- Badges coloridos por tipo (`run.start`, `run.end`, `tool_invocation`, `progress`, `milestone`, `error`, `shutdown`)
|
|
117
|
+
- Status conexão (CONNECTING/OPEN/CLOSED) + reconexão automática
|
|
118
|
+
- Filter por tipo (chips) + substring search
|
|
119
|
+
- Pause/resume com buffer + autoscroll toggle
|
|
120
|
+
- Dark mode automático via `prefers-color-scheme`
|
|
121
|
+
- Banner de shutdown PT-BR em CLOSED >5s ou evento `shutdown`
|
|
122
|
+
- CSP estrito (`default-src 'self'; ...; frame-ancestors 'none'`)
|
|
123
|
+
|
|
124
|
+
### Adicionado — Phase 15: Publisher + wrapper + browser-open
|
|
125
|
+
- `src/ui/client.js` — `publish(event, {projectRoot})` fire-and-forget, cache TTL 5s, falha silenciosa em ECONNREFUSED
|
|
126
|
+
- `src/ui/wrapper.js` — `wrapProgressForUi(onProgress, ctx)` multiplexa terminal + sidecar; helpers `.done/.error/.emit`; `redactPath` central scrubando `$HOME → ~` e `projectRoot → <project>` em TODO payload
|
|
127
|
+
- `src/ui/browser.js` — wrapper sobre `open@11` com detection de headless (CI, DISPLAY, SSH, WSL, sandbox); fallback "imprime URL no stderr"
|
|
128
|
+
- Nova dep: `open@^11.0.0` (única adição; budget atingido em 6/6)
|
|
129
|
+
|
|
130
|
+
### Adicionado — Phase 16: CLI integration
|
|
131
|
+
- `kit ui start` — sobe sidecar foreground (Ctrl+C mata); flags `--port`, `--idle-ms`, `--no-open`
|
|
132
|
+
- `kit ui stop` — POST /shutdown
|
|
133
|
+
- `kit ui status` — exibe pid, port, uptime, eventos, subscribers
|
|
134
|
+
- `kit ui open` — reabre browser na sidecar atual
|
|
135
|
+
- Auto-detect: `kit sync install` e `kit reverse-sync apply` checam lockfile e wrappam `onProgress` automaticamente quando sidecar está rodando
|
|
136
|
+
- Opt-out global via `--no-ui` flag ou `KIT_MCP_NO_UI=1` env var
|
|
137
|
+
|
|
138
|
+
### Adicionado — Phase 17: MCP --auto-spawn
|
|
139
|
+
- `src/ui/auto-spawn.js` — `ensureSidecar({projectRoot})` checa lockfile + healthz; se ausente, spawna `bin/ui.js` em **detached** com `windowsHide: true` e `stdio: ['ignore', 'ignore', 'inherit']` (fecha stdout completamente — não pode poluir canal MCP do parent)
|
|
140
|
+
- 3 tools MCP ganham campo opcional `autoSpawn: boolean` no inputSchema:
|
|
141
|
+
- `sync` (action=install)
|
|
142
|
+
- `reverse-sync` (action=apply)
|
|
143
|
+
- `gates` (nova action `run`, com autoSpawn)
|
|
144
|
+
- Tools triviais (`kit`, `forensics`, `install`) **não** ganham autoSpawn — explicit-out por design
|
|
145
|
+
|
|
146
|
+
### Adicionado — Phase 18: Hardening + release
|
|
147
|
+
- 3 hardening tests novos: kill -9 recovery, multi-publisher race, MCP stdio uncorrupted (validação rigorosa do REQ SEC-04 em produção)
|
|
148
|
+
- README seção "Live UI" com primeiros passos
|
|
149
|
+
- `npm pack --dry-run` valida que `src/ui/static/index.html` é incluído no tarball
|
|
150
|
+
|
|
151
|
+
### Corrigido
|
|
152
|
+
- **REL-01 (bug pré-existente):** `kit --version` agora lê de `package.json` em vez de retornar string hardcoded `1.0.0`. Em v1.0/v1.1 o comando exibia versão errada — corrigido nesta release.
|
|
153
|
+
|
|
154
|
+
### Stable API additions (1.x compatible)
|
|
155
|
+
|
|
156
|
+
A v1.0 commitment continua válida. Estas adições são parte do contrato:
|
|
157
|
+
|
|
158
|
+
- **MCP tool `sync` inputSchema:** campo opcional `autoSpawn: boolean` em action=install. Tools que não passam mantêm comportamento idêntico.
|
|
159
|
+
- **MCP tool `reverse-sync` inputSchema:** campo opcional `autoSpawn: boolean` em action=apply.
|
|
160
|
+
- **MCP tool `gates` inputSchema:** campo opcional `autoSpawn: boolean` E nova action `run` com `id`/`projectRoot`/`autoSpawn` campos.
|
|
161
|
+
- **CLI subgroup `kit ui`:** novo grupo com `start | stop | status | open` subcommands.
|
|
162
|
+
- **CLI flag `--no-ui` global** + env var `KIT_MCP_NO_UI=1` — opt-out do auto-detect de sidecar.
|
|
163
|
+
- **Stable runtime guarantee:** core (`syncTo`, `applyReverse`, `runGate`) é literalmente intocado. Wrapper de `onProgress` é montado APENAS no callsite (CLI handler ou MCP tool handler).
|
|
164
|
+
|
|
165
|
+
### Migration
|
|
166
|
+
|
|
167
|
+
**Usuários v1.1 não precisam fazer nada.** Sidecar é estritamente opt-in.
|
|
168
|
+
|
|
169
|
+
Para experimentar a UI:
|
|
170
|
+
```bash
|
|
171
|
+
# 1. Em um terminal:
|
|
172
|
+
kit ui start
|
|
173
|
+
|
|
174
|
+
# 2. Em outro (ou via Claude Code/Cursor):
|
|
175
|
+
kit sync install claude-code
|
|
176
|
+
|
|
177
|
+
# A janela mostra o progresso em tempo real.
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Para tools MCP, passe `autoSpawn: true` quando quiser auto-abrir:
|
|
181
|
+
```jsonc
|
|
182
|
+
{ "tool": "sync", "arguments": { "action": "install", "target": "claude-code", "autoSpawn": true } }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Threat model resumido
|
|
186
|
+
|
|
187
|
+
Sidecar é **localhost only**, single-user, dev workstation. Sem auth (mitigado por bind 127.0.0.1 + Host/Origin check + CSP estrito + path scrubbing). Sem persistência. Sem TLS (loopback). Detalhes em [`docs/sidecar-security.md`](docs/sidecar-security.md).
|
|
188
|
+
|
|
9
189
|
## [1.1.0] - 2026-05-03
|
|
10
190
|
|
|
11
191
|
**Visual feedback in the terminal.** Running `kit ...` now prints colored tables, progress bars, summary panels and interactive selectors instead of the raw JSON-to-stdout default of v1.0. Programmatic consumers add `--json` to restore the previous behavior.
|
package/README.md
CHANGED
|
@@ -272,6 +272,38 @@ kit forensics load-replay <id> --project-root .
|
|
|
272
272
|
|
|
273
273
|
The proposal is always saved to `.planning/learnings/{agent}.proposal.md` first; the canonical is only modified after explicit confirmation (or `--apply`). MCP `forensics.reflect` never auto-applies.
|
|
274
274
|
|
|
275
|
+
### `kit ui ...` — live process viewer (sidecar) — _new in 1.2_
|
|
276
|
+
|
|
277
|
+
A tiny localhost web app that streams what kit-mcp is doing while your IDE drives it. Strictly opt-in: ignore it and v1.1 behavior is unchanged.
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
# In one terminal — keeps running until Ctrl+C
|
|
281
|
+
kit ui start
|
|
282
|
+
|
|
283
|
+
# In another terminal (or via Claude Code / Cursor) — runs as before, but events
|
|
284
|
+
# are now also broadcast to the sidecar window
|
|
285
|
+
kit sync install claude-code
|
|
286
|
+
|
|
287
|
+
# Tools too: pass autoSpawn:true on the MCP side, or just `kit ui start` first
|
|
288
|
+
kit ui status
|
|
289
|
+
kit ui stop
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
What you get:
|
|
293
|
+
|
|
294
|
+
- A single browser tab at `http://127.0.0.1:7100` (or the next free port up to 7199)
|
|
295
|
+
- Live event stream over Server-Sent Events — `tool_invocation`, `progress`, `error`, `milestone`, `run.start`, `run.end`
|
|
296
|
+
- Filters by event type and substring; pause/resume; auto-scroll; dark mode tracks the OS
|
|
297
|
+
- The sidecar shuts itself down after 30 minutes of idle; `--idle-ms 0` disables that
|
|
298
|
+
|
|
299
|
+
**Auto-spawn from MCP tools.** Pass `autoSpawn: true` in the inputSchema of `sync` (action=install), `reverse-sync` (action=apply), or `gates` (action=run). The MCP server will spawn `bin/ui.js` detached, wait for it to come online, open your default browser, and stream that tool's progress. Trivial tools (`kit list-*`, `forensics`, `install`) deliberately don't accept `autoSpawn` — the overhead isn't worth it.
|
|
300
|
+
|
|
301
|
+
**Opt-out always available.** From the CLI: pass `--no-ui` or set `KIT_MCP_NO_UI=1`. The sidecar is never started behind your back; it's only used when a lockfile is already present (someone ran `kit ui start` or `autoSpawn: true`).
|
|
302
|
+
|
|
303
|
+
**Security model.** Sidecar binds to `127.0.0.1` literally — never `0.0.0.0`, never `localhost` (which resolves to `::1` on Windows). Every route validates the `Host` header to mitigate DNS rebinding. CSP is strict (`default-src 'self'; …; frame-ancestors 'none'`). Paths in payloads are scrubbed (`$HOME → ~`, `<projectRoot> → <project>`) so screenshots don't leak directory structure. No persistence, no TLS, no auth — single-user dev workstation only. Full threat model in [`docs/sidecar-security.md`](docs/sidecar-security.md).
|
|
304
|
+
|
|
305
|
+
**First-run quirks.** Windows Defender / macOS firewall may prompt the first time `kit ui start` binds. Approving "Private networks" is enough — the server doesn't accept anything from outside loopback regardless. On WSL, `kit ui start` opens the URL in the Windows host browser via `wslview`. In CI / SSH / `TERM=dumb`, browser launch is suppressed and the URL is printed to stderr instead.
|
|
306
|
+
|
|
275
307
|
---
|
|
276
308
|
|
|
277
309
|
## MCP usage
|
package/bin/ui.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bin/ui.js — entry point for the sidecar HTTP server.
|
|
3
|
+
//
|
|
4
|
+
// Used both directly (when the user runs `kit ui start`) and as the spawn target
|
|
5
|
+
// when `--auto-spawn` is enabled on an MCP tool.
|
|
6
|
+
//
|
|
7
|
+
// Logging discipline: all output goes to stderr. stdout is reserved so that
|
|
8
|
+
// callers can pipe `node bin/ui.js | jq` without UI server chatter contaminating
|
|
9
|
+
// data streams (and so that this file can never poison an MCP stdio channel).
|
|
10
|
+
|
|
11
|
+
import process from 'node:process';
|
|
12
|
+
import { createServer } from '../src/ui/server.js';
|
|
13
|
+
|
|
14
|
+
function parseArgs(argv) {
|
|
15
|
+
const args = { projectRoot: process.cwd(), port: undefined, idleMs: undefined };
|
|
16
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
17
|
+
const a = argv[i];
|
|
18
|
+
if (a === '--project-root' && argv[i + 1]) { args.projectRoot = argv[i + 1]; i += 1; }
|
|
19
|
+
else if (a === '--port' && argv[i + 1]) { args.port = Number(argv[i + 1]); i += 1; }
|
|
20
|
+
else if (a === '--idle-ms' && argv[i + 1]) { args.idleMs = Number(argv[i + 1]); i += 1; }
|
|
21
|
+
else if (a === '--version') { args.printVersion = true; }
|
|
22
|
+
else if (a === '--help' || a === '-h') { args.help = true; }
|
|
23
|
+
}
|
|
24
|
+
return args;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const args = parseArgs(process.argv.slice(2));
|
|
28
|
+
|
|
29
|
+
if (args.help) {
|
|
30
|
+
process.stderr.write([
|
|
31
|
+
'kit-mcp sidecar entry — usually invoked via `kit ui start`',
|
|
32
|
+
'',
|
|
33
|
+
'Usage: node bin/ui.js [options]',
|
|
34
|
+
' --project-root <path> project root for lockfile keying (default: cwd)',
|
|
35
|
+
' --port <n> bind to a specific port (default: auto-pick 7100-7199)',
|
|
36
|
+
' --idle-ms <ms> idle shutdown timeout (default: 1800000 = 30min; 0 = never)',
|
|
37
|
+
' --version print version and exit',
|
|
38
|
+
' --help this text',
|
|
39
|
+
'',
|
|
40
|
+
].join('\n'));
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let pkgVersion = null;
|
|
45
|
+
try {
|
|
46
|
+
const { default: pkg } = await import('../package.json', { with: { type: 'json' } });
|
|
47
|
+
pkgVersion = pkg.version;
|
|
48
|
+
} catch {
|
|
49
|
+
// ok — version may not be available in some packaged contexts
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (args.printVersion) {
|
|
53
|
+
process.stderr.write(`${pkgVersion ?? 'unknown'}\n`);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const server = createServer({
|
|
58
|
+
projectRoot: args.projectRoot,
|
|
59
|
+
version: pkgVersion,
|
|
60
|
+
idleMs: args.idleMs,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const { port } = await server.start({ port: args.port });
|
|
65
|
+
process.stderr.write(`[kit-mcp ui] listening on http://127.0.0.1:${port}/\n`);
|
|
66
|
+
process.stderr.write(`[kit-mcp ui] project: ${args.projectRoot}\n`);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err.code === 'ELIVE') {
|
|
69
|
+
process.stderr.write(`[kit-mcp ui] sidecar already running for this project (pid=${err.lock?.pid}, port=${err.lock?.port})\n`);
|
|
70
|
+
process.exit(2);
|
|
71
|
+
}
|
|
72
|
+
process.stderr.write(`[kit-mcp ui] failed to start: ${err.message}\n`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luanpdd/kit-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.3",
|
|
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": {
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
52
52
|
"chokidar": "^5.0.0",
|
|
53
53
|
"commander": "^12.1.0",
|
|
54
|
+
"open": "^11.0.0",
|
|
54
55
|
"picocolors": "^1.1.1"
|
|
55
56
|
}
|
|
56
57
|
}
|
package/src/cli/index.js
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
// programmatic consumers.
|
|
14
14
|
|
|
15
15
|
import { Command } from 'commander';
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import path from 'node:path';
|
|
16
19
|
import { listKit, searchKit, findItem } from '../core/kit.js';
|
|
17
20
|
import { listTargets } from '../core/registry.js';
|
|
18
21
|
import { syncTo, statusOf, removeFrom } from '../core/sync.js';
|
|
@@ -26,13 +29,31 @@ import { listReplays, loadReplay } from '../core/replays.js';
|
|
|
26
29
|
import { installMcp, listInstallTargets } from '../mcp-server/install.js';
|
|
27
30
|
import * as render from './render.js';
|
|
28
31
|
import { c, icons, spinner, progress, select, confirm } from '../core/ui.js';
|
|
32
|
+
import { createServer } from '../ui/server.js';
|
|
33
|
+
import { readLock, lockPathFor } from '../ui/lockfile.js';
|
|
34
|
+
import { wrapProgressForUi } from '../ui/wrapper.js';
|
|
35
|
+
import { openBrowser } from '../ui/browser.js';
|
|
36
|
+
import http from 'node:http';
|
|
37
|
+
|
|
38
|
+
// Read package.json version at boot so `--version` is always accurate. Falls
|
|
39
|
+
// back to a string if the file lookup fails (e.g. unusual install layout).
|
|
40
|
+
function readPkgVersion() {
|
|
41
|
+
try {
|
|
42
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
const pkgPath = path.resolve(here, '..', '..', 'package.json');
|
|
44
|
+
return JSON.parse(readFileSync(pkgPath, 'utf8')).version;
|
|
45
|
+
} catch {
|
|
46
|
+
return 'unknown';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
29
49
|
|
|
30
50
|
const program = new Command()
|
|
31
51
|
.name('kit')
|
|
32
52
|
.description('Personal kit (agents/commands/skills) — CLI mirror of the kit-mcp server.')
|
|
33
|
-
.version(
|
|
53
|
+
.version(readPkgVersion())
|
|
34
54
|
.option('--kit-root <path>', 'Override the kit root (default: bundled example kit, or KIT_MCP_KIT_ROOT env)')
|
|
35
|
-
.option('--json', 'Output JSON to stdout (machine-readable, restores pre-1.1 default)')
|
|
55
|
+
.option('--json', 'Output JSON to stdout (machine-readable, restores pre-1.1 default)')
|
|
56
|
+
.option('--no-ui', 'Suppress sidecar event publishing for this run (default: auto-detect lockfile)');
|
|
36
57
|
|
|
37
58
|
program.hook('preAction', (thisCommand, actionCommand) => {
|
|
38
59
|
const opts = program.opts();
|
|
@@ -67,22 +88,57 @@ async function withSpinner(text, fn) {
|
|
|
67
88
|
}
|
|
68
89
|
|
|
69
90
|
// withProgress wraps a long op; passes onProgress callback to the core fn.
|
|
70
|
-
|
|
91
|
+
// Also auto-detects a running sidecar (via lockfile) and multiplexes events to
|
|
92
|
+
// it when present. Opt-out via --no-ui or KIT_MCP_NO_UI=1.
|
|
93
|
+
async function withProgress(label, total, fn, { tool, projectRoot } = {}) {
|
|
71
94
|
const opts = program.opts();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
95
|
+
let onProgress;
|
|
96
|
+
let p = null;
|
|
97
|
+
if (opts.json) {
|
|
98
|
+
onProgress = () => {};
|
|
99
|
+
} else {
|
|
100
|
+
p = progress({ total, label });
|
|
101
|
+
let last = '';
|
|
102
|
+
onProgress = ({ current, label }) => { last = label || last; p.tick({ label: last }); };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Auto-wrap if a sidecar is running for this projectRoot.
|
|
106
|
+
const wrapper = maybeWrapForUi(onProgress, { tool, projectRoot });
|
|
76
107
|
try {
|
|
77
|
-
const r = await fn(
|
|
78
|
-
p.finish(label);
|
|
108
|
+
const r = await fn(wrapper);
|
|
109
|
+
if (p) p.finish(label);
|
|
110
|
+
if (wrapper.done) wrapper.done({ ok: true });
|
|
79
111
|
return r;
|
|
80
112
|
} catch (e) {
|
|
81
|
-
p.finish();
|
|
113
|
+
if (p) p.finish();
|
|
114
|
+
if (wrapper.error) wrapper.error(e);
|
|
82
115
|
throw e;
|
|
83
116
|
}
|
|
84
117
|
}
|
|
85
118
|
|
|
119
|
+
// maybeWrapForUi — returns the original callback unchanged when no sidecar is up
|
|
120
|
+
// or the user opted out. Otherwise returns a wrapped callback with .done/.error.
|
|
121
|
+
function maybeWrapForUi(onProgress, { tool, projectRoot } = {}) {
|
|
122
|
+
const globalOpts = program.opts();
|
|
123
|
+
// commander stores `--no-ui` as opts.ui === false
|
|
124
|
+
if (globalOpts.ui === false || process.env.KIT_MCP_NO_UI === '1') {
|
|
125
|
+
return passthroughWrapper(onProgress);
|
|
126
|
+
}
|
|
127
|
+
const root = projectRoot || process.cwd();
|
|
128
|
+
if (!readLock(root)) {
|
|
129
|
+
return passthroughWrapper(onProgress);
|
|
130
|
+
}
|
|
131
|
+
return wrapProgressForUi(onProgress, { projectRoot: root, tool: tool ?? null });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function passthroughWrapper(onProgress) {
|
|
135
|
+
const cb = (p) => { if (typeof onProgress === 'function') onProgress(p); };
|
|
136
|
+
cb.done = () => {};
|
|
137
|
+
cb.error = () => {};
|
|
138
|
+
cb.emit = () => {};
|
|
139
|
+
return cb;
|
|
140
|
+
}
|
|
141
|
+
|
|
86
142
|
function fail(msg) {
|
|
87
143
|
process.stderr.write(`${c.red(icons.cross)} ${msg}\n`);
|
|
88
144
|
process.exit(1);
|
|
@@ -134,6 +190,7 @@ sync.command('install [target]')
|
|
|
134
190
|
`Syncing kit → ${target}`,
|
|
135
191
|
300,
|
|
136
192
|
(onProgress) => syncTo(target, { projectRoot: opts.projectRoot, mode: opts.mode, dryRun: opts.dryRun, onProgress }),
|
|
193
|
+
{ tool: 'sync.install', projectRoot: opts.projectRoot },
|
|
137
194
|
);
|
|
138
195
|
out(result, render.renderSyncInstall);
|
|
139
196
|
});
|
|
@@ -181,6 +238,7 @@ reverse.command('apply <target>')
|
|
|
181
238
|
`Applying reverse-sync (${opts.strategy})`,
|
|
182
239
|
50,
|
|
183
240
|
(onProgress) => applyReverse(target, { projectRoot: opts.projectRoot, strategy: opts.strategy, only: opts.only, dryRun: opts.dryRun, onProgress }),
|
|
241
|
+
{ tool: 'reverse-sync.apply', projectRoot: opts.projectRoot },
|
|
184
242
|
);
|
|
185
243
|
out(result, render.renderReverseApply);
|
|
186
244
|
});
|
|
@@ -314,4 +372,147 @@ async function pickTarget(targets, message) {
|
|
|
314
372
|
}
|
|
315
373
|
}
|
|
316
374
|
|
|
375
|
+
// --- ui (sidecar process viewer) ---
|
|
376
|
+
const ui = program.command('ui').description('Live process viewer in a localhost browser tab.');
|
|
377
|
+
|
|
378
|
+
ui.command('start')
|
|
379
|
+
.description('Start the sidecar HTTP server in foreground (Ctrl+C to stop). Prints URL on stderr.')
|
|
380
|
+
.option('--project-root <path>', 'Project root for lockfile keying (default: cwd)')
|
|
381
|
+
.option('--port <n>', 'Bind to a specific port (default: auto-pick 7100-7199)')
|
|
382
|
+
.option('--idle-ms <ms>', 'Idle shutdown timeout (default 30min; 0 = never)')
|
|
383
|
+
.option('--no-open', 'Skip auto-opening the browser')
|
|
384
|
+
.action(async (opts) => {
|
|
385
|
+
const projectRoot = opts.projectRoot || process.cwd();
|
|
386
|
+
const port = opts.port ? Number(opts.port) : undefined;
|
|
387
|
+
const idleMs = opts.idleMs !== undefined ? Number(opts.idleMs) : undefined;
|
|
388
|
+
const srv = createServer({ projectRoot, idleMs });
|
|
389
|
+
try {
|
|
390
|
+
const { port: actualPort } = await srv.start({ port });
|
|
391
|
+
const url = `http://127.0.0.1:${actualPort}/`;
|
|
392
|
+
process.stderr.write(`${c.cyan(icons.info)} kit-mcp ui listening on ${url}\n`);
|
|
393
|
+
process.stderr.write(`${c.dim(` project: ${projectRoot}`)}\n`);
|
|
394
|
+
if (opts.open !== false) {
|
|
395
|
+
await openBrowser(url);
|
|
396
|
+
}
|
|
397
|
+
// The server's own SIGINT handler will perform shutdown + cleanup.
|
|
398
|
+
// We just stay alive — server is foreground.
|
|
399
|
+
} catch (err) {
|
|
400
|
+
if (err.code === 'ELIVE') {
|
|
401
|
+
process.stderr.write(`${c.yellow(icons.warn)} sidecar already running for this project\n`);
|
|
402
|
+
process.stderr.write(` pid: ${err.lock?.pid}, port: ${err.lock?.port}\n`);
|
|
403
|
+
process.stderr.write(` use 'kit ui status' or 'kit ui open' to inspect\n`);
|
|
404
|
+
process.exit(2);
|
|
405
|
+
}
|
|
406
|
+
fail(err.message);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
ui.command('stop')
|
|
411
|
+
.description('Stop the sidecar running for this project (POST /shutdown).')
|
|
412
|
+
.option('--project-root <path>')
|
|
413
|
+
.action(async (opts) => {
|
|
414
|
+
const projectRoot = opts.projectRoot || process.cwd();
|
|
415
|
+
const lock = readLock(projectRoot);
|
|
416
|
+
if (!lock) return out({ ok: false, reason: 'no_sidecar' }, () => `${icons.warn} no sidecar running for this project\n`);
|
|
417
|
+
try {
|
|
418
|
+
await postShutdown(lock.port);
|
|
419
|
+
out({ ok: true, port: lock.port }, () => `${icons.check} sidecar at port ${lock.port} stopped\n`);
|
|
420
|
+
} catch (err) {
|
|
421
|
+
fail(`could not stop sidecar at port ${lock.port}: ${err.message}`);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
ui.command('status')
|
|
426
|
+
.description('Show whether a sidecar is running for this project.')
|
|
427
|
+
.option('--project-root <path>')
|
|
428
|
+
.action(async (opts) => {
|
|
429
|
+
const projectRoot = opts.projectRoot || process.cwd();
|
|
430
|
+
const lock = readLock(projectRoot);
|
|
431
|
+
if (!lock) {
|
|
432
|
+
out({ running: false, reason: 'no_lockfile' }, () => `${icons.warn} no sidecar running\n`);
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
const health = await getHealthz(lock.port);
|
|
437
|
+
out({ running: true, ...health, lockfile: lockPathFor(projectRoot) }, render.renderUiStatus ?? renderUiStatusFallback);
|
|
438
|
+
} catch (err) {
|
|
439
|
+
out({ running: false, reason: 'unreachable', lockfile: lockPathFor(projectRoot), error: err.message },
|
|
440
|
+
() => `${icons.cross} lockfile present but sidecar unreachable: ${err.message}\n`);
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
ui.command('open')
|
|
446
|
+
.description('Open the running sidecar in a browser. Fails if no sidecar is up.')
|
|
447
|
+
.option('--project-root <path>')
|
|
448
|
+
.action(async (opts) => {
|
|
449
|
+
const projectRoot = opts.projectRoot || process.cwd();
|
|
450
|
+
const lock = readLock(projectRoot);
|
|
451
|
+
if (!lock) return fail('no sidecar running — start one with `kit ui start`');
|
|
452
|
+
const url = `http://127.0.0.1:${lock.port}/`;
|
|
453
|
+
const r = await openBrowser(url, { force: true });
|
|
454
|
+
if (!r.opened) {
|
|
455
|
+
process.stderr.write(`${c.yellow(icons.warn)} could not open browser (${r.reason}); copy the URL above\n`);
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Helpers for kit ui (live in cli/ — stdout/console allowed here)
|
|
461
|
+
async function postShutdown(port) {
|
|
462
|
+
return new Promise((resolve, reject) => {
|
|
463
|
+
const req = http.request({
|
|
464
|
+
method: 'POST',
|
|
465
|
+
host: '127.0.0.1',
|
|
466
|
+
port,
|
|
467
|
+
path: '/shutdown',
|
|
468
|
+
agent: false,
|
|
469
|
+
headers: { host: `127.0.0.1:${port}`, origin: `http://127.0.0.1:${port}`, 'content-length': 0, connection: 'close' },
|
|
470
|
+
}, (res) => {
|
|
471
|
+
res.resume();
|
|
472
|
+
res.on('end', () => res.statusCode < 400 ? resolve() : reject(new Error(`http_${res.statusCode}`)));
|
|
473
|
+
});
|
|
474
|
+
req.on('error', reject);
|
|
475
|
+
req.setTimeout(2000, () => { try { req.destroy(); } catch {} ; reject(new Error('timeout')); });
|
|
476
|
+
req.end();
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function getHealthz(port) {
|
|
481
|
+
return new Promise((resolve, reject) => {
|
|
482
|
+
const req = http.request({
|
|
483
|
+
method: 'GET',
|
|
484
|
+
host: '127.0.0.1',
|
|
485
|
+
port,
|
|
486
|
+
path: '/healthz',
|
|
487
|
+
agent: false,
|
|
488
|
+
headers: { host: `127.0.0.1:${port}`, connection: 'close' },
|
|
489
|
+
}, (res) => {
|
|
490
|
+
const chunks = [];
|
|
491
|
+
res.on('data', (c) => chunks.push(c));
|
|
492
|
+
res.on('end', () => {
|
|
493
|
+
if (res.statusCode >= 400) return reject(new Error(`http_${res.statusCode}`));
|
|
494
|
+
try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8'))); }
|
|
495
|
+
catch (e) { reject(e); }
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
req.on('error', reject);
|
|
499
|
+
req.setTimeout(2000, () => { try { req.destroy(); } catch {} ; reject(new Error('timeout')); });
|
|
500
|
+
req.end();
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function renderUiStatusFallback(v) {
|
|
505
|
+
if (!v.running) return `${icons.warn} not running\n`;
|
|
506
|
+
return [
|
|
507
|
+
`${icons.check} sidecar running`,
|
|
508
|
+
` port: ${v.port}`,
|
|
509
|
+
` pid (sdcr): ${v.lockfile ? readLock(process.cwd())?.pid : '?'}`,
|
|
510
|
+
` uptime: ${Math.round((v.uptime || 0) / 1000)}s`,
|
|
511
|
+
` events: ${v.eventsTotal}`,
|
|
512
|
+
` subscribers: ${v.subscribers}`,
|
|
513
|
+
` url: http://127.0.0.1:${v.port}/`,
|
|
514
|
+
'',
|
|
515
|
+
].join('\n');
|
|
516
|
+
}
|
|
517
|
+
|
|
317
518
|
program.parseAsync(process.argv);
|