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 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 **até 6 horas** (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
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 (normalmente até 6 h). O residente pausa os pedidos e orienta
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, capabilities: { seletorPasta: true },
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) =>
@@ -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)) throw conflict();
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); owned(l, previous);
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 até 6h (template `convex-agente`) — por isso não fica em texto puro no
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 (6h): sem o cache, a pessoa veria o servidor
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` (6h). O browser já
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 (6h no
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 algumas horas (6h no padrão).\n\n" +
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
- return ok(id, texto(valor ?? { ok: true }));
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 (6h); se o servidor morresse
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatflux-gestao-agente",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
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
6
  "keywords": [