polydev-ai 1.9.0 → 1.9.2
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/mcp/server.js +44 -0
- package/mcp/stdio-wrapper.js +150 -70
- package/package.json +1 -1
package/mcp/server.js
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// Ensure fetch is available (polyfill for environments where native fetch is missing)
|
|
4
|
+
if (typeof globalThis.fetch === 'undefined') {
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const { URL } = require('url');
|
|
8
|
+
|
|
9
|
+
globalThis.fetch = function fetchPolyfill(url, options = {}) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const parsed = new URL(url);
|
|
12
|
+
const mod = parsed.protocol === 'https:' ? https : http;
|
|
13
|
+
const reqOptions = {
|
|
14
|
+
hostname: parsed.hostname,
|
|
15
|
+
port: parsed.port,
|
|
16
|
+
path: parsed.pathname + parsed.search,
|
|
17
|
+
method: options.method || 'GET',
|
|
18
|
+
headers: options.headers || {}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const req = mod.request(reqOptions, (res) => {
|
|
22
|
+
const chunks = [];
|
|
23
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
24
|
+
res.on('end', () => {
|
|
25
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
26
|
+
resolve({
|
|
27
|
+
ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
28
|
+
status: res.statusCode,
|
|
29
|
+
statusText: res.statusMessage,
|
|
30
|
+
headers: { get: (name) => res.headers[name.toLowerCase()] },
|
|
31
|
+
json: () => Promise.resolve(JSON.parse(body)),
|
|
32
|
+
text: () => Promise.resolve(body)
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
req.on('error', reject);
|
|
38
|
+
req.setTimeout(30000, () => { req.destroy(); reject(new Error('Request timed out')); });
|
|
39
|
+
if (options.body) req.write(options.body);
|
|
40
|
+
req.end();
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
console.error('[Polydev MCP] fetch polyfill loaded (native fetch unavailable)');
|
|
45
|
+
}
|
|
46
|
+
|
|
3
47
|
// Register ts-node for TypeScript support
|
|
4
48
|
try {
|
|
5
49
|
require('ts-node/register');
|
package/mcp/stdio-wrapper.js
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// Ensure fetch is available (polyfill for environments where native fetch is missing)
|
|
4
|
+
if (typeof globalThis.fetch === 'undefined') {
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const { URL } = require('url');
|
|
8
|
+
|
|
9
|
+
globalThis.fetch = function fetchPolyfill(url, options = {}) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const parsed = new URL(url);
|
|
12
|
+
const mod = parsed.protocol === 'https:' ? https : http;
|
|
13
|
+
const reqOptions = {
|
|
14
|
+
hostname: parsed.hostname,
|
|
15
|
+
port: parsed.port,
|
|
16
|
+
path: parsed.pathname + parsed.search,
|
|
17
|
+
method: options.method || 'GET',
|
|
18
|
+
headers: options.headers || {}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const req = mod.request(reqOptions, (res) => {
|
|
22
|
+
const chunks = [];
|
|
23
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
24
|
+
res.on('end', () => {
|
|
25
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
26
|
+
resolve({
|
|
27
|
+
ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
28
|
+
status: res.statusCode,
|
|
29
|
+
statusText: res.statusMessage,
|
|
30
|
+
headers: { get: (name) => res.headers[name.toLowerCase()] },
|
|
31
|
+
json: () => Promise.resolve(JSON.parse(body)),
|
|
32
|
+
text: () => Promise.resolve(body)
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
req.on('error', reject);
|
|
38
|
+
req.setTimeout(30000, () => { req.destroy(); reject(new Error('Request timed out')); });
|
|
39
|
+
if (options.body) req.write(options.body);
|
|
40
|
+
req.end();
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
console.error('[Polydev MCP] fetch polyfill loaded (native fetch unavailable)');
|
|
45
|
+
}
|
|
46
|
+
|
|
3
47
|
// Lightweight stdio wrapper with local CLI functionality and remote Polydev MCP server fallback
|
|
4
48
|
const fs = require('fs');
|
|
5
49
|
const path = require('path');
|
|
@@ -1406,93 +1450,129 @@ Error: ${error.message}`
|
|
|
1406
1450
|
const result = await this.cliManager.sendCliPrompt(provider_id, prompt, mode, gracefulTimeout, model);
|
|
1407
1451
|
localResults = [{ provider_id, ...result }];
|
|
1408
1452
|
} else {
|
|
1409
|
-
// No specific provider -
|
|
1410
|
-
//
|
|
1411
|
-
|
|
1453
|
+
// No specific provider - SMART ROUTING: CLIs first (free), then API/credits
|
|
1454
|
+
// Priority order: Available CLIs > User's API keys > Credits tier models
|
|
1455
|
+
// If a configured API/credits model can be accessed through an available CLI, use CLI
|
|
1456
|
+
console.error(`[Stdio Wrapper] Smart routing: CLIs first, then API/credits (max: ${maxPerspectives})`);
|
|
1412
1457
|
|
|
1413
1458
|
// Get available CLIs for checking
|
|
1414
1459
|
const { available: availableClis } = await this.getAllAvailableProviders();
|
|
1415
1460
|
console.error(`[Stdio Wrapper] Available CLIs: ${availableClis.join(', ') || 'none'}`);
|
|
1416
1461
|
|
|
1417
|
-
// Use allProviders from API (
|
|
1418
|
-
// Falls back to CLI-only providers if allProviders not available
|
|
1462
|
+
// Use allProviders from API (user's configured providers from dashboard)
|
|
1419
1463
|
const allProviders = this.allProviders || [];
|
|
1420
1464
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1465
|
+
// CLI to provider name mapping
|
|
1466
|
+
const cliToProviderMap = {
|
|
1467
|
+
'claude_code': 'anthropic',
|
|
1468
|
+
'gemini_cli': 'google',
|
|
1469
|
+
'codex_cli': 'openai'
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
// Provider name normalization (matches model-preferences API)
|
|
1473
|
+
const normalizeProvider = (provider) => {
|
|
1474
|
+
const map = {
|
|
1475
|
+
'gemini': 'google',
|
|
1476
|
+
'google-ai': 'google',
|
|
1477
|
+
'x-ai': 'xai',
|
|
1478
|
+
'open-ai': 'openai',
|
|
1479
|
+
'anthropic-ai': 'anthropic'
|
|
1480
|
+
};
|
|
1481
|
+
const lower = provider.toLowerCase();
|
|
1482
|
+
return map[lower] || lower;
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
// CLI priority order: Claude Code first, then Gemini, then Codex
|
|
1486
|
+
const cliPriorityOrder = ['claude_code', 'gemini_cli', 'codex_cli'];
|
|
1487
|
+
|
|
1488
|
+
// Build merged provider list: CLIs first, then API-only
|
|
1489
|
+
const finalProviders = [];
|
|
1490
|
+
const usedProviderNames = new Set();
|
|
1491
|
+
|
|
1492
|
+
// STEP 1: Add ALL available CLIs first (in priority order) - they're FREE
|
|
1493
|
+
for (const cliId of cliPriorityOrder) {
|
|
1494
|
+
if (finalProviders.length >= maxPerspectives) break;
|
|
1495
|
+
if (!availableClis.includes(cliId)) continue;
|
|
1496
|
+
|
|
1497
|
+
const providerName = cliToProviderMap[cliId];
|
|
1498
|
+
usedProviderNames.add(providerName);
|
|
1499
|
+
|
|
1500
|
+
// Check if user has a configured model for this provider (from API keys or credits)
|
|
1501
|
+
// If so, use that model through CLI instead of burning API credits
|
|
1502
|
+
const configuredProvider = allProviders.find(p => {
|
|
1503
|
+
const normalized = normalizeProvider(p.provider);
|
|
1504
|
+
return normalized === providerName;
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
finalProviders.push({
|
|
1508
|
+
provider: providerName,
|
|
1509
|
+
model: configuredProvider?.model || null,
|
|
1510
|
+
cliId: cliId,
|
|
1511
|
+
source: 'cli',
|
|
1512
|
+
tier: configuredProvider?.tier || null
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
console.error(`[Stdio Wrapper] [CLI-FIRST] Added CLI: ${cliId} (${providerName})${configuredProvider ? ` with configured model: ${configuredProvider.model}` : ' with CLI default'}`);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// STEP 2: Fill remaining slots with API/credits providers (skip those already covered by CLI)
|
|
1519
|
+
for (const p of allProviders) {
|
|
1520
|
+
if (finalProviders.length >= maxPerspectives) break;
|
|
1521
|
+
|
|
1522
|
+
const normalizedProvider = normalizeProvider(p.provider);
|
|
1523
|
+
if (usedProviderNames.has(normalizedProvider)) {
|
|
1524
|
+
console.error(`[Stdio Wrapper] [CLI-FIRST] Skipping ${normalizedProvider} (already covered by CLI)`);
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1429
1527
|
|
|
1430
|
-
|
|
1528
|
+
usedProviderNames.add(normalizedProvider);
|
|
1529
|
+
finalProviders.push({
|
|
1530
|
+
...p,
|
|
1531
|
+
provider: normalizedProvider,
|
|
1532
|
+
source: 'api'
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
console.error(`[Stdio Wrapper] [CLI-FIRST] Added API/credits: ${normalizedProvider} (${p.model})${p.tier ? ` [${p.tier}]` : ''}`);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
console.error(`[Stdio Wrapper] Final provider list (${finalProviders.length}/${maxPerspectives}): ${finalProviders.map(p => `${p.provider}[${p.source}]`).join(', ')}`);
|
|
1539
|
+
|
|
1540
|
+
// Separate into CLI entries (local execution) vs API entries (remote execution)
|
|
1541
|
+
const cliProviderEntries = finalProviders.filter(p => p.source === 'cli');
|
|
1542
|
+
const apiOnlyProviders = finalProviders.filter(p => p.source === 'api');
|
|
1543
|
+
|
|
1544
|
+
console.error(`[Stdio Wrapper] Provider breakdown: CLI=${cliProviderEntries.map(p => p.cliId).join(', ') || 'none'}, API-only=${apiOnlyProviders.map(p => p.provider).join(', ') || 'none'}`);
|
|
1545
|
+
|
|
1546
|
+
// Run all CLI prompts concurrently
|
|
1547
|
+
if (cliProviderEntries.length > 0) {
|
|
1548
|
+
const cliPromises = cliProviderEntries.map(async (providerEntry) => {
|
|
1431
1549
|
try {
|
|
1432
|
-
const model = modelPreferences[
|
|
1433
|
-
|
|
1550
|
+
const model = providerEntry.model || modelPreferences[providerEntry.cliId] || null;
|
|
1551
|
+
if (model) {
|
|
1552
|
+
console.error(`[Stdio Wrapper] Using model for ${providerEntry.cliId}: ${model}`);
|
|
1553
|
+
}
|
|
1554
|
+
const result = await this.cliManager.sendCliPrompt(providerEntry.cliId, prompt, mode, gracefulTimeout, model);
|
|
1434
1555
|
return {
|
|
1435
|
-
provider_id:
|
|
1436
|
-
original_provider:
|
|
1556
|
+
provider_id: providerEntry.cliId,
|
|
1557
|
+
original_provider: providerEntry.provider,
|
|
1437
1558
|
...result
|
|
1438
1559
|
};
|
|
1439
1560
|
} catch (error) {
|
|
1440
|
-
|
|
1561
|
+
console.error(`[Stdio Wrapper] CLI ${providerEntry.cliId} failed:`, error.message);
|
|
1562
|
+
return {
|
|
1563
|
+
provider_id: providerEntry.cliId,
|
|
1564
|
+
original_provider: providerEntry.provider,
|
|
1565
|
+
success: false,
|
|
1566
|
+
error: error.message,
|
|
1567
|
+
latency_ms: gracefulTimeout
|
|
1568
|
+
};
|
|
1441
1569
|
}
|
|
1442
1570
|
});
|
|
1443
1571
|
localResults = await Promise.all(cliPromises);
|
|
1444
|
-
} else {
|
|
1445
|
-
// NEW: Use allProviders list (includes CLI + API-only providers)
|
|
1446
|
-
const providersToUse = allProviders.slice(0, maxPerspectives);
|
|
1447
|
-
console.error(`[Stdio Wrapper] Using ${providersToUse.length} providers from dashboard`);
|
|
1448
|
-
|
|
1449
|
-
// Separate into CLI providers vs API-only providers
|
|
1450
|
-
const cliProviderEntries = [];
|
|
1451
|
-
const apiOnlyProviders = [];
|
|
1452
|
-
|
|
1453
|
-
for (const p of providersToUse) {
|
|
1454
|
-
if (p.cliId && availableClis.includes(p.cliId)) {
|
|
1455
|
-
// Has CLI and CLI is available
|
|
1456
|
-
cliProviderEntries.push(p);
|
|
1457
|
-
} else {
|
|
1458
|
-
// No CLI or CLI unavailable - needs API
|
|
1459
|
-
apiOnlyProviders.push(p);
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
console.error(`[Stdio Wrapper] Provider breakdown: CLI=${cliProviderEntries.map(p => p.cliId).join(', ') || 'none'}, API-only=${apiOnlyProviders.map(p => p.provider).join(', ') || 'none'}`);
|
|
1464
|
-
|
|
1465
|
-
// Run all CLI prompts concurrently
|
|
1466
|
-
if (cliProviderEntries.length > 0) {
|
|
1467
|
-
const cliPromises = cliProviderEntries.map(async (providerEntry) => {
|
|
1468
|
-
try {
|
|
1469
|
-
const model = providerEntry.model || modelPreferences[providerEntry.cliId] || null;
|
|
1470
|
-
if (model) {
|
|
1471
|
-
console.error(`[Stdio Wrapper] Using model for ${providerEntry.cliId}: ${model}`);
|
|
1472
|
-
}
|
|
1473
|
-
const result = await this.cliManager.sendCliPrompt(providerEntry.cliId, prompt, mode, gracefulTimeout, model);
|
|
1474
|
-
return {
|
|
1475
|
-
provider_id: providerEntry.cliId,
|
|
1476
|
-
original_provider: providerEntry.provider,
|
|
1477
|
-
...result
|
|
1478
|
-
};
|
|
1479
|
-
} catch (error) {
|
|
1480
|
-
console.error(`[Stdio Wrapper] CLI ${providerEntry.cliId} failed:`, error.message);
|
|
1481
|
-
return {
|
|
1482
|
-
provider_id: providerEntry.cliId,
|
|
1483
|
-
original_provider: providerEntry.provider,
|
|
1484
|
-
success: false,
|
|
1485
|
-
error: error.message,
|
|
1486
|
-
latency_ms: gracefulTimeout
|
|
1487
|
-
};
|
|
1488
|
-
}
|
|
1489
|
-
});
|
|
1490
|
-
localResults = await Promise.all(cliPromises);
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
// Store API-only providers info for remote API call
|
|
1494
|
-
this._apiOnlyProviders = apiOnlyProviders;
|
|
1495
1572
|
}
|
|
1573
|
+
|
|
1574
|
+
// Store API-only providers info for remote API call
|
|
1575
|
+
this._apiOnlyProviders = apiOnlyProviders;
|
|
1496
1576
|
}
|
|
1497
1577
|
|
|
1498
1578
|
// Calculate how many successful local perspectives we got
|