primocode 8.1.0 → 8.2.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/README.md +3 -3
- package/bin/primocode.js +3 -1
- package/lib/browser.js +5 -31
- package/lib/catalogo.js +2 -0
- package/lib/config.js +8 -3
- package/lib/desktop.js +13 -2
- package/lib/modo.js +70 -12
- package/lib/referencia.js +28 -0
- package/lib/studio.js +35 -0
- package/lib/tools.js +35 -8
- package/lib/ui.js +1 -68
- package/package.json +1 -1
- package/studio/ferramentas/corte.md +33 -0
- package/studio/server.py +24 -0
- package/studio/web/tool.html +4 -0
- package/studio/web/view.html +2 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode
|
|
1
|
+
# PrimoCode v8.1.1
|
|
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
|
|
12
|
+
▐▛███▜▌ Primo Code v8.1.1
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
|
@@ -263,7 +263,7 @@ primocode -f app.js "onde está o bug?" # com arquivo anexado
|
|
|
263
263
|
| `/model top\|main\|fast` | **Todos grátis.** top = o mais capaz (550B, 1M de contexto), main = agente de código (padrão), fast = o mais rápido. Aceita qualquer ID do OpenRouter |
|
|
264
264
|
| `/attach <arquivo>` | Anexa arquivo ao próximo pedido |
|
|
265
265
|
| `/detach` · `/files` | Remove / lista anexos |
|
|
266
|
-
| `/tudo` | Lista as
|
|
266
|
+
| `/tudo` | Lista as 44 ferramentas que ele sabe usar, por grupo |
|
|
267
267
|
| `/tree [n]` | Árvore do projeto atual |
|
|
268
268
|
| `/read <arquivo>` | Mostra um arquivo do projeto |
|
|
269
269
|
| `/run <comando>` | Executa no shell, dentro da pasta do projeto |
|
package/bin/primocode.js
CHANGED
|
@@ -516,7 +516,9 @@ async function sendPrompt(userText) {
|
|
|
516
516
|
// tokens). No auto, quem não vai usar ferramenta não paga por ela.
|
|
517
517
|
let modo = state.mode;
|
|
518
518
|
if (modo === 'auto') {
|
|
519
|
-
|
|
519
|
+
// O modo do pedido ANTERIOR entra na decisão: no meio de uma tarefa,
|
|
520
|
+
// "refaz" e "agora põe música" são continuação, não papo novo.
|
|
521
|
+
const escolha = escolherModo(userText, { ultimoModo: state.modoEfetivo });
|
|
520
522
|
modo = escolha.modo;
|
|
521
523
|
state.modoEfetivo = modo;
|
|
522
524
|
const rotulo = modo === 'primocode' ? c.warn('Primo Code') : c.brandDim('Chat');
|
package/lib/browser.js
CHANGED
|
@@ -88,8 +88,9 @@ const APPS_MAC = [
|
|
|
88
88
|
*
|
|
89
89
|
* Um app rodando de dentro do `.dmg` funciona, mas é frágil: o disco é
|
|
90
90
|
* somente-leitura, o macOS o executa por AppTranslocation (caminho temporário
|
|
91
|
-
* aleatório) e ele desaparece na hora que o disco for ejetado. Achamos e
|
|
92
|
-
*
|
|
91
|
+
* aleatório) e ele desaparece na hora que o disco for ejetado. Achamos e
|
|
92
|
+
* usamos mesmo assim — sem aviso adicional, o disco ejetado já falha na hora
|
|
93
|
+
* de abrir o navegador, o que já é sinal suficiente do que aconteceu.
|
|
93
94
|
*/
|
|
94
95
|
function procurarEmVolumes() {
|
|
95
96
|
const bases = [];
|
|
@@ -107,24 +108,6 @@ function procurarEmVolumes() {
|
|
|
107
108
|
return null;
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
/**
|
|
111
|
-
* O navegador achado está num lugar problemático? Devolve o aviso, ou null.
|
|
112
|
-
*
|
|
113
|
-
* Não bloqueia nada: o controle funciona de dentro do disco montado. Só avisa,
|
|
114
|
-
* porque quem ejetar o disco vai ver a ferramenta parar de funcionar sem entender
|
|
115
|
-
* o motivo.
|
|
116
|
-
*/
|
|
117
|
-
function avisoDeLocal(caminho) {
|
|
118
|
-
if (!caminho) return null;
|
|
119
|
-
if (caminho.startsWith('/Volumes/')) {
|
|
120
|
-
const app = (caminho.match(/\/Volumes\/[^/]+\/([^/]+\.app)/) || [, 'o navegador'])[1];
|
|
121
|
-
return `${app} está rodando do disco de instalação, não instalado. `
|
|
122
|
-
+ 'Funciona, mas para de funcionar quando você ejetar esse disco — '
|
|
123
|
-
+ 'arraste o app para a pasta Aplicativos.';
|
|
124
|
-
}
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
111
|
function httpGetJson(url, timeoutMs = 3000) {
|
|
129
112
|
return new Promise((resolve, reject) => {
|
|
130
113
|
const req = http.get(url, (res) => {
|
|
@@ -450,8 +433,6 @@ async function open(url) {
|
|
|
450
433
|
return { ok: true, url };
|
|
451
434
|
}
|
|
452
435
|
|
|
453
|
-
async function navigate(url) { return open(url); }
|
|
454
|
-
|
|
455
436
|
async function evaluate(js) {
|
|
456
437
|
await ensure();
|
|
457
438
|
const r = await currentSession.send('Runtime.evaluate', {
|
|
@@ -639,11 +620,6 @@ async function screenshot(saveTo) {
|
|
|
639
620
|
return { ok: true, path: filename, bytes: Buffer.byteLength(r.data, 'base64') };
|
|
640
621
|
}
|
|
641
622
|
|
|
642
|
-
async function currentUrl() {
|
|
643
|
-
await ensure();
|
|
644
|
-
return evaluate('location.href');
|
|
645
|
-
}
|
|
646
|
-
|
|
647
623
|
async function waitFor(selector, timeoutMs = 15000) {
|
|
648
624
|
await ensure();
|
|
649
625
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -669,8 +645,6 @@ function close() {
|
|
|
669
645
|
}
|
|
670
646
|
|
|
671
647
|
module.exports = {
|
|
672
|
-
open,
|
|
673
|
-
key, screenshot, evaluate,
|
|
674
|
-
avisoDeLocal,
|
|
675
|
-
_internal: { findChrome, procurarEmVolumes, PROFILE_DIR, DEBUG_PORT },
|
|
648
|
+
open, click, clickText, clickSelector, type, typeInSelector,
|
|
649
|
+
key, screenshot, evaluate, waitFor, wait, scroll, close,
|
|
676
650
|
};
|
package/lib/catalogo.js
CHANGED
|
@@ -85,6 +85,8 @@ const GRUPOS = [
|
|
|
85
85
|
{ tool: 'studio_atualizar', escreve: false, faz: 'ajusta o que já existe, sem refazer do zero',
|
|
86
86
|
args: 'id. titulo (opcional). doc (opcional).' },
|
|
87
87
|
{ tool: 'studio_listar', escreve: false, faz: 'lista o que já existe no estúdio', args: null },
|
|
88
|
+
{ tool: 'studio_midia', escreve: false, faz: 'manda imagem, música ou vídeo seu para usar nas peças',
|
|
89
|
+
args: 'arquivo (opcional): caminho no projeto — sem ele, lista a galeria. nome (opcional).' },
|
|
88
90
|
],
|
|
89
91
|
},
|
|
90
92
|
{
|
package/lib/config.js
CHANGED
|
@@ -73,10 +73,15 @@ function ensureStructure() {
|
|
|
73
73
|
return { rootDir: ROOT_DIR };
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// Tira acento (NFD + remove os diacríticos combinantes). Compartilhado com
|
|
77
|
+
// lib/desktop.js — as duas versões faziam a mesma conta separadas.
|
|
78
|
+
function stripAccents(s) {
|
|
79
|
+
return String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '');
|
|
80
|
+
}
|
|
81
|
+
|
|
76
82
|
// "Jogo de Carro!" -> "jogo-de-carro"
|
|
77
83
|
function slugify(name) {
|
|
78
|
-
return
|
|
79
|
-
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
84
|
+
return stripAccents(name)
|
|
80
85
|
.toLowerCase()
|
|
81
86
|
.replace(/[^a-z0-9]+/g, '-')
|
|
82
87
|
.replace(/^-+|-+$/g, '')
|
|
@@ -208,7 +213,7 @@ function appendHistory(entry) {
|
|
|
208
213
|
module.exports = {
|
|
209
214
|
loadConfig, saveConfig, appendHistory,
|
|
210
215
|
CONFIG_DIR, CONFIG_FILE, HISTORY_FILE, ROOT_DIR, GENERAL_MEMORY,
|
|
211
|
-
ensureStructure, slugify,
|
|
216
|
+
ensureStructure, slugify, stripAccents,
|
|
212
217
|
createProjectDir, getProjectDir, listProjects, findSimilarProject,
|
|
213
218
|
readGeneralMemory, readProjectMemory, appendProjectMemory, appendGeneralMemory,
|
|
214
219
|
};
|
package/lib/desktop.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
const { execSync } = require('child_process');
|
|
17
17
|
const os = require('os');
|
|
18
|
+
const { stripAccents } = require('./config');
|
|
18
19
|
|
|
19
20
|
const PLATFORM = os.platform();
|
|
20
21
|
|
|
@@ -344,7 +345,7 @@ const APP_ALIASES = {
|
|
|
344
345
|
};
|
|
345
346
|
|
|
346
347
|
function normaliza(s) {
|
|
347
|
-
return
|
|
348
|
+
return stripAccents(s).toLowerCase().trim();
|
|
348
349
|
}
|
|
349
350
|
|
|
350
351
|
function aliasDoApp(nome, platform = PLATFORM) {
|
|
@@ -371,7 +372,17 @@ function openApp(nome) {
|
|
|
371
372
|
} else {
|
|
372
373
|
const bin = app.split(/\s+/)[0];
|
|
373
374
|
if (hasCmd('gtk-launch') && run(`gtk-launch ${sh1(bin)}`).ok) r = { ok: true };
|
|
374
|
-
else if (hasCmd(bin))
|
|
375
|
+
else if (hasCmd(bin)) {
|
|
376
|
+
// sh1() aqui só protegia o script INTEIRO como um argumento pro
|
|
377
|
+
// `bash -c` — dentro do script, bash reparsa o texto, e "app" cru
|
|
378
|
+
// continuava vulnerável a injeção (achado de segurança: um app tipo
|
|
379
|
+
// "a;touch /tmp/pwned;" executava o comando extra). Cada palavra de
|
|
380
|
+
// "app" agora entra entre aspas simples individualmente — preserva
|
|
381
|
+
// "programa --flag" como dois argumentos, mas nenhuma delas deixa
|
|
382
|
+
// metacaractere de shell passar como sintaxe.
|
|
383
|
+
const appEscapado = app.split(/\s+/).map(sh1).join(' ');
|
|
384
|
+
r = run(`bash -c ${sh1(`setsid ${appEscapado} >/dev/null 2>&1 < /dev/null &`)}`);
|
|
385
|
+
}
|
|
375
386
|
else r = { ok: false, error: `Não achei o programa "${pedido}" no PATH.` };
|
|
376
387
|
}
|
|
377
388
|
|
package/lib/modo.js
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Escolha automática de modo.
|
|
3
3
|
*
|
|
4
|
-
* O modo primocode manda o schema das
|
|
5
|
-
* tokens antes de você digitar a primeira letra. Um "oi" gasta o mesmo que
|
|
6
|
-
* refatoração, e num plano com teto diário isso é dinheiro jogado fora (foi
|
|
4
|
+
* O modo primocode manda o schema das 45 ferramentas em todo pedido — milhares
|
|
5
|
+
* de tokens antes de você digitar a primeira letra. Um "oi" gasta o mesmo que
|
|
6
|
+
* uma refatoração, e num plano com teto diário isso é dinheiro jogado fora (foi
|
|
7
7
|
* assim que a cota de 100 mil tokens/dia acabou às 15h).
|
|
8
8
|
*
|
|
9
9
|
* O modo chat não manda schema nenhum. Então a regra é: só paga o preço das
|
|
10
10
|
* ferramentas quem vai usar ferramenta.
|
|
11
11
|
*
|
|
12
12
|
* A decisão é local e sem custo — nenhuma chamada ao modelo para descobrir a
|
|
13
|
-
* intenção, senão o remédio custaria como a doença.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* intenção, senão o remédio custaria como a doença.
|
|
14
|
+
*
|
|
15
|
+
* ── POR QUE ELE OLHA O TURNO ANTERIOR ────────────────────────────────────
|
|
16
|
+
* A primeira versão julgava cada frase sozinha, e errava feio no uso real: o
|
|
17
|
+
* usuário pedia um vídeo (tarefa), e o "refaz", o "continua" e o "agora põe
|
|
18
|
+
* música de fundo" logo depois caíam no chat — o agente perdia o fio no meio
|
|
19
|
+
* do trabalho. Medido na conversa do dono: 7 erros em 18 frases, quase todos
|
|
20
|
+
* de continuação.
|
|
21
|
+
*
|
|
22
|
+
* Conversa humana não repete o objeto a cada frase. Então, QUANDO O TURNO
|
|
23
|
+
* ANTERIOR FOI UMA TAREFA, a régua abaixa: o que não é claramente cumprimento
|
|
24
|
+
* ou dúvida conceitual continua sendo trabalho. Fora de uma tarefa, a régua
|
|
25
|
+
* volta a subir — na dúvida, chat.
|
|
17
26
|
*/
|
|
18
27
|
|
|
19
28
|
'use strict';
|
|
@@ -34,6 +43,13 @@ const VERBOS = [
|
|
|
34
43
|
// "pesquisa no google", "aperta enter", "arrasta o arquivo".
|
|
35
44
|
'clic[ao]r?', 'clique', 'digit[ae]r?', 'pesquis[ae]r?', 'apert[ae]r?',
|
|
36
45
|
'arrast[ae]r?', 'rol[ae]r?', 'minimiz[ae]r?', 'maximiz[ae]r?', 'fech[ae]r?',
|
|
46
|
+
// Ajuste do que já foi criado — o vocabulário do estúdio. Sem estes, "agora
|
|
47
|
+
// põe música de fundo" caía no chat, no meio de um vídeo.
|
|
48
|
+
'p[õo]e', 'ponha', 'p[oô]r', 'coloc[ae]r?', 'tir[ae]r?', 'troc[ae]r?',
|
|
49
|
+
'inclu[ai]r?', 'insir[ae]', 'insert', 'aument[ae]r?', 'diminu[ai]r?',
|
|
50
|
+
'grav[ae]r?', 'narr[ae]r?', 'export[ae]r?', 'salv[ae]r?', 'envi[ae]r?',
|
|
51
|
+
'convert[ae]r?', 'transform[ae]r?', 'divid[ai]r?', 'junt[ae]r?',
|
|
52
|
+
'desenh[ae]r?', 'ilustr[ae]r?', 'resum[ae]r?', 'traduz[ai]r?',
|
|
37
53
|
];
|
|
38
54
|
const RE_VERBO = new RegExp(`(^|\\s)(${VERBOS.join('|')})\\b`, 'i');
|
|
39
55
|
|
|
@@ -49,33 +65,75 @@ const RE_ARTEFATO = /\.(js|ts|tsx|jsx|html|css|json|md|py|sh|yml|yaml|txt|csv|en
|
|
|
49
65
|
const RE_FERRAMENTA = /\b(navegador|browser|desktop|terminal|shell|git|npm|node|deploy|servidor|localhost|porta \d+|screenshot|print da tela|mouse|teclado|janela|[áa]rea de trabalho|aplicativo|[íi]cone)\b/i;
|
|
50
66
|
const RE_PASTA = /\b(pasta|diret[óo]rio|arquivo|arquivos|reposit[óo]rio|repo|projeto)\b/i;
|
|
51
67
|
|
|
68
|
+
// O que o estúdio produz. "faz uns slides", "quero um post", "monta a planilha"
|
|
69
|
+
// — o substantivo sozinho já denuncia a tarefa, mesmo sem verbo claro.
|
|
70
|
+
const RE_PECA = /\b(slides?|apresenta[çc][ãa]o|deck|v[íi]deo|reels?|anima[çc][ãa]o|post|banner|capa|thumbnail|arte|cartaz|planilha|tabela|documento|relat[óo]rio|artigo|proposta|contrato|diagrama|fluxograma|organograma|mapa mental|narra[çc][ãa]o|trilha|legenda)\b/i;
|
|
71
|
+
|
|
52
72
|
// Conversa: cumprimento, agradecimento, pergunta conceitual.
|
|
53
|
-
const RE_SAUDACAO = /^\s*(oi+|ol[áa]|e?a[íi]|bom dia|boa tarde|boa noite|tudo bem|beleza|valeu|obrigad[oa]|brigad[oa]|tchau|at
|
|
73
|
+
const RE_SAUDACAO = /^\s*(oi+|ol[áa]|e?a[íi]|bom dia|boa tarde|boa noite|tudo bem|beleza|valeu|obrigad[oa]|brigad[oa]|tchau|at[ée]|ok|okay|entendi|legal|show|massa|top|perfeito|isso|isso a[íi]|ta bom|t[áa] bom|certo)\b[\s!.?]*$/i;
|
|
54
74
|
const RE_CONCEITUAL = /\b(o que [ée]|o q [ée]|que [ée]|qual (a|o|as|os)?\s*(diferen[çc]a|melhor|sentido)|por qu[êe]|pra que serve|para que serve|como funciona|como (fa[çc]o|posso|se faz|fazer)|explica|explique|me explica|significa|vale a pena|devo usar|qual usar|prefere)\b/i;
|
|
75
|
+
// "o que você faz?", "o que vc sabe fazer?" — é pergunta sobre ele, não pedido.
|
|
76
|
+
// Sem esta linha o "faz" batia no verbo e virava tarefa.
|
|
77
|
+
const RE_SOBRE_ELE = /\bo que (voc[êe]|vc|tu)\s+(faz|sabe|consegue|pode|[ée])\b|\bquem [ée] voc[êe]\b|\bcomo voc[êe] funciona\b/i;
|
|
78
|
+
|
|
79
|
+
// Satisfação: "ficou bom", "tá bom assim", "pode parar" — o usuário está
|
|
80
|
+
// dizendo que ACABOU, não pedindo mais trabalho. Sem isto, no meio de uma
|
|
81
|
+
// tarefa a régua abaixada mandava um elogio de volta para o agente.
|
|
82
|
+
const RE_SATISFEITO = /^\s*(t[áa]|ta|ficou|est[áa]|ta[' ]?)?\s*(bom|[óo]timo|legal|show|massa|perfeito|lindo|maneiro|isso)\b(\s+(assim|mesmo|ent[ãa]o|demais))?[\s!.]*$|^\s*(pode parar|chega|s[óo] isso|era isso|nada mais)\b/i;
|
|
83
|
+
|
|
84
|
+
// Continuação forte: sozinhas já significam "siga com o que estava fazendo".
|
|
85
|
+
const RE_CONTINUACAO = /^\s*(refa[çz]a?|refaz|de novo|novamente|outra vez|mais uma vez|continu[ae]|prossiga|segue|siga|vai|manda|tenta( de novo| outra vez)?|tente( de novo| outra vez)?|repete|repita)\b/i;
|
|
86
|
+
|
|
87
|
+
// Continuação fraca: só vale quando o turno anterior foi tarefa.
|
|
88
|
+
// "agora …", "e o …?", "também …", "mais …", "sem …", "com …".
|
|
89
|
+
const RE_CONTINUACAO_FRACA = /^\s*(agora|depois|ent[ãa]o|e (o|a|os|as|no|na)\b|tamb[ée]m|mais|menos|sem|com|s[óo]|apenas|troca|muda)\b/i;
|
|
55
90
|
|
|
56
91
|
/**
|
|
57
92
|
* Decide o modo de um pedido.
|
|
58
|
-
*
|
|
93
|
+
*
|
|
94
|
+
* @param {string} texto o que o usuário digitou
|
|
95
|
+
* @param {{ultimoModo?: string}} ctx contexto da conversa; `ultimoModo` é o
|
|
96
|
+
* modo que o pedido ANTERIOR usou de fato ('primocode' | 'chat').
|
|
97
|
+
* @returns {{modo:'primocode'|'chat', motivo:string}}
|
|
98
|
+
*
|
|
59
99
|
* O motivo existe para o CLI mostrar por que escolheu — automode que decide em
|
|
60
100
|
* silêncio parece defeito na primeira vez que erra.
|
|
61
101
|
*/
|
|
62
|
-
function escolherModo(texto) {
|
|
102
|
+
function escolherModo(texto, ctx = {}) {
|
|
63
103
|
const t = String(texto || '').trim();
|
|
64
104
|
if (!t) return { modo: 'chat', motivo: 'nada a fazer' };
|
|
65
105
|
|
|
106
|
+
const noMeioDeUmaTarefa = (ctx && ctx.ultimoModo) === 'primocode';
|
|
107
|
+
|
|
66
108
|
// Cumprimento é chat mesmo que traga um verbo depois ("oi, tudo bem?").
|
|
67
109
|
if (RE_SAUDACAO.test(t)) return { modo: 'chat', motivo: 'cumprimento' };
|
|
110
|
+
if (RE_SATISFEITO.test(t)) return { modo: 'chat', motivo: 'está satisfeito' };
|
|
68
111
|
|
|
69
|
-
// Pergunta
|
|
70
|
-
// dúvida, não pedido de execução.
|
|
112
|
+
// Pergunta sobre ele mesmo, e pergunta conceitual, ganham do verbo:
|
|
113
|
+
// "como faço para criar um site?" é dúvida, não pedido de execução.
|
|
114
|
+
if (RE_SOBRE_ELE.test(t)) return { modo: 'chat', motivo: 'pergunta sobre ele' };
|
|
71
115
|
if (RE_CONCEITUAL.test(t)) return { modo: 'chat', motivo: 'pergunta conceitual' };
|
|
72
116
|
|
|
117
|
+
// "refaz", "de novo", "continua" — retomada, com ou sem contexto.
|
|
118
|
+
if (RE_CONTINUACAO.test(t)) return { modo: 'primocode', motivo: 'retoma a tarefa' };
|
|
119
|
+
|
|
120
|
+
// Pistas diretas.
|
|
73
121
|
if (RE_ARTEFATO.test(t)) return { modo: 'primocode', motivo: 'cita arquivo ou caminho' };
|
|
74
122
|
if (RE_VERBO.test(t)) return { modo: 'primocode', motivo: 'pede uma ação' };
|
|
123
|
+
if (RE_PECA.test(t)) return { modo: 'primocode', motivo: 'fala de algo para criar' };
|
|
75
124
|
if (RE_DESEJO.test(t)) return { modo: 'primocode', motivo: 'expressa o que quer que exista' };
|
|
76
125
|
if (RE_FERRAMENTA.test(t)) return { modo: 'primocode', motivo: 'menciona ferramenta ou máquina' };
|
|
77
126
|
if (RE_PASTA.test(t)) return { modo: 'primocode', motivo: 'fala de arquivo ou projeto' };
|
|
78
127
|
|
|
128
|
+
// No meio de uma tarefa a régua abaixa: conversa não repete o objeto a cada
|
|
129
|
+
// frase, e o custo de errar aqui é o agente perder o fio.
|
|
130
|
+
if (noMeioDeUmaTarefa) {
|
|
131
|
+
if (RE_CONTINUACAO_FRACA.test(t)) return { modo: 'primocode', motivo: 'continua o que estava fazendo' };
|
|
132
|
+
// Pergunta curta no meio do trabalho costuma ser sobre o trabalho
|
|
133
|
+
// ("e não dá pra deixar rodando?"), não papo novo.
|
|
134
|
+
if (t.split(/\s+/).length <= 12) return { modo: 'primocode', motivo: 'segue a tarefa em andamento' };
|
|
135
|
+
}
|
|
136
|
+
|
|
79
137
|
return { modo: 'chat', motivo: 'sem pista de tarefa' };
|
|
80
138
|
}
|
|
81
139
|
|
package/lib/referencia.js
CHANGED
|
@@ -29,6 +29,33 @@ const net = require('net');
|
|
|
29
29
|
const tls = require('tls');
|
|
30
30
|
const { URL } = require('url');
|
|
31
31
|
|
|
32
|
+
// Achado de segurança: redirect sem checar destino é SSRF — uma fonte de
|
|
33
|
+
// busca (conteúdo de terceiro, não confiável) pode responder com um Location
|
|
34
|
+
// pra localhost ou pro metadado de nuvem (169.254.169.254), e pegar() seguia
|
|
35
|
+
// sem checar host nenhum. Bloqueia loopback/link-local/rede privada antes de
|
|
36
|
+
// abrir a conexão, tanto na URL original quanto em cada redirect seguido.
|
|
37
|
+
function hostBloqueado(hostname) {
|
|
38
|
+
const h = String(hostname || '').toLowerCase();
|
|
39
|
+
if (h === 'localhost' || h.endsWith('.localhost')) return true;
|
|
40
|
+
const versao = net.isIP(h);
|
|
41
|
+
if (versao === 4) {
|
|
42
|
+
const partes = h.split('.').map(Number);
|
|
43
|
+
const [a, b] = partes;
|
|
44
|
+
if (a === 127 || a === 10 || a === 0) return true; // loopback, privada, "esta rede"
|
|
45
|
+
if (a === 169 && b === 254) return true; // link-local — inclui metadado de nuvem
|
|
46
|
+
if (a === 172 && b >= 16 && b <= 31) return true; // privada
|
|
47
|
+
if (a === 192 && b === 168) return true; // privada
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (versao === 6) {
|
|
51
|
+
if (h === '::1' || h === '::') return true; // loopback / unspecified
|
|
52
|
+
if (h.startsWith('fc') || h.startsWith('fd')) return true; // unique local (fc00::/7)
|
|
53
|
+
if (h.startsWith('fe80:') || h.startsWith('fe8') || h.startsWith('fe9') || h.startsWith('fea') || h.startsWith('feb')) return true; // link-local
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return false; // hostname comum (DNS) — não dá pra saber o IP sem resolver
|
|
57
|
+
}
|
|
58
|
+
|
|
32
59
|
const TIMEOUT_MS = 12000;
|
|
33
60
|
const MAX_POR_FONTE = 4;
|
|
34
61
|
const MAX_TEXTO = 4500;
|
|
@@ -72,6 +99,7 @@ function pegar(url, { timeout = TIMEOUT_MS, saltos = 0 } = {}) {
|
|
|
72
99
|
// 5 saltos cobre qualquer doc real; sem teto, um redirect em loop
|
|
73
100
|
// (A→B→A→...) trava essa fonte — e a pesquisa inteira — para sempre.
|
|
74
101
|
if (saltos > 5) return resolve(null);
|
|
102
|
+
if (hostBloqueado(alvo.hostname)) return resolve(null);
|
|
75
103
|
const cliente = alvo.protocol === 'http:' ? http : https;
|
|
76
104
|
|
|
77
105
|
const opcoes = {
|
package/lib/studio.js
CHANGED
|
@@ -191,8 +191,43 @@ const atualizar = ({ id, titulo, doc }) => comStudio(async () => {
|
|
|
191
191
|
return { ok: true, ...r };
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
+
// ── Mídia: a imagem de fundo e a música do usuário ───────────────────────
|
|
195
|
+
// Sem isto o agente não tinha como pôr foto de fundo nem trilha própria: a
|
|
196
|
+
// API pede base64, e ele não ia montar data URI na mão dentro de um curl.
|
|
197
|
+
const TIPOS_MIDIA = {
|
|
198
|
+
'.png': 1, '.jpg': 1, '.jpeg': 1, '.gif': 1, '.webp': 1, '.svg': 1, '.avif': 1,
|
|
199
|
+
'.mp3': 1, '.m4a': 1, '.wav': 1, '.ogg': 1, '.aac': 1, '.flac': 1, '.opus': 1, '.weba': 1,
|
|
200
|
+
'.mp4': 1, '.webm': 1, '.mov': 1,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const midiaListar = () => comStudio(async () => ({ ok: true, midia: await pedir('GET', '/api/midia') }));
|
|
204
|
+
|
|
205
|
+
const midiaEnviar = ({ arquivo, nome }) => comStudio(async () => {
|
|
206
|
+
if (!arquivo) return { ok: false, error: 'Diga o caminho do arquivo (arquivo: "foto.jpg").' };
|
|
207
|
+
const caminho = path.resolve(arquivo);
|
|
208
|
+
if (!fs.existsSync(caminho)) return { ok: false, error: `Arquivo não existe: ${arquivo}` };
|
|
209
|
+
|
|
210
|
+
const ext = path.extname(caminho).toLowerCase();
|
|
211
|
+
if (!TIPOS_MIDIA[ext]) {
|
|
212
|
+
return { ok: false, error: `Formato não aceito: ${ext}`, aceitos: Object.keys(TIPOS_MIDIA).join(' ') };
|
|
213
|
+
}
|
|
214
|
+
const bytes = fs.statSync(caminho).size;
|
|
215
|
+
if (bytes > 60 * 1024 * 1024) return { ok: false, error: `Arquivo tem ${(bytes / 1048576).toFixed(1)} MB; o limite é 60 MB.` };
|
|
216
|
+
|
|
217
|
+
const tipo = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
|
|
218
|
+
'.webp': 'image/webp', '.svg': 'image/svg+xml', '.avif': 'image/avif', '.mp3': 'audio/mpeg',
|
|
219
|
+
'.m4a': 'audio/mp4', '.wav': 'audio/wav', '.ogg': 'audio/ogg', '.aac': 'audio/aac',
|
|
220
|
+
'.flac': 'audio/flac', '.opus': 'audio/opus', '.weba': 'audio/webm', '.mp4': 'video/mp4',
|
|
221
|
+
'.webm': 'video/webm', '.mov': 'video/quicktime' }[ext] || 'application/octet-stream';
|
|
222
|
+
|
|
223
|
+
const dados = `data:${tipo};base64,` + fs.readFileSync(caminho).toString('base64');
|
|
224
|
+
const r = await pedir('POST', '/api/midia', { nome: nome || path.basename(caminho), dados }, 120000);
|
|
225
|
+
return { ok: true, ...r, comoUsar: `Use "${r.url}" em fundo.imagem, em src de uma imagem, ou em trilha.src.` };
|
|
226
|
+
});
|
|
227
|
+
|
|
194
228
|
module.exports = {
|
|
195
229
|
BASE, PORTA, CASA,
|
|
196
230
|
garantirNoAr, estaNoAr, prepararCasa, pythonCmd,
|
|
197
231
|
manual, ferramentas, spec, listar, ler, criar, atualizar,
|
|
232
|
+
midiaListar, midiaEnviar,
|
|
198
233
|
};
|
package/lib/tools.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const os = require('os');
|
|
13
|
-
const { exec } = require('child_process');
|
|
13
|
+
const { exec, execFile } = require('child_process');
|
|
14
14
|
const browser = require('./browser');
|
|
15
15
|
const desktop = require('./desktop');
|
|
16
16
|
const referencia = require('./referencia');
|
|
@@ -165,19 +165,41 @@ async function runShell(command, timeout = 60000, cwd) {
|
|
|
165
165
|
});
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
|
|
168
|
+
// `target` vem do modelo (args.target) — que por sua vez pode ter lido de uma
|
|
169
|
+
// página não confiável (prompt injection via pesquisar_referencia/browser_read).
|
|
170
|
+
// execFile com array de argumentos nunca passa por shell nenhum: "target" vira
|
|
171
|
+
// UM argumento, não texto que um shell reinterpreta — sem isso, um valor tipo
|
|
172
|
+
// `https://x/"; touch /tmp/pwned; echo "` injetava comando (achado de segurança).
|
|
173
|
+
function openInBrowser(target) {
|
|
169
174
|
const p = os.platform();
|
|
170
|
-
const cmd = p === 'darwin' ?
|
|
171
|
-
|
|
172
|
-
|
|
175
|
+
const [cmd, args, opener] = p === 'darwin' ? ['open', [target], 'open']
|
|
176
|
+
: p === 'win32' ? ['cmd.exe', ['/c', 'start', '', target], 'start']
|
|
177
|
+
: ['xdg-open', [target], 'xdg-open'];
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
execFile(cmd, args, { timeout: 10000 }, (error, stdout, stderr) => {
|
|
180
|
+
resolve(error ? { ok: false, error: (stderr || error.message || '').toString() } : { ok: true, opener });
|
|
181
|
+
});
|
|
182
|
+
});
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
async function runInNewTerminal(command, cwd) {
|
|
176
186
|
const p = os.platform();
|
|
187
|
+
const pastaAlvo = cwd || '.';
|
|
188
|
+
// pastaAlvo normalmente é a pasta do projeto, mas pode vir de input do
|
|
189
|
+
// modelo — sem checar, um valor malicioso quebra pra fora do "cd" embutido
|
|
190
|
+
// no comando que abre o terminal (AppleScript/cmd/x-terminal-emulator),
|
|
191
|
+
// que reinterpreta a string inteira como shell (achado de segurança).
|
|
192
|
+
// Recusa em vez de tentar escapar certo em três dialetos de shell ao
|
|
193
|
+
// mesmo tempo (osascript, cmd.exe, sh) — "command" continua livre de
|
|
194
|
+
// propósito, é o próprio contrato desta tool (abrir terminal com um
|
|
195
|
+
// comando de shell de verdade).
|
|
196
|
+
if (/["'`;&|$\\]/.test(pastaAlvo)) {
|
|
197
|
+
return { ok: false, error: `Pasta com caractere não permitido: ${pastaAlvo}` };
|
|
198
|
+
}
|
|
177
199
|
let cmd;
|
|
178
|
-
if (p === 'darwin') cmd = `osascript -e 'tell application "Terminal" to do script "cd ${
|
|
179
|
-
else if (p === 'win32') cmd = `start cmd /k "cd /d ${
|
|
180
|
-
else cmd = `x-terminal-emulator -e "bash -c 'cd ${
|
|
200
|
+
if (p === 'darwin') cmd = `osascript -e 'tell application "Terminal" to do script "cd ${pastaAlvo} && ${command}"'`;
|
|
201
|
+
else if (p === 'win32') cmd = `start cmd /k "cd /d ${pastaAlvo} && ${command}"`;
|
|
202
|
+
else cmd = `x-terminal-emulator -e "bash -c 'cd ${pastaAlvo} && ${command}; exec bash'"`;
|
|
181
203
|
const r = await runShell(cmd);
|
|
182
204
|
return r.ok ? { ok: true, opener: cmd.split(' ')[0] } : { ok: false, error: r.stderr || 'falhou' };
|
|
183
205
|
}
|
|
@@ -476,6 +498,11 @@ async function despachar(name, args, ctx) {
|
|
|
476
498
|
return studio.atualizar({ id: args.id, titulo: args.titulo, doc: args.doc });
|
|
477
499
|
case 'studio_listar':
|
|
478
500
|
return studio.listar();
|
|
501
|
+
case 'studio_midia':
|
|
502
|
+
// Sem arquivo, lista o que já foi enviado; com arquivo, envia.
|
|
503
|
+
return args.arquivo
|
|
504
|
+
? studio.midiaEnviar({ arquivo: resolveInProject(args.arquivo, projectDir), nome: args.nome })
|
|
505
|
+
: studio.midiaListar();
|
|
479
506
|
|
|
480
507
|
case 'desktop_double_click':
|
|
481
508
|
return desktop.doubleClick(args.x, args.y);
|
package/lib/ui.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
'use strict';
|
|
6
6
|
|
|
7
|
-
const readline = require('readline');
|
|
8
7
|
|
|
9
8
|
// ── Paleta ───────────────────────────────────────────────────────────────
|
|
10
9
|
// Vem do design system do produto (product-plan/design-system): primária cyan,
|
|
@@ -264,39 +263,6 @@ class Spinner {
|
|
|
264
263
|
}
|
|
265
264
|
}
|
|
266
265
|
|
|
267
|
-
// Barra de progresso estilo Claude Code — silenciosa, elegante
|
|
268
|
-
class ProgressBar {
|
|
269
|
-
constructor(label = 'Instalando') {
|
|
270
|
-
this.label = label;
|
|
271
|
-
this.width = 30;
|
|
272
|
-
this.progress = 0;
|
|
273
|
-
this.timer = null;
|
|
274
|
-
this.done = false;
|
|
275
|
-
}
|
|
276
|
-
start() {
|
|
277
|
-
process.stdout.write(` ${c.muted(this.label)} [`);
|
|
278
|
-
this.timer = setInterval(() => {
|
|
279
|
-
if (this.done) return;
|
|
280
|
-
this.progress = Math.min(this.progress + 1, this.width);
|
|
281
|
-
const filled = '█'.repeat(this.progress);
|
|
282
|
-
const empty = '░'.repeat(this.width - this.progress);
|
|
283
|
-
process.stdout.write(`\r ${c.muted(this.label)} [${c.brand(filled)}${c.dim(empty)}] ${c.muted(Math.round((this.progress/this.width)*100) + '%')}`);
|
|
284
|
-
}, 200);
|
|
285
|
-
return this;
|
|
286
|
-
}
|
|
287
|
-
tick() {
|
|
288
|
-
this.progress = Math.min(this.progress + 2, this.width);
|
|
289
|
-
const filled = '█'.repeat(this.progress);
|
|
290
|
-
const empty = '░'.repeat(this.width - this.progress);
|
|
291
|
-
process.stdout.write(`\r ${c.muted(this.label)} [${c.brand(filled)}${c.dim(empty)}] ${c.muted(Math.round((this.progress/this.width)*100) + '%')}`);
|
|
292
|
-
}
|
|
293
|
-
stop() {
|
|
294
|
-
this.done = true;
|
|
295
|
-
if (this.timer) { clearInterval(this.timer); this.timer = null; }
|
|
296
|
-
process.stdout.write(`\r ${c.muted(this.label)} [${c.ok('██████████ DONE ██████████')}] ${c.ok('100%')}\n`);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
266
|
function actionHeader(name, args) {
|
|
301
267
|
const nice = {
|
|
302
268
|
read_file:'Read', write_file:'Write', edit_file:'Edit', multi_edit:'MultiEdit',
|
|
@@ -422,38 +388,5 @@ function friendlyError(raw) {
|
|
|
422
388
|
return clean.length > 200 ? clean.slice(0, 200) + '…' : clean || 'Algo deu errado.';
|
|
423
389
|
}
|
|
424
390
|
|
|
425
|
-
function toolLine(text) { return ` ${c.dim('⎿')} ${c.muted(text)}`; }
|
|
426
|
-
|
|
427
|
-
function toolResult(result) {
|
|
428
|
-
if (!result) return '';
|
|
429
|
-
if (result.ok === false) return c.err(` ✗ ${result.error || 'failed'}`);
|
|
430
|
-
if (result.created !== undefined) return c.ok(` ${result.created?'created':'updated'} · ${fmtBytes(result.bytes||0)}`);
|
|
431
|
-
return '';
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function fmtBytes(n) {
|
|
435
|
-
if (n < 1024) return `${n} B`;
|
|
436
|
-
if (n < 1024*1024) return `${(n/1024).toFixed(1)} KB`;
|
|
437
|
-
return `${(n/(1024*1024)).toFixed(1)} MB`;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function renderMarkdown(text) {
|
|
441
|
-
return text.replace(/\*\*([^*]+)\*\*/g,'$1').replace(/__([^_]+)__/g,'$1').replace(/`([^`]+)`/g,'$1').replace(/^#{1,6}\s+/gm,'');
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Pergunta Yes/No — aguarda resposta real do usuário
|
|
445
|
-
function askYesNo(question) {
|
|
446
|
-
return new Promise((resolve) => {
|
|
447
|
-
const rl = readline.createInterface({
|
|
448
|
-
input: process.stdin,
|
|
449
|
-
output: process.stdout,
|
|
450
|
-
});
|
|
451
|
-
rl.question(`${c.brand('?')} ${question} ${c.muted('(Y/n)')} `, (answer) => {
|
|
452
|
-
rl.close();
|
|
453
|
-
const a = (answer || '').trim().toLowerCase();
|
|
454
|
-
resolve(a === 'y' || a === 'yes' || a === '');
|
|
455
|
-
});
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
391
|
|
|
459
|
-
module.exports = { c, mascot, welcomeBox, Spinner,
|
|
392
|
+
module.exports = { c, mascot, welcomeBox, Spinner, actionHeader, friendlyError, visibleWidth, stripAnsi, truncate, padRight };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.2.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": {
|
|
@@ -60,6 +60,39 @@ livre de direitos, ou uma das trilhas geradas acima.
|
|
|
60
60
|
|
|
61
61
|
---
|
|
62
62
|
|
|
63
|
+
## Narração
|
|
64
|
+
|
|
65
|
+
A voz que fala por cima do vídeo. Vai **na cena**, e o estúdio narra sozinho
|
|
66
|
+
na hora de tocar — sem gravar nada, sem arquivo:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{ "layout": "centro", "duracao": 6,
|
|
70
|
+
"narracao": "O texto que a voz vai falar nesta cena.",
|
|
71
|
+
"elementos": [ ... ] }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Ajuste da voz no **documento** (opcional):
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
"voz": { "velocidade": 1, "tom": 1, "volume": 1, "lang": "pt-BR" }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Regras que fazem diferença:
|
|
81
|
+
|
|
82
|
+
- **Escreva para o ouvido, não para o olho.** A narração NÃO repete o texto da
|
|
83
|
+
tela: a tela dá o título, a voz dá o argumento. Repetir os dois é o erro mais
|
|
84
|
+
comum, e soa a leitura de slide.
|
|
85
|
+
- **Caiba na duração.** Fale ~2,5 palavras por segundo: uma cena de 6s aguenta
|
|
86
|
+
~15 palavras. Texto mais longo que a cena é cortado no meio.
|
|
87
|
+
- **Uma ideia por cena**, em uma ou duas frases curtas. Ponto final ajuda a voz
|
|
88
|
+
a respirar.
|
|
89
|
+
- Para o vídeo ficar mudo de propósito, é só não pôr `narracao` em cena nenhuma.
|
|
90
|
+
|
|
91
|
+
O usuário também pode gravar a própria voz pelo editor; aí a cena ganha
|
|
92
|
+
`narracaoAudio` com o arquivo, e ela tem prioridade sobre a voz sintetizada.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
63
96
|
## Transições
|
|
64
97
|
|
|
65
98
|
Na cena, `transicao` define como ela **entra**:
|
package/studio/server.py
CHANGED
|
@@ -395,6 +395,24 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
395
395
|
except Exception:
|
|
396
396
|
return {}
|
|
397
397
|
|
|
398
|
+
# Achado de segurança: POST/PUT/DELETE sem checagem de Origin é CSRF — uma
|
|
399
|
+
# página de terceiro (aberta no MESMO navegador, sem relação nenhuma com o
|
|
400
|
+
# Studio) consegue disparar um fetch() com Content-Type: text/plain (que
|
|
401
|
+
# não exige preflight de CORS) contra http://127.0.0.1:7777 e o pedido
|
|
402
|
+
# chega igual a um legítimo. Bloqueia quando o Origin existe e não é o
|
|
403
|
+
# próprio servidor — sem Origin nenhum (cliente não-navegador, como o CLI
|
|
404
|
+
# do PrimoCode via http nativo do Node) continua liberado, já que isso
|
|
405
|
+
# nunca é o vetor de um navegador sendo usado contra o usuário.
|
|
406
|
+
def origem_ok(self):
|
|
407
|
+
origem = self.headers.get("Origin")
|
|
408
|
+
if not origem:
|
|
409
|
+
return True
|
|
410
|
+
esperados = {
|
|
411
|
+
"http://127.0.0.1:%d" % PORTA,
|
|
412
|
+
"http://localhost:%d" % PORTA,
|
|
413
|
+
}
|
|
414
|
+
return origem in esperados
|
|
415
|
+
|
|
398
416
|
# -------------------------------------------------------------- rotas --
|
|
399
417
|
|
|
400
418
|
@seguro
|
|
@@ -502,6 +520,8 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
502
520
|
|
|
503
521
|
@seguro
|
|
504
522
|
def do_POST(self):
|
|
523
|
+
if not self.origem_ok():
|
|
524
|
+
return self.js({"erro": "origem não permitida"}, 403)
|
|
505
525
|
rota = urlparse(self.path).path
|
|
506
526
|
if rota == "/api/midia":
|
|
507
527
|
corpo, status = guardar_midia(self.corpo())
|
|
@@ -527,6 +547,8 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
527
547
|
|
|
528
548
|
@seguro
|
|
529
549
|
def do_PUT(self):
|
|
550
|
+
if not self.origem_ok():
|
|
551
|
+
return self.js({"erro": "origem não permitida"}, 403)
|
|
530
552
|
m = re.fullmatch(r"/api/arquivos/([^/]+)", urlparse(self.path).path)
|
|
531
553
|
if not m:
|
|
532
554
|
return self.js({"erro": "não encontrado"}, 404)
|
|
@@ -547,6 +569,8 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
547
569
|
|
|
548
570
|
@seguro
|
|
549
571
|
def do_DELETE(self):
|
|
572
|
+
if not self.origem_ok():
|
|
573
|
+
return self.js({"erro": "origem não permitida"}, 403)
|
|
550
574
|
rota = urlparse(self.path).path
|
|
551
575
|
m = re.fullmatch(r"/api/midia/([^/]+)", rota)
|
|
552
576
|
if m:
|
package/studio/web/tool.html
CHANGED
|
@@ -65,6 +65,10 @@
|
|
|
65
65
|
document.getElementById("btn-pdf").innerHTML = svgIcone("tela");
|
|
66
66
|
document.getElementById("btn-baixar").innerHTML = svgIcone("baixar");
|
|
67
67
|
</script>
|
|
68
|
+
<script src="/js/audio.js"></script>
|
|
69
|
+
<script src="/js/narracao.js"></script>
|
|
70
|
+
<script src="/js/midia.js"></script>
|
|
71
|
+
<script src="/js/exportar.js"></script>
|
|
68
72
|
<script src="/js/editor.js"></script>
|
|
69
73
|
</body>
|
|
70
74
|
</html>
|
package/studio/web/view.html
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
<script src="/js/render.js"></script>
|
|
16
16
|
<script src="/js/render-prosa.js"></script>
|
|
17
17
|
<script src="/js/render-grade.js"></script>
|
|
18
|
+
<script src="/js/audio.js"></script>
|
|
19
|
+
<script src="/js/narracao.js"></script>
|
|
18
20
|
<script src="/js/player.js"></script>
|
|
19
21
|
</body>
|
|
20
22
|
</html>
|