@rainfall-devkit/sdk 0.1.8 → 0.2.1

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.
Files changed (58) hide show
  1. package/README.md +51 -0
  2. package/dist/chunk-7MRE4ZVI.mjs +662 -0
  3. package/dist/chunk-AQFC7YAX.mjs +27 -0
  4. package/dist/chunk-EI7SJH5K.mjs +85 -0
  5. package/dist/chunk-NTTAVKRT.mjs +89 -0
  6. package/dist/chunk-RVKW5KBT.mjs +269 -0
  7. package/dist/chunk-V5QWJVLC.mjs +662 -0
  8. package/dist/chunk-VDPKDC3R.mjs +869 -0
  9. package/dist/chunk-WOITG5TG.mjs +84 -0
  10. package/dist/chunk-XAHJQRBJ.mjs +269 -0
  11. package/dist/chunk-XEQ6U3JQ.mjs +269 -0
  12. package/dist/cli/index.js +3797 -632
  13. package/dist/cli/index.mjs +453 -36
  14. package/dist/config-7UT7GYSN.mjs +16 -0
  15. package/dist/config-DDTQQBN7.mjs +14 -0
  16. package/dist/config-MD45VGWD.mjs +14 -0
  17. package/dist/config-ZKNHII2A.mjs +8 -0
  18. package/dist/daemon/index.d.mts +168 -0
  19. package/dist/daemon/index.d.ts +168 -0
  20. package/dist/daemon/index.js +3182 -0
  21. package/dist/daemon/index.mjs +1548 -0
  22. package/dist/errors-BMPseAnM.d.mts +47 -0
  23. package/dist/errors-BMPseAnM.d.ts +47 -0
  24. package/dist/errors-CZdRoYyw.d.ts +332 -0
  25. package/dist/errors-Chjq1Mev.d.mts +332 -0
  26. package/dist/index.d.mts +249 -2
  27. package/dist/index.d.ts +249 -2
  28. package/dist/index.js +1247 -3
  29. package/dist/index.mjs +227 -2
  30. package/dist/listeners-B5Vy9Ao5.d.ts +372 -0
  31. package/dist/listeners-BbYIaNCs.d.mts +372 -0
  32. package/dist/listeners-CP2A9J_2.d.ts +372 -0
  33. package/dist/listeners-CTRSofnm.d.mts +372 -0
  34. package/dist/listeners-CYI-YwIF.d.mts +372 -0
  35. package/dist/listeners-DRwITBW_.d.mts +372 -0
  36. package/dist/listeners-DrMrvFT5.d.ts +372 -0
  37. package/dist/listeners-MNAnpZj-.d.mts +372 -0
  38. package/dist/listeners-PZI7iT85.d.ts +372 -0
  39. package/dist/listeners-QJeEtLbV.d.ts +372 -0
  40. package/dist/listeners-hp0Ib2Ox.d.ts +372 -0
  41. package/dist/listeners-jLwetUnx.d.mts +372 -0
  42. package/dist/mcp.d.mts +7 -2
  43. package/dist/mcp.d.ts +7 -2
  44. package/dist/mcp.js +92 -1
  45. package/dist/mcp.mjs +1 -1
  46. package/dist/sdk-4OvXPr8E.d.mts +1054 -0
  47. package/dist/sdk-4OvXPr8E.d.ts +1054 -0
  48. package/dist/sdk-CJ9g5lFo.d.mts +772 -0
  49. package/dist/sdk-CJ9g5lFo.d.ts +772 -0
  50. package/dist/sdk-CN1ezZrI.d.mts +1054 -0
  51. package/dist/sdk-CN1ezZrI.d.ts +1054 -0
  52. package/dist/sdk-DD1OeGRJ.d.mts +871 -0
  53. package/dist/sdk-DD1OeGRJ.d.ts +871 -0
  54. package/dist/sdk-Xw0BjsLd.d.mts +1054 -0
  55. package/dist/sdk-Xw0BjsLd.d.ts +1054 -0
  56. package/dist/types-GnRAfH-h.d.mts +489 -0
  57. package/dist/types-GnRAfH-h.d.ts +489 -0
  58. package/package.json +17 -5
@@ -0,0 +1,1548 @@
1
+ import {
2
+ RainfallDaemonContext,
3
+ RainfallListenerRegistry,
4
+ RainfallNetworkedExecutor
5
+ } from "../chunk-7MRE4ZVI.mjs";
6
+ import {
7
+ Rainfall
8
+ } from "../chunk-VDPKDC3R.mjs";
9
+
10
+ // src/daemon/index.ts
11
+ import { WebSocketServer } from "ws";
12
+ import express from "express";
13
+
14
+ // src/services/mcp-proxy.ts
15
+ import { WebSocket } from "ws";
16
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
17
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
18
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
19
+ import {
20
+ ListToolsResultSchema,
21
+ CallToolResultSchema,
22
+ ListResourcesResultSchema,
23
+ ReadResourceResultSchema,
24
+ ListPromptsResultSchema,
25
+ GetPromptResultSchema,
26
+ McpError
27
+ } from "@modelcontextprotocol/sdk/types.js";
28
+ var MCPProxyHub = class {
29
+ clients = /* @__PURE__ */ new Map();
30
+ options;
31
+ refreshTimer;
32
+ reconnectTimeouts = /* @__PURE__ */ new Map();
33
+ requestId = 0;
34
+ constructor(options = {}) {
35
+ this.options = {
36
+ debug: options.debug ?? false,
37
+ autoReconnect: options.autoReconnect ?? true,
38
+ reconnectDelay: options.reconnectDelay ?? 5e3,
39
+ toolTimeout: options.toolTimeout ?? 3e4,
40
+ refreshInterval: options.refreshInterval ?? 3e4
41
+ };
42
+ }
43
+ /**
44
+ * Initialize the MCP proxy hub
45
+ */
46
+ async initialize() {
47
+ this.log("\u{1F50C} Initializing MCP Proxy Hub...");
48
+ this.startRefreshTimer();
49
+ this.log("\u2705 MCP Proxy Hub initialized");
50
+ }
51
+ /**
52
+ * Shutdown the MCP proxy hub and disconnect all clients
53
+ */
54
+ async shutdown() {
55
+ this.log("\u{1F6D1} Shutting down MCP Proxy Hub...");
56
+ if (this.refreshTimer) {
57
+ clearInterval(this.refreshTimer);
58
+ this.refreshTimer = void 0;
59
+ }
60
+ for (const timeout of this.reconnectTimeouts.values()) {
61
+ clearTimeout(timeout);
62
+ }
63
+ this.reconnectTimeouts.clear();
64
+ const disconnectPromises = Array.from(this.clients.entries()).map(
65
+ async ([name, client]) => {
66
+ try {
67
+ await this.disconnectClient(name);
68
+ } catch (error) {
69
+ this.log(`Error disconnecting ${name}:`, error);
70
+ }
71
+ }
72
+ );
73
+ await Promise.allSettled(disconnectPromises);
74
+ this.clients.clear();
75
+ this.log("\u{1F44B} MCP Proxy Hub shut down");
76
+ }
77
+ /**
78
+ * Connect to an MCP server
79
+ */
80
+ async connectClient(config) {
81
+ const { name, transport } = config;
82
+ if (this.clients.has(name)) {
83
+ this.log(`Reconnecting client: ${name}`);
84
+ await this.disconnectClient(name);
85
+ }
86
+ this.log(`Connecting to MCP server: ${name} (${transport})...`);
87
+ try {
88
+ const client = new Client(
89
+ {
90
+ name: `rainfall-daemon-${name}`,
91
+ version: "0.2.0"
92
+ },
93
+ {
94
+ capabilities: {}
95
+ }
96
+ );
97
+ let lastErrorTime = 0;
98
+ client.onerror = (error) => {
99
+ const now = Date.now();
100
+ if (now - lastErrorTime > 5e3) {
101
+ this.log(`MCP Server Error (${name}):`, error.message);
102
+ lastErrorTime = now;
103
+ }
104
+ if (this.options.autoReconnect) {
105
+ this.scheduleReconnect(name, config);
106
+ }
107
+ };
108
+ let transportInstance;
109
+ if (transport === "stdio" && config.command) {
110
+ const env = {};
111
+ for (const [key, value] of Object.entries({ ...process.env, ...config.env })) {
112
+ if (value !== void 0) {
113
+ env[key] = value;
114
+ }
115
+ }
116
+ transportInstance = new StdioClientTransport({
117
+ command: config.command,
118
+ args: config.args,
119
+ env
120
+ });
121
+ } else if (transport === "http" && config.url) {
122
+ transportInstance = new StreamableHTTPClientTransport(
123
+ new URL(config.url),
124
+ {
125
+ requestInit: {
126
+ headers: config.headers
127
+ }
128
+ }
129
+ );
130
+ } else if (transport === "websocket" && config.url) {
131
+ transportInstance = new WebSocket(config.url);
132
+ await new Promise((resolve, reject) => {
133
+ transportInstance.on("open", () => resolve());
134
+ transportInstance.on("error", reject);
135
+ setTimeout(() => reject(new Error("WebSocket connection timeout")), 1e4);
136
+ });
137
+ } else {
138
+ throw new Error(`Invalid transport configuration for ${name}`);
139
+ }
140
+ await client.connect(transportInstance);
141
+ const toolsResult = await client.request(
142
+ {
143
+ method: "tools/list",
144
+ params: {}
145
+ },
146
+ ListToolsResultSchema
147
+ );
148
+ const tools = toolsResult.tools.map((tool) => ({
149
+ name: tool.name,
150
+ description: tool.description || "",
151
+ inputSchema: tool.inputSchema,
152
+ serverName: name
153
+ }));
154
+ const clientInfo = {
155
+ name,
156
+ client,
157
+ transport: transportInstance,
158
+ transportType: transport,
159
+ tools,
160
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
161
+ lastUsed: (/* @__PURE__ */ new Date()).toISOString(),
162
+ config,
163
+ status: "connected"
164
+ };
165
+ this.clients.set(name, clientInfo);
166
+ this.log(`\u2705 Connected to ${name} (${tools.length} tools)`);
167
+ this.printAvailableTools(name, tools);
168
+ return name;
169
+ } catch (error) {
170
+ const errorMessage = error instanceof Error ? error.message : String(error);
171
+ this.log(`\u274C Failed to connect to ${name}:`, errorMessage);
172
+ if (this.options.autoReconnect) {
173
+ this.scheduleReconnect(name, config);
174
+ }
175
+ throw error;
176
+ }
177
+ }
178
+ /**
179
+ * Disconnect a specific MCP client
180
+ */
181
+ async disconnectClient(name) {
182
+ const client = this.clients.get(name);
183
+ if (!client) return;
184
+ const timeout = this.reconnectTimeouts.get(name);
185
+ if (timeout) {
186
+ clearTimeout(timeout);
187
+ this.reconnectTimeouts.delete(name);
188
+ }
189
+ try {
190
+ await client.client.close();
191
+ if ("close" in client.transport && typeof client.transport.close === "function") {
192
+ await client.transport.close();
193
+ }
194
+ } catch (error) {
195
+ this.log(`Error closing client ${name}:`, error);
196
+ }
197
+ this.clients.delete(name);
198
+ this.log(`Disconnected from ${name}`);
199
+ }
200
+ /**
201
+ * Schedule a reconnection attempt
202
+ */
203
+ scheduleReconnect(name, config) {
204
+ if (this.reconnectTimeouts.has(name)) return;
205
+ const timeout = setTimeout(async () => {
206
+ this.reconnectTimeouts.delete(name);
207
+ this.log(`Attempting to reconnect to ${name}...`);
208
+ try {
209
+ await this.connectClient(config);
210
+ } catch (error) {
211
+ this.log(`Reconnection failed for ${name}`);
212
+ }
213
+ }, this.options.reconnectDelay);
214
+ this.reconnectTimeouts.set(name, timeout);
215
+ }
216
+ /**
217
+ * Call a tool on the appropriate MCP client
218
+ */
219
+ async callTool(toolName, args, options = {}) {
220
+ const timeout = options.timeout ?? this.options.toolTimeout;
221
+ let clientInfo;
222
+ let actualToolName = toolName;
223
+ if (options.namespace) {
224
+ clientInfo = this.clients.get(options.namespace);
225
+ if (!clientInfo) {
226
+ throw new Error(`Namespace '${options.namespace}' not found`);
227
+ }
228
+ const prefix = `${options.namespace}-`;
229
+ if (actualToolName.startsWith(prefix)) {
230
+ actualToolName = actualToolName.slice(prefix.length);
231
+ }
232
+ if (!clientInfo.tools.some((t) => t.name === actualToolName)) {
233
+ throw new Error(`Tool '${actualToolName}' not found in namespace '${options.namespace}'`);
234
+ }
235
+ } else {
236
+ for (const [, info] of this.clients) {
237
+ const tool = info.tools.find((t) => t.name === toolName);
238
+ if (tool) {
239
+ clientInfo = info;
240
+ break;
241
+ }
242
+ }
243
+ }
244
+ if (!clientInfo) {
245
+ throw new Error(`Tool '${toolName}' not found on any connected MCP server`);
246
+ }
247
+ const requestId = `req_${++this.requestId}`;
248
+ clientInfo.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
249
+ try {
250
+ this.log(`[${requestId}] Calling '${actualToolName}' on '${clientInfo.name}'`);
251
+ const result = await Promise.race([
252
+ clientInfo.client.request(
253
+ {
254
+ method: "tools/call",
255
+ params: {
256
+ name: actualToolName,
257
+ arguments: args
258
+ }
259
+ },
260
+ CallToolResultSchema
261
+ ),
262
+ new Promise(
263
+ (_, reject) => setTimeout(() => reject(new Error(`Tool call timeout after ${timeout}ms`)), timeout)
264
+ )
265
+ ]);
266
+ this.log(`[${requestId}] Completed successfully`);
267
+ return this.formatToolResult(result);
268
+ } catch (error) {
269
+ this.log(`[${requestId}] Failed:`, error instanceof Error ? error.message : error);
270
+ if (error instanceof McpError) {
271
+ throw new Error(`MCP Error (${toolName}): ${error.message} (code: ${error.code})`);
272
+ }
273
+ throw error;
274
+ }
275
+ }
276
+ /**
277
+ * Format MCP tool result for consistent output
278
+ */
279
+ formatToolResult(result) {
280
+ if (!result || !result.content) {
281
+ return "";
282
+ }
283
+ return result.content.map((item) => {
284
+ if (item.type === "text") {
285
+ return item.text || "";
286
+ } else if (item.type === "resource") {
287
+ return `[Resource: ${item.resource?.uri || "unknown"}]`;
288
+ } else if (item.type === "image") {
289
+ return `[Image: ${item.mimeType || "unknown"}]`;
290
+ } else if (item.type === "audio") {
291
+ return `[Audio: ${item.mimeType || "unknown"}]`;
292
+ } else {
293
+ return JSON.stringify(item);
294
+ }
295
+ }).join("\n");
296
+ }
297
+ /**
298
+ * Get all tools from all connected MCP clients
299
+ * Optionally with namespace prefix
300
+ */
301
+ getAllTools(options = {}) {
302
+ const allTools = [];
303
+ for (const [clientName, client] of this.clients) {
304
+ for (const tool of client.tools) {
305
+ if (options.namespacePrefix) {
306
+ allTools.push({
307
+ ...tool,
308
+ name: `${clientName}-${tool.name}`
309
+ });
310
+ } else {
311
+ allTools.push(tool);
312
+ }
313
+ }
314
+ }
315
+ return allTools;
316
+ }
317
+ /**
318
+ * Get tools from a specific client
319
+ */
320
+ getClientTools(clientName) {
321
+ const client = this.clients.get(clientName);
322
+ return client?.tools || [];
323
+ }
324
+ /**
325
+ * Get list of connected MCP clients
326
+ */
327
+ listClients() {
328
+ return Array.from(this.clients.entries()).map(([name, info]) => ({
329
+ name,
330
+ status: info.status,
331
+ toolCount: info.tools.length,
332
+ connectedAt: info.connectedAt,
333
+ lastUsed: info.lastUsed,
334
+ transportType: info.transportType
335
+ }));
336
+ }
337
+ /**
338
+ * Get client info by name
339
+ */
340
+ getClient(name) {
341
+ return this.clients.get(name);
342
+ }
343
+ /**
344
+ * Refresh tool lists from all connected clients
345
+ */
346
+ async refreshTools() {
347
+ for (const [name, client] of this.clients) {
348
+ try {
349
+ const toolsResult = await client.client.request(
350
+ {
351
+ method: "tools/list",
352
+ params: {}
353
+ },
354
+ ListToolsResultSchema
355
+ );
356
+ client.tools = toolsResult.tools.map((tool) => ({
357
+ name: tool.name,
358
+ description: tool.description || "",
359
+ inputSchema: tool.inputSchema,
360
+ serverName: name
361
+ }));
362
+ this.log(`Refreshed ${name}: ${client.tools.length} tools`);
363
+ } catch (error) {
364
+ this.log(`Failed to refresh tools for ${name}:`, error);
365
+ client.status = "error";
366
+ client.error = error instanceof Error ? error.message : String(error);
367
+ }
368
+ }
369
+ }
370
+ /**
371
+ * List resources from a specific client or all clients
372
+ */
373
+ async listResources(clientName) {
374
+ const results = [];
375
+ const clients = clientName ? [clientName] : Array.from(this.clients.keys());
376
+ for (const name of clients) {
377
+ const client = this.clients.get(name);
378
+ if (!client) continue;
379
+ try {
380
+ const result = await client.client.request(
381
+ {
382
+ method: "resources/list",
383
+ params: {}
384
+ },
385
+ ListResourcesResultSchema
386
+ );
387
+ results.push({
388
+ clientName: name,
389
+ resources: result.resources
390
+ });
391
+ } catch (error) {
392
+ this.log(`Failed to list resources for ${name}:`, error);
393
+ }
394
+ }
395
+ return results;
396
+ }
397
+ /**
398
+ * Read a resource from a specific client
399
+ */
400
+ async readResource(uri, clientName) {
401
+ if (clientName) {
402
+ const client = this.clients.get(clientName);
403
+ if (!client) {
404
+ throw new Error(`Client '${clientName}' not found`);
405
+ }
406
+ const result = await client.client.request(
407
+ {
408
+ method: "resources/read",
409
+ params: { uri }
410
+ },
411
+ ReadResourceResultSchema
412
+ );
413
+ return result;
414
+ } else {
415
+ for (const [name, client] of this.clients) {
416
+ try {
417
+ const result = await client.client.request(
418
+ {
419
+ method: "resources/read",
420
+ params: { uri }
421
+ },
422
+ ReadResourceResultSchema
423
+ );
424
+ return { clientName: name, ...result };
425
+ } catch {
426
+ }
427
+ }
428
+ throw new Error(`Resource '${uri}' not found on any client`);
429
+ }
430
+ }
431
+ /**
432
+ * List prompts from a specific client or all clients
433
+ */
434
+ async listPrompts(clientName) {
435
+ const results = [];
436
+ const clients = clientName ? [clientName] : Array.from(this.clients.keys());
437
+ for (const name of clients) {
438
+ const client = this.clients.get(name);
439
+ if (!client) continue;
440
+ try {
441
+ const result = await client.client.request(
442
+ {
443
+ method: "prompts/list",
444
+ params: {}
445
+ },
446
+ ListPromptsResultSchema
447
+ );
448
+ results.push({
449
+ clientName: name,
450
+ prompts: result.prompts
451
+ });
452
+ } catch (error) {
453
+ this.log(`Failed to list prompts for ${name}:`, error);
454
+ }
455
+ }
456
+ return results;
457
+ }
458
+ /**
459
+ * Get a prompt from a specific client
460
+ */
461
+ async getPrompt(name, args, clientName) {
462
+ if (clientName) {
463
+ const client = this.clients.get(clientName);
464
+ if (!client) {
465
+ throw new Error(`Client '${clientName}' not found`);
466
+ }
467
+ const result = await client.client.request(
468
+ {
469
+ method: "prompts/get",
470
+ params: { name, arguments: args }
471
+ },
472
+ GetPromptResultSchema
473
+ );
474
+ return result;
475
+ } else {
476
+ for (const [cName, client] of this.clients) {
477
+ try {
478
+ const result = await client.client.request(
479
+ {
480
+ method: "prompts/get",
481
+ params: { name, arguments: args }
482
+ },
483
+ GetPromptResultSchema
484
+ );
485
+ return { clientName: cName, ...result };
486
+ } catch {
487
+ }
488
+ }
489
+ throw new Error(`Prompt '${name}' not found on any client`);
490
+ }
491
+ }
492
+ /**
493
+ * Health check for all connected clients
494
+ */
495
+ async healthCheck() {
496
+ const results = /* @__PURE__ */ new Map();
497
+ for (const [name, client] of this.clients) {
498
+ try {
499
+ const startTime = Date.now();
500
+ await client.client.request(
501
+ {
502
+ method: "tools/list",
503
+ params: {}
504
+ },
505
+ ListToolsResultSchema
506
+ );
507
+ results.set(name, {
508
+ status: "healthy",
509
+ responseTime: Date.now() - startTime
510
+ });
511
+ } catch (error) {
512
+ results.set(name, {
513
+ status: "unhealthy",
514
+ responseTime: 0,
515
+ error: error instanceof Error ? error.message : String(error)
516
+ });
517
+ if (this.options.autoReconnect) {
518
+ this.scheduleReconnect(name, client.config);
519
+ }
520
+ }
521
+ }
522
+ return results;
523
+ }
524
+ /**
525
+ * Start the automatic refresh timer
526
+ */
527
+ startRefreshTimer() {
528
+ if (this.refreshTimer) {
529
+ clearInterval(this.refreshTimer);
530
+ }
531
+ if (this.options.refreshInterval > 0) {
532
+ this.refreshTimer = setInterval(async () => {
533
+ try {
534
+ await this.refreshTools();
535
+ } catch (error) {
536
+ this.log("Auto-refresh failed:", error);
537
+ }
538
+ }, this.options.refreshInterval);
539
+ }
540
+ }
541
+ /**
542
+ * Print available tools for a client
543
+ */
544
+ printAvailableTools(clientName, tools) {
545
+ if (tools.length === 0) {
546
+ this.log(` No tools available from ${clientName}`);
547
+ return;
548
+ }
549
+ this.log(`
550
+ --- ${clientName} Tools (${tools.length}) ---`);
551
+ for (const tool of tools) {
552
+ this.log(` \u2022 ${tool.name}: ${tool.description.slice(0, 60)}${tool.description.length > 60 ? "..." : ""}`);
553
+ }
554
+ }
555
+ /**
556
+ * Debug logging
557
+ */
558
+ log(...args) {
559
+ if (this.options.debug) {
560
+ console.log("[MCP-Proxy]", ...args);
561
+ }
562
+ }
563
+ /**
564
+ * Get statistics about the MCP proxy hub
565
+ */
566
+ getStats() {
567
+ const clients = Array.from(this.clients.entries()).map(([name, info]) => ({
568
+ name,
569
+ toolCount: info.tools.length,
570
+ status: info.status,
571
+ transportType: info.transportType
572
+ }));
573
+ return {
574
+ totalClients: this.clients.size,
575
+ totalTools: clients.reduce((sum, c) => sum + c.toolCount, 0),
576
+ clients
577
+ };
578
+ }
579
+ };
580
+
581
+ // src/daemon/index.ts
582
+ var RainfallDaemon = class {
583
+ wss;
584
+ openaiApp;
585
+ rainfall;
586
+ port;
587
+ openaiPort;
588
+ rainfallConfig;
589
+ tools = [];
590
+ toolSchemas = /* @__PURE__ */ new Map();
591
+ clients = /* @__PURE__ */ new Set();
592
+ debug;
593
+ // New services
594
+ networkedExecutor;
595
+ context;
596
+ listeners;
597
+ mcpProxy;
598
+ enableMcpProxy;
599
+ mcpNamespacePrefix;
600
+ constructor(config = {}) {
601
+ this.port = config.port || 8765;
602
+ this.openaiPort = config.openaiPort || 8787;
603
+ this.rainfallConfig = config.rainfallConfig;
604
+ this.debug = config.debug || false;
605
+ this.enableMcpProxy = config.enableMcpProxy ?? true;
606
+ this.mcpNamespacePrefix = config.mcpNamespacePrefix ?? true;
607
+ this.openaiApp = express();
608
+ this.openaiApp.use(express.json());
609
+ }
610
+ async start() {
611
+ this.log("\u{1F327}\uFE0F Rainfall Daemon starting...");
612
+ await this.initializeRainfall();
613
+ if (!this.rainfall) {
614
+ throw new Error("Failed to initialize Rainfall SDK");
615
+ }
616
+ this.context = new RainfallDaemonContext(this.rainfall, {
617
+ maxLocalMemories: 1e3,
618
+ maxMessageHistory: 100,
619
+ ...this.rainfallConfig
620
+ });
621
+ await this.context.initialize();
622
+ this.networkedExecutor = new RainfallNetworkedExecutor(this.rainfall, {
623
+ wsPort: this.port,
624
+ httpPort: this.openaiPort,
625
+ hostname: process.env.HOSTNAME || "local-daemon",
626
+ capabilities: {
627
+ localExec: true,
628
+ fileWatch: true,
629
+ passiveListen: true
630
+ }
631
+ });
632
+ await this.networkedExecutor.registerEdgeNode();
633
+ await this.networkedExecutor.subscribeToResults((jobId, result, error) => {
634
+ this.log(`\u{1F4EC} Job ${jobId} ${error ? "failed" : "completed"}`, error || result);
635
+ });
636
+ this.listeners = new RainfallListenerRegistry(
637
+ this.rainfall,
638
+ this.context,
639
+ this.networkedExecutor
640
+ );
641
+ await this.loadTools();
642
+ if (this.enableMcpProxy) {
643
+ this.mcpProxy = new MCPProxyHub({ debug: this.debug });
644
+ await this.mcpProxy.initialize();
645
+ if (this.rainfallConfig?.mcpClients) {
646
+ for (const clientConfig of this.rainfallConfig.mcpClients) {
647
+ try {
648
+ await this.mcpProxy.connectClient(clientConfig);
649
+ } catch (error) {
650
+ this.log(`Failed to connect MCP client ${clientConfig.name}:`, error);
651
+ }
652
+ }
653
+ }
654
+ }
655
+ await this.startWebSocketServer();
656
+ await this.startOpenAIProxy();
657
+ console.log(`\u{1F680} Rainfall daemon running`);
658
+ console.log(` WebSocket (MCP): ws://localhost:${this.port}`);
659
+ console.log(` OpenAI API: http://localhost:${this.openaiPort}/v1/chat/completions`);
660
+ console.log(` Health Check: http://localhost:${this.openaiPort}/health`);
661
+ console.log(` Edge Node ID: ${this.networkedExecutor.getEdgeNodeId() || "local"}`);
662
+ console.log(` Tools loaded: ${this.tools.length}`);
663
+ console.log(` Press Ctrl+C to stop`);
664
+ process.on("SIGINT", () => this.stop());
665
+ process.on("SIGTERM", () => this.stop());
666
+ }
667
+ async stop() {
668
+ this.log("\u{1F6D1} Shutting down Rainfall daemon...");
669
+ if (this.listeners) {
670
+ await this.listeners.stopAll();
671
+ }
672
+ if (this.networkedExecutor) {
673
+ await this.networkedExecutor.unregisterEdgeNode();
674
+ }
675
+ if (this.mcpProxy) {
676
+ await this.mcpProxy.shutdown();
677
+ this.mcpProxy = void 0;
678
+ }
679
+ for (const client of this.clients) {
680
+ client.close();
681
+ }
682
+ this.clients.clear();
683
+ if (this.wss) {
684
+ this.wss.close();
685
+ this.wss = void 0;
686
+ }
687
+ console.log("\u{1F44B} Rainfall daemon stopped");
688
+ }
689
+ /**
690
+ * Get the networked executor for distributed job management
691
+ */
692
+ getNetworkedExecutor() {
693
+ return this.networkedExecutor;
694
+ }
695
+ /**
696
+ * Get the context for memory/session management
697
+ */
698
+ getContext() {
699
+ return this.context;
700
+ }
701
+ /**
702
+ * Get the listener registry for passive triggers
703
+ */
704
+ getListenerRegistry() {
705
+ return this.listeners;
706
+ }
707
+ /**
708
+ * Get the MCP Proxy Hub for managing external MCP clients
709
+ */
710
+ getMCPProxy() {
711
+ return this.mcpProxy;
712
+ }
713
+ /**
714
+ * Connect an MCP client dynamically
715
+ */
716
+ async connectMCPClient(config) {
717
+ if (!this.mcpProxy) {
718
+ throw new Error("MCP Proxy Hub is not enabled");
719
+ }
720
+ return this.mcpProxy.connectClient(config);
721
+ }
722
+ /**
723
+ * Disconnect an MCP client
724
+ */
725
+ async disconnectMCPClient(name) {
726
+ if (!this.mcpProxy) {
727
+ throw new Error("MCP Proxy Hub is not enabled");
728
+ }
729
+ return this.mcpProxy.disconnectClient(name);
730
+ }
731
+ async initializeRainfall() {
732
+ if (this.rainfallConfig?.apiKey) {
733
+ this.rainfall = new Rainfall(this.rainfallConfig);
734
+ } else {
735
+ const { loadConfig } = await import("../config-7UT7GYSN.mjs");
736
+ const config = loadConfig();
737
+ if (config.apiKey) {
738
+ this.rainfall = new Rainfall({
739
+ apiKey: config.apiKey,
740
+ baseUrl: config.baseUrl
741
+ });
742
+ } else {
743
+ throw new Error("No API key configured. Run: rainfall auth login <api-key>");
744
+ }
745
+ }
746
+ }
747
+ async loadTools() {
748
+ if (!this.rainfall) return;
749
+ try {
750
+ this.tools = await this.rainfall.listTools();
751
+ this.log(`\u{1F4E6} Loaded ${this.tools.length} tools`);
752
+ } catch (error) {
753
+ console.warn("\u26A0\uFE0F Failed to load tools:", error instanceof Error ? error.message : error);
754
+ this.tools = [];
755
+ }
756
+ }
757
+ async getToolSchema(toolId) {
758
+ if (this.toolSchemas.has(toolId)) {
759
+ return this.toolSchemas.get(toolId);
760
+ }
761
+ if (!this.rainfall) return null;
762
+ try {
763
+ const schema = await this.rainfall.getToolSchema(toolId);
764
+ this.toolSchemas.set(toolId, schema);
765
+ return schema;
766
+ } catch {
767
+ return null;
768
+ }
769
+ }
770
+ async startWebSocketServer() {
771
+ this.wss = new WebSocketServer({ port: this.port });
772
+ this.wss.on("connection", (ws) => {
773
+ this.log("\u{1F7E2} MCP client connected");
774
+ this.clients.add(ws);
775
+ ws.on("message", async (data) => {
776
+ try {
777
+ const message = JSON.parse(data.toString());
778
+ const response = await this.handleMCPMessage(message);
779
+ ws.send(JSON.stringify(response));
780
+ } catch (error) {
781
+ const errorResponse = {
782
+ jsonrpc: "2.0",
783
+ id: void 0,
784
+ error: {
785
+ code: -32700,
786
+ message: error instanceof Error ? error.message : "Parse error"
787
+ }
788
+ };
789
+ ws.send(JSON.stringify(errorResponse));
790
+ }
791
+ });
792
+ ws.on("close", () => {
793
+ this.log("\u{1F534} MCP client disconnected");
794
+ this.clients.delete(ws);
795
+ });
796
+ ws.on("error", (error) => {
797
+ console.error("WebSocket error:", error);
798
+ this.clients.delete(ws);
799
+ });
800
+ });
801
+ }
802
+ async handleMCPMessage(message) {
803
+ const { id, method, params } = message;
804
+ switch (method) {
805
+ case "initialize":
806
+ return {
807
+ jsonrpc: "2.0",
808
+ id,
809
+ result: {
810
+ protocolVersion: "2024-11-05",
811
+ capabilities: {
812
+ tools: { listChanged: true }
813
+ },
814
+ serverInfo: {
815
+ name: "rainfall-daemon",
816
+ version: "0.1.0"
817
+ }
818
+ }
819
+ };
820
+ case "tools/list":
821
+ return {
822
+ jsonrpc: "2.0",
823
+ id,
824
+ result: {
825
+ tools: await this.getMCPTools()
826
+ }
827
+ };
828
+ case "tools/call": {
829
+ const toolName = params?.name;
830
+ const toolParams = params?.arguments;
831
+ try {
832
+ const startTime = Date.now();
833
+ const result = await this.executeToolWithMCP(toolName, toolParams);
834
+ const duration = Date.now() - startTime;
835
+ if (this.context) {
836
+ this.context.recordExecution(toolName, toolParams || {}, result, { duration });
837
+ }
838
+ return {
839
+ jsonrpc: "2.0",
840
+ id,
841
+ result: {
842
+ content: [
843
+ {
844
+ type: "text",
845
+ text: typeof result === "string" ? result : JSON.stringify(result, null, 2)
846
+ }
847
+ ]
848
+ }
849
+ };
850
+ } catch (error) {
851
+ const errorMessage = error instanceof Error ? error.message : "Tool execution failed";
852
+ if (this.context) {
853
+ this.context.recordExecution(toolName, toolParams || {}, null, {
854
+ error: errorMessage,
855
+ duration: 0
856
+ });
857
+ }
858
+ return {
859
+ jsonrpc: "2.0",
860
+ id,
861
+ error: {
862
+ code: -32603,
863
+ message: errorMessage
864
+ }
865
+ };
866
+ }
867
+ }
868
+ case "ping":
869
+ return {
870
+ jsonrpc: "2.0",
871
+ id,
872
+ result: {}
873
+ };
874
+ default:
875
+ return {
876
+ jsonrpc: "2.0",
877
+ id,
878
+ error: {
879
+ code: -32601,
880
+ message: `Method not found: ${method}`
881
+ }
882
+ };
883
+ }
884
+ }
885
+ async getMCPTools() {
886
+ const mcpTools = [];
887
+ for (const tool of this.tools) {
888
+ const schema = await this.getToolSchema(tool.id);
889
+ if (schema) {
890
+ const toolSchema = schema;
891
+ mcpTools.push({
892
+ name: tool.id,
893
+ description: toolSchema.description || tool.description,
894
+ inputSchema: toolSchema.parameters || { type: "object", properties: {} }
895
+ });
896
+ }
897
+ }
898
+ if (this.mcpProxy) {
899
+ const proxyTools = this.mcpProxy.getAllTools({ namespacePrefix: this.mcpNamespacePrefix });
900
+ for (const tool of proxyTools) {
901
+ mcpTools.push({
902
+ name: this.mcpNamespacePrefix ? `${tool.serverName}-${tool.name}` : tool.name,
903
+ description: tool.description,
904
+ inputSchema: tool.inputSchema || { type: "object", properties: {} }
905
+ });
906
+ }
907
+ }
908
+ return mcpTools;
909
+ }
910
+ async executeTool(toolId, params) {
911
+ if (!this.rainfall) {
912
+ throw new Error("Rainfall SDK not initialized");
913
+ }
914
+ return this.rainfall.executeTool(toolId, params);
915
+ }
916
+ /**
917
+ * Execute a tool, trying MCP proxy first, then falling back to Rainfall tools
918
+ */
919
+ async executeToolWithMCP(toolName, params) {
920
+ if (this.mcpProxy) {
921
+ try {
922
+ if (this.mcpNamespacePrefix && toolName.includes("-")) {
923
+ const namespace = toolName.split("-")[0];
924
+ const actualToolName = toolName.slice(namespace.length + 1);
925
+ if (this.mcpProxy.getClient(namespace)) {
926
+ return await this.mcpProxy.callTool(toolName, params || {}, {
927
+ namespace
928
+ });
929
+ }
930
+ }
931
+ return await this.mcpProxy.callTool(toolName, params || {});
932
+ } catch (error) {
933
+ if (error instanceof Error && !error.message.includes("not found")) {
934
+ throw error;
935
+ }
936
+ }
937
+ }
938
+ return this.executeTool(toolName, params);
939
+ }
940
+ async startOpenAIProxy() {
941
+ this.openaiApp.get("/v1/models", async (_req, res) => {
942
+ try {
943
+ if (this.rainfall) {
944
+ const models = await this.rainfall.listModels();
945
+ res.json({
946
+ object: "list",
947
+ data: models.map((m) => ({
948
+ id: m.id,
949
+ object: "model",
950
+ created: Math.floor(Date.now() / 1e3),
951
+ owned_by: "rainfall"
952
+ }))
953
+ });
954
+ } else {
955
+ res.json({
956
+ object: "list",
957
+ data: [
958
+ { id: "llama-3.3-70b-versatile", object: "model", created: Date.now(), owned_by: "groq" },
959
+ { id: "gpt-4o", object: "model", created: Date.now(), owned_by: "openai" },
960
+ { id: "claude-3-5-sonnet", object: "model", created: Date.now(), owned_by: "anthropic" },
961
+ { id: "gemini-2.0-flash-exp", object: "model", created: Date.now(), owned_by: "gemini" }
962
+ ]
963
+ });
964
+ }
965
+ } catch (error) {
966
+ res.status(500).json({ error: "Failed to fetch models" });
967
+ }
968
+ });
969
+ this.openaiApp.post("/v1/chat/completions", async (req, res) => {
970
+ const body = req.body;
971
+ if (!body.messages || !Array.isArray(body.messages)) {
972
+ res.status(400).json({
973
+ error: {
974
+ message: "Missing required field: messages",
975
+ type: "invalid_request_error"
976
+ }
977
+ });
978
+ return;
979
+ }
980
+ if (!this.rainfall) {
981
+ res.status(503).json({
982
+ error: {
983
+ message: "Rainfall SDK not initialized",
984
+ type: "service_unavailable"
985
+ }
986
+ });
987
+ return;
988
+ }
989
+ try {
990
+ const me = await this.rainfall.getMe();
991
+ const subscriberId = me.id;
992
+ const localToolMap = await this.buildLocalToolMap();
993
+ let allTools = [];
994
+ if (body.tools && body.tools.length > 0) {
995
+ allTools = body.tools;
996
+ } else if (body.tool_choice) {
997
+ const openaiTools = await this.getOpenAITools();
998
+ allTools = openaiTools;
999
+ }
1000
+ let messages = [...body.messages];
1001
+ const maxToolIterations = 10;
1002
+ let toolIterations = 0;
1003
+ while (toolIterations < maxToolIterations) {
1004
+ toolIterations++;
1005
+ const llmResponse = await this.callLLM({
1006
+ subscriberId,
1007
+ model: body.model,
1008
+ messages,
1009
+ tools: allTools.length > 0 ? allTools : void 0,
1010
+ tool_choice: body.tool_choice,
1011
+ temperature: body.temperature,
1012
+ max_tokens: body.max_tokens,
1013
+ stream: false,
1014
+ // Always non-streaming for tool loop
1015
+ tool_priority: body.tool_priority,
1016
+ enable_stacked: body.enable_stacked
1017
+ });
1018
+ const choice = llmResponse.choices?.[0];
1019
+ let toolCalls = choice?.message?.tool_calls || [];
1020
+ const content = choice?.message?.content || "";
1021
+ const reasoningContent = choice?.message?.reasoning_content || "";
1022
+ const fullContent = content + " " + reasoningContent;
1023
+ const xmlToolCalls = this.parseXMLToolCalls(fullContent);
1024
+ if (xmlToolCalls.length > 0) {
1025
+ this.log(`\u{1F4CB} Parsed ${xmlToolCalls.length} XML tool calls from content`);
1026
+ toolCalls = xmlToolCalls;
1027
+ }
1028
+ if (!toolCalls || toolCalls.length === 0) {
1029
+ if (body.stream) {
1030
+ await this.streamResponse(res, llmResponse);
1031
+ } else {
1032
+ res.json(llmResponse);
1033
+ }
1034
+ this.updateContext(body.messages, llmResponse);
1035
+ return;
1036
+ }
1037
+ messages.push({
1038
+ role: "assistant",
1039
+ content: choice?.message?.content || "",
1040
+ tool_calls: toolCalls
1041
+ });
1042
+ for (const toolCall of toolCalls) {
1043
+ const toolName = toolCall.function?.name;
1044
+ const toolArgsStr = toolCall.function?.arguments || "{}";
1045
+ if (!toolName) continue;
1046
+ this.log(`\u{1F527} Tool call: ${toolName}`);
1047
+ let toolResult;
1048
+ let toolError;
1049
+ try {
1050
+ const localTool = this.findLocalTool(toolName, localToolMap);
1051
+ if (localTool) {
1052
+ this.log(` \u2192 Executing locally`);
1053
+ const args = JSON.parse(toolArgsStr);
1054
+ toolResult = await this.executeLocalTool(localTool.id, args);
1055
+ } else if (this.mcpProxy) {
1056
+ this.log(` \u2192 Trying MCP proxy`);
1057
+ const args = JSON.parse(toolArgsStr);
1058
+ toolResult = await this.executeToolWithMCP(toolName.replace(/_/g, "-"), args);
1059
+ } else {
1060
+ const shouldExecuteLocal = body.tool_priority === "local" || body.tool_priority === "stacked";
1061
+ if (shouldExecuteLocal) {
1062
+ try {
1063
+ const args = JSON.parse(toolArgsStr);
1064
+ toolResult = await this.rainfall.executeTool(toolName.replace(/_/g, "-"), args);
1065
+ } catch {
1066
+ toolResult = { _pending: true, tool: toolName, args: toolArgsStr };
1067
+ }
1068
+ } else {
1069
+ toolResult = { _pending: true, tool: toolName, args: toolArgsStr };
1070
+ }
1071
+ }
1072
+ } catch (error) {
1073
+ toolError = error instanceof Error ? error.message : String(error);
1074
+ this.log(` \u2192 Error: ${toolError}`);
1075
+ }
1076
+ messages.push({
1077
+ role: "tool",
1078
+ content: toolError ? JSON.stringify({ error: toolError }) : typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult),
1079
+ tool_call_id: toolCall.id
1080
+ });
1081
+ if (this.context) {
1082
+ this.context.recordExecution(
1083
+ toolName,
1084
+ JSON.parse(toolArgsStr || "{}"),
1085
+ toolResult,
1086
+ { error: toolError, duration: 0 }
1087
+ );
1088
+ }
1089
+ }
1090
+ }
1091
+ res.status(500).json({
1092
+ error: {
1093
+ message: "Maximum tool execution iterations reached",
1094
+ type: "tool_execution_error"
1095
+ }
1096
+ });
1097
+ } catch (error) {
1098
+ this.log("Chat completions error:", error);
1099
+ res.status(500).json({
1100
+ error: {
1101
+ message: error instanceof Error ? error.message : "Internal server error",
1102
+ type: "internal_error"
1103
+ }
1104
+ });
1105
+ }
1106
+ });
1107
+ this.openaiApp.get("/health", (_req, res) => {
1108
+ const mcpStats = this.mcpProxy?.getStats();
1109
+ res.json({
1110
+ status: "ok",
1111
+ daemon: "rainfall",
1112
+ version: "0.2.0",
1113
+ tools_loaded: this.tools.length,
1114
+ mcp_clients: mcpStats?.totalClients || 0,
1115
+ mcp_tools: mcpStats?.totalTools || 0,
1116
+ edge_node_id: this.networkedExecutor?.getEdgeNodeId(),
1117
+ clients_connected: this.clients.size
1118
+ });
1119
+ });
1120
+ this.openaiApp.get("/v1/mcp/clients", (_req, res) => {
1121
+ if (!this.mcpProxy) {
1122
+ res.status(503).json({ error: "MCP proxy not enabled" });
1123
+ return;
1124
+ }
1125
+ res.json(this.mcpProxy.listClients());
1126
+ });
1127
+ this.openaiApp.post("/v1/mcp/connect", async (req, res) => {
1128
+ if (!this.mcpProxy) {
1129
+ res.status(503).json({ error: "MCP proxy not enabled" });
1130
+ return;
1131
+ }
1132
+ try {
1133
+ const name = await this.mcpProxy.connectClient(req.body);
1134
+ res.json({ success: true, client: name });
1135
+ } catch (error) {
1136
+ res.status(500).json({
1137
+ error: error instanceof Error ? error.message : "Failed to connect MCP client"
1138
+ });
1139
+ }
1140
+ });
1141
+ this.openaiApp.post("/v1/mcp/disconnect", async (req, res) => {
1142
+ if (!this.mcpProxy) {
1143
+ res.status(503).json({ error: "MCP proxy not enabled" });
1144
+ return;
1145
+ }
1146
+ const { name } = req.body;
1147
+ if (!name) {
1148
+ res.status(400).json({ error: "Missing required field: name" });
1149
+ return;
1150
+ }
1151
+ await this.mcpProxy.disconnectClient(name);
1152
+ res.json({ success: true });
1153
+ });
1154
+ this.openaiApp.get("/status", (_req, res) => {
1155
+ res.json(this.getStatus());
1156
+ });
1157
+ this.openaiApp.post("/v1/queue", async (req, res) => {
1158
+ const { tool_id, params, execution_mode = "any" } = req.body;
1159
+ if (!tool_id) {
1160
+ res.status(400).json({ error: "Missing required field: tool_id" });
1161
+ return;
1162
+ }
1163
+ if (!this.networkedExecutor) {
1164
+ res.status(503).json({ error: "Networked executor not available" });
1165
+ return;
1166
+ }
1167
+ try {
1168
+ const jobId = await this.networkedExecutor.queueToolExecution(
1169
+ tool_id,
1170
+ params || {},
1171
+ { executionMode: execution_mode }
1172
+ );
1173
+ res.json({ job_id: jobId, status: "queued" });
1174
+ } catch (error) {
1175
+ res.status(500).json({
1176
+ error: error instanceof Error ? error.message : "Failed to queue job"
1177
+ });
1178
+ }
1179
+ });
1180
+ return new Promise((resolve) => {
1181
+ this.openaiApp.listen(this.openaiPort, () => {
1182
+ resolve();
1183
+ });
1184
+ });
1185
+ }
1186
+ /**
1187
+ * Build a map of local Rainfall tools for quick lookup
1188
+ * Maps OpenAI-style underscore names to Rainfall tool IDs
1189
+ */
1190
+ async buildLocalToolMap() {
1191
+ const map = /* @__PURE__ */ new Map();
1192
+ for (const tool of this.tools) {
1193
+ const openAiName = tool.id.replace(/-/g, "_");
1194
+ map.set(openAiName, {
1195
+ id: tool.id,
1196
+ name: openAiName,
1197
+ description: tool.description
1198
+ });
1199
+ map.set(tool.id, {
1200
+ id: tool.id,
1201
+ name: openAiName,
1202
+ description: tool.description
1203
+ });
1204
+ }
1205
+ return map;
1206
+ }
1207
+ /**
1208
+ * Find a local Rainfall tool by name (OpenAI underscore format or original)
1209
+ */
1210
+ findLocalTool(toolName, localToolMap) {
1211
+ if (localToolMap.has(toolName)) {
1212
+ return localToolMap.get(toolName);
1213
+ }
1214
+ const dashedName = toolName.replace(/_/g, "-");
1215
+ if (localToolMap.has(dashedName)) {
1216
+ return localToolMap.get(dashedName);
1217
+ }
1218
+ return void 0;
1219
+ }
1220
+ /**
1221
+ * Execute a local Rainfall tool
1222
+ */
1223
+ async executeLocalTool(toolId, args) {
1224
+ if (!this.rainfall) {
1225
+ throw new Error("Rainfall SDK not initialized");
1226
+ }
1227
+ const startTime = Date.now();
1228
+ try {
1229
+ const result = await this.rainfall.executeTool(toolId, args);
1230
+ const duration = Date.now() - startTime;
1231
+ this.log(` \u2713 Completed in ${duration}ms`);
1232
+ return result;
1233
+ } catch (error) {
1234
+ const duration = Date.now() - startTime;
1235
+ this.log(` \u2717 Failed after ${duration}ms`);
1236
+ throw error;
1237
+ }
1238
+ }
1239
+ /**
1240
+ * Parse XML-style tool calls from model output
1241
+ * Handles formats like: <function=name><parameter=key>value</parameter></function>
1242
+ */
1243
+ parseXMLToolCalls(content) {
1244
+ const toolCalls = [];
1245
+ const functionRegex = /<function=([^>]+)>([\s\S]*?)<\/function>/gi;
1246
+ let match;
1247
+ while ((match = functionRegex.exec(content)) !== null) {
1248
+ const functionName = match[1].trim();
1249
+ const paramsBlock = match[2];
1250
+ const params = {};
1251
+ const paramRegex = /<parameter=([^>]+)>([\s\S]*?)<\/parameter>/gi;
1252
+ let paramMatch;
1253
+ while ((paramMatch = paramRegex.exec(paramsBlock)) !== null) {
1254
+ const paramName = paramMatch[1].trim();
1255
+ const paramValue = paramMatch[2].trim();
1256
+ params[paramName] = paramValue;
1257
+ }
1258
+ toolCalls.push({
1259
+ id: `xml-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
1260
+ type: "function",
1261
+ function: {
1262
+ name: functionName,
1263
+ arguments: JSON.stringify(params)
1264
+ }
1265
+ });
1266
+ this.log(`\u{1F4CB} Parsed XML tool call: ${functionName}(${JSON.stringify(params)})`);
1267
+ }
1268
+ return toolCalls;
1269
+ }
1270
+ /**
1271
+ * Call the LLM via Rainfall backend, LM Studio, RunPod, or other providers
1272
+ *
1273
+ * Provider priority:
1274
+ * 1. Config file (llm.provider, llm.baseUrl)
1275
+ * 2. Environment variables (OPENAI_API_KEY, OLLAMA_HOST, etc.)
1276
+ * 3. Default to Rainfall (credits-based)
1277
+ */
1278
+ async callLLM(params) {
1279
+ if (!this.rainfall) {
1280
+ throw new Error("Rainfall SDK not initialized");
1281
+ }
1282
+ const { loadConfig, getProviderBaseUrl } = await import("../config-7UT7GYSN.mjs");
1283
+ const config = loadConfig();
1284
+ const provider = config.llm?.provider || "rainfall";
1285
+ switch (provider) {
1286
+ case "local":
1287
+ case "ollama":
1288
+ case "custom":
1289
+ return this.callLocalLLM(params, config);
1290
+ case "openai":
1291
+ case "anthropic":
1292
+ return this.callExternalLLM(params, config, provider);
1293
+ case "rainfall":
1294
+ default:
1295
+ return this.rainfall.chatCompletions({
1296
+ subscriber_id: params.subscriberId,
1297
+ model: params.model,
1298
+ messages: params.messages,
1299
+ stream: params.stream || false,
1300
+ temperature: params.temperature,
1301
+ max_tokens: params.max_tokens,
1302
+ tools: params.tools,
1303
+ tool_choice: params.tool_choice,
1304
+ tool_priority: params.tool_priority,
1305
+ enable_stacked: params.enable_stacked
1306
+ });
1307
+ }
1308
+ }
1309
+ /**
1310
+ * Call external LLM provider (OpenAI, Anthropic) via their OpenAI-compatible APIs
1311
+ */
1312
+ async callExternalLLM(params, config, provider) {
1313
+ const { getProviderBaseUrl } = await import("../config-7UT7GYSN.mjs");
1314
+ const baseUrl = config.llm?.baseUrl || getProviderBaseUrl({ llm: { provider } });
1315
+ const apiKey = config.llm?.apiKey;
1316
+ if (!apiKey) {
1317
+ throw new Error(`${provider} API key not configured. Set via: rainfall config set llm.apiKey <key>`);
1318
+ }
1319
+ const model = params.model || config.llm?.model || (provider === "anthropic" ? "claude-3-5-sonnet-20241022" : "gpt-4o");
1320
+ const url = `${baseUrl}/chat/completions`;
1321
+ const response = await fetch(url, {
1322
+ method: "POST",
1323
+ headers: {
1324
+ "Content-Type": "application/json",
1325
+ "Authorization": `Bearer ${apiKey}`,
1326
+ "User-Agent": "Rainfall-DevKit/1.0"
1327
+ },
1328
+ body: JSON.stringify({
1329
+ model,
1330
+ messages: params.messages,
1331
+ tools: params.tools,
1332
+ tool_choice: params.tool_choice,
1333
+ temperature: params.temperature,
1334
+ max_tokens: params.max_tokens,
1335
+ stream: false
1336
+ // Tool loop requires non-streaming
1337
+ })
1338
+ });
1339
+ if (!response.ok) {
1340
+ const error = await response.text();
1341
+ throw new Error(`${provider} API error: ${error}`);
1342
+ }
1343
+ return response.json();
1344
+ }
1345
+ /**
1346
+ * Call a local LLM (LM Studio, Ollama, etc.)
1347
+ */
1348
+ async callLocalLLM(params, config) {
1349
+ const baseUrl = config.llm?.baseUrl || "http://localhost:1234/v1";
1350
+ const apiKey = config.llm?.apiKey || "not-needed";
1351
+ const model = params.model || config.llm?.model || "local-model";
1352
+ const url = `${baseUrl}/chat/completions`;
1353
+ const response = await fetch(url, {
1354
+ method: "POST",
1355
+ headers: {
1356
+ "Content-Type": "application/json",
1357
+ "Authorization": `Bearer ${apiKey}`,
1358
+ "User-Agent": "Rainfall-DevKit/1.0"
1359
+ },
1360
+ body: JSON.stringify({
1361
+ model,
1362
+ messages: params.messages,
1363
+ tools: params.tools,
1364
+ tool_choice: params.tool_choice,
1365
+ temperature: params.temperature,
1366
+ max_tokens: params.max_tokens,
1367
+ stream: false
1368
+ // Tool loop requires non-streaming
1369
+ })
1370
+ });
1371
+ if (!response.ok) {
1372
+ const error = await response.text();
1373
+ throw new Error(`Local LLM error: ${error}`);
1374
+ }
1375
+ return response.json();
1376
+ }
1377
+ /**
1378
+ * Stream a response to the client (converts non-streaming to SSE format)
1379
+ */
1380
+ async streamResponse(res, response) {
1381
+ res.setHeader("Content-Type", "text/event-stream");
1382
+ res.setHeader("Cache-Control", "no-cache");
1383
+ res.setHeader("Connection", "keep-alive");
1384
+ const message = response.choices?.[0]?.message;
1385
+ const id = response.id || `chatcmpl-${Date.now()}`;
1386
+ const model = response.model || "unknown";
1387
+ const created = Math.floor(Date.now() / 1e3);
1388
+ res.write(`data: ${JSON.stringify({
1389
+ id,
1390
+ object: "chat.completion.chunk",
1391
+ created,
1392
+ model,
1393
+ choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }]
1394
+ })}
1395
+
1396
+ `);
1397
+ const content = message?.content || "";
1398
+ const chunkSize = 10;
1399
+ for (let i = 0; i < content.length; i += chunkSize) {
1400
+ const chunk = content.slice(i, i + chunkSize);
1401
+ res.write(`data: ${JSON.stringify({
1402
+ id,
1403
+ object: "chat.completion.chunk",
1404
+ created,
1405
+ model,
1406
+ choices: [{ index: 0, delta: { content: chunk }, finish_reason: null }]
1407
+ })}
1408
+
1409
+ `);
1410
+ }
1411
+ res.write(`data: ${JSON.stringify({
1412
+ id,
1413
+ object: "chat.completion.chunk",
1414
+ created,
1415
+ model,
1416
+ choices: [{ index: 0, delta: {}, finish_reason: "stop" }]
1417
+ })}
1418
+
1419
+ `);
1420
+ res.write("data: [DONE]\n\n");
1421
+ res.end();
1422
+ }
1423
+ /**
1424
+ * Update context with conversation history
1425
+ */
1426
+ updateContext(originalMessages, response) {
1427
+ if (!this.context) return;
1428
+ const lastUserMessage = originalMessages.filter((m) => m.role === "user").pop();
1429
+ if (lastUserMessage) {
1430
+ this.context.addMessage("user", lastUserMessage.content);
1431
+ }
1432
+ const assistantContent = response.choices?.[0]?.message?.content;
1433
+ if (assistantContent) {
1434
+ this.context.addMessage("assistant", assistantContent);
1435
+ }
1436
+ }
1437
+ async getOpenAITools() {
1438
+ const tools = [];
1439
+ for (const tool of this.tools.slice(0, 100)) {
1440
+ const schema = await this.getToolSchema(tool.id);
1441
+ if (schema) {
1442
+ const toolSchema = schema;
1443
+ let parameters = { type: "object", properties: {}, required: [] };
1444
+ if (toolSchema.parameters && typeof toolSchema.parameters === "object") {
1445
+ const rawParams = toolSchema.parameters;
1446
+ parameters = {
1447
+ type: rawParams.type || "object",
1448
+ properties: rawParams.properties || {},
1449
+ required: rawParams.required || []
1450
+ };
1451
+ }
1452
+ tools.push({
1453
+ type: "function",
1454
+ function: {
1455
+ name: tool.id.replace(/-/g, "_"),
1456
+ // OpenAI requires underscore names
1457
+ description: toolSchema.description || tool.description,
1458
+ parameters
1459
+ }
1460
+ });
1461
+ }
1462
+ }
1463
+ if (this.mcpProxy) {
1464
+ const proxyTools = this.mcpProxy.getAllTools({ namespacePrefix: this.mcpNamespacePrefix });
1465
+ for (const tool of proxyTools.slice(0, 28)) {
1466
+ const inputSchema = tool.inputSchema || {};
1467
+ tools.push({
1468
+ type: "function",
1469
+ function: {
1470
+ name: this.mcpNamespacePrefix ? `${tool.serverName}_${tool.name}`.replace(/-/g, "_") : tool.name.replace(/-/g, "_"),
1471
+ description: `[${tool.serverName}] ${tool.description}`,
1472
+ parameters: {
1473
+ type: "object",
1474
+ properties: inputSchema.properties || {},
1475
+ required: inputSchema.required || []
1476
+ }
1477
+ }
1478
+ });
1479
+ }
1480
+ }
1481
+ return tools;
1482
+ }
1483
+ buildResponseContent() {
1484
+ const edgeNodeId = this.networkedExecutor?.getEdgeNodeId();
1485
+ const toolCount = this.tools.length;
1486
+ return `Rainfall daemon online. Edge node: ${edgeNodeId || "local"}. ${toolCount} tools available. What would you like to execute locally or in the cloud?`;
1487
+ }
1488
+ getStatus() {
1489
+ return {
1490
+ running: !!this.wss,
1491
+ port: this.port,
1492
+ openaiPort: this.openaiPort,
1493
+ toolsLoaded: this.tools.length,
1494
+ clientsConnected: this.clients.size,
1495
+ edgeNodeId: this.networkedExecutor?.getEdgeNodeId(),
1496
+ context: this.context?.getStatus() || {
1497
+ memoriesCached: 0,
1498
+ activeSessions: 0,
1499
+ executionHistorySize: 0
1500
+ },
1501
+ listeners: this.listeners?.getStatus() || {
1502
+ fileWatchers: 0,
1503
+ cronTriggers: 0,
1504
+ recentEvents: 0
1505
+ }
1506
+ };
1507
+ }
1508
+ log(...args) {
1509
+ if (this.debug) {
1510
+ console.log(...args);
1511
+ }
1512
+ }
1513
+ };
1514
+ var daemonInstance = null;
1515
+ async function startDaemon(config = {}) {
1516
+ if (daemonInstance) {
1517
+ console.log("Daemon already running");
1518
+ return daemonInstance;
1519
+ }
1520
+ daemonInstance = new RainfallDaemon(config);
1521
+ await daemonInstance.start();
1522
+ return daemonInstance;
1523
+ }
1524
+ async function stopDaemon() {
1525
+ if (!daemonInstance) {
1526
+ console.log("Daemon not running");
1527
+ return;
1528
+ }
1529
+ await daemonInstance.stop();
1530
+ daemonInstance = null;
1531
+ }
1532
+ function getDaemonStatus() {
1533
+ if (!daemonInstance) {
1534
+ return null;
1535
+ }
1536
+ return daemonInstance.getStatus();
1537
+ }
1538
+ function getDaemonInstance() {
1539
+ return daemonInstance;
1540
+ }
1541
+ export {
1542
+ MCPProxyHub,
1543
+ RainfallDaemon,
1544
+ getDaemonInstance,
1545
+ getDaemonStatus,
1546
+ startDaemon,
1547
+ stopDaemon
1548
+ };