chatflux-gestao-agente 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -0
- package/bin/gestao.mjs +80 -0
- package/lib/config.mjs +102 -0
- package/lib/gestao.mjs +125 -0
- package/lib/login.mjs +95 -0
- package/lib/mcp.mjs +193 -0
- package/lib/tools.mjs +95 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# `gestao` — o portal pela linha de comando e por agente (MCP)
|
|
2
|
+
|
|
3
|
+
Deixa um agente (Claude Code, Claude Desktop, qualquer harness que fale MCP)
|
|
4
|
+
operar o Gestão **com a sua identidade e o seu papel** — nada além do que você já
|
|
5
|
+
faz na tela, e tudo no histórico com o seu nome.
|
|
6
|
+
|
|
7
|
+
## Instalar (equipe — um comando, sem clonar nada)
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
claude mcp add --scope user gestao -- npx -y chatflux-gestao-agente mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Pronto. Abra o Claude Code e peça qualquer coisa do portal: na primeira vez ele
|
|
14
|
+
mesmo abre o navegador para você autorizar. **Você não digita mais nenhum
|
|
15
|
+
comando** — nem `login`, nem `status`.
|
|
16
|
+
|
|
17
|
+
### Instalar (desenvolvimento, dentro deste repo)
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm link
|
|
21
|
+
claude mcp add --scope user gestao -- gestao mcp
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**`--scope user` é o ponto:** registra no seu `~/.claude.json` e vale em
|
|
25
|
+
**qualquer diretório**. Não crie `.mcp.json` na raiz de um repo — aquilo é escopo
|
|
26
|
+
`project`, viaja no git e amarra o Gestão a um repositório só.
|
|
27
|
+
|
|
28
|
+
Para apontar ao dev: `claude mcp add --scope user -e GESTAO_ENV=dev gestao -- gestao mcp`.
|
|
29
|
+
|
|
30
|
+
No Claude Desktop (que não tem a CLI), a mesma coisa em
|
|
31
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{ "mcpServers": { "gestao": { "command": "gestao", "args": ["mcp"] } } }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Como a identidade funciona
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
gestao login
|
|
41
|
+
└─ sobe um listener em 127.0.0.1:<porta efêmera>
|
|
42
|
+
└─ abre <portal>/agente/login?port=…&state=…
|
|
43
|
+
└─ a página (só staff) mostra quem você é e pede confirmação
|
|
44
|
+
└─ o browser — que JÁ tem sua sessão do Clerk — emite o JWT do
|
|
45
|
+
template `convex` e entrega no loopback
|
|
46
|
+
└─ guarda no Keychain (macOS) ou em ~/.config/gestao/token.json (0600)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Vale **1 hora**. E na prática você não faz nada disso à mão: o próprio agente
|
|
50
|
+
chama a tool `gestao_conectar` quando precisa, o navegador abre, você clica em
|
|
51
|
+
**Autorizar** e ele refaz a ação. O `gestao login` no terminal existe como
|
|
52
|
+
alternativa, não como o caminho normal.
|
|
53
|
+
|
|
54
|
+
**O servidor NÃO morre quando a sessão expira.** Ele sobe sempre: lista as tools a
|
|
55
|
+
partir do último contrato buscado (cache em `~/.config/gestao/`), e `gestao_conectar`
|
|
56
|
+
fica SEMPRE disponível — é justamente quando a sessão vence que ela precisa existir.
|
|
57
|
+
Sem isso, a pessoa veria "Failed to connect — Connection closed" de hora em hora,
|
|
58
|
+
sem pista do motivo.
|
|
59
|
+
|
|
60
|
+
O cache é conveniência, nunca fonte: qualquer ação de verdade exige token válido.
|
|
61
|
+
|
|
62
|
+
**Sem segredo compartilhado.** Este pacote nunca vê secret key nem chave de
|
|
63
|
+
serviço. O que ele guarda é um token seu, de vida curta, que o Convex valida
|
|
64
|
+
sozinho. Arquivar sua conta (`active:false`) revoga o agente junto.
|
|
65
|
+
|
|
66
|
+
Por que não OAuth: o Clerk não documenta caminho de credencial OAuth → JWT com
|
|
67
|
+
`aud: "convex"`, e emitir pelo backend exigiria secret key **mais** um
|
|
68
|
+
`session_id` que o token OAuth não carrega. O browser é o único lugar que já tem
|
|
69
|
+
sessão — daí o loopback.
|
|
70
|
+
|
|
71
|
+
## As tools não estão aqui
|
|
72
|
+
|
|
73
|
+
Elas vêm do **contrato servido pelo deployment** (`/chatflux/contrato`), montado a
|
|
74
|
+
partir das capacidades do L2. Consequências:
|
|
75
|
+
|
|
76
|
+
- capacidade nova no portal vira tool **sem release deste pacote**;
|
|
77
|
+
- nunca há cópia do contrato para apodrecer;
|
|
78
|
+
- o `inputSchema` de cada tool é o JSON Schema gerado do validador Convex real —
|
|
79
|
+
enums de verdade, não `string` solta.
|
|
80
|
+
|
|
81
|
+
Uma tool por **endpoint**, nomeada pela **capacidade** (`gestao_projeto_criar`).
|
|
82
|
+
Capacidade com mais de um endpoint dá o nome limpo ao principal e sufixo aos
|
|
83
|
+
outros (`gestao_projeto_editar` e `gestao_projeto_editar_generate_cover_upload_url`).
|
|
84
|
+
|
|
85
|
+
Tools de `efeito: destrutivo` vêm com `destructiveHint` e um aviso no texto:
|
|
86
|
+
arquivar empresa e encerrar projeto **cascateiam e não têm desfazer**.
|
|
87
|
+
|
|
88
|
+
## Arquivos
|
|
89
|
+
|
|
90
|
+
| arquivo | o quê |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `bin/gestao.mjs` | CLI: `login`, `status`, `logout`, `mcp` |
|
|
93
|
+
| `lib/login.mjs` | loopback + `state` + abre o navegador |
|
|
94
|
+
| `lib/config.mjs` | ambientes e guarda do token (Keychain / arquivo 0600) |
|
|
95
|
+
| `lib/gestao.mjs` | busca o contrato e chama o Convex por nome |
|
|
96
|
+
| `lib/tools.mjs` | contrato → lista de tools MCP |
|
|
97
|
+
| `lib/mcp.mjs` | servidor MCP (stdio, JSON-RPC) |
|
|
98
|
+
|
|
99
|
+
Zero dependências de propósito: isto roda na máquina de cada pessoa, e cada
|
|
100
|
+
dependência é superfície.
|
package/bin/gestao.mjs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CLI do Gestão. Três verbos: `login`, `status`, `mcp`.
|
|
3
|
+
|
|
4
|
+
import { login } from "../lib/login.mjs";
|
|
5
|
+
import { servir } from "../lib/mcp.mjs";
|
|
6
|
+
import { contrato, euSou, NaoAutenticado } from "../lib/gestao.mjs";
|
|
7
|
+
import {
|
|
8
|
+
ambiente, lerToken, apagarToken, expiraEm, tokenValido,
|
|
9
|
+
} from "../lib/config.mjs";
|
|
10
|
+
import { montarTools } from "../lib/tools.mjs";
|
|
11
|
+
|
|
12
|
+
const AJUDA = `gestao — portal Gestão pela linha de comando e por agente (MCP)
|
|
13
|
+
|
|
14
|
+
gestao login autoriza este computador a agir como você (abre o navegador)
|
|
15
|
+
gestao status mostra quem está conectado e quanto tempo falta
|
|
16
|
+
gestao logout esquece a credencial desta máquina
|
|
17
|
+
gestao mcp roda o servidor MCP (é o que o Claude Code/Desktop chama)
|
|
18
|
+
|
|
19
|
+
Ambiente: ${ambiente().nome} (GESTAO_ENV=dev para apontar ao dev)
|
|
20
|
+
|
|
21
|
+
No Claude Code/Desktop, registre assim:
|
|
22
|
+
{ "mcpServers": { "gestao": { "command": "gestao", "args": ["mcp"] } } }
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
const [, , verbo] = process.argv;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
switch (verbo) {
|
|
29
|
+
case "login": {
|
|
30
|
+
const { email, onde } = await login();
|
|
31
|
+
const eu = await euSou();
|
|
32
|
+
console.log(
|
|
33
|
+
`Conectado como ${eu?.name ?? email ?? "?"} (${eu?.role ?? "?"}) no ambiente ${ambiente().nome}.`,
|
|
34
|
+
);
|
|
35
|
+
console.log(`Credencial guardada em: ${onde}`);
|
|
36
|
+
console.log("Vale por 1 hora. Quando expirar, rode `gestao login` de novo.");
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
case "status": {
|
|
41
|
+
const dados = lerToken();
|
|
42
|
+
if (!dados) {
|
|
43
|
+
console.log("Sem credencial nesta máquina. Rode `gestao login`.");
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
const restaMin = Math.round((expiraEm(dados.token) - Date.now()) / 60000);
|
|
47
|
+
if (!tokenValido(dados)) {
|
|
48
|
+
console.log(`Credencial EXPIRADA (${dados.email ?? "?"}). Rode \`gestao login\`.`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const eu = await euSou();
|
|
52
|
+
const tools = montarTools(await contrato());
|
|
53
|
+
console.log(`Conectado: ${eu?.name ?? "?"} <${eu?.email ?? "?"}> — papel ${eu?.role ?? "?"}`);
|
|
54
|
+
console.log(`Ambiente: ${ambiente().nome}`);
|
|
55
|
+
console.log(`Expira em: ${restaMin} min`);
|
|
56
|
+
console.log(`Tools: ${tools.length} (do contrato servido pelo deployment)`);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
case "logout":
|
|
61
|
+
apagarToken();
|
|
62
|
+
console.log("Credencial removida desta máquina.");
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
case "mcp":
|
|
66
|
+
await servir();
|
|
67
|
+
break;
|
|
68
|
+
|
|
69
|
+
default:
|
|
70
|
+
console.log(AJUDA);
|
|
71
|
+
process.exit(verbo ? 1 : 0);
|
|
72
|
+
}
|
|
73
|
+
} catch (e) {
|
|
74
|
+
if (e instanceof NaoAutenticado) {
|
|
75
|
+
console.error(e.message);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
console.error(`Erro: ${e.message}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
package/lib/config.mjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Endereços e armazenamento do token. Sem dependência externa de propósito:
|
|
2
|
+
// este pacote roda na máquina de cada pessoa, e cada dependência é superfície.
|
|
3
|
+
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
const AMBIENTES = {
|
|
10
|
+
prod: {
|
|
11
|
+
convex: "https://dependable-stingray-656.convex.cloud",
|
|
12
|
+
site: "https://dependable-stingray-656.convex.site",
|
|
13
|
+
portal: "https://portal.chatflux.ai",
|
|
14
|
+
},
|
|
15
|
+
dev: {
|
|
16
|
+
convex: "https://agreeable-woodpecker-190.convex.cloud",
|
|
17
|
+
site: "https://agreeable-woodpecker-190.convex.site",
|
|
18
|
+
portal: "http://localhost:3000",
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** `GESTAO_ENV=dev` troca de ambiente inteiro. Default prod: quem usa isto é a
|
|
23
|
+
* equipe, não o dev do portal. */
|
|
24
|
+
export function ambiente() {
|
|
25
|
+
const nome = process.env.GESTAO_ENV === "dev" ? "dev" : "prod";
|
|
26
|
+
return { nome, ...AMBIENTES[nome] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---- token ------------------------------------------------------------------
|
|
30
|
+
//
|
|
31
|
+
// Keychain do macOS quando houver (é onde o SO já guarda segredo e o usuário
|
|
32
|
+
// controla o acesso); arquivo 0600 como fallback nos outros sistemas. O token
|
|
33
|
+
// vale 1h — vazamento tem janela curta, mas ainda assim não fica em texto puro
|
|
34
|
+
// no macOS, que é a máquina real da equipe.
|
|
35
|
+
|
|
36
|
+
const SERVICO = "chatflux-gestao";
|
|
37
|
+
const ARQUIVO = path.join(os.homedir(), ".config", "gestao", "token.json");
|
|
38
|
+
const temKeychain = () => process.platform === "darwin";
|
|
39
|
+
|
|
40
|
+
export function salvarToken(dados) {
|
|
41
|
+
const json = JSON.stringify(dados);
|
|
42
|
+
if (temKeychain()) {
|
|
43
|
+
try {
|
|
44
|
+
execFileSync("security", [
|
|
45
|
+
"add-generic-password", "-U",
|
|
46
|
+
"-s", SERVICO, "-a", ambiente().nome, "-w", json,
|
|
47
|
+
], { stdio: "ignore" });
|
|
48
|
+
return "keychain";
|
|
49
|
+
} catch {
|
|
50
|
+
/* cai no arquivo */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
fs.mkdirSync(path.dirname(ARQUIVO), { recursive: true });
|
|
54
|
+
fs.writeFileSync(ARQUIVO, json, { mode: 0o600 });
|
|
55
|
+
return ARQUIVO;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function lerToken() {
|
|
59
|
+
if (temKeychain()) {
|
|
60
|
+
try {
|
|
61
|
+
const out = execFileSync("security", [
|
|
62
|
+
"find-generic-password", "-s", SERVICO, "-a", ambiente().nome, "-w",
|
|
63
|
+
], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
64
|
+
return JSON.parse(out.trim());
|
|
65
|
+
} catch {
|
|
66
|
+
/* cai no arquivo */
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(fs.readFileSync(ARQUIVO, "utf8"));
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function apagarToken() {
|
|
77
|
+
if (temKeychain()) {
|
|
78
|
+
try {
|
|
79
|
+
execFileSync("security", [
|
|
80
|
+
"delete-generic-password", "-s", SERVICO, "-a", ambiente().nome,
|
|
81
|
+
], { stdio: "ignore" });
|
|
82
|
+
} catch { /* pode não existir */ }
|
|
83
|
+
}
|
|
84
|
+
try { fs.unlinkSync(ARQUIVO); } catch { /* idem */ }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** `exp` do JWT, em ms. Sem verificar assinatura: quem valida é o Convex — aqui
|
|
88
|
+
* só queremos saber se já venceu, pra dar mensagem boa em vez de erro cru. */
|
|
89
|
+
export function expiraEm(jwt) {
|
|
90
|
+
try {
|
|
91
|
+
const payload = JSON.parse(
|
|
92
|
+
Buffer.from(jwt.split(".")[1], "base64url").toString("utf8"),
|
|
93
|
+
);
|
|
94
|
+
return typeof payload.exp === "number" ? payload.exp * 1000 : 0;
|
|
95
|
+
} catch {
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function tokenValido(dados, margemMs = 60_000) {
|
|
101
|
+
return Boolean(dados?.token) && expiraEm(dados.token) - margemMs > Date.now();
|
|
102
|
+
}
|
package/lib/gestao.mjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Cliente do Gestão: busca o contrato e chama função do Convex COMO A PESSOA.
|
|
2
|
+
//
|
|
3
|
+
// Chamamos por NOME (`"ops/tasks:create"`) via a HTTP API do Convex, sem o SDK e
|
|
4
|
+
// sem o `_generated/api` do repo — este pacote não tem (nem quer) acoplamento com
|
|
5
|
+
// o codegen do portal. As tools vêm do contrato servido; capacidade nova aparece
|
|
6
|
+
// aqui sem release novo.
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { ambiente, lerToken, tokenValido } from "./config.mjs";
|
|
12
|
+
|
|
13
|
+
const arquivoCache = () =>
|
|
14
|
+
path.join(os.homedir(), ".config", "gestao", `contrato-${ambiente().nome}.json`);
|
|
15
|
+
|
|
16
|
+
export class NaoAutenticado extends Error {
|
|
17
|
+
constructor(msg = "Sessão expirada ou ausente. Rode `gestao login`.") {
|
|
18
|
+
super(msg);
|
|
19
|
+
this.name = "NaoAutenticado";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function token() {
|
|
24
|
+
const dados = lerToken();
|
|
25
|
+
if (!tokenValido(dados)) throw new NaoAutenticado();
|
|
26
|
+
return dados.token;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `ops.tasks.create` (id do contrato) → `ops/tasks:create` (nome no Convex). */
|
|
30
|
+
export const paraNomeConvex = (id) => {
|
|
31
|
+
const p = id.split(".");
|
|
32
|
+
return `${p.slice(0, -1).join("/")}:${p[p.length - 1]}`;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Chama query/mutation/action pública. `tipo` vem do contrato (`kind`). */
|
|
36
|
+
export async function chamar(id, args, tipo = "mutation") {
|
|
37
|
+
const rota = tipo === "query" ? "query" : tipo === "action" ? "action" : "mutation";
|
|
38
|
+
const res = await fetch(`${ambiente().convex}/api/${rota}`, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
Authorization: `Bearer ${token()}`,
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
path: paraNomeConvex(id),
|
|
46
|
+
args: args ?? {},
|
|
47
|
+
format: "json",
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
const corpo = await res.json().catch(() => ({}));
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
throw new Error(corpo?.message ?? `HTTP ${res.status} em ${id}`);
|
|
53
|
+
}
|
|
54
|
+
// A HTTP API devolve {status:"success"|"error", value|errorMessage}.
|
|
55
|
+
if (corpo.status === "error") {
|
|
56
|
+
// Erro de regra de negócio (ex.: "Empresa arquivada…") chega aqui. É
|
|
57
|
+
// informação para o modelo, não falha de transporte — propaga a mensagem.
|
|
58
|
+
throw new Error(corpo.errorMessage ?? `Erro em ${id}`);
|
|
59
|
+
}
|
|
60
|
+
return corpo.value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let cache = null;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Contrato servido pelo deployment. Nunca embarcamos cópia NO PACOTE (cópia
|
|
67
|
+
* apodrece e o gate do repo não a enxerga), mas guardamos o último buscado em
|
|
68
|
+
* disco — é cache, não fonte: só serve pra listar tools quando a credencial
|
|
69
|
+
* expirou, e qualquer chamada de verdade continua exigindo login.
|
|
70
|
+
*
|
|
71
|
+
* Isso existe porque o token vale 1h: sem o cache, a pessoa veria o servidor
|
|
72
|
+
* MCP "falhar ao conectar" toda hora, sem pista do motivo.
|
|
73
|
+
*/
|
|
74
|
+
export async function contrato({ permitirCache = false } = {}) {
|
|
75
|
+
const emDisco = () => {
|
|
76
|
+
if (cache) return cache;
|
|
77
|
+
try {
|
|
78
|
+
cache = JSON.parse(fs.readFileSync(arquivoCache(), "utf8"));
|
|
79
|
+
return cache;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
let jwt;
|
|
86
|
+
try {
|
|
87
|
+
jwt = token();
|
|
88
|
+
} catch (e) {
|
|
89
|
+
const c = permitirCache && emDisco();
|
|
90
|
+
if (c) return c;
|
|
91
|
+
throw e;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const atual = cache ?? emDisco();
|
|
95
|
+
const res = await fetch(
|
|
96
|
+
`${ambiente().site}/chatflux/contrato${atual ? `?versao=${atual.versao}` : ""}`,
|
|
97
|
+
{ headers: { Authorization: `Bearer ${jwt}` } },
|
|
98
|
+
);
|
|
99
|
+
if (res.status === 304 && atual) return atual;
|
|
100
|
+
if (res.status === 401 || res.status === 403) {
|
|
101
|
+
const c = permitirCache && atual;
|
|
102
|
+
if (c) return c;
|
|
103
|
+
throw new NaoAutenticado(
|
|
104
|
+
"O portal recusou a credencial ao buscar o contrato. Rode `gestao login`.",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
const c = permitirCache && atual;
|
|
109
|
+
if (c) return c;
|
|
110
|
+
throw new Error(`Falha ao buscar o contrato: HTTP ${res.status}`);
|
|
111
|
+
}
|
|
112
|
+
cache = await res.json();
|
|
113
|
+
try {
|
|
114
|
+
fs.mkdirSync(path.dirname(arquivoCache()), { recursive: true });
|
|
115
|
+
fs.writeFileSync(arquivoCache(), JSON.stringify(cache));
|
|
116
|
+
} catch {
|
|
117
|
+
/* cache é conveniência; falhar em gravar não é erro */
|
|
118
|
+
}
|
|
119
|
+
return cache;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Quem sou eu, segundo o portal (papel real, não o que o token diz). */
|
|
123
|
+
export async function euSou() {
|
|
124
|
+
return await chamar("ops.users.current", {}, "query");
|
|
125
|
+
}
|
package/lib/login.mjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// `gestao login` — o handshake de identidade.
|
|
2
|
+
//
|
|
3
|
+
// Sobe um listener em 127.0.0.1 numa porta efêmera, abre o portal com `port` e
|
|
4
|
+
// `state`, e espera a página devolver o JWT do template `convex`. O browser já
|
|
5
|
+
// tem a sessão do Clerk; nós nunca vemos senha, e o backend não ganha rota nova.
|
|
6
|
+
//
|
|
7
|
+
// Por que NÃO OAuth: o Clerk não documenta caminho de credencial OAuth → JWT com
|
|
8
|
+
// `aud: "convex"`, e emitir pelo backend exigiria secret key mais um `session_id`
|
|
9
|
+
// que o token OAuth não carrega. O browser é o único lugar que já tem sessão.
|
|
10
|
+
|
|
11
|
+
import { createServer } from "node:http";
|
|
12
|
+
import crypto from "node:crypto";
|
|
13
|
+
import { execFile } from "node:child_process";
|
|
14
|
+
import { ambiente, salvarToken } from "./config.mjs";
|
|
15
|
+
|
|
16
|
+
const ESPERA_MS = 120_000;
|
|
17
|
+
|
|
18
|
+
function abrirNavegador(url) {
|
|
19
|
+
const cmd =
|
|
20
|
+
process.platform === "darwin" ? "open"
|
|
21
|
+
: process.platform === "win32" ? "start"
|
|
22
|
+
: "xdg-open";
|
|
23
|
+
execFile(cmd, [url], () => {});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function login({ silencioso = false } = {}) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const state = crypto.randomBytes(16).toString("hex");
|
|
29
|
+
let terminou = false;
|
|
30
|
+
|
|
31
|
+
const server = createServer((req, res) => {
|
|
32
|
+
// A página roda em outra origem — sem CORS o POST nem sai.
|
|
33
|
+
const cors = {
|
|
34
|
+
"Access-Control-Allow-Origin": "*",
|
|
35
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
36
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
37
|
+
};
|
|
38
|
+
if (req.method === "OPTIONS") {
|
|
39
|
+
res.writeHead(204, cors);
|
|
40
|
+
return res.end();
|
|
41
|
+
}
|
|
42
|
+
if (req.method !== "POST" || !req.url.startsWith("/callback")) {
|
|
43
|
+
res.writeHead(404, cors);
|
|
44
|
+
return res.end();
|
|
45
|
+
}
|
|
46
|
+
let corpo = "";
|
|
47
|
+
req.on("data", (c) => {
|
|
48
|
+
corpo += c;
|
|
49
|
+
// Um POST legítimo tem ~2 KB. Corta qualquer coisa absurda.
|
|
50
|
+
if (corpo.length > 64_000) req.destroy();
|
|
51
|
+
});
|
|
52
|
+
req.on("end", () => {
|
|
53
|
+
let dados;
|
|
54
|
+
try {
|
|
55
|
+
dados = JSON.parse(corpo);
|
|
56
|
+
} catch {
|
|
57
|
+
res.writeHead(400, cors);
|
|
58
|
+
return res.end();
|
|
59
|
+
}
|
|
60
|
+
// O `state` amarra esta resposta ao NOSSO pedido: sem ele, outra aba (ou
|
|
61
|
+
// outro processo local) conseguiria despejar um token aqui.
|
|
62
|
+
if (dados.state !== state || typeof dados.token !== "string") {
|
|
63
|
+
res.writeHead(403, cors);
|
|
64
|
+
return res.end();
|
|
65
|
+
}
|
|
66
|
+
res.writeHead(200, { ...cors, "Content-Type": "application/json" });
|
|
67
|
+
res.end(JSON.stringify({ ok: true }));
|
|
68
|
+
terminou = true;
|
|
69
|
+
const onde = salvarToken({
|
|
70
|
+
token: dados.token,
|
|
71
|
+
email: dados.email ?? null,
|
|
72
|
+
env: ambiente().nome,
|
|
73
|
+
});
|
|
74
|
+
server.close();
|
|
75
|
+
resolve({ email: dados.email ?? null, onde });
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
server.listen(0, "127.0.0.1", () => {
|
|
80
|
+
const porta = server.address().port;
|
|
81
|
+
const url = `${ambiente().portal}/agente/login?port=${porta}&state=${state}`;
|
|
82
|
+
if (!silencioso) {
|
|
83
|
+
console.error(`Abrindo o navegador para autorizar…\n ${url}\n`);
|
|
84
|
+
}
|
|
85
|
+
abrirNavegador(url);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
server.on("error", reject);
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
if (terminou) return;
|
|
91
|
+
server.close();
|
|
92
|
+
reject(new Error("Tempo esgotado (2 min) esperando a autorização."));
|
|
93
|
+
}, ESPERA_MS).unref();
|
|
94
|
+
});
|
|
95
|
+
}
|
package/lib/mcp.mjs
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Servidor MCP do Gestão (stdio, JSON-RPC 2.0).
|
|
2
|
+
//
|
|
3
|
+
// Sem SDK de propósito: a fatia do protocolo que um servidor de tools usa é
|
|
4
|
+
// `initialize` + `tools/list` + `tools/call` + `ping`. Escrever isso são ~80
|
|
5
|
+
// linhas e zero dependência — e este pacote roda na máquina de cada pessoa, onde
|
|
6
|
+
// cada dependência é superfície e mais uma coisa pra travar numa versão.
|
|
7
|
+
//
|
|
8
|
+
// As tools NÃO são hardcoded: vêm do contrato servido pelo deployment. Capacidade
|
|
9
|
+
// nova no L2 vira tool sem release deste pacote.
|
|
10
|
+
|
|
11
|
+
import { contrato, chamar, euSou } from "./gestao.mjs";
|
|
12
|
+
import { login } from "./login.mjs";
|
|
13
|
+
import { ambiente } from "./config.mjs";
|
|
14
|
+
import { montarTools } from "./tools.mjs";
|
|
15
|
+
|
|
16
|
+
const PROTOCOLO = "2024-11-05";
|
|
17
|
+
const escrever = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
|
|
18
|
+
const ok = (id, result) => escrever({ jsonrpc: "2.0", id, result });
|
|
19
|
+
const falha = (id, code, message) =>
|
|
20
|
+
escrever({ jsonrpc: "2.0", id, error: { code, message } });
|
|
21
|
+
|
|
22
|
+
/** Resultado de tool: MCP quer conteúdo textual; JSON legível serve ao modelo. */
|
|
23
|
+
const texto = (v) => ({
|
|
24
|
+
content: [
|
|
25
|
+
{ type: "text", text: typeof v === "string" ? v : JSON.stringify(v, null, 2) },
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
let tools = null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A tool que CONECTA — não a que pede pra pessoa digitar comando.
|
|
33
|
+
*
|
|
34
|
+
* Fica SEMPRE na lista, autenticado ou não, porque a sessão vence de hora em
|
|
35
|
+
* hora: quando qualquer ação falhar por sessão, o modelo chama esta e a pessoa
|
|
36
|
+
* só clica no navegador. Ela nunca precisa saber que existe uma CLI.
|
|
37
|
+
*/
|
|
38
|
+
const TOOL_CONECTAR = {
|
|
39
|
+
name: "gestao_conectar",
|
|
40
|
+
description:
|
|
41
|
+
"Conecta esta máquina ao Gestão. ABRE O NAVEGADOR para a pessoa autorizar com " +
|
|
42
|
+
"o login dela (leva segundos) e vale 1 hora.\n\n" +
|
|
43
|
+
"CHAME ISTO SEM PEDIR PERMISSÃO sempre que uma ação do Gestão falhar por " +
|
|
44
|
+
"sessão expirada ou ausente, ou quando não houver nenhuma outra tool do Gestão " +
|
|
45
|
+
"disponível. Depois de conectar, refaça a ação que falhou.\n\n" +
|
|
46
|
+
"Avise a pessoa em uma frase que o navegador vai abrir e que ela precisa " +
|
|
47
|
+
"clicar em Autorizar.",
|
|
48
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
49
|
+
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
50
|
+
_endpoint: null,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Estamos no estado degradado (só a tool de conectar)? */
|
|
54
|
+
const soConectar = (t) => t?.length === 1 && t[0]._endpoint === null;
|
|
55
|
+
|
|
56
|
+
async function carregarTools() {
|
|
57
|
+
// Cache normal — MENOS quando estamos degradados. Ordem real das coisas: o
|
|
58
|
+
// harness sobe o servidor ao abrir, e a conexão só acontece depois. Se o
|
|
59
|
+
// fallback ficasse cacheado, a pessoa veria 1 tool até reiniciar o Claude Code
|
|
60
|
+
// inteiro, sem nenhuma pista de que era isso.
|
|
61
|
+
if (tools && !soConectar(tools)) return tools;
|
|
62
|
+
// `permitirCache`: com o token expirado (acontece de hora em hora) ainda
|
|
63
|
+
// listamos as tools do último contrato buscado. A CHAMADA continua exigindo
|
|
64
|
+
// sessão — o que muda é o modelo poder chamar `gestao_conectar` em vez de o
|
|
65
|
+
// servidor morrer com "Connection closed".
|
|
66
|
+
try {
|
|
67
|
+
tools = [TOOL_CONECTAR, ...montarTools(await contrato({ permitirCache: true }))];
|
|
68
|
+
} catch {
|
|
69
|
+
tools = [TOOL_CONECTAR];
|
|
70
|
+
}
|
|
71
|
+
return tools;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Refaz a lista após conectar e avisa o harness pra reler (senão ele fica com a
|
|
75
|
+
* lista velha, de 1 tool, e a pessoa não entende por que nada apareceu). */
|
|
76
|
+
async function recarregarEAvisar() {
|
|
77
|
+
tools = null;
|
|
78
|
+
await carregarTools();
|
|
79
|
+
escrever({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function tratar(msg) {
|
|
83
|
+
const { id, method, params } = msg;
|
|
84
|
+
|
|
85
|
+
if (method === "initialize") {
|
|
86
|
+
const env = ambiente().nome;
|
|
87
|
+
// O ambiente é a PRIMEIRA coisa que o modelo lê, e em dev vem como aviso.
|
|
88
|
+
// Motivo: o dev é uma cópia do prod — mesmos clientes, mesmos nomes. Escrever
|
|
89
|
+
// no banco errado não dá erro, dá resultado plausível, e ninguém percebe.
|
|
90
|
+
const aviso =
|
|
91
|
+
env === "prod"
|
|
92
|
+
? "AMBIENTE: PRODUÇÃO — o que você fizer aqui é real e a equipe vê."
|
|
93
|
+
: "AMBIENTE: DESENVOLVIMENTO (cópia desatualizada do portal real). " +
|
|
94
|
+
"Os nomes de cliente são reais, mas NADA daqui aparece pra equipe. " +
|
|
95
|
+
"AVISE a pessoa que está em dev antes de qualquer escrita — ela pode " +
|
|
96
|
+
"achar que está operando o portal de verdade.";
|
|
97
|
+
return ok(id, {
|
|
98
|
+
protocolVersion: PROTOCOLO,
|
|
99
|
+
capabilities: { tools: { listChanged: true } },
|
|
100
|
+
serverInfo: { name: `gestao (${env})`, version: "0.1.0" },
|
|
101
|
+
instructions:
|
|
102
|
+
`${aviso}\n\n` +
|
|
103
|
+
"Portal interno da ChatFlux: empresas-cliente → projetos → entregáveis. " +
|
|
104
|
+
"Você age COM A IDENTIDADE E O PAPEL da pessoa que autorizou (`gestao login`) — " +
|
|
105
|
+
"nada além do que ela já pode fazer na tela, e tudo fica no histórico com o nome dela. " +
|
|
106
|
+
"IDs nunca são adivinháveis: use as tools de leitura para resolver nome → id antes de escrever. " +
|
|
107
|
+
"Tools marcadas como DESTRUTIVO cascateiam e não têm desfazer — confirme com a pessoa antes.",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (method === "notifications/initialized") return; // notificação: sem resposta
|
|
111
|
+
if (method === "ping") return ok(id, {});
|
|
112
|
+
|
|
113
|
+
if (method === "tools/list") {
|
|
114
|
+
const lista = await carregarTools();
|
|
115
|
+
return ok(id, {
|
|
116
|
+
tools: lista.map(({ _endpoint, _kind, _efeito, ...t }) => t),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (method === "tools/call") {
|
|
121
|
+
const lista = await carregarTools();
|
|
122
|
+
const tool = lista.find((t) => t.name === params?.name);
|
|
123
|
+
if (!tool) return falha(id, -32602, `Tool desconhecida: ${params?.name}`);
|
|
124
|
+
// `gestao_conectar`: executa o login de verdade — abre o navegador, espera a
|
|
125
|
+
// pessoa autorizar, guarda a credencial e republica as tools.
|
|
126
|
+
if (tool._endpoint === null) {
|
|
127
|
+
try {
|
|
128
|
+
await login({ silencioso: true });
|
|
129
|
+
const eu = await euSou();
|
|
130
|
+
await recarregarEAvisar();
|
|
131
|
+
return ok(
|
|
132
|
+
id,
|
|
133
|
+
texto(
|
|
134
|
+
`Conectado ao Gestão (${ambiente().nome}) como ${eu?.name ?? "?"} ` +
|
|
135
|
+
`— papel ${eu?.role ?? "?"}. Vale 1 hora. ` +
|
|
136
|
+
`As ferramentas do Gestão já estão disponíveis: refaça a ação que falhou.`,
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
return ok(id, {
|
|
141
|
+
...texto(
|
|
142
|
+
`Não consegui conectar: ${e.message}\n` +
|
|
143
|
+
`A pessoa precisa clicar em "Autorizar" na aba que abriu no navegador. ` +
|
|
144
|
+
`Se nenhuma aba abriu, ela pode rodar \`gestao login\` no terminal.`,
|
|
145
|
+
),
|
|
146
|
+
isError: true,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const valor = await chamar(tool._endpoint, params.arguments ?? {}, tool._kind);
|
|
152
|
+
return ok(id, texto(valor ?? { ok: true }));
|
|
153
|
+
} catch (e) {
|
|
154
|
+
// Erro de REGRA (ex.: "Empresa arquivada…") volta como conteúdo, não como
|
|
155
|
+
// erro de protocolo: é informação que o modelo deve ler e agir em cima.
|
|
156
|
+
return ok(id, { ...texto(`ERRO: ${e.message}`), isError: true });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (id !== undefined) falha(id, -32601, `Método não suportado: ${method}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function servir() {
|
|
164
|
+
// NUNCA sai por falta de credencial. O token vale 1h; se o servidor morresse
|
|
165
|
+
// ao expirar, a pessoa veria "Failed to connect — Connection closed" de hora
|
|
166
|
+
// em hora, sem pista do motivo. Aqui ele sobe sempre, e a falta de sessão
|
|
167
|
+
// aparece como uma tool que EXPLICA o que fazer.
|
|
168
|
+
await carregarTools();
|
|
169
|
+
|
|
170
|
+
let buffer = "";
|
|
171
|
+
process.stdin.setEncoding("utf8");
|
|
172
|
+
process.stdin.on("data", async (chunk) => {
|
|
173
|
+
buffer += chunk;
|
|
174
|
+
let quebra;
|
|
175
|
+
while ((quebra = buffer.indexOf("\n")) !== -1) {
|
|
176
|
+
const linha = buffer.slice(0, quebra).trim();
|
|
177
|
+
buffer = buffer.slice(quebra + 1);
|
|
178
|
+
if (!linha) continue;
|
|
179
|
+
let msg;
|
|
180
|
+
try {
|
|
181
|
+
msg = JSON.parse(linha);
|
|
182
|
+
} catch {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
await tratar(msg);
|
|
187
|
+
} catch (e) {
|
|
188
|
+
if (msg.id !== undefined) falha(msg.id, -32603, e.message);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
process.stdin.on("end", () => process.exit(0));
|
|
193
|
+
}
|
package/lib/tools.mjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Contrato → lista de tools MCP.
|
|
2
|
+
//
|
|
3
|
+
// GRANULARIDADE: uma tool por ENDPOINT exposto (não por capacidade). Medimos: 36
|
|
4
|
+
// capacidades expostas embrulham 69 endpoints distintos, e 10 delas têm mais de
|
|
5
|
+
// um endpoint de ESCRITA (`cap.trilha.esforcos` tem seis). Uma tool por
|
|
6
|
+
// capacidade obrigaria um discriminador `acao` + união de schemas — mais peça,
|
|
7
|
+
// pior validação, e o modelo escolhendo duas vezes. Uma por endpoint mantém o
|
|
8
|
+
// JSON Schema exato de cada chamada e nenhuma máquina nova.
|
|
9
|
+
//
|
|
10
|
+
// A capacidade não some: ela dá o NOME, a descrição, o `efeito` e o `cuidado:` —
|
|
11
|
+
// é o que faz a tool ser escolhida pela intenção da pessoa e não pelo nome da
|
|
12
|
+
// função do backend.
|
|
13
|
+
|
|
14
|
+
const EFEITO_AVISO = {
|
|
15
|
+
leitura: "",
|
|
16
|
+
escrita: "",
|
|
17
|
+
destrutivo:
|
|
18
|
+
"\n\n⚠️ DESTRUTIVO E EM CASCATA: desfazer NÃO restaura o que isto leva. " +
|
|
19
|
+
"CONFIRME com a pessoa, em texto, antes de chamar.",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** `ops.tasks.create` → `criar`; usado como sufixo quando a capacidade tem mais
|
|
23
|
+
* de um endpoint (senão duas tools teriam o mesmo nome). */
|
|
24
|
+
const sufixo = (id) =>
|
|
25
|
+
id
|
|
26
|
+
.split(".")
|
|
27
|
+
.pop()
|
|
28
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
29
|
+
.toLowerCase();
|
|
30
|
+
|
|
31
|
+
const base = (capId) =>
|
|
32
|
+
"gestao_" + capId.replace(/^cap\./, "").replace(/[.-]/g, "_");
|
|
33
|
+
|
|
34
|
+
export function montarTools(contrato) {
|
|
35
|
+
const tools = [];
|
|
36
|
+
const vistos = new Set();
|
|
37
|
+
|
|
38
|
+
const caps = Object.values(contrato.capabilities)
|
|
39
|
+
.filter((c) => c.agente === "exposta")
|
|
40
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
41
|
+
|
|
42
|
+
for (const cap of caps) {
|
|
43
|
+
const ends = cap.endpoints.filter((id) => contrato.endpoints[id]);
|
|
44
|
+
// O endpoint PRINCIPAL fica com o nome limpo da capacidade; os outros ganham
|
|
45
|
+
// sufixo. `gestao_projeto_editar` lê muito melhor que
|
|
46
|
+
// `gestao_projeto_editar_update`, e é por esse nome que o modelo escolhe.
|
|
47
|
+
// Principal = a única escrita, quando há exatamente uma (o caso comum: a
|
|
48
|
+
// capacidade age e as leituras são apoio); senão, o primeiro que o autor da
|
|
49
|
+
// capacidade listou — a ordem no L2 é deliberada.
|
|
50
|
+
const escritas = ends.filter((id) => contrato.endpoints[id].kind !== "query");
|
|
51
|
+
const principal = escritas.length === 1 ? escritas[0] : ends[0];
|
|
52
|
+
|
|
53
|
+
for (const id of ends) {
|
|
54
|
+
const ep = contrato.endpoints[id];
|
|
55
|
+
// Leituras se repetem entre capacidades (`ops.companies.list` aparece em
|
|
56
|
+
// várias). Dedup por endpoint: a primeira capacidade que o cita nomeia.
|
|
57
|
+
if (vistos.has(id)) continue;
|
|
58
|
+
vistos.add(id);
|
|
59
|
+
|
|
60
|
+
const nome =
|
|
61
|
+
ends.length === 1 || id === principal
|
|
62
|
+
? base(cap.id)
|
|
63
|
+
: `${base(cap.id)}_${sufixo(id)}`;
|
|
64
|
+
|
|
65
|
+
const descricao = [
|
|
66
|
+
cap.fala ?? cap.id,
|
|
67
|
+
"",
|
|
68
|
+
`Capacidade: ${cap.id} · papel mínimo: ${cap.papel} · ${ep.kind}`,
|
|
69
|
+
cap.cuidado ? `\nATENÇÃO: ${cap.cuidado}` : "",
|
|
70
|
+
cap.antes?.length ? `\nCostuma vir depois de: ${cap.antes.join(", ")}.` : "",
|
|
71
|
+
cap.depois?.length ? `\nCostuma levar a: ${cap.depois.join(", ")}.` : "",
|
|
72
|
+
EFEITO_AVISO[cap.efeito] ?? "",
|
|
73
|
+
]
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.join("\n");
|
|
76
|
+
|
|
77
|
+
tools.push({
|
|
78
|
+
name: nome.slice(0, 64),
|
|
79
|
+
description: descricao,
|
|
80
|
+
inputSchema: ep.inputSchema,
|
|
81
|
+
// Metadados que o servidor usa no dispatch e o harness usa pra decidir
|
|
82
|
+
// se pede confirmação.
|
|
83
|
+
_endpoint: id,
|
|
84
|
+
_kind: ep.kind,
|
|
85
|
+
_efeito: cap.efeito,
|
|
86
|
+
annotations: {
|
|
87
|
+
readOnlyHint: ep.kind === "query",
|
|
88
|
+
destructiveHint: cap.efeito === "destrutivo",
|
|
89
|
+
idempotentHint: ep.kind === "query",
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return tools;
|
|
95
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chatflux-gestao-agente",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Opere o portal Gestão da ChatFlux pelo Claude Code, Claude Desktop ou qualquer harness MCP — com a sua identidade e o seu papel.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mcp",
|
|
8
|
+
"chatflux",
|
|
9
|
+
"gestao",
|
|
10
|
+
"claude"
|
|
11
|
+
],
|
|
12
|
+
"license": "UNLICENSED",
|
|
13
|
+
"bin": {
|
|
14
|
+
"gestao": "bin/gestao.mjs"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin",
|
|
21
|
+
"lib",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/Danielmtex/chatflux-gestao.git",
|
|
27
|
+
"directory": "agente"
|
|
28
|
+
}
|
|
29
|
+
}
|