primocode 7.17.0 → 8.0.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
@@ -119,12 +119,11 @@ function completarSlash(line) {
119
119
  // Régua com os níveis, o marcador ▲ embaixo do escolhido, "Faster" à
120
120
  // esquerda e "Smarter" à direita. ←/→ move, Enter confirma, Esc cancela.
121
121
 
122
- function desenharEffort(idx) {
122
+ // O layout sai em texto PLANO, com as marcas de onde pintar. Assim os dois
123
+ // pintores (o normal e o do ultracode animado) desenham exatamente a mesma
124
+ // régua — sem duas contas de posição para manter em sincronia.
125
+ function layoutEffort(idx) {
123
126
  const cols = Math.max(52, Math.min(process.stdout.columns || 100, 104));
124
- const regua = '▔'.repeat(cols);
125
- const linhas = [];
126
-
127
- // Cada nível ocupa uma coluna própria; o ultracode fica destacado no fim.
128
127
  const rotulos = effort.NOMES;
129
128
  const larguras = rotulos.map((n) => Math.max(n.length + 3, 9));
130
129
  const posicoes = [];
@@ -134,38 +133,130 @@ function desenharEffort(idx) {
134
133
  x += larguras[i];
135
134
  }
136
135
  const fimRegua = x;
137
-
138
- linhas.push(c.dim(regua));
139
- linhas.push(' ' + c.bold(c.white('Effort')));
140
- linhas.push('');
141
-
142
- // Faster ....▲.... Smarter — a trilha, com o marcador na posição atual.
143
136
  const marca = posicoes[idx];
137
+
144
138
  let trilha = '';
145
139
  for (let k = 23; k < fimRegua; k++) {
146
- if (k === marca) trilha += c.brand('▲');
147
- else if (k === fimRegua - 12) trilha += c.dim('┆'); // ultracode é um degrau à parte
148
- else trilha += c.dim(k < marca ? '─' : '─');
140
+ if (k === marca) trilha += '▲';
141
+ else if (k === fimRegua - 12) trilha += '┆'; // ultracode é um degrau à parte
142
+ else trilha += '─';
149
143
  }
150
- linhas.push(' ' + c.muted('Faster') + ' '.repeat(Math.max(1, fimRegua - 23 - 6 - 7)) + c.muted('Smarter'));
151
- linhas.push(' ' + trilha);
152
144
 
153
- // Os nomes, o escolhido aceso.
154
- let linhaNomes = ' '.repeat(23);
145
+ let nomes = ' '.repeat(23);
146
+ let deNome = 0, ateNome = 0;
155
147
  for (let i = 0; i < rotulos.length; i++) {
156
148
  const nome = rotulos[i];
157
- const espaco = larguras[i];
158
- const pad = Math.floor((espaco - nome.length) / 2);
159
- const texto = i === idx ? c.bold(c.brand(nome)) : c.muted(nome);
160
- linhaNomes += ' '.repeat(pad) + texto + ' '.repeat(espaco - nome.length - pad);
149
+ const pad = Math.floor((larguras[i] - nome.length) / 2);
150
+ if (i === idx) { deNome = nomes.length + pad; ateNome = deNome + nome.length; }
151
+ nomes += ' '.repeat(pad) + nome + ' '.repeat(larguras[i] - nome.length - pad);
161
152
  }
162
- linhas.push(linhaNomes);
163
153
 
164
154
  const nivel = effort.nivelDe(rotulos[idx]);
165
- linhas.push(' ' + c.dim(nivel.resumo));
166
- linhas.push('');
167
- linhas.push(' ' + c.muted('←/→ ajusta · Enter confirma · Esc cancela'));
168
- return linhas.join('\n');
155
+ const vaoExtremos = Math.max(1, fimRegua - 23 - 6 - 7);
156
+
157
+ return {
158
+ cols,
159
+ marca,
160
+ destaque: { de: deNome, ate: ateNome },
161
+ linhas: [
162
+ { tipo: 'regua', texto: '▔'.repeat(cols) },
163
+ { tipo: 'titulo', texto: ' Effort' },
164
+ { tipo: 'vazio', texto: '' },
165
+ { tipo: 'extremos', texto: ' '.repeat(23) + 'Faster' + ' '.repeat(vaoExtremos) + 'Smarter' },
166
+ { tipo: 'trilha', texto: ' '.repeat(23) + trilha },
167
+ { tipo: 'nomes', texto: nomes },
168
+ { tipo: 'resumo', texto: ' '.repeat(23) + nivel.resumo },
169
+ { tipo: 'vazio', texto: '' },
170
+ { tipo: 'ajuda', texto: ' ←/→ ajusta · Enter confirma · Esc cancela' },
171
+ ],
172
+ };
173
+ }
174
+
175
+ // ── O brilho do ultracode ────────────────────────────────────────────────
176
+ // No nível máximo o painel inteiro ganha um fundo roxo que ondula, como no
177
+ // Claude Code. São faixas verticais de roxo em brilhos diferentes; a onda
178
+ // anda para o lado a cada quadro, e o texto fica branco por cima.
179
+ //
180
+ // Roxos do cubo de 256 cores, ordenados pelo brilho REAL (luminância), do
181
+ // mais escuro ao mais claro — a onda escolhe um deles por faixa. Fora da
182
+ // lista ficaram os roxos escuros que puxam para o vinho (53-56): quebravam a
183
+ // família violeta. Só 256 cores (não truecolor): funciona em qualquer
184
+ // terminal, inclusive no Terminal.app do macOS.
185
+ const ROXOS = [57, 92, 93, 63, 98, 99, 105, 141];
186
+
187
+ function fundoRoxo(x, fase) {
188
+ // Duas ondas de períodos diferentes: o encontro delas dá o desenho
189
+ // irregular de faixas, em vez de listras repetidas e óbvias.
190
+ const faixa = Math.floor(x / 2);
191
+ const onda = Math.sin(faixa * 0.55 + fase) * 0.6 + Math.sin(faixa * 0.17 - fase * 0.7) * 0.4;
192
+ const i = Math.round((onda + 1) / 2 * (ROXOS.length - 1));
193
+ return ROXOS[Math.max(0, Math.min(ROXOS.length - 1, i))];
194
+ }
195
+
196
+ // Pinta a linha inteira (até a largura do painel) com a onda por baixo.
197
+ // `destaque` é o trecho que fica branco e forte (o nível escolhido); o resto
198
+ // da linha sai num lilás claro, para o escolhido saltar aos olhos.
199
+ function pintarLinhaUltra(texto, largura, fase, opts = {}) {
200
+ const base = opts.base || 231; // branco
201
+ const apagado = opts.apagado || base;
202
+ const destaque = opts.destaque;
203
+ let saida = '';
204
+ let corFundo = -1;
205
+ let corTexto = -1;
206
+ let forte = null;
207
+
208
+ for (let x = 0; x < largura; x++) {
209
+ const fundo = fundoRoxo(x, fase);
210
+ if (fundo !== corFundo) { saida += `\x1b[48;5;${fundo}m`; corFundo = fundo; }
211
+
212
+ const dentro = destaque && x >= destaque.de && x < destaque.ate;
213
+ const querForte = opts.negrito || dentro;
214
+ const querCor = dentro ? base : apagado;
215
+ if (querForte !== forte) { saida += querForte ? '\x1b[1m' : '\x1b[22m'; forte = querForte; }
216
+ if (querCor !== corTexto) { saida += `\x1b[38;5;${querCor}m`; corTexto = querCor; }
217
+
218
+ saida += texto[x] || ' ';
219
+ }
220
+ return saida + '\x1b[0m';
221
+ }
222
+
223
+ function desenharEffort(idx, fase) {
224
+ const L = layoutEffort(idx);
225
+ const ultra = effort.NOMES[idx] === 'ultracode';
226
+
227
+ if (ultra) {
228
+ // No ultracode o painel todo vira fundo animado. Cada linha é pintada
229
+ // com a MESMA onda, então as faixas ficam alinhadas de cima a baixo.
230
+ const LILAS = 189; // lilás bem claro, para o que não é o escolhido
231
+ return L.linhas.map((l) => {
232
+ if (l.tipo === 'titulo') return pintarLinhaUltra(l.texto, L.cols, fase, { negrito: true });
233
+ if (l.tipo === 'nomes') return pintarLinhaUltra(l.texto, L.cols, fase, { destaque: L.destaque, apagado: LILAS });
234
+ if (l.tipo === 'resumo' || l.tipo === 'extremos') return pintarLinhaUltra(l.texto, L.cols, fase, { base: LILAS, apagado: LILAS });
235
+ return pintarLinhaUltra(l.texto, L.cols, fase, {});
236
+ }).join('\n');
237
+ }
238
+
239
+ return L.linhas.map((l) => {
240
+ const t = l.texto;
241
+ switch (l.tipo) {
242
+ case 'regua': return c.dim(t);
243
+ case 'titulo': return c.bold(c.white(t));
244
+ case 'extremos': return c.muted(t);
245
+ case 'trilha': {
246
+ // O marcador aceso, o resto da trilha recuada.
247
+ const antes = t.slice(0, L.marca);
248
+ const depois = t.slice(L.marca + 1);
249
+ return c.dim(antes) + c.brand('▲') + c.dim(depois);
250
+ }
251
+ case 'nomes': {
252
+ const { de, ate } = L.destaque;
253
+ return c.muted(t.slice(0, de)) + c.bold(c.brand(t.slice(de, ate))) + c.muted(t.slice(ate));
254
+ }
255
+ case 'resumo': return c.dim(t);
256
+ case 'ajuda': return c.muted(t);
257
+ default: return t;
258
+ }
259
+ }).join('\n');
169
260
  }
170
261
 
171
262
  // Desenha, escuta as setas, e devolve o nome escolhido (ou null se cancelou).
@@ -177,21 +268,38 @@ function sliderEffort(atual) {
177
268
  }
178
269
  let idx = effort.indiceDe(atual);
179
270
  const ALTURA = 9; // linhas que o desenho ocupa — para redesenhar por cima
271
+ let fase = 0;
272
+ let anima = null;
180
273
 
181
274
  const pintar = (primeira) => {
182
275
  if (!primeira) process.stdout.write(`\x1b[${ALTURA}A\r`);
183
- process.stdout.write(desenharEffort(idx).split('\n').map((l) => '\x1b[2K' + l).join('\n') + '\n');
276
+ process.stdout.write(desenharEffort(idx, fase).split('\n').map((l) => '\x1b[2K' + l).join('\n') + '\n');
184
277
  };
278
+
279
+ // O fundo do ultracode ondula; sair dele apaga a animação na hora, para
280
+ // não redesenhar sozinho um painel que já está parado.
281
+ const cuidarDaAnimacao = () => {
282
+ const ultra = effort.NOMES[idx] === 'ultracode';
283
+ if (ultra && !anima) {
284
+ anima = setInterval(() => { fase += 0.32; pintar(false); }, 90);
285
+ anima.unref();
286
+ } else if (!ultra && anima) {
287
+ clearInterval(anima); anima = null;
288
+ }
289
+ };
290
+
185
291
  pintar(true);
292
+ cuidarDaAnimacao();
186
293
 
187
294
  const aoTeclar = (ch, key) => {
188
295
  if (!key) return;
189
- if (key.name === 'right' || key.name === 'l') { idx = Math.min(effort.NOMES.length - 1, idx + 1); pintar(false); return; }
190
- if (key.name === 'left' || key.name === 'h') { idx = Math.max(0, idx - 1); pintar(false); return; }
296
+ if (key.name === 'right' || key.name === 'l') { idx = Math.min(effort.NOMES.length - 1, idx + 1); cuidarDaAnimacao(); pintar(false); return; }
297
+ if (key.name === 'left' || key.name === 'h') { idx = Math.max(0, idx - 1); cuidarDaAnimacao(); pintar(false); return; }
191
298
  if (key.name === 'return' || key.name === 'enter') { encerrar(effort.NOMES[idx]); return; }
192
299
  if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { encerrar(null); return; }
193
300
  };
194
301
  const encerrar = (valor) => {
302
+ if (anima) { clearInterval(anima); anima = null; }
195
303
  process.stdin.removeListener('keypress', aoTeclar);
196
304
  sliderAtivo = false;
197
305
  resolve(valor);
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/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;
@@ -446,6 +447,20 @@ async function despachar(name, args, ctx) {
446
447
  return desktop.scroll(args.dy != null ? args.dy : 300);
447
448
  case 'desktop_size':
448
449
  return desktop.getScreenSize();
450
+ // ── PrimoCode Studio — a suíte de criação (sobe sozinha) ───────
451
+ case 'studio_manual':
452
+ return studio.manual();
453
+ case 'studio_spec':
454
+ return studio.spec(args.ferramenta || args.slug || '');
455
+ case 'studio_criar':
456
+ return studio.criar({ ferramenta: args.ferramenta, titulo: args.titulo, doc: args.doc });
457
+ case 'studio_ler':
458
+ return studio.ler(args.id);
459
+ case 'studio_atualizar':
460
+ return studio.atualizar({ id: args.id, titulo: args.titulo, doc: args.doc });
461
+ case 'studio_listar':
462
+ return studio.listar();
463
+
449
464
  case 'desktop_double_click':
450
465
  return desktop.doubleClick(args.x, args.y);
451
466
  case 'desktop_open_app':
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": "7.17.0",
3
+ "version": "8.0.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": {
@@ -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.