pokt-cli 1.0.9 → 1.0.11

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 CHANGED
@@ -71,7 +71,10 @@ pokt --help
71
71
  - `pokt config set-ollama -v <url>` — URL base do Ollama local.
72
72
  - `pokt config set-ollama-cloud -v <key>` — API key Ollama Cloud.
73
73
  - `pokt config set-gemini -v <key>` — API key Google Gemini.
74
- - `pokt config set-pokt-token -v <token>` — Token do controller Pokt.
74
+ - `pokt config set-pokt-token -v <token>` — Token Pokt (gerado no painel na Railway).
75
+ - `pokt config set-pokt-api-url -v <url>` — API com token Pokt (provider `controller`; padrão Railway). O provider **openai** continua em `api.openai.com`.
76
+ - `pokt config set-pro-portal-url -v <url>` — Painel / serviço (padrão Railway).
77
+ - `pokt config set-token-purchase-url -v <url>` — Só a página de **comprar token** (padrão: Controller Vercel).
75
78
  - `pokt config clear-openrouter` — Remove o token OpenRouter.
76
79
  - `pokt config clear-openai` — Remove a API key OpenAI.
77
80
  - `pokt config clear-grok` — Remove a API key Grok (xAI).
@@ -98,6 +101,9 @@ Se preferir não salvar chaves no computador (ou para CI), você pode usar env v
98
101
  - `OLLAMA_BASE_URL`
99
102
  - `OLLAMA_CLOUD_API_KEY`
100
103
  - `POKT_TOKEN`
104
+ - `POKT_API_BASE_URL` — API com token Pokt (Railway por padrão).
105
+ - `POKT_PRO_PORTAL_URL` (ou `POKT_CONTROLLER_PORTAL_URL`) — Painel / serviço (Railway por padrão).
106
+ - `POKT_TOKEN_PURCHASE_URL` — Só checkout / compra de token (Vercel por padrão). Na atualização, URLs antigas `pokt-cli-controller.vercel.app` salvas em API/painel migram automaticamente para a Railway.
101
107
 
102
108
  ### Provedores (`provider`)
103
109
 
package/dist/bin/pokt.js CHANGED
@@ -257,7 +257,7 @@ async function handleConfigMenu() {
257
257
  if (response.type === 'back')
258
258
  return showMenu();
259
259
  if (response.type === 'show') {
260
- const { getControllerBaseUrl } = await import('../config.js');
260
+ const { getPoktApiBaseUrl, getProPortalBaseUrl, getTokenPurchaseUrl } = await import('../config.js');
261
261
  const openai = config.get('openaiApiKey');
262
262
  const grok = config.get('grokApiKey');
263
263
  const openrouter = config.get('openrouterToken');
@@ -266,7 +266,9 @@ async function handleConfigMenu() {
266
266
  const ollamaCloud = config.get('ollamaCloudApiKey');
267
267
  const poktToken = config.get('poktToken');
268
268
  console.log(chalk.blue('\nCurrent config (tokens masked):'));
269
- console.log(ui.dim(' Controller URL:'), getControllerBaseUrl(), ui.dim('(já configurado)'));
269
+ console.log(ui.dim(' Pokt API (chat):'), getPoktApiBaseUrl());
270
+ console.log(ui.dim(' Painel / serviço:'), getProPortalBaseUrl());
271
+ console.log(ui.dim(' Comprar token:'), getTokenPurchaseUrl());
270
272
  console.log(ui.dim(' Pokt Token:'), poktToken ? poktToken.slice(0, 10) + '****' : '(not set)');
271
273
  console.log(ui.dim(' OpenAI API Key:'), openai ? openai.slice(0, 8) + '****' : '(not set)');
272
274
  console.log(ui.dim(' Grok (xAI) API Key:'), grok ? grok.slice(0, 8) + '****' : '(not set)');
@@ -312,9 +314,8 @@ async function handleConfigMenu() {
312
314
  async function handleProviderMenu() {
313
315
  const { config, getEffectiveActiveModel, PROVIDER_LABELS, ALL_PROVIDERS } = await import('../config.js');
314
316
  let models = config.get('registeredModels');
315
- const hasControllerUrl = !!(config.get('controllerBaseUrl'));
316
317
  const hasPoktToken = !!(config.get('poktToken'));
317
- if ((hasControllerUrl || hasPoktToken) && !models.some((m) => m.provider === 'controller')) {
318
+ if (hasPoktToken && !models.some((m) => m.provider === 'controller')) {
318
319
  models = [{ provider: 'controller', id: 'default' }, ...models];
319
320
  config.set('registeredModels', models);
320
321
  }
@@ -1,11 +1,12 @@
1
1
  import OpenAI from 'openai';
2
- import { getControllerBaseUrl, getOpenAIApiKey, getGrokApiKey, getOpenRouterToken, getGeminiApiKey, getOllamaCloudApiKey, getOllamaBaseUrl, getPoktToken, } from '../config.js';
2
+ import { getPoktApiBaseUrl, getProPortalBaseUrl, getOpenAIApiKey, getGrokApiKey, getOpenRouterToken, getGeminiApiKey, getOllamaCloudApiKey, getOllamaBaseUrl, getPoktToken, } from '../config.js';
3
3
  export async function getClient(modelConfig) {
4
+ // openai / grok / … → hosts oficiais abaixo. Só `controller` usa getPoktApiBaseUrl (token Pokt, não é api.openai.com).
4
5
  if (modelConfig.provider === 'controller') {
5
- const baseUrl = getControllerBaseUrl();
6
+ const baseUrl = getPoktApiBaseUrl();
6
7
  const token = getPoktToken();
7
8
  if (!token) {
8
- throw new Error('Token Pokt não configurado. No painel gere um token e use: pokt config set-pokt-token -v <token>');
9
+ throw new Error(`Token Pokt não configurado. Painel: ${getProPortalBaseUrl()} pokt config set-pokt-token -v <token>`);
9
10
  }
10
11
  return new OpenAI({
11
12
  baseURL: `${baseUrl}/api/v1`,
package/dist/chat/loop.js CHANGED
@@ -21,7 +21,8 @@ CORE CAPABILITIES:
21
21
  3. **Problem Solving**: You analyze errors and propose/apply fixes.
22
22
 
23
23
  FUNCTION CALLING (native tools — USE THEM):
24
- - This chat uses OpenAI-style **tool_calls**. You MUST use the provided functions for actions: \`read_file\`, \`write_file\`, \`run_command\`, \`list_files\`, etc., and any tool whose name starts with \`mcp_\`.
24
+ - This chat uses OpenAI-style **tool_calls**. You MUST use the provided functions for actions: \`read_file\`, \`search_replace\`, \`write_file\`, \`run_command\`, \`list_files\`, etc., and any tool whose name starts with \`mcp_\`.
25
+ - **Edits vs rewrites**: For modifying existing files, prefer \`search_replace\` (old_string, new_string, path) — targeted, minimal changes. Use \`write_file\` only for new files or full rewrites. Always call \`read_file\` first to get exact content before \`search_replace\`.
25
26
  - **Avoid** shell lines like \`mcp_Something_tool "..."\` in markdown — the CLI may run them as **fallback** if they match a registered tool, but **native tool_calls are always better** (correct args, one round-trip).
26
27
  - For databases/APIs exposed via MCP, call the real \`mcp_*\` tools with the correct JSON arguments (e.g. Neon: run SQL via the server's SQL tool, not a invented command name).
27
28
  - **Neon MCP**: tools like \`get_database_tables\`, \`describe_project\`, \`list_branch_computes\` need \`projectId\`. Call \`list_projects\` first and pass the \`id\` of the target project, or rely on the CLI: if your account has exactly one project, Pokt may inject \`projectId\` automatically. To list logical Postgres databases, prefer \`run_sql\` with \`SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY 1;\`.
@@ -34,6 +35,7 @@ WHEN THE MODEL DOES NOT RETURN tool_calls (rare):
34
35
 
35
36
  GUIDELINES:
36
37
  - You will receive the user request first, then the current project structure. Use the project structure to understand the context before creating or editing anything.
38
+ - **Session file focus**: User messages may include a \`[Pokt]\` line with the last path used via \`read_file\` / \`write_file\`. If the user asks to modify, migrate, or rewrite "the" script/app without naming a file, call \`read_file\` on that path and then \`write_file\` to the **same** path. Do **not** switch to \`main.py\` or another new name unless the user explicitly asks.
37
39
  - When asked to fix something, first **read** the relevant files to understand the context.
38
40
  - When creating a project, start by planning the structure, then use \`write_file\` (tool call) to create each file.
39
41
  - You have full access to the current terminal via \`run_command\` for \`npm install\`, \`tsc\`, etc. You may also emit **scripts executáveis** (Node, Python, npx, \`psql\`, etc.) via \`run_command\` when MCP não estiver disponível ou o usuário pedir código para rodar localmente.
@@ -69,6 +71,45 @@ async function loadProjectStructure() {
69
71
  }
70
72
  }
71
73
  }
74
+ /** Detecta se o usuário está pedindo modificação/migração em projeto existente (como gemini-cli faz). */
75
+ function suggestsProjectModification(userInput) {
76
+ const lower = userInput.toLowerCase().trim();
77
+ const modificationKeywords = [
78
+ 'mudar', 'alterar', 'mudança', 'alteração', 'migrar', 'migração', 'trocar', 'converter',
79
+ 'converta', 'modificar', 'refatorar', 'atualizar', 'substituir', 'troque', 'mude',
80
+ 'change', 'migrate', 'convert', 'modify', 'refactor', 'update', 'replace', 'switch',
81
+ ];
82
+ return modificationKeywords.some((kw) => lower.includes(kw));
83
+ }
84
+ /** Extrai arquivos .py da estrutura do projeto (list_files retorna paths separados por newline). */
85
+ function extractPyFilesFromStructure(structure) {
86
+ const lines = structure.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
87
+ return lines.filter((p) => /\.py$/i.test(p) && !p.includes('node_modules') && !p.includes('.git'));
88
+ }
89
+ /** Infere o arquivo alvo para edição quando usuário pede modificar "calculadora", "app", etc. */
90
+ function inferTargetFileFromProject(projectStructure, userInput) {
91
+ const pyFiles = extractPyFilesFromStructure(projectStructure);
92
+ if (pyFiles.length === 0)
93
+ return null;
94
+ const lower = userInput.toLowerCase();
95
+ // Palavras-chave que podem indicar o nome do arquivo
96
+ const keywords = ['calculadora', 'calculator', 'app', 'aplicativo', 'script', 'arquivo', 'projeto'];
97
+ for (const kw of keywords) {
98
+ if (lower.includes(kw)) {
99
+ const stem = kw.replace(/a$/, ''); // calculadora -> calculador
100
+ const match = pyFiles.find((f) => {
101
+ const base = f.replace(/\\/g, '/').split('/').pop()?.replace(/\.py$/, '') ?? '';
102
+ return base.toLowerCase().includes(kw) || base.toLowerCase().includes(stem);
103
+ });
104
+ if (match)
105
+ return match;
106
+ }
107
+ }
108
+ // Se só tem um .py no projeto, é provavelmente o alvo
109
+ if (pyFiles.length === 1)
110
+ return pyFiles[0];
111
+ return null;
112
+ }
72
113
  export async function startChatLoop(modelConfig) {
73
114
  let activeModel = modelConfig;
74
115
  let client = await getClient(activeModel);
@@ -185,11 +226,16 @@ Comandos do chat:
185
226
  ${ui.accent('/chat')} — checkpoints/sessões (list/save/resume/delete/share)
186
227
  ${ui.accent('/resume')} — alias de /chat
187
228
  ${ui.accent('/copy')} — copia a última resposta do Pokt (Windows: clip)
188
- ${ui.accent('/pro')} — abrir Pokt Pro no navegador
229
+ ${ui.accent('/pro')} — abrir compra de token no navegador (Vercel)
189
230
  ${ui.accent('/quit')} ou ${ui.accent('exit')} — sair do chat
190
231
  `));
191
232
  }
192
233
  let lastAssistantText = '';
234
+ /** Último caminho relativo usado em read_file/write_file pelas tool_calls do assistente (foco da sessão). */
235
+ const sessionFileCtx = {
236
+ lastFocusedRelativePath: null,
237
+ suggestedModificationTarget: null,
238
+ };
193
239
  async function handleChatCommand(raw) {
194
240
  const parts = raw.trim().split(/\s+/);
195
241
  const cmd = parts[0].toLowerCase();
@@ -331,23 +377,42 @@ Comandos do chat:
331
377
  if (trimmed === '?') {
332
378
  console.log(ui.dim(`
333
379
  Atalhos:
334
- ${ui.accent('/pro')} ou ${ui.accent('/torne-se-pro')} — abrir Pokt Pro no navegador (pagamento + chave)
380
+ ${ui.accent('/pro')} ou ${ui.accent('/torne-se-pro')} — comprar token (abre Vercel; painel/API na Railway)
335
381
  exit, ${ui.accent('/quit')} — sair do chat
336
382
  ${ui.accent('/help')} — ver comandos do chat
337
383
  `));
338
384
  continue;
339
385
  }
340
- messages.push({ role: 'user', content: userInput });
386
+ let userContent = userInput;
387
+ if (sessionFileCtx.lastFocusedRelativePath) {
388
+ userContent = `${userInput}\n\n[Pokt] Último arquivo usado com read_file/write_file/search_replace nesta sessão: \`${sessionFileCtx.lastFocusedRelativePath}\`. Para alterar esse trabalho sem o usuário citar o nome, leia esse caminho e edite com search_replace ou write_file no mesmo caminho — não crie outro arquivo (ex.: main.py) por padrão.]`;
389
+ }
390
+ messages.push({ role: 'user', content: userContent });
341
391
  saveAuto(messages);
342
- // Primeiro o modelo o pedido; depois carregamos a estrutura do projeto para ele entender e então criar/editar
392
+ // Injeta estrutura do projeto para o modelo entender o contexto (como gemini-cli verifica antes de editar)
393
+ // Sempre na 1ª mensagem; também quando pedido sugere modificação/migração em projeto existente
343
394
  const isFirstUserMessage = messages.filter(m => m.role === 'user').length === 1;
344
- if (isFirstUserMessage) {
345
- const loadSpinner = ora('Carregando estrutura do projeto...').start();
395
+ const needsProjectContext = isFirstUserMessage || suggestsProjectModification(userInput);
396
+ if (needsProjectContext) {
397
+ const loadSpinner = ora('Verificando estrutura do projeto...').start();
346
398
  const projectStructure = await loadProjectStructure();
347
399
  loadSpinner.stop();
348
- messages.push({ role: 'system', content: `Current Project Structure:\n${projectStructure}` });
400
+ const targetFile = suggestsProjectModification(userInput)
401
+ ? inferTargetFileFromProject(projectStructure, userInput)
402
+ : null;
403
+ if (targetFile) {
404
+ sessionFileCtx.suggestedModificationTarget = targetFile;
405
+ }
406
+ let structureHint = '[Pokt] Use list_files/read_file para confirmar arquivos existentes. Para modificar: leia com read_file, edite com search_replace ou write_file no MESMO caminho — NÃO crie main.py ou novo projeto.';
407
+ if (targetFile) {
408
+ structureHint += ` OBRIGATÓRIO: o arquivo a editar é \`${targetFile}\`. Use tool_calls em \`${targetFile}\`, NUNCA em main.py.`;
409
+ }
410
+ messages.push({
411
+ role: 'system',
412
+ content: `Current Project Structure:\n${projectStructure}\n\n${structureHint}`,
413
+ });
349
414
  }
350
- await processLLMResponse(client, activeModel.id, messages, toolsForApi);
415
+ await processLLMResponse(client, activeModel.id, messages, toolsForApi, sessionFileCtx);
351
416
  // Atualiza auto-save após resposta
352
417
  saveAuto(messages);
353
418
  // Captura última resposta do assistente para /copy (melhor esforço)
@@ -451,9 +516,12 @@ function looksLikeFakeMcpInvocation(code) {
451
516
  * Extrai blocos de código da resposta (```lang\n...\n```) e, se encontrar
452
517
  * um nome de arquivo mencionado antes do bloco, aplica write_file.
453
518
  */
454
- /** Remove markdown/formatting do nome de arquivo (ex: **hello.py** → hello.py). */
519
+ /** Remove markdown/formatting do nome de arquivo (ex: **hello.py** → hello.py, (main.py → main.py). */
455
520
  function cleanFilename(candidate) {
456
- return candidate.replace(/^[\s*`'"]+/g, '').replace(/[\s*`'")\]\s]+$/g, '').trim();
521
+ return candidate
522
+ .replace(/^[\s*`'"(\[]+/g, '')
523
+ .replace(/[\s*`'")\]\s]+$/g, '')
524
+ .trim();
457
525
  }
458
526
  /**
459
527
  * Aplica blocos de código da resposta e retorna conteúdo para exibição (sem repetir o código).
@@ -471,7 +539,70 @@ function messageContentToString(content) {
471
539
  }
472
540
  return content != null ? String(content) : '';
473
541
  }
474
- async function applyCodeBlocksFromContent(content) {
542
+ function getFirstChoiceMessage(completion) {
543
+ const msg = completion.choices?.[0]?.message;
544
+ return msg ?? null;
545
+ }
546
+ function trackToolPathFocus(name, argsStr, ctx) {
547
+ if (name !== 'read_file' && name !== 'write_file' && name !== 'search_replace')
548
+ return;
549
+ try {
550
+ const args = JSON.parse(argsStr);
551
+ if (typeof args.path === 'string' && args.path.trim()) {
552
+ ctx.lastFocusedRelativePath = args.path.replace(/\\/g, '/').trim();
553
+ }
554
+ }
555
+ catch {
556
+ /* ignore */
557
+ }
558
+ }
559
+ /**
560
+ * Se o fallback iria gravar em generated.* mas já há um arquivo “foco” na sessão com a mesma extensão,
561
+ * reutiliza esse caminho (evita criar generated.py ao lado de calculadora.py).
562
+ */
563
+ function preferSessionPathOverGenerated(resolvedPath, ctx) {
564
+ const sessionLast = ctx?.lastFocusedRelativePath ?? null;
565
+ const suggested = ctx?.suggestedModificationTarget ?? null;
566
+ // Quando usuário pediu modificar e há arquivo inferido, NUNCA criar main.py/generated.py
567
+ if (suggested && /^(main|generated)\./i.test(resolvedPath)) {
568
+ return suggested.replace(/\\/g, '/');
569
+ }
570
+ if (!sessionLast || !/^generated\./i.test(resolvedPath))
571
+ return resolvedPath;
572
+ const suffix = resolvedPath.slice(resolvedPath.indexOf('.') + 1).toLowerCase();
573
+ const normLast = sessionLast.replace(/\\/g, '/').toLowerCase();
574
+ const extensionsForSuffix = {
575
+ py: ['.py'],
576
+ python: ['.py'],
577
+ js: ['.js'],
578
+ javascript: ['.js'],
579
+ mjs: ['.mjs'],
580
+ cjs: ['.cjs'],
581
+ ts: ['.ts', '.tsx'],
582
+ typescript: ['.ts', '.tsx'],
583
+ tsx: ['.tsx'],
584
+ jsx: ['.jsx'],
585
+ java: ['.java'],
586
+ go: ['.go'],
587
+ rs: ['.rs'],
588
+ html: ['.html', '.htm'],
589
+ css: ['.css'],
590
+ json: ['.json'],
591
+ md: ['.md', '.markdown'],
592
+ txt: ['.txt'],
593
+ cpp: ['.cpp', '.cc', '.cxx'],
594
+ c: ['.c'],
595
+ rb: ['.rb'],
596
+ php: ['.php'],
597
+ };
598
+ const exts = extensionsForSuffix[suffix];
599
+ if (exts?.some((e) => normLast.endsWith(e)))
600
+ return sessionLast.replace(/\\/g, '/');
601
+ if (normLast.endsWith('.' + suffix))
602
+ return sessionLast.replace(/\\/g, '/');
603
+ return resolvedPath;
604
+ }
605
+ async function applyCodeBlocksFromContent(content, sessionFileCtx) {
475
606
  const codeBlockRe = /```(\w*)\n([\s\S]*?)```/g;
476
607
  const appliedBlocks = [];
477
608
  let applied = false;
@@ -495,7 +626,10 @@ async function applyCodeBlocksFromContent(content) {
495
626
  const fileMatch = beforeBlock.match(/(\S+\.(?:py|js|ts|tsx|jsx|html|css|json|md|txt|java|go|rs|c|cpp|rb|php))(?=\s|$|[:.)\]*`"])/gi);
496
627
  const rawCandidate = fileMatch ? fileMatch[fileMatch.length - 1].trim() : null;
497
628
  const candidate = rawCandidate ? cleanFilename(rawCandidate) : null;
498
- const path = candidate && CODE_EXT.test(candidate) ? candidate : (lang === 'python' ? 'generated.py' : lang ? `generated.${lang}` : null);
629
+ let path = candidate && CODE_EXT.test(candidate) ? candidate : lang === 'python' ? 'generated.py' : lang ? `generated.${lang}` : null;
630
+ if (path) {
631
+ path = preferSessionPathOverGenerated(path, sessionFileCtx);
632
+ }
499
633
  if (path && isFallbackPathBlocked(path))
500
634
  continue;
501
635
  if (path &&
@@ -513,6 +647,8 @@ async function applyCodeBlocksFromContent(content) {
513
647
  }
514
648
  await executeTool('write_file', JSON.stringify({ path, content: code }));
515
649
  applied = true;
650
+ if (sessionFileCtx)
651
+ sessionFileCtx.lastFocusedRelativePath = path.replace(/\\/g, '/');
516
652
  appliedBlocks.push({ start: index, end: index + fullMatch.length, path });
517
653
  }
518
654
  catch {
@@ -556,7 +692,7 @@ async function createCompletionWithRetry(client, modelId, messages, toolsList, t
556
692
  /**
557
693
  * Executa todas as rodadas de tool_calls até o modelo devolver mensagem sem ferramentas.
558
694
  */
559
- async function drainToolCalls(client, modelId, messages, toolsList, startMessage, spinner) {
695
+ async function drainToolCalls(client, modelId, messages, toolsList, startMessage, spinner, sessionFileCtx) {
560
696
  let message = startMessage;
561
697
  let writeFileExecuted = false;
562
698
  let anyToolExecuted = false;
@@ -567,11 +703,12 @@ async function drainToolCalls(client, modelId, messages, toolsList, startMessage
567
703
  for (const toolCall of message.tool_calls) {
568
704
  anyToolExecuted = true;
569
705
  const name = toolCall.function.name;
570
- if (name === 'write_file')
706
+ if (name === 'write_file' || name === 'search_replace')
571
707
  writeFileExecuted = true;
572
708
  if (isMcpTool(name))
573
709
  mcpToolExecuted = true;
574
710
  const args = toolCall.function.arguments ?? '{}';
711
+ trackToolPathFocus(name, args, sessionFileCtx);
575
712
  const isMcp = isMcpTool(name);
576
713
  if (isMcp && !verbose) {
577
714
  console.log(ui.dim(`[MCP] ${name}…`));
@@ -602,17 +739,28 @@ async function drainToolCalls(client, modelId, messages, toolsList, startMessage
602
739
  spinner.start('Thinking...');
603
740
  const completion = await createCompletionWithRetry(client, modelId, messages, toolsList, 'auto');
604
741
  spinner.stop();
605
- message = completion.choices[0].message;
742
+ const nextMsg = getFirstChoiceMessage(completion);
743
+ if (!nextMsg) {
744
+ throw new Error('Resposta da API sem choices após execução de ferramentas.');
745
+ }
746
+ message = nextMsg;
606
747
  }
607
748
  return { message, writeFileExecuted, anyToolExecuted, mcpToolExecuted };
608
749
  }
609
- async function processLLMResponse(client, modelId, messages, toolsList) {
750
+ async function processLLMResponse(client, modelId, messages, toolsList, sessionFileCtx) {
610
751
  const spinner = ora('Thinking...').start();
611
752
  try {
612
753
  let completion = await createCompletionWithRetry(client, modelId, messages, toolsList);
613
- let message = completion.choices[0].message;
754
+ const first = getFirstChoiceMessage(completion);
755
+ if (!first) {
756
+ spinner.stop();
757
+ console.log(ui.error('\nResposta da API sem choices/mensagem. Tente novamente ou outro modelo.'));
758
+ messages.pop();
759
+ return;
760
+ }
761
+ let message = first;
614
762
  spinner.stop();
615
- const drained = await drainToolCalls(client, modelId, messages, toolsList, message, spinner);
763
+ const drained = await drainToolCalls(client, modelId, messages, toolsList, message, spinner, sessionFileCtx);
616
764
  message = drained.message;
617
765
  let writeFileExecutedThisTurn = drained.writeFileExecuted;
618
766
  let anyToolExecutedThisTurn = drained.anyToolExecuted;
@@ -640,14 +788,20 @@ async function processLLMResponse(client, modelId, messages, toolsList) {
640
788
  contentStr = mcpFromText.augmentedAssistantText;
641
789
  finalContent = mcpFromText.augmentedAssistantText;
642
790
  }
643
- // Resposta completamente vazia: recuperação (tool_choice required) + dreno + MCP em texto + SQL automático
791
+ // Resposta completamente vazia: recuperação (tool_choice required)
644
792
  if (!contentStr.trim() && toolsList.length > 0) {
645
- messages.push({
646
- role: 'system',
647
- content: '[Pokt recuperação] A última resposta veio vazia (sem texto útil). Responda em português. ' +
648
- 'Use tool_calls: para listar bancos PostgreSQL use mcp_*_run_sql com {"sql":"SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY 1;"}. ' +
649
- 'Alternativa: run_command com script executável (node, python, npx, psql). Nunca devolva corpo vazio.',
650
- });
793
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === 'user');
794
+ const lastUserContent = typeof lastUserMsg?.content === 'string' ? lastUserMsg.content : '';
795
+ const isModificationRequest = suggestsProjectModification(lastUserContent);
796
+ let recoveryHint = '[Pokt recuperação] A última resposta veio vazia. Responda em português. Use tool_calls. Nunca devolva corpo vazio. ';
797
+ if (isModificationRequest) {
798
+ recoveryHint +=
799
+ 'O usuário pediu modificação em projeto existente: chame list_files para ver arquivos, read_file no arquivo relevante, depois search_replace ou write_file no MESMO caminho. NÃO crie projeto novo. ';
800
+ }
801
+ recoveryHint +=
802
+ 'Para bancos PostgreSQL: mcp_*_run_sql com {"sql":"SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY 1;"}. ' +
803
+ 'Alternativa: run_command com script (node, python, npx, psql).';
804
+ messages.push({ role: 'system', content: recoveryHint });
651
805
  spinner.start('Recuperando resposta vazia…');
652
806
  let recovery;
653
807
  try {
@@ -657,7 +811,13 @@ async function processLLMResponse(client, modelId, messages, toolsList) {
657
811
  recovery = await createCompletionWithRetry(client, modelId, messages, toolsList, 'auto');
658
812
  }
659
813
  spinner.stop();
660
- const drained2 = await drainToolCalls(client, modelId, messages, toolsList, recovery.choices[0].message, spinner);
814
+ const recoveryFirst = getFirstChoiceMessage(recovery);
815
+ if (!recoveryFirst) {
816
+ console.log(ui.error('\nResposta da API sem choices na recuperação. Tente outro modelo.'));
817
+ messages.pop();
818
+ return;
819
+ }
820
+ const drained2 = await drainToolCalls(client, modelId, messages, toolsList, recoveryFirst, spinner, sessionFileCtx);
661
821
  message = drained2.message;
662
822
  writeFileExecutedThisTurn = writeFileExecutedThisTurn || drained2.writeFileExecuted;
663
823
  anyToolExecutedThisTurn = anyToolExecutedThisTurn || drained2.anyToolExecuted;
@@ -693,27 +853,33 @@ async function processLLMResponse(client, modelId, messages, toolsList) {
693
853
  }
694
854
  // Quando a API não executa tools, tentar aplicar blocos de código da resposta
695
855
  if (!writeFileExecutedThisTurn) {
696
- let result = await applyCodeBlocksFromContent(contentStr);
856
+ let result = await applyCodeBlocksFromContent(contentStr, sessionFileCtx);
697
857
  // Se a IA só disse "We will call read_file/write_file" e não há código, pedir o código em um follow-up
698
- const looksLikeToolIntentOnly = /(We will call|We need to call|Let's call|I will call)\s+(read_file|write_file|run_command)/i.test(contentStr)
699
- || (/call\s+(read_file|write_file)/i.test(contentStr) && contentStr.length < 400);
858
+ const looksLikeToolIntentOnly = /(We will call|We need to call|Let's call|I will call)\s+(read_file|search_replace|write_file|run_command)/i.test(contentStr)
859
+ || (/call\s+(read_file|search_replace|write_file)/i.test(contentStr) && contentStr.length < 400);
700
860
  if (!result.applied && looksLikeToolIntentOnly) {
701
861
  messages.push({ role: 'assistant', content: rawContent ?? contentStr });
702
- const followUpSystem = `You replied as if tools would run in text only. Use tool_calls for read_file/write_file/run_command/mcp_* when possible. If you must output a file as markdown only: mention the filename then a full \`\`\`lang\`\`\` block — never use fake shell lines like mcp_Foo_bar. Do that now for the user's last request.`;
862
+ const followUpSystem = `You replied as if tools would run in text only. Use tool_calls for read_file/search_replace/write_file/run_command/mcp_* when possible. Prefer search_replace for edits. If you must output a file as markdown only: mention the filename then a full \`\`\`lang\`\`\` block — never use fake shell lines like mcp_Foo_bar. Do that now for the user's last request.`;
703
863
  messages.push({ role: 'system', content: followUpSystem });
704
864
  spinner.start('Getting code...');
705
865
  const followUp = await createCompletionWithRetry(client, modelId, messages, toolsList);
706
866
  spinner.stop();
707
- const followUpMsg = followUp.choices[0].message;
708
- const followUpStr = messageContentToString(followUpMsg.content);
709
- if (followUpStr.trim() !== '') {
710
- result = await applyCodeBlocksFromContent(followUpStr);
711
- contentStr = followUpStr;
712
- finalContent = followUpMsg.content ?? followUpStr;
713
- messages.push({ role: 'assistant', content: finalContent });
867
+ const followUpMsg = getFirstChoiceMessage(followUp);
868
+ if (!followUpMsg) {
869
+ console.log(ui.error('\nFollow-up da API sem choices/mensagem.'));
870
+ messages.push({ role: 'assistant', content: '' });
714
871
  }
715
872
  else {
716
- messages.push({ role: 'assistant', content: '' });
873
+ const followUpStr = messageContentToString(followUpMsg.content);
874
+ if (followUpStr.trim() !== '') {
875
+ result = await applyCodeBlocksFromContent(followUpStr, sessionFileCtx);
876
+ contentStr = followUpStr;
877
+ finalContent = followUpMsg.content ?? followUpStr;
878
+ messages.push({ role: 'assistant', content: finalContent });
879
+ }
880
+ else {
881
+ messages.push({ role: 'assistant', content: '' });
882
+ }
717
883
  }
718
884
  }
719
885
  if (contentStr.trim() !== '') {
@@ -726,7 +892,13 @@ async function processLLMResponse(client, modelId, messages, toolsList) {
726
892
  }
727
893
  else {
728
894
  console.log('\n' + ui.labelPokt());
729
- console.log(ui.dim('(Sem resposta da IA após recuperação. Tente: outro modelo em /model, ou peça explicitamente “chame mcp_Neon_run_sql com SELECT datname FROM pg_database…”, ou use run_command com psql/node.)'));
895
+ const lastUser = [...messages].reverse().find((m) => m.role === 'user');
896
+ const lastContent = typeof lastUser?.content === 'string' ? lastUser.content : '';
897
+ const isMod = suggestsProjectModification(lastContent);
898
+ const hint = isMod
899
+ ? '(Sem resposta da IA. Para modificar projeto: peça "liste os arquivos e leia o arquivo". Tente /model.)'
900
+ : '(Sem resposta da IA após recuperação. Tente: /model, ou peça mcp_Neon_run_sql, ou run_command.)';
901
+ console.log(ui.dim(hint));
730
902
  messages.push({ role: 'assistant', content: '' });
731
903
  }
732
904
  }
@@ -30,6 +30,56 @@ function looksLikeMcpJsonDump(s) {
30
30
  }
31
31
  return false;
32
32
  }
33
+ /**
34
+ * When exact old_string is not found, try to find a similar region and return helpful error.
35
+ * Inspired by gemini-cli fuzzy matcher fallback.
36
+ */
37
+ function findClosestMatchAndBuildError(fileContent, oldString, filePath) {
38
+ const oldLines = oldString.split('\n').filter((l) => l.trim().length > 0);
39
+ if (oldLines.length === 0) {
40
+ return `search_replace failed: old_string is empty or only whitespace.`;
41
+ }
42
+ const firstLine = oldLines[0];
43
+ const idx = fileContent.indexOf(firstLine);
44
+ if (idx >= 0) {
45
+ const start = Math.max(0, idx - 200);
46
+ const end = Math.min(fileContent.length, idx + firstLine.length + 400);
47
+ const snippet = fileContent.slice(start, end);
48
+ const diffResult = diff.diffLines(oldString, snippet);
49
+ const diffPreview = diffResult
50
+ .slice(0, 8)
51
+ .map((p) => (p.added ? `+${p.value}` : p.removed ? `-${p.value}` : ` ${p.value}`))
52
+ .join('')
53
+ .slice(0, 600);
54
+ return `search_replace failed: old_string not found exactly. First line was found at position ${idx}. Suggestion: check whitespace, indentation, line endings. Diff (expected vs file snippet):\n${diffPreview}\n\nCall read_file to see full content and retry with exact match.`;
55
+ }
56
+ const norm = (s) => s.replace(/\r\n/g, '\n').replace(/\s+/g, ' ').trim();
57
+ const normOld = norm(oldString);
58
+ const fileLines = fileContent.split('\n');
59
+ let bestLineIdx = -1;
60
+ let bestLen = 0;
61
+ for (let i = 0; i < fileLines.length; i++) {
62
+ const lineNorm = norm(fileLines[i]);
63
+ if (lineNorm.length < 10)
64
+ continue;
65
+ let matchLen = 0;
66
+ for (let j = 0; j < Math.min(lineNorm.length, normOld.length); j++) {
67
+ if (lineNorm[j] === normOld[j])
68
+ matchLen++;
69
+ else
70
+ break;
71
+ }
72
+ if (matchLen > bestLen && matchLen >= 15) {
73
+ bestLen = matchLen;
74
+ bestLineIdx = i;
75
+ }
76
+ }
77
+ if (bestLineIdx >= 0) {
78
+ const ctx = fileLines.slice(Math.max(0, bestLineIdx - 2), bestLineIdx + 4).join('\n');
79
+ return `search_replace failed: old_string not found. Closest region (line ${bestLineIdx + 1}):\n---\n${ctx}\n---\nUse read_file("${filePath}") and retry with exact content including indentation and newlines.`;
80
+ }
81
+ return `search_replace failed: old_string not found in ${filePath}. Use read_file to get current content and ensure old_string matches exactly (whitespace, tabs, newlines).`;
82
+ }
33
83
  function showDiff(filePath, oldContent, newContent) {
34
84
  const relativePath = path.relative(process.cwd(), filePath);
35
85
  console.log(chalk.blue.bold(`\n📝 Edit ${relativePath}:`));
@@ -125,11 +175,28 @@ export const tools = [
125
175
  }
126
176
  }
127
177
  },
178
+ {
179
+ type: 'function',
180
+ function: {
181
+ name: 'search_replace',
182
+ description: 'Replaces old_string with new_string in a file. PREFERRED for edits and modifications: targeted, minimal changes. Use read_file first to get current content. For new files or complete rewrites use write_file. expected_replacements (optional): if set, fails when count of matches ≠ value (prevents unintended broad changes).',
183
+ parameters: {
184
+ type: 'object',
185
+ properties: {
186
+ path: { type: 'string', description: 'The path to the file' },
187
+ old_string: { type: 'string', description: 'Exact text to find and replace (must match file content including whitespace)' },
188
+ new_string: { type: 'string', description: 'Replacement text' },
189
+ expected_replacements: { type: 'number', description: 'Optional: fail if number of occurrences ≠ this (safety guard)' }
190
+ },
191
+ required: ['path', 'old_string', 'new_string']
192
+ }
193
+ }
194
+ },
128
195
  {
129
196
  type: 'function',
130
197
  function: {
131
198
  name: 'write_file',
132
- description: 'Writes content to a file. Overwrites if exists, creates if not.',
199
+ description: 'Writes full content to a file. Overwrites if exists, creates if not. Use for new files or complete rewrites. For small edits, prefer search_replace. When modifying existing work from this chat, use the same path as before (see user message [Pokt] hint or prior tool calls); call read_file first if you need the current contents.',
133
200
  parameters: {
134
201
  type: 'object',
135
202
  properties: {
@@ -188,6 +255,34 @@ export async function executeTool(name, argsStr) {
188
255
  if (name === 'read_file') {
189
256
  return fs.readFileSync(path.resolve(process.cwd(), args.path), 'utf8');
190
257
  }
258
+ if (name === 'search_replace') {
259
+ const filePath = path.resolve(process.cwd(), args.path);
260
+ if (!fs.existsSync(filePath)) {
261
+ return `Error: File not found: ${args.path}. Use write_file to create it.`;
262
+ }
263
+ const content = fs.readFileSync(filePath, 'utf8');
264
+ const oldStr = args.old_string ?? '';
265
+ const newStr = args.new_string ?? '';
266
+ if (oldStr === newStr) {
267
+ return `search_replace: old_string and new_string are identical; no change made.`;
268
+ }
269
+ const expected = typeof args.expected_replacements === 'number' ? args.expected_replacements : undefined;
270
+ const occurrences = content.split(oldStr).length - 1;
271
+ if (occurrences === 0) {
272
+ const errMsg = findClosestMatchAndBuildError(content, oldStr, args.path);
273
+ return errMsg;
274
+ }
275
+ if (expected !== undefined && occurrences !== expected) {
276
+ return `search_replace failed: found ${occurrences} occurrence(s) of old_string, but expected_replacements=${expected}. Adjust old_string to be unique or set expected_replacements to ${occurrences}.`;
277
+ }
278
+ const newContent = content.split(oldStr).join(newStr);
279
+ fs.writeFileSync(filePath, newContent, 'utf8');
280
+ const rel = path.relative(process.cwd(), filePath);
281
+ if (!isGeneratedNoisePath(rel) || !looksLikeMcpJsonDump(newContent)) {
282
+ showDiff(filePath, content, newContent);
283
+ }
284
+ return `Successfully applied search_replace to ${args.path} (${occurrences} replacement(s)).`;
285
+ }
191
286
  if (name === 'write_file') {
192
287
  const filePath = path.resolve(process.cwd(), args.path);
193
288
  const oldContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
@@ -1,4 +1,4 @@
1
- import { getEffectiveActiveModel, getOpenAIApiKey, getGrokApiKey, getOpenRouterToken, getGeminiApiKey, getPoktToken } from '../config.js';
1
+ import { getEffectiveActiveModel, getOpenAIApiKey, getGrokApiKey, getOpenRouterToken, getGeminiApiKey, getPoktToken, getProPortalBaseUrl, } from '../config.js';
2
2
  import { startChatLoop } from '../chat/loop.js';
3
3
  import { ui } from '../ui.js';
4
4
  export const chatCommand = {
@@ -29,7 +29,7 @@ export const chatCommand = {
29
29
  }
30
30
  if (activeModel.provider === 'controller') {
31
31
  if (!getPoktToken()) {
32
- console.log(ui.error('Pokt token not set. Generate one at the panel and: pokt config set-pokt-token -v <token>'));
32
+ console.log(ui.error(`Pokt token not set. Painel: ${getProPortalBaseUrl()} pokt config set-pokt-token -v <token>`));
33
33
  return;
34
34
  }
35
35
  }
@@ -1,4 +1,4 @@
1
- import { config, getControllerBaseUrl } from '../config.js';
1
+ import { config, getPoktApiBaseUrl, getProPortalBaseUrl, getTokenPurchaseUrl } from '../config.js';
2
2
  import chalk from 'chalk';
3
3
  import { ui } from '../ui.js';
4
4
  export const configCommand = {
@@ -8,7 +8,22 @@ export const configCommand = {
8
8
  .positional('action', {
9
9
  describe: 'Action to perform',
10
10
  type: 'string',
11
- choices: ['set-openai', 'set-grok', 'set-openrouter', 'set-ollama', 'set-ollama-cloud', 'set-gemini', 'set-pokt-token', 'clear-openrouter', 'clear-openai', 'clear-grok', 'show']
11
+ choices: [
12
+ 'set-openai',
13
+ 'set-grok',
14
+ 'set-openrouter',
15
+ 'set-ollama',
16
+ 'set-ollama-cloud',
17
+ 'set-gemini',
18
+ 'set-pokt-token',
19
+ 'set-pokt-api-url',
20
+ 'set-pro-portal-url',
21
+ 'set-token-purchase-url',
22
+ 'clear-openrouter',
23
+ 'clear-openai',
24
+ 'clear-grok',
25
+ 'show',
26
+ ]
12
27
  })
13
28
  .option('value', {
14
29
  describe: 'The value to set',
@@ -32,7 +47,9 @@ export const configCommand = {
32
47
  console.log(ui.dim(' Gemini API Key:'), gemini ? gemini.slice(0, 8) + '****' : '(not set)');
33
48
  console.log(ui.dim(' Ollama Base URL (local):'), ollama || '(not set)');
34
49
  console.log(ui.dim(' Ollama Cloud API Key:'), ollamaCloud ? ollamaCloud.slice(0, 8) + '****' : '(not set) — https://ollama.com/settings/keys');
35
- console.log(ui.dim(' Controller URL:'), getControllerBaseUrl(), ui.dim('(já configurado)'));
50
+ console.log(ui.dim(' Pokt API (chat / token):'), getPoktApiBaseUrl());
51
+ console.log(ui.dim(' Painel / serviço (Railway):'), getProPortalBaseUrl());
52
+ console.log(ui.dim(' Comprar token (Vercel):'), getTokenPurchaseUrl());
36
53
  console.log(ui.dim(' Pokt Token:'), poktToken ? poktToken.slice(0, 10) + '****' : '(not set) — use: pokt config set-pokt-token -v <token>');
37
54
  console.log(ui.warn('\nTokens are stored in your user config directory. Do not share it.\n'));
38
55
  return;
@@ -52,7 +69,16 @@ export const configCommand = {
52
69
  console.log(ui.success('Grok (xAI) API key cleared.'));
53
70
  return;
54
71
  }
55
- if (action !== 'set-openai' && action !== 'set-grok' && action !== 'set-openrouter' && action !== 'set-ollama' && action !== 'set-ollama-cloud' && action !== 'set-gemini' && action !== 'set-pokt-token')
72
+ if (action !== 'set-openai' &&
73
+ action !== 'set-grok' &&
74
+ action !== 'set-openrouter' &&
75
+ action !== 'set-ollama' &&
76
+ action !== 'set-ollama-cloud' &&
77
+ action !== 'set-gemini' &&
78
+ action !== 'set-pokt-token' &&
79
+ action !== 'set-pokt-api-url' &&
80
+ action !== 'set-pro-portal-url' &&
81
+ action !== 'set-token-purchase-url')
56
82
  return;
57
83
  const raw = Array.isArray(value) ? value[0] : value;
58
84
  const strValue = typeof raw === 'string' ? raw : (raw != null ? String(raw) : '');
@@ -92,7 +118,20 @@ export const configCommand = {
92
118
  config.set('registeredModels', [controllerModel, ...models]);
93
119
  }
94
120
  config.set('activeModel', controllerModel);
95
- console.log(ui.success('Pokt token salvo. Controller é seu provedor principal. Gere tokens em: https://pokt-cli-controller.vercel.app'));
121
+ const portal = getProPortalBaseUrl();
122
+ console.log(ui.success(`Pokt token salvo. Provedor Pokt ativo. Gere tokens no painel: ${portal}`));
123
+ }
124
+ else if (action === 'set-pokt-api-url') {
125
+ config.set('poktApiBaseUrl', strValue.replace(/\/$/, ''));
126
+ console.log(ui.success(`URL da API Pokt (token Bearer / provider controller) salva: ${strValue.replace(/\/$/, '')}`));
127
+ }
128
+ else if (action === 'set-pro-portal-url') {
129
+ config.set('controllerBaseUrl', strValue.replace(/\/$/, ''));
130
+ console.log(ui.success(`URL do painel/serviço (Railway) salva: ${strValue.replace(/\/$/, '')}`));
131
+ }
132
+ else if (action === 'set-token-purchase-url') {
133
+ config.set('tokenPurchaseBaseUrl', strValue.replace(/\/$/, ''));
134
+ console.log(ui.success(`URL de compra de token (checkout) salva: ${strValue.replace(/\/$/, '')}`));
96
135
  }
97
136
  }
98
137
  };
@@ -1,6 +1,6 @@
1
1
  import ora from 'ora';
2
2
  import { ui } from '../ui.js';
3
- import { PROVIDER_LABELS, getEffectiveActiveModel, getOpenAIApiKey, getGrokApiKey, getOpenRouterToken, getGeminiApiKey, getOllamaBaseUrl, getOllamaCloudApiKey, getPoktToken, } from '../config.js';
3
+ import { PROVIDER_LABELS, getEffectiveActiveModel, getOpenAIApiKey, getGrokApiKey, getOpenRouterToken, getGeminiApiKey, getOllamaBaseUrl, getOllamaCloudApiKey, getPoktToken, getPoktApiBaseUrl, getProPortalBaseUrl, getTokenPurchaseUrl, } from '../config.js';
4
4
  import { getClient } from '../chat/client.js';
5
5
  function mask(value) {
6
6
  if (!value)
@@ -55,6 +55,11 @@ export const doctorCommand = {
55
55
  return;
56
56
  }
57
57
  console.log(ui.success(`Credencial OK: ${req.name} = ${mask(req.value)}`));
58
+ if (active.provider === 'controller') {
59
+ console.log(ui.dim(` API Pokt (chat): ${getPoktApiBaseUrl()}`));
60
+ console.log(ui.dim(` Painel / serviço: ${getProPortalBaseUrl()}`));
61
+ console.log(ui.dim(` Comprar token: ${getTokenPurchaseUrl()}`));
62
+ }
58
63
  }
59
64
  else if (active.provider === 'ollama') {
60
65
  console.log(ui.success(`Ollama (local) não precisa de chave. Base URL: ${getOllamaBaseUrl()}`));
@@ -4,7 +4,7 @@ import { ui } from '../ui.js';
4
4
  export const proCommand = {
5
5
  command: 'pro',
6
6
  aliases: ['Pro'],
7
- describe: 'Abre a página inicial do Controller (botão "Torne-se Pro"). Use --url só para imprimir o link.',
7
+ describe: 'Abre a página de compra de token no Controller (Vercel). Painel/API usam a Railway. Use --url só para imprimir o link.',
8
8
  builder: (yargs) => yargs.option('url', {
9
9
  type: 'boolean',
10
10
  default: false,
@@ -25,7 +25,7 @@ export function runProFlow(printOnlyUrl = false) {
25
25
  console.log(proHomeUrl);
26
26
  return;
27
27
  }
28
- console.log(ui.dim('Pokt Pro abra o site e clique em "Torne-se Pro" (pagamento + chave imediata).\n'));
28
+ console.log(ui.dim('Comprar token Pokt — abre o site na Vercel (pagamento). Painel/API: Railway.\n'));
29
29
  console.log(ui.accent(proHomeUrl));
30
30
  try {
31
31
  openBrowser(proHomeUrl);
@@ -1,4 +1,4 @@
1
- import { config, ALL_PROVIDERS, getOpenAIApiKey, getGrokApiKey, getOpenRouterToken, getGeminiApiKey, getOllamaCloudApiKey, getPoktToken } from '../config.js';
1
+ import { config, ALL_PROVIDERS, getOpenAIApiKey, getGrokApiKey, getOpenRouterToken, getGeminiApiKey, getOllamaCloudApiKey, getPoktToken, getProPortalBaseUrl, } from '../config.js';
2
2
  import { ui } from '../ui.js';
3
3
  export const providerCommand = {
4
4
  command: 'provider use <provider>',
@@ -54,7 +54,7 @@ export const providerCommand = {
54
54
  return;
55
55
  }
56
56
  if (provider === 'controller' && !getPoktToken()) {
57
- console.log(ui.error('Pokt token not set. Use: pokt config set-pokt-token -v <token>'));
57
+ console.log(ui.error(`Pokt token not set. Painel: ${getProPortalBaseUrl()} — pokt config set-pokt-token -v <token>`));
58
58
  return;
59
59
  }
60
60
  if (provider === 'ollama-cloud' && !getOllamaCloudApiKey()) {
package/dist/config.d.ts CHANGED
@@ -34,7 +34,12 @@ interface AppConfig {
34
34
  geminiApiKey: string;
35
35
  ollamaBaseUrl: string;
36
36
  ollamaCloudApiKey: string;
37
+ /** Painel / links gerais (Railway); não usar para compra de token */
37
38
  controllerBaseUrl: string;
39
+ /** Base para API com token Pokt — provider `controller` (Railway) */
40
+ poktApiBaseUrl: string;
41
+ /** Só compra de token / checkout — Vercel */
42
+ tokenPurchaseBaseUrl: string;
38
43
  poktToken: string;
39
44
  registeredModels: ModelConfig[];
40
45
  activeModel: ModelConfig | null;
@@ -49,6 +54,11 @@ export declare const env: {
49
54
  readonly ollamaBaseUrl: readonly ["OLLAMA_BASE_URL"];
50
55
  readonly ollamaCloudApiKey: readonly ["OLLAMA_CLOUD_API_KEY"];
51
56
  readonly poktToken: readonly ["POKT_TOKEN"];
57
+ readonly poktApiBaseUrl: readonly ["POKT_API_BASE_URL"];
58
+ /** Painel e URLs gerais (Railway) */
59
+ readonly proPortalUrl: readonly ["POKT_PRO_PORTAL_URL", "POKT_CONTROLLER_PORTAL_URL"];
60
+ /** Apenas página de compra de token (Vercel) */
61
+ readonly tokenPurchaseUrl: readonly ["POKT_TOKEN_PURCHASE_URL"];
52
62
  };
53
63
  export declare function getOpenAIApiKey(): string;
54
64
  export declare function getGrokApiKey(): string;
@@ -57,8 +67,15 @@ export declare function getGeminiApiKey(): string;
57
67
  export declare function getOllamaBaseUrl(): string;
58
68
  export declare function getOllamaCloudApiKey(): string;
59
69
  export declare function getPoktToken(): string;
60
- export declare const getControllerBaseUrl: () => string;
61
- /** Página inicial do Pokt Pro (aí tem o botão de assinatura/pagamento). */
70
+ /** Base da API só para provider `controller` (Bearer Pokt). OpenAI direto usa outro ramo no getClient. */
71
+ export declare function getPoktApiBaseUrl(): string;
72
+ /** Painel e links gerais (Railway), exceto compra de token — ver getTokenPurchaseUrl(). */
73
+ export declare function getProPortalBaseUrl(): string;
74
+ /** Somente comprar token / checkout — Vercel (Controller). Usado por `pokt pro`. */
75
+ export declare function getTokenPurchaseUrl(): string;
76
+ /** @deprecated Use getPoktApiBaseUrl() ou getProPortalBaseUrl() conforme o caso. */
77
+ export declare const getControllerBaseUrl: typeof getPoktApiBaseUrl;
78
+ /** URL aberta por `pokt pro` (comprar token) — Vercel por padrão. */
62
79
  export declare const getProPurchaseUrl: () => string;
63
80
  /** Prioridade: modelo ativo explícito → Pokt (controller) se token setado → OpenRouter → Gemini → Ollama Cloud → Ollama local */
64
81
  export declare function getEffectiveActiveModel(): ModelConfig | null;
package/dist/config.js CHANGED
@@ -10,7 +10,24 @@ export const PROVIDER_LABELS = {
10
10
  };
11
11
  /** Lista de todos os provedores disponíveis (único lugar para incluir novos no futuro) */
12
12
  export const ALL_PROVIDERS = Object.keys(PROVIDER_LABELS);
13
- const DEFAULT_CONTROLLER_URL = 'https://pokt-cli-controller.vercel.app';
13
+ /** Serviço Pokt na Railway: API (`/api/v1`), painel e tudo que substituiu o host antigo da Vercel. */
14
+ const DEFAULT_POKT_SERVICE_BASE_URL = 'https://poktcliback-production.up.railway.app';
15
+ /** Somente fluxo de compra de token (Stripe / “Torne-se Pro”) — permanece no Controller Vercel. */
16
+ const DEFAULT_TOKEN_PURCHASE_BASE_URL = 'https://pokt-cli-controller.vercel.app';
17
+ /** Host legado: configs antigas são migradas automaticamente para `DEFAULT_POKT_SERVICE_BASE_URL`. */
18
+ const LEGACY_VERCEL_POKT_HOST = 'pokt-cli-controller.vercel.app';
19
+ function normalizeBaseUrl(url) {
20
+ return url.trim().replace(/\/$/, '');
21
+ }
22
+ function isLegacyVercelPoktUrl(url) {
23
+ try {
24
+ const u = new URL(url.trim());
25
+ return u.hostname.toLowerCase() === LEGACY_VERCEL_POKT_HOST;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
14
31
  export const config = new Conf({
15
32
  projectName: 'pokt-cli',
16
33
  defaults: {
@@ -20,7 +37,9 @@ export const config = new Conf({
20
37
  geminiApiKey: '',
21
38
  ollamaBaseUrl: 'http://localhost:11434',
22
39
  ollamaCloudApiKey: '',
23
- controllerBaseUrl: DEFAULT_CONTROLLER_URL,
40
+ controllerBaseUrl: DEFAULT_POKT_SERVICE_BASE_URL,
41
+ poktApiBaseUrl: DEFAULT_POKT_SERVICE_BASE_URL,
42
+ tokenPurchaseBaseUrl: DEFAULT_TOKEN_PURCHASE_BASE_URL,
24
43
  poktToken: '',
25
44
  registeredModels: [
26
45
  { provider: 'controller', id: 'default' },
@@ -51,7 +70,26 @@ export const env = {
51
70
  ollamaBaseUrl: ['OLLAMA_BASE_URL'],
52
71
  ollamaCloudApiKey: ['OLLAMA_CLOUD_API_KEY'],
53
72
  poktToken: ['POKT_TOKEN'],
73
+ poktApiBaseUrl: ['POKT_API_BASE_URL'],
74
+ /** Painel e URLs gerais (Railway) */
75
+ proPortalUrl: ['POKT_PRO_PORTAL_URL', 'POKT_CONTROLLER_PORTAL_URL'],
76
+ /** Apenas página de compra de token (Vercel) */
77
+ tokenPurchaseUrl: ['POKT_TOKEN_PURCHASE_URL'],
54
78
  };
79
+ /**
80
+ * Migração: quem tinha a URL antiga da Vercel em API ou portal passa a usar a Railway.
81
+ * A URL de compra de token não é alterada aqui.
82
+ */
83
+ function migrateLegacyPoktUrls() {
84
+ const target = DEFAULT_POKT_SERVICE_BASE_URL;
85
+ for (const key of ['poktApiBaseUrl', 'controllerBaseUrl']) {
86
+ const val = config.get(key);
87
+ if (typeof val === 'string' && val.trim() !== '' && isLegacyVercelPoktUrl(val)) {
88
+ config.set(key, target);
89
+ }
90
+ }
91
+ }
92
+ migrateLegacyPoktUrls();
55
93
  export function getOpenAIApiKey() {
56
94
  return readEnvFirst(env.openaiApiKey) || config.get('openaiApiKey') || '';
57
95
  }
@@ -75,12 +113,28 @@ export function getOllamaCloudApiKey() {
75
113
  export function getPoktToken() {
76
114
  return readEnvFirst(env.poktToken) || config.get('poktToken') || '';
77
115
  }
78
- export const getControllerBaseUrl = () => {
79
- const url = config.get('controllerBaseUrl') || DEFAULT_CONTROLLER_URL;
80
- return url.replace(/\/$/, '');
81
- };
82
- /** Página inicial do Pokt Pro (aí tem o botão de assinatura/pagamento). */
83
- export const getProPurchaseUrl = () => getControllerBaseUrl();
116
+ /** Base da API só para provider `controller` (Bearer Pokt). OpenAI direto usa outro ramo no getClient. */
117
+ export function getPoktApiBaseUrl() {
118
+ const fromEnv = readEnvFirst(env.poktApiBaseUrl);
119
+ const url = fromEnv || config.get('poktApiBaseUrl') || DEFAULT_POKT_SERVICE_BASE_URL;
120
+ return normalizeBaseUrl(url);
121
+ }
122
+ /** Painel e links gerais (Railway), exceto compra de token — ver getTokenPurchaseUrl(). */
123
+ export function getProPortalBaseUrl() {
124
+ const fromEnv = readEnvFirst(env.proPortalUrl);
125
+ const url = fromEnv || config.get('controllerBaseUrl') || DEFAULT_POKT_SERVICE_BASE_URL;
126
+ return normalizeBaseUrl(url);
127
+ }
128
+ /** Somente comprar token / checkout — Vercel (Controller). Usado por `pokt pro`. */
129
+ export function getTokenPurchaseUrl() {
130
+ const fromEnv = readEnvFirst(env.tokenPurchaseUrl);
131
+ const url = fromEnv || config.get('tokenPurchaseBaseUrl') || DEFAULT_TOKEN_PURCHASE_BASE_URL;
132
+ return normalizeBaseUrl(url);
133
+ }
134
+ /** @deprecated Use getPoktApiBaseUrl() ou getProPortalBaseUrl() conforme o caso. */
135
+ export const getControllerBaseUrl = getPoktApiBaseUrl;
136
+ /** URL aberta por `pokt pro` (comprar token) — Vercel por padrão. */
137
+ export const getProPurchaseUrl = () => getTokenPurchaseUrl();
84
138
  /** Prioridade: modelo ativo explícito → Pokt (controller) se token setado → OpenRouter → Gemini → Ollama Cloud → Ollama local */
85
139
  export function getEffectiveActiveModel() {
86
140
  const explicit = config.get('activeModel');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pokt-cli",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Vibe Coding AI CLI for OpenRouter and Ollama",
5
5
  "main": "./dist/bin/pokt.js",
6
6
  "type": "module",