dd-harness-mcp 0.1.0 → 0.1.2

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.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Leitura de argumentos da linha de comando.
3
+ *
4
+ * Vive fora do `index.ts` porque aquele modulo executa o CLI ao ser importado
5
+ * (`principal()` no topo) — teste que o importasse rodaria o programa inteiro.
6
+ */
7
+ /**
8
+ * Os termos livres de uma consulta, sem os pares `--flag valor`.
9
+ *
10
+ * Filtrar so o que comeca com `--` deixava o VALOR da flag para tras, e ele entrava na
11
+ * consulta: `buscar "efeito" --limite 2` virava a pergunta `"efeito 2"`. A busca roda, a
12
+ * resposta parece valida e responde a outra coisa — quem chamou nao tem como notar.
13
+ */
14
+ export function termosDaConsulta(argv) {
15
+ const termos = [];
16
+ for (let i = 0; i < argv.length; i += 1) {
17
+ const atual = argv[i];
18
+ if (atual.startsWith("--")) {
19
+ // `--flag=valor` carrega o valor no proprio token; `--flag valor` consome o proximo.
20
+ if (!atual.includes("="))
21
+ i += 1;
22
+ continue;
23
+ }
24
+ termos.push(atual);
25
+ }
26
+ return termos.join(" ").trim();
27
+ }
@@ -72,6 +72,7 @@ export async function status(raiz) {
72
72
  vencidas: ativas
73
73
  .filter((m) => m.revisar_ate && new Date(m.revisar_ate).getTime() < agora)
74
74
  .map((m) => ({ pasta: m.pasta, memoria: m.slug, titulo: m.titulo })),
75
+ travadasNaFila: brain.travadas_na_fila ?? 0,
75
76
  };
76
77
  }
77
78
  export async function check(raiz, commit) {
@@ -30,6 +30,11 @@ export async function caminhosDoCommit(raiz, commit) {
30
30
  /**
31
31
  * Cruza caminhos com ancoras. A ancora casa quando e o proprio caminho ou quando e um
32
32
  * diretorio que o contem — `supabase/migrations` tem que casar com a migration nova.
33
+ *
34
+ * Ancora de trecho (`arquivo.js#alvo`) casa pelo ARQUIVO: o `git diff-tree` devolve
35
+ * caminho puro, nunca com `#`, entao comparar o valor inteiro nao casava nunca — e em
36
+ * silencio, porque nao casar e resultado valido. O alvo continua no `Tocada.ancora`, que
37
+ * e o que a mensagem mostra: quem le precisa saber qual trecho a memoria guarda.
33
38
  */
34
39
  export function memoriasTocadas(brain, caminhos) {
35
40
  const tocadas = [];
@@ -37,7 +42,8 @@ export function memoriasTocadas(brain, caminhos) {
37
42
  if (memoria.status !== "ativa")
38
43
  continue;
39
44
  for (const ancora of memoria.ancoras) {
40
- const casa = caminhos.some((c) => c === ancora.valor || c.startsWith(`${ancora.valor}/`));
45
+ const alvo = ancora.valor.split("#")[0];
46
+ const casa = caminhos.some((c) => c === alvo || c.startsWith(`${alvo}/`));
41
47
  if (casa) {
42
48
  tocadas.push({
43
49
  pasta: memoria.pasta,
@@ -46,9 +46,18 @@ export function interpreta(texto) {
46
46
  throw new Error(`frontmatter sem \`${chave}\`.`);
47
47
  }
48
48
  // O corpo vai ate a secao dos filtros; dali para baixo e metadado, nao conteudo.
49
- const corte = resto.search(/\n##\s+Os tr[êe]s filtros\s*\n/i);
50
- if (corte === -1)
49
+ const cabecalhoDosFiltros = /\n##\s+Os tr[êe]s filtros\s*\n/gi;
50
+ const ocorrencias = [...resto.matchAll(cabecalhoDosFiltros)];
51
+ if (ocorrencias.length === 0)
51
52
  throw new Error("falta a seção `## Os três filtros`.");
53
+ // Duas ocorrencias tornam o arquivo ambiguo: cortar na primeira trunca o corpo (e os
54
+ // filtros ainda saem certos da segunda, entao grava e perde dado sem avisar), e cortar
55
+ // na ultima escolheria em silencio qual secao e a de verdade. Recusar diz o que houve.
56
+ if (ocorrencias.length > 1) {
57
+ throw new Error("a seção `## Os três filtros` aparece mais de uma vez — deixe só a real, " +
58
+ "ou reescreva a citação no corpo (por exemplo, entre crases).");
59
+ }
60
+ const corte = ocorrencias[0].index;
52
61
  const corpo = resto
53
62
  .slice(0, corte)
54
63
  .replace(/<!--[\s\S]*?-->/g, "")
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import { termosDaConsulta } from "./argv.js";
2
3
  import { check, status } from "./check.js";
3
4
  import { leConfigDoRepo, guardaToken } from "./config.js";
4
5
  import { grava } from "./gravar.js";
5
6
  import { init, SUGESTAO_MCP } from "./init.js";
6
7
  import { LINHA_DE_IMPORT } from "./materializa.js";
8
+ import { buscaPolitica } from "./politica.js";
7
9
  import { busca } from "./buscar.js";
8
10
  import { arquiva, edita } from "./curar.js";
9
11
  import { criaPasta } from "./pasta.js";
@@ -36,6 +38,9 @@ const AJUDA = `dd-harness — materializa política, briefing e Brain no reposit
36
38
  dd-harness sync escreve os artefatos em disco
37
39
  dd-harness check [--commit <sha>] mede as âncoras e reporta a deriva
38
40
  dd-harness status só lê: o tamanho do Brain e o que espera julgamento
41
+ dd-harness politica imprime a política do serviço (para o hook de sessão)
42
+ saída 0 = veio; 3 = projeto sem política;
43
+ 1 = não consegui buscar
39
44
  dd-harness --help
40
45
 
41
46
  O gerado vive em dd-harness/. Na raiz fica só o seu CLAUDE.md, que importa a
@@ -164,7 +169,7 @@ async function comandoArquivar(argv) {
164
169
  console.log(" foi para o histórico, não foi apagada. Rode `dd-harness sync`.");
165
170
  }
166
171
  async function comandoBuscar(argv) {
167
- const consulta = argv.filter((a) => !a.startsWith("--")).join(" ").trim();
172
+ const consulta = termosDaConsulta(argv);
168
173
  if (!consulta)
169
174
  throw new Error('uso: dd-harness buscar "<pergunta>"');
170
175
  const limite = Number(argumento(argv, "limite")) || undefined;
@@ -261,6 +266,27 @@ async function comandoCheck(argv) {
261
266
  }
262
267
  }
263
268
  }
269
+ /**
270
+ * Imprime a politica no stdout, para o hook `SessionStart` injetar no contexto.
271
+ *
272
+ * Os codigos de saida sao o contrato com o hook, e existem para separar duas coisas que
273
+ * um "falhou" generico colapsaria: projeto novo (que precisa de briefing) de politica
274
+ * inalcancavel (que precisa PARAR a sessao). Mudar estes numeros quebra o hook.
275
+ */
276
+ async function comandoPolitica() {
277
+ const r = await buscaPolitica(process.cwd());
278
+ if (r.estado === "ok") {
279
+ console.log(r.conteudo);
280
+ return;
281
+ }
282
+ if (r.estado === "sem-politica") {
283
+ console.error("Este projeto ainda não tem política — nunca foi briefado. Rode `/briefar`.");
284
+ process.exitCode = 3;
285
+ return;
286
+ }
287
+ console.error(`não consegui buscar a política: ${r.motivo}`);
288
+ process.exitCode = 1;
289
+ }
264
290
  async function comandoStatus() {
265
291
  const r = await status(process.cwd());
266
292
  // O acervo vem primeiro, e sempre: e a resposta para "o que ha no Brain deste projeto?",
@@ -276,6 +302,14 @@ async function comandoStatus() {
276
302
  (arquivadas ? ` e ${arquivadas} arquivada(s)` : "") +
277
303
  (detalhe ? ` — ${detalhe}` : ""));
278
304
  }
305
+ // Antes do early return abaixo: fila travada nao e "julgamento esperando", e sumiria
306
+ // justamente na sessao mais comum — a que nao tem deriva nem vencida.
307
+ if (r.travadasNaFila > 0) {
308
+ console.log("");
309
+ console.log(`AVISO: ${r.travadasNaFila} memória(s) desistiram de ser indexadas e não entram na busca semântica.`);
310
+ console.log(" O indexador tentou 5 vezes e parou. Rode `pnpm worker --reindexar`;");
311
+ console.log(" se repetir, o motivo está em `embedding_queue.ultimo_erro`.");
312
+ }
279
313
  if (!r.comDeriva.length && !r.vencidas.length) {
280
314
  console.log("Nada esperando julgamento.");
281
315
  return;
@@ -366,6 +400,8 @@ async function principal() {
366
400
  return comandoCheck(resto);
367
401
  case "status":
368
402
  return comandoStatus();
403
+ case "politica":
404
+ return comandoPolitica();
369
405
  case "--help":
370
406
  case "-h":
371
407
  case undefined:
@@ -0,0 +1,35 @@
1
+ import { cabecalhos, credencial, pede } from "./api.js";
2
+ export async function buscaPolitica(raiz) {
3
+ let config;
4
+ try {
5
+ config = await credencial(raiz);
6
+ }
7
+ catch (erro) {
8
+ // Sem `.dd-harness.json` ou sem token nao da para nem perguntar. E inalcancavel, nao
9
+ // "sem politica": a politica pode muito bem existir do outro lado.
10
+ return { estado: "inalcancavel", motivo: mensagem(erro) };
11
+ }
12
+ const url = new URL(`${config.config.api}/api/v1/artefatos`);
13
+ url.searchParams.set("tenant", config.config.tenant);
14
+ url.searchParams.set("projeto", config.config.projeto);
15
+ try {
16
+ const resposta = await pede(url, { headers: cabecalhos(config.token) });
17
+ if (!resposta.ok) {
18
+ const { erro } = (await resposta.json().catch(() => ({})));
19
+ return {
20
+ estado: "inalcancavel",
21
+ motivo: erro ?? `a API respondeu ${resposta.status}.`,
22
+ };
23
+ }
24
+ const payload = (await resposta.json());
25
+ const conteudo = payload.politica?.trim();
26
+ // Vazio e nulo sao a mesma coisa aqui, e os dois significam "nunca foi escrita".
27
+ if (!conteudo)
28
+ return { estado: "sem-politica" };
29
+ return { estado: "ok", conteudo };
30
+ }
31
+ catch (erro) {
32
+ return { estado: "inalcancavel", motivo: mensagem(erro) };
33
+ }
34
+ }
35
+ const mensagem = (erro) => erro instanceof Error ? erro.message : String(erro);
package/package.json CHANGED
@@ -1,41 +1,40 @@
1
- {
2
- "name": "dd-harness-mcp",
3
- "version": "0.1.0",
4
- "type": "module",
5
- "description": "Servidor MCP do dd-harness: o agente consulta e grava memoria como ferramenta, sem passar por arquivo.",
6
- "license": "UNLICENSED",
7
- "author": "Diego Dias",
8
- "keywords": [
9
- "claude-code",
10
- "mcp",
11
- "model-context-protocol",
12
- "ai-agents",
13
- "memory"
14
- ],
15
- "homepage": "https://dd-harness.vercel.app",
16
- "repository": {
17
- "type": "git",
18
- "url": "git+https://github.com/diegodias93/dd-harness-online.git",
19
- "directory": "packages/mcp"
20
- },
21
- "//bin": "`dist/mcp/src/` e nao `dist/` porque o build abrange dois diretorios (este pacote e o fonte do CLI), e o `rootDir` comum e `packages/`. Conferir com `node dist/mcp/src/index.js` antes de publicar: caminho errado aqui instala e quebra no primeiro uso.",
22
- "bin": {
23
- "dd-harness-mcp": "dist/mcp/src/index.js"
24
- },
25
- "files": [
26
- "dist"
27
- ],
28
- "engines": {
29
- "node": ">=20"
30
- },
31
- "dependencies": {
32
- "@modelcontextprotocol/server": "^2.0.0",
33
- "dd-harness": "workspace:*",
34
- "zod": "^4.5.4"
35
- },
36
- "scripts": {
37
- "typecheck": "tsc -p . --noEmit",
38
- "build": "tsc -p tsconfig.build.json",
39
- "prepublishOnly": "npm run build"
40
- }
41
- }
1
+ {
2
+ "name": "dd-harness-mcp",
3
+ "version": "0.1.2",
4
+ "type": "module",
5
+ "description": "Servidor MCP do dd-harness: o agente consulta e grava memoria como ferramenta, sem passar por arquivo.",
6
+ "license": "UNLICENSED",
7
+ "author": "Diego Dias",
8
+ "keywords": [
9
+ "claude-code",
10
+ "mcp",
11
+ "model-context-protocol",
12
+ "ai-agents",
13
+ "memory"
14
+ ],
15
+ "homepage": "https://dd-harness.vercel.app",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/diegodias93/dd-harness-online.git",
19
+ "directory": "packages/mcp"
20
+ },
21
+ "//bin": "`dist/mcp/src/` e nao `dist/` porque o build abrange dois diretorios (este pacote e o fonte do CLI), e o `rootDir` comum e `packages/`. Conferir com `node dist/mcp/src/index.js` antes de publicar: caminho errado aqui instala e quebra no primeiro uso.",
22
+ "bin": {
23
+ "dd-harness-mcp": "dist/mcp/src/index.js"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "dependencies": {
32
+ "@modelcontextprotocol/server": "^2.0.0",
33
+ "zod": "^4.5.4"
34
+ },
35
+ "scripts": {
36
+ "typecheck": "tsc -p . --noEmit",
37
+ "build": "tsc -p tsconfig.build.json",
38
+ "prepublishOnly": "npm run build"
39
+ }
40
+ }