@fontdata/ai-harness 2.7.0 → 3.0.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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "fontdata",
3
- "version": "2.7.0",
3
+ "version": "3.0.0",
4
4
  "mcpServers": {
5
- "banco": { "command": "npx", "args": ["-y", "@fontdata/ai-harness@2.7.0", "banco-mcp"] }
5
+ "banco_dados": { "command": "npx", "args": ["-y", "@fontdata/ai-harness@3.0.0", "banco-dados-mcp"] }
6
6
  },
7
7
  "description": "Harness de engenharia com IA da Fontdata para Claude Code",
8
8
  "author": { "name": "Fontdata" },
package/README.md CHANGED
@@ -96,7 +96,7 @@ npx @fontdata/ai-harness init --comandos resolver-bug,nova-tarefa --sem-harness
96
96
  | `--sem-harness` | Não inclui o comando-mestre `/harness` |
97
97
  | `--sem-hooks` / `--sem-agents` | Não instala hooks / agents |
98
98
  | `--sem-comentarios` | Não instala a rule de comentários, o hook e a catraca do legado |
99
- | `--sem-banco` | Não registra o servidor MCP de banco no `.mcp.json` e lembra a recusa no config |
99
+ | `--sem-banco-dados` | Não registra o servidor MCP de banco no `.mcp.json` e lembra a recusa no config |
100
100
  | `--claude-md criar\|manter\|backup\|sobrescrever` | O que fazer com o `CLAUDE.md` (padrão: `criar`) |
101
101
  | `--conflito sobrescrever\|pular` | Arquivos que já existem (padrão: `pular`) |
102
102
  | `--yes` | Aplica; sem ele, só simula |
@@ -129,26 +129,26 @@ da conexão e o nome da variável de ambiente (`dsn_env`), e cada dev guarda o D
129
129
  `.claude/settings.local.json` (fora do git), no `.env` ou no ambiente:
130
130
 
131
131
  ```bash
132
- npx @fontdata/ai-harness banco adicionar erp --projeto # pergunta os dados, testa e grava ERP_DATABASE_URL
133
- npx @fontdata/ai-harness banco testar
132
+ npx @fontdata/ai-harness banco-dados adicionar erp --projeto # pergunta os dados, testa e grava ERP_DATABASE_URL
133
+ npx @fontdata/ai-harness banco-dados testar
134
134
  ```
135
135
 
136
- O `adicionar --projeto` grava `{ "nome": "erp", "dsn_env": "ERP_DATABASE_URL" }` em `banco.conexoes` e a variável
136
+ O `adicionar --projeto` grava `{ "nome": "erp", "dsn_env": "ERP_DATABASE_URL" }` em `banco_dados.conexoes` e a variável
137
137
  no `.claude/settings.local.json`. Faça commit do `config.json`; cada colega roda o mesmo comando, que grava só a
138
138
  variável. `"producao": true` na entrada vale para a equipe toda. Quem prefere um cadastro por máquina
139
- (`~/.config/ai-harness/conexoes.json`) usa `banco adicionar erp-dev` sem `--projeto` e `banco usar erp-dev` no
139
+ (`~/.config/ai-harness/conexoes.json`) usa `banco-dados adicionar erp-dev` sem `--projeto` e `banco-dados usar erp-dev` no
140
140
  projeto.
141
141
 
142
- O servidor MCP `banco` entrega ao Claude `listar_conexoes`, `listar_tabelas`, `descrever_tabela` e `consultar`,
143
- tudo somente leitura. Ele vem no plugin; sem plugin, o `init` e o `update` o registram no `.mcp.json` (`--sem-banco`
144
- pula). Conexão de produção nasce bloqueada. Use um usuário de banco só com leitura.
142
+ O servidor MCP `banco_dados` entrega ao Claude `listar_conexoes`, `listar_tabelas`, `descrever_tabela` e
143
+ `consultar`, tudo somente leitura. Ele vem no plugin; sem plugin, o `init` e o `update` o registram no `.mcp.json`
144
+ (`--sem-banco-dados` pula). Conexão de produção nasce bloqueada. Use um usuário de banco só com leitura.
145
145
 
146
146
  Os dados pessoais (CPF, CNPJ, e-mail, telefone, nome, endereço e afins) saem com **mascaramento LGPD**, feito no
147
147
  servidor antes de a resposta chegar ao Claude; os filtros rodam sobre o dado real. Por isso a `consultar` fica
148
148
  liberada nas permissões, sem pedir aprovação. Falsos positivos e colunas extras se ajustam em
149
- `banco.lgpd.liberar_colunas` e `banco.lgpd.mascarar_colunas`; desligar, só no cadastro da máquina. Conexões do
149
+ `banco_dados.lgpd.liberar_colunas` e `banco_dados.lgpd.mascarar_colunas`; desligar, só no cadastro da máquina. Conexões do
150
150
  projeto, cadastro, limites do filtro, produção, mascaramento e dúvidas frequentes estão no
151
- [guia de banco de dados](docs/guias/banco-de-dados.md).
151
+ [guia de banco de dados](docs/guias/banco-dados.md).
152
152
 
153
153
  ## Subcomandos do CLI
154
154
 
@@ -161,8 +161,8 @@ projeto, cadastro, limites do filtro, produção, mascaramento e dúvidas freque
161
161
  | `memoria [caminho]` | Índice de quem mexeu em cada arquivo (`--arquivo <caminho>`, `--regressao <id>`) |
162
162
  | `evento <evento>` | Grava no rastro do trabalho (usado por fluxos e agents) |
163
163
  | `conflitos <id>` | Aponta trabalhos em execução que mexem nos mesmos arquivos; `--aguardar` espera liberar (usado pelo `/executar-plan`) |
164
- | `banco <subcomando>` | Conexões de banco somente leitura: `adicionar`, `listar`, `testar`, `usar`, `remover` |
165
- | `banco-mcp` | Servidor MCP de leitura do banco, iniciado pelo Claude Code |
164
+ | `banco-dados <subcomando>` | Conexões de banco somente leitura: `adicionar`, `listar`, `testar`, `usar`, `remover` |
165
+ | `banco-dados-mcp` | Servidor MCP de leitura do banco, iniciado pelo Claude Code |
166
166
  | `comentarios [caminho]` | Instala o hook do padrão de comentários com a catraca do legado (`--yes`) |
167
167
  | `list` | Lista o catálogo |
168
168
 
@@ -2,23 +2,23 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { EOL } from 'node:os';
3
3
  import { join, resolve } from 'node:path';
4
4
  import * as p from '@clack/prompts';
5
- import { CONFIG_PATH, gravarConexaoProjeto, readConfig, REGRA_DSN_ENV, REGRA_NOME_CONEXAO, validarConfigParaEscrita, writeDefaultConfig, } from '../config/config.js';
5
+ import { comSecaoBanco, CONFIG_PATH, gravarConexaoProjeto, readConfig, REGRA_DSN_ENV, REGRA_NOME_CONEXAO, secaoBanco, validarConfigParaEscrita, writeDefaultConfig, } from '../config/config.js';
6
6
  import { suggestFlag } from '../install/flags.js';
7
7
  import { caminhoCadastro, lerCadastro, ocultarSenha, removerConexao, resumirConexao, salvarConexao, TIPOS_BANCO, } from './cadastro.js';
8
8
  import { gravarVariavelSettingsLocal, lerEnvSettingsLocal, lerVariavel, SETTINGS_LOCAL_PATH, validarSettingsLocal } from './ambiente.js';
9
9
  import { interpretarDsn, resolverConexoes } from './conexao.js';
10
10
  import { criarDriver as criarDriverPadrao } from './drivers/index.js';
11
11
  const AJUDA = [
12
- 'Uso: ai-harness banco <subcomando>',
12
+ 'Uso: ai-harness banco-dados <subcomando>',
13
13
  '',
14
14
  ' adicionar <nome> [--dsn <dsn>] [--producao] [--sem-testar] [--substituir]',
15
15
  ' cadastra a conexão nesta máquina (sem --dsn, pergunta os dados)',
16
16
  ' adicionar <nome> --projeto [--env <VAR>] [--dsn <dsn>] [--producao] [--sem-testar] [--substituir]',
17
17
  ' grava o DSN na variável <NOME>_DATABASE_URL (ou --env) do .claude/settings.local.json',
18
- ' e a conexão { nome, dsn_env } em banco.conexoes do .ai-harness/config.json',
18
+ ' e a conexão { nome, dsn_env } em banco_dados.conexoes do .ai-harness/config.json',
19
19
  ' listar mostra as conexões cadastradas e as do projeto, sem a senha',
20
20
  ' testar [nome] testa a conexão pedida ou todas as do projeto',
21
- ' usar <nome...> grava banco.conexoes no .ai-harness/config.json (mantém as entradas { nome, dsn_env })',
21
+ ' usar <nome...> grava banco_dados.conexoes no .ai-harness/config.json (mantém as entradas { nome, dsn_env })',
22
22
  ' remover <nome> apaga a conexão do cadastro',
23
23
  ].join(EOL);
24
24
  const PORTA_PADRAO = { postgres: '5432', mariadb: '3306' };
@@ -121,7 +121,7 @@ async function adicionar(ctx, argv) {
121
121
  const { posicionais, marcadas, valores } = lerArgumentos(argv, ['--producao', '--sem-testar', '--substituir', '--projeto'], ['--dsn', '--env']);
122
122
  const [nome, ...sobra] = posicionais;
123
123
  if (!nome || sobra.length > 0)
124
- throw new Error('Uso: ai-harness banco adicionar <nome> [--projeto [--env <VAR>]] [--dsn <dsn>] [--producao] [--sem-testar] [--substituir]');
124
+ throw new Error('Uso: ai-harness banco-dados adicionar <nome> [--projeto [--env <VAR>]] [--dsn <dsn>] [--producao] [--sem-testar] [--substituir]');
125
125
  if (marcadas.has('--projeto'))
126
126
  return adicionarNoProjeto(ctx, nome, marcadas, valores);
127
127
  if (valores['--env'] !== undefined)
@@ -144,25 +144,25 @@ async function adicionar(ctx, argv) {
144
144
  }
145
145
  salvarConexao(nome, conexao, { substituir, caminho: ctx.cadastro });
146
146
  ctx.log(`Conexão ${nome} gravada em ${ctx.cadastro}: ${resumirConexao(conexao)}${conexao.producao ? ' (produção)' : ''}.`);
147
- ctx.log(`No projeto, rode: npx @fontdata/ai-harness banco usar ${nome}`);
147
+ ctx.log(`No projeto, rode: npx @fontdata/ai-harness banco-dados usar ${nome}`);
148
148
  return 0;
149
149
  }
150
- /** Procura o nome no JSON bruto de banco.conexoes; recusa lista inválida antes de qualquer gravação. */
150
+ /** Procura o nome no JSON bruto de banco_dados.conexoes; recusa lista inválida antes de qualquer gravação. */
151
151
  function entradaDeclarada(cwd, nome) {
152
152
  validarConfigParaEscrita(cwd);
153
153
  const arquivo = join(cwd, CONFIG_PATH);
154
154
  if (!existsSync(arquivo))
155
155
  return { forma: 'ausente' };
156
156
  const dados = JSON.parse(readFileSync(arquivo, 'utf8'));
157
- const banco = isObject(dados) && isObject(dados.banco) ? dados.banco : {};
157
+ const banco = isObject(dados) ? secaoBanco(dados) : {};
158
158
  if (banco.conexoes === undefined)
159
159
  return { forma: 'ausente' };
160
160
  if (!Array.isArray(banco.conexoes))
161
- throw new Error(`banco.conexoes em ${CONFIG_PATH} não é uma lista; corrija-o antes de gravar a conexão ${nome}.`);
161
+ throw new Error(`banco_dados.conexoes em ${CONFIG_PATH} não é uma lista; corrija-o antes de gravar a conexão ${nome}.`);
162
162
  const achada = banco.conexoes.some((entrada) => (isObject(entrada) ? entrada.nome : entrada) === nome);
163
163
  if (!achada)
164
164
  return { forma: 'ausente' };
165
- const conexao = readConfig(cwd).config.banco.projeto[nome];
165
+ const conexao = readConfig(cwd).config.banco_dados.projeto[nome];
166
166
  return conexao ? { forma: 'objeto', conexao } : { forma: 'outra' };
167
167
  }
168
168
  function variavelPadrao(nome) {
@@ -183,7 +183,7 @@ async function adicionarNoProjeto(ctx, nome, marcadas, valores) {
183
183
  const declarada = entradaDeclarada(ctx.cwd, nome);
184
184
  let variavel = pedida ?? variavelPadrao(nome);
185
185
  if (declarada.forma === 'outra' && !substituir) {
186
- ctx.erro(`A conexão ${nome} já está em banco.conexoes de ${CONFIG_PATH} sem dsn_env. Use --substituir para trocá-la pela conexão do projeto.`);
186
+ ctx.erro(`A conexão ${nome} já está em banco_dados.conexoes de ${CONFIG_PATH} sem dsn_env. Use --substituir para trocá-la pela conexão do projeto.`);
187
187
  return 1;
188
188
  }
189
189
  if (declarada.forma === 'objeto') {
@@ -234,7 +234,7 @@ async function adicionarNoProjeto(ctx, nome, marcadas, valores) {
234
234
  const { changed } = gravarVariavelSettingsLocal(ctx.cwd, variavel, valor);
235
235
  ctx.log(changed ? `Variável ${variavel} gravada em ${SETTINGS_LOCAL_PATH}.` : `Variável ${variavel} já estava com esse valor em ${SETTINGS_LOCAL_PATH}.`);
236
236
  if (declarada.forma === 'objeto' && !promover) {
237
- ctx.log(`Conexão ${nome} já declarada em banco.conexoes de ${CONFIG_PATH}; a entrada não foi alterada.`);
237
+ ctx.log(`Conexão ${nome} já declarada em banco_dados.conexoes de ${CONFIG_PATH}; a entrada não foi alterada.`);
238
238
  return 0;
239
239
  }
240
240
  const entrada = declarada.forma === 'objeto'
@@ -246,7 +246,7 @@ async function adicionarNoProjeto(ctx, nome, marcadas, valores) {
246
246
  ...(producao ? { producao: true } : {}),
247
247
  };
248
248
  gravarConexaoProjeto(ctx.cwd, entrada, { substituir: declarada.forma !== 'ausente' });
249
- ctx.log(`Conexão ${nome} gravada em banco.conexoes de ${CONFIG_PATH} (dsn_env ${variavel})${entrada.producao ? ', produção' : ''}.`);
249
+ ctx.log(`Conexão ${nome} gravada em banco_dados.conexoes de ${CONFIG_PATH} (dsn_env ${variavel})${entrada.producao ? ', produção' : ''}.`);
250
250
  if (declarada.forma === 'ausente')
251
251
  ctx.log(`Versione o ${CONFIG_PATH}; cada colega define ${variavel} no próprio ambiente.`);
252
252
  return 0;
@@ -257,10 +257,10 @@ function listar(ctx, argv) {
257
257
  for (const problema of problemas)
258
258
  ctx.erro(problema);
259
259
  const { config } = readConfig(ctx.cwd);
260
- const doProjeto = config.banco.conexoes.flatMap((nome) => config.banco.projeto[nome] ?? []);
260
+ const doProjeto = config.banco_dados.conexoes.flatMap((nome) => config.banco_dados.projeto[nome] ?? []);
261
261
  const nomes = Object.keys(conexoes).sort();
262
262
  if (nomes.length === 0 && doProjeto.length === 0) {
263
- ctx.log(`Nenhuma conexão cadastrada em ${ctx.cadastro}. Cadastre com: npx @fontdata/ai-harness banco adicionar <nome>`);
263
+ ctx.log(`Nenhuma conexão cadastrada em ${ctx.cadastro}. Cadastre com: npx @fontdata/ai-harness banco-dados adicionar <nome>`);
264
264
  return 0;
265
265
  }
266
266
  for (const nome of nomes) {
@@ -291,16 +291,16 @@ function listar(ctx, argv) {
291
291
  async function testar(ctx, argv) {
292
292
  const { posicionais } = lerArgumentos(argv, []);
293
293
  if (posicionais.length > 1)
294
- throw new Error('Uso: ai-harness banco testar [nome]');
294
+ throw new Error('Uso: ai-harness banco-dados testar [nome]');
295
295
  const [nome] = posicionais;
296
296
  const cadastro = lerCadastro(ctx.cadastro);
297
- const doProjeto = nome !== undefined && nome in readConfig(ctx.cwd).config.banco.projeto;
297
+ const doProjeto = nome !== undefined && nome in readConfig(ctx.cwd).config.banco_dados.projeto;
298
298
  let alvos;
299
299
  let ok = true;
300
300
  if (nome !== undefined && !doProjeto) {
301
301
  const conexao = cadastro.conexoes[nome];
302
302
  if (!conexao) {
303
- ctx.erro(`Conexão ${nome} não encontrada em ${ctx.cadastro}; rode banco adicionar ${nome}.`);
303
+ ctx.erro(`Conexão ${nome} não encontrada em ${ctx.cadastro}; rode banco-dados adicionar ${nome}.`);
304
304
  return 1;
305
305
  }
306
306
  alvos = [{ nome, ...conexao }];
@@ -313,7 +313,7 @@ async function testar(ctx, argv) {
313
313
  for (const problema of resolvidas.problemas)
314
314
  ctx.erro(problema);
315
315
  for (const faltando of resolvidas.faltando.filter((item) => nome === undefined || item === nome)) {
316
- ctx.erro(`Conexão ${faltando} não encontrada nesta máquina; rode banco adicionar ${faltando}.`);
316
+ ctx.erro(`Conexão ${faltando} não encontrada nesta máquina; rode banco-dados adicionar ${faltando}.`);
317
317
  ok = false;
318
318
  }
319
319
  for (const ausente of resolvidas.variaveisAusentes.filter(pedida)) {
@@ -327,7 +327,7 @@ async function testar(ctx, argv) {
327
327
  }
328
328
  alvos = resolvidas.conexoes.filter(pedida);
329
329
  if (alvos.length === 0 && ok) {
330
- ctx.erro('Nenhuma conexão de banco configurada neste projeto; rode npx @fontdata/ai-harness banco usar <nome>.');
330
+ ctx.erro('Nenhuma conexão de banco configurada neste projeto; rode npx @fontdata/ai-harness banco-dados usar <nome>.');
331
331
  return 1;
332
332
  }
333
333
  }
@@ -346,7 +346,7 @@ async function testar(ctx, argv) {
346
346
  function usar(ctx, argv) {
347
347
  const { posicionais: nomes } = lerArgumentos(argv, []);
348
348
  if (nomes.length === 0)
349
- throw new Error('Uso: ai-harness banco usar <nome...>');
349
+ throw new Error('Uso: ai-harness banco-dados usar <nome...>');
350
350
  writeDefaultConfig(ctx.cwd);
351
351
  const arquivo = join(ctx.cwd, CONFIG_PATH);
352
352
  let dados;
@@ -354,11 +354,11 @@ function usar(ctx, argv) {
354
354
  dados = JSON.parse(readFileSync(arquivo, 'utf8'));
355
355
  }
356
356
  catch (erro) {
357
- throw new Error(`${CONFIG_PATH} está com JSON inválido (${erro.message}); corrija antes de rodar banco usar.`);
357
+ throw new Error(`${CONFIG_PATH} está com JSON inválido (${erro.message}); corrija antes de rodar banco-dados usar.`);
358
358
  }
359
359
  if (!isObject(dados))
360
- throw new Error(`${CONFIG_PATH} precisa ser um objeto JSON; corrija antes de rodar banco usar.`);
361
- const banco = isObject(dados.banco) ? dados.banco : {};
360
+ throw new Error(`${CONFIG_PATH} precisa ser um objeto JSON; corrija antes de rodar banco-dados usar.`);
361
+ const banco = secaoBanco(dados);
362
362
  const objetos = new Map();
363
363
  for (const entrada of Array.isArray(banco.conexoes) ? banco.conexoes : []) {
364
364
  if (isObject(entrada) && typeof entrada.nome === 'string' && !objetos.has(entrada.nome))
@@ -366,12 +366,12 @@ function usar(ctx, argv) {
366
366
  }
367
367
  const unicos = [...new Set(nomes)];
368
368
  const proximas = unicos.map((nome) => objetos.get(nome) ?? nome);
369
- writeFileSync(arquivo, `${JSON.stringify({ ...dados, banco: { ...banco, conexoes: proximas } }, null, 2)}\n`);
370
- ctx.log(`banco.conexoes em ${CONFIG_PATH}: ${unicos.join(', ')}.`);
369
+ writeFileSync(arquivo, `${JSON.stringify(comSecaoBanco(dados, { ...banco, conexoes: proximas }), null, 2)}\n`);
370
+ ctx.log(`banco_dados.conexoes em ${CONFIG_PATH}: ${unicos.join(', ')}.`);
371
371
  const { conexoes } = lerCadastro(ctx.cadastro);
372
372
  const ausentes = unicos.filter((nome) => !(nome in conexoes) && !objetos.has(nome));
373
373
  if (ausentes.length > 0) {
374
- ctx.erro(`Aviso: sem cadastro nesta máquina para ${ausentes.join(', ')}; rode banco adicionar <nome> para cada uma.`);
374
+ ctx.erro(`Aviso: sem cadastro nesta máquina para ${ausentes.join(', ')}; rode banco-dados adicionar <nome> para cada uma.`);
375
375
  }
376
376
  return 0;
377
377
  }
@@ -379,7 +379,7 @@ function remover(ctx, argv) {
379
379
  const { posicionais } = lerArgumentos(argv, []);
380
380
  const [nome, ...sobra] = posicionais;
381
381
  if (!nome || sobra.length > 0)
382
- throw new Error('Uso: ai-harness banco remover <nome>');
382
+ throw new Error('Uso: ai-harness banco-dados remover <nome>');
383
383
  if (!removerConexao(nome, ctx.cadastro)) {
384
384
  ctx.erro(`Conexão ${nome} não existe em ${ctx.cadastro}.`);
385
385
  return 1;
@@ -387,7 +387,7 @@ function remover(ctx, argv) {
387
387
  ctx.log(`Conexão ${nome} removida de ${ctx.cadastro}.`);
388
388
  return 0;
389
389
  }
390
- /** `ai-harness banco <subcomando>`: cadastro de conexões da máquina e escolha das conexões do projeto. */
390
+ /** `ai-harness banco-dados <subcomando>`: cadastro de conexões da máquina e escolha das conexões do projeto. */
391
391
  export async function runBancoCommand(argv, opcoes = {}) {
392
392
  const saida = opcoes.saida ?? { log: (texto) => console.log(texto), erro: (texto) => console.error(texto) };
393
393
  const cadastro = opcoes.caminhoCadastro ?? caminhoCadastro();
@@ -420,7 +420,7 @@ export async function runBancoCommand(argv, opcoes = {}) {
420
420
  return 0;
421
421
  default:
422
422
  if (subcomando !== undefined)
423
- ctx.erro(`Subcomando desconhecido: banco ${subcomando}`);
423
+ ctx.erro(`Subcomando desconhecido: banco-dados ${subcomando}`);
424
424
  ctx.erro(AJUDA);
425
425
  return 1;
426
426
  }
@@ -72,7 +72,7 @@ function resolverDoProjeto(declarada, raiz, env, saida) {
72
72
  saida.descartadas.push({ nome, motivo });
73
73
  saida.problemas.push(`Conexão ${nome} descartada: ${motivo}.`);
74
74
  }
75
- /** Percorre banco.conexoes na ordem; sem a lista declarada no projeto, vale o DATABASE_URL do .env como a conexão `env`. */
75
+ /** Percorre banco_dados.conexoes na ordem; sem a lista declarada no projeto, vale o DATABASE_URL do .env como a conexão `env`. */
76
76
  export function resolverConexoes(raiz, opcoes = {}) {
77
77
  const { config, problems } = readConfig(raiz);
78
78
  const saida = {
@@ -83,7 +83,7 @@ export function resolverConexoes(raiz, opcoes = {}) {
83
83
  problemas: [...problems],
84
84
  segredos: [],
85
85
  };
86
- const { conexoes: nomes, projeto, declarado } = config.banco;
86
+ const { conexoes: nomes, projeto, declarado } = config.banco_dados;
87
87
  if (!declarado) {
88
88
  const url = lerDatabaseUrl(raiz);
89
89
  if (url) {
@@ -118,10 +118,10 @@ export function resolverConexoes(raiz, opcoes = {}) {
118
118
  }
119
119
  export function escolherConexao(resolvidas, nome) {
120
120
  if (resolvidas.length === 0) {
121
- throw new Error('Nenhuma conexão de banco configurada neste projeto; rode npx @fontdata/ai-harness banco usar <nome>');
121
+ throw new Error('Nenhuma conexão de banco configurada neste projeto; rode npx @fontdata/ai-harness banco-dados usar <nome>');
122
122
  }
123
123
  const escolhida = nome === undefined ? resolvidas[0] : resolvidas.find((conexao) => conexao.nome === nome);
124
124
  if (!escolhida)
125
- throw new Error(`Conexão ${nome} não está em banco.conexoes deste projeto`);
125
+ throw new Error(`Conexão ${nome} não está em banco_dados.conexoes deste projeto`);
126
126
  return escolhida;
127
127
  }
@@ -6,7 +6,7 @@ import { categoriaDaColuna, mascararResultado, mascararTexto } from './lgpd/apli
6
6
  import { validarLeitura } from './somente-leitura.js';
7
7
  const CONEXAO = {
8
8
  type: 'string',
9
- description: 'Nome da conexão do projeto; sem ele vale a primeira de banco.conexoes.',
9
+ description: 'Nome da conexão do projeto; sem ele vale a primeira de banco_dados.conexoes.',
10
10
  };
11
11
  export const FERRAMENTAS = [
12
12
  {
@@ -42,7 +42,7 @@ export const FERRAMENTAS = [
42
42
  limite: {
43
43
  type: 'integer',
44
44
  minimum: 1,
45
- description: 'Máximo de linhas devolvidas; nunca passa de banco.limite_linhas do projeto.',
45
+ description: 'Máximo de linhas devolvidas; nunca passa de banco_dados.limite_linhas do projeto.',
46
46
  },
47
47
  conexao: CONEXAO,
48
48
  },
@@ -54,17 +54,17 @@ export const FERRAMENTAS = [
54
54
  export const INSTRUCOES = [
55
55
  'Use estas ferramentas para ler o banco de dados do projeto; o acesso é somente leitura.',
56
56
  'Nunca peça nem repita credenciais, senhas ou DSNs no chat.',
57
- 'As conexões vêm do cadastro da máquina ou do projeto: uma conexão do projeto é declarada em banco.conexoes do .ai-harness/config.json com dsn_env (o nome da variável), e o valor da variável fica no .claude/settings.local.json, no .env ou no ambiente.',
58
- 'Sem conexão configurada, oriente o usuário a rodar no terminal `npx @fontdata/ai-harness banco adicionar <nome>` e, no projeto, `npx @fontdata/ai-harness banco usar <nome>`.',
57
+ 'As conexões vêm do cadastro da máquina ou do projeto: uma conexão do projeto é declarada em banco_dados.conexoes do .ai-harness/config.json com dsn_env (o nome da variável), e o valor da variável fica no .claude/settings.local.json, no .env ou no ambiente.',
58
+ 'Sem conexão configurada, oriente o usuário a rodar no terminal `npx @fontdata/ai-harness banco-dados adicionar <nome>` e, no projeto, `npx @fontdata/ai-harness banco-dados usar <nome>`.',
59
59
  'Se a variável de uma conexão do projeto não estiver definida, oriente a defini-la no .claude/settings.local.json, no .env ou no ambiente.',
60
60
  'Antes de consultar, rode listar_tabelas e descrever_tabela para conhecer o esquema.',
61
61
  'Em consultar, selecione só as colunas necessárias e filtre as linhas; evite SELECT *.',
62
62
  'Somente leitura: comandos que alteram dados ou esquema são recusados.',
63
63
  'Os dados pessoais (CPF, e-mail, telefone, nome e afins) vêm mascarados, e o rodapé de consultar diz o que foi mascarado. Não tente contornar o mascaramento: não concatene, fatie, converta nem compare valores para reconstruir o original. Peça só as colunas necessárias.',
64
64
  ].join('\n');
65
- const ORIENTACAO_USAR = 'rode npx @fontdata/ai-harness banco usar <nome> no projeto';
65
+ const ORIENTACAO_USAR = 'rode npx @fontdata/ai-harness banco-dados usar <nome> no projeto';
66
66
  function orientarAdicionar(nome) {
67
- return `Conexão ${nome} não encontrada nesta máquina; rode npx @fontdata/ai-harness banco adicionar ${nome}`;
67
+ return `Conexão ${nome} não encontrada nesta máquina; rode npx @fontdata/ai-harness banco-dados adicionar ${nome}`;
68
68
  }
69
69
  function orientarVariavel(nome, variavel) {
70
70
  return `A variável ${variavel} da conexão ${nome} não está definida; defina-a no .claude/settings.local.json, no .env ou no ambiente`;
@@ -125,7 +125,7 @@ function lerLimite(valor, banco) {
125
125
  }
126
126
  return Math.min(valor, banco.limite_linhas);
127
127
  }
128
- /** Sem nome, vale a primeira declarada em banco.conexoes; se ela não estiver disponível, o erro é dela, nunca outra conexão. */
128
+ /** Sem nome, vale a primeira declarada em banco_dados.conexoes; se ela não estiver disponível, o erro é dela, nunca outra conexão. */
129
129
  function escolher(resolvidas, banco, nome) {
130
130
  const alvo = nome ?? banco.conexoes[0];
131
131
  if (alvo !== undefined) {
@@ -173,7 +173,7 @@ async function executar(nome, args, contexto, banco, resolvidas) {
173
173
  if (descartadas.length > 0)
174
174
  dados.descartadas = descartadas;
175
175
  if (conexoes.length + faltando.length + variaveisAusentes.length + descartadas.length === 0) {
176
- dados.orientacao = `Nenhuma conexão configurada; rode npx @fontdata/ai-harness banco adicionar <nome> e ${ORIENTACAO_USAR}`;
176
+ dados.orientacao = `Nenhuma conexão configurada; rode npx @fontdata/ai-harness banco-dados adicionar <nome> e ${ORIENTACAO_USAR}`;
177
177
  }
178
178
  if (resolvidas.problemas.length > 0)
179
179
  dados.problemas = resolvidas.problemas;
@@ -183,7 +183,7 @@ async function executar(nome, args, contexto, banco, resolvidas) {
183
183
  return erro(`Ferramenta desconhecida: ${nome}`);
184
184
  const conexao = escolher(resolvidas, banco, argumentoTexto(args, 'conexao', false));
185
185
  if (conexao.producao && banco.producao === 'bloqueio') {
186
- return erro(`Conexão ${conexao.nome} é de produção e está bloqueada (banco.producao: "bloqueio"). Use uma cópia ou troque banco.producao para "aviso" em .ai-harness/config.json de propósito.`);
186
+ return erro(`Conexão ${conexao.nome} é de produção e está bloqueada (banco_dados.producao: "bloqueio"). Use uma cópia ou troque banco_dados.producao para "aviso" em .ai-harness/config.json de propósito.`);
187
187
  }
188
188
  let dados;
189
189
  let rodape = '';
@@ -229,7 +229,7 @@ export async function executarFerramenta(nome, args, contexto) {
229
229
  conexoes = resolvidas.conexoes;
230
230
  segredos = resolvidas.segredos;
231
231
  const argumentos = typeof args === 'object' && args !== null && !Array.isArray(args) ? args : {};
232
- const resultado = await executar(nome, argumentos, contexto, config.banco, resolvidas);
232
+ const resultado = await executar(nome, argumentos, contexto, config.banco_dados, resolvidas);
233
233
  const texto = limpar(resultado.content[0].text, conexoes, segredos);
234
234
  resultado.content[0].text = resultado.isError ? mascararTexto(texto) : texto;
235
235
  return resultado;
package/dist/cli.js CHANGED
@@ -5,6 +5,8 @@ import { fileURLToPath } from 'node:url';
5
5
  import { allCommands, loadCatalog } from './catalog.js';
6
6
  import { runCommand, runnableIds, suggestIds } from './run.js';
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ /** Comandos renomeados depois da 2.7.0: o nome antigo aponta o novo em vez de cair em "Comando desconhecido". */
9
+ const RENAMED_COMMANDS = { banco: 'banco-dados', 'banco-mcp': 'banco-dados-mcp' };
8
10
  const RESERVED = new Set([
9
11
  'init',
10
12
  'doctor',
@@ -14,8 +16,8 @@ const RESERVED = new Set([
14
16
  'conflitos',
15
17
  'comentarios',
16
18
  'memoria',
17
- 'banco',
18
- 'banco-mcp',
19
+ 'banco-dados',
20
+ 'banco-dados-mcp',
19
21
  'list',
20
22
  'run',
21
23
  'help',
@@ -49,15 +51,15 @@ Gerenciar:
49
51
  init [caminho] Instala os slash commands em .claude/commands do projeto (interativo)
50
52
  doctor [caminho] Diagnostica a instalação (arquivos, lock, skills, agents, hooks, Node)
51
53
  update [caminho] Atualiza o que foi instalado: --check só lista, --yes aplica, --forcar sobrescreve conflitos
52
- --sem-banco: não registra o MCP de banco no .mcp.json e lembra a recusa no config
54
+ --sem-banco-dados: não registra o MCP de banco no .mcp.json e lembra a recusa no config
53
55
  migrar [caminho] Move docs/specs, plans, bugs e investigacoes para docs/trabalhos/<id>/ (--yes aplica)
54
56
  --reclassificar: planos já migrados em em-execucao viram concluido ou legado
55
57
  comentarios [caminho] Instala o hook de comentários e a catraca do legado (--yes aplica)
56
58
  memoria [caminho] Índice de quem mexeu em cada arquivo (--arquivo <caminho>, --regressao <id do bug>)
57
59
  evento <evento> Grava no rastro do trabalho (--trabalho, --task, --nome, --resultado, --dados '{json}')
58
60
  conflitos <id> Aponta trabalhos em execução que mexem nos mesmos arquivos (--aguardar espera liberar)
59
- banco <subcomando> Conexões de banco somente leitura: adicionar, listar, testar, usar, remover
60
- banco-mcp Servidor MCP de leitura do banco, usado pelo Claude Code (stdio)
61
+ banco-dados <subcomando> Conexões de banco somente leitura: adicionar, listar, testar, usar, remover
62
+ banco-dados-mcp Servidor MCP de leitura do banco, usado pelo Claude Code (stdio)
61
63
  list Lista todos os comandos disponíveis no catálogo
62
64
 
63
65
  Init sem perguntas (CI):
@@ -66,7 +68,7 @@ Init sem perguntas (CI):
66
68
  --sem-hooks não instala os hooks do harness
67
69
  --sem-agents não instala os agents somente leitura
68
70
  --sem-comentarios não instala a rule, o hook e a catraca de comentários
69
- --sem-banco não registra o servidor MCP de banco no .mcp.json
71
+ --sem-banco-dados não registra o servidor MCP de banco no .mcp.json
70
72
  --claude-md criar|manter|backup|sobrescrever (padrão: criar)
71
73
  --conflito sobrescrever|pular (padrão: pular)
72
74
  --yes aplica; sem ele o init só simula
@@ -152,12 +154,12 @@ async function main() {
152
154
  const { runConflictsCommand } = await import('./trabalhos/conflitos.js');
153
155
  process.exit(await runConflictsCommand(argv.slice(1)));
154
156
  }
155
- if (cmd === 'banco') {
156
- const { runBancoCommand } = await import('./banco/command.js');
157
+ if (cmd === 'banco-dados') {
158
+ const { runBancoCommand } = await import('./banco-dados/command.js');
157
159
  process.exit(await runBancoCommand(argv.slice(1)));
158
160
  }
159
- if (cmd === 'banco-mcp') {
160
- const { iniciarServidorMcp } = await import('./banco/mcp.js');
161
+ if (cmd === 'banco-dados-mcp') {
162
+ const { iniciarServidorMcp } = await import('./banco-dados/mcp.js');
161
163
  await iniciarServidorMcp({ entrada: process.stdin, saida: process.stdout });
162
164
  return;
163
165
  }
@@ -180,6 +182,11 @@ async function main() {
180
182
  const { args, claudeFlags } = splitArgs(argv.slice(1));
181
183
  process.exit(runCommand(cmd, args, claudeFlags));
182
184
  }
185
+ const renamed = RENAMED_COMMANDS[cmd];
186
+ if (renamed) {
187
+ console.error(`O comando ${cmd} mudou de nome: use ${renamed}.`);
188
+ process.exit(1);
189
+ }
183
190
  if (!RESERVED.has(cmd)) {
184
191
  const near = suggestIds(cmd.replace(/^\//, ''), ids);
185
192
  console.error(`Comando desconhecido: ${cmd}`);
@@ -16,13 +16,53 @@ export const TIPOS_CONEXAO_PROJETO = ['postgres', 'mariadb', 'sqlite'];
16
16
  function isObject(value) {
17
17
  return typeof value === 'object' && value !== null && !Array.isArray(value);
18
18
  }
19
+ /** Seção do banco no config; até a 2.7.0 ela se chamava `banco`. */
20
+ export const BANCO_CHAVE = 'banco_dados';
21
+ export const BANCO_CHAVE_LEGADA = 'banco';
22
+ /** Só a seção com o nome antigo existe: ela vale e o update a renomeia. */
23
+ function soSecaoLegada(raw) {
24
+ return !(BANCO_CHAVE in raw) && BANCO_CHAVE_LEGADA in raw;
25
+ }
26
+ /** A seção do banco no JSON bruto: `banco_dados`, ou a `banco` antiga quando só ela existe. */
27
+ export function secaoBanco(raw) {
28
+ const secao = soSecaoLegada(raw) ? raw[BANCO_CHAVE_LEGADA] : raw[BANCO_CHAVE];
29
+ return isObject(secao) ? secao : {};
30
+ }
31
+ /** O JSON com a seção do banco gravada como `banco_dados`; a `banco` antiga é trocada na mesma posição. */
32
+ export function comSecaoBanco(raw, secao) {
33
+ if (!soSecaoLegada(raw))
34
+ return { ...raw, [BANCO_CHAVE]: secao };
35
+ return Object.fromEntries(Object.entries(raw).map(([chave, valor]) => (chave === BANCO_CHAVE_LEGADA ? [BANCO_CHAVE, secao] : [chave, valor])));
36
+ }
37
+ function lerJsonBruto(root) {
38
+ try {
39
+ const parsed = JSON.parse(readFileSync(join(root, CONFIG_PATH), 'utf8'));
40
+ return isObject(parsed) ? parsed : undefined;
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ }
46
+ /** O config ainda tem a seção do banco com o nome antigo (`banco`), que o update renomeia. */
47
+ export function temSecaoBancoLegada(root) {
48
+ const raw = lerJsonBruto(root);
49
+ return raw !== undefined && soSecaoLegada(raw);
50
+ }
51
+ /** Renomeia a seção `banco` para `banco_dados`, preservando o resto do JSON; devolve se mudou. */
52
+ export function migrarSecaoBanco(root) {
53
+ const raw = lerJsonBruto(root);
54
+ if (!raw || !soSecaoLegada(raw))
55
+ return false;
56
+ writeFileSync(join(root, CONFIG_PATH), `${JSON.stringify(comSecaoBanco(raw, secaoBanco(raw)), null, 2)}\n`);
57
+ return true;
58
+ }
19
59
  export function defaultConfig() {
20
60
  return {
21
61
  config_versao: CONFIG_VERSION,
22
62
  hooks: { ...DEFAULT_HOOK_MODES },
23
63
  rastro: { versionar: false },
24
64
  bloqueios: { dias_para_aviso: 7 },
25
- banco: {
65
+ banco_dados: {
26
66
  conexoes: [],
27
67
  producao: 'bloqueio',
28
68
  limite_linhas: 200,
@@ -41,23 +81,23 @@ const SO_NO_CADASTRO = 'O mascaramento LGPD só pode ser desligado no cadastro d
41
81
  const REGRA_DE_COLUNA = /^[^.\s]+(\.[^.\s]+){0,2}$/;
42
82
  function lerLgpd(raw, lgpd, problems) {
43
83
  if (!isObject(raw)) {
44
- problems.push(`banco.lgpd deve ser um objeto com mascarar_colunas e liberar_colunas em ${CONFIG_PATH}.`);
84
+ problems.push(`banco_dados.lgpd deve ser um objeto com mascarar_colunas e liberar_colunas em ${CONFIG_PATH}.`);
45
85
  return;
46
86
  }
47
87
  if ('desligar' in raw)
48
- problems.push(`${SO_NO_CADASTRO}; banco.lgpd.desligar em ${CONFIG_PATH} foi ignorado.`);
88
+ problems.push(`${SO_NO_CADASTRO}; banco_dados.lgpd.desligar em ${CONFIG_PATH} foi ignorado.`);
49
89
  for (const chave of ['mascarar_colunas', 'liberar_colunas']) {
50
90
  if (!(chave in raw))
51
91
  continue;
52
92
  const lista = raw[chave];
53
93
  if (!Array.isArray(lista)) {
54
- problems.push(`banco.lgpd.${chave} deve ser uma lista de colunas em ${CONFIG_PATH}.`);
94
+ problems.push(`banco_dados.lgpd.${chave} deve ser uma lista de colunas em ${CONFIG_PATH}.`);
55
95
  continue;
56
96
  }
57
97
  lgpd[chave] = lista.filter((regra) => {
58
98
  if (typeof regra === 'string' && REGRA_DE_COLUNA.test(regra))
59
99
  return true;
60
- problems.push(`Entrada ${JSON.stringify(regra)} ignorada em banco.lgpd.${chave} de ${CONFIG_PATH}; use coluna, tabela.coluna ou schema.tabela.coluna.`);
100
+ problems.push(`Entrada ${JSON.stringify(regra)} ignorada em banco_dados.lgpd.${chave} de ${CONFIG_PATH}; use coluna, tabela.coluna ou schema.tabela.coluna.`);
61
101
  return false;
62
102
  });
63
103
  }
@@ -83,7 +123,7 @@ function motivoConexaoInvalida(entrada) {
83
123
  function lerConexoes(conexoes, banco, problems) {
84
124
  banco.declarado = !(Array.isArray(conexoes) && conexoes.length === 0);
85
125
  if (!Array.isArray(conexoes)) {
86
- problems.push(`banco.conexoes deve ser uma lista de nomes ou de objetos { nome, dsn_env } em ${CONFIG_PATH}.`);
126
+ problems.push(`banco_dados.conexoes deve ser uma lista de nomes ou de objetos { nome, dsn_env } em ${CONFIG_PATH}.`);
87
127
  return;
88
128
  }
89
129
  for (const entrada of conexoes) {
@@ -94,17 +134,17 @@ function lerConexoes(conexoes, banco, problems) {
94
134
  else if (isObject(entrada)) {
95
135
  const motivo = motivoConexaoInvalida(entrada);
96
136
  if (motivo) {
97
- problems.push(`Entrada ${citada} descartada em banco.conexoes de ${CONFIG_PATH}: ${motivo}.`);
137
+ problems.push(`Entrada ${citada} descartada em banco_dados.conexoes de ${CONFIG_PATH}: ${motivo}.`);
98
138
  continue;
99
139
  }
100
140
  nome = entrada.nome;
101
141
  }
102
142
  else {
103
- problems.push(`Entrada ${citada} descartada em banco.conexoes de ${CONFIG_PATH}; use um nome (texto não vazio) ou um objeto { nome, dsn_env }.`);
143
+ problems.push(`Entrada ${citada} descartada em banco_dados.conexoes de ${CONFIG_PATH}; use um nome (texto não vazio) ou um objeto { nome, dsn_env }.`);
104
144
  continue;
105
145
  }
106
146
  if (banco.conexoes.includes(nome)) {
107
- problems.push(`Entrada ${citada} descartada em banco.conexoes de ${CONFIG_PATH}: o nome "${nome}" já foi declarado antes.`);
147
+ problems.push(`Entrada ${citada} descartada em banco_dados.conexoes de ${CONFIG_PATH}: o nome "${nome}" já foi declarado antes.`);
108
148
  continue;
109
149
  }
110
150
  banco.conexoes.push(nome);
@@ -112,8 +152,8 @@ function lerConexoes(conexoes, banco, problems) {
112
152
  continue;
113
153
  for (const chave of Object.keys(entrada).filter((chave) => !CHAVES_CONEXAO_PROJETO.includes(chave))) {
114
154
  problems.push(chave === 'mascaramento'
115
- ? `${SO_NO_CADASTRO}; mascaramento da entrada ${citada} em banco.conexoes de ${CONFIG_PATH} foi ignorado.`
116
- : `Chave desconhecida ${chave} ignorada na entrada ${citada} de banco.conexoes em ${CONFIG_PATH}.`);
155
+ ? `${SO_NO_CADASTRO}; mascaramento da entrada ${citada} em banco_dados.conexoes de ${CONFIG_PATH} foi ignorado.`
156
+ : `Chave desconhecida ${chave} ignorada na entrada ${citada} de banco_dados.conexoes em ${CONFIG_PATH}.`);
117
157
  }
118
158
  banco.projeto[nome] = {
119
159
  nome,
@@ -130,28 +170,28 @@ function lerBanco(raw, banco, problems) {
130
170
  if (MODOS_PRODUCAO.includes(raw.producao))
131
171
  banco.producao = raw.producao;
132
172
  else
133
- problems.push(`Modo "${String(raw.producao)}" inválido para banco.producao em ${CONFIG_PATH}; use aviso ou bloqueio.`);
173
+ problems.push(`Modo "${String(raw.producao)}" inválido para banco_dados.producao em ${CONFIG_PATH}; use aviso ou bloqueio.`);
134
174
  }
135
175
  if ('limite_linhas' in raw) {
136
176
  if (inteiroEntre(raw.limite_linhas, 1, 5000))
137
177
  banco.limite_linhas = raw.limite_linhas;
138
178
  else
139
- problems.push(`banco.limite_linhas deve ser um inteiro de 1 a 5000 em ${CONFIG_PATH}.`);
179
+ problems.push(`banco_dados.limite_linhas deve ser um inteiro de 1 a 5000 em ${CONFIG_PATH}.`);
140
180
  }
141
181
  if ('timeout_segundos' in raw) {
142
182
  if (inteiroEntre(raw.timeout_segundos, 1, 600))
143
183
  banco.timeout_segundos = raw.timeout_segundos;
144
184
  else
145
- problems.push(`banco.timeout_segundos deve ser um inteiro de 1 a 600 em ${CONFIG_PATH}.`);
185
+ problems.push(`banco_dados.timeout_segundos deve ser um inteiro de 1 a 600 em ${CONFIG_PATH}.`);
146
186
  }
147
187
  if ('registrar_mcp' in raw) {
148
188
  if (typeof raw.registrar_mcp === 'boolean')
149
189
  banco.registrar_mcp = raw.registrar_mcp;
150
190
  else
151
- problems.push(`banco.registrar_mcp deve ser true ou false em ${CONFIG_PATH}.`);
191
+ problems.push(`banco_dados.registrar_mcp deve ser true ou false em ${CONFIG_PATH}.`);
152
192
  }
153
193
  if ('mascaramento' in raw)
154
- problems.push(`${SO_NO_CADASTRO}; banco.mascaramento em ${CONFIG_PATH} foi ignorado.`);
194
+ problems.push(`${SO_NO_CADASTRO}; banco_dados.mascaramento em ${CONFIG_PATH} foi ignorado.`);
155
195
  if ('lgpd' in raw)
156
196
  lerLgpd(raw.lgpd, banco.lgpd, problems);
157
197
  }
@@ -192,17 +232,24 @@ export function readConfig(root) {
192
232
  else
193
233
  problems.push(`bloqueios.dias_para_aviso deve ser um inteiro positivo em ${CONFIG_PATH}.`);
194
234
  }
195
- if (isObject(raw.banco))
196
- lerBanco(raw.banco, config.banco, problems);
235
+ const secao = soSecaoLegada(raw) ? raw[BANCO_CHAVE_LEGADA] : raw[BANCO_CHAVE];
236
+ if (isObject(secao))
237
+ lerBanco(secao, config.banco_dados, problems);
238
+ if (soSecaoLegada(raw)) {
239
+ problems.push(`A seção banco de ${CONFIG_PATH} agora se chama banco_dados; rode npx @fontdata/ai-harness update --yes para renomeá-la.`);
240
+ }
241
+ else if (BANCO_CHAVE_LEGADA in raw) {
242
+ problems.push(`A seção banco de ${CONFIG_PATH} foi ignorada: vale a banco_dados. Apague a banco.`);
243
+ }
197
244
  return { config, problems };
198
245
  }
199
- /** Cria a config com os padrões, sem a seção banco; retorna false se já existia (nunca sobrescreve). */
246
+ /** Cria a config com os padrões, sem a seção banco_dados; retorna false se já existia (nunca sobrescreve). */
200
247
  export function writeDefaultConfig(root) {
201
248
  const file = join(root, CONFIG_PATH);
202
249
  if (existsSync(file))
203
250
  return false;
204
251
  mkdirSync(dirname(file), { recursive: true });
205
- const { banco: _banco, ...gravada } = defaultConfig();
252
+ const { banco_dados: _banco, ...gravada } = defaultConfig();
206
253
  writeFileSync(file, `${JSON.stringify(gravada, null, 2)}\n`);
207
254
  return true;
208
255
  }
@@ -222,10 +269,10 @@ function lerJsonParaEscrita(file, chave) {
222
269
  export function validarConfigParaEscrita(root) {
223
270
  const file = join(root, CONFIG_PATH);
224
271
  if (existsSync(file))
225
- lerJsonParaEscrita(file, 'banco.conexoes');
272
+ lerJsonParaEscrita(file, 'banco_dados.conexoes');
226
273
  }
227
274
  /**
228
- * Grava a conexão em `banco.conexoes`, preservando o resto do JSON; sem config, cria uma com os padrões.
275
+ * Grava a conexão em `banco_dados.conexoes`, preservando o resto do JSON; sem config, cria uma com os padrões.
229
276
  * Se o nome já existir (objeto ou string), lança, ou troca a entrada na mesma posição com `substituir`.
230
277
  * JSON inválido é recusado sem tocar no arquivo.
231
278
  */
@@ -234,14 +281,14 @@ export function gravarConexaoProjeto(root, conexao, { substituir = false } = {})
234
281
  if (motivo)
235
282
  throw new Error(`Conexão ${JSON.stringify(conexao.nome)} inválida: ${motivo}.`);
236
283
  const file = join(root, CONFIG_PATH);
237
- const raw = existsSync(file) ? lerJsonParaEscrita(file, 'banco.conexoes') : undefined;
238
- const banco = raw && isObject(raw.banco) ? raw.banco : {};
284
+ const raw = existsSync(file) ? lerJsonParaEscrita(file, 'banco_dados.conexoes') : undefined;
285
+ const banco = raw ? secaoBanco(raw) : {};
239
286
  const conexoes = banco.conexoes === undefined ? [] : banco.conexoes;
240
287
  if (!Array.isArray(conexoes))
241
- throw new Error(`banco.conexoes em ${CONFIG_PATH} não é uma lista; corrija-o antes de gravar a conexão ${conexao.nome}.`);
288
+ throw new Error(`banco_dados.conexoes em ${CONFIG_PATH} não é uma lista; corrija-o antes de gravar a conexão ${conexao.nome}.`);
242
289
  const posicao = conexoes.findIndex((entrada) => (isObject(entrada) ? entrada.nome : entrada) === conexao.nome);
243
290
  if (posicao !== -1 && !substituir)
244
- throw new Error(`A conexão ${conexao.nome} já está em banco.conexoes de ${CONFIG_PATH}.`);
291
+ throw new Error(`A conexão ${conexao.nome} já está em banco_dados.conexoes de ${CONFIG_PATH}.`);
245
292
  const entrada = {
246
293
  nome: conexao.nome,
247
294
  dsn_env: conexao.dsn_env,
@@ -251,28 +298,28 @@ export function gravarConexaoProjeto(root, conexao, { substituir = false } = {})
251
298
  const proximas = posicao === -1 ? [...conexoes, entrada] : conexoes.map((atual, i) => (i === posicao ? entrada : atual));
252
299
  if (!raw)
253
300
  writeDefaultConfig(root);
254
- const base = raw ?? lerJsonParaEscrita(file, 'banco.conexoes');
255
- writeFileSync(file, `${JSON.stringify({ ...base, banco: { ...banco, conexoes: proximas } }, null, 2)}\n`);
301
+ const base = raw ?? lerJsonParaEscrita(file, 'banco_dados.conexoes');
302
+ writeFileSync(file, `${JSON.stringify(comSecaoBanco(base, { ...banco, conexoes: proximas }), null, 2)}\n`);
256
303
  }
257
304
  /**
258
- * Grava só `banco.registrar_mcp`, preservando o resto do JSON; sem config, cria uma com os padrões
305
+ * Grava só `banco_dados.registrar_mcp`, preservando o resto do JSON; sem config, cria uma com os padrões
259
306
  * e essa chave. JSON inválido é recusado sem tocar no arquivo.
260
307
  */
261
308
  export function gravarRegistrarMcp(root, valor) {
262
309
  const file = join(root, CONFIG_PATH);
263
310
  let raw;
264
311
  if (existsSync(file)) {
265
- raw = lerJsonParaEscrita(file, 'banco.registrar_mcp');
312
+ raw = lerJsonParaEscrita(file, 'banco_dados.registrar_mcp');
266
313
  }
267
314
  else {
268
- const { banco: _banco, ...padrao } = defaultConfig();
315
+ const { banco_dados: _banco, ...padrao } = defaultConfig();
269
316
  raw = padrao;
270
317
  }
271
- const banco = isObject(raw.banco) ? raw.banco : {};
318
+ const banco = secaoBanco(raw);
272
319
  if (banco.registrar_mcp === valor)
273
320
  return;
274
321
  mkdirSync(dirname(file), { recursive: true });
275
- writeFileSync(file, `${JSON.stringify({ ...raw, banco: { ...banco, registrar_mcp: valor } }, null, 2)}\n`);
322
+ writeFileSync(file, `${JSON.stringify(comSecaoBanco(raw, { ...banco, registrar_mcp: valor }), null, 2)}\n`);
276
323
  }
277
324
  const BLOCK_START = '# ai-harness (gerenciado pelo harness; não edite entre estas linhas)';
278
325
  const BLOCK_END = '# /ai-harness';
@@ -1,8 +1,8 @@
1
1
  import { execFileSync, spawnSync } from 'node:child_process';
2
2
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { lerEnvSettingsLocal, lerVariavel, SETTINGS_LOCAL_PATH } from '../banco/ambiente.js';
5
- import { caminhoCadastro, lerCadastro } from '../banco/cadastro.js';
4
+ import { lerEnvSettingsLocal, lerVariavel, SETTINGS_LOCAL_PATH } from '../banco-dados/ambiente.js';
5
+ import { caminhoCadastro, lerCadastro } from '../banco-dados/cadastro.js';
6
6
  import { allCommands, loadCatalog } from '../catalog.js';
7
7
  import { CONFIG_PATH, readConfig } from '../config/config.js';
8
8
  import { readArtifact, validateAllWorks, WORKS_DIR } from '../contratos/artefatos.js';
@@ -414,7 +414,7 @@ function settingsLocalIgnorado(root, env) {
414
414
  .some((linha) => LINHAS_IGNORE_SETTINGS_LOCAL.has(linha));
415
415
  }
416
416
  function checkBancoProjeto(root, config, env) {
417
- const conexoes = config.banco.conexoes.flatMap((nome) => config.banco.projeto[nome] ?? []);
417
+ const conexoes = config.banco_dados.conexoes.flatMap((nome) => config.banco_dados.projeto[nome] ?? []);
418
418
  if (!conexoes.length)
419
419
  return [];
420
420
  const findings = [];
@@ -424,8 +424,8 @@ function checkBancoProjeto(root, config, env) {
424
424
  findings.push({
425
425
  id: 'banco-variavel-ausente',
426
426
  severidade: 'aviso',
427
- mensagem: `A conexão ${nome}, declarada em banco.conexoes de ${join(root, CONFIG_PATH)}, lê o DSN da variável ${dsn_env}, que não está definida no ambiente, em ${SETTINGS_LOCAL_PATH} nem no .env.`,
428
- correcao: `Defina ${dsn_env} com \`npx @fontdata/ai-harness banco adicionar ${nome} --projeto\` ou no ambiente do terminal.`,
427
+ mensagem: `A conexão ${nome}, declarada em banco_dados.conexoes de ${join(root, CONFIG_PATH)}, lê o DSN da variável ${dsn_env}, que não está definida no ambiente, em ${SETTINGS_LOCAL_PATH} nem no .env.`,
428
+ correcao: `Defina ${dsn_env} com \`npx @fontdata/ai-harness banco-dados adicionar ${nome} --projeto\` ou no ambiente do terminal.`,
429
429
  });
430
430
  }
431
431
  const envLocal = lerEnvSettingsLocal(root);
@@ -453,12 +453,12 @@ function checkBancoProjeto(root, config, env) {
453
453
  }
454
454
  return findings;
455
455
  }
456
- /** Confere o cadastro desta máquina e as variáveis das conexões do projeto contra `banco.conexoes`, sem abrir conexão. */
456
+ /** Confere o cadastro desta máquina e as variáveis das conexões do projeto contra `banco_dados.conexoes`, sem abrir conexão. */
457
457
  export function checkBanco(root, config, env = process.env) {
458
- if (!config.banco.conexoes.length)
458
+ if (!config.banco_dados.conexoes.length)
459
459
  return [];
460
460
  const findings = checkBancoProjeto(root, config, env);
461
- const doCadastro = config.banco.conexoes.filter((nome) => !(nome in config.banco.projeto));
461
+ const doCadastro = config.banco_dados.conexoes.filter((nome) => !(nome in config.banco_dados.projeto));
462
462
  if (!doCadastro.length)
463
463
  return findings;
464
464
  const caminho = caminhoCadastro(env);
@@ -486,8 +486,8 @@ export function checkBanco(root, config, env = process.env) {
486
486
  findings.push({
487
487
  id: 'banco-conexao-ausente',
488
488
  severidade: 'aviso',
489
- mensagem: `A conexão ${nome}, citada em banco.conexoes de ${join(root, CONFIG_PATH)}, não está no cadastro desta máquina (${caminho}).`,
490
- correcao: `Cadastre-a com \`npx @fontdata/ai-harness banco adicionar ${nome}\`.`,
489
+ mensagem: `A conexão ${nome}, citada em banco_dados.conexoes de ${join(root, CONFIG_PATH)}, não está no cadastro desta máquina (${caminho}).`,
490
+ correcao: `Cadastre-a com \`npx @fontdata/ai-harness banco-dados adicionar ${nome}\`.`,
491
491
  });
492
492
  }
493
493
  for (const nome of doCadastro.filter((nome) => conexoes[nome]?.mascaramento === false)) {
package/dist/init.js CHANGED
@@ -14,7 +14,7 @@ const PLUGIN_HOOKS_NOTE = 'O plugin fontdata@ai-harness está habilitado: os hoo
14
14
  const PLUGIN_MCP_NOTE = 'O plugin fontdata@ai-harness está habilitado: o servidor MCP de banco já roda por ele e não foi registrado no .mcp.json, para não duplicar.';
15
15
  /** Registra o MCP de banco no .mcp.json quando não foi recusado (flag, wizard ou config) e o plugin não o fornece. */
16
16
  export function decidirRegistroMcp(options, root, home = homedir()) {
17
- return options.mcpBanco && readConfig(root).config.banco.registrar_mcp && !isHarnessPluginEnabled(root, home);
17
+ return options.mcpBanco && readConfig(root).config.banco_dados.registrar_mcp && !isHarnessPluginEnabled(root, home);
18
18
  }
19
19
  function applyMcpDecision(plan, options, root) {
20
20
  plan.registerMcp = decidirRegistroMcp(options, root);
@@ -7,7 +7,7 @@ const CLAUDE_MD = {
7
7
  };
8
8
  const CONFLICTS = { sobrescrever: 'overwrite', pular: 'skip' };
9
9
  const VALUE_FLAGS = ['--escopo', '--comandos', '--claude-md', '--conflito'];
10
- const BOOLEAN_FLAGS = ['--sem-harness', '--sem-hooks', '--sem-agents', '--sem-comentarios', '--sem-banco', '--yes', '-y', '--sim'];
10
+ const BOOLEAN_FLAGS = ['--sem-harness', '--sem-hooks', '--sem-agents', '--sem-comentarios', '--sem-banco-dados', '--yes', '-y', '--sim'];
11
11
  function pick(flag, value, options) {
12
12
  const chosen = options[value];
13
13
  if (chosen === undefined) {
@@ -28,8 +28,13 @@ function distance(a, b) {
28
28
  }
29
29
  return row[b.length] ?? 0;
30
30
  }
31
+ /** Flags que mudaram de nome: o erro aponta a nova, que a distância de edição não alcança. */
32
+ const RENAMED_FLAGS = { '--sem-banco': '--sem-banco-dados' };
31
33
  /** Flag conhecida mais parecida com a digitada, para sugerir em mensagens de erro. */
32
34
  export function suggestFlag(token, flags) {
35
+ const renamed = RENAMED_FLAGS[token];
36
+ if (renamed && flags.includes(renamed))
37
+ return renamed;
33
38
  const [best] = flags
34
39
  .map((flag) => ({ flag, d: distance(token, flag) }))
35
40
  .filter(({ d }) => d <= 2)
@@ -69,7 +74,7 @@ export function parseInitArgs(argv) {
69
74
  args.agents = false;
70
75
  else if (token === '--sem-comentarios')
71
76
  args.comments = false;
72
- else if (token === '--sem-banco')
77
+ else if (token === '--sem-banco-dados')
73
78
  args.mcpBanco = false;
74
79
  else
75
80
  args.yes = true;
@@ -2,13 +2,17 @@ import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { timestamp } from './settings.js';
4
4
  export const MCP_PATH = '.mcp.json';
5
+ /** Nome do servidor de banco no `.mcp.json`; as ferramentas ficam `mcp__banco_dados__*`. */
6
+ export const BANCO_MCP_NOME = 'banco_dados';
7
+ /** Nome do servidor até a 2.7.0; a entrada gerenciada com ele é trocada pela nova no update. */
8
+ export const BANCO_MCP_NOME_LEGADO = 'banco';
5
9
  const PIN_PREFIX = '@fontdata/ai-harness@';
6
10
  function isPlainObject(value) {
7
11
  return typeof value === 'object' && value !== null && !Array.isArray(value);
8
12
  }
9
13
  /** Servidor MCP de banco com a versão do pacote fixada no npx, no mesmo formato do plugin. */
10
14
  export function bancoMcpServer(versao) {
11
- return { command: 'npx', args: ['-y', `${PIN_PREFIX}${versao}`, 'banco-mcp'] };
15
+ return { command: 'npx', args: ['-y', `${PIN_PREFIX}${versao}`, 'banco-dados-mcp'] };
12
16
  }
13
17
  function pinOf(entrada) {
14
18
  if (!isPlainObject(entrada) || !Array.isArray(entrada.args))
@@ -22,9 +26,10 @@ export function ehEntradaGerenciada(entrada) {
22
26
  /**
23
27
  * Acrescenta os servidores ausentes em `mcpServers` do `.mcp.json`, preservando o resto. Com
24
28
  * `atualizarGerenciada`, regrava também a entrada gerenciada que fixa outra versão; entrada
25
- * personalizada nunca é tocada. Backup datado só quando muda; JSON inválido é recusado sem tocar.
29
+ * personalizada nunca é tocada. Os nomes de `removerGerenciadas` saem só quando a entrada é gerenciada. Backup
30
+ * datado só quando muda; JSON inválido é recusado sem tocar.
26
31
  */
27
- export function mergeMcpServers(root, servidores, now = new Date(), { atualizarGerenciada = false } = {}) {
32
+ export function mergeMcpServers(root, servidores, now = new Date(), { atualizarGerenciada = false, removerGerenciadas = [] } = {}) {
28
33
  const file = join(root, MCP_PATH);
29
34
  const original = existsSync(file) ? readFileSync(file, 'utf8') : null;
30
35
  let config = {};
@@ -43,6 +48,12 @@ export function mergeMcpServers(root, servidores, now = new Date(), { atualizarG
43
48
  }
44
49
  const mcpServers = { ...(config.mcpServers ?? {}) };
45
50
  let changed = false;
51
+ for (const nome of removerGerenciadas) {
52
+ if (!ehEntradaGerenciada(mcpServers[nome]))
53
+ continue;
54
+ delete mcpServers[nome];
55
+ changed = true;
56
+ }
46
57
  for (const [nome, servidor] of Object.entries(servidores)) {
47
58
  const atual = mcpServers[nome];
48
59
  if (atual === undefined) {
@@ -6,10 +6,10 @@ import { applyAtomically } from './atomic.js';
6
6
  import { buildLockItem, LOCK_VERSION, readLock, writeLock } from './lock.js';
7
7
  import { HOOKS_ITEM_ID, PROJECT_HOOKS_DIR, projectHooksConfig } from './hooks.js';
8
8
  import { neededSkillsFor, resolveSelection } from './selection.js';
9
- import { bancoMcpServer, MCP_PATH, mergeMcpServers } from './mcp.js';
9
+ import { BANCO_MCP_NOME, bancoMcpServer, MCP_PATH, mergeMcpServers } from './mcp.js';
10
10
  import { gravarRegistrarMcp } from '../config/config.js';
11
11
  import { COMMENTS_RULE_PATH, commentsHooksConfig, ensureCommentsBaseline } from '../comentarios/instalar.js';
12
- import { combineHooks, HARNESS_DENY, HARNESS_PERMISSIONS, mergeSettings } from './settings.js';
12
+ import { combineHooks, HARNESS_DENY, harnessPermissions, mergeSettings } from './settings.js';
13
13
  /** Rule de convenção com o que é específico do projeto (D-38): é do projeto, então fica fora do lock. */
14
14
  export const PROJECT_RULE_PATH = '.claude/rules/harness-projeto.md';
15
15
  /** Esqueleto da rule de projeto; os comandos `-legado` renomeados pelo update entram na última seção. */
@@ -165,12 +165,12 @@ export function executePlan(root, plan, pkg, now = new Date()) {
165
165
  const permission = needsEventPermission(plan);
166
166
  const hooks = combineHooks(plan.registerHooks ? projectHooksConfig() : {}, plan.registerComments ? commentsHooksConfig() : {});
167
167
  if (Object.keys(hooks).length || permission) {
168
- mergeSettings(root, { hooks, allow: permission ? HARNESS_PERMISSIONS : [], deny: permission ? HARNESS_DENY : [] }, now);
168
+ mergeSettings(root, { hooks, allow: permission ? harnessPermissions({ banco: bancoAtivo(plan) }) : [], deny: permission ? HARNESS_DENY : [] }, now);
169
169
  }
170
170
  if (plan.registerComments)
171
171
  ensureCommentsBaseline(root);
172
172
  if (plan.registerMcp)
173
- mergeMcpServers(root, { banco: bancoMcpServer(pkg.versao) }, now);
173
+ mergeMcpServers(root, { [BANCO_MCP_NOME]: bancoMcpServer(pkg.versao) }, now);
174
174
  if (plan.mcpRecusado)
175
175
  gravarRegistrarMcp(root, false);
176
176
  const items = new Map();
@@ -193,6 +193,10 @@ export function executePlan(root, plan, pkg, now = new Date()) {
193
193
  writeLock(root, lock);
194
194
  return lock;
195
195
  }
196
+ /** O banco não foi recusado (`--sem-banco-dados` ou `banco_dados.registrar_mcp: false`): as permissões do MCP entram. */
197
+ function bancoAtivo(plan) {
198
+ return !plan.mcpRecusado && plan.mcpSkipReason !== 'config';
199
+ }
196
200
  /** Skills e agents do harness registram o rastro com `npx @fontdata/ai-harness evento`. */
197
201
  function needsEventPermission(plan) {
198
202
  return plan.items.some((item) => item.tipo === 'skill' || item.tipo === 'agent');
@@ -209,7 +213,7 @@ export function describePlan(plan) {
209
213
  if (plan.registerHooks)
210
214
  lines.push('Hooks do harness registrados em .claude/settings.json (sem duplicar os existentes).');
211
215
  if (needsEventPermission(plan)) {
212
- lines.push(`Permissões ${HARNESS_PERMISSIONS.join(', ')} em .claude/settings.json, para o rastro, a checagem de conflitos, ` +
216
+ lines.push(`Permissões ${harnessPermissions({ banco: bancoAtivo(plan) }).join(', ')} em .claude/settings.json, para o rastro, a checagem de conflitos, ` +
213
217
  'o /git-commit-push, os sub-agents e as consultas de leitura não pedirem aprovação (inclusive no modo auto); ' +
214
218
  'o push forçado fica bloqueado em permissions.deny.');
215
219
  }
@@ -217,13 +221,13 @@ export function describePlan(plan) {
217
221
  lines.push(`Servidor MCP de banco (somente leitura) registrado em ${MCP_PATH}, sem duplicar os servidores existentes.`);
218
222
  }
219
223
  else if (plan.mcpRecusado) {
220
- lines.push(`Servidor MCP de banco não registrado (recusado); a escolha fica em banco.registrar_mcp: false no .ai-harness/config.json.`);
224
+ lines.push(`Servidor MCP de banco não registrado (recusado); a escolha fica em banco_dados.registrar_mcp: false no .ai-harness/config.json.`);
221
225
  }
222
226
  else if (plan.mcpSkipReason === 'plugin') {
223
227
  lines.push(`Servidor MCP de banco não registrado em ${MCP_PATH}: o plugin fontdata@ai-harness já o fornece.`);
224
228
  }
225
229
  else if (plan.mcpSkipReason === 'config') {
226
- lines.push(`Servidor MCP de banco não registrado em ${MCP_PATH}: banco.registrar_mcp está false no .ai-harness/config.json.`);
230
+ lines.push(`Servidor MCP de banco não registrado em ${MCP_PATH}: banco_dados.registrar_mcp está false no .ai-harness/config.json.`);
227
231
  }
228
232
  if (plan.registerComments) {
229
233
  lines.push('Padrão de comentários: rule carregada em toda sessão, hook que bloqueia a violação e catraca do legado (gerada só na primeira instalação).');
@@ -33,11 +33,14 @@ export const READ_PERMISSIONS = [
33
33
  'Bash(gh pr diff:*)',
34
34
  'Bash(npm outdated:*)',
35
35
  ];
36
+ const BANCO_TOOLS = ['listar_conexoes', 'listar_tabelas', 'descrever_tabela', 'consultar'];
36
37
  /**
37
- * Ferramentas de esquema do servidor MCP de banco, registrado pelo `.mcp.json` ou pelo plugin. A `consultar`
38
- * fica de fora: lê linhas das tabelas e continua pedindo aprovação.
38
+ * Ferramentas do servidor MCP de banco, registrado pelo `.mcp.json` (`banco_dados`) ou pelo plugin
39
+ * (`banco_dados`). A `consultar` também é liberada: os dados pessoais chegam mascarados.
39
40
  */
40
- export const BANCO_PERMISSIONS = ['mcp__banco__', 'mcp__plugin_fontdata_banco__'].flatMap((prefixo) => ['listar_conexoes', 'listar_tabelas', 'descrever_tabela', 'consultar'].map((ferramenta) => `${prefixo}${ferramenta}`));
41
+ export const BANCO_PERMISSIONS = ['mcp__banco_dados__', 'mcp__plugin_fontdata_banco_dados__'].flatMap((prefixo) => BANCO_TOOLS.map((ferramenta) => `${prefixo}${ferramenta}`));
42
+ /** As mesmas regras com o nome antigo do servidor (`banco`, até a 2.7.0); o update as remove. */
43
+ export const BANCO_PERMISSIONS_LEGADAS = ['mcp__banco__', 'mcp__plugin_fontdata_banco__'].flatMap((prefixo) => BANCO_TOOLS.map((ferramenta) => `${prefixo}${ferramenta}`));
41
44
  /** Regras de `permissions.allow` que o init e o update gravam para as skills e os agents do harness. */
42
45
  export const HARNESS_PERMISSIONS = [
43
46
  EVENT_PERMISSION,
@@ -48,6 +51,10 @@ export const HARNESS_PERMISSIONS = [
48
51
  ...READ_PERMISSIONS,
49
52
  ...BANCO_PERMISSIONS,
50
53
  ];
54
+ /** As regras do harness; sem o banco (recusado com `--sem-banco-dados` ou `banco_dados.registrar_mcp: false`), sem as do MCP. */
55
+ export function harnessPermissions({ banco }) {
56
+ return banco ? HARNESS_PERMISSIONS : HARNESS_PERMISSIONS.filter((rule) => !BANCO_PERMISSIONS.includes(rule));
57
+ }
51
58
  /** O push liberado não vale para reescrever o histórico remoto: `deny` vence o `allow`. */
52
59
  export const HARNESS_DENY = [
53
60
  'Bash(git push --force:*)',
@@ -92,7 +99,7 @@ export function mergeHooksIntoSettings(root, hooks, now = new Date()) {
92
99
  * Acrescenta hooks e regras de `permissions.allow` ao `.claude/settings.json` do projeto
93
100
  * sem perder o que já existe. Um hook é considerado presente quando algum grupo do mesmo
94
101
  * evento já tem o mesmo `command`, e uma regra quando já está na lista, o que torna a
95
- * operação idempotente. Tudo é gravado de uma vez, com um único backup datado do arquivo
102
+ * operação idempotente. As regras de `removeAllow` saem de `permissions.allow`. Tudo é gravado de uma vez, com um único backup datado do arquivo
96
103
  * original; JSON inválido é recusado sem tocar em nada.
97
104
  */
98
105
  export function mergeSettings(root, additions, now = new Date()) {
@@ -155,6 +162,12 @@ export function mergeSettings(root, additions, now = new Date()) {
155
162
  }
156
163
  next.permissions = { ...permissions, [key]: list };
157
164
  }
165
+ const remove = additions.removeAllow ?? [];
166
+ const currentAllow = next.permissions?.allow;
167
+ if (remove.length && currentAllow?.some((rule) => remove.includes(rule))) {
168
+ next.permissions = { ...next.permissions, allow: currentAllow.filter((rule) => !remove.includes(rule)) };
169
+ changed = true;
170
+ }
158
171
  if (!changed)
159
172
  return { changed: false, backup: null };
160
173
  let backup = null;
@@ -2,15 +2,15 @@ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync
2
2
  import { dirname, join, resolve } from 'node:path';
3
3
  import { allCommands, loadCatalog } from '../catalog.js';
4
4
  import { commentsHooksConfig, ensureCommentsBaseline } from '../comentarios/instalar.js';
5
- import { CONFIG_PATH, gravarRegistrarMcp, readConfig, syncGitignore, writeDefaultConfig } from '../config/config.js';
5
+ import { CONFIG_PATH, gravarRegistrarMcp, migrarSecaoBanco, readConfig, syncGitignore, temSecaoBancoLegada, writeDefaultConfig, } from '../config/config.js';
6
6
  import { applyAtomically } from '../install/atomic.js';
7
7
  import { suggestFlag } from '../install/flags.js';
8
8
  import { HOOKS_ITEM_ID, isHarnessPluginEnabled, projectHooksConfig } from '../install/hooks.js';
9
- import { bancoMcpServer, ehEntradaGerenciada, MCP_PATH, mergeMcpServers } from '../install/mcp.js';
9
+ import { BANCO_MCP_NOME, BANCO_MCP_NOME_LEGADO, bancoMcpServer, ehEntradaGerenciada, MCP_PATH, mergeMcpServers } from '../install/mcp.js';
10
10
  import { hashContent, LOCK_PATH, LOCK_VERSION, readLock, writeLock } from '../install/lock.js';
11
11
  import { itemSources, packageInfo, PROJECT_RULE_PATH, projectRuleContent } from '../install/plan.js';
12
12
  import { neededSkillsFor } from '../install/selection.js';
13
- import { combineHooks, HARNESS_DENY, HARNESS_PERMISSIONS, hasAllowRule, mergeSettings } from '../install/settings.js';
13
+ import { BANCO_PERMISSIONS_LEGADAS, combineHooks, HARNESS_DENY, harnessPermissions, hasAllowRule, mergeSettings, SETTINGS_PATH, } from '../install/settings.js';
14
14
  import { adoptLegacyInstall } from './legado.js';
15
15
  function isPlainObject(value) {
16
16
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -32,7 +32,9 @@ function mcpChange(root, versao) {
32
32
  const servers = parsed.mcpServers === undefined ? {} : parsed.mcpServers;
33
33
  if (!isPlainObject(servers))
34
34
  return 'invalido';
35
- const atual = servers.banco;
35
+ if (ehEntradaGerenciada(servers[BANCO_MCP_NOME_LEGADO]))
36
+ return 'renomear';
37
+ const atual = servers[BANCO_MCP_NOME];
36
38
  if (atual === undefined)
37
39
  return 'faltando';
38
40
  const pin = bancoMcpServer(versao).args.find((arg) => arg.startsWith('@fontdata/'));
@@ -136,11 +138,13 @@ export function checkUpdate(root, catalog = loadCatalog(), legacy, home, { semBa
136
138
  addItem('rule', 'comentarios', true);
137
139
  addItem('hook', 'comentarios', true);
138
140
  }
141
+ const registrarMcpConfig = readConfig(root).config.banco_dados.registrar_mcp;
142
+ const permissoesBanco = !semBanco && registrarMcpConfig;
139
143
  const permissoesFaltando = [
140
- ...HARNESS_PERMISSIONS.filter((rule) => !hasAllowRule(root, rule)),
144
+ ...harnessPermissions({ banco: permissoesBanco }).filter((rule) => !hasAllowRule(root, rule)),
141
145
  ...HARNESS_DENY.filter((rule) => !hasAllowRule(root, rule, 'deny')).map((rule) => `deny ${rule}`),
142
146
  ];
143
- const registrarMcpConfig = readConfig(root).config.banco.registrar_mcp;
147
+ const permissoesLegadas = BANCO_PERMISSIONS_LEGADAS.filter((rule) => hasAllowRule(root, rule));
144
148
  const registrarMcp = !semBanco && registrarMcpConfig && !isHarnessPluginEnabled(root, home);
145
149
  return {
146
150
  changes,
@@ -151,9 +155,12 @@ export function checkUpdate(root, catalog = loadCatalog(), legacy, home, { semBa
151
155
  registrarComentarios: adotada,
152
156
  permissaoAusente: permissoesFaltando.length > 0,
153
157
  permissoesFaltando,
158
+ permissoesLegadas,
159
+ permissoesBanco,
154
160
  registrarMcp,
155
161
  mudancaMcp: registrarMcp ? mcpChange(root, packageInfo().versao) : null,
156
162
  recusarMcp: semBanco && registrarMcpConfig,
163
+ renomearSecaoBanco: temSecaoBancoLegada(root),
157
164
  };
158
165
  }
159
166
  /**
@@ -192,7 +199,10 @@ export function applyUpdate(root, report, options, pkg, now = new Date()) {
192
199
  return { de: path, para };
193
200
  });
194
201
  const hooks = combineHooks(report.registrarHooks ? projectHooksConfig() : {}, report.registrarComentarios ? commentsHooksConfig() : {});
195
- mergeSettings(root, { hooks, allow: HARNESS_PERMISSIONS, deny: HARNESS_DENY }, now);
202
+ const ajustes = [];
203
+ const settings = mergeSettings(root, { hooks, allow: harnessPermissions({ banco: report.permissoesBanco }), deny: HARNESS_DENY, removeAllow: BANCO_PERMISSIONS_LEGADAS }, now);
204
+ if (settings.changed)
205
+ ajustes.push(`${SETTINGS_PATH} atualizado${settings.backup ? ` (backup em ${settings.backup})` : ''}.`);
196
206
  if (report.registrarComentarios)
197
207
  ensureCommentsBaseline(root);
198
208
  if (report.adotada) {
@@ -211,14 +221,23 @@ export function applyUpdate(root, report, options, pkg, now = new Date()) {
211
221
  else if (report.registrarMcp) {
212
222
  // Uma falha no .mcp.json nunca deixa o update pela metade, sem lock.
213
223
  try {
214
- mergeMcpServers(root, { banco: bancoMcpServer(pkg.versao) }, now, { atualizarGerenciada: true });
224
+ const mcp = mergeMcpServers(root, { [BANCO_MCP_NOME]: bancoMcpServer(pkg.versao) }, now, {
225
+ atualizarGerenciada: true,
226
+ removerGerenciadas: [BANCO_MCP_NOME_LEGADO],
227
+ });
228
+ if (mcp.changed)
229
+ ajustes.push(`${MCP_PATH} com mcpServers.${BANCO_MCP_NOME} na versão ${pkg.versao}${mcp.backup ? ` (backup em ${mcp.backup})` : ''}.`);
215
230
  }
216
231
  catch (error) {
217
232
  avisos.push(error instanceof Error ? error.message : String(error));
218
233
  }
219
234
  }
220
- if (report.recusarMcp)
235
+ if (report.renomearSecaoBanco && migrarSecaoBanco(root))
236
+ ajustes.push(`seção banco renomeada para banco_dados em ${CONFIG_PATH}.`);
237
+ if (report.recusarMcp) {
221
238
  gravarRegistrarMcp(root, false);
239
+ ajustes.push(`banco_dados.registrar_mcp: false gravado em ${CONFIG_PATH}.`);
240
+ }
222
241
  const itemOf = new Map(report.changes.map((change) => [change.caminho, change.item]));
223
242
  const writtenHash = new Map(write.map((file) => [file.path, hashContent(file.content)]));
224
243
  const dropped = new Set([...remove, ...report.stale, ...legacy]);
@@ -252,13 +271,14 @@ export function applyUpdate(root, report, options, pkg, now = new Date()) {
252
271
  itens.push({ ...added, arquivos: added.arquivos.sort((a, b) => a.caminho.localeCompare(b.caminho)) });
253
272
  }
254
273
  writeLock(root, { lock_versao: LOCK_VERSION, pacote: pkg, gerado_em: now.toISOString(), itens });
255
- return { written: write.map((file) => file.path), removed: remove, kept, renamed, avisos };
274
+ return { written: write.map((file) => file.path), removed: remove, kept, renamed, avisos, ajustes };
256
275
  }
257
276
  const MCP_INVALIDO = `O ${MCP_PATH} está com JSON inválido: o registro do MCP de banco foi pulado e o arquivo não foi tocado. Corrija-o e rode o update de novo.`;
258
277
  const MCP_DESCRIPTIONS = {
259
278
  ausente: 'não existe',
260
- faltando: 'não tem a entrada banco',
261
- versao: 'fixa outra versão do servidor banco',
279
+ faltando: `não tem a entrada ${BANCO_MCP_NOME}`,
280
+ versao: `fixa outra versão do servidor ${BANCO_MCP_NOME}`,
281
+ renomear: `tem a entrada ${BANCO_MCP_NOME_LEGADO} do harness, com o nome antigo; ela é trocada pela ${BANCO_MCP_NOME}`,
262
282
  };
263
283
  const DESCRIPTIONS = {
264
284
  novo: 'novo no pacote, será gravado',
@@ -283,27 +303,32 @@ export function describeUpdate(report) {
283
303
  `Faltam permissões do harness em .claude/settings.json (${report.permissoesFaltando.join(', ')}); o --yes as acrescenta.`,
284
304
  ]
285
305
  : [];
306
+ const legadas = report.permissoesLegadas.length
307
+ ? [`Permissões do MCP de banco com o nome antigo em .claude/settings.json (${report.permissoesLegadas.join(', ')}); o --yes as remove.`]
308
+ : [];
286
309
  const mcp = [
287
310
  ...(report.mudancaMcp === 'invalido'
288
311
  ? [`O ${MCP_PATH} está com JSON inválido; o --yes pula o registro do MCP de banco e não toca no arquivo.`]
289
312
  : report.mudancaMcp
290
- ? [`O ${MCP_PATH} ${MCP_DESCRIPTIONS[report.mudancaMcp]}; o --yes grava mcpServers.banco na versão ${packageInfo().versao}.`]
313
+ ? [`O ${MCP_PATH} ${MCP_DESCRIPTIONS[report.mudancaMcp]}; o --yes grava mcpServers.${BANCO_MCP_NOME} na versão ${packageInfo().versao}.`]
291
314
  : []),
292
- ...(report.recusarMcp ? [`--sem-banco: o --yes grava banco.registrar_mcp: false em ${CONFIG_PATH} e não registra o MCP.`] : []),
315
+ ...(report.renomearSecaoBanco ? [`A seção banco de ${CONFIG_PATH} agora se chama banco_dados; o --yes a renomeia.`] : []),
316
+ ...(report.recusarMcp ? [`--sem-banco-dados: o --yes grava banco_dados.registrar_mcp: false em ${CONFIG_PATH} e não registra o MCP.`] : []),
293
317
  ];
294
318
  if (!report.changes.length) {
295
- const status = adopted.length || permission.length || mcp.length ? 'Nenhum arquivo a atualizar.' : 'A instalação está em dia: nenhum arquivo a atualizar.';
296
- return [...adopted, ...permission, ...mcp, status].join('\n');
319
+ const status = adopted.length || permission.length || legadas.length || mcp.length ? 'Nenhum arquivo a atualizar.' : 'A instalação está em dia: nenhum arquivo a atualizar.';
320
+ return [...adopted, ...permission, ...legadas, ...mcp, status].join('\n');
297
321
  }
298
322
  return [
299
323
  ...adopted,
300
324
  ...permission,
325
+ ...legadas,
301
326
  ...mcp,
302
327
  `Mudanças em relação ao pacote ${packageInfo().versao} (instalado: ${report.lock.pacote.versao}):`,
303
328
  ...report.changes.map((change) => ` ${change.tipo.padEnd(16)} ${change.caminho}${change.destino ? ` → ${change.destino}` : ''} (${DESCRIPTIONS[change.tipo]})`),
304
329
  ].join('\n');
305
330
  }
306
- const UPDATE_FLAGS = ['--check', '--yes', '-y', '--sim', '--forcar', '--sem-banco'];
331
+ const UPDATE_FLAGS = ['--check', '--yes', '-y', '--sim', '--forcar', '--sem-banco-dados'];
307
332
  export function parseUpdateArgs(argv) {
308
333
  const args = { path: undefined, check: false, yes: false, force: false, semBanco: false };
309
334
  for (const token of argv) {
@@ -316,7 +341,7 @@ export function parseUpdateArgs(argv) {
316
341
  args.check = true;
317
342
  else if (token === '--forcar')
318
343
  args.force = true;
319
- else if (token === '--sem-banco')
344
+ else if (token === '--sem-banco-dados')
320
345
  args.semBanco = true;
321
346
  else if (token === '--yes' || token === '-y' || token === '--sim')
322
347
  args.yes = true;
@@ -327,7 +352,7 @@ export function parseUpdateArgs(argv) {
327
352
  }
328
353
  return args;
329
354
  }
330
- /** `ai-harness update [caminho] [--check] [--yes] [--forcar] [--sem-banco]`; sem `--yes` só simula. */
355
+ /** `ai-harness update [caminho] [--check] [--yes] [--forcar] [--sem-banco-dados]`; sem `--yes` só simula. */
331
356
  export function runUpdateCommand(argv) {
332
357
  const args = parseUpdateArgs(argv);
333
358
  const root = resolve(process.cwd(), args.path ?? '.');
@@ -336,7 +361,7 @@ export function runUpdateCommand(argv) {
336
361
  const report = checkUpdate(root, undefined, undefined, undefined, { semBanco: args.semBanco });
337
362
  console.log(describeUpdate(report));
338
363
  // Sem arquivo a atualizar, a adoção ainda grava o lock, e a permissão e o MCP ainda são acrescentados.
339
- const pendente = report.adotada || report.permissaoAusente || report.mudancaMcp !== null || report.recusarMcp;
364
+ const pendente = report.adotada || report.permissaoAusente || report.permissoesLegadas.length > 0 || report.mudancaMcp !== null || report.recusarMcp || report.renomearSecaoBanco;
340
365
  if (args.check || (!report.changes.length && !pendente))
341
366
  return 0;
342
367
  if (!args.yes) {
@@ -347,6 +372,8 @@ export function runUpdateCommand(argv) {
347
372
  const result = applyUpdate(root, report, { force: args.force }, packageInfo());
348
373
  console.log(`\nAtualização aplicada: ${result.written.length} gravado(s), ${result.removed.length} apagado(s), ` +
349
374
  `${result.renamed.length} renomeado(s), ${result.kept.length} preservado(s).`);
375
+ for (const ajuste of result.ajustes)
376
+ console.log(`Gravado: ${ajuste}`);
350
377
  for (const aviso of result.avisos)
351
378
  console.log(`Aviso: ${aviso}`);
352
379
  if (result.renamed.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fontdata/ai-harness",
3
- "version": "2.7.0",
3
+ "version": "3.0.0",
4
4
  "description": "Harness de engenharia com IA da Fontdata para Claude Code: roda os slash commands direto na pasta do projeto via npx, sem instalar nada — ou instala-os em .claude/commands.",
5
5
  "type": "module",
6
6
  "bin": {
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes