primocode 8.14.0 → 8.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/primocode.js CHANGED
@@ -142,142 +142,6 @@ function completarSlash(line) {
142
142
  // O layout sai em texto PLANO, com as marcas de onde pintar. Assim os dois
143
143
  // pintores (o normal e o do ultracode animado) desenham exatamente a mesma
144
144
  // régua — sem duas contas de posição para manter em sincronia.
145
- function layoutEffort(idx) {
146
- const cols = Math.max(52, Math.min(process.stdout.columns || 100, 104));
147
- const rotulos = effort.NOMES;
148
- const larguras = rotulos.map((n) => Math.max(n.length + 3, 9));
149
- const posicoes = [];
150
- let x = 23; // recuo da régua, igual ao do mockup
151
- for (let i = 0; i < rotulos.length; i++) {
152
- posicoes.push(x + Math.floor(larguras[i] / 2));
153
- x += larguras[i];
154
- }
155
- const fimRegua = x;
156
- const marca = posicoes[idx];
157
-
158
- let trilha = '';
159
- for (let k = 23; k < fimRegua; k++) {
160
- if (k === marca) trilha += '▲';
161
- else if (k === fimRegua - 12) trilha += '┆'; // ultracode é um degrau à parte
162
- else trilha += '─';
163
- }
164
-
165
- let nomes = ' '.repeat(23);
166
- let deNome = 0, ateNome = 0;
167
- for (let i = 0; i < rotulos.length; i++) {
168
- const nome = rotulos[i];
169
- const pad = Math.floor((larguras[i] - nome.length) / 2);
170
- if (i === idx) { deNome = nomes.length + pad; ateNome = deNome + nome.length; }
171
- nomes += ' '.repeat(pad) + nome + ' '.repeat(larguras[i] - nome.length - pad);
172
- }
173
-
174
- const nivel = effort.nivelDe(rotulos[idx]);
175
- const vaoExtremos = Math.max(1, fimRegua - 23 - 6 - 7);
176
-
177
- return {
178
- cols,
179
- marca,
180
- destaque: { de: deNome, ate: ateNome },
181
- linhas: [
182
- { tipo: 'regua', texto: '▔'.repeat(cols) },
183
- { tipo: 'titulo', texto: ' Effort' },
184
- { tipo: 'vazio', texto: '' },
185
- { tipo: 'extremos', texto: ' '.repeat(23) + 'Faster' + ' '.repeat(vaoExtremos) + 'Smarter' },
186
- { tipo: 'trilha', texto: ' '.repeat(23) + trilha },
187
- { tipo: 'nomes', texto: nomes },
188
- { tipo: 'resumo', texto: ' '.repeat(23) + nivel.resumo },
189
- { tipo: 'vazio', texto: '' },
190
- { tipo: 'ajuda', texto: ' ←/→ ajusta · Enter confirma · Esc cancela' },
191
- ],
192
- };
193
- }
194
-
195
- // ── O brilho do ultracode ────────────────────────────────────────────────
196
- // No nível máximo o painel inteiro ganha um fundo roxo que ondula, como no
197
- // Claude Code. São faixas verticais de roxo em brilhos diferentes; a onda
198
- // anda para o lado a cada quadro, e o texto fica branco por cima.
199
- //
200
- // Roxos do cubo de 256 cores, ordenados pelo brilho REAL (luminância), do
201
- // mais escuro ao mais claro — a onda escolhe um deles por faixa. Fora da
202
- // lista ficaram os roxos escuros que puxam para o vinho (53-56): quebravam a
203
- // família violeta. Só 256 cores (não truecolor): funciona em qualquer
204
- // terminal, inclusive no Terminal.app do macOS.
205
- const ROXOS = [57, 92, 93, 63, 98, 99, 105, 141];
206
-
207
- function fundoRoxo(x, fase) {
208
- // Duas ondas de períodos diferentes: o encontro delas dá o desenho
209
- // irregular de faixas, em vez de listras repetidas e óbvias.
210
- const faixa = Math.floor(x / 2);
211
- const onda = Math.sin(faixa * 0.55 + fase) * 0.6 + Math.sin(faixa * 0.17 - fase * 0.7) * 0.4;
212
- const i = Math.round((onda + 1) / 2 * (ROXOS.length - 1));
213
- return ROXOS[Math.max(0, Math.min(ROXOS.length - 1, i))];
214
- }
215
-
216
- // Pinta a linha inteira (até a largura do painel) com a onda por baixo.
217
- // `destaque` é o trecho que fica branco e forte (o nível escolhido); o resto
218
- // da linha sai num lilás claro, para o escolhido saltar aos olhos.
219
- function pintarLinhaUltra(texto, largura, fase, opts = {}) {
220
- const base = opts.base || 231; // branco
221
- const apagado = opts.apagado || base;
222
- const destaque = opts.destaque;
223
- let saida = '';
224
- let corFundo = -1;
225
- let corTexto = -1;
226
- let forte = null;
227
-
228
- for (let x = 0; x < largura; x++) {
229
- const fundo = fundoRoxo(x, fase);
230
- if (fundo !== corFundo) { saida += `\x1b[48;5;${fundo}m`; corFundo = fundo; }
231
-
232
- const dentro = destaque && x >= destaque.de && x < destaque.ate;
233
- const querForte = opts.negrito || dentro;
234
- const querCor = dentro ? base : apagado;
235
- if (querForte !== forte) { saida += querForte ? '\x1b[1m' : '\x1b[22m'; forte = querForte; }
236
- if (querCor !== corTexto) { saida += `\x1b[38;5;${querCor}m`; corTexto = querCor; }
237
-
238
- saida += texto[x] || ' ';
239
- }
240
- return saida + '\x1b[0m';
241
- }
242
-
243
- function desenharEffort(idx, fase) {
244
- const L = layoutEffort(idx);
245
- const ultra = effort.NOMES[idx] === 'ultracode';
246
-
247
- if (ultra) {
248
- // No ultracode o painel todo vira fundo animado. Cada linha é pintada
249
- // com a MESMA onda, então as faixas ficam alinhadas de cima a baixo.
250
- const LILAS = 189; // lilás bem claro, para o que não é o escolhido
251
- return L.linhas.map((l) => {
252
- if (l.tipo === 'titulo') return pintarLinhaUltra(l.texto, L.cols, fase, { negrito: true });
253
- if (l.tipo === 'nomes') return pintarLinhaUltra(l.texto, L.cols, fase, { destaque: L.destaque, apagado: LILAS });
254
- if (l.tipo === 'resumo' || l.tipo === 'extremos') return pintarLinhaUltra(l.texto, L.cols, fase, { base: LILAS, apagado: LILAS });
255
- return pintarLinhaUltra(l.texto, L.cols, fase, {});
256
- }).join('\n');
257
- }
258
-
259
- return L.linhas.map((l) => {
260
- const t = l.texto;
261
- switch (l.tipo) {
262
- case 'regua': return c.dim(t);
263
- case 'titulo': return c.bold(c.white(t));
264
- case 'extremos': return c.muted(t);
265
- case 'trilha': {
266
- // O marcador aceso, o resto da trilha recuada.
267
- const antes = t.slice(0, L.marca);
268
- const depois = t.slice(L.marca + 1);
269
- return c.dim(antes) + c.brand('▲') + c.dim(depois);
270
- }
271
- case 'nomes': {
272
- const { de, ate } = L.destaque;
273
- return c.muted(t.slice(0, de)) + c.bold(c.brand(t.slice(de, ate))) + c.muted(t.slice(ate));
274
- }
275
- case 'resumo': return c.dim(t);
276
- case 'ajuda': return c.muted(t);
277
- default: return t;
278
- }
279
- }).join('\n');
280
- }
281
145
 
282
146
  // Desenha, escuta as setas, e devolve o nome escolhido (ou null se cancelou).
283
147
  function sliderEffort(atual) {
@@ -293,7 +157,7 @@ function sliderEffort(atual) {
293
157
 
294
158
  const pintar = (primeira) => {
295
159
  if (!primeira) process.stdout.write(`\x1b[${ALTURA}A\r`);
296
- process.stdout.write(desenharEffort(idx, fase).split('\n').map((l) => '\x1b[2K' + l).join('\n') + '\n');
160
+ process.stdout.write(effort.desenharEffort(idx, fase).split('\n').map((l) => '\x1b[2K' + l).join('\n') + '\n');
297
161
  };
298
162
 
299
163
  // O fundo do ultracode ondula; sair dele apaga a animação na hora, para
@@ -425,10 +289,28 @@ function pastaSegura(dir) {
425
289
  // memory.md dele não carrega neste turno.
426
290
  // 3. projeto novo — fica PENDENTE: age em ~/primocode e só cria a pasta se o
427
291
  // agente for mexer em arquivo. Pedir para abrir o Chrome não cria projeto.
292
+ // Pedido que se resolve DENTRO do estúdio: vídeo, slides, arte, documento,
293
+ // planilha, diagrama. O que sai dali já é guardado pelo próprio estúdio, em
294
+ // ~/.primocode/studio/arquivos — não precisa (e não deve) virar pasta de
295
+ // código em ~/primocode, cheia de memory.md sobre um vídeo.
296
+ const PEDIDO_DE_ESTUDIO = /\b(v[íi]deo|reels?|anima[çc][ãa]o|slides?|apresenta[çc][ãa]o|deck|post|banner|capa|thumbnail|arte|cartaz|planilha|documento|relat[óo]rio|diagrama|fluxograma|narra[çc][ãa]o|est[úu]dio)\b/i;
297
+ // ...a menos que ele também peça código ou página: aí é projeto de verdade.
298
+ const PEDIDO_DE_CODIGO = /\b(html|css|javascript|js|react|next|python|api|site|p[áa]gina|app|aplicativo|script|reposit[óo]rio|c[óo]digo|programa|servidor|banco de dados)\b/i;
299
+
300
+ function ehSoEstudio(texto) {
301
+ const t = String(texto || '');
302
+ return PEDIDO_DE_ESTUDIO.test(t) && !PEDIDO_DE_CODIGO.test(t);
303
+ }
304
+
428
305
  function planWorkDir(userText) {
429
306
  if (['/dir', 'detectada', 'projeto'].includes(state.workDirOrigem)) {
430
307
  return { dir: state.workDir, pending: false, name: state.currentProject };
431
308
  }
309
+ // "Quando eu peço um vídeo, não é pra ficar com projeto. Só vai criar
310
+ // projeto quando tiver um HTML, quando tiver um código."
311
+ if (ehSoEstudio(userText)) {
312
+ return { dir: cfg.ROOT_DIR, pending: false, name: state.currentProject, estudio: true };
313
+ }
432
314
  const proposto = deriveProjectName(userText);
433
315
  const parecido = cfg.findSimilarProject(proposto);
434
316
  if (parecido) return { dir: parecido.dir, pending: false, name: parecido.name, similar: true };
package/lib/act.js CHANGED
@@ -668,6 +668,23 @@ async function runAct(userPrompt, opts) {
668
668
  cfg.appendProjectMemory(pastaAtual, `**Pedido:** ${userPrompt.slice(0, 300)}\n**Resultado:** ${finalText.slice(0, 500)}`);
669
669
  }
670
670
 
671
+ // A memória GERAL é gravada sempre, tenha havido pasta ou não.
672
+ //
673
+ // Antes ela só era escrita se o agente chamasse `remember_general` — e ele
674
+ // quase nunca chamava. Pior: a memória de projeto acima depende de
675
+ // `temPasta`, e tarefa de estúdio (vídeo, slides) não cria pasta nenhuma.
676
+ // Resultado prático: quem só fazia vídeo NUNCA teve memória gravada, de
677
+ // tipo nenhum. O usuário notou: "nunca atualiza os arquivos de memória, e
678
+ // ela deve atualizar independente do projeto".
679
+ //
680
+ // Fica curto de propósito — isto entra em todo pedido seguinte. Uma linha
681
+ // por tarefa concluída, e podada por config.js quando passa do limite.
682
+ if (!subagent && concluiu && finalText.trim()) {
683
+ const pedido = userPrompt.replace(/\s+/g, ' ').trim().slice(0, 160);
684
+ const feito = finalText.replace(/\s+/g, ' ').trim().slice(0, 240);
685
+ cfg.appendGeneralMemory(`- **${pedido}** → ${feito}`);
686
+ }
687
+
671
688
  return {
672
689
  userMessage: { role: 'user', content: userPrompt },
673
690
  assistantMessage: { role: 'assistant', content: finalText },
package/lib/config.js CHANGED
@@ -165,6 +165,30 @@ function appendGeneralMemory(entry) {
165
165
  try {
166
166
  ensureStructure();
167
167
  fs.appendFileSync(GENERAL_MEMORY, `\n### ${today()}\n${entry}\n`, 'utf-8');
168
+ podarMemoriaGeral();
169
+ } catch {}
170
+ }
171
+
172
+ // A memória geral entra em TODO pedido, então tamanho aqui é custo por
173
+ // chamada. Sem poda ela cresceria para sempre e, ironicamente, seria cortada
174
+ // no meio pelo orçamento de tokens — perdendo justo o mais recente. Podando
175
+ // pelo fim, o que sobrevive é o que vale: o começo escrito à mão pelo usuário
176
+ // e as últimas coisas aprendidas.
177
+ const LIMITE_MEMORIA_GERAL = 12000;
178
+
179
+ function podarMemoriaGeral() {
180
+ try {
181
+ const texto = fs.readFileSync(GENERAL_MEMORY, 'utf-8');
182
+ if (texto.length <= LIMITE_MEMORIA_GERAL) return;
183
+ const blocos = texto.split(/\n(?=### )/);
184
+ const cabeca = blocos.shift() || ''; // o template e o que o usuário escreveu
185
+ let corpo = '';
186
+ // De trás para frente: os mais novos entram primeiro e ficam.
187
+ for (let i = blocos.length - 1; i >= 0; i--) {
188
+ if (cabeca.length + corpo.length + blocos[i].length > LIMITE_MEMORIA_GERAL) break;
189
+ corpo = '\n' + blocos[i] + corpo;
190
+ }
191
+ fs.writeFileSync(GENERAL_MEMORY, cabeca + corpo, 'utf-8');
168
192
  } catch {}
169
193
  }
170
194
 
package/lib/effort.js CHANGED
@@ -48,4 +48,181 @@ function pesquisaDe(nome) {
48
48
  return nivelDe(nome).pesquisa;
49
49
  }
50
50
 
51
- module.exports = { NIVEIS, NOMES, normalizar, nivelDe, indiceDe, modeloDe, pesquisaDe };
51
+
52
+ const { c } = require('./ui.js');
53
+
54
+ /* ═══════════════════════════════════════════════════════════════════════
55
+ O painel do /effort — e o fundo que ondula no ultracode.
56
+
57
+ Isto morava dentro do bin/, junto com o loop do programa. Lá não dava
58
+ para desenhar um quadro e OLHAR: o bin sobe o CLI inteiro quando é
59
+ carregado. Aqui é função pura — recebe o índice e a fase, devolve texto
60
+ com cor. Dá para renderizar quadro a quadro num teste, que é como a
61
+ animação foi conferida.
62
+ ═══════════════════════════════════════════════════════════════════════ */
63
+
64
+ function layoutEffort(idx) {
65
+ // Sem teto de largura: o fundo do ultracode preenche o terminal inteiro.
66
+ // Travado em 104 colunas, o painel parava no meio de uma tela larga e o
67
+ // bloco roxo ficava com um degrau escuro do lado — parecia recorte, não
68
+ // fundo. A régua e os rótulos continuam no mesmo lugar; só o pano cresce.
69
+ const cols = Math.max(52, process.stdout.columns || 100);
70
+ const rotulos = NOMES;
71
+ const larguras = rotulos.map((n) => Math.max(n.length + 3, 9));
72
+ const posicoes = [];
73
+ let x = 23; // recuo da régua, igual ao do mockup
74
+ for (let i = 0; i < rotulos.length; i++) {
75
+ posicoes.push(x + Math.floor(larguras[i] / 2));
76
+ x += larguras[i];
77
+ }
78
+ const fimRegua = x;
79
+ const marca = posicoes[idx];
80
+
81
+ let trilha = '';
82
+ for (let k = 23; k < fimRegua; k++) {
83
+ if (k === marca) trilha += '▲';
84
+ else if (k === fimRegua - 12) trilha += '┆'; // ultracode é um degrau à parte
85
+ else trilha += '─';
86
+ }
87
+
88
+ let nomes = ' '.repeat(23);
89
+ let deNome = 0, ateNome = 0;
90
+ for (let i = 0; i < rotulos.length; i++) {
91
+ const nome = rotulos[i];
92
+ const pad = Math.floor((larguras[i] - nome.length) / 2);
93
+ if (i === idx) { deNome = nomes.length + pad; ateNome = deNome + nome.length; }
94
+ nomes += ' '.repeat(pad) + nome + ' '.repeat(larguras[i] - nome.length - pad);
95
+ }
96
+
97
+ const nivel = nivelDe(rotulos[idx]);
98
+ const vaoExtremos = Math.max(1, fimRegua - 23 - 6 - 7);
99
+
100
+ return {
101
+ cols,
102
+ marca,
103
+ destaque: { de: deNome, ate: ateNome },
104
+ linhas: [
105
+ { tipo: 'regua', texto: '▔'.repeat(cols) },
106
+ { tipo: 'titulo', texto: ' Effort' },
107
+ { tipo: 'vazio', texto: '' },
108
+ { tipo: 'extremos', texto: ' '.repeat(23) + 'Faster' + ' '.repeat(vaoExtremos) + 'Smarter' },
109
+ { tipo: 'trilha', texto: ' '.repeat(23) + trilha },
110
+ { tipo: 'nomes', texto: nomes },
111
+ { tipo: 'resumo', texto: ' '.repeat(23) + nivel.resumo },
112
+ { tipo: 'vazio', texto: '' },
113
+ { tipo: 'ajuda', texto: ' ←/→ ajusta · Enter confirma · Esc cancela' },
114
+ ],
115
+ };
116
+ }
117
+
118
+ // ── O brilho do ultracode ────────────────────────────────────────────────
119
+ // No nível máximo o painel inteiro ganha um fundo roxo que ondula, como no
120
+ // Claude Code. São faixas verticais de roxo em brilhos diferentes; a onda
121
+ // anda para o lado a cada quadro, e o texto fica branco por cima.
122
+ //
123
+ // Roxos do cubo de 256 cores, ordenados pelo brilho REAL (luminância), do
124
+ // mais escuro ao mais claro — a onda escolhe um deles por faixa. Fora da
125
+ // lista ficaram os roxos escuros que puxam para o vinho (53-56): quebravam a
126
+ // família violeta. Só 256 cores (não truecolor): funciona em qualquer
127
+ // terminal, inclusive no Terminal.app do macOS.
128
+ // A rampa é ESTREITA de propósito: só violetas vizinhos, do escuro ao médio.
129
+ //
130
+ // A lista antiga ia de 57 a 141 e passava por 63 (azul) e 92 (magenta
131
+ // escuro). Vizinhas na tela, essas cores não leem como uma superfície só:
132
+ // leem como listras — e listra clara demais apaga o texto branco por cima.
133
+ // O fundo tem de parecer um pano roxo respirando atrás do painel, não uma
134
+ // bandeira. Os tons claros ficam de fora por isso.
135
+ const ROXOS = [54, 55, 56, 57, 57, 93, 93, 99, 98, 92, 56, 55];
136
+
137
+ function fundoRoxo(x, fase) {
138
+ // Duas ondas de períodos diferentes: o encontro delas dá o desenho
139
+ // irregular de faixas, em vez de listras repetidas e óbvias.
140
+ //
141
+ // As frequências caíram (0.55 → 0.13) porque as faixas tinham três
142
+ // colunas de largura: de longe aquilo não era um fundo ondulando, era
143
+ // chuviscado. Faixa larga anda devagar pela tela, que é o movimento que
144
+ // se quer ver — o painel respira em vez de piscar.
145
+ const faixa = x / 2;
146
+ const onda = Math.sin(faixa * 0.13 + fase) * 0.62 + Math.sin(faixa * 0.041 - fase * 0.55) * 0.38;
147
+ // A curva ³ empurra os valores para o MEIO da rampa: os extremos (o mais
148
+ // escuro e o mais claro) só aparecem no pico da onda, e o painel passa a
149
+ // maior parte do tempo num roxo médio, onde o texto branco se lê.
150
+ const suave = Math.pow(onda, 3) * 0.55 + onda * 0.45;
151
+ const i = Math.round((suave + 1) / 2 * (ROXOS.length - 1));
152
+ return ROXOS[Math.max(0, Math.min(ROXOS.length - 1, i))];
153
+ }
154
+
155
+ // Pinta a linha inteira (até a largura do painel) com a onda por baixo.
156
+ // `destaque` é o trecho que fica branco e forte (o nível escolhido); o resto
157
+ // da linha sai num lilás claro, para o escolhido saltar aos olhos.
158
+ function pintarLinhaUltra(texto, largura, fase, opts = {}) {
159
+ const base = opts.base || 231; // branco
160
+ const apagado = opts.apagado || base;
161
+ const destaque = opts.destaque;
162
+ let saida = '';
163
+ let corFundo = -1;
164
+ let corTexto = -1;
165
+ let forte = null;
166
+
167
+ for (let x = 0; x < largura; x++) {
168
+ const fundo = fundoRoxo(x, fase);
169
+ if (fundo !== corFundo) { saida += `\x1b[48;5;${fundo}m`; corFundo = fundo; }
170
+
171
+ const dentro = destaque && x >= destaque.de && x < destaque.ate;
172
+ const querForte = opts.negrito || dentro;
173
+ const querCor = dentro ? base : apagado;
174
+ if (querForte !== forte) { saida += querForte ? '\x1b[1m' : '\x1b[22m'; forte = querForte; }
175
+ if (querCor !== corTexto) { saida += `\x1b[38;5;${querCor}m`; corTexto = querCor; }
176
+
177
+ saida += texto[x] || ' ';
178
+ }
179
+ return saida + '\x1b[0m';
180
+ }
181
+
182
+ function desenharEffort(idx, fase) {
183
+ const L = layoutEffort(idx);
184
+ const ultra = NOMES[idx] === 'ultracode';
185
+
186
+ if (ultra) {
187
+ // No ultracode o painel todo vira fundo animado. Cada linha é pintada
188
+ // com a MESMA onda, então as faixas ficam alinhadas de cima a baixo.
189
+ const LILAS = 189; // lilás bem claro, para o que não é o escolhido
190
+ return L.linhas.map((l) => {
191
+ // A régua é uma fileira de "▔". Sobre o roxo ela virava uma
192
+ // listra clara atravessando o topo e cortava o bloco em dois.
193
+ // No ultra ela some e vira fundo liso: a borda de cima do painel
194
+ // passa a ser o próprio degradê, que é o que se quer ver.
195
+ if (l.tipo === 'regua') return pintarLinhaUltra('', L.cols, fase, {});
196
+ if (l.tipo === 'titulo') return pintarLinhaUltra(l.texto, L.cols, fase, { negrito: true });
197
+ if (l.tipo === 'nomes') return pintarLinhaUltra(l.texto, L.cols, fase, { destaque: L.destaque, apagado: LILAS });
198
+ if (l.tipo === 'resumo' || l.tipo === 'extremos') return pintarLinhaUltra(l.texto, L.cols, fase, { base: LILAS, apagado: LILAS });
199
+ return pintarLinhaUltra(l.texto, L.cols, fase, {});
200
+ }).join('\n');
201
+ }
202
+
203
+ return L.linhas.map((l) => {
204
+ const t = l.texto;
205
+ switch (l.tipo) {
206
+ case 'regua': return c.dim(t);
207
+ case 'titulo': return c.bold(c.white(t));
208
+ case 'extremos': return c.muted(t);
209
+ case 'trilha': {
210
+ // O marcador aceso, o resto da trilha recuada.
211
+ const antes = t.slice(0, L.marca);
212
+ const depois = t.slice(L.marca + 1);
213
+ return c.dim(antes) + c.brand('▲') + c.dim(depois);
214
+ }
215
+ case 'nomes': {
216
+ const { de, ate } = L.destaque;
217
+ return c.muted(t.slice(0, de)) + c.bold(c.brand(t.slice(de, ate))) + c.muted(t.slice(ate));
218
+ }
219
+ case 'resumo': return c.dim(t);
220
+ case 'ajuda': return c.muted(t);
221
+ default: return t;
222
+ }
223
+ }).join('\n');
224
+ }
225
+
226
+ module.exports = { NIVEIS, NOMES, normalizar, nivelDe, indiceDe, modeloDe, pesquisaDe,
227
+ layoutEffort, fundoRoxo, pintarLinhaUltra, desenharEffort, ROXOS };
228
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.14.0",
3
+ "version": "8.16.0",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/primocode.js",
11
- "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js"
11
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js && node test/estudio-projeto.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"
package/studio/server.py CHANGED
@@ -649,9 +649,16 @@ def validar_doc(modelo, doc):
649
649
  if problema:
650
650
  return problema
651
651
  dur = c.get("duracao")
652
- if dur is not None and not isinstance(dur, (int, float)):
652
+ if dur is not None and (isinstance(dur, bool) or not isinstance(dur, (int, float))):
653
653
  return ('cena %d com "duracao" que não é número (veio %r). Use número de segundos: '
654
654
  '"duracao": 3' % (i + 1, dur))
655
+ # Duração zero passava: era número, afinal. E um vídeo cujas cenas
656
+ # somam zero é o "vídeo de 0 segundos" que o usuário viu mais de
657
+ # uma vez — arquivo válido, conteúdo inalcançável.
658
+ if isinstance(dur, (int, float)) and not isinstance(dur, bool) and dur <= 0:
659
+ return ('cena %d tem "duracao": %r. Zero (ou negativo) faz o vídeo inteiro durar '
660
+ 'nada — foi assim que saiu um vídeo de 0 segundos. Use pelo menos 1.'
661
+ % (i + 1, dur))
655
662
  for j, e in enumerate(els):
656
663
  if not isinstance(e, dict) or not e.get("tipo"):
657
664
  return 'elemento %d da cena %d sem "tipo" (texto, imagem, lista, numeros, forma...)' % (j + 1, i + 1)
@@ -1265,7 +1272,21 @@ class Handler(SimpleHTTPRequestHandler):
1265
1272
  problema = validar_doc(
1266
1273
  POR_SLUG.get(a.get("ferramenta"), {}).get("modelo"), novo)
1267
1274
  if problema:
1268
- return self.js({"erro": problema}, 400)
1275
+ # As frases de validar_doc foram escritas para a CRIAÇÃO:
1276
+ # elas terminam em "crie de novo" e "refaça". Devolver isso
1277
+ # numa ATUALIZAÇÃO manda o agente fazer exatamente a coisa
1278
+ # errada — e foi o que aconteceu: ele recebia o erro, jogava
1279
+ # fora o vídeo pronto e recomeçava do zero, às vezes saindo
1280
+ # pior. Aqui a instrução é a oposta, e vem por último para
1281
+ # ser a última coisa que ele lê.
1282
+ problema = re.sub(r"(crie de novo|e refaça)\.?", "", problema).strip()
1283
+ return self.js({
1284
+ "erro": problema
1285
+ + ' Isto é uma ATUALIZAÇÃO: NÃO crie outro arquivo e NÃO recomece do zero. '
1286
+ 'Corrija só o que está errado acima e chame studio_atualizar de novo '
1287
+ 'com o MESMO id "%s". O arquivo continua intacto — nada foi perdido.' % a["id"],
1288
+ "id": a["id"],
1289
+ }, 400)
1269
1290
 
1270
1291
  preparar_voz(novo)
1271
1292
  versao = guardar_versao(a)