@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 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: execSync10 } = await import("child_process");
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
- execSync10(`curl -sL "${url}" -o "${tempFile}"`, { stdio: "pipe" });
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
- execSync10(`tar -xzf "${tempFile}" -C "${destPath}" --strip-components=1`, { stdio: "pipe" });
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
- execSync10(`tar -xJf "${tempFile}" -C "${destPath}" --strip-components=1`, { stdio: "pipe" });
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
- execSync10(`unzip -o "${tempFile}" -d "${destPath}"`, { stdio: "pipe" });
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: execSync10 } = await import("child_process");
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
- execSync10(`unzip -o "${tempFile}" -d "${destPath}"`, { stdio: "pipe" });
788
+ execSync11(`unzip -o "${tempFile}" -d "${destPath}"`, { stdio: "pipe" });
789
789
  } else if (archiveType === "tar.xz") {
790
- execSync10(`tar -xJf "${tempFile}" -C "${destPath}"`, { stdio: "pipe" });
790
+ execSync11(`tar -xJf "${tempFile}" -C "${destPath}"`, { stdio: "pipe" });
791
791
  } else if (archiveType === "tar.gz" || archiveType === "tgz") {
792
- execSync10(`tar -xzf "${tempFile}" -C "${destPath}"`, { stdio: "pipe" });
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
- execSync10(`unzip -o "${tempFile}" -d "${destPath}"`, { stdio: "pipe" });
841
+ execSync11(`unzip -o "${tempFile}" -d "${destPath}"`, { stdio: "pipe" });
842
842
  } else if (archiveType === "tar.xz") {
843
- execSync10(`tar -xJf "${tempFile}" -C "${destPath}"${stripFlag}`, { stdio: "pipe" });
843
+ execSync11(`tar -xJf "${tempFile}" -C "${destPath}"${stripFlag}`, { stdio: "pipe" });
844
844
  } else if (archiveType === "tar.gz" || archiveType === "tgz") {
845
- execSync10(`tar -xzf "${tempFile}" -C "${destPath}"${stripFlag}`, { stdio: "pipe" });
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: execSync10 } = await import("child_process");
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
- execSync10(`"${npmCmd}" init -y`, { cwd: installPath, stdio: "pipe" });
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
- execSync10(`"${npmCmd}" install ${pkg.npmPackage} ${installFlags}`, { cwd: installPath, stdio: "pipe" });
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
- execSync10(postInstallCmd, { cwd: installPath, stdio: "pipe" });
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: execSync10 } = await import("child_process");
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
- execSync10(`"${npmCmd}" install`, { cwd: nodePath, stdio: "pipe" });
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: execSync10 } = require("child_process");
9390
- execSync10("uv --version", { stdio: "pipe" });
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: execSync10 } = await import("child_process");
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
- execSync10(`"${uvCmd}" venv "${installPath}/venv"`, { stdio: "pipe" });
9419
- execSync10(`"${uvCmd}" pip install --python "${installPath}/venv/bin/python" ${pipPackage}`, { stdio: "pipe" });
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
- execSync10(`"${pythonCmd}" -m venv "${installPath}/venv"`, { stdio: "pipe" });
9425
- execSync10(`"${installPath}/venv/bin/pip" install ${pipPackage}`, { stdio: "pipe" });
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: execSync10 } = await import("child_process");
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
- execSync10(`"${uvCmd}" venv "${pythonPath}/venv"`, { cwd: pythonPath, stdio: "pipe" });
9437
- execSync10(`"${uvCmd}" pip install --python "${venvPython}" -r requirements.txt`, { cwd: pythonPath, stdio: "pipe" });
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
- execSync10(`"${pythonCmd}" -m venv venv`, { cwd: pythonPath, stdio: "pipe" });
9442
+ execSync11(`"${pythonCmd}" -m venv venv`, { cwd: pythonPath, stdio: "pipe" });
9443
9443
  const pipCmd = isWindows ? ".\\venv\\Scripts\\pip" : "./venv/bin/pip";
9444
- execSync10(`${pipCmd} install -r requirements.txt`, { cwd: pythonPath, stdio: "pipe" });
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: execSync10 } = await import("child_process");
31646
+ const { execSync: execSync11 } = await import("child_process");
31647
31647
  try {
31648
- execSync10("which ollama", { stdio: "pipe" });
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: execSync10 } = await import("child_process");
31724
+ const { execSync: execSync11 } = await import("child_process");
31725
31725
  console.log("Pulling nomic-embed-text model...");
31726
- execSync10("ollama pull nomic-embed-text", { stdio: "inherit" });
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: execSync10 } = await import("child_process");
36346
- execSync10("ollama pull nomic-embed-text", { stdio: "inherit" });
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) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "1.0.0",
3
- "generated": "2026-01-10T22:42:24.427Z",
3
+ "generated": "2026-01-11T00:47:39.830Z",
4
4
  "packages": {
5
5
  "runtimes": [
6
6
  {
@@ -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 && !existing.process.killed) {
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.process.kill();
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.process.kill();
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.process.kill();
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.4",
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
- "better-sqlite3": "^12.5.0",
17
- "@learnrudi/core": "1.0.5",
18
- "@learnrudi/embeddings": "0.1.0",
19
- "@learnrudi/db": "1.0.2",
20
- "@learnrudi/env": "1.0.1",
21
- "@learnrudi/manifest": "1.0.0",
22
- "@learnrudi/mcp": "1.0.0",
23
- "@learnrudi/runner": "1.0.1",
24
- "@learnrudi/registry-client": "1.0.5",
25
- "@learnrudi/secrets": "1.0.1",
26
- "@learnrudi/utils": "1.0.0"
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
- "scripts": {
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
+ }