polydev-ai 1.9.51 → 1.10.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.
@@ -6,9 +6,9 @@
6
6
  *
7
7
  * Lifecycle:
8
8
  * 1. start() → begins heartbeat (30s) + polling (3s)
9
- * 2. pollForRequests() → GET /api/tunnel/pending
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('[Tunnel] Starting CLI-as-API tunnel client');
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
- * Poll for pending tunnel requests
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/pending`;
207
+ const url = `${this.serverBaseUrl}/api/tunnel/claim`;
170
208
  const res = await fetch(url, {
171
- method: 'GET',
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(`Poll failed (${res.status}): ${text}`);
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
@@ -262,11 +262,101 @@
262
262
  },
263
263
  "base_model": {
264
264
  "type": "string",
265
- "description": "Optional: the model making this ranking (auto-detected from IDE if not provided, e.g. 'claude-opus-4-6', 'gpt-5.3-codex')"
265
+ "description": "IMPORTANT: Identify yourself pass YOUR model name/ID (e.g. 'claude-opus-4-6', 'claude-sonnet-4-5', 'gpt-5.3-codex', 'gemini-3-pro'). This tracks which AI judge made the ranking."
266
266
  }
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
 
@@ -460,19 +460,47 @@ Token will be saved automatically after login.`
460
460
  return await this.handleGetPerspectivesWithCLIs(params, id);
461
461
  }
462
462
 
463
- // Enrich rank_perspectives with base model + client info before forwarding
463
+ // Enrich rank_perspectives with IDE + base model info before forwarding
464
464
  if (toolName === 'rank_perspectives' || toolName === 'polydev.rank_perspectives') {
465
- if (params.arguments && !params.arguments.base_model) {
466
- // Inject base model from IDE client info (set during MCP initialize handshake)
467
- if (this.clientInfo?.name) {
468
- params.arguments.base_model = this.clientInfo.name;
465
+ if (params.arguments) {
466
+ // Always inject IDE info from MCP clientInfo (set during initialize handshake)
467
+ // Known clientInfo.name values from MCP clients:
468
+ // claude-code, Claude Desktop, cursor, windsurf, cline, continue,
469
+ // github-copilot-developer, vscode, jetbrains
470
+ if (!params.arguments.ide && this.clientInfo?.name) {
471
+ params.arguments.ide = this.clientInfo.name;
469
472
  if (this.clientInfo.version) {
470
- params.arguments.base_model += `/${this.clientInfo.version}`;
473
+ params.arguments.ide_version = this.clientInfo.version;
471
474
  }
472
475
  }
473
- }
474
- if (params.arguments && !params.arguments.client_id) {
475
- params.arguments.client_id = 'stdio-wrapper';
476
+
477
+ // If model didn't self-identify, infer base_model from IDE name
478
+ // Note: This is a fallback — the model's own self-report (from tool args) is preferred
479
+ if (!params.arguments.base_model && this.clientInfo?.name) {
480
+ const ideName = (this.clientInfo.name || '').toLowerCase();
481
+ // Map IDE name → likely base model family (not specific model version)
482
+ if (ideName.includes('claude')) {
483
+ params.arguments.base_model = 'claude'; // Could be opus, sonnet, haiku
484
+ } else if (ideName.includes('cursor')) {
485
+ params.arguments.base_model = 'cursor'; // Could be claude, gpt, etc.
486
+ } else if (ideName.includes('windsurf') || ideName.includes('codeium')) {
487
+ params.arguments.base_model = 'windsurf';
488
+ } else if (ideName.includes('cline')) {
489
+ params.arguments.base_model = 'cline';
490
+ } else if (ideName.includes('continue')) {
491
+ params.arguments.base_model = 'continue';
492
+ } else if (ideName.includes('codex')) {
493
+ params.arguments.base_model = 'codex';
494
+ } else if (ideName.includes('gemini')) {
495
+ params.arguments.base_model = 'gemini';
496
+ } else {
497
+ params.arguments.base_model = ideName;
498
+ }
499
+ }
500
+
501
+ if (!params.arguments.client_id) {
502
+ params.arguments.client_id = 'stdio-wrapper';
503
+ }
476
504
  }
477
505
  }
478
506
 
@@ -3212,20 +3240,32 @@ To re-login: /polydev:login`
3212
3240
  process.stdin.on('end', () => {
3213
3241
  console.error('Polydev MCP Server shutting down...');
3214
3242
  this.stopSmartRefreshScheduler();
3215
- process.exit(0);
3243
+ if (this.tunnelClient) {
3244
+ this.tunnelClient.stop().catch(() => {}).finally(() => process.exit(0));
3245
+ } else {
3246
+ process.exit(0);
3247
+ }
3216
3248
  });
3217
3249
 
3218
3250
  // Handle process signals
3219
3251
  process.on('SIGINT', () => {
3220
3252
  console.error('Received SIGINT, shutting down...');
3221
3253
  this.stopSmartRefreshScheduler();
3222
- process.exit(0);
3254
+ if (this.tunnelClient) {
3255
+ this.tunnelClient.stop().catch(() => {}).finally(() => process.exit(0));
3256
+ } else {
3257
+ process.exit(0);
3258
+ }
3223
3259
  });
3224
3260
 
3225
3261
  process.on('SIGTERM', () => {
3226
3262
  console.error('Received SIGTERM, shutting down...');
3227
3263
  this.stopSmartRefreshScheduler();
3228
- process.exit(0);
3264
+ if (this.tunnelClient) {
3265
+ this.tunnelClient.stop().catch(() => {}).finally(() => process.exit(0));
3266
+ } else {
3267
+ process.exit(0);
3268
+ }
3229
3269
  });
3230
3270
 
3231
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.9.51",
3
+ "version": "1.10.0",
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
- "@supabase/ssr": "^0.4.0",
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.50",
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": "^18.5.0",
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
  }