primocode 9.8.0-beta.16 → 9.8.0-beta.17
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 +2 -2
- package/lib/estudio/assets.js +34 -0
- package/lib/estudio/composicao.js +35 -9
- package/lib/estudio/edicao.js +1 -1
- package/lib/estudio/index.js +33 -4
- package/lib/estudio/remotion.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v9.8.0-beta.
|
|
1
|
+
# PrimoCode v9.8.0-beta.17
|
|
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.8.0-beta.
|
|
12
|
+
▐▛███▜▌ Primo Code v9.8.0-beta.17
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/lib/estudio/assets.js
CHANGED
|
@@ -120,7 +120,41 @@ const COLECOES = [
|
|
|
120
120
|
* O SVG vem com `currentColor` no traço para a cor ser decidida na cena — um
|
|
121
121
|
* ícone com cor cravada no arquivo obriga a baixar de novo para mudar de cor.
|
|
122
122
|
*/
|
|
123
|
+
/* ── EMOJI 3D ─────────────────────────────────────────────────────────
|
|
124
|
+
* "Os emojis, quero emojis da Apple." (25/09/2026) Os da Apple têm direito
|
|
125
|
+
* autoral e não podem ir num vídeo publicado; os Fluent 3D da Microsoft têm
|
|
126
|
+
* licença MIT e o mesmo visual brilhante e com volume. "fluent-3d:robot" é o
|
|
127
|
+
* PNG do repositório oficial (pelo jsDelivr), embutido como <img> — o resto
|
|
128
|
+
* do render trata igual a um ícone SVG. Sem o 3D, cai no emoji plano. */
|
|
129
|
+
const cache3d = new Map();
|
|
130
|
+
async function emoji3d(nome, { pasta = null } = {}) {
|
|
131
|
+
if (cache3d.has(nome)) return cache3d.get(nome);
|
|
132
|
+
const palavras = nome.split('-');
|
|
133
|
+
const pastas = [
|
|
134
|
+
palavras.join(' ').replace(/^./, (c) => c.toUpperCase()),
|
|
135
|
+
palavras.map((w) => w[0].toUpperCase() + w.slice(1)).join(' '),
|
|
136
|
+
];
|
|
137
|
+
for (const pastaGit of pastas) {
|
|
138
|
+
const url = `https://cdn.jsdelivr.net/gh/microsoft/fluentui-emoji@main/assets/${encodeURIComponent(pastaGit)}/3D/${palavras.join('_')}_3d.png`;
|
|
139
|
+
try {
|
|
140
|
+
const png = await pegar(url, { binario: true });
|
|
141
|
+
if (png && png.length > 1000 && png[0] === 0x89) {
|
|
142
|
+
if (pasta) guardar(path.join(pasta, 'midia', 'icones', `fluent-3d-${nome}.png`), png, { licenca: 'MIT (Microsoft Fluent UI Emoji)', origem: url });
|
|
143
|
+
const r = { ok: true, nome: 'fluent-3d:' + nome,
|
|
144
|
+
svg: `<img src="data:image/png;base64,${png.toString('base64')}" style="width:100%;height:100%;object-fit:contain;display:block" />` };
|
|
145
|
+
cache3d.set(nome, r);
|
|
146
|
+
return r;
|
|
147
|
+
}
|
|
148
|
+
} catch { /* a próxima grafia */ }
|
|
149
|
+
}
|
|
150
|
+
const plano = await icone('fluent-emoji-flat:' + nome, { pasta });
|
|
151
|
+
const r = plano.ok ? { ...plano, nome: 'fluent-3d:' + nome } : plano;
|
|
152
|
+
cache3d.set(nome, r);
|
|
153
|
+
return r;
|
|
154
|
+
}
|
|
155
|
+
|
|
123
156
|
async function icone(nome, { pasta = null } = {}) {
|
|
157
|
+
if (/^fluent-3d:/i.test(String(nome))) return emoji3d(String(nome).split(':')[1], { pasta });
|
|
124
158
|
const m = String(nome || '').match(/^([a-z0-9-]+):([a-z0-9-]+)$/i);
|
|
125
159
|
if (!m) return { ok: false, error: 'o ícone é "coleção:nome", como "lucide:zap"' };
|
|
126
160
|
const url = `https://api.iconify.design/${m[1]}/${m[2]}.svg?color=currentColor`;
|
|
@@ -64,7 +64,8 @@ function paleta(marca = {}) {
|
|
|
64
64
|
/* ── a letra que cabe ─────────────────────────────────────────────────────
|
|
65
65
|
* Largura média do caractere em "em" por família. Não é exata — é o
|
|
66
66
|
* bastante para escolher o maior tamanho que cabe em N linhas, com folga. */
|
|
67
|
-
|
|
67
|
+
// Com folga: medido em 25/09, 0,52 deixava "Fundadores" quebrar no meio no card.
|
|
68
|
+
const LARGURA = { condensada: 0.46, display: 0.6, texto: 0.58, serifa: 0.54 };
|
|
68
69
|
const FAMILIA_TIPO = (f) => (/condensed|narrow|anton|oswald|bebas|teko|league gothic/i.test(f) ? 'condensada'
|
|
69
70
|
: /serif|fraunces|playfair|garamond|lora|newsreader|spectral|baskerville|dm serif/i.test(f) ? 'serifa'
|
|
70
71
|
: /black|bricolage|archivo|sora|grotesk|syne|unbounded/i.test(f) ? 'display' : 'texto');
|
|
@@ -183,7 +184,7 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
|
|
|
183
184
|
const fotoFundo = fundos[i];
|
|
184
185
|
if (fotoFundo && p.tipo !== 'imagem') {
|
|
185
186
|
const ehV = fotoFundo.tipo === 'video';
|
|
186
|
-
els.push({ tipo: ehV ? 'video' : 'imagem', src: fotoFundo.src, ...(ehV ? { volume: 0 } : {}), x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover',
|
|
187
|
+
els.push({ tipo: ehV ? 'video' : 'imagem', src: fotoFundo.src, ...(ehV ? { volume: 0 } : {}), x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover', foco: 'center 28%',
|
|
187
188
|
anima: { escala: [{ t: 0, v: variante ? 1.14 : 1.02 }, { t: dur, v: variante ? 1.02 : 1.14, e: 'suave' }] } });
|
|
188
189
|
els.push({ tipo: 'forma', forma: 'retangulo', x: 0, y: 0, largura: '100%', altura: '100%',
|
|
189
190
|
fundo: { tipo: 'gradiente', cores: [alfa(cor.fundo, 0.93), alfa(cor.fundo, 0.72), alfa(cor.fundo, 0.55)], angulo: 90 } });
|
|
@@ -192,7 +193,8 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
|
|
|
192
193
|
const canto = [[W - R * 0.55, -R * 0.45], [-R * 0.45, H - R * 0.5], [W - R * 0.4, H - R * 0.55], [-R * 0.5, -R * 0.4]][i % 4];
|
|
193
194
|
els.push({
|
|
194
195
|
tipo: 'forma', forma: 'circulo', largura: R, altura: R, x: Math.round(canto[0]), y: Math.round(canto[1]),
|
|
195
|
-
|
|
196
|
+
// O brilho alterna entre o destaque e a cor de apoio: as duas cores da marca aparecem.
|
|
197
|
+
cor: p.tipo === 'numero' ? cor.sobreDestaque : (variante && marca.cores && marca.cores.apoio ? cor.apoio : cor.destaque), opacidade: p.tipo === 'numero' ? 0.12 : (cor.escuro ? 0.26 : 0.16),
|
|
196
198
|
anima: {
|
|
197
199
|
desfoque: [{ t: 0, v: Math.round(R * 0.22) }],
|
|
198
200
|
x: [{ t: 0, v: 0 }, { t: dur, v: variante ? 60 : -60, e: 'suave' }],
|
|
@@ -293,7 +295,7 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
|
|
|
293
295
|
vazia embaixo. */
|
|
294
296
|
const fT = p.titulo ? caber(p.titulo, { largura: W0, linhas: 2, max: H * 0.1, fonte: F.titulo }) : { tamanho: 0, linhas: 0 };
|
|
295
297
|
const hTit = fT.tamanho * 1.04 * fT.linhas;
|
|
296
|
-
const hCard0 = colunas > 1 ? (p.tipo === 'passos' ? H * 0.26 : H * 0.44) : H * 0.14;
|
|
298
|
+
const hCard0 = colunas > 1 ? (p.tipo === 'passos' ? H * 0.26 : H * 0.44) : H * (deitado ? 0.14 : 0.13);
|
|
297
299
|
const hItens = colunas > 1 ? hCard0 : n * hCard0 + (n - 1) * gap;
|
|
298
300
|
const bloco = hTit + (hTit ? H * 0.07 : 0) + hItens;
|
|
299
301
|
const yTit = Math.max(M, (H - bloco) / 2);
|
|
@@ -301,6 +303,23 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
|
|
|
301
303
|
const topo = yTit + hTit + (hTit ? H * 0.07 : 0);
|
|
302
304
|
const hCard = Math.min(hCard0, colunas > 1 ? H - topo - M : (H - topo - M - gap * (n - 1)) / n);
|
|
303
305
|
const dur0 = vid ? Math.max(1, (tp.ate || dur) - (tp.de || 0)) : 1;
|
|
306
|
+
/* Um tamanho de letra para TODOS os itens: o do mais apertado. Cada
|
|
307
|
+
item com o seu tamanho deixava um card com a letra maior que os
|
|
308
|
+
outros, e esse texto saía do card (medido em 25/09). */
|
|
309
|
+
const padG = Math.round(Math.min(wCard, hCard) * 0.14);
|
|
310
|
+
const tamIG = Math.round(Math.min(hCard * (colunas > 1 ? 0.28 : 0.5), H * 0.09));
|
|
311
|
+
const lwG = colunas > 1 ? wCard - 2 * padG : wCard - (tamIG + 3 * padG);
|
|
312
|
+
// No vertical a linha é estreita: 3 linhas por item, para a letra caber grande.
|
|
313
|
+
const linhasG = colunas > 1 ? 4 : (deitado ? 2 : 3);
|
|
314
|
+
const hTexto = colunas > 1 ? hCard - (padG * 1.8 + tamIG + padG) : hCard - padG;
|
|
315
|
+
const maxItem = deitado ? Math.min(H * 0.05, hCard * 0.3) : Math.min(W * 0.062, hCard * 0.26);
|
|
316
|
+
const fits = itens.map((it) => caber(it.texto, { largura: lwG, linhas: linhasG, max: maxItem, fonte: F.texto }));
|
|
317
|
+
let tamItens = Math.min(...fits.map((f) => f.tamanho));
|
|
318
|
+
// Só na lista o texto mora DENTRO do card: ali a altura também manda.
|
|
319
|
+
if (p.tipo === 'lista') {
|
|
320
|
+
const linhasReais = Math.max(...itens.map((it) => caber(it.texto, { largura: lwG, linhas: linhasG, max: tamItens, min: tamItens, fonte: F.texto }).linhas));
|
|
321
|
+
tamItens = Math.min(tamItens, Math.floor(hTexto / (1.2 * linhasReais)));
|
|
322
|
+
}
|
|
304
323
|
itens.forEach((it, k) => {
|
|
305
324
|
const cx = colunas > 1 ? M + k * (wCard + gap) : M;
|
|
306
325
|
const cy = colunas > 1 ? topo : topo + k * (hCard + gap);
|
|
@@ -327,7 +346,8 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
|
|
|
327
346
|
const tx = colunas > 1 ? cx + pad : cx + pad + tamI + pad;
|
|
328
347
|
const ty = colunas > 1 ? cy + pad + tamI + pad * 0.8 : cy + hCard / 2;
|
|
329
348
|
const lw = colunas > 1 ? wCard - 2 * pad : wCard - (tamI + 3 * pad);
|
|
330
|
-
const
|
|
349
|
+
const fit0 = caber(it.texto, { largura: lw, linhas: linhasG, max: tamItens, min: Math.min(18, tamItens), fonte: F.texto });
|
|
350
|
+
const fit = { tamanho: Math.min(fit0.tamanho, tamItens), linhas: fit0.linhas };
|
|
331
351
|
els.push({
|
|
332
352
|
tipo: 'texto', texto: it.texto, fonte: F.texto, peso: 650, tamanho: fit.tamanho, cor: tinta, entrelinha: 1.2,
|
|
333
353
|
x: Math.round(tx), y: Math.round(colunas > 1 ? ty : ty - fit.tamanho * 1.2 * fit.linhas / 2), largura: Math.round(lw), anima: entra,
|
|
@@ -365,13 +385,19 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
|
|
|
365
385
|
const src = m && typeof m === 'object' ? m.src : m;
|
|
366
386
|
if (src) {
|
|
367
387
|
const ehVideo = m && typeof m === 'object' && m.tipo === 'video';
|
|
368
|
-
|
|
388
|
+
// O enquadramento puxa para cima: é onde ficam os rostos.
|
|
389
|
+
els.push({ tipo: ehVideo ? 'video' : 'imagem', src, ...(ehVideo ? { volume: 0 } : {}), x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover', foco: 'center 28%', anima: { escala: [{ t: 0, v: 1.12 }, { t: dur, v: 1, e: 'suave' }] } });
|
|
369
390
|
els.push({ tipo: 'forma', forma: 'retangulo', x: 0, y: 0, largura: '100%', altura: '100%', fundo: { tipo: 'gradiente', cores: ['rgba(0,0,0,0)', 'rgba(0,0,0,0.25)', 'rgba(0,0,0,0.85)'], angulo: 180 } });
|
|
370
391
|
}
|
|
371
|
-
const larg = W0 * 0.8;
|
|
372
|
-
const fit = caber(p.titulo || '', { largura: larg, linhas: 2, max: H * 0.1, fonte: F.titulo });
|
|
392
|
+
const larg = W0 * (deitado ? 0.8 : 1);
|
|
393
|
+
const fit = caber(p.titulo || '', { largura: larg, linhas: 2, max: H * (deitado ? 0.1 : 0.06), fonte: F.titulo });
|
|
373
394
|
const hT = fit.tamanho * 1.04 * fit.linhas;
|
|
374
|
-
|
|
395
|
+
/* Vídeo da marca (depoimento, bastidor) costuma ter o nome da
|
|
396
|
+
pessoa gravado embaixo: o título vai para o alto, com o véu
|
|
397
|
+
de cima, para não escrever por cima dele. */
|
|
398
|
+
const noTopo = Boolean(m && typeof m === 'object' && m.tipo === 'video');
|
|
399
|
+
if (noTopo) els.push({ tipo: 'forma', forma: 'retangulo', x: 0, y: 0, largura: '100%', altura: '100%', fundo: { tipo: 'gradiente', cores: ['rgba(0,0,0,0.8)', 'rgba(0,0,0,0.2)', 'rgba(0,0,0,0)'], angulo: 180 } });
|
|
400
|
+
const yT = noTopo ? M * 1.2 : H - M - hT - (p.sub ? H * 0.08 : 0);
|
|
375
401
|
if (p.titulo) titulo(p.titulo, { y: yT, larg, max: fit.tamanho, cor: '#FFFFFF', de: 0.3 });
|
|
376
402
|
if (p.sub) linhaFina(p.sub, { y: yT + hT + H * 0.02, larg, c: 'rgba(255,255,255,0.8)', de: 0.7 });
|
|
377
403
|
break;
|
package/lib/estudio/edicao.js
CHANGED
|
@@ -116,7 +116,7 @@ function iconeDe(palavra) {
|
|
|
116
116
|
if (!w || VAZIAS.has(w)) return null;
|
|
117
117
|
for (const [raizes, nome] of ICONES) {
|
|
118
118
|
for (const r of raizes) {
|
|
119
|
-
if (r.length <= 3 ? w === r : w.startsWith(r)) return 'fluent-
|
|
119
|
+
if (r.length <= 3 ? w === r : w.startsWith(r)) return 'fluent-3d:' + nome;
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
return null;
|
package/lib/estudio/index.js
CHANGED
|
@@ -306,6 +306,8 @@ async function ferramentaPlano(args) {
|
|
|
306
306
|
return { ok: false, error: 'o roteiro virou um plano com problemas (defeito do PrimoCode, não seu).', problemas: conferida.problemas.slice(0, 12) };
|
|
307
307
|
}
|
|
308
308
|
projeto.salvarPlano(args.id, r.plano, args.motivo || 'montado do roteiro');
|
|
309
|
+
// O roteiro fica guardado: ajustar uma frase é mandar o roteiro de novo, não reescrever tudo.
|
|
310
|
+
try { fs.writeFileSync(path.join(p.pasta, 'roteiro.json'), JSON.stringify(args.roteiro, null, 2)); } catch { /* segue */ }
|
|
309
311
|
projeto.salvar(args.id, { duracao: conferida.duracao });
|
|
310
312
|
return {
|
|
311
313
|
ok: true, duracao: conferida.duracao, ...r.resumo, resumo: plano.resumo(r.plano),
|
|
@@ -959,6 +961,36 @@ async function ferramentaRender(args, ctx = {}) {
|
|
|
959
961
|
|
|
960
962
|
// ── estudio_site ─────────────────────────────────────────────────────────
|
|
961
963
|
|
|
964
|
+
/* A PALETA INTEIRA do site, não só a primeira cor. O fundo é o neutro
|
|
965
|
+
escuro que o site mais usa (#121218 na Paris Group); o destaque é a cor da
|
|
966
|
+
marca que mais aparece sobre ele (o laranja, não o azul escuro que some); o
|
|
967
|
+
apoio é a seguinte. A fonte serifada, quando existe, vai para citação. */
|
|
968
|
+
function paletaDoSite(r) {
|
|
969
|
+
const hexRgb = (h) => { const n = parseInt(String(h).slice(1).padEnd(6, '0').slice(0, 6), 16); return [n >> 16, (n >> 8) & 255, n & 255]; };
|
|
970
|
+
const luz = (h) => { const [a, b, c] = hexRgb(h); return (0.299 * a + 0.587 * b + 0.114 * c) / 255; };
|
|
971
|
+
const sat = (h) => { const v = hexRgb(h); return (Math.max(...v) - Math.min(...v)) / 255; };
|
|
972
|
+
const neutras = ((r.cores && (r.cores.neutras || r.cores.fundo)) || []).map((x) => x.cor || x).filter((c) => /^#/.test(c));
|
|
973
|
+
const fundo = neutras.find((c) => luz(c) < 0.15 && luz(c) > 0.02) || neutras.find((c) => luz(c) < 0.2) || undefined;
|
|
974
|
+
const base = fundo || '#121218';
|
|
975
|
+
const marca = ((r.cores && r.cores.marca) || []).map((x) => x.cor).filter(Boolean);
|
|
976
|
+
const nota = (c) => sat(c) * 1.2 + Math.abs(luz(c) - luz(base));
|
|
977
|
+
const ordenadas = [...marca].sort((a, b) => nota(b) - nota(a));
|
|
978
|
+
const destaque = ordenadas[0];
|
|
979
|
+
// O apoio é OUTRA cor, de outro matiz: dois laranjas não fazem paleta.
|
|
980
|
+
const matiz = (h) => { const [a, b, c] = hexRgb(h).map((v) => v / 255); const mx = Math.max(a, b, c), mn = Math.min(a, b, c), d = mx - mn;
|
|
981
|
+
if (!d) return 0; const x = mx === a ? ((b - c) / d) % 6 : mx === b ? (c - a) / d + 2 : (a - b) / d + 4; return (x * 60 + 360) % 360; };
|
|
982
|
+
const longe = (a, b) => { const d = Math.abs(matiz(a) - matiz(b)); return Math.min(d, 360 - d) > 60; };
|
|
983
|
+
const apoio = ordenadas.find((c) => c !== destaque && longe(c, destaque) && sat(c) > 0.3) || ordenadas[1];
|
|
984
|
+
const fontes = ((r.fontes && r.fontes.declaradas) || []).map((x) => x.fonte).filter((f) => !/mono|menlo|courier|system|arial|helvetica/i.test(f));
|
|
985
|
+
return {
|
|
986
|
+
cores: { ...(fundo ? { fundo } : {}), ...(destaque ? { destaque } : {}), ...(apoio ? { apoio } : {}) },
|
|
987
|
+
fontes: {
|
|
988
|
+
...(fontes[0] ? { titulo: fontes.find((f) => !/serif/i.test(f) || /sans/i.test(f)) || fontes[0], texto: fontes.find((f) => !/serif/i.test(f) || /sans/i.test(f)) || fontes[0] } : {}),
|
|
989
|
+
...(fontes.find((f) => /serif/i.test(f) && !/sans/i.test(f)) ? { serifa: fontes.find((f) => /serif/i.test(f) && !/sans/i.test(f)) } : {}),
|
|
990
|
+
},
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
962
994
|
async function ferramentaSite(args) {
|
|
963
995
|
let alvo = String(args.url || args.site || '').trim();
|
|
964
996
|
|
|
@@ -989,10 +1021,7 @@ async function ferramentaSite(args) {
|
|
|
989
1021
|
const html = r.html;
|
|
990
1022
|
delete r.html; // o HTML cru não vai para o modelo
|
|
991
1023
|
if (r.ok) {
|
|
992
|
-
const marcaLida =
|
|
993
|
-
cores: { destaque: ((r.cores && r.cores.marca) || [])[0] ? r.cores.marca[0].cor : undefined },
|
|
994
|
-
fontes: { titulo: ((r.fontes && r.fontes.declaradas) || [])[0] ? r.fontes.declaradas[0].fonte : undefined },
|
|
995
|
-
};
|
|
1024
|
+
const marcaLida = paletaDoSite(r);
|
|
996
1025
|
ultimoSite = { url: r.url, html, quando: Date.now(), marca: marcaLida };
|
|
997
1026
|
}
|
|
998
1027
|
if (r.ok && args.id && args.logo !== false) {
|
package/lib/estudio/remotion.js
CHANGED
|
@@ -323,7 +323,7 @@ function Elemento({ el, t, dados }) {
|
|
|
323
323
|
startFrom={el.deOrigem ? Math.round(el.deOrigem * dados.fps) : undefined}
|
|
324
324
|
endAt={el.ateOrigem ? Math.round(el.ateOrigem * dados.fps) : undefined}
|
|
325
325
|
volume={el.volume ?? 0}
|
|
326
|
-
style={{ width: '100%', height: '100%', objectFit: el.ajuste || 'cover' }}
|
|
326
|
+
style={{ width: '100%', height: '100%', objectFit: el.ajuste || 'cover', objectPosition: el.foco || 'center' }}
|
|
327
327
|
/>
|
|
328
328
|
</div>
|
|
329
329
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "9.8.0-beta.
|
|
3
|
+
"version": "9.8.0-beta.17",
|
|
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": {
|