pokt-cli 1.0.7 → 1.0.9
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 +39 -3
- package/dist/bin/pokt.js +39 -8
- package/dist/chat/client.js +26 -6
- package/dist/chat/loop.js +523 -66
- package/dist/chat/mcp-from-text.d.ts +23 -0
- package/dist/chat/mcp-from-text.js +364 -0
- package/dist/chat/sessions.d.ts +18 -0
- package/dist/chat/sessions.js +120 -0
- package/dist/chat/slim-tools.d.ts +6 -0
- package/dist/chat/slim-tools.js +65 -0
- package/dist/chat/tools.js +30 -1
- package/dist/commands/chat.js +13 -5
- package/dist/commands/config.js +25 -3
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +82 -0
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +103 -37
- package/dist/commands/models.js +96 -7
- package/dist/commands/provider.js +20 -6
- package/dist/config.d.ts +29 -1
- package/dist/config.js +60 -4
- package/dist/mcp/client.d.ts +12 -7
- package/dist/mcp/client.js +254 -46
- package/dist/mcp/oauth-provider.d.ts +34 -0
- package/dist/mcp/oauth-provider.js +159 -0
- package/dist/mcp/project-mcp.d.ts +38 -0
- package/dist/mcp/project-mcp.js +208 -0
- package/dist/ui.d.ts +6 -1
- package/dist/ui.js +114 -21
- package/package.json +1 -1
package/dist/chat/loop.js
CHANGED
|
@@ -2,11 +2,17 @@ import prompts from 'prompts';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { ui } from '../ui.js';
|
|
4
4
|
import { runProFlow } from '../commands/pro.js';
|
|
5
|
+
import { PROVIDER_LABELS, ALL_PROVIDERS } from '../config.js';
|
|
5
6
|
import { config } from '../config.js';
|
|
6
7
|
import { getClient } from './client.js';
|
|
7
8
|
import { tools, executeTool } from './tools.js';
|
|
9
|
+
import { saveAuto, loadAuto, listCheckpoints, saveCheckpoint, loadCheckpoint, deleteCheckpoint, exportConversation, getSessionsDir } from './sessions.js';
|
|
8
10
|
import { connectMcpServer, getAllMcpToolsOpenAI, callMcpTool, isMcpTool, disconnectAllMcp, } from '../mcp/client.js';
|
|
9
|
-
|
|
11
|
+
import { getMergedMcpServers } from '../mcp/project-mcp.js';
|
|
12
|
+
import { runMcpFromBashMarkdown, stripExecutedStyleMcpBashBlocks, tryAutoMcpForListDatabases, } from './mcp-from-text.js';
|
|
13
|
+
import { slimToolsForUpstreamPayload } from './slim-tools.js';
|
|
14
|
+
/** Base do system prompt; a lista de ferramentas MCP é anexada em runtime quando houver servidores. */
|
|
15
|
+
const SYSTEM_PROMPT_BASE = `You are Pokt CLI, an elite AI Software Engineer.
|
|
10
16
|
Your goal is to help the user build, fix, and maintain software projects with high quality.
|
|
11
17
|
|
|
12
18
|
CORE CAPABILITIES:
|
|
@@ -14,20 +20,38 @@ CORE CAPABILITIES:
|
|
|
14
20
|
2. **Autonomous Coding**: You can create new files, rewrite existing ones, and run terminal commands.
|
|
15
21
|
3. **Problem Solving**: You analyze errors and propose/apply fixes.
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
- For
|
|
21
|
-
-
|
|
23
|
+
FUNCTION CALLING (native tools — USE THEM):
|
|
24
|
+
- This chat uses OpenAI-style **tool_calls**. You MUST use the provided functions for actions: \`read_file\`, \`write_file\`, \`run_command\`, \`list_files\`, etc., and any tool whose name starts with \`mcp_\`.
|
|
25
|
+
- **Avoid** shell lines like \`mcp_Something_tool "..."\` in markdown — the CLI may run them as **fallback** if they match a registered tool, but **native tool_calls are always better** (correct args, one round-trip).
|
|
26
|
+
- For databases/APIs exposed via MCP, call the real \`mcp_*\` tools with the correct JSON arguments (e.g. Neon: run SQL via the server's SQL tool, not a invented command name).
|
|
27
|
+
- **Neon MCP**: tools like \`get_database_tables\`, \`describe_project\`, \`list_branch_computes\` need \`projectId\`. Call \`list_projects\` first and pass the \`id\` of the target project, or rely on the CLI: if your account has exactly one project, Pokt may inject \`projectId\` automatically. To list logical Postgres databases, prefer \`run_sql\` with \`SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY 1;\`.
|
|
28
|
+
- **Neon \`run_sql\` (tool_calls)**: pass **JSON** fields expected by the schema (e.g. \`projectId\`, \`branchId\`, \`sql\`). Do **not** put \`neonctl\`-style flags inside \`sql\` (wrong: \`{"sql":"--project-id ... --sql \\"SELECT 1\\""}\`; right: \`{"projectId":"...","branchId":"...","sql":"SELECT 1"}\`). Use the \`id\` from \`list_projects\` for \`projectId\` — do not invent project ids.
|
|
29
|
+
- If a tool call fails, read the error, adjust arguments, and retry or explain what the user must fix in config.
|
|
30
|
+
|
|
31
|
+
WHEN THE MODEL DOES NOT RETURN tool_calls (rare):
|
|
32
|
+
- You may still output the complete file content in a markdown code block so the CLI fallback can apply it. Format: mention the filename (e.g. hello.py) then \`\`\`lang ... \`\`\`.
|
|
33
|
+
- Do NOT use this for shell-only or SQL-in-bash blocks meant to be executed — use \`run_command\` or MCP tools instead.
|
|
22
34
|
|
|
23
35
|
GUIDELINES:
|
|
24
36
|
- You will receive the user request first, then the current project structure. Use the project structure to understand the context before creating or editing anything.
|
|
25
37
|
- When asked to fix something, first **read** the relevant files to understand the context.
|
|
26
|
-
- When creating a project, start by planning the structure, then use \`write_file\` to create each file.
|
|
27
|
-
- You have full access to the current terminal
|
|
38
|
+
- When creating a project, start by planning the structure, then use \`write_file\` (tool call) to create each file.
|
|
39
|
+
- You have full access to the current terminal via \`run_command\` for \`npm install\`, \`tsc\`, etc. You may also emit **scripts executáveis** (Node, Python, npx, \`psql\`, etc.) via \`run_command\` when MCP não estiver disponível ou o usuário pedir código para rodar localmente.
|
|
40
|
+
- **MCP tools**: Tools named \`mcp_<ServerName>_<toolName>\` connect to external services. Prefer them when they match the task.
|
|
41
|
+
- **Never** return a completely empty assistant message: always include a short natural-language answer and/or use tool_calls. After tools run, summarize results for the user in Portuguese.
|
|
42
|
+
- **After MCP/SQL succeeds**: give a **short** confirmation plus a **markdown table** (or bullet list) for rows/columns — do **not** repeat bash blocks with mcp_* lines, raw tool JSON, or invented shell commands; the CLI already executed native tool_calls.
|
|
28
43
|
- Be extremely concise in your explanations.
|
|
29
44
|
- The current working directory is: ${process.cwd()}
|
|
30
45
|
`;
|
|
46
|
+
function buildSystemPrompt(mcpToolNames) {
|
|
47
|
+
if (mcpToolNames.length === 0)
|
|
48
|
+
return SYSTEM_PROMPT_BASE;
|
|
49
|
+
const list = mcpToolNames.join(', ');
|
|
50
|
+
return `${SYSTEM_PROMPT_BASE}
|
|
51
|
+
|
|
52
|
+
REGISTERED MCP TOOL NAMES (use only these exact names in tool_calls, never bash):
|
|
53
|
+
${list}`;
|
|
54
|
+
}
|
|
31
55
|
async function loadProjectStructure() {
|
|
32
56
|
try {
|
|
33
57
|
const timeoutMs = 8000;
|
|
@@ -46,9 +70,13 @@ async function loadProjectStructure() {
|
|
|
46
70
|
}
|
|
47
71
|
}
|
|
48
72
|
export async function startChatLoop(modelConfig) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
73
|
+
let activeModel = modelConfig;
|
|
74
|
+
let client = await getClient(activeModel);
|
|
75
|
+
// MCP: config global + pokt_cli/mcp.json na raiz do projeto (projeto sobrescreve por nome)
|
|
76
|
+
const { merged: mcpServers, poktDir, mcpJsonPath } = getMergedMcpServers(process.cwd());
|
|
77
|
+
if (poktDir) {
|
|
78
|
+
console.log(ui.dim(`[MCP] Projeto: ${mcpJsonPath ?? poktDir}`));
|
|
79
|
+
}
|
|
52
80
|
for (const server of mcpServers) {
|
|
53
81
|
const session = await connectMcpServer(server);
|
|
54
82
|
if (session) {
|
|
@@ -59,14 +87,179 @@ export async function startChatLoop(modelConfig) {
|
|
|
59
87
|
...tools,
|
|
60
88
|
...getAllMcpToolsOpenAI(),
|
|
61
89
|
];
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
/** Controller (Express) usa body parser com limite finito; MCPs como Neon geram schemas enormes. */
|
|
91
|
+
const toolsForApi = activeModel.provider === 'controller' ? slimToolsForUpstreamPayload(allTools) : allTools;
|
|
92
|
+
const mcpToolNames = allTools
|
|
93
|
+
.filter((t) => t.type === 'function' && Boolean(t.function?.name?.startsWith('mcp_')))
|
|
94
|
+
.map((t) => t.function.name);
|
|
95
|
+
const messages = [{ role: 'system', content: buildSystemPrompt(mcpToolNames) }];
|
|
96
|
+
// Auto-resume do projeto (estilo gemini): se existir, adiciona mensagens anteriores (sem duplicar system prompt)
|
|
97
|
+
const prev = loadAuto();
|
|
98
|
+
if (prev && prev.length > 0) {
|
|
99
|
+
for (const m of prev) {
|
|
100
|
+
if (m.role === 'system')
|
|
101
|
+
continue;
|
|
102
|
+
messages.push({ role: m.role, content: m.content });
|
|
103
|
+
}
|
|
104
|
+
console.log(ui.dim(`Sessão anterior carregada (projeto). Use /chat list | /chat save <tag>.`));
|
|
105
|
+
}
|
|
106
|
+
function modelLabel(m) {
|
|
107
|
+
const provider = PROVIDER_LABELS[m.provider] ?? m.provider;
|
|
108
|
+
return `[${provider}] ${m.id}`;
|
|
109
|
+
}
|
|
110
|
+
async function switchModelFlow(mode = 'model') {
|
|
111
|
+
const models = config.get('registeredModels') ?? [];
|
|
112
|
+
if (!Array.isArray(models) || models.length === 0) {
|
|
113
|
+
console.log(ui.error('Nenhum modelo registrado. Rode: pokt models list'));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const providerChoices = ALL_PROVIDERS.map((p) => {
|
|
117
|
+
const label = PROVIDER_LABELS[p] ?? p;
|
|
118
|
+
const hasAny = models.some((m) => m.provider === p);
|
|
119
|
+
const star = activeModel.provider === p ? '★ ' : '';
|
|
120
|
+
return {
|
|
121
|
+
title: `${star}${label}${hasAny ? '' : ' (sem modelos)'}`,
|
|
122
|
+
value: p,
|
|
123
|
+
disabled: !hasAny,
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
const providerPick = await prompts({
|
|
127
|
+
type: 'select',
|
|
128
|
+
name: 'provider',
|
|
129
|
+
message: mode === 'provider' ? 'Trocar provedor:' : 'Selecione o provedor:',
|
|
130
|
+
choices: [...providerChoices, { title: '🔙 Cancelar', value: 'cancel' }],
|
|
131
|
+
});
|
|
132
|
+
if (!providerPick.provider || providerPick.provider === 'cancel')
|
|
133
|
+
return;
|
|
134
|
+
const provider = providerPick.provider;
|
|
135
|
+
const providerModels = models.filter((m) => m.provider === provider);
|
|
136
|
+
if (providerModels.length === 0) {
|
|
137
|
+
console.log(ui.error(`Sem modelos para ${PROVIDER_LABELS[provider] ?? provider}.`));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
let selected = null;
|
|
141
|
+
if (mode === 'provider') {
|
|
142
|
+
selected = providerModels[0];
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const pick = await prompts({
|
|
146
|
+
type: 'select',
|
|
147
|
+
name: 'idx',
|
|
148
|
+
message: `Modelos em ${PROVIDER_LABELS[provider] ?? provider}:`,
|
|
149
|
+
choices: [
|
|
150
|
+
...providerModels.map((m, i) => ({
|
|
151
|
+
title: `${activeModel.provider === m.provider && activeModel.id === m.id ? '★ ' : ''}${m.id}`,
|
|
152
|
+
value: i,
|
|
153
|
+
})),
|
|
154
|
+
{ title: '🔙 Cancelar', value: 'cancel' },
|
|
155
|
+
],
|
|
156
|
+
});
|
|
157
|
+
if (pick.idx === 'cancel' || typeof pick.idx !== 'number')
|
|
158
|
+
return;
|
|
159
|
+
selected = providerModels[pick.idx] ?? null;
|
|
160
|
+
}
|
|
161
|
+
if (!selected)
|
|
162
|
+
return;
|
|
163
|
+
// Validar chaves/credenciais necessárias (reaproveita mesma lógica do providerCommand / getClient)
|
|
164
|
+
try {
|
|
165
|
+
const newClient = await getClient(selected);
|
|
166
|
+
client = newClient;
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
console.log(ui.error(e?.message ?? String(e)));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
activeModel = selected;
|
|
173
|
+
config.set('activeModel', selected);
|
|
174
|
+
console.log(ui.success(`Modelo ativo atualizado: ${modelLabel(selected)}`));
|
|
175
|
+
console.log(ui.dim('Dica: o histórico do chat foi mantido; apenas o modelo/provedor mudou.'));
|
|
176
|
+
}
|
|
177
|
+
function printHelp() {
|
|
178
|
+
console.log(ui.dim(`
|
|
179
|
+
Comandos do chat:
|
|
180
|
+
${ui.accent('/help')} — mostra esta ajuda
|
|
181
|
+
${ui.accent('/clear')} — limpa a tela
|
|
182
|
+
${ui.accent('/status')} — mostra modelo/provider atual
|
|
183
|
+
${ui.accent('/model')} — trocar modelo (menu interativo)
|
|
184
|
+
${ui.accent('/provider')} — trocar provedor (usa o 1º modelo disponível)
|
|
185
|
+
${ui.accent('/chat')} — checkpoints/sessões (list/save/resume/delete/share)
|
|
186
|
+
${ui.accent('/resume')} — alias de /chat
|
|
187
|
+
${ui.accent('/copy')} — copia a última resposta do Pokt (Windows: clip)
|
|
188
|
+
${ui.accent('/pro')} — abrir Pokt Pro no navegador
|
|
189
|
+
${ui.accent('/quit')} ou ${ui.accent('exit')} — sair do chat
|
|
190
|
+
`));
|
|
191
|
+
}
|
|
192
|
+
let lastAssistantText = '';
|
|
193
|
+
async function handleChatCommand(raw) {
|
|
194
|
+
const parts = raw.trim().split(/\s+/);
|
|
195
|
+
const cmd = parts[0].toLowerCase();
|
|
196
|
+
const sub = (parts[1] ?? 'list').toLowerCase();
|
|
197
|
+
const arg = parts.slice(2).join(' ').trim();
|
|
198
|
+
if (sub === 'dir') {
|
|
199
|
+
console.log(ui.dim(`Sessões: ${getSessionsDir()}`));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (sub === 'list') {
|
|
203
|
+
const items = listCheckpoints();
|
|
204
|
+
if (items.length === 0) {
|
|
205
|
+
console.log(ui.dim('Nenhum checkpoint salvo ainda. Use: /chat save <tag>'));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
console.log(ui.dim('Checkpoints:'));
|
|
209
|
+
for (const it of items) {
|
|
210
|
+
console.log(`- ${ui.accent(it.tag)} ${it.updatedAt ? ui.dim(`(${it.updatedAt})`) : ''}`);
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (sub === 'save') {
|
|
215
|
+
if (!arg) {
|
|
216
|
+
console.log(ui.error('Uso: /chat save <tag>'));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
saveCheckpoint(arg, messages);
|
|
220
|
+
console.log(ui.success(`Checkpoint salvo: ${arg}`));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (sub === 'resume' || sub === 'load') {
|
|
224
|
+
if (!arg) {
|
|
225
|
+
console.log(ui.error('Uso: /chat resume <tag>'));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const loaded = loadCheckpoint(arg);
|
|
229
|
+
// mantém system prompt; substitui resto
|
|
230
|
+
const sys = messages[0];
|
|
231
|
+
messages.length = 0;
|
|
232
|
+
messages.push(sys);
|
|
233
|
+
for (const m of loaded) {
|
|
234
|
+
if (m.role === 'system')
|
|
235
|
+
continue;
|
|
236
|
+
messages.push({ role: m.role, content: m.content });
|
|
237
|
+
}
|
|
238
|
+
console.log(ui.success(`Checkpoint carregado: ${arg}`));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (sub === 'delete' || sub === 'rm') {
|
|
242
|
+
if (!arg) {
|
|
243
|
+
console.log(ui.error('Uso: /chat delete <tag>'));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
deleteCheckpoint(arg);
|
|
247
|
+
console.log(ui.success(`Checkpoint removido: ${arg}`));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (sub === 'share' || sub === 'export') {
|
|
251
|
+
const filename = arg || `pokt-chat-${Date.now()}.md`;
|
|
252
|
+
const out = exportConversation(filename, messages);
|
|
253
|
+
console.log(ui.success(`Exportado: ${out}`));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
console.log(ui.warn(`Subcomando desconhecido: ${sub}. Use /chat list|save|resume|delete|share`));
|
|
257
|
+
}
|
|
65
258
|
while (true) {
|
|
66
259
|
console.log('');
|
|
67
260
|
const cwd = process.cwd();
|
|
68
261
|
console.log(ui.dim(`Diretório atual: ${cwd}`));
|
|
69
|
-
console.log(ui.shortcutsLine(
|
|
262
|
+
console.log(ui.shortcutsLine(undefined, '? · /help · /pro'));
|
|
70
263
|
const response = await prompts({
|
|
71
264
|
type: 'text',
|
|
72
265
|
name: 'input',
|
|
@@ -82,6 +275,55 @@ export async function startChatLoop(modelConfig) {
|
|
|
82
275
|
}
|
|
83
276
|
const trimmed = userInput.trim();
|
|
84
277
|
const low = trimmed.toLowerCase();
|
|
278
|
+
if (low === '/help' || low === '/?' || low === 'help') {
|
|
279
|
+
printHelp();
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (low === '/clear') {
|
|
283
|
+
console.clear();
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (low === '/status') {
|
|
287
|
+
console.log(ui.statusBar({ cwd: process.cwd(), model: `/model ${activeModel.provider} (${activeModel.id})` }));
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (low.startsWith('/chat') || low.startsWith('/resume')) {
|
|
291
|
+
await handleChatCommand(trimmed.replace(/^\/resume/i, '/chat'));
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (low === '/copy') {
|
|
295
|
+
try {
|
|
296
|
+
if (!lastAssistantText) {
|
|
297
|
+
console.log(ui.warn('Nada para copiar ainda.'));
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
// Sem interpolar conteúdo no comando (evita quebra por aspas/newlines).
|
|
301
|
+
// Escreve para um arquivo temporário e copia via Get-Content | clip (Windows).
|
|
302
|
+
const tmp = `.pokt_copy_${Date.now()}.txt`;
|
|
303
|
+
await executeTool('write_file', JSON.stringify({ path: tmp, content: lastAssistantText }));
|
|
304
|
+
await executeTool('run_command', JSON.stringify({ command: `powershell -NoProfile -Command "Get-Content -Raw '${tmp}' | clip"` }));
|
|
305
|
+
// best-effort cleanup
|
|
306
|
+
try {
|
|
307
|
+
await executeTool('delete_file', JSON.stringify({ path: tmp }));
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// ignore
|
|
311
|
+
}
|
|
312
|
+
console.log(ui.success('Copiado para a área de transferência.'));
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
console.log(ui.warn('Falha ao copiar. Se estiver no Windows, verifique se o comando "clip" está disponível.'));
|
|
316
|
+
}
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (low === '/model') {
|
|
320
|
+
await switchModelFlow('model');
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (low === '/provider') {
|
|
324
|
+
await switchModelFlow('provider');
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
85
327
|
if (low === '/pro' || low === '/torne-se-pro' || low === 'torne-se pro') {
|
|
86
328
|
runProFlow();
|
|
87
329
|
continue;
|
|
@@ -91,10 +333,12 @@ export async function startChatLoop(modelConfig) {
|
|
|
91
333
|
Atalhos:
|
|
92
334
|
${ui.accent('/pro')} ou ${ui.accent('/torne-se-pro')} — abrir Pokt Pro no navegador (pagamento + chave)
|
|
93
335
|
exit, ${ui.accent('/quit')} — sair do chat
|
|
336
|
+
${ui.accent('/help')} — ver comandos do chat
|
|
94
337
|
`));
|
|
95
338
|
continue;
|
|
96
339
|
}
|
|
97
340
|
messages.push({ role: 'user', content: userInput });
|
|
341
|
+
saveAuto(messages);
|
|
98
342
|
// Primeiro o modelo vê o pedido; depois carregamos a estrutura do projeto para ele entender e então criar/editar
|
|
99
343
|
const isFirstUserMessage = messages.filter(m => m.role === 'user').length === 1;
|
|
100
344
|
if (isFirstUserMessage) {
|
|
@@ -103,25 +347,110 @@ Atalhos:
|
|
|
103
347
|
loadSpinner.stop();
|
|
104
348
|
messages.push({ role: 'system', content: `Current Project Structure:\n${projectStructure}` });
|
|
105
349
|
}
|
|
106
|
-
await processLLMResponse(client,
|
|
350
|
+
await processLLMResponse(client, activeModel.id, messages, toolsForApi);
|
|
351
|
+
// Atualiza auto-save após resposta
|
|
352
|
+
saveAuto(messages);
|
|
353
|
+
// Captura última resposta do assistente para /copy (melhor esforço)
|
|
354
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
355
|
+
const m = messages[i];
|
|
356
|
+
if (m?.role === 'assistant') {
|
|
357
|
+
const c = m.content;
|
|
358
|
+
lastAssistantText = typeof c === 'string' ? c : Array.isArray(c) ? c.map((p) => (p && p.text ? p.text : String(p))).join('') : String(c ?? '');
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
107
362
|
}
|
|
108
363
|
await disconnectAllMcp();
|
|
109
364
|
}
|
|
110
|
-
const
|
|
111
|
-
const
|
|
365
|
+
const MAX_RETRIES = 4;
|
|
366
|
+
const BASE_RETRY_DELAY_MS = 1500;
|
|
367
|
+
const MAX_RETRY_DELAY_MS = 15000;
|
|
368
|
+
function sleep(ms) {
|
|
369
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
370
|
+
}
|
|
371
|
+
function getStatusCode(err) {
|
|
372
|
+
const s = err?.status ?? err?.response?.status;
|
|
373
|
+
return typeof s === 'number' ? s : null;
|
|
374
|
+
}
|
|
375
|
+
function isRetryable(err) {
|
|
376
|
+
const status = getStatusCode(err);
|
|
377
|
+
if (status === 429 || status === 408)
|
|
378
|
+
return true;
|
|
379
|
+
if (status && status >= 500 && status <= 599)
|
|
380
|
+
return true;
|
|
381
|
+
const msg = String(err?.message ?? '');
|
|
382
|
+
// erros comuns de rede/timeout
|
|
383
|
+
return /(ETIMEDOUT|ECONNRESET|EAI_AGAIN|ENOTFOUND|fetch failed|network|timeout)/i.test(msg);
|
|
384
|
+
}
|
|
385
|
+
function computeBackoff(attempt) {
|
|
386
|
+
const exp = Math.min(MAX_RETRY_DELAY_MS, BASE_RETRY_DELAY_MS * Math.pow(2, attempt));
|
|
387
|
+
const jitter = Math.floor(Math.random() * 250);
|
|
388
|
+
return exp + jitter;
|
|
389
|
+
}
|
|
112
390
|
/** Extensões que consideramos como arquivos de código para aplicar fallback */
|
|
113
391
|
const CODE_EXT = /\.(py|js|ts|tsx|jsx|html|css|json|md|txt|java|go|rs|c|cpp|rb|php)$/i;
|
|
392
|
+
/** Blocos shell/console: nunca viram arquivo no fallback (evita SQL/comandos fictícios em mcp.json). */
|
|
393
|
+
function isShellLikeBlock(lang) {
|
|
394
|
+
return /^(bash|sh|shell|zsh|powershell|ps1|cmd|console)$/i.test(lang);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Caminhos que o fallback nunca deve sobrescrever (config MCP, env, lockfile).
|
|
398
|
+
*/
|
|
399
|
+
const FALLBACK_PATH_BLOCKLIST = /(^|\/|\\)(mcp\.json|\.env(\.[a-zA-Z0-9_-]+)?|package-lock\.json)$/i;
|
|
400
|
+
function isFallbackPathBlocked(relPath) {
|
|
401
|
+
const norm = relPath.replace(/\\/g, '/');
|
|
402
|
+
const base = norm.split('/').pop() ?? norm;
|
|
403
|
+
return FALLBACK_PATH_BLOCKLIST.test(norm) || FALLBACK_PATH_BLOCKLIST.test(base);
|
|
404
|
+
}
|
|
405
|
+
/** Evita gravar `generated.*` com JSON de resposta MCP/SQL (eco do modelo). */
|
|
406
|
+
function looksLikeMcpOrSqlResultSnippet(code) {
|
|
407
|
+
const t = code.trim();
|
|
408
|
+
if (!t)
|
|
409
|
+
return false;
|
|
410
|
+
if (/MCP error|"invalid_type"|Input validation error/i.test(t))
|
|
411
|
+
return true;
|
|
412
|
+
try {
|
|
413
|
+
const j = JSON.parse(t);
|
|
414
|
+
if (j !== null && typeof j === 'object' && !Array.isArray(j)) {
|
|
415
|
+
const o = j;
|
|
416
|
+
if ('success' in o && 'result' in o)
|
|
417
|
+
return true;
|
|
418
|
+
if (typeof o.error === 'string' && /mcp|tool|validation|invalid/i.test(o.error))
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
if (Array.isArray(j) && j.length > 0 && j.length <= 200) {
|
|
422
|
+
const first = j[0];
|
|
423
|
+
if (first && typeof first === 'object' && !Array.isArray(first)) {
|
|
424
|
+
const row = first;
|
|
425
|
+
const keys = Object.keys(row);
|
|
426
|
+
if (keys.length === 0)
|
|
427
|
+
return false;
|
|
428
|
+
if (!keys.every((k) => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(k)))
|
|
429
|
+
return false;
|
|
430
|
+
const vals = Object.values(row);
|
|
431
|
+
if (vals.every((v) => v === null || typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')) {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
// não é JSON — não marcar
|
|
439
|
+
}
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
function toolsVerbose() {
|
|
443
|
+
return process.env.POKT_VERBOSE_TOOLS === '1' || process.env.POKT_VERBOSE === '1';
|
|
444
|
+
}
|
|
445
|
+
/** Linhas estilo "mcp_Server_tool ..." não são arquivos — são invocações inventadas. */
|
|
446
|
+
function looksLikeFakeMcpInvocation(code) {
|
|
447
|
+
return /^\s*mcp_[A-Za-z0-9_.-]+\s+/m.test(code.trim());
|
|
448
|
+
}
|
|
114
449
|
/**
|
|
115
450
|
* Quando a API não retorna tool_calls, alguns backends só devolvem texto.
|
|
116
451
|
* Extrai blocos de código da resposta (```lang\n...\n```) e, se encontrar
|
|
117
452
|
* um nome de arquivo mencionado antes do bloco, aplica write_file.
|
|
118
453
|
*/
|
|
119
|
-
/** Blocos de comando "como rodar" (bash/sh de 1–2 linhas) não viram arquivo para não poluir. */
|
|
120
|
-
function isRunCommandOnly(lang, code) {
|
|
121
|
-
const shellLike = /^(bash|sh|shell|zsh)$/i.test(lang);
|
|
122
|
-
const lines = code.split('\n').filter((l) => l.trim().length > 0);
|
|
123
|
-
return shellLike && lines.length <= 2;
|
|
124
|
-
}
|
|
125
454
|
/** Remove markdown/formatting do nome de arquivo (ex: **hello.py** → hello.py). */
|
|
126
455
|
function cleanFilename(candidate) {
|
|
127
456
|
return candidate.replace(/^[\s*`'"]+/g, '').replace(/[\s*`'")\]\s]+$/g, '').trim();
|
|
@@ -157,7 +486,9 @@ async function applyCodeBlocksFromContent(content) {
|
|
|
157
486
|
});
|
|
158
487
|
}
|
|
159
488
|
for (const { fullMatch, index, lang, code } of matches) {
|
|
160
|
-
if (
|
|
489
|
+
if (isShellLikeBlock(lang))
|
|
490
|
+
continue;
|
|
491
|
+
if (looksLikeFakeMcpInvocation(code))
|
|
161
492
|
continue;
|
|
162
493
|
const beforeBlock = content.substring(0, index);
|
|
163
494
|
// Nome de arquivo: aceita "**hello.py**", "hello.py" antes de espaço/newline/backtick, etc.
|
|
@@ -165,9 +496,21 @@ async function applyCodeBlocksFromContent(content) {
|
|
|
165
496
|
const rawCandidate = fileMatch ? fileMatch[fileMatch.length - 1].trim() : null;
|
|
166
497
|
const candidate = rawCandidate ? cleanFilename(rawCandidate) : null;
|
|
167
498
|
const path = candidate && CODE_EXT.test(candidate) ? candidate : (lang === 'python' ? 'generated.py' : lang ? `generated.${lang}` : null);
|
|
499
|
+
if (path && isFallbackPathBlocked(path))
|
|
500
|
+
continue;
|
|
501
|
+
if (path &&
|
|
502
|
+
/^generated\./i.test(path) &&
|
|
503
|
+
looksLikeMcpOrSqlResultSnippet(code)) {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
168
506
|
if (path && code) {
|
|
169
507
|
try {
|
|
170
|
-
|
|
508
|
+
if (toolsVerbose()) {
|
|
509
|
+
console.log(ui.warn(`\n[Fallback] Aplicando código da resposta ao arquivo: ${path}`));
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
console.log(ui.dim(`\n[Fallback] ${path}`));
|
|
513
|
+
}
|
|
171
514
|
await executeTool('write_file', JSON.stringify({ path, content: code }));
|
|
172
515
|
applied = true;
|
|
173
516
|
appliedBlocks.push({ start: index, end: index + fullMatch.length, path });
|
|
@@ -186,23 +529,23 @@ async function applyCodeBlocksFromContent(content) {
|
|
|
186
529
|
}
|
|
187
530
|
return { applied, displayContent };
|
|
188
531
|
}
|
|
189
|
-
async function createCompletionWithRetry(client, modelId, messages, toolsList) {
|
|
532
|
+
async function createCompletionWithRetry(client, modelId, messages, toolsList, toolChoice = 'auto') {
|
|
190
533
|
let lastError;
|
|
191
|
-
for (let attempt = 0; attempt <=
|
|
534
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
192
535
|
try {
|
|
193
536
|
return await client.chat.completions.create({
|
|
194
537
|
model: modelId,
|
|
195
538
|
messages,
|
|
196
539
|
tools: toolsList,
|
|
197
|
-
tool_choice:
|
|
540
|
+
tool_choice: toolChoice,
|
|
198
541
|
});
|
|
199
542
|
}
|
|
200
543
|
catch (err) {
|
|
201
544
|
lastError = err;
|
|
202
|
-
const
|
|
203
|
-
if (
|
|
204
|
-
const delayMs =
|
|
205
|
-
await
|
|
545
|
+
const retryable = isRetryable(err);
|
|
546
|
+
if (retryable && attempt < MAX_RETRIES) {
|
|
547
|
+
const delayMs = computeBackoff(attempt);
|
|
548
|
+
await sleep(delayMs);
|
|
206
549
|
continue;
|
|
207
550
|
}
|
|
208
551
|
throw err;
|
|
@@ -210,42 +553,144 @@ async function createCompletionWithRetry(client, modelId, messages, toolsList) {
|
|
|
210
553
|
}
|
|
211
554
|
throw lastError;
|
|
212
555
|
}
|
|
556
|
+
/**
|
|
557
|
+
* Executa todas as rodadas de tool_calls até o modelo devolver mensagem sem ferramentas.
|
|
558
|
+
*/
|
|
559
|
+
async function drainToolCalls(client, modelId, messages, toolsList, startMessage, spinner) {
|
|
560
|
+
let message = startMessage;
|
|
561
|
+
let writeFileExecuted = false;
|
|
562
|
+
let anyToolExecuted = false;
|
|
563
|
+
let mcpToolExecuted = false;
|
|
564
|
+
const verbose = toolsVerbose();
|
|
565
|
+
while (message.tool_calls && message.tool_calls.length > 0) {
|
|
566
|
+
messages.push(message);
|
|
567
|
+
for (const toolCall of message.tool_calls) {
|
|
568
|
+
anyToolExecuted = true;
|
|
569
|
+
const name = toolCall.function.name;
|
|
570
|
+
if (name === 'write_file')
|
|
571
|
+
writeFileExecuted = true;
|
|
572
|
+
if (isMcpTool(name))
|
|
573
|
+
mcpToolExecuted = true;
|
|
574
|
+
const args = toolCall.function.arguments ?? '{}';
|
|
575
|
+
const isMcp = isMcpTool(name);
|
|
576
|
+
if (isMcp && !verbose) {
|
|
577
|
+
console.log(ui.dim(`[MCP] ${name}…`));
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
console.log(ui.warn(`\n[Executing Tool: ${name}]`));
|
|
581
|
+
if (verbose || !isMcp)
|
|
582
|
+
console.log(ui.dim(`Arguments: ${args}`));
|
|
583
|
+
}
|
|
584
|
+
const toolSpinner = ora('Running tool...').start();
|
|
585
|
+
const result = isMcp ? await callMcpTool(name, args) : await executeTool(name, args);
|
|
586
|
+
toolSpinner.stop();
|
|
587
|
+
if (verbose) {
|
|
588
|
+
console.log(ui.dim(`Result: ${result.length} characters`));
|
|
589
|
+
}
|
|
590
|
+
else if (isMcp) {
|
|
591
|
+
console.log(ui.dim(`[MCP] ${name} ✓ (${result.length} chars)`));
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
console.log(ui.dim(`Result: ${result.length} characters`));
|
|
595
|
+
}
|
|
596
|
+
messages.push({
|
|
597
|
+
role: 'tool',
|
|
598
|
+
tool_call_id: toolCall.id,
|
|
599
|
+
content: result,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
spinner.start('Thinking...');
|
|
603
|
+
const completion = await createCompletionWithRetry(client, modelId, messages, toolsList, 'auto');
|
|
604
|
+
spinner.stop();
|
|
605
|
+
message = completion.choices[0].message;
|
|
606
|
+
}
|
|
607
|
+
return { message, writeFileExecuted, anyToolExecuted, mcpToolExecuted };
|
|
608
|
+
}
|
|
213
609
|
async function processLLMResponse(client, modelId, messages, toolsList) {
|
|
214
610
|
const spinner = ora('Thinking...').start();
|
|
215
611
|
try {
|
|
216
612
|
let completion = await createCompletionWithRetry(client, modelId, messages, toolsList);
|
|
217
613
|
let message = completion.choices[0].message;
|
|
218
614
|
spinner.stop();
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
615
|
+
const drained = await drainToolCalls(client, modelId, messages, toolsList, message, spinner);
|
|
616
|
+
message = drained.message;
|
|
617
|
+
let writeFileExecutedThisTurn = drained.writeFileExecuted;
|
|
618
|
+
let anyToolExecutedThisTurn = drained.anyToolExecuted;
|
|
619
|
+
let mcpToolExecutedThisTurn = drained.mcpToolExecuted;
|
|
620
|
+
let rawContent = message.content;
|
|
621
|
+
let contentStr = messageContentToString(rawContent);
|
|
622
|
+
let finalContent = rawContent ?? contentStr;
|
|
623
|
+
if (mcpToolExecutedThisTurn) {
|
|
624
|
+
contentStr = stripExecutedStyleMcpBashBlocks(contentStr);
|
|
625
|
+
finalContent = contentStr;
|
|
626
|
+
}
|
|
627
|
+
// Modelo devolveu só tool_calls e texto vazio — evita mensagem inútil
|
|
628
|
+
if (!contentStr.trim() && anyToolExecutedThisTurn) {
|
|
629
|
+
contentStr =
|
|
630
|
+
'*(Ferramentas foram executadas (veja os logs `[MCP]` acima). O modelo não gerou texto final — peça um resumo com tabelas/dados se precisar.)*';
|
|
631
|
+
finalContent = contentStr;
|
|
632
|
+
}
|
|
633
|
+
// Só executa fallback bash se não houve MCP nativo (evita SQL duplicado e ruído).
|
|
634
|
+
let mcpFromText = mcpToolExecutedThisTurn
|
|
635
|
+
? { invocationCount: 0, executedCount: 0, augmentedAssistantText: contentStr }
|
|
636
|
+
: await runMcpFromBashMarkdown(contentStr, {
|
|
637
|
+
skipDuplicateAppendix: /\n##\s+Resultados\s+MCP\b/i.test(contentStr),
|
|
638
|
+
});
|
|
639
|
+
if (mcpFromText.invocationCount > 0) {
|
|
640
|
+
contentStr = mcpFromText.augmentedAssistantText;
|
|
641
|
+
finalContent = mcpFromText.augmentedAssistantText;
|
|
642
|
+
}
|
|
643
|
+
// Resposta completamente vazia: recuperação (tool_choice required) + dreno + MCP em texto + SQL automático
|
|
644
|
+
if (!contentStr.trim() && toolsList.length > 0) {
|
|
645
|
+
messages.push({
|
|
646
|
+
role: 'system',
|
|
647
|
+
content: '[Pokt — recuperação] A última resposta veio vazia (sem texto útil). Responda em português. ' +
|
|
648
|
+
'Use tool_calls: para listar bancos PostgreSQL use mcp_*_run_sql com {"sql":"SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY 1;"}. ' +
|
|
649
|
+
'Alternativa: run_command com script executável (node, python, npx, psql). Nunca devolva corpo vazio.',
|
|
650
|
+
});
|
|
651
|
+
spinner.start('Recuperando resposta vazia…');
|
|
652
|
+
let recovery;
|
|
653
|
+
try {
|
|
654
|
+
recovery = await createCompletionWithRetry(client, modelId, messages, toolsList, 'required');
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
recovery = await createCompletionWithRetry(client, modelId, messages, toolsList, 'auto');
|
|
240
658
|
}
|
|
241
|
-
spinner.start('Thinking...');
|
|
242
|
-
completion = await createCompletionWithRetry(client, modelId, messages, toolsList);
|
|
243
|
-
message = completion.choices[0].message;
|
|
244
659
|
spinner.stop();
|
|
660
|
+
const drained2 = await drainToolCalls(client, modelId, messages, toolsList, recovery.choices[0].message, spinner);
|
|
661
|
+
message = drained2.message;
|
|
662
|
+
writeFileExecutedThisTurn = writeFileExecutedThisTurn || drained2.writeFileExecuted;
|
|
663
|
+
anyToolExecutedThisTurn = anyToolExecutedThisTurn || drained2.anyToolExecuted;
|
|
664
|
+
mcpToolExecutedThisTurn = mcpToolExecutedThisTurn || drained2.mcpToolExecuted;
|
|
665
|
+
rawContent = message.content;
|
|
666
|
+
contentStr = messageContentToString(rawContent);
|
|
667
|
+
finalContent = rawContent ?? contentStr;
|
|
668
|
+
if (mcpToolExecutedThisTurn) {
|
|
669
|
+
contentStr = stripExecutedStyleMcpBashBlocks(contentStr);
|
|
670
|
+
finalContent = contentStr;
|
|
671
|
+
}
|
|
672
|
+
if (!contentStr.trim() && anyToolExecutedThisTurn) {
|
|
673
|
+
contentStr =
|
|
674
|
+
'*(Ferramentas executadas no terminal acima; o modelo ainda não resumiu em texto — diga “resuma” se quiser.)*';
|
|
675
|
+
finalContent = contentStr;
|
|
676
|
+
}
|
|
677
|
+
mcpFromText = mcpToolExecutedThisTurn
|
|
678
|
+
? { invocationCount: 0, executedCount: 0, augmentedAssistantText: contentStr }
|
|
679
|
+
: await runMcpFromBashMarkdown(contentStr, {
|
|
680
|
+
skipDuplicateAppendix: /\n##\s+Resultados\s+MCP\b/i.test(contentStr),
|
|
681
|
+
});
|
|
682
|
+
if (mcpFromText.invocationCount > 0) {
|
|
683
|
+
contentStr = mcpFromText.augmentedAssistantText;
|
|
684
|
+
finalContent = mcpFromText.augmentedAssistantText;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (!contentStr.trim()) {
|
|
688
|
+
const autoDb = await tryAutoMcpForListDatabases(messages);
|
|
689
|
+
if (autoDb) {
|
|
690
|
+
contentStr = autoDb;
|
|
691
|
+
finalContent = autoDb;
|
|
692
|
+
}
|
|
245
693
|
}
|
|
246
|
-
const rawContent = message.content;
|
|
247
|
-
let contentStr = messageContentToString(rawContent);
|
|
248
|
-
let finalContent = rawContent ?? contentStr;
|
|
249
694
|
// Quando a API não executa tools, tentar aplicar blocos de código da resposta
|
|
250
695
|
if (!writeFileExecutedThisTurn) {
|
|
251
696
|
let result = await applyCodeBlocksFromContent(contentStr);
|
|
@@ -254,7 +699,7 @@ async function processLLMResponse(client, modelId, messages, toolsList) {
|
|
|
254
699
|
|| (/call\s+(read_file|write_file)/i.test(contentStr) && contentStr.length < 400);
|
|
255
700
|
if (!result.applied && looksLikeToolIntentOnly) {
|
|
256
701
|
messages.push({ role: 'assistant', content: rawContent ?? contentStr });
|
|
257
|
-
const followUpSystem = `
|
|
702
|
+
const followUpSystem = `You replied as if tools would run in text only. Use tool_calls for read_file/write_file/run_command/mcp_* when possible. If you must output a file as markdown only: mention the filename then a full \`\`\`lang\`\`\` block — never use fake shell lines like mcp_Foo_bar. Do that now for the user's last request.`;
|
|
258
703
|
messages.push({ role: 'system', content: followUpSystem });
|
|
259
704
|
spinner.start('Getting code...');
|
|
260
705
|
const followUp = await createCompletionWithRetry(client, modelId, messages, toolsList);
|
|
@@ -281,7 +726,7 @@ async function processLLMResponse(client, modelId, messages, toolsList) {
|
|
|
281
726
|
}
|
|
282
727
|
else {
|
|
283
728
|
console.log('\n' + ui.labelPokt());
|
|
284
|
-
console.log(ui.dim('(
|
|
729
|
+
console.log(ui.dim('(Sem resposta da IA após recuperação. Tente: outro modelo em /model, ou peça explicitamente “chame mcp_Neon_run_sql com SELECT datname FROM pg_database…”, ou use run_command com psql/node.)'));
|
|
285
730
|
messages.push({ role: 'assistant', content: '' });
|
|
286
731
|
}
|
|
287
732
|
}
|
|
@@ -300,10 +745,22 @@ async function processLLMResponse(client, modelId, messages, toolsList) {
|
|
|
300
745
|
}
|
|
301
746
|
catch (error) {
|
|
302
747
|
spinner.stop();
|
|
303
|
-
const
|
|
304
|
-
if (
|
|
305
|
-
console.log(ui.error('\nLimite de taxa
|
|
306
|
-
console.log(ui.dim('
|
|
748
|
+
const status = getStatusCode(error);
|
|
749
|
+
if (status === 429) {
|
|
750
|
+
console.log(ui.error('\nLimite de taxa (429). O provedor está te limitando por volume ou quota.'));
|
|
751
|
+
console.log(ui.dim('Dica: aguarde um pouco e tente novamente; se persistir, troque o provider/model ou verifique sua quota.'));
|
|
752
|
+
}
|
|
753
|
+
else if (status === 401 || status === 403) {
|
|
754
|
+
console.log(ui.error(`\nNão autorizado (${status}). Sua chave/token pode estar inválida ou sem permissão.`));
|
|
755
|
+
console.log(ui.dim('Dica: rode "pokt doctor" e confira suas variáveis de ambiente / pokt config show.'));
|
|
756
|
+
}
|
|
757
|
+
else if (status && status >= 500 && status <= 599) {
|
|
758
|
+
console.log(ui.error(`\nFalha no servidor (${status}). O provedor está instável no momento.`));
|
|
759
|
+
console.log(ui.dim('Dica: tente novamente em alguns segundos ou troque de provider.'));
|
|
760
|
+
}
|
|
761
|
+
else if (status === 413 || /413|Payload Too Large/i.test(String(error?.message ?? ''))) {
|
|
762
|
+
console.log(ui.error('\nCorpo da requisição muito grande (413 Payload Too Large).'));
|
|
763
|
+
console.log(ui.dim('Dica: use /chat save e /clear ou comece sessão nova; histórico + ferramentas MCP (Neon) estouram o limite do servidor. O Pokt já reduz schemas ao usar o Controller — atualize o Controller (limite JSON) e o CLI.'));
|
|
307
764
|
}
|
|
308
765
|
else {
|
|
309
766
|
console.log(ui.error(`\nError: ${error?.message ?? error}`));
|