kafox-cli 0.6.0 → 0.8.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 CHANGED
@@ -12,7 +12,7 @@ npm install -g kafox-cli
12
12
  O comando instalado e `kafox`. Alternativa sem npm:
13
13
 
14
14
  ```bash
15
- npm install -g https://kafox.pypag.com/static/downloads/kafox-cli-0.3.0.tgz
15
+ npm install -g https://kafox.pypag.com/static/downloads/kafox-cli-0.8.0.tgz
16
16
  ```
17
17
 
18
18
  Requisito: Node.js 22.5+.
@@ -23,9 +23,10 @@ Requisito: Node.js 22.5+.
23
23
  kafox login
24
24
  ```
25
25
 
26
- A CLI mostra um codigo e a URL `https://kafox.pypag.com/cli?code=...`. Abra no
27
- navegador, entre na sua conta e clique em **Autorizar**. A sessao fica em
28
- `~/.kafox/token.json` (permissao 600).
26
+ O terminal vira uma tela de login (arte grande em cima, link no centro): abra a
27
+ URL `https://kafox.pypag.com/cli?code=...`, entre na sua conta e clique em
28
+ **Autorizar**. A sessao fica em `~/.kafox/token.json` (permissao 600). No primeiro
29
+ `kafox` sem sessao ele pergunta se quer entrar e abre a mesma tela.
29
30
 
30
31
  ## Comandos
31
32
 
@@ -39,17 +40,70 @@ navegador, entre na sua conta e clique em **Autorizar**. A sessao fica em
39
40
  | `kafox balance` | saldo atual |
40
41
  | `kafox key` | cria uma API key |
41
42
  | `kafox ask "pergunta" [-m modelo]` | pergunta ao gateway |
43
+ | `kafox agent "objetivo" [-y] [--max-steps 12] [--json]` | roda o agente sem TUI (script/CI) |
44
+ | `kafox history [n]` | lista as conversas salvas (titulo - data + como retomar) |
45
+ | `kafox resume [codigo]` | retoma uma conversa; sem codigo abre o seletor na tela |
46
+ | `kafox continue` / `kafox -c` | volta para a conversa mais recente |
42
47
  | `kafox status` | conta, saldo e gateway |
43
48
  | `kafox theme` | seletor de tema |
44
49
  | `kafox background [url]` | imagem de fundo do terminal |
45
50
  | `kafox compact` / `clear` | compacta / limpa o contexto do REPL |
46
51
 
52
+ ## Conversas salvas
53
+
54
+ Toda conversa fica em `~/.kafox/chats/<codigo>.json`. Ao sair com **Ctrl+C** a
55
+ CLI mostra o codigo:
56
+
57
+ ```
58
+ Retome esta conversa: kafox --resume 3f1c9a0b
59
+ ```
60
+
61
+ `kafox history` lista da mais nova para a mais antiga, cada uma com o comando
62
+ que retoma (e `kafox history 20` mostra so as 20 ultimas):
63
+
64
+ ```
65
+ hello world em python, tem como fazer um sis... - 28/10
66
+ kafox resume 3f1c9a0b
67
+
68
+ nessa cli que voce ve, tem como fazer alguma co... - 27/10
69
+ kafox resume 8ad2f19c
70
+ ```
71
+
72
+ `kafox resume` (sem codigo) abre o seletor com os titulos das conversas — setas
73
+ ou a roda do mouse navegam, clique ou Enter retomam a conversa escolhida.
74
+ `kafox resume 3f1c` aceita o codigo inteiro ou so o comeco dele, e
75
+ `kafox continue` (ou `-c`) vai direto para a ultima.
76
+
77
+ ## Agentes e subagentes
78
+
79
+ O agente tem a ferramenta `spawn_subagent`: ele lança subagentes com objetivo
80
+ próprio, cada um num contexto separado, e recebe só o relatório final. Vão até
81
+ 3 tarefas por chamada e as independentes rodam em paralelo. Por padrão o
82
+ subagente só lê (`read_file`, `list_dir`); escrever, editar ou executar exige
83
+ declarar a ferramenta na tarefa — e continua passando pela aprovação normal
84
+ (ou pelo `/allow`).
85
+
86
+ ```bash
87
+ kafox agent "revise src/agent.js e aponte bugs" --max-steps 8
88
+ kafox agent "rode os testes do backend e resuma as falhas" -y --json
89
+ ```
90
+
91
+ Para ver o que os subagentes da sessão fizeram (objetivo, ferramentas, tempo e
92
+ se terminaram), use `/agents` — o mesmo comando funciona no REPL e na TUI.
93
+
47
94
  Na interface: digite `/` para o menu (↑/↓ navega, Tab completa, Enter executa),
48
95
  `/model` abre o seletor de modelo e `/theme` o de tema (↑/↓, clique do mouse,
49
96
  Enter aplica, Esc cancela). A resposta chega em streaming, com bloco `Thinking`
50
97
  quando o modelo manda o raciocinio. PgUp/PgDn ou a roda do mouse rolam a conversa;
51
98
  Ctrl+Y copia a ultima resposta e F2 solta o mouse para selecionar texto.
52
99
 
100
+ Enquanto o modelo trabalha a caixa de texto mostra `trabalhando...` com os
101
+ pontinhos correndo, e o nome do modelo aparece fora dela, na linha de baixo.
102
+ Se a rede cair (`fetch failed`), a CLI tenta de novo sozinha ate 20 vezes
103
+ dobrando a espera (3s, 6s, 12s, 24s... ate 60s) e mostra a contagem na linha do
104
+ modelo; **Esc** cancela a espera na hora. O agente nao tem limite de passos: ele
105
+ continua ate terminar.
106
+
53
107
  Nao precisa esperar a resposta para escrever de novo: **Enter enfileira** a
54
108
  mensagem (a fila aparece acima do input e roda em ordem assim que a atual
55
109
  termina). Para parar a IA na hora, **Esc duas vezes** — o primeiro Esc so avisa,
@@ -127,7 +181,9 @@ motivo do erro quando falha) e `/mcp` lista tudo; `/mcp reload` reconecta.
127
181
  - `~/.kafox/mcp.json` guarda os servidores MCP.
128
182
 
129
183
  A CLI so responde se a conta tiver saldo; sem credito ela avisa e nao chama o
130
- gateway.
184
+ gateway. Nao existe limite de mensagens por chamada nem de passos do agente: a
185
+ conversa continua ate o modelo terminar (ou ate voce apertar Esc). O unico freio
186
+ e o saldo.
131
187
 
132
188
  ## Escopo
133
189
 
package/bin/kafox.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ const chalk = require("chalk");
2
3
  const { Command } = require("commander");
3
4
  const { version } = require("../package.json");
4
5
  const { runREPL, compact, clearContext, showStatus } = require("../src/cli");
@@ -6,6 +7,7 @@ const { login, logout, whoami, createKey } = require("../src/auth");
6
7
  const { listModels, showBalance, showPricing } = require("../src/models");
7
8
  const { ask } = require("../src/gateway");
8
9
  const { runTheme, setContentBackground } = require("../src/theme");
10
+ const { runHeadless } = require("../src/runner");
9
11
 
10
12
  const program = new Command();
11
13
 
@@ -16,21 +18,102 @@ program
16
18
  "-y, --allow-all",
17
19
  "abre ja aprovando tudo: ferramentas de arquivo, comando e MCP sem pedir aprovacao"
18
20
  )
21
+ .option(
22
+ "--resume [codigo]",
23
+ "retoma uma conversa salva (o codigo aparece no Ctrl+C e no `kafox history`)"
24
+ )
25
+ .option("-c, --continue", "retoma a conversa mais recente")
19
26
  .version(version);
20
27
 
28
+ /**
29
+ * `kafox history [n]`: lista as conversas salvas, da mais nova para a mais
30
+ * antiga, no formato "titulo - 28/10" seguido do comando para retomar.
31
+ * Sem argumento lista todas; `kafox history 20` mostra as 20 ultimas.
32
+ */
33
+ function printHistory(limit) {
34
+ const sessions = require("../src/sessions");
35
+ const chats = sessions.list();
36
+ if (!chats.length) {
37
+ console.log("Nenhuma conversa salva ainda.");
38
+ console.log(chalk.dim("Comece um chat com `kafox` e saia com Ctrl+C para ele ser salvo."));
39
+ return;
40
+ }
41
+
42
+ const wanted = Number.parseInt(String(limit ?? ""), 10);
43
+ const shown =
44
+ Number.isFinite(wanted) && wanted > 0 ? chats.slice(0, wanted) : chats;
45
+
46
+ for (const chat of shown) {
47
+ const when = sessions.dayMonth(chat.updatedAt);
48
+ console.log(`${chalk.bold(chat.title)}${when ? ` - ${when}` : ""}`);
49
+ console.log(chalk.cyan(`kafox resume ${chat.id}`));
50
+ console.log("");
51
+ }
52
+ if (shown.length < chats.length) {
53
+ console.log(
54
+ chalk.dim(`${shown.length} de ${chats.length} conversa(s). Use \`kafox history\` para ver todas.`)
55
+ );
56
+ }
57
+ }
58
+
21
59
  program.command("login").description("Login with your Kafox account (device code SSO)").action(login);
22
60
  program.command("logout").description("End the CLI session").action(logout);
23
61
  program.command("whoami").description("Show the account linked to this CLI").action(whoami);
24
- program.command("models").description("List available models and prices").action(listModels);
62
+ program.command("models").description("List available models").action(listModels);
25
63
  program.command("balance").description("Show current balance").action(showBalance);
26
64
  program.command("pricing").description("Show recharge packages").action(showPricing);
27
65
  program.command("key").description("Create an API key for this CLI").action(createKey);
28
66
  program.command("ask").argument("[question...]", "Question").option("-m, --model <id>", "Model").action(ask);
67
+ program
68
+ .command("agent")
69
+ .description("Run the agent without the TUI (tools, subagents, scripting)")
70
+ .argument("<objective...>", "Objetivo do agente")
71
+ .option("-m, --model <id>", "Model")
72
+ // -y/--allow-all é global (declarado no programa): declarar de novo aqui
73
+ // fazia o commander esconder a flag e o agente nunca rodava liberado.
74
+ .option("--max-steps <n>", "limite de passos (padrao: sem limite)", "0")
75
+ .option("--json", "imprime só o resultado em JSON")
76
+ .option("-q, --quiet", "esconde o progresso das ferramentas")
77
+ .action(async (objective, options) => {
78
+ const globals = program.opts();
79
+ const payload = await runHeadless({
80
+ prompt: objective.join(" "),
81
+ model: options.model,
82
+ allowAll: Boolean(options.allowAll || globals.allowAll),
83
+ maxSteps: Number(options.maxSteps) > 0 ? Number(options.maxSteps) : Infinity,
84
+ json: Boolean(options.json),
85
+ quiet: Boolean(options.quiet),
86
+ });
87
+ if (!payload.ok) process.exitCode = 1;
88
+ });
29
89
  program.command("theme").description("Open the theme selector").action(runTheme);
30
90
  program.command("background").description("Set terminal background image (URL)").argument("[url]", "Image URL").action(setContentBackground);
91
+ program
92
+ .command("history [quantidade]")
93
+ .alias("historico")
94
+ .description("Lista as conversas salvas e como retomar (ex.: kafox history 20)")
95
+ .action((quantidade) => printHistory(quantidade));
96
+
97
+ program
98
+ .command("resume [codigo]")
99
+ .description("Retoma uma conversa salva (sem codigo abre o seletor na tela)")
100
+ .action((codigo) =>
101
+ runREPL({ allowAll: Boolean(program.opts().allowAll), resume: codigo || "pick" })
102
+ );
103
+
104
+ program
105
+ .command("continue")
106
+ .alias("c")
107
+ .description("Continua a conversa mais recente")
108
+ .action(() => runREPL({ allowAll: Boolean(program.opts().allowAll), resume: "last" }));
31
109
  program.command("compact").description("Compact the current conversation context").action(compact);
32
110
  program.command("clear").description("Clear the conversation context").action(clearContext);
33
111
  program.command("status").description("Show account, balance and gateway status").action(showStatus);
34
112
 
35
- program.action((options) => runREPL({ allowAll: Boolean(options.allowAll) }));
113
+ program.action((options) =>
114
+ runREPL({
115
+ allowAll: Boolean(options.allowAll),
116
+ resume: options.continue ? "last" : options.resume || null,
117
+ })
118
+ );
36
119
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kafox-cli",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Kafox CLI: TUI com streaming, thinking, edit com diff, agent tools (files/shell), MCP e temas",
5
5
  "bin": {
6
6
  "kafox": "bin/kafox.js"
package/src/agent.js CHANGED
@@ -7,8 +7,21 @@ const {
7
7
  previewToolChange,
8
8
  summarizeOutput,
9
9
  } = require("./tools");
10
+ const {
11
+ SUBAGENT_TOOL,
12
+ SUBAGENT_TOOL_NAME,
13
+ MAX_DEPTH,
14
+ normalizeTasks,
15
+ subagentPrompt,
16
+ describeSubagentCall,
17
+ recordRun,
18
+ DEFAULT_MAX_STEPS,
19
+ } = require("./subagents");
10
20
 
11
- const MAX_STEPS = 10;
21
+ // Sem teto de passos: o agente continua ate terminar (ou ate o usuario parar
22
+ // com Esc). `maxSteps` so vale quando alguem pede um limite de proposito.
23
+ const MAX_STEPS = Infinity;
24
+ const MAX_SUBAGENT_REPORT_CHARS = 6000;
12
25
 
13
26
  function parseArgs(raw) {
14
27
  if (!raw) return {};
@@ -28,12 +41,160 @@ function systemPrompt(cwd, model) {
28
41
  "Ferramentas locais: read_file, list_dir, write_file, edit_file, run_command.",
29
42
  "Para mudar um arquivo existente use edit_file (troca de trecho exato); write_file e para arquivo novo ou reescrita completa.",
30
43
  "Ferramentas com nome mcp__<servidor>__<ferramenta> vem de servidores MCP conectados: use quando ajudarem.",
44
+ `spawn_subagent lanca subagentes com objetivo proprio em contexto separado (ate 3 em paralelo). Use para investigar varios pontos ao mesmo tempo, para auditoria/teste ou para tarefa longa; cada subagente devolve so o relatorio final.`,
31
45
  "Nunca invente conteudo de arquivo: leia antes de editar. Prefira caminhos relativos.",
32
46
  "Quem escreve ou executa passa por aprovacao do usuario, entao explique em uma frase o que vai fazer.",
33
47
  "Responda no idioma do usuario, de forma direta, com Markdown.",
34
48
  ].join("\n");
35
49
  }
36
50
 
51
+ /**
52
+ * Roda um subagente: contexto proprio, ferramentas limitadas e relatorio final.
53
+ * Os eventos (onSubagentEvent) deixam a TUI mostrar o progresso ao vivo.
54
+ */
55
+ async function runSubagentTask({
56
+ task,
57
+ index,
58
+ total,
59
+ model,
60
+ cwd,
61
+ approve,
62
+ autoAllowed,
63
+ signal,
64
+ depth,
65
+ onSubagentEvent,
66
+ }) {
67
+ const started = Date.now();
68
+ const baseTools = TOOL_SPECS.filter((spec) => task.tools.includes(spec.function.name));
69
+ // Um nivel de subagente: o filho nao abre netos (evita recursao descontrolada).
70
+ const nested = [];
71
+
72
+ onSubagentEvent?.({
73
+ index,
74
+ total,
75
+ objective: task.objective,
76
+ phase: "start",
77
+ tools: task.tools,
78
+ });
79
+
80
+ let lastText = "";
81
+ try {
82
+ const result = await runAgent({
83
+ messages: [
84
+ {
85
+ role: "system",
86
+ content: subagentPrompt({ objective: task.objective, context: task.context, cwd, depth }),
87
+ },
88
+ { role: "user", content: task.objective },
89
+ ],
90
+ model,
91
+ cwd,
92
+ baseTools,
93
+ extraTools: nested,
94
+ maxSteps: task.maxSteps || DEFAULT_MAX_STEPS,
95
+ approve: approve
96
+ ? (name, args, context) =>
97
+ approve(name, args, { ...context, subagent: { index, objective: task.objective } })
98
+ : undefined,
99
+ autoApprove: autoAllowed,
100
+ signal,
101
+ depth: depth + 1,
102
+ subagents: depth + 1 < MAX_DEPTH,
103
+ onSubagentEvent,
104
+ onText: (chunk) => {
105
+ lastText += chunk;
106
+ onSubagentEvent?.({ index, total, phase: "text", text: chunk, objective: task.objective });
107
+ },
108
+ onReasoning: (chunk) =>
109
+ onSubagentEvent?.({ index, total, phase: "thought", text: chunk, objective: task.objective }),
110
+ onToolStart: ({ name, label }) =>
111
+ onSubagentEvent?.({ index, total, phase: "tool", tool: name, text: label, objective: task.objective }),
112
+ onToolResult: ({ name, ok }) =>
113
+ onSubagentEvent?.({ index, total, phase: "tool-done", tool: name, ok, objective: task.objective }),
114
+ });
115
+
116
+ const report = String(result.text || lastText || "").trim();
117
+ const body = report
118
+ ? report.slice(0, MAX_SUBAGENT_REPORT_CHARS)
119
+ : result.ok
120
+ ? "(subagente terminou sem texto final)"
121
+ : `(subagente falhou: ${result.error || "erro desconhecido"})`;
122
+ recordRun({
123
+ objective: task.objective,
124
+ tools: task.tools,
125
+ ok: Boolean(result.ok),
126
+ ms: Date.now() - started,
127
+ steps: result.steps || 0,
128
+ report: body,
129
+ });
130
+ onSubagentEvent?.({
131
+ index,
132
+ total,
133
+ objective: task.objective,
134
+ phase: "done",
135
+ ok: Boolean(result.ok),
136
+ ms: Date.now() - started,
137
+ });
138
+ return { index, objective: task.objective, ok: Boolean(result.ok), body, ms: Date.now() - started };
139
+ } catch (error) {
140
+ onSubagentEvent?.({
141
+ index,
142
+ total,
143
+ objective: task.objective,
144
+ phase: "done",
145
+ ok: false,
146
+ ms: Date.now() - started,
147
+ });
148
+ recordRun({
149
+ objective: task.objective,
150
+ tools: task.tools,
151
+ ok: false,
152
+ ms: Date.now() - started,
153
+ report: error.message,
154
+ });
155
+ return {
156
+ index,
157
+ objective: task.objective,
158
+ ok: false,
159
+ body: `(subagente falhou: ${error.message})`,
160
+ ms: Date.now() - started,
161
+ };
162
+ }
163
+ }
164
+
165
+ /** Dispara as tarefas (o que for independente roda em paralelo) e junta os relatorios. */
166
+ async function runSubagents({ args, model, cwd, approve, autoAllowed, signal, depth, onSubagentEvent }) {
167
+ const tasks = normalizeTasks(args);
168
+ if (!tasks.length) {
169
+ return "Nenhuma tarefa recebida: informe `tasks: [{objective}]` (ou `objective`) para o subagente.";
170
+ }
171
+
172
+ const results = await Promise.all(
173
+ tasks.map((task, index) =>
174
+ runSubagentTask({
175
+ task,
176
+ index: index + 1,
177
+ total: tasks.length,
178
+ model: args?.model || model,
179
+ cwd,
180
+ approve,
181
+ autoAllowed,
182
+ signal,
183
+ depth,
184
+ onSubagentEvent,
185
+ })
186
+ )
187
+ );
188
+
189
+ return results
190
+ .map(
191
+ (result) =>
192
+ `[subagente ${result.index}/${results.length}] objetivo: ${result.objective}\n` +
193
+ `status: ${result.ok ? "ok" : "falhou"} · ${result.ms}ms\n${result.body}`
194
+ )
195
+ .join("\n\n---\n\n");
196
+ }
197
+
37
198
  /**
38
199
  * Roda o loop: modelo -> tool calls -> resultados -> modelo, ate a resposta final.
39
200
  * Ferramenta que escreve, edita, executa ou vem de MCP passa por `approve`
@@ -46,26 +207,37 @@ async function runAgent({
46
207
  approve,
47
208
  autoApprove = false,
48
209
  extraTools = [],
210
+ baseTools = TOOL_SPECS,
49
211
  mcp = null,
50
212
  signal,
51
- maxSteps = MAX_STEPS,
213
+ maxSteps = Infinity,
214
+ depth = 0,
215
+ subagents = true,
52
216
  onText,
53
217
  onReasoning,
54
218
  onToolStart,
55
219
  onToolResult,
220
+ onSubagentEvent,
221
+ onRetry,
56
222
  } = {}) {
57
223
  const conversation = messages.map((message) => ({ ...message }));
58
224
  if (!conversation.length || conversation[0].role !== "system") {
59
225
  conversation.unshift({ role: "system", content: systemPrompt(cwd, model) });
60
226
  }
61
227
 
62
- const tools = extraTools.length ? [...TOOL_SPECS, ...extraTools] : TOOL_SPECS;
228
+ // O subagente entra na lista de ferramentas enquanto a profundidade permitir.
229
+ const canSpawn = subagents && depth < MAX_DEPTH;
230
+ const tools = [
231
+ ...baseTools,
232
+ ...extraTools,
233
+ ...(canSpawn ? [SUBAGENT_TOOL] : []),
234
+ ];
63
235
  // `autoApprove` pode ser um valor ou uma funcao: o "Permitir sempre" da TUI
64
236
  // liga o modo no meio do loop, e o resto da execucao ja nao pergunta mais.
65
237
  const autoAllowed = typeof autoApprove === "function" ? autoApprove : () => Boolean(autoApprove);
66
238
  let last = { text: "", reasoning: "", ms: 0, usage: null };
67
239
 
68
- for (let step = 0; step < maxSteps; step += 1) {
240
+ for (let step = 0; step < (maxSteps || Infinity); step += 1) {
69
241
  // Esc duas vezes na TUI aborta o signal: nao comeca outro passo.
70
242
  if (signal?.aborted) {
71
243
  return { ok: true, aborted: true, text: last.text, messages: conversation, steps: step };
@@ -79,6 +251,7 @@ async function runAgent({
79
251
  signal,
80
252
  onDelta: onText,
81
253
  onReasoning,
254
+ onRetry,
82
255
  });
83
256
 
84
257
  if (!result.ok) {
@@ -123,7 +296,11 @@ async function runAgent({
123
296
  const name = call?.function?.name || "";
124
297
  const args = parseArgs(call?.function?.arguments);
125
298
  const fromMcp = Boolean(mcp?.has?.(name));
126
- const needsApproval = APPROVAL_REQUIRED.has(name) || fromMcp;
299
+ const isSubagent = name === SUBAGENT_TOOL_NAME;
300
+ // O subagente em si nao pede aprovacao: quem escreve/executa dentro dele
301
+ // continua passando pelo portao normal.
302
+ const needsApproval = !isSubagent && (APPROVAL_REQUIRED.has(name) || fromMcp);
303
+ const callId = call.id || `call_${step}_${name}`;
127
304
 
128
305
  let change = null;
129
306
  try {
@@ -132,13 +309,18 @@ async function runAgent({
132
309
  change = null;
133
310
  }
134
311
 
312
+ const label = isSubagent ? describeSubagentCall(args) : describeToolCall(name, args);
313
+ const tasks = isSubagent ? normalizeTasks(args) : null;
314
+
135
315
  onToolStart?.({
316
+ id: callId,
136
317
  name,
137
318
  args,
138
- label: describeToolCall(name, args),
319
+ label,
139
320
  change,
140
321
  fromMcp,
141
322
  needsApproval,
323
+ subagent: tasks ? { count: tasks.length, objectives: tasks.map((task) => task.objective) } : null,
142
324
  });
143
325
 
144
326
  const gate = needsApproval && !autoAllowed();
@@ -151,6 +333,17 @@ async function runAgent({
151
333
  let output;
152
334
  if (!allowed) {
153
335
  output = "O usuario negou a execucao desta ferramenta. Peca instrucoes se precisar.";
336
+ } else if (isSubagent) {
337
+ output = await runSubagents({
338
+ args,
339
+ model,
340
+ cwd,
341
+ approve,
342
+ autoAllowed,
343
+ signal,
344
+ depth,
345
+ onSubagentEvent: (event) => onSubagentEvent?.({ ...event, id: callId }),
346
+ });
154
347
  } else {
155
348
  try {
156
349
  output = await executeTool(name, args, { cwd, mcp });
@@ -160,6 +353,7 @@ async function runAgent({
160
353
  }
161
354
 
162
355
  onToolResult?.({
356
+ id: callId,
163
357
  name,
164
358
  args,
165
359
  output,
@@ -169,11 +363,12 @@ async function runAgent({
169
363
  fromMcp,
170
364
  ms: Date.now() - toolStarted,
171
365
  summary: summarizeOutput(output),
366
+ subagent: tasks ? { count: tasks.length } : null,
172
367
  });
173
368
 
174
369
  conversation.push({
175
370
  role: "tool",
176
- tool_call_id: call.id || `call_${step}`,
371
+ tool_call_id: callId,
177
372
  content: String(output),
178
373
  });
179
374
  }
@@ -181,10 +376,10 @@ async function runAgent({
181
376
 
182
377
  return {
183
378
  ok: false,
184
- error: `Limite de ${maxSteps} passos do agente atingido. Tente dividir o pedido.`,
379
+ error: `Limite de ${maxSteps} passos do agente atingido.`,
185
380
  ...last,
186
381
  messages: conversation,
187
382
  };
188
383
  }
189
384
 
190
- module.exports = { runAgent, MAX_STEPS, systemPrompt };
385
+ module.exports = { runAgent, runSubagents, MAX_STEPS, systemPrompt };
package/src/auth.js CHANGED
@@ -20,12 +20,11 @@ async function ensureSession() {
20
20
  return null;
21
21
  }
22
22
 
23
+ /**
24
+ * Login por device code. Com TTY abre a tela cheia (logo grande, link no meio);
25
+ * sem TTY (pipe/CI) mantem o fluxo de texto, que continua funcionando.
26
+ */
23
27
  async function login() {
24
- console.log(MASCOT);
25
- console.log(
26
- chalk.cyan(" Kafox CLI - login by device code (SSO)\n")
27
- );
28
-
29
28
  const res = await fetchApi("/api/cli/device", {
30
29
  method: "POST",
31
30
  body: "{}",
@@ -36,10 +35,15 @@ async function login() {
36
35
  return;
37
36
  }
38
37
 
39
- console.log(
40
- chalk.bold("\n Open this URL in your browser to approve:\n") +
41
- chalk.cyan(` ${storage.getApiUrl()}/cli?code=${data.user_code}\n`)
42
- );
38
+ const url = `${storage.getApiUrl()}/cli?code=${data.user_code}`;
39
+
40
+ if (process.stdout.isTTY && process.stdin.isTTY) {
41
+ return loginWithScreen({ url, data });
42
+ }
43
+
44
+ console.log(MASCOT);
45
+ console.log(chalk.cyan(" Kafox CLI - login by device code (SSO)\n"));
46
+ console.log(chalk.bold(" Open this URL in your browser to approve:\n") + chalk.cyan(` ${url}\n`));
43
47
  console.log(` Code: ${chalk.bold(data.user_code)} (expires in ${data.expires_in}s)\n`);
44
48
 
45
49
  const interval = Math.max(1, data.interval) * 1000;
@@ -50,32 +54,75 @@ async function login() {
50
54
  process.stdout.write(`\r Waiting for approval${".".repeat(dots % 4)} `);
51
55
  dots += 1;
52
56
  await new Promise((r) => setTimeout(r, interval));
53
- try {
54
- const poll = await fetchApi(`/api/cli/poll/${encodeURIComponent(data.device_code)}`);
55
- const pd = await poll.json().catch(() => ({}));
56
- if (poll.status === 200 && pd.status === "approved") {
57
- const previous = storage.getToken() || {};
58
- storage.setToken({ session: pd.session, csrf: previous.csrf });
59
- console.log(
60
- chalk.green("\n Approved! Session saved to ~/.kafox/token.json")
61
- );
62
- return;
63
- }
64
- if (pd.status === "denied" || poll.status === 403) {
65
- console.log(chalk.red("\n Login denied."));
66
- return;
67
- }
68
- if (poll.status === 404 || poll.status === 410) {
69
- console.log(chalk.red("\n Device code expired. Run `kafox login` again."));
70
- return;
71
- }
72
- } catch {
73
- // ignore transient errors, keep polling
57
+ const outcome = await pollDevice(data);
58
+ if (outcome.ok) {
59
+ console.log(chalk.green("\n Approved! Session saved to ~/.kafox/token.json"));
60
+ return;
61
+ }
62
+ if (outcome.stop) {
63
+ console.log(chalk.red(`\n ${outcome.error}`));
64
+ return;
74
65
  }
75
66
  }
76
67
  console.log(chalk.red("\n Device code expired. Run `kafox login` again."));
77
68
  }
78
69
 
70
+ /** Uma consulta do device code; grava a sessao quando aprovada. */
71
+ async function pollDevice(data) {
72
+ let poll;
73
+ try {
74
+ poll = await fetchApi(`/api/cli/poll/${encodeURIComponent(data.device_code)}`);
75
+ } catch {
76
+ return { ok: false, stop: false };
77
+ }
78
+ const pd = await poll.json().catch(() => ({}));
79
+ if (poll.status === 200 && pd.status === "approved") {
80
+ const previous = storage.getToken() || {};
81
+ storage.setToken({ session: pd.session, csrf: previous.csrf });
82
+ return { ok: true, stop: true };
83
+ }
84
+ if (pd.status === "denied" || poll.status === 403) {
85
+ return { ok: false, stop: true, error: "Login denied." };
86
+ }
87
+ if (poll.status === 404 || poll.status === 410) {
88
+ return { ok: false, stop: true, error: "Device code expired. Run `kafox login` again." };
89
+ }
90
+ return { ok: false, stop: false };
91
+ }
92
+
93
+ async function loginWithScreen({ url, data }) {
94
+ const { runLoginScreen } = require("./login-screen");
95
+ const { version } = require("../package.json");
96
+ const theme = storage.getConfig().theme || "default";
97
+
98
+ const result = await runLoginScreen({
99
+ url,
100
+ code: data.user_code,
101
+ interval: data.interval,
102
+ expiresIn: data.expires_in,
103
+ theme,
104
+ version,
105
+ poll: async () => {
106
+ const outcome = await pollDevice(data);
107
+ if (outcome.ok) return { ok: true };
108
+ if (outcome.stop) return { ok: false, error: outcome.error };
109
+ return { ok: false };
110
+ },
111
+ });
112
+
113
+ if (result.ok) {
114
+ console.log(chalk.green(" Aprovado! Sessao salva em ~/.kafox/token.json"));
115
+ console.log(chalk.dim(" Bom proveito.\n"));
116
+ return true;
117
+ }
118
+ if (result.cancelled) {
119
+ console.log(chalk.dim(" Login cancelado.\n"));
120
+ return false;
121
+ }
122
+ console.log(chalk.red(` ${result.error || "Nao foi possivel entrar"}.\n`));
123
+ return false;
124
+ }
125
+
79
126
  async function logout() {
80
127
  storage.clearToken();
81
128
  console.log(chalk.green(" Session ended."));