chatflux-gestao-agente 0.2.0 → 0.2.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.
- package/README.md +2 -2
- package/lib/computador-adaptadores.mjs +3 -2
- package/lib/computador-cli.mjs +1 -0
- package/lib/computador-estado.mjs +1 -0
- package/lib/computador-executor.mjs +13 -1
- package/lib/computador-pastas.mjs +11 -0
- package/lib/computador-startup.mjs +13 -4
- package/lib/config.mjs +21 -1
- 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
|
@@ -50,7 +50,7 @@ gestao login
|
|
|
50
50
|
└─ guarda no Keychain (macOS) ou em ~/.config/gestao/token.json (0600)
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
Vale **
|
|
53
|
+
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
54
|
chama a tool `gestao_conectar` quando precisa, o navegador abre, você clica em
|
|
55
55
|
**Autorizar** e ele refaz a ação. O `gestao login` no terminal existe como
|
|
56
56
|
alternativa, não como o caminho normal.
|
|
@@ -143,7 +143,7 @@ continuam disponíveis; não contam como prova da abertura assistida.
|
|
|
143
143
|
|
|
144
144
|
### Reconectar, trocar ambiente ou conta
|
|
145
145
|
|
|
146
|
-
O login vence (
|
|
146
|
+
O login vence (1 ano desde o último `gestao login`). O residente pausa os pedidos e orienta
|
|
147
147
|
`gestao login` ou `gestao_conectar`; ele não renova o token silenciosamente.
|
|
148
148
|
`gestao status` mostra ambiente, startup, computador e a causa local mesmo sem
|
|
149
149
|
rede/login. O indicador do portal pode levar até 180 s para perder a conexão.
|
|
@@ -31,7 +31,7 @@ export async function createComputerAdapters(deps = {}) {
|
|
|
31
31
|
const unavailable = async () => { throw new Error('Receita Windows sem ensaio de campo.'); };
|
|
32
32
|
return { editor: createEditorAdapter({ platform, spawnImpl }), reply: createReplyAdapter({ platform }),
|
|
33
33
|
history: { capabilities: { lerHistoricoClaude: false, lerHistoricoCodex: false }, read: unavailable },
|
|
34
|
-
folders: { ...folders, capabilities: { seletorPasta: false }, chooseFolder: unavailable },
|
|
34
|
+
folders: { ...folders, openLinkedFolder: options => folders.openLinkedFolder({ ...options, platform, fsImpl, execFileImpl }), capabilities: { seletorPasta: false }, chooseFolder: unavailable },
|
|
35
35
|
git: {}, plugins: {}, capabilityDetails };
|
|
36
36
|
}
|
|
37
37
|
// launchd does not inherit the login-shell PATH. Keep personal credentials,
|
|
@@ -245,7 +245,8 @@ export async function createComputerAdapters(deps = {}) {
|
|
|
245
245
|
if (!executable) throw Object.assign(new Error('CLI local ausente.'), { code: 'ENOENT' });
|
|
246
246
|
return spawnImpl(executable, argv, { ...options, env: commandEnv });
|
|
247
247
|
};
|
|
248
|
-
return { editor, reply, history, folders: { ...folders,
|
|
248
|
+
return { editor, reply, history, folders: { ...folders,
|
|
249
|
+
openLinkedFolder: options => folders.openLinkedFolder({ ...options, platform, fsImpl, execFileImpl }), capabilities: { seletorPasta: true },
|
|
249
250
|
chooseFolder: options => folders.chooseFolder({ ...options, platform, fsImpl, spawnImpl }),
|
|
250
251
|
adoptKnownFolder: options => folders.adoptKnownFolder({ ...options, platform, fsImpl, home, execFileImpl }) },
|
|
251
252
|
git: { ...createGitAdapter({ platform, fsImpl, spawnImpl: (binary, argv, options) =>
|
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;
|
|
@@ -44,6 +44,7 @@ const plugin = x => ({
|
|
|
44
44
|
const opening = { git: optional(git), openingSent: optional(bool),
|
|
45
45
|
sessaoNaPasta: optional(bool), atualizarPelaIaIndisponivel: optional(bool) };
|
|
46
46
|
const privateResult = x => ({
|
|
47
|
+
abrir_pasta: shape({ kind: oneOf('abrir_pasta') }),
|
|
47
48
|
abrir_sessao: shape({ kind: oneOf('abrir_sessao'), canonicalPath: optional(text), ...opening }),
|
|
48
49
|
baixar_abrir: shape({ kind: oneOf('baixar_abrir'), canonicalPath: text, ...opening }),
|
|
49
50
|
escolher_pasta_projeto: shape({ kind: oneOf('escolher_pasta_projeto'), canonicalPath: text, ...opening }),
|
|
@@ -12,7 +12,7 @@ import { readUserJson } from './computador-identidade.mjs';
|
|
|
12
12
|
import { createReplyAdapter } from './computador-resposta.mjs';
|
|
13
13
|
|
|
14
14
|
const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url))).version;
|
|
15
|
-
const ACTIONS = ['abrir_sessao', 'baixar_abrir', 'escolher_pasta_projeto', 'escolher_pasta_padrao', 'atualizar_plugin', 'responder_sessao', 'ler_historico'];
|
|
15
|
+
const ACTIONS = ['abrir_pasta', 'abrir_sessao', 'baixar_abrir', 'escolher_pasta_projeto', 'escolher_pasta_padrao', 'atualizar_plugin', 'responder_sessao', 'ler_historico'];
|
|
16
16
|
const OPENING = ['abrindo_pasta', 'aguardando_editor', 'abrindo_conversa'];
|
|
17
17
|
const RETRIES = [2000, 5000, 15000, 30000, 60000, 120000];
|
|
18
18
|
const CLAIM = 'ops.computerRequestActions.claim', REPORT = 'ops.computerRequestActions.report';
|
|
@@ -508,6 +508,18 @@ export async function runComputer({ identity, portal, journal, paths, folders, e
|
|
|
508
508
|
check(r);
|
|
509
509
|
deadlineTimer = later(() => controller.abort(), Math.max(0, r.expiresAt - now()));
|
|
510
510
|
renew();
|
|
511
|
+
if (r.action === 'abrir_pasta') {
|
|
512
|
+
await phase('conferindo_pasta');
|
|
513
|
+
const link = await portal.call('ops.computers.projectPath', { projectId: r.projectId }, { deadlineAt: budget() }); check(r);
|
|
514
|
+
if (!link) throw fail('FOLDER_REQUIRED');
|
|
515
|
+
if (link.computerId !== identity.computerId || link.projectId !== r.projectId || link.repoFullName !== r.repoFullName) throw fail('PATH_NOT_AUTHORIZED');
|
|
516
|
+
await phase('abrindo_pasta');
|
|
517
|
+
await effect('abrindo_pasta', () => {
|
|
518
|
+
const local = authorized(link.canonicalPath);
|
|
519
|
+
return folders.openLinkedFolder({ path: local.path, registeredPath: link.canonicalPath, signal: operationSignal });
|
|
520
|
+
}, () => ({ kind: 'concluir', privateResult: { kind: 'abrir_pasta' } }));
|
|
521
|
+
return r;
|
|
522
|
+
}
|
|
511
523
|
if (r.action === 'atualizar_plugin') {
|
|
512
524
|
if (!capabilities.atualizarPlugin) throw fail('EDITOR_RECIPE_UNVERIFIED');
|
|
513
525
|
await phase('atualizando_plugin');
|
|
@@ -155,3 +155,14 @@ export async function adoptKnownFolder(options) {
|
|
|
155
155
|
}
|
|
156
156
|
return null;
|
|
157
157
|
}
|
|
158
|
+
|
|
159
|
+
/** Only the resident supplies this path after resolving the project binding. */
|
|
160
|
+
export async function openLinkedFolder({ path: candidate, registeredPath, platform = process.platform, fsImpl = fs, execFileImpl = execFile, signal }) {
|
|
161
|
+
const canonicalPath = validatePath({ candidate, registeredPath, platform, fsImpl });
|
|
162
|
+
if (canonicalPath !== registeredPath || !fsImpl.statSync(canonicalPath).isDirectory()) throw pathError();
|
|
163
|
+
const binary = platform === 'darwin' ? '/usr/bin/open' : platform === 'win32' ? 'explorer.exe' : null;
|
|
164
|
+
if (!binary) throw fail('FOLDER_PLATFORM_UNSUPPORTED');
|
|
165
|
+
await new Promise((resolve, reject) => execFileImpl(binary, [canonicalPath], {
|
|
166
|
+
shell: false, windowsHide: true, timeout: 10000, ...(signal ? { signal } : {}),
|
|
167
|
+
}, error => error ? reject(error) : resolve()));
|
|
168
|
+
}
|
|
@@ -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
|
}
|
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