polydev-ai 1.9.16 → 1.9.18

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/cliManager.js CHANGED
@@ -180,6 +180,16 @@ class CLIManager {
180
180
  return result;
181
181
  }
182
182
 
183
+ /**
184
+ * Pre-seed the status cache from external data (e.g., disk cache)
185
+ * This allows instant CLI availability without waiting for detection
186
+ */
187
+ setCachedStatus(statusMap) {
188
+ for (const [providerId, status] of Object.entries(statusMap)) {
189
+ this.statusCache.set(providerId, status);
190
+ }
191
+ }
192
+
183
193
  async detectCliProvider(provider) {
184
194
  try {
185
195
  const cliPath = await this.findCliPath(provider.command);
@@ -299,11 +299,17 @@ class StdioMCPWrapper {
299
299
 
300
300
  // Pending session file for surviving restarts
301
301
  this.PENDING_SESSION_FILE = path.join(os.homedir(), '.polydev-pending-session');
302
-
302
+
303
303
  // MCP client info (set during initialize handshake)
304
304
  // Used to detect which IDE is running us and exclude its CLI to avoid recursive calls
305
305
  this.clientInfo = null;
306
-
306
+
307
+ // Adaptive timeout tracking: stores recent response times per CLI
308
+ // Used to set smarter timeouts instead of fixed 240s
309
+ this.cliResponseTimes = {}; // { cliId: [latency_ms, ...] }
310
+ this.CLI_RESPONSE_HISTORY_SIZE = 5; // Keep last 5 response times
311
+ this.loadCliResponseTimes(); // Load from disk
312
+
307
313
  // Initialize CLI Manager for local CLI functionality
308
314
  // Disable StatusReporter - it's redundant (updateCliStatusInDatabase handles DB updates via /api/cli-status-update)
309
315
  // and causes 401 errors because /api/mcp uses different auth than /api/cli-status-update
@@ -1000,10 +1006,11 @@ Token will be saved automatically after login.`
1000
1006
  perspectivesSection += ` (${perspectiveCount} active)`;
1001
1007
 
1002
1008
  // Show providers in order with their selected models
1003
- for (const p of modelPrefs.allProviders) {
1009
+ for (let i = 0; i < modelPrefs.allProviders.length; i++) {
1010
+ const p = modelPrefs.allProviders[i];
1004
1011
  const providerName = p.provider.charAt(0).toUpperCase() + p.provider.slice(1);
1005
1012
  const source = p.cliId ? 'CLI' : (p.tier ? `Credits/${p.tier}` : 'API');
1006
- perspectivesSection += `\n ${providerName.padEnd(12)} ${p.model} [${source}]`;
1013
+ perspectivesSection += `\n ${i + 1}. ${providerName.padEnd(12)} ${p.model} [${source}]`;
1007
1014
  }
1008
1015
  } else {
1009
1016
  perspectivesSection += '\n (none configured - using defaults)';
@@ -1421,6 +1428,279 @@ Error: ${error.message}`
1421
1428
  </html>`;
1422
1429
  }
1423
1430
 
1431
+ async forwardToRemoteServer(request) {
1432
+ console.error(`[Stdio Wrapper] Forwarding request to remote server`);
1433
+
1434
+ try {
1435
+ // Use AbortController for timeout if available, otherwise rely on fetch timeout
1436
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
1437
+ const timeoutId = controller ? setTimeout(() => controller.abort(), 240000) : null; // 240s timeout
1438
+
1439
+ const response = await fetch('https://www.polydev.ai/api/mcp', {
1440
+ method: 'POST',
1441
+ headers: {
1442
+ 'Content-Type': 'application/json',
1443
+ 'Authorization': `Bearer ${this.userToken}`,
1444
+ 'User-Agent': 'polydev-stdio-wrapper/1.0.0'
1445
+ },
1446
+ body: JSON.stringify(request),
1447
+ ...(controller ? { signal: controller.signal } : {})
1448
+ });
1449
+
1450
+ if (timeoutId) clearTimeout(timeoutId);
1451
+
1452
+ if (!response.ok) {
1453
+ const errorText = await response.text();
1454
+ console.error(`[Stdio Wrapper] Remote server error: ${response.status} - ${errorText}`);
1455
+
1456
+ // Handle 401 specifically - token expired/invalid, trigger re-auth
1457
+ if (response.status === 401) {
1458
+ console.error('[Polydev] Remote API returned 401, auto-triggering re-authentication...');
1459
+ return await this.triggerReAuth(request.id, 'Authentication expired. Re-authenticating...');
1460
+ }
1461
+
1462
+ return {
1463
+ jsonrpc: '2.0',
1464
+ id: request.id,
1465
+ error: {
1466
+ code: -32603,
1467
+ message: `Remote server error: ${response.status} - ${errorText}`
1468
+ }
1469
+ };
1470
+ }
1471
+
1472
+ const result = await response.json();
1473
+ console.error(`[Stdio Wrapper] Got response from remote server`);
1474
+ return result;
1475
+ } catch (error) {
1476
+ console.error(`[Stdio Wrapper] Network error:`, error.message);
1477
+ return {
1478
+ jsonrpc: '2.0',
1479
+ id: request.id,
1480
+ error: {
1481
+ code: -32603,
1482
+ message: `Network error: ${error.message}`
1483
+ }
1484
+ };
1485
+ }
1486
+ }
1487
+
1488
+ /**
1489
+ * Check if a tool is a CLI tool that should be handled locally
1490
+ */
1491
+ isCliTool(toolName) {
1492
+ const cliTools = [
1493
+ 'force_cli_detection',
1494
+ 'get_cli_status',
1495
+ 'send_cli_prompt',
1496
+ 'polydev.force_cli_detection',
1497
+ 'polydev.get_cli_status',
1498
+ 'polydev.send_cli_prompt'
1499
+ ];
1500
+ return cliTools.includes(toolName);
1501
+ }
1502
+
1503
+ /**
1504
+ * Handle get_perspectives with local CLIs + remote perspectives
1505
+ */
1506
+ async handleGetPerspectivesWithCLIs(params, id) {
1507
+ console.error(`[Stdio Wrapper] Handling get_perspectives with local CLIs + remote`);
1508
+
1509
+ try {
1510
+ // Use existing localSendCliPrompt logic which already:
1511
+ // 1. Checks all local CLIs
1512
+ // 2. Calls remote perspectives
1513
+ // 3. Combines results
1514
+ const result = await this.localSendCliPrompt(params.arguments);
1515
+
1516
+ // Check if result indicates auth failure (from forwardToRemoteServer 401 handling)
1517
+ if (result?.result?.content?.[0]?.text?.includes('RE-AUTHENTICATION REQUIRED')) {
1518
+ // Auth failure already handled with re-auth response - pass through
1519
+ return result;
1520
+ }
1521
+
1522
+ // localSendCliPrompt returns either:
1523
+ // - A string (from combineAllCliAndPerspectives) on success
1524
+ // - An object { success, error, timestamp } on exception
1525
+ const resultText = typeof result === 'string'
1526
+ ? result
1527
+ : (result.content || this.formatCliResponse(result));
1528
+
1529
+ return {
1530
+ jsonrpc: '2.0',
1531
+ id,
1532
+ result: {
1533
+ content: [
1534
+ {
1535
+ type: 'text',
1536
+ text: resultText
1537
+ }
1538
+ ]
1539
+ }
1540
+ };
1541
+
1542
+ } catch (error) {
1543
+ console.error(`[Stdio Wrapper] get_perspectives error:`, error);
1544
+
1545
+ // Check if error is auth-related
1546
+ const errMsg = (error.message || '').toLowerCase();
1547
+ if (errMsg.includes('401') || errMsg.includes('unauthorized') || errMsg.includes('auth') || errMsg.includes('token')) {
1548
+ console.error('[Polydev] Perspectives auth error, auto-triggering re-authentication...');
1549
+ return await this.triggerReAuth(id, 'Authentication expired. Re-authenticating...');
1550
+ }
1551
+
1552
+ return {
1553
+ jsonrpc: '2.0',
1554
+ id,
1555
+ error: {
1556
+ code: -32603,
1557
+ message: error.message
1558
+ }
1559
+ };
1560
+ }
1561
+ }
1562
+
1563
+ /**
1564
+ * Handle CLI tools locally without remote server calls
1565
+ */
1566
+ async handleLocalCliTool(request) {
1567
+ const { method, params, id } = request;
1568
+ const { name: toolName, arguments: args } = params;
1569
+
1570
+ console.error(`[Stdio Wrapper] Handling local CLI tool: ${toolName}`);
1571
+
1572
+ try {
1573
+ let result;
1574
+
1575
+ switch (toolName) {
1576
+ case 'force_cli_detection':
1577
+ case 'polydev.force_cli_detection':
1578
+ result = await this.localForceCliDetection(args);
1579
+ break;
1580
+
1581
+ case 'get_cli_status':
1582
+ case 'polydev.get_cli_status':
1583
+ result = await this.localGetCliStatus(args);
1584
+ break;
1585
+
1586
+ case 'send_cli_prompt':
1587
+ case 'polydev.send_cli_prompt':
1588
+ result = await this.localSendCliPrompt(args);
1589
+ break;
1590
+
1591
+ default:
1592
+ throw new Error(`Unknown CLI tool: ${toolName}`);
1593
+ }
1594
+
1595
+ return {
1596
+ jsonrpc: '2.0',
1597
+ id,
1598
+ result: {
1599
+ content: [
1600
+ {
1601
+ type: 'text',
1602
+ text: this.formatCliResponse(result)
1603
+ }
1604
+ ]
1605
+ }
1606
+ };
1607
+
1608
+ } catch (error) {
1609
+ console.error(`[Stdio Wrapper] CLI tool error:`, error);
1610
+ return {
1611
+ jsonrpc: '2.0',
1612
+ id,
1613
+ error: {
1614
+ code: -32603,
1615
+ message: error.message
1616
+ }
1617
+ };
1618
+ }
1619
+ }
1620
+
1621
+ /**
1622
+ * Local CLI detection implementation with database updates
1623
+ */
1624
+ async localForceCliDetection(args) {
1625
+ console.error(`[Stdio Wrapper] Local CLI detection with model detection started`);
1626
+
1627
+ try {
1628
+ const providerId = args.provider_id; // Optional - detect specific provider
1629
+
1630
+ // Force detection using CLI Manager (no remote API calls)
1631
+ const results = await this.cliManager.forceCliDetection(providerId);
1632
+ console.error(`[Stdio Wrapper] CLI detection results:`, JSON.stringify(results, null, 2));
1633
+
1634
+ // Save status locally to file-based cache
1635
+ await this.saveLocalCliStatus(results);
1636
+
1637
+ // Update database with CLI status
1638
+ await this.updateCliStatusInDatabase(results);
1639
+
1640
+ return {
1641
+ success: true,
1642
+ results,
1643
+ message: `Local CLI detection completed for ${providerId || 'all providers'}`,
1644
+ timestamp: new Date().toISOString(),
1645
+ local_only: true
1646
+ };
1647
+
1648
+ } catch (error) {
1649
+ console.error('[Stdio Wrapper] Local CLI detection error:', error);
1650
+ return {
1651
+ success: false,
1652
+ error: error.message,
1653
+ timestamp: new Date().toISOString(),
1654
+ local_only: true
1655
+ };
1656
+ }
1657
+ }
1658
+
1659
+ /**
1660
+ * Local CLI status retrieval
1661
+ */
1662
+ async localGetCliStatus(args) {
1663
+ console.error(`[Stdio Wrapper] Local CLI status retrieval`);
1664
+
1665
+ try {
1666
+ const providerId = args.provider_id;
1667
+ let results = {};
1668
+
1669
+ if (providerId) {
1670
+ // Get specific provider status
1671
+ const status = await this.cliManager.getCliStatus(providerId);
1672
+ results = status;
1673
+ } else {
1674
+ // Get all providers status
1675
+ const providers = this.cliManager.getProviders();
1676
+ for (const provider of providers) {
1677
+ const status = await this.cliManager.getCliStatus(provider.id);
1678
+ results[provider.id] = status;
1679
+ }
1680
+ }
1681
+
1682
+ // Update database with current status
1683
+ await this.updateCliStatusInDatabase(results);
1684
+
1685
+ return {
1686
+ success: true,
1687
+ results,
1688
+ message: 'Local CLI status retrieved successfully',
1689
+ timestamp: new Date().toISOString(),
1690
+ local_only: true
1691
+ };
1692
+
1693
+ } catch (error) {
1694
+ console.error('[Stdio Wrapper] Local CLI status error:', error);
1695
+ return {
1696
+ success: false,
1697
+ error: error.message,
1698
+ timestamp: new Date().toISOString(),
1699
+ local_only: true
1700
+ };
1701
+ }
1702
+ }
1703
+
1424
1704
  /**
1425
1705
  * Get the CLI ID to exclude based on the current IDE client.
1426
1706
  * Prevents recursive calls (e.g., Claude Code calling claude_code CLI which spawns another Claude Code).
@@ -1428,9 +1708,9 @@ Error: ${error.message}`
1428
1708
  */
1429
1709
  getExcludedCliForCurrentIDE() {
1430
1710
  if (!this.clientInfo?.name) return null;
1431
-
1711
+
1432
1712
  const clientName = this.clientInfo.name.toLowerCase();
1433
-
1713
+
1434
1714
  // Map known IDE client names to their corresponding CLI IDs
1435
1715
  // These are the clientInfo.name values sent during MCP initialize handshake
1436
1716
  const ideToCliMap = {
@@ -1439,8 +1719,7 @@ Error: ${error.message}`
1439
1719
  'claude_code': 'claude_code',
1440
1720
  'claude code': 'claude_code',
1441
1721
  '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
1722
+ // Cursor uses Claude under the hood but is a separate IDE — don't exclude
1444
1723
  // Gemini CLI / Google IDX
1445
1724
  'gemini-cli': 'gemini_cli',
1446
1725
  'gemini_cli': 'gemini_cli',
@@ -1449,17 +1728,17 @@ Error: ${error.message}`
1449
1728
  'codex_cli': 'codex_cli',
1450
1729
  'codex': 'codex_cli',
1451
1730
  };
1452
-
1731
+
1453
1732
  // Direct match first
1454
1733
  if (ideToCliMap[clientName]) {
1455
1734
  return ideToCliMap[clientName];
1456
1735
  }
1457
-
1736
+
1458
1737
  // Fuzzy match: check if client name contains known patterns
1459
1738
  if (clientName.includes('claude')) return 'claude_code';
1460
1739
  if (clientName.includes('gemini')) return 'gemini_cli';
1461
1740
  if (clientName.includes('codex')) return 'codex_cli';
1462
-
1741
+
1463
1742
  return null;
1464
1743
  }
1465
1744
 
@@ -1548,17 +1827,17 @@ Error: ${error.message}`
1548
1827
 
1549
1828
  // CLI priority order: Claude Code first, then Gemini, then Codex
1550
1829
  const cliPriorityOrder = ['claude_code', 'gemini_cli', 'codex_cli'];
1551
-
1830
+
1552
1831
  // Detect if we should exclude the current IDE's CLI to avoid recursive calls
1553
1832
  const excludedCli = this.getExcludedCliForCurrentIDE();
1554
1833
  if (excludedCli) {
1555
1834
  console.error(`[Stdio Wrapper] Excluding CLI '${excludedCli}' (current IDE: ${this.clientInfo?.name}) to avoid recursive calls`);
1556
1835
  }
1557
-
1836
+
1558
1837
  // Build merged provider list: CLIs first, then API-only
1559
1838
  const finalProviders = [];
1560
1839
  const usedProviderNames = new Set();
1561
-
1840
+
1562
1841
  // STEP 1: Add ALL available CLIs (in priority order) - they're FREE
1563
1842
  // Don't limit to maxPerspectives here — we run all CLIs in parallel
1564
1843
  // and take the first maxPerspectives successes (fast-collect pattern)
@@ -1584,7 +1863,7 @@ Error: ${error.message}`
1584
1863
 
1585
1864
  finalProviders.push({
1586
1865
  provider: providerName,
1587
- model: configuredProvider?.model || null, // null = use CLI default model
1866
+ model: configuredProvider?.model || null, // null = use CLI's default model
1588
1867
  cliId: cliId,
1589
1868
  source: 'cli',
1590
1869
  tier: null // CLIs don't use credits tiers
@@ -1626,31 +1905,41 @@ Error: ${error.message}`
1626
1905
  if (cliProviderEntries.length > 0) {
1627
1906
  const cliPromises = cliProviderEntries.map(async (providerEntry) => {
1628
1907
  try {
1629
- const model = providerEntry.model || modelPreferences[providerEntry.cliId] || null;
1908
+ // ONLY use the model from providerEntry (which is filtered to user's own API keys, not credits)
1909
+ // Do NOT fall back to modelPreferences[cliId] — it may contain credits-tier model names
1910
+ // (e.g., 'gemini-3-flash') that cause ModelNotFoundError on the actual CLI
1911
+ const model = providerEntry.model || null;
1630
1912
  if (model) {
1631
1913
  console.error(`[Stdio Wrapper] Using model for ${providerEntry.cliId}: ${model}`);
1632
1914
  }
1633
- let result = await this.cliManager.sendCliPrompt(providerEntry.cliId, prompt, mode, gracefulTimeout, model);
1634
-
1915
+ // Use adaptive timeout based on historical response times (instead of fixed 240s)
1916
+ const cliTimeout = this.getAdaptiveTimeout(providerEntry.cliId, gracefulTimeout);
1917
+ let result = await this.cliManager.sendCliPrompt(providerEntry.cliId, prompt, mode, cliTimeout, model);
1918
+
1635
1919
  // If CLI failed with a model and the error suggests model issue, retry with CLI default
1636
1920
  if (!result.success && model) {
1637
1921
  const errorLower = (result.error || '').toLowerCase();
1638
- const isModelError = errorLower.includes('not found') ||
1639
- errorLower.includes('not supported') ||
1922
+ const isModelError = errorLower.includes('not found') ||
1923
+ errorLower.includes('not supported') ||
1640
1924
  errorLower.includes('invalid model') ||
1641
1925
  errorLower.includes('entity was not found') ||
1642
1926
  errorLower.includes('does not exist') ||
1643
1927
  errorLower.includes('unknown model');
1644
1928
  if (isModelError) {
1645
1929
  console.error(`[Stdio Wrapper] Model '${model}' failed for ${providerEntry.cliId}, retrying with CLI default...`);
1646
- result = await this.cliManager.sendCliPrompt(providerEntry.cliId, prompt, mode, gracefulTimeout, null);
1930
+ result = await this.cliManager.sendCliPrompt(providerEntry.cliId, prompt, mode, cliTimeout, null);
1647
1931
  }
1648
1932
  }
1649
-
1650
- return {
1651
- provider_id: providerEntry.cliId,
1933
+
1934
+ // Record response time for adaptive timeout calculation
1935
+ if (result.success && result.latency_ms) {
1936
+ this.recordCliResponseTime(providerEntry.cliId, result.latency_ms);
1937
+ }
1938
+
1939
+ return {
1940
+ provider_id: providerEntry.cliId,
1652
1941
  original_provider: providerEntry.provider,
1653
- ...result
1942
+ ...result
1654
1943
  };
1655
1944
  } catch (error) {
1656
1945
  console.error(`[Stdio Wrapper] CLI ${providerEntry.cliId} failed:`, error.message);
@@ -1664,7 +1953,7 @@ Error: ${error.message}`
1664
1953
  }
1665
1954
  });
1666
1955
 
1667
- // Fast-collect: resolve early once we have maxPerspectives successes OR all complete
1956
+ // Fast-collect: resolve once we have maxPerspectives successes OR all complete
1668
1957
  localResults = await this.collectFirstNSuccesses(cliPromises, maxPerspectives);
1669
1958
  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`);
1670
1959
  }
@@ -1946,17 +2235,6 @@ Error: ${error.message}`
1946
2235
  return this.getModelPreferenceForCli(providerId);
1947
2236
  }
1948
2237
 
1949
- /**
1950
- * Get default model name for a CLI tool (used when model not specified in result)
1951
- * These are just display labels - actual model selection is done by:
1952
- * 1. User's configured default_model in dashboard API keys
1953
- * 2. CLI tool's own default if no preference set
1954
- */
1955
- getDefaultModelForCli(providerId) {
1956
- // Prefer user's model preference if available
1957
- return this.getModelPreferenceForCli(providerId);
1958
- }
1959
-
1960
2238
  /**
1961
2239
  * Call remote perspectives for CLI prompts
1962
2240
  * Only calls remote APIs for providers NOT covered by successful local CLIs
@@ -2261,6 +2539,56 @@ Error: ${error.message}`
2261
2539
  }
2262
2540
  }
2263
2541
 
2542
+ /**
2543
+ * Load CLI response times from disk for adaptive timeouts
2544
+ */
2545
+ loadCliResponseTimes() {
2546
+ try {
2547
+ const timesFile = path.join(os.homedir(), '.polydev', 'cli-response-times.json');
2548
+ if (fs.existsSync(timesFile)) {
2549
+ this.cliResponseTimes = JSON.parse(fs.readFileSync(timesFile, 'utf8'));
2550
+ console.error(`[Stdio Wrapper] Loaded CLI response times from disk`);
2551
+ }
2552
+ } catch (e) {
2553
+ // Non-critical
2554
+ }
2555
+ }
2556
+
2557
+ /**
2558
+ * Record a CLI response time for adaptive timeout calculation
2559
+ */
2560
+ recordCliResponseTime(cliId, latencyMs) {
2561
+ if (!this.cliResponseTimes[cliId]) {
2562
+ this.cliResponseTimes[cliId] = [];
2563
+ }
2564
+ this.cliResponseTimes[cliId].push(latencyMs);
2565
+ // Keep only the last N entries
2566
+ if (this.cliResponseTimes[cliId].length > this.CLI_RESPONSE_HISTORY_SIZE) {
2567
+ this.cliResponseTimes[cliId] = this.cliResponseTimes[cliId].slice(-this.CLI_RESPONSE_HISTORY_SIZE);
2568
+ }
2569
+ // Save to disk (fire-and-forget)
2570
+ try {
2571
+ const timesFile = path.join(os.homedir(), '.polydev', 'cli-response-times.json');
2572
+ fs.writeFileSync(timesFile, JSON.stringify(this.cliResponseTimes));
2573
+ } catch (e) { /* non-critical */ }
2574
+ }
2575
+
2576
+ /**
2577
+ * Get adaptive timeout for a CLI based on historical response times
2578
+ * Returns timeout in ms (2.5x the average, with min 30s and max 240s)
2579
+ */
2580
+ getAdaptiveTimeout(cliId, defaultTimeout = 240000) {
2581
+ const times = this.cliResponseTimes[cliId];
2582
+ if (!times || times.length === 0) {
2583
+ return defaultTimeout; // No history, use default
2584
+ }
2585
+ const avg = times.reduce((a, b) => a + b, 0) / times.length;
2586
+ // 2.5x average, clamped between 30s and 240s
2587
+ const adaptive = Math.min(240000, Math.max(30000, Math.round(avg * 2.5)));
2588
+ console.error(`[Stdio Wrapper] Adaptive timeout for ${cliId}: ${adaptive}ms (avg: ${Math.round(avg)}ms from ${times.length} samples)`);
2589
+ return adaptive;
2590
+ }
2591
+
2264
2592
  /**
2265
2593
  * Load CLI status from local file cache
2266
2594
  */
@@ -2413,8 +2741,11 @@ Error: ${error.message}`
2413
2741
  * Format CLI response for MCP output
2414
2742
  */
2415
2743
  formatCliResponse(result) {
2744
+ // Handle string results (from combineAllCliAndPerspectives)
2745
+ if (typeof result === 'string') return result;
2746
+
2416
2747
  if (!result.success) {
2417
- return `❌ **CLI Error**\n\n${result.error}\n\n*Timestamp: ${result.timestamp}*`;
2748
+ return `❌ **CLI Error**\n\n${result.error || 'Unknown error'}\n\n*Timestamp: ${result.timestamp || new Date().toISOString()}*`;
2418
2749
  }
2419
2750
 
2420
2751
  // Handle combined CLI + perspectives response (single or multiple CLIs)
@@ -2456,7 +2787,7 @@ Error: ${error.message}`
2456
2787
  /**
2457
2788
  * Fetch user's model preferences from API keys
2458
2789
  * Returns a map of CLI provider -> default_model
2459
- * Also fetches and caches perspectives_per_message setting and allProviders list
2790
+ * Also fetches and caches perspectivesPerMessage setting and allProviders list
2460
2791
  */
2461
2792
  async fetchUserModelPreferences() {
2462
2793
  // Check cache first
@@ -2612,8 +2943,25 @@ Error: ${error.message}`
2612
2943
  // Only run CLI detection here if we already have a token
2613
2944
  // (If no token, CLI detection runs after login completes in runStartupFlow)
2614
2945
  if (this.userToken) {
2946
+ // Pre-seed CLI status from disk cache for instant availability
2947
+ // This eliminates the 3-7s startup penalty from shelling out to CLI --version
2948
+ try {
2949
+ const cachedStatus = await this.loadLocalCliStatus();
2950
+ if (cachedStatus && Object.keys(cachedStatus).length > 0) {
2951
+ console.error(`[Polydev] Pre-seeded CLI status from disk cache (${Object.keys(cachedStatus).length} providers)`);
2952
+ this.cliManager.setCachedStatus(cachedStatus);
2953
+ this._cliDetectionComplete = true; // Mark as complete so requests don't wait
2954
+ if (this._cliDetectionResolver) {
2955
+ this._cliDetectionResolver(); // Unblock any waiting requests
2956
+ }
2957
+ }
2958
+ } catch (e) {
2959
+ console.error('[Polydev] Disk cache pre-seed failed (will detect fresh):', e.message);
2960
+ }
2961
+
2962
+ // Run full CLI detection in background to refresh the cache
2615
2963
  console.error('[Polydev] Detecting local CLI tools...');
2616
-
2964
+
2617
2965
  this.localForceCliDetection({})
2618
2966
  .then(async () => {
2619
2967
  this._cliDetectionComplete = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polydev-ai",
3
- "version": "1.9.16",
3
+ "version": "1.9.18",
4
4
  "engines": {
5
5
  "node": ">=20.x <=22.x"
6
6
  },