primocode 8.31.0 → 8.33.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/app/app.css +175 -106
- package/app/app.js +171 -4
- package/app/index.html +125 -48
- package/app/primo-marca.js +113 -0
- package/bin/primocode.js +133 -18
- package/lib/api.js +67 -3
- package/lib/app.js +39 -1
- package/lib/boas-vindas.js +161 -0
- package/lib/chaves.js +149 -0
- package/lib/claude-engine.js +71 -13
- package/lib/codex-engine.js +201 -0
- package/lib/config.js +26 -1
- package/lib/modo.js +43 -3
- package/package.json +2 -2
package/app/index.html
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html lang="pt-BR">
|
|
2
|
+
<html lang="pt-BR" data-tema="escuro">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
@@ -7,62 +7,139 @@
|
|
|
7
7
|
<link rel="stylesheet" href="/app.css">
|
|
8
8
|
<link rel="icon" href="/icone.svg">
|
|
9
9
|
</head>
|
|
10
|
+
<!--
|
|
11
|
+
A janela do PrimoCode (/rc).
|
|
12
|
+
|
|
13
|
+
É a MESMA interface da página /code do Conecta Primo — de propósito. Quem
|
|
14
|
+
usa o terminal, abre a janela e depois entra pelo navegador tem de
|
|
15
|
+
reconhecer onde está no primeiro olhar: mesma barra lateral, mesmo botão de
|
|
16
|
+
novo chat, mesmo prompt `❯`, mesmo mascote na tela vazia.
|
|
17
|
+
|
|
18
|
+
O que muda é de onde vem o conteúdo: aqui o histórico vem do servidor local
|
|
19
|
+
(/api/conversas), que usa o lib/nuvem.js — a mesma cifra e a mesma coleção
|
|
20
|
+
que a web lê direto do Firestore.
|
|
21
|
+
-->
|
|
10
22
|
<body>
|
|
11
23
|
|
|
12
|
-
<
|
|
13
|
-
<
|
|
14
|
-
<
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
</div>
|
|
28
|
-
</header>
|
|
24
|
+
<div class="tela">
|
|
25
|
+
<aside class="lado" id="lado" aria-label="Conversas e ajustes">
|
|
26
|
+
<div class="lado-topo">
|
|
27
|
+
<div class="marca-linha">
|
|
28
|
+
<span id="marcaTopo"></span>
|
|
29
|
+
<span class="nome">PrimoCode</span>
|
|
30
|
+
<span class="versao" id="versao"></span>
|
|
31
|
+
</div>
|
|
32
|
+
<button class="novo" id="btnNovo">
|
|
33
|
+
<svg width="15" height="15" viewBox="0 0 24 24" aria-hidden="true">
|
|
34
|
+
<path d="M12 5v14M5 12h14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
|
|
35
|
+
</svg>
|
|
36
|
+
Novo chat
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
29
39
|
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<p>Escreva do jeito que você falaria. Ele lê a pasta, escreve o código,
|
|
35
|
-
roda o que precisar e conta o que fez.</p>
|
|
36
|
-
<div class="atalhos">
|
|
37
|
-
<button data-manda="/tudo">o que ele sabe fazer</button>
|
|
38
|
-
<button data-manda="/project">meus projetos</button>
|
|
39
|
-
<button data-manda="/effort">esforço</button>
|
|
40
|
-
<button data-manda="/help">comandos</button>
|
|
40
|
+
<div class="rotulo">Conversas</div>
|
|
41
|
+
<div class="recentes" id="recentes">
|
|
42
|
+
<div class="vazio-lista" id="listaVazia">
|
|
43
|
+
As conversas aparecem aqui quando você entra na sua conta com <code>/conectar</code>.
|
|
41
44
|
</div>
|
|
42
45
|
</div>
|
|
43
|
-
</div>
|
|
44
|
-
</main>
|
|
45
46
|
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
47
|
+
<div class="lado-pe">
|
|
48
|
+
<button class="pe-btn" id="btnConfig">
|
|
49
|
+
<svg width="15" height="15" viewBox="0 0 24 24" aria-hidden="true">
|
|
50
|
+
<circle cx="12" cy="12" r="3.1" fill="none" stroke="currentColor" stroke-width="1.9"/>
|
|
51
|
+
<path d="M12 2.8v2.4M12 18.8v2.4M21.2 12h-2.4M5.2 12H2.8M18.5 5.5l-1.7 1.7M7.2 16.8l-1.7 1.7M18.5 18.5l-1.7-1.7M7.2 7.2 5.5 5.5"
|
|
52
|
+
fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/>
|
|
53
|
+
</svg>
|
|
54
|
+
Configurações
|
|
55
|
+
</button>
|
|
56
|
+
<div class="chips">
|
|
57
|
+
<span class="chip" id="pasta" title="pasta de trabalho"></span>
|
|
58
|
+
<span class="chip" id="modo"></span>
|
|
59
|
+
<span class="chip" id="esforco"></span>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</aside>
|
|
63
|
+
|
|
64
|
+
<main class="conversa">
|
|
65
|
+
<div class="rolo" id="rolo">
|
|
66
|
+
<div class="vazio" id="vazio">
|
|
67
|
+
<span id="marcaGrande"></span>
|
|
68
|
+
<h1>O que a gente vai construir?</h1>
|
|
69
|
+
<p>Escreva do jeito que você falaria. Ele lê a pasta, escreve o código,
|
|
70
|
+
roda o que precisar e conta o que fez.</p>
|
|
71
|
+
<div class="sugestoes">
|
|
72
|
+
<button class="sugestao" data-manda="/tudo">o que ele sabe fazer</button>
|
|
73
|
+
<button class="sugestao" data-manda="/project">meus projetos</button>
|
|
74
|
+
<button class="sugestao" data-manda="/effort">esforço</button>
|
|
75
|
+
<button class="sugestao" data-manda="/help">comandos</button>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<div id="conversa" class="tira" hidden></div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="rodape">
|
|
82
|
+
<div id="pensando" hidden><span class="pulso"></span><span id="pensando-txt">trabalhando</span></div>
|
|
83
|
+
<form id="forma" class="caixa">
|
|
84
|
+
<div class="campo-volta">
|
|
85
|
+
<span class="chevron">❯</span>
|
|
86
|
+
<textarea id="campo" rows="1" placeholder="Peça alguma coisa…"
|
|
87
|
+
autocomplete="off" spellcheck="false"></textarea>
|
|
88
|
+
</div>
|
|
89
|
+
<button type="submit" id="mandar" class="enviar" title="Enviar (Enter)" aria-label="Enviar">
|
|
90
|
+
<svg width="17" height="17" viewBox="0 0 24 24" aria-hidden="true">
|
|
91
|
+
<path d="M5 12h13M12.5 6l6 6-6 6" fill="none" stroke="currentColor"
|
|
92
|
+
stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
93
|
+
</svg>
|
|
94
|
+
</button>
|
|
95
|
+
<button type="button" id="parar" class="enviar parar" title="Parar (Esc)" hidden aria-label="Parar">
|
|
96
|
+
<svg width="15" height="15" viewBox="0 0 24 24" aria-hidden="true"><rect x="7" y="7" width="10" height="10" rx="2" fill="currentColor"/></svg>
|
|
97
|
+
</button>
|
|
98
|
+
</form>
|
|
99
|
+
<div class="dica" id="dica">Enter manda · Shift+Enter quebra linha · arraste um arquivo aqui</div>
|
|
100
|
+
</div>
|
|
101
|
+
</main>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<dialog id="dlgConfig">
|
|
105
|
+
<div class="dlg-topo">
|
|
106
|
+
<h2>Configurações</h2>
|
|
107
|
+
<button class="copiar" id="fecharConfig">Fechar</button>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="dlg-corpo">
|
|
110
|
+
<div class="grupo">
|
|
111
|
+
<h3>Tema</h3>
|
|
112
|
+
<div class="opcoes" id="temas">
|
|
113
|
+
<button class="opcao" data-tema="escuro">Escuro</button>
|
|
114
|
+
<button class="opcao" data-tema="claro">Claro</button>
|
|
115
|
+
<button class="opcao" data-tema="sistema">Do sistema</button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="grupo">
|
|
119
|
+
<h3>Cor</h3>
|
|
120
|
+
<div class="cores" id="cores">
|
|
121
|
+
<button class="cor" data-cor="ciano" style="background:#22d3ee" aria-label="Ciano"></button>
|
|
122
|
+
<button class="cor" data-cor="verde" style="background:#34d399" aria-label="Verde"></button>
|
|
123
|
+
<button class="cor" data-cor="roxo" style="background:#a78bfa" aria-label="Roxo"></button>
|
|
124
|
+
<button class="cor" data-cor="ambar" style="background:#fbbf24" aria-label="Âmbar"></button>
|
|
125
|
+
<button class="cor" data-cor="rosa" style="background:#fb7185" aria-label="Rosa"></button>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="grupo">
|
|
129
|
+
<h3>Esta janela</h3>
|
|
130
|
+
<div class="linha-info"><span>Pasta</span><b id="pastaConfig">—</b></div>
|
|
131
|
+
<div class="linha-info"><span>Modo</span><b id="modoConfig">—</b></div>
|
|
132
|
+
<div class="linha-info"><span>Esforço</span><b id="esforcoConfig">—</b></div>
|
|
133
|
+
<p style="margin:10px 0 0;font-size:13px;color:var(--fraco)">
|
|
134
|
+
Estes três se mudam no terminal: <code>/dir</code>, <code>/mode</code> e <code>/effort</code>.
|
|
135
|
+
</p>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</dialog>
|
|
63
139
|
|
|
64
140
|
<div id="alvo-solto"><div><strong>Solte aqui</strong><span>o caminho entra no pedido</span></div></div>
|
|
65
141
|
|
|
142
|
+
<script src="/primo-marca.js"></script>
|
|
66
143
|
<script src="/app.js"></script>
|
|
67
144
|
</body>
|
|
68
145
|
</html>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* primo-marca.js — o mascote do PrimoCode, desenhado em pixel.
|
|
3
|
+
*
|
|
4
|
+
* CÓPIA de conecta-primo-ai/frontend/primo-marca.js. São repositórios
|
|
5
|
+
* diferentes e não há como importar de um para o outro; o que dá para fazer é
|
|
6
|
+
* manter os dois IDÊNTICOS e o teste conferir isso — um mascote que muda num
|
|
7
|
+
* lado e não no outro é a marca do produto se partindo em duas sem ninguém
|
|
8
|
+
* ver.
|
|
9
|
+
*
|
|
10
|
+
* ── POR QUE MATRIZ, E NÃO O PNG ──────────────────────────────────────────
|
|
11
|
+
* O logo veio como imagem. Guardar o PNG funcionaria, mas ele já é pixel art:
|
|
12
|
+
* redesenhá-lo como MATRIZ dá o mesmo desenho nítido em qualquer tamanho —
|
|
13
|
+
* favicon de 16px, cabeçalho de 28px, tela vazia de 96px — sem borrar, sem
|
|
14
|
+
* três arquivos para manter, e pesando algumas centenas de bytes em vez de
|
|
15
|
+
* dezenas de kB.
|
|
16
|
+
*
|
|
17
|
+
* E deixa a cor viver no TEMA: o mesmo bicho no claro e no escuro, sem PNG
|
|
18
|
+
* com fundo grudado.
|
|
19
|
+
*
|
|
20
|
+
* Se um dia o desenho mudar, muda a matriz — é uma linha por fileira, e dá
|
|
21
|
+
* para ler o bicho olhando o código.
|
|
22
|
+
*
|
|
23
|
+
* C = corpo (ciano claro) · D = detalhe (azul escuro): orelhas,
|
|
24
|
+
* braços e pés · . = vazio
|
|
25
|
+
*/
|
|
26
|
+
(function (raiz) {
|
|
27
|
+
"use strict";
|
|
28
|
+
|
|
29
|
+
var MATRIZ = [
|
|
30
|
+
"...DD....DD...",
|
|
31
|
+
"...DD....DD...",
|
|
32
|
+
"..CCCCCCCCCC..",
|
|
33
|
+
"..CCCCCCCCCC..",
|
|
34
|
+
"DDCCCCCCCCCCDD",
|
|
35
|
+
"DDCCCCCCCCCCDD",
|
|
36
|
+
"..CCCCCCCCCC..",
|
|
37
|
+
"..CCCCCCCCCC..",
|
|
38
|
+
"..CCCCCCCCCC..",
|
|
39
|
+
"...DD....DD...",
|
|
40
|
+
"...DD....DD...",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
var CORPO = "#5ec8ef";
|
|
44
|
+
var DETALHE = "#2b7fa8";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* O mascote como SVG.
|
|
48
|
+
*
|
|
49
|
+
* Retângulos colados, sem `shape-rendering: crispEdges`: em pixel art
|
|
50
|
+
* ampliado, o antialiasing do navegador deixa fios claros entre um quadrado
|
|
51
|
+
* e outro. Colar meio pixel a mais em cada lado (o `+0.02`) some com a
|
|
52
|
+
* costura sem mudar o desenho.
|
|
53
|
+
*
|
|
54
|
+
* @param {object} op
|
|
55
|
+
* @param {number} [op.tamanho] lado em px (o SVG é quadrado)
|
|
56
|
+
* @param {string} [op.corpo] cor do corpo
|
|
57
|
+
* @param {string} [op.detalhe] cor das orelhas, braços e pés
|
|
58
|
+
* @param {string} [op.fundo] cor do quadrado de trás (vazio = sem fundo)
|
|
59
|
+
* @param {string} [op.rotulo] texto alternativo; vazio = decorativo
|
|
60
|
+
*/
|
|
61
|
+
function svg(op) {
|
|
62
|
+
op = op || {};
|
|
63
|
+
var lado = op.tamanho || 32;
|
|
64
|
+
var corpo = op.corpo || CORPO;
|
|
65
|
+
var detalhe = op.detalhe || DETALHE;
|
|
66
|
+
var linhas = MATRIZ.length;
|
|
67
|
+
var colunas = MATRIZ[0].length;
|
|
68
|
+
// O desenho é mais largo que alto; a caixa é quadrada e ele fica centrado.
|
|
69
|
+
var caixa = Math.max(linhas, colunas);
|
|
70
|
+
var dx = (caixa - colunas) / 2;
|
|
71
|
+
var dy = (caixa - linhas) / 2;
|
|
72
|
+
|
|
73
|
+
/* UM caminho por cor, e não um retângulo por célula.
|
|
74
|
+
*
|
|
75
|
+
* Com retângulos separados, o navegador antialiasa a borda de cada um
|
|
76
|
+
* contra a do vizinho e o desenho ganha uma grade de fios claros no meio
|
|
77
|
+
* do corpo — visível já em 96px, feio em 200px. Num caminho só, as
|
|
78
|
+
* células vizinhas viram uma forma única e não há borda interna nenhuma
|
|
79
|
+
* para suavizar. */
|
|
80
|
+
var caminho = { C: [], D: [] };
|
|
81
|
+
for (var y = 0; y < linhas; y++) {
|
|
82
|
+
var fileira = MATRIZ[y];
|
|
83
|
+
for (var x = 0; x < colunas; x++) {
|
|
84
|
+
var ch = fileira[x];
|
|
85
|
+
if (ch !== "C" && ch !== "D") continue;
|
|
86
|
+
caminho[ch].push("M" + (x + dx) + " " + (y + dy) + "h1v1h-1z");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
var partes = [];
|
|
91
|
+
if (op.fundo) {
|
|
92
|
+
partes.push('<rect width="' + caixa + '" height="' + caixa
|
|
93
|
+
+ '" rx="' + (caixa * 0.18).toFixed(2) + '" fill="' + op.fundo + '"/>');
|
|
94
|
+
}
|
|
95
|
+
if (caminho.C.length) partes.push('<path d="' + caminho.C.join("") + '" fill="' + corpo + '"/>');
|
|
96
|
+
if (caminho.D.length) partes.push('<path d="' + caminho.D.join("") + '" fill="' + detalhe + '"/>');
|
|
97
|
+
|
|
98
|
+
var acessivel = op.rotulo
|
|
99
|
+
? ' role="img" aria-label="' + String(op.rotulo).replace(/"/g, """) + '"'
|
|
100
|
+
: ' aria-hidden="true"';
|
|
101
|
+
|
|
102
|
+
return '<svg viewBox="0 0 ' + caixa + " " + caixa + '" width="' + lado
|
|
103
|
+
+ '" height="' + lado + '"' + acessivel + ">" + partes.join("") + "</svg>";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** O mesmo bicho como favicon, sem pedir arquivo ao servidor. */
|
|
107
|
+
function favicon() {
|
|
108
|
+
var s = svg({ tamanho: 32, fundo: "#0a0f16" });
|
|
109
|
+
return "data:image/svg+xml," + encodeURIComponent(s);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
raiz.PrimoMarca = { svg: svg, favicon: favicon, MATRIZ: MATRIZ, CORPO: CORPO, DETALHE: DETALHE };
|
|
113
|
+
})(typeof window !== "undefined" ? window : globalThis);
|
package/bin/primocode.js
CHANGED
|
@@ -21,6 +21,9 @@ const tools = require('../lib/tools');
|
|
|
21
21
|
const cfg = require('../lib/config');
|
|
22
22
|
const { runAct, interromper } = require('../lib/act');
|
|
23
23
|
const claudeEngine = require('../lib/claude-engine');
|
|
24
|
+
const codexEngine = require('../lib/codex-engine');
|
|
25
|
+
const chaves = require('../lib/chaves');
|
|
26
|
+
const boasVindas = require('../lib/boas-vindas');
|
|
24
27
|
const pasta = require('../lib/pasta');
|
|
25
28
|
const catalogo = require('../lib/catalogo');
|
|
26
29
|
const effort = require('../lib/effort');
|
|
@@ -64,10 +67,10 @@ const state = {
|
|
|
64
67
|
currentProject: null,
|
|
65
68
|
workDir: null, // pasta onde o agente trabalha nesta sessão
|
|
66
69
|
workDirOrigem: null, // 'padrao' | '/dir' | 'detectada' | 'projeto' — para o /dir explicar
|
|
67
|
-
engine: 'server', // 'server' (OpenRouter/Groq) | 'claude-code' (/claude)
|
|
70
|
+
engine: 'server', // 'server' (OpenRouter/Groq) | 'claude-code' (/claude) | 'codex' (/codex)
|
|
68
71
|
};
|
|
69
72
|
|
|
70
|
-
// A janela do /
|
|
73
|
+
// A janela do /rc, quando aberta. Uma por sessão: o segundo /rc traz a que
|
|
71
74
|
// existe para a frente em vez de subir outro servidor ao lado.
|
|
72
75
|
let janelaAberta = null;
|
|
73
76
|
|
|
@@ -85,8 +88,11 @@ ${c.bold('Comandos')}
|
|
|
85
88
|
${c.brand('/read')} <arquivo> mostra um arquivo
|
|
86
89
|
${c.brand('/run')} <comando> executa no shell do projeto
|
|
87
90
|
${c.brand('/open')} <arquivo|url> abre no navegador
|
|
88
|
-
${c.brand('/
|
|
89
|
-
${c.brand('/
|
|
91
|
+
${c.brand('/rc')} abre a janela do PrimoCode (chat, sem terminal)
|
|
92
|
+
${c.brand('/claude')} usa o Claude Code da SUA conta como motor (Opus)
|
|
93
|
+
${c.brand('/codex')} usa o Codex da SUA conta como motor (modelo barato)
|
|
94
|
+
${c.brand('/chave')} <provedor> sua própria chave de API (Claude, Codex)
|
|
95
|
+
${c.brand('/conectar')} entra na sua conta Conecta Primo · ${c.brand('/conta')} mostra o plano
|
|
90
96
|
${c.brand('/conversas')} [n] suas conversas salvas — as mesmas do app e do celular
|
|
91
97
|
${c.brand('/sair')} desconecta este computador
|
|
92
98
|
${c.brand('/status')} checa o servidor
|
|
@@ -129,8 +135,11 @@ const SLASH_COMANDOS = [
|
|
|
129
135
|
['/read', 'mostra um arquivo'],
|
|
130
136
|
['/run', 'roda um comando'],
|
|
131
137
|
['/open', 'abre no navegador'],
|
|
132
|
-
['/
|
|
133
|
-
['/
|
|
138
|
+
['/rc', 'abre a janela do PrimoCode'],
|
|
139
|
+
['/claude', 'usa o Claude Code da sua conta como motor'],
|
|
140
|
+
['/codex', 'usa o Codex da sua conta como motor'],
|
|
141
|
+
['/chave', 'sua própria chave de API (Claude, Codex)'],
|
|
142
|
+
['/conectar', 'entra na sua conta Conecta Primo'],
|
|
134
143
|
['/conta', 'sua conta, plano e computadores conectados'],
|
|
135
144
|
['/conversas', 'suas conversas salvas, as mesmas do app e do celular'],
|
|
136
145
|
['/sair', 'desconecta este computador'],
|
|
@@ -483,7 +492,7 @@ async function sendPrompt(userText) {
|
|
|
483
492
|
// Só chamado se o agente for mesmo MEXER EM ARQUIVO — abrir o Chrome
|
|
484
493
|
// não é motivo para criar pasta de projeto.
|
|
485
494
|
ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
|
|
486
|
-
motor:
|
|
495
|
+
motor: motorDaVez(),
|
|
487
496
|
});
|
|
488
497
|
|
|
489
498
|
r = await rodarPasso();
|
|
@@ -656,6 +665,17 @@ async function runChat(promptForModel, userTextForMemory) {
|
|
|
656
665
|
|
|
657
666
|
// ── Slash commands ─────────────────────────────────────────────────────
|
|
658
667
|
|
|
668
|
+
/* Qual motor executa este turno.
|
|
669
|
+
*
|
|
670
|
+
* Um `if` a mais aqui e um `if` a menos em qualquer outro lugar seria o
|
|
671
|
+
* caminho para o /codex funcionar no modo agente e não no chat, ou ao
|
|
672
|
+
* contrário. Um lugar só decide, e os dois modos chamam este. */
|
|
673
|
+
function motorDaVez() {
|
|
674
|
+
if (state.engine === 'claude-code') return claudeEngine;
|
|
675
|
+
if (state.engine === 'codex') return codexEngine;
|
|
676
|
+
return api;
|
|
677
|
+
}
|
|
678
|
+
|
|
659
679
|
function isSlash(text) { return text.trim().startsWith('/'); }
|
|
660
680
|
|
|
661
681
|
async function handleSlash(text) {
|
|
@@ -755,6 +775,72 @@ async function handleSlash(text) {
|
|
|
755
775
|
break;
|
|
756
776
|
}
|
|
757
777
|
|
|
778
|
+
/* A chave de API da própria pessoa. Guardada em ~/.primocode/
|
|
779
|
+
chaves.json com permissão 600 — nunca no config.json, que é o
|
|
780
|
+
arquivo que se cola num chamado de suporte. */
|
|
781
|
+
case 'chave': case 'chaves': {
|
|
782
|
+
const [qual, ...resto] = rest.split(/\s+/).filter(Boolean);
|
|
783
|
+
const valor = resto.join(' ');
|
|
784
|
+
|
|
785
|
+
if (!qual) {
|
|
786
|
+
console.log(c.white('\n Suas chaves de API'));
|
|
787
|
+
for (const p of chaves.listar()) {
|
|
788
|
+
console.log(' ' + c.white(p.nome.padEnd(8))
|
|
789
|
+
+ (p.tem ? c.ok(p.disfarce) : c.dim('não configurada'))
|
|
790
|
+
+ c.dim(` ${p.variavel}`));
|
|
791
|
+
}
|
|
792
|
+
console.log(c.dim('\n /chave claude sk-ant-… guarda a chave'));
|
|
793
|
+
console.log(c.dim(' /chave codex remover apaga a que estiver lá'));
|
|
794
|
+
console.log(c.muted('\n Sem chave própria, cada motor usa a conta em que você já entrou nele.'));
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
if (!chaves.PROVEDORES[qual]) {
|
|
798
|
+
console.log(c.err(` Não conheço "${qual}". Use: ${Object.keys(chaves.PROVEDORES).join(', ')}`));
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
if (valor === 'remover' || valor === 'apagar') {
|
|
802
|
+
const r = chaves.esquecer(qual);
|
|
803
|
+
console.log(r.ok ? c.ok(` Chave do ${chaves.PROVEDORES[qual].nome} apagada.`)
|
|
804
|
+
: c.muted(` ${r.error}`));
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
if (!valor) {
|
|
808
|
+
const p = chaves.PROVEDORES[qual];
|
|
809
|
+
console.log(c.muted(` Uso: `) + c.brand(`/chave ${qual} ${p.prefixo}…`));
|
|
810
|
+
console.log(c.dim(` Pegue a sua em ${p.onde}`));
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
const r = chaves.guardar(qual, valor);
|
|
814
|
+
if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
|
|
815
|
+
// O que se mostra de volta é o disfarce, nunca a chave: a linha
|
|
816
|
+
// fica no scrollback do terminal e no print que a pessoa manda.
|
|
817
|
+
console.log(c.ok(` Chave do ${r.nome} guardada.`) + c.muted(` ${r.disfarce}`));
|
|
818
|
+
console.log(c.dim(` Ela vai como ${chaves.PROVEDORES[qual].variavel} só quando o motor rodar.`));
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/* O Codex da conta da pessoa, com o modelo barato.
|
|
823
|
+
O protocolo é o MESMO do /claude — ver lib/codex-engine.js. */
|
|
824
|
+
case 'codex': {
|
|
825
|
+
if (rest.toLowerCase() === 'off') {
|
|
826
|
+
if (state.engine !== 'codex') { console.log(c.muted(' Já estava no motor padrão.')); break; }
|
|
827
|
+
state.engine = 'server';
|
|
828
|
+
console.log(c.ok(' Motor: OpenRouter/Groq (padrão)'));
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
console.log(c.dim(' Checando o Codex…'));
|
|
832
|
+
const r = codexEngine.garantirEngine();
|
|
833
|
+
if (!r.ok) {
|
|
834
|
+
console.log(c.err(` ${r.error}`));
|
|
835
|
+
if (r.comoResolver) console.log(c.muted(` ${r.comoResolver}`));
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
state.engine = 'codex';
|
|
839
|
+
console.log(c.ok(` Motor: Codex (${r.modelo})`) + c.muted(' — na sua conta, não na minha'));
|
|
840
|
+
console.log(c.muted(' /codex off volta pro motor padrão.'));
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
|
|
758
844
|
case 'claude': {
|
|
759
845
|
if (rest.toLowerCase() === 'off') {
|
|
760
846
|
if (state.engine !== 'claude-code') { console.log(c.muted(' Já estava no motor padrão.')); break; }
|
|
@@ -771,7 +857,8 @@ async function handleSlash(text) {
|
|
|
771
857
|
}
|
|
772
858
|
state.engine = 'claude-code';
|
|
773
859
|
const onde = r.caminho === 'A' ? 'sessão contínua via ORCA' : 'chamada avulsa (claude -p, sem ORCA)';
|
|
774
|
-
console.log(c.ok(` Motor: Claude Code (${onde})`)
|
|
860
|
+
console.log(c.ok(` Motor: Claude Code (${onde})`)
|
|
861
|
+
+ c.muted(` — ${claudeEngine.MODELO_CLAUDE || 'o padrão da sua conta'}, na sua conta`));
|
|
775
862
|
console.log(c.muted(' /claude off volta pro motor padrão.'));
|
|
776
863
|
break;
|
|
777
864
|
}
|
|
@@ -953,14 +1040,22 @@ async function handleSlash(text) {
|
|
|
953
1040
|
break;
|
|
954
1041
|
}
|
|
955
1042
|
|
|
956
|
-
/* A janela. "Se mandar /
|
|
1043
|
+
/* A janela. "Se mandar /rc pra ele, ele abre o aplicativo — para o
|
|
957
1044
|
Mac e o Windows, já instalado junto com o npm, com as cores do
|
|
958
1045
|
PrimoCode. Um chat estilo terminal para quem não quer o terminal."
|
|
959
1046
|
|
|
960
|
-
Uma janela por sessão: mandar /
|
|
1047
|
+
Uma janela por sessão: mandar /rc de novo traz a que já existe
|
|
961
1048
|
para a frente em vez de abrir outra do lado. */
|
|
962
|
-
|
|
963
|
-
|
|
1049
|
+
/* O nome antigo some da ajuda e do menu, mas continua respondendo —
|
|
1050
|
+
avisando. Some de vez e quem tinha o dedo viciado bate num "comando
|
|
1051
|
+
não existe" e acha que a atualização quebrou alguma coisa; o aviso
|
|
1052
|
+
custa uma linha e ensina o nome novo na hora em que ele é
|
|
1053
|
+
procurado, que é a única hora em que alguém lê. */
|
|
1054
|
+
case 'rc': case 'app': {
|
|
1055
|
+
if (cmd.toLowerCase() === 'app') {
|
|
1056
|
+
console.log(c.muted(' Agora é ') + c.brand('/rc') + c.muted('. Abrindo…'));
|
|
1057
|
+
}
|
|
1058
|
+
// `/rc --instalar` põe o PrimoCode no Dock, no menu Iniciar ou no
|
|
964
1059
|
// menu de aplicativos, com o ícone do mascote. Uma vez só.
|
|
965
1060
|
if (/^--?(instalar|install)$/.test(rest)) {
|
|
966
1061
|
const r = require('../lib/instalar-app.js').instalar(VERSION);
|
|
@@ -1007,7 +1102,7 @@ async function handleSlash(text) {
|
|
|
1007
1102
|
}
|
|
1008
1103
|
console.log(c.muted(' O terminal continua funcionando: os dois mostram a mesma coisa.'));
|
|
1009
1104
|
// Oferecido UMA vez, e só a quem ainda não tem: repetir todo
|
|
1010
|
-
// /
|
|
1105
|
+
// /rc viraria ruído para quem já instalou de propósito.
|
|
1011
1106
|
if (!require('../lib/instalar-app.js').instalado()) {
|
|
1012
1107
|
console.log(c.dim(' Quer o ícone no ') + c.dim(os.platform() === 'darwin' ? 'Dock' : 'menu Iniciar')
|
|
1013
1108
|
+ c.dim('? ') + c.brand('/app --instalar'));
|
|
@@ -1021,12 +1116,15 @@ async function handleSlash(text) {
|
|
|
1021
1116
|
|
|
1022
1117
|
/* A conta do Conecta Primo. O login inteiro mora em lib/conta.js;
|
|
1023
1118
|
aqui é a tela dele. */
|
|
1024
|
-
case 'entrar': case 'login': {
|
|
1119
|
+
case 'conectar': case 'entrar': case 'login': {
|
|
1120
|
+
if (/^(entrar|login)$/.test(cmd.toLowerCase())) {
|
|
1121
|
+
console.log(c.muted(' Agora é ') + c.brand('/conectar') + c.muted('.'));
|
|
1122
|
+
}
|
|
1025
1123
|
const jaEsta = conta.atual();
|
|
1026
1124
|
if (jaEsta && rest !== 'trocar') {
|
|
1027
1125
|
console.log(c.muted(' Já conectado como ') + c.brand(jaEsta.uid.slice(0, 12) + '…')
|
|
1028
1126
|
+ c.muted(` · plano ${jaEsta.plano}`));
|
|
1029
|
-
console.log(c.dim(' Para entrar com outra conta: ') + c.brand('/
|
|
1127
|
+
console.log(c.dim(' Para entrar com outra conta: ') + c.brand('/conectar trocar'));
|
|
1030
1128
|
break;
|
|
1031
1129
|
}
|
|
1032
1130
|
if (rest === 'trocar') conta.sair();
|
|
@@ -1062,7 +1160,7 @@ async function handleSlash(text) {
|
|
|
1062
1160
|
mostram. Sem número, lista; com número, abre e continua dali. */
|
|
1063
1161
|
case 'conversas': case 'historico': {
|
|
1064
1162
|
if (!conta.atual()) {
|
|
1065
|
-
console.log(c.muted(' Não conectado. ') + c.brand('/
|
|
1163
|
+
console.log(c.muted(' Não conectado. ') + c.brand('/conectar')
|
|
1066
1164
|
+ c.muted(' para ver suas conversas aqui, no app e no celular.'));
|
|
1067
1165
|
break;
|
|
1068
1166
|
}
|
|
@@ -1111,14 +1209,14 @@ async function handleSlash(text) {
|
|
|
1111
1209
|
if (!conta.atual()) { console.log(c.muted(' Você não está conectado.')); break; }
|
|
1112
1210
|
conta.sair();
|
|
1113
1211
|
console.log(c.ok(' Desconectado deste computador.')
|
|
1114
|
-
+ c.dim(' A conta continua intacta; entre de novo com /
|
|
1212
|
+
+ c.dim(' A conta continua intacta; entre de novo com /conectar.'));
|
|
1115
1213
|
break;
|
|
1116
1214
|
}
|
|
1117
1215
|
|
|
1118
1216
|
case 'conta': {
|
|
1119
1217
|
const eu = conta.atual();
|
|
1120
1218
|
if (!eu) {
|
|
1121
|
-
console.log(c.muted(' Não conectado. ') + c.brand('/
|
|
1219
|
+
console.log(c.muted(' Não conectado. ') + c.brand('/conectar') + c.muted(' para entrar.'));
|
|
1122
1220
|
break;
|
|
1123
1221
|
}
|
|
1124
1222
|
const sp = new Spinner('conferindo').start();
|
|
@@ -1304,6 +1402,23 @@ async function main() {
|
|
|
1304
1402
|
pastaPadrao: state.workDirOrigem === 'padrao',
|
|
1305
1403
|
}));
|
|
1306
1404
|
rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: makePrompt(), completer: completarSlash });
|
|
1405
|
+
|
|
1406
|
+
/* A primeira vez. Antes o CLI abria direto no prompt e quem instalava não
|
|
1407
|
+
descobria que existe conta, nem que dá para usar a própria chave — ia
|
|
1408
|
+
usar o gratuito achando que era só aquilo, até bater na cota e concluir
|
|
1409
|
+
que o produto não presta. Aparece UMA vez; depois disso, /conectar e
|
|
1410
|
+
/chave é que são o caminho. */
|
|
1411
|
+
if (!boasVindas.jaAbriu({ arquivoConfig: cfg.CONFIG_FILE })) {
|
|
1412
|
+
await boasVindas.rodar({
|
|
1413
|
+
perguntar: ask,
|
|
1414
|
+
mostrar: (t) => console.log(t),
|
|
1415
|
+
cores: c,
|
|
1416
|
+
interativo: !!(process.stdin.isTTY && process.stdout.isTTY),
|
|
1417
|
+
conectar: () => handleSlash('/conectar'),
|
|
1418
|
+
configurarChave: () => handleSlash('/chave'),
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1307
1422
|
rl.prompt();
|
|
1308
1423
|
|
|
1309
1424
|
// Fila: o readline continua emitindo 'line' enquanto um pedido está sendo
|