@neetru/cli 2.14.0 → 2.15.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/CHANGELOG.md +16 -0
- package/dist/commands/code.d.ts +10 -31
- package/dist/commands/code.js +254 -218
- package/dist/commands/code.js.map +1 -1
- package/dist/lib/ai/orchestrator.js +1 -42
- package/dist/lib/ai/orchestrator.js.map +1 -1
- package/dist/lib/code-agent-files.d.ts +38 -0
- package/dist/lib/code-agent-files.js +256 -1
- package/dist/lib/code-agent-files.js.map +1 -1
- package/dist/lib/code-agent-gateway.d.ts +27 -0
- package/dist/lib/code-agent-gateway.js +115 -0
- package/dist/lib/code-agent-gateway.js.map +1 -0
- package/dist/lib/code-agent-protocol.d.ts +8 -0
- package/dist/lib/code-agent-protocol.js +22 -0
- package/dist/lib/code-agent-protocol.js.map +1 -1
- package/dist/lib/code-agent-session.d.ts +54 -0
- package/dist/lib/code-agent-session.js +147 -0
- package/dist/lib/code-agent-session.js.map +1 -0
- package/dist/lib/code-agent-tools.d.ts +17 -0
- package/dist/lib/code-agent-tools.js +52 -0
- package/dist/lib/code-agent-tools.js.map +1 -0
- package/dist/lib/code-agent-types.d.ts +34 -0
- package/dist/lib/code-agent-types.js +7 -0
- package/dist/lib/code-agent-types.js.map +1 -0
- package/dist/lib/code-agent-ui.d.ts +30 -0
- package/dist/lib/code-agent-ui.js +258 -0
- package/dist/lib/code-agent-ui.js.map +1 -0
- package/dist/lib/sse.d.ts +14 -0
- package/dist/lib/sse.js +43 -0
- package/dist/lib/sse.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
Convenção: [Keep a Changelog](https://keepachangelog.com/) + [SemVer](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [2.15.0] — 2026-07-10 (`neetru code` — Fases 2 a 5: núcleo separado, ferramentas, memória, streaming)
|
|
6
|
+
|
|
7
|
+
> Continuação direta do 2.14.0 (mesmo subcomando, mesma decisão B0). Sem breaking change nos flags/comandos existentes — tudo aditivo. Detalhe completo em `NEETRU_CODE_ARCHITECTURE.md`.
|
|
8
|
+
|
|
9
|
+
### Adicionado
|
|
10
|
+
|
|
11
|
+
- **Núcleo separado da interface** (Fase 2) — `code-agent-session.ts` ganhou `CodeAgentStateMachine` (idle→thinking→tool_requested→tool_running→patch_ready→awaiting_confirm→applied/failed/aborted, com validação de transição); renderização extraída pra `code-agent-ui.ts`; chamada ao AI Gateway isolada em `code-agent-gateway.ts`; registro de ferramentas centralizado em `code-agent-tools.ts` (`buildSystemPrompt()` monta o prompt a partir do registro — não precisa mais editar prompt à mão a cada ferramenta nova).
|
|
12
|
+
- **4 ferramentas produtivas novas** (Fase 3): `run_tests` (roda só o script `test` já configurado — allowlist fechada, não aceita comando livre do modelo), `read_many_files` (até 20 arquivos por chamada), `search_regex` (busca linha-a-linha com limites anti-ReDoS), `project_symbols` (outline leve de `export function/class/const/interface/type/enum`).
|
|
13
|
+
- **Memória e retomada** (Fase 4): `/save` grava a sessão em `.neetru/code/sessions`; `/resume` lista sessões salvas numeradas (mais recente primeiro, com preview da 1ª mensagem) e recarrega a conversa escolhida; log local de patch aplicado em `.neetru/code/patches.log`.
|
|
14
|
+
- **Streaming de resposta** (Fase 5): `streamGatewayChat` consome SSE do AI Gateway (mesmo endpoint/formato que `neetru ai --debug:model gateway` já usa) — mostra contador de chars recebidos ao vivo no terminal em vez de ficar parado durante respostas longas; cai automaticamente pra chamada sem streaming em qualquer falha (a corretude do turno nunca depende do streaming funcionar). Parser SSE (`readSseLines`) extraído pra `src/lib/sse.ts` — compartilhado com `src/lib/ai/orchestrator.ts`, sem duplicar a lógica de buffer/decode entre os dois subcomandos.
|
|
15
|
+
- **`/pick`** (Fase 5): lista arquivos do projeto numerados e anexa a escolha do usuário ao contexto da conversa.
|
|
16
|
+
|
|
17
|
+
### Notas
|
|
18
|
+
|
|
19
|
+
- Painel TUI completo (viewport fixo pra conversa/status/diff) fica de fora deste release — exige trocar o modelo de entrada (`readline`) por controle de terminal em modo raw, escopo estrutural próprio.
|
|
20
|
+
|
|
5
21
|
## [2.14.0] — 2026-07-09 (`neetru code` — assistente local via AI Gateway)
|
|
6
22
|
|
|
7
23
|
> Novo subcomando, decisão B0 (ver `PLANO_NEETRU_AI_CODE_2026-07-02.md`): assistente de código local roteado pelo Neetru AI Gateway (Groq/NVIDIA — `llama-3.3-70b-versatile` default), não pelo Anthropic/OpenAI SDK direto. Fecha 3 HIGH + 1 MEDIUM de um review de segurança antes de mergear.
|
package/dist/commands/code.d.ts
CHANGED
|
@@ -1,32 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
content: string;
|
|
5
|
-
}
|
|
6
|
-
export interface CodeCommandOptions {
|
|
7
|
-
cwd?: string;
|
|
8
|
-
file?: string[];
|
|
9
|
-
model?: string;
|
|
10
|
-
maxTurns?: string | number;
|
|
11
|
-
maxTokens?: string | number;
|
|
12
|
-
temperature?: string | number;
|
|
13
|
-
plan?: boolean;
|
|
14
|
-
apply?: boolean;
|
|
15
|
-
yes?: boolean;
|
|
16
|
-
}
|
|
17
|
-
export interface CodeSettings {
|
|
18
|
-
cwd: string;
|
|
19
|
-
files: string[];
|
|
20
|
-
model: string;
|
|
21
|
-
modelWarning?: string;
|
|
22
|
-
maxTurns: number;
|
|
23
|
-
maxTokens: number;
|
|
24
|
-
temperature: number;
|
|
25
|
-
apply: boolean;
|
|
26
|
-
yes: boolean;
|
|
27
|
-
timeoutMs: number;
|
|
28
|
-
}
|
|
29
|
-
type ConfirmFn = (question: string) => Promise<boolean>;
|
|
1
|
+
import { type CodeAgentState } from '../lib/code-agent-session.js';
|
|
2
|
+
import type { CodeMessage, CodeCommandOptions, CodeSettings, ConfirmFn } from '../lib/code-agent-types.js';
|
|
3
|
+
export type { CodeMessage, CodeCommandOptions, CodeSettings } from '../lib/code-agent-types.js';
|
|
30
4
|
export declare function resolveSettings(opts: CodeCommandOptions): CodeSettings;
|
|
31
5
|
/**
|
|
32
6
|
* `history` e mutado e reaproveitado entre chamadas: e o que da ao modo
|
|
@@ -34,8 +8,14 @@ export declare function resolveSettings(opts: CodeCommandOptions): CodeSettings;
|
|
|
34
8
|
* em vez de cada linha digitada virar uma requisicao amnesica. O one-shot
|
|
35
9
|
* (`neetru code "tarefa"`) chama sem `history` — cada `runAgentTask` comeca do
|
|
36
10
|
* zero, igual sempre foi.
|
|
11
|
+
*
|
|
12
|
+
* `onState` (opcional): observa a maquina de estados da sessao (Fase 2,
|
|
13
|
+
* NEETRU_CODE_ARCHITECTURE.md) — usado por testes pra verificar a sequencia
|
|
14
|
+
* idle -> thinking -> tool_requested -> tool_running -> ... sem mockar o
|
|
15
|
+
* AI Gateway inteiro. Cada chamada de runAgentTask e um ciclo completo
|
|
16
|
+
* (comeca e termina em idle/applied/failed/aborted).
|
|
37
17
|
*/
|
|
38
|
-
export declare function runAgentTask(task: string, settings: CodeSettings, confirm: ConfirmFn, history?: CodeMessage[]): Promise<void>;
|
|
18
|
+
export declare function runAgentTask(task: string, settings: CodeSettings, confirm: ConfirmFn, history?: CodeMessage[], onState?: (state: CodeAgentState) => void): Promise<void>;
|
|
39
19
|
/**
|
|
40
20
|
* Entrada a partir do menu interativo do CLI (`neetru` sem args / `neetru ui`).
|
|
41
21
|
* Usa settings default (cwd do processo) e gerencia o raw-mode do stdin como
|
|
@@ -45,4 +25,3 @@ export declare function runAgentTask(task: string, settings: CodeSettings, confi
|
|
|
45
25
|
*/
|
|
46
26
|
export declare function runCodeMenu(): Promise<void>;
|
|
47
27
|
export declare function runCodeCommand(promptParts: string[] | undefined, opts?: CodeCommandOptions): Promise<void>;
|
|
48
|
-
export {};
|
package/dist/commands/code.js
CHANGED
|
@@ -3,146 +3,18 @@ import path from 'node:path';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { createInterface } from 'node:readline/promises';
|
|
6
|
-
import { apiRequest } from '../lib/api-client.js';
|
|
7
6
|
import { config } from '../lib/config.js';
|
|
8
7
|
import { getActiveTokenSync } from '../lib/token-resolver.js';
|
|
9
8
|
import { log } from '../utils/logger.js';
|
|
10
9
|
import { setSafeExitCode } from '../utils/safe-exit.js';
|
|
11
10
|
import { parseToolRequest, extractPatch } from '../lib/code-agent-protocol.js';
|
|
12
|
-
import { applyUnifiedPatch, listFiles, readFileContext, renderColoredDiff, runCodeTool, } from '../lib/code-agent-files.js';
|
|
11
|
+
import { applyUnifiedPatch, getGitDiff, getProjectOverview, listFiles, readFileContext, renderColoredDiff, runCodeTool, } from '../lib/code-agent-files.js';
|
|
12
|
+
import { compactCodeHistory, saveCodeSession, listCodeSessions, loadCodeSession, logAppliedPatch, CodeAgentStateMachine, } from '../lib/code-agent-session.js';
|
|
13
13
|
import { DEFAULT_CODE_MAX_TOKENS, DEFAULT_CODE_MAX_TURNS, DEFAULT_CODE_TEMPERATURE, parseBoundedNumber, resolveCodeModel, } from '../lib/code-agent-settings.js';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const SYSTEM_PROMPT =
|
|
18
|
-
Voce e o Neetru Code, um assistente local de codificacao da Neetru.
|
|
19
|
-
Voce conversa com modelos do AI Gateway do Neetru Core, normalmente Groq/NVIDIA.
|
|
20
|
-
Nao use Anthropic/Claude como caminho assumido.
|
|
21
|
-
|
|
22
|
-
Voce nao tem tool calling nativo. Use exatamente estes protocolos textuais:
|
|
23
|
-
|
|
24
|
-
NEETRU_TOOL {"tool":"read_file","path":"caminho/relativo.ext"}
|
|
25
|
-
NEETRU_TOOL {"tool":"list_files","pattern":"texto opcional"}
|
|
26
|
-
NEETRU_TOOL {"tool":"search","query":"texto literal"}
|
|
27
|
-
|
|
28
|
-
Regras:
|
|
29
|
-
- Peca arquivos antes de alterar codigo quando faltar contexto.
|
|
30
|
-
- Nunca invente conteudo de arquivo.
|
|
31
|
-
- Peca uma tool por resposta, no maximo.
|
|
32
|
-
- Quando estiver pronto para alterar, responda somente com um diff aplicavel por git:
|
|
33
|
-
|
|
34
|
-
NEETRU_PATCH
|
|
35
|
-
diff --git a/path b/path
|
|
36
|
-
...
|
|
37
|
-
NEETRU_END
|
|
38
|
-
|
|
39
|
-
- Nao coloque explicacao dentro do bloco NEETRU_PATCH.
|
|
40
|
-
- Se a tarefa nao exigir alteracao, responda em texto curto.
|
|
41
|
-
`.trim();
|
|
42
|
-
function stripAnsi(text) {
|
|
43
|
-
return text.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
|
|
44
|
-
}
|
|
45
|
-
function visibleLength(text) {
|
|
46
|
-
return Array.from(stripAnsi(text)).length;
|
|
47
|
-
}
|
|
48
|
-
function padVisible(text, width) {
|
|
49
|
-
return `${text}${' '.repeat(Math.max(0, width - visibleLength(text)))}`;
|
|
50
|
-
}
|
|
51
|
-
function terminalWidth() {
|
|
52
|
-
const raw = process.stdout.columns ?? 88;
|
|
53
|
-
return Math.max(UI_MIN_WIDTH, Math.min(UI_MAX_WIDTH, raw));
|
|
54
|
-
}
|
|
55
|
-
function truncateMiddle(text, max) {
|
|
56
|
-
if (visibleLength(text) <= max)
|
|
57
|
-
return text;
|
|
58
|
-
if (max <= 5)
|
|
59
|
-
return text.slice(0, max);
|
|
60
|
-
const clean = stripAnsi(text);
|
|
61
|
-
const left = Math.ceil((max - 3) / 2);
|
|
62
|
-
const right = Math.floor((max - 3) / 2);
|
|
63
|
-
return `${clean.slice(0, left)}...${clean.slice(clean.length - right)}`;
|
|
64
|
-
}
|
|
65
|
-
function wrapPlain(text, width) {
|
|
66
|
-
const clean = stripAnsi(text);
|
|
67
|
-
if (clean.length <= width)
|
|
68
|
-
return [clean];
|
|
69
|
-
const out = [];
|
|
70
|
-
let rest = clean;
|
|
71
|
-
while (rest.length > width) {
|
|
72
|
-
const slice = rest.slice(0, width + 1);
|
|
73
|
-
const breakAt = Math.max(slice.lastIndexOf(' '), slice.lastIndexOf('/'), slice.lastIndexOf('\\'));
|
|
74
|
-
const idx = breakAt > Math.floor(width * 0.45) ? breakAt : width;
|
|
75
|
-
out.push(rest.slice(0, idx).trimEnd());
|
|
76
|
-
rest = rest.slice(idx).trimStart();
|
|
77
|
-
}
|
|
78
|
-
if (rest)
|
|
79
|
-
out.push(rest);
|
|
80
|
-
return out;
|
|
81
|
-
}
|
|
82
|
-
function printBox(title, rows = []) {
|
|
83
|
-
const width = terminalWidth();
|
|
84
|
-
const inner = width - 2;
|
|
85
|
-
const label = title ? ` ${title} ` : '';
|
|
86
|
-
const top = `+${label}${'-'.repeat(Math.max(0, inner - visibleLength(label)))}+`;
|
|
87
|
-
console.log(chalk.dim(top));
|
|
88
|
-
for (const row of rows) {
|
|
89
|
-
const wrapped = wrapPlain(row, inner - 2);
|
|
90
|
-
for (const line of wrapped) {
|
|
91
|
-
console.log(chalk.dim('| ') + padVisible(truncateMiddle(line, inner - 2), inner - 2) + chalk.dim(' |'));
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
console.log(chalk.dim(`+${'-'.repeat(inner)}+`));
|
|
95
|
-
}
|
|
96
|
-
function printDivider(label) {
|
|
97
|
-
const width = terminalWidth();
|
|
98
|
-
if (!label) {
|
|
99
|
-
console.log(chalk.dim('-'.repeat(width)));
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const title = ` ${label} `;
|
|
103
|
-
console.log(chalk.dim(`${title}${'-'.repeat(Math.max(0, width - visibleLength(title)))}`));
|
|
104
|
-
}
|
|
105
|
-
function printEvent(kind, title, detail) {
|
|
106
|
-
const colors = {
|
|
107
|
-
wait: chalk.cyan,
|
|
108
|
-
tool: chalk.blue,
|
|
109
|
-
ok: chalk.green,
|
|
110
|
-
warn: chalk.yellow,
|
|
111
|
-
error: chalk.red,
|
|
112
|
-
};
|
|
113
|
-
const tag = colors[kind](kind.toUpperCase().padEnd(5));
|
|
114
|
-
const suffix = detail ? chalk.dim(` ${detail}`) : '';
|
|
115
|
-
console.log(`${tag} ${chalk.bold(title)}${suffix}`);
|
|
116
|
-
}
|
|
117
|
-
function toolDetail(tool) {
|
|
118
|
-
if (!tool)
|
|
119
|
-
return '';
|
|
120
|
-
if (tool.tool === 'read_file' && typeof tool.path === 'string')
|
|
121
|
-
return tool.path;
|
|
122
|
-
if (tool.tool === 'list_files' && typeof tool.pattern === 'string' && tool.pattern)
|
|
123
|
-
return tool.pattern;
|
|
124
|
-
if (tool.tool === 'search' && typeof tool.query === 'string')
|
|
125
|
-
return tool.query;
|
|
126
|
-
return '';
|
|
127
|
-
}
|
|
128
|
-
function summarizeToolResult(result) {
|
|
129
|
-
const lines = result.split(/\r?\n/).filter(Boolean).length;
|
|
130
|
-
const chars = result.length;
|
|
131
|
-
return `${lines} linha(s), ${chars} char(s)`;
|
|
132
|
-
}
|
|
133
|
-
function printAssistantText(content) {
|
|
134
|
-
printDivider('assistant');
|
|
135
|
-
process.stdout.write(`${content.trim()}\n`);
|
|
136
|
-
printDivider();
|
|
137
|
-
}
|
|
138
|
-
function printPatchPreview(patch) {
|
|
139
|
-
const files = Array.from(patch.matchAll(/^diff --git a\/(.+?) b\//gm)).map((m) => m[1]);
|
|
140
|
-
printBox('patch proposto', [
|
|
141
|
-
files.length ? `arquivos: ${files.join(', ')}` : 'arquivos: nao detectado',
|
|
142
|
-
'aplicacao: git apply depois de confirmacao',
|
|
143
|
-
]);
|
|
144
|
-
process.stdout.write(renderColoredDiff(patch));
|
|
145
|
-
}
|
|
14
|
+
import { callGateway, streamGatewayChat, AI_GATEWAY_CHAT_PATH } from '../lib/code-agent-gateway.js';
|
|
15
|
+
import { buildSystemPrompt } from '../lib/code-agent-tools.js';
|
|
16
|
+
import { printBox, printEvent, printAssistantText, printPatchPreview, patchSummaryRows, renderHeader, renderInteractiveHelp, renderSessionStatus, renderPrompt, printFileSample, printFilePicker, printSessionList, summarizeToolResult, toolDetail, } from '../lib/code-agent-ui.js';
|
|
17
|
+
const SYSTEM_PROMPT = buildSystemPrompt();
|
|
146
18
|
function asOptionalString(value) {
|
|
147
19
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
148
20
|
}
|
|
@@ -189,28 +61,22 @@ export function resolveSettings(opts) {
|
|
|
189
61
|
timeoutMs: 120_000,
|
|
190
62
|
};
|
|
191
63
|
}
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
'/clear limpar tela e esquecer o historico da conversa',
|
|
209
|
-
'/help mostrar estes controles',
|
|
210
|
-
'/exit sair',
|
|
211
|
-
]);
|
|
212
|
-
console.log(chalk.dim('Digite uma tarefa em linguagem natural para a IA inspecionar e propor patch.'));
|
|
213
|
-
console.log();
|
|
64
|
+
async function runReviewCommand(settings, confirm, history) {
|
|
65
|
+
const previousApply = settings.apply;
|
|
66
|
+
const previousYes = settings.yes;
|
|
67
|
+
settings.apply = false;
|
|
68
|
+
settings.yes = false;
|
|
69
|
+
try {
|
|
70
|
+
await runAgentTask([
|
|
71
|
+
'Revise as mudancas atuais do projeto.',
|
|
72
|
+
'Use NEETRU_TOOL {"tool":"git_status"} e NEETRU_TOOL {"tool":"git_diff"} se precisar de contexto.',
|
|
73
|
+
'Nao proponha patch automaticamente. Liste achados por severidade, com arquivo/linha quando possivel.',
|
|
74
|
+
].join('\n'), settings, confirm, history);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
settings.apply = previousApply;
|
|
78
|
+
settings.yes = previousYes;
|
|
79
|
+
}
|
|
214
80
|
}
|
|
215
81
|
function buildInitialPrompt(task, settings) {
|
|
216
82
|
const fileBlocks = [];
|
|
@@ -225,24 +91,6 @@ function buildInitialPrompt(task, settings) {
|
|
|
225
91
|
: 'Nenhum arquivo foi anexado inicialmente. Use NEETRU_TOOL para pedir contexto local.',
|
|
226
92
|
].join('\n\n');
|
|
227
93
|
}
|
|
228
|
-
async function callGateway(settings, messages) {
|
|
229
|
-
const response = await apiRequest(AI_GATEWAY_CHAT_PATH, {
|
|
230
|
-
method: 'POST',
|
|
231
|
-
body: {
|
|
232
|
-
model: settings.model,
|
|
233
|
-
messages,
|
|
234
|
-
stream: false,
|
|
235
|
-
maxTokens: settings.maxTokens,
|
|
236
|
-
temperature: settings.temperature,
|
|
237
|
-
},
|
|
238
|
-
timeoutMs: settings.timeoutMs,
|
|
239
|
-
});
|
|
240
|
-
const content = response.choices?.[0]?.message?.content;
|
|
241
|
-
if (typeof content !== 'string') {
|
|
242
|
-
throw new Error('AI Gateway retornou resposta sem choices[0].message.content.');
|
|
243
|
-
}
|
|
244
|
-
return content;
|
|
245
|
-
}
|
|
246
94
|
async function defaultConfirm(question) {
|
|
247
95
|
if (!process.stdin.isTTY)
|
|
248
96
|
return false;
|
|
@@ -255,17 +103,61 @@ async function defaultConfirm(question) {
|
|
|
255
103
|
rl.close();
|
|
256
104
|
}
|
|
257
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Chama o AI Gateway com streaming (Fase 5) mostrando progresso ao vivo
|
|
108
|
+
* (contador de chars recebidos na mesma linha, via `\r`) em vez de ficar
|
|
109
|
+
* estatico durante respostas longas. So atualiza a linha em TTY real — em
|
|
110
|
+
* saida nao-interativa (pipe/CI/teste) isso so poluiria o log.
|
|
111
|
+
*
|
|
112
|
+
* Fallback: qualquer falha no streaming (rede, formato inesperado) refaz a
|
|
113
|
+
* chamada sem streaming — a corretude do turno nunca depende do streaming
|
|
114
|
+
* funcionar, so a experiencia visual.
|
|
115
|
+
*/
|
|
116
|
+
async function callGatewayWithProgress(settings, messages) {
|
|
117
|
+
const live = Boolean(process.stdout.isTTY);
|
|
118
|
+
let received = 0;
|
|
119
|
+
try {
|
|
120
|
+
const content = await streamGatewayChat(settings, messages, (_delta, total) => {
|
|
121
|
+
received = total;
|
|
122
|
+
if (live) {
|
|
123
|
+
process.stdout.write(`\r${chalk.dim(` ... recebendo resposta (${received} char(s))`)}`);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
if (live && received > 0) {
|
|
127
|
+
process.stdout.write('\r' + ' '.repeat(40) + '\r');
|
|
128
|
+
}
|
|
129
|
+
return content;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (live && received > 0) {
|
|
133
|
+
process.stdout.write('\r' + ' '.repeat(40) + '\r');
|
|
134
|
+
}
|
|
135
|
+
printEvent('warn', 'Streaming falhou, tentando sem streaming', error instanceof Error ? error.message : String(error));
|
|
136
|
+
return callGateway(settings, messages);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
258
139
|
/**
|
|
259
140
|
* `history` e mutado e reaproveitado entre chamadas: e o que da ao modo
|
|
260
141
|
* interativo memoria de conversa entre turnos do REPL (como Claude Code/Codex),
|
|
261
142
|
* em vez de cada linha digitada virar uma requisicao amnesica. O one-shot
|
|
262
143
|
* (`neetru code "tarefa"`) chama sem `history` — cada `runAgentTask` comeca do
|
|
263
144
|
* zero, igual sempre foi.
|
|
145
|
+
*
|
|
146
|
+
* `onState` (opcional): observa a maquina de estados da sessao (Fase 2,
|
|
147
|
+
* NEETRU_CODE_ARCHITECTURE.md) — usado por testes pra verificar a sequencia
|
|
148
|
+
* idle -> thinking -> tool_requested -> tool_running -> ... sem mockar o
|
|
149
|
+
* AI Gateway inteiro. Cada chamada de runAgentTask e um ciclo completo
|
|
150
|
+
* (comeca e termina em idle/applied/failed/aborted).
|
|
264
151
|
*/
|
|
265
|
-
export async function runAgentTask(task, settings, confirm, history = []) {
|
|
152
|
+
export async function runAgentTask(task, settings, confirm, history = [], onState) {
|
|
266
153
|
if (!getActiveTokenSync()) {
|
|
267
154
|
throw new Error('Sessao Neetru ausente ou expirada. Rode `neetru login`.');
|
|
268
155
|
}
|
|
156
|
+
const machine = new CodeAgentStateMachine();
|
|
157
|
+
const goto = (next) => {
|
|
158
|
+
machine.transition(next);
|
|
159
|
+
onState?.(next);
|
|
160
|
+
};
|
|
269
161
|
const messages = history;
|
|
270
162
|
const isFirstTurn = messages.length === 0;
|
|
271
163
|
if (isFirstTurn)
|
|
@@ -274,54 +166,68 @@ export async function runAgentTask(task, settings, confirm, history = []) {
|
|
|
274
166
|
role: 'user',
|
|
275
167
|
content: isFirstTurn ? buildInitialPrompt(task, settings) : task,
|
|
276
168
|
});
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
printPatchPreview(patch);
|
|
296
|
-
if (!settings.apply) {
|
|
297
|
-
printEvent('warn', 'Patch nao aplicado', '--plan/--no-apply');
|
|
298
|
-
return;
|
|
169
|
+
try {
|
|
170
|
+
for (let turn = 1; turn <= settings.maxTurns; turn++) {
|
|
171
|
+
goto('thinking');
|
|
172
|
+
printEvent('wait', 'AI Gateway', `turno ${turn}/${settings.maxTurns}`);
|
|
173
|
+
const content = await callGatewayWithProgress(settings, messages);
|
|
174
|
+
messages.push({ role: 'assistant', content });
|
|
175
|
+
const tool = parseToolRequest(content);
|
|
176
|
+
if (tool) {
|
|
177
|
+
goto('tool_requested');
|
|
178
|
+
printEvent('tool', tool.tool, toolDetail(tool));
|
|
179
|
+
goto('tool_running');
|
|
180
|
+
const result = runCodeTool(settings.cwd, tool);
|
|
181
|
+
printEvent('ok', 'contexto local', summarizeToolResult(result));
|
|
182
|
+
messages.push({
|
|
183
|
+
role: 'user',
|
|
184
|
+
content: `Resultado da tool ${tool.tool}:\n\n${result}`,
|
|
185
|
+
});
|
|
186
|
+
continue;
|
|
299
187
|
}
|
|
300
|
-
const
|
|
301
|
-
if (
|
|
302
|
-
|
|
188
|
+
const patch = extractPatch(content);
|
|
189
|
+
if (patch) {
|
|
190
|
+
goto('patch_ready');
|
|
191
|
+
console.log();
|
|
192
|
+
printPatchPreview(patch);
|
|
193
|
+
if (!settings.apply) {
|
|
194
|
+
printEvent('warn', 'Patch nao aplicado', '--plan/--no-apply');
|
|
195
|
+
goto('aborted');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const approved = settings.yes || (await (async () => {
|
|
199
|
+
goto('awaiting_confirm');
|
|
200
|
+
return confirm('\nAplicar este patch? [y/N] ');
|
|
201
|
+
})());
|
|
202
|
+
if (!approved) {
|
|
203
|
+
printEvent('warn', 'Patch nao aplicado');
|
|
204
|
+
goto('aborted');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
applyUnifiedPatch(settings.cwd, patch);
|
|
208
|
+
logAppliedPatch(settings.cwd, patchSummaryRows(patch)[0] ?? 'arquivos: nao detectado');
|
|
209
|
+
printEvent('ok', 'Patch aplicado', 'git apply');
|
|
210
|
+
goto('applied');
|
|
303
211
|
return;
|
|
304
212
|
}
|
|
305
|
-
|
|
306
|
-
|
|
213
|
+
console.log();
|
|
214
|
+
printAssistantText(content);
|
|
215
|
+
goto('idle');
|
|
307
216
|
return;
|
|
308
217
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
return;
|
|
218
|
+
goto('failed');
|
|
219
|
+
throw new Error(`Limite de turnos atingido (${settings.maxTurns}) sem resposta final.`);
|
|
312
220
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
printEvent('warn', 'Nenhum arquivo encontrado');
|
|
319
|
-
return;
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (machine.state !== 'failed' && machine.state !== 'aborted' && machine.state !== 'applied') {
|
|
223
|
+
goto('failed');
|
|
224
|
+
}
|
|
225
|
+
throw error;
|
|
320
226
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
227
|
+
}
|
|
228
|
+
function printFileSampleForSettings(settings) {
|
|
229
|
+
const all = listFiles(settings.cwd);
|
|
230
|
+
printFileSample(all.slice(0, 60), all.length);
|
|
325
231
|
}
|
|
326
232
|
async function runInteractive(settings) {
|
|
327
233
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
@@ -329,11 +235,13 @@ async function runInteractive(settings) {
|
|
|
329
235
|
}
|
|
330
236
|
renderHeader(settings, 'interactive');
|
|
331
237
|
renderInteractiveHelp();
|
|
238
|
+
if (settings.modelWarning)
|
|
239
|
+
log.warn(settings.modelWarning);
|
|
332
240
|
let history = [];
|
|
333
241
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
334
242
|
try {
|
|
335
243
|
for (;;) {
|
|
336
|
-
const line = (await rl.question(
|
|
244
|
+
const line = (await rl.question(renderPrompt(settings))).trim();
|
|
337
245
|
if (!line)
|
|
338
246
|
continue;
|
|
339
247
|
const lower = line.toLowerCase();
|
|
@@ -344,16 +252,139 @@ async function runInteractive(settings) {
|
|
|
344
252
|
process.stdout.write('\x1B[2J\x1B[H');
|
|
345
253
|
renderHeader(settings, 'interactive');
|
|
346
254
|
renderInteractiveHelp();
|
|
255
|
+
printEvent('ok', 'Conversa limpa');
|
|
347
256
|
continue;
|
|
348
257
|
}
|
|
349
258
|
if (lower === '/files') {
|
|
350
|
-
|
|
259
|
+
printFileSampleForSettings(settings);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (lower === '/pick') {
|
|
263
|
+
const all = listFiles(settings.cwd);
|
|
264
|
+
const shown = all.slice(0, 60);
|
|
265
|
+
printFilePicker(shown, all.length);
|
|
266
|
+
if (shown.length === 0)
|
|
267
|
+
continue;
|
|
268
|
+
const answer = await rl.question('Numero(s) separado(s) por virgula (Enter cancela): ');
|
|
269
|
+
const raw = answer.trim();
|
|
270
|
+
if (!raw) {
|
|
271
|
+
printEvent('warn', 'Cancelado');
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const picked = [];
|
|
275
|
+
for (const part of raw.split(',')) {
|
|
276
|
+
const idx = Number.parseInt(part.trim(), 10);
|
|
277
|
+
if (Number.isInteger(idx) && idx >= 1 && idx <= shown.length) {
|
|
278
|
+
picked.push(shown[idx - 1]);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (picked.length === 0) {
|
|
282
|
+
printEvent('warn', 'Nenhum arquivo valido selecionado');
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const blocks = picked.map((file) => readFileContext(settings.cwd, file));
|
|
286
|
+
history.push({
|
|
287
|
+
role: 'user',
|
|
288
|
+
content: `Arquivos anexados manualmente via /pick:\n\n${blocks.join('\n\n')}`,
|
|
289
|
+
});
|
|
290
|
+
printEvent('ok', 'Arquivo(s) anexado(s) ao contexto', picked.join(', '));
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (lower === '/overview' || lower === '/visao') {
|
|
294
|
+
printBox('visao do projeto', getProjectOverview(settings.cwd).split(/\r?\n/));
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (lower === '/diff') {
|
|
298
|
+
const diff = getGitDiff(settings.cwd);
|
|
299
|
+
if (diff === '(sem diff)') {
|
|
300
|
+
printEvent('ok', 'Sem diff local');
|
|
301
|
+
}
|
|
302
|
+
else if (diff.startsWith('ERRO:')) {
|
|
303
|
+
printEvent('error', 'git diff', diff);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
printBox('diff atual', ['git diff -- .']);
|
|
307
|
+
process.stdout.write(renderColoredDiff(diff));
|
|
308
|
+
if (!diff.endsWith('\n'))
|
|
309
|
+
process.stdout.write('\n');
|
|
310
|
+
}
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (lower === '/review') {
|
|
314
|
+
await runReviewCommand(settings, async (question) => {
|
|
315
|
+
const answer = await rl.question(question);
|
|
316
|
+
return /^(y|yes|s|sim)$/i.test(answer.trim());
|
|
317
|
+
}, history);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (lower === '/save') {
|
|
321
|
+
const saved = saveCodeSession(settings, history);
|
|
322
|
+
printEvent('ok', 'Sessao salva', saved);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (lower === '/resume') {
|
|
326
|
+
const sessions = listCodeSessions(settings.cwd);
|
|
327
|
+
printSessionList(sessions);
|
|
328
|
+
if (sessions.length === 0)
|
|
329
|
+
continue;
|
|
330
|
+
if (history.length > 0) {
|
|
331
|
+
const overwrite = await rl.question('\nConversa atual sera substituida. Continuar? [y/N] ');
|
|
332
|
+
if (!/^(y|yes|s|sim)$/i.test(overwrite.trim())) {
|
|
333
|
+
printEvent('warn', 'Cancelado');
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const answer = await rl.question('Numero da sessao (Enter cancela): ');
|
|
338
|
+
const idx = Number.parseInt(answer.trim(), 10);
|
|
339
|
+
if (!Number.isInteger(idx) || idx < 1 || idx > sessions.length) {
|
|
340
|
+
printEvent('warn', 'Cancelado');
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const picked = sessions[idx - 1];
|
|
344
|
+
try {
|
|
345
|
+
history = loadCodeSession(settings.cwd, picked.file);
|
|
346
|
+
printEvent('ok', 'Sessao retomada', `${picked.file} (${history.length} mensagem(ns))`);
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
printEvent('error', 'Falha ao retomar sessao', error instanceof Error ? error.message : String(error));
|
|
350
|
+
}
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (lower === '/compact') {
|
|
354
|
+
const removed = compactCodeHistory(history);
|
|
355
|
+
printEvent('ok', 'Historico compactado', `${removed} mensagem(ns) removida(s)`);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (lower === '/status' || lower === '/context') {
|
|
359
|
+
renderSessionStatus(settings, history);
|
|
351
360
|
continue;
|
|
352
361
|
}
|
|
353
362
|
if (lower === '/model') {
|
|
354
363
|
printBox('modelo', [settings.model]);
|
|
355
364
|
continue;
|
|
356
365
|
}
|
|
366
|
+
if (lower === '/plan') {
|
|
367
|
+
settings.apply = false;
|
|
368
|
+
printEvent('ok', 'Modo plano', 'patches nao serao aplicados');
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (lower === '/apply') {
|
|
372
|
+
settings.apply = true;
|
|
373
|
+
settings.yes = false;
|
|
374
|
+
printEvent('ok', 'Modo aplicar', 'pedira confirmacao antes do git apply');
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (lower === '/yes') {
|
|
378
|
+
settings.apply = true;
|
|
379
|
+
settings.yes = true;
|
|
380
|
+
printEvent('warn', 'Auto-aplicar ativo', 'patches validos serao aplicados sem pergunta');
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (lower === '/no') {
|
|
384
|
+
settings.yes = false;
|
|
385
|
+
printEvent('ok', 'Confirmacao manual ativa');
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
357
388
|
if (lower === '/help' || lower === '/ajuda') {
|
|
358
389
|
renderInteractiveHelp();
|
|
359
390
|
continue;
|
|
@@ -378,9 +409,12 @@ function runDoctor(settings) {
|
|
|
378
409
|
const apiUrl = config.get('neetruApiUrl') ?? 'https://api.neetru.com';
|
|
379
410
|
const git = spawnSync('git', ['--version'], { cwd: settings.cwd, encoding: 'utf8' });
|
|
380
411
|
renderHeader(settings, 'doctor');
|
|
412
|
+
if (settings.modelWarning)
|
|
413
|
+
log.warn(settings.modelWarning);
|
|
381
414
|
console.log(chalk.bold('Doctor'));
|
|
382
415
|
console.log(` token Neetru: ${token ? chalk.green('ok') : chalk.red('ausente')}`);
|
|
383
416
|
console.log(` Core API: ${chalk.dim(apiUrl)}`);
|
|
417
|
+
console.log(` AI Gateway: ${chalk.dim(AI_GATEWAY_CHAT_PATH)}`);
|
|
384
418
|
console.log(` git: ${git.status === 0 ? chalk.green('ok') : chalk.red('falhou')}`);
|
|
385
419
|
console.log(` modo patch: ${settings.apply ? 'aplica com confirmacao' : 'somente plano'}`);
|
|
386
420
|
console.log();
|
|
@@ -423,6 +457,8 @@ export async function runCodeCommand(promptParts, opts = {}) {
|
|
|
423
457
|
return;
|
|
424
458
|
}
|
|
425
459
|
renderHeader(settings, 'oneshot');
|
|
460
|
+
if (settings.modelWarning)
|
|
461
|
+
log.warn(settings.modelWarning);
|
|
426
462
|
await runAgentTask(task, settings, defaultConfirm);
|
|
427
463
|
}
|
|
428
464
|
catch (error) {
|