polydev-ai 1.9.52 → 1.10.1
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/lib/tunnelClient.js +52 -9
- package/mcp/manifest.json +91 -1
- package/mcp/postinstall.js +99 -0
- package/mcp/preuninstall.js +51 -0
- package/mcp/server.js +234 -0
- package/mcp/stdio-wrapper.js +15 -3
- package/package.json +12 -6
package/lib/tunnelClient.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Lifecycle:
|
|
8
8
|
* 1. start() → begins heartbeat (30s) + polling (3s)
|
|
9
|
-
* 2.
|
|
9
|
+
* 2. claimRequests() → POST /api/tunnel/claim (atomic, race-free)
|
|
10
10
|
* 3. handleRequest() → cliManager.sendCliPrompt() → POST /api/tunnel/respond
|
|
11
|
-
* 4. stop() → clears intervals
|
|
11
|
+
* 4. stop() → sends disconnect heartbeat, clears intervals
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
class TunnelClient {
|
|
@@ -17,6 +17,10 @@ class TunnelClient {
|
|
|
17
17
|
this.authToken = authToken;
|
|
18
18
|
this.cliManager = cliManager;
|
|
19
19
|
|
|
20
|
+
// Generate unique instance ID: pid-random4-timestamp
|
|
21
|
+
const crypto = require('crypto');
|
|
22
|
+
this.instanceId = `${process.pid}-${crypto.randomBytes(2).toString('hex')}-${Date.now()}`;
|
|
23
|
+
|
|
20
24
|
this.heartbeatInterval = null;
|
|
21
25
|
this.pollInterval = null;
|
|
22
26
|
this._processing = new Set(); // track in-flight request IDs
|
|
@@ -84,7 +88,7 @@ class TunnelClient {
|
|
|
84
88
|
if (this._started) return;
|
|
85
89
|
this._started = true;
|
|
86
90
|
|
|
87
|
-
console.error(
|
|
91
|
+
console.error(`[Tunnel] Starting CLI-as-API tunnel client (instance: ${this.instanceId})`);
|
|
88
92
|
console.error(`[Tunnel] Auth token prefix: ${this.authToken ? this.authToken.substring(0, 8) + '...' : 'NONE'}`);
|
|
89
93
|
|
|
90
94
|
// Send initial heartbeat immediately
|
|
@@ -112,9 +116,9 @@ class TunnelClient {
|
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
/**
|
|
115
|
-
* Stop the tunnel client
|
|
119
|
+
* Stop the tunnel client — sends disconnect heartbeat before clearing intervals
|
|
116
120
|
*/
|
|
117
|
-
stop() {
|
|
121
|
+
async stop() {
|
|
118
122
|
if (this.heartbeatInterval) {
|
|
119
123
|
clearInterval(this.heartbeatInterval);
|
|
120
124
|
this.heartbeatInterval = null;
|
|
@@ -124,9 +128,40 @@ class TunnelClient {
|
|
|
124
128
|
this.pollInterval = null;
|
|
125
129
|
}
|
|
126
130
|
this._started = false;
|
|
131
|
+
|
|
132
|
+
// Send disconnect heartbeat (best-effort, don't block exit)
|
|
133
|
+
try {
|
|
134
|
+
await this.sendDisconnect();
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error('[Tunnel] Disconnect heartbeat failed:', err.message);
|
|
137
|
+
}
|
|
138
|
+
|
|
127
139
|
console.error('[Tunnel] Tunnel client stopped');
|
|
128
140
|
}
|
|
129
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Send disconnect heartbeat to remove this instance from tunnel_connections
|
|
144
|
+
*/
|
|
145
|
+
async sendDisconnect() {
|
|
146
|
+
const url = `${this.serverBaseUrl}/api/tunnel/heartbeat`;
|
|
147
|
+
const res = await fetch(url, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: {
|
|
150
|
+
'Authorization': `Bearer ${this.authToken}`,
|
|
151
|
+
'Content-Type': 'application/json',
|
|
152
|
+
},
|
|
153
|
+
body: JSON.stringify({
|
|
154
|
+
instance_id: this.instanceId,
|
|
155
|
+
disconnect: true,
|
|
156
|
+
}),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
const text = await res.text().catch(() => '');
|
|
161
|
+
throw new Error(`Disconnect failed (${res.status}): ${text}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
130
165
|
/**
|
|
131
166
|
* Send heartbeat with available CLI providers
|
|
132
167
|
*/
|
|
@@ -148,6 +183,7 @@ class TunnelClient {
|
|
|
148
183
|
body: JSON.stringify({
|
|
149
184
|
available_providers: providers,
|
|
150
185
|
client_version: packageVersion,
|
|
186
|
+
instance_id: this.instanceId,
|
|
151
187
|
}),
|
|
152
188
|
});
|
|
153
189
|
|
|
@@ -163,15 +199,22 @@ class TunnelClient {
|
|
|
163
199
|
}
|
|
164
200
|
|
|
165
201
|
/**
|
|
166
|
-
*
|
|
202
|
+
* Claim pending tunnel requests atomically via POST /api/tunnel/claim.
|
|
203
|
+
* Uses FOR UPDATE SKIP LOCKED on the server to prevent multiple instances
|
|
204
|
+
* from grabbing the same request.
|
|
167
205
|
*/
|
|
168
206
|
async pollForRequests() {
|
|
169
|
-
const url = `${this.serverBaseUrl}/api/tunnel/
|
|
207
|
+
const url = `${this.serverBaseUrl}/api/tunnel/claim`;
|
|
170
208
|
const res = await fetch(url, {
|
|
171
|
-
method: '
|
|
209
|
+
method: 'POST',
|
|
172
210
|
headers: {
|
|
173
211
|
'Authorization': `Bearer ${this.authToken}`,
|
|
212
|
+
'Content-Type': 'application/json',
|
|
174
213
|
},
|
|
214
|
+
body: JSON.stringify({
|
|
215
|
+
instance_id: this.instanceId,
|
|
216
|
+
limit: 5,
|
|
217
|
+
}),
|
|
175
218
|
});
|
|
176
219
|
|
|
177
220
|
if (!res.ok) {
|
|
@@ -180,7 +223,7 @@ class TunnelClient {
|
|
|
180
223
|
return;
|
|
181
224
|
}
|
|
182
225
|
const text = await res.text().catch(() => '');
|
|
183
|
-
throw new Error(`
|
|
226
|
+
throw new Error(`Claim failed (${res.status}): ${text}`);
|
|
184
227
|
}
|
|
185
228
|
this._consecutive401s = 0; // reset on success
|
|
186
229
|
|
package/mcp/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polydev-perspectives",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Multi-model AI perspectives - query GPT 5.2, Claude Opus 4.5, Gemini 3, and Grok 4.1 simultaneously. Get diverse perspectives when stuck or need enhanced reasoning. Achieved 74.6% on SWE-bench Verified.",
|
|
5
5
|
"author": "Polydev AI",
|
|
6
6
|
"license": "MIT",
|
|
@@ -267,6 +267,96 @@
|
|
|
267
267
|
},
|
|
268
268
|
"required": ["ranked_models"]
|
|
269
269
|
}
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"name": "search_x",
|
|
273
|
+
"description": "Search X (Twitter) for real-time posts, discussions, and trending topics using the xAI API. Get 50 free searches, then add your own xAI API key at https://polydev.ai/dashboard/models. Returns synthesized results from Grok plus individual post links.",
|
|
274
|
+
"inputSchema": {
|
|
275
|
+
"type": "object",
|
|
276
|
+
"properties": {
|
|
277
|
+
"query": {
|
|
278
|
+
"type": "string",
|
|
279
|
+
"description": "What to search for on X. Can be keywords, questions, or topics (e.g. 'MCP servers', 'what are people saying about Claude Code', '@elonmusk AI')",
|
|
280
|
+
"minLength": 1
|
|
281
|
+
},
|
|
282
|
+
"user_token": {
|
|
283
|
+
"type": "string",
|
|
284
|
+
"description": "Polydev user authentication token"
|
|
285
|
+
},
|
|
286
|
+
"model": {
|
|
287
|
+
"type": "string",
|
|
288
|
+
"description": "xAI model to use (default: grok-4-1-fast-reasoning)",
|
|
289
|
+
"default": "grok-4-1-fast-reasoning"
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
"required": ["query"]
|
|
293
|
+
},
|
|
294
|
+
"examples": [
|
|
295
|
+
{
|
|
296
|
+
"description": "Search X for trending AI topics",
|
|
297
|
+
"input": {
|
|
298
|
+
"query": "What are developers saying about MCP servers today?"
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"description": "Find posts from a specific user",
|
|
303
|
+
"input": {
|
|
304
|
+
"query": "@AnthropicAI latest announcements"
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"name": "generate_image",
|
|
311
|
+
"description": "Generate images using OpenAI (gpt-image-1.5) or Google Gemini (gemini-3.1-flash-image-preview). Requires your own API key added at https://polydev.ai/dashboard/models. Returns base64 PNG image data.",
|
|
312
|
+
"inputSchema": {
|
|
313
|
+
"type": "object",
|
|
314
|
+
"properties": {
|
|
315
|
+
"prompt": {
|
|
316
|
+
"type": "string",
|
|
317
|
+
"description": "Description of the image to generate (e.g. 'a futuristic dashboard with neon lights', 'logo for a developer tools company')",
|
|
318
|
+
"minLength": 1
|
|
319
|
+
},
|
|
320
|
+
"user_token": {
|
|
321
|
+
"type": "string",
|
|
322
|
+
"description": "Polydev user authentication token"
|
|
323
|
+
},
|
|
324
|
+
"provider": {
|
|
325
|
+
"type": "string",
|
|
326
|
+
"enum": ["openai", "gemini"],
|
|
327
|
+
"description": "Which provider to use for image generation (default: openai)",
|
|
328
|
+
"default": "openai"
|
|
329
|
+
},
|
|
330
|
+
"size": {
|
|
331
|
+
"type": "string",
|
|
332
|
+
"enum": ["1024x1024", "1024x1536", "1536x1024", "auto"],
|
|
333
|
+
"description": "Image size (OpenAI only). Default: 1024x1024",
|
|
334
|
+
"default": "1024x1024"
|
|
335
|
+
},
|
|
336
|
+
"quality": {
|
|
337
|
+
"type": "string",
|
|
338
|
+
"enum": ["low", "medium", "high", "auto"],
|
|
339
|
+
"description": "Image quality (OpenAI only). Default: auto",
|
|
340
|
+
"default": "auto"
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
"required": ["prompt"]
|
|
344
|
+
},
|
|
345
|
+
"examples": [
|
|
346
|
+
{
|
|
347
|
+
"description": "Generate a product logo",
|
|
348
|
+
"input": {
|
|
349
|
+
"prompt": "Modern minimalist logo for an AI developer tools company called Polydev, blue and purple gradient"
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
"description": "Generate a diagram",
|
|
354
|
+
"input": {
|
|
355
|
+
"prompt": "Architecture diagram showing microservices with API gateway, clean technical illustration",
|
|
356
|
+
"provider": "gemini"
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
]
|
|
270
360
|
}
|
|
271
361
|
],
|
|
272
362
|
"configuration": {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Polydev AI - Post-install hook
|
|
5
|
+
* Shows install success message and optionally prompts for login.
|
|
6
|
+
* Never fails install — all wrapped in try/catch.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
|
|
14
|
+
// Skip in CI environments
|
|
15
|
+
const CI_VARS = ['CI', 'CONTINUOUS_INTEGRATION', 'BUILD_NUMBER', 'GITHUB_ACTIONS', 'GITLAB_CI', 'CIRCLECI', 'TRAVIS', 'JENKINS_URL', 'CODEBUILD_BUILD_ID'];
|
|
16
|
+
if (CI_VARS.some(v => process.env[v])) {
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Skip if running as part of a larger npm install (not direct install)
|
|
21
|
+
if (process.env.npm_config_global === 'false' && process.env.INIT_CWD && !process.env.INIT_CWD.includes('polydev-ai')) {
|
|
22
|
+
// Being installed as a dependency of another project, not directly
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check if already authenticated
|
|
27
|
+
const envPath = path.join(os.homedir(), '.polydev.env');
|
|
28
|
+
const hasToken = (() => {
|
|
29
|
+
try {
|
|
30
|
+
if (fs.existsSync(envPath)) {
|
|
31
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
32
|
+
return content.includes('POLYDEV_USER_TOKEN=') && content.match(/POLYDEV_USER_TOKEN=\S+/);
|
|
33
|
+
}
|
|
34
|
+
} catch {}
|
|
35
|
+
return false;
|
|
36
|
+
})();
|
|
37
|
+
|
|
38
|
+
if (hasToken) {
|
|
39
|
+
// Already authenticated — short success message
|
|
40
|
+
console.log('\n \x1b[32m✓\x1b[0m Polydev AI updated successfully\n');
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Show install success box
|
|
45
|
+
const width = 52;
|
|
46
|
+
const line = '─'.repeat(width);
|
|
47
|
+
console.log(`
|
|
48
|
+
\x1b[1m┌${line}┐\x1b[0m
|
|
49
|
+
\x1b[1m│\x1b[0m \x1b[32m✓\x1b[0m \x1b[1mPolydev AI installed successfully!\x1b[0m${' '.repeat(width - 39)}\x1b[1m│\x1b[0m
|
|
50
|
+
\x1b[1m│\x1b[0m${' '.repeat(width)}\x1b[1m│\x1b[0m
|
|
51
|
+
\x1b[1m│\x1b[0m To get started, authenticate:${' '.repeat(width - 32)}\x1b[1m│\x1b[0m
|
|
52
|
+
\x1b[1m│\x1b[0m${' '.repeat(width)}\x1b[1m│\x1b[0m
|
|
53
|
+
\x1b[1m│\x1b[0m \x1b[36mnpx polydev-ai login\x1b[0m${' '.repeat(width - 24)}\x1b[1m│\x1b[0m
|
|
54
|
+
\x1b[1m│\x1b[0m${' '.repeat(width)}\x1b[1m│\x1b[0m
|
|
55
|
+
\x1b[1m│\x1b[0m This opens your browser for one-click auth.${' '.repeat(width - 47)}\x1b[1m│\x1b[0m
|
|
56
|
+
\x1b[1m└${line}┘\x1b[0m
|
|
57
|
+
`);
|
|
58
|
+
|
|
59
|
+
// Only prompt in interactive terminals
|
|
60
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Prompt for login with 15s timeout
|
|
65
|
+
const readline = require('readline');
|
|
66
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
67
|
+
|
|
68
|
+
const timeout = setTimeout(() => {
|
|
69
|
+
console.log('\n (Skipped — run \x1b[36mnpx polydev-ai login\x1b[0m anytime)\n');
|
|
70
|
+
rl.close();
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}, 15000);
|
|
73
|
+
|
|
74
|
+
rl.question(' Login now? (Y/n) ', (answer) => {
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
rl.close();
|
|
77
|
+
|
|
78
|
+
if (answer && answer.toLowerCase() === 'n') {
|
|
79
|
+
console.log('\n Run \x1b[36mnpx polydev-ai login\x1b[0m when ready.\n');
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fork login.js for browser-based auth
|
|
84
|
+
const { fork } = require('child_process');
|
|
85
|
+
const loginScript = path.join(__dirname, 'login.js');
|
|
86
|
+
|
|
87
|
+
if (fs.existsSync(loginScript)) {
|
|
88
|
+
const child = fork(loginScript, ['login'], { stdio: 'inherit' });
|
|
89
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
90
|
+
} else {
|
|
91
|
+
console.log(' Login script not found. Run \x1b[36mnpx polydev-ai login\x1b[0m manually.\n');
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
} catch (err) {
|
|
97
|
+
// Never fail the install
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Polydev AI - Pre-uninstall hook
|
|
5
|
+
* Cleans up auth token file and shell config lines.
|
|
6
|
+
* Never fails the uninstall — all best-effort.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
|
|
14
|
+
const home = os.homedir();
|
|
15
|
+
|
|
16
|
+
// 1. Remove ~/.polydev.env
|
|
17
|
+
const envPath = path.join(home, '.polydev.env');
|
|
18
|
+
try {
|
|
19
|
+
if (fs.existsSync(envPath)) {
|
|
20
|
+
fs.unlinkSync(envPath);
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
|
|
24
|
+
// 2. Remove POLYDEV_USER_TOKEN lines from shell configs
|
|
25
|
+
const shellConfigs = ['.zshrc', '.bashrc', '.profile'].map(f => path.join(home, f));
|
|
26
|
+
|
|
27
|
+
for (const configPath of shellConfigs) {
|
|
28
|
+
try {
|
|
29
|
+
if (!fs.existsSync(configPath)) continue;
|
|
30
|
+
|
|
31
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
32
|
+
|
|
33
|
+
// Remove lines containing POLYDEV_USER_TOKEN (export and comments)
|
|
34
|
+
const filtered = content
|
|
35
|
+
.split('\n')
|
|
36
|
+
.filter(line => !line.includes('POLYDEV_USER_TOKEN'))
|
|
37
|
+
.join('\n');
|
|
38
|
+
|
|
39
|
+
// Only write if changed
|
|
40
|
+
if (filtered !== content) {
|
|
41
|
+
fs.writeFileSync(configPath, filtered, 'utf8');
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(' Polydev AI credentials cleaned up.\n');
|
|
47
|
+
|
|
48
|
+
} catch {
|
|
49
|
+
// Never fail the uninstall
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
package/mcp/server.js
CHANGED
|
@@ -249,6 +249,14 @@ class MCPServer {
|
|
|
249
249
|
result = await this.manageMemoryPreferences(args);
|
|
250
250
|
break;
|
|
251
251
|
|
|
252
|
+
case 'search_x':
|
|
253
|
+
result = await this.searchX(args);
|
|
254
|
+
break;
|
|
255
|
+
|
|
256
|
+
case 'generate_image':
|
|
257
|
+
result = await this.generateImage(args);
|
|
258
|
+
break;
|
|
259
|
+
|
|
252
260
|
default:
|
|
253
261
|
throw new Error(`Tool ${name} not implemented`);
|
|
254
262
|
}
|
|
@@ -1039,6 +1047,12 @@ class MCPServer {
|
|
|
1039
1047
|
case 'manage_memory_preferences':
|
|
1040
1048
|
return this.formatMemoryResponse(result);
|
|
1041
1049
|
|
|
1050
|
+
case 'search_x':
|
|
1051
|
+
return this.formatXSearchResponse(result);
|
|
1052
|
+
|
|
1053
|
+
case 'generate_image':
|
|
1054
|
+
return this.formatImageResponse(result);
|
|
1055
|
+
|
|
1042
1056
|
default:
|
|
1043
1057
|
return JSON.stringify(result, null, 2);
|
|
1044
1058
|
}
|
|
@@ -1770,6 +1784,226 @@ class MCPServer {
|
|
|
1770
1784
|
// CLI status and usage tracking is handled locally by CLIManager
|
|
1771
1785
|
// No database integration needed - this MCP server runs independently
|
|
1772
1786
|
|
|
1787
|
+
/**
|
|
1788
|
+
* Search X (Twitter) using xAI Responses API
|
|
1789
|
+
*/
|
|
1790
|
+
async searchX(args) {
|
|
1791
|
+
console.error('[Polydev MCP] X search requested');
|
|
1792
|
+
|
|
1793
|
+
if (!args.query || typeof args.query !== 'string') {
|
|
1794
|
+
throw new Error('query is required and must be a string');
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
const userToken = args.user_token || process.env.POLYDEV_USER_TOKEN;
|
|
1798
|
+
if (!userToken) {
|
|
1799
|
+
throw new Error(
|
|
1800
|
+
'Authentication required for X search.\n' +
|
|
1801
|
+
'Set POLYDEV_USER_TOKEN in your MCP config or pass user_token parameter.\n' +
|
|
1802
|
+
'Get your token at: https://polydev.ai/dashboard/mcp-tools'
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
const serverUrl = 'https://www.polydev.ai/api/x-search';
|
|
1807
|
+
|
|
1808
|
+
try {
|
|
1809
|
+
const response = await fetch(serverUrl, {
|
|
1810
|
+
method: 'POST',
|
|
1811
|
+
headers: {
|
|
1812
|
+
'Content-Type': 'application/json',
|
|
1813
|
+
'Authorization': `Bearer ${userToken}`,
|
|
1814
|
+
'User-Agent': 'polydev-mcp/1.4.0'
|
|
1815
|
+
},
|
|
1816
|
+
body: JSON.stringify({
|
|
1817
|
+
query: args.query,
|
|
1818
|
+
user_token: userToken,
|
|
1819
|
+
model: args.model || 'grok-4-1-fast-reasoning'
|
|
1820
|
+
})
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1823
|
+
if (!response.ok) {
|
|
1824
|
+
const errorData = await response.json().catch(() => ({}));
|
|
1825
|
+
|
|
1826
|
+
if (response.status === 429) {
|
|
1827
|
+
throw new Error(
|
|
1828
|
+
'Free X search limit reached (50 searches).\n\n' +
|
|
1829
|
+
'To continue searching X, add your own xAI API key:\n' +
|
|
1830
|
+
'1. Get an API key at https://console.x.ai\n' +
|
|
1831
|
+
'2. Add it at https://polydev.ai/dashboard/models (select "X-AI" provider)\n\n' +
|
|
1832
|
+
'With your own key, you get unlimited X searches.'
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
throw new Error(errorData.error || `X search failed: HTTP ${response.status}`);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
return await response.json();
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
if (error.message.includes('Free X search limit')) throw error;
|
|
1842
|
+
console.error('[Polydev MCP] X search error:', error);
|
|
1843
|
+
throw new Error(`X search failed: ${error.message}`);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
/**
|
|
1848
|
+
* Generate images using OpenAI or Gemini
|
|
1849
|
+
*/
|
|
1850
|
+
async generateImage(args) {
|
|
1851
|
+
console.error('[Polydev MCP] Image generation requested');
|
|
1852
|
+
|
|
1853
|
+
if (!args.prompt || typeof args.prompt !== 'string') {
|
|
1854
|
+
throw new Error('prompt is required and must be a string');
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
const userToken = args.user_token || process.env.POLYDEV_USER_TOKEN;
|
|
1858
|
+
if (!userToken) {
|
|
1859
|
+
throw new Error(
|
|
1860
|
+
'Authentication required for image generation.\n' +
|
|
1861
|
+
'Set POLYDEV_USER_TOKEN in your MCP config or pass user_token parameter.\n' +
|
|
1862
|
+
'Get your token at: https://polydev.ai/dashboard/mcp-tools'
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
const serverUrl = 'https://www.polydev.ai/api/generate-image';
|
|
1867
|
+
|
|
1868
|
+
try {
|
|
1869
|
+
const response = await fetch(serverUrl, {
|
|
1870
|
+
method: 'POST',
|
|
1871
|
+
headers: {
|
|
1872
|
+
'Content-Type': 'application/json',
|
|
1873
|
+
'Authorization': `Bearer ${userToken}`,
|
|
1874
|
+
'User-Agent': 'polydev-mcp/1.4.0'
|
|
1875
|
+
},
|
|
1876
|
+
body: JSON.stringify({
|
|
1877
|
+
prompt: args.prompt,
|
|
1878
|
+
user_token: userToken,
|
|
1879
|
+
provider: args.provider || 'openai',
|
|
1880
|
+
model: args.model,
|
|
1881
|
+
size: args.size || '1024x1024',
|
|
1882
|
+
quality: args.quality || 'auto'
|
|
1883
|
+
})
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
if (!response.ok) {
|
|
1887
|
+
const errorData = await response.json().catch(() => ({}));
|
|
1888
|
+
|
|
1889
|
+
if (response.status === 400 && errorData.setup_url) {
|
|
1890
|
+
throw new Error(
|
|
1891
|
+
`No ${errorData.provider || 'API'} key found for image generation.\n\n` +
|
|
1892
|
+
'Image generation requires your own API key:\n' +
|
|
1893
|
+
'1. Get an API key from OpenAI (https://platform.openai.com) or Google AI (https://aistudio.google.com)\n' +
|
|
1894
|
+
'2. Add it at https://polydev.ai/dashboard/models\n\n' +
|
|
1895
|
+
'Supported providers: OpenAI (gpt-image-1) and Google Gemini'
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
throw new Error(errorData.error || `Image generation failed: HTTP ${response.status}`);
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
const result = await response.json();
|
|
1903
|
+
|
|
1904
|
+
// Save image to file if we have base64 data
|
|
1905
|
+
if (result.image_base64) {
|
|
1906
|
+
try {
|
|
1907
|
+
const os = require('os');
|
|
1908
|
+
const path = require('path');
|
|
1909
|
+
const fs = require('fs');
|
|
1910
|
+
|
|
1911
|
+
const timestamp = Date.now();
|
|
1912
|
+
const filename = `polydev-image-${timestamp}.png`;
|
|
1913
|
+
const outputDir = path.join(os.homedir(), '.polydev', 'images');
|
|
1914
|
+
|
|
1915
|
+
if (!fs.existsSync(outputDir)) {
|
|
1916
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
const outputPath = path.join(outputDir, filename);
|
|
1920
|
+
fs.writeFileSync(outputPath, Buffer.from(result.image_base64, 'base64'));
|
|
1921
|
+
|
|
1922
|
+
result.saved_to = outputPath;
|
|
1923
|
+
console.error(`[Polydev MCP] Image saved to: ${outputPath}`);
|
|
1924
|
+
} catch (saveError) {
|
|
1925
|
+
console.error('[Polydev MCP] Failed to save image file:', saveError.message);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
return result;
|
|
1930
|
+
} catch (error) {
|
|
1931
|
+
if (error.message.includes('No ') && error.message.includes('key found')) throw error;
|
|
1932
|
+
console.error('[Polydev MCP] Image generation error:', error);
|
|
1933
|
+
throw new Error(`Image generation failed: ${error.message}`);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
/**
|
|
1938
|
+
* Format X search response
|
|
1939
|
+
*/
|
|
1940
|
+
formatXSearchResponse(result) {
|
|
1941
|
+
let formatted = `# X Search Results\n\n`;
|
|
1942
|
+
|
|
1943
|
+
if (result.answer) {
|
|
1944
|
+
formatted += `## Summary\n`;
|
|
1945
|
+
formatted += `${result.answer}\n\n`;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
if (result.search_results && result.search_results.length > 0) {
|
|
1949
|
+
formatted += `## Posts Found\n\n`;
|
|
1950
|
+
result.search_results.forEach((post, index) => {
|
|
1951
|
+
formatted += `### ${index + 1}. ${post.author || 'Unknown'}\n`;
|
|
1952
|
+
if (post.snippet) formatted += `${post.snippet}\n`;
|
|
1953
|
+
if (post.url) formatted += `[View post](${post.url})\n`;
|
|
1954
|
+
if (post.date) formatted += `*${post.date}*\n`;
|
|
1955
|
+
formatted += `\n`;
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
if (result.using_free_tier) {
|
|
1960
|
+
formatted += `---\n`;
|
|
1961
|
+
formatted += `*Free tier: ${result.free_searches_remaining} searches remaining of 50*\n`;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
if (result.model) {
|
|
1965
|
+
formatted += `\n*Model: ${result.model} | Latency: ${result.latency_ms}ms*\n`;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
return formatted;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
/**
|
|
1972
|
+
* Format image generation response
|
|
1973
|
+
*/
|
|
1974
|
+
formatImageResponse(result) {
|
|
1975
|
+
let formatted = `# Image Generated\n\n`;
|
|
1976
|
+
|
|
1977
|
+
formatted += `**Provider**: ${result.provider || 'openai'}\n`;
|
|
1978
|
+
formatted += `**Model**: ${result.model || 'gpt-image-1'}\n`;
|
|
1979
|
+
|
|
1980
|
+
if (result.size) {
|
|
1981
|
+
formatted += `**Size**: ${result.size}\n`;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
if (result.revised_prompt) {
|
|
1985
|
+
formatted += `**Revised prompt**: ${result.revised_prompt}\n`;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
if (result.text_response) {
|
|
1989
|
+
formatted += `**Description**: ${result.text_response}\n`;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
if (result.saved_to) {
|
|
1993
|
+
formatted += `\n**Saved to**: \`${result.saved_to}\`\n`;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
if (result.image_base64) {
|
|
1997
|
+
formatted += `\nImage data: ${(result.image_base64.length / 1024).toFixed(0)}KB base64 PNG\n`;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
if (result.latency_ms) {
|
|
2001
|
+
formatted += `\n*Latency: ${result.latency_ms}ms*\n`;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
return formatted;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
1773
2007
|
async start() {
|
|
1774
2008
|
console.log('Starting Polydev Perspectives MCP Server...');
|
|
1775
2009
|
|
package/mcp/stdio-wrapper.js
CHANGED
|
@@ -3240,20 +3240,32 @@ To re-login: /polydev:login`
|
|
|
3240
3240
|
process.stdin.on('end', () => {
|
|
3241
3241
|
console.error('Polydev MCP Server shutting down...');
|
|
3242
3242
|
this.stopSmartRefreshScheduler();
|
|
3243
|
-
|
|
3243
|
+
if (this.tunnelClient) {
|
|
3244
|
+
this.tunnelClient.stop().catch(() => {}).finally(() => process.exit(0));
|
|
3245
|
+
} else {
|
|
3246
|
+
process.exit(0);
|
|
3247
|
+
}
|
|
3244
3248
|
});
|
|
3245
3249
|
|
|
3246
3250
|
// Handle process signals
|
|
3247
3251
|
process.on('SIGINT', () => {
|
|
3248
3252
|
console.error('Received SIGINT, shutting down...');
|
|
3249
3253
|
this.stopSmartRefreshScheduler();
|
|
3250
|
-
|
|
3254
|
+
if (this.tunnelClient) {
|
|
3255
|
+
this.tunnelClient.stop().catch(() => {}).finally(() => process.exit(0));
|
|
3256
|
+
} else {
|
|
3257
|
+
process.exit(0);
|
|
3258
|
+
}
|
|
3251
3259
|
});
|
|
3252
3260
|
|
|
3253
3261
|
process.on('SIGTERM', () => {
|
|
3254
3262
|
console.error('Received SIGTERM, shutting down...');
|
|
3255
3263
|
this.stopSmartRefreshScheduler();
|
|
3256
|
-
|
|
3264
|
+
if (this.tunnelClient) {
|
|
3265
|
+
this.tunnelClient.stop().catch(() => {}).finally(() => process.exit(0));
|
|
3266
|
+
} else {
|
|
3267
|
+
process.exit(0);
|
|
3268
|
+
}
|
|
3257
3269
|
});
|
|
3258
3270
|
|
|
3259
3271
|
console.error('Polydev MCP Server ready.\n');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polydev-ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.1",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=20.x <=22.x"
|
|
6
6
|
},
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
"mcp": "node mcp/server.js",
|
|
47
47
|
"mcp-stdio": "node mcp/stdio-wrapper.js",
|
|
48
48
|
"cli-detect": "node -e \"const CLIManager = require('./lib/cliManager').default; const m = new CLIManager(); m.forceCliDetection().then(console.log);\"",
|
|
49
|
+
"postinstall": "node mcp/postinstall.js || true",
|
|
50
|
+
"preuninstall": "node mcp/preuninstall.js || true",
|
|
49
51
|
"release": "bash scripts/release.sh",
|
|
50
52
|
"release:patch": "bash scripts/release.sh patch",
|
|
51
53
|
"release:minor": "bash scripts/release.sh minor",
|
|
@@ -66,9 +68,11 @@
|
|
|
66
68
|
"@radix-ui/react-select": "^2.2.6",
|
|
67
69
|
"@radix-ui/react-slot": "^1.2.3",
|
|
68
70
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
69
|
-
"@
|
|
71
|
+
"@sentry/nextjs": "^10.40.0",
|
|
72
|
+
"@supabase/ssr": "^0.8.0",
|
|
70
73
|
"@supabase/supabase-js": "^2.45.0",
|
|
71
74
|
"@upstash/redis": "^1.34.0",
|
|
75
|
+
"@vercel/speed-insights": "^1.3.1",
|
|
72
76
|
"class-variance-authority": "^0.7.1",
|
|
73
77
|
"clsx": "^2.1.1",
|
|
74
78
|
"date-fns": "^4.1.0",
|
|
@@ -78,21 +82,20 @@
|
|
|
78
82
|
"marked": "^16.2.1",
|
|
79
83
|
"next": "^15.5.7",
|
|
80
84
|
"open": "^11.0.0",
|
|
81
|
-
"polydev-ai": "^1.9.51",
|
|
82
85
|
"posthog-js": "^1.157.2",
|
|
83
86
|
"prismjs": "^1.30.0",
|
|
84
87
|
"react": "^18.3.1",
|
|
85
88
|
"react-dom": "^18.3.1",
|
|
86
89
|
"resend": "^6.0.2",
|
|
87
|
-
"shelljs": "^0.8.5",
|
|
88
90
|
"sonner": "^2.0.7",
|
|
89
|
-
"stripe": "^
|
|
91
|
+
"stripe": "^20.3.0",
|
|
90
92
|
"tailwind-merge": "^3.3.1",
|
|
91
93
|
"tailwindcss-animate": "^1.0.7",
|
|
92
94
|
"ts-node": "^10.9.2",
|
|
93
95
|
"undici": "^6.21.0",
|
|
94
96
|
"use-debounce": "^10.0.6",
|
|
95
|
-
"which": "^5.0.0"
|
|
97
|
+
"which": "^5.0.0",
|
|
98
|
+
"zod": "^4.3.6"
|
|
96
99
|
},
|
|
97
100
|
"devDependencies": {
|
|
98
101
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -110,5 +113,8 @@
|
|
|
110
113
|
"tailwindcss": "^3.4.10",
|
|
111
114
|
"ts-jest": "^29.4.4",
|
|
112
115
|
"typescript": "^5.5.4"
|
|
116
|
+
},
|
|
117
|
+
"overrides": {
|
|
118
|
+
"minimatch": ">=10.2.1"
|
|
113
119
|
}
|
|
114
120
|
}
|