primocode 7.18.0 → 8.0.1
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 +0 -0
- package/lib/act.js +13 -3
- package/lib/catalogo.js +12 -0
- package/lib/desktop.js +3 -1
- package/lib/referencia.js +8 -4
- package/lib/studio.js +198 -0
- package/lib/tools.js +24 -22
- package/lib/ui.js +3 -1
- package/package.json +2 -1
- package/studio/PRIMOCODE.md +58 -0
- package/studio/README.md +154 -0
- package/studio/ferramentas/00-protocolo.md +111 -0
- package/studio/ferramentas/01-modelos.md +202 -0
- package/studio/ferramentas/README.md +32 -0
- package/studio/ferramentas/corte.md +193 -0
- package/studio/ferramentas/grade.md +92 -0
- package/studio/ferramentas/prisma.md +99 -0
- package/studio/ferramentas/prosa.md +86 -0
- package/studio/ferramentas/tela.md +72 -0
- package/studio/ferramentas/traco.md +79 -0
- package/studio/server.py +862 -0
- package/studio/web/core.css +723 -0
- package/studio/web/home.html +149 -0
- package/studio/web/js/audio.js +262 -0
- package/studio/web/js/editor.js +1270 -0
- package/studio/web/js/exportar.js +135 -0
- package/studio/web/js/ferramentas.js +105 -0
- package/studio/web/js/icones.js +91 -0
- package/studio/web/js/midia.js +157 -0
- package/studio/web/js/narracao.js +254 -0
- package/studio/web/js/player.js +504 -0
- package/studio/web/js/render-grade.js +243 -0
- package/studio/web/js/render-prosa.js +104 -0
- package/studio/web/js/render.js +622 -0
- package/studio/web/render.css +565 -0
- package/studio/web/tool.html +70 -0
- package/studio/web/view.html +20 -0
package/bin/primocode.js
CHANGED
|
File without changes
|
package/lib/act.js
CHANGED
|
@@ -299,9 +299,19 @@ async function runAct(userPrompt, opts) {
|
|
|
299
299
|
const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
|
|
300
300
|
logResult(tc.name, result, profundidade);
|
|
301
301
|
|
|
302
|
-
// Uma escrita bem-sucedida limpa a dívida
|
|
303
|
-
|
|
304
|
-
|
|
302
|
+
// Uma escrita bem-sucedida limpa a dívida — mas só a do MESMO
|
|
303
|
+
// arquivo. Escrever algo sem relação (ou um run_shell qualquer)
|
|
304
|
+
// não corrige um teste quebrado noutro lugar; antes isto zerava
|
|
305
|
+
// a lista inteira e o agente conseguia dizer "concluído" com uma
|
|
306
|
+
// falha real ainda pendente.
|
|
307
|
+
if (result && result.ok === false) {
|
|
308
|
+
falhasPendentes.push(`${tc.name}(${tc.arguments?.path || ''}): ${result.error}`);
|
|
309
|
+
} else if (['write_file', 'edit_file'].includes(tc.name) && tc.arguments?.path) {
|
|
310
|
+
const alvo = tc.arguments.path;
|
|
311
|
+
const mantidas = falhasPendentes.filter((f) => !f.includes(`(${alvo})`));
|
|
312
|
+
falhasPendentes.length = 0;
|
|
313
|
+
falhasPendentes.push(...mantidas);
|
|
314
|
+
}
|
|
305
315
|
|
|
306
316
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: summarizeForModel(result) });
|
|
307
317
|
}
|
package/lib/catalogo.js
CHANGED
|
@@ -53,6 +53,18 @@ const GRUPOS = [
|
|
|
53
53
|
{ tool: 'open_in_browser', escreve: false, faz: 'abre no seu navegador normal, sem controlar' },
|
|
54
54
|
],
|
|
55
55
|
},
|
|
56
|
+
{
|
|
57
|
+
nome: 'Estúdio',
|
|
58
|
+
resumo: 'a suíte de criação do PrimoCode — sobe sozinha, não precisa de app externo',
|
|
59
|
+
itens: [
|
|
60
|
+
{ tool: 'studio_manual', escreve: false, faz: 'lê o manual do estúdio e as seis ferramentas' },
|
|
61
|
+
{ tool: 'studio_spec', escreve: false, faz: 'lê a spec de uma ferramenta (prisma, tela, prosa…)' },
|
|
62
|
+
{ tool: 'studio_criar', escreve: false, faz: 'cria slides, arte, documento, planilha, vídeo ou diagrama' },
|
|
63
|
+
{ tool: 'studio_ler', escreve: false, faz: 'lê algo que já foi criado' },
|
|
64
|
+
{ tool: 'studio_atualizar', escreve: false, faz: 'ajusta o que já existe, sem refazer do zero' },
|
|
65
|
+
{ tool: 'studio_listar', escreve: false, faz: 'lista o que já existe no estúdio' },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
56
68
|
{
|
|
57
69
|
nome: 'Computador',
|
|
58
70
|
resumo: 'janelas, botões, mouse e teclado — age em qualquer programa e na área de trabalho',
|
package/lib/desktop.js
CHANGED
|
@@ -19,7 +19,9 @@ const os = require('os');
|
|
|
19
19
|
const PLATFORM = os.platform();
|
|
20
20
|
|
|
21
21
|
function hasCmd(cmd) {
|
|
22
|
-
|
|
22
|
+
// cmd pode vir de texto do usuário (desktop_open_app com nome sem alias
|
|
23
|
+
// conhecido) — sem aspas aqui, um ";"/"&&" no meio vira comando extra.
|
|
24
|
+
try { execSync(`which ${sh1(cmd)} || where ${sh1(cmd)}`, { stdio: 'ignore' }); return true; } catch { return false; }
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
// Detectar ferramenta disponível
|
package/lib/referencia.js
CHANGED
|
@@ -64,11 +64,15 @@ function viaProxy(alvo, { timeout }) {
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function pegar(url, { timeout = TIMEOUT_MS } = {}) {
|
|
67
|
+
function pegar(url, { timeout = TIMEOUT_MS, saltos = 0 } = {}) {
|
|
68
68
|
return new Promise((resolve) => {
|
|
69
69
|
let req;
|
|
70
70
|
const alvo = (() => { try { return new URL(url); } catch { return null; } })();
|
|
71
71
|
if (!alvo) return resolve(null);
|
|
72
|
+
// 5 saltos cobre qualquer doc real; sem teto, um redirect em loop
|
|
73
|
+
// (A→B→A→...) trava essa fonte — e a pesquisa inteira — para sempre.
|
|
74
|
+
if (saltos > 5) return resolve(null);
|
|
75
|
+
const cliente = alvo.protocol === 'http:' ? http : https;
|
|
72
76
|
|
|
73
77
|
const opcoes = {
|
|
74
78
|
headers: {
|
|
@@ -88,12 +92,12 @@ function pegar(url, { timeout = TIMEOUT_MS } = {}) {
|
|
|
88
92
|
abrir.then((cfg) => {
|
|
89
93
|
if (!cfg) return resolve(null);
|
|
90
94
|
try {
|
|
91
|
-
req =
|
|
92
|
-
// Redirecionamento: segue
|
|
95
|
+
req = cliente.get(url, cfg, (res) => {
|
|
96
|
+
// Redirecionamento: segue até o teto de saltos acima.
|
|
93
97
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
94
98
|
res.resume();
|
|
95
99
|
const destino = new URL(res.headers.location, url).href;
|
|
96
|
-
return pegar(destino, { timeout }).then(resolve);
|
|
100
|
+
return pegar(destino, { timeout, saltos: saltos + 1 }).then(resolve);
|
|
97
101
|
}
|
|
98
102
|
if (res.statusCode !== 200) { res.resume(); return resolve(null); }
|
|
99
103
|
let corpo = '';
|
package/lib/studio.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* studio.js — o PrimoCode Studio, ligado ao agente.
|
|
3
|
+
*
|
|
4
|
+
* O Studio é a suíte de criação que vem junto com o PrimoCode (pasta studio/):
|
|
5
|
+
* Prisma (apresentações), Tela (design), Prosa (documentos), Grade (planilhas),
|
|
6
|
+
* Corte (vídeo) e Traço (diagramas). É um servidor Python de biblioteca padrão,
|
|
7
|
+
* sem nada para instalar.
|
|
8
|
+
*
|
|
9
|
+
* ── POR QUE ESTE MÓDULO EXISTE ───────────────────────────────────────────
|
|
10
|
+
* Medido em uso real: o agente sabia do Studio, tentou falar com ele em
|
|
11
|
+
* localhost:7777, tomou "conexão recusada" e DESISTIU — respondeu ao usuário
|
|
12
|
+
* que "o estúdio não está rodando na sua máquina". O usuário não tem que ligar
|
|
13
|
+
* ferramenta nenhuma: ele pediu um vídeo, quer o vídeo.
|
|
14
|
+
*
|
|
15
|
+
* Então aqui o Studio sobe SOZINHO, na primeira vez que o agente precisa dele.
|
|
16
|
+
* `garantirNoAr()` é chamado antes de toda tool studio_*, e o modelo nem sabe
|
|
17
|
+
* que houve um passo a mais.
|
|
18
|
+
*
|
|
19
|
+
* ── POR QUE ELE RODA DE ~/.primocode/studio, E NÃO DE DENTRO DO PACOTE ────
|
|
20
|
+
* O Studio grava o que cria (arquivos/ e midia/) ao lado do server.py. Com o
|
|
21
|
+
* pacote instalado global (`npm i -g`), essa pasta costuma ser de sistema e a
|
|
22
|
+
* escrita falharia — e, pior, um `npm update` apagaria o trabalho do usuário.
|
|
23
|
+
* Então o código é copiado para ~/.primocode/studio na primeira vez, e as
|
|
24
|
+
* criações ficam lá, a salvo de atualização do pacote.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
const http = require('http');
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const path = require('path');
|
|
32
|
+
const os = require('os');
|
|
33
|
+
const { spawn, execSync } = require('child_process');
|
|
34
|
+
|
|
35
|
+
const ORIGEM = path.join(__dirname, '..', 'studio');
|
|
36
|
+
const CASA = path.join(os.homedir(), '.primocode', 'studio');
|
|
37
|
+
const PORTA = Number(process.env.PRIMOCODE_STUDIO_PORT) || 7777;
|
|
38
|
+
const BASE = `http://127.0.0.1:${PORTA}`;
|
|
39
|
+
|
|
40
|
+
// Só o código do Studio é atualizado; `arquivos/` e `midia/` (o que o usuário
|
|
41
|
+
// criou) nunca são tocados.
|
|
42
|
+
const COPIAVEIS = new Set(['server.py', 'web', 'ferramentas', 'README.md', 'PRIMOCODE.md']);
|
|
43
|
+
|
|
44
|
+
function pythonCmd() {
|
|
45
|
+
const candidatos = os.platform() === 'win32' ? ['python', 'py', 'python3'] : ['python3', 'python'];
|
|
46
|
+
for (const c of candidatos) {
|
|
47
|
+
try {
|
|
48
|
+
const v = execSync(`${c} --version`, { stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 }).toString();
|
|
49
|
+
if (/Python 3/.test(v)) return c;
|
|
50
|
+
} catch {}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Cópia recursiva que só reescreve o que mudou — assim `npm update` traz o
|
|
56
|
+
// Studio novo sem custo, e o que o usuário criou continua onde está.
|
|
57
|
+
function copiar(de, para) {
|
|
58
|
+
const st = fs.statSync(de);
|
|
59
|
+
if (st.isDirectory()) {
|
|
60
|
+
fs.mkdirSync(para, { recursive: true });
|
|
61
|
+
for (const item of fs.readdirSync(de)) copiar(path.join(de, item), path.join(para, item));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
let precisa = true;
|
|
65
|
+
try {
|
|
66
|
+
const alvo = fs.statSync(para);
|
|
67
|
+
precisa = alvo.size !== st.size || alvo.mtimeMs < st.mtimeMs;
|
|
68
|
+
} catch {}
|
|
69
|
+
if (precisa) fs.copyFileSync(de, para);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function prepararCasa() {
|
|
73
|
+
if (!fs.existsSync(ORIGEM)) return { ok: false, error: 'A pasta studio/ não veio no pacote.' };
|
|
74
|
+
fs.mkdirSync(CASA, { recursive: true });
|
|
75
|
+
for (const item of COPIAVEIS) {
|
|
76
|
+
const de = path.join(ORIGEM, item);
|
|
77
|
+
if (fs.existsSync(de)) copiar(de, path.join(CASA, item));
|
|
78
|
+
}
|
|
79
|
+
return { ok: true, casa: CASA };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── HTTP ─────────────────────────────────────────────────────────────────
|
|
83
|
+
function pedir(metodo, rota, corpo, timeout = 30000) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const dados = corpo == null ? null : Buffer.from(JSON.stringify(corpo), 'utf8');
|
|
86
|
+
const req = http.request({
|
|
87
|
+
hostname: '127.0.0.1',
|
|
88
|
+
port: PORTA,
|
|
89
|
+
path: rota,
|
|
90
|
+
method: metodo,
|
|
91
|
+
timeout,
|
|
92
|
+
headers: dados ? { 'Content-Type': 'application/json', 'Content-Length': dados.length } : {},
|
|
93
|
+
}, (res) => {
|
|
94
|
+
let texto = '';
|
|
95
|
+
res.setEncoding('utf8');
|
|
96
|
+
res.on('data', (c) => { texto += c; });
|
|
97
|
+
res.on('end', () => {
|
|
98
|
+
if (res.statusCode >= 400) return reject(new Error(`HTTP ${res.statusCode}: ${texto.slice(0, 200)}`));
|
|
99
|
+
try { resolve(JSON.parse(texto)); }
|
|
100
|
+
catch { resolve({ texto }); }
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
req.on('timeout', () => { req.destroy(new Error('tempo esgotado falando com o Studio')); });
|
|
104
|
+
req.on('error', reject);
|
|
105
|
+
if (dados) req.write(dados);
|
|
106
|
+
req.end();
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function estaNoAr() {
|
|
111
|
+
try { await pedir('GET', '/api/ferramentas', null, 1500); return true; }
|
|
112
|
+
catch { return false; }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Subir ────────────────────────────────────────────────────────────────
|
|
116
|
+
// Uma subida por vez: sem esta trava, duas tools chamadas no mesmo turno
|
|
117
|
+
// disparariam dois servidores na mesma porta e o segundo morreria com
|
|
118
|
+
// "address in use" — barulho por nada.
|
|
119
|
+
let subindo = null;
|
|
120
|
+
|
|
121
|
+
async function garantirNoAr() {
|
|
122
|
+
if (await estaNoAr()) return { ok: true, url: BASE, jaEstava: true };
|
|
123
|
+
if (subindo) return subindo;
|
|
124
|
+
|
|
125
|
+
subindo = (async () => {
|
|
126
|
+
const casa = prepararCasa();
|
|
127
|
+
if (!casa.ok) return casa;
|
|
128
|
+
|
|
129
|
+
const py = pythonCmd();
|
|
130
|
+
if (!py) {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
error: 'O Studio precisa do Python 3, que não está instalado.',
|
|
134
|
+
comoResolver: os.platform() === 'darwin'
|
|
135
|
+
? 'Instale com: brew install python3 (ou baixe em python.org).'
|
|
136
|
+
: 'Instale o Python 3 (python.org) e tente de novo.',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Solto do processo do CLI: o Studio continua de pé quando a sessão
|
|
141
|
+
// fecha, que é o que o usuário espera de um app aberto.
|
|
142
|
+
const filho = spawn(py, ['server.py', String(PORTA)], {
|
|
143
|
+
cwd: CASA,
|
|
144
|
+
detached: true,
|
|
145
|
+
stdio: 'ignore',
|
|
146
|
+
});
|
|
147
|
+
filho.unref();
|
|
148
|
+
|
|
149
|
+
// Espera ficar de pé (~8s no pior caso). Máquina lenta continua sendo
|
|
150
|
+
// atendida; máquina rápida sai na primeira volta.
|
|
151
|
+
for (let i = 0; i < 40; i++) {
|
|
152
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
153
|
+
if (await estaNoAr()) return { ok: true, url: BASE, subiuAgora: true };
|
|
154
|
+
}
|
|
155
|
+
return { ok: false, error: `O Studio não respondeu em ${BASE} depois de subir. Veja se a porta ${PORTA} está livre.` };
|
|
156
|
+
})();
|
|
157
|
+
|
|
158
|
+
try { return await subindo; }
|
|
159
|
+
finally { subindo = null; }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── O que o agente usa ───────────────────────────────────────────────────
|
|
163
|
+
// Toda função garante o Studio no ar ANTES de falar com ele. É isto que faz o
|
|
164
|
+
// "abre o Canva e faz um post" virar um post pronto, sem o usuário ligar nada.
|
|
165
|
+
|
|
166
|
+
async function comStudio(fn) {
|
|
167
|
+
const no = await garantirNoAr();
|
|
168
|
+
if (!no.ok) return no;
|
|
169
|
+
return fn();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const manual = () => comStudio(async () => ({ ok: true, ...(await pedir('GET', '/api/manual')) }));
|
|
173
|
+
const ferramentas = () => comStudio(async () => ({ ok: true, ferramentas: await pedir('GET', '/api/ferramentas') }));
|
|
174
|
+
const spec = (slug) => comStudio(async () => ({ ok: true, spec: await pedir('GET', `/api/ferramentas/${encodeURIComponent(slug)}`) }));
|
|
175
|
+
const listar = () => comStudio(async () => ({ ok: true, arquivos: await pedir('GET', '/api/arquivos') }));
|
|
176
|
+
const ler = (id) => comStudio(async () => ({ ok: true, arquivo: await pedir('GET', `/api/arquivos/${encodeURIComponent(id)}`) }));
|
|
177
|
+
|
|
178
|
+
const criar = ({ ferramenta, titulo, doc }) => comStudio(async () => {
|
|
179
|
+
if (!ferramenta) return { ok: false, error: 'Diga a ferramenta: prisma, tela, prosa, grade, corte ou traco.' };
|
|
180
|
+
if (!doc) return { ok: false, error: 'Falta o doc. Leia a spec com studio_spec antes de criar.' };
|
|
181
|
+
const r = await pedir('POST', '/api/arquivos', { ferramenta, titulo: titulo || 'Sem título', doc });
|
|
182
|
+
return { ok: true, ...r };
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const atualizar = ({ id, titulo, doc }) => comStudio(async () => {
|
|
186
|
+
if (!id) return { ok: false, error: 'Falta o id do arquivo.' };
|
|
187
|
+
const corpo = {};
|
|
188
|
+
if (titulo != null) corpo.titulo = titulo;
|
|
189
|
+
if (doc != null) corpo.doc = doc;
|
|
190
|
+
const r = await pedir('PUT', `/api/arquivos/${encodeURIComponent(id)}`, corpo);
|
|
191
|
+
return { ok: true, ...r };
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
module.exports = {
|
|
195
|
+
BASE, PORTA, CASA,
|
|
196
|
+
garantirNoAr, estaNoAr, prepararCasa, pythonCmd,
|
|
197
|
+
manual, ferramentas, spec, listar, ler, criar, atualizar,
|
|
198
|
+
};
|
package/lib/tools.js
CHANGED
|
@@ -14,6 +14,7 @@ const { exec } = require('child_process');
|
|
|
14
14
|
const browser = require('./browser');
|
|
15
15
|
const desktop = require('./desktop');
|
|
16
16
|
const referencia = require('./referencia');
|
|
17
|
+
const studio = require('./studio');
|
|
17
18
|
|
|
18
19
|
const IGNORAR = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.cache', 'vendor']);
|
|
19
20
|
const MAX_ARQUIVOS_VARREDURA = 5000;
|
|
@@ -288,7 +289,7 @@ function multiEdit({ path: rawPath, edits }, projectDir) {
|
|
|
288
289
|
if (ocorrencias > 1 && !replace_all) {
|
|
289
290
|
return { ok: false, error: `edit ${i + 1}: trecho aparece ${ocorrencias}x — use replace_all ou inclua mais contexto para torná-lo único` };
|
|
290
291
|
}
|
|
291
|
-
conteudo = replace_all ? conteudo.split(old_string).join(new_string) : conteudo.replace(old_string, new_string);
|
|
292
|
+
conteudo = replace_all ? conteudo.split(old_string).join(new_string ?? '') : conteudo.replace(old_string, new_string ?? '');
|
|
292
293
|
substituicoes++;
|
|
293
294
|
}
|
|
294
295
|
|
|
@@ -421,7 +422,10 @@ async function despachar(name, args, ctx) {
|
|
|
421
422
|
return texto == null ? { ok: false, error: 'nada na página (ou seletor não encontrado)' } : { ok: true, text: texto };
|
|
422
423
|
}
|
|
423
424
|
case 'browser_screenshot':
|
|
424
|
-
|
|
425
|
+
// Sem path, cai num nome-padrão — mas SEMPRE dentro do projeto: sem
|
|
426
|
+
// isso o arquivo ia para o cwd real do processo, não para a pasta
|
|
427
|
+
// virtual que o CLI mostra ao usuário (o processo nunca faz chdir).
|
|
428
|
+
return browser.screenshot(resolveInProject(args.path || `screenshot-${Date.now()}.png`, projectDir));
|
|
425
429
|
case 'browser_scroll':
|
|
426
430
|
return browser.scroll(args.dy != null ? args.dy : 600);
|
|
427
431
|
case 'browser_wait':
|
|
@@ -431,7 +435,9 @@ async function despachar(name, args, ctx) {
|
|
|
431
435
|
|
|
432
436
|
// ── Desktop físico — mouse, teclado e tela do usuário ───────────
|
|
433
437
|
case 'desktop_screenshot':
|
|
434
|
-
|
|
438
|
+
// Mesmo motivo do browser_screenshot: nome-padrão também precisa
|
|
439
|
+
// resolver dentro do projeto, não do cwd real do processo.
|
|
440
|
+
return desktop.screenshot(resolveInProject(args.path || `desktop-${Date.now()}.png`, projectDir));
|
|
435
441
|
case 'desktop_click':
|
|
436
442
|
return desktop.click(args.x, args.y, args.button || 'left');
|
|
437
443
|
case 'desktop_move':
|
|
@@ -446,6 +452,20 @@ async function despachar(name, args, ctx) {
|
|
|
446
452
|
return desktop.scroll(args.dy != null ? args.dy : 300);
|
|
447
453
|
case 'desktop_size':
|
|
448
454
|
return desktop.getScreenSize();
|
|
455
|
+
// ── PrimoCode Studio — a suíte de criação (sobe sozinha) ───────
|
|
456
|
+
case 'studio_manual':
|
|
457
|
+
return studio.manual();
|
|
458
|
+
case 'studio_spec':
|
|
459
|
+
return studio.spec(args.ferramenta || args.slug || '');
|
|
460
|
+
case 'studio_criar':
|
|
461
|
+
return studio.criar({ ferramenta: args.ferramenta, titulo: args.titulo, doc: args.doc });
|
|
462
|
+
case 'studio_ler':
|
|
463
|
+
return studio.ler(args.id);
|
|
464
|
+
case 'studio_atualizar':
|
|
465
|
+
return studio.atualizar({ id: args.id, titulo: args.titulo, doc: args.doc });
|
|
466
|
+
case 'studio_listar':
|
|
467
|
+
return studio.listar();
|
|
468
|
+
|
|
449
469
|
case 'desktop_double_click':
|
|
450
470
|
return desktop.doubleClick(args.x, args.y);
|
|
451
471
|
case 'desktop_open_app':
|
|
@@ -469,24 +489,6 @@ async function despachar(name, args, ctx) {
|
|
|
469
489
|
}
|
|
470
490
|
}
|
|
471
491
|
|
|
472
|
-
function applyCodeBlocks(response) {
|
|
473
|
-
const regex = /```[\w]*\n([\s\S]*?)```/g;
|
|
474
|
-
const applied = [];
|
|
475
|
-
let match;
|
|
476
|
-
while ((match = regex.exec(response || '')) !== null) {
|
|
477
|
-
const block = match[1];
|
|
478
|
-
const nome = block.match(/\/\/\s*filename:\s*(.+)/);
|
|
479
|
-
if (!nome) continue;
|
|
480
|
-
const filename = nome[1].trim();
|
|
481
|
-
const content = block.replace(/\/\/\s*filename:.+\n?/, '');
|
|
482
|
-
const created = !fs.existsSync(filename);
|
|
483
|
-
fs.mkdirSync(path.dirname(filename), { recursive: true });
|
|
484
|
-
fs.writeFileSync(filename, content, 'utf-8');
|
|
485
|
-
applied.push({ path: filename, created });
|
|
486
|
-
}
|
|
487
|
-
return applied;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
492
|
async function uninstall() {
|
|
491
493
|
const configDir = path.join(os.homedir(), '.primocode');
|
|
492
494
|
console.log('Removendo PrimoCode…');
|
|
@@ -504,7 +506,7 @@ async function uninstall() {
|
|
|
504
506
|
}
|
|
505
507
|
|
|
506
508
|
module.exports = {
|
|
507
|
-
fmtBytes, treeDir,
|
|
509
|
+
fmtBytes, treeDir, runShell, openInBrowser, runInNewTerminal,
|
|
508
510
|
execute, uninstall, resolveInProject, globToRegex, globFiles, grepFiles, editFile,
|
|
509
511
|
multiEdit, writeTodos, getTodos,
|
|
510
512
|
};
|
package/lib/ui.js
CHANGED
|
@@ -310,11 +310,13 @@ function actionHeader(name, args) {
|
|
|
310
310
|
desktop_drag:'Desktop·drag', desktop_type:'Desktop·type', desktop_key:'Desktop·key',
|
|
311
311
|
desktop_scroll:'Desktop·scroll', desktop_size:'Desktop·size',
|
|
312
312
|
desktop_double_click:'Desktop·2click', desktop_open_app:'Desktop·app',
|
|
313
|
+
studio_manual:'Estúdio·manual', studio_spec:'Estúdio·spec', studio_criar:'Estúdio·criar',
|
|
314
|
+
studio_ler:'Estúdio·ler', studio_atualizar:'Estúdio·ajustar', studio_listar:'Estúdio·lista',
|
|
313
315
|
desktop_windows:'Desktop·janelas', desktop_focus:'Desktop·foco',
|
|
314
316
|
desktop_elements:'Desktop·elementos', desktop_click_element:'Desktop·click',
|
|
315
317
|
};
|
|
316
318
|
const label = nice[name] || name;
|
|
317
|
-
const argStr = args ? (args.consulta || args.path || args.pattern || args.command || args.target || args.url || args.selector || args.text || args.app || args.window || args.name || args.description || (args.todos ? `${args.todos.length} tarefas` : '') || (args.x != null ? `${args.x},${args.y}` : '') || args.key || JSON.stringify(args).slice(0,60)) : '';
|
|
319
|
+
const argStr = args ? (args.consulta || args.path || args.pattern || args.command || args.target || args.url || args.selector || args.text || args.app || args.window || args.name || args.ferramenta || args.titulo || args.description || (args.todos ? `${args.todos.length} tarefas` : '') || (args.x != null ? `${args.x},${args.y}` : '') || args.key || JSON.stringify(args).slice(0,60)) : '';
|
|
318
320
|
return ` ${c.dim('⎿')} ${c.brand(label)}${argStr ? ' '+c.muted(argStr) : ''}`;
|
|
319
321
|
}
|
|
320
322
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.1",
|
|
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": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"bin/",
|
|
29
29
|
"lib/",
|
|
30
|
+
"studio/",
|
|
30
31
|
"README.md"
|
|
31
32
|
]
|
|
32
33
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# PRIMOCODE — instruções para agentes
|
|
2
|
+
|
|
3
|
+
Se você é um agente de IA trabalhando neste repositório ou com este servidor,
|
|
4
|
+
leia isto antes de agir.
|
|
5
|
+
|
|
6
|
+
## O que é
|
|
7
|
+
|
|
8
|
+
Um estúdio de criação local. Você **não** opera pela interface — você escreve o
|
|
9
|
+
documento em JSON e envia por API. A interface existe para o humano refinar depois.
|
|
10
|
+
|
|
11
|
+
## O que fazer quando o usuário pede uma peça
|
|
12
|
+
|
|
13
|
+
1. Confira que o servidor responde:
|
|
14
|
+
`curl -s -o /dev/null -w '%{http_code}' http://localhost:7777/api/ferramentas`
|
|
15
|
+
Se não der `200`, suba com `python3 server.py` a partir desta pasta.
|
|
16
|
+
|
|
17
|
+
2. Se o pedido citou um app externo (Canva, PowerPoint, Word, Excel, Photoshop,
|
|
18
|
+
CapCut, Figma, Miro…), responda primeiro, em uma linha:
|
|
19
|
+
|
|
20
|
+
> Posso abrir o(a) **[app pedido]**, mas para te entregar com mais qualidade
|
|
21
|
+
> vou usar minhas ferramentas nativas.
|
|
22
|
+
|
|
23
|
+
Não espere confirmação. Se o usuário insistir no app externo, abra e siga.
|
|
24
|
+
|
|
25
|
+
3. Leia `curl -s http://localhost:7777/api/manual` — traz os três modelos de
|
|
26
|
+
documento, todos os layouts e um exemplo completo por ferramenta.
|
|
27
|
+
|
|
28
|
+
4. Escolha a ferramenta:
|
|
29
|
+
|
|
30
|
+
| pedido | ferramenta |
|
|
31
|
+
|---|---|
|
|
32
|
+
| slides, deck, apresentação, pitch | `prisma` |
|
|
33
|
+
| post, capa, banner, thumbnail, arte | `tela` |
|
|
34
|
+
| documento, relatório, proposta, artigo | `prosa` |
|
|
35
|
+
| planilha, orçamento, controle, projeção | `grade` |
|
|
36
|
+
| vídeo, reels, animação, teaser | `corte` |
|
|
37
|
+
| fluxograma, diagrama, mapa mental, arquitetura | `traco` |
|
|
38
|
+
|
|
39
|
+
5. `POST /api/arquivos` com `{ferramenta, titulo, doc}` e devolva a URL `ver`.
|
|
40
|
+
|
|
41
|
+
6. Para ajustes posteriores, `GET` o arquivo, mude o que precisa e `PUT` de volta.
|
|
42
|
+
O link do usuário continua valendo.
|
|
43
|
+
|
|
44
|
+
## Regras de conteúdo
|
|
45
|
+
|
|
46
|
+
- Escreva o conteúdo de verdade. Nada de "Lorem ipsum" nem "[inserir aqui]".
|
|
47
|
+
- Não invente números com cara de dado real. Sem fonte, diga que falta a fonte.
|
|
48
|
+
- Uma ideia por cena. Se precisa rolar o olho para ler, dividiu errado.
|
|
49
|
+
- Não peça para o usuário montar sozinho na interface.
|
|
50
|
+
|
|
51
|
+
## Se você for mexer no código
|
|
52
|
+
|
|
53
|
+
- `ferramentas/*.md` é a única fonte da spec. Mudou comportamento? Atualize lá —
|
|
54
|
+
`/api/manual` serve direto desses arquivos.
|
|
55
|
+
- `web/js/render.js` desenha cenas; `render-prosa.js` e `render-grade.js` os outros
|
|
56
|
+
dois modelos. `render.css` é o único CSS que vai junto no export autônomo.
|
|
57
|
+
- Sem dependências externas. Nada de CDN, nada de `npm install`, nada de build.
|
|
58
|
+
Se precisou de uma biblioteca, escreva a função.
|
package/studio/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# PrimoCode Studio
|
|
4
|
+
|
|
5
|
+
**O estúdio de criação que a IA opera.**
|
|
6
|
+
|
|
7
|
+
Seis ferramentas nativas, um modelo de documento em comum, zero dependências.
|
|
8
|
+
A IA escreve o documento, o estúdio desenha, você refina à mão.
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Rodar
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
python3 server.py
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Abra <http://localhost:7777>. Só precisa de Python 3 — nada para instalar,
|
|
21
|
+
nada para compilar, funciona offline.
|
|
22
|
+
|
|
23
|
+
Outra porta: `python3 server.py 8080`.
|
|
24
|
+
|
|
25
|
+
## As ferramentas
|
|
26
|
+
|
|
27
|
+
| Ferramenta | Papel | No lugar de |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| **Prisma** | Apresentações | PowerPoint, Keynote, Canva Apresentações |
|
|
30
|
+
| **Tela** | Design gráfico | Photoshop, Figma, Canva |
|
|
31
|
+
| **Prosa** | Documentos | Word, Google Docs, Notion |
|
|
32
|
+
| **Grade** | Planilhas | Excel, Google Sheets |
|
|
33
|
+
| **Corte** | Vídeo | CapCut, Premiere, After Effects |
|
|
34
|
+
| **Traço** | Diagramas | Miro, Whimsical, Lucidchart |
|
|
35
|
+
|
|
36
|
+
Cada uma tem editor completo — trilho de inserção, camadas, inspetor de
|
|
37
|
+
propriedades, arrastar e redimensionar, desfazer, edição no lugar — e todas
|
|
38
|
+
compartilham o mesmo modelo de documento, o que faz a IA aprender uma vez e
|
|
39
|
+
operar as seis.
|
|
40
|
+
|
|
41
|
+
## Como a IA usa
|
|
42
|
+
|
|
43
|
+
Ela não clica em nada. Lê o manual, escreve o documento e devolve o link:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
curl -s http://localhost:7777/api/manual
|
|
47
|
+
|
|
48
|
+
curl -s -X POST http://localhost:7777/api/arquivos \
|
|
49
|
+
-H 'Content-Type: application/json' \
|
|
50
|
+
-d '{"ferramenta":"prisma","titulo":"Energia solar","doc":{
|
|
51
|
+
"tema":"meia-noite",
|
|
52
|
+
"cenas":[{"layout":"capa","fundo":"malha","elementos":[
|
|
53
|
+
{"tipo":"texto","papel":"chapeu","texto":"Panorama 2026"},
|
|
54
|
+
{"tipo":"texto","papel":"titulo","texto":"Energia solar no Brasil"}]}]}}'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
A resposta traz `ver` (tela cheia) e `editar` (estúdio). O usuário recebe um link.
|
|
58
|
+
|
|
59
|
+
### A regra de abertura
|
|
60
|
+
|
|
61
|
+
Quando alguém pede "abre o Canva", "abre o PowerPoint", o agente responde:
|
|
62
|
+
|
|
63
|
+
> Posso abrir o(a) **[app pedido]**, mas para te entregar com mais qualidade vou
|
|
64
|
+
> usar minhas ferramentas nativas.
|
|
65
|
+
|
|
66
|
+
E entrega o arquivo pronto em vez de um app vazio.
|
|
67
|
+
|
|
68
|
+
## A pasta `ferramentas/`
|
|
69
|
+
|
|
70
|
+
É a fonte da verdade. O servidor lê os `.md` de lá e serve tudo junto em
|
|
71
|
+
`/api/manual` — não existe spec duplicada. Editar um arquivo muda o que a IA
|
|
72
|
+
sabe na hora, sem reiniciar nada.
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
ferramentas/
|
|
76
|
+
00-protocolo.md comportamento do agente, frase de abertura, tabela de equivalência
|
|
77
|
+
01-modelos.md os três modelos: cena, documento, planilha
|
|
78
|
+
prisma.md tela.md prosa.md grade.md corte.md traco.md
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Para Claude Code há também `.claude/skills/primocode/` — a skill dispara sozinha
|
|
82
|
+
quando alguém fala em slides, arte, documento, planilha, vídeo ou diagrama.
|
|
83
|
+
|
|
84
|
+
## Os três modelos
|
|
85
|
+
|
|
86
|
+
Aprenda `cena` e você opera quatro ferramentas.
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{ "tema": "meia-noite", "largura": 1920, "altura": 1080,
|
|
90
|
+
"cenas": [{ "layout": "capa", "fundo": "malha", "elementos": [
|
|
91
|
+
{ "tipo": "texto", "papel": "titulo", "texto": "Título" }
|
|
92
|
+
]}]}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Com `layout` nomeado, o estúdio posiciona: `capa` `titulo` `secao` `duas-colunas`
|
|
96
|
+
`tres-colunas` `numeros` `citacao` `imagem-direita` `imagem-esquerda` `centro`.
|
|
97
|
+
Com `layout: "livre"`, você define `x`, `y`, `l`, `a` — é o canvas do Tela e do Traço.
|
|
98
|
+
|
|
99
|
+
Elementos: `texto` `lista` `cartoes` `numeros` `tabela` `citacao` `codigo`
|
|
100
|
+
`grafico` `imagem` `icone` `forma` `no` `conector`.
|
|
101
|
+
|
|
102
|
+
`documento` (Prosa) é uma lista de blocos. `planilha` (Grade) tem abas com
|
|
103
|
+
fórmulas de verdade — `=SOMA(D1:D4)`, `=ARRED(D1/B1*100)` — resolvidas no
|
|
104
|
+
navegador e também no export CSV.
|
|
105
|
+
|
|
106
|
+
Detalhe completo em [`ferramentas/01-modelos.md`](ferramentas/01-modelos.md).
|
|
107
|
+
|
|
108
|
+
## API
|
|
109
|
+
|
|
110
|
+
| Método | Rota | O que faz |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `GET` | `/api/manual` | manual completo, em um fetch |
|
|
113
|
+
| `GET` | `/api/ferramentas` | as seis, em JSON |
|
|
114
|
+
| `GET` | `/api/ferramentas/<slug>` | spec de uma |
|
|
115
|
+
| `GET` | `/api/arquivos` | lista |
|
|
116
|
+
| `POST` | `/api/arquivos` | cria — `{ferramenta, titulo, doc}` |
|
|
117
|
+
| `GET` `PUT` `DELETE` | `/api/arquivos/<id>` | lê, atualiza, apaga |
|
|
118
|
+
| `GET` | `/api/arquivos/<id>/export` | `.html` autônomo · `.csv` (Grade) · `.md` (Prosa) |
|
|
119
|
+
|
|
120
|
+
O export HTML é um arquivo único com CSS, renderizadores e documento embutidos —
|
|
121
|
+
zero requisição externa, abre offline, dá para mandar por e-mail.
|
|
122
|
+
|
|
123
|
+
## Atalhos no modo Abrir
|
|
124
|
+
|
|
125
|
+
`→` `espaço` avança · `←` volta · `K` toca/pausa (Corte) · `F` tela cheia ·
|
|
126
|
+
`P` imprime ou salva em PDF, uma cena por página.
|
|
127
|
+
|
|
128
|
+
## Estrutura
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
server.py servidor e API — biblioteca padrão do Python
|
|
132
|
+
web/
|
|
133
|
+
core.css interface do estúdio
|
|
134
|
+
render.css aparência do que é criado (vai junto no export)
|
|
135
|
+
home.html biblioteca e lançador
|
|
136
|
+
tool.html editor
|
|
137
|
+
view.html apresentação e reprodução
|
|
138
|
+
js/
|
|
139
|
+
icones.js ~60 ícones em SVG
|
|
140
|
+
render.js desenha cenas: layouts, elementos, gráficos, conectores
|
|
141
|
+
render-prosa.js desenha documentos
|
|
142
|
+
render-grade.js desenha planilhas e resolve fórmulas
|
|
143
|
+
editor.js o estúdio manual
|
|
144
|
+
player.js apresentar, reproduzir, imprimir
|
|
145
|
+
ferramentas.js o que cada ferramenta oferece na interface
|
|
146
|
+
ferramentas/ a spec que a IA lê
|
|
147
|
+
arquivos/ o que foi criado, um JSON por peça
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Os arquivos ficam em `arquivos/*.json` — texto puro, versionável, diffável.
|
|
151
|
+
|
|
152
|
+
## Licença
|
|
153
|
+
|
|
154
|
+
MIT.
|