polydev-ai 1.9.15 → 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 +60 -265
  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
- /**
1473
- * Check if a tool is a CLI tool that should be handled locally
1474
- */
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
1424
  /**
1488
- * Handle get_perspectives with local CLIs + remote perspectives
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.
1489
1428
  */
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,6 +1549,12 @@ 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();
@@ -1772,8 +1562,13 @@ Error: ${error.message}`
1772
1562
  // STEP 1: Add ALL available CLIs (in priority order) - they're FREE
1773
1563
  // Don't limit to maxPerspectives here — we run all CLIs in parallel
1774
1564
  // and take the first maxPerspectives successes (fast-collect pattern)
1565
+ // Skip the CLI that matches the current IDE to avoid recursive calls
1775
1566
  for (const cliId of cliPriorityOrder) {
1776
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
+ }
1777
1572
 
1778
1573
  const providerName = cliToProviderMap[cliId];
1779
1574
  usedProviderNames.add(providerName);
@@ -1789,7 +1584,7 @@ Error: ${error.message}`
1789
1584
 
1790
1585
  finalProviders.push({
1791
1586
  provider: providerName,
1792
- model: configuredProvider?.model || null, // null = use CLI's default model
1587
+ model: configuredProvider?.model || null, // null = use CLI default model
1793
1588
  cliId: cliId,
1794
1589
  source: 'cli',
1795
1590
  tier: null // CLIs don't use credits tiers
@@ -1869,7 +1664,7 @@ Error: ${error.message}`
1869
1664
  }
1870
1665
  });
1871
1666
 
1872
- // Fast-collect: resolve once we have maxPerspectives successes OR all complete
1667
+ // Fast-collect: resolve early once we have maxPerspectives successes OR all complete
1873
1668
  localResults = await this.collectFirstNSuccesses(cliPromises, maxPerspectives);
1874
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`);
1875
1670
  }
@@ -2661,7 +2456,7 @@ Error: ${error.message}`
2661
2456
  /**
2662
2457
  * Fetch user's model preferences from API keys
2663
2458
  * Returns a map of CLI provider -> default_model
2664
- * Also fetches and caches perspectivesPerMessage setting and allProviders list
2459
+ * Also fetches and caches perspectives_per_message setting and allProviders list
2665
2460
  */
2666
2461
  async fetchUserModelPreferences() {
2667
2462
  // Check cache first
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polydev-ai",
3
- "version": "1.9.15",
3
+ "version": "1.9.16",
4
4
  "engines": {
5
5
  "node": ">=20.x <=22.x"
6
6
  },