primocode 7.1.0 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/primocode.js CHANGED
@@ -10,6 +10,7 @@
10
10
  const readline = require('readline');
11
11
  const path = require('path');
12
12
  const fs = require('fs');
13
+ const os = require('os');
13
14
 
14
15
  const pkg = require('../package.json');
15
16
  const { c, welcomeBox, Spinner } = require('../lib/ui');
@@ -27,6 +28,7 @@ const state = {
27
28
  attachedFiles: [],
28
29
  lastResponse: '',
29
30
  currentProject: null,
31
+ workDir: null, // pasta onde o agente trabalha nesta sessão
30
32
  };
31
33
 
32
34
  const HELP = `
@@ -96,33 +98,40 @@ function deriveProjectName(text) {
96
98
  return picked || 'projeto';
97
99
  }
98
100
 
99
- // Garante que a sessão tem um projeto. Se já existe algo parecido, pergunta
100
- // antes de criar uma pasta nova — senão o disco vira um cemitério de duplicatas.
101
- async function ensureProject(userText) {
102
- if (state.currentProject) return state.currentProject;
101
+ // Como o Claude Code: se você abriu o PrimoCode dentro de uma pasta de
102
+ // trabalho, é ali que ele trabalha. Home e raiz de disco não contam — ninguém
103
+ // quer um agente soltando arquivo no meio de C:\Users\você.
104
+ function isUsableCwd(dir) {
105
+ const home = os.homedir();
106
+ const resolved = path.resolve(dir);
107
+ if (resolved === path.resolve(home)) return false;
108
+ if (resolved === path.parse(resolved).root) return false;
109
+ if (resolved.startsWith(path.join(home, 'primocode'))) return false;
110
+ return true;
111
+ }
103
112
 
113
+ // Escolhe onde trabalhar SEM criar nada. A pasta só é criada de verdade em
114
+ // createPendingProject(), quando o agente for mexer em arquivo.
115
+ function planWorkDir(userText) {
116
+ if (state.workDir) return { dir: state.workDir, pending: false };
104
117
  const proposed = deriveProjectName(userText);
105
118
  const similar = cfg.findSimilarProject(proposed);
119
+ const name = similar ? similar.name : proposed;
120
+ return { dir: cfg.getProjectDir(name), pending: true, name, similar: !!similar };
121
+ }
106
122
 
107
- if (similar) {
108
- console.log(c.muted(`\n Já existe um projeto parecido: ${c.brand(similar.name)}`));
109
- if (await confirm(`Continuar nele?`)) {
110
- state.currentProject = similar.name;
111
- console.log(c.ok(` Projeto: ${similar.name}`));
112
- refreshPrompt();
113
- return state.currentProject;
114
- }
115
- }
116
-
117
- const dir = cfg.createProjectDir(proposed);
123
+ function createPendingProject(plan) {
124
+ const dir = cfg.createProjectDir(plan.name);
125
+ state.workDir = dir;
118
126
  state.currentProject = path.basename(dir);
119
- console.log(c.ok(`\n Projeto novo: ${state.currentProject}`) + c.muted(` (${dir})`));
127
+ const rotulo = plan.similar ? 'Continuando em' : 'Projeto novo:';
128
+ console.log(c.ok(` ${rotulo} ${state.currentProject}`) + c.muted(` (${dir})`));
120
129
  refreshPrompt();
121
- return state.currentProject;
130
+ return dir;
122
131
  }
123
132
 
124
133
  function projectDir() {
125
- return state.currentProject ? cfg.getProjectDir(state.currentProject) : cfg.ROOT_DIR;
134
+ return state.workDir || process.cwd();
126
135
  }
127
136
 
128
137
  // ── Envio ──────────────────────────────────────────────────────────────
@@ -135,14 +144,16 @@ async function sendPrompt(userText) {
135
144
  }
136
145
 
137
146
  if (state.mode === 'primocode') {
138
- await ensureProject(userText);
147
+ const plan = planWorkDir(userText);
139
148
  const r = await runAct(prompt, {
140
149
  server: state.config.server,
141
150
  model: state.config.model,
142
151
  token: state.config.token,
143
152
  history: state.history,
144
- projectDir: projectDir(),
145
- projectName: state.currentProject,
153
+ projectDir: plan.dir,
154
+ projectName: plan.pending ? plan.name : state.currentProject,
155
+ // Só chamado se o agente for mesmo mexer em arquivo.
156
+ ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
146
157
  });
147
158
  state.history.push({ role: 'user', content: userText });
148
159
  state.history.push({ role: 'assistant', content: r.assistantMessage.content });
@@ -214,6 +225,7 @@ async function handleSlash(text) {
214
225
  break;
215
226
  }
216
227
  const dir = cfg.createProjectDir(rest);
228
+ state.workDir = dir;
217
229
  state.currentProject = path.basename(dir);
218
230
  console.log(c.ok(`Projeto: ${state.currentProject}`) + c.muted(` (${dir})`));
219
231
  refreshPrompt();
@@ -314,8 +326,24 @@ async function handleSlash(text) {
314
326
 
315
327
  // ── Main ───────────────────────────────────────────────────────────────
316
328
 
329
+ function shortPath(p) {
330
+ // Normaliza as barras antes de comparar — no Windows o cwd vem com "\"
331
+ // e a home pode vir com "/", e aí o prefixo nunca batia.
332
+ const norm = (s) => path.resolve(s).replace(/\\/g, '/');
333
+ const home = norm(os.homedir());
334
+ const full = norm(p);
335
+ return full.startsWith(home + '/') ? '~' + full.slice(home.length) : full;
336
+ }
337
+
317
338
  async function main() {
318
339
  cfg.ensureStructure();
340
+
341
+ // Abriu dentro de uma pasta de trabalho? Então é ali que ele atua.
342
+ if (isUsableCwd(process.cwd())) {
343
+ state.workDir = process.cwd();
344
+ state.currentProject = path.basename(state.workDir);
345
+ }
346
+
319
347
  const args = process.argv.slice(2);
320
348
 
321
349
  // Modo one-shot: primocode "crie um jogo de carro"
@@ -332,19 +360,20 @@ async function main() {
332
360
  } catch { console.log(c.err(`Não consegui ler: ${args[fileFlag + 1]}`)); process.exit(1); }
333
361
  }
334
362
 
335
- await ensureProject(promptText);
363
+ const plan = planWorkDir(promptText);
336
364
  await runAct(fullPrompt, {
337
365
  server: state.config.server,
338
366
  model: state.config.model,
339
367
  token: state.config.token,
340
368
  history: [],
341
- projectDir: projectDir(),
342
- projectName: state.currentProject,
369
+ projectDir: plan.dir,
370
+ projectName: plan.pending ? plan.name : state.currentProject,
371
+ ensureWorkDir: plan.pending ? () => createPendingProject(plan) : null,
343
372
  });
344
373
  process.exit(0);
345
374
  }
346
375
 
347
- console.log(welcomeBox(VERSION, state.currentProject));
376
+ console.log(welcomeBox(VERSION, state.workDir ? shortPath(state.workDir) : null));
348
377
  rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: makePrompt() });
349
378
  rl.prompt();
350
379
 
package/lib/act.js CHANGED
@@ -49,21 +49,24 @@ function summarizeForModel(result) {
49
49
  return json.length > 4000 ? json.slice(0, 4000) + '…(truncado)' : json;
50
50
  }
51
51
 
52
- function buildMemoryContext(projectName) {
52
+ function buildMemoryContext(projectDir, projectName) {
53
53
  const parts = [];
54
54
  const geral = cfg.readGeneralMemory();
55
55
  if (geral.trim()) parts.push(`[memory-geral.md]\n${geral}`);
56
- const projeto = cfg.readProjectMemory(projectName);
56
+ const projeto = cfg.readProjectMemory(projectDir);
57
57
  if (projeto.trim()) parts.push(`[${projectName}/memory.md]\n${projeto}`);
58
58
  return parts.join('\n\n');
59
59
  }
60
60
 
61
61
  async function runAct(userPrompt, opts) {
62
- const { server, model, token, history, projectDir, projectName } = opts || {};
62
+ const { server, model, token, history, projectDir, projectName, ensureWorkDir } = opts || {};
63
+ // A pasta só nasce quando o agente for mesmo mexer em arquivo. Um "oi"
64
+ // não deve deixar diretório vazio espalhado pelo disco.
65
+ let workDirReady = !ensureWorkDir;
63
66
 
64
67
  // Conversa completa desta tarefa: histórico recente + memória + pedido.
65
68
  const messages = [];
66
- const memory = buildMemoryContext(projectName);
69
+ const memory = buildMemoryContext(projectDir, projectName);
67
70
  if (memory) messages.push({ role: 'system', content: `Memória persistente:\n\n${memory}` });
68
71
  for (const m of (history || []).slice(-20)) {
69
72
  if (m && m.role && m.content) messages.push({ role: m.role, content: m.content });
@@ -133,6 +136,8 @@ async function runAct(userPrompt, opts) {
133
136
  continue;
134
137
  }
135
138
 
139
+ if (!workDirReady) { ensureWorkDir(); workDirReady = true; }
140
+
136
141
  console.log(actionHeader(tc.name, tc.arguments));
137
142
  const result = await tools.execute(tc.name, tc.arguments, { projectDir });
138
143
  logResult(tc.name, result);
@@ -144,8 +149,9 @@ async function runAct(userPrompt, opts) {
144
149
 
145
150
  if (!finalText) finalText = 'Cheguei ao limite de passos sem concluir. Refaça o pedido em partes menores.';
146
151
 
147
- if (projectName) {
148
- cfg.appendProjectMemory(projectName, `**Pedido:** ${userPrompt.slice(0, 300)}\n**Resultado:** ${finalText.slice(0, 500)}`);
152
+ // Nada de arquivo mexido = nada para lembrar.
153
+ if (projectName && workDirReady) {
154
+ cfg.appendProjectMemory(projectDir, `**Pedido:** ${userPrompt.slice(0, 300)}\n**Resultado:** ${finalText.slice(0, 500)}`);
149
155
  }
150
156
 
151
157
  return {
package/lib/config.js CHANGED
@@ -135,18 +135,19 @@ function readGeneralMemory(limit = 2000) {
135
135
  catch { return ''; }
136
136
  }
137
137
 
138
- function readProjectMemory(projectName, limit = 2000) {
139
- if (!projectName) return '';
140
- try { return fs.readFileSync(path.join(getProjectDir(projectName), 'memory.md'), 'utf-8').slice(0, limit); }
138
+ // Recebem o DIRETÓRIO de trabalho, não o nome do projeto: quando o PrimoCode
139
+ // roda dentro de uma pasta sua, a memória fica lá, junto do código.
140
+ function readProjectMemory(dir, limit = 2000) {
141
+ if (!dir) return '';
142
+ try { return fs.readFileSync(path.join(dir, 'memory.md'), 'utf-8').slice(0, limit); }
141
143
  catch { return ''; }
142
144
  }
143
145
 
144
- function appendProjectMemory(projectName, entry) {
145
- if (!projectName || !entry) return;
146
- const memoryPath = path.join(getProjectDir(projectName), 'memory.md');
146
+ function appendProjectMemory(dir, entry) {
147
+ if (!dir || !entry) return;
147
148
  try {
148
- ensureDir(path.dirname(memoryPath));
149
- fs.appendFileSync(memoryPath, `\n### ${today()}\n${entry}\n`, 'utf-8');
149
+ ensureDir(dir);
150
+ fs.appendFileSync(path.join(dir, 'memory.md'), `\n### ${today()}\n${entry}\n`, 'utf-8');
150
151
  } catch {}
151
152
  }
152
153
 
package/lib/ui.js CHANGED
@@ -36,24 +36,31 @@ const c = {
36
36
  bold: (s) => `${PALETTE.bold}${s}${PALETTE.reset}`,
37
37
  };
38
38
 
39
- // Primo — o mascote. Robozinho de antena, 4 linhas.
40
- const MASCOT = [' ╷ ', '╭─┴─╮', '│◕ ◕│', '╰───╯'];
39
+ // Primo — o mascote. Antena que brilha, olhos acesos, sorriso discreto.
40
+ function mascot() {
41
+ return [
42
+ ` ${c.dim('·')} ${c.magenta('✦')} ${c.dim('·')} `,
43
+ ` ${c.brand('╭─────╮')} `,
44
+ `${c.brandDim('─')}${c.brand('┤')} ${c.cyan('◉')} ${c.cyan('◉')} ${c.brand('├')}${c.brandDim('─')}`,
45
+ ` ${c.brand('│')} ${c.magenta('‿')} ${c.brand('│')} `,
46
+ ` ${c.brand('╰─┬─┬─╯')} `,
47
+ ];
48
+ }
41
49
 
42
- function welcomeBox(version, project) {
50
+ function welcomeBox(version, contexto) {
43
51
  const side = [
44
- c.brand(c.bold('PrimoCode')),
45
- c.muted('v' + version),
46
- c.muted(project ? `projeto: ${project}` : 'nenhum projeto ativo'),
52
+ '',
53
+ `${c.bold(c.brand('P R I M O C O D E'))}`,
54
+ `${c.muted('agente de engenharia')} ${c.dim('v' + version)}`,
55
+ contexto ? c.brandDim(contexto) : c.dim('nenhuma pasta ativa'),
56
+ '',
47
57
  ];
48
- const rows = MASCOT.map((art, i) => {
49
- const text = i === 0 ? '' : side[i - 1] || '';
50
- return ` ${c.brand(art)} ${text}`;
51
- });
58
+ const rows = mascot().map((art, i) => ` ${art} ${side[i] || ''}`);
52
59
  return [
53
60
  '',
54
61
  ...rows,
55
62
  '',
56
- ` ${c.magenta('●')} ${c.muted('Primo Code')} ${c.brand('●')} ${c.muted('Chat')} ${c.dim('/help para comandos')}`,
63
+ ` ${c.magenta('●')} ${c.muted('Primo Code')} ${c.brand('●')} ${c.muted('Chat')} ${c.dim('/help')}`,
57
64
  '',
58
65
  ].join('\n');
59
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "7.1.0",
3
+ "version": "7.2.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": {