polydev-ai 1.9.17 → 1.9.19

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
@@ -839,14 +845,16 @@ Still waiting for login. Use /polydev:auth to check status after signing in.`
839
845
  }
840
846
  };
841
847
  } catch (error) {
842
- const cause = error.cause ? ` (${error.cause.code || error.cause.message || error.cause})` : '';
848
+ const errCode = error.cause?.code || error.code || '';
849
+ const cause = errCode ? ` (${errCode})` : (error.cause?.message ? ` (${error.cause.message})` : '');
850
+ console.error(`[Polydev] Re-auth failed: ${error.message}${cause}`);
843
851
  return {
844
852
  jsonrpc: '2.0',
845
853
  id,
846
854
  result: {
847
855
  content: [{
848
856
  type: 'text',
849
- text: `${reason}\n\nCould not reach server: ${error.message}${cause}\nPlease run: npx polydev-ai`
857
+ text: `${reason}\n\nCould not reach Polydev server: ${error.message}${cause}\n\nTroubleshooting:\n 1. Check your internet connection\n 2. Try: /polydev:login\n 3. Or run in terminal: npx polydev-ai`
850
858
  }],
851
859
  isError: true
852
860
  }
@@ -934,25 +942,9 @@ Still waiting for login. Use /polydev:auth to check status after signing in.`
934
942
  }
935
943
 
936
944
  if (!this.isAuthenticated || !this.userToken) {
937
- return {
938
- jsonrpc: '2.0',
939
- id,
940
- result: {
941
- content: [{
942
- type: 'text',
943
- text: `POLYDEV STATUS
944
- ==============
945
-
946
- Authentication: Not connected
947
-
948
- To login:
949
- 1. Use the "login" tool (opens browser)
950
- 2. Or run: npx polydev-ai
951
-
952
- Token will be saved automatically after login.`
953
- }]
954
- }
955
- };
945
+ // No token found anywhere — auto-trigger login instead of showing static message
946
+ console.error('[Polydev] No token found, auto-triggering login flow...');
947
+ return await this.triggerReAuth(id, 'Not authenticated. Opening browser for login...');
956
948
  }
957
949
 
958
950
  try {
@@ -1047,6 +1039,16 @@ Configure: https://polydev.ai/dashboard/models`
1047
1039
  return await this.triggerReAuth(id, 'Token invalid or expired.');
1048
1040
  }
1049
1041
  } catch (error) {
1042
+ // Categorize the error for clearer user messaging
1043
+ const errCode = error.cause?.code || error.code || '';
1044
+ const isNetwork = ['ECONNREFUSED', 'ENOTFOUND', 'ETIMEDOUT', 'ECONNRESET', 'UND_ERR_CONNECT_TIMEOUT', 'FETCH_ERROR'].includes(errCode)
1045
+ || error.message?.includes('fetch') || error.message?.includes('network');
1046
+ const errorDetail = isNetwork
1047
+ ? `Network error: ${error.message}${errCode ? ` (${errCode})` : ''}\nCheck your internet connection and try again.`
1048
+ : `Error: ${error.message}${errCode ? ` (${errCode})` : ''}`;
1049
+
1050
+ console.error(`[Polydev] Auth status check failed: ${error.message} (code: ${errCode})`);
1051
+
1050
1052
  return {
1051
1053
  jsonrpc: '2.0',
1052
1054
  id,
@@ -1056,9 +1058,12 @@ Configure: https://polydev.ai/dashboard/models`
1056
1058
  text: `POLYDEV STATUS
1057
1059
  ==============
1058
1060
 
1059
- Could not verify status (offline?)
1061
+ Could not verify authentication status.
1062
+
1063
+ ${errorDetail}
1060
1064
 
1061
- Error: ${error.message}`
1065
+ To retry: /polydev:auth
1066
+ To re-login: /polydev:login`
1062
1067
  }],
1063
1068
  isError: true
1064
1069
  }
@@ -1506,14 +1511,20 @@ Error: ${error.message}`
1506
1511
  // 2. Calls remote perspectives
1507
1512
  // 3. Combines results
1508
1513
  const result = await this.localSendCliPrompt(params.arguments);
1509
-
1514
+
1510
1515
  // 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')) {
1516
+ if (result?.result?.content?.[0]?.text?.includes('RE-AUTHENTICATION REQUIRED')) {
1513
1517
  // Auth failure already handled with re-auth response - pass through
1514
1518
  return result;
1515
1519
  }
1516
-
1520
+
1521
+ // localSendCliPrompt returns either:
1522
+ // - A string (from combineAllCliAndPerspectives) on success
1523
+ // - An object { success, error, timestamp } on exception
1524
+ const resultText = typeof result === 'string'
1525
+ ? result
1526
+ : (result.content || this.formatCliResponse(result));
1527
+
1517
1528
  return {
1518
1529
  jsonrpc: '2.0',
1519
1530
  id,
@@ -1819,7 +1830,7 @@ Error: ${error.message}`
1819
1830
  // Detect if we should exclude the current IDE's CLI to avoid recursive calls
1820
1831
  const excludedCli = this.getExcludedCliForCurrentIDE();
1821
1832
  if (excludedCli) {
1822
- console.error(`[Stdio Wrapper] Excluding CLI '${excludedCli}' (current IDE: ${this.clientInfo?.name}) to avoid recursive calls`);
1833
+ console.error(`[Stdio Wrapper] [CLI-FIRST] Skipping ${excludedCli} (same as current IDE would cause recursive call)`);
1823
1834
  }
1824
1835
 
1825
1836
  // Build merged provider list: CLIs first, then API-only
@@ -1889,35 +1900,45 @@ Error: ${error.message}`
1889
1900
  console.error(`[Stdio Wrapper] Provider breakdown: CLI=${cliProviderEntries.map(p => p.cliId).join(', ') || 'none'}, API-only=${apiOnlyProviders.map(p => p.provider).join(', ') || 'none'}`);
1890
1901
 
1891
1902
  // Run ALL CLI prompts concurrently with fast-collect pattern
1892
- // Resolves once we have maxPerspectives successes (don't wait for slow CLIs)
1903
+ // Resolves once we have maxPerspectives successes OR all complete
1893
1904
  if (cliProviderEntries.length > 0) {
1894
1905
  const cliPromises = cliProviderEntries.map(async (providerEntry) => {
1895
1906
  try {
1896
- const model = providerEntry.model || modelPreferences[providerEntry.cliId] || null;
1907
+ // ONLY use the model from providerEntry (which is filtered to user's own API keys, not credits)
1908
+ // Do NOT fall back to modelPreferences[cliId] — it may contain credits-tier model names
1909
+ // (e.g., 'gemini-3-flash') that cause ModelNotFoundError on the actual CLI
1910
+ const model = providerEntry.model || null;
1897
1911
  if (model) {
1898
1912
  console.error(`[Stdio Wrapper] Using model for ${providerEntry.cliId}: ${model}`);
1899
1913
  }
1900
- let result = await this.cliManager.sendCliPrompt(providerEntry.cliId, prompt, mode, gracefulTimeout, model);
1901
-
1914
+ // Use adaptive timeout based on historical response times (instead of fixed 240s)
1915
+ const cliTimeout = this.getAdaptiveTimeout(providerEntry.cliId, gracefulTimeout);
1916
+ let result = await this.cliManager.sendCliPrompt(providerEntry.cliId, prompt, mode, cliTimeout, model);
1917
+
1902
1918
  // If CLI failed with a model and the error suggests model issue, retry with CLI default
1903
1919
  if (!result.success && model) {
1904
1920
  const errorLower = (result.error || '').toLowerCase();
1905
- const isModelError = errorLower.includes('not found') ||
1906
- errorLower.includes('not supported') ||
1921
+ const isModelError = errorLower.includes('not found') ||
1922
+ errorLower.includes('not supported') ||
1907
1923
  errorLower.includes('invalid model') ||
1908
1924
  errorLower.includes('entity was not found') ||
1909
1925
  errorLower.includes('does not exist') ||
1910
1926
  errorLower.includes('unknown model');
1911
1927
  if (isModelError) {
1912
1928
  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);
1929
+ result = await this.cliManager.sendCliPrompt(providerEntry.cliId, prompt, mode, cliTimeout, null);
1914
1930
  }
1915
1931
  }
1916
-
1917
- return {
1918
- provider_id: providerEntry.cliId,
1932
+
1933
+ // Record response time for adaptive timeout calculation
1934
+ if (result.success && result.latency_ms) {
1935
+ this.recordCliResponseTime(providerEntry.cliId, result.latency_ms);
1936
+ }
1937
+
1938
+ return {
1939
+ provider_id: providerEntry.cliId,
1919
1940
  original_provider: providerEntry.provider,
1920
- ...result
1941
+ ...result
1921
1942
  };
1922
1943
  } catch (error) {
1923
1944
  console.error(`[Stdio Wrapper] CLI ${providerEntry.cliId} failed:`, error.message);
@@ -1931,7 +1952,7 @@ Error: ${error.message}`
1931
1952
  }
1932
1953
  });
1933
1954
 
1934
- // Fast-collect: resolve once we have maxPerspectives successes OR all complete
1955
+ // Fast-collect: resolve early once we have maxPerspectives successes OR all complete
1935
1956
  localResults = await this.collectFirstNSuccesses(cliPromises, maxPerspectives);
1936
1957
  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`);
1937
1958
  }
@@ -2213,17 +2234,6 @@ Error: ${error.message}`
2213
2234
  return this.getModelPreferenceForCli(providerId);
2214
2235
  }
2215
2236
 
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
2237
  /**
2228
2238
  * Call remote perspectives for CLI prompts
2229
2239
  * Only calls remote APIs for providers NOT covered by successful local CLIs
@@ -2528,6 +2538,56 @@ Error: ${error.message}`
2528
2538
  }
2529
2539
  }
2530
2540
 
2541
+ /**
2542
+ * Load CLI response times from disk for adaptive timeouts
2543
+ */
2544
+ loadCliResponseTimes() {
2545
+ try {
2546
+ const timesFile = path.join(os.homedir(), '.polydev', 'cli-response-times.json');
2547
+ if (fs.existsSync(timesFile)) {
2548
+ this.cliResponseTimes = JSON.parse(fs.readFileSync(timesFile, 'utf8'));
2549
+ console.error(`[Stdio Wrapper] Loaded CLI response times from disk`);
2550
+ }
2551
+ } catch (e) {
2552
+ // Non-critical
2553
+ }
2554
+ }
2555
+
2556
+ /**
2557
+ * Record a CLI response time for adaptive timeout calculation
2558
+ */
2559
+ recordCliResponseTime(cliId, latencyMs) {
2560
+ if (!this.cliResponseTimes[cliId]) {
2561
+ this.cliResponseTimes[cliId] = [];
2562
+ }
2563
+ this.cliResponseTimes[cliId].push(latencyMs);
2564
+ // Keep only the last N entries
2565
+ if (this.cliResponseTimes[cliId].length > this.CLI_RESPONSE_HISTORY_SIZE) {
2566
+ this.cliResponseTimes[cliId] = this.cliResponseTimes[cliId].slice(-this.CLI_RESPONSE_HISTORY_SIZE);
2567
+ }
2568
+ // Save to disk (fire-and-forget)
2569
+ try {
2570
+ const timesFile = path.join(os.homedir(), '.polydev', 'cli-response-times.json');
2571
+ fs.writeFileSync(timesFile, JSON.stringify(this.cliResponseTimes));
2572
+ } catch (e) { /* non-critical */ }
2573
+ }
2574
+
2575
+ /**
2576
+ * Get adaptive timeout for a CLI based on historical response times
2577
+ * Returns timeout in ms (2.5x the average, with min 30s and max 240s)
2578
+ */
2579
+ getAdaptiveTimeout(cliId, defaultTimeout = 240000) {
2580
+ const times = this.cliResponseTimes[cliId];
2581
+ if (!times || times.length === 0) {
2582
+ return defaultTimeout; // No history, use default
2583
+ }
2584
+ const avg = times.reduce((a, b) => a + b, 0) / times.length;
2585
+ // 2.5x average, clamped between 30s and 240s
2586
+ const adaptive = Math.min(240000, Math.max(30000, Math.round(avg * 2.5)));
2587
+ console.error(`[Stdio Wrapper] Adaptive timeout for ${cliId}: ${adaptive}ms (avg: ${Math.round(avg)}ms from ${times.length} samples)`);
2588
+ return adaptive;
2589
+ }
2590
+
2531
2591
  /**
2532
2592
  * Load CLI status from local file cache
2533
2593
  */
@@ -2603,7 +2663,7 @@ Error: ${error.message}`
2603
2663
  const staleProviders = [];
2604
2664
  for (const [providerId, status] of Object.entries(currentStatus)) {
2605
2665
  if (this.isStale(status)) {
2606
- const minutesOld = Math.floor((new Date().getTime() - new Date(status.last_checked).getTime()) / (1000 * 60));
2666
+ const minutesOld = Math.floor((now.getTime() - lastChecked.getTime()) / (1000 * 60));
2607
2667
  const timeout = this.getSmartTimeout(status);
2608
2668
  staleProviders.push({ providerId, minutesOld, timeout });
2609
2669
  }
@@ -2680,8 +2740,11 @@ Error: ${error.message}`
2680
2740
  * Format CLI response for MCP output
2681
2741
  */
2682
2742
  formatCliResponse(result) {
2743
+ // Handle string results (from combineAllCliAndPerspectives)
2744
+ if (typeof result === 'string') return result;
2745
+
2683
2746
  if (!result.success) {
2684
- return `❌ **CLI Error**\n\n${result.error}\n\n*Timestamp: ${result.timestamp}*`;
2747
+ return `❌ **CLI Error**\n\n${result.error || 'Unknown error'}\n\n*Timestamp: ${result.timestamp || new Date().toISOString()}*`;
2685
2748
  }
2686
2749
 
2687
2750
  // Handle combined CLI + perspectives response (single or multiple CLIs)
@@ -2879,8 +2942,25 @@ Error: ${error.message}`
2879
2942
  // Only run CLI detection here if we already have a token
2880
2943
  // (If no token, CLI detection runs after login completes in runStartupFlow)
2881
2944
  if (this.userToken) {
2945
+ // Pre-seed CLI status from disk cache for instant availability
2946
+ // This eliminates the 3-7s startup penalty from shelling out to CLI --version
2947
+ try {
2948
+ const cachedStatus = await this.loadLocalCliStatus();
2949
+ if (cachedStatus && Object.keys(cachedStatus).length > 0) {
2950
+ console.error(`[Polydev] Pre-seeded CLI status from disk cache (${Object.keys(cachedStatus).length} providers)`);
2951
+ this.cliManager.setCachedStatus(cachedStatus);
2952
+ this._cliDetectionComplete = true; // Mark as complete so requests don't wait
2953
+ if (this._cliDetectionResolver) {
2954
+ this._cliDetectionResolver(); // Unblock any waiting requests
2955
+ }
2956
+ }
2957
+ } catch (e) {
2958
+ console.error('[Polydev] Disk cache pre-seed failed (will detect fresh):', e.message);
2959
+ }
2960
+
2961
+ // Run full CLI detection in background to refresh the cache
2882
2962
  console.error('[Polydev] Detecting local CLI tools...');
2883
-
2963
+
2884
2964
  this.localForceCliDetection({})
2885
2965
  .then(async () => {
2886
2966
  this._cliDetectionComplete = true;
@@ -3072,16 +3152,18 @@ Dashboard: https://polydev.ai/dashboard`
3072
3152
  console.error(`Credits: ${credits} | Tier: ${tier}`);
3073
3153
  console.error('─'.repeat(50) + '\n');
3074
3154
  } else {
3075
- this.isAuthenticated = false;
3155
+ // Don't clear isAuthenticated here — handleGetAuthStatus has its own
3156
+ // verification and triggerReAuth logic. Clearing here causes a race condition
3157
+ // where startup verification can invalidate auth before the user checks status.
3076
3158
  console.error('─'.repeat(50));
3077
- console.error('Polydev - Token Invalid');
3159
+ console.error('Polydev - Token may be invalid');
3078
3160
  console.error('─'.repeat(50));
3079
- console.error('Your token may have expired.');
3161
+ console.error('Server returned non-OK. Will re-verify on next auth check.');
3080
3162
  console.error('Use the "login" tool or run: npx polydev-ai login');
3081
3163
  console.error('─'.repeat(50) + '\n');
3082
3164
  }
3083
3165
  } catch (error) {
3084
- console.error('[Polydev] Could not verify auth (offline?)');
3166
+ console.error('[Polydev] Could not verify auth (offline?):', error.message || 'unknown');
3085
3167
  }
3086
3168
  }
3087
3169
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polydev-ai",
3
- "version": "1.9.17",
3
+ "version": "1.9.19",
4
4
  "engines": {
5
5
  "node": ">=20.x <=22.x"
6
6
  },