chatflux-gestao-agente 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -6
- package/bin/gestao.mjs +6 -0
- package/lib/computador-cli.mjs +1 -0
- package/lib/computador-plugin.mjs +13 -9
- package/lib/computador-startup.mjs +13 -4
- package/lib/config.mjs +21 -1
- package/lib/configurar.mjs +319 -0
- package/lib/gestao.mjs +1 -1
- package/lib/login.mjs +1 -1
- package/lib/mcp.mjs +22 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,12 +11,30 @@ faz na tela, e tudo no histórico com o seu nome.
|
|
|
11
11
|
## Instalar (equipe — um comando, sem clonar nada)
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
|
|
14
|
+
npx -y chatflux-gestao-agente@latest configurar
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
No Terminal do Mac ou PowerShell do Windows, esse comando instala a cópia
|
|
18
|
+
permanente do programa, confere a loja oficial de plugins do Claude Code e do
|
|
19
|
+
Codex, atualiza o `chatflux-app`, renova o login quando necessário (abre o
|
|
20
|
+
navegador) e registra/liga o computador. Ferramentas ausentes são puladas; cada
|
|
21
|
+
etapa mostra seu resultado e o que ainda falta fazer. Reinicie o VS Code ao final.
|
|
22
|
+
|
|
23
|
+
Para trocar uma loja antiga, ele pede confirmação. Use `--sim` para autorizar sem
|
|
24
|
+
perguntas, ou `--verificar` para apenas diagnosticar, sem instalar, atualizar,
|
|
25
|
+
abrir login ou ligar o computador. O toolkit é privado: sua conta do GitHub
|
|
26
|
+
precisa ter acesso a `Chatflux-Infra-Tools/chatflux-agent-toolkit`.
|
|
27
|
+
|
|
28
|
+
Depois da instalação, você também pode rodar `gestao configurar`. Se o npm negar
|
|
29
|
+
permissão para instalar globalmente, configure o Node/npm em uma pasta do seu
|
|
30
|
+
usuário e repita, sem sudo nem administrador. Login e ligação do computador
|
|
31
|
+
usam sempre a cópia permanente, nunca o cache temporário do npx.
|
|
32
|
+
|
|
33
|
+
Para registrar o conector MCP diretamente no Claude Code:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
claude mcp add --scope user gestao -- gestao mcp
|
|
37
|
+
```
|
|
20
38
|
|
|
21
39
|
### Instalar (desenvolvimento, dentro deste repo)
|
|
22
40
|
|
|
@@ -50,7 +68,7 @@ gestao login
|
|
|
50
68
|
└─ guarda no Keychain (macOS) ou em ~/.config/gestao/token.json (0600)
|
|
51
69
|
```
|
|
52
70
|
|
|
53
|
-
Vale **
|
|
71
|
+
Vale **1 ano** (decisão de 10/11; a validade real aparece no login e no `gestao status`). E na prática você não faz nada disso à mão: o próprio agente
|
|
54
72
|
chama a tool `gestao_conectar` quando precisa, o navegador abre, você clica em
|
|
55
73
|
**Autorizar** e ele refaz a ação. O `gestao login` no terminal existe como
|
|
56
74
|
alternativa, não como o caminho normal.
|
|
@@ -143,7 +161,7 @@ continuam disponíveis; não contam como prova da abertura assistida.
|
|
|
143
161
|
|
|
144
162
|
### Reconectar, trocar ambiente ou conta
|
|
145
163
|
|
|
146
|
-
O login vence (
|
|
164
|
+
O login vence (1 ano desde o último `gestao login`). O residente pausa os pedidos e orienta
|
|
147
165
|
`gestao login` ou `gestao_conectar`; ele não renova o token silenciosamente.
|
|
148
166
|
`gestao status` mostra ambiente, startup, computador e a causa local mesmo sem
|
|
149
167
|
rede/login. O indicador do portal pode levar até 180 s para perder a conexão.
|
package/bin/gestao.mjs
CHANGED
|
@@ -14,6 +14,7 @@ const AJUDA = `gestao — portal Gestão pela linha de comando e por agente (MCP
|
|
|
14
14
|
|
|
15
15
|
gestao login autoriza este computador a agir como você (abre o navegador)
|
|
16
16
|
gestao status mostra quem está conectado e quanto tempo falta
|
|
17
|
+
gestao configurar [--verificar] [--sim] prepara plugins, login e computador
|
|
17
18
|
gestao ligar instala início automático e associa este computador ao navegador
|
|
18
19
|
gestao desligar remove início automático e interrompe o computador, mesmo offline
|
|
19
20
|
gestao computador executar residente técnico (usado pelo login do SO)
|
|
@@ -30,6 +31,11 @@ const [, , verbo] = process.argv;
|
|
|
30
31
|
|
|
31
32
|
try {
|
|
32
33
|
switch (verbo) {
|
|
34
|
+
case "configurar": {
|
|
35
|
+
const { configurar } = await import("../lib/configurar.mjs");
|
|
36
|
+
await configurar(process.argv.slice(3));
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
33
39
|
case "login": {
|
|
34
40
|
if (nomeServico()) {
|
|
35
41
|
console.error("Modo serviço (GESTAO_SERVICO) ativo: não há login de pessoa. Use `gestao status`.");
|
package/lib/computador-cli.mjs
CHANGED
|
@@ -257,6 +257,7 @@ export function createComputerCli(options = {}) {
|
|
|
257
257
|
const state = await startup.installStartup(startupOptions);
|
|
258
258
|
if (!state.installed || !state.healthy) throw fail('Startup não confirmado.');
|
|
259
259
|
await startResident(identity);
|
|
260
|
+
if (state.correctedEntryPath) announce('Caminho do início automático corrigido.');
|
|
260
261
|
} catch {
|
|
261
262
|
journal.transition({ kind: 'stop', computerId: identity.computerId, generation: registration.generation });
|
|
262
263
|
let rollbackFailed = false;
|
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { spawn } from 'node:child_process';
|
|
9
9
|
|
|
10
|
-
const PLUGIN = 'chatflux-app@chatflux-marketplace';
|
|
11
|
-
const MARKETPLACE = 'chatflux-marketplace';
|
|
10
|
+
export const PLUGIN = 'chatflux-app@chatflux-marketplace';
|
|
11
|
+
export const MARKETPLACE = 'chatflux-marketplace';
|
|
12
12
|
const MAC_BINARIES = { claude: '/opt/homebrew/bin/claude', codex: '/opt/homebrew/bin/codex' };
|
|
13
|
-
const LIST = {
|
|
13
|
+
export const LIST = {
|
|
14
14
|
claude: ['plugin', 'list', '--json'],
|
|
15
15
|
codex: ['plugin', 'list', '--marketplace', MARKETPLACE, '--json'],
|
|
16
16
|
};
|
|
17
|
-
const UPDATES = {
|
|
17
|
+
export const UPDATES = {
|
|
18
18
|
claude: [['plugin', 'marketplace', 'update', MARKETPLACE], ['plugin', 'update', PLUGIN]],
|
|
19
19
|
codex: [['plugin', 'marketplace', 'upgrade', MARKETPLACE]],
|
|
20
20
|
};
|
|
@@ -28,11 +28,13 @@ const failed = (pluginVersion = null, exitCode) => ({
|
|
|
28
28
|
|
|
29
29
|
function dependencies(options, updating = false) {
|
|
30
30
|
const allowed = ['signal', 'spawnImpl', 'platform', 'windowsAdapter', 'timeoutMs'];
|
|
31
|
-
if (updating) allowed.push('previousResults');
|
|
31
|
+
if (updating) allowed.push('previousResults', 'tools', 'marketplaceUpdated');
|
|
32
32
|
if (!options || Object.keys(options).some(k => !allowed.includes(k))) throw new Error('Opções de plugin inválidas.');
|
|
33
33
|
const { spawnImpl = spawn, platform = process.platform, timeoutMs = 120_000, windowsAdapter, signal } = options;
|
|
34
34
|
if (typeof spawnImpl !== 'function' || (windowsAdapter !== undefined && typeof windowsAdapter !== 'function') ||
|
|
35
35
|
!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 120_000) throw new Error('Opções de plugin inválidas.');
|
|
36
|
+
if (updating && ['tools', 'marketplaceUpdated'].some(key => options[key] !== undefined &&
|
|
37
|
+
(!Array.isArray(options[key]) || options[key].some(tool => !['claude', 'codex'].includes(tool))))) throw new Error('Opções de plugin inválidas.');
|
|
36
38
|
return { spawnImpl, platform, timeoutMs, windowsAdapter, signal };
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -86,7 +88,7 @@ function run(tool, argv, deps, capture = false) {
|
|
|
86
88
|
});
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
function installedVersion(tool, stdout) {
|
|
91
|
+
export function installedVersion(tool, stdout) {
|
|
90
92
|
try {
|
|
91
93
|
const data = JSON.parse(stdout);
|
|
92
94
|
const rows = tool === 'claude' ? data : data.installed;
|
|
@@ -111,7 +113,7 @@ export async function readPluginVersions(options = {}) {
|
|
|
111
113
|
return { claudePluginVersion: claude.pluginVersion, codexPluginVersion: codex.pluginVersion };
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
async function update(tool, previous, deps) {
|
|
116
|
+
async function update(tool, previous, deps, marketplaceUpdated = []) {
|
|
115
117
|
// Só sucesso é terminal na retentativa. Projeção explícita descarta campos livres
|
|
116
118
|
// mesmo se o journal antigo contiver dados além do protocolo B1.
|
|
117
119
|
if (previous?.status === 'atualizado') return { status: 'atualizado', pluginVersion: version(previous.pluginVersion) };
|
|
@@ -119,6 +121,7 @@ async function update(tool, previous, deps) {
|
|
|
119
121
|
if (before.missing) return { status: 'nao_instalado' };
|
|
120
122
|
if (!before.ok || !before.pluginVersion) return failed(before.pluginVersion, before.exitCode);
|
|
121
123
|
for (const argv of UPDATES[tool]) {
|
|
124
|
+
if (marketplaceUpdated.includes(tool) && argv[1] === 'marketplace') continue;
|
|
122
125
|
const result = await run(tool, argv, deps);
|
|
123
126
|
if (!result.ok) return failed(before.pluginVersion, result.exitCode);
|
|
124
127
|
}
|
|
@@ -129,8 +132,9 @@ async function update(tool, previous, deps) {
|
|
|
129
132
|
|
|
130
133
|
export async function updatePlugins(options = {}) {
|
|
131
134
|
const deps = dependencies(options, true);
|
|
132
|
-
const
|
|
133
|
-
const
|
|
135
|
+
const tools = options.tools ?? ['claude', 'codex'];
|
|
136
|
+
const claude = tools.includes('claude') ? await update('claude', options.previousResults?.claude, deps, options.marketplaceUpdated) : { status: 'nao_instalado' };
|
|
137
|
+
const codex = tools.includes('codex') ? await update('codex', options.previousResults?.codex, deps, options.marketplaceUpdated) : { status: 'nao_instalado' };
|
|
134
138
|
// Conservador mesmo em sucesso parcial: o comando pode ter alterado arquivos
|
|
135
139
|
// antes da falha. Reinício é orientação; esta API não encerra editor/conversa.
|
|
136
140
|
return { claude, codex, restartRequired: options.previousResults?.restartRequired === true || deps.updateSent === true || claude.status === 'atualizado' || codex.status === 'atualizado' };
|
|
@@ -116,7 +116,16 @@ export function createStartupManager({
|
|
|
116
116
|
const content = read(l.path);
|
|
117
117
|
const entry = l.platform === 'darwin' ? content !== null : win().get(l.name) !== null;
|
|
118
118
|
if (entry && !c) throw conflict();
|
|
119
|
-
if (c && l.platform === 'darwin' && content && content.toString() !== plist(l, c))
|
|
119
|
+
if (c && l.platform === 'darwin' && content && content.toString() !== plist(l, c)) {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(command('/usr/bin/plutil', ['-convert', 'json', '-o', '-', l.path]));
|
|
122
|
+
const args = parsed.ProgramArguments;
|
|
123
|
+
if (parsed.Label !== l.label || !Array.isArray(args) || args.length !== 4 ||
|
|
124
|
+
typeof args[1] !== 'string' || !args[1].endsWith('/bin/gestao.mjs') ||
|
|
125
|
+
args[2] !== 'computador' || args[3] !== 'executar' || parsed.EnvironmentVariables?.GESTAO_ENV !== l.env) throw conflict();
|
|
126
|
+
return true;
|
|
127
|
+
} catch { throw conflict(); }
|
|
128
|
+
}
|
|
120
129
|
if (c && l.platform === 'win32') {
|
|
121
130
|
const value = win().get(l.name);
|
|
122
131
|
if (value !== null && value !== c.command) throw conflict();
|
|
@@ -155,7 +164,7 @@ export function createStartupManager({
|
|
|
155
164
|
const c = { version: 1, env: l.env, platform: l.platform, nodePath: validateFile(options.nodePath, l.platform), entryPath: validateFile(options.entryPath, l.platform), identityPath: validateFile(options.identityPath, l.platform), command: null };
|
|
156
165
|
ensureUserDirectory(l.root, protect); ensureUserDirectory(l.directory, protect);
|
|
157
166
|
return withFileGate(l.directory, () => {
|
|
158
|
-
const previous = metadata(l)
|
|
167
|
+
const previous = metadata(l), changedPlist = owned(l, previous);
|
|
159
168
|
const files = [l.path, l.configPath, ...(l.platform === 'win32' ? [l.launchConfig] : [])];
|
|
160
169
|
const backup = files.map(file => [file, read(file)]);
|
|
161
170
|
const oldService = l.platform === 'darwin' ? serviceState(l) : null;
|
|
@@ -172,7 +181,7 @@ export function createStartupManager({
|
|
|
172
181
|
c.command = `${quoteWindows(path.win32.join(windowsDirectory || 'C:\\Windows', 'System32/wscript.exe'))} //B //Nologo ${quoteWindows(launcher)}`;
|
|
173
182
|
if (c.command.length > 260) throw failure();
|
|
174
183
|
}
|
|
175
|
-
if (JSON.stringify(previous) === JSON.stringify(c) && startupStatus(options).healthy && (l.platform === 'win32' || wasRunning)) return startupStatus(options);
|
|
184
|
+
if (!changedPlist && JSON.stringify(previous) === JSON.stringify(c) && startupStatus(options).healthy && (l.platform === 'win32' || wasRunning)) return startupStatus(options);
|
|
176
185
|
touched = true;
|
|
177
186
|
if (l.platform === 'darwin') {
|
|
178
187
|
fs.mkdirSync(path.dirname(l.path), { recursive: true });
|
|
@@ -187,7 +196,7 @@ export function createStartupManager({
|
|
|
187
196
|
if (win().get(l.name) !== c.command) throw failure();
|
|
188
197
|
}
|
|
189
198
|
write(l.configPath, JSON.stringify(c));
|
|
190
|
-
return startupStatus(options);
|
|
199
|
+
return { ...startupStatus(options), correctedEntryPath: l.platform === 'darwin' && !!previous && (changedPlist || previous.entryPath !== c.entryPath || previous.nodePath !== c.nodePath) };
|
|
191
200
|
} catch {
|
|
192
201
|
if (touched) {
|
|
193
202
|
try {
|
package/lib/config.mjs
CHANGED
|
@@ -39,7 +39,7 @@ export function ambiente() {
|
|
|
39
39
|
//
|
|
40
40
|
// Keychain do macOS quando houver (é onde o SO já guarda segredo e o usuário
|
|
41
41
|
// controla o acesso); arquivo 0600 como fallback nos outros sistemas. O token
|
|
42
|
-
// vale
|
|
42
|
+
// vale 1 ano (template `convex-agente`) — por isso não fica em texto puro no
|
|
43
43
|
// macOS, que é a máquina real da equipe. Revogação antes do `exp`: arquivar a
|
|
44
44
|
// conta (`active:false`); `gestao logout` só apaga a cópia local.
|
|
45
45
|
|
|
@@ -113,6 +113,12 @@ export function descreverValidade(jwt, agora = Date.now()) {
|
|
|
113
113
|
const exp = expiraEm(jwt);
|
|
114
114
|
const restaMin = Math.max(0, Math.round((exp - agora) / 60000));
|
|
115
115
|
if (!exp || restaMin === 0) return "validade desconhecida";
|
|
116
|
+
// Acima de um dia, hora sem data engana ("87599h59 (até 18:40)"): mostra dias e data.
|
|
117
|
+
if (restaMin > 24 * 60) {
|
|
118
|
+
const dias = Math.floor(restaMin / (24 * 60));
|
|
119
|
+
const data = new Date(exp).toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" });
|
|
120
|
+
return `${dias} dia${dias === 1 ? "" : "s"} (até ${data})`;
|
|
121
|
+
}
|
|
116
122
|
const h = Math.floor(restaMin / 60);
|
|
117
123
|
const m = restaMin % 60;
|
|
118
124
|
const dur = h === 0 ? `${m} min` : m === 0 ? `${h}h` : `${h}h${String(m).padStart(2, "0")}`;
|
|
@@ -120,6 +126,20 @@ export function descreverValidade(jwt, agora = Date.now()) {
|
|
|
120
126
|
return `${dur} (até ${hora})`;
|
|
121
127
|
}
|
|
122
128
|
|
|
129
|
+
/** Aviso para o modelo repassar à pessoa quando faltam 15 dias ou menos para o
|
|
130
|
+
* login vencer. Com validade de 1 ano, sem isso o vencimento pega de surpresa.
|
|
131
|
+
* "" fora da janela, com token inválido ou já vencido (aí a chamada falha e o
|
|
132
|
+
* modelo chama `gestao_conectar` por conta própria). */
|
|
133
|
+
export function avisoVencimento(jwt, agora = Date.now()) {
|
|
134
|
+
const exp = expiraEm(jwt);
|
|
135
|
+
if (!exp || exp <= agora || exp - agora > 15 * 86_400_000) return "";
|
|
136
|
+
return (
|
|
137
|
+
`AVISO: o login do Gestão desta máquina vence em ${descreverValidade(jwt, agora)}. ` +
|
|
138
|
+
"Diga isso à pessoa em uma frase e ofereça renovar agora com a tool `gestao_conectar` " +
|
|
139
|
+
"(abre o navegador; vale mais 1 ano)."
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
123
143
|
export function tokenValido(dados, margemMs = 60_000) {
|
|
124
144
|
return Boolean(dados?.token) && expiraEm(dados.token) - margemMs > Date.now();
|
|
125
145
|
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/** Preparação pessoal, sem comandos recebidos do portal. Efeitos só fora de --verificar. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import http from 'node:http';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { createInterface } from 'node:readline/promises';
|
|
9
|
+
import { lerToken, expiraEm } from './config.mjs';
|
|
10
|
+
import { scopeDirectory } from './computador-identidade.mjs';
|
|
11
|
+
import { LIST, UPDATES, MARKETPLACE, PLUGIN, installedVersion, updatePlugins } from './computador-plugin.mjs';
|
|
12
|
+
|
|
13
|
+
const REPO = 'Chatflux-Infra-Tools/chatflux-agent-toolkit';
|
|
14
|
+
const URL_GIT = `https://github.com/${REPO}.git`;
|
|
15
|
+
const ENTRY = fileURLToPath(new URL('../bin/gestao.mjs', import.meta.url));
|
|
16
|
+
const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url))).version;
|
|
17
|
+
// Mesmo predicado cached de computador-startup.mjs; aquele módulo não o exporta.
|
|
18
|
+
const cached = value => /(?:^|[\\/])(?:_npx|_cacache)(?:[\\/]|$)/i.test(value);
|
|
19
|
+
const names = { claude: 'Claude Code', codex: 'Codex' };
|
|
20
|
+
const accessHelp = 'Confira seu acesso ao repositório privado Chatflux-Infra-Tools/chatflux-agent-toolkit no GitHub e a conexão; depois rode gestao configurar de novo.';
|
|
21
|
+
|
|
22
|
+
async function ask(question) {
|
|
23
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
24
|
+
const input = createInterface({ input: process.stdin, output: process.stdout });
|
|
25
|
+
try { return /^(s|sim)$/i.test((await input.question(`${question} `)).trim()); }
|
|
26
|
+
finally { input.close(); }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function configurar(flags = [], options = {}) {
|
|
30
|
+
if (flags.some(flag => !['--sim', '--verificar'].includes(flag))) throw new Error('Use gestao configurar [--sim] [--verificar].');
|
|
31
|
+
const verify = flags.includes('--verificar'), yes = flags.includes('--sim');
|
|
32
|
+
const platform = options.platform ?? process.platform;
|
|
33
|
+
if (!['darwin', 'win32'].includes(platform)) throw new Error('Configurar está disponível para Mac e Windows.');
|
|
34
|
+
const p = platform === 'win32' ? path.win32 : path.posix;
|
|
35
|
+
const fsImpl = options.fsImpl ?? fs, spawnImpl = options.spawnImpl ?? spawn;
|
|
36
|
+
const home = options.home ?? os.homedir(), env = options.env ?? process.env;
|
|
37
|
+
const environment = env.GESTAO_ENV === 'dev' ? 'dev' : 'prod';
|
|
38
|
+
const commandEnv = { ...env, GESTAO_ENV: environment };
|
|
39
|
+
const nodePath = options.nodePath ?? process.execPath, entryPath = fsImpl.realpathSync(options.entryPath ?? ENTRY);
|
|
40
|
+
const version = options.version ?? VERSION, now = options.now ?? Date.now;
|
|
41
|
+
const announce = options.announce ?? console.log, confirm = options.confirm ?? ask;
|
|
42
|
+
const readToken = options.readToken ?? lerToken;
|
|
43
|
+
const results = [];
|
|
44
|
+
let restart = false;
|
|
45
|
+
function result(item, ok, message) {
|
|
46
|
+
const line = `${item}: ${message} ${ok === 'warn' ? '⚠' : ok ? '✓' : '✗'}`;
|
|
47
|
+
results.push({ item, ok, message }); announce(line);
|
|
48
|
+
}
|
|
49
|
+
function read(file) {
|
|
50
|
+
try { return fsImpl.readFileSync(file, 'utf8'); }
|
|
51
|
+
catch (e) { if (e.code === 'ENOENT') return null; throw e; }
|
|
52
|
+
}
|
|
53
|
+
const json = file => { const text = read(file); return text === null ? null : JSON.parse(text); };
|
|
54
|
+
function resolveCommand(tool) {
|
|
55
|
+
const directories = [...new Set([...(env.PATH ?? env.Path ?? '').split(platform === 'win32' ? ';' : ':'),
|
|
56
|
+
p.dirname(nodePath), p.join(home, '.local/bin'), ...(platform === 'darwin' ? ['/opt/homebrew/bin', '/usr/local/bin'] : [])])].filter(Boolean);
|
|
57
|
+
for (const directory of directories) {
|
|
58
|
+
for (const ext of platform === 'win32' ? ['.exe', '.cmd'] : ['']) {
|
|
59
|
+
const binary = p.join(directory, `${tool}${ext}`);
|
|
60
|
+
try {
|
|
61
|
+
fsImpl.accessSync(binary, platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK);
|
|
62
|
+
if (!fsImpl.statSync(binary).isFile()) continue;
|
|
63
|
+
if (ext !== '.cmd') return { binary, prefix: [] };
|
|
64
|
+
// npm gera .cmd que não são executáveis via execFile. Rodar seu JS com
|
|
65
|
+
// o Node evita shell, aspas e expansão de %/& em caminhos do Windows.
|
|
66
|
+
const script = p.join(directory, 'node_modules', { npm: 'npm/bin/npm-cli.js', codex: '@openai/codex/bin/codex.js', claude: '@anthropic-ai/claude-code/cli.js' }[tool]);
|
|
67
|
+
if (fsImpl.existsSync(script)) return { binary: nodePath, prefix: [script] };
|
|
68
|
+
} catch (e) { if (!['ENOENT', 'EACCES'].includes(e.code)) throw e; }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const commands = {};
|
|
74
|
+
function start(command, argv, spawnOptions = {}) {
|
|
75
|
+
if (!command) throw new Error('Programa não encontrado.');
|
|
76
|
+
return spawnImpl(command.binary, [...command.prefix, ...argv], { ...spawnOptions, ...(command.cwd ? { cwd: command.cwd } : {}), env: commandEnv, shell: false, windowsHide: true });
|
|
77
|
+
}
|
|
78
|
+
async function run(command, argv, interactive = false) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
let child, timer, bytes = 0, stdout = '', stderr = '', done = false;
|
|
81
|
+
const finish = (error, value) => { if (done) return; done = true; clearTimeout(timer); error ? reject(error) : resolve(value); };
|
|
82
|
+
const stop = () => { try { child?.kill('SIGKILL'); } catch { /* somente filho próprio */ } finish(new Error('O comando demorou demais; tente novamente.')); };
|
|
83
|
+
try {
|
|
84
|
+
child = start(command, argv, { stdio: interactive ? 'inherit' : ['ignore', 'pipe', 'pipe'] });
|
|
85
|
+
for (const stream of ['stdout', 'stderr']) child[stream]?.on('data', chunk => {
|
|
86
|
+
bytes += Buffer.byteLength(chunk); if (bytes > 1024 * 1024) return stop();
|
|
87
|
+
if (stream === 'stdout') stdout += chunk; else stderr += chunk;
|
|
88
|
+
});
|
|
89
|
+
child.once('error', error => finish(error));
|
|
90
|
+
child.once('close', code => code === 0 ? finish(null, stdout.trim()) : finish(Object.assign(new Error('Não foi possível concluir o comando.'), { permission: /EACCES|EPERM|permission denied/i.test(stderr) })));
|
|
91
|
+
timer = setTimeout(stop, interactive ? 5 * 60_000 : 120_000);
|
|
92
|
+
} catch (error) { finish(error); }
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
announce(`Conferindo este computador · ambiente ${environment}${verify ? ' · somente diagnóstico' : ''}.`);
|
|
97
|
+
// Nada instala startup ou abre login com o ENTRY efêmero do npx.
|
|
98
|
+
let persistent = entryPath;
|
|
99
|
+
if (verify) {
|
|
100
|
+
try {
|
|
101
|
+
commands.npm = resolveCommand('npm');
|
|
102
|
+
const root = await run(commands.npm, ['root', '-g']);
|
|
103
|
+
if (!p.isAbsolute(root)) throw new Error('Pasta global inválida.');
|
|
104
|
+
const installed = json(p.join(root, 'chatflux-gestao-agente/package.json'))?.version;
|
|
105
|
+
let latest = null;
|
|
106
|
+
try { latest = await run(commands.npm, ['view', 'chatflux-gestao-agente', 'version']); } catch { /* sem registro */ }
|
|
107
|
+
const valid = v => typeof v === 'string' && /^\d+\.\d+\.\d+$/.test(v);
|
|
108
|
+
if (!valid(installed)) result('Programa do portal', false, 'instalação permanente não encontrada');
|
|
109
|
+
else if (!valid(latest)) result('Programa do portal', 'warn', `${installed}; versão do npm não confirmada`);
|
|
110
|
+
else if (installed !== latest) result('Programa do portal', false, `${installed} → ${latest} disponível`);
|
|
111
|
+
else result('Programa do portal', true, installed);
|
|
112
|
+
} catch { result('Programa do portal', 'warn', 'instalação permanente não confirmada'); }
|
|
113
|
+
} else if (cached(entryPath)) {
|
|
114
|
+
persistent = null;
|
|
115
|
+
try {
|
|
116
|
+
commands.npm = resolveCommand('npm');
|
|
117
|
+
await run(commands.npm, ['install', '-g', `chatflux-gestao-agente@${version}`]);
|
|
118
|
+
const root = await run(commands.npm, ['root', '-g']);
|
|
119
|
+
if (!p.isAbsolute(root)) throw new Error('Pasta global inválida.');
|
|
120
|
+
const candidate = fsImpl.realpathSync(p.join(root, 'chatflux-gestao-agente/bin/gestao.mjs'));
|
|
121
|
+
if (cached(candidate) || !fsImpl.statSync(candidate).isFile() || json(p.join(root, 'chatflux-gestao-agente/package.json'))?.version !== version) throw new Error('Instalação permanente não confirmada.');
|
|
122
|
+
persistent = candidate;
|
|
123
|
+
result('Programa do portal instalado', true, version);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
const permission = error.permission || ['EACCES', 'EPERM'].includes(error.code);
|
|
126
|
+
result('Programa do portal instalado', false, permission
|
|
127
|
+
? 'Sem permissão para instalar. Configure o npm/Node em uma pasta do seu usuário e repita gestao configurar, sem sudo nem administrador.'
|
|
128
|
+
: 'Instalação não confirmada. Confira o npm e a conexão; repita gestao configurar.');
|
|
129
|
+
}
|
|
130
|
+
} else result('Programa do portal instalado', true, version);
|
|
131
|
+
|
|
132
|
+
for (const tool of ['claude', 'codex']) {
|
|
133
|
+
try { commands[tool] = resolveCommand(tool); result(names[tool], true, commands[tool] ? 'instalado' : 'não instalado; etapa pulada'); }
|
|
134
|
+
catch { result(names[tool], false, 'não foi possível localizar o programa'); }
|
|
135
|
+
}
|
|
136
|
+
function source(tool) {
|
|
137
|
+
if (tool === 'claude') {
|
|
138
|
+
const data = json(p.join(home, '.claude/plugins/known_marketplaces.json'));
|
|
139
|
+
const entry = data?.[MARKETPLACE];
|
|
140
|
+
return { exists: !!entry, official: entry?.source?.source === 'github' && entry.source.repo === REPO };
|
|
141
|
+
}
|
|
142
|
+
const text = read(p.join(env.CODEX_HOME ?? p.join(home, '.codex'), 'config.toml')) ?? '';
|
|
143
|
+
let inside = false, exists = false;
|
|
144
|
+
const values = {};
|
|
145
|
+
for (const line of text.split(/\r?\n/)) {
|
|
146
|
+
const section = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
|
|
147
|
+
if (section) {
|
|
148
|
+
inside = /^marketplaces\.(?:chatflux-marketplace|"chatflux-marketplace"|'chatflux-marketplace')$/.test(section[1].trim());
|
|
149
|
+
if (inside) { if (exists) throw new Error('Loja duplicada no Codex.'); exists = true; }
|
|
150
|
+
} else if (inside && /^\s*source(?:_type)?\s*=/.test(line)) {
|
|
151
|
+
const field = line.match(/^\s*(source_type|source)\s*=\s*("(?:[^"\\]|\\.)*"|'[^']*')\s*(?:#.*)?$/);
|
|
152
|
+
if (!field || Object.hasOwn(values, field[1])) throw new Error('Fonte do Codex não reconhecida.');
|
|
153
|
+
values[field[1]] = field[2][0] === '"' ? JSON.parse(field[2]) : field[2].slice(1, -1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { exists, official: values.source_type === 'git' && values.source === URL_GIT };
|
|
157
|
+
}
|
|
158
|
+
const snapshots = {}, ready = [];
|
|
159
|
+
for (const tool of ['claude', 'codex']) {
|
|
160
|
+
if (!commands[tool]) continue;
|
|
161
|
+
try {
|
|
162
|
+
const store = source(tool);
|
|
163
|
+
if (verify) {
|
|
164
|
+
result(`${names[tool]} — loja de plugins`, store.official, store.official ? 'fonte oficial; pode atualizar' : 'falta trocar para a fonte oficial');
|
|
165
|
+
if (tool === 'claude') {
|
|
166
|
+
const installed = json(p.join(home, '.claude/plugins/installed_plugins.json'));
|
|
167
|
+
const rows = installed?.plugins?.[PLUGIN];
|
|
168
|
+
if (Array.isArray(rows)) snapshots[tool] = { version: installedVersion(tool, JSON.stringify(rows.map(row => ({ ...row, id: PLUGIN })))) };
|
|
169
|
+
} else try { snapshots[tool] = { version: installedVersion(tool, await run(commands[tool], LIST[tool])) }; } catch { /* versão desconhecida */ }
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const output = await run(commands[tool], LIST[tool]);
|
|
173
|
+
const data = JSON.parse(output), rows = tool === 'claude' ? data : data.installed;
|
|
174
|
+
if (!Array.isArray(rows)) throw new Error('Lista de plugins não reconhecida.');
|
|
175
|
+
snapshots[tool] = { output, version: installedVersion(tool, output) };
|
|
176
|
+
const plugins = rows.filter(row => tool === 'claude' ? row?.id?.endsWith(`@${MARKETPLACE}`) : row?.installed === true && row.pluginId?.endsWith(`@${MARKETPLACE}`))
|
|
177
|
+
.map(row => ({ id: tool === 'claude' ? row.id : row.pluginId, scope: tool === 'claude' ? row.scope ?? 'user' : 'user', cwd: row.projectPath }));
|
|
178
|
+
if (!plugins.some(row => row.id === PLUGIN && row.scope === 'user')) plugins.push({ id: PLUGIN, scope: 'user' });
|
|
179
|
+
if (plugins.some(row => !/^[a-zA-Z0-9_-]+@chatflux-marketplace$/.test(row.id) || !['user', 'project', 'local'].includes(row.scope) || (row.scope !== 'user' && !p.isAbsolute(row.cwd ?? '')))) throw new Error('Não foi possível preservar os plugins instalados.');
|
|
180
|
+
if (!store.official) {
|
|
181
|
+
if (!yes && !await confirm('Trocar a loja de plugins para a oficial? (s/n)')) {
|
|
182
|
+
result(`${names[tool]} — loja de plugins`, false, 'troca não autorizada; repita no terminal ou use --sim para autorizar'); continue;
|
|
183
|
+
}
|
|
184
|
+
restart = true;
|
|
185
|
+
if (store.exists) await run(commands[tool], ['plugin', 'marketplace', 'remove', MARKETPLACE]);
|
|
186
|
+
await run(commands[tool], ['plugin', 'marketplace', 'add', tool === 'claude' ? REPO : URL_GIT]);
|
|
187
|
+
}
|
|
188
|
+
restart = true;
|
|
189
|
+
await run(commands[tool], UPDATES[tool][0]);
|
|
190
|
+
ready.push(tool);
|
|
191
|
+
const missingApp = !rows.some(row => tool === 'claude' ? row.id === PLUGIN && row.scope === 'user' : row.pluginId === PLUGIN && row.installed);
|
|
192
|
+
let restored = true;
|
|
193
|
+
for (const row of !store.official ? plugins : missingApp ? [{ id: PLUGIN, scope: 'user' }] : []) {
|
|
194
|
+
try {
|
|
195
|
+
// O escopo de projeto usa o diretório original, sem alterar o cwd do processo.
|
|
196
|
+
await run({ ...commands[tool], cwd: row.cwd }, tool === 'claude' ? ['plugin', 'install', row.id, '--scope', row.scope] : ['plugin', 'add', row.id]);
|
|
197
|
+
} catch { restored = false; }
|
|
198
|
+
}
|
|
199
|
+
result(`${names[tool]} — loja de plugins`, restored, restored ? 'fonte oficial atualizada; plugins preservados' : `fonte oficial atualizada; falta reinstalar um plugin. ${accessHelp}`);
|
|
200
|
+
} catch {
|
|
201
|
+
result(`${names[tool]} — loja de plugins`, false, `Não foi possível conferir ou atualizar a loja. ${accessHelp}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (verify) {
|
|
205
|
+
let latest = null, origin = '';
|
|
206
|
+
try {
|
|
207
|
+
const data = readToken();
|
|
208
|
+
if (data?.env === environment && expiraEm(data.token) > now()) {
|
|
209
|
+
const host = environment === 'dev' ? 'https://agreeable-woodpecker-190.convex.cloud' : 'https://dependable-stingray-656.convex.cloud';
|
|
210
|
+
const response = await (options.fetchImpl ?? fetch)(`${host}/api/query`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${data.token}` }, body: JSON.stringify({ path: 'ops/plugin:meuStatus', args: {}, format: 'json' }), signal: AbortSignal.timeout(10000) });
|
|
211
|
+
const body = await response.json();
|
|
212
|
+
if (response.ok && body.status === 'success') latest = body.value?.atual;
|
|
213
|
+
}
|
|
214
|
+
} catch { /* tenta loja local */ }
|
|
215
|
+
if (!/^\d+\.\d+\.\d+$/.test(latest ?? '')) {
|
|
216
|
+
try { latest = json(p.join(home, '.claude/plugins/marketplaces/chatflux-marketplace/plugins/chatflux-app/.claude-plugin/plugin.json'))?.version; origin = ' (versão da loja)'; } catch { /* indisponível */ }
|
|
217
|
+
}
|
|
218
|
+
if (!/^\d+\.\d+\.\d+$/.test(latest ?? '')) latest = null;
|
|
219
|
+
for (const tool of ['claude', 'codex']) if (commands[tool]) {
|
|
220
|
+
const installed = snapshots[tool]?.version;
|
|
221
|
+
if (!installed) result(`${names[tool]} — chatflux-app`, 'warn', `versão não confirmada${latest && origin ? `; versão da loja: ${latest}` : ''}`);
|
|
222
|
+
else if (!latest) result(`${names[tool]} — chatflux-app`, 'warn', `${installed}; versão atual não confirmada`);
|
|
223
|
+
else if (installed !== latest) result(`${names[tool]} — chatflux-app`, false, `${installed} → atual ${latest}${origin} (vai atualizar)`);
|
|
224
|
+
else result(`${names[tool]} — chatflux-app`, true, `${installed}${origin}`);
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
const bridge = (tool, argv, spawnOptions) => start(commands[tool], argv, spawnOptions);
|
|
228
|
+
const updated = await updatePlugins({ platform, tools: ready, marketplaceUpdated: ready,
|
|
229
|
+
...(platform === 'win32' ? { windowsAdapter: bridge } : { spawnImpl: (binary, argv, spawnOptions) => bridge(binary.endsWith('/claude') ? 'claude' : 'codex', argv, spawnOptions) }) });
|
|
230
|
+
restart ||= updated.restartRequired;
|
|
231
|
+
for (const tool of ['claude', 'codex']) if (commands[tool]) {
|
|
232
|
+
const item = updated[tool], ok = ready.includes(tool) && item.status === 'atualizado';
|
|
233
|
+
result(`${names[tool]} — chatflux-app`, ok, ok ? item.pluginVersion : `atualização não confirmada${item.pluginVersion ? `; versão ${item.pluginVersion}` : ''}. ${accessHelp}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const credential = () => { const value = readToken(); return value?.env === environment ? value : null; };
|
|
238
|
+
const expiry = value => value?.token ? expiraEm(value.token) : 0;
|
|
239
|
+
const until = value => new Date(expiry(value)).toLocaleString('pt-BR');
|
|
240
|
+
let loginOk = false;
|
|
241
|
+
try {
|
|
242
|
+
let data = credential();
|
|
243
|
+
if (expiry(data) - now() <= 15 * 86400000) {
|
|
244
|
+
if (verify) result('Login do portal', false, 'ausente, vencido ou vence em até 15 dias; precisa renovar no navegador');
|
|
245
|
+
else if (!persistent) result('Login do portal', false, 'pulado; instale o programa do portal em uma pasta permanente');
|
|
246
|
+
else {
|
|
247
|
+
await run({ binary: nodePath, prefix: [persistent] }, ['login'], true);
|
|
248
|
+
data = credential(); loginOk = expiry(data) > now();
|
|
249
|
+
result('Login do portal', loginOk, loginOk ? `conectado até ${until(data)}` : 'login não confirmado; rode gestao login');
|
|
250
|
+
}
|
|
251
|
+
} else { loginOk = true; result('Login do portal', true, `válido até ${until(data)}`); }
|
|
252
|
+
} catch { result('Login do portal', false, 'não foi possível conectar; rode gestao login e autorize no navegador'); }
|
|
253
|
+
|
|
254
|
+
// statusComputer protege arquivos e openJournal cria diretórios. O diagnóstico
|
|
255
|
+
// usa apenas um retrato dos metadados, sem ACL, journal aberto ou canal local.
|
|
256
|
+
function readOnlyComputer() {
|
|
257
|
+
const baseDir = p.join(home, '.config/gestao/computadores');
|
|
258
|
+
const active = json(p.join(baseDir, environment, 'ativo.json'));
|
|
259
|
+
if (!active?.subject) return { computerId: null, startupInstalled: false, startupHealthy: false };
|
|
260
|
+
const directory = p.join(baseDir, environment, p.basename(scopeDirectory({ env: environment, subject: active.subject })));
|
|
261
|
+
const identity = json(p.join(directory, 'identidade.json'));
|
|
262
|
+
const journal = json(p.join(directory, 'journal.json'));
|
|
263
|
+
const startupRoot = platform === 'darwin' ? p.join(home, '.config/gestao/startup') : p.join(env.LOCALAPPDATA || home, 'ChatFluxGestao/startup');
|
|
264
|
+
const config = json(p.join(startupRoot, environment, 'startup.json'));
|
|
265
|
+
const launcher = platform === 'darwin'
|
|
266
|
+
? p.join(home, 'Library/LaunchAgents', `ai.chatflux.gestao-computador.${environment}.plist`)
|
|
267
|
+
: p.join(startupRoot, environment, 'launcher.vbs');
|
|
268
|
+
const configured = !!config && config.env === environment && config.platform === platform && fsImpl.existsSync(launcher) &&
|
|
269
|
+
[config.nodePath, config.entryPath, config.identityPath].every(file => typeof file === 'string' && !cached(file) && fsImpl.existsSync(file));
|
|
270
|
+
return { directory, computerId: identity?.computerId, startupInstalled: configured, startupHealthy: configured,
|
|
271
|
+
state: journal?.stopped ? 'desligado' : 'configurado; execução não conferida' };
|
|
272
|
+
}
|
|
273
|
+
async function checkResident(directory) {
|
|
274
|
+
const file = p.join(directory, 'controle.json');
|
|
275
|
+
let stat;
|
|
276
|
+
try { stat = fsImpl.lstatSync(file); } catch (e) { if (e.code === 'ENOENT') return null; throw e; }
|
|
277
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 8192) throw new Error('Controle inválido.');
|
|
278
|
+
const data = json(file);
|
|
279
|
+
if (data?.version !== 1 || !Number.isSafeInteger(data.port) || data.port < 1 || data.port > 65535 || !Number.isSafeInteger(data.pid) || data.pid < 1 || !/^[a-f0-9]{64}$/.test(data.key)) throw new Error('Controle inválido.');
|
|
280
|
+
return new Promise((resolve, reject) => {
|
|
281
|
+
const req = http.request({ host: '127.0.0.1', port: data.port, path: '/status', method: 'POST', headers: { Authorization: `Bearer ${data.key}` }, timeout: 3000, agent: false }, res => {
|
|
282
|
+
let body = ''; res.setEncoding('utf8');
|
|
283
|
+
res.on('data', chunk => { body += chunk; if (body.length > 8192) req.destroy(); });
|
|
284
|
+
res.on('end', () => { try { if (res.statusCode !== 200) throw new Error(); resolve(JSON.parse(body)); } catch { reject(new Error('Controle indisponível.')); } });
|
|
285
|
+
});
|
|
286
|
+
req.on('timeout', () => req.destroy()); req.on('error', reject); req.end();
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const status = verify ? (options.readOnlyComputer ?? readOnlyComputer)() : await (options.statusComputer ?? (await import('./computador-cli.mjs')).statusComputer)();
|
|
291
|
+
if (verify && status.computerId && status.startupInstalled && status.startupHealthy && status.state !== 'desligado') {
|
|
292
|
+
let live;
|
|
293
|
+
try { live = await (options.checkResident ?? checkResident)(status.directory); }
|
|
294
|
+
catch { live = undefined; }
|
|
295
|
+
if (live?.state) result('Computador', ['conectado', 'residente iniciado'].includes(live.state) ? true : 'warn', `residente em execução; ${live.state}`);
|
|
296
|
+
else if (live === null) result('Computador', false, 'configurado; residente não está rodando');
|
|
297
|
+
else result('Computador', 'warn', 'configurado; não deu para conferir se está rodando');
|
|
298
|
+
} else {
|
|
299
|
+
const connected = !!status.computerId && status.startupInstalled && status.startupHealthy &&
|
|
300
|
+
['conectado', 'residente iniciado'].includes(status.state);
|
|
301
|
+
if (connected) result('Computador', true, verify ? status.state : 'já registrado e ligado');
|
|
302
|
+
else if (verify) result('Computador', false, 'precisa registrar ou ligar; rode gestao configurar');
|
|
303
|
+
else if (!persistent || !loginOk) result('Computador', false, 'etapa pulada; conclua a instalação e o login, depois repita gestao configurar');
|
|
304
|
+
else {
|
|
305
|
+
await run({ binary: nodePath, prefix: [persistent] }, ['ligar'], true);
|
|
306
|
+
result('Computador', true, 'registrado e ligado');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} catch { result('Computador', false, 'não foi possível conferir ou ligar; rode gestao ligar e siga a orientação exibida'); }
|
|
310
|
+
const pending = results.filter(item => item.ok !== true);
|
|
311
|
+
if (!pending.length) announce('\nTudo pronto neste computador ✓');
|
|
312
|
+
else {
|
|
313
|
+
announce('\nResumo:');
|
|
314
|
+
for (const item of pending) announce(`${item.item}: ${item.message} ${item.ok === 'warn' ? '⚠' : '✗'}`);
|
|
315
|
+
}
|
|
316
|
+
if (restart) announce('Para terminar: reinicie o VS Code e abra uma nova conversa no Claude Code/Codex.');
|
|
317
|
+
if (verify && pending.length) announce('Para resolver, rode: npx -y chatflux-gestao-agente@latest configurar');
|
|
318
|
+
return { results, restartRequired: restart };
|
|
319
|
+
}
|
package/lib/gestao.mjs
CHANGED
|
@@ -73,7 +73,7 @@ let cache = null;
|
|
|
73
73
|
* disco — é cache, não fonte: só serve pra listar tools quando a credencial
|
|
74
74
|
* expirou, e qualquer chamada de verdade continua exigindo login.
|
|
75
75
|
*
|
|
76
|
-
* Isso existe porque o token vence (
|
|
76
|
+
* Isso existe porque o token vence (1 ano): sem o cache, a pessoa veria o servidor
|
|
77
77
|
* MCP "falhar ao conectar" a cada expiração, sem pista do motivo.
|
|
78
78
|
*/
|
|
79
79
|
export async function contrato({ permitirCache = false } = {}) {
|
package/lib/login.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// `gestao login` — o handshake de identidade.
|
|
2
2
|
//
|
|
3
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-agente` (
|
|
4
|
+
// `state`, e espera a página devolver o JWT do template `convex-agente` (1 ano). O browser já
|
|
5
5
|
// tem a sessão do Clerk; nós nunca vemos senha, e o backend não ganha rota nova.
|
|
6
6
|
//
|
|
7
7
|
// Por que NÃO OAuth: o Clerk não documenta caminho de credencial OAuth → JWT com
|
package/lib/mcp.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import path from "node:path";
|
|
|
13
13
|
import { contrato, chamar, euSou } from "./gestao.mjs";
|
|
14
14
|
import { loginVerificado } from "./login.mjs";
|
|
15
15
|
import { nomeServico } from "./servico.mjs";
|
|
16
|
-
import { ambiente, descreverValidade } from "./config.mjs";
|
|
16
|
+
import { ambiente, avisoVencimento, descreverValidade, lerToken } from "./config.mjs";
|
|
17
17
|
import { montarTools } from "./tools.mjs";
|
|
18
18
|
|
|
19
19
|
const PROTOCOLO = "2024-11-05";
|
|
@@ -41,7 +41,7 @@ let tools = null;
|
|
|
41
41
|
/**
|
|
42
42
|
* A tool que CONECTA — não a que pede pra pessoa digitar comando.
|
|
43
43
|
*
|
|
44
|
-
* Fica SEMPRE na lista, autenticado ou não, porque a sessão vence (
|
|
44
|
+
* Fica SEMPRE na lista, autenticado ou não, porque a sessão vence (1 ano no
|
|
45
45
|
* template `convex-agente`): quando qualquer ação falhar por sessão, o modelo chama esta e a pessoa
|
|
46
46
|
* só clica no navegador. Ela nunca precisa saber que existe uma CLI.
|
|
47
47
|
*/
|
|
@@ -49,7 +49,7 @@ const TOOL_CONECTAR = {
|
|
|
49
49
|
name: "gestao_conectar",
|
|
50
50
|
description:
|
|
51
51
|
"Conecta esta máquina ao Gestão. ABRE O NAVEGADOR para a pessoa autorizar com " +
|
|
52
|
-
"o login dela (leva segundos) e vale
|
|
52
|
+
"o login dela (leva segundos) e vale 1 ano.\n\n" +
|
|
53
53
|
"CHAME ISTO SEM PEDIR PERMISSÃO sempre que uma ação do Gestão falhar por " +
|
|
54
54
|
"sessão expirada ou ausente, ou quando não houver nenhuma outra tool do Gestão " +
|
|
55
55
|
"disponível. Depois de conectar, refaça a ação que falhou.\n\n" +
|
|
@@ -60,6 +60,20 @@ const TOOL_CONECTAR = {
|
|
|
60
60
|
_endpoint: null,
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
+
/** Uma vez por processo (= por sessão do harness), na primeira chamada que deu
|
|
64
|
+
* certo: se o login vence em até 15 dias, devolve o aviso para ir junto da
|
|
65
|
+
* resposta. Modo serviço não tem login de pessoa. */
|
|
66
|
+
let vencimentoConferido = false;
|
|
67
|
+
function conferirVencimento() {
|
|
68
|
+
if (vencimentoConferido || nomeServico()) return "";
|
|
69
|
+
vencimentoConferido = true;
|
|
70
|
+
try {
|
|
71
|
+
return avisoVencimento(lerToken()?.token);
|
|
72
|
+
} catch {
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
/** Estamos no estado degradado (só a tool de conectar)? */
|
|
64
78
|
const soConectar = (t) => t?.length === 1 && t[0]._endpoint === null;
|
|
65
79
|
|
|
@@ -167,7 +181,10 @@ async function tratar(msg) {
|
|
|
167
181
|
}
|
|
168
182
|
try {
|
|
169
183
|
const valor = await chamar(tool._endpoint, params.arguments ?? {}, tool._kind);
|
|
170
|
-
|
|
184
|
+
const resultado = texto(valor ?? { ok: true });
|
|
185
|
+
const aviso = conferirVencimento();
|
|
186
|
+
if (aviso) resultado.content.push({ type: "text", text: aviso });
|
|
187
|
+
return ok(id, resultado);
|
|
171
188
|
} catch (e) {
|
|
172
189
|
// Erro de REGRA (ex.: "Empresa arquivada…") volta como conteúdo, não como
|
|
173
190
|
// erro de protocolo: é informação que o modelo deve ler e agir em cima.
|
|
@@ -179,7 +196,7 @@ async function tratar(msg) {
|
|
|
179
196
|
}
|
|
180
197
|
|
|
181
198
|
export async function servir() {
|
|
182
|
-
// NUNCA sai por falta de credencial. O token vence (
|
|
199
|
+
// NUNCA sai por falta de credencial. O token vence (1 ano); se o servidor morresse
|
|
183
200
|
// ao expirar, a pessoa veria "Failed to connect — Connection closed" a cada
|
|
184
201
|
// expiração, sem pista do motivo. Aqui ele sobe sempre, e a falta de sessão
|
|
185
202
|
// aparece como uma tool que EXPLICA o que fazer.
|
package/package.json
CHANGED