primocode 9.2.2 → 9.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # PrimoCode v9.2.2
1
+ # PrimoCode v9.2.3
2
2
 
3
3
  Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
4
4
  Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
9
9
  está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
10
10
 
11
11
  ```
12
- ▐▛███▜▌ Primo Code v9.2.2
12
+ ▐▛███▜▌ Primo Code v9.2.3
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
@@ -81,6 +81,31 @@ function rodarPython(script, args, { timeout = 20 * 60000 } = {}) {
81
81
 
82
82
  // ── estudio_projeto ──────────────────────────────────────────────────────
83
83
 
84
+ /**
85
+ * A queixa quando o projeto não aparece — e ela muda conforme o motivo.
86
+ *
87
+ * "não achei o projeto 'undefined'" é o que o agente recebia ao chamar a
88
+ * ferramenta SEM id, e não ensina nada: ele não sabe se inventou o id, se o
89
+ * projeto sumiu, ou se esqueceu o argumento. Visto em produção — o modelo
90
+ * chamou `estudio_plano` sem id e ficou repetindo a chamada.
91
+ */
92
+ function semProjeto(id) {
93
+ if (id == null || String(id).trim() === '') {
94
+ return {
95
+ ok: false,
96
+ error: 'faltou dizer em qual projeto (id).',
97
+ comoResolver: 'crie um com estudio_projeto acao:"criar" e use o id que ele devolve, '
98
+ + 'ou ache o existente com acao:"listar" / acao:"procurar".',
99
+ };
100
+ }
101
+ return {
102
+ ok: false,
103
+ error: `não achei o projeto "${id}".`,
104
+ comoResolver: 'liste com estudio_projeto acao:"listar" e use um id de lá — '
105
+ + 'o id é o nome da pasta (ex: "2026-09-22-anuncio-picpay"), não o título.',
106
+ };
107
+ }
108
+
84
109
  async function ferramentaProjeto(args) {
85
110
  const acao = String(args.acao || 'listar').toLowerCase();
86
111
 
@@ -93,7 +118,7 @@ async function ferramentaProjeto(args) {
93
118
 
94
119
  if (acao === 'abrir' || acao === 'ler') {
95
120
  const p = projeto.ler(args.id);
96
- if (!p) return { ok: false, error: `não achei o projeto "${args.id}". Use acao:"procurar".` };
121
+ if (!p) return semProjeto(args.id);
97
122
  const pl = projeto.lerPlano(args.id);
98
123
  return {
99
124
  ok: true, projeto: p,
@@ -142,7 +167,7 @@ function listarArquivos(pasta) {
142
167
 
143
168
  async function ferramentaPlano(args) {
144
169
  const p = projeto.ler(args.id);
145
- if (!p) return { ok: false, error: `não achei o projeto "${args.id}".` };
170
+ if (!p) return semProjeto(args.id);
146
171
 
147
172
  if (args.voltarPara) return projeto.voltarPara(args.id, args.voltarPara);
148
173
 
@@ -193,7 +218,7 @@ async function ferramentaPlano(args) {
193
218
  async function ferramentaMidia(args) {
194
219
  const acao = String(args.acao || 'imagem').toLowerCase();
195
220
  const p = args.id ? projeto.ler(args.id) : null;
196
- if (args.id && !p) return { ok: false, error: `não achei o projeto "${args.id}".` };
221
+ if (args.id && !p) return semProjeto(args.id);
197
222
 
198
223
  if (acao === 'fontes') {
199
224
  if (args.conferir) return assets.conferirFonte(args.conferir);
@@ -282,7 +307,7 @@ async function ferramentaNarrar(args) {
282
307
  if (args.vozes) return rodarPython('narrar.py', ['vozes'], { timeout: 30000 });
283
308
 
284
309
  const p = projeto.ler(args.id);
285
- if (!p) return { ok: false, error: `não achei o projeto "${args.id}".` };
310
+ if (!p) return semProjeto(args.id);
286
311
  const texto = String(args.texto || '').trim();
287
312
  if (!texto) return { ok: false, error: 'não há texto para narrar.' };
288
313
 
@@ -313,7 +338,7 @@ async function ferramentaNarrar(args) {
313
338
  async function ferramentaVideo(args, ctx = {}) {
314
339
  const acao = String(args.acao || 'medir').toLowerCase();
315
340
  const p = projeto.ler(args.id);
316
- if (!p) return { ok: false, error: `não achei o projeto "${args.id}".` };
341
+ if (!p) return semProjeto(args.id);
317
342
 
318
343
  const pronto = video.conferir();
319
344
  if (!pronto.ok) return pronto;
@@ -436,7 +461,7 @@ async function transcrever(p, alvo, ctx) {
436
461
 
437
462
  async function ferramentaRender(args, ctx = {}) {
438
463
  const p = projeto.ler(args.id);
439
- if (!p) return { ok: false, error: `não achei o projeto "${args.id}".` };
464
+ if (!p) return semProjeto(args.id);
440
465
  const pl = projeto.lerPlano(args.id);
441
466
  if (!pl) return { ok: false, error: 'não há plano neste projeto.' };
442
467
 
@@ -103,13 +103,38 @@ function lerJSON(arquivo, padrao = null) {
103
103
  try { return JSON.parse(fs.readFileSync(arquivo, 'utf8')); } catch { return padrao; }
104
104
  }
105
105
 
106
- const pastaDe = (id) => path.join(PROJETOS, id);
106
+ /**
107
+ * A pasta de um projeto — ou null, quando o id não serve.
108
+ *
109
+ * ── POR QUE ISTO VALIDA, EM VEZ DE SÓ CONCATENAR ────────────────────────
110
+ * O `id` vem do MODELO, e o modelo erra de dois jeitos que doem aqui:
111
+ *
112
+ * 1. Não manda nada. Visto em produção: o agente chamou `estudio_plano` sem
113
+ * `id`, o `path.join` recebeu `undefined` e a tool explodiu com
114
+ * 'The "path" argument must be of type string. Received undefined' — uma
115
+ * mensagem que não fala de projeto nenhum, e que o agente não tem como
116
+ * corrigir porque não diz o que faltou. A checagem de "achei o projeto?"
117
+ * vinha DEPOIS, e nunca era alcançada.
118
+ *
119
+ * 2. Manda um id com caminho dentro ("../../etc/passwd", "/tmp/x"). O
120
+ * `path.join` obedece: sobe de pasta e sai da casa dos projetos. O id de
121
+ * um projeto é um nome de pasta, não um caminho — então tudo que tem
122
+ * barra ou `..` é recusado, em vez de resolvido.
123
+ */
124
+ function pastaDe(id) {
125
+ if (typeof id !== 'string' || !id.trim()) return null;
126
+ const limpo = id.trim();
127
+ if (limpo.includes('/') || limpo.includes('\\') || limpo.includes('..')) return null;
128
+ return path.join(PROJETOS, limpo);
129
+ }
107
130
 
108
131
  /** O meta de um projeto, ou null. */
109
132
  function ler(id) {
110
- const meta = lerJSON(path.join(pastaDe(id), 'projeto.json'));
133
+ const pasta = pastaDe(id);
134
+ if (!pasta) return null;
135
+ const meta = lerJSON(path.join(pasta, 'projeto.json'));
111
136
  if (!meta) return null;
112
- return { ...meta, pasta: pastaDe(id) };
137
+ return { ...meta, pasta };
113
138
  }
114
139
 
115
140
  function salvar(id, mudancas) {
@@ -142,17 +167,21 @@ function listar({ limite = 50 } = {}) {
142
167
  * perde um caderno quando o processo morre no meio.
143
168
  */
144
169
  function anotar(id, tipo, texto, extra = {}) {
170
+ const pasta = pastaDe(id);
171
+ if (!pasta) return false;
145
172
  const linha = JSON.stringify({ quando: agora(), tipo, texto: String(texto || ''), ...extra });
146
173
  try {
147
- fs.mkdirSync(pastaDe(id), { recursive: true });
148
- fs.appendFileSync(path.join(pastaDe(id), 'caderno.jsonl'), linha + '\n', 'utf8');
174
+ fs.mkdirSync(pasta, { recursive: true });
175
+ fs.appendFileSync(path.join(pasta, 'caderno.jsonl'), linha + '\n', 'utf8');
149
176
  return true;
150
177
  } catch { return false; }
151
178
  }
152
179
 
153
180
  function caderno(id, { ultimas = 40 } = {}) {
181
+ const pasta = pastaDe(id);
182
+ if (!pasta) return [];
154
183
  let bruto = '';
155
- try { bruto = fs.readFileSync(path.join(pastaDe(id), 'caderno.jsonl'), 'utf8'); } catch { return []; }
184
+ try { bruto = fs.readFileSync(path.join(pasta, 'caderno.jsonl'), 'utf8'); } catch { return []; }
156
185
  const linhas = bruto.split('\n').filter(Boolean).slice(-ultimas);
157
186
  return linhas.map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
158
187
  }
@@ -193,9 +222,11 @@ function procurar(texto, { limite = 5 } = {}) {
193
222
 
194
223
  // ── o plano ──────────────────────────────────────────────────────────────
195
224
 
196
- const planoDe = (id) => path.join(pastaDe(id), 'plano.json');
225
+ // null quando o id não serve — e `lerJSON(null)` devolve null, que é o que
226
+ // todo mundo aqui já sabe tratar.
227
+ const planoDe = (id) => { const p = pastaDe(id); return p ? path.join(p, 'plano.json') : null; };
197
228
 
198
- function lerPlano(id) { return lerJSON(planoDe(id)); }
229
+ function lerPlano(id) { const a = planoDe(id); return a ? lerJSON(a) : null; }
199
230
 
200
231
  /**
201
232
  * Grava o plano e guarda a versão anterior.
@@ -205,9 +236,11 @@ function lerPlano(id) { return lerJSON(planoDe(id)); }
205
236
  * nada é sobrescrito sem uma cópia ficar para trás.
206
237
  */
207
238
  function salvarPlano(id, plano, motivo = '') {
239
+ const pasta = pastaDe(id);
240
+ if (!pasta) return { ok: false, error: `Projeto "${id}" não existe.` };
208
241
  const anterior = lerPlano(id);
209
242
  if (anterior) {
210
- const versoes = path.join(pastaDe(id), 'versoes');
243
+ const versoes = path.join(pasta, 'versoes');
211
244
  fs.mkdirSync(versoes, { recursive: true });
212
245
  const n = fs.readdirSync(versoes).filter((f) => f.endsWith('.json')).length + 1;
213
246
  escreverJSON(path.join(versoes, `${String(n).padStart(3, '0')}.json`), anterior);
@@ -218,12 +251,16 @@ function salvarPlano(id, plano, motivo = '') {
218
251
  }
219
252
 
220
253
  function versoes(id) {
221
- const dir = path.join(pastaDe(id), 'versoes');
254
+ const pasta = pastaDe(id);
255
+ if (!pasta) return [];
256
+ const dir = path.join(pasta, 'versoes');
222
257
  try { return fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort(); } catch { return []; }
223
258
  }
224
259
 
225
260
  function voltarPara(id, versao) {
226
- const arquivo = path.join(pastaDe(id), 'versoes', versao);
261
+ const pasta = pastaDe(id);
262
+ if (!pasta) return { ok: false, error: `Projeto "${id}" não existe.` };
263
+ const arquivo = path.join(pasta, 'versoes', String(versao || '').replace(/[^\w.-]+/g, ''));
227
264
  const plano = lerJSON(arquivo);
228
265
  if (!plano) return { ok: false, error: `Versão "${versao}" não existe.` };
229
266
  salvarPlano(id, plano, `voltou para a versão ${versao}`);
@@ -232,9 +269,11 @@ function voltarPara(id, versao) {
232
269
 
233
270
  /** Onde guardar uma mídia baixada, com nome que não colide. */
234
271
  function caminhoDeMidia(id, nome) {
272
+ const pasta = pastaDe(id);
273
+ if (!pasta) return null;
235
274
  const limpo = String(nome || 'arquivo').replace(/[^\w.-]+/g, '-').slice(-60);
236
275
  const marca = crypto.randomBytes(3).toString('hex');
237
- return path.join(pastaDe(id), 'midia', `${marca}-${limpo}`);
276
+ return path.join(pasta, 'midia', `${marca}-${limpo}`);
238
277
  }
239
278
 
240
279
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "9.2.2",
3
+ "version": "9.2.3",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). 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": {