polydev-ai 1.9.17 → 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);
@@ -304,6 +304,12 @@ class StdioMCPWrapper {
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
@@ -1506,14 +1512,20 @@ Error: ${error.message}`
1506
1512
  // 2. Calls remote perspectives
1507
1513
  // 3. Combines results
1508
1514
  const result = await this.localSendCliPrompt(params.arguments);
1509
-
1515
+
1510
1516
  // Check if result indicates auth failure (from forwardToRemoteServer 401 handling)
1511
- const resultText = result.content || this.formatCliResponse(result);
1512
- if (result.result?.content?.[0]?.text?.includes('RE-AUTHENTICATION REQUIRED')) {
1517
+ if (result?.result?.content?.[0]?.text?.includes('RE-AUTHENTICATION REQUIRED')) {
1513
1518
  // Auth failure already handled with re-auth response - pass through
1514
1519
  return result;
1515
1520
  }
1516
-
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
+
1517
1529
  return {
1518
1530
  jsonrpc: '2.0',
1519
1531
  id,
@@ -1893,31 +1905,41 @@ Error: ${error.message}`
1893
1905
  if (cliProviderEntries.length > 0) {
1894
1906
  const cliPromises = cliProviderEntries.map(async (providerEntry) => {
1895
1907
  try {
1896
- 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;
1897
1912
  if (model) {
1898
1913
  console.error(`[Stdio Wrapper] Using model for ${providerEntry.cliId}: ${model}`);
1899
1914
  }
1900
- let result = await this.cliManager.sendCliPrompt(providerEntry.cliId, prompt, mode, gracefulTimeout, model);
1901
-
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
+
1902
1919
  // If CLI failed with a model and the error suggests model issue, retry with CLI default
1903
1920
  if (!result.success && model) {
1904
1921
  const errorLower = (result.error || '').toLowerCase();
1905
- const isModelError = errorLower.includes('not found') ||
1906
- errorLower.includes('not supported') ||
1922
+ const isModelError = errorLower.includes('not found') ||
1923
+ errorLower.includes('not supported') ||
1907
1924
  errorLower.includes('invalid model') ||
1908
1925
  errorLower.includes('entity was not found') ||
1909
1926
  errorLower.includes('does not exist') ||
1910
1927
  errorLower.includes('unknown model');
1911
1928
  if (isModelError) {
1912
1929
  console.error(`[Stdio Wrapper] Model '${model}' failed for ${providerEntry.cliId}, retrying with CLI default...`);
1913
- result = await this.cliManager.sendCliPrompt(providerEntry.cliId, prompt, mode, gracefulTimeout, null);
1930
+ result = await this.cliManager.sendCliPrompt(providerEntry.cliId, prompt, mode, cliTimeout, null);
1914
1931
  }
1915
1932
  }
1916
-
1917
- return {
1918
- 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,
1919
1941
  original_provider: providerEntry.provider,
1920
- ...result
1942
+ ...result
1921
1943
  };
1922
1944
  } catch (error) {
1923
1945
  console.error(`[Stdio Wrapper] CLI ${providerEntry.cliId} failed:`, error.message);
@@ -2213,17 +2235,6 @@ Error: ${error.message}`
2213
2235
  return this.getModelPreferenceForCli(providerId);
2214
2236
  }
2215
2237
 
2216
- /**
2217
- * Get default model name for a CLI tool (used when model not specified in result)
2218
- * These are just display labels - actual model selection is done by:
2219
- * 1. User's configured default_model in dashboard API keys
2220
- * 2. CLI tool's own default if no preference set
2221
- */
2222
- getDefaultModelForCli(providerId) {
2223
- // Prefer user's model preference if available
2224
- return this.getModelPreferenceForCli(providerId);
2225
- }
2226
-
2227
2238
  /**
2228
2239
  * Call remote perspectives for CLI prompts
2229
2240
  * Only calls remote APIs for providers NOT covered by successful local CLIs
@@ -2528,6 +2539,56 @@ Error: ${error.message}`
2528
2539
  }
2529
2540
  }
2530
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
+
2531
2592
  /**
2532
2593
  * Load CLI status from local file cache
2533
2594
  */
@@ -2680,8 +2741,11 @@ Error: ${error.message}`
2680
2741
  * Format CLI response for MCP output
2681
2742
  */
2682
2743
  formatCliResponse(result) {
2744
+ // Handle string results (from combineAllCliAndPerspectives)
2745
+ if (typeof result === 'string') return result;
2746
+
2683
2747
  if (!result.success) {
2684
- 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()}*`;
2685
2749
  }
2686
2750
 
2687
2751
  // Handle combined CLI + perspectives response (single or multiple CLIs)
@@ -2879,8 +2943,25 @@ Error: ${error.message}`
2879
2943
  // Only run CLI detection here if we already have a token
2880
2944
  // (If no token, CLI detection runs after login completes in runStartupFlow)
2881
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
2882
2963
  console.error('[Polydev] Detecting local CLI tools...');
2883
-
2964
+
2884
2965
  this.localForceCliDetection({})
2885
2966
  .then(async () => {
2886
2967
  this._cliDetectionComplete = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polydev-ai",
3
- "version": "1.9.17",
3
+ "version": "1.9.18",
4
4
  "engines": {
5
5
  "node": ">=20.x <=22.x"
6
6
  },