wizz-method 1.18.2 → 1.18.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/README.md +1 -1
- package/package.json +1 -1
- package/skills-registry.yaml +0 -9
- package/src/modules/wizz/README.md +1 -1
- package/src/modules/wizz/_shared/model-ladder.md +2 -0
- package/src/modules/wizz/agents/wizz-ads/SKILL.md +3 -6
- package/src/modules/wizz/agents/wizz-ads/customize.toml +1 -8
- package/src/modules/wizz/agents/wizz-maestro/SKILL.md +1 -1
- package/src/modules/wizz/module.yaml +1 -1
- package/src/skills-lib/ad-creative/SKILL.md +1 -1
- package/src/skills-lib/paid-ads/SKILL.md +1 -1
- package/src/skills-lib/wizz-router/references/routing-table-flat.md +2 -5
- package/tools/installer/core/installer.js +20 -3
- package/tools/installer/modules/env-vars.js +41 -0
- package/tools/installer/modules/mcp-config.js +98 -14
- package/tools/installer/ui.js +6 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ O ponto de entrada técnico é o `wizz-router`: ele faz a triagem e manda pedido
|
|
|
21
21
|
| ✍️ `wizz-copy` | Copy, e-mail, headlines | copywriting, email-sequence, humanizer, copy-editing |
|
|
22
22
|
| 🔍 `wizz-seo` | SEO tradicional e de IA | seo-audit, ai-seo, schema-markup, programmatic-seo, site-architecture |
|
|
23
23
|
| 📈 `wizz-growth` | Marketing, CRO, lançamento, preço, churn | marketing-ideas, page-cro, launch-strategy, pricing-strategy, churn-prevention |
|
|
24
|
-
| 📢 `wizz-ads` | Mídia paga | paid-ads, ad-creative, analytics-tracking
|
|
24
|
+
| 📢 `wizz-ads` | Mídia paga | paid-ads, ad-creative, analytics-tracking |
|
|
25
25
|
| 🧠 `wizz-memoria` | Memória do projeto | cerebro |
|
|
26
26
|
|
|
27
27
|
Os papéis de dev e produto reusam os agentes WIZZ (Mary, John, Winston, Amelia, Sally, Paige), personalizados em PT-BR pelos overrides em `src/modules/wizz/overrides/`.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "wizz-method",
|
|
4
|
-
"version": "1.18.
|
|
4
|
+
"version": "1.18.3",
|
|
5
5
|
"description": "Wizz Method — método de agência orientado por IA em PT-BR (fork independente do BMad Method)",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agile",
|
package/skills-registry.yaml
CHANGED
|
@@ -355,15 +355,6 @@ areas:
|
|
|
355
355
|
not_when:
|
|
356
356
|
- "Google Ads institucional, campanha de marca, lead gen B2B."
|
|
357
357
|
- "Escrever copy ou criativo: a skill lê número, não produz anúncio."
|
|
358
|
-
mcps:
|
|
359
|
-
- id: meta-ads
|
|
360
|
-
when: "Gestão real de campanha Meta/Facebook/Instagram via API (criar/editar/medir ads)."
|
|
361
|
-
server:
|
|
362
|
-
command: npx
|
|
363
|
-
# Pin de supply chain (2026-08-23): versão fixa; atualizar conscientemente.
|
|
364
|
-
args: ["-y", "mcp-meta-ads@1.0.0"]
|
|
365
|
-
env:
|
|
366
|
-
META_ACCESS_TOKEN: "${META_ACCESS_TOKEN}"
|
|
367
358
|
|
|
368
359
|
architect:
|
|
369
360
|
agent: wizz-agent-architect
|
|
@@ -11,7 +11,7 @@ Personalização do WIZZ para a Wizz: linguagem fácil e resumida, cada agente t
|
|
|
11
11
|
| `wizz-copy` | ✍️ | Copy, e-mail, headlines | copywriting, email-sequence, humanizer, copy-editing |
|
|
12
12
|
| `wizz-seo` | 🔍 | SEO tradicional e de IA | seo-audit, ai-seo, schema-markup, programmatic-seo, site-architecture |
|
|
13
13
|
| `wizz-growth` | 📈 | Marketing, CRO, lançamento, preço, churn | marketing-ideas, page-cro, launch-strategy, pricing-strategy, churn-prevention |
|
|
14
|
-
| `wizz-ads` | 📢 | Mídia paga | paid-ads, ad-creative, analytics-tracking
|
|
14
|
+
| `wizz-ads` | 📢 | Mídia paga | paid-ads, ad-creative, analytics-tracking |
|
|
15
15
|
| `wizz-memoria` | 🧠 | Memória do projeto | cerebro |
|
|
16
16
|
|
|
17
17
|
Os papéis de dev/produto reusam os agentes WIZZ (Mary, John, Winston, Amelia, Sally, Paige), personalizados em PT-BR pelos overrides em `overrides/`.
|
|
@@ -14,6 +14,8 @@ O modelo da sessão principal é o chefe: orquestra, arbitra conflito entre exec
|
|
|
14
14
|
|
|
15
15
|
Como saber quem é o chefe em Claude Code: o system prompt declara o modelo da sessão ("You are powered by the model named ..."). Fable e Mythos ficam acima de Opus.
|
|
16
16
|
|
|
17
|
+
A mesma lógica vale em toda plataforma, trocando os nomes pelos da tabela abaixo. Em Codex: sessão num modelo acima de gpt-5.6 (ex.: astra) → `wizz-exec-opus` (gpt-5.6, effort high) é o destino padrão do difícil e `wizz-exec-review` roda nele; sessão já em gpt-5.6 → o chefe absorve o degrau Forte. Em OpenCode e Gemini CLI, idem com claude-opus-5 e gemini-3-pro-preview.
|
|
18
|
+
|
|
17
19
|
## Tabela de degraus
|
|
18
20
|
|
|
19
21
|
| Degrau | Subagente | Uso | Claude Code | Codex | OpenCode | Gemini CLI |
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wizz-ads
|
|
3
|
-
description: Wizz Method Paid Media Agent. Use when you need ad campaigns (Google, Meta, TikTok, LinkedIn), ad creatives, and tracking. Routes to paid-ads, ad-creative and analytics-tracking
|
|
3
|
+
description: Wizz Method Paid Media Agent. Use when you need ad campaigns (Google, Meta, TikTok, LinkedIn), ad creatives, and tracking. Routes to paid-ads, ad-creative and analytics-tracking.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Ads — Mídia Paga
|
|
7
7
|
|
|
8
8
|
## Visão geral
|
|
9
|
-
Você é o Ads do Wizz. Planeja campanhas, gera variações de criativo e acompanha métricas. Roteia para as skills globais via a ferramenta `Skill
|
|
9
|
+
Você é o Ads do Wizz. Planeja campanhas, gera variações de criativo e acompanha métricas. Roteia para as skills globais via a ferramenta `Skill`.
|
|
10
10
|
|
|
11
11
|
## Na ativação
|
|
12
12
|
1. **Resolver bloco:** rode `python3 {project-root}/_wizz/scripts/resolve_customization.py --skill {skill-root} --key agent`. Se falhar, mescle base → time → pessoal (`{skill-root}/customize.toml`, `{project-root}/_wizz/custom/{skill-name}.toml`, `.user.toml`).
|
|
@@ -20,13 +20,10 @@ Você é o Ads do Wizz. Planeja campanhas, gera variações de criativo e acompa
|
|
|
20
20
|
|
|
21
21
|
## Como trabalho (ponte global)
|
|
22
22
|
|
|
23
|
-
> **Fonte única (registry) — leia SEMPRE antes dos exemplos abaixo:** a lista real da sua área (`ads`) vive no `skills-registry.yaml`. Resolva primeiro a **fatia leve da sua área**, `{project-root}/_wizz/_config/registry/ads.yaml` (já vem como o bloco `areas.ads` completo); se faltar (install antigo), caia pro monólito na ordem `{project-root}/_wizz/_config/skills-registry.yaml` → `{project-root}/_wizz/skills-registry.yaml` → `{project-root}/skills-registry.yaml` e ache o bloco `areas.ads` lá dentro. Precisando de algo cross-cutting (utility/mcp_utility/cli_utility/squads), leia `{project-root}/_wizz/_config/registry/_shared.yaml`. Ofereça **tudo que casar** com o pedido pelo `when:` — `skills:` (via `Skill`), `clis:` (`check:` → se faltar mostre o `install:`, opt-in, respeite `platform:`) e `mcps:` (`claude mcp add <id>` com o bloco `server
|
|
23
|
+
> **Fonte única (registry) — leia SEMPRE antes dos exemplos abaixo:** a lista real da sua área (`ads`) vive no `skills-registry.yaml`. Resolva primeiro a **fatia leve da sua área**, `{project-root}/_wizz/_config/registry/ads.yaml` (já vem como o bloco `areas.ads` completo); se faltar (install antigo), caia pro monólito na ordem `{project-root}/_wizz/_config/skills-registry.yaml` → `{project-root}/_wizz/skills-registry.yaml` → `{project-root}/skills-registry.yaml` e ache o bloco `areas.ads` lá dentro. Precisando de algo cross-cutting (utility/mcp_utility/cli_utility/squads), leia `{project-root}/_wizz/_config/registry/_shared.yaml`. Ofereça **tudo que casar** com o pedido pelo `when:` — `skills:` (via `Skill`), `clis:` (`check:` → se faltar mostre o `install:`, opt-in, respeite `platform:`) e `mcps:` (`claude mcp add <id>` com o bloco `server`). Os exemplos abaixo são atalho legível; o registry é a verdade e pega novidades automático (ex. `arcads` p/ gerar vídeo-ad).
|
|
24
24
|
- Estratégia de campanha, segmentação, lances, ROAS → `paid-ads`
|
|
25
25
|
- Variações de criativo, headlines, copy de anúncio → `ad-creative`
|
|
26
26
|
- Tracking, conversões, UTMs, pixel → `analytics-tracking`
|
|
27
|
-
- Gestão real de campanha Meta (criar/editar/medir) → MCP `meta-ads`
|
|
28
|
-
|
|
29
|
-
Nunca exponha o `META_ACCESS_TOKEN` em log ou código.
|
|
30
27
|
|
|
31
28
|
## Encerramento
|
|
32
29
|
Termine com `✅ O que fiz` / `➡️ Próximo passo` / `🎯 Comando`.
|
|
@@ -21,12 +21,10 @@ activation_steps_append = [
|
|
|
21
21
|
|
|
22
22
|
persistent_facts = [
|
|
23
23
|
"Gate de planejamento: pedido de execução com complexidade alta e sem PRD/story/brief correspondente → pergunto 1x se crio o artefato antes (recomendado) ou pulo; nunca 2x na mesma cadeia. Regras completas: ler _shared/planning-gate.md do módulo wizz SÓ quando o gate disparar.",
|
|
24
|
-
"Nunca exponho o META_ACCESS_TOKEN em log, código ou commit.",
|
|
25
|
-
"Para gestão real de Meta uso o MCP meta-ads; para estratégia uso a skill paid-ads.",
|
|
26
24
|
"Falo PT-BR fácil. Toda campanha precisa de tracking antes de subir.",
|
|
27
25
|
]
|
|
28
26
|
|
|
29
|
-
role = "Planejar e gerenciar mídia paga (estratégia, criativo, tracking) roteando para as skills globais
|
|
27
|
+
role = "Planejar e gerenciar mídia paga (estratégia, criativo, tracking) roteando para as skills globais."
|
|
30
28
|
identity = "Gestor de tráfego orientado a ROAS: testa criativos, corta o que não performa, escala o que funciona."
|
|
31
29
|
communication_style = "Direto e numérico, em PT-BR fácil. Fala em CPA, ROAS e próximos testes."
|
|
32
30
|
|
|
@@ -51,8 +49,3 @@ prompt = "Invoque a skill GLOBAL 'ad-creative' via a ferramenta Skill para gerar
|
|
|
51
49
|
code = "TR"
|
|
52
50
|
description = "Tracking, conversões, UTMs, pixel"
|
|
53
51
|
prompt = "Invoque a skill GLOBAL 'analytics-tracking' via a ferramenta Skill."
|
|
54
|
-
|
|
55
|
-
[[agent.menu]]
|
|
56
|
-
code = "ME"
|
|
57
|
-
description = "Gestão real de campanha Meta (criar/editar/medir)"
|
|
58
|
-
prompt = "Use o MCP 'meta-ads' (via ToolSearch para carregar as ferramentas) para gerenciar campanhas reais no Meta. Nunca exponha o token."
|
|
@@ -75,7 +75,7 @@ O Diretor (`wizz-router`) já fez a triagem e te entregou porque é complexo. Se
|
|
|
75
75
|
2. **Enriquecimento = o `skills-registry.yaml`**. Para a área escolhida, ele diz O QUE o agente puxa:
|
|
76
76
|
- `areas:` — `agent` (deve casar com o do menu) e `skills:` (cada uma com `id` + `when` curto). Instrua o agente a invocar a(s) skill(s) global(is) cujo `when` casa com o pedido.
|
|
77
77
|
- `utility:` — skills cross-cutting (find-skills, enhance-prompt, wizz-router). Ofereça quando couber.
|
|
78
|
-
- `mcps:` (por área) e `mcp_utility:` (cross-cutting) — MCP servers que a área usa pra AGIR de verdade (ex: architect→supabase,
|
|
78
|
+
- `mcps:` (por área) e `mcp_utility:` (cross-cutting) — MCP servers que a área usa pra AGIR de verdade (ex: architect→supabase, analyst→exa, util→context7; qa NÃO usa MCP de browser — é agent-browser via CLI). Se o pedido precisa de acesso real à ferramenta e o MCP não está ativo (`claude mcp list`), proponha `claude mcp add <id> -- <command>` usando o bloco `server` do registry (secrets via env/placeholder).
|
|
79
79
|
- `clis:` (por área) e `cli_utility:` (cross-cutting) — ferramentas de linha de comando que o agente chama direto (não são skill nem MCP): ex. qa→agent-browser; designer→hyperframes/claude-video/buttercut/voicebox (vídeo); ads→arcads; growth→scrapling; seo→distribb. Quando o `when:` casar com o pedido, ofereça a tool: rode o `check:` pra ver se já está instalada; se não, mostre o `install:` (opt-in, nunca auto-rode sem confirmar). Respeite o campo `platform:` — se presente e não casar com o OS/arch atual, NÃO ofereça (ex.: `buttercut` é `darwin-arm64`, só Apple Silicon). Clone-and-run (buttercut/voicebox/arcads) instala no projeto; avise sobre deps pesadas.
|
|
80
80
|
- `squads:` — painéis consultivos (rodam via `wizz-party-mode`). Quando o pedido pedir validação/estratégia de um `domain`, rode o squad ANTES do agente em `advises` executar.
|
|
81
81
|
|
|
@@ -114,7 +114,7 @@ agents:
|
|
|
114
114
|
title: Mídia Paga
|
|
115
115
|
icon: "📢"
|
|
116
116
|
team: wizz
|
|
117
|
-
description: "Gerencia anúncios pagos roteando para paid-ads, ad-creative e analytics-tracking
|
|
117
|
+
description: "Gerencia anúncios pagos roteando para paid-ads, ad-creative e analytics-tracking. Cria variações de criativo e acompanha métricas."
|
|
118
118
|
|
|
119
119
|
- code: wizz-memoria
|
|
120
120
|
name: Memória
|
|
@@ -335,7 +335,7 @@ For pulling performance data and managing campaigns, see the [tools registry](..
|
|
|
335
335
|
| Platform | Pull Performance Data | Manage Campaigns | Guide |
|
|
336
336
|
|----------|:---------------------:|:----------------:|-------|
|
|
337
337
|
| **Google Ads** | `google-ads campaigns list`, `google-ads reports get` | `google-ads campaigns create` | [google-ads.md](../../tools/integrations/google-ads.md) |
|
|
338
|
-
| **Meta Ads** |
|
|
338
|
+
| **Meta Ads** | - | - | - |
|
|
339
339
|
| **LinkedIn Ads** | `linkedin-ads analytics get` | `linkedin-ads campaigns list` | [linkedin-ads.md](../../tools/integrations/linkedin-ads.md) |
|
|
340
340
|
| **TikTok Ads** | `tiktok-ads reports get` | `tiktok-ads campaigns list` | [tiktok-ads.md](../../tools/integrations/tiktok-ads.md) |
|
|
341
341
|
|
|
@@ -298,7 +298,7 @@ For implementation, see the [tools registry](../../tools/REGISTRY.md). Key adver
|
|
|
298
298
|
| Platform | Best For | MCP | Guide |
|
|
299
299
|
|----------|----------|:---:|-------|
|
|
300
300
|
| **Google Ads** | Search intent, high-intent traffic | ✓ | [google-ads.md](../../tools/integrations/google-ads.md) |
|
|
301
|
-
| **Meta Ads** | Demand gen, visual products, B2C | - |
|
|
301
|
+
| **Meta Ads** | Demand gen, visual products, B2C | - | - |
|
|
302
302
|
| **LinkedIn Ads** | B2B, job title targeting | - | [linkedin-ads.md](../../tools/integrations/linkedin-ads.md) |
|
|
303
303
|
| **TikTok Ads** | Younger demographics, video | - | [tiktok-ads.md](../../tools/integrations/tiktok-ads.md) |
|
|
304
304
|
|
|
@@ -59,8 +59,7 @@ Use esta tabela pra mapear skills/CLIs/MCPs direto **fora de projeto Wizz** (sem
|
|
|
59
59
|
| Intenção / palavras-chave | Skills a oferecer | Prioridade |
|
|
60
60
|
|---|---|---|
|
|
61
61
|
| Ideias de marketing, estratégia de marketing, posicionamento | `marketing-ideas` + `marketing-psychology` + `product-marketing-context` | 1 |
|
|
62
|
-
| Paid ads, anúncios, Google Ads, Meta Ads, TikTok Ads, mídia paga | `paid-ads` + `ad-creative` + `analytics-tracking`
|
|
63
|
-
| Gestão de campanha Meta/Facebook/Instagram via API real | **MCP meta-ads** (`mcp-meta-ads`) direto | 1 |
|
|
62
|
+
| Paid ads, anúncios, Google Ads, Meta Ads, TikTok Ads, mídia paga | `paid-ads` + `ad-creative` + `analytics-tracking` | 1 |
|
|
64
63
|
| Pixel/UTM/atribuição erradas, "a venda não bate com o gerenciador", "onde está furando o funil", tráfego orgânico inflado, conferir tracking antes de subir campanha | `tracking-audit` (+ `analytics-tracking` se for SaaS/produto) | 1 |
|
|
65
64
|
| Lançamento de feature, lançamento de produto, go-to-market | `launch-strategy` + `social-content` + `email-sequence` | 1 |
|
|
66
65
|
| Site pronto pra subir, pré-lançamento de SITE, "revisa antes do deploy", checklist de go-live, ou item pontual pré-lançamento (og:image/prévia de link, FAQ com schema, robots.txt, LGPD, alt text) | `site-launch-kit` | 1 |
|
|
@@ -97,6 +96,4 @@ Quando nenhuma skill/MCP instalado cobrir o pedido, **classifique o que falta**
|
|
|
97
96
|
|
|
98
97
|
**Falta um MCP:** informe → `claude mcp list` → consulte `skills-registry.yaml` (`mcps:`/`mcp_utility:`, com `server` pronto) → proponha `claude mcp add <id> [-e VAR=$VAR] -- <command> [args]`. Secrets sempre via env/placeholder, nunca token real.
|
|
99
98
|
|
|
100
|
-
MCPs comuns: context7 (docs de libs), supabase (Postgres),
|
|
101
|
-
|
|
102
|
-
Para paid ads Meta, o MCP `mcp-meta-ads` dá acesso real à API Meta Marketing (campanhas, ad sets, ads, métricas, criativos). Combine com `paid-ads` + `ad-creative` + `analytics-tracking`. O `META_ACCESS_TOKEN` vem de env local: nunca exponha em logs ou código commitado.
|
|
99
|
+
MCPs comuns: context7 (docs de libs), supabase (Postgres), exa (pesquisa). Browser/E2E é sempre via CLI `agent-browser`, nunca via MCP Playwright.
|
|
@@ -4,7 +4,7 @@ const { Manifest } = require('./manifest');
|
|
|
4
4
|
const { OfficialModules } = require('../modules/official-modules');
|
|
5
5
|
const { installSkillsLib } = require('../modules/skills-lib');
|
|
6
6
|
const { writeMcpConfig, renderAddCommand, prepareMcps, partitionAlreadyConfigured } = require('../modules/mcp-config');
|
|
7
|
-
const { promptMissingEnvVars } = require('../modules/env-vars');
|
|
7
|
+
const { promptMissingEnvVars, mcpsWithMissingRequiredEnv } = require('../modules/env-vars');
|
|
8
8
|
const { installClis, renderInstallCommand } = require('../modules/cli-config');
|
|
9
9
|
const { writeDepsCache, readPreviousMcpPins } = require('../modules/deps-cache');
|
|
10
10
|
const { IdeManager } = require('../ide/manager');
|
|
@@ -423,7 +423,7 @@ class Installer {
|
|
|
423
423
|
// is still cheap for an already-installed binary: prepareMcp is
|
|
424
424
|
// detect-first, so only a genuinely missing binary pays for the
|
|
425
425
|
// network-heavy install/post_install steps.
|
|
426
|
-
|
|
426
|
+
let { ready: mcpsReady, failed: mcpsFailed } = await prepareMcps({ mcps: toWrite });
|
|
427
427
|
for (const f of mcpsFailed) {
|
|
428
428
|
await prompts.log.warn(
|
|
429
429
|
`MCP ${f.id} não pôde ser preparado (${f.error}). Config NÃO escrita para evitar ENOENT — instale manualmente e adicione depois com \`claude mcp add\`.`,
|
|
@@ -451,10 +451,27 @@ class Installer {
|
|
|
451
451
|
mcps: mcpsReady,
|
|
452
452
|
});
|
|
453
453
|
const envInteractive = !config.skipPrompts && !!process.stdin.isTTY;
|
|
454
|
-
await promptMissingEnvVars(mcpsForEnvVars, {
|
|
454
|
+
const envResult = await promptMissingEnvVars(mcpsForEnvVars, {
|
|
455
455
|
projectDir: paths.projectRoot,
|
|
456
456
|
interactive: envInteractive,
|
|
457
457
|
});
|
|
458
|
+
|
|
459
|
+
// A required (no-default) var that stayed unresolved means
|
|
460
|
+
// the server would be written with a permanently-broken
|
|
461
|
+
// literal `${VAR}` — never write it (E1: never fail the
|
|
462
|
+
// install, just drop that server and tell the user how to
|
|
463
|
+
// retry).
|
|
464
|
+
const missingEnv = mcpsWithMissingRequiredEnv(mcpsReady, envResult.skipped);
|
|
465
|
+
if (missingEnv.length > 0) {
|
|
466
|
+
const missingIds = new Set(missingEnv.map((m) => m.id));
|
|
467
|
+
mcpsReady = mcpsReady.filter((m) => !missingIds.has(m.id));
|
|
468
|
+
for (const m of missingEnv) {
|
|
469
|
+
await prompts.log.warn(
|
|
470
|
+
`MCP ${m.id} não gravado: faltou ${m.missing.join(', ')}. Rode o installer de novo quando tiver a chave, ou adicione com \`claude mcp add\`.`,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
addResult('MCP servers', 'warn', `não gravados por falta de env var: ${missingEnv.map((m) => m.id).join(', ')}`);
|
|
474
|
+
}
|
|
458
475
|
} catch (error) {
|
|
459
476
|
await prompts.log.warn(`Falha ao configurar env vars dos MCPs: ${error.message}`);
|
|
460
477
|
addResult('MCP env vars', 'warn', error.message);
|
|
@@ -589,9 +589,50 @@ async function promptMissingEnvVars(mcps, opts = {}) {
|
|
|
589
589
|
};
|
|
590
590
|
}
|
|
591
591
|
|
|
592
|
+
/**
|
|
593
|
+
* Find which of `mcps` have at least one REQUIRED (`hasDefault: false`) env
|
|
594
|
+
* placeholder that stayed unresolved, per the `skipped` list returned by
|
|
595
|
+
* `promptMissingEnvVars`/`resolveEnvVars`. A var WITH a default
|
|
596
|
+
* (`hasDefault: true`) is fine even when skipped — the Claude Code runtime
|
|
597
|
+
* resolves `${VAR:-default}` itself — so only `hasDefault: false` entries
|
|
598
|
+
* count here.
|
|
599
|
+
*
|
|
600
|
+
* Writing a server whose required placeholder never resolved to anywhere
|
|
601
|
+
* (not `process.env`, not `.env`, not the global store, not typed
|
|
602
|
+
* interactively) would embed a literal, permanently-broken `${VAR}` into
|
|
603
|
+
* `.mcp.json`: the server sits in the config but can never actually start.
|
|
604
|
+
* Callers use this to drop such a server from the write instead. Pure: no
|
|
605
|
+
* I/O.
|
|
606
|
+
*
|
|
607
|
+
* @param {Array<{id: string}>} mcps - Resolved MCP entries to check
|
|
608
|
+
* @param {Array<{name: string, mcpIds: string[], hasDefault: boolean}>} [skipped]
|
|
609
|
+
* - The `skipped` array from `promptMissingEnvVars`/`resolveEnvVars`
|
|
610
|
+
* @returns {Array<{id: string, missing: string[]}>} One entry per affected
|
|
611
|
+
* MCP id, with the names of its unresolved required vars (in encounter order)
|
|
612
|
+
*/
|
|
613
|
+
function mcpsWithMissingRequiredEnv(mcps, skipped) {
|
|
614
|
+
const missingByMcpId = new Map();
|
|
615
|
+
for (const entry of skipped || []) {
|
|
616
|
+
if (!entry || entry.hasDefault) continue;
|
|
617
|
+
for (const id of entry.mcpIds || []) {
|
|
618
|
+
if (!missingByMcpId.has(id)) missingByMcpId.set(id, []);
|
|
619
|
+
missingByMcpId.get(id).push(entry.name);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const result = [];
|
|
624
|
+
for (const mcp of mcps || []) {
|
|
625
|
+
if (mcp && mcp.id && missingByMcpId.has(mcp.id)) {
|
|
626
|
+
result.push({ id: mcp.id, missing: missingByMcpId.get(mcp.id) });
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return result;
|
|
630
|
+
}
|
|
631
|
+
|
|
592
632
|
module.exports = {
|
|
593
633
|
extractEnvPlaceholders,
|
|
594
634
|
resolveEnvVars,
|
|
635
|
+
mcpsWithMissingRequiredEnv,
|
|
595
636
|
persistEnvValues,
|
|
596
637
|
persistProjectEnv,
|
|
597
638
|
persistGlobalEnv,
|
|
@@ -326,25 +326,101 @@ async function partitionAlreadyConfigured({ projectDir, mcps }) {
|
|
|
326
326
|
return { toPrepare, alreadyConfigured };
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Resolve the set of MCP ids covered by an installed Claude Code plugin
|
|
331
|
+
* (`~/.claude/plugins/installed_plugins.json`). A plugin key in that file's
|
|
332
|
+
* `plugins` map is `<name>@<marketplace>` (e.g.
|
|
333
|
+
* `supabase@claude-plugins-official`); the plugin covers catalog MCP id
|
|
334
|
+
* `<name>`. If the plugin ships its own `.mcp.json`
|
|
335
|
+
* (`<installPath>/.mcp.json`, `installPath` from the plugin's install
|
|
336
|
+
* record) with an `mcpServers` block, every key in that block is ALSO
|
|
337
|
+
* covered (e.g. the `vercel` plugin ships a server named `vercel`).
|
|
338
|
+
*
|
|
339
|
+
* Read failures at the top level (missing/malformed
|
|
340
|
+
* `installed_plugins.json`) fall back to "nothing covered", same spirit as
|
|
341
|
+
* the `~/.claude.json` handling in `partitionGloballyConfigured`. A failure
|
|
342
|
+
* reading ONE plugin's own `.mcp.json` (missing `installPath`, missing
|
|
343
|
+
* file, malformed JSON) only skips that plugin's extra ids — the plugin's
|
|
344
|
+
* own name stays covered, since that didn't require reading anything past
|
|
345
|
+
* the already-parsed top-level file.
|
|
346
|
+
*
|
|
347
|
+
* @param {string} [installedPluginsPath] - Override of
|
|
348
|
+
* `~/.claude/plugins/installed_plugins.json` (tests)
|
|
349
|
+
* @returns {Promise<Set<string>>}
|
|
350
|
+
*/
|
|
351
|
+
async function resolvePluginCoveredIds(installedPluginsPath) {
|
|
352
|
+
const file = installedPluginsPath || path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
353
|
+
const ids = new Set();
|
|
354
|
+
|
|
355
|
+
if (!(await fs.pathExists(file))) return ids;
|
|
356
|
+
|
|
357
|
+
let data;
|
|
358
|
+
try {
|
|
359
|
+
data = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
360
|
+
} catch {
|
|
361
|
+
return ids;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const plugins = data && typeof data === 'object' && data.plugins && typeof data.plugins === 'object' ? data.plugins : null;
|
|
365
|
+
if (!plugins) return ids;
|
|
366
|
+
|
|
367
|
+
for (const [key, entries] of Object.entries(plugins)) {
|
|
368
|
+
const name = String(key).split('@')[0];
|
|
369
|
+
if (name) ids.add(name);
|
|
370
|
+
|
|
371
|
+
if (!Array.isArray(entries)) continue;
|
|
372
|
+
for (const entry of entries) {
|
|
373
|
+
const installPath = entry && entry.installPath;
|
|
374
|
+
if (!installPath || typeof installPath !== 'string') continue;
|
|
375
|
+
|
|
376
|
+
const mcpJsonPath = path.join(installPath, '.mcp.json');
|
|
377
|
+
try {
|
|
378
|
+
if (!(await fs.pathExists(mcpJsonPath))) continue;
|
|
379
|
+
const mcpConfig = JSON.parse(await fs.readFile(mcpJsonPath, 'utf8'));
|
|
380
|
+
const servers =
|
|
381
|
+
mcpConfig && typeof mcpConfig === 'object' && mcpConfig.mcpServers && !Array.isArray(mcpConfig.mcpServers)
|
|
382
|
+
? mcpConfig.mcpServers
|
|
383
|
+
: null;
|
|
384
|
+
if (servers) {
|
|
385
|
+
for (const serverKey of Object.keys(servers)) ids.add(serverKey);
|
|
386
|
+
}
|
|
387
|
+
} catch {
|
|
388
|
+
// Malformed/unreadable plugin .mcp.json: skip its extra ids, the
|
|
389
|
+
// plugin's own name (added above) stays covered.
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return ids;
|
|
395
|
+
}
|
|
396
|
+
|
|
329
397
|
/**
|
|
330
398
|
* Split resolved MCP entries into those still worth offering/installing in
|
|
331
|
-
* the project and those the user already
|
|
332
|
-
* `mcpServers` key of `~/.claude.json`)
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
399
|
+
* the project and those the user already has covered elsewhere: configured
|
|
400
|
+
* GLOBALLY (user scope, the `mcpServers` key of `~/.claude.json`), or
|
|
401
|
+
* covered by an installed Claude Code PLUGIN
|
|
402
|
+
* (`~/.claude/plugins/installed_plugins.json`, see
|
|
403
|
+
* `resolvePluginCoveredIds`). Either way, the server is already live —
|
|
404
|
+
* usually with the real key embedded — so writing the registry's
|
|
405
|
+
* `${VAR}`-placeholder copy into the project `.mcp.json` would at best
|
|
406
|
+
* duplicate it and at worst shadow a working config with a broken
|
|
407
|
+
* placeholder one, then prompt the user for a key they already have.
|
|
337
408
|
*
|
|
338
|
-
* Read failures (missing/malformed `~/.claude.json`
|
|
339
|
-
*
|
|
409
|
+
* Read failures (missing/malformed `~/.claude.json` or
|
|
410
|
+
* `installed_plugins.json`) fall back to "nothing is global"/"nothing is
|
|
411
|
+
* plugin-covered", the safe default: worst case the user sees the old
|
|
412
|
+
* behavior. An id present in BOTH sources is reported once, in
|
|
413
|
+
* `globallyConfigured` only (never duplicated into `pluginCovered`).
|
|
340
414
|
*
|
|
341
415
|
* @param {Object} args
|
|
342
416
|
* @param {Array<{id: string}>} args.mcps - Resolved MCP entries to partition
|
|
343
417
|
* @param {string} [args.claudeJsonPath] - Override of `~/.claude.json` (tests)
|
|
344
|
-
* @
|
|
418
|
+
* @param {string} [args.installedPluginsPath] - Override of
|
|
419
|
+
* `~/.claude/plugins/installed_plugins.json` (tests)
|
|
420
|
+
* @returns {Promise<{toInstall: Array<Object>, globallyConfigured: string[], pluginCovered: string[]}>}
|
|
345
421
|
*/
|
|
346
|
-
async function partitionGloballyConfigured({ mcps, claudeJsonPath }) {
|
|
347
|
-
if (!mcps || mcps.length === 0) return { toInstall: [], globallyConfigured: [] };
|
|
422
|
+
async function partitionGloballyConfigured({ mcps, claudeJsonPath, installedPluginsPath }) {
|
|
423
|
+
if (!mcps || mcps.length === 0) return { toInstall: [], globallyConfigured: [], pluginCovered: [] };
|
|
348
424
|
|
|
349
425
|
const file = claudeJsonPath || path.join(os.homedir(), '.claude.json');
|
|
350
426
|
let globalIds = new Set();
|
|
@@ -359,13 +435,21 @@ async function partitionGloballyConfigured({ mcps, claudeJsonPath }) {
|
|
|
359
435
|
}
|
|
360
436
|
}
|
|
361
437
|
|
|
438
|
+
const pluginIds = await resolvePluginCoveredIds(installedPluginsPath);
|
|
439
|
+
|
|
362
440
|
const toInstall = [];
|
|
363
441
|
const globallyConfigured = [];
|
|
442
|
+
const pluginCovered = [];
|
|
364
443
|
for (const mcp of mcps) {
|
|
365
|
-
if (mcp && mcp.id && globalIds.has(mcp.id))
|
|
366
|
-
|
|
444
|
+
if (mcp && mcp.id && globalIds.has(mcp.id)) {
|
|
445
|
+
globallyConfigured.push(mcp.id);
|
|
446
|
+
} else if (mcp && mcp.id && pluginIds.has(mcp.id)) {
|
|
447
|
+
pluginCovered.push(mcp.id);
|
|
448
|
+
} else {
|
|
449
|
+
toInstall.push(mcp);
|
|
450
|
+
}
|
|
367
451
|
}
|
|
368
|
-
return { toInstall, globallyConfigured };
|
|
452
|
+
return { toInstall, globallyConfigured, pluginCovered };
|
|
369
453
|
}
|
|
370
454
|
|
|
371
455
|
/**
|
package/tools/installer/ui.js
CHANGED
|
@@ -629,9 +629,14 @@ class UI {
|
|
|
629
629
|
// duplicate config and re-prompt for a key that already works. Filter it
|
|
630
630
|
// out of every path below (multiselect, --mcps, --yes) with an info line
|
|
631
631
|
// so nothing disappears silently.
|
|
632
|
-
const { toInstall, globallyConfigured } = await partitionGloballyConfigured({ mcps: resolved });
|
|
632
|
+
const { toInstall, globallyConfigured, pluginCovered } = await partitionGloballyConfigured({ mcps: resolved });
|
|
633
633
|
if (globallyConfigured.length > 0) {
|
|
634
634
|
await prompts.log.info(`MCPs já configurados no seu Claude global (~/.claude.json), pulados: ${globallyConfigured.join(', ')}`);
|
|
635
|
+
}
|
|
636
|
+
if (pluginCovered.length > 0) {
|
|
637
|
+
await prompts.log.info(`MCPs já cobertos por plugin do Claude Code (user scope), pulados: ${pluginCovered.join(', ')}`);
|
|
638
|
+
}
|
|
639
|
+
if (globallyConfigured.length > 0 || pluginCovered.length > 0) {
|
|
635
640
|
resolved = toInstall;
|
|
636
641
|
if (resolved.length === 0) return { toWrite: [], toRecommend: [] };
|
|
637
642
|
}
|