@learnrudi/cli 1.10.4 → 1.10.6
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/dist/index.cjs +61 -36
- package/dist/packages-manifest.json +1 -1
- package/dist/router-mcp.js +152 -5
- package/package.json +22 -21
package/dist/index.cjs
CHANGED
|
@@ -662,7 +662,7 @@ async function downloadDirectoryFromGitHub(dirUrl, destDir, onProgress) {
|
|
|
662
662
|
async function downloadRuntime(runtime, version, destPath, options = {}) {
|
|
663
663
|
const { onProgress } = options;
|
|
664
664
|
const platformArch = getPlatformArch2();
|
|
665
|
-
const { execSync:
|
|
665
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
666
666
|
const runtimeManifest = await loadRuntimeManifest(runtime);
|
|
667
667
|
const customDownload = runtimeManifest?.download?.[platformArch];
|
|
668
668
|
const tempDir = import_path3.default.join(PATHS2.cache, "downloads");
|
|
@@ -688,7 +688,7 @@ async function downloadRuntime(runtime, version, destPath, options = {}) {
|
|
|
688
688
|
const tempFile = import_path3.default.join(tempDir, `${runtime}-${version}-${platformArch}.download`);
|
|
689
689
|
try {
|
|
690
690
|
if (url.includes("github.com")) {
|
|
691
|
-
|
|
691
|
+
execSync11(`curl -sL "${url}" -o "${tempFile}"`, { stdio: "pipe" });
|
|
692
692
|
} else {
|
|
693
693
|
const response = await fetch(url, {
|
|
694
694
|
headers: {
|
|
@@ -709,13 +709,13 @@ async function downloadRuntime(runtime, version, destPath, options = {}) {
|
|
|
709
709
|
import_fs2.default.renameSync(tempFile, binaryPath);
|
|
710
710
|
import_fs2.default.chmodSync(binaryPath, 493);
|
|
711
711
|
} else if (downloadType === "tar.gz" || downloadType === "tgz") {
|
|
712
|
-
|
|
712
|
+
execSync11(`tar -xzf "${tempFile}" -C "${destPath}" --strip-components=1`, { stdio: "pipe" });
|
|
713
713
|
import_fs2.default.unlinkSync(tempFile);
|
|
714
714
|
} else if (downloadType === "tar.xz") {
|
|
715
|
-
|
|
715
|
+
execSync11(`tar -xJf "${tempFile}" -C "${destPath}" --strip-components=1`, { stdio: "pipe" });
|
|
716
716
|
import_fs2.default.unlinkSync(tempFile);
|
|
717
717
|
} else if (downloadType === "zip") {
|
|
718
|
-
|
|
718
|
+
execSync11(`unzip -o "${tempFile}" -d "${destPath}"`, { stdio: "pipe" });
|
|
719
719
|
import_fs2.default.unlinkSync(tempFile);
|
|
720
720
|
} else {
|
|
721
721
|
throw new Error(`Unsupported download type: ${downloadType}`);
|
|
@@ -756,7 +756,7 @@ async function downloadTool(toolName, destPath, options = {}) {
|
|
|
756
756
|
import_fs2.default.rmSync(destPath, { recursive: true });
|
|
757
757
|
}
|
|
758
758
|
import_fs2.default.mkdirSync(destPath, { recursive: true });
|
|
759
|
-
const { execSync:
|
|
759
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
760
760
|
const downloads = toolManifest.downloads?.[platformArch];
|
|
761
761
|
if (downloads && Array.isArray(downloads)) {
|
|
762
762
|
const downloadedUrls = /* @__PURE__ */ new Set();
|
|
@@ -785,11 +785,11 @@ async function downloadTool(toolName, destPath, options = {}) {
|
|
|
785
785
|
onProgress?.({ phase: "extracting", tool: toolName, binary: import_path3.default.basename(binary) });
|
|
786
786
|
const archiveType = type || guessArchiveType(urlFilename);
|
|
787
787
|
if (archiveType === "zip") {
|
|
788
|
-
|
|
788
|
+
execSync11(`unzip -o "${tempFile}" -d "${destPath}"`, { stdio: "pipe" });
|
|
789
789
|
} else if (archiveType === "tar.xz") {
|
|
790
|
-
|
|
790
|
+
execSync11(`tar -xJf "${tempFile}" -C "${destPath}"`, { stdio: "pipe" });
|
|
791
791
|
} else if (archiveType === "tar.gz" || archiveType === "tgz") {
|
|
792
|
-
|
|
792
|
+
execSync11(`tar -xzf "${tempFile}" -C "${destPath}"`, { stdio: "pipe" });
|
|
793
793
|
} else {
|
|
794
794
|
throw new Error(`Unsupported archive type: ${archiveType}`);
|
|
795
795
|
}
|
|
@@ -838,11 +838,11 @@ async function downloadTool(toolName, destPath, options = {}) {
|
|
|
838
838
|
const stripComponents = extractConfig.strip || 0;
|
|
839
839
|
const stripFlag = stripComponents > 0 ? ` --strip-components=${stripComponents}` : "";
|
|
840
840
|
if (archiveType === "zip") {
|
|
841
|
-
|
|
841
|
+
execSync11(`unzip -o "${tempFile}" -d "${destPath}"`, { stdio: "pipe" });
|
|
842
842
|
} else if (archiveType === "tar.xz") {
|
|
843
|
-
|
|
843
|
+
execSync11(`tar -xJf "${tempFile}" -C "${destPath}"${stripFlag}`, { stdio: "pipe" });
|
|
844
844
|
} else if (archiveType === "tar.gz" || archiveType === "tgz") {
|
|
845
|
-
|
|
845
|
+
execSync11(`tar -xzf "${tempFile}" -C "${destPath}"${stripFlag}`, { stdio: "pipe" });
|
|
846
846
|
} else {
|
|
847
847
|
throw new Error(`Unsupported archive type: ${archiveType}`);
|
|
848
848
|
}
|
|
@@ -8918,7 +8918,7 @@ async function installSinglePackage(pkg, options = {}) {
|
|
|
8918
8918
|
onProgress?.({ phase: "downloading", package: pkg.id });
|
|
8919
8919
|
if (pkg.npmPackage) {
|
|
8920
8920
|
try {
|
|
8921
|
-
const { execSync:
|
|
8921
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
8922
8922
|
if (!import_fs5.default.existsSync(installPath)) {
|
|
8923
8923
|
import_fs5.default.mkdirSync(installPath, { recursive: true });
|
|
8924
8924
|
}
|
|
@@ -8926,11 +8926,11 @@ async function installSinglePackage(pkg, options = {}) {
|
|
|
8926
8926
|
const resourcesPath = process.env.RESOURCES_PATH;
|
|
8927
8927
|
const npmCmd = resourcesPath ? import_path6.default.join(resourcesPath, "bundled-runtimes", "node", "bin", "npm") : "npm";
|
|
8928
8928
|
if (!import_fs5.default.existsSync(import_path6.default.join(installPath, "package.json"))) {
|
|
8929
|
-
|
|
8929
|
+
execSync11(`"${npmCmd}" init -y`, { cwd: installPath, stdio: "pipe" });
|
|
8930
8930
|
}
|
|
8931
8931
|
const shouldIgnoreScripts = pkg.source?.type === "npm" && !allowScripts;
|
|
8932
8932
|
const installFlags = shouldIgnoreScripts ? "--ignore-scripts --no-audit --no-fund" : "--no-audit --no-fund";
|
|
8933
|
-
|
|
8933
|
+
execSync11(`"${npmCmd}" install ${pkg.npmPackage} ${installFlags}`, { cwd: installPath, stdio: "pipe" });
|
|
8934
8934
|
let bins = pkg.bins;
|
|
8935
8935
|
if (!bins || bins.length === 0) {
|
|
8936
8936
|
bins = discoverNpmBins(installPath, pkg.npmPackage);
|
|
@@ -8951,7 +8951,7 @@ async function installSinglePackage(pkg, options = {}) {
|
|
|
8951
8951
|
/^npx\s+(\S+)/,
|
|
8952
8952
|
`"${import_path6.default.join(installPath, "node_modules", ".bin", "$1")}"`
|
|
8953
8953
|
);
|
|
8954
|
-
|
|
8954
|
+
execSync11(postInstallCmd, { cwd: installPath, stdio: "pipe" });
|
|
8955
8955
|
}
|
|
8956
8956
|
const scriptsDetected = hasInstallScripts(installPath, pkg.npmPackage);
|
|
8957
8957
|
const scriptsPolicy = installFlags.includes("--ignore-scripts") ? "ignore" : "allow";
|
|
@@ -9320,7 +9320,7 @@ async function updateAll(options = {}) {
|
|
|
9320
9320
|
return results;
|
|
9321
9321
|
}
|
|
9322
9322
|
async function installStackDependencies(stackPath, onProgress) {
|
|
9323
|
-
const { execSync:
|
|
9323
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
9324
9324
|
const nodePath = import_path6.default.join(stackPath, "node");
|
|
9325
9325
|
if (import_fs5.default.existsSync(nodePath)) {
|
|
9326
9326
|
const packageJsonPath = import_path6.default.join(nodePath, "package.json");
|
|
@@ -9328,7 +9328,7 @@ async function installStackDependencies(stackPath, onProgress) {
|
|
|
9328
9328
|
onProgress?.({ phase: "installing-deps", message: "Installing Node.js dependencies..." });
|
|
9329
9329
|
try {
|
|
9330
9330
|
const npmCmd = await findNpmExecutable();
|
|
9331
|
-
|
|
9331
|
+
execSync11(`"${npmCmd}" install`, { cwd: nodePath, stdio: "pipe" });
|
|
9332
9332
|
} catch (error) {
|
|
9333
9333
|
console.warn(`Warning: Failed to install Node.js dependencies: ${error.message}`);
|
|
9334
9334
|
}
|
|
@@ -9386,8 +9386,8 @@ function findUvExecutable() {
|
|
|
9386
9386
|
return uvPath;
|
|
9387
9387
|
}
|
|
9388
9388
|
try {
|
|
9389
|
-
const { execSync:
|
|
9390
|
-
|
|
9389
|
+
const { execSync: execSync11 } = require("child_process");
|
|
9390
|
+
execSync11("uv --version", { stdio: "pipe" });
|
|
9391
9391
|
return "uv";
|
|
9392
9392
|
} catch {
|
|
9393
9393
|
return null;
|
|
@@ -9411,37 +9411,37 @@ async function ensureUv(onProgress) {
|
|
|
9411
9411
|
return null;
|
|
9412
9412
|
}
|
|
9413
9413
|
async function installPythonPackage(installPath, pipPackage, onProgress) {
|
|
9414
|
-
const { execSync:
|
|
9414
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
9415
9415
|
const uvCmd = findUvExecutable();
|
|
9416
9416
|
if (uvCmd) {
|
|
9417
9417
|
onProgress?.({ phase: "installing", message: `uv pip install ${pipPackage}` });
|
|
9418
|
-
|
|
9419
|
-
|
|
9418
|
+
execSync11(`"${uvCmd}" venv "${installPath}/venv"`, { stdio: "pipe" });
|
|
9419
|
+
execSync11(`"${uvCmd}" pip install --python "${installPath}/venv/bin/python" ${pipPackage}`, { stdio: "pipe" });
|
|
9420
9420
|
return { usedUv: true };
|
|
9421
9421
|
} else {
|
|
9422
9422
|
onProgress?.({ phase: "installing", message: `pip install ${pipPackage}` });
|
|
9423
9423
|
const pythonCmd = await findPythonExecutable();
|
|
9424
|
-
|
|
9425
|
-
|
|
9424
|
+
execSync11(`"${pythonCmd}" -m venv "${installPath}/venv"`, { stdio: "pipe" });
|
|
9425
|
+
execSync11(`"${installPath}/venv/bin/pip" install ${pipPackage}`, { stdio: "pipe" });
|
|
9426
9426
|
return { usedUv: false };
|
|
9427
9427
|
}
|
|
9428
9428
|
}
|
|
9429
9429
|
async function installPythonRequirements(pythonPath, onProgress) {
|
|
9430
|
-
const { execSync:
|
|
9430
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
9431
9431
|
const uvCmd = findUvExecutable();
|
|
9432
9432
|
const isWindows = process.platform === "win32";
|
|
9433
9433
|
const venvPython = isWindows ? import_path6.default.join(pythonPath, "venv", "Scripts", "python.exe") : import_path6.default.join(pythonPath, "venv", "bin", "python");
|
|
9434
9434
|
if (uvCmd) {
|
|
9435
9435
|
onProgress?.({ phase: "installing-deps", message: "Installing Python dependencies with uv..." });
|
|
9436
|
-
|
|
9437
|
-
|
|
9436
|
+
execSync11(`"${uvCmd}" venv "${pythonPath}/venv"`, { cwd: pythonPath, stdio: "pipe" });
|
|
9437
|
+
execSync11(`"${uvCmd}" pip install --python "${venvPython}" -r requirements.txt`, { cwd: pythonPath, stdio: "pipe" });
|
|
9438
9438
|
return { usedUv: true };
|
|
9439
9439
|
} else {
|
|
9440
9440
|
onProgress?.({ phase: "installing-deps", message: "Installing Python dependencies..." });
|
|
9441
9441
|
const pythonCmd = await findPythonExecutable();
|
|
9442
|
-
|
|
9442
|
+
execSync11(`"${pythonCmd}" -m venv venv`, { cwd: pythonPath, stdio: "pipe" });
|
|
9443
9443
|
const pipCmd = isWindows ? ".\\venv\\Scripts\\pip" : "./venv/bin/pip";
|
|
9444
|
-
|
|
9444
|
+
execSync11(`${pipCmd} install -r requirements.txt`, { cwd: pythonPath, stdio: "pipe" });
|
|
9445
9445
|
return { usedUv: false };
|
|
9446
9446
|
}
|
|
9447
9447
|
}
|
|
@@ -31643,9 +31643,9 @@ async function checkProviderStatus() {
|
|
|
31643
31643
|
}
|
|
31644
31644
|
};
|
|
31645
31645
|
try {
|
|
31646
|
-
const { execSync:
|
|
31646
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
31647
31647
|
try {
|
|
31648
|
-
|
|
31648
|
+
execSync11("which ollama", { stdio: "pipe" });
|
|
31649
31649
|
status.ollama.installed = true;
|
|
31650
31650
|
} catch {
|
|
31651
31651
|
}
|
|
@@ -31721,9 +31721,9 @@ async function autoSetupOllama() {
|
|
|
31721
31721
|
};
|
|
31722
31722
|
}
|
|
31723
31723
|
try {
|
|
31724
|
-
const { execSync:
|
|
31724
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
31725
31725
|
console.log("Pulling nomic-embed-text model...");
|
|
31726
|
-
|
|
31726
|
+
execSync11("ollama pull nomic-embed-text", { stdio: "inherit" });
|
|
31727
31727
|
return { success: true, message: "Ollama configured with nomic-embed-text" };
|
|
31728
31728
|
} catch (err) {
|
|
31729
31729
|
return { success: false, message: `Failed to pull model: ${err.message}` };
|
|
@@ -36342,8 +36342,8 @@ async function ensureEmbeddingProvider(preferredProvider = "auto", options = {})
|
|
|
36342
36342
|
server.unref();
|
|
36343
36343
|
await new Promise((r2) => setTimeout(r2, 2e3));
|
|
36344
36344
|
console.log(" Pulling nomic-embed-text model (274MB)...");
|
|
36345
|
-
const { execSync:
|
|
36346
|
-
|
|
36345
|
+
const { execSync: execSync11 } = await import("child_process");
|
|
36346
|
+
execSync11("ollama pull nomic-embed-text", { stdio: "inherit" });
|
|
36347
36347
|
console.log(" \u2713 Model ready\n");
|
|
36348
36348
|
return await getProvider2("ollama");
|
|
36349
36349
|
} catch (err) {
|
|
@@ -40481,6 +40481,31 @@ function findStudioPath() {
|
|
|
40481
40481
|
return p2;
|
|
40482
40482
|
}
|
|
40483
40483
|
}
|
|
40484
|
+
if (platform === "darwin") {
|
|
40485
|
+
try {
|
|
40486
|
+
const result = (0, import_child_process13.execSync)(`mdfind "kMDItemCFBundleIdentifier == 'com.rudi.studio'" 2>/dev/null`, {
|
|
40487
|
+
encoding: "utf-8",
|
|
40488
|
+
timeout: 5e3
|
|
40489
|
+
}).trim();
|
|
40490
|
+
if (result) {
|
|
40491
|
+
const foundPath = result.split("\n")[0];
|
|
40492
|
+
if (import_fs29.default.existsSync(foundPath)) {
|
|
40493
|
+
return foundPath;
|
|
40494
|
+
}
|
|
40495
|
+
}
|
|
40496
|
+
const nameResult = (0, import_child_process13.execSync)(`mdfind "kMDItemDisplayName == 'RUDI Studio' && kMDItemContentType == 'com.apple.application-bundle'" 2>/dev/null`, {
|
|
40497
|
+
encoding: "utf-8",
|
|
40498
|
+
timeout: 5e3
|
|
40499
|
+
}).trim();
|
|
40500
|
+
if (nameResult) {
|
|
40501
|
+
const foundPath = nameResult.split("\n")[0];
|
|
40502
|
+
if (import_fs29.default.existsSync(foundPath)) {
|
|
40503
|
+
return foundPath;
|
|
40504
|
+
}
|
|
40505
|
+
}
|
|
40506
|
+
} catch {
|
|
40507
|
+
}
|
|
40508
|
+
}
|
|
40484
40509
|
return null;
|
|
40485
40510
|
}
|
|
40486
40511
|
function getStudioVersion(studioPath) {
|
package/dist/router-mcp.js
CHANGED
|
@@ -32,6 +32,16 @@ const TOOL_INDEX_PATH = path.join(RUDI_HOME, 'cache', 'tool-index.json');
|
|
|
32
32
|
|
|
33
33
|
const REQUEST_TIMEOUT_MS = 30000;
|
|
34
34
|
const PROTOCOL_VERSION = '2024-11-05';
|
|
35
|
+
const DEFAULT_IDLE_TTL_MS = 10 * 60 * 1000;
|
|
36
|
+
const DEFAULT_MAX_SERVERS = 8;
|
|
37
|
+
const DEFAULT_CLEANUP_INTERVAL_MS = 30000;
|
|
38
|
+
const DEFAULT_FORCE_KILL_MS = 2000;
|
|
39
|
+
|
|
40
|
+
const IDLE_TTL_MS = readIntEnv('RUDI_ROUTER_IDLE_TTL_MS', DEFAULT_IDLE_TTL_MS);
|
|
41
|
+
const MAX_SERVERS = readIntEnv('RUDI_ROUTER_MAX_SERVERS', DEFAULT_MAX_SERVERS);
|
|
42
|
+
const CLEANUP_INTERVAL_MS = readIntEnv('RUDI_ROUTER_CLEANUP_INTERVAL_MS', DEFAULT_CLEANUP_INTERVAL_MS);
|
|
43
|
+
const FORCE_KILL_MS = readIntEnv('RUDI_ROUTER_FORCE_KILL_MS', DEFAULT_FORCE_KILL_MS);
|
|
44
|
+
const LIVE_TOOL_LIST = readBoolEnv('RUDI_ROUTER_LIVE_TOOL_LIST', false);
|
|
35
45
|
|
|
36
46
|
// =============================================================================
|
|
37
47
|
// STATE
|
|
@@ -45,6 +55,7 @@ let rudiConfig = null;
|
|
|
45
55
|
|
|
46
56
|
/** @type {Object | null} */
|
|
47
57
|
let toolIndex = null;
|
|
58
|
+
let cleanupTimer = null;
|
|
48
59
|
|
|
49
60
|
// =============================================================================
|
|
50
61
|
// TYPES (JSDoc)
|
|
@@ -57,6 +68,10 @@ let toolIndex = null;
|
|
|
57
68
|
* @property {Map<string|number, PendingRequest>} pending
|
|
58
69
|
* @property {string} buffer
|
|
59
70
|
* @property {boolean} initialized
|
|
71
|
+
* @property {string} stackId
|
|
72
|
+
* @property {number} spawnedAt
|
|
73
|
+
* @property {number} lastUsedAt
|
|
74
|
+
* @property {boolean} terminating
|
|
60
75
|
*/
|
|
61
76
|
|
|
62
77
|
/**
|
|
@@ -96,6 +111,102 @@ function debug(msg) {
|
|
|
96
111
|
}
|
|
97
112
|
}
|
|
98
113
|
|
|
114
|
+
// =============================================================================
|
|
115
|
+
// ENV & PROCESS HELPERS
|
|
116
|
+
// =============================================================================
|
|
117
|
+
|
|
118
|
+
function readIntEnv(name, fallback) {
|
|
119
|
+
const raw = process.env[name];
|
|
120
|
+
if (raw === undefined) return fallback;
|
|
121
|
+
const value = Number(raw);
|
|
122
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readBoolEnv(name, fallback) {
|
|
126
|
+
const raw = process.env[name];
|
|
127
|
+
if (raw === undefined) return fallback;
|
|
128
|
+
return ['1', 'true', 'yes', 'on'].includes(String(raw).toLowerCase());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function hasProcessExited(proc) {
|
|
132
|
+
return proc.exitCode !== null || proc.signalCode !== null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isProcessUsable(proc) {
|
|
136
|
+
return proc && !hasProcessExited(proc) && !proc.killed;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function markServerUsed(server) {
|
|
140
|
+
server.lastUsedAt = Date.now();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function rejectPending(server, reason) {
|
|
144
|
+
for (const [, pending] of server.pending) {
|
|
145
|
+
clearTimeout(pending.timeout);
|
|
146
|
+
pending.reject(new Error(reason));
|
|
147
|
+
}
|
|
148
|
+
server.pending.clear();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function terminateServer(stackId, server, reason) {
|
|
152
|
+
if (!server || server.terminating) return;
|
|
153
|
+
|
|
154
|
+
server.terminating = true;
|
|
155
|
+
serverPool.delete(stackId);
|
|
156
|
+
|
|
157
|
+
if (hasProcessExited(server.process)) {
|
|
158
|
+
rejectPending(server, `Stack ${stackId} exited`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
log(`Stopping stack ${stackId}: ${reason}`);
|
|
163
|
+
try {
|
|
164
|
+
server.process.kill('SIGTERM');
|
|
165
|
+
} catch {
|
|
166
|
+
// Ignore kill errors; process may already be gone.
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const killTimer = setTimeout(() => {
|
|
170
|
+
if (!hasProcessExited(server.process)) {
|
|
171
|
+
log(`Force killing stack ${stackId}`);
|
|
172
|
+
try {
|
|
173
|
+
server.process.kill('SIGKILL');
|
|
174
|
+
} catch {
|
|
175
|
+
// Ignore kill errors; process may already be gone.
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}, FORCE_KILL_MS);
|
|
179
|
+
if (killTimer.unref) killTimer.unref();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function cleanupServerPool() {
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
|
|
185
|
+
for (const [stackId, server] of serverPool) {
|
|
186
|
+
if (server.terminating) continue;
|
|
187
|
+
if (!isProcessUsable(server.process)) {
|
|
188
|
+
terminateServer(stackId, server, 'process-not-usable');
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (server.pending.size > 0) continue;
|
|
192
|
+
if (IDLE_TTL_MS > 0 && now - server.lastUsedAt > IDLE_TTL_MS) {
|
|
193
|
+
terminateServer(stackId, server, `idle ${Math.round((now - server.lastUsedAt) / 1000)}s`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (MAX_SERVERS > 0 && serverPool.size > MAX_SERVERS) {
|
|
198
|
+
const evictable = Array.from(serverPool.entries())
|
|
199
|
+
.filter(([, server]) => !server.terminating && server.pending.size === 0)
|
|
200
|
+
.sort((a, b) => a[1].lastUsedAt - b[1].lastUsedAt);
|
|
201
|
+
|
|
202
|
+
let index = 0;
|
|
203
|
+
while (serverPool.size > MAX_SERVERS && index < evictable.length) {
|
|
204
|
+
const [stackId, server] = evictable[index++];
|
|
205
|
+
terminateServer(stackId, server, 'pool-limit');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
99
210
|
// =============================================================================
|
|
100
211
|
// CONFIG & SECRETS
|
|
101
212
|
// =============================================================================
|
|
@@ -208,7 +319,11 @@ function spawnStackServer(stackId, stackConfig) {
|
|
|
208
319
|
rl,
|
|
209
320
|
pending: new Map(),
|
|
210
321
|
buffer: '',
|
|
211
|
-
initialized: false
|
|
322
|
+
initialized: false,
|
|
323
|
+
stackId,
|
|
324
|
+
spawnedAt: Date.now(),
|
|
325
|
+
lastUsedAt: Date.now(),
|
|
326
|
+
terminating: false
|
|
212
327
|
};
|
|
213
328
|
|
|
214
329
|
// Handle responses from stack
|
|
@@ -227,6 +342,7 @@ function spawnStackServer(stackId, stackConfig) {
|
|
|
227
342
|
if (pending) {
|
|
228
343
|
clearTimeout(pending.timeout);
|
|
229
344
|
server.pending.delete(response.id);
|
|
345
|
+
markServerUsed(server);
|
|
230
346
|
pending.resolve(response);
|
|
231
347
|
}
|
|
232
348
|
} catch (err) {
|
|
@@ -241,11 +357,13 @@ function spawnStackServer(stackId, stackConfig) {
|
|
|
241
357
|
|
|
242
358
|
childProcess.on('error', (err) => {
|
|
243
359
|
log(`Stack process error (${stackId}): ${err.message}`);
|
|
360
|
+
rejectPending(server, `Stack ${stackId} error: ${err.message}`);
|
|
244
361
|
serverPool.delete(stackId);
|
|
245
362
|
});
|
|
246
363
|
|
|
247
364
|
childProcess.on('exit', (code, signal) => {
|
|
248
365
|
debug(`Stack ${stackId} exited: code=${code}, signal=${signal}`);
|
|
366
|
+
rejectPending(server, `Stack ${stackId} exited (code=${code}, signal=${signal || 'none'})`);
|
|
249
367
|
rl.close();
|
|
250
368
|
serverPool.delete(stackId);
|
|
251
369
|
});
|
|
@@ -260,9 +378,13 @@ function spawnStackServer(stackId, stackConfig) {
|
|
|
260
378
|
*/
|
|
261
379
|
function getOrSpawnServer(stackId) {
|
|
262
380
|
const existing = serverPool.get(stackId);
|
|
263
|
-
if (existing &&
|
|
381
|
+
if (existing && isProcessUsable(existing.process) && !existing.terminating) {
|
|
382
|
+
markServerUsed(existing);
|
|
264
383
|
return existing;
|
|
265
384
|
}
|
|
385
|
+
if (existing) {
|
|
386
|
+
terminateServer(stackId, existing, 'stale');
|
|
387
|
+
}
|
|
266
388
|
|
|
267
389
|
const stackConfig = rudiConfig?.stacks?.[stackId];
|
|
268
390
|
if (!stackConfig) {
|
|
@@ -287,6 +409,11 @@ function getOrSpawnServer(stackId) {
|
|
|
287
409
|
*/
|
|
288
410
|
async function sendToStack(server, request, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
289
411
|
return new Promise((resolve, reject) => {
|
|
412
|
+
if (!isProcessUsable(server.process) || server.terminating) {
|
|
413
|
+
reject(new Error(`Stack ${server.stackId} is not available`));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
290
417
|
const timeout = setTimeout(() => {
|
|
291
418
|
server.pending.delete(request.id);
|
|
292
419
|
reject(new Error(`Request timeout: ${request.method}`));
|
|
@@ -296,6 +423,7 @@ async function sendToStack(server, request, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
296
423
|
|
|
297
424
|
const line = JSON.stringify(request) + '\n';
|
|
298
425
|
debug(`>> ${line.slice(0, 200)}`);
|
|
426
|
+
markServerUsed(server);
|
|
299
427
|
server.process.stdin?.write(line);
|
|
300
428
|
});
|
|
301
429
|
}
|
|
@@ -307,6 +435,7 @@ async function sendToStack(server, request, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
307
435
|
*/
|
|
308
436
|
async function initializeStack(server, stackId) {
|
|
309
437
|
if (server.initialized) return;
|
|
438
|
+
markServerUsed(server);
|
|
310
439
|
|
|
311
440
|
const initRequest = {
|
|
312
441
|
jsonrpc: '2.0',
|
|
@@ -350,6 +479,7 @@ async function initializeStack(server, stackId) {
|
|
|
350
479
|
*/
|
|
351
480
|
async function listTools() {
|
|
352
481
|
const tools = [];
|
|
482
|
+
const skippedStacks = [];
|
|
353
483
|
|
|
354
484
|
for (const [stackId, stackConfig] of Object.entries(rudiConfig?.stacks || {})) {
|
|
355
485
|
if (!stackConfig.installed) continue;
|
|
@@ -376,6 +506,10 @@ async function listTools() {
|
|
|
376
506
|
}
|
|
377
507
|
|
|
378
508
|
// 3. Fall back to querying the stack (slow, spawns server)
|
|
509
|
+
if (!LIVE_TOOL_LIST) {
|
|
510
|
+
skippedStacks.push(stackId);
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
379
513
|
try {
|
|
380
514
|
const server = getOrSpawnServer(stackId);
|
|
381
515
|
await initializeStack(server, stackId);
|
|
@@ -399,6 +533,10 @@ async function listTools() {
|
|
|
399
533
|
}
|
|
400
534
|
}
|
|
401
535
|
|
|
536
|
+
if (skippedStacks.length > 0) {
|
|
537
|
+
log(`Skipped live tools/list for ${skippedStacks.length} stacks (enable RUDI_ROUTER_LIVE_TOOL_LIST=1 or run "rudi index")`);
|
|
538
|
+
}
|
|
539
|
+
|
|
402
540
|
return tools;
|
|
403
541
|
}
|
|
404
542
|
|
|
@@ -521,6 +659,8 @@ async function handleRequest(request) {
|
|
|
521
659
|
*/
|
|
522
660
|
async function main() {
|
|
523
661
|
log('Starting RUDI Router MCP Server');
|
|
662
|
+
log(`Pool config: max=${MAX_SERVERS <= 0 ? 'unlimited' : MAX_SERVERS}, idleTTL=${IDLE_TTL_MS}ms, cleanup=${CLEANUP_INTERVAL_MS}ms`);
|
|
663
|
+
log(`Live tools/list: ${LIVE_TOOL_LIST ? 'enabled' : 'disabled'}`);
|
|
524
664
|
|
|
525
665
|
// Load config
|
|
526
666
|
rudiConfig = loadRudiConfig();
|
|
@@ -574,8 +714,9 @@ async function main() {
|
|
|
574
714
|
// Clean up all spawned servers
|
|
575
715
|
for (const [stackId, server] of serverPool) {
|
|
576
716
|
debug(`Killing stack ${stackId}`);
|
|
577
|
-
server
|
|
717
|
+
terminateServer(stackId, server, 'stdin-closed');
|
|
578
718
|
}
|
|
719
|
+
if (cleanupTimer) clearInterval(cleanupTimer);
|
|
579
720
|
process.exit(0);
|
|
580
721
|
});
|
|
581
722
|
|
|
@@ -583,18 +724,24 @@ async function main() {
|
|
|
583
724
|
process.on('SIGTERM', () => {
|
|
584
725
|
log('SIGTERM received, shutting down');
|
|
585
726
|
for (const [stackId, server] of serverPool) {
|
|
586
|
-
server
|
|
727
|
+
terminateServer(stackId, server, 'sigterm');
|
|
587
728
|
}
|
|
729
|
+
if (cleanupTimer) clearInterval(cleanupTimer);
|
|
588
730
|
process.exit(0);
|
|
589
731
|
});
|
|
590
732
|
|
|
591
733
|
process.on('SIGINT', () => {
|
|
592
734
|
log('SIGINT received, shutting down');
|
|
593
735
|
for (const [stackId, server] of serverPool) {
|
|
594
|
-
server
|
|
736
|
+
terminateServer(stackId, server, 'sigint');
|
|
595
737
|
}
|
|
738
|
+
if (cleanupTimer) clearInterval(cleanupTimer);
|
|
596
739
|
process.exit(0);
|
|
597
740
|
});
|
|
741
|
+
|
|
742
|
+
// Periodic pool cleanup (idle eviction, LRU capping)
|
|
743
|
+
cleanupTimer = setInterval(cleanupServerPool, CLEANUP_INTERVAL_MS);
|
|
744
|
+
if (cleanupTimer.unref) cleanupTimer.unref();
|
|
598
745
|
}
|
|
599
746
|
|
|
600
747
|
// Run
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@learnrudi/cli",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.6",
|
|
4
4
|
"description": "RUDI CLI - Install and manage MCP stacks, runtimes, and AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -12,18 +12,26 @@
|
|
|
12
12
|
"scripts",
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node src/index.js",
|
|
17
|
+
"prebuild": "node scripts/generate-manifest.js",
|
|
18
|
+
"build": "esbuild src/index.js --bundle --platform=node --format=cjs --outfile=dist/index.cjs --external:better-sqlite3 && cp src/router-mcp.js dist/router-mcp.js && cp src/packages-manifest.json dist/packages-manifest.json",
|
|
19
|
+
"postinstall": "node scripts/postinstall.js",
|
|
20
|
+
"prepublishOnly": "npm run build",
|
|
21
|
+
"test": "node --test src/__tests__/"
|
|
22
|
+
},
|
|
15
23
|
"dependencies": {
|
|
16
|
-
"
|
|
17
|
-
"@learnrudi/
|
|
18
|
-
"@learnrudi/embeddings": "
|
|
19
|
-
"@learnrudi/
|
|
20
|
-
"@learnrudi/
|
|
21
|
-
"@learnrudi/
|
|
22
|
-
"@learnrudi/
|
|
23
|
-
"@learnrudi/runner": "
|
|
24
|
-
"@learnrudi/
|
|
25
|
-
"@learnrudi/
|
|
26
|
-
"
|
|
24
|
+
"@learnrudi/core": "workspace:*",
|
|
25
|
+
"@learnrudi/db": "workspace:*",
|
|
26
|
+
"@learnrudi/embeddings": "workspace:*",
|
|
27
|
+
"@learnrudi/env": "workspace:*",
|
|
28
|
+
"@learnrudi/manifest": "workspace:*",
|
|
29
|
+
"@learnrudi/mcp": "workspace:*",
|
|
30
|
+
"@learnrudi/registry-client": "workspace:*",
|
|
31
|
+
"@learnrudi/runner": "workspace:*",
|
|
32
|
+
"@learnrudi/secrets": "workspace:*",
|
|
33
|
+
"@learnrudi/utils": "workspace:*",
|
|
34
|
+
"better-sqlite3": "^12.5.0"
|
|
27
35
|
},
|
|
28
36
|
"devDependencies": {
|
|
29
37
|
"esbuild": "^0.27.2"
|
|
@@ -49,12 +57,5 @@
|
|
|
49
57
|
"publishConfig": {
|
|
50
58
|
"access": "public"
|
|
51
59
|
},
|
|
52
|
-
"license": "MIT"
|
|
53
|
-
|
|
54
|
-
"start": "node src/index.js",
|
|
55
|
-
"prebuild": "node scripts/generate-manifest.js",
|
|
56
|
-
"build": "esbuild src/index.js --bundle --platform=node --format=cjs --outfile=dist/index.cjs --external:better-sqlite3 && cp src/router-mcp.js dist/router-mcp.js && cp src/packages-manifest.json dist/packages-manifest.json",
|
|
57
|
-
"postinstall": "node scripts/postinstall.js",
|
|
58
|
-
"test": "node --test src/__tests__/"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
60
|
+
"license": "MIT"
|
|
61
|
+
}
|