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 +10 -0
- package/mcp/stdio-wrapper.js +388 -40
- 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
|
@@ -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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
1930
|
+
result = await this.cliManager.sendCliPrompt(providerEntry.cliId, prompt, mode, cliTimeout, null);
|
|
1647
1931
|
}
|
|
1648
1932
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
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
|
|
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
|
|
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;
|