pokt-cli 1.0.3 → 1.0.4
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/dist/chat/loop.js +143 -7
- package/dist/commands/chat.js +1 -1
- package/dist/ui.js +1 -1
- package/package.json +1 -1
package/dist/chat/loop.js
CHANGED
|
@@ -13,12 +13,17 @@ CORE CAPABILITIES:
|
|
|
13
13
|
2. **Autonomous Coding**: You can create new files, rewrite existing ones, and run terminal commands.
|
|
14
14
|
3. **Problem Solving**: You analyze errors and propose/apply fixes.
|
|
15
15
|
|
|
16
|
+
CRITICAL - FILE CREATION/EDITS (this API does NOT support tool calls):
|
|
17
|
+
- Do NOT reply with only "We will call read_file", "We will call write_file" or similar. Those tools will NOT run. The user will get no file.
|
|
18
|
+
- You MUST output the complete file content in a markdown code block so the CLI can create/edit the file. Format: mention the filename (e.g. hello.py or **hello.py**) then a newline then \`\`\`python then newline then the full file content then \`\`\`.
|
|
19
|
+
- For edits: first "read" the file by inferring its content from the user request and project context, then output the full updated file in a \`\`\`python (or correct language) block with the filename mentioned just above the block.
|
|
20
|
+
- Never end your response with only an intention to call a tool. Always include the actual code in a block.
|
|
21
|
+
|
|
16
22
|
GUIDELINES:
|
|
17
23
|
- 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.
|
|
18
24
|
- When asked to fix something, first **read** the relevant files to understand the context.
|
|
19
|
-
- When creating a project, start by planning the structure, then use \`write_file\` to create
|
|
20
|
-
-
|
|
21
|
-
- You have full access to the current terminal. You can run \`npm install\`, \`tsc\`, or any other command.
|
|
25
|
+
- When creating a project, start by planning the structure, then use \`write_file\` to create each file.
|
|
26
|
+
- You have full access to the current terminal. You can run \`run_command\` for \`npm install\`, \`tsc\`, or any other command.
|
|
22
27
|
- Be extremely concise in your explanations.
|
|
23
28
|
- The current working directory is: ${process.cwd()}
|
|
24
29
|
`;
|
|
@@ -58,6 +63,8 @@ export async function startChatLoop(modelConfig) {
|
|
|
58
63
|
];
|
|
59
64
|
while (true) {
|
|
60
65
|
console.log('');
|
|
66
|
+
const cwd = process.cwd();
|
|
67
|
+
console.log(ui.dim(`Diretório atual: ${cwd}`));
|
|
61
68
|
console.log(ui.shortcutsLine('shift+tab to accept edits', '? for shortcuts'));
|
|
62
69
|
const response = await prompts({
|
|
63
70
|
type: 'text',
|
|
@@ -87,6 +94,83 @@ export async function startChatLoop(modelConfig) {
|
|
|
87
94
|
}
|
|
88
95
|
const MAX_429_RETRIES = 3;
|
|
89
96
|
const BASE_429_DELAY_MS = 5000;
|
|
97
|
+
/** Extensões que consideramos como arquivos de código para aplicar fallback */
|
|
98
|
+
const CODE_EXT = /\.(py|js|ts|tsx|jsx|html|css|json|md|txt|java|go|rs|c|cpp|rb|php)$/i;
|
|
99
|
+
/**
|
|
100
|
+
* Quando a API não retorna tool_calls, alguns backends só devolvem texto.
|
|
101
|
+
* Extrai blocos de código da resposta (```lang\n...\n```) e, se encontrar
|
|
102
|
+
* um nome de arquivo mencionado antes do bloco, aplica write_file.
|
|
103
|
+
*/
|
|
104
|
+
/** Blocos de comando "como rodar" (bash/sh de 1–2 linhas) não viram arquivo para não poluir. */
|
|
105
|
+
function isRunCommandOnly(lang, code) {
|
|
106
|
+
const shellLike = /^(bash|sh|shell|zsh)$/i.test(lang);
|
|
107
|
+
const lines = code.split('\n').filter((l) => l.trim().length > 0);
|
|
108
|
+
return shellLike && lines.length <= 2;
|
|
109
|
+
}
|
|
110
|
+
/** Remove markdown/formatting do nome de arquivo (ex: **hello.py** → hello.py). */
|
|
111
|
+
function cleanFilename(candidate) {
|
|
112
|
+
return candidate.replace(/^[\s*`'"]+/g, '').replace(/[\s*`'")\]\s]+$/g, '').trim();
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Aplica blocos de código da resposta e retorna conteúdo para exibição (sem repetir o código).
|
|
116
|
+
* - Detecta nome de arquivo na mensagem (ex: "updated **hello.py**") para editar o existente.
|
|
117
|
+
* - Blocos já aplicados são substituídos por "[Código aplicado ao arquivo: path]" na mensagem exibida.
|
|
118
|
+
*/
|
|
119
|
+
/** Garante que o conteúdo da mensagem seja string (algumas APIs devolvem array). */
|
|
120
|
+
function messageContentToString(content) {
|
|
121
|
+
if (typeof content === 'string')
|
|
122
|
+
return content;
|
|
123
|
+
if (Array.isArray(content)) {
|
|
124
|
+
return content
|
|
125
|
+
.map((part) => (typeof part === 'object' && part != null && 'text' in part ? part.text : String(part)))
|
|
126
|
+
.join('');
|
|
127
|
+
}
|
|
128
|
+
return content != null ? String(content) : '';
|
|
129
|
+
}
|
|
130
|
+
async function applyCodeBlocksFromContent(content) {
|
|
131
|
+
const codeBlockRe = /```(\w*)\n([\s\S]*?)```/g;
|
|
132
|
+
const appliedBlocks = [];
|
|
133
|
+
let applied = false;
|
|
134
|
+
let m;
|
|
135
|
+
const matches = [];
|
|
136
|
+
while ((m = codeBlockRe.exec(content)) !== null) {
|
|
137
|
+
matches.push({
|
|
138
|
+
fullMatch: m[0],
|
|
139
|
+
index: m.index,
|
|
140
|
+
lang: m[1] || '',
|
|
141
|
+
code: m[2].replace(/\r\n/g, '\n').trimEnd(),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
for (const { fullMatch, index, lang, code } of matches) {
|
|
145
|
+
if (isRunCommandOnly(lang, code))
|
|
146
|
+
continue;
|
|
147
|
+
const beforeBlock = content.substring(0, index);
|
|
148
|
+
// Nome de arquivo: aceita "**hello.py**", "hello.py" antes de espaço/newline/backtick, etc.
|
|
149
|
+
const fileMatch = beforeBlock.match(/(\S+\.(?:py|js|ts|tsx|jsx|html|css|json|md|txt|java|go|rs|c|cpp|rb|php))(?=\s|$|[:.)\]*`"])/gi);
|
|
150
|
+
const rawCandidate = fileMatch ? fileMatch[fileMatch.length - 1].trim() : null;
|
|
151
|
+
const candidate = rawCandidate ? cleanFilename(rawCandidate) : null;
|
|
152
|
+
const path = candidate && CODE_EXT.test(candidate) ? candidate : (lang === 'python' ? 'generated.py' : lang ? `generated.${lang}` : null);
|
|
153
|
+
if (path && code) {
|
|
154
|
+
try {
|
|
155
|
+
console.log(ui.warn(`\n[Fallback] Aplicando código da resposta ao arquivo: ${path}`));
|
|
156
|
+
await executeTool('write_file', JSON.stringify({ path, content: code }));
|
|
157
|
+
applied = true;
|
|
158
|
+
appliedBlocks.push({ start: index, end: index + fullMatch.length, path });
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// ignora falha em um bloco
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Monta mensagem para exibição: blocos aplicados viram uma linha curta (evita repetir o código)
|
|
166
|
+
let displayContent = content;
|
|
167
|
+
for (let i = appliedBlocks.length - 1; i >= 0; i--) {
|
|
168
|
+
const { start, end, path } = appliedBlocks[i];
|
|
169
|
+
const placeholder = `\n${ui.dim('[Código aplicado ao arquivo: ' + path + ']')}\n`;
|
|
170
|
+
displayContent = displayContent.substring(0, start) + placeholder + displayContent.substring(end);
|
|
171
|
+
}
|
|
172
|
+
return { applied, displayContent };
|
|
173
|
+
}
|
|
90
174
|
async function createCompletionWithRetry(client, modelId, messages, toolsList) {
|
|
91
175
|
let lastError;
|
|
92
176
|
for (let attempt = 0; attempt <= MAX_429_RETRIES; attempt++) {
|
|
@@ -117,10 +201,13 @@ async function processLLMResponse(client, modelId, messages, toolsList) {
|
|
|
117
201
|
let completion = await createCompletionWithRetry(client, modelId, messages, toolsList);
|
|
118
202
|
let message = completion.choices[0].message;
|
|
119
203
|
spinner.stop();
|
|
204
|
+
let writeFileExecutedThisTurn = false;
|
|
120
205
|
while (message.tool_calls && message.tool_calls.length > 0) {
|
|
121
206
|
messages.push(message);
|
|
122
207
|
for (const toolCall of message.tool_calls) {
|
|
123
208
|
const name = toolCall.function.name;
|
|
209
|
+
if (name === 'write_file')
|
|
210
|
+
writeFileExecutedThisTurn = true;
|
|
124
211
|
const args = toolCall.function.arguments ?? '{}';
|
|
125
212
|
console.log(ui.warn(`\n[Executing Tool: ${name}]`));
|
|
126
213
|
console.log(ui.dim(`Arguments: ${args}`));
|
|
@@ -141,10 +228,59 @@ async function processLLMResponse(client, modelId, messages, toolsList) {
|
|
|
141
228
|
message = completion.choices[0].message;
|
|
142
229
|
spinner.stop();
|
|
143
230
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
231
|
+
const rawContent = message.content;
|
|
232
|
+
let contentStr = messageContentToString(rawContent);
|
|
233
|
+
let finalContent = rawContent ?? contentStr;
|
|
234
|
+
// Quando a API não executa tools, tentar aplicar blocos de código da resposta
|
|
235
|
+
if (!writeFileExecutedThisTurn) {
|
|
236
|
+
let result = await applyCodeBlocksFromContent(contentStr);
|
|
237
|
+
// 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
|
|
238
|
+
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)
|
|
239
|
+
|| (/call\s+(read_file|write_file)/i.test(contentStr) && contentStr.length < 400);
|
|
240
|
+
if (!result.applied && looksLikeToolIntentOnly) {
|
|
241
|
+
messages.push({ role: 'assistant', content: rawContent ?? contentStr });
|
|
242
|
+
const followUpSystem = `This API does not support tool calls. You must NOT reply with "We will call X". Output the complete file content in a markdown code block so the user's CLI can create/edit the file. Format: mention the filename (e.g. hello.py) then newline then \`\`\`python then newline then the FULL file content then \`\`\`. Do that now for the user's last request.`;
|
|
243
|
+
messages.push({ role: 'system', content: followUpSystem });
|
|
244
|
+
spinner.start('Getting code...');
|
|
245
|
+
const followUp = await createCompletionWithRetry(client, modelId, messages, toolsList);
|
|
246
|
+
spinner.stop();
|
|
247
|
+
const followUpMsg = followUp.choices[0].message;
|
|
248
|
+
const followUpStr = messageContentToString(followUpMsg.content);
|
|
249
|
+
if (followUpStr.trim() !== '') {
|
|
250
|
+
result = await applyCodeBlocksFromContent(followUpStr);
|
|
251
|
+
contentStr = followUpStr;
|
|
252
|
+
finalContent = followUpMsg.content ?? followUpStr;
|
|
253
|
+
messages.push({ role: 'assistant', content: finalContent });
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
messages.push({ role: 'assistant', content: '' });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (contentStr.trim() !== '') {
|
|
260
|
+
let contentToShow = result.applied ? result.displayContent : contentStr;
|
|
261
|
+
console.log('\n' + ui.labelPokt());
|
|
262
|
+
console.log(contentToShow);
|
|
263
|
+
if (!messages.some((m) => m.role === 'assistant' && m.content === finalContent)) {
|
|
264
|
+
messages.push({ role: 'assistant', content: finalContent });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
console.log('\n' + ui.labelPokt());
|
|
269
|
+
console.log(ui.dim('(A IA não retornou código utilizável. Tente reformular o pedido.)'));
|
|
270
|
+
messages.push({ role: 'assistant', content: '' });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
if (contentStr.trim() !== '') {
|
|
275
|
+
console.log('\n' + ui.labelPokt());
|
|
276
|
+
console.log(contentStr);
|
|
277
|
+
messages.push({ role: 'assistant', content: finalContent });
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
console.log('\n' + ui.labelPokt());
|
|
281
|
+
console.log(ui.dim('(Sem resposta de texto.)'));
|
|
282
|
+
messages.push({ role: 'assistant', content: '' });
|
|
283
|
+
}
|
|
148
284
|
}
|
|
149
285
|
}
|
|
150
286
|
catch (error) {
|
package/dist/commands/chat.js
CHANGED
|
@@ -34,7 +34,7 @@ export const chatCommand = {
|
|
|
34
34
|
console.log('');
|
|
35
35
|
}
|
|
36
36
|
console.log(ui.dim('Type "exit" or /quit to end the session.'));
|
|
37
|
-
console.log(ui.statusBar({ model: `/model ${activeModel.provider} (${activeModel.id})` }));
|
|
37
|
+
console.log(ui.statusBar({ cwd: process.cwd(), model: `/model ${activeModel.provider} (${activeModel.id})` }));
|
|
38
38
|
console.log('');
|
|
39
39
|
await startChatLoop(activeModel);
|
|
40
40
|
}
|
package/dist/ui.js
CHANGED
|
@@ -2,7 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
|
-
const VERSION = '1.0.
|
|
5
|
+
const VERSION = '1.0.4';
|
|
6
6
|
/** Logo em estilo chevron com gradiente (azul → rosa → roxo) */
|
|
7
7
|
function logo() {
|
|
8
8
|
const c = (s, color) => color(s);
|