pokt-cli 1.0.7 → 1.0.9

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.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Remove blocos ```bash``` que são só invocações MCP (às vezes `mcp_*` numa linha e JSON na seguinte).
3
+ */
4
+ export declare function stripExecutedStyleMcpBashBlocks(content: string): string;
5
+ /** Uma linha tipo: mcp_Neon_run_sql "SELECT 1" ou mcp_Neon_list_projects */
6
+ export declare function parseMcpShellLine(line: string): {
7
+ tool: string;
8
+ args: string;
9
+ } | null;
10
+ export declare function runMcpFromBashMarkdown(content: string, options?: {
11
+ skipDuplicateAppendix?: boolean;
12
+ }): Promise<{
13
+ invocationCount: number;
14
+ executedCount: number;
15
+ augmentedAssistantText: string;
16
+ }>;
17
+ /**
18
+ * Quando o modelo devolve vazio e o pedido parece “listar bancos”, executa mcp_*_run_sql uma vez.
19
+ */
20
+ export declare function tryAutoMcpForListDatabases(messages: Array<{
21
+ role: string;
22
+ content?: unknown;
23
+ }>): Promise<string | null>;
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Fallback: alguns modelos emitem invocações MCP em blocos ```bash``` em vez de tool_calls.
3
+ * Extraímos linhas `mcp_* ...` e executamos contra os servidores MCP conectados.
4
+ */
5
+ import { ui } from '../ui.js';
6
+ import { callMcpTool, getMcpExposedToolNames, getMcpToolParameterKeys } from '../mcp/client.js';
7
+ const BASH_BLOCK_RE = /```(?:bash|sh|shell|zsh|powershell|ps1|cmd)\s*\n([\s\S]*?)```/gi;
8
+ function mcpBashFallbackVerbose() {
9
+ return process.env.POKT_VERBOSE_MCP === '1' || process.env.POKT_VERBOSE === '1';
10
+ }
11
+ /**
12
+ * Remove blocos ```bash``` que são só invocações MCP (às vezes `mcp_*` numa linha e JSON na seguinte).
13
+ */
14
+ export function stripExecutedStyleMcpBashBlocks(content) {
15
+ const re = new RegExp(BASH_BLOCK_RE.source, BASH_BLOCK_RE.flags);
16
+ return content.replace(re, (full, body) => {
17
+ if (!/mcp_[A-Za-z0-9_-]+/i.test(body))
18
+ return full;
19
+ const lower = body.toLowerCase();
20
+ const shellish = /\b(if |then|\bfi\b|\bfor\b|\bdo\b|\bdone\b|\bwhile\b|curl |wget |\bgit\b|\bnpm\b|\bpnpm\b|\bcd |\bexport |\$\{)/.test(lower);
21
+ if (shellish)
22
+ return full;
23
+ const lines = body
24
+ .split('\n')
25
+ .map((l) => l.trim())
26
+ .filter(Boolean);
27
+ if (lines.length === 0)
28
+ return full;
29
+ const mcpOrJsonLine = (l) => {
30
+ if (parseMcpShellLine(l) !== null)
31
+ return true;
32
+ const t = l.trim();
33
+ if (t.startsWith('{') && t.endsWith('}'))
34
+ return true;
35
+ if (t.startsWith('[') && t.endsWith(']'))
36
+ return true;
37
+ return false;
38
+ };
39
+ if (lines.every(mcpOrJsonLine))
40
+ return '\n';
41
+ return full;
42
+ });
43
+ }
44
+ function trunc(s, max) {
45
+ if (s.length <= max)
46
+ return s;
47
+ return `${s.slice(0, max)}\n\n… (${s.length - max} caracteres omitidos)`;
48
+ }
49
+ /** Uma linha tipo: mcp_Neon_run_sql "SELECT 1" ou mcp_Neon_list_projects */
50
+ export function parseMcpShellLine(line) {
51
+ const m = line.trim().match(/^(mcp_[A-Za-z0-9_-]+)(?:\s+(.*))?$/);
52
+ if (!m)
53
+ return null;
54
+ return { tool: m[1], args: m[2] ?? '' };
55
+ }
56
+ function extractStringArg(raw) {
57
+ const t = raw.trim();
58
+ if (!t)
59
+ return null;
60
+ if (t.startsWith('"') && t.endsWith('"') && t.length >= 2) {
61
+ return t.slice(1, -1).replace(/\\"/g, '"');
62
+ }
63
+ if (t.startsWith("'") && t.endsWith("'") && t.length >= 2) {
64
+ return t.slice(1, -1).replace(/\\'/g, "'");
65
+ }
66
+ if (/^\s*(SELECT|WITH|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|TRUNCATE|SHOW)\b/i.test(t)) {
67
+ return t;
68
+ }
69
+ if (/^[a-zA-Z0-9_.-]+$/.test(t))
70
+ return t;
71
+ return t;
72
+ }
73
+ /** neonctl / modelo: --project-id "x" --branch-id "y" --sql "SELECT 1" */
74
+ function neonFlagNameToJsonKey(flagRaw) {
75
+ const f = flagRaw.toLowerCase();
76
+ if (f === 'project-id')
77
+ return 'projectId';
78
+ if (f === 'branch-id')
79
+ return 'branchId';
80
+ if (f === 'database-name' || f === 'database')
81
+ return 'databaseName';
82
+ return flagRaw.replace(/-([a-z])/gi, (_, c) => c.toUpperCase());
83
+ }
84
+ function readQuotedSegment(s, pos) {
85
+ const q = s[pos];
86
+ if (q !== '"' && q !== "'")
87
+ return null;
88
+ let j = pos + 1;
89
+ let buf = '';
90
+ while (j < s.length) {
91
+ if (s[j] === '\\' && j + 1 < s.length) {
92
+ buf += s[j + 1];
93
+ j += 2;
94
+ continue;
95
+ }
96
+ if (s[j] === q)
97
+ return { value: buf, next: j + 1 };
98
+ buf += s[j];
99
+ j++;
100
+ }
101
+ return { value: buf, next: j };
102
+ }
103
+ /**
104
+ * Extrai flags estilo CLI a partir de uma string (linha bash ou valor errado em `sql`).
105
+ */
106
+ function parseNeonCtlStyleFlags(argsRaw) {
107
+ const s = argsRaw.trim();
108
+ if (!s || !/--[a-zA-Z]/.test(s))
109
+ return null;
110
+ const out = {};
111
+ let i = 0;
112
+ while (i < s.length) {
113
+ while (i < s.length && /\s/.test(s[i]))
114
+ i++;
115
+ if (i >= s.length)
116
+ break;
117
+ if (!s.startsWith('--', i))
118
+ break;
119
+ i += 2;
120
+ const nameStart = i;
121
+ while (i < s.length && /[a-zA-Z0-9-]/.test(s[i]))
122
+ i++;
123
+ const flagRaw = s.slice(nameStart, i);
124
+ if (!flagRaw)
125
+ break;
126
+ while (i < s.length && /\s/.test(s[i]))
127
+ i++;
128
+ let value = '';
129
+ if (i < s.length && s[i] === '=') {
130
+ i++;
131
+ while (i < s.length && /\s/.test(s[i]))
132
+ i++;
133
+ }
134
+ if (i >= s.length) {
135
+ out[neonFlagNameToJsonKey(flagRaw)] = value;
136
+ break;
137
+ }
138
+ const quoted = readQuotedSegment(s, i);
139
+ if (quoted) {
140
+ value = quoted.value;
141
+ i = quoted.next;
142
+ }
143
+ else {
144
+ const us = i;
145
+ while (i < s.length) {
146
+ if (/\s/.test(s[i]))
147
+ break;
148
+ if (s.startsWith('--', i))
149
+ break;
150
+ i++;
151
+ }
152
+ value = s.slice(us, i);
153
+ }
154
+ out[neonFlagNameToJsonKey(flagRaw)] = value;
155
+ }
156
+ return Object.keys(out).length > 0 ? out : null;
157
+ }
158
+ function alignNeonRunSqlArgsToSchema(parsed, schemaKeys) {
159
+ const keys = new Set(schemaKeys);
160
+ const out = {};
161
+ const trySet = (schemaKey, val) => {
162
+ if (!val || val.trim() === '')
163
+ return;
164
+ if (keys.has(schemaKey))
165
+ out[schemaKey] = val;
166
+ };
167
+ trySet('projectId', parsed.projectId);
168
+ trySet('project_id', parsed.projectId);
169
+ trySet('branchId', parsed.branchId);
170
+ trySet('branch_id', parsed.branchId);
171
+ trySet('sql', parsed.sql);
172
+ trySet('query', parsed.query);
173
+ trySet('databaseName', parsed.databaseName);
174
+ trySet('database', parsed.databaseName);
175
+ return out;
176
+ }
177
+ function fixRunSqlJsonObject(resolvedTool, jo) {
178
+ if (!/_run_sql$/i.test(resolvedTool))
179
+ return null;
180
+ const sqlField = jo.sql;
181
+ if (typeof sqlField !== 'string' || !/--(project-id|branch-id)\b/.test(sqlField))
182
+ return null;
183
+ const merged = parseNeonCtlStyleFlags(sqlField.trim());
184
+ if (!merged || !merged.sql?.trim())
185
+ return null;
186
+ const schemaKeys = getMcpToolParameterKeys(resolvedTool);
187
+ const keySet = new Set(schemaKeys);
188
+ let aligned = alignNeonRunSqlArgsToSchema(merged, schemaKeys);
189
+ for (const [k, v] of Object.entries(jo)) {
190
+ if (k === 'sql')
191
+ continue;
192
+ if (typeof v === 'string' && v.trim() && keySet.has(k)) {
193
+ aligned = { ...aligned, [k]: v };
194
+ }
195
+ }
196
+ return Object.keys(aligned).length > 0 ? aligned : null;
197
+ }
198
+ function buildJsonArgsString(resolvedTool, argsRaw) {
199
+ const t = argsRaw.trim();
200
+ if (!t)
201
+ return '{}';
202
+ try {
203
+ const j = JSON.parse(t);
204
+ if (j !== null && typeof j === 'object' && !Array.isArray(j)) {
205
+ const jo = j;
206
+ const fixedObj = fixRunSqlJsonObject(resolvedTool, jo);
207
+ if (fixedObj)
208
+ return JSON.stringify(fixedObj);
209
+ return JSON.stringify(j);
210
+ }
211
+ }
212
+ catch {
213
+ // continua
214
+ }
215
+ if (/_run_sql$/i.test(resolvedTool)) {
216
+ const merged = parseNeonCtlStyleFlags(t);
217
+ if (merged) {
218
+ const keys = getMcpToolParameterKeys(resolvedTool);
219
+ const aligned = alignNeonRunSqlArgsToSchema(merged, keys);
220
+ if (Object.keys(aligned).length > 0)
221
+ return JSON.stringify(aligned);
222
+ }
223
+ }
224
+ const keys = getMcpToolParameterKeys(resolvedTool);
225
+ const unq = extractStringArg(t);
226
+ if (unq !== null) {
227
+ if (keys.includes('sql'))
228
+ return JSON.stringify({ sql: unq });
229
+ if (keys.includes('query'))
230
+ return JSON.stringify({ query: unq });
231
+ if (keys.includes('project_id'))
232
+ return JSON.stringify({ project_id: unq });
233
+ if (keys.includes('projectId'))
234
+ return JSON.stringify({ projectId: unq });
235
+ if (keys.length === 1)
236
+ return JSON.stringify({ [keys[0]]: unq });
237
+ }
238
+ return '{}';
239
+ }
240
+ function resolveToolName(requested, argsRaw, registered) {
241
+ if (registered.has(requested))
242
+ return requested;
243
+ const underscored = requested.replace(/-/g, '_');
244
+ if (registered.has(underscored))
245
+ return underscored;
246
+ const rl = requested.toLowerCase();
247
+ for (const n of registered) {
248
+ if (n.toLowerCase() === rl)
249
+ return n;
250
+ if (n.replace(/-/g, '_').toLowerCase() === underscored.toLowerCase())
251
+ return n;
252
+ }
253
+ const arg = extractStringArg(argsRaw);
254
+ const looksSql = arg !== null && /^\s*(SELECT|WITH|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|TRUNCATE|SHOW)\b/i.test(arg);
255
+ if (looksSql || /neon.*postgres.*execute|postgres.*execute/i.test(requested)) {
256
+ const runSqls = [...registered].filter((n) => /_run_sql$/i.test(n));
257
+ if (runSqls.length === 1)
258
+ return runSqls[0];
259
+ if (runSqls.length > 1) {
260
+ const neon = runSqls.find((n) => /neon/i.test(n));
261
+ return neon ?? runSqls[0];
262
+ }
263
+ }
264
+ return null;
265
+ }
266
+ function collectInvocationsFromMarkdown(content) {
267
+ if (getMcpExposedToolNames().length === 0)
268
+ return [];
269
+ const out = [];
270
+ const seen = new Set();
271
+ let m;
272
+ const re = new RegExp(BASH_BLOCK_RE.source, BASH_BLOCK_RE.flags);
273
+ while ((m = re.exec(content)) !== null) {
274
+ const body = m[1] ?? '';
275
+ for (const line of body.split('\n')) {
276
+ const parsed = parseMcpShellLine(line);
277
+ if (!parsed)
278
+ continue;
279
+ const key = `${parsed.tool}|${parsed.args}`;
280
+ if (seen.has(key))
281
+ continue;
282
+ seen.add(key);
283
+ out.push(parsed);
284
+ }
285
+ }
286
+ return out;
287
+ }
288
+ export async function runMcpFromBashMarkdown(content, options) {
289
+ const invocations = collectInvocationsFromMarkdown(content);
290
+ if (invocations.length === 0) {
291
+ return { invocationCount: 0, executedCount: 0, augmentedAssistantText: content };
292
+ }
293
+ const registered = new Set(getMcpExposedToolNames());
294
+ const sections = [];
295
+ let executedCount = 0;
296
+ const verbose = mcpBashFallbackVerbose();
297
+ for (const inv of invocations) {
298
+ const resolved = resolveToolName(inv.tool, inv.args, registered);
299
+ if (!resolved) {
300
+ sections.push(`\n### ${inv.tool}\n_Ferramenta não encontrada nos MCPs conectados._`);
301
+ continue;
302
+ }
303
+ const jsonArgs = buildJsonArgsString(resolved, inv.args);
304
+ if (verbose) {
305
+ console.log(ui.warn(`\n[MCP · texto/bash] ${resolved}`));
306
+ console.log(ui.dim(jsonArgs.length > 220 ? `${jsonArgs.slice(0, 220)}…` : jsonArgs));
307
+ }
308
+ else {
309
+ console.log(ui.dim(`[MCP] ${resolved} (fallback bash)`));
310
+ }
311
+ const result = await callMcpTool(resolved, jsonArgs);
312
+ executedCount++;
313
+ sections.push(`\n### ${resolved}\n\`\`\`text\n${trunc(result, 12000)}\n\`\`\``);
314
+ }
315
+ const appendixBody = sections.join('\n');
316
+ const alreadyHasMcpSection = options?.skipDuplicateAppendix === true &&
317
+ /\n##\s+Resultados\s+MCP\b/i.test(content);
318
+ const appendix = alreadyHasMcpSection
319
+ ? ''
320
+ : '\n\n---\n## Resultados MCP (fallback bash — Pokt CLI)\n' + appendixBody;
321
+ return {
322
+ invocationCount: invocations.length,
323
+ executedCount,
324
+ augmentedAssistantText: content + appendix,
325
+ };
326
+ }
327
+ function lastUserPlainText(messages) {
328
+ for (let i = messages.length - 1; i >= 0; i--) {
329
+ const m = messages[i];
330
+ if (m.role !== 'user')
331
+ continue;
332
+ const c = m.content;
333
+ if (typeof c === 'string')
334
+ return c;
335
+ if (Array.isArray(c)) {
336
+ return c
337
+ .map((part) => typeof part === 'object' && part != null && 'text' in part ? part.text : String(part))
338
+ .join('');
339
+ }
340
+ }
341
+ return '';
342
+ }
343
+ /**
344
+ * Quando o modelo devolve vazio e o pedido parece “listar bancos”, executa mcp_*_run_sql uma vez.
345
+ */
346
+ export async function tryAutoMcpForListDatabases(messages) {
347
+ const text = lastUserPlainText(messages);
348
+ if (!/\b(bancos?\s+de\s+dados|banco\s+de\s+dados|listar.*bancos?|quais\s+bancos|databases?\b|postgresql|postgres|neon|\bmcp\b)\b/i.test(text)) {
349
+ return null;
350
+ }
351
+ const runSqls = getMcpExposedToolNames().filter((n) => /_run_sql$/i.test(n));
352
+ if (runSqls.length === 0)
353
+ return null;
354
+ const tool = runSqls.find((n) => /neon/i.test(n)) ?? runSqls[0];
355
+ const sql = 'SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY 1;';
356
+ if (mcpBashFallbackVerbose()) {
357
+ console.log(ui.warn(`\n[MCP · automático] ${tool} — listando bancos (modelo sem resposta útil)`));
358
+ }
359
+ else {
360
+ console.log(ui.dim(`[MCP] ${tool} (automático — listar bancos)`));
361
+ }
362
+ const out = await callMcpTool(tool, JSON.stringify({ sql }));
363
+ return `## Bancos de dados (Postgres)\n\n**Ferramenta:** \`${tool}\`\n\n\`\`\`text\n${trunc(out, 12000)}\n\`\`\`\n\n_A automação do Pokt executou esta consulta porque o pedido parecia listar bancos e o modelo não retornou conteúdo._`;
364
+ }
@@ -0,0 +1,18 @@
1
+ import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions/completions.js';
2
+ type PersistedMessage = {
3
+ role: 'system' | 'user' | 'assistant' | 'tool';
4
+ content: string;
5
+ };
6
+ export declare function getProjectHash(cwd?: string): string;
7
+ export declare function getSessionsDir(): string;
8
+ export declare function saveAuto(messages: ChatCompletionMessageParam[]): void;
9
+ export declare function loadAuto(): PersistedMessage[] | null;
10
+ export declare function listCheckpoints(): Array<{
11
+ tag: string;
12
+ updatedAt?: string;
13
+ }>;
14
+ export declare function saveCheckpoint(tag: string, messages: ChatCompletionMessageParam[]): void;
15
+ export declare function loadCheckpoint(tag: string): PersistedMessage[];
16
+ export declare function deleteCheckpoint(tag: string): void;
17
+ export declare function exportConversation(filename: string, messages: ChatCompletionMessageParam[]): string;
18
+ export {};
@@ -0,0 +1,120 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+ function toPersisted(messages) {
6
+ return messages
7
+ .filter((m) => !!m && typeof m === 'object' && 'role' in m)
8
+ .map((m) => {
9
+ const role = m.role;
10
+ const content = m.content;
11
+ if (typeof content === 'string')
12
+ return { role, content };
13
+ if (Array.isArray(content)) {
14
+ const text = content
15
+ .map((part) => (part && typeof part === 'object' && 'text' in part ? String(part.text) : String(part)))
16
+ .join('');
17
+ return { role, content: text };
18
+ }
19
+ return { role, content: content != null ? String(content) : '' };
20
+ });
21
+ }
22
+ function ensureDir(p) {
23
+ fs.mkdirSync(p, { recursive: true });
24
+ }
25
+ function safeTag(tag) {
26
+ return tag.trim().replace(/[<>:"/\\|?*\x00-\x1F]/g, '_').slice(0, 80);
27
+ }
28
+ export function getProjectHash(cwd = process.cwd()) {
29
+ const h = crypto.createHash('sha1');
30
+ h.update(path.resolve(cwd));
31
+ return h.digest('hex').slice(0, 12);
32
+ }
33
+ export function getSessionsDir() {
34
+ // compatível Windows/mac/linux
35
+ const base = path.join(os.homedir(), '.pokt', 'tmp');
36
+ const proj = getProjectHash();
37
+ const dir = path.join(base, proj);
38
+ ensureDir(dir);
39
+ return dir;
40
+ }
41
+ function getAutoPath() {
42
+ return path.join(getSessionsDir(), 'auto.json');
43
+ }
44
+ export function saveAuto(messages) {
45
+ const payload = {
46
+ updatedAt: new Date().toISOString(),
47
+ cwd: process.cwd(),
48
+ messages: toPersisted(messages),
49
+ };
50
+ fs.writeFileSync(getAutoPath(), JSON.stringify(payload, null, 2), 'utf8');
51
+ }
52
+ export function loadAuto() {
53
+ try {
54
+ const raw = fs.readFileSync(getAutoPath(), 'utf8');
55
+ const parsed = JSON.parse(raw);
56
+ return Array.isArray(parsed?.messages) ? parsed.messages : null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ function tagPath(tag) {
63
+ return path.join(getSessionsDir(), `checkpoint.${safeTag(tag)}.json`);
64
+ }
65
+ export function listCheckpoints() {
66
+ const dir = getSessionsDir();
67
+ const files = fs.readdirSync(dir).filter((f) => f.startsWith('checkpoint.') && f.endsWith('.json'));
68
+ return files
69
+ .map((f) => {
70
+ const tag = f.replace(/^checkpoint\./, '').replace(/\.json$/, '');
71
+ try {
72
+ const raw = fs.readFileSync(path.join(dir, f), 'utf8');
73
+ const parsed = JSON.parse(raw);
74
+ return { tag, updatedAt: parsed.updatedAt };
75
+ }
76
+ catch {
77
+ return { tag };
78
+ }
79
+ })
80
+ .sort((a, b) => String(b.updatedAt ?? '').localeCompare(String(a.updatedAt ?? '')));
81
+ }
82
+ export function saveCheckpoint(tag, messages) {
83
+ const t = safeTag(tag);
84
+ if (!t)
85
+ throw new Error('Tag inválida.');
86
+ const payload = {
87
+ tag: t,
88
+ updatedAt: new Date().toISOString(),
89
+ cwd: process.cwd(),
90
+ messages: toPersisted(messages),
91
+ };
92
+ fs.writeFileSync(tagPath(t), JSON.stringify(payload, null, 2), 'utf8');
93
+ }
94
+ export function loadCheckpoint(tag) {
95
+ const t = safeTag(tag);
96
+ const raw = fs.readFileSync(tagPath(t), 'utf8');
97
+ const parsed = JSON.parse(raw);
98
+ if (!Array.isArray(parsed?.messages))
99
+ throw new Error('Checkpoint inválido.');
100
+ return parsed.messages;
101
+ }
102
+ export function deleteCheckpoint(tag) {
103
+ const t = safeTag(tag);
104
+ fs.unlinkSync(tagPath(t));
105
+ }
106
+ export function exportConversation(filename, messages) {
107
+ const out = path.isAbsolute(filename) ? filename : path.join(process.cwd(), filename);
108
+ const ext = path.extname(out).toLowerCase();
109
+ if (ext === '.json') {
110
+ fs.writeFileSync(out, JSON.stringify({ exportedAt: new Date().toISOString(), messages: toPersisted(messages) }, null, 2), 'utf8');
111
+ return out;
112
+ }
113
+ // markdown
114
+ const md = toPersisted(messages)
115
+ .filter((m) => m.role !== 'system')
116
+ .map((m) => `## ${m.role}\n\n${m.content}\n`)
117
+ .join('\n');
118
+ fs.writeFileSync(out, `# Pokt CLI chat export\n\nExportado em: ${new Date().toISOString()}\n\n${md}`, 'utf8');
119
+ return out;
120
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Reduz o tamanho do array `tools` enviado à API (ex.: Pokt Controller com body limit baixo).
3
+ * Schemas MCP (Neon etc.) podem ter dezenas de KB por tool.
4
+ */
5
+ import type { ChatCompletionTool } from 'openai/resources/chat/completions/completions.js';
6
+ export declare function slimToolsForUpstreamPayload(tools: ChatCompletionTool[]): ChatCompletionTool[];
@@ -0,0 +1,65 @@
1
+ const MAX_FUNCTION_DESCRIPTION_CHARS = 520;
2
+ const MAX_SCHEMA_STRING_CHARS = 480;
3
+ const MAX_TOOL_PARAMETERS_JSON_CHARS = 22_000;
4
+ function deepTruncateStrings(obj, maxLen, depth = 0) {
5
+ if (depth > 14)
6
+ return obj;
7
+ if (typeof obj === 'string') {
8
+ if (obj.length <= maxLen)
9
+ return obj;
10
+ return `${obj.slice(0, Math.max(0, maxLen - 1))}…`;
11
+ }
12
+ if (Array.isArray(obj)) {
13
+ const cap = 80;
14
+ const arr = obj.length > cap ? obj.slice(0, cap) : obj;
15
+ return arr.map((x) => deepTruncateStrings(x, maxLen, depth + 1));
16
+ }
17
+ if (obj !== null && typeof obj === 'object') {
18
+ const o = obj;
19
+ const out = {};
20
+ for (const k of Object.keys(o)) {
21
+ out[k] = deepTruncateStrings(o[k], maxLen, depth + 1);
22
+ }
23
+ return out;
24
+ }
25
+ return obj;
26
+ }
27
+ /** Último recurso: schema aberto (o modelo já vê nomes exatos no system prompt). */
28
+ function fallbackOpenEndedParameters() {
29
+ return {
30
+ type: 'object',
31
+ additionalProperties: true,
32
+ description: 'JSON com os argumentos da tool MCP (ex.: projectId, branchId, sql).',
33
+ };
34
+ }
35
+ export function slimToolsForUpstreamPayload(tools) {
36
+ return tools.map((t) => {
37
+ if (t.type !== 'function' || !t.function)
38
+ return t;
39
+ const fn = t.function;
40
+ const rawDesc = fn.description ?? '';
41
+ const description = rawDesc.length <= MAX_FUNCTION_DESCRIPTION_CHARS
42
+ ? rawDesc
43
+ : `${rawDesc.slice(0, MAX_FUNCTION_DESCRIPTION_CHARS - 1)}…`;
44
+ let parameters = fn.parameters;
45
+ if (parameters && typeof parameters === 'object') {
46
+ parameters = deepTruncateStrings(parameters, MAX_SCHEMA_STRING_CHARS);
47
+ }
48
+ let jsonLen = JSON.stringify(parameters ?? {}).length;
49
+ if (jsonLen > MAX_TOOL_PARAMETERS_JSON_CHARS) {
50
+ parameters = deepTruncateStrings(parameters, 220);
51
+ jsonLen = JSON.stringify(parameters ?? {}).length;
52
+ }
53
+ if (jsonLen > MAX_TOOL_PARAMETERS_JSON_CHARS) {
54
+ parameters = fallbackOpenEndedParameters();
55
+ }
56
+ return {
57
+ type: 'function',
58
+ function: {
59
+ name: fn.name,
60
+ description,
61
+ parameters,
62
+ },
63
+ };
64
+ });
65
+ }
@@ -7,6 +7,29 @@ import * as diff from 'diff';
7
7
  const execAsync = promisify(exec);
8
8
  /** Máximo de linhas de diff exibidas; além disso mostra resumo para não imprimir arquivo inteiro */
9
9
  const MAX_DIFF_LINES = 45;
10
+ function isGeneratedNoisePath(relPath) {
11
+ const base = path.basename(relPath);
12
+ return /^generated\.(text|json)$/i.test(base);
13
+ }
14
+ function looksLikeMcpJsonDump(s) {
15
+ const t = s.trim();
16
+ if (!t)
17
+ return false;
18
+ try {
19
+ const j = JSON.parse(t);
20
+ if (j !== null && typeof j === 'object' && !Array.isArray(j)) {
21
+ const o = j;
22
+ return 'success' in o && 'result' in o;
23
+ }
24
+ if (Array.isArray(j) && j.length > 0 && j.every((x) => x && typeof x === 'object')) {
25
+ return true;
26
+ }
27
+ }
28
+ catch {
29
+ /* ignore */
30
+ }
31
+ return false;
32
+ }
10
33
  function showDiff(filePath, oldContent, newContent) {
11
34
  const relativePath = path.relative(process.cwd(), filePath);
12
35
  console.log(chalk.blue.bold(`\n📝 Edit ${relativePath}:`));
@@ -170,7 +193,13 @@ export async function executeTool(name, argsStr) {
170
193
  const oldContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
171
194
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
172
195
  fs.writeFileSync(filePath, args.content, 'utf8');
173
- showDiff(filePath, oldContent, args.content);
196
+ const rel = path.relative(process.cwd(), filePath);
197
+ if (isGeneratedNoisePath(rel) && looksLikeMcpJsonDump(args.content)) {
198
+ console.log(chalk.dim(`\n📝 ${rel} (omitido diff — parece resposta MCP/JSON; use tabela no texto do assistente)\n`));
199
+ }
200
+ else {
201
+ showDiff(filePath, oldContent, args.content);
202
+ }
174
203
  return `Successfully wrote to ${args.path}`;
175
204
  }
176
205
  if (name === 'run_command') {
@@ -1,4 +1,4 @@
1
- import { config, getEffectiveActiveModel } from '../config.js';
1
+ import { getEffectiveActiveModel, getOpenAIApiKey, getGrokApiKey, getOpenRouterToken, getGeminiApiKey, getPoktToken } from '../config.js';
2
2
  import { startChatLoop } from '../chat/loop.js';
3
3
  import { ui } from '../ui.js';
4
4
  export const chatCommand = {
@@ -11,23 +11,31 @@ export const chatCommand = {
11
11
  console.log(ui.error('No active model selected. Run: pokt models list then pokt models use -p <provider> -i <id>'));
12
12
  return;
13
13
  }
14
- if (activeModel.provider === 'openrouter' && !config.get('openrouterToken')) {
14
+ if (activeModel.provider === 'openrouter' && !getOpenRouterToken()) {
15
15
  console.log(ui.error('OpenRouter token not set. Use: pokt config set-openrouter -v <token>'));
16
16
  return;
17
17
  }
18
- if (activeModel.provider === 'gemini' && !config.get('geminiApiKey')) {
18
+ if (activeModel.provider === 'openai' && !getOpenAIApiKey()) {
19
+ console.log(ui.error('OpenAI API key not set. Use: pokt config set-openai -v <key>'));
20
+ return;
21
+ }
22
+ if (activeModel.provider === 'grok' && !getGrokApiKey()) {
23
+ console.log(ui.error('Grok (xAI) API key not set. Use: pokt config set-grok -v <key>'));
24
+ return;
25
+ }
26
+ if (activeModel.provider === 'gemini' && !getGeminiApiKey()) {
19
27
  console.log(ui.error('Gemini API key not set. Use: pokt config set-gemini -v <key>'));
20
28
  return;
21
29
  }
22
30
  if (activeModel.provider === 'controller') {
23
- if (!config.get('poktToken')) {
31
+ if (!getPoktToken()) {
24
32
  console.log(ui.error('Pokt token not set. Generate one at the panel and: pokt config set-pokt-token -v <token>'));
25
33
  return;
26
34
  }
27
35
  }
28
36
  // Se veio do menu interativo, não repetir banner/tips (já foram exibidos)
29
37
  if (!fromMenu) {
30
- console.log(ui.banner());
38
+ await ui.printBanner({ animate: true });
31
39
  console.log(ui.statusLine(`[${activeModel.provider}] ${activeModel.id}`));
32
40
  console.log('');
33
41
  console.log(ui.tips());