polydev-ai 1.9.14 → 1.9.16

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.
Files changed (2) hide show
  1. package/mcp/stdio-wrapper.js +105 -317
  2. package/package.json +1 -1
@@ -300,6 +300,10 @@ class StdioMCPWrapper {
300
300
  // Pending session file for surviving restarts
301
301
  this.PENDING_SESSION_FILE = path.join(os.homedir(), '.polydev-pending-session');
302
302
 
303
+ // MCP client info (set during initialize handshake)
304
+ // Used to detect which IDE is running us and exclude its CLI to avoid recursive calls
305
+ this.clientInfo = null;
306
+
303
307
  // Initialize CLI Manager for local CLI functionality
304
308
  // Disable StatusReporter - it's redundant (updateCliStatusInDatabase handles DB updates via /api/cli-status-update)
305
309
  // and causes 401 errors because /api/mcp uses different auth than /api/cli-status-update
@@ -363,6 +367,12 @@ class StdioMCPWrapper {
363
367
  try {
364
368
  switch (method) {
365
369
  case 'initialize':
370
+ // Capture client info to detect which IDE is running us
371
+ // This lets us exclude its CLI to avoid recursive calls (e.g., Claude Code → claude_code CLI)
372
+ this.clientInfo = params?.clientInfo || null;
373
+ if (this.clientInfo) {
374
+ console.error(`[Stdio Wrapper] IDE detected: ${this.clientInfo.name} v${this.clientInfo.version || 'unknown'}`);
375
+ }
366
376
  return {
367
377
  jsonrpc: '2.0',
368
378
  id,
@@ -990,11 +1000,10 @@ Token will be saved automatically after login.`
990
1000
  perspectivesSection += ` (${perspectiveCount} active)`;
991
1001
 
992
1002
  // Show providers in order with their selected models
993
- for (let i = 0; i < modelPrefs.allProviders.length; i++) {
994
- const p = modelPrefs.allProviders[i];
1003
+ for (const p of modelPrefs.allProviders) {
995
1004
  const providerName = p.provider.charAt(0).toUpperCase() + p.provider.slice(1);
996
1005
  const source = p.cliId ? 'CLI' : (p.tier ? `Credits/${p.tier}` : 'API');
997
- perspectivesSection += `\n ${i + 1}. ${providerName.padEnd(12)} ${p.model} [${source}]`;
1006
+ perspectivesSection += `\n ${providerName.padEnd(12)} ${p.model} [${source}]`;
998
1007
  }
999
1008
  } else {
1000
1009
  perspectivesSection += '\n (none configured - using defaults)';
@@ -1412,271 +1421,46 @@ Error: ${error.message}`
1412
1421
  </html>`;
1413
1422
  }
1414
1423
 
1415
- async forwardToRemoteServer(request) {
1416
- console.error(`[Stdio Wrapper] Forwarding request to remote server`);
1417
-
1418
- try {
1419
- // Use AbortController for timeout if available, otherwise rely on fetch timeout
1420
- const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
1421
- const timeoutId = controller ? setTimeout(() => controller.abort(), 240000) : null; // 240s timeout
1422
-
1423
- const response = await fetch('https://www.polydev.ai/api/mcp', {
1424
- method: 'POST',
1425
- headers: {
1426
- 'Content-Type': 'application/json',
1427
- 'Authorization': `Bearer ${this.userToken}`,
1428
- 'User-Agent': 'polydev-stdio-wrapper/1.0.0'
1429
- },
1430
- body: JSON.stringify(request),
1431
- ...(controller ? { signal: controller.signal } : {})
1432
- });
1433
-
1434
- if (timeoutId) clearTimeout(timeoutId);
1435
-
1436
- if (!response.ok) {
1437
- const errorText = await response.text();
1438
- console.error(`[Stdio Wrapper] Remote server error: ${response.status} - ${errorText}`);
1439
-
1440
- // Handle 401 specifically - token expired/invalid, trigger re-auth
1441
- if (response.status === 401) {
1442
- console.error('[Polydev] Remote API returned 401, auto-triggering re-authentication...');
1443
- return await this.triggerReAuth(request.id, 'Authentication expired. Re-authenticating...');
1444
- }
1445
-
1446
- return {
1447
- jsonrpc: '2.0',
1448
- id: request.id,
1449
- error: {
1450
- code: -32603,
1451
- message: `Remote server error: ${response.status} - ${errorText}`
1452
- }
1453
- };
1454
- }
1455
-
1456
- const result = await response.json();
1457
- console.error(`[Stdio Wrapper] Got response from remote server`);
1458
- return result;
1459
- } catch (error) {
1460
- console.error(`[Stdio Wrapper] Network error:`, error.message);
1461
- return {
1462
- jsonrpc: '2.0',
1463
- id: request.id,
1464
- error: {
1465
- code: -32603,
1466
- message: `Network error: ${error.message}`
1467
- }
1468
- };
1469
- }
1470
- }
1471
-
1472
1424
  /**
1473
- * Check if a tool is a CLI tool that should be handled locally
1425
+ * Get the CLI ID to exclude based on the current IDE client.
1426
+ * Prevents recursive calls (e.g., Claude Code calling claude_code CLI which spawns another Claude Code).
1427
+ * Returns null if no CLI should be excluded.
1474
1428
  */
1475
- isCliTool(toolName) {
1476
- const cliTools = [
1477
- 'force_cli_detection',
1478
- 'get_cli_status',
1479
- 'send_cli_prompt',
1480
- 'polydev.force_cli_detection',
1481
- 'polydev.get_cli_status',
1482
- 'polydev.send_cli_prompt'
1483
- ];
1484
- return cliTools.includes(toolName);
1485
- }
1486
-
1487
- /**
1488
- * Handle get_perspectives with local CLIs + remote perspectives
1489
- */
1490
- async handleGetPerspectivesWithCLIs(params, id) {
1491
- console.error(`[Stdio Wrapper] Handling get_perspectives with local CLIs + remote`);
1429
+ getExcludedCliForCurrentIDE() {
1430
+ if (!this.clientInfo?.name) return null;
1492
1431
 
1493
- try {
1494
- // Use existing localSendCliPrompt logic which already:
1495
- // 1. Checks all local CLIs
1496
- // 2. Calls remote perspectives
1497
- // 3. Combines results
1498
- const result = await this.localSendCliPrompt(params.arguments);
1499
-
1500
- // Check if result indicates auth failure (from forwardToRemoteServer 401 handling)
1501
- const resultText = result.content || this.formatCliResponse(result);
1502
- if (result.result?.content?.[0]?.text?.includes('RE-AUTHENTICATION REQUIRED')) {
1503
- // Auth failure already handled with re-auth response - pass through
1504
- return result;
1505
- }
1506
-
1507
- return {
1508
- jsonrpc: '2.0',
1509
- id,
1510
- result: {
1511
- content: [
1512
- {
1513
- type: 'text',
1514
- text: resultText
1515
- }
1516
- ]
1517
- }
1518
- };
1519
-
1520
- } catch (error) {
1521
- console.error(`[Stdio Wrapper] get_perspectives error:`, error);
1522
-
1523
- // Check if error is auth-related
1524
- const errMsg = (error.message || '').toLowerCase();
1525
- if (errMsg.includes('401') || errMsg.includes('unauthorized') || errMsg.includes('auth') || errMsg.includes('token')) {
1526
- console.error('[Polydev] Perspectives auth error, auto-triggering re-authentication...');
1527
- return await this.triggerReAuth(id, 'Authentication expired. Re-authenticating...');
1528
- }
1529
-
1530
- return {
1531
- jsonrpc: '2.0',
1532
- id,
1533
- error: {
1534
- code: -32603,
1535
- message: error.message
1536
- }
1537
- };
1538
- }
1539
- }
1540
-
1541
- /**
1542
- * Handle CLI tools locally without remote server calls
1543
- */
1544
- async handleLocalCliTool(request) {
1545
- const { method, params, id } = request;
1546
- const { name: toolName, arguments: args } = params;
1547
-
1548
- console.error(`[Stdio Wrapper] Handling local CLI tool: ${toolName}`);
1549
-
1550
- try {
1551
- let result;
1552
-
1553
- switch (toolName) {
1554
- case 'force_cli_detection':
1555
- case 'polydev.force_cli_detection':
1556
- result = await this.localForceCliDetection(args);
1557
- break;
1558
-
1559
- case 'get_cli_status':
1560
- case 'polydev.get_cli_status':
1561
- result = await this.localGetCliStatus(args);
1562
- break;
1563
-
1564
- case 'send_cli_prompt':
1565
- case 'polydev.send_cli_prompt':
1566
- result = await this.localSendCliPrompt(args);
1567
- break;
1568
-
1569
- default:
1570
- throw new Error(`Unknown CLI tool: ${toolName}`);
1571
- }
1572
-
1573
- return {
1574
- jsonrpc: '2.0',
1575
- id,
1576
- result: {
1577
- content: [
1578
- {
1579
- type: 'text',
1580
- text: this.formatCliResponse(result)
1581
- }
1582
- ]
1583
- }
1584
- };
1585
-
1586
- } catch (error) {
1587
- console.error(`[Stdio Wrapper] CLI tool error:`, error);
1588
- return {
1589
- jsonrpc: '2.0',
1590
- id,
1591
- error: {
1592
- code: -32603,
1593
- message: error.message
1594
- }
1595
- };
1596
- }
1597
- }
1598
-
1599
- /**
1600
- * Local CLI detection implementation with database updates
1601
- */
1602
- async localForceCliDetection(args) {
1603
- console.error(`[Stdio Wrapper] Local CLI detection with model detection started`);
1432
+ const clientName = this.clientInfo.name.toLowerCase();
1604
1433
 
1605
- try {
1606
- const providerId = args.provider_id; // Optional - detect specific provider
1607
-
1608
- // Force detection using CLI Manager (no remote API calls)
1609
- const results = await this.cliManager.forceCliDetection(providerId);
1610
- console.error(`[Stdio Wrapper] CLI detection results:`, JSON.stringify(results, null, 2));
1611
-
1612
- // Save status locally to file-based cache
1613
- await this.saveLocalCliStatus(results);
1614
-
1615
- // Update database with CLI status
1616
- await this.updateCliStatusInDatabase(results);
1617
-
1618
- return {
1619
- success: true,
1620
- results,
1621
- message: `Local CLI detection completed for ${providerId || 'all providers'}`,
1622
- timestamp: new Date().toISOString(),
1623
- local_only: true
1624
- };
1625
-
1626
- } catch (error) {
1627
- console.error('[Stdio Wrapper] Local CLI detection error:', error);
1628
- return {
1629
- success: false,
1630
- error: error.message,
1631
- timestamp: new Date().toISOString(),
1632
- local_only: true
1633
- };
1634
- }
1635
- }
1636
-
1637
- /**
1638
- * Local CLI status retrieval
1639
- */
1640
- async localGetCliStatus(args) {
1641
- console.error(`[Stdio Wrapper] Local CLI status retrieval`);
1434
+ // Map known IDE client names to their corresponding CLI IDs
1435
+ // These are the clientInfo.name values sent during MCP initialize handshake
1436
+ const ideToCliMap = {
1437
+ // Claude Code variants
1438
+ 'claude-code': 'claude_code',
1439
+ 'claude_code': 'claude_code',
1440
+ 'claude code': 'claude_code',
1441
+ 'claudecode': 'claude_code',
1442
+ // Cursor (uses Claude under the hood but is a separate IDE)
1443
+ // Don't exclude any CLI for Cursor since it's not calling itself
1444
+ // Gemini CLI / Google IDX
1445
+ 'gemini-cli': 'gemini_cli',
1446
+ 'gemini_cli': 'gemini_cli',
1447
+ // Codex CLI / OpenAI
1448
+ 'codex-cli': 'codex_cli',
1449
+ 'codex_cli': 'codex_cli',
1450
+ 'codex': 'codex_cli',
1451
+ };
1642
1452
 
1643
- try {
1644
- const providerId = args.provider_id;
1645
- let results = {};
1646
-
1647
- if (providerId) {
1648
- // Get specific provider status
1649
- const status = await this.cliManager.getCliStatus(providerId);
1650
- results = status;
1651
- } else {
1652
- // Get all providers status
1653
- const providers = this.cliManager.getProviders();
1654
- for (const provider of providers) {
1655
- const status = await this.cliManager.getCliStatus(provider.id);
1656
- results[provider.id] = status;
1657
- }
1658
- }
1659
-
1660
- // Update database with current status
1661
- await this.updateCliStatusInDatabase(results);
1662
-
1663
- return {
1664
- success: true,
1665
- results,
1666
- message: 'Local CLI status retrieved successfully',
1667
- timestamp: new Date().toISOString(),
1668
- local_only: true
1669
- };
1670
-
1671
- } catch (error) {
1672
- console.error('[Stdio Wrapper] Local CLI status error:', error);
1673
- return {
1674
- success: false,
1675
- error: error.message,
1676
- timestamp: new Date().toISOString(),
1677
- local_only: true
1678
- };
1453
+ // Direct match first
1454
+ if (ideToCliMap[clientName]) {
1455
+ return ideToCliMap[clientName];
1679
1456
  }
1457
+
1458
+ // Fuzzy match: check if client name contains known patterns
1459
+ if (clientName.includes('claude')) return 'claude_code';
1460
+ if (clientName.includes('gemini')) return 'gemini_cli';
1461
+ if (clientName.includes('codex')) return 'codex_cli';
1462
+
1463
+ return null;
1680
1464
  }
1681
1465
 
1682
1466
  /**
@@ -1765,14 +1549,26 @@ Error: ${error.message}`
1765
1549
  // CLI priority order: Claude Code first, then Gemini, then Codex
1766
1550
  const cliPriorityOrder = ['claude_code', 'gemini_cli', 'codex_cli'];
1767
1551
 
1552
+ // Detect if we should exclude the current IDE's CLI to avoid recursive calls
1553
+ const excludedCli = this.getExcludedCliForCurrentIDE();
1554
+ if (excludedCli) {
1555
+ console.error(`[Stdio Wrapper] Excluding CLI '${excludedCli}' (current IDE: ${this.clientInfo?.name}) to avoid recursive calls`);
1556
+ }
1557
+
1768
1558
  // Build merged provider list: CLIs first, then API-only
1769
1559
  const finalProviders = [];
1770
1560
  const usedProviderNames = new Set();
1771
1561
 
1772
- // STEP 1: Add ALL available CLIs first (in priority order) - they're FREE
1562
+ // STEP 1: Add ALL available CLIs (in priority order) - they're FREE
1563
+ // Don't limit to maxPerspectives here — we run all CLIs in parallel
1564
+ // and take the first maxPerspectives successes (fast-collect pattern)
1565
+ // Skip the CLI that matches the current IDE to avoid recursive calls
1773
1566
  for (const cliId of cliPriorityOrder) {
1774
- if (finalProviders.length >= maxPerspectives) break;
1775
1567
  if (!availableClis.includes(cliId)) continue;
1568
+ if (cliId === excludedCli) {
1569
+ console.error(`[Stdio Wrapper] [CLI-FIRST] Skipping ${cliId} (same as current IDE — would cause recursive call)`);
1570
+ continue;
1571
+ }
1776
1572
 
1777
1573
  const providerName = cliToProviderMap[cliId];
1778
1574
  usedProviderNames.add(providerName);
@@ -1788,7 +1584,7 @@ Error: ${error.message}`
1788
1584
 
1789
1585
  finalProviders.push({
1790
1586
  provider: providerName,
1791
- model: configuredProvider?.model || null, // null = use CLI's default model
1587
+ model: configuredProvider?.model || null, // null = use CLI default model
1792
1588
  cliId: cliId,
1793
1589
  source: 'cli',
1794
1590
  tier: null // CLIs don't use credits tiers
@@ -1817,7 +1613,7 @@ Error: ${error.message}`
1817
1613
  console.error(`[Stdio Wrapper] [CLI-FIRST] Added API/credits: ${normalizedProvider} (${p.model})${p.tier ? ` [${p.tier}]` : ''}`);
1818
1614
  }
1819
1615
 
1820
- console.error(`[Stdio Wrapper] Final provider list (${finalProviders.length}/${maxPerspectives}): ${finalProviders.map(p => `${p.provider}[${p.source}]`).join(', ')}`);
1616
+ console.error(`[Stdio Wrapper] Final provider list (${finalProviders.length}, need ${maxPerspectives}): ${finalProviders.map(p => `${p.provider}[${p.source}]`).join(', ')}`);
1821
1617
 
1822
1618
  // Separate into CLI entries (local execution) vs API entries (remote execution)
1823
1619
  const cliProviderEntries = finalProviders.filter(p => p.source === 'cli');
@@ -1825,7 +1621,8 @@ Error: ${error.message}`
1825
1621
 
1826
1622
  console.error(`[Stdio Wrapper] Provider breakdown: CLI=${cliProviderEntries.map(p => p.cliId).join(', ') || 'none'}, API-only=${apiOnlyProviders.map(p => p.provider).join(', ') || 'none'}`);
1827
1623
 
1828
- // Run all CLI prompts concurrently
1624
+ // Run ALL CLI prompts concurrently with fast-collect pattern
1625
+ // Resolves once we have maxPerspectives successes (don't wait for slow CLIs)
1829
1626
  if (cliProviderEntries.length > 0) {
1830
1627
  const cliPromises = cliProviderEntries.map(async (providerEntry) => {
1831
1628
  try {
@@ -1866,7 +1663,10 @@ Error: ${error.message}`
1866
1663
  };
1867
1664
  }
1868
1665
  });
1869
- localResults = await Promise.all(cliPromises);
1666
+
1667
+ // Fast-collect: resolve early once we have maxPerspectives successes OR all complete
1668
+ localResults = await this.collectFirstNSuccesses(cliPromises, maxPerspectives);
1669
+ console.error(`[Stdio Wrapper] Fast-collect: got ${localResults.filter(r => r.success).length} successful, ${localResults.filter(r => !r.success).length} failed out of ${cliPromises.length} CLIs`);
1870
1670
  }
1871
1671
 
1872
1672
  // Store API-only providers info for remote API call
@@ -1941,6 +1741,42 @@ Error: ${error.message}`
1941
1741
  }
1942
1742
  }
1943
1743
 
1744
+ /**
1745
+ * Collect results from parallel promises, resolving early once we have N successes
1746
+ * This avoids waiting for slow/timed-out CLIs when we already have enough results
1747
+ * @param {Promise[]} promises - Array of promises to collect from
1748
+ * @param {number} needed - Number of successful results needed
1749
+ * @returns {Promise<Array>} Array of results (may include failures if not enough successes)
1750
+ */
1751
+ collectFirstNSuccesses(promises, needed) {
1752
+ return new Promise((resolve) => {
1753
+ const results = [];
1754
+ let successCount = 0;
1755
+ let completedCount = 0;
1756
+ let resolved = false;
1757
+
1758
+ if (promises.length === 0) {
1759
+ resolve([]);
1760
+ return;
1761
+ }
1762
+
1763
+ for (const promise of promises) {
1764
+ promise.then(result => {
1765
+ if (resolved) return;
1766
+ results.push(result);
1767
+ if (result.success) successCount++;
1768
+ completedCount++;
1769
+
1770
+ // Resolve early if we have enough successes OR all promises done
1771
+ if (successCount >= needed || completedCount >= promises.length) {
1772
+ resolved = true;
1773
+ resolve([...results]); // Copy to prevent mutation from late arrivals
1774
+ }
1775
+ });
1776
+ }
1777
+ });
1778
+ }
1779
+
1944
1780
  /**
1945
1781
  * Get all available and authenticated CLI providers
1946
1782
  * Uses user's provider order from dashboard (display_order) instead of hardcoded order
@@ -2278,54 +2114,6 @@ Error: ${error.message}`
2278
2114
  combineAllCliAndPerspectives(localResults, perspectivesResult, args) {
2279
2115
  // Ensure perspectivesResult is always an object to prevent undefined errors
2280
2116
  const safePersp = perspectivesResult || { success: false, error: 'No response from perspectives server' };
2281
-
2282
- const combinedResult = {
2283
- success: true,
2284
- timestamp: new Date().toISOString(),
2285
- mode: args.mode,
2286
- local_cli_count: localResults.length,
2287
- sections: {
2288
- local: localResults,
2289
- remote: safePersp
2290
- }
2291
- };
2292
-
2293
- // Check if any local CLIs succeeded
2294
- const successfulClis = localResults.filter(result => result.success);
2295
- const hasSomeLocalSuccess = successfulClis.length > 0;
2296
-
2297
- // Determine overall success and content
2298
- if (hasSomeLocalSuccess && safePersp.success) {
2299
- combinedResult.content = this.formatMultipleCliResponse(localResults, safePersp, false);
2300
- combinedResult.tokens_used = successfulClis.reduce((total, cli) => total + (cli.tokens_used || 0), 0);
2301
- combinedResult.latency_ms = Math.max(...successfulClis.map(cli => cli.latency_ms || 0));
2302
- } else if (!hasSomeLocalSuccess && safePersp.success) {
2303
- // Complete fallback case - no local CLIs worked
2304
- combinedResult.content = this.formatMultipleCliResponse(localResults, safePersp, true);
2305
- combinedResult.fallback_used = true;
2306
- combinedResult.tokens_used = 0; // No local tokens used
2307
- } else if (hasSomeLocalSuccess && !safePersp.success) {
2308
- // Local CLIs succeeded, remote failed
2309
- combinedResult.content = this.formatMultipleCliResponse(localResults, safePersp, false);
2310
- combinedResult.tokens_used = successfulClis.reduce((total, cli) => total + (cli.tokens_used || 0), 0);
2311
- combinedResult.latency_ms = Math.max(...successfulClis.map(cli => cli.latency_ms || 0));
2312
- } else {
2313
- // Both failed
2314
- combinedResult.success = false;
2315
- const cliErrors = localResults.map(cli => `${cli.provider_id}: ${cli.error || 'Unknown error'}`).join('; ');
2316
- const perspectivesError = safePersp.error || 'Unknown remote error';
2317
- combinedResult.error = `All local CLIs failed: ${cliErrors}; Perspectives also failed: ${perspectivesError}`;
2318
- }
2319
-
2320
- return combinedResult;
2321
- }
2322
-
2323
- /**
2324
- * Format multiple CLI responses with remote perspectives
2325
- */
2326
- formatMultipleCliResponse(localResults, perspectivesResult, isFallback) {
2327
- // Safety check - ensure perspectivesResult is always an object
2328
- const safePersp = perspectivesResult || { success: false, error: 'No perspectives data' };
2329
2117
  let formatted = '';
2330
2118
 
2331
2119
  // Show all local CLI responses
@@ -2668,7 +2456,7 @@ Error: ${error.message}`
2668
2456
  /**
2669
2457
  * Fetch user's model preferences from API keys
2670
2458
  * Returns a map of CLI provider -> default_model
2671
- * Also fetches and caches perspectivesPerMessage setting and allProviders list
2459
+ * Also fetches and caches perspectives_per_message setting and allProviders list
2672
2460
  */
2673
2461
  async fetchUserModelPreferences() {
2674
2462
  // Check cache first
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polydev-ai",
3
- "version": "1.9.14",
3
+ "version": "1.9.16",
4
4
  "engines": {
5
5
  "node": ">=20.x <=22.x"
6
6
  },