terminal-smart-cli 0.97.29 → 0.97.31

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/lib/core.js CHANGED
@@ -136,10 +136,10 @@ const EVENT_TYPES = ['system', 'tool', 'assistant', 'result'];
136
136
  // raw (retrocompatível). Classe de erro alimenta o Recovery Engine e o ledger; `ok:false`
137
137
  // deve barrar a marcação de sucesso a montante (meta/orquestrador).
138
138
  const ERROR_CLASSES = ['permission_denied', 'not_found', 'port_in_use', 'missing_dep', 'package_lock',
139
- 'timeout', 'compile_error', 'test_failed', 'network', 'auth', 'git_conflict', 'invalid_schema',
140
- 'blocked', 'unknown'];
139
+ 'timeout', 'compile_error', 'test_failed', 'network', 'auth', 'git_conflict', 'invalid_schema', 'resource_locked',
140
+ 'invalid_command', 'invalid_workdir', 'blocked', 'unknown'];
141
141
  // classes transitórias → vale re-tentar (mesma abordagem); as demais exigem mudar de abordagem.
142
- const RETRYABLE_CLASSES = new Set(['network', 'timeout', 'package_lock', 'port_in_use']);
142
+ const RETRYABLE_CLASSES = new Set(['network', 'timeout', 'package_lock', 'resource_locked', 'port_in_use']);
143
143
 
144
144
  function classifyError(msg) {
145
145
  const s = String(msg || '').toLowerCase();
@@ -147,8 +147,15 @@ function classifyError(msg) {
147
147
  if (/\bbloquead|\brecus|read-only|só-leitura|loop detectad|\bloop:|aprova(ção|r) neg|negou.*aprova/i.test(s)) return 'blocked';
148
148
  if (/eacces|permission denied|access is denied|operation not permitted|acesso negado|permissão negada/i.test(s)) return 'permission_denied';
149
149
  if (/could not get lock|dpkg was interrupted|resource temporarily unavailable.*lock|another (app|process) .*lock|unable to (acquire|lock)/i.test(s)) return 'package_lock';
150
+ if (/arquivo[_ ]ocupado|file[_ ]locked|resource[_ ]locked|another terminal smart process.*(?:editing|altering)|eexist.*\.lock/i.test(s)) return 'resource_locked';
150
151
  if (/eaddrinuse|address already in use|porta .*(ocupad|em uso)|already running/i.test(s)) return 'port_in_use';
151
- if (/cannot find module|command not found|not recognized as|modulenotfounderror|no module named|no such command|is not installed|não (foi )?encontrad[oa] o comando/i.test(s)) return 'missing_dep';
152
+ // Binário/comando inválido pede dialeto/sintaxe correto; módulo ausente pode justificar instalar pacote.
153
+ if (/cannot find module|modulenotfounderror|no module named|is not installed/i.test(s)) return 'missing_dep';
154
+ // cmd.exe pode devolver texto OEM/UTF-8 corrompido ("não" vira "n�o").
155
+ // A classe precisa sobreviver a isso: o recovery correto depende dela e não
156
+ // pode cair em unknown só por causa do encoding do terminal.
157
+ if (/is not recognized as (an )?(internal|the name of)|command not found|no such command|n[^a-z\s]{0,3}o (?:foi )?encontrad[oa] o comando|n[^a-z\s]{0,3}o [^\r\n]{0,80}reconhecid[oa]|invalid switch|unrecognized option|syntax of the command is incorrect|unexpectedtoken|parsererror|terminator.*string/i.test(s)) return 'invalid_command';
158
+ if (/working directory|invalid directory|directory name is invalid|system cannot find the path specified|n[^a-z\s]{0,3}o pode encontrar o caminho especificado|enoent.*(?:spawn|chdir)|chdir.*enoent/i.test(s)) return 'invalid_workdir';
152
159
  if (/enoent|no such file|não encontrad|not found|arquivo inexistente|404/i.test(s)) return 'not_found';
153
160
  if (/etimedout|timed out|timeout|excedeu o tempo/i.test(s)) return 'timeout';
154
161
  if (/econnrefused|enotfound|getaddrinfo|network|connection refused|conexão recusada|conn|dns/i.test(s)) return 'network';
package/lib/eval.js CHANGED
@@ -152,7 +152,7 @@ async function judge({ key, baseUrl, model, caseObj, evidence }) {
152
152
  // opts: { token, lang, model (executor), judgeModel, onCase({i,total,id,phase}) }
153
153
  // Roda os casos EM SÉRIE (sandbox isolado por caso; evita colisão e rate-limit).
154
154
  async function runSuite(suite, opts = {}) {
155
- const { token, lang = 'pt', model = null, judgeModel = DEFAULT_JUDGE } = opts;
155
+ const { token, lang = 'pt', model = null, judgeModel = DEFAULT_JUDGE, maxIter, maxDurationMs, maxCredits } = opts;
156
156
  const onCase = opts.onCase || (() => {});
157
157
  // chave sk-hub (mesma do agente) — reusada pra chamar o juiz direto no gateway.
158
158
  const k = await keyring.resolve(token, { feature: 'cli_agent' });
@@ -188,7 +188,7 @@ async function runSuite(suite, opts = {}) {
188
188
  out = await agent.run(c.task, {
189
189
  token, lang, yes: true, model,
190
190
  cwd: sandbox, confineDir: sandbox, readOnly: c.readonly,
191
- skipSessionStart: true,
191
+ skipSessionStart: true, maxIter, maxDurationMs, maxCredits,
192
192
  });
193
193
  } catch (e) {
194
194
  if (e && (e.code === 'no_credits' || e.status === 402)) throw e; // teto → aborta a suíte
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ // Autoevolução é deliberadamente separada da execução: incidentes geram uma
4
+ // PROPOSTA revisável, nunca um patch, comando ou deploy automático. Por decisão
5
+ // de produto, somente o GPT SOL pode assinar esse diagnóstico de harness.
6
+ const DIAGNOSTICIAN_MODEL = 'gpt-5.6-sol';
7
+ const telemetry = require('./evolution-telemetry');
8
+
9
+ function diagnosticianFor() { return DIAGNOSTICIAN_MODEL; }
10
+
11
+ function buildRequest(events = [], context = {}) {
12
+ const cleanEvents = telemetry.buildEvents(events, [], context).map(event => ({
13
+ tool: event.tool, errorClass: event.errorClass, pattern: event.pattern,
14
+ stage: event.stage, fingerprint: event.fingerprint, retryable: event.retryable,
15
+ recovered: event.recovered,
16
+ }));
17
+ return {
18
+ model: DIAGNOSTICIAN_MODEL,
19
+ mode: 'proposal_only',
20
+ instruction: 'Analise somente os incidentes sanitizados. Proponha uma hipótese, testes de regressão e uma mudança mínima para revisão humana. Não gere comandos de produção, não aplique patches, não faça deploy e não trate a proposta como correção aprovada.',
21
+ incidents: cleanEvents.slice(0, 20),
22
+ };
23
+ }
24
+
25
+ function normalizeProposal(value, model) {
26
+ const v = value && typeof value === 'object' ? value : {};
27
+ const bySol = String(model || '') === DIAGNOSTICIAN_MODEL;
28
+ const list = (items, limit, width) => (Array.isArray(items) ? items : [])
29
+ .map(item => String(item || '').replace(/\s+/g, ' ').trim().slice(0, width))
30
+ .filter(Boolean).slice(0, limit);
31
+ return {
32
+ accepted: bySol,
33
+ model: bySol ? DIAGNOSTICIAN_MODEL : '',
34
+ mode: 'proposal_only',
35
+ requiresHumanReview: true,
36
+ autoApply: false,
37
+ title: String(v.title || '').replace(/\s+/g, ' ').trim().slice(0, 160),
38
+ diagnosis: String(v.diagnosis || v.summary || '').replace(/\s+/g, ' ').trim().slice(0, 1200),
39
+ evidence: list(v.evidence, 8, 360),
40
+ proposedChanges: list(v.proposedChanges || v.recommendations, 8, 500),
41
+ regressionTests: list(v.regressionTests || v.tests, 8, 360),
42
+ };
43
+ }
44
+
45
+ function canAutoApply() { return false; }
46
+
47
+ module.exports = { DIAGNOSTICIAN_MODEL, diagnosticianFor, buildRequest, normalizeProposal, canAutoApply };
@@ -0,0 +1,42 @@
1
+ // Lock cooperativo entre processos do Terminal Smart. `wx` cria o arquivo de
2
+ // forma atômica: duas CLIs não conseguem adquirir o mesmo alvo ao mesmo tempo.
3
+ 'use strict';
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const crypto = require('crypto');
8
+
9
+ const DIR = path.join(os.homedir(), '.ts', 'locks');
10
+ const STALE_MS = 2 * 60 * 1000;
11
+ const keyFor = (target) => crypto.createHash('sha256').update(path.resolve(target).toLowerCase()).digest('hex').slice(0, 24);
12
+ const pathFor = (target) => path.join(DIR, keyFor(target) + '.lock');
13
+
14
+ function acquire(target, { staleMs = STALE_MS } = {}) {
15
+ const lockPath = pathFor(target);
16
+ try { fs.mkdirSync(DIR, { recursive: true }); } catch (_) { return null; }
17
+ for (let attempt = 0; attempt < 2; attempt++) {
18
+ try {
19
+ const fd = fs.openSync(lockPath, 'wx', 0o600);
20
+ try { fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, target: path.resolve(target), createdAt: new Date().toISOString() })); } finally { fs.closeSync(fd); }
21
+ let released = false;
22
+ return {
23
+ release() {
24
+ if (released) return;
25
+ released = true;
26
+ try { fs.unlinkSync(lockPath); } catch (_) {}
27
+ },
28
+ lockPath,
29
+ };
30
+ } catch (err) {
31
+ if (!err || err.code !== 'EEXIST') return null;
32
+ try {
33
+ const age = Date.now() - fs.statSync(lockPath).mtimeMs;
34
+ if (age > staleMs) { fs.unlinkSync(lockPath); continue; }
35
+ } catch (_) { continue; }
36
+ return null;
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+
42
+ module.exports = { DIR, STALE_MS, keyFor, pathFor, acquire };
@@ -0,0 +1,69 @@
1
+ // Gateways operacionais: aliases OpenSSH com comando forçado (não são shells
2
+ // remotos normais). A configuração guarda apenas alias e allowlist; chaves e
3
+ // credenciais continuam exclusivamente no OpenSSH do sistema.
4
+ const config = require('./config');
5
+
6
+ const OPS = Object.freeze(['status', 'logs', 'deploy']);
7
+
8
+ function normalized(value) {
9
+ return String(value || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim();
10
+ }
11
+
12
+ function safeName(value) {
13
+ const raw = String(value || '').trim();
14
+ if (!raw || /[\\/]/.test(raw) || raw === '.' || raw === '..' || raw.includes('..')) return null;
15
+ const name = normalized(raw).replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
16
+ return /^[a-z0-9][a-z0-9_-]{0,63}$/.test(name) ? name : null;
17
+ }
18
+
19
+ function safeAlias(value) {
20
+ const alias = String(value || '').trim();
21
+ return /^[A-Za-z0-9_.-]{1,128}$/.test(alias) ? alias : null;
22
+ }
23
+
24
+ function normalizeOps(value) {
25
+ const values = Array.isArray(value) ? value : String(value || '').split(',');
26
+ const ops = [...new Set(values.map(x => String(x).trim().toLowerCase()).filter(x => OPS.includes(x)))];
27
+ return ops.length ? ops : ['status'];
28
+ }
29
+
30
+ function list() {
31
+ const raw = config.load().gateways || {};
32
+ return Object.entries(raw).map(([name, item]) => {
33
+ const alias = safeAlias(item && item.sshAlias);
34
+ if (!alias) return null;
35
+ return { name, label: String(item.label || name), sshAlias: alias, operations: normalizeOps(item.operations) };
36
+ }).filter(Boolean);
37
+ }
38
+
39
+ function save(name, input = {}) {
40
+ const id = safeName(name);
41
+ const alias = safeAlias(input.sshAlias);
42
+ if (!id) throw new Error('Nome do gateway inválido. Use letras, números, hífen ou sublinhado.');
43
+ if (!alias) throw new Error('Alias OpenSSH inválido. Use apenas letras, números, ponto, hífen ou sublinhado.');
44
+ const cfg = config.load();
45
+ const gateways = { ...(cfg.gateways || {}) };
46
+ gateways[id] = { label: String(input.label || id).trim().slice(0, 80), sshAlias: alias, operations: normalizeOps(input.operations) };
47
+ config.save({ gateways });
48
+ return { name: id, ...gateways[id] };
49
+ }
50
+
51
+ function remove(name) {
52
+ const id = safeName(name);
53
+ const cfg = config.load();
54
+ const gateways = { ...(cfg.gateways || {}) };
55
+ if (!id || !gateways[id]) return false;
56
+ delete gateways[id];
57
+ config.save({ gateways });
58
+ return true;
59
+ }
60
+
61
+ function findInText(text, entries = list()) {
62
+ const normalizedText = normalized(text);
63
+ return entries.find(entry => {
64
+ const terms = [entry.name, entry.label, entry.sshAlias].map(normalized).filter(Boolean);
65
+ return terms.some(term => normalizedText.includes(term));
66
+ }) || null;
67
+ }
68
+
69
+ module.exports = { OPS, normalized, safeName, safeAlias, normalizeOps, list, save, remove, findInText };
@@ -8,7 +8,7 @@
8
8
 
9
9
  const crypto = require('crypto');
10
10
 
11
- const INTELLIGENCE_CONTRACT = 1;
11
+ const INTELLIGENCE_CONTRACT = 2;
12
12
  // Keep this machine-readable. Human/provider notes belong in commit history;
13
13
  // catalogFreshness appends an ISO time before parsing this value.
14
14
  const PRICE_REVISION = '2026-08-11';
@@ -53,11 +53,6 @@ const MODEL_CATALOG = Object.freeze({
53
53
  provider: 'moonshot', contextWindow: 262144, price: { input: 0.75, output: 3.50, cachedInput: 0.15 },
54
54
  capabilities: ['tools', 'code', 'vision', 'long-context'], toolReliability: 0.86, quality: 0.84,
55
55
  },
56
- 'kimi-k3': {
57
- upstreamId: 'moonshotai/kimi-k3',
58
- provider: 'moonshot', contextWindow: 262144, price: { input: 3.00, output: 15.00, cachedInput: 0.30 },
59
- capabilities: ['tools', 'code', 'planning', 'long-context'], toolReliability: 0.88, quality: 0.91,
60
- },
61
56
  'glm-4.6': {
62
57
  upstreamId: 'z-ai/glm-4.6',
63
58
  provider: 'zai', contextWindow: 204800, price: { input: 0.50, output: 2.00, cachedInput: 0.10 },
@@ -109,7 +104,9 @@ const MODEL_CATALOG = Object.freeze({
109
104
  capabilities: ['tools', 'code', 'planning', 'review'], toolReliability: 0.96, quality: 0.95,
110
105
  },
111
106
  'claude-haiku-4-5': {
112
- upstreamId: 'anthropic/claude-haiku-4.5',
107
+ // ID canônico exposto pelo OpenRouter. O alias interno permanece estável
108
+ // para planos e histórico, mas o gateway precisa receber este identificador.
109
+ upstreamId: 'anthropic/claude-4.5-haiku-20251001',
113
110
  provider: 'anthropic', contextWindow: 200000, price: { input: 1.00, output: 5.00, cachedInput: 0.10 },
114
111
  capabilities: ['tools', 'code', 'planning', 'execution', 'review', 'vision'], toolReliability: 0.94, quality: 0.88,
115
112
  },
@@ -176,16 +173,24 @@ const MODEL_CATALOG = Object.freeze({
176
173
  const MODEL_ALIASES = Object.freeze({
177
174
  deepseek: 'deepseek-v4-flash',
178
175
  'deepseek-v4': 'deepseek-v4-flash',
179
- kimi: 'kimi-k2.7-code-highspeed',
180
176
  glm: 'glm-4.6',
181
177
  mimo: 'mimo-v2.5',
182
178
  'gemini-lite': 'gemini-2.5-flash-lite',
183
179
  'claude-haiku': 'claude-haiku-4-5',
184
180
  });
185
181
 
182
+ // Equipe oficial do modo Automático: especialização e prova objetiva vêm antes
183
+ // de cascatas longas de modelos. Premium fica fora até validação real.
184
+ const OFFICIAL_AUTOMATIC_TEAM = Object.freeze({
185
+ planner: Object.freeze({ model: 'gpt-5.6-luna', label: 'Luna', responsibility: 'planejamento' }),
186
+ executor: Object.freeze({ model: 'deepseek-v4-pro', label: 'DeepSeek Pro', responsibility: 'execução' }),
187
+ corrector: Object.freeze({ model: 'deepseek-v4-pro', label: 'DeepSeek Pro', responsibility: 'correção' }),
188
+ multimodal: Object.freeze({ model: 'mimo-v2.5', fallback: 'gemini-2.5-flash', label: 'MiMo', responsibility: 'imagem, arquivo e documento visual' }),
189
+ verifier: Object.freeze({ model: null, label: 'Verificadores determinísticos', responsibility: 'prova objetiva de entrega' }),
190
+ });
191
+
186
192
  // Contrato dos agentes funcionais. Isto não é fine-tuning: cada papel recebe um
187
- // prompt curto, uma cadeia de modelos por plano e continua sujeito à prova das
188
- // ferramentas. O primeiro modelo permitido é o padrão; os seguintes são fallback.
193
+ // prompt curto e continua sujeito à prova das ferramentas.
189
194
  const AGENT_ROLES = Object.freeze({
190
195
  classifier: Object.freeze({
191
196
  prompt: 'Classifique a intenção e devolva somente a estrutura pedida. Não execute ferramentas nem invente dados.',
@@ -193,35 +198,35 @@ const AGENT_ROLES = Object.freeze({
193
198
  }),
194
199
  planner: Object.freeze({
195
200
  prompt: 'Decomponha o objetivo em etapas executáveis, dependências, riscos, critérios de aceite e provas. Não execute a tarefa.',
196
- models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro', 'deepseek-v4-flash'], pro: ['gpt-5.6-luna', 'deepseek-v4-pro', 'glm-5.2'] }),
201
+ models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['gpt-5.6-luna'], pro: ['gpt-5.6-luna'] }),
197
202
  }),
198
203
  executor: Object.freeze({
199
204
  prompt: 'Execute somente a etapa recebida com as ferramentas autorizadas. Não declare sucesso sem resultado verificável.',
200
- models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-flash', 'deepseek-v4-pro'], pro: ['deepseek-v4-flash', 'deepseek-v4-pro', 'glm-5.2', 'claude-haiku-4-5'] }),
205
+ models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro'], pro: ['deepseek-v4-pro'] }),
201
206
  }),
202
207
  inspector: Object.freeze({
203
208
  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.',
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'] }),
209
+ models: Object.freeze({ free: ['gemini-2.5-flash-lite'], basic: ['gpt-5.6-luna', 'glm-5.2'], pro: ['gpt-5.6-luna', 'glm-5.2'] }),
205
210
  }),
206
211
  corrector: Object.freeze({
207
212
  prompt: 'Corrija apenas falhas confirmadas pelo inspetor, preserve o que funciona e repita as provas afetadas. Não amplie o escopo.',
208
- models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro', 'deepseek-v4-flash'], pro: ['deepseek-v4-pro', 'claude-haiku-4-5', 'glm-5.2'] }),
213
+ models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro'], pro: ['deepseek-v4-pro'] }),
209
214
  }),
210
215
  vision: Object.freeze({
211
216
  prompt: 'Extraia fatos observáveis da imagem e separe leitura, inferência e incerteza. Não invente texto ilegível.',
212
- models: Object.freeze({ free: ['smart'], basic: ['gemini-2.5-flash'], pro: ['gpt-5.6-luna', 'gemini-2.5-flash', 'claude-haiku-4-5'] }),
217
+ models: Object.freeze({ free: ['gemini-2.5-flash'], basic: ['mimo-v2.5', 'gemini-2.5-flash'], pro: ['mimo-v2.5', 'gemini-2.5-flash', 'gpt-5.6-luna'] }),
213
218
  }),
214
219
  document: Object.freeze({
215
220
  prompt: 'Produza conteúdo estruturado e use o motor nativo do formato solicitado. Preserve editabilidade e valide o arquivo gerado.',
216
- models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro', 'deepseek-v4-flash'], pro: ['gpt-5.6-luna', 'deepseek-v4-pro', 'claude-haiku-4-5'] }),
221
+ models: Object.freeze({ free: ['gemini-2.5-flash'], basic: ['mimo-v2.5', 'gemini-2.5-flash'], pro: ['mimo-v2.5', 'gemini-2.5-flash'] }),
217
222
  }),
218
223
  site_builder: Object.freeze({
219
224
  prompt: 'Crie ou edite o site completo, responsivo e acessível. Preserve o existente quando editar e valide a URL publicada.',
220
- models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-flash', 'deepseek-v4-pro'], pro: ['deepseek-v4-pro', 'deepseek-v4-flash', 'claude-haiku-4-5'] }),
225
+ models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro', 'gpt-5.6-luna'], pro: ['deepseek-v4-pro', 'gpt-5.6-luna'] }),
221
226
  }),
222
227
  cloud_box: Object.freeze({
223
228
  prompt: 'Opere a box com mudanças mínimas, registre comandos e valide serviço, porta e URL. Nunca exponha segredos.',
224
- models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-flash'], pro: ['deepseek-v4-flash', 'deepseek-v4-pro', 'claude-haiku-4-5'] }),
229
+ models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro'], pro: ['deepseek-v4-pro', 'gpt-5.6-luna'] }),
225
230
  }),
226
231
  });
227
232
 
@@ -338,6 +343,74 @@ function _cleanText(value, max) {
338
343
  .slice(0, max);
339
344
  }
340
345
 
346
+ // Dados que cruzam a fronteira da máquina (handoff, painel e telemetria) não
347
+ // precisam de e-mails, URLs, caminhos locais ou comandos literais. Mantemos o
348
+ // sinal operacional, mas removemos detalhes que pertencem somente à sessão local.
349
+ function cleanOperationalText(value, max) {
350
+ return _cleanText(value, max || 300)
351
+ .replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '<email>')
352
+ .replace(/https?:\/\/[^\s"']+/gi, '<url>')
353
+ .replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, '<ip>')
354
+ .replace(/[A-Za-z]:\\[^\s"']+/g, '<path>')
355
+ .replace(/\/(?:home|root|opt|var|tmp|Users|mnt)\/[^\s"']+/g, '<path>')
356
+ .replace(/\b(?:nvapi-|AIza)[A-Za-z0-9._-]{12,}\b/gi, '<secret>')
357
+ .replace(/\b[0-9a-f]{32,}\b/gi, '<id>')
358
+ .replace(/\s+/g, ' ').trim().slice(0, max || 300);
359
+ }
360
+
361
+ function _safeToolName(value) {
362
+ const name = String(value || 'unknown').replace(/[^a-z0-9_-]/gi, '_').slice(0, 80);
363
+ return name || 'unknown';
364
+ }
365
+
366
+ function _safeModelId(value) {
367
+ const normalized = normalizeModelId(value);
368
+ return MODEL_CATALOG[normalized] || normalized === 'smart' ? normalized : _safeToolName(value);
369
+ }
370
+
371
+ // Contrato compacto para painéis e comparação de execuções. É deliberadamente
372
+ // agregado: o painel sabe se a missão funcionou e onde falhou, sem receber o
373
+ // pedido do usuário, conteúdo de e-mail, arquivo, comando ou diretório.
374
+ function buildOperationalSummary(input) {
375
+ const x = input || {};
376
+ const usage = x.tokens || x.usage || {};
377
+ const failures = new Map();
378
+ for (const item of (Array.isArray(x.toolErrors) ? x.toolErrors : []).slice(-50)) {
379
+ const tool = _safeToolName(item && item.tool);
380
+ const errorClass = _safeToolName(item && (item.errorClass || item.class || 'unknown'));
381
+ const key = tool + ':' + errorClass;
382
+ const row = failures.get(key) || { tool, errorClass, count: 0, retryable: false };
383
+ row.count++;
384
+ row.retryable = row.retryable || item.retryable === true;
385
+ failures.set(key, row);
386
+ }
387
+ const actions = {};
388
+ for (const action of (Array.isArray(x.actions) ? x.actions : []).slice(0, 200)) {
389
+ const name = _safeToolName(action && action.name);
390
+ actions[name] = (actions[name] || 0) + 1;
391
+ }
392
+ const verification = x.verification || {};
393
+ return {
394
+ version: 1,
395
+ surface: _safeToolName(x.surface || 'cli'),
396
+ model: _safeModelId(x.model || 'smart'),
397
+ status: x.completion && x.completion.ok ? 'completed' : (x.guard ? 'stopped_by_guard' : 'blocked'),
398
+ completionReason: _safeToolName(x.completion && x.completion.reason || 'unknown'),
399
+ steps: Math.max(0, Number(x.steps) || 0),
400
+ durationMs: Math.max(0, Number(x.durationMs) || 0),
401
+ credits: Math.max(0, Number(x.credits) || 0),
402
+ tokens: {
403
+ input: Math.max(0, Number(usage.inTok || usage.input_tokens) || 0),
404
+ output: Math.max(0, Number(usage.outTok || usage.output_tokens) || 0),
405
+ cached: Math.max(0, Number(usage.cachedTok || usage.cacheTok || usage.cached_tokens) || 0),
406
+ },
407
+ actions,
408
+ failures: [...failures.values()].sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 20),
409
+ verification: { total: Math.max(0, Number(verification.total) || 0), passed: Math.max(0, Number(verification.passed) || 0), allOk: verification.allOk === true },
410
+ cache: Object.assign({ hits: 0, misses: 0, entries: 0 }, x.missionCache || {}),
411
+ };
412
+ }
413
+
341
414
  function createMemoryRecord(input) {
342
415
  const x = input || {};
343
416
  const type = MEMORY_TYPES.includes(x.type) ? x.type : 'episodic';
@@ -385,16 +458,16 @@ function createHandoff(input) {
385
458
  return {
386
459
  version: 1,
387
460
  workstreamId: _cleanText(x.workstreamId || 'default', 160),
388
- goal: _cleanText(x.goal, 2000),
461
+ goal: cleanOperationalText(x.goal, 600),
389
462
  status: ['running', 'blocked', 'done', 'paused'].includes(x.status) ? x.status : 'running',
390
- decisions: (x.decisions || []).slice(0, 20).map(v => _cleanText(v, 800)),
463
+ decisions: (x.decisions || []).slice(0, 20).map(v => cleanOperationalText(v, 400)),
391
464
  changedFiles: (x.changedFiles || []).slice(0, 100).map(v => typeof v === 'string'
392
- ? { path: _cleanText(v, 500), hash: '' }
393
- : { path: _cleanText(v.path, 500), hash: _cleanText(v.hash, 128) }),
394
- evidence: (x.evidence || []).slice(0, 30).map(v => _cleanText(v, 1000)),
395
- failedApproaches: (x.failedApproaches || []).slice(0, 20).map(v => _cleanText(v, 1000)),
396
- openQuestions: (x.openQuestions || []).slice(0, 20).map(v => _cleanText(v, 800)),
397
- nextActions: (x.nextActions || []).slice(0, 20).map(v => _cleanText(v, 800)),
465
+ ? { path: cleanOperationalText(v, 180), hash: '' }
466
+ : { path: cleanOperationalText(v.path, 180), hash: _cleanText(v.hash, 128) }),
467
+ evidence: (x.evidence || []).slice(0, 30).map(v => cleanOperationalText(v, 300)),
468
+ failedApproaches: (x.failedApproaches || []).slice(0, 20).map(v => cleanOperationalText(v, 300)),
469
+ openQuestions: (x.openQuestions || []).slice(0, 20).map(v => cleanOperationalText(v, 300)),
470
+ nextActions: (x.nextActions || []).slice(0, 20).map(v => cleanOperationalText(v, 300)),
398
471
  budget: Object.assign({ currency: 'USD', spent: 0, remaining: null }, x.budget || {}),
399
472
  createdAt: x.createdAt || new Date().toISOString(),
400
473
  };
@@ -411,8 +484,14 @@ function agentRoleContract(role, plan, options) {
411
484
  const id = Object.prototype.hasOwnProperty.call(AGENT_ROLES, role) ? role : 'executor';
412
485
  const planId = normalizeAgentPlan(plan);
413
486
  const spec = AGENT_ROLES[id];
414
- const requested = spec.models[planId] || spec.models.free;
415
- const allowedRaw = options && Array.isArray(options.allowedModels) ? options.allowedModels : null;
487
+ // Tarefas complexas nunca usam modelos Flash nos papeis que decidem ou
488
+ // alteram o projeto. Isto vale inclusive no Free: o limite desse plano e de
489
+ // tentativas/orcamento, nao uma degradacao silenciosa da qualidade do papel.
490
+ const complexCore = options && options.complex === true && ['planner', 'executor', 'corrector'].includes(id)
491
+ ? (id === 'planner' ? ['gpt-5.6-luna'] : ['deepseek-v4-pro'])
492
+ : null;
493
+ const requested = complexCore || spec.models[planId] || spec.models.free;
494
+ const allowedRaw = complexCore && planId === 'free' ? null : (options && Array.isArray(options.allowedModels) ? options.allowedModels : null);
416
495
  const allowAll = !allowedRaw || allowedRaw.includes('*');
417
496
  const allowed = allowAll ? null : new Set(allowedRaw.map(normalizeModelId));
418
497
  const candidates = requested.map(normalizeModelId).filter(model => model === 'smart' || MODEL_CATALOG[model])
@@ -436,23 +515,26 @@ function routeModel(task, reliability) {
436
515
  if (t.kind === 'vision') required.add('vision');
437
516
  const candidates = (t.candidates || Object.keys(MODEL_CATALOG)).map(normalizeModelId).filter(id => MODEL_CATALOG[id] && id !== 'smart');
438
517
  const rel = reliability || {};
518
+ const plan = String(t.plan || '').toLowerCase();
519
+ const isPaid = plan && plan !== 'free' && plan !== 'gratuito';
520
+
439
521
  const scored = candidates.map(id => {
440
522
  const m = MODEL_CATALOG[id];
441
523
  const missing = [...required].filter(c => !m.capabilities.includes(c)).length;
442
524
  const observed = rel[id] && Number.isFinite(rel[id].successRate) ? rel[id].successRate : m.toolReliability;
443
525
  const price = m.price.input + m.price.output;
444
- const qualityWeight = t.risk === 'high' ? 0.55 : 0.35;
445
- const costWeight = t.risk === 'high' ? 0.10 : 0.30;
526
+ const qualityWeight = isPaid ? 0.75 : (t.risk === 'high' ? 0.55 : 0.35);
527
+ const costWeight = isPaid ? 0.05 : (t.risk === 'high' ? 0.10 : 0.30);
446
528
  const score = (observed * 0.35) + (m.quality * qualityWeight) + ((1 / (1 + price)) * costWeight) - (missing * 2);
447
529
  return { id, score: +score.toFixed(6), missing };
448
530
  }).sort((a, b) => b.score - a.score);
449
- return { model: scored[0] ? scored[0].id : 'deepseek-v4-flash', ranked: scored };
531
+ return { model: scored[0] ? scored[0].id : (isPaid ? 'deepseek-v4-pro' : 'deepseek-v4-flash'), ranked: scored };
450
532
  }
451
533
 
452
534
  module.exports = {
453
535
  INTELLIGENCE_CONTRACT, PRICE_REVISION, MODEL_CATALOG, MODEL_ALIASES, DEFAULT_PRICE,
454
- MEMORY_TYPES, MEMORY_TRUST, CONTEXT_SOURCES, AGENT_ROLES,
536
+ MEMORY_TYPES, MEMORY_TRUST, CONTEXT_SOURCES, OFFICIAL_AUTOMATIC_TEAM, AGENT_ROLES,
455
537
  normalizeModelId, modelInfo, legacyPriceMap, normalizeUsage, estimateCostUsd, catalogFreshness,
456
- createContextLedger, createMemoryRecord, scoreMemoryRecord, createHandoff,
538
+ createContextLedger, createMemoryRecord, scoreMemoryRecord, createHandoff, cleanOperationalText, buildOperationalSummary,
457
539
  normalizeAgentPlan, agentRoleContract, routeModel,
458
540
  };
package/lib/meta.js CHANGED
@@ -973,6 +973,10 @@ async function _llmText({ token, model, system, user, maxTokens = 1200, creditBu
973
973
  // "pensador" numa análise FOCADA (sem ferramentas, sem loop) com o erro + o código
974
974
  // dos arquivos citados, e devolve um diagnóstico + fix EXATO pro executor aplicar.
975
975
  async function escalate(buildErr, projDir, thinker, token) {
976
+ // Diagnóstico de falha repetida não segue a escolha genérica de "pensador":
977
+ // somente o GPT SOL pode propor causa/fix. O executor continua sendo quem
978
+ // aplica a alteração e os gates continuam exigindo prova posterior.
979
+ const diagnostician = require('./evolution-proposal').diagnosticianFor(thinker);
976
980
  // ── DIAGNÓSTICO ANTES DE ESCALAR (v0.52): em vez de só chutar pelo TEXTO do erro, o TS
977
981
  // INVESTIGA o sistema real com sondas só-leitura (hipótese→sonda→verificação adversarial)
978
982
  // e entrega a CAUSA RAIZ ao pensador. Só na escalação (já estamos travados) → custo contido.
@@ -980,7 +984,7 @@ async function escalate(buildErr, projDir, thinker, token) {
980
984
  let diagBlock = '', diagCred = 0;
981
985
  if (process.env.TS_META_DIAGNOSE !== '0') {
982
986
  try {
983
- const dr = await require('./diagnose').diagnose(String(buildErr).slice(0, 1400), { token, cwd: projDir, model: thinker, maxRounds: Number(process.env.TS_META_DIAGNOSE_ROUNDS) || 4 });
987
+ const dr = await require('./diagnose').diagnose(String(buildErr).slice(0, 1400), { token, cwd: projDir, model: diagnostician, maxRounds: Number(process.env.TS_META_DIAGNOSE_ROUNDS) || 4 });
984
988
  diagCred = dr.credits || 0;
985
989
  if (dr.status === 'solved' && dr.rootCause) diagBlock = `\n\nDIAGNÓSTICO (o TS investigou o sistema com sondas só-leitura${dr.verified ? ', causa VERIFICADA' : ''}):\nCAUSA RAIZ: ${dr.rootCause}\nFIX apontado: ${dr.fix || '—'}\n(Use isto como base — é evidência do sistema real, não palpite.)`;
986
990
  } catch (_) {}
@@ -991,7 +995,7 @@ async function escalate(buildErr, projDir, thinker, token) {
991
995
  const sys = 'Você é um engenheiro sênior de DEBUG. Recebe o ERRO REAL de um build, um DIAGNÓSTICO do sistema (se houver) e o código dos arquivos citados. '
992
996
  + 'Se houver DIAGNÓSTICO, PARTA DELE (é evidência do sistema real). Diga a CAUSA-RAIZ em 1-2 linhas e depois o FIX EXATO e mínimo: em QUAL arquivo, QUAL linha/trecho, e o QUE trocar (mostre o antes→depois curto). '
993
997
  + 'NÃO reescreva o app inteiro, só o necessário pra compilar. Seja preciso e direto.';
994
- const r = await _llmText({ token, model: thinker, system: sys, user: `ERRO DO BUILD:\n${String(buildErr).slice(0, 2500)}${diagBlock}\n\nCÓDIGO RELEVANTE:${ctx.slice(0, 6000)}`, maxTokens: 1200 });
998
+ const r = await _llmText({ token, model: diagnostician, system: sys, user: `ERRO DO BUILD:\n${String(buildErr).slice(0, 2500)}${diagBlock}\n\nCÓDIGO RELEVANTE:${ctx.slice(0, 6000)}`, maxTokens: 1200 });
995
999
  return { hint: (diagBlock ? diagBlock.trim() + '\n\n' : '') + r.text, credits: (r.credits || 0) + diagCred };
996
1000
  }
997
1001
 
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const core = require('./core');
5
+
6
+ /**
7
+ * Módulo de Auditoria Privada do Dono para sessões de teste e homologação.
8
+ * Opt-in, temporário, estritamente sanitizado.
9
+ */
10
+
11
+ function sanitizeAuditString(value, allowContent = false) {
12
+ if (typeof value !== 'string') return '';
13
+ let str = core.redactSecrets ? core.redactSecrets(value) : value;
14
+
15
+ // Remoção incondicional de segredos e tokens sensíveis
16
+ str = str
17
+ .replace(/\b(?:Bearer\s+)[A-Za-z0-9._~+/-]+=*/gi, 'Bearer [REDACTED]')
18
+ .replace(/\b(?:ya29\.[A-Za-z0-9_-]+)/gi, '[GOOGLE_OAUTH_TOKEN]')
19
+ .replace(/\b(?:ghp_|gho_|github_pat_)[A-Za-z0-9_]{16,}/gi, '[GITHUB_TOKEN]')
20
+ .replace(/\b(?:nvapi-|AIza|sk-|xox[baprs]-)[A-Za-z0-9._-]{10,}/gi, '[API_KEY]')
21
+ .replace(/-----BEGIN[ A-Z0-9_-]*PRIVATE KEY-----[\s\S]*?-----END[ A-Z0-9_-]*PRIVATE KEY-----/gi, '[PRIVATE_KEY]')
22
+ .replace(/(?:password|passwd|secret|token|authorization|auth_token)\s*[:=]\s*["']?[^"',\s]+["']?/gi, '$1=[REDACTED]');
23
+
24
+ if (!allowContent) {
25
+ // Mascara URLs completas com parâmetros e e-mails se não for incidente com autorização explícita
26
+ str = str
27
+ .replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '<email>')
28
+ .replace(/https?:\/\/[^\s"']+/gi, '<url>');
29
+ }
30
+
31
+ return str.replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 1000);
32
+ }
33
+
34
+ function isOwnerAuditActive(options = {}, env = process.env) {
35
+ if (options.ownerAudit === true) return true;
36
+ const envVal = String(env.TS_OWNER_AUDIT || '').toLowerCase();
37
+ return envVal === '1' || envVal === 'true' || envVal === 'on';
38
+ }
39
+
40
+ class OwnerAuditSession {
41
+ constructor(options = {}) {
42
+ this.sessionId = options.sessionId || crypto.randomBytes(8).toString('hex');
43
+ this.startedAt = Date.now();
44
+ this.appVersion = options.appVersion || '0.97.29';
45
+ this.surface = options.surface || 'cli';
46
+ this.accountType = 'owner';
47
+ this.explicitContentAuthorization = options.explicitContentAuthorization === true;
48
+ this.records = [];
49
+ }
50
+
51
+ recordStep({ stage, tool, durationMs, result, recovered, costTokens, costUsd, meta = {} }) {
52
+ const sanitizedMeta = {};
53
+ if (meta && typeof meta === 'object') {
54
+ for (const [k, v] of Object.entries(meta)) {
55
+ if (typeof v === 'string') {
56
+ sanitizedMeta[k] = sanitizeAuditString(v, this.explicitContentAuthorization);
57
+ } else if (typeof v === 'number' || typeof v === 'boolean') {
58
+ sanitizedMeta[k] = v;
59
+ }
60
+ }
61
+ }
62
+
63
+ const entry = {
64
+ id: crypto.randomBytes(6).toString('hex'),
65
+ timestamp: Date.now(),
66
+ stage: String(stage || 'step').slice(0, 50),
67
+ tool: String(tool || 'system').slice(0, 80),
68
+ durationMs: Number(durationMs) || 0,
69
+ result: result === 'success' || result === true ? 'success' : (result === 'blocked' ? 'blocked' : 'failed'),
70
+ recovered: recovered === true,
71
+ version: this.appVersion,
72
+ costTokens: Number(costTokens) || 0,
73
+ costUsd: Number(costUsd) || 0,
74
+ meta: sanitizedMeta
75
+ };
76
+
77
+ this.records.push(entry);
78
+ if (this.records.length > 500) {
79
+ this.records.shift();
80
+ }
81
+ return entry;
82
+ }
83
+
84
+ getSummary() {
85
+ const totalDuration = this.records.reduce((sum, r) => sum + r.durationMs, 0);
86
+ const totalCostTokens = this.records.reduce((sum, r) => sum + r.costTokens, 0);
87
+ const totalCostUsd = this.records.reduce((sum, r) => sum + r.costUsd, 0);
88
+ const successCount = this.records.filter(r => r.result === 'success').length;
89
+ const failedCount = this.records.filter(r => r.result === 'failed').length;
90
+ const recoveredCount = this.records.filter(r => r.recovered).length;
91
+
92
+ return {
93
+ sessionId: this.sessionId,
94
+ appVersion: this.appVersion,
95
+ surface: this.surface,
96
+ totalSteps: this.records.length,
97
+ successCount,
98
+ failedCount,
99
+ recoveredCount,
100
+ totalDurationMs: totalDuration,
101
+ totalCostTokens,
102
+ totalCostUsd: Number(totalCostUsd.toFixed(6)),
103
+ records: [...this.records]
104
+ };
105
+ }
106
+ }
107
+
108
+ module.exports = {
109
+ isOwnerAuditActive,
110
+ sanitizeAuditString,
111
+ OwnerAuditSession
112
+ };
package/lib/providers.js CHANGED
@@ -59,7 +59,10 @@ const PROVEDORES = {
59
59
  openrouter: {
60
60
  nome: 'OpenRouter',
61
61
  baseUrl: 'https://openrouter.ai/api/v1',
62
- modeloPadrao: 'deepseek/deepseek-chat',
62
+ // O padrão precisa refletir o produto: Flash fica restrito ao Free no
63
+ // roteador do TS; para uma conexão OpenRouter sem modelo explícito, use o
64
+ // executor oficial pago em vez do legado deepseek-chat.
65
+ modeloPadrao: 'deepseek/deepseek-v4-pro',
63
66
  prefixo: /^sk-or-/,
64
67
  gratuito: false,
65
68
  limite: 'free tier com rate limit frequente',
package/lib/recovery.js CHANGED
@@ -16,10 +16,22 @@ const STRATEGIES = {
16
16
  diag: 'confirme o nome EXATO do módulo/binário que faltou na mensagem de erro',
17
17
  fix: 'instale-o: Node `npm i <pkg>` · Python `pip install <pkg>` · sistema `sudo DEBIAN_FRONTEND=noninteractive apt-get install -y <pkg>` (timeout_s alto). Depois re-tente o comando original.',
18
18
  },
19
+ invalid_command: {
20
+ diag: 'leia a mensagem e confirme o shell/sistema atual; no Windows cmd.exe não aceita grep, tail, ls ou cmdlets PowerShell soltos',
21
+ fix: 'não repita a mesma linha. Use a ferramenta de arquivo quando houver uma, ou refaça no dialeto correto: Windows `dir`/`findstr` ou `powershell -NoProfile -Command "..."`; Linux o comando POSIX correspondente.',
22
+ },
23
+ invalid_workdir: {
24
+ diag: 'confirme a pasta atual com info_sistema/listar_diretorio e localize o projeto com buscar_arquivos; não suponha que o caminho existe',
25
+ fix: 'mude para a pasta encontrada com mudar_diretorio e só então execute novamente. Não tente sair da raiz autorizada nem invente caminho absoluto.',
26
+ },
19
27
  package_lock: {
20
28
  diag: 'veja qual processo segura o lock (apt/dpkg/npm) — NÃO o mate no meio',
21
29
  fix: 'espere o processo dono do lock terminar e re-tente. Se o dpkg foi interrompido antes: `sudo dpkg --configure -a`. Nunca apague o lockfile na força.',
22
30
  },
31
+ resource_locked: {
32
+ diag: 'outro Terminal Smart está alterando o mesmo arquivo; não leia/escreva uma cópia concorrente',
33
+ fix: 'aguarde alguns segundos e tente a MESMA alteração uma vez. Se continuar ocupado, informe o arquivo e peça para concluir/cancelar a outra missão; nunca force remoção do lock.',
34
+ },
23
35
  permission_denied: {
24
36
  diag: 'cheque dono e modo do alvo: `ls -l <caminho>` (Linux) / atributos do arquivo',
25
37
  fix: 'ajuste o dono/modo do arquivo ESPECÍFICO (`chown user:grp <arquivo>` / `chmod u+w <arquivo>`) ou use sudo só onde apropriado. NUNCA `chmod 777` nem recursivo na raiz.',