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 +10 -0
- package/mcp/stdio-wrapper.js +140 -58
- package/package.json +1 -1
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);
|
package/mcp/stdio-wrapper.js
CHANGED
|
@@ -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
|
|
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}\
|
|
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
|
-
|
|
938
|
-
|
|
939
|
-
|
|
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
|
|
1061
|
+
Could not verify authentication status.
|
|
1062
|
+
|
|
1063
|
+
${errorDetail}
|
|
1060
1064
|
|
|
1061
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
1929
|
+
result = await this.cliManager.sendCliPrompt(providerEntry.cliId, prompt, mode, cliTimeout, null);
|
|
1914
1930
|
}
|
|
1915
1931
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
|
3159
|
+
console.error('Polydev - Token may be invalid');
|
|
3078
3160
|
console.error('─'.repeat(50));
|
|
3079
|
-
console.error('
|
|
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
|
|