pokt-cli 1.0.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.
@@ -0,0 +1,162 @@
1
+ import prompts from 'prompts';
2
+ import ora from 'ora';
3
+ import { ui } from '../ui.js';
4
+ import { config } from '../config.js';
5
+ import { getClient } from './client.js';
6
+ import { tools, executeTool } from './tools.js';
7
+ import { connectMcpServer, getAllMcpToolsOpenAI, callMcpTool, isMcpTool, disconnectAllMcp, } from '../mcp/client.js';
8
+ const SYSTEM_PROMPT = `You are Pokt CLI, an elite AI Software Engineer.
9
+ Your goal is to help the user build, fix, and maintain software projects with high quality.
10
+
11
+ CORE CAPABILITIES:
12
+ 1. **Project Understanding**: You can see the whole file structure and read any file.
13
+ 2. **Autonomous Coding**: You can create new files, rewrite existing ones, and run terminal commands.
14
+ 3. **Problem Solving**: You analyze errors and propose/apply fixes.
15
+
16
+ GUIDELINES:
17
+ - 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
+ - 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 files.
20
+ - **DO NOT repeat the generated code in your chat response** if you have already used the \`write_file\` tool. Simply state that the file has been updated or created.
21
+ - You have full access to the current terminal. You can run \`npm install\`, \`tsc\`, or any other command.
22
+ - Be extremely concise in your explanations.
23
+ - The current working directory is: ${process.cwd()}
24
+ `;
25
+ async function loadProjectStructure() {
26
+ try {
27
+ const timeoutMs = 8000;
28
+ return await Promise.race([
29
+ executeTool('list_files', JSON.stringify({ path: '.' })),
30
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeoutMs))
31
+ ]);
32
+ }
33
+ catch {
34
+ try {
35
+ return await executeTool('list_directory', JSON.stringify({ path: '.' }));
36
+ }
37
+ catch {
38
+ return 'Could not list files.';
39
+ }
40
+ }
41
+ }
42
+ export async function startChatLoop(modelConfig) {
43
+ const client = await getClient(modelConfig);
44
+ // Conectar servidores MCP configurados e montar lista de tools (nativas + MCP)
45
+ const mcpServers = config.get('mcpServers') ?? [];
46
+ for (const server of mcpServers) {
47
+ const session = await connectMcpServer(server);
48
+ if (session) {
49
+ console.log(ui.dim(`[MCP] Connected: ${session.serverName} (${session.tools.length} tools)`));
50
+ }
51
+ }
52
+ const allTools = [
53
+ ...tools,
54
+ ...getAllMcpToolsOpenAI(),
55
+ ];
56
+ const messages = [
57
+ { role: 'system', content: SYSTEM_PROMPT }
58
+ ];
59
+ while (true) {
60
+ console.log('');
61
+ console.log(ui.shortcutsLine('shift+tab to accept edits', '? for shortcuts'));
62
+ const response = await prompts({
63
+ type: 'text',
64
+ name: 'input',
65
+ message: '>',
66
+ initial: '',
67
+ style: 'default'
68
+ });
69
+ const userInput = response.input;
70
+ if (!userInput || userInput.toLowerCase() === 'exit' || userInput.trim().toLowerCase() === '/quit') {
71
+ await disconnectAllMcp();
72
+ console.log(ui.dim('Goodbye!'));
73
+ break;
74
+ }
75
+ messages.push({ role: 'user', content: userInput });
76
+ // Primeiro o modelo vê o pedido; depois carregamos a estrutura do projeto para ele entender e então criar/editar
77
+ const isFirstUserMessage = messages.filter(m => m.role === 'user').length === 1;
78
+ if (isFirstUserMessage) {
79
+ const loadSpinner = ora('Carregando estrutura do projeto...').start();
80
+ const projectStructure = await loadProjectStructure();
81
+ loadSpinner.stop();
82
+ messages.push({ role: 'system', content: `Current Project Structure:\n${projectStructure}` });
83
+ }
84
+ await processLLMResponse(client, modelConfig.id, messages, allTools);
85
+ }
86
+ await disconnectAllMcp();
87
+ }
88
+ const MAX_429_RETRIES = 3;
89
+ const BASE_429_DELAY_MS = 5000;
90
+ async function createCompletionWithRetry(client, modelId, messages, toolsList) {
91
+ let lastError;
92
+ for (let attempt = 0; attempt <= MAX_429_RETRIES; attempt++) {
93
+ try {
94
+ return await client.chat.completions.create({
95
+ model: modelId,
96
+ messages,
97
+ tools: toolsList,
98
+ tool_choice: 'auto'
99
+ });
100
+ }
101
+ catch (err) {
102
+ lastError = err;
103
+ const is429 = err?.status === 429 || (err?.message && String(err.message).includes('429'));
104
+ if (is429 && attempt < MAX_429_RETRIES) {
105
+ const delayMs = BASE_429_DELAY_MS * (attempt + 1);
106
+ await new Promise(r => setTimeout(r, delayMs));
107
+ continue;
108
+ }
109
+ throw err;
110
+ }
111
+ }
112
+ throw lastError;
113
+ }
114
+ async function processLLMResponse(client, modelId, messages, toolsList) {
115
+ const spinner = ora('Thinking...').start();
116
+ try {
117
+ let completion = await createCompletionWithRetry(client, modelId, messages, toolsList);
118
+ let message = completion.choices[0].message;
119
+ spinner.stop();
120
+ while (message.tool_calls && message.tool_calls.length > 0) {
121
+ messages.push(message);
122
+ for (const toolCall of message.tool_calls) {
123
+ const name = toolCall.function.name;
124
+ const args = toolCall.function.arguments ?? '{}';
125
+ console.log(ui.warn(`\n[Executing Tool: ${name}]`));
126
+ console.log(ui.dim(`Arguments: ${args}`));
127
+ const toolSpinner = ora('Running tool...').start();
128
+ const result = isMcpTool(name)
129
+ ? await callMcpTool(name, args)
130
+ : await executeTool(name, args);
131
+ toolSpinner.stop();
132
+ console.log(ui.dim(`Result: ${result.length} characters`));
133
+ messages.push({
134
+ role: 'tool',
135
+ tool_call_id: toolCall.id,
136
+ content: result,
137
+ });
138
+ }
139
+ spinner.start('Thinking...');
140
+ completion = await createCompletionWithRetry(client, modelId, messages, toolsList);
141
+ message = completion.choices[0].message;
142
+ spinner.stop();
143
+ }
144
+ if (message.content) {
145
+ console.log('\n' + ui.labelPokt());
146
+ console.log(message.content);
147
+ messages.push({ role: 'assistant', content: message.content });
148
+ }
149
+ }
150
+ catch (error) {
151
+ spinner.stop();
152
+ const is429 = error?.status === 429 || (error?.message && String(error.message).includes('429'));
153
+ if (is429) {
154
+ console.log(ui.error('\nLimite de taxa atingido (429). O provedor está recebendo muitas requisições.'));
155
+ console.log(ui.dim('Aguarde alguns segundos e tente novamente.'));
156
+ }
157
+ else {
158
+ console.log(ui.error(`\nError: ${error?.message ?? error}`));
159
+ }
160
+ messages.pop();
161
+ }
162
+ }
@@ -0,0 +1,3 @@
1
+ import type { ChatCompletionTool } from 'openai/resources/chat/completions/completions.js';
2
+ export declare const tools: ChatCompletionTool[];
3
+ export declare function executeTool(name: string, argsStr: string): Promise<string>;
@@ -0,0 +1,215 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { exec } from 'child_process';
4
+ import { promisify } from 'util';
5
+ import chalk from 'chalk';
6
+ import * as diff from 'diff';
7
+ const execAsync = promisify(exec);
8
+ /** Máximo de linhas de diff exibidas; além disso mostra resumo para não imprimir arquivo inteiro */
9
+ const MAX_DIFF_LINES = 45;
10
+ function showDiff(filePath, oldContent, newContent) {
11
+ const relativePath = path.relative(process.cwd(), filePath);
12
+ console.log(chalk.blue.bold(`\n📝 Edit ${relativePath}:`));
13
+ if (!oldContent) {
14
+ const lineCount = newContent.split('\n').length;
15
+ if (lineCount <= MAX_DIFF_LINES) {
16
+ newContent.split('\n').forEach((line, idx) => {
17
+ console.log(`${chalk.gray((idx + 1).toString().padStart(3))} ${chalk.green('+')} ${chalk.green(line)}`);
18
+ });
19
+ }
20
+ else {
21
+ newContent.split('\n').slice(0, MAX_DIFF_LINES).forEach((line, idx) => {
22
+ console.log(`${chalk.gray((idx + 1).toString().padStart(3))} ${chalk.green('+')} ${chalk.green(line)}`);
23
+ });
24
+ console.log(chalk.gray(` ... (and ${lineCount - MAX_DIFF_LINES} more lines)`));
25
+ }
26
+ console.log('');
27
+ return;
28
+ }
29
+ const changes = diff.diffLines(oldContent, newContent);
30
+ const CONTEXT_LINES = 3;
31
+ let currentLine = 1;
32
+ let linesPrinted = 0;
33
+ for (let i = 0; i < changes.length && linesPrinted < MAX_DIFF_LINES; i++) {
34
+ const part = changes[i];
35
+ const lines = part.value.split('\n');
36
+ if (lines[lines.length - 1] === '')
37
+ lines.pop();
38
+ if (part.added || part.removed) {
39
+ // Show some context from previous part if it was unchanged
40
+ if (i > 0 && !changes[i - 1].added && !changes[i - 1].removed && linesPrinted < MAX_DIFF_LINES) {
41
+ const prevLines = changes[i - 1].value.split('\n');
42
+ if (prevLines[prevLines.length - 1] === '')
43
+ prevLines.pop();
44
+ const contextToShow = prevLines.slice(-CONTEXT_LINES);
45
+ const startLine = currentLine - contextToShow.length;
46
+ contextToShow.forEach((l, idx) => {
47
+ if (linesPrinted >= MAX_DIFF_LINES)
48
+ return;
49
+ console.log(`${chalk.gray((startLine + idx).toString().padStart(3))} ${chalk.gray(l)}`);
50
+ linesPrinted++;
51
+ });
52
+ }
53
+ // Show the actual change (só até MAX_DIFF_LINES)
54
+ for (const line of lines) {
55
+ if (linesPrinted >= MAX_DIFF_LINES)
56
+ break;
57
+ const lineDisplay = part.added ? chalk.green(line) : chalk.red(line);
58
+ const symbolDisplay = part.added ? chalk.green('+') : chalk.red('-');
59
+ console.log(`${chalk.gray(currentLine.toString().padStart(3))} ${symbolDisplay} ${lineDisplay}`);
60
+ if (!part.removed)
61
+ currentLine++;
62
+ linesPrinted++;
63
+ }
64
+ // Show some context from next part if it is unchanged
65
+ if (i < changes.length - 1 && !changes[i + 1].added && !changes[i + 1].removed && linesPrinted < MAX_DIFF_LINES) {
66
+ const nextLines = changes[i + 1].value.split('\n');
67
+ if (nextLines[nextLines.length - 1] === '')
68
+ nextLines.pop();
69
+ const contextToShow = nextLines.slice(0, CONTEXT_LINES);
70
+ contextToShow.forEach((l, idx) => {
71
+ if (linesPrinted >= MAX_DIFF_LINES)
72
+ return;
73
+ console.log(`${chalk.gray((currentLine + idx).toString().padStart(3))} ${chalk.gray(l)}`);
74
+ linesPrinted++;
75
+ });
76
+ if (linesPrinted < MAX_DIFF_LINES)
77
+ console.log(chalk.gray(' ...'));
78
+ linesPrinted++;
79
+ }
80
+ }
81
+ else {
82
+ currentLine += lines.length;
83
+ }
84
+ }
85
+ if (linesPrinted >= MAX_DIFF_LINES) {
86
+ console.log(chalk.gray(` ... (diff truncated, more changes in file)`));
87
+ }
88
+ console.log('');
89
+ }
90
+ export const tools = [
91
+ {
92
+ type: 'function',
93
+ function: {
94
+ name: 'read_file',
95
+ description: 'Reads the contents of a file.',
96
+ parameters: {
97
+ type: 'object',
98
+ properties: {
99
+ path: { type: 'string', description: 'The path to the file' }
100
+ },
101
+ required: ['path']
102
+ }
103
+ }
104
+ },
105
+ {
106
+ type: 'function',
107
+ function: {
108
+ name: 'write_file',
109
+ description: 'Writes content to a file. Overwrites if exists, creates if not.',
110
+ parameters: {
111
+ type: 'object',
112
+ properties: {
113
+ path: { type: 'string', description: 'The path to the file' },
114
+ content: { type: 'string', description: 'The full content to write to the file' }
115
+ },
116
+ required: ['path', 'content']
117
+ }
118
+ }
119
+ },
120
+ {
121
+ type: 'function',
122
+ function: {
123
+ name: 'run_command',
124
+ description: 'Runs a shell command in the current directory.',
125
+ parameters: {
126
+ type: 'object',
127
+ properties: {
128
+ command: { type: 'string', description: 'The shell command to run' }
129
+ },
130
+ required: ['command']
131
+ }
132
+ }
133
+ },
134
+ {
135
+ type: 'function',
136
+ function: {
137
+ name: 'list_directory',
138
+ description: 'Lists files and folders in a single directory.',
139
+ parameters: {
140
+ type: 'object',
141
+ properties: {
142
+ path: { type: 'string', description: 'Directory path to list' }
143
+ },
144
+ required: ['path']
145
+ }
146
+ }
147
+ },
148
+ {
149
+ type: 'function',
150
+ function: {
151
+ name: 'list_files',
152
+ description: 'Lists ALL files in the project recursively (ignores node_modules, .git, dist).',
153
+ parameters: {
154
+ type: 'object',
155
+ properties: {
156
+ path: { type: 'string', description: 'Root directory to start from (default ".")' }
157
+ }
158
+ }
159
+ }
160
+ }
161
+ ];
162
+ export async function executeTool(name, argsStr) {
163
+ try {
164
+ const args = JSON.parse(argsStr);
165
+ if (name === 'read_file') {
166
+ return fs.readFileSync(path.resolve(process.cwd(), args.path), 'utf8');
167
+ }
168
+ if (name === 'write_file') {
169
+ const filePath = path.resolve(process.cwd(), args.path);
170
+ const oldContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
171
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
172
+ fs.writeFileSync(filePath, args.content, 'utf8');
173
+ showDiff(filePath, oldContent, args.content);
174
+ return `Successfully wrote to ${args.path}`;
175
+ }
176
+ if (name === 'run_command') {
177
+ const { stdout, stderr } = await execAsync(args.command, { cwd: process.cwd() });
178
+ let out = '';
179
+ if (stdout)
180
+ out += `STDOUT:\n${stdout}\n`;
181
+ if (stderr)
182
+ out += `STDERR:\n${stderr}\n`;
183
+ return out || 'Command executed successfully with no output.';
184
+ }
185
+ if (name === 'list_directory') {
186
+ const dirPath = path.resolve(process.cwd(), args.path);
187
+ const items = fs.readdirSync(dirPath);
188
+ return items.join('\n');
189
+ }
190
+ if (name === 'list_files') {
191
+ const root = path.resolve(process.cwd(), args.path || '.');
192
+ const files = [];
193
+ const walk = (dir) => {
194
+ const items = fs.readdirSync(dir);
195
+ for (const item of items) {
196
+ const fullPath = path.join(dir, item);
197
+ if (item === 'node_modules' || item === '.git' || item === 'dist')
198
+ continue;
199
+ if (fs.statSync(fullPath).isDirectory()) {
200
+ walk(fullPath);
201
+ }
202
+ else {
203
+ files.push(path.relative(process.cwd(), fullPath));
204
+ }
205
+ }
206
+ };
207
+ walk(root);
208
+ return files.join('\n') || 'No files found.';
209
+ }
210
+ return `Unknown tool: ${name}`;
211
+ }
212
+ catch (error) {
213
+ return `Error executing tool ${name}: ${error.message}`;
214
+ }
215
+ }
@@ -0,0 +1,2 @@
1
+ import { CommandModule } from 'yargs';
2
+ export declare const authCommand: CommandModule;
@@ -0,0 +1,16 @@
1
+ import { loginWithGoogle } from '../auth/google.js';
2
+ export const authCommand = {
3
+ command: 'auth <action>',
4
+ describe: 'Manage authentication',
5
+ builder: (yargs) => yargs
6
+ .positional('action', {
7
+ describe: 'Action to perform',
8
+ type: 'string',
9
+ choices: ['login-google']
10
+ }),
11
+ handler: async (argv) => {
12
+ if (argv.action === 'login-google') {
13
+ await loginWithGoogle();
14
+ }
15
+ }
16
+ };
@@ -0,0 +1,2 @@
1
+ import type * as Yargs from 'yargs';
2
+ export declare const chatCommand: Yargs.CommandModule;
@@ -0,0 +1,41 @@
1
+ import { config, getEffectiveActiveModel } from '../config.js';
2
+ import { startChatLoop } from '../chat/loop.js';
3
+ import { ui } from '../ui.js';
4
+ export const chatCommand = {
5
+ command: 'chat',
6
+ describe: 'Start a Vibe Coding chat session',
7
+ handler: async (argv) => {
8
+ const fromMenu = argv?.fromMenu;
9
+ const activeModel = getEffectiveActiveModel();
10
+ if (!activeModel) {
11
+ console.log(ui.error('No active model selected. Run: pokt models list then pokt models use -p <provider> -i <id>'));
12
+ return;
13
+ }
14
+ if (activeModel.provider === 'openrouter' && !config.get('openrouterToken')) {
15
+ console.log(ui.error('OpenRouter token not set. Use: pokt config set-openrouter -v <token>'));
16
+ return;
17
+ }
18
+ if (activeModel.provider === 'gemini' && !config.get('geminiApiKey')) {
19
+ console.log(ui.error('Gemini API key not set. Use: pokt config set-gemini -v <key>'));
20
+ return;
21
+ }
22
+ if (activeModel.provider === 'controller') {
23
+ if (!config.get('poktToken')) {
24
+ console.log(ui.error('Pokt token not set. Generate one at the panel and: pokt config set-pokt-token -v <token>'));
25
+ return;
26
+ }
27
+ }
28
+ // Se veio do menu interativo, não repetir banner/tips (já foram exibidos)
29
+ if (!fromMenu) {
30
+ console.log(ui.banner());
31
+ console.log(ui.statusLine(`[${activeModel.provider}] ${activeModel.id}`));
32
+ console.log('');
33
+ console.log(ui.tips());
34
+ console.log('');
35
+ }
36
+ console.log(ui.dim('Type "exit" or /quit to end the session.'));
37
+ console.log(ui.statusBar({ model: `/model ${activeModel.provider} (${activeModel.id})` }));
38
+ console.log('');
39
+ await startChatLoop(activeModel);
40
+ }
41
+ };
@@ -0,0 +1,7 @@
1
+ import type * as Yargs from 'yargs';
2
+ interface ConfigArgs {
3
+ action?: string;
4
+ value?: string | string[];
5
+ }
6
+ export declare const configCommand: Yargs.CommandModule<{}, ConfigArgs>;
7
+ export {};
@@ -0,0 +1,76 @@
1
+ import { config, getControllerBaseUrl } from '../config.js';
2
+ import chalk from 'chalk';
3
+ import { ui } from '../ui.js';
4
+ export const configCommand = {
5
+ command: 'config <action>',
6
+ describe: 'Configure Pokt CLI settings',
7
+ builder: (yargs) => yargs
8
+ .positional('action', {
9
+ describe: 'Action to perform',
10
+ type: 'string',
11
+ choices: ['set-openrouter', 'set-ollama', 'set-ollama-cloud', 'set-gemini', 'set-pokt-token', 'clear-openrouter', 'show']
12
+ })
13
+ .option('value', {
14
+ describe: 'The value to set',
15
+ type: 'string',
16
+ alias: 'v'
17
+ }),
18
+ handler: (argv) => {
19
+ const { action, value } = argv;
20
+ if (action === 'show') {
21
+ const openrouter = config.get('openrouterToken');
22
+ const gemini = config.get('geminiApiKey');
23
+ const ollama = config.get('ollamaBaseUrl');
24
+ const ollamaCloud = config.get('ollamaCloudApiKey');
25
+ const poktToken = config.get('poktToken');
26
+ console.log(chalk.blue('\nCurrent config (tokens masked):'));
27
+ console.log(ui.dim(' OpenRouter Token:'), openrouter ? openrouter.slice(0, 8) + '****' : '(not set)');
28
+ console.log(ui.dim(' Gemini API Key:'), gemini ? gemini.slice(0, 8) + '****' : '(not set)');
29
+ console.log(ui.dim(' Ollama Base URL (local):'), ollama || '(not set)');
30
+ console.log(ui.dim(' Ollama Cloud API Key:'), ollamaCloud ? ollamaCloud.slice(0, 8) + '****' : '(not set) — https://ollama.com/settings/keys');
31
+ console.log(ui.dim(' Controller URL:'), getControllerBaseUrl(), ui.dim('(já configurado)'));
32
+ console.log(ui.dim(' Pokt Token:'), poktToken ? poktToken.slice(0, 10) + '****' : '(not set) — use: pokt config set-pokt-token -v <token>');
33
+ console.log(ui.warn('\nTokens are stored in your user config directory. Do not share it.\n'));
34
+ return;
35
+ }
36
+ if (action === 'clear-openrouter') {
37
+ config.set('openrouterToken', '');
38
+ console.log(ui.success('OpenRouter token cleared.'));
39
+ return;
40
+ }
41
+ if (action !== 'set-openrouter' && action !== 'set-ollama' && action !== 'set-ollama-cloud' && action !== 'set-gemini' && action !== 'set-pokt-token')
42
+ return;
43
+ const raw = Array.isArray(value) ? value[0] : value;
44
+ const strValue = typeof raw === 'string' ? raw : (raw != null ? String(raw) : '');
45
+ if (strValue === '') {
46
+ console.log(ui.error('Error: --value is required. Use: pokt config ' + action + ' -v <value>'));
47
+ return;
48
+ }
49
+ if (action === 'set-openrouter') {
50
+ config.set('openrouterToken', strValue);
51
+ console.log(ui.success('OpenRouter token saved successfully.'));
52
+ }
53
+ else if (action === 'set-ollama') {
54
+ config.set('ollamaBaseUrl', strValue);
55
+ console.log(ui.success(`Ollama base URL set to: ${strValue}`));
56
+ }
57
+ else if (action === 'set-ollama-cloud') {
58
+ config.set('ollamaCloudApiKey', strValue);
59
+ console.log(ui.success('Ollama Cloud API key saved. Create keys at: https://ollama.com/settings/keys'));
60
+ }
61
+ else if (action === 'set-gemini') {
62
+ config.set('geminiApiKey', strValue);
63
+ console.log(ui.success('Gemini API key saved successfully.'));
64
+ }
65
+ else if (action === 'set-pokt-token') {
66
+ config.set('poktToken', strValue);
67
+ const controllerModel = { provider: 'controller', id: 'default' };
68
+ const models = config.get('registeredModels');
69
+ if (!models.some((m) => m.provider === 'controller' && m.id === 'default')) {
70
+ config.set('registeredModels', [controllerModel, ...models]);
71
+ }
72
+ config.set('activeModel', controllerModel);
73
+ console.log(ui.success('Pokt token salvo. Controller é seu provedor principal. Gere tokens em: https://pokt-cli-controller.vercel.app'));
74
+ }
75
+ }
76
+ };
@@ -0,0 +1,11 @@
1
+ import type * as Yargs from 'yargs';
2
+ interface McpArgs {
3
+ action?: string;
4
+ name?: string;
5
+ type?: string;
6
+ command?: string;
7
+ args?: string;
8
+ url?: string;
9
+ }
10
+ export declare const mcpCommand: Yargs.CommandModule<{}, McpArgs>;
11
+ export {};
@@ -0,0 +1,127 @@
1
+ import { config } from '../config.js';
2
+ import { ui } from '../ui.js';
3
+ import { connectMcpServer, getAllMcpToolsOpenAI, disconnectAllMcp } from '../mcp/client.js';
4
+ export const mcpCommand = {
5
+ command: 'mcp [action]',
6
+ describe: 'Manage MCP (Model Context Protocol) servers',
7
+ builder: (yargs) => yargs
8
+ .positional('action', {
9
+ describe: 'Action: list, add, remove, test',
10
+ type: 'string',
11
+ choices: ['list', 'add', 'remove', 'test'],
12
+ })
13
+ .option('name', { describe: 'Server name (for add/remove)', type: 'string', alias: 'n' })
14
+ .option('type', { describe: 'Server type: stdio or http', type: 'string', choices: ['stdio', 'http'] })
15
+ .option('command', { describe: 'Command for stdio (e.g. npx)', type: 'string', alias: 'c' })
16
+ .option('args', { describe: 'JSON array of args for stdio (e.g. \'["-y","mcp-server"]\')', type: 'string', alias: 'a' })
17
+ .option('url', { describe: 'URL for http server', type: 'string', alias: 'u' }),
18
+ handler: async (argv) => {
19
+ const action = argv.action || 'list';
20
+ const servers = config.get('mcpServers') ?? [];
21
+ if (action === 'list') {
22
+ if (servers.length === 0) {
23
+ console.log(ui.dim('No MCP servers configured. Add one with: pokt mcp add -n <name> -t stdio -c npx -a \'["-y","mcp-server-name"]\''));
24
+ return;
25
+ }
26
+ console.log(ui.title('\nMCP Servers:\n'));
27
+ for (const s of servers) {
28
+ const typeInfo = s.type === 'stdio'
29
+ ? `${s.command} ${(s.args ?? []).join(' ')}`
30
+ : (s.url ?? '');
31
+ console.log(ui.dim(` ${s.name}`), typeInfo);
32
+ }
33
+ console.log('');
34
+ return;
35
+ }
36
+ if (action === 'add') {
37
+ const name = argv.name;
38
+ const type = argv.type ?? 'stdio';
39
+ if (!name?.trim()) {
40
+ console.log(ui.error('--name is required. Example: pokt mcp add -n filesystem -t stdio -c npx -a \'["-y","@modelcontextprotocol/server-filesystem"]\''));
41
+ return;
42
+ }
43
+ if (servers.some(s => s.name === name)) {
44
+ console.log(ui.error(`Server "${name}" already exists. Use "pokt mcp remove -n ${name}" first.`));
45
+ return;
46
+ }
47
+ if (type === 'stdio') {
48
+ const command = argv.command;
49
+ if (!command?.trim()) {
50
+ console.log(ui.error('For stdio, --command is required (e.g. npx).'));
51
+ return;
52
+ }
53
+ let args = [];
54
+ if (argv.args) {
55
+ try {
56
+ args = JSON.parse(argv.args);
57
+ }
58
+ catch {
59
+ console.log(ui.error('--args must be a JSON array, e.g. \'["-y","mcp-server"]\''));
60
+ return;
61
+ }
62
+ }
63
+ config.set('mcpServers', [...servers, { name, type: 'stdio', command, args }]);
64
+ }
65
+ else {
66
+ const url = argv.url;
67
+ if (!url?.trim()) {
68
+ console.log(ui.error('For http, --url is required.'));
69
+ return;
70
+ }
71
+ config.set('mcpServers', [...servers, { name, type: 'http', url }]);
72
+ }
73
+ console.log(ui.success(`MCP server "${name}" added. Use "pokt chat" to use its tools.`));
74
+ return;
75
+ }
76
+ if (action === 'remove') {
77
+ const name = argv.name;
78
+ if (!name?.trim()) {
79
+ console.log(ui.error('--name is required. Example: pokt mcp remove -n filesystem'));
80
+ return;
81
+ }
82
+ const next = servers.filter(s => s.name !== name);
83
+ if (next.length === servers.length) {
84
+ console.log(ui.warn(`No server named "${name}" found.`));
85
+ return;
86
+ }
87
+ config.set('mcpServers', next);
88
+ console.log(ui.success(`MCP server "${name}" removed.`));
89
+ return;
90
+ }
91
+ if (action === 'test') {
92
+ const name = argv.name;
93
+ const toTest = name ? servers.filter(s => s.name === name) : servers;
94
+ if (toTest.length === 0) {
95
+ console.log(ui.warn(name ? `No server named "${name}".` : 'No MCP servers configured.'));
96
+ return;
97
+ }
98
+ console.log(ui.dim('\nConnecting to MCP server(s)...\n'));
99
+ for (const server of toTest) {
100
+ if (server.type !== 'stdio') {
101
+ console.log(ui.warn(` ${server.name}: HTTP not yet supported in test.`));
102
+ continue;
103
+ }
104
+ try {
105
+ const session = await connectMcpServer(server);
106
+ if (session) {
107
+ const openaiTools = getAllMcpToolsOpenAI();
108
+ const count = session.tools.length;
109
+ console.log(ui.success(` ${server.name}: OK (${count} tools)`));
110
+ for (const t of session.tools) {
111
+ console.log(ui.dim(` - ${t.exposedName}`));
112
+ }
113
+ }
114
+ else {
115
+ console.log(ui.error(` ${server.name}: Connection failed.`));
116
+ }
117
+ }
118
+ catch (e) {
119
+ console.log(ui.error(` ${server.name}: ${e.message}`));
120
+ }
121
+ }
122
+ await disconnectAllMcp();
123
+ console.log('');
124
+ return;
125
+ }
126
+ },
127
+ };