primocode 7.3.0 → 7.4.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 +65 -8
- package/bin/primocode.js +54 -5
- package/lib/act.js +48 -6
- package/lib/browser.js +507 -492
- package/lib/config.js +5 -2
- package/lib/desktop.js +189 -142
- package/lib/tools.js +113 -0
- package/lib/ui.js +236 -165
- package/package.json +1 -1
package/lib/ui.js
CHANGED
|
@@ -1,166 +1,237 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* UI components — estilo Claude Code
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
'use strict';
|
|
6
|
-
|
|
7
|
-
const readline = require('readline');
|
|
8
|
-
|
|
9
|
-
const PALETTE = {
|
|
10
|
-
brand: '\x1b[38;5;183m',
|
|
11
|
-
brandDim: '\x1b[38;5;146m',
|
|
12
|
-
white: '\x1b[38;5;253m',
|
|
13
|
-
muted: '\x1b[38;5;245m',
|
|
14
|
-
dim: '\x1b[38;5;240m',
|
|
15
|
-
ok: '\x1b[38;5;114m',
|
|
16
|
-
warn: '\x1b[38;5;180m',
|
|
17
|
-
err: '\x1b[38;5;174m',
|
|
18
|
-
magenta: '\x1b[38;5;176m',
|
|
19
|
-
cyan: '\x1b[38;5;152m',
|
|
20
|
-
bold: '\x1b[1m',
|
|
21
|
-
reset: '\x1b[0m',
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const c = {
|
|
25
|
-
...PALETTE,
|
|
26
|
-
brand: (s) => `${PALETTE.brand}${s}${PALETTE.reset}`,
|
|
27
|
-
brandDim: (s) => `${PALETTE.brandDim}${s}${PALETTE.reset}`,
|
|
28
|
-
white: (s) => `${PALETTE.white}${s}${PALETTE.reset}`,
|
|
29
|
-
muted: (s) => `${PALETTE.muted}${s}${PALETTE.reset}`,
|
|
30
|
-
dim: (s) => `${PALETTE.dim}${s}${PALETTE.reset}`,
|
|
31
|
-
ok: (s) => `${PALETTE.ok}${s}${PALETTE.reset}`,
|
|
32
|
-
warn: (s) => `${PALETTE.warn}${s}${PALETTE.reset}`,
|
|
33
|
-
err: (s) => `${PALETTE.err}${s}${PALETTE.reset}`,
|
|
34
|
-
magenta: (s) => `${PALETTE.magenta}${s}${PALETTE.reset}`,
|
|
35
|
-
cyan: (s) => `${PALETTE.cyan}${s}${PALETTE.reset}`,
|
|
36
|
-
bold: (s) => `${PALETTE.bold}${s}${PALETTE.reset}`,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
''
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
''
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
stop() {
|
|
115
|
-
this.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
1
|
+
/**
|
|
2
|
+
* UI components — estilo Claude Code
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
|
|
9
|
+
const PALETTE = {
|
|
10
|
+
brand: '\x1b[38;5;183m',
|
|
11
|
+
brandDim: '\x1b[38;5;146m',
|
|
12
|
+
white: '\x1b[38;5;253m',
|
|
13
|
+
muted: '\x1b[38;5;245m',
|
|
14
|
+
dim: '\x1b[38;5;240m',
|
|
15
|
+
ok: '\x1b[38;5;114m',
|
|
16
|
+
warn: '\x1b[38;5;180m',
|
|
17
|
+
err: '\x1b[38;5;174m',
|
|
18
|
+
magenta: '\x1b[38;5;176m',
|
|
19
|
+
cyan: '\x1b[38;5;152m',
|
|
20
|
+
bold: '\x1b[1m',
|
|
21
|
+
reset: '\x1b[0m',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const c = {
|
|
25
|
+
...PALETTE,
|
|
26
|
+
brand: (s) => `${PALETTE.brand}${s}${PALETTE.reset}`,
|
|
27
|
+
brandDim: (s) => `${PALETTE.brandDim}${s}${PALETTE.reset}`,
|
|
28
|
+
white: (s) => `${PALETTE.white}${s}${PALETTE.reset}`,
|
|
29
|
+
muted: (s) => `${PALETTE.muted}${s}${PALETTE.reset}`,
|
|
30
|
+
dim: (s) => `${PALETTE.dim}${s}${PALETTE.reset}`,
|
|
31
|
+
ok: (s) => `${PALETTE.ok}${s}${PALETTE.reset}`,
|
|
32
|
+
warn: (s) => `${PALETTE.warn}${s}${PALETTE.reset}`,
|
|
33
|
+
err: (s) => `${PALETTE.err}${s}${PALETTE.reset}`,
|
|
34
|
+
magenta: (s) => `${PALETTE.magenta}${s}${PALETTE.reset}`,
|
|
35
|
+
cyan: (s) => `${PALETTE.cyan}${s}${PALETTE.reset}`,
|
|
36
|
+
bold: (s) => `${PALETTE.bold}${s}${PALETTE.reset}`,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ── Mascotes ─────────────────────────────────────────────────────────────
|
|
40
|
+
// Primo, o mascote — expressivo e personalizável. Fones de "coach", antena
|
|
41
|
+
// que brilha, olhos acesos. Escolha o estilo com /mascot.
|
|
42
|
+
// Sem emoji — só formas geométricas de largura 1, alinhadas em qualquer
|
|
43
|
+
// terminal monospace (Windows, Linux e Mac). Cada linha tem 9 colunas.
|
|
44
|
+
const MASCOTS = {
|
|
45
|
+
// Padrão: robô-coach — antena que brilha, headset lateral, olhar vivo,
|
|
46
|
+
// sorriso e pezinhos. Limpo e simétrico, 11 colunas por linha.
|
|
47
|
+
primo: () => [
|
|
48
|
+
` ${c.magenta('╭')}${c.magenta('◠')}${c.magenta('╮')} `,
|
|
49
|
+
` ${c.brand('╭─┴─┴─╮')} `,
|
|
50
|
+
` ${c.cyan('◖')}${c.brand('┫')} ${c.cyan('●')} ${c.cyan('●')} ${c.brand('┣')}${c.cyan('◗')} `,
|
|
51
|
+
` ${c.brand('┃')} ${c.magenta('‿')} ${c.brand('┃')} `,
|
|
52
|
+
` ${c.brand('╰──┬──╯')} `,
|
|
53
|
+
` ${c.brandDim('╰─┴─╯')} `,
|
|
54
|
+
],
|
|
55
|
+
// Raposa — carismática e simétrica.
|
|
56
|
+
fox: () => [
|
|
57
|
+
` ${c.warn('╱╲')} ${c.warn('╱╲')} `,
|
|
58
|
+
`${c.warn('╱')} ${c.warn('╲_╱')} ${c.warn('╲')}`,
|
|
59
|
+
`${c.warn('▏')} ${c.cyan('◕')} ${c.cyan('◕')} ${c.warn('▕')}`,
|
|
60
|
+
`${c.warn('╲')} ${c.magenta('▾')} ${c.warn('╱')}`,
|
|
61
|
+
` ${c.warn('╲__')}${c.magenta('ᵕ')}${c.warn('__╱')} `,
|
|
62
|
+
],
|
|
63
|
+
// Gato — minimalista e fofo.
|
|
64
|
+
cat: () => [
|
|
65
|
+
` ${c.brand('╱╲_╱╲')} `,
|
|
66
|
+
`${c.brand('│')} ${c.cyan('◕')} ${c.magenta('◡')} ${c.cyan('◕')} ${c.brand('│')}`,
|
|
67
|
+
` ${c.brand('╲')} ${c.magenta('▿')} ${c.brand('╱')} `,
|
|
68
|
+
` ${c.brand('╲───╯')} `,
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function mascot(style) {
|
|
73
|
+
const fn = MASCOTS[style] || MASCOTS.primo;
|
|
74
|
+
return fn();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function welcomeBox(version, contexto, opts = {}) {
|
|
78
|
+
const { mascot: style, userName } = opts;
|
|
79
|
+
const art = mascot(style);
|
|
80
|
+
const saudacao = userName ? `${c.white('Olá, ')}${c.bold(c.brand(userName))}` : c.muted('agente de engenharia');
|
|
81
|
+
const side = [
|
|
82
|
+
'',
|
|
83
|
+
`${c.bold(c.brand('P R I M O C O D E'))}`,
|
|
84
|
+
`${saudacao} ${c.dim('v' + version)}`,
|
|
85
|
+
contexto ? c.brandDim('▸ ' + contexto) : c.dim('nenhuma pasta ativa'),
|
|
86
|
+
c.dim('cria arquivos · roda comandos · controla navegador e desktop'),
|
|
87
|
+
'',
|
|
88
|
+
];
|
|
89
|
+
const rows = art.map((linha, i) => ` ${linha} ${side[i] || ''}`);
|
|
90
|
+
return [
|
|
91
|
+
'',
|
|
92
|
+
...rows,
|
|
93
|
+
'',
|
|
94
|
+
` ${c.magenta('●')} ${c.muted('Primo Code')} ${c.brand('●')} ${c.muted('Chat')} ${c.dim('/help')} ${c.dim('para tudo')}`,
|
|
95
|
+
'',
|
|
96
|
+
].join('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
class Spinner {
|
|
100
|
+
constructor(text='pensando') {
|
|
101
|
+
this.text = text;
|
|
102
|
+
this.frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
103
|
+
this.i = 0; this.timer = null;
|
|
104
|
+
}
|
|
105
|
+
start() {
|
|
106
|
+
// Fora de um terminal (pipe, CI) a animação só sujaria a saída.
|
|
107
|
+
if (!process.stdout.isTTY) return this;
|
|
108
|
+
this.timer = setInterval(() => {
|
|
109
|
+
process.stdout.write(`\r ${c.muted(this.frames[this.i])} ${c.muted(this.text)}`);
|
|
110
|
+
this.i = (this.i+1) % this.frames.length;
|
|
111
|
+
}, 80);
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
stop() {
|
|
115
|
+
if (this.timer) { clearInterval(this.timer); this.timer = null; process.stdout.write(`\r${' '.repeat(80)}\r`); }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Barra de progresso estilo Claude Code — silenciosa, elegante
|
|
120
|
+
class ProgressBar {
|
|
121
|
+
constructor(label = 'Instalando') {
|
|
122
|
+
this.label = label;
|
|
123
|
+
this.width = 30;
|
|
124
|
+
this.progress = 0;
|
|
125
|
+
this.timer = null;
|
|
126
|
+
this.done = false;
|
|
127
|
+
}
|
|
128
|
+
start() {
|
|
129
|
+
process.stdout.write(` ${c.muted(this.label)} [`);
|
|
130
|
+
this.timer = setInterval(() => {
|
|
131
|
+
if (this.done) return;
|
|
132
|
+
this.progress = Math.min(this.progress + 1, this.width);
|
|
133
|
+
const filled = '█'.repeat(this.progress);
|
|
134
|
+
const empty = '░'.repeat(this.width - this.progress);
|
|
135
|
+
process.stdout.write(`\r ${c.muted(this.label)} [${c.brand(filled)}${c.dim(empty)}] ${c.muted(Math.round((this.progress/this.width)*100) + '%')}`);
|
|
136
|
+
}, 200);
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
tick() {
|
|
140
|
+
this.progress = Math.min(this.progress + 2, this.width);
|
|
141
|
+
const filled = '█'.repeat(this.progress);
|
|
142
|
+
const empty = '░'.repeat(this.width - this.progress);
|
|
143
|
+
process.stdout.write(`\r ${c.muted(this.label)} [${c.brand(filled)}${c.dim(empty)}] ${c.muted(Math.round((this.progress/this.width)*100) + '%')}`);
|
|
144
|
+
}
|
|
145
|
+
stop() {
|
|
146
|
+
this.done = true;
|
|
147
|
+
if (this.timer) { clearInterval(this.timer); this.timer = null; }
|
|
148
|
+
process.stdout.write(`\r ${c.muted(this.label)} [${c.ok('██████████ DONE ██████████')}] ${c.ok('100%')}\n`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function actionHeader(name, args) {
|
|
122
153
|
const nice = {
|
|
123
|
-
read_file:'Read', write_file:'Write', edit_file:'Edit',
|
|
124
|
-
glob:'Glob', grep:'Grep', run_shell:'Bash',
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
154
|
+
read_file:'Read', write_file:'Write', edit_file:'Edit', multi_edit:'MultiEdit',
|
|
155
|
+
list_dir:'List', glob:'Glob', grep:'Grep', run_shell:'Bash',
|
|
156
|
+
run_in_new_terminal:'Terminal', open_in_browser:'Open', spawn_agent:'Agent',
|
|
157
|
+
write_todos:'Plano', finish:'Finish',
|
|
158
|
+
browser_open:'Browser', browser_click:'Browser·click', browser_type:'Browser·type',
|
|
159
|
+
browser_key:'Browser·key', browser_read:'Browser·read', browser_screenshot:'Browser·shot',
|
|
160
|
+
browser_scroll:'Browser·scroll', browser_wait:'Browser·wait', browser_eval:'Browser·eval',
|
|
161
|
+
desktop_screenshot:'Desktop·shot', desktop_click:'Desktop·click', desktop_move:'Desktop·move',
|
|
162
|
+
desktop_drag:'Desktop·drag', desktop_type:'Desktop·type', desktop_key:'Desktop·key',
|
|
163
|
+
desktop_scroll:'Desktop·scroll', desktop_size:'Desktop·size',
|
|
164
|
+
};
|
|
165
|
+
const label = nice[name] || name;
|
|
166
|
+
const argStr = args ? (args.path || args.pattern || args.command || args.target || args.url || args.selector || args.text || args.description || (args.todos ? `${args.todos.length} tarefas` : '') || (args.x != null ? `${args.x},${args.y}` : '') || args.key || JSON.stringify(args).slice(0,60)) : '';
|
|
167
|
+
return ` ${c.dim('⎿')} ${c.brand(label)}${argStr ? ' '+c.muted(argStr) : ''}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Transforma erro cru (JSON do Groq, código HTTP, erro de rede) numa frase
|
|
171
|
+
// humana. O usuário nunca deveria ver um paredão de JSON.
|
|
172
|
+
function friendlyError(raw) {
|
|
173
|
+
const s = String(raw && raw.message ? raw.message : raw || '').trim();
|
|
174
|
+
const low = s.toLowerCase();
|
|
175
|
+
// Muitas vezes o texto útil vem dentro de um JSON: {"error":{"message":"..."}}
|
|
176
|
+
let inner = s;
|
|
177
|
+
try {
|
|
178
|
+
const m = s.match(/\{[\s\S]*\}/);
|
|
179
|
+
if (m) {
|
|
180
|
+
const j = JSON.parse(m[0]);
|
|
181
|
+
inner = (j.error && (j.error.message || j.error)) || j.erro || j.message || s;
|
|
182
|
+
if (typeof inner !== 'string') inner = s;
|
|
183
|
+
}
|
|
184
|
+
} catch {}
|
|
185
|
+
|
|
186
|
+
if (/rate.?limit|tokens per minute|tpm|request too large|too many requests|\b429\b|\b413\b/i.test(low)) {
|
|
187
|
+
return '⏳ O servidor está no limite de uso agora. Espere alguns segundos e tente de novo.';
|
|
188
|
+
}
|
|
189
|
+
if (/\b401\b|unauthorized|token/i.test(low)) {
|
|
190
|
+
return '🔒 Este servidor pede um token. Configure PRIMOCODE_TOKEN ou "token" em ~/.primocode/config.json.';
|
|
191
|
+
}
|
|
192
|
+
if (/econnrefused|enotfound|etimedout|timeout|socket hang up|network|getaddrinfo|econnreset/i.test(low)) {
|
|
193
|
+
return '📡 Não consegui falar com o servidor. Verifique sua conexão e tente de novo.';
|
|
194
|
+
}
|
|
195
|
+
if (/\b5\d\d\b|internal server|bad gateway|service unavailable|gateway timeout/i.test(low)) {
|
|
196
|
+
return '🔧 O servidor está fora do ar no momento. Tente de novo em instantes.';
|
|
197
|
+
}
|
|
198
|
+
// Sobrou algo específico: mostra só a mensagem interna, sem o JSON gigante.
|
|
199
|
+
const clean = inner.replace(/\s+/g, ' ').trim();
|
|
200
|
+
return clean.length > 200 ? clean.slice(0, 200) + '…' : clean || 'Algo deu errado.';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function toolLine(text) { return ` ${c.dim('⎿')} ${c.muted(text)}`; }
|
|
204
|
+
|
|
205
|
+
function toolResult(result) {
|
|
206
|
+
if (!result) return '';
|
|
207
|
+
if (result.ok === false) return c.err(` ✗ ${result.error || 'failed'}`);
|
|
208
|
+
if (result.created !== undefined) return c.ok(` ${result.created?'created':'updated'} · ${fmtBytes(result.bytes||0)}`);
|
|
209
|
+
return '';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function fmtBytes(n) {
|
|
213
|
+
if (n < 1024) return `${n} B`;
|
|
214
|
+
if (n < 1024*1024) return `${(n/1024).toFixed(1)} KB`;
|
|
215
|
+
return `${(n/(1024*1024)).toFixed(1)} MB`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function renderMarkdown(text) {
|
|
219
|
+
return text.replace(/\*\*([^*]+)\*\*/g,'$1').replace(/__([^_]+)__/g,'$1').replace(/`([^`]+)`/g,'$1').replace(/^#{1,6}\s+/gm,'');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Pergunta Yes/No — aguarda resposta real do usuário
|
|
223
|
+
function askYesNo(question) {
|
|
224
|
+
return new Promise((resolve) => {
|
|
225
|
+
const rl = readline.createInterface({
|
|
226
|
+
input: process.stdin,
|
|
227
|
+
output: process.stdout,
|
|
228
|
+
});
|
|
229
|
+
rl.question(`${c.brand('?')} ${question} ${c.muted('(Y/n)')} `, (answer) => {
|
|
230
|
+
rl.close();
|
|
231
|
+
const a = (answer || '').trim().toLowerCase();
|
|
232
|
+
resolve(a === 'y' || a === 'yes' || a === '');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = { c, mascot, welcomeBox, Spinner, ProgressBar, actionHeader, toolLine, toolResult, fmtBytes, renderMarkdown, askYesNo, friendlyError };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.4.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modo primocode cria arquivos, controla o desktop e navegador. Modo chat é conversa pura.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|