terminal-smart-cli 0.97.11 → 0.97.13
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/bin/ts.js +108 -9
- package/lib/agent.js +49 -19
- package/lib/api.js +9 -2
- package/lib/intelligence-core.js +49 -12
- package/lib/meta.js +314 -14
- package/lib/tools.js +42 -16
- package/package.json +3 -3
package/bin/ts.js
CHANGED
|
@@ -293,6 +293,75 @@ async function imageCmd(args) {
|
|
|
293
293
|
if (JSON_OUT) console.log(JSON.stringify(output));
|
|
294
294
|
else console.log('\n' + ui.okLine((en ? 'image saved: ' : 'imagem salva: ') + C.cyan(target)) + '\n ' + C.dim(`${result.model} · ${result.charged} créditos · saldo ${result.remaining}`));
|
|
295
295
|
}
|
|
296
|
+
|
|
297
|
+
// `ts video` continua analisando URLs. Geração usa um nome separado para não
|
|
298
|
+
// quebrar scripts existentes: `ts gerar-video "descrição"`.
|
|
299
|
+
async function generateVideoCmd(args) {
|
|
300
|
+
const token = needToken();
|
|
301
|
+
const en = cfg.lang === 'en';
|
|
302
|
+
const prompt = args.join(' ').trim() || await readStdin();
|
|
303
|
+
if (!prompt || prompt.length < 8) throw new ApiError(en ? 'Describe the video.' : 'Descreva o vídeo (mínimo de 8 caracteres).', { code: 'invalid_prompt' });
|
|
304
|
+
const catalog = await api('/api/ia/video/options', { token });
|
|
305
|
+
const options = Array.isArray(catalog.options) ? catalog.options : [];
|
|
306
|
+
let choice = FLAGS.has('--premium') ? 'premium'
|
|
307
|
+
: (FLAGS.has('--economico') || FLAGS.has('--economy')) ? 'economy'
|
|
308
|
+
: (FLAGS.has('--sem-video') || FLAGS.has('--none')) ? 'none' : '';
|
|
309
|
+
if (!choice && (!process.stdin.isTTY || JSON_OUT)) {
|
|
310
|
+
throw new ApiError(en ? 'Choose --premium, --economy or --none.' : 'Escolha --premium, --economico ou --sem-video. Vídeo pago nunca é aprovado silenciosamente.', { code: 'video_confirmation_required' });
|
|
311
|
+
}
|
|
312
|
+
if (!choice) {
|
|
313
|
+
const byId = id => options.find(option => option.id === id) || {};
|
|
314
|
+
const economy = byId('economy'), premium = byId('premium');
|
|
315
|
+
console.log('\n' + ui.box([
|
|
316
|
+
C.bold(en ? 'Generate video' : 'Gerar vídeo'),
|
|
317
|
+
C.dim(en ? 'The charge happens only after secure delivery.' : 'A cobrança só acontece depois da entrega segura.'), '',
|
|
318
|
+
` ${C.cyan('1')} ${C.bold(economy.label || 'Econômico · Veo Lite')} ${C.dim((economy.estimatedCredits || '?') + ' créditos')}${economy.allowed === false ? C.warn(' · requer Pro') : ''}`,
|
|
319
|
+
` ${C.cyan('2')} ${C.bold(premium.label || 'Premium · Sora 2')} ${C.dim((premium.estimatedCredits || '?') + ' créditos')}${premium.allowed === false ? C.warn(' · requer Pro') : ''}`,
|
|
320
|
+
` ${C.cyan('3')} ${C.bold(en ? 'Do not generate' : 'Não gerar')} ${C.dim(en ? 'no charge' : 'sem cobrança')}`, '',
|
|
321
|
+
C.dim((en ? 'Available ' : 'Disponível ') + (catalog.available ?? catalog.remaining ?? 0) + ' créditos'),
|
|
322
|
+
], { title: 'ts gerar-video' }));
|
|
323
|
+
const answer = String(await ui.ask(C.dim(en ? ' Choose 1, 2 or 3 › ' : ' Escolha 1, 2 ou 3 › '))).trim();
|
|
324
|
+
choice = answer === '1' ? 'economy' : answer === '2' ? 'premium' : 'none';
|
|
325
|
+
}
|
|
326
|
+
const selected = options.find(option => option.id === choice);
|
|
327
|
+
if (!selected || selected.allowed === false) throw new ApiError(en ? 'Video generation requires Pro.' : 'Geração de vídeo requer plano Pro.', { code: 'plan_limit' });
|
|
328
|
+
if (choice === 'none') { if (JSON_OUT) console.log(JSON.stringify({ ok: true, skipped: true, charged: 0 })); else console.log(ui.infoLine(en ? 'No video generated.' : 'Nenhum vídeo gerado e nenhum crédito cobrado.')); return; }
|
|
329
|
+
const conversationId = await ensureConv(token);
|
|
330
|
+
const spinner = JSON_OUT ? { stop() {}, text() {} } : ui.spinner(en ? 'starting video…' : 'iniciando vídeo…').start();
|
|
331
|
+
let submitted;
|
|
332
|
+
try {
|
|
333
|
+
submitted = await api('/api/ia/video', { method: 'POST', token, timeoutMs: 120000, body: { conversationId, prompt, choice, confirmed: true, aspectRatio: '16:9' } });
|
|
334
|
+
const id = submitted && submitted.job && submitted.job.id;
|
|
335
|
+
if (!id) throw new Error(en ? 'Video job was not created.' : 'O job de vídeo não foi criado.');
|
|
336
|
+
let job;
|
|
337
|
+
for (let attempt = 0; attempt < 180; attempt++) {
|
|
338
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
339
|
+
const state = await api('/api/ia/video/jobs/' + encodeURIComponent(id), { token, timeoutMs: 60000 });
|
|
340
|
+
job = state.job || {};
|
|
341
|
+
spinner.text((en ? 'generating video' : 'gerando vídeo') + ` · ${job.progress || 0}%`);
|
|
342
|
+
if (job.status === 'failed') throw new Error(job.error || (en ? 'Video failed.' : 'Falha ao gerar vídeo.'));
|
|
343
|
+
if (job.status === 'completed') break;
|
|
344
|
+
}
|
|
345
|
+
if (!job || job.status !== 'completed' || !job.contentUrl) throw new Error(en ? 'Video is still processing.' : 'O vídeo continua em processamento no servidor.');
|
|
346
|
+
const response = await fetch(base() + job.contentUrl, { headers: { 'x-session-token': token }, signal: AbortSignal.timeout(600000) });
|
|
347
|
+
if (!response.ok) throw new Error('Download HTTP ' + response.status);
|
|
348
|
+
const fs = require('fs'), os = require('os'), path = require('path');
|
|
349
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
350
|
+
if (bytes.length < 1024 || bytes.length > 250 * 1024 * 1024) throw new Error(en ? 'Invalid video payload.' : 'Conteúdo de vídeo inválido.');
|
|
351
|
+
const target = path.join(os.homedir(), 'Downloads', `terminal-smart-video-${Date.now()}.mp4`);
|
|
352
|
+
const partial = target + '.partial-' + process.pid;
|
|
353
|
+
try {
|
|
354
|
+
fs.writeFileSync(partial, bytes, { flag: 'wx' });
|
|
355
|
+
fs.renameSync(partial, target);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
try { fs.unlinkSync(partial); } catch (_) {}
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
const output = { ok: true, path: target, model: job.model, charged: job.charged, choice, seconds: job.seconds };
|
|
361
|
+
spinner.stop();
|
|
362
|
+
if (JSON_OUT) console.log(JSON.stringify(output)); else console.log('\n' + ui.okLine((en ? 'video saved: ' : 'vídeo salvo: ') + C.cyan(target)) + '\n ' + C.dim(`${job.model} · ${job.charged} créditos · ${job.seconds}s`));
|
|
363
|
+
} finally { spinner.stop(); }
|
|
364
|
+
}
|
|
296
365
|
// Envia uma mensagem na conversa ativa e devolve o texto (recria a conversa 1x se apagada na web).
|
|
297
366
|
async function sendMessage(token, content) {
|
|
298
367
|
const doStream = async (convId) => {
|
|
@@ -1734,7 +1803,7 @@ async function metaCmd() {
|
|
|
1734
1803
|
// Com --noturno, quando a run PAUSA por falta de crédito, esperamos o intervalo
|
|
1735
1804
|
// e RE-INVOCAMOS run() — que retoma sozinho do .ts-meta.json — até concluir ou
|
|
1736
1805
|
// bater um teto (créditos totais / janelas / horas / travamento sem progresso).
|
|
1737
|
-
const RETOMAVEL = new Set(['no_credits', 'budget', 'connection']); // pausas que valem re-tentar
|
|
1806
|
+
const RETOMAVEL = new Set(['no_credits', 'budget', 'connection', 'model_no_action', 'invalid_artifact']); // pausas que valem re-tentar
|
|
1738
1807
|
const _sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
1739
1808
|
let st = null, janela = 0, prevPend = null, semProgresso = 0;
|
|
1740
1809
|
const loopStart = Date.now();
|
|
@@ -1769,7 +1838,11 @@ async function metaCmd() {
|
|
|
1769
1838
|
onRoundDone: ({ checklist, spent }) => {
|
|
1770
1839
|
sp.stop();
|
|
1771
1840
|
const d = checklist.filter(i => i.passes).length;
|
|
1772
|
-
|
|
1841
|
+
// On resumed missions `budget` is only the new window size, while
|
|
1842
|
+
// `spent` is cumulative. Display the persisted cumulative ceiling.
|
|
1843
|
+
const live = metaMod.load(dir);
|
|
1844
|
+
const cumulativeBudget = live && Number(live.budget) > 0 ? Number(live.budget) : budget;
|
|
1845
|
+
console.log(' ' + C.dim(T.meta_progress(d, checklist.length, spent, cumulativeBudget)) + '\n');
|
|
1773
1846
|
sp.text(T.meta_marking).start();
|
|
1774
1847
|
},
|
|
1775
1848
|
});
|
|
@@ -1835,6 +1908,12 @@ async function metaCmd() {
|
|
|
1835
1908
|
console.log(' ' + C.dim('Resolva o bloqueio manualmente e rode "ts meta", ou ajuste o objetivo com --novo.'));
|
|
1836
1909
|
} else if (st.pause_reason === 'timeout') {
|
|
1837
1910
|
console.log(ui.infoLine('Tempo limite atingido. Retome com "ts meta".'));
|
|
1911
|
+
} else if (st.pause_reason === 'model_no_action') {
|
|
1912
|
+
console.log(ui.infoLine('O executor respondeu sem criar o artefato. A retomada usará geração direta ou outro modelo; rode "ts meta".'));
|
|
1913
|
+
} else if (st.pause_reason === 'connection') {
|
|
1914
|
+
console.log(ui.infoLine('O provedor interrompeu a geração. O progresso foi salvo; rode "ts meta" para retomar.'));
|
|
1915
|
+
} else if (st.pause_reason === 'invalid_artifact') {
|
|
1916
|
+
console.log(ui.infoLine('O artefato gerado não passou na validação. As partes válidas foram salvas; rode "ts meta".'));
|
|
1838
1917
|
} else {
|
|
1839
1918
|
console.log(ui.infoLine(st.pause_reason === 'budget' ? T.meta_paused_budget : T.meta_paused_rounds));
|
|
1840
1919
|
metaMod.notify(token, T.meta_notify_paused(st.goal, done, st.checklist.length, st.pause_reason));
|
|
@@ -2933,11 +3012,18 @@ async function uso() {
|
|
|
2933
3012
|
|
|
2934
3013
|
async function quem() {
|
|
2935
3014
|
const token = needToken();
|
|
2936
|
-
let ok = false;
|
|
2937
|
-
try {
|
|
3015
|
+
let ok = false, planoAtual = cfg.plan || '?';
|
|
3016
|
+
try {
|
|
3017
|
+
const [chk, conta] = await Promise.all([
|
|
3018
|
+
api('/api/auth/check', { token }),
|
|
3019
|
+
api('/api/credits', { token }),
|
|
3020
|
+
]);
|
|
3021
|
+
ok = !!(chk && (chk.success || chk.authenticated));
|
|
3022
|
+
if (conta && conta.plan) planoAtual = conta.plan;
|
|
3023
|
+
} catch (_) {}
|
|
2938
3024
|
console.log('\n' + ui.box([
|
|
2939
3025
|
C.dim(T.quem_user + ': ') + C.bold(cfg.username || '?') + (ok ? ' ' + C.ok('•') : ' ' + C.err('• offline')),
|
|
2940
|
-
C.dim(T.quem_plan + ': ') +
|
|
3026
|
+
C.dim(T.quem_plan + ': ') + planoAtual,
|
|
2941
3027
|
C.dim(T.quem_server + ': ') + base(),
|
|
2942
3028
|
C.dim(T.quem_conv + ': ') + (cfg.convId ? '#' + cfg.convId : '—'),
|
|
2943
3029
|
], { title: T.quem_title }) + '\n');
|
|
@@ -2958,13 +3044,22 @@ async function integracoesCmd(args) {
|
|
|
2958
3044
|
const [google, microsoft] = await Promise.all([getStatus('google'), getStatus('microsoft')]);
|
|
2959
3045
|
const data = { google, microsoft };
|
|
2960
3046
|
if (JSON_OUT) { console.log(JSON.stringify(data)); return; }
|
|
2961
|
-
const line = (label, item) => C.bold(label.padEnd(22)) + (item.connected ? C.ok(en ? 'connected' : 'conectado') + C.dim(item.email ? ` · ${item.email}` : '') : C.dim(en ? 'not connected' : 'não conectado'));
|
|
2962
|
-
console.log('\n' + ui.box([line('Google Workspace', google), line('Microsoft/Outlook', microsoft), '', C.dim(en ? 'Connect: ts integrations connect google|
|
|
3047
|
+
const line = (label, item) => C.bold(label.padEnd(22)) + (item.connected ? C.ok(en ? 'connected' : 'conectado') + C.dim(item.email ? ` · ${item.email}` : '') + C.dim(item.accessLevel ? ` · ${item.accessLevel === 'advanced' ? (en ? 'advanced beta' : 'avançado beta') : (en ? 'essential' : 'essencial')}` : '') : C.dim(en ? 'not connected' : 'não conectado'));
|
|
3048
|
+
console.log('\n' + ui.box([line('Google Workspace', google), line('Microsoft/Outlook', microsoft), '', C.dim(en ? 'Connect: ts integrations connect google [essential|advanced]' : 'Conectar: ts integracoes conectar google [essencial|avancado]'), C.dim(en ? 'Manage on web: terminalsmart.com.br/integracoes' : 'Gerenciar na Web: terminalsmart.com.br/integracoes')], { title: en ? 'Integrations' : 'Integrações' }) + '\n');
|
|
3049
|
+
return;
|
|
3050
|
+
}
|
|
3051
|
+
if (['arquivos', 'files', 'drive'].includes(sub)) {
|
|
3052
|
+
const url = 'https://terminalsmart.com.br/integracoes#google-files';
|
|
3053
|
+
if (JSON_OUT) { console.log(JSON.stringify({ provider: 'google', url })); return; }
|
|
3054
|
+
console.log(ui.infoLine(en ? 'Opening Google Drive file selection…' : 'Abrindo a seleção de arquivos do Google Drive…'));
|
|
3055
|
+
try { require('child_process').exec(process.platform === 'win32' ? `start "" "${url}"` : process.platform === 'darwin' ? `open "${url}"` : `xdg-open "${url}"`); } catch (_) {}
|
|
2963
3056
|
return;
|
|
2964
3057
|
}
|
|
2965
3058
|
if (!provider) { console.error(ui.infoLine(en ? 'Choose google or microsoft.' : 'Escolha google ou microsoft.')); process.exit(2); }
|
|
2966
3059
|
if (['conectar', 'connect', 'reconectar', 'reconnect'].includes(sub)) {
|
|
2967
|
-
const
|
|
3060
|
+
const requestedLevel = String(args[2] || '').toLowerCase();
|
|
3061
|
+
const accessLevel = provider === 'google' && ['avancado', 'advanced'].includes(requestedLevel) ? 'advanced' : 'essential';
|
|
3062
|
+
const data = await api(`/api/integrations/${provider}/connect`, { method: 'POST', token, body: { source: 'cli', ...(provider === 'google' ? { accessLevel } : {}) } });
|
|
2968
3063
|
if (JSON_OUT) { console.log(JSON.stringify({ provider, authUrl: data.authUrl })); return; }
|
|
2969
3064
|
console.log('\n' + ui.box([C.bold(en ? `Authorize ${names[provider]}` : `Autorize ${names[provider]}`), '', data.authUrl, '', C.dim(en ? 'After authorization, run: ts integrations' : 'Depois de autorizar, rode: ts integracoes')], { title: 'OAuth' }) + '\n');
|
|
2970
3065
|
try { require('child_process').exec(process.platform === 'win32' ? `start "" "${data.authUrl}"` : process.platform === 'darwin' ? `open "${data.authUrl}"` : `xdg-open "${data.authUrl}"`); } catch (_) {}
|
|
@@ -2977,7 +3072,7 @@ async function integracoesCmd(args) {
|
|
|
2977
3072
|
await api(`/api/integrations/${provider}/disconnect`, { method: 'POST', token, body: { confirmed: true } });
|
|
2978
3073
|
console.log(ui.okLine(`${names[provider]} ${en ? 'disconnected' : 'desconectado'}`)); return;
|
|
2979
3074
|
}
|
|
2980
|
-
console.log(ui.infoLine(en ? 'Usage: ts integrations [status|connect|disconnect] [google|microsoft]' : 'Uso: ts integracoes [status|conectar|desconectar] [google|microsoft]'));
|
|
3075
|
+
console.log(ui.infoLine(en ? 'Usage: ts integrations [status|connect|disconnect|files] [google|microsoft] [essential|advanced]' : 'Uso: ts integracoes [status|conectar|desconectar|arquivos] [google|microsoft] [essencial|avancado]'));
|
|
2981
3076
|
}
|
|
2982
3077
|
|
|
2983
3078
|
function idioma(l) {
|
|
@@ -3046,6 +3141,7 @@ function recallCmd(args) {
|
|
|
3046
3141
|
case 'logout': return logout();
|
|
3047
3142
|
case 'chat': case 'conversa': return chatRepl();
|
|
3048
3143
|
case 'imagem': case 'image': case 'img': return imageCmd(POS.slice(1));
|
|
3144
|
+
case 'gerar-video': case 'video-gerar': case 'generate-video': return generateVideoCmd(POS.slice(1));
|
|
3049
3145
|
case 'quem': case 'whoami': return quem();
|
|
3050
3146
|
case 'nova': case 'new': return nova();
|
|
3051
3147
|
case 'run': return runCmd(POS.slice(1));
|
|
@@ -3102,6 +3198,9 @@ function recallCmd(args) {
|
|
|
3102
3198
|
if (/^\s*(?:(?:quero|preciso)\s+(?:que\s+)?(?:voc[eê]\s+)?|(?:por favor[,\s]+)?)?(?:gere|gerar|crie|criar|fa[çc]a|produza|desenhe|ilustre|edite)\s+(?:uma|a|minha)?\s*(?:imagem|foto|ilustra[çc][aã]o|arte|banner|capa|mockup)\b/i.test(text)) {
|
|
3103
3199
|
return imageCmd([text]);
|
|
3104
3200
|
}
|
|
3201
|
+
if (/^\s*(?:(?:quero|preciso)\s+(?:que\s+)?(?:voc[eê]\s+)?|(?:por favor[,\s]+)?)?(?:gere|gerar|crie|criar|fa[çc]a|produza)\s+(?:um|o|meu)?\s*(?:vídeo|video|clipe)\b/i.test(text)) {
|
|
3202
|
+
return generateVideoCmd([text]);
|
|
3203
|
+
}
|
|
3105
3204
|
const r = await router.route(text, cfg.token);
|
|
3106
3205
|
if (r.dest === 'agente') { console.log(' ' + C.cyan('⚙') + ' ' + C.dim(T.route_agent)); return agentCmd([text]); }
|
|
3107
3206
|
if (r.dest === 'run') { console.log(' ' + C.indigo('◆') + ' ' + C.dim(T.route_run)); return runCmd([text]); }
|
package/lib/agent.js
CHANGED
|
@@ -115,7 +115,7 @@ REGRAS:
|
|
|
115
115
|
- NÃO DESISTA sem TENTAR: é PROIBIDO responder "não tenho acesso" / "não consigo" / "preciso que você me passe X" enquanto houver uma ferramenta que você ainda não usou pra tentar. Antes de declarar que algo é impossível, AJA: conecte (conectar_vps), procure (buscar_arquivos, ou grep/find via executar_comando/executar_remoto), leia (ler_arquivo). Ex.: pediram pra ler um código-fonte que "você não tem"? Se há uma VPS/pasta onde ele pode estar, CONECTE e procure (grep -rn "<símbolo>" <dir>) ANTES de dizer que não tem. Só afirme que não conseguiu DEPOIS de ter tentado de fato e mostre o erro/saída REAL que te barrou.
|
|
116
116
|
- NUNCA pergunte "onde está o arquivo X?" / "há um diretório com Y?" / "posso gerar Z?" sem ANTES ter PROCURADO de fato: rode find/buscar_arquivos por ele em TODOS os lugares plausíveis — na VPS conectada (find / -name "arquivo" 2>/dev/null, e nas pastas do projeto) E na máquina local do usuário (buscar_arquivos, incluindo o pack/instalação de origem que ele citou). Se um config/arquivo obrigatório faltar mesmo depois de procurar, tente ACHAR um exemplo/modelo (outro .conf parecido, o default no código-fonte) e GERAR a partir dele — só peça ajuda ao usuário como ÚLTIMO recurso, dizendo exatamente onde já procurou e não achou.
|
|
117
117
|
- BINÁRIO NATIVO: ao subir/instalar um executável numa máquina, valide com file (arquitetura: 32 vs 64-bit) E ldd (as bibliotecas resolvem?) ANTES de declarar "pronto/instalado" — "está no lugar" NÃO é "roda". Se ldd mostrar "not found", instale a lib faltante e revalide.
|
|
118
|
-
- WINDOWS / SHELL
|
|
118
|
+
- WINDOWS / SHELL CMD: executar_comando usa cmd.exe. (a) use "dir", "mkdir pasta", "type"; NUNCA "mkdir -p", "ls", "rm" ou caminhos "/c/..."; (b) para PowerShell, chame "powershell -NoProfile -Command ..."; (c) "node -e"/"python -c" multilinha falha — escreva .mjs/.py; (d) para esperar use "timeout /t N /nobreak"; (e) mate servidor pelo PID da PORTA (netstat/findstr e taskkill /PID), nunca por imagem.
|
|
119
119
|
- PROVE O CAMINHO REAL antes de declarar "pronto/corrigido": "testes de API passando" NÃO é "o app funciona". Se há UI ou rotas condicionais (por papel/role, por filtro), rode o FLUXO EXATO do usuário afetado — a tela/rota que quebrava — e veja o resultado; numa entrega WEB, verifique com olhos (navegador). Em fix de código com ramos (SQL com subqueries/parâmetros, condicional por role): conte placeholder-a-placeholder e rode de novo o caminho que falhava. Só diga "corrigido" COM a prova verde do caminho real — e NÃO peça pro usuário rodar a prova que você mesmo consegue rodar. Se subir um server de teste, ENCERRE-o (por PID) ao terminar — não deixe processo órfão na porta.
|
|
120
120
|
- DEPLOY EM CONTAINER: edite sempre a FONTE (o diretório do projeto no host, ex /opt/projects/<app>, ou o código local), NUNCA dentro do container (/app/... é efêmero) — o "docker build" monta a imagem a partir da FONTE, então um fix feito dentro do container SOME no rebuild e você fica achando que corrigiu. Do mesmo jeito: NUNCA inclua arquivos de segredo (.env) no pacote/tar de deploy — o .env de exemplo do repo sobrescreve o .env REAL do servidor e a app volta pro modo stub. Ao recriar um container, repasse rede, portas, volumes E o --env-file que ele já tinha.
|
|
121
121
|
- SEU PRÓPRIO TESTE PODE ESTAR ERRADO: antes de confiar num veredito "FALHOU", confira a ASSERÇÃO (HTTP 200/201 é SUCESSO, não falha; 401/403 numa rota protegida sem token é o comportamento CORRETO). E não re-leia/re-escreva o MESMO arquivo várias vezes: se você já leu, use o que leu.
|
|
@@ -134,7 +134,7 @@ RULES:
|
|
|
134
134
|
- Prefer READ commands to diagnose before changing anything.
|
|
135
135
|
- Destructive commands go through user approval; if denied, explain and offer a safe alternative.
|
|
136
136
|
- NEVER expose or ask for secrets (.env, keys, passwords, tokens); never send data off this machine.
|
|
137
|
-
- Windows:
|
|
137
|
+
- Windows: executar_comando runs cmd.exe. Use native commands (dir, mkdir folder, type) or call PowerShell explicitly. NEVER use mkdir -p, ls, rm, or /c/... paths. Put multiline Python in a .py file created with escrever_arquivo.
|
|
138
138
|
- If a dependency is missing, install it (winget/apt/pip/npm) and continue.
|
|
139
139
|
- LINUX/apt: NEVER run a bare "apt-get install" — on a freshly-created machine unattended-upgrades holds the apt lock and the install HANGS silently (no output). ALWAYS install like this: "sudo DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=600 install -y <packages>" (the -o Lock::Timeout WAITS for the lock up to 10min instead of hanging; noninteractive+-y avoids prompts that block without a TTY). Use "add-apt-repository -y" + "apt-get update" for PPAs. A command that doesn't return in ~1min is likely stuck on the lock/prompt — don't wait forever.
|
|
140
140
|
- STARTING A DAEMON/SERVICE in the background on a VPS (via executar_remoto): NEVER run the binary directly or use just "nohup cmd &" — the SSH channel WAITS on the process (a service never exits) and TIMES OUT; then you think it hung and RETRY (loop). ALWAYS use this pattern, which detaches the process from SSH and RETURNS immediately: "setsid nohup ./daemon args >log 2>&1 </dev/null & echo started" (setsid + redirecting ALL fds, INCLUDING stdin with </dev/null, is what frees the channel). Then confirm it's up with pgrep/ss in a SEPARATE call. If the start still times out, the daemon is likely failing at init (run it in the FOREGROUND with a short timeout to see the real error and fix it) — do NOT repeat the same start.
|
|
@@ -145,7 +145,7 @@ RULES:
|
|
|
145
145
|
- DON'T GIVE UP without TRYING: it is FORBIDDEN to answer "I don't have access" / "I can't" / "I need you to give me X" while there's a tool you haven't used yet to try. Before declaring something impossible, ACT: connect (conectar_vps), search (buscar_arquivos, or grep/find via executar_comando/executar_remoto), read (ler_arquivo). E.g. asked to read source code you "don't have"? If there's a VPS/folder where it might live, CONNECT and search (grep -rn "<symbol>" <dir>) BEFORE saying you don't have it. Only claim you couldn't do it AFTER actually trying, and show the REAL error/output that blocked you.
|
|
146
146
|
- NEVER ask "where is file X?" / "is there a folder with Y?" / "may I generate Z?" without having actually SEARCHED first: run find/buscar_arquivos for it in EVERY plausible place — on the connected VPS (find / -name "file" 2>/dev/null, and the project folders) AND on the user's local machine (buscar_arquivos, including the source pack/install they mentioned). If a required config/file is still missing after searching, try to FIND a template/example (a similar .conf, the default in the source) and GENERATE from it — only ask the user as a LAST resort, stating exactly where you already looked and didn't find it.
|
|
147
147
|
- NATIVE BINARY: when uploading/installing an executable on a machine, validate with file (architecture: 32 vs 64-bit) AND ldd (do the libraries resolve?) BEFORE declaring "done/installed" — "it's in place" is NOT "it runs". If ldd shows "not found", install the missing lib and re-validate.
|
|
148
|
-
- WINDOWS /
|
|
148
|
+
- WINDOWS / CMD SHELL: executar_comando runs cmd.exe. (a) use dir, mkdir folder, type; NEVER mkdir -p, ls, rm, or /c/... paths; (b) call PowerShell explicitly when needed; (c) put multiline node/python code in .mjs/.py files; (d) wait with timeout /t N /nobreak; (e) kill a server by its PORT PID, never by image name.
|
|
149
149
|
- PROVE THE REAL PATH before declaring "done/fixed": "API tests passing" is NOT "the app works". If there's UI or conditional routes (by role, by filter), run the EXACT flow of the affected user — the screen/route that was breaking — and see the result; on a WEB deliverable, verify with eyes (browser). In a code fix with branches (SQL with subqueries/params, role conditionals): count placeholder-by-placeholder and re-run the path that was failing. Only say "fixed" WITH green proof of the real path — and do NOT ask the user to run a proof you can run yourself. If you start a test server, SHUT IT DOWN (by PID) when done — don't leave an orphan process on the port.
|
|
150
150
|
- CONTAINER DEPLOY: always edit the SOURCE (the project dir on the host, e.g. /opt/projects/<app>, or the local code), NEVER inside the container (/app/... is ephemeral) — "docker build" builds the image FROM THE SOURCE, so a fix made inside the container VANISHES on rebuild while you think it's fixed. Likewise: NEVER include secret files (.env) in the deploy tar/package — the repo's sample .env overwrites the REAL server .env and the app falls back to stub mode. When recreating a container, re-pass its network, ports, volumes AND the --env-file it had.
|
|
151
151
|
- YOUR OWN TEST MAY BE WRONG: before trusting a "FAILED" verdict, check the ASSERTION (HTTP 200/201 is SUCCESS, not failure; 401/403 on a protected route without a token is the CORRECT behavior). And don't re-read/re-write the SAME file repeatedly: if you already read it, use what you read.
|
|
@@ -162,7 +162,7 @@ RULES:
|
|
|
162
162
|
// fechamento garantido pra o modelo não tentar chamar ferramenta de novo).
|
|
163
163
|
// Ferramentas SÓ-LEITURA: usadas no modo Ask (--ler), no Plan (--plano) e no sub-agente
|
|
164
164
|
// de exploração (nunca escrevem/rodam comando destrutivo → seguras por construção).
|
|
165
|
-
const READONLY = new Set(['ler_arquivo', 'ler_documento', 'ler_apresentacao', 'listar_diretorio', 'buscar_arquivos', 'buscar_codigo', 'mapa_projeto', 'info_sistema', 'buscar_web', 'buscar_skill', 'android_dispositivos', 'android_logs', 'status_microsoft365', 'listar_emails_outlook', 'ler_email_outlook', 'listar_pastas_outlook', 'obter_anexo_outlook', 'status_google_workspace', 'listar_emails_gmail', 'ler_email_gmail', 'listar_pastas_gmail', 'obter_anexo_gmail', 'listar_arquivos_drive', 'ler_google_docs', 'ler_google_sheets']);
|
|
165
|
+
const READONLY = new Set(['ler_arquivo', 'ler_documento', 'ler_apresentacao', 'listar_diretorio', 'buscar_arquivos', 'buscar_codigo', 'mapa_projeto', 'info_sistema', 'buscar_web', 'buscar_skill', 'android_dispositivos', 'android_logs', 'status_microsoft365', 'listar_emails_outlook', 'ler_email_outlook', 'listar_pastas_outlook', 'obter_anexo_outlook', 'status_google_workspace', 'listar_emails_gmail', 'ler_email_gmail', 'listar_pastas_gmail', 'obter_anexo_gmail', 'listar_arquivos_drive', 'selecionar_arquivos_drive', 'ler_google_docs', 'ler_google_sheets']);
|
|
166
166
|
|
|
167
167
|
function scopeToolDefs(defs, allowedTools) {
|
|
168
168
|
if (!Array.isArray(allowedTools)) return defs;
|
|
@@ -170,8 +170,8 @@ function scopeToolDefs(defs, allowedTools) {
|
|
|
170
170
|
return defs.filter(def => allow.has(def && def.function && def.function.name));
|
|
171
171
|
}
|
|
172
172
|
const DEVICE_MUTATING = new Set(['android_parear', 'android_conectar', 'android_instalar', 'android_iniciar', 'android_capturar_tela']);
|
|
173
|
-
const CLOUD_MUTATING = new Set(['conectar_microsoft365', 'alterar_email_outlook', 'criar_resposta_outlook', 'criar_encaminhamento_outlook', 'criar_rascunho_outlook', 'enviar_rascunho_outlook', 'alterar_email_gmail', 'criar_resposta_gmail', 'criar_encaminhamento_gmail', 'criar_rascunho_gmail', 'enviar_rascunho_gmail', 'editar_google_docs', 'editar_google_sheets']);
|
|
174
|
-
async function llm({ baseUrl, key, messages, model, signalMs = 180000, noTools = false, toolsOverride = null, onRetry = null }) {
|
|
173
|
+
const CLOUD_MUTATING = new Set(['conectar_microsoft365', 'alterar_email_outlook', 'criar_resposta_outlook', 'criar_encaminhamento_outlook', 'criar_rascunho_outlook', 'enviar_rascunho_outlook', 'alterar_email_gmail', 'criar_resposta_gmail', 'criar_encaminhamento_gmail', 'criar_rascunho_gmail', 'enviar_rascunho_gmail', 'criar_google_docs', 'editar_google_docs', 'criar_google_sheets', 'editar_google_sheets']);
|
|
174
|
+
async function llm({ baseUrl, key, messages, model, signalMs = 180000, noTools = false, toolsOverride = null, onRetry = null, creditBudget = null }) {
|
|
175
175
|
// RESILIÊNCIA: o gateway CDC pode reiniciar/oscilar no meio de uma missão longa.
|
|
176
176
|
// withRetry cobre conn/timeout/5xx (backoff+jitter); NUNCA re-tenta no_credits/auth.
|
|
177
177
|
return withRetry(async () => {
|
|
@@ -181,7 +181,13 @@ async function llm({ baseUrl, key, messages, model, signalMs = 180000, noTools =
|
|
|
181
181
|
try {
|
|
182
182
|
res = await fetch(baseUrl.replace(/\/+$/, '') + '/chat/completions', {
|
|
183
183
|
method: 'POST', signal: ctrl.signal,
|
|
184
|
-
headers: {
|
|
184
|
+
headers: {
|
|
185
|
+
'Content-Type': 'application/json',
|
|
186
|
+
'Authorization': 'Bearer ' + key,
|
|
187
|
+
...(Number.isFinite(Number(creditBudget)) && Number(creditBudget) > 0
|
|
188
|
+
? { 'X-TS-Credit-Budget': String(Math.floor(Number(creditBudget))) }
|
|
189
|
+
: {}),
|
|
190
|
+
},
|
|
185
191
|
body: JSON.stringify({ model: model || DEFAULT_EXECUTOR, messages, ...(noTools ? {} : { tools: toolsOverride || tools.DEFS, tool_choice: 'auto' }), stream: false }),
|
|
186
192
|
});
|
|
187
193
|
} catch (_) { clearTimeout(timer); throw new ApiError('conn', { code: 'conn' }); }
|
|
@@ -192,7 +198,13 @@ async function llm({ baseUrl, key, messages, model, signalMs = 180000, noTools =
|
|
|
192
198
|
// marca code:'no_credits' pra virar CTA de upgrade limpo, nunca "HTTP 402" cru.
|
|
193
199
|
const _em = (j && (j.error?.message || j.error || j.message)) || ('HTTP ' + res.status);
|
|
194
200
|
const _cap = res.status === 402 || (res.status === 429 && /cost_cap|tenant_cost|teto|insufficient|quota|no_credits/i.test(JSON.stringify((j && j.error) || j || '')));
|
|
195
|
-
|
|
201
|
+
const _providerCode = j && ((j.error && j.error.code) || j.code);
|
|
202
|
+
const err = new ApiError(_em, { status: res.status, code: _providerCode === 'mission_credit_budget' ? 'mission_budget' : (_providerCode || (_cap ? 'no_credits' : '')) });
|
|
203
|
+
if (_providerCode === 'mission_credit_budget') {
|
|
204
|
+
err.creditsRequired = Number(j?.error?.creditsRequired) || 0;
|
|
205
|
+
err.creditsAvailable = Number(j?.error?.creditsAvailable) || 0;
|
|
206
|
+
}
|
|
207
|
+
throw err;
|
|
196
208
|
}
|
|
197
209
|
const ch = (j.choices && j.choices[0]) || {};
|
|
198
210
|
return { msg: ch.message || { content: '' }, usage: j.usage || {}, model: j.model || 'smart', billing: j.ts_billing || null };
|
|
@@ -225,7 +237,11 @@ function isCycle(sigs) {
|
|
|
225
237
|
// Veredito do watchdog (PURO, testável): 'break' | 'warn' | 'ok'.
|
|
226
238
|
// break = 2º strike (repetiu demais OU já avisado e ainda em ciclo) → encerra o loop.
|
|
227
239
|
// warn = 1º strike (bateu o limite de repetição OU 1º ciclo) e ainda não avisou ESTA sig.
|
|
228
|
-
function loopDecision({ nSig, cycling, warnedThis, loopWarned, WARN, BREAK }) {
|
|
240
|
+
function loopDecision({ nSig, cycling, warnedThis, loopWarned, WARN, BREAK, cacheHit = false }) {
|
|
241
|
+
// Uma leitura idêntica já atendida pelo cache não é ausência de progresso:
|
|
242
|
+
// ela não consulta a fonte, não cria efeito colateral e deve voltar ao modelo
|
|
243
|
+
// como referência curta. O watchdog continua valendo para execuções reais.
|
|
244
|
+
if (cacheHit) return 'cache';
|
|
229
245
|
if (nSig >= BREAK || (cycling && loopWarned)) return 'break';
|
|
230
246
|
if ((nSig >= WARN || cycling) && !warnedThis) return 'warn';
|
|
231
247
|
return 'ok';
|
|
@@ -607,7 +623,7 @@ async function run(task, opts = {}) {
|
|
|
607
623
|
const sys = lang !== 'en'
|
|
608
624
|
? 'Resuma a conversa de agente abaixo em UM bloco curto e denso (máx ~300 palavras): objetivo, o que já foi feito (arquivos/comandos e resultados), decisões tomadas e o que falta. Preserve caminhos de arquivos e fatos técnicos EXATOS. Sem preâmbulo.'
|
|
609
625
|
: 'Summarize the agent conversation below into ONE short dense block (max ~300 words): goal, what was done (files/commands and results), decisions, and what remains. Preserve EXACT file paths and technical facts. No preamble.';
|
|
610
|
-
const r = await llm({ baseUrl: k.baseUrl, key: k.key, model: selectedModel, noTools: true, signalMs: 60000, messages: [{ role: 'system', content: sys }, { role: 'user', content: lines }] });
|
|
626
|
+
const r = await llm({ baseUrl: k.baseUrl, key: k.key, model: selectedModel, noTools: true, signalMs: 60000, creditBudget: Math.max(1, maxCredits - charged - _visionCredits), messages: [{ role: 'system', content: sys }, { role: 'user', content: lines }] });
|
|
611
627
|
const u = r.usage || {};
|
|
612
628
|
acc.inTok += u.prompt_tokens || 0; acc.outTok += u.completion_tokens || 0;
|
|
613
629
|
charged += (r.billing && r.billing.charged) || 0;
|
|
@@ -648,14 +664,19 @@ async function run(task, opts = {}) {
|
|
|
648
664
|
// gateway oscilou → mostra "reconectando" em vez de morrer calado (confiabilidade visível)
|
|
649
665
|
const _onGwRetry = (e) => onStep({ name: 'gateway', detail: (lang !== 'en' ? 'reconectando ' : 'reconnecting ') + e.attempt + '/' + e.tries + ' (' + e.reason + ')', retry: true });
|
|
650
666
|
try {
|
|
651
|
-
r = await llm({ baseUrl: k.baseUrl, key: k.key, messages, model: selectedModel, toolsOverride: mainTools, onRetry: _onGwRetry });
|
|
667
|
+
r = await llm({ baseUrl: k.baseUrl, key: k.key, messages, model: selectedModel, toolsOverride: mainTools, onRetry: _onGwRetry, creditBudget: Math.max(1, maxCredits - charged - _visionCredits) });
|
|
652
668
|
} catch (e) {
|
|
669
|
+
if (e instanceof ApiError && e.code === 'mission_budget') {
|
|
670
|
+
guardStopped = { kind: 'credits', limit: maxCredits, spent: charged + _visionCredits,
|
|
671
|
+
required: e.creditsRequired || 0, available: e.creditsAvailable || maxCredits };
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
653
674
|
// Estourou a janela mesmo assim (turno gigante)? Compacta FORÇADO e tenta 1x —
|
|
654
675
|
// o erro de contexto nunca chega cru ao usuário se der pra recuperar.
|
|
655
676
|
const ctxErr = e instanceof ApiError && e.status === 400 && /context|length|token|maximum|too (long|large)/i.test(String(e.message || ''));
|
|
656
677
|
if (!ctxErr) throw e;
|
|
657
678
|
await _compactIfNeeded(true);
|
|
658
|
-
r = await llm({ baseUrl: k.baseUrl, key: k.key, messages, model: selectedModel, toolsOverride: mainTools });
|
|
679
|
+
r = await llm({ baseUrl: k.baseUrl, key: k.key, messages, model: selectedModel, toolsOverride: mainTools, creditBudget: Math.max(1, maxCredits - charged - _visionCredits) });
|
|
659
680
|
}
|
|
660
681
|
const u = r.usage || {};
|
|
661
682
|
acc.inTok += u.prompt_tokens || 0; acc.outTok += u.completion_tokens || 0;
|
|
@@ -695,13 +716,22 @@ async function run(task, opts = {}) {
|
|
|
695
716
|
result = { erro: 'MISSÃO INTERROMPIDA PELO LIMITE DE CUSTO/TEMPO. Esta ferramenta não foi executada.', guard: guardStopped };
|
|
696
717
|
}
|
|
697
718
|
|
|
698
|
-
//
|
|
719
|
+
// Consulta o cache antes do watchdog, mas só entrega o resultado depois dos gates
|
|
720
|
+
// de segurança. Assim uma leitura repetida não vira falso loop, sem permitir que
|
|
721
|
+
// cache contorne escopo estrito, modo somente leitura ou outras políticas.
|
|
722
|
+
const _cachedRead = READONLY.has(name) ? _missionCache.get(name, input) : null;
|
|
723
|
+
|
|
724
|
+
// ── WATCHDOG anti-loop: mede somente chamadas que realmente precisariam executar ──
|
|
699
725
|
const _sig = loopSig(name, input);
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
726
|
+
let _nSig = _callCounts.get(_sig) || 0;
|
|
727
|
+
let _cycling = false;
|
|
728
|
+
if (!_cachedRead) {
|
|
729
|
+
_recentSigs.push(_sig); if (_recentSigs.length > 8) _recentSigs.shift();
|
|
730
|
+
_nSig++; _callCounts.set(_sig, _nSig);
|
|
731
|
+
_cycling = isCycle(_recentSigs);
|
|
732
|
+
}
|
|
703
733
|
const _verdict = loopedOut ? 'ended'
|
|
704
|
-
: loopDecision({ nSig: _nSig, cycling: _cycling, warnedThis: _warnedSigs.has(_sig), loopWarned: _loopWarned, WARN: LOOP_WARN, BREAK: LOOP_BREAK });
|
|
734
|
+
: loopDecision({ nSig: _nSig, cycling: _cycling, warnedThis: _warnedSigs.has(_sig), loopWarned: _loopWarned, WARN: LOOP_WARN, BREAK: LOOP_BREAK, cacheHit: !!_cachedRead });
|
|
705
735
|
if (_verdict === 'ended') {
|
|
706
736
|
// um tc anterior deste lote já disparou o corte → os demais fecham sem executar
|
|
707
737
|
result = { erro: lang !== 'en' ? 'LOOP encerrado — não execute mais ferramentas; conclua.' : 'LOOP ended — do not run more tools; conclude.' };
|
|
@@ -985,7 +1015,7 @@ async function run(task, opts = {}) {
|
|
|
985
1015
|
steps++; _ran = true;
|
|
986
1016
|
}
|
|
987
1017
|
if (result === undefined && READONLY.has(name)) {
|
|
988
|
-
const _cached =
|
|
1018
|
+
const _cached = _cachedRead;
|
|
989
1019
|
if (_cached) {
|
|
990
1020
|
const _visible = messages.some(m => m && m.role === 'tool' && m.tool_call_id === _cached.toolCallId);
|
|
991
1021
|
_cachedContent = cacheReference(_cached, name, _visible);
|
|
@@ -1098,7 +1128,7 @@ async function run(task, opts = {}) {
|
|
|
1098
1128
|
? 'PARE de usar ferramentas. Você atingiu o limite de passos (ou não produziu uma resposta em texto). Escreva AGORA, em português, um fechamento CURTO e honesto pro usuário: o que você tentou, o que descobriu e o que ainda falta OU o que você precisa dele pra concluir (ex.: confirmar o caminho completo de um arquivo). Não invente resultado nem chame ferramenta.'
|
|
1099
1129
|
: 'STOP using tools. You hit the step limit (or produced no text answer). Write NOW a SHORT, honest closing for the user: what you tried, what you found, and what is still missing OR what you need from them to finish (e.g. confirm a file\'s full path). Do not invent results or call tools.';
|
|
1100
1130
|
try {
|
|
1101
|
-
const r = await llm({ baseUrl: k.baseUrl, key: k.key, messages: [...messages, { role: 'user', content: pedido }], model: selectedModel, noTools: true, signalMs: 60000 });
|
|
1131
|
+
const r = await llm({ baseUrl: k.baseUrl, key: k.key, messages: [...messages, { role: 'user', content: pedido }], model: selectedModel, noTools: true, signalMs: 60000, creditBudget: Math.max(1, maxCredits - charged - _visionCredits) });
|
|
1102
1132
|
const u = r.usage || {};
|
|
1103
1133
|
acc.inTok += u.prompt_tokens || 0; acc.outTok += u.completion_tokens || 0;
|
|
1104
1134
|
charged += (r.billing && r.billing.charged) || 0;
|
package/lib/api.js
CHANGED
|
@@ -9,7 +9,10 @@ class ApiError extends Error {
|
|
|
9
9
|
|
|
10
10
|
// ── RESILIÊNCIA a queda de gateway (backend + CDC de IA) ─────────────────────
|
|
11
11
|
// Status HTTP transitórios (vale re-tentar). 402/401/4xx-de-lógica NÃO entram.
|
|
12
|
-
function isRetryableStatus(s) {
|
|
12
|
+
function isRetryableStatus(s) {
|
|
13
|
+
return s === 408 || s === 425 || s === 429 || s === 500 || s === 502 || s === 503 || s === 504
|
|
14
|
+
|| (s >= 520 && s <= 527); // Cloudflare/origin transitórios, incluindo timeout 524
|
|
15
|
+
}
|
|
13
16
|
// Re-tenta fn com backoff exponencial + jitter. NUNCA re-tenta erros de lógica
|
|
14
17
|
// (sem crédito, auth, contexto, 4xx não-transitório). onRetry avisa a UI.
|
|
15
18
|
async function withRetry(fn, { tries = 3, baseMs = 500, onRetry = null, connOnly = false } = {}) {
|
|
@@ -26,7 +29,11 @@ async function withRetry(fn, { tries = 3, baseMs = 500, onRetry = null, connOnly
|
|
|
26
29
|
const fatal = code === 'no_credits' || code === 'auth' || code === 'context'
|
|
27
30
|
|| st === 401 || st === 402 || st === 403 || st === 404 || st === 409 || st === 422
|
|
28
31
|
|| (st >= 400 && st < 500 && !isRetryableStatus(st));
|
|
29
|
-
|
|
32
|
+
// A 524 already consumed the proxy's long origin timeout. One retry is
|
|
33
|
+
// enough; repeated 524s should return control so the model router can
|
|
34
|
+
// switch provider instead of freezing a mission for many minutes.
|
|
35
|
+
const slowGatewayTimeoutExhausted = st === 524 && attempt >= 2;
|
|
36
|
+
if (fatal || !retryable || attempt === tries || slowGatewayTimeoutExhausted) throw e;
|
|
30
37
|
const wait = Math.round(baseMs * Math.pow(2.5, attempt - 1) * (0.75 + Math.random() * 0.5));
|
|
31
38
|
if (onRetry) { try { onRetry({ attempt, tries, waitMs: wait, reason: code || ('http_' + st) }); } catch (_) {} }
|
|
32
39
|
await new Promise(r => setTimeout(r, wait));
|
package/lib/intelligence-core.js
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
const crypto = require('crypto');
|
|
10
10
|
|
|
11
11
|
const INTELLIGENCE_CONTRACT = 1;
|
|
12
|
-
|
|
12
|
+
// Keep this machine-readable. Human/provider notes belong in commit history;
|
|
13
|
+
// catalogFreshness appends an ISO time before parsing this value.
|
|
14
|
+
const PRICE_REVISION = '2026-08-11';
|
|
13
15
|
|
|
14
16
|
const MODEL_CATALOG = Object.freeze({
|
|
15
17
|
smart: {
|
|
@@ -28,7 +30,7 @@ const MODEL_CATALOG = Object.freeze({
|
|
|
28
30
|
},
|
|
29
31
|
'deepseek-v4-pro': {
|
|
30
32
|
upstreamId: 'deepseek/deepseek-v4-pro',
|
|
31
|
-
provider: 'deepseek', contextWindow: 1048576, price: { input: 0.
|
|
33
|
+
provider: 'deepseek', contextWindow: 1048576, price: { input: 0.63168, output: 1.26336, cachedInput: 0.063168 },
|
|
32
34
|
capabilities: ['tools', 'code', 'planning', 'review', 'long-context'], toolReliability: 0.95, quality: 0.93,
|
|
33
35
|
},
|
|
34
36
|
'gemini-2.5-flash-lite': {
|
|
@@ -63,7 +65,7 @@ const MODEL_CATALOG = Object.freeze({
|
|
|
63
65
|
},
|
|
64
66
|
'glm-5.2': {
|
|
65
67
|
upstreamId: 'z-ai/glm-5.2',
|
|
66
|
-
provider: 'zai', contextWindow:
|
|
68
|
+
provider: 'zai', contextWindow: 1048576, price: { input: 0.76, output: 2.42, cachedInput: 0.152 },
|
|
67
69
|
capabilities: ['tools', 'planning', 'code', 'long-context'], toolReliability: 0.88, quality: 0.89,
|
|
68
70
|
},
|
|
69
71
|
'mimo-v2.5': {
|
|
@@ -83,19 +85,24 @@ const MODEL_CATALOG = Object.freeze({
|
|
|
83
85
|
},
|
|
84
86
|
'gpt-5.6-terra': {
|
|
85
87
|
upstreamId: 'openai/gpt-5.6-terra',
|
|
86
|
-
provider: 'openai', contextWindow:
|
|
88
|
+
provider: 'openai', contextWindow: 1050000, price: { input: 1.00, output: 6.00, cachedInput: 0.10 },
|
|
87
89
|
capabilities: ['tools', 'code', 'planning', 'review', 'long-context'], toolReliability: 0.96, quality: 0.95,
|
|
88
90
|
},
|
|
89
91
|
'gpt-5.6-luna': {
|
|
90
92
|
upstreamId: 'openai/gpt-5.6-luna',
|
|
91
|
-
provider: 'openai', contextWindow:
|
|
92
|
-
capabilities: ['tools', 'code', 'planning', 'execution', 'summarization', 'review', 'long-context'], toolReliability: 0.93, quality: 0.89,
|
|
93
|
+
provider: 'openai', contextWindow: 1050000, price: { input: 0.10, output: 0.60, cachedInput: 0.01 },
|
|
94
|
+
capabilities: ['tools', 'code', 'planning', 'execution', 'summarization', 'review', 'vision', 'long-context'], toolReliability: 0.93, quality: 0.89,
|
|
93
95
|
},
|
|
94
96
|
'claude-sonnet-5': {
|
|
95
97
|
upstreamId: 'anthropic/claude-sonnet-5',
|
|
96
98
|
provider: 'anthropic', contextWindow: 1000000, price: { input: 2.00, output: 10.00, cachedInput: 0.20 },
|
|
97
99
|
capabilities: ['tools', 'code', 'planning', 'review', 'long-context'], toolReliability: 0.97, quality: 0.96,
|
|
98
100
|
},
|
|
101
|
+
'claude-opus-5': {
|
|
102
|
+
upstreamId: 'anthropic/claude-opus-5',
|
|
103
|
+
provider: 'anthropic', contextWindow: 1000000, price: { input: 5.00, output: 25.00, cachedInput: 0.50 },
|
|
104
|
+
capabilities: ['tools', 'vision', 'code', 'planning', 'review', 'long-context'], toolReliability: 0.98, quality: 0.99,
|
|
105
|
+
},
|
|
99
106
|
'claude-sonnet-4-6': {
|
|
100
107
|
upstreamId: 'anthropic/claude-sonnet-4.6',
|
|
101
108
|
provider: 'anthropic', contextWindow: 1000000, price: { input: 3.00, output: 15.00, cachedInput: 0.30 },
|
|
@@ -126,8 +133,33 @@ const MODEL_CATALOG = Object.freeze({
|
|
|
126
133
|
},
|
|
127
134
|
'laguna-s-2.1': {
|
|
128
135
|
upstreamId: 'poolside/laguna-s-2.1',
|
|
129
|
-
provider: 'openrouter', contextWindow: 1048576, price: { input: 0.
|
|
130
|
-
capabilities: ['code', 'review', 'long-context'], toolReliability: 0.
|
|
136
|
+
provider: 'openrouter', contextWindow: 1048576, price: { input: 0.09, output: 0.18, cachedInput: 0.009 },
|
|
137
|
+
capabilities: ['tools', 'code', 'review', 'long-context'], toolReliability: 0.80, quality: 0.78,
|
|
138
|
+
},
|
|
139
|
+
'qwen3.8-max': {
|
|
140
|
+
upstreamId: 'qwen/qwen3.8-max',
|
|
141
|
+
provider: 'openrouter', contextWindow: 1000000, price: { input: 2.00, output: 6.00, cachedInput: 0.20 },
|
|
142
|
+
capabilities: ['tools', 'vision', 'code', 'planning', 'review', 'long-context'], toolReliability: 0.91, quality: 0.93,
|
|
143
|
+
},
|
|
144
|
+
'gemini-3.6-flash': {
|
|
145
|
+
upstreamId: 'google/gemini-3.6-flash',
|
|
146
|
+
provider: 'openrouter', contextWindow: 1048576, price: { input: 1.50, output: 7.50, cachedInput: 0.15 },
|
|
147
|
+
capabilities: ['tools', 'vision', 'classification', 'planning', 'review', 'long-context'], toolReliability: 0.92, quality: 0.92,
|
|
148
|
+
},
|
|
149
|
+
'muse-spark-1.2': {
|
|
150
|
+
upstreamId: 'meta/muse-spark-1.2',
|
|
151
|
+
provider: 'meta', contextWindow: 1048576, price: { input: 1.25, output: 4.25, cachedInput: 0.125 },
|
|
152
|
+
capabilities: ['tools', 'vision', 'code', 'planning', 'review', 'long-context'], toolReliability: 0.95, quality: 0.95,
|
|
153
|
+
},
|
|
154
|
+
'muse-glimmer-30b': {
|
|
155
|
+
upstreamId: 'meta/muse-glimmer-30b',
|
|
156
|
+
provider: 'openrouter', contextWindow: 131072, price: { input: 0.35, output: 1.50, cachedInput: 0.035 },
|
|
157
|
+
capabilities: ['tools', 'vision', 'planning', 'review'], toolReliability: 0.88, quality: 0.86,
|
|
158
|
+
},
|
|
159
|
+
'solar-pro4': {
|
|
160
|
+
upstreamId: 'upstage/solar-pro4',
|
|
161
|
+
provider: 'openrouter', contextWindow: 524288, price: { input: 0.03, output: 0.12, cachedInput: 0.003 },
|
|
162
|
+
capabilities: ['tools', 'code', 'review', 'long-context'], toolReliability: 0.82, quality: 0.78,
|
|
131
163
|
},
|
|
132
164
|
'gpt-oss-120b': {
|
|
133
165
|
upstreamId: 'openai/gpt-oss-120b',
|
|
@@ -169,7 +201,7 @@ const AGENT_ROLES = Object.freeze({
|
|
|
169
201
|
}),
|
|
170
202
|
inspector: Object.freeze({
|
|
171
203
|
prompt: 'Inspecione em modo somente leitura e compare evidências com os critérios de aceite. Não altere nada e não elogie por cortesia.',
|
|
172
|
-
models: Object.freeze({ free: ['gemini-2.5-flash-lite'], basic: ['gemini-2.5-flash', 'gpt-4o-mini'], pro: ['claude-haiku-4-5', 'gemini-2.5-flash'
|
|
204
|
+
models: Object.freeze({ free: ['gemini-2.5-flash-lite'], basic: ['gemini-2.5-flash', 'gpt-4o-mini'], pro: ['gpt-5.6-luna', 'claude-haiku-4-5', 'gemini-2.5-flash'] }),
|
|
173
205
|
}),
|
|
174
206
|
corrector: Object.freeze({
|
|
175
207
|
prompt: 'Corrija apenas falhas confirmadas pelo inspetor, preserve o que funciona e repita as provas afetadas. Não amplie o escopo.',
|
|
@@ -177,7 +209,7 @@ const AGENT_ROLES = Object.freeze({
|
|
|
177
209
|
}),
|
|
178
210
|
vision: Object.freeze({
|
|
179
211
|
prompt: 'Extraia fatos observáveis da imagem e separe leitura, inferência e incerteza. Não invente texto ilegível.',
|
|
180
|
-
models: Object.freeze({ free: ['smart'], basic: ['gemini-2.5-flash'], pro: ['gemini-2.5-flash', 'claude-haiku-4-5'] }),
|
|
212
|
+
models: Object.freeze({ free: ['smart'], basic: ['gemini-2.5-flash'], pro: ['gpt-5.6-luna', 'gemini-2.5-flash', 'claude-haiku-4-5'] }),
|
|
181
213
|
}),
|
|
182
214
|
document: Object.freeze({
|
|
183
215
|
prompt: 'Produza conteúdo estruturado e use o motor nativo do formato solicitado. Preserve editabilidade e valide o arquivo gerado.',
|
|
@@ -205,13 +237,18 @@ function clamp(n, min, max) {
|
|
|
205
237
|
|
|
206
238
|
function normalizeModelId(model) {
|
|
207
239
|
const raw = String(model || 'smart').trim();
|
|
208
|
-
|
|
240
|
+
// O prefixo e uma instrucao de transporte do CDC, nao parte da identidade/preco.
|
|
241
|
+
// Sem remove-lo, todo modelo OpenRouter caia no preco `_default`: Luna era
|
|
242
|
+
// sobrecobrado e Opus subcobrado, com risco direto de margem negativa.
|
|
243
|
+
const transportFree = raw.replace(/^openrouter:/i, '');
|
|
244
|
+
const lower = transportFree.toLowerCase();
|
|
209
245
|
if (MODEL_CATALOG[raw]) return raw;
|
|
246
|
+
if (MODEL_CATALOG[transportFree]) return transportFree;
|
|
210
247
|
if (MODEL_CATALOG[lower]) return lower;
|
|
211
248
|
if (MODEL_ALIASES[lower]) return MODEL_ALIASES[lower];
|
|
212
249
|
const upstream = Object.entries(MODEL_CATALOG)
|
|
213
250
|
.find(([, info]) => String(info.upstreamId || '').toLowerCase() === lower);
|
|
214
|
-
return upstream ? upstream[0] :
|
|
251
|
+
return upstream ? upstream[0] : transportFree;
|
|
215
252
|
}
|
|
216
253
|
|
|
217
254
|
function modelInfo(model, now) {
|
package/lib/meta.js
CHANGED
|
@@ -29,6 +29,96 @@ const MAX_VISUAL_FIXES = 5; // rodadas de polimento VISUAL (o olho crítico)
|
|
|
29
29
|
const MAX_VISUAL_FIXES_FLUTTER = 3; // Flutter: rebuild é MUITO mais pesado/lento → teto menor de polimento (economia)
|
|
30
30
|
const VISUAL_ESCALATE_AT = 2; // após N reprovações visuais, a MÃO do fix vira o modelo forte (grok) — o deepseek erra layout complexo
|
|
31
31
|
const MAX_BUILD_FIXES = 12; // rodadas extras de correção de build antes de desistir
|
|
32
|
+
const META_PROJECT_TOOLS = Object.freeze([
|
|
33
|
+
'executar_comando', 'ler_arquivo', 'escrever_arquivo', 'editar_arquivo',
|
|
34
|
+
'restaurar_arquivo', 'listar_diretorio', 'buscar_arquivos', 'buscar_codigo',
|
|
35
|
+
'mapa_projeto', 'preciso_de_voce',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
// Most meta rounds work inside one local project. Do not send schemas for
|
|
39
|
+
// Gmail, Outlook, Android, SSH and Drive unless the item needs integrations.
|
|
40
|
+
function toolsForMetaItem(item, goal) {
|
|
41
|
+
const text = `${item && item.desc || ''}\n${goal || ''}`.toLowerCase();
|
|
42
|
+
if (/\b(gmail|outlook|e-?mail|google\s+(drive|docs|sheets)|onedrive|microsoft\s*365|ssh|vps|servidor remoto|android|celular|telegram|whatsapp)\b/i.test(text)) return null;
|
|
43
|
+
return [...META_PROJECT_TOOLS];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Connect each checklist item to a concrete artifact. Planner criteria have
|
|
47
|
+
// priority; deterministic fallbacks cover later items that exceed the criteria cap.
|
|
48
|
+
function artifactForMetaItem(item, st) {
|
|
49
|
+
const list = Array.isArray(st && st.checklist) ? st.checklist : [];
|
|
50
|
+
const index = Math.max(0, list.indexOf(item));
|
|
51
|
+
const criterion = Array.isArray(st && st.criteria) ? st.criteria[index] : null;
|
|
52
|
+
if (criterion && criterion.type === 'file_exists' && criterion.path) return String(criterion.path);
|
|
53
|
+
const d = String(item && item.desc || '').toLowerCase();
|
|
54
|
+
if (/controller.*8 dire|8 dire.*controller/.test(d)) return 'src/player_controller.gd';
|
|
55
|
+
if (/combate b[aá]sico|sistema de combate/.test(d)) return 'src/combat_system.gd';
|
|
56
|
+
if (/interface|\bui\b|\bhud\b/.test(d)) return 'docs/UI_SPEC.md';
|
|
57
|
+
if (/80-100 monstros|lista.*monstros/.test(d)) return 'data/monsters.json';
|
|
58
|
+
if (/economia|zeny|vending/.test(d)) return 'docs/ECONOMY.md';
|
|
59
|
+
if (/prioriza|\bmvp\b.*alpha|roadmap/.test(d)) return 'docs/ROADMAP.md';
|
|
60
|
+
return `artifacts/${String(item && item.id || 'item').replace(/[^a-z0-9_-]/gi, '_')}.md`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeMetaArtifact(relativePath, raw) {
|
|
64
|
+
let text = String(raw || '').trim().replace(/^```(?:json|gdscript|markdown|md)?\s*/i, '').replace(/\s*```\s*$/i, '').trim();
|
|
65
|
+
if (/\.json$/i.test(relativePath)) {
|
|
66
|
+
const a = text.indexOf('['), o = text.indexOf('{');
|
|
67
|
+
const start = a >= 0 && (o < 0 || a < o) ? a : o;
|
|
68
|
+
const end = Math.max(text.lastIndexOf(']'), text.lastIndexOf('}'));
|
|
69
|
+
if (start < 0 || end < start) throw new Error('resposta sem JSON');
|
|
70
|
+
const parsed = JSON.parse(text.slice(start, end + 1));
|
|
71
|
+
text = JSON.stringify(parsed, null, 2) + '\n';
|
|
72
|
+
}
|
|
73
|
+
if (!text || text.length < 80) throw new Error('artefato vazio ou curto demais');
|
|
74
|
+
return text;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function persistMetaArtifact(dir, relativePath, raw) {
|
|
78
|
+
const root = path.resolve(dir);
|
|
79
|
+
const target = path.resolve(root, relativePath);
|
|
80
|
+
if (target !== root && !target.startsWith(root + path.sep)) throw new Error('artefato fora da pasta confinada');
|
|
81
|
+
const content = normalizeMetaArtifact(relativePath, raw);
|
|
82
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
83
|
+
fs.writeFileSync(target, content, 'utf8');
|
|
84
|
+
return { target, bytes: Buffer.byteLength(content) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function directArtifactPlan(item) {
|
|
88
|
+
const d = String(item && item.desc || '').toLowerCase();
|
|
89
|
+
const groups = (values, size) => { const out = []; for (let i = 0; i < values.length; i += size) out.push(values.slice(i, i + size)); return out; };
|
|
90
|
+
const uniqueIds = (data, key) => {
|
|
91
|
+
const rows = data && data[key];
|
|
92
|
+
if (!Array.isArray(rows) || rows.some(row => !row || row.id === undefined || row.id === null || String(row.id).trim() === '')) return false;
|
|
93
|
+
const ids = rows.map(row => String(row.id));
|
|
94
|
+
return new Set(ids).size === ids.length;
|
|
95
|
+
};
|
|
96
|
+
if (/classes|novice|[aá]rvores de skills/.test(d)) return {
|
|
97
|
+
key: 'classes', minimum: 20,
|
|
98
|
+
parts: groups(['Novice','Swordsman','Mage','Archer','Merchant','Thief','Acolyte','Knight','Crusader','Wizard','Sage','Hunter','Bard','Dancer','Blacksmith','Alchemist','Assassin','Rogue','Priest','Monk'], 4),
|
|
99
|
+
schema: '{"classes":[{"id":"string","name":"string","tier":0,"base_attributes":{"STR":1,"AGI":1,"VIT":1,"INT":1,"DEX":1,"LUK":1},"skills":[{"id":"string","name":"string","max_level":10,"sp_cost":1,"cooldown_seconds":0,"description":"string"}]}]}',
|
|
100
|
+
validate: data => uniqueIds(data, 'classes') && data.classes.every(c => Array.isArray(c.skills) && c.skills.length >= 8 && new Set(c.skills.map(s => String(s.id))).size === c.skills.length),
|
|
101
|
+
};
|
|
102
|
+
if (/lista completa de mapas|cidades.*masmorras/.test(d)) return {
|
|
103
|
+
key: 'maps', minimum: 18,
|
|
104
|
+
parts: groups(['Prontera','Geffen','Payon','Morroc','Alberta','Izlude','Prontera Field North','Prontera Field South','Geffen Field','Payon Forest','Sograt Desert','Prontera Culvert','Byalan Island','Undersea Tunnel','Payon Cave','Geffen Dungeon','Pyramid','Orc Dungeon','Glast Heim'], 4),
|
|
105
|
+
schema: '{"maps":[{"id":"string","name":"string","type":"city|field|dungeon","level_range":[1,10],"layout":"string","npcs":["string"],"monsters":["string"],"connections":["string"],"safe_areas":["string"],"events":["string"]}]}',
|
|
106
|
+
validate: data => uniqueIds(data, 'maps'),
|
|
107
|
+
};
|
|
108
|
+
if (/50\+ quests|pelo menos 50 quests/.test(d)) return {
|
|
109
|
+
key: 'quests', minimum: 50,
|
|
110
|
+
parts: Array.from({ length: 5 }, (_, i) => `quests ${i * 10 + 1} a ${i * 10 + 10}`),
|
|
111
|
+
schema: '{"quests":[{"id":"string","title":"string","type":"main|side|job|daily|weekly","objectives":["string"],"rewards":{"exp":1,"job_exp":1,"zeny":1,"items":["string"]},"dialogue":"string","prerequisites":["string"]}]}',
|
|
112
|
+
validate: data => uniqueIds(data, 'quests'),
|
|
113
|
+
};
|
|
114
|
+
if (/80-100 monstros|lista detalhada.*monstros/.test(d)) return {
|
|
115
|
+
key: 'monsters', minimum: 80,
|
|
116
|
+
parts: Array.from({ length: 10 }, (_, i) => `monstros ${i * 10 + 1} a ${i * 10 + 10}`),
|
|
117
|
+
schema: '{"monsters":[{"id":"string","name":"string","level":1,"hp":10,"element":"Neutral","size":"Small|Medium|Large","map":"string","drops":[{"item":"string","chance_percent":1}],"is_mvp":false}]}',
|
|
118
|
+
validate: data => uniqueIds(data, 'monsters') && data.monsters.every(m => Number(m.level) > 0 && Number(m.hp) > 0 && Array.isArray(m.drops)),
|
|
119
|
+
};
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
32
122
|
const WEB_CANVAS_MOVE_MIN = 0.02; // check de MOVIMENTO do canvas web (jogo/cena 3D): diferença MÍNIMA
|
|
33
123
|
// entre 2 quadros pra a cena contar como VIVA. Abaixo disso = parada/congelada (T-pose, mixer parado,
|
|
34
124
|
// loop sem avançar o tempo). SÓ se aplica a canvas de JOGO/3D — dashboard/gráfico pode ficar estático.
|
|
@@ -819,7 +909,7 @@ async function _llmJson({ token, system, user, maxTokens = 900 }) {
|
|
|
819
909
|
}
|
|
820
910
|
|
|
821
911
|
// Chamada de TEXTO com modelo arbitrário — usada pelo PENSADOR (escalonamento).
|
|
822
|
-
async function _llmText({ token, model, system, user, maxTokens = 1200 }) {
|
|
912
|
+
async function _llmText({ token, model, system, user, maxTokens = 1200, creditBudget = null }) {
|
|
823
913
|
if (!_key) _key = await keyring.resolve(token, { feature: 'cli_agent' });
|
|
824
914
|
const ctrl = new AbortController();
|
|
825
915
|
const timer = setTimeout(() => ctrl.abort(), 120000);
|
|
@@ -827,7 +917,8 @@ async function _llmText({ token, model, system, user, maxTokens = 1200 }) {
|
|
|
827
917
|
try {
|
|
828
918
|
res = await fetch(String(_key.baseUrl).replace(/\/+$/, '') + '/chat/completions', {
|
|
829
919
|
method: 'POST', signal: ctrl.signal,
|
|
830
|
-
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + _key.key
|
|
920
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + _key.key,
|
|
921
|
+
...(Number.isFinite(Number(creditBudget)) && Number(creditBudget) > 0 ? { 'X-TS-Credit-Budget': String(Math.floor(Number(creditBudget))) } : {}) },
|
|
831
922
|
body: JSON.stringify({ model: keyring.modeloPara(_key, model), stream: false, max_completion_tokens: maxTokens, messages: [{ role: 'system', content: system }, { role: 'user', content: user }] }),
|
|
832
923
|
});
|
|
833
924
|
} finally { clearTimeout(timer); }
|
|
@@ -836,7 +927,16 @@ async function _llmText({ token, model, system, user, maxTokens = 1200 }) {
|
|
|
836
927
|
const u = j?.usage || {};
|
|
837
928
|
let credits = (j?.ts_billing && j.ts_billing.charged) || 0;
|
|
838
929
|
if (u.prompt_tokens && !_key.billingAuthoritative) { try { const c = await api('/api/credit/charge', { method: 'POST', token, body: { model, inTok: u.prompt_tokens || 0, outTok: u.completion_tokens || 0 } }); credits = (c && c.charged) || 0; } catch (_) {} }
|
|
839
|
-
|
|
930
|
+
const text = String(j?.choices?.[0]?.message?.content || '').trim();
|
|
931
|
+
// Some providers may return HTTP 200 without `choices` during an internal
|
|
932
|
+
// failure. Empty output is not a valid brief and must never become success.
|
|
933
|
+
if (!text) {
|
|
934
|
+
const providerMessage = String(j?.error?.message || j?.message || '').trim();
|
|
935
|
+
throw new ApiError(`O modelo ${model || 'selecionado'} retornou uma resposta vazia${providerMessage ? ': ' + providerMessage : '.'}`, {
|
|
936
|
+
status: 502, code: 'empty_model_response',
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
return { text, credits };
|
|
840
940
|
}
|
|
841
941
|
|
|
842
942
|
// ESCALONAMENTO: o executor barato travou no mesmo erro. Chama o modelo CARO
|
|
@@ -1057,7 +1157,7 @@ async function run(goal, opts = {}) {
|
|
|
1057
1157
|
const feasModel = thinker || 'grok-4.5';
|
|
1058
1158
|
if (feasMode === true || (feasMode === 'auto' && looksComplex(goal))) {
|
|
1059
1159
|
onRound({ n: 0, item: (lang === 'en' ? `Checking feasibility with ${feasModel}…` : `Avaliando viabilidade com ${feasModel}…`), attempt: 1 });
|
|
1060
|
-
try { const fe = await feasibilityPhase(goal, feasModel, token); feasBrief = fe.brief; feasCred = fe.credits || 0; try { fs.writeFileSync(path.join(dir, 'VIABILIDADE.md'), feasBrief); } catch (_) {} onAlert({ type: 'design', text: `🔎 ts: viabilidade avaliada (${feasModel}, ${feasCred} créditos) — o que dá e o que não dá está fixado. Salvo em VIABILIDADE.md.` }); } catch (_e) { if (_e && _e.code === 'no_credits') throw _e; }
|
|
1160
|
+
try { const fe = await feasibilityPhase(goal, feasModel, token); feasBrief = fe.brief; feasCred = fe.credits || 0; try { fs.writeFileSync(path.join(dir, 'VIABILIDADE.md'), feasBrief); } catch (_) {} onAlert({ type: 'design', text: `🔎 ts: viabilidade avaliada (${feasModel}, ${feasCred} créditos) — o que dá e o que não dá está fixado. Salvo em VIABILIDADE.md.` }); } catch (_e) { if (_e && _e.code === 'no_credits') throw _e; onAlert({ type: 'phase_error', text: `⚠ ts: viabilidade não concluída: ${String(_e && _e.message || _e).slice(0, 220)}` }); }
|
|
1061
1161
|
}
|
|
1062
1162
|
// FASE DE DESIGN (nova missão visual): o designer forte projeta o design system
|
|
1063
1163
|
// ANTES do checklist e do código, pra o app sair bonito de fábrica.
|
|
@@ -1067,7 +1167,7 @@ async function run(goal, opts = {}) {
|
|
|
1067
1167
|
onRound({ n: 0, item: (lang === 'en' ? `Designing the visual with ${designer}…` : `Projetando o visual com ${designer}…`), attempt: 1 });
|
|
1068
1168
|
let mockupB64 = null;
|
|
1069
1169
|
if (opts.mockup) { try { mockupB64 = fs.readFileSync(opts.mockup).toString('base64'); onAlert({ type: 'design', text: `🖼 ts: mockup de referência carregado (${path.basename(opts.mockup)}) — o designer vai projetar a partir dele e o olho vai cobrar fidelidade.` }); } catch (_) { onAlert({ type: 'design', text: `⚠ ts: não consegui ler o mockup ${opts.mockup} — seguindo sem ele.` }); } }
|
|
1070
|
-
try { const d = await designPhase(goal, designer, token, { dir, mockupB64, kind: _projKind(goal, dir) }); designBrief = d.brief; designCred = d.credits || 0; try { fs.writeFileSync(path.join(dir, 'DESIGN.md'), designBrief); } catch (_) {} onAlert({ type: 'design', text: `🎨 ts: design system criado pelo ${designer} (${designCred} créditos) — o app vai seguir esse visual. Salvo em DESIGN.md.` }); } catch (_e) { if (_e && _e.code === 'no_credits') throw _e; }
|
|
1170
|
+
try { const d = await designPhase(goal, designer, token, { dir, mockupB64, kind: _projKind(goal, dir) }); designBrief = d.brief; designCred = d.credits || 0; try { fs.writeFileSync(path.join(dir, 'DESIGN.md'), designBrief); } catch (_) {} onAlert({ type: 'design', text: `🎨 ts: design system criado pelo ${designer} (${designCred} créditos) — o app vai seguir esse visual. Salvo em DESIGN.md.` }); } catch (_e) { if (_e && _e.code === 'no_credits') throw _e; onAlert({ type: 'phase_error', text: `⚠ ts: design não concluído: ${String(_e && _e.message || _e).slice(0, 220)}` }); }
|
|
1071
1171
|
}
|
|
1072
1172
|
// FASE DE ARQUITETURA (3º pilar): app complexo ganha um CONTRATO antes do checklist
|
|
1073
1173
|
let archBrief = null, archCred = 0;
|
|
@@ -1075,7 +1175,7 @@ async function run(goal, opts = {}) {
|
|
|
1075
1175
|
const archModel = thinker || 'grok-4.5';
|
|
1076
1176
|
if (archMode === true || (archMode === 'auto' && looksComplex(goal))) {
|
|
1077
1177
|
onRound({ n: 0, item: (lang === 'en' ? `Designing the architecture with ${archModel}…` : `Projetando a arquitetura com ${archModel}…`), attempt: 1 });
|
|
1078
|
-
try { const a = await archPhase(goal, archModel, token, { dir }); archBrief = a.brief; archCred = a.credits || 0; try { fs.writeFileSync(path.join(dir, 'ARQUITETURA.md'), archBrief); } catch (_) {} onAlert({ type: 'design', text: `📐 ts: contrato de arquitetura criado pelo ${archModel} (${archCred} créditos) — componentes, arquivos e assinaturas fixados. Salvo em ARQUITETURA.md.` }); } catch (_e) { if (_e && _e.code === 'no_credits') throw _e; }
|
|
1178
|
+
try { const a = await archPhase(goal, archModel, token, { dir }); archBrief = a.brief; archCred = a.credits || 0; try { fs.writeFileSync(path.join(dir, 'ARQUITETURA.md'), archBrief); } catch (_) {} onAlert({ type: 'design', text: `📐 ts: contrato de arquitetura criado pelo ${archModel} (${archCred} créditos) — componentes, arquivos e assinaturas fixados. Salvo em ARQUITETURA.md.` }); } catch (_e) { if (_e && _e.code === 'no_credits') throw _e; onAlert({ type: 'phase_error', text: `⚠ ts: arquitetura não concluída: ${String(_e && _e.message || _e).slice(0, 220)}` }); }
|
|
1079
1179
|
}
|
|
1080
1180
|
const mk = await makeChecklist(goal + (designBrief ? '\n\n[Há um DESIGN SYSTEM definido — o checklist deve refletir a aplicação desse visual]' : '') + (archBrief ? '\n\n[Há um CONTRATO DE ARQUITETURA definido — os itens devem seguir os componentes/arquivos dele]' : ''), token);
|
|
1081
1181
|
// CRITÉRIOS (TaskSpec): usuário (--criterios) manda; senão, com --provar, combina o que o
|
|
@@ -1121,6 +1221,16 @@ async function run(goal, opts = {}) {
|
|
|
1121
1221
|
// ABRE sem crashar E o "olho" aprova o layout. O portão REENTRA enquanto o visual não passa.
|
|
1122
1222
|
if (!pend.length) {
|
|
1123
1223
|
const b = detectBuild(dir);
|
|
1224
|
+
// A documentation/data/code-sample package has no build target by
|
|
1225
|
+
// design. Verify its executable file criteria and finish honestly;
|
|
1226
|
+
// absence of a build system is not a "build-fix ceiling" failure.
|
|
1227
|
+
if (!b) {
|
|
1228
|
+
const criteriaOk = await _checkCriteria(st, dir);
|
|
1229
|
+
st.verification = criteriaOk ? ((st.criteria || []).length ? 'verified' : 'no_gate') : 'unverified';
|
|
1230
|
+
st.status = 'done'; st.finished_at = new Date().toISOString();
|
|
1231
|
+
st.runNote = criteriaOk ? 'pacote sem alvo compilavel; criterios de artefato verificados' : 'criterios de artefato falharam';
|
|
1232
|
+
save(st, dir); onRoundDone({ checklist: st.checklist, spent: st.creditsSpent }); return st;
|
|
1233
|
+
}
|
|
1124
1234
|
const buildCapLeft = (st.buildFixes || 0) < MAX_BUILD_FIXES;
|
|
1125
1235
|
// teto de polimento visual: menor pra Flutter (rebuild caro/lento) que pra Android
|
|
1126
1236
|
const visualCap = (b && b.kind === 'flutter') ? MAX_VISUAL_FIXES_FLUTTER : MAX_VISUAL_FIXES;
|
|
@@ -1256,9 +1366,119 @@ async function run(goal, opts = {}) {
|
|
|
1256
1366
|
}
|
|
1257
1367
|
|
|
1258
1368
|
const item = pend[0];
|
|
1369
|
+
const expectedArtifact = artifactForMetaItem(item, st);
|
|
1370
|
+
// A process can be interrupted after a read-only round but before the
|
|
1371
|
+
// no-artifact branch persists `directArtifact`. Recover that evidence on
|
|
1372
|
+
// resume instead of repeating the same exploratory attempt.
|
|
1373
|
+
const expectedOnDisk = expectedArtifact
|
|
1374
|
+
? (path.isAbsolute(expectedArtifact) ? expectedArtifact : path.resolve(dir, expectedArtifact))
|
|
1375
|
+
: null;
|
|
1376
|
+
if (!item.directArtifact && (item.attempts || 0) > 0 && expectedOnDisk && !fs.existsSync(expectedOnDisk)) {
|
|
1377
|
+
item.directArtifact = true;
|
|
1378
|
+
item.directModel = item.directModel || 'glm-5.2';
|
|
1379
|
+
item.attempts = Math.max(0, item.attempts - 1);
|
|
1380
|
+
}
|
|
1259
1381
|
item.attempts = (item.attempts || 0) + 1;
|
|
1260
1382
|
onRound({ n: st.rounds.length + 1, item: item.desc, attempt: item.attempts });
|
|
1261
1383
|
|
|
1384
|
+
// Known large structured deliverables are cheaper and more reliable when
|
|
1385
|
+
// generated in validated batches from the start (classes/maps/quests/monsters).
|
|
1386
|
+
if (!item.directArtifact && directArtifactPlan(item)) {
|
|
1387
|
+
item.directArtifact = true;
|
|
1388
|
+
item.directModel = 'glm-5.2';
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// Some providers can generate a large document but refuse to place that
|
|
1392
|
+
// content inside tool-call arguments. After a proven zero-tool response,
|
|
1393
|
+
// generate content-only, validate it and let the confined harness persist it.
|
|
1394
|
+
if (item.directArtifact) {
|
|
1395
|
+
const directModel = item.directModel || 'glm-5.2';
|
|
1396
|
+
const plan = directArtifactPlan(item);
|
|
1397
|
+
let totalCredits = 0, finalText = '';
|
|
1398
|
+
if (plan) {
|
|
1399
|
+
const merged = [];
|
|
1400
|
+
item.directParts = item.directParts || {};
|
|
1401
|
+
for (let partIndex = 0; partIndex < plan.parts.length; partIndex++) {
|
|
1402
|
+
if (Array.isArray(item.directParts[partIndex])) {
|
|
1403
|
+
merged.push(...item.directParts[partIndex]);
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
const part = plan.parts[partIndex];
|
|
1407
|
+
let generated = null, parsedPart = null, lastDirectError = null;
|
|
1408
|
+
for (let directAttempt = 0; directAttempt < 2 && !parsedPart; directAttempt++) {
|
|
1409
|
+
try {
|
|
1410
|
+
const remainingBudget = Math.max(1, Math.floor(st.budget - st.creditsSpent));
|
|
1411
|
+
generated = await _llmText({
|
|
1412
|
+
token, model: directModel, maxTokens: 7500, creditBudget: remainingBudget,
|
|
1413
|
+
system: `Gere SOMENTE JSON estritamente valido no schema pedido. Sem introducao, sem markdown fence, sem comentarios, sem virgula sobrando e com todas as aspas/chaves fechadas. Entregue apenas a parte solicitada. SCHEMA: ${plan.schema}`,
|
|
1414
|
+
user: `OBJETIVO:\n${item.desc}\n\nPARTE ${partIndex + 1}/${plan.parts.length}: ${Array.isArray(part) ? part.join(', ') : part}. Inclua exatamente estas entidades e detalhe cada uma.`,
|
|
1415
|
+
});
|
|
1416
|
+
totalCredits += generated.credits || 0;
|
|
1417
|
+
st.creditsSpent += generated.credits || 0;
|
|
1418
|
+
save(st, dir); // custo existe mesmo se a saida abaixo for invalida
|
|
1419
|
+
const parsed = JSON.parse(normalizeMetaArtifact(expectedArtifact, generated.text));
|
|
1420
|
+
if (!Array.isArray(parsed[plan.key])) throw new Error(`parte ${partIndex + 1} sem array ${plan.key}`);
|
|
1421
|
+
parsedPart = parsed[plan.key];
|
|
1422
|
+
} catch (e) {
|
|
1423
|
+
lastDirectError = e;
|
|
1424
|
+
generated = null;
|
|
1425
|
+
if (directAttempt === 0) await new Promise(r => setTimeout(r, 1500));
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
if (!parsedPart) {
|
|
1429
|
+
item.attempts = Math.max(0, (item.attempts || 1) - 1);
|
|
1430
|
+
const transient = /abort|timeout|fetch|network/i.test(String(lastDirectError && lastDirectError.message || lastDirectError)) || Number(lastDirectError && lastDirectError.status) >= 500;
|
|
1431
|
+
st.status = 'paused'; st.pause_reason = transient ? 'connection' : 'invalid_artifact';
|
|
1432
|
+
st.budget = st.creditsSpent;
|
|
1433
|
+
save(st, dir);
|
|
1434
|
+
onAlert({ type: transient ? 'conn' : 'retry', text: `Geracao em lotes pausada na parte ${partIndex + 1}/${plan.parts.length}: ${String(lastDirectError && lastDirectError.message || lastDirectError).slice(0, 180)}. As partes ja validadas e os custos foram preservados.` });
|
|
1435
|
+
return st;
|
|
1436
|
+
}
|
|
1437
|
+
item.directParts[partIndex] = parsedPart;
|
|
1438
|
+
merged.push(...item.directParts[partIndex]);
|
|
1439
|
+
save(st, dir);
|
|
1440
|
+
}
|
|
1441
|
+
const data = { [plan.key]: merged };
|
|
1442
|
+
if (merged.length < plan.minimum) throw new Error(`${plan.key}: esperado minimo ${plan.minimum}, recebido ${merged.length}`);
|
|
1443
|
+
if (plan.validate && !plan.validate(data)) throw new Error(`${plan.key}: validacao semantica falhou`);
|
|
1444
|
+
finalText = JSON.stringify(data, null, 2);
|
|
1445
|
+
} else {
|
|
1446
|
+
let generated = null, lastDirectError = null;
|
|
1447
|
+
for (let directAttempt = 0; directAttempt < 2 && !generated; directAttempt++) {
|
|
1448
|
+
try {
|
|
1449
|
+
const remainingBudget = Math.max(1, Math.floor(st.budget - st.creditsSpent));
|
|
1450
|
+
generated = await _llmText({
|
|
1451
|
+
token, model: directModel, maxTokens: /\.json$/i.test(expectedArtifact) ? 16000 : 14000,
|
|
1452
|
+
creditBudget: remainingBudget,
|
|
1453
|
+
system: 'Gere o CONTEUDO COMPLETO do artefato solicitado. Responda SOMENTE com o conteudo final, sem introducao, sem promessas e sem markdown fence. Se for JSON, devolva JSON estritamente valido. Nao use ferramentas.',
|
|
1454
|
+
user: `OBJETIVO MAIOR:\n${st.goal.slice(0, 1800)}\n\nITEM ATUAL:\n${item.desc}\n\nCAMINHO/FORMATO OBRIGATORIO: ${expectedArtifact}`,
|
|
1455
|
+
});
|
|
1456
|
+
} catch (e) {
|
|
1457
|
+
lastDirectError = e;
|
|
1458
|
+
if (directAttempt === 0) await new Promise(r => setTimeout(r, 1500));
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
if (!generated) {
|
|
1462
|
+
item.attempts = Math.max(0, (item.attempts || 1) - 1);
|
|
1463
|
+
const transient = /abort|timeout|fetch|network/i.test(String(lastDirectError && lastDirectError.message || lastDirectError)) || Number(lastDirectError && lastDirectError.status) >= 500;
|
|
1464
|
+
st.status = 'paused'; st.pause_reason = transient ? 'connection' : 'invalid_artifact';
|
|
1465
|
+
st.budget = st.creditsSpent;
|
|
1466
|
+
save(st, dir);
|
|
1467
|
+
onAlert({ type: transient ? 'conn' : 'retry', text: `Geracao direta pausada: ${String(lastDirectError && lastDirectError.message || lastDirectError).slice(0, 180)}. O item continua pendente e sera retomado sem consumir tentativa.` });
|
|
1468
|
+
return st;
|
|
1469
|
+
}
|
|
1470
|
+
totalCredits += generated.credits || 0;
|
|
1471
|
+
finalText = generated.text;
|
|
1472
|
+
}
|
|
1473
|
+
if (!plan) st.creditsSpent += totalCredits;
|
|
1474
|
+
const persisted = persistMetaArtifact(dir, expectedArtifact, finalText);
|
|
1475
|
+
item.passes = true; item.blocked = false; item.directArtifact = false; delete item.directParts;
|
|
1476
|
+
st.rounds.push({ item: item.desc, id: item.id, result: `Artefato validado e persistido pelo harness: ${persisted.target} (${persisted.bytes} bytes)`, credits: totalCredits, steps: plan ? plan.parts.length : 1, at: new Date().toISOString() });
|
|
1477
|
+
save(st, dir);
|
|
1478
|
+
onRoundDone({ marks: [item.id], credits: totalCredits, checklist: st.checklist, spent: st.creditsSpent });
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1262
1482
|
// Rodada com CONTEXTO ZERADO: o agente recebe só o essencial (objetivo resumido +
|
|
1263
1483
|
// estado do checklist + resultado da rodada anterior) — nunca o histórico inteiro.
|
|
1264
1484
|
const last = st.rounds[st.rounds.length - 1];
|
|
@@ -1295,7 +1515,10 @@ async function run(goal, opts = {}) {
|
|
|
1295
1515
|
const minimalBlock = (lang === 'en'
|
|
1296
1516
|
? `\nMINIMAL-CODE RULES (economy — mandatory, think like a pragmatic lazy senior):\n1) Does this really need to exist? If not, don't build it.\n2) Does it already exist in this project? If yes, REUSE it — never rewrite a second version.\n3) Does the language/platform/browser already do it natively? If yes, use the native thing (e.g. <input type="date"> instead of a datepicker lib; fetch instead of axios; CSS instead of a JS animation lib).\n4) Only then write code — the MINIMUM that solves the item. NO extra library without real need, no gratuitous abstractions/wrappers, no speculative "future-proofing".\n`
|
|
1297
1517
|
: `\nREGRAS DE CÓDIGO MÍNIMO (economia — obrigatórias; pense como um sênior pragmático e "preguiçoso"):\n1) Isso precisa mesmo existir? Se não precisa, NÃO construa.\n2) Já existe neste projeto? Se sim, REUSE — nunca escreva uma segunda versão.\n3) A linguagem/plataforma/navegador já faz isso nativo? Se sim, use o nativo (ex: <input type="date"> em vez de lib de datepicker; fetch em vez de axios; CSS em vez de lib JS de animação).\n4) Só então escreva código — o MÍNIMO que resolve o item. SEM biblioteca extra sem necessidade real, sem abstração/wrapper gratuito, sem "preparar pro futuro" especulativo.\n`);
|
|
1298
|
-
const
|
|
1518
|
+
const artifactBlock = lang === 'en'
|
|
1519
|
+
? `\nMANDATORY PROOF ARTIFACT FOR THIS ITEM: ${expectedArtifact}\nYou MUST call escrever_arquivo or editar_arquivo and persist the deliverable at exactly this relative path. A text-only answer does NOT complete the item. Create parent directories through the file tool as needed.\n`
|
|
1520
|
+
: `\nARTEFATO DE PROVA OBRIGATORIO DESTE ITEM: ${expectedArtifact}\nVoce DEVE chamar escrever_arquivo ou editar_arquivo e persistir a entrega exatamente nesse caminho relativo. Resposta apenas em texto NAO conclui o item. Crie os diretorios-pai pela ferramenta de arquivo quando necessario.\n`;
|
|
1521
|
+
const task = feasBlock + archBlock + designBlock + seedBlock + rulesBlock + minimalBlock + artifactBlock + (lang === 'en'
|
|
1299
1522
|
? `Bigger goal (context — do NOT do everything now):\n${st.goal.slice(0, 800)}\n\nCHECKLIST (state):\n${fmtChecklist(st.checklist)}\n${mapBlock}`
|
|
1300
1523
|
: `Objetivo maior (contexto — NÃO faça tudo agora):\n${st.goal.slice(0, 800)}\n\nCHECKLIST (estado):\n${fmtChecklist(st.checklist)}\n${mapBlock}`)
|
|
1301
1524
|
+ (last ? `\n${lang === 'en' ? 'Previous round' : 'Rodada anterior'}: ${String(last.result || '').slice(0, 600)}\n` : '')
|
|
@@ -1324,13 +1547,34 @@ async function run(goal, opts = {}) {
|
|
|
1324
1547
|
// ser o modelo forte (o pensador/olho, grok-4.5) — que sabe compor layout. Volta ao
|
|
1325
1548
|
// barato assim que o olho aprovar (visual_fix some do checklist). Só nas rodadas visuais.
|
|
1326
1549
|
let roundModel = useModel;
|
|
1550
|
+
// If the automatic executor family was just proven unavailable, do not
|
|
1551
|
+
// spend three HTTP retries on it again for every checklist item. Keep the
|
|
1552
|
+
// cooldown mission-local and short so normal routing recovers naturally.
|
|
1553
|
+
if (!model && Number(st.autoExecutorDegradedUntil || 0) > Date.now()) {
|
|
1554
|
+
roundModel = 'glm-5.2';
|
|
1555
|
+
}
|
|
1556
|
+
// A model that answered with promises but executed zero tools is avoided
|
|
1557
|
+
// for this item on the next window. This is a capability failure, not a
|
|
1558
|
+
// reason to burn the item's second implementation attempt.
|
|
1559
|
+
if (!model && Array.isArray(item.avoidModels) && item.avoidModels.length) {
|
|
1560
|
+
const avoid = item.avoidModels.map(v => String(v).toLowerCase());
|
|
1561
|
+
const candidates = ['glm-5.2', 'claude-haiku-4-5', 'deepseek-v4-pro'];
|
|
1562
|
+
roundModel = candidates.find(c => !avoid.some(a => a.includes(c) || c.includes(a))) || roundModel;
|
|
1563
|
+
}
|
|
1327
1564
|
if (visualLadder && item.id === 'visual_fix' && (st.visualFixes || 0) >= VISUAL_ESCALATE_AT && (useThinker || eye)) {
|
|
1328
1565
|
roundModel = useThinker || eye;
|
|
1329
1566
|
onAlert({ type: 'escalate', text: `🎨 ts: o olho reprovou o layout ${st.visualFixes}x — passando a MÃO do fix visual pro ${roundModel} (mais forte em layout) até aprovar.` });
|
|
1330
1567
|
}
|
|
1331
1568
|
let out = null, connErr = null;
|
|
1332
1569
|
for (let att = 0; att < 3 && !out; att++) {
|
|
1333
|
-
try {
|
|
1570
|
+
try {
|
|
1571
|
+
// `agent.run` defaults to a defensive 40-credit cap. Complex meta rounds
|
|
1572
|
+
// can have a minimum context larger than that, so use the remaining
|
|
1573
|
+
// window budget while preserving the mission-wide ceiling.
|
|
1574
|
+
const roundCreditBudget = Math.max(1, Math.min(5000, Math.floor(st.budget - st.creditsSpent)));
|
|
1575
|
+
const roundAllowedTools = Array.isArray(opts.allowedTools) ? opts.allowedTools : toolsForMetaItem(item, st.goal);
|
|
1576
|
+
out = await agent.run(task, { token, lang, yes, autoAll, model: roundModel, maxCredits: roundCreditBudget, allowedTools: roundAllowedTools, confineDir: dir, skipSessionStart: true, onStep: opts.onStep, onStepDone: opts.onStepDone, askApprove: opts.askApprove, onRemote: opts.onRemote, onThinking: opts.onThinking });
|
|
1577
|
+
}
|
|
1334
1578
|
catch (e) {
|
|
1335
1579
|
connErr = e;
|
|
1336
1580
|
// teto de IA estourado → PAUSA limpa e resumível com CTA de upgrade (nunca segue em silêncio)
|
|
@@ -1339,13 +1583,63 @@ async function run(goal, opts = {}) {
|
|
|
1339
1583
|
onAlert({ type: 'no_credits', text: `💳 ts: seu limite de IA acabou — a missão foi PAUSADA e salva. Assine um plano em terminalsmart.com.br/planos e retome com "ts meta".` });
|
|
1340
1584
|
return st;
|
|
1341
1585
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1586
|
+
const transientStatus = e && Number(e.status);
|
|
1587
|
+
const transientProvider = e && ([408, 425, 429, 500, 502, 503, 504].includes(transientStatus) || (transientStatus >= 520 && transientStatus <= 527) || e.code === 'conn' || /fetch|network|temporariamente indispon[ií]vel/i.test(String(e.message)));
|
|
1588
|
+
if (transientProvider) {
|
|
1589
|
+
// Automatic Pro mode: once the selected executor exhausts its HTTP
|
|
1590
|
+
// retries, switch to another tool-capable model before pausing.
|
|
1591
|
+
// Change provider family first: if Flash is down, Pro from the same
|
|
1592
|
+
// family is likely affected too. GLM is the economical independent fallback.
|
|
1593
|
+
const fallbacks = ['glm-5.2', 'deepseek-v4-pro', 'claude-haiku-4-5'];
|
|
1594
|
+
if (!model && att === 0 && !roundModel) {
|
|
1595
|
+
st.autoExecutorDegradedUntil = Date.now() + (10 * 60 * 1000);
|
|
1596
|
+
save(st, dir);
|
|
1597
|
+
}
|
|
1598
|
+
if (!model && att < fallbacks.length) roundModel = fallbacks[att];
|
|
1599
|
+
onAlert({ type: 'retry', text: `📡 ts: provedor indisponível — tentando de novo (${att + 1}/3)${roundModel ? ` com ${roundModel}` : ''}…` });
|
|
1600
|
+
await new Promise(r => setTimeout(r, 4000 * (att + 1)));
|
|
1601
|
+
} else throw e; // erro não-transitório: sobe
|
|
1344
1602
|
}
|
|
1345
1603
|
}
|
|
1346
1604
|
if (!out) { st.status = 'paused'; st.pause_reason = 'connection'; save(st, dir); onAlert({ type: 'conn', text: `📡 ts: sem conexão com o servidor após 3 tentativas. Missão PAUSADA e salva. Retome com "ts meta" quando a internet voltar.` }); return st; }
|
|
1347
1605
|
st.creditsSpent += out.credits || 0;
|
|
1348
1606
|
|
|
1607
|
+
// A preflight budget stop is financial state, not an implementation
|
|
1608
|
+
// failure. Do not consume an attempt or block a valid checklist item.
|
|
1609
|
+
if (out.guard && out.guard.kind === 'credits' && !(out.steps > 0)) {
|
|
1610
|
+
item.attempts = Math.max(0, (item.attempts || 1) - 1);
|
|
1611
|
+
st.status = 'paused';
|
|
1612
|
+
st.pause_reason = 'budget';
|
|
1613
|
+
// Signals night mode to open a fresh window without inventing spend.
|
|
1614
|
+
st.budget = st.creditsSpent;
|
|
1615
|
+
save(st, dir);
|
|
1616
|
+
const need = out.guard.required ? ` (minimo estimado: ${out.guard.required}; disponivel: ${out.guard.available || 0})` : '';
|
|
1617
|
+
onAlert({ type: 'budget', text: `A rodada precisava de mais contexto que o saldo desta janela${need}. Pausei sem marcar o item como falho; a proxima janela retoma limpo.` });
|
|
1618
|
+
return st;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
const expectedNow = String(expectedArtifact || '').replace(/\\/g, '/').toLowerCase();
|
|
1622
|
+
const wroteExpectedNow = (out.actions || []).some(a => {
|
|
1623
|
+
if (!['escrever_arquivo', 'editar_arquivo'].includes(a && a.name)) return false;
|
|
1624
|
+
const target = String(a.target || '').replace(/\\/g, '/').toLowerCase();
|
|
1625
|
+
return target === expectedNow || target.endsWith('/' + expectedNow);
|
|
1626
|
+
});
|
|
1627
|
+
// Reading/searching is not implementation progress. A model can consume
|
|
1628
|
+
// several tool steps exploring the project and still fail to create the
|
|
1629
|
+
// promised deliverable; treat that exactly like a zero-tool promise.
|
|
1630
|
+
if (expectedNow && !wroteExpectedNow) {
|
|
1631
|
+
item.attempts = Math.max(0, (item.attempts || 1) - 1);
|
|
1632
|
+
item.avoidModels = [...new Set([...(item.avoidModels || []), String(out.model || roundModel || 'automatico')])];
|
|
1633
|
+
item.directArtifact = true;
|
|
1634
|
+
const failedModel = String(out.model || roundModel || 'glm-5.2');
|
|
1635
|
+
item.directModel = /glm/i.test(failedModel) ? 'glm-5.2' : failedModel;
|
|
1636
|
+
st.status = 'paused'; st.pause_reason = 'model_no_action';
|
|
1637
|
+
st.budget = st.creditsSpent;
|
|
1638
|
+
save(st, dir);
|
|
1639
|
+
onAlert({ type: 'retry', text: `O modelo ${out.model || roundModel || 'automatico'} respondeu, mas executou zero ferramentas. Nao marquei falha do item; a retomada usara outro executor.` });
|
|
1640
|
+
return st;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1349
1643
|
// ── BLOQUEIO HUMANO: o agente pediu uma ação externa que só o usuário faz ──
|
|
1350
1644
|
// A missão PAUSA (gasto zero enquanto espera) e chama o usuário. Retoma com "ts meta".
|
|
1351
1645
|
if (out.needHuman) {
|
|
@@ -1364,7 +1658,7 @@ async function run(goal, opts = {}) {
|
|
|
1364
1658
|
let marks = [];
|
|
1365
1659
|
const written = (out.actions || []).filter(a => a.name === 'escrever_arquivo' || a.name === 'editar_arquivo').map(a => String(a.target || ''));
|
|
1366
1660
|
const _basename = (p) => String(p).replace(/\\/g, '/').split('/').pop().toLowerCase();
|
|
1367
|
-
const _fileHints = (txt) => (String(txt).match(/[\w.\-]+\.(kt|java|xml|gradle|json|md|txt|kts|properties|pro|png|webp|py|js|ts|html|css|sh)\b/gi) || []).map(s => s.toLowerCase());
|
|
1661
|
+
const _fileHints = (txt) => (String(txt).match(/[\w.\-]+\.(kt|java|xml|gradle|json|md|txt|kts|properties|pro|png|webp|py|gd|js|ts|html|css|sh)\b/gi) || []).map(s => s.toLowerCase());
|
|
1368
1662
|
const itemFiles = _fileHints(item.desc);
|
|
1369
1663
|
// Uma escrita/comando genérico não prova um item sem vínculo. Só há marcação automática
|
|
1370
1664
|
// quando o próprio texto do item identifica o artefato e a rodada gravou esse artefato.
|
|
@@ -1372,7 +1666,13 @@ async function run(goal, opts = {}) {
|
|
|
1372
1666
|
// para outros itens só porque algum arquivo/comando apareceu na mesma rodada.
|
|
1373
1667
|
const wroteForItem = itemFiles.length > 0
|
|
1374
1668
|
&& itemFiles.some(f => written.some(w => _basename(w) === f));
|
|
1375
|
-
|
|
1669
|
+
const _normPath = (p) => String(p || '').replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase();
|
|
1670
|
+
const expectedNorm = _normPath(expectedArtifact);
|
|
1671
|
+
const wroteExpectedArtifact = !!expectedNorm && written.some(w => {
|
|
1672
|
+
const n = _normPath(w);
|
|
1673
|
+
return n === expectedNorm || n.endsWith('/' + expectedNorm);
|
|
1674
|
+
});
|
|
1675
|
+
if (wroteForItem || wroteExpectedArtifact) marks.push(item.id);
|
|
1376
1676
|
|
|
1377
1677
|
// Item de correção de build: NÃO usa marcador — o PORTÃO DE BUILD é a única
|
|
1378
1678
|
// autoridade (recompila de verdade). Marca provisório pra reabrir o gate; se o
|
|
@@ -1394,7 +1694,7 @@ async function run(goal, opts = {}) {
|
|
|
1394
1694
|
const mk = await _llmJson({ token, system: MARK_SYS,
|
|
1395
1695
|
user: `OBJETIVO:\n${st.goal.slice(0, 600)}\n\nCHECKLIST (contexto):\n${fmtChecklist(st.checklist)}\n\nITEM ALVO: (${item.id}) ${item.desc}\n\nRESULTADO DA RODADA:\n${String(out.text || '').slice(0, 2000)}${evid}` });
|
|
1396
1696
|
st.creditsSpent += mk.credits || 0;
|
|
1397
|
-
if (mk.json && mk.json.passou === true) marks.push(item.id);
|
|
1697
|
+
if (mk.json && mk.json.passou === true && wroteExpectedArtifact) marks.push(item.id);
|
|
1398
1698
|
} catch (_) {}
|
|
1399
1699
|
marks = [...new Set(marks)];
|
|
1400
1700
|
for (const it of st.checklist) if (marks.includes(it.id)) it.passes = true;
|
|
@@ -1456,4 +1756,4 @@ function trailSummary(st, lang) {
|
|
|
1456
1756
|
};
|
|
1457
1757
|
}
|
|
1458
1758
|
|
|
1459
|
-
module.exports = { webRunGate, run, load, notify, stateFile, detectBuild, ensureToolchain, runBuild, escalate, designPhase, archPhase, looksVisual, looksComplex, runApp, visualGate, ensureEmulator, _projKind, _llmVision, verificationLabel, _checkCriteria, trailSummary };
|
|
1759
|
+
module.exports = { webRunGate, run, load, notify, stateFile, detectBuild, ensureToolchain, runBuild, escalate, designPhase, archPhase, looksVisual, looksComplex, runApp, visualGate, ensureEmulator, _projKind, _llmVision, toolsForMetaItem, artifactForMetaItem, normalizeMetaArtifact, persistMetaArtifact, directArtifactPlan, verificationLabel, _checkCriteria, trailSummary };
|
package/lib/tools.js
CHANGED
|
@@ -190,10 +190,12 @@ const DEFS = [
|
|
|
190
190
|
{ type: 'function', function: { name: 'obter_anexo_outlook', description: 'Obtém o link autenticado para baixar um anexo do Outlook.', parameters: { type: 'object', properties: { id_email: { type: 'string' }, id_anexo: { type: 'string' } }, required: ['id_email','id_anexo'] } } },
|
|
191
191
|
{ type: 'function', function: { name: 'criar_rascunho_outlook', description: 'Cria rascunho no Outlook. Exige confirmação e não envia.', parameters: { type: 'object', properties: { para: { type: 'array', items: { type: 'string' } }, cc: { type: 'array', items: { type: 'string' } }, assunto: { type: 'string' }, corpo: { type: 'string' }, formato_html: { type: 'boolean' } }, required: ['para','assunto','corpo'] } } },
|
|
192
192
|
{ type: 'function', function: { name: 'enviar_rascunho_outlook', description: 'Envia um rascunho existente do Outlook. Exige confirmação.', parameters: { type: 'object', properties: { rascunho_id: { type: 'string' } }, required: ['rascunho_id'] } } },
|
|
193
|
-
{ type: 'function', function: { name: 'status_google_workspace', description: 'Verifica a conexão
|
|
194
|
-
{ type: 'function', function: { name: 'conectar_google_workspace', description: 'Gera o link
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
{ type: 'function', function: { name: 'status_google_workspace', description: 'Verifica a conexão Google e informa se o acesso é Essencial ou Avançado.', parameters: { type: 'object', properties: {} } } },
|
|
194
|
+
{ type: 'function', function: { name: 'conectar_google_workspace', description: 'Gera o link Google. Essencial acessa arquivos escolhidos/criados pelo app; Avançado beta inclui Gmail completo e pesquisa global do Drive.', parameters: { type: 'object', properties: {
|
|
195
|
+
nivel: { type: 'string', enum: ['essencial', 'avancado'] }
|
|
196
|
+
} } } },
|
|
197
|
+
{ type: 'function', function: { name: 'listar_emails_gmail', description: 'Lista e prioriza e-mails do Gmail, sem alterar nada. Use pagina com nextPageToken para continuar em lotes.', parameters: { type: 'object', properties: {
|
|
198
|
+
limite: { type: 'number' }, somente_nao_lidos: { type: 'boolean' }, priorizar: { type: 'boolean' }, consulta: { type: 'string' }, pasta: { type: 'string', enum: ['inbox','sent','drafts','spam','trash','starred','important','all'] }, pagina: { type: 'string' }
|
|
197
199
|
} } } },
|
|
198
200
|
{ type: 'function', function: { name: 'ler_email_gmail', description: 'Lê o corpo completo de um e-mail ou a conversa inteira sem marcar como lido.', parameters: { type: 'object', properties: {
|
|
199
201
|
id_email: { type: 'string' }, conversa_completa: { type: 'boolean' }
|
|
@@ -215,9 +217,13 @@ const DEFS = [
|
|
|
215
217
|
para: { type: 'array', items: { type: 'string' } }, cc: { type: 'array', items: { type: 'string' } }, assunto: { type: 'string' }, corpo: { type: 'string' }, formato_html: { type: 'boolean' }
|
|
216
218
|
}, required: ['para', 'assunto', 'corpo'] } } },
|
|
217
219
|
{ type: 'function', function: { name: 'enviar_rascunho_gmail', description: 'Envia rascunho existente do Gmail. Exige confirmação.', parameters: { type: 'object', properties: { rascunho_id: { type: 'string' } }, required: ['rascunho_id'] } } },
|
|
218
|
-
{ type: 'function', function: { name: 'listar_arquivos_drive', description: 'Lista arquivos no
|
|
220
|
+
{ type: 'function', function: { name: 'listar_arquivos_drive', description: 'Lista arquivos acessíveis no Drive; no Essencial limita-se aos escolhidos/criados pelo app e no Avançado pesquisa todo o Drive.', parameters: { type: 'object', properties: {
|
|
219
221
|
busca: { type: 'string' }, tipo_mime: { type: 'string' }, limite: { type: 'number' }
|
|
220
222
|
} } } },
|
|
223
|
+
{ type: 'function', function: { name: 'selecionar_arquivos_drive', description: 'Fornece o link da Central de Integrações para escolher arquivos existentes no Google Drive.', parameters: { type: 'object', properties: {} } } },
|
|
224
|
+
{ type: 'function', function: { name: 'criar_google_docs', description: 'Cria um novo Google Docs. Exige confirmação.', parameters: { type: 'object', properties: {
|
|
225
|
+
nome: { type: 'string' }, texto_inicial: { type: 'string' }, id_pasta: { type: 'string' }
|
|
226
|
+
}, required: ['nome'] } } },
|
|
221
227
|
{ type: 'function', function: { name: 'ler_google_docs', description: 'Lê texto de Google Docs pelo ID.', parameters: { type: 'object', properties: { id_documento: { type: 'string' } }, required: ['id_documento'] } } },
|
|
222
228
|
{ type: 'function', function: { name: 'editar_google_docs', description: 'Edita o Google Docs original por padrão; cria cópia somente se solicitado. Exige confirmação.', parameters: { type: 'object', properties: {
|
|
223
229
|
id_documento: { type: 'string' }, localizar: { type: 'string' }, substituir_por: { type: 'string' }, diferenciar_maiusculas: { type: 'boolean' }, criar_copia: { type: 'boolean' }, nome_copia: { type: 'string' }
|
|
@@ -225,6 +231,9 @@ const DEFS = [
|
|
|
225
231
|
{ type: 'function', function: { name: 'ler_google_sheets', description: 'Lê células de Google Sheets pelo ID e intervalo.', parameters: { type: 'object', properties: {
|
|
226
232
|
id_planilha: { type: 'string' }, intervalo: { type: 'string' }
|
|
227
233
|
}, required: ['id_planilha'] } } },
|
|
234
|
+
{ type: 'function', function: { name: 'criar_google_sheets', description: 'Cria uma nova Google Sheets e pode preencher valores iniciais. Exige confirmação.', parameters: { type: 'object', properties: {
|
|
235
|
+
nome: { type: 'string' }, intervalo: { type: 'string' }, valores: { type: 'array', items: { type: 'array', items: {} } }, id_pasta: { type: 'string' }
|
|
236
|
+
}, required: ['nome'] } } },
|
|
228
237
|
{ type: 'function', function: { name: 'editar_google_sheets', description: 'Edita a Google Sheets original por padrão; cria cópia somente se solicitado. Exige confirmação.', parameters: { type: 'object', properties: {
|
|
229
238
|
id_planilha: { type: 'string' }, intervalo: { type: 'string' }, valores: { type: 'array', items: { type: 'array', items: {} } }, criar_copia: { type: 'boolean' }, nome_copia: { type: 'string' }
|
|
230
239
|
}, required: ['id_planilha', 'intervalo', 'valores'] } } },
|
|
@@ -364,9 +373,14 @@ function _shellGotcha(cmd) {
|
|
|
364
373
|
return 'python -c MULTILINHA falha em silêncio no Windows. Escreva um arquivo .py com escrever_arquivo e rode "python arquivo.py".';
|
|
365
374
|
if (/\bnode\s+-e\b/.test(s) && (multi || /\bimport\s*\(/.test(s) || /\bawait\b/.test(s)))
|
|
366
375
|
return 'node -e MULTILINHA ou com import()/await de topo falha no eval. Escreva um arquivo .mjs com escrever_arquivo e rode "node arquivo.mjs".';
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
|
|
376
|
+
// child_process.exec usa cmd.exe no Windows. Alguns comandos POSIX têm outro
|
|
377
|
+
// significado: `mkdir -p app` cria por engano uma pasta literal chamada `-p`.
|
|
378
|
+
if (process.platform === 'win32' && /(^|[&|]\s*|\s)mkdir\s+-p(?:\s|$)/i.test(s))
|
|
379
|
+
return 'Este shell é cmd.exe: "mkdir -p" criaria uma pasta chamada "-p". Use "mkdir pasta" ou PowerShell New-Item.';
|
|
380
|
+
if (process.platform === 'win32' && /(^|[&|]\s*|\s)ls(?:\s|$)/i.test(s))
|
|
381
|
+
return 'Este shell é cmd.exe: use "dir" (ou chame PowerShell explicitamente) em vez de "ls".';
|
|
382
|
+
if (process.platform === 'win32' && /(?:^|\s)\/[a-zA-Z]\//.test(s))
|
|
383
|
+
return 'Caminho no formato /c/... não é válido no cmd.exe. Use C:\\... ou um caminho relativo ao diretório de trabalho.';
|
|
370
384
|
// matar processo por IMAGEM mata tudo com esse nome (inclusive a própria missão)
|
|
371
385
|
if (/\btaskkill\b[^\n]*\/im\b/i.test(s))
|
|
372
386
|
return 'taskkill /IM mata TODOS os processos com esse nome (inclusive os do usuário e possivelmente o próprio ts). Mate pelo PID da PORTA: netstat -ano | findstr :PORTA → taskkill /PID <pid> /F.';
|
|
@@ -889,20 +903,24 @@ async function execute(name, input, opts = {}) {
|
|
|
889
903
|
return await require('./api').api('/api/integrations/microsoft/mail/forwards', { method: 'POST', token: opts.token, body: { messageId: input.id_email, to: input.para, cc: input.cc || [], body: input.corpo || '', confirmed: true } });
|
|
890
904
|
case 'obter_anexo_outlook':
|
|
891
905
|
return { downloadUrl: '/api/integrations/microsoft/mail/attachments/download?' + new URLSearchParams({ messageId: input.id_email, attachmentId: input.id_anexo }), instrucao: 'Use uma sessão autenticada do Terminal Smart para baixar o anexo.' };
|
|
892
|
-
case 'criar_rascunho_outlook':
|
|
893
|
-
|
|
906
|
+
case 'criar_rascunho_outlook': {
|
|
907
|
+
const data = await require('./api').api('/api/integrations/microsoft/mail/drafts', { method: 'POST', token: opts.token, body: { to: input.para, cc: input.cc || [], subject: input.assunto, body: input.corpo, contentType: input.formato_html ? 'html' : 'text', confirmed: true } });
|
|
908
|
+
return { ...data, rascunho_id: data.rascunho_id || data.draftId || data.draft?.id || data.id || null };
|
|
909
|
+
}
|
|
894
910
|
case 'enviar_rascunho_outlook':
|
|
895
911
|
return await require('./api').api('/api/integrations/microsoft/mail/send', { method: 'POST', token: opts.token, body: { draftId: input.rascunho_id, confirmed: true } });
|
|
896
912
|
case 'status_google_workspace':
|
|
897
913
|
return await require('./api').api('/api/integrations/google/status', { token: opts.token });
|
|
898
914
|
case 'conectar_google_workspace': {
|
|
899
|
-
const
|
|
900
|
-
|
|
915
|
+
const accessLevel = String(input.nivel || '').toLowerCase() === 'avancado' ? 'advanced' : 'essential';
|
|
916
|
+
const data = await require('./api').api('/api/integrations/google/connect', { method: 'POST', body: { source: 'cli', accessLevel }, token: opts.token });
|
|
917
|
+
return { ok: true, accessLevel, authUrl: data.authUrl, instrucao: `Abra o link no navegador, autorize o acesso Google ${accessLevel === 'advanced' ? 'Avançado (beta)' : 'Essencial'} e volte ao Terminal Smart.` };
|
|
901
918
|
}
|
|
902
919
|
case 'listar_emails_gmail': {
|
|
903
920
|
const p = new URLSearchParams({ limit: String(Math.min(30, Math.max(1, parseInt(input.limite) || 15))) });
|
|
904
921
|
if (input.somente_nao_lidos) p.set('unread', '1'); if (input.priorizar) p.set('priority', '1');
|
|
905
922
|
if (input.consulta) p.set('query', input.consulta); if (input.pasta) p.set('folder', input.pasta);
|
|
923
|
+
if (input.pagina) p.set('page', input.pagina);
|
|
906
924
|
return await require('./api').api('/api/integrations/google/gmail/messages?' + p, { token: opts.token });
|
|
907
925
|
}
|
|
908
926
|
case 'ler_email_gmail': {
|
|
@@ -919,15 +937,21 @@ async function execute(name, input, opts = {}) {
|
|
|
919
937
|
return await require('./api').api('/api/integrations/google/gmail/forwards', { method: 'POST', token: opts.token, body: { messageId: input.id_email, to: input.para, cc: input.cc || [], body: input.corpo || '', confirmed: true } });
|
|
920
938
|
case 'obter_anexo_gmail':
|
|
921
939
|
return { downloadUrl: '/api/integrations/google/gmail/attachments/download?' + new URLSearchParams({ messageId: input.id_email, attachmentId: input.id_anexo }), instrucao: 'Use uma sessão autenticada do Terminal Smart para baixar o anexo.' };
|
|
922
|
-
case 'criar_rascunho_gmail':
|
|
923
|
-
|
|
940
|
+
case 'criar_rascunho_gmail': {
|
|
941
|
+
const data = await require('./api').api('/api/integrations/google/gmail/drafts', { method: 'POST', token: opts.token, body: { to: input.para, cc: input.cc || [], subject: input.assunto, body: input.corpo, contentType: input.formato_html ? 'html' : 'text', confirmed: true } });
|
|
942
|
+
return { ...data, id_rascunho: data.id_rascunho || data.draftId || data.draft?.id || data.id || null };
|
|
943
|
+
}
|
|
924
944
|
case 'enviar_rascunho_gmail':
|
|
925
|
-
return await require('./api').api('/api/integrations/google/gmail/send', { method: 'POST', token: opts.token, body: { draftId: input.
|
|
945
|
+
return await require('./api').api('/api/integrations/google/gmail/send', { method: 'POST', token: opts.token, body: { draftId: input.id_rascunho, confirmed: true } });
|
|
926
946
|
case 'listar_arquivos_drive': {
|
|
927
947
|
const p = new URLSearchParams({ limit: String(Math.min(100, Math.max(1, parseInt(input.limite) || 30))) });
|
|
928
948
|
if (input.busca) p.set('query', input.busca); if (input.tipo_mime) p.set('mimeType', input.tipo_mime);
|
|
929
949
|
return await require('./api').api('/api/integrations/google/drive/files?' + p, { token: opts.token });
|
|
930
950
|
}
|
|
951
|
+
case 'selecionar_arquivos_drive':
|
|
952
|
+
return { url: 'https://terminalsmart.com.br/integracoes#google-files', instrucao: 'Abra o link e clique em Escolher arquivos do Drive.' };
|
|
953
|
+
case 'criar_google_docs':
|
|
954
|
+
return await require('./api').api('/api/integrations/google/docs/create', { method: 'POST', token: opts.token, body: { name: input.nome, text: input.texto_inicial || '', folderId: input.id_pasta, confirmed: true } });
|
|
931
955
|
case 'ler_google_docs':
|
|
932
956
|
return await require('./api').api('/api/integrations/google/docs/document?id=' + encodeURIComponent(input.id_documento), { token: opts.token });
|
|
933
957
|
case 'editar_google_docs':
|
|
@@ -936,6 +960,8 @@ async function execute(name, input, opts = {}) {
|
|
|
936
960
|
const p = new URLSearchParams({ id: input.id_planilha }); if (input.intervalo) p.set('range', input.intervalo);
|
|
937
961
|
return await require('./api').api('/api/integrations/google/sheets/values?' + p, { token: opts.token });
|
|
938
962
|
}
|
|
963
|
+
case 'criar_google_sheets':
|
|
964
|
+
return await require('./api').api('/api/integrations/google/sheets/create', { method: 'POST', token: opts.token, body: { name: input.nome, range: input.intervalo || 'A1', values: input.valores || [], folderId: input.id_pasta, confirmed: true } });
|
|
939
965
|
case 'editar_google_sheets':
|
|
940
966
|
return await require('./api').api('/api/integrations/google/sheets/update', { method: 'POST', token: opts.token, body: { spreadsheetId: input.id_planilha, range: input.intervalo, values: input.valores, createCopy: input.criar_copia === true, copyName: input.nome_copia, confirmed: true } });
|
|
941
967
|
case 'ler_apresentacao': {
|
|
@@ -971,4 +997,4 @@ async function execute(name, input, opts = {}) {
|
|
|
971
997
|
} catch (e) { return { erro: String((e && e.message) || e).slice(0, 400) }; }
|
|
972
998
|
}
|
|
973
999
|
|
|
974
|
-
module.exports = { DEFS, execute, isDestructive, selfDestructiveReason, buildProjectMap };
|
|
1000
|
+
module.exports = { DEFS, execute, isDestructive, selfDestructiveReason, buildProjectMap, _test: { shellGotcha: _shellGotcha } };
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terminal-smart-cli",
|
|
3
|
-
"version": "0.97.
|
|
3
|
+
"version": "0.97.13",
|
|
4
4
|
"description": "Terminal Smart no seu terminal — pergunte, analise logs por pipe e orquestre agentes de IA. Comando: ts",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ts": "bin/ts.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
-
"test": "node test/core.test.js && node test/intelligence-core.test.js && node test/cloud-slug.test.js && node test/eval-model.test.js && node test/project-cache.test.js && node test/memory-bus.test.js && node test/capabilities.test.js && node test/mcp-e2e.test.js && node test/erros.test.js && node test/capability-pack.test.js && node test/byok.test.js && node test/conhecimento.test.js && node test/policy.test.js && node test/temas.test.js && node test/skill-index.test.js && node test/doctor.test.js && node test/google-workspace-tools.test.js && node test/office-editors.test.js"
|
|
9
|
+
"test": "node test/core.test.js && node test/intelligence-core.test.js && node test/cloud-slug.test.js && node test/eval-model.test.js && node test/project-cache.test.js && node test/memory-bus.test.js && node test/capabilities.test.js && node test/mcp-e2e.test.js && node test/erros.test.js && node test/capability-pack.test.js && node test/video-generation.test.js && node test/byok.test.js && node test/conhecimento.test.js && node test/policy.test.js && node test/temas.test.js && node test/skill-index.test.js && node test/doctor.test.js && node test/google-workspace-tools.test.js && node test/office-editors.test.js"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"bin",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"ssh",
|
|
25
25
|
"agente"
|
|
26
26
|
],
|
|
27
|
-
"author": "Terminal Smart <contato@
|
|
27
|
+
"author": "Terminal Smart <contato@g3tecnegocios.com>",
|
|
28
28
|
"homepage": "https://terminalsmart.com.br/cli",
|
|
29
29
|
"bugs": {
|
|
30
30
|
"url": "https://terminalsmart.com.br/cli"
|