primocode 8.0.2 → 8.1.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/README.md +3 -3
- package/bin/primocode.js +26 -0
- package/lib/act.js +5 -3
- package/lib/browser.js +5 -31
- package/lib/catalogo.js +80 -44
- package/lib/claude-engine.js +354 -0
- package/lib/config.js +8 -3
- package/lib/desktop.js +13 -2
- package/lib/modo.js +1 -1
- package/lib/referencia.js +28 -0
- package/lib/tools.js +30 -8
- package/lib/ui.js +1 -68
- package/package.json +2 -2
- package/studio/server.py +24 -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
|
@@ -20,6 +20,7 @@ const api = require('../lib/api');
|
|
|
20
20
|
const tools = require('../lib/tools');
|
|
21
21
|
const cfg = require('../lib/config');
|
|
22
22
|
const { runAct, interromper } = require('../lib/act');
|
|
23
|
+
const claudeEngine = require('../lib/claude-engine');
|
|
23
24
|
const pasta = require('../lib/pasta');
|
|
24
25
|
const catalogo = require('../lib/catalogo');
|
|
25
26
|
const effort = require('../lib/effort');
|
|
@@ -53,6 +54,7 @@ const state = {
|
|
|
53
54
|
currentProject: null,
|
|
54
55
|
workDir: null, // pasta onde o agente trabalha nesta sessão
|
|
55
56
|
workDirOrigem: null, // 'padrao' | '/dir' | 'detectada' | 'projeto' — para o /dir explicar
|
|
57
|
+
engine: 'server', // 'server' (OpenRouter/Groq) | 'claude-code' (/claude)
|
|
56
58
|
};
|
|
57
59
|
|
|
58
60
|
const HELP = `
|
|
@@ -61,6 +63,7 @@ ${c.bold('Comandos')}
|
|
|
61
63
|
${c.brand('/project')} lista projetos ou cria em ~/primocode: /project <nome>
|
|
62
64
|
${c.brand('/mode')} /mode primocode|chat|auto
|
|
63
65
|
${c.brand('/effort')} esforço do agente — do rápido ao mais esperto
|
|
66
|
+
${c.brand('/claude')} liga o Claude Code local como motor (sem API paga) · /claude off volta ao padrão
|
|
64
67
|
${c.brand('/attach')} <arquivo> anexa arquivo ao próximo pedido
|
|
65
68
|
${c.brand('/files')} lista anexos ${c.brand('/detach')} remove
|
|
66
69
|
${c.brand('/tudo')} lista tudo que ele sabe fazer
|
|
@@ -99,6 +102,7 @@ const SLASH_COMANDOS = [
|
|
|
99
102
|
['/dir', 'trabalhar numa pasta sua'],
|
|
100
103
|
['/mode', 'primocode | chat | auto'],
|
|
101
104
|
['/effort', 'esforço: low → ultracode (←/→ para ajustar)'],
|
|
105
|
+
['/claude', 'liga o Claude Code local como motor · /claude off volta ao padrão'],
|
|
102
106
|
['/attach', 'anexa arquivo ao pedido'],
|
|
103
107
|
['/detach', 'remove os anexos'],
|
|
104
108
|
['/files', 'lista os anexos'],
|
|
@@ -534,6 +538,7 @@ async function sendPrompt(userText) {
|
|
|
534
538
|
// Só chamado se o agente for mesmo MEXER EM ARQUIVO — abrir o Chrome
|
|
535
539
|
// não é motivo para criar pasta de projeto.
|
|
536
540
|
ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
|
|
541
|
+
motor: state.engine === 'claude-code' ? claudeEngine : api,
|
|
537
542
|
});
|
|
538
543
|
state.history.push({ role: 'user', content: userText });
|
|
539
544
|
state.history.push({ role: 'assistant', content: r.assistantMessage.content });
|
|
@@ -682,6 +687,27 @@ async function handleSlash(text) {
|
|
|
682
687
|
break;
|
|
683
688
|
}
|
|
684
689
|
|
|
690
|
+
case 'claude': {
|
|
691
|
+
if (rest.toLowerCase() === 'off') {
|
|
692
|
+
if (state.engine !== 'claude-code') { console.log(c.muted(' Já estava no motor padrão.')); break; }
|
|
693
|
+
state.engine = 'server';
|
|
694
|
+
console.log(c.ok(' Motor: OpenRouter/Groq (padrão)'));
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
console.log(c.dim(' Checando Claude Code e ORCA…'));
|
|
698
|
+
const r = await claudeEngine.garantirEngine();
|
|
699
|
+
if (!r.ok) {
|
|
700
|
+
console.log(c.err(` ${r.error}`));
|
|
701
|
+
if (r.comoResolver) console.log(c.muted(` ${r.comoResolver}`));
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
state.engine = 'claude-code';
|
|
705
|
+
const onde = r.caminho === 'A' ? 'sessão contínua via ORCA' : 'chamada avulsa (claude -p, sem ORCA)';
|
|
706
|
+
console.log(c.ok(` Motor: Claude Code (${onde})`));
|
|
707
|
+
console.log(c.muted(' /claude off volta pro motor padrão.'));
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
|
|
685
711
|
// O /model virou /effort: você escolhe o ESFORÇO num slider (←/→,
|
|
686
712
|
// Enter confirma, Esc cancela) e o modelo sai daí. Com argumento
|
|
687
713
|
// ("/effort high") aplica direto, para quem já sabe o que quer.
|
package/lib/act.js
CHANGED
|
@@ -173,6 +173,7 @@ async function runAct(userPrompt, opts) {
|
|
|
173
173
|
const {
|
|
174
174
|
server, model, token, history, projectDir, projectName,
|
|
175
175
|
ensureWorkDir, profundidade = 0, subagent = false, effort,
|
|
176
|
+
motor = api,
|
|
176
177
|
} = opts || {};
|
|
177
178
|
|
|
178
179
|
// A pasta de projeto só nasce quando o agente for mesmo MEXER EM ARQUIVO.
|
|
@@ -209,7 +210,7 @@ async function runAct(userPrompt, opts) {
|
|
|
209
210
|
let streaming = false;
|
|
210
211
|
|
|
211
212
|
try {
|
|
212
|
-
await
|
|
213
|
+
await motor.requestAct(server, { messages, model, effort, token, projectDir, projectName, subagent }, (evt) => {
|
|
213
214
|
if (evt.erro) { streamError = evt.erro; return; }
|
|
214
215
|
if (evt.tool_call) { toolCalls.push(evt.tool_call); return; }
|
|
215
216
|
if (evt.delta) {
|
|
@@ -282,7 +283,7 @@ async function runAct(userPrompt, opts) {
|
|
|
282
283
|
|
|
283
284
|
if (tc.name === 'spawn_agent') {
|
|
284
285
|
const r = await runSubagent(tc.arguments || {}, {
|
|
285
|
-
server, model, token, projectDir: pastaAtual, projectName, profundidade,
|
|
286
|
+
server, model, token, projectDir: pastaAtual, projectName, profundidade, motor,
|
|
286
287
|
});
|
|
287
288
|
messages.push({ role: 'tool', tool_call_id: tc.id, content: r });
|
|
288
289
|
continue;
|
|
@@ -358,7 +359,7 @@ async function runAct(userPrompt, opts) {
|
|
|
358
359
|
}
|
|
359
360
|
|
|
360
361
|
async function runSubagent(args, ctx) {
|
|
361
|
-
const { server, model, token, projectDir, projectName, profundidade } = ctx;
|
|
362
|
+
const { server, model, token, projectDir, projectName, profundidade, motor } = ctx;
|
|
362
363
|
const descricao = args.description || 'sub-tarefa';
|
|
363
364
|
|
|
364
365
|
if (profundidade + 1 >= MAX_PROFUNDIDADE) {
|
|
@@ -373,6 +374,7 @@ async function runSubagent(args, ctx) {
|
|
|
373
374
|
history: [],
|
|
374
375
|
profundidade: profundidade + 1,
|
|
375
376
|
subagent: true,
|
|
377
|
+
motor,
|
|
376
378
|
});
|
|
377
379
|
|
|
378
380
|
const resumo = r.assistantMessage.content || 'sem resultado';
|
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
|
@@ -20,85 +20,121 @@ const GRUPOS = [
|
|
|
20
20
|
nome: 'Arquivos',
|
|
21
21
|
resumo: 'ler, criar e mudar arquivo na pasta de trabalho',
|
|
22
22
|
itens: [
|
|
23
|
-
{ tool: 'write_file', escreve: true, faz: 'cria arquivo, ou reescreve inteiro'
|
|
24
|
-
|
|
25
|
-
{ tool: '
|
|
26
|
-
|
|
27
|
-
{ tool: '
|
|
28
|
-
|
|
29
|
-
{ tool: '
|
|
23
|
+
{ tool: 'write_file', escreve: true, faz: 'cria arquivo, ou reescreve inteiro',
|
|
24
|
+
args: 'path: caminho relativo, ex "src/app.js". content: conteúdo completo do arquivo.' },
|
|
25
|
+
{ tool: 'edit_file', escreve: true, faz: 'troca um trecho exato, sem reescrever o resto',
|
|
26
|
+
args: 'path. old_string: trecho exato a substituir, precisa ser único no arquivo. new_string: texto que entra no lugar. replace_all (opcional, bool, padrão false): troca todas as ocorrências.' },
|
|
27
|
+
{ tool: 'multi_edit', escreve: true, faz: 'várias trocas no mesmo arquivo, tudo ou nada',
|
|
28
|
+
args: 'path. edits: lista de {old_string, new_string, replace_all (opcional)}, aplicadas em ordem.' },
|
|
29
|
+
{ tool: 'read_file', escreve: false, faz: 'lê arquivo, inteiro ou por pedaço',
|
|
30
|
+
args: 'path. offset (opcional, número): primeira linha a ler. limit (opcional, número): quantas linhas.' },
|
|
31
|
+
{ tool: 'list_dir', escreve: false, faz: 'mostra a árvore de pastas',
|
|
32
|
+
args: 'path (opcional, padrão "."). depth (opcional, número, padrão 2).' },
|
|
33
|
+
{ tool: 'glob', escreve: false, faz: 'acha arquivo pelo nome (**/*.js)',
|
|
34
|
+
args: 'pattern: padrão de nome, ex "**/*.js" ou "src/**/*.{ts,tsx}".' },
|
|
35
|
+
{ tool: 'grep', escreve: false, faz: 'acha texto dentro dos arquivos',
|
|
36
|
+
args: 'pattern: expressão regular. glob (opcional): restringe aos arquivos que casam. ignoreCase (opcional, bool). contexto (opcional, número): linhas ao redor do achado.' },
|
|
30
37
|
],
|
|
31
38
|
},
|
|
32
39
|
{
|
|
33
40
|
nome: 'Terminal',
|
|
34
41
|
resumo: 'rodar comando na sua máquina',
|
|
35
42
|
itens: [
|
|
36
|
-
{ tool: 'run_shell', escreve: false, faz: 'roda comando e devolve a saída'
|
|
37
|
-
|
|
43
|
+
{ tool: 'run_shell', escreve: false, faz: 'roda comando e devolve a saída',
|
|
44
|
+
args: 'command. timeout_ms (opcional, número, padrão 60000).' },
|
|
45
|
+
{ tool: 'run_in_new_terminal', escreve: false, faz: 'abre outro terminal para o que fica de pé (servidor, watch)',
|
|
46
|
+
args: 'command. cwd (opcional): pasta onde rodar.' },
|
|
38
47
|
],
|
|
39
48
|
},
|
|
40
49
|
{
|
|
41
50
|
nome: 'Navegador',
|
|
42
51
|
resumo: 'controla um navegador de verdade (Chrome, Edge, Brave, Arc…) — clica, digita, lê, fotografa',
|
|
43
52
|
itens: [
|
|
44
|
-
{ tool: 'browser_open', escreve: false, faz: 'abre uma página ou um arquivo seu'
|
|
45
|
-
|
|
46
|
-
{ tool: '
|
|
47
|
-
|
|
48
|
-
{ tool: '
|
|
49
|
-
|
|
50
|
-
{ tool: '
|
|
51
|
-
|
|
52
|
-
{ tool: '
|
|
53
|
-
|
|
53
|
+
{ tool: 'browser_open', escreve: false, faz: 'abre uma página ou um arquivo seu',
|
|
54
|
+
args: 'url: http(s) ou caminho de arquivo do projeto.' },
|
|
55
|
+
{ tool: 'browser_click', escreve: false, faz: 'clica por texto, por seletor ou por coordenada',
|
|
56
|
+
args: 'text (opcional): texto visível do elemento. selector (opcional): seletor CSS. x, y (opcional, número): último recurso.' },
|
|
57
|
+
{ tool: 'browser_type', escreve: false, faz: 'digita num campo',
|
|
58
|
+
args: 'text. selector (opcional): foca esse campo antes de digitar. clear (opcional, bool, padrão true).' },
|
|
59
|
+
{ tool: 'browser_key', escreve: false, faz: 'aperta tecla (Enter, Tab, Escape)',
|
|
60
|
+
args: 'key: nome da tecla (Enter, Tab, Escape, ArrowDown, ArrowUp, ArrowLeft, ArrowRight, Backspace).' },
|
|
61
|
+
{ tool: 'browser_read', escreve: false, faz: 'lê o texto da página para conferir o resultado',
|
|
62
|
+
args: 'selector (opcional): lê só esse elemento; sem isso, a página inteira.' },
|
|
63
|
+
{ tool: 'browser_screenshot', escreve: true, faz: 'tira foto da página',
|
|
64
|
+
args: 'path (opcional): onde salvar, ex "print.png". Sem path, usa um nome padrão.' },
|
|
65
|
+
{ tool: 'browser_scroll', escreve: false, faz: 'rola a página',
|
|
66
|
+
args: 'dy (opcional, número, padrão 600).' },
|
|
67
|
+
{ tool: 'browser_wait', escreve: false, faz: 'espera um elemento aparecer',
|
|
68
|
+
args: 'selector: seletor CSS a esperar. timeout_ms (opcional, número, padrão 15000).' },
|
|
69
|
+
{ tool: 'browser_eval', escreve: false, faz: 'roda JavaScript na página',
|
|
70
|
+
args: 'js: trecho de JavaScript a rodar na página, devolve o resultado.' },
|
|
71
|
+
{ tool: 'open_in_browser', escreve: false, faz: 'abre no seu navegador normal, sem controlar',
|
|
72
|
+
args: 'target: arquivo do projeto ou URL.' },
|
|
54
73
|
],
|
|
55
74
|
},
|
|
56
75
|
{
|
|
57
76
|
nome: 'Estúdio',
|
|
58
77
|
resumo: 'a suíte de criação do PrimoCode — sobe sozinha, não precisa de app externo',
|
|
59
78
|
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
|
-
|
|
63
|
-
{ tool: '
|
|
64
|
-
|
|
65
|
-
{ tool: '
|
|
79
|
+
{ tool: 'studio_manual', escreve: false, faz: 'lê o manual do estúdio e as seis ferramentas', args: null },
|
|
80
|
+
{ tool: 'studio_spec', escreve: false, faz: 'lê a spec de uma ferramenta (prisma, tela, prosa…)',
|
|
81
|
+
args: 'ferramenta: prisma | tela | prosa | grade | corte | traco.' },
|
|
82
|
+
{ tool: 'studio_criar', escreve: false, faz: 'cria slides, arte, documento, planilha, vídeo ou diagrama',
|
|
83
|
+
args: 'ferramenta: prisma | tela | prosa | grade | corte | traco. titulo. doc: objeto no formato que studio_spec daquela ferramenta descreve.' },
|
|
84
|
+
{ tool: 'studio_ler', escreve: false, faz: 'lê algo que já foi criado', args: 'id.' },
|
|
85
|
+
{ tool: 'studio_atualizar', escreve: false, faz: 'ajusta o que já existe, sem refazer do zero',
|
|
86
|
+
args: 'id. titulo (opcional). doc (opcional).' },
|
|
87
|
+
{ tool: 'studio_listar', escreve: false, faz: 'lista o que já existe no estúdio', args: null },
|
|
66
88
|
],
|
|
67
89
|
},
|
|
68
90
|
{
|
|
69
91
|
nome: 'Computador',
|
|
70
92
|
resumo: 'janelas, botões, mouse e teclado — age em qualquer programa e na área de trabalho',
|
|
71
93
|
itens: [
|
|
72
|
-
{ tool: 'desktop_open_app', escreve: false, faz: 'abre um programa pelo nome (Chrome, Bloco de Notas…)'
|
|
73
|
-
|
|
74
|
-
{ tool: '
|
|
75
|
-
{ tool: '
|
|
76
|
-
|
|
77
|
-
{ tool: '
|
|
78
|
-
|
|
79
|
-
{ tool: '
|
|
80
|
-
|
|
81
|
-
{ tool: '
|
|
82
|
-
|
|
83
|
-
{ tool: '
|
|
84
|
-
|
|
85
|
-
{ tool: '
|
|
94
|
+
{ tool: 'desktop_open_app', escreve: false, faz: 'abre um programa pelo nome (Chrome, Bloco de Notas…)',
|
|
95
|
+
args: 'app: nome do programa, como o usuário fala.' },
|
|
96
|
+
{ tool: 'desktop_windows', escreve: false, faz: 'lista as janelas abertas, com título', args: null },
|
|
97
|
+
{ tool: 'desktop_focus', escreve: false, faz: 'traz uma janela para a frente, pelo título',
|
|
98
|
+
args: 'window: título (ou pedaço único) da janela.' },
|
|
99
|
+
{ tool: 'desktop_elements', escreve: false, faz: 'lê os botões, campos e ícones da janela, com posição',
|
|
100
|
+
args: 'window (opcional): título da janela alvo; sem isso, a janela em foco. No Windows, "Program Manager" é a área de trabalho.' },
|
|
101
|
+
{ tool: 'desktop_click_element', escreve: false, faz: 'clica num botão ou ícone pelo NOME, sem coordenada',
|
|
102
|
+
args: 'name: nome (ou pedaço) do botão/campo/ícone. window (opcional). double (opcional, bool, padrão false): clique duplo.' },
|
|
103
|
+
{ tool: 'desktop_click', escreve: false, faz: 'clica na tela por coordenada',
|
|
104
|
+
args: 'x, y (número). button (opcional: left|right, padrão left).' },
|
|
105
|
+
{ tool: 'desktop_double_click', escreve: false, faz: 'clique duplo — abre ícone da área de trabalho',
|
|
106
|
+
args: 'x, y (número).' },
|
|
107
|
+
{ tool: 'desktop_move', escreve: false, faz: 'move o mouse', args: 'x, y (número).' },
|
|
108
|
+
{ tool: 'desktop_drag', escreve: false, faz: 'arrasta de um ponto a outro',
|
|
109
|
+
args: 'from_x, from_y, to_x, to_y (número).' },
|
|
110
|
+
{ tool: 'desktop_type', escreve: false, faz: 'digita no programa que está na frente', args: 'text.' },
|
|
111
|
+
{ tool: 'desktop_key', escreve: false, faz: 'aperta tecla ou atalho (Enter, ctrl+c, alt+tab)',
|
|
112
|
+
args: 'key: tecla ou atalho, ex "Enter", "ctrl+c", "ctrl+shift+t", "alt+tab", "win".' },
|
|
113
|
+
{ tool: 'desktop_scroll', escreve: false, faz: 'rola a tela', args: 'dy (opcional, número, padrão 300).' },
|
|
114
|
+
{ tool: 'desktop_size', escreve: false, faz: 'mede a tela, para se orientar antes de clicar', args: null },
|
|
115
|
+
{ tool: 'desktop_screenshot', escreve: true, faz: 'tira foto da sua tela',
|
|
116
|
+
args: 'path (opcional): onde salvar. Sem path, usa um nome padrão.' },
|
|
86
117
|
],
|
|
87
118
|
},
|
|
88
119
|
{
|
|
89
120
|
nome: 'Organização',
|
|
90
121
|
resumo: 'como ele se organiza numa tarefa grande',
|
|
91
122
|
itens: [
|
|
92
|
-
{ tool: 'write_todos', escreve: false, faz: 'monta o plano e vai marcando o que terminou'
|
|
93
|
-
|
|
123
|
+
{ tool: 'write_todos', escreve: false, faz: 'monta o plano e vai marcando o que terminou',
|
|
124
|
+
args: 'todos: lista de {content: a tarefa curta no imperativo, status: pending|in_progress|completed}. Reescreve a lista inteira a cada chamada; só uma "in_progress" por vez.' },
|
|
125
|
+
{ tool: 'pesquisar_referencia', escreve: false, faz: 'lê docs e exemplos antes de escrever — sobe a qualidade',
|
|
126
|
+
args: 'consulta: o que pesquisar, de preferência em inglês. paginas (opcional, número, padrão 3, máximo 5).' },
|
|
94
127
|
// Vai para memory-geral.md, FORA da pasta do projeto — por isso não
|
|
95
128
|
// é write_file/edit_file (a sandbox de resolveInProject bloqueia
|
|
96
129
|
// qualquer caminho fora do projeto, e é assim mesmo que tem que
|
|
97
130
|
// ser para os outros casos). Esta tool é a única porta pra lá.
|
|
98
|
-
{ tool: 'remember_general', escreve: false, faz: 'grava algo sobre o usuário que vale para todo projeto (memory-geral.md)'
|
|
131
|
+
{ tool: 'remember_general', escreve: false, faz: 'grava algo sobre o usuário que vale para todo projeto (memory-geral.md)',
|
|
132
|
+
args: 'fato: o que lembrar, em uma frase.' },
|
|
99
133
|
// Estas duas o loop do agente atende (lib/act.js), não o tools.execute.
|
|
100
|
-
{ tool: 'spawn_agent', escreve: false, faz: 'chama um subagente para uma parte da tarefa', noLoop: true
|
|
101
|
-
|
|
134
|
+
{ tool: 'spawn_agent', escreve: false, faz: 'chama um subagente para uma parte da tarefa', noLoop: true,
|
|
135
|
+
args: 'description: 3 a 5 palavras, aparece na tela do usuário. prompt: instrução completa e autossuficiente — o subagente não vê esta conversa.' },
|
|
136
|
+
{ tool: 'finish', escreve: false, faz: 'encerra dizendo o que fez', noLoop: true,
|
|
137
|
+
args: 'summary (opcional): uma frase do que foi feito.' },
|
|
102
138
|
],
|
|
103
139
|
},
|
|
104
140
|
];
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-engine.js — motor alternativo do agente: Claude Code local em vez
|
|
3
|
+
* de OpenRouter/Groq via primocode-server.
|
|
4
|
+
*
|
|
5
|
+
* Implementa a MESMA forma que lib/api.js (requestAct(server, payload,
|
|
6
|
+
* onEvent)) — é o que permite lib/act.js trocar de motor sem saber disso.
|
|
7
|
+
* Ver docs/superpowers/specs/2026-08-15-claude-engine-design.md.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const { execSync, execFileSync } = require('child_process');
|
|
16
|
+
|
|
17
|
+
const MARCA_INICIO = '<<<PRIMOCODE>>>';
|
|
18
|
+
const MARCA_FIM = '<<<FIM>>>';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Acha o ÚLTIMO bloco marcado no texto (pode vir ruído de terminal — prompt
|
|
22
|
+
* ANSI, texto solto — antes e depois) e devolve o JSON de dentro dele.
|
|
23
|
+
* null quando não achou bloco válido: quem chama decide o que fazer.
|
|
24
|
+
*/
|
|
25
|
+
function extrairDecisao(texto) {
|
|
26
|
+
const t = String(texto || '');
|
|
27
|
+
const inicio = t.lastIndexOf(MARCA_INICIO);
|
|
28
|
+
if (inicio === -1) return null;
|
|
29
|
+
const fim = t.indexOf(MARCA_FIM, inicio);
|
|
30
|
+
if (fim === -1) return null;
|
|
31
|
+
const bloco = t.slice(inicio + MARCA_INICIO.length, fim).trim();
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(bloco);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const catalogo = require('./catalogo');
|
|
40
|
+
|
|
41
|
+
/** Serializa lib/catalogo.js em texto — única fonte de verdade, sem
|
|
42
|
+
* duplicar a lista de ferramentas num arquivo separado. */
|
|
43
|
+
function catalogoParaTexto() {
|
|
44
|
+
const linhas = [];
|
|
45
|
+
for (const grupo of catalogo.GRUPOS) {
|
|
46
|
+
linhas.push(`## ${grupo.nome} — ${grupo.resumo}`);
|
|
47
|
+
for (const item of grupo.itens) {
|
|
48
|
+
const args = item.args ? ` | args: ${item.args}` : ' | sem args';
|
|
49
|
+
linhas.push(`- ${item.tool}: ${item.faz}${args}`);
|
|
50
|
+
}
|
|
51
|
+
linhas.push('');
|
|
52
|
+
}
|
|
53
|
+
return linhas.join('\n').trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Texto completo que ensina o Claude Code a se comportar como motor do
|
|
57
|
+
* PrimoCode. Vira o conteúdo de ~/.claude/skills/primocode-engine/SKILL.md
|
|
58
|
+
* (Task 4) e é referenciado na primeira mensagem de cada sessão. */
|
|
59
|
+
function textoBootstrap() {
|
|
60
|
+
return `---
|
|
61
|
+
name: primocode-engine
|
|
62
|
+
description: Skill instalada pelo PrimoCode para o modo /claude — motor de decisão do agente, não um agente autônomo.
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
# PrimoCode — modo motor
|
|
66
|
+
|
|
67
|
+
Você está sendo usado como o "cérebro" do PrimoCode, um CLI que executa
|
|
68
|
+
tarefas de engenharia. Não use suas próprias ferramentas nativas (Read,
|
|
69
|
+
Write, Edit, Bash, Glob, Grep, WebFetch, etc.) — quem executa é o
|
|
70
|
+
PrimoCode, não você. Sua única saída é dizer QUAL ferramenta do PrimoCode
|
|
71
|
+
chamar, com quais argumentos, ou a resposta final.
|
|
72
|
+
|
|
73
|
+
## Ferramentas disponíveis
|
|
74
|
+
|
|
75
|
+
${catalogoParaTexto()}
|
|
76
|
+
|
|
77
|
+
## Formato de resposta — obrigatório
|
|
78
|
+
|
|
79
|
+
Termine toda resposta com exatamente um bloco, sem nada depois dele.
|
|
80
|
+
|
|
81
|
+
Para chamar uma ferramenta:
|
|
82
|
+
|
|
83
|
+
${MARCA_INICIO}
|
|
84
|
+
{"tool_call": {"name": "write_file", "arguments": {"path": "index.html", "content": "..."}}}
|
|
85
|
+
${MARCA_FIM}
|
|
86
|
+
|
|
87
|
+
Para terminar a tarefa (sem mais ferramenta pra chamar):
|
|
88
|
+
|
|
89
|
+
${MARCA_INICIO}
|
|
90
|
+
{"assistant_message": "frase curta do que foi feito"}
|
|
91
|
+
${MARCA_FIM}
|
|
92
|
+
|
|
93
|
+
Nunca omita o bloco. Nunca escreva mais de um bloco por resposta.`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const SKILL_DIR = () => path.join(os.homedir(), '.claude', 'skills', 'primocode-engine');
|
|
97
|
+
const SKILL_PATH = () => path.join(SKILL_DIR(), 'SKILL.md');
|
|
98
|
+
|
|
99
|
+
/** Grava (ou regrava) a skill em ~/.claude/skills/primocode-engine/SKILL.md.
|
|
100
|
+
* Sempre reescreve com o texto atual — barato, e garante que uma versão
|
|
101
|
+
* nova do PrimoCode (com ferramenta nova em catalogo.js) atualiza a skill
|
|
102
|
+
* sozinha na próxima vez que /claude ligar. */
|
|
103
|
+
function garantirSkill() {
|
|
104
|
+
const dir = SKILL_DIR();
|
|
105
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
106
|
+
const caminho = SKILL_PATH();
|
|
107
|
+
fs.writeFileSync(caminho, textoBootstrap(), 'utf-8');
|
|
108
|
+
return { ok: true, caminho };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** executor(cmd) roda um comando e devolve a saída, ou lança. Default:
|
|
112
|
+
* execSync de verdade. Testes injetam uma função fake — sem mock framework. */
|
|
113
|
+
function temClaudeCode(executor = (cmd) => execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] })) {
|
|
114
|
+
try { executor('claude --version'); return true; }
|
|
115
|
+
catch { return false; }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function temOrca(executor = (cmd) => execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] })) {
|
|
119
|
+
try { executor('orca --version'); return true; }
|
|
120
|
+
catch { return false; }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** executor aqui devolve TEXTO (o JSON cru de `orca status --json`), não
|
|
124
|
+
* só "roda ou lança" — por isso um default diferente do de cima. */
|
|
125
|
+
function orcaAlcancavel(executor = (cmd) => execSync(cmd, { encoding: 'utf-8' })) {
|
|
126
|
+
try {
|
|
127
|
+
const saida = executor('orca status --json');
|
|
128
|
+
const j = JSON.parse(saida);
|
|
129
|
+
return !!(j && j.result && j.result.runtime && j.result.runtime.reachable);
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Função pura — nenhum I/O aqui, só decide. */
|
|
136
|
+
function escolherCaminho({ temClaude, temOrcaCli, orcaOk }) {
|
|
137
|
+
if (!temClaude) return 'sem-claude';
|
|
138
|
+
if (temOrcaCli && orcaOk) return 'A';
|
|
139
|
+
return 'B';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Handle do terminal ORCA desta execução do processo — sessão contínua
|
|
143
|
+
// enquanto o CLI ficar de pé. Cursor de leitura por handle, pra "terminal
|
|
144
|
+
// read --cursor" trazer só o que é novo desde o turno anterior.
|
|
145
|
+
let sessao = { handle: null, cursor: '0', bootstrapEnviado: false };
|
|
146
|
+
|
|
147
|
+
function rodarOrca(args, opts = {}) {
|
|
148
|
+
return execFileSync('orca', args, { encoding: 'utf-8', timeout: opts.timeout || 15000 });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let contadorId = 0;
|
|
152
|
+
/** Id sintético pro tool_call — o bloco marcado do Claude Code não traz um
|
|
153
|
+
* (só existe UM tool_call por resposta, pela própria regra do bootstrap),
|
|
154
|
+
* mas lib/act.js precisa de tc.id pra parear com a mensagem role:'tool' que
|
|
155
|
+
* vem depois. Único dentro do processo já basta. */
|
|
156
|
+
function proximoId() {
|
|
157
|
+
contadorId += 1;
|
|
158
|
+
return `claude-engine-${contadorId}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Caminho A (sessão contínua via ORCA) tem um problema arquitetural real,
|
|
162
|
+
// achado rodando de verdade na verificação manual (Task 9 do plano): o
|
|
163
|
+
// Claude Code interativo desenha a TUI em tela alternada, e
|
|
164
|
+
// `orca terminal read` (scrollback linha a linha) nunca enxerga esse
|
|
165
|
+
// conteúdo — o tail fica preso no eco do comando de lançamento. `orca
|
|
166
|
+
// terminal show`'s preview mostra a tela renderizada de verdade, mas é só
|
|
167
|
+
// um recorte das últimas linhas visíveis, sem garantia de conter o bloco
|
|
168
|
+
// marcado inteiro numa resposta mais longa que o painel. Até esse
|
|
169
|
+
// mecanismo de leitura ser resolvido, o Caminho B (testado de ponta a
|
|
170
|
+
// ponta, funciona) é o único confiável — força ele sempre, mesmo com ORCA
|
|
171
|
+
// disponível. O código do Caminho A fica pronto pra quando isso for
|
|
172
|
+
// resolvido, só não é escolhido.
|
|
173
|
+
const CAMINHO_A_HABILITADO = false;
|
|
174
|
+
|
|
175
|
+
async function garantirEngine() {
|
|
176
|
+
// execFileSync('claude', …) no caminho headless não resolve o shim
|
|
177
|
+
// .cmd/.bat do Windows sem shell:true — quebraria em EVERY turno, mesmo
|
|
178
|
+
// com a detecção acima (que usa execSync com shell) passando. Recusa
|
|
179
|
+
// antes de instalar a skill: honesto e seguro, em vez de "sucesso" que
|
|
180
|
+
// quebra na primeira mensagem real.
|
|
181
|
+
if (process.platform === 'win32') {
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
error: 'O motor Claude Code ainda não funciona no Windows.',
|
|
185
|
+
comoResolver: 'Aguarde uma próxima versão, ou use o motor padrão (OpenRouter/Groq).',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (!temClaudeCode()) {
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
error: 'Claude Code não está instalado nesta máquina.',
|
|
192
|
+
comoResolver: 'Instale o Claude Code (veja https://docs.claude.com/claude-code) e rode /claude de novo.',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
garantirSkill();
|
|
196
|
+
const orcaCli = CAMINHO_A_HABILITADO && temOrca();
|
|
197
|
+
const caminho = escolherCaminho({
|
|
198
|
+
temClaude: true,
|
|
199
|
+
temOrcaCli: orcaCli,
|
|
200
|
+
orcaOk: orcaCli ? orcaAlcancavel() : false,
|
|
201
|
+
});
|
|
202
|
+
return { ok: true, caminho };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function abrirTerminalOrca() {
|
|
206
|
+
// --dangerously-skip-permissions aqui não dá poder novo ao Claude Code:
|
|
207
|
+
// a skill (textoBootstrap) já instrui ele a nunca usar suas próprias
|
|
208
|
+
// ferramentas nativas. A flag só pula os PRÓPRIOS prompts de permissão
|
|
209
|
+
// dele, que travariam a sessão sem ninguém pra responder.
|
|
210
|
+
const saida = rodarOrca(['terminal', 'create', '--command', 'claude --dangerously-skip-permissions', '--title', 'PrimoCode', '--json']);
|
|
211
|
+
const r = JSON.parse(saida);
|
|
212
|
+
// `orca terminal create --json` aninha o handle em result.terminal.handle,
|
|
213
|
+
// não em result.handle — confirmado rodando o comando de verdade (Task 9).
|
|
214
|
+
return r.result.terminal.handle;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function esperarIdleOrca(handle, timeoutMs) {
|
|
218
|
+
rodarOrca(['terminal', 'wait', '--terminal', handle, '--for', 'tui-idle', '--timeout-ms', String(timeoutMs)], { timeout: timeoutMs + 5000 });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function ultimaMensagem(messages) {
|
|
222
|
+
const m = (messages || [])[messages.length - 1];
|
|
223
|
+
return m && m.content ? String(m.content) : '';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Devolve a decisão pro onEvent — tool_call, ou assistant_message (com
|
|
227
|
+
* degradação pra texto puro quando o bloco marcado não veio, em vez de
|
|
228
|
+
* travar o turno em silêncio). */
|
|
229
|
+
function emitirDecisao(textoLido, onEvent) {
|
|
230
|
+
const decisao = extrairDecisao(textoLido);
|
|
231
|
+
if (!decisao) {
|
|
232
|
+
onEvent({ assistant_message: { content: String(textoLido || '').trim() } });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (decisao.tool_call) {
|
|
236
|
+
// lib/act.js usa tc.id pra parear a mensagem role:'tool' com este
|
|
237
|
+
// tool_call (messages.push({ tool_call_id: tc.id, ... })) — sem id
|
|
238
|
+
// aqui, esse pareamento quebra.
|
|
239
|
+
onEvent({ tool_call: { id: proximoId(), name: decisao.tool_call.name, arguments: decisao.tool_call.arguments || {} } });
|
|
240
|
+
}
|
|
241
|
+
else if (decisao.assistant_message) {
|
|
242
|
+
// O modelo pode confundir a forma com a do tool_call (que É um
|
|
243
|
+
// objeto) e emitir {"assistant_message": {"content": "..."}} em vez
|
|
244
|
+
// de string pura. Sem essa coerção, `content` vira um objeto
|
|
245
|
+
// aninhado e lib/act.js quebra com TypeError no `.trim()` — a
|
|
246
|
+
// travessia direta que este arquivo inteiro existe pra evitar.
|
|
247
|
+
const am = decisao.assistant_message;
|
|
248
|
+
const conteudo = typeof am === 'string'
|
|
249
|
+
? am
|
|
250
|
+
: (am && typeof am.content === 'string' ? am.content : JSON.stringify(am));
|
|
251
|
+
onEvent({ assistant_message: { content: conteudo } });
|
|
252
|
+
}
|
|
253
|
+
else onEvent({ assistant_message: { content: JSON.stringify(decisao) } });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function requestActViaOrca(payload, onEvent) {
|
|
257
|
+
if (!sessao.handle) {
|
|
258
|
+
try {
|
|
259
|
+
sessao.handle = abrirTerminalOrca();
|
|
260
|
+
sessao.cursor = '0';
|
|
261
|
+
sessao.bootstrapEnviado = false;
|
|
262
|
+
esperarIdleOrca(sessao.handle, 30000);
|
|
263
|
+
} catch (e) {
|
|
264
|
+
// Mesmo tratamento do try/catch abaixo: se o terminal não
|
|
265
|
+
// chegou a ficar pronto (timeout do tui-idle, por exemplo), o
|
|
266
|
+
// handle nunca foi confirmado usável — zera pra próxima
|
|
267
|
+
// chamada abrir um terminal novo do zero, em vez de deixar
|
|
268
|
+
// sessao.handle setado com algo que nunca funcionou.
|
|
269
|
+
sessao.handle = null;
|
|
270
|
+
throw e;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
let texto = ultimaMensagem(payload.messages);
|
|
274
|
+
if (!sessao.bootstrapEnviado) {
|
|
275
|
+
texto = 'Use a skill primocode-engine para esta sessão inteira — ela está em ~/.claude/skills/primocode-engine/SKILL.md.\n\n' + texto;
|
|
276
|
+
sessao.bootstrapEnviado = true;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
rodarOrca(['terminal', 'send', '--terminal', sessao.handle, '--text', texto, '--enter']);
|
|
280
|
+
esperarIdleOrca(sessao.handle, 120000);
|
|
281
|
+
const lida = rodarOrca(['terminal', 'read', '--terminal', sessao.handle, '--cursor', sessao.cursor, '--json']);
|
|
282
|
+
const leitura = JSON.parse(lida);
|
|
283
|
+
// Mesma correção do handle: `terminal read --json` também aninha em
|
|
284
|
+
// result.terminal — e o texto vem como ARRAY de linhas (`tail`), não
|
|
285
|
+
// uma string única (`text`). Confirmado rodando de verdade (Task 9).
|
|
286
|
+
const t = leitura.result.terminal || {};
|
|
287
|
+
sessao.cursor = t.nextCursor != null ? String(t.nextCursor) : sessao.cursor;
|
|
288
|
+
emitirDecisao((t.tail || []).join('\n'), onEvent);
|
|
289
|
+
} catch (e) {
|
|
290
|
+
// Handle obsoleto (terminal fechado, ORCA reiniciou) — documentado
|
|
291
|
+
// pelo próprio `orca agent-context` como terminal_handle_stale. Zera
|
|
292
|
+
// a sessão: a PRÓXIMA chamada abre um terminal novo do zero. Esta
|
|
293
|
+
// chamada ainda falha (o usuário vê o erro e pode só pedir de novo).
|
|
294
|
+
sessao.handle = null;
|
|
295
|
+
throw e;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Sem sessão viva (caminho B), o histórico inteiro vira texto a cada
|
|
300
|
+
* chamada — mas `content` sozinho perde a mensagem quando é um turno de
|
|
301
|
+
* tool_calls (content vem null ali, o que importa está em tool_calls) ou
|
|
302
|
+
* uma resposta de ferramenta (role:'tool'). Reconstrói os dois casos em
|
|
303
|
+
* vez de deixar "assistant: null" no texto. */
|
|
304
|
+
function linhaDoHistorico(m) {
|
|
305
|
+
if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
|
306
|
+
const chamadas = m.tool_calls
|
|
307
|
+
.map((tc) => `chamou ${tc.function.name} com ${tc.function.arguments}`)
|
|
308
|
+
.join('; ');
|
|
309
|
+
return `assistant: ${chamadas}`;
|
|
310
|
+
}
|
|
311
|
+
if (m.role === 'tool') return `resultado da ferramenta (${m.tool_call_id}): ${m.content}`;
|
|
312
|
+
return `${m.role}: ${m.content}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function requestActViaHeadless(payload, onEvent) {
|
|
316
|
+
const historico = (payload.messages || []).map(linhaDoHistorico).join('\n\n');
|
|
317
|
+
const prompt = 'Use a skill primocode-engine para esta chamada.\n\n' + historico;
|
|
318
|
+
// --dangerously-skip-permissions: mesma razão do caminho ORCA acima —
|
|
319
|
+
// a skill já proíbe o Claude Code de usar suas próprias ferramentas
|
|
320
|
+
// nativas; a flag só evita que ele pare pedindo permissão pra si mesmo
|
|
321
|
+
// numa chamada headless sem ninguém pra responder.
|
|
322
|
+
const saida = execFileSync('claude', [
|
|
323
|
+
'-p', prompt,
|
|
324
|
+
'--output-format', 'json',
|
|
325
|
+
'--dangerously-skip-permissions',
|
|
326
|
+
], { encoding: 'utf-8', timeout: 180000 });
|
|
327
|
+
const r = JSON.parse(saida);
|
|
328
|
+
emitirDecisao(r.result || '', onEvent);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Mesma forma de api.requestAct(server, payload, onEvent) — `server` não
|
|
332
|
+
* é usado aqui (não existe servidor neste motor), fica só pra manter a
|
|
333
|
+
* assinatura uniforme com o outro lado da escolha em lib/act.js. */
|
|
334
|
+
async function requestAct(server, payload, onEvent) {
|
|
335
|
+
try {
|
|
336
|
+
const engineInfo = await garantirEngine();
|
|
337
|
+
if (!engineInfo.ok) {
|
|
338
|
+
onEvent({ erro: `${engineInfo.error} ${engineInfo.comoResolver || ''}`.trim() });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (engineInfo.caminho === 'A') await requestActViaOrca(payload, onEvent);
|
|
342
|
+
else await requestActViaHeadless(payload, onEvent);
|
|
343
|
+
} catch (e) {
|
|
344
|
+
onEvent({ erro: e.message });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
module.exports = {
|
|
349
|
+
MARCA_INICIO, MARCA_FIM, extrairDecisao,
|
|
350
|
+
catalogoParaTexto, textoBootstrap,
|
|
351
|
+
SKILL_DIR, SKILL_PATH, garantirSkill,
|
|
352
|
+
temClaudeCode, temOrca, orcaAlcancavel, escolherCaminho,
|
|
353
|
+
garantirEngine, requestAct, emitirDecisao,
|
|
354
|
+
};
|
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,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Escolha automática de modo.
|
|
3
3
|
*
|
|
4
|
-
* O modo primocode manda o schema das
|
|
4
|
+
* O modo primocode manda o schema das 44 ferramentas em todo pedido — ~4.700
|
|
5
5
|
* tokens antes de você digitar a primeira letra. Um "oi" gasta o mesmo que uma
|
|
6
6
|
* 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).
|
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/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
|
}
|
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.1.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": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
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:
|