@neetru/cli 2.14.0 → 2.16.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 +40 -0
- package/dist/commands/build.d.ts +14 -0
- package/dist/commands/build.js +26 -2
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/code.d.ts +10 -31
- package/dist/commands/code.js +269 -220
- package/dist/commands/code.js.map +1 -1
- package/dist/commands/servers.js +8 -7
- package/dist/commands/servers.js.map +1 -1
- package/dist/lib/agent-commands.d.ts +16 -7
- package/dist/lib/agent-commands.js +40 -26
- package/dist/lib/agent-commands.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 +48 -0
- package/dist/lib/code-agent-files.js +285 -2
- 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 +78 -0
- package/dist/lib/code-agent-session.js +188 -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 +49 -0
- package/dist/lib/code-agent-ui.js +313 -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/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, truncateForGateway, } from '../lib/code-agent-files.js';
|
|
12
|
+
import { compactCodeHistory, compactHistoryToFit, MAX_HISTORY_TOTAL_CHARS, 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, startTurnSpinner, 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,59 +61,35 @@ 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 = [];
|
|
217
83
|
for (const file of settings.files) {
|
|
218
84
|
fileBlocks.push(readFileContext(settings.cwd, file));
|
|
219
85
|
}
|
|
220
|
-
return [
|
|
86
|
+
return truncateForGateway([
|
|
221
87
|
`Tarefa do usuario:\n${task}`,
|
|
222
88
|
`Cwd: ${settings.cwd}`,
|
|
223
89
|
fileBlocks.length
|
|
224
90
|
? `Contexto inicial de arquivos:\n\n${fileBlocks.join('\n\n')}`
|
|
225
91
|
: 'Nenhum arquivo foi anexado inicialmente. Use NEETRU_TOOL para pedir contexto local.',
|
|
226
|
-
].join('\n\n');
|
|
227
|
-
}
|
|
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;
|
|
92
|
+
].join('\n\n'));
|
|
245
93
|
}
|
|
246
94
|
async function defaultConfirm(question) {
|
|
247
95
|
if (!process.stdin.isTTY)
|
|
@@ -255,17 +103,64 @@ async function defaultConfirm(question) {
|
|
|
255
103
|
rl.close();
|
|
256
104
|
}
|
|
257
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Chama o AI Gateway mostrando progresso ao vivo — spinner animado + cor
|
|
108
|
+
* rotativa + tempo decorrido (`startTurnSpinner`, code-agent-ui.ts) em vez de
|
|
109
|
+
* ficar estatico durante respostas longas. Cobre TANTO o streaming quanto o
|
|
110
|
+
* fallback sem stream (antes o fallback ficava mudo do aviso ate a resposta
|
|
111
|
+
* chegar ou o timeout estourar). So anima em TTY real — em saida
|
|
112
|
+
* nao-interativa (pipe/CI/teste) isso so poluiria o log.
|
|
113
|
+
*
|
|
114
|
+
* Fallback: qualquer falha no streaming (rede, formato inesperado) refaz a
|
|
115
|
+
* chamada sem streaming — a corretude do turno nunca depende do streaming
|
|
116
|
+
* funcionar, so a experiencia visual.
|
|
117
|
+
*/
|
|
118
|
+
async function callGatewayWithProgress(settings, messages) {
|
|
119
|
+
const spinner = startTurnSpinner('AI Gateway');
|
|
120
|
+
try {
|
|
121
|
+
const content = await streamGatewayChat(settings, messages, (_delta, total) => {
|
|
122
|
+
spinner.update(`${total} char(s) recebido(s)`);
|
|
123
|
+
});
|
|
124
|
+
spinner.stop();
|
|
125
|
+
return content;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
spinner.stop();
|
|
129
|
+
printEvent('warn', 'Streaming falhou, tentando sem streaming', error instanceof Error ? error.message : String(error));
|
|
130
|
+
const fallbackSpinner = startTurnSpinner('AI Gateway (sem stream)');
|
|
131
|
+
try {
|
|
132
|
+
const content = await callGateway(settings, messages);
|
|
133
|
+
fallbackSpinner.stop();
|
|
134
|
+
return content;
|
|
135
|
+
}
|
|
136
|
+
catch (fallbackError) {
|
|
137
|
+
fallbackSpinner.stop();
|
|
138
|
+
throw fallbackError;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
258
142
|
/**
|
|
259
143
|
* `history` e mutado e reaproveitado entre chamadas: e o que da ao modo
|
|
260
144
|
* interativo memoria de conversa entre turnos do REPL (como Claude Code/Codex),
|
|
261
145
|
* em vez de cada linha digitada virar uma requisicao amnesica. O one-shot
|
|
262
146
|
* (`neetru code "tarefa"`) chama sem `history` — cada `runAgentTask` comeca do
|
|
263
147
|
* zero, igual sempre foi.
|
|
148
|
+
*
|
|
149
|
+
* `onState` (opcional): observa a maquina de estados da sessao (Fase 2,
|
|
150
|
+
* NEETRU_CODE_ARCHITECTURE.md) — usado por testes pra verificar a sequencia
|
|
151
|
+
* idle -> thinking -> tool_requested -> tool_running -> ... sem mockar o
|
|
152
|
+
* AI Gateway inteiro. Cada chamada de runAgentTask e um ciclo completo
|
|
153
|
+
* (comeca e termina em idle/applied/failed/aborted).
|
|
264
154
|
*/
|
|
265
|
-
export async function runAgentTask(task, settings, confirm, history = []) {
|
|
155
|
+
export async function runAgentTask(task, settings, confirm, history = [], onState) {
|
|
266
156
|
if (!getActiveTokenSync()) {
|
|
267
157
|
throw new Error('Sessao Neetru ausente ou expirada. Rode `neetru login`.');
|
|
268
158
|
}
|
|
159
|
+
const machine = new CodeAgentStateMachine();
|
|
160
|
+
const goto = (next) => {
|
|
161
|
+
machine.transition(next);
|
|
162
|
+
onState?.(next);
|
|
163
|
+
};
|
|
269
164
|
const messages = history;
|
|
270
165
|
const isFirstTurn = messages.length === 0;
|
|
271
166
|
if (isFirstTurn)
|
|
@@ -274,54 +169,78 @@ export async function runAgentTask(task, settings, confirm, history = []) {
|
|
|
274
169
|
role: 'user',
|
|
275
170
|
content: isFirstTurn ? buildInitialPrompt(task, settings) : task,
|
|
276
171
|
});
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
messages
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
});
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
const patch = extractPatch(content);
|
|
293
|
-
if (patch) {
|
|
294
|
-
console.log();
|
|
295
|
-
printPatchPreview(patch);
|
|
296
|
-
if (!settings.apply) {
|
|
297
|
-
printEvent('warn', 'Patch nao aplicado', '--plan/--no-apply');
|
|
298
|
-
return;
|
|
172
|
+
try {
|
|
173
|
+
for (let turn = 1; turn <= settings.maxTurns; turn++) {
|
|
174
|
+
goto('thinking');
|
|
175
|
+
// GAP-M1-15: auto-compact por TAMANHO antes de cada chamada — sem isso
|
|
176
|
+
// uma sessao com varias tool results grandes (cada uma ate
|
|
177
|
+
// MAX_GATEWAY_SAFE_CHARS) estoura o limite de payload TOTAL do
|
|
178
|
+
// provider (HTTP 413) mesmo com cada mensagem individual dentro do
|
|
179
|
+
// limite. `/compact` manual continua existindo pro usuario forcar
|
|
180
|
+
// antes disso se quiser.
|
|
181
|
+
const autoCompacted = compactHistoryToFit(messages, MAX_HISTORY_TOTAL_CHARS);
|
|
182
|
+
if (autoCompacted > 0) {
|
|
183
|
+
printEvent('warn', 'Historico compactado automaticamente', `${autoCompacted} mensagem(ns) antiga(s) removida(s)`);
|
|
299
184
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
185
|
+
printEvent('wait', 'AI Gateway', `turno ${turn}/${settings.maxTurns}`);
|
|
186
|
+
const content = await callGatewayWithProgress(settings, messages);
|
|
187
|
+
messages.push({ role: 'assistant', content });
|
|
188
|
+
const tool = parseToolRequest(content);
|
|
189
|
+
if (tool) {
|
|
190
|
+
goto('tool_requested');
|
|
191
|
+
printEvent('tool', tool.tool, toolDetail(tool));
|
|
192
|
+
goto('tool_running');
|
|
193
|
+
const result = runCodeTool(settings.cwd, tool);
|
|
194
|
+
printEvent('ok', 'contexto local', summarizeToolResult(result));
|
|
195
|
+
messages.push({
|
|
196
|
+
role: 'user',
|
|
197
|
+
content: truncateForGateway(`Resultado da tool ${tool.tool}:\n\n${result}`),
|
|
198
|
+
});
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const patch = extractPatch(content);
|
|
202
|
+
if (patch) {
|
|
203
|
+
goto('patch_ready');
|
|
204
|
+
console.log();
|
|
205
|
+
printPatchPreview(patch);
|
|
206
|
+
if (!settings.apply) {
|
|
207
|
+
printEvent('warn', 'Patch nao aplicado', '--plan/--no-apply');
|
|
208
|
+
goto('aborted');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const approved = settings.yes || (await (async () => {
|
|
212
|
+
goto('awaiting_confirm');
|
|
213
|
+
return confirm('\nAplicar este patch? [y/N] ');
|
|
214
|
+
})());
|
|
215
|
+
if (!approved) {
|
|
216
|
+
printEvent('warn', 'Patch nao aplicado');
|
|
217
|
+
goto('aborted');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
applyUnifiedPatch(settings.cwd, patch);
|
|
221
|
+
logAppliedPatch(settings.cwd, patchSummaryRows(patch)[0] ?? 'arquivos: nao detectado');
|
|
222
|
+
printEvent('ok', 'Patch aplicado', 'git apply');
|
|
223
|
+
goto('applied');
|
|
303
224
|
return;
|
|
304
225
|
}
|
|
305
|
-
|
|
306
|
-
|
|
226
|
+
console.log();
|
|
227
|
+
printAssistantText(content);
|
|
228
|
+
goto('idle');
|
|
307
229
|
return;
|
|
308
230
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
return;
|
|
231
|
+
goto('failed');
|
|
232
|
+
throw new Error(`Limite de turnos atingido (${settings.maxTurns}) sem resposta final.`);
|
|
312
233
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
printEvent('warn', 'Nenhum arquivo encontrado');
|
|
319
|
-
return;
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (machine.state !== 'failed' && machine.state !== 'aborted' && machine.state !== 'applied') {
|
|
236
|
+
goto('failed');
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
320
239
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
240
|
+
}
|
|
241
|
+
function printFileSampleForSettings(settings) {
|
|
242
|
+
const all = listFiles(settings.cwd);
|
|
243
|
+
printFileSample(all.slice(0, 60), all.length);
|
|
325
244
|
}
|
|
326
245
|
async function runInteractive(settings) {
|
|
327
246
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
@@ -329,11 +248,13 @@ async function runInteractive(settings) {
|
|
|
329
248
|
}
|
|
330
249
|
renderHeader(settings, 'interactive');
|
|
331
250
|
renderInteractiveHelp();
|
|
251
|
+
if (settings.modelWarning)
|
|
252
|
+
log.warn(settings.modelWarning);
|
|
332
253
|
let history = [];
|
|
333
254
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
334
255
|
try {
|
|
335
256
|
for (;;) {
|
|
336
|
-
const line = (await rl.question(
|
|
257
|
+
const line = (await rl.question(renderPrompt(settings))).trim();
|
|
337
258
|
if (!line)
|
|
338
259
|
continue;
|
|
339
260
|
const lower = line.toLowerCase();
|
|
@@ -344,16 +265,139 @@ async function runInteractive(settings) {
|
|
|
344
265
|
process.stdout.write('\x1B[2J\x1B[H');
|
|
345
266
|
renderHeader(settings, 'interactive');
|
|
346
267
|
renderInteractiveHelp();
|
|
268
|
+
printEvent('ok', 'Conversa limpa');
|
|
347
269
|
continue;
|
|
348
270
|
}
|
|
349
271
|
if (lower === '/files') {
|
|
350
|
-
|
|
272
|
+
printFileSampleForSettings(settings);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (lower === '/pick') {
|
|
276
|
+
const all = listFiles(settings.cwd);
|
|
277
|
+
const shown = all.slice(0, 60);
|
|
278
|
+
printFilePicker(shown, all.length);
|
|
279
|
+
if (shown.length === 0)
|
|
280
|
+
continue;
|
|
281
|
+
const answer = await rl.question('Numero(s) separado(s) por virgula (Enter cancela): ');
|
|
282
|
+
const raw = answer.trim();
|
|
283
|
+
if (!raw) {
|
|
284
|
+
printEvent('warn', 'Cancelado');
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const picked = [];
|
|
288
|
+
for (const part of raw.split(',')) {
|
|
289
|
+
const idx = Number.parseInt(part.trim(), 10);
|
|
290
|
+
if (Number.isInteger(idx) && idx >= 1 && idx <= shown.length) {
|
|
291
|
+
picked.push(shown[idx - 1]);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (picked.length === 0) {
|
|
295
|
+
printEvent('warn', 'Nenhum arquivo valido selecionado');
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const blocks = picked.map((file) => readFileContext(settings.cwd, file));
|
|
299
|
+
history.push({
|
|
300
|
+
role: 'user',
|
|
301
|
+
content: truncateForGateway(`Arquivos anexados manualmente via /pick:\n\n${blocks.join('\n\n')}`),
|
|
302
|
+
});
|
|
303
|
+
printEvent('ok', 'Arquivo(s) anexado(s) ao contexto', picked.join(', '));
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (lower === '/overview' || lower === '/visao') {
|
|
307
|
+
printBox('visao do projeto', getProjectOverview(settings.cwd).split(/\r?\n/));
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (lower === '/diff') {
|
|
311
|
+
const diff = getGitDiff(settings.cwd);
|
|
312
|
+
if (diff === '(sem diff)') {
|
|
313
|
+
printEvent('ok', 'Sem diff local');
|
|
314
|
+
}
|
|
315
|
+
else if (diff.startsWith('ERRO:')) {
|
|
316
|
+
printEvent('error', 'git diff', diff);
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
printBox('diff atual', ['git diff -- .']);
|
|
320
|
+
process.stdout.write(renderColoredDiff(diff));
|
|
321
|
+
if (!diff.endsWith('\n'))
|
|
322
|
+
process.stdout.write('\n');
|
|
323
|
+
}
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (lower === '/review') {
|
|
327
|
+
await runReviewCommand(settings, async (question) => {
|
|
328
|
+
const answer = await rl.question(question);
|
|
329
|
+
return /^(y|yes|s|sim)$/i.test(answer.trim());
|
|
330
|
+
}, history);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (lower === '/save') {
|
|
334
|
+
const saved = saveCodeSession(settings, history);
|
|
335
|
+
printEvent('ok', 'Sessao salva', saved);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (lower === '/resume') {
|
|
339
|
+
const sessions = listCodeSessions(settings.cwd);
|
|
340
|
+
printSessionList(sessions);
|
|
341
|
+
if (sessions.length === 0)
|
|
342
|
+
continue;
|
|
343
|
+
if (history.length > 0) {
|
|
344
|
+
const overwrite = await rl.question('\nConversa atual sera substituida. Continuar? [y/N] ');
|
|
345
|
+
if (!/^(y|yes|s|sim)$/i.test(overwrite.trim())) {
|
|
346
|
+
printEvent('warn', 'Cancelado');
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const answer = await rl.question('Numero da sessao (Enter cancela): ');
|
|
351
|
+
const idx = Number.parseInt(answer.trim(), 10);
|
|
352
|
+
if (!Number.isInteger(idx) || idx < 1 || idx > sessions.length) {
|
|
353
|
+
printEvent('warn', 'Cancelado');
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const picked = sessions[idx - 1];
|
|
357
|
+
try {
|
|
358
|
+
history = loadCodeSession(settings.cwd, picked.file);
|
|
359
|
+
printEvent('ok', 'Sessao retomada', `${picked.file} (${history.length} mensagem(ns))`);
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
printEvent('error', 'Falha ao retomar sessao', error instanceof Error ? error.message : String(error));
|
|
363
|
+
}
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (lower === '/compact') {
|
|
367
|
+
const removed = compactCodeHistory(history);
|
|
368
|
+
printEvent('ok', 'Historico compactado', `${removed} mensagem(ns) removida(s)`);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (lower === '/status' || lower === '/context') {
|
|
372
|
+
renderSessionStatus(settings, history);
|
|
351
373
|
continue;
|
|
352
374
|
}
|
|
353
375
|
if (lower === '/model') {
|
|
354
376
|
printBox('modelo', [settings.model]);
|
|
355
377
|
continue;
|
|
356
378
|
}
|
|
379
|
+
if (lower === '/plan') {
|
|
380
|
+
settings.apply = false;
|
|
381
|
+
printEvent('ok', 'Modo plano', 'patches nao serao aplicados');
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (lower === '/apply') {
|
|
385
|
+
settings.apply = true;
|
|
386
|
+
settings.yes = false;
|
|
387
|
+
printEvent('ok', 'Modo aplicar', 'pedira confirmacao antes do git apply');
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (lower === '/yes') {
|
|
391
|
+
settings.apply = true;
|
|
392
|
+
settings.yes = true;
|
|
393
|
+
printEvent('warn', 'Auto-aplicar ativo', 'patches validos serao aplicados sem pergunta');
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (lower === '/no') {
|
|
397
|
+
settings.yes = false;
|
|
398
|
+
printEvent('ok', 'Confirmacao manual ativa');
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
357
401
|
if (lower === '/help' || lower === '/ajuda') {
|
|
358
402
|
renderInteractiveHelp();
|
|
359
403
|
continue;
|
|
@@ -378,9 +422,12 @@ function runDoctor(settings) {
|
|
|
378
422
|
const apiUrl = config.get('neetruApiUrl') ?? 'https://api.neetru.com';
|
|
379
423
|
const git = spawnSync('git', ['--version'], { cwd: settings.cwd, encoding: 'utf8' });
|
|
380
424
|
renderHeader(settings, 'doctor');
|
|
425
|
+
if (settings.modelWarning)
|
|
426
|
+
log.warn(settings.modelWarning);
|
|
381
427
|
console.log(chalk.bold('Doctor'));
|
|
382
428
|
console.log(` token Neetru: ${token ? chalk.green('ok') : chalk.red('ausente')}`);
|
|
383
429
|
console.log(` Core API: ${chalk.dim(apiUrl)}`);
|
|
430
|
+
console.log(` AI Gateway: ${chalk.dim(AI_GATEWAY_CHAT_PATH)}`);
|
|
384
431
|
console.log(` git: ${git.status === 0 ? chalk.green('ok') : chalk.red('falhou')}`);
|
|
385
432
|
console.log(` modo patch: ${settings.apply ? 'aplica com confirmacao' : 'somente plano'}`);
|
|
386
433
|
console.log();
|
|
@@ -423,6 +470,8 @@ export async function runCodeCommand(promptParts, opts = {}) {
|
|
|
423
470
|
return;
|
|
424
471
|
}
|
|
425
472
|
renderHeader(settings, 'oneshot');
|
|
473
|
+
if (settings.modelWarning)
|
|
474
|
+
log.warn(settings.modelWarning);
|
|
426
475
|
await runAgentTask(task, settings, defaultConfirm);
|
|
427
476
|
}
|
|
428
477
|
catch (error) {
|