@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.
- package/README.md +51 -0
- package/dist/chunk-7MRE4ZVI.mjs +662 -0
- package/dist/chunk-AQFC7YAX.mjs +27 -0
- package/dist/chunk-EI7SJH5K.mjs +85 -0
- package/dist/chunk-NTTAVKRT.mjs +89 -0
- package/dist/chunk-RVKW5KBT.mjs +269 -0
- package/dist/chunk-V5QWJVLC.mjs +662 -0
- package/dist/chunk-VDPKDC3R.mjs +869 -0
- package/dist/chunk-WOITG5TG.mjs +84 -0
- package/dist/chunk-XAHJQRBJ.mjs +269 -0
- package/dist/chunk-XEQ6U3JQ.mjs +269 -0
- package/dist/cli/index.js +3797 -632
- package/dist/cli/index.mjs +453 -36
- package/dist/config-7UT7GYSN.mjs +16 -0
- package/dist/config-DDTQQBN7.mjs +14 -0
- package/dist/config-MD45VGWD.mjs +14 -0
- package/dist/config-ZKNHII2A.mjs +8 -0
- package/dist/daemon/index.d.mts +168 -0
- package/dist/daemon/index.d.ts +168 -0
- package/dist/daemon/index.js +3182 -0
- package/dist/daemon/index.mjs +1548 -0
- package/dist/errors-BMPseAnM.d.mts +47 -0
- package/dist/errors-BMPseAnM.d.ts +47 -0
- package/dist/errors-CZdRoYyw.d.ts +332 -0
- package/dist/errors-Chjq1Mev.d.mts +332 -0
- package/dist/index.d.mts +249 -2
- package/dist/index.d.ts +249 -2
- package/dist/index.js +1247 -3
- package/dist/index.mjs +227 -2
- package/dist/listeners-B5Vy9Ao5.d.ts +372 -0
- package/dist/listeners-BbYIaNCs.d.mts +372 -0
- package/dist/listeners-CP2A9J_2.d.ts +372 -0
- package/dist/listeners-CTRSofnm.d.mts +372 -0
- package/dist/listeners-CYI-YwIF.d.mts +372 -0
- package/dist/listeners-DRwITBW_.d.mts +372 -0
- package/dist/listeners-DrMrvFT5.d.ts +372 -0
- package/dist/listeners-MNAnpZj-.d.mts +372 -0
- package/dist/listeners-PZI7iT85.d.ts +372 -0
- package/dist/listeners-QJeEtLbV.d.ts +372 -0
- package/dist/listeners-hp0Ib2Ox.d.ts +372 -0
- package/dist/listeners-jLwetUnx.d.mts +372 -0
- package/dist/mcp.d.mts +7 -2
- package/dist/mcp.d.ts +7 -2
- package/dist/mcp.js +92 -1
- package/dist/mcp.mjs +1 -1
- package/dist/sdk-4OvXPr8E.d.mts +1054 -0
- package/dist/sdk-4OvXPr8E.d.ts +1054 -0
- package/dist/sdk-CJ9g5lFo.d.mts +772 -0
- package/dist/sdk-CJ9g5lFo.d.ts +772 -0
- package/dist/sdk-CN1ezZrI.d.mts +1054 -0
- package/dist/sdk-CN1ezZrI.d.ts +1054 -0
- package/dist/sdk-DD1OeGRJ.d.mts +871 -0
- package/dist/sdk-DD1OeGRJ.d.ts +871 -0
- package/dist/sdk-Xw0BjsLd.d.mts +1054 -0
- package/dist/sdk-Xw0BjsLd.d.ts +1054 -0
- package/dist/types-GnRAfH-h.d.mts +489 -0
- package/dist/types-GnRAfH-h.d.ts +489 -0
- package/package.json +17 -5
package/dist/cli/index.js
CHANGED
|
@@ -6,6 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
9
16
|
var __copyProps = (to, from, except, desc) => {
|
|
10
17
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
18
|
for (let key of __getOwnPropNames(from))
|
|
@@ -23,93 +30,17 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
30
|
mod
|
|
24
31
|
));
|
|
25
32
|
|
|
26
|
-
//
|
|
27
|
-
var
|
|
28
|
-
var
|
|
29
|
-
|
|
33
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
34
|
+
var getImportMetaUrl, importMetaUrl;
|
|
35
|
+
var init_cjs_shims = __esm({
|
|
36
|
+
"node_modules/tsup/assets/cjs_shims.js"() {
|
|
37
|
+
"use strict";
|
|
38
|
+
getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
39
|
+
importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
30
42
|
|
|
31
43
|
// src/errors.ts
|
|
32
|
-
var RainfallError = class _RainfallError extends Error {
|
|
33
|
-
constructor(message, code, statusCode, details) {
|
|
34
|
-
super(message);
|
|
35
|
-
this.code = code;
|
|
36
|
-
this.statusCode = statusCode;
|
|
37
|
-
this.details = details;
|
|
38
|
-
this.name = "RainfallError";
|
|
39
|
-
Object.setPrototypeOf(this, _RainfallError.prototype);
|
|
40
|
-
}
|
|
41
|
-
toJSON() {
|
|
42
|
-
return {
|
|
43
|
-
name: this.name,
|
|
44
|
-
code: this.code,
|
|
45
|
-
message: this.message,
|
|
46
|
-
statusCode: this.statusCode,
|
|
47
|
-
details: this.details
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
var AuthenticationError = class _AuthenticationError extends RainfallError {
|
|
52
|
-
constructor(message = "Invalid API key", details) {
|
|
53
|
-
super(message, "AUTHENTICATION_ERROR", 401, details);
|
|
54
|
-
this.name = "AuthenticationError";
|
|
55
|
-
Object.setPrototypeOf(this, _AuthenticationError.prototype);
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
var RateLimitError = class _RateLimitError extends RainfallError {
|
|
59
|
-
retryAfter;
|
|
60
|
-
limit;
|
|
61
|
-
remaining;
|
|
62
|
-
resetAt;
|
|
63
|
-
constructor(message = "Rate limit exceeded", retryAfter = 60, limit = 0, remaining = 0, resetAt) {
|
|
64
|
-
super(message, "RATE_LIMIT_ERROR", 429, { retryAfter, limit, remaining });
|
|
65
|
-
this.name = "RateLimitError";
|
|
66
|
-
this.retryAfter = retryAfter;
|
|
67
|
-
this.limit = limit;
|
|
68
|
-
this.remaining = remaining;
|
|
69
|
-
this.resetAt = resetAt || new Date(Date.now() + retryAfter * 1e3);
|
|
70
|
-
Object.setPrototypeOf(this, _RateLimitError.prototype);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
var ValidationError = class _ValidationError extends RainfallError {
|
|
74
|
-
constructor(message, details) {
|
|
75
|
-
super(message, "VALIDATION_ERROR", 400, details);
|
|
76
|
-
this.name = "ValidationError";
|
|
77
|
-
Object.setPrototypeOf(this, _ValidationError.prototype);
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
var NotFoundError = class _NotFoundError extends RainfallError {
|
|
81
|
-
constructor(resource, identifier) {
|
|
82
|
-
super(
|
|
83
|
-
`${resource}${identifier ? ` '${identifier}'` : ""} not found`,
|
|
84
|
-
"NOT_FOUND_ERROR",
|
|
85
|
-
404,
|
|
86
|
-
{ resource, identifier }
|
|
87
|
-
);
|
|
88
|
-
this.name = "NotFoundError";
|
|
89
|
-
Object.setPrototypeOf(this, _NotFoundError.prototype);
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
var ServerError = class _ServerError extends RainfallError {
|
|
93
|
-
constructor(message = "Internal server error", statusCode = 500) {
|
|
94
|
-
super(message, "SERVER_ERROR", statusCode);
|
|
95
|
-
this.name = "ServerError";
|
|
96
|
-
Object.setPrototypeOf(this, _ServerError.prototype);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
var TimeoutError = class _TimeoutError extends RainfallError {
|
|
100
|
-
constructor(timeoutMs) {
|
|
101
|
-
super(`Request timed out after ${timeoutMs}ms`, "TIMEOUT_ERROR", 408);
|
|
102
|
-
this.name = "TimeoutError";
|
|
103
|
-
Object.setPrototypeOf(this, _TimeoutError.prototype);
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
var NetworkError = class _NetworkError extends RainfallError {
|
|
107
|
-
constructor(message = "Network error", details) {
|
|
108
|
-
super(message, "NETWORK_ERROR", void 0, details);
|
|
109
|
-
this.name = "NetworkError";
|
|
110
|
-
Object.setPrototypeOf(this, _NetworkError.prototype);
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
44
|
function parseErrorResponse(response, data) {
|
|
114
45
|
const statusCode = response.status;
|
|
115
46
|
if (statusCode === 429) {
|
|
@@ -158,295 +89,441 @@ function parseErrorResponse(response, data) {
|
|
|
158
89
|
);
|
|
159
90
|
}
|
|
160
91
|
}
|
|
92
|
+
var RainfallError, AuthenticationError, RateLimitError, ValidationError, NotFoundError, ServerError, TimeoutError, NetworkError;
|
|
93
|
+
var init_errors = __esm({
|
|
94
|
+
"src/errors.ts"() {
|
|
95
|
+
"use strict";
|
|
96
|
+
init_cjs_shims();
|
|
97
|
+
RainfallError = class _RainfallError extends Error {
|
|
98
|
+
constructor(message, code, statusCode, details) {
|
|
99
|
+
super(message);
|
|
100
|
+
this.code = code;
|
|
101
|
+
this.statusCode = statusCode;
|
|
102
|
+
this.details = details;
|
|
103
|
+
this.name = "RainfallError";
|
|
104
|
+
Object.setPrototypeOf(this, _RainfallError.prototype);
|
|
105
|
+
}
|
|
106
|
+
toJSON() {
|
|
107
|
+
return {
|
|
108
|
+
name: this.name,
|
|
109
|
+
code: this.code,
|
|
110
|
+
message: this.message,
|
|
111
|
+
statusCode: this.statusCode,
|
|
112
|
+
details: this.details
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
AuthenticationError = class _AuthenticationError extends RainfallError {
|
|
117
|
+
constructor(message = "Invalid API key", details) {
|
|
118
|
+
super(message, "AUTHENTICATION_ERROR", 401, details);
|
|
119
|
+
this.name = "AuthenticationError";
|
|
120
|
+
Object.setPrototypeOf(this, _AuthenticationError.prototype);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
RateLimitError = class _RateLimitError extends RainfallError {
|
|
124
|
+
retryAfter;
|
|
125
|
+
limit;
|
|
126
|
+
remaining;
|
|
127
|
+
resetAt;
|
|
128
|
+
constructor(message = "Rate limit exceeded", retryAfter = 60, limit = 0, remaining = 0, resetAt) {
|
|
129
|
+
super(message, "RATE_LIMIT_ERROR", 429, { retryAfter, limit, remaining });
|
|
130
|
+
this.name = "RateLimitError";
|
|
131
|
+
this.retryAfter = retryAfter;
|
|
132
|
+
this.limit = limit;
|
|
133
|
+
this.remaining = remaining;
|
|
134
|
+
this.resetAt = resetAt || new Date(Date.now() + retryAfter * 1e3);
|
|
135
|
+
Object.setPrototypeOf(this, _RateLimitError.prototype);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
ValidationError = class _ValidationError extends RainfallError {
|
|
139
|
+
constructor(message, details) {
|
|
140
|
+
super(message, "VALIDATION_ERROR", 400, details);
|
|
141
|
+
this.name = "ValidationError";
|
|
142
|
+
Object.setPrototypeOf(this, _ValidationError.prototype);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
NotFoundError = class _NotFoundError extends RainfallError {
|
|
146
|
+
constructor(resource, identifier) {
|
|
147
|
+
super(
|
|
148
|
+
`${resource}${identifier ? ` '${identifier}'` : ""} not found`,
|
|
149
|
+
"NOT_FOUND_ERROR",
|
|
150
|
+
404,
|
|
151
|
+
{ resource, identifier }
|
|
152
|
+
);
|
|
153
|
+
this.name = "NotFoundError";
|
|
154
|
+
Object.setPrototypeOf(this, _NotFoundError.prototype);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
ServerError = class _ServerError extends RainfallError {
|
|
158
|
+
constructor(message = "Internal server error", statusCode = 500) {
|
|
159
|
+
super(message, "SERVER_ERROR", statusCode);
|
|
160
|
+
this.name = "ServerError";
|
|
161
|
+
Object.setPrototypeOf(this, _ServerError.prototype);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
TimeoutError = class _TimeoutError extends RainfallError {
|
|
165
|
+
constructor(timeoutMs) {
|
|
166
|
+
super(`Request timed out after ${timeoutMs}ms`, "TIMEOUT_ERROR", 408);
|
|
167
|
+
this.name = "TimeoutError";
|
|
168
|
+
Object.setPrototypeOf(this, _TimeoutError.prototype);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
NetworkError = class _NetworkError extends RainfallError {
|
|
172
|
+
constructor(message = "Network error", details) {
|
|
173
|
+
super(message, "NETWORK_ERROR", void 0, details);
|
|
174
|
+
this.name = "NetworkError";
|
|
175
|
+
Object.setPrototypeOf(this, _NetworkError.prototype);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
});
|
|
161
180
|
|
|
162
181
|
// src/client.ts
|
|
163
|
-
var DEFAULT_BASE_URL
|
|
164
|
-
var
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
182
|
+
var DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_RETRIES, DEFAULT_RETRY_DELAY, RainfallClient;
|
|
183
|
+
var init_client = __esm({
|
|
184
|
+
"src/client.ts"() {
|
|
185
|
+
"use strict";
|
|
186
|
+
init_cjs_shims();
|
|
187
|
+
init_errors();
|
|
188
|
+
DEFAULT_BASE_URL = "https://olympic-api.pragma-digital.org/v1";
|
|
189
|
+
DEFAULT_TIMEOUT = 3e4;
|
|
190
|
+
DEFAULT_RETRIES = 3;
|
|
191
|
+
DEFAULT_RETRY_DELAY = 1e3;
|
|
192
|
+
RainfallClient = class {
|
|
193
|
+
apiKey;
|
|
194
|
+
baseUrl;
|
|
195
|
+
defaultTimeout;
|
|
196
|
+
defaultRetries;
|
|
197
|
+
defaultRetryDelay;
|
|
198
|
+
lastRateLimitInfo;
|
|
199
|
+
subscriberId;
|
|
200
|
+
constructor(config) {
|
|
201
|
+
this.apiKey = config.apiKey;
|
|
202
|
+
this.baseUrl = config.baseUrl || DEFAULT_BASE_URL;
|
|
203
|
+
this.defaultTimeout = config.timeout || DEFAULT_TIMEOUT;
|
|
204
|
+
this.defaultRetries = config.retries ?? DEFAULT_RETRIES;
|
|
205
|
+
this.defaultRetryDelay = config.retryDelay || DEFAULT_RETRY_DELAY;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Get the last rate limit info from the API
|
|
209
|
+
*/
|
|
210
|
+
getRateLimitInfo() {
|
|
211
|
+
return this.lastRateLimitInfo;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Make an authenticated request to the Rainfall API
|
|
215
|
+
*/
|
|
216
|
+
async request(path, options = {}, requestOptions) {
|
|
217
|
+
const timeout = requestOptions?.timeout ?? this.defaultTimeout;
|
|
218
|
+
const maxRetries = requestOptions?.retries ?? this.defaultRetries;
|
|
219
|
+
const retryDelay = requestOptions?.retryDelay ?? this.defaultRetryDelay;
|
|
220
|
+
const url = `${this.baseUrl}${path}`;
|
|
221
|
+
const method = options.method || "GET";
|
|
222
|
+
let lastError;
|
|
223
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
224
|
+
try {
|
|
225
|
+
const controller = new AbortController();
|
|
226
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
227
|
+
const response = await fetch(url, {
|
|
228
|
+
method,
|
|
229
|
+
headers: {
|
|
230
|
+
"x-api-key": this.apiKey,
|
|
231
|
+
"Content-Type": "application/json",
|
|
232
|
+
"Accept": "application/json",
|
|
233
|
+
"X-Rainfall-SDK-Version": "0.1.0",
|
|
234
|
+
...options.headers
|
|
235
|
+
},
|
|
236
|
+
body: options.body ? JSON.stringify(options.body) : void 0,
|
|
237
|
+
signal: controller.signal
|
|
238
|
+
});
|
|
239
|
+
clearTimeout(timeoutId);
|
|
240
|
+
const limit = response.headers.get("x-ratelimit-limit");
|
|
241
|
+
const remaining = response.headers.get("x-ratelimit-remaining");
|
|
242
|
+
const reset = response.headers.get("x-ratelimit-reset");
|
|
243
|
+
if (limit && remaining && reset) {
|
|
244
|
+
this.lastRateLimitInfo = {
|
|
245
|
+
limit: parseInt(limit, 10),
|
|
246
|
+
remaining: parseInt(remaining, 10),
|
|
247
|
+
resetAt: new Date(parseInt(reset, 10) * 1e3)
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
let data;
|
|
251
|
+
const contentType = response.headers.get("content-type");
|
|
252
|
+
if (contentType?.includes("application/json")) {
|
|
253
|
+
data = await response.json();
|
|
254
|
+
} else {
|
|
255
|
+
data = await response.text();
|
|
256
|
+
}
|
|
257
|
+
if (!response.ok) {
|
|
258
|
+
throw parseErrorResponse(response, data);
|
|
259
|
+
}
|
|
260
|
+
return data;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (error instanceof RainfallError) {
|
|
263
|
+
if (error.statusCode && error.statusCode >= 400 && error.statusCode < 500 && error.statusCode !== 429) {
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
if (error.statusCode === 401) {
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
271
|
+
lastError = new TimeoutError(timeout);
|
|
272
|
+
} else if (error instanceof TypeError) {
|
|
273
|
+
lastError = new NetworkError(error.message);
|
|
274
|
+
} else {
|
|
275
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
276
|
+
}
|
|
277
|
+
if (attempt >= maxRetries) {
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
const delay = retryDelay * Math.pow(2, attempt) + Math.random() * 1e3;
|
|
281
|
+
await this.sleep(delay);
|
|
282
|
+
}
|
|
224
283
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
284
|
+
throw lastError || new RainfallError("Request failed", "REQUEST_FAILED");
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Execute a tool/node by ID
|
|
288
|
+
*/
|
|
289
|
+
async executeTool(toolId, params, options) {
|
|
290
|
+
const subscriberId = await this.ensureSubscriberId();
|
|
291
|
+
const response = await this.request(`/olympic/subscribers/${subscriberId}/nodes/${toolId}`, {
|
|
292
|
+
method: "POST",
|
|
293
|
+
body: params || {}
|
|
294
|
+
}, options);
|
|
295
|
+
return response.result;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* List all available tools
|
|
299
|
+
*/
|
|
300
|
+
async listTools() {
|
|
301
|
+
const subscriberId = await this.ensureSubscriberId();
|
|
302
|
+
const result = await this.request(`/olympic/subscribers/${subscriberId}/nodes/_utils/node-descriptions`);
|
|
303
|
+
if (result.success && result.nodes) {
|
|
304
|
+
return Object.values(result.nodes);
|
|
231
305
|
}
|
|
232
|
-
|
|
233
|
-
|
|
306
|
+
const legacyResult = await this.request(`/olympic/subscribers/${subscriberId}/nodes/_utils/node-list`);
|
|
307
|
+
if (legacyResult.keys && Array.isArray(legacyResult.keys)) {
|
|
308
|
+
return legacyResult.keys.map((key) => ({
|
|
309
|
+
id: key,
|
|
310
|
+
name: key,
|
|
311
|
+
description: "",
|
|
312
|
+
category: "general"
|
|
313
|
+
}));
|
|
234
314
|
}
|
|
235
|
-
return
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
315
|
+
return legacyResult.nodes || [];
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Get tool schema/parameters
|
|
319
|
+
*/
|
|
320
|
+
async getToolSchema(toolId) {
|
|
321
|
+
const subscriberId = await this.ensureSubscriberId();
|
|
322
|
+
const response = await this.request(`/olympic/subscribers/${subscriberId}/nodes/${toolId}/params`);
|
|
323
|
+
return response.params;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Get subscriber info
|
|
327
|
+
*/
|
|
328
|
+
async getMe() {
|
|
329
|
+
const result = await this.request("/olympic/subscribers/me");
|
|
330
|
+
if (result.subscriber?.id) {
|
|
331
|
+
this.subscriberId = result.subscriber.id;
|
|
332
|
+
}
|
|
333
|
+
const subscriber = result.subscriber;
|
|
334
|
+
return {
|
|
335
|
+
id: subscriber.id,
|
|
336
|
+
name: subscriber.name,
|
|
337
|
+
email: subscriber.google_id,
|
|
338
|
+
billingStatus: subscriber.billing_status,
|
|
339
|
+
plan: subscriber.billing_status,
|
|
340
|
+
usage: {
|
|
341
|
+
callsThisMonth: subscriber.metadata?.usage?.callsThisMonth ?? 0,
|
|
342
|
+
callsLimit: subscriber.metadata?.usage?.callsLimit ?? 5e3
|
|
243
343
|
}
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Ensure we have a subscriber ID, fetching it if necessary
|
|
348
|
+
*/
|
|
349
|
+
async ensureSubscriberId() {
|
|
350
|
+
if (this.subscriberId) {
|
|
351
|
+
return this.subscriberId;
|
|
244
352
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
lastError = new NetworkError(error.message);
|
|
249
|
-
} else {
|
|
250
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
353
|
+
const me = await this.getMe();
|
|
354
|
+
if (!me.id) {
|
|
355
|
+
throw new RainfallError("Failed to get subscriber ID", "NO_SUBSCRIBER_ID");
|
|
251
356
|
}
|
|
252
|
-
|
|
253
|
-
|
|
357
|
+
return me.id;
|
|
358
|
+
}
|
|
359
|
+
sleep(ms) {
|
|
360
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* OpenAI-compatible chat completions with tool support
|
|
364
|
+
*/
|
|
365
|
+
async chatCompletions(params) {
|
|
366
|
+
const { subscriber_id, ...body } = params;
|
|
367
|
+
if (body.stream) {
|
|
368
|
+
const url = `${this.baseUrl}/olympic/subscribers/${subscriber_id}/v1/chat/completions`;
|
|
369
|
+
const response = await fetch(url, {
|
|
370
|
+
method: "POST",
|
|
371
|
+
headers: {
|
|
372
|
+
"x-api-key": this.apiKey,
|
|
373
|
+
"Content-Type": "application/json",
|
|
374
|
+
"Accept": "text/event-stream"
|
|
375
|
+
},
|
|
376
|
+
body: JSON.stringify(body)
|
|
377
|
+
});
|
|
378
|
+
if (!response.ok) {
|
|
379
|
+
const error = await response.text();
|
|
380
|
+
throw new RainfallError(`Chat completions failed: ${error}`, "CHAT_ERROR");
|
|
381
|
+
}
|
|
382
|
+
if (!response.body) {
|
|
383
|
+
throw new RainfallError("No response body", "CHAT_ERROR");
|
|
384
|
+
}
|
|
385
|
+
return response.body;
|
|
254
386
|
}
|
|
255
|
-
|
|
256
|
-
|
|
387
|
+
return this.request(
|
|
388
|
+
`/olympic/subscribers/${subscriber_id}/v1/chat/completions`,
|
|
389
|
+
{
|
|
390
|
+
method: "POST",
|
|
391
|
+
body
|
|
392
|
+
}
|
|
393
|
+
);
|
|
257
394
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
method: "POST",
|
|
268
|
-
body: params || {}
|
|
269
|
-
}, options);
|
|
270
|
-
return response.result;
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* List all available tools
|
|
274
|
-
*/
|
|
275
|
-
async listTools() {
|
|
276
|
-
const subscriberId = await this.ensureSubscriberId();
|
|
277
|
-
const result = await this.request(`/olympic/subscribers/${subscriberId}/nodes/_utils/node-descriptions`);
|
|
278
|
-
if (result.success && result.nodes) {
|
|
279
|
-
return Object.values(result.nodes);
|
|
280
|
-
}
|
|
281
|
-
const legacyResult = await this.request(`/olympic/subscribers/${subscriberId}/nodes/_utils/node-list`);
|
|
282
|
-
if (legacyResult.keys && Array.isArray(legacyResult.keys)) {
|
|
283
|
-
return legacyResult.keys.map((key) => ({
|
|
284
|
-
id: key,
|
|
285
|
-
name: key,
|
|
286
|
-
description: "",
|
|
287
|
-
category: "general"
|
|
288
|
-
}));
|
|
289
|
-
}
|
|
290
|
-
return legacyResult.nodes || [];
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Get tool schema/parameters
|
|
294
|
-
*/
|
|
295
|
-
async getToolSchema(toolId) {
|
|
296
|
-
const subscriberId = await this.ensureSubscriberId();
|
|
297
|
-
const response = await this.request(`/olympic/subscribers/${subscriberId}/nodes/${toolId}/params`);
|
|
298
|
-
return response.params;
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Get subscriber info
|
|
302
|
-
*/
|
|
303
|
-
async getMe() {
|
|
304
|
-
const result = await this.request("/olympic/subscribers/me");
|
|
305
|
-
if (result.subscriber?.id) {
|
|
306
|
-
this.subscriberId = result.subscriber.id;
|
|
307
|
-
}
|
|
308
|
-
const subscriber = result.subscriber;
|
|
309
|
-
return {
|
|
310
|
-
id: subscriber.id,
|
|
311
|
-
name: subscriber.name,
|
|
312
|
-
email: subscriber.google_id,
|
|
313
|
-
billingStatus: subscriber.billing_status,
|
|
314
|
-
plan: subscriber.billing_status,
|
|
315
|
-
usage: {
|
|
316
|
-
callsThisMonth: subscriber.metadata?.usage?.callsThisMonth ?? 0,
|
|
317
|
-
callsLimit: subscriber.metadata?.usage?.callsLimit ?? 5e3
|
|
395
|
+
/**
|
|
396
|
+
* List available models (OpenAI-compatible format)
|
|
397
|
+
*/
|
|
398
|
+
async listModels(subscriberId) {
|
|
399
|
+
const sid = subscriberId || this.subscriberId || await this.ensureSubscriberId();
|
|
400
|
+
const result = await this.request(
|
|
401
|
+
`/olympic/subscribers/${sid}/v1/models`
|
|
402
|
+
);
|
|
403
|
+
return result.data || [];
|
|
318
404
|
}
|
|
319
405
|
};
|
|
320
406
|
}
|
|
321
|
-
|
|
322
|
-
* Ensure we have a subscriber ID, fetching it if necessary
|
|
323
|
-
*/
|
|
324
|
-
async ensureSubscriberId() {
|
|
325
|
-
if (this.subscriberId) {
|
|
326
|
-
return this.subscriberId;
|
|
327
|
-
}
|
|
328
|
-
const me = await this.getMe();
|
|
329
|
-
if (!me.id) {
|
|
330
|
-
throw new RainfallError("Failed to get subscriber ID", "NO_SUBSCRIBER_ID");
|
|
331
|
-
}
|
|
332
|
-
return me.id;
|
|
333
|
-
}
|
|
334
|
-
sleep(ms) {
|
|
335
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
336
|
-
}
|
|
337
|
-
};
|
|
407
|
+
});
|
|
338
408
|
|
|
339
409
|
// src/namespaces/integrations.ts
|
|
340
410
|
function createIntegrations(client) {
|
|
341
411
|
return new IntegrationsNamespace(client);
|
|
342
412
|
}
|
|
343
|
-
var IntegrationsNamespace
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
list: (params) => this.client.executeTool("github-list-issues", params),
|
|
352
|
-
get: (params) => this.client.executeTool("github-get-issue", params),
|
|
353
|
-
update: (params) => this.client.executeTool("github-update-issue", params),
|
|
354
|
-
addComment: (params) => this.client.executeTool("github-add-issue-comment", params)
|
|
355
|
-
},
|
|
356
|
-
repos: {
|
|
357
|
-
get: (params) => this.client.executeTool("github-get-repository", params),
|
|
358
|
-
listBranches: (params) => this.client.executeTool("github-list-branches", params)
|
|
359
|
-
},
|
|
360
|
-
pullRequests: {
|
|
361
|
-
list: (params) => this.client.executeTool("github-list-pull-requests", params),
|
|
362
|
-
get: (params) => this.client.executeTool("github-get-pull-request", params)
|
|
413
|
+
var IntegrationsNamespace;
|
|
414
|
+
var init_integrations = __esm({
|
|
415
|
+
"src/namespaces/integrations.ts"() {
|
|
416
|
+
"use strict";
|
|
417
|
+
init_cjs_shims();
|
|
418
|
+
IntegrationsNamespace = class {
|
|
419
|
+
constructor(client) {
|
|
420
|
+
this.client = client;
|
|
363
421
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
422
|
+
get github() {
|
|
423
|
+
return {
|
|
424
|
+
issues: {
|
|
425
|
+
create: (params) => this.client.executeTool("github-create-issue", params),
|
|
426
|
+
list: (params) => this.client.executeTool("github-list-issues", params),
|
|
427
|
+
get: (params) => this.client.executeTool("github-get-issue", params),
|
|
428
|
+
update: (params) => this.client.executeTool("github-update-issue", params),
|
|
429
|
+
addComment: (params) => this.client.executeTool("github-add-issue-comment", params)
|
|
430
|
+
},
|
|
431
|
+
repos: {
|
|
432
|
+
get: (params) => this.client.executeTool("github-get-repository", params),
|
|
433
|
+
listBranches: (params) => this.client.executeTool("github-list-branches", params)
|
|
434
|
+
},
|
|
435
|
+
pullRequests: {
|
|
436
|
+
list: (params) => this.client.executeTool("github-list-pull-requests", params),
|
|
437
|
+
get: (params) => this.client.executeTool("github-get-pull-request", params)
|
|
438
|
+
}
|
|
439
|
+
};
|
|
380
440
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
441
|
+
get notion() {
|
|
442
|
+
return {
|
|
443
|
+
pages: {
|
|
444
|
+
create: (params) => this.client.executeTool("notion-pages-create", params),
|
|
445
|
+
retrieve: (params) => this.client.executeTool("notion-pages-retrieve", params),
|
|
446
|
+
update: (params) => this.client.executeTool("notion-pages-update", params)
|
|
447
|
+
},
|
|
448
|
+
databases: {
|
|
449
|
+
query: (params) => this.client.executeTool("notion-databases-query", params),
|
|
450
|
+
retrieve: (params) => this.client.executeTool("notion-databases-retrieve", params)
|
|
451
|
+
},
|
|
452
|
+
blocks: {
|
|
453
|
+
appendChildren: (params) => this.client.executeTool("notion-blocks-append-children", params),
|
|
454
|
+
retrieveChildren: (params) => this.client.executeTool("notion-blocks-retrieve-children", params)
|
|
455
|
+
}
|
|
456
|
+
};
|
|
394
457
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
},
|
|
409
|
-
reactions: {
|
|
410
|
-
add: (params) => this.client.executeTool("slack-core-addReaction", params)
|
|
458
|
+
get linear() {
|
|
459
|
+
return {
|
|
460
|
+
issues: {
|
|
461
|
+
create: (params) => this.client.executeTool("linear-core-issueCreate", params),
|
|
462
|
+
list: (params) => this.client.executeTool("linear-core-issues", params),
|
|
463
|
+
get: (params) => this.client.executeTool("linear-core-issue", params),
|
|
464
|
+
update: (params) => this.client.executeTool("linear-core-issueUpdate", params),
|
|
465
|
+
archive: (params) => this.client.executeTool("linear-core-issueArchive", params)
|
|
466
|
+
},
|
|
467
|
+
teams: {
|
|
468
|
+
list: () => this.client.executeTool("linear-core-teams", {})
|
|
469
|
+
}
|
|
470
|
+
};
|
|
411
471
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
472
|
+
get slack() {
|
|
473
|
+
return {
|
|
474
|
+
messages: {
|
|
475
|
+
send: (params) => this.client.executeTool("slack-core-postMessage", params),
|
|
476
|
+
list: (params) => this.client.executeTool("slack-core-listMessages", params)
|
|
477
|
+
},
|
|
478
|
+
channels: {
|
|
479
|
+
list: () => this.client.executeTool("slack-core-listChannels", {})
|
|
480
|
+
},
|
|
481
|
+
users: {
|
|
482
|
+
list: () => this.client.executeTool("slack-core-listUsers", {})
|
|
483
|
+
},
|
|
484
|
+
reactions: {
|
|
485
|
+
add: (params) => this.client.executeTool("slack-core-addReaction", params)
|
|
486
|
+
}
|
|
487
|
+
};
|
|
426
488
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
489
|
+
get figma() {
|
|
490
|
+
return {
|
|
491
|
+
files: {
|
|
492
|
+
get: (params) => this.client.executeTool("figma-files-getFile", { fileKey: params.fileKey }),
|
|
493
|
+
getNodes: (params) => this.client.executeTool("figma-files-getFileNodes", { fileKey: params.fileKey, nodeIds: params.nodeIds }),
|
|
494
|
+
getImages: (params) => this.client.executeTool("figma-files-getFileImage", { fileKey: params.fileKey, nodeIds: params.nodeIds, format: params.format }),
|
|
495
|
+
getComments: (params) => this.client.executeTool("figma-comments-getFileComments", { fileKey: params.fileKey }),
|
|
496
|
+
postComment: (params) => this.client.executeTool("figma-comments-postComment", { fileKey: params.fileKey, message: params.message, nodeId: params.nodeId })
|
|
497
|
+
},
|
|
498
|
+
projects: {
|
|
499
|
+
list: (params) => this.client.executeTool("figma-projects-getTeamProjects", { teamId: params.teamId }),
|
|
500
|
+
getFiles: (params) => this.client.executeTool("figma-projects-getProjectFiles", { projectId: params.projectId })
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
get stripe() {
|
|
505
|
+
return {
|
|
506
|
+
customers: {
|
|
507
|
+
create: (params) => this.client.executeTool("stripe-customers-create", params),
|
|
508
|
+
retrieve: (params) => this.client.executeTool("stripe-customers-retrieve", { customerId: params.customerId }),
|
|
509
|
+
update: (params) => this.client.executeTool("stripe-customers-update", params),
|
|
510
|
+
listPaymentMethods: (params) => this.client.executeTool("stripe-customers-list-payment-methods", { customerId: params.customerId })
|
|
511
|
+
},
|
|
512
|
+
paymentIntents: {
|
|
513
|
+
create: (params) => this.client.executeTool("stripe-payment-intents-create", params),
|
|
514
|
+
retrieve: (params) => this.client.executeTool("stripe-payment-intents-retrieve", { paymentIntentId: params.paymentIntentId }),
|
|
515
|
+
confirm: (params) => this.client.executeTool("stripe-payment-intents-confirm", { paymentIntentId: params.paymentIntentId })
|
|
516
|
+
},
|
|
517
|
+
subscriptions: {
|
|
518
|
+
create: (params) => this.client.executeTool("stripe-subscriptions-create", params),
|
|
519
|
+
retrieve: (params) => this.client.executeTool("stripe-subscriptions-retrieve", { subscriptionId: params.subscriptionId }),
|
|
520
|
+
cancel: (params) => this.client.executeTool("stripe-subscriptions-cancel", { subscriptionId: params.subscriptionId })
|
|
521
|
+
}
|
|
522
|
+
};
|
|
446
523
|
}
|
|
447
524
|
};
|
|
448
525
|
}
|
|
449
|
-
};
|
|
526
|
+
});
|
|
450
527
|
|
|
451
528
|
// src/namespaces/memory.ts
|
|
452
529
|
function createMemory(client) {
|
|
@@ -459,6 +536,12 @@ function createMemory(client) {
|
|
|
459
536
|
delete: (params) => client.executeTool("memory-delete", { memoryId: params.memoryId })
|
|
460
537
|
};
|
|
461
538
|
}
|
|
539
|
+
var init_memory = __esm({
|
|
540
|
+
"src/namespaces/memory.ts"() {
|
|
541
|
+
"use strict";
|
|
542
|
+
init_cjs_shims();
|
|
543
|
+
}
|
|
544
|
+
});
|
|
462
545
|
|
|
463
546
|
// src/namespaces/articles.ts
|
|
464
547
|
function createArticles(client) {
|
|
@@ -473,6 +556,12 @@ function createArticles(client) {
|
|
|
473
556
|
extractTopics: (params) => client.executeTool("article-topic-extractor", params)
|
|
474
557
|
};
|
|
475
558
|
}
|
|
559
|
+
var init_articles = __esm({
|
|
560
|
+
"src/namespaces/articles.ts"() {
|
|
561
|
+
"use strict";
|
|
562
|
+
init_cjs_shims();
|
|
563
|
+
}
|
|
564
|
+
});
|
|
476
565
|
|
|
477
566
|
// src/namespaces/web.ts
|
|
478
567
|
function createWeb(client) {
|
|
@@ -486,335 +575,2974 @@ function createWeb(client) {
|
|
|
486
575
|
extractHtml: (params) => client.executeTool("extract-html-selector", params)
|
|
487
576
|
};
|
|
488
577
|
}
|
|
578
|
+
var init_web = __esm({
|
|
579
|
+
"src/namespaces/web.ts"() {
|
|
580
|
+
"use strict";
|
|
581
|
+
init_cjs_shims();
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// src/namespaces/ai.ts
|
|
586
|
+
function createAI(client) {
|
|
587
|
+
return {
|
|
588
|
+
embeddings: {
|
|
589
|
+
document: (params) => client.executeTool("jina-document-embedding", params),
|
|
590
|
+
query: (params) => client.executeTool("jina-query-embedding", params),
|
|
591
|
+
image: (params) => client.executeTool("jina-image-embedding", { image: params.imageBase64 })
|
|
592
|
+
},
|
|
593
|
+
image: {
|
|
594
|
+
generate: (params) => client.executeTool("image-generation", params)
|
|
595
|
+
},
|
|
596
|
+
ocr: (params) => client.executeTool("ocr-text-extraction", { image: params.imageBase64 }),
|
|
597
|
+
vision: (params) => client.executeTool("llama-scout-vision", { image: params.imageBase64, prompt: params.prompt }),
|
|
598
|
+
chat: (params) => client.executeTool("xai-chat-completions", params),
|
|
599
|
+
complete: (params) => client.executeTool("fim", params),
|
|
600
|
+
classify: (params) => client.executeTool("jina-document-classifier", params),
|
|
601
|
+
segment: (params) => client.executeTool("jina-text-segmenter", params),
|
|
602
|
+
/**
|
|
603
|
+
* OpenAI-compatible chat completions with full tool support
|
|
604
|
+
* This is the recommended method for multi-turn conversations with tools
|
|
605
|
+
*/
|
|
606
|
+
chatCompletions: (params) => client.chatCompletions(params)
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
var init_ai = __esm({
|
|
610
|
+
"src/namespaces/ai.ts"() {
|
|
611
|
+
"use strict";
|
|
612
|
+
init_cjs_shims();
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// src/namespaces/data.ts
|
|
617
|
+
function createData(client) {
|
|
618
|
+
return {
|
|
619
|
+
csv: {
|
|
620
|
+
query: (params) => client.executeTool("query-csv", params),
|
|
621
|
+
convert: (params) => client.executeTool("csv-convert", params)
|
|
622
|
+
},
|
|
623
|
+
scripts: {
|
|
624
|
+
create: (params) => client.executeTool("create-saved-script", params),
|
|
625
|
+
execute: (params) => client.executeTool("execute-saved-script", params),
|
|
626
|
+
list: () => client.executeTool("list-saved-scripts", {}),
|
|
627
|
+
update: (params) => client.executeTool("update-saved-script", params),
|
|
628
|
+
delete: (params) => client.executeTool("delete-saved-script", params)
|
|
629
|
+
},
|
|
630
|
+
similarity: {
|
|
631
|
+
search: (params) => client.executeTool("duck-db-similarity-search", params),
|
|
632
|
+
duckDbSearch: (params) => client.executeTool("duck-db-similarity-search", params)
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
var init_data = __esm({
|
|
637
|
+
"src/namespaces/data.ts"() {
|
|
638
|
+
"use strict";
|
|
639
|
+
init_cjs_shims();
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// src/namespaces/utils.ts
|
|
644
|
+
function createUtils(client) {
|
|
645
|
+
return {
|
|
646
|
+
mermaid: (params) => client.executeTool("mermaid-diagram-generator", { mermaid: params.diagram }),
|
|
647
|
+
documentConvert: (params) => client.executeTool("document-format-converter", {
|
|
648
|
+
base64: `data:${params.mimeType};base64,${Buffer.from(params.document).toString("base64")}`,
|
|
649
|
+
format: params.format
|
|
650
|
+
}),
|
|
651
|
+
regex: {
|
|
652
|
+
match: (params) => client.executeTool("regex-match", params),
|
|
653
|
+
replace: (params) => client.executeTool("regex-replace", params)
|
|
654
|
+
},
|
|
655
|
+
jsonExtract: (params) => client.executeTool("json-extract", params),
|
|
656
|
+
digest: (params) => client.executeTool("digest-generator", { text: params.data }),
|
|
657
|
+
monteCarlo: (params) => client.executeTool("monte-carlo-simulation", params)
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
var init_utils = __esm({
|
|
661
|
+
"src/namespaces/utils.ts"() {
|
|
662
|
+
"use strict";
|
|
663
|
+
init_cjs_shims();
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// src/sdk.ts
|
|
668
|
+
var Rainfall;
|
|
669
|
+
var init_sdk = __esm({
|
|
670
|
+
"src/sdk.ts"() {
|
|
671
|
+
"use strict";
|
|
672
|
+
init_cjs_shims();
|
|
673
|
+
init_client();
|
|
674
|
+
init_integrations();
|
|
675
|
+
init_memory();
|
|
676
|
+
init_articles();
|
|
677
|
+
init_web();
|
|
678
|
+
init_ai();
|
|
679
|
+
init_data();
|
|
680
|
+
init_utils();
|
|
681
|
+
Rainfall = class {
|
|
682
|
+
client;
|
|
683
|
+
_integrations;
|
|
684
|
+
_memory;
|
|
685
|
+
_articles;
|
|
686
|
+
_web;
|
|
687
|
+
_ai;
|
|
688
|
+
_data;
|
|
689
|
+
_utils;
|
|
690
|
+
constructor(config) {
|
|
691
|
+
this.client = new RainfallClient(config);
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Integrations namespace - GitHub, Notion, Linear, Slack, Figma, Stripe
|
|
695
|
+
*
|
|
696
|
+
* @example
|
|
697
|
+
* ```typescript
|
|
698
|
+
* // GitHub
|
|
699
|
+
* await rainfall.integrations.github.issues.create({
|
|
700
|
+
* owner: 'facebook',
|
|
701
|
+
* repo: 'react',
|
|
702
|
+
* title: 'Bug report'
|
|
703
|
+
* });
|
|
704
|
+
*
|
|
705
|
+
* // Slack
|
|
706
|
+
* await rainfall.integrations.slack.messages.send({
|
|
707
|
+
* channelId: 'C123456',
|
|
708
|
+
* text: 'Hello team!'
|
|
709
|
+
* });
|
|
710
|
+
*
|
|
711
|
+
* // Linear
|
|
712
|
+
* const issues = await rainfall.integrations.linear.issues.list();
|
|
713
|
+
* ```
|
|
714
|
+
*/
|
|
715
|
+
get integrations() {
|
|
716
|
+
if (!this._integrations) {
|
|
717
|
+
this._integrations = createIntegrations(this.client);
|
|
718
|
+
}
|
|
719
|
+
return this._integrations;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Memory namespace - Semantic memory storage and retrieval
|
|
723
|
+
*
|
|
724
|
+
* @example
|
|
725
|
+
* ```typescript
|
|
726
|
+
* // Store a memory
|
|
727
|
+
* await rainfall.memory.create({
|
|
728
|
+
* content: 'User prefers dark mode',
|
|
729
|
+
* keywords: ['preference', 'ui']
|
|
730
|
+
* });
|
|
731
|
+
*
|
|
732
|
+
* // Recall similar memories
|
|
733
|
+
* const memories = await rainfall.memory.recall({
|
|
734
|
+
* query: 'user preferences',
|
|
735
|
+
* topK: 5
|
|
736
|
+
* });
|
|
737
|
+
* ```
|
|
738
|
+
*/
|
|
739
|
+
get memory() {
|
|
740
|
+
if (!this._memory) {
|
|
741
|
+
this._memory = createMemory(this.client);
|
|
742
|
+
}
|
|
743
|
+
return this._memory;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Articles namespace - News aggregation and article management
|
|
747
|
+
*
|
|
748
|
+
* @example
|
|
749
|
+
* ```typescript
|
|
750
|
+
* // Search news
|
|
751
|
+
* const articles = await rainfall.articles.search({
|
|
752
|
+
* query: 'artificial intelligence'
|
|
753
|
+
* });
|
|
754
|
+
*
|
|
755
|
+
* // Create from URL
|
|
756
|
+
* const article = await rainfall.articles.createFromUrl({
|
|
757
|
+
* url: 'https://example.com/article'
|
|
758
|
+
* });
|
|
759
|
+
*
|
|
760
|
+
* // Summarize
|
|
761
|
+
* const summary = await rainfall.articles.summarize({
|
|
762
|
+
* text: article.content
|
|
763
|
+
* });
|
|
764
|
+
* ```
|
|
765
|
+
*/
|
|
766
|
+
get articles() {
|
|
767
|
+
if (!this._articles) {
|
|
768
|
+
this._articles = createArticles(this.client);
|
|
769
|
+
}
|
|
770
|
+
return this._articles;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Web namespace - Web search, scraping, and content extraction
|
|
774
|
+
*
|
|
775
|
+
* @example
|
|
776
|
+
* ```typescript
|
|
777
|
+
* // Search with Exa
|
|
778
|
+
* const results = await rainfall.web.search.exa({
|
|
779
|
+
* query: 'latest AI research'
|
|
780
|
+
* });
|
|
781
|
+
*
|
|
782
|
+
* // Fetch and convert
|
|
783
|
+
* const html = await rainfall.web.fetch({ url: 'https://example.com' });
|
|
784
|
+
* const markdown = await rainfall.web.htmlToMarkdown({ html });
|
|
785
|
+
*
|
|
786
|
+
* // Extract specific elements
|
|
787
|
+
* const links = await rainfall.web.extractHtml({
|
|
788
|
+
* html,
|
|
789
|
+
* selector: 'a[href]'
|
|
790
|
+
* });
|
|
791
|
+
* ```
|
|
792
|
+
*/
|
|
793
|
+
get web() {
|
|
794
|
+
if (!this._web) {
|
|
795
|
+
this._web = createWeb(this.client);
|
|
796
|
+
}
|
|
797
|
+
return this._web;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* AI namespace - Embeddings, image generation, OCR, vision, chat
|
|
801
|
+
*
|
|
802
|
+
* @example
|
|
803
|
+
* ```typescript
|
|
804
|
+
* // Generate embeddings
|
|
805
|
+
* const embedding = await rainfall.ai.embeddings.document({
|
|
806
|
+
* text: 'Hello world'
|
|
807
|
+
* });
|
|
808
|
+
*
|
|
809
|
+
* // Generate image
|
|
810
|
+
* const image = await rainfall.ai.image.generate({
|
|
811
|
+
* prompt: 'A serene mountain landscape'
|
|
812
|
+
* });
|
|
813
|
+
*
|
|
814
|
+
* // OCR
|
|
815
|
+
* const text = await rainfall.ai.ocr({ imageBase64: '...' });
|
|
816
|
+
*
|
|
817
|
+
* // Chat
|
|
818
|
+
* const response = await rainfall.ai.chat({
|
|
819
|
+
* messages: [{ role: 'user', content: 'Hello!' }]
|
|
820
|
+
* });
|
|
821
|
+
* ```
|
|
822
|
+
*/
|
|
823
|
+
get ai() {
|
|
824
|
+
if (!this._ai) {
|
|
825
|
+
this._ai = createAI(this.client);
|
|
826
|
+
}
|
|
827
|
+
return this._ai;
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Data namespace - CSV processing, scripts, similarity search
|
|
831
|
+
*
|
|
832
|
+
* @example
|
|
833
|
+
* ```typescript
|
|
834
|
+
* // Query CSV with SQL
|
|
835
|
+
* const results = await rainfall.data.csv.query({
|
|
836
|
+
* sql: 'SELECT * FROM data WHERE value > 100'
|
|
837
|
+
* });
|
|
838
|
+
*
|
|
839
|
+
* // Execute saved script
|
|
840
|
+
* const result = await rainfall.data.scripts.execute({
|
|
841
|
+
* name: 'my-script',
|
|
842
|
+
* params: { input: 'data' }
|
|
843
|
+
* });
|
|
844
|
+
* ```
|
|
845
|
+
*/
|
|
846
|
+
get data() {
|
|
847
|
+
if (!this._data) {
|
|
848
|
+
this._data = createData(this.client);
|
|
849
|
+
}
|
|
850
|
+
return this._data;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Utils namespace - Mermaid diagrams, document conversion, regex, JSON extraction
|
|
854
|
+
*
|
|
855
|
+
* @example
|
|
856
|
+
* ```typescript
|
|
857
|
+
* // Generate diagram
|
|
858
|
+
* const diagram = await rainfall.utils.mermaid({
|
|
859
|
+
* diagram: 'graph TD; A-->B;'
|
|
860
|
+
* });
|
|
861
|
+
*
|
|
862
|
+
* // Convert document
|
|
863
|
+
* const pdf = await rainfall.utils.documentConvert({
|
|
864
|
+
* document: markdownContent,
|
|
865
|
+
* mimeType: 'text/markdown',
|
|
866
|
+
* format: 'pdf'
|
|
867
|
+
* });
|
|
868
|
+
*
|
|
869
|
+
* // Extract JSON from text
|
|
870
|
+
* const json = await rainfall.utils.jsonExtract({
|
|
871
|
+
* text: 'Here is some data: {"key": "value"}'
|
|
872
|
+
* });
|
|
873
|
+
* ```
|
|
874
|
+
*/
|
|
875
|
+
get utils() {
|
|
876
|
+
if (!this._utils) {
|
|
877
|
+
this._utils = createUtils(this.client);
|
|
878
|
+
}
|
|
879
|
+
return this._utils;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Get the underlying HTTP client for advanced usage
|
|
883
|
+
*/
|
|
884
|
+
getClient() {
|
|
885
|
+
return this.client;
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* List all available tools
|
|
889
|
+
*/
|
|
890
|
+
async listTools() {
|
|
891
|
+
return this.client.listTools();
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Get schema for a specific tool
|
|
895
|
+
*/
|
|
896
|
+
async getToolSchema(toolId) {
|
|
897
|
+
return this.client.getToolSchema(toolId);
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Execute any tool by ID (low-level access)
|
|
901
|
+
*/
|
|
902
|
+
async executeTool(toolId, params) {
|
|
903
|
+
return this.client.executeTool(toolId, params);
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Get current subscriber info and usage
|
|
907
|
+
*/
|
|
908
|
+
async getMe() {
|
|
909
|
+
return this.client.getMe();
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Get current rate limit info
|
|
913
|
+
*/
|
|
914
|
+
getRateLimitInfo() {
|
|
915
|
+
return this.client.getRateLimitInfo();
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* OpenAI-compatible chat completions with tool support
|
|
919
|
+
*
|
|
920
|
+
* @example
|
|
921
|
+
* ```typescript
|
|
922
|
+
* // Simple chat
|
|
923
|
+
* const response = await rainfall.chatCompletions({
|
|
924
|
+
* subscriber_id: 'my-subscriber',
|
|
925
|
+
* messages: [{ role: 'user', content: 'Hello!' }],
|
|
926
|
+
* model: 'llama-3.3-70b-versatile'
|
|
927
|
+
* });
|
|
928
|
+
*
|
|
929
|
+
* // With tools
|
|
930
|
+
* const response = await rainfall.chatCompletions({
|
|
931
|
+
* subscriber_id: 'my-subscriber',
|
|
932
|
+
* messages: [{ role: 'user', content: 'Search for AI news' }],
|
|
933
|
+
* tools: [{ type: 'function', function: { name: 'web-search' } }],
|
|
934
|
+
* enable_stacked: true
|
|
935
|
+
* });
|
|
936
|
+
*
|
|
937
|
+
* // Streaming
|
|
938
|
+
* const stream = await rainfall.chatCompletions({
|
|
939
|
+
* subscriber_id: 'my-subscriber',
|
|
940
|
+
* messages: [{ role: 'user', content: 'Tell me a story' }],
|
|
941
|
+
* stream: true
|
|
942
|
+
* });
|
|
943
|
+
* ```
|
|
944
|
+
*/
|
|
945
|
+
async chatCompletions(params) {
|
|
946
|
+
return this.client.chatCompletions(params);
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* List available models (OpenAI-compatible format)
|
|
950
|
+
*
|
|
951
|
+
* @example
|
|
952
|
+
* ```typescript
|
|
953
|
+
* const models = await rainfall.listModels();
|
|
954
|
+
* console.log(models); // [{ id: 'llama-3.3-70b-versatile', ... }]
|
|
955
|
+
* ```
|
|
956
|
+
*/
|
|
957
|
+
async listModels(subscriberId) {
|
|
958
|
+
return this.client.listModels(subscriberId);
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
// src/cli/config.ts
|
|
965
|
+
var config_exports = {};
|
|
966
|
+
__export(config_exports, {
|
|
967
|
+
getConfigDir: () => getConfigDir,
|
|
968
|
+
getLLMConfig: () => getLLMConfig,
|
|
969
|
+
getProviderBaseUrl: () => getProviderBaseUrl,
|
|
970
|
+
isLocalProvider: () => isLocalProvider,
|
|
971
|
+
loadConfig: () => loadConfig,
|
|
972
|
+
saveConfig: () => saveConfig
|
|
973
|
+
});
|
|
974
|
+
function getConfigDir() {
|
|
975
|
+
return CONFIG_DIR;
|
|
976
|
+
}
|
|
977
|
+
function loadConfig() {
|
|
978
|
+
let config = {};
|
|
979
|
+
if ((0, import_fs.existsSync)(CONFIG_FILE)) {
|
|
980
|
+
try {
|
|
981
|
+
config = JSON.parse((0, import_fs.readFileSync)(CONFIG_FILE, "utf8"));
|
|
982
|
+
} catch {
|
|
983
|
+
config = {};
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
if (process.env.RAINFALL_API_KEY) {
|
|
987
|
+
config.apiKey = process.env.RAINFALL_API_KEY;
|
|
988
|
+
}
|
|
989
|
+
if (process.env.RAINFALL_BASE_URL) {
|
|
990
|
+
config.baseUrl = process.env.RAINFALL_BASE_URL;
|
|
991
|
+
}
|
|
992
|
+
if (!config.llm) {
|
|
993
|
+
config.llm = { provider: "rainfall" };
|
|
994
|
+
}
|
|
995
|
+
if (process.env.OPENAI_API_KEY) {
|
|
996
|
+
config.llm.provider = config.llm.provider || "openai";
|
|
997
|
+
config.llm.apiKey = process.env.OPENAI_API_KEY;
|
|
998
|
+
}
|
|
999
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
1000
|
+
config.llm.provider = "anthropic";
|
|
1001
|
+
config.llm.apiKey = process.env.ANTHROPIC_API_KEY;
|
|
1002
|
+
}
|
|
1003
|
+
if (process.env.OLLAMA_HOST || process.env.OLLAMA_URL) {
|
|
1004
|
+
config.llm.provider = "ollama";
|
|
1005
|
+
config.llm.baseUrl = process.env.OLLAMA_HOST || process.env.OLLAMA_URL;
|
|
1006
|
+
}
|
|
1007
|
+
if (process.env.LLM_MODEL) {
|
|
1008
|
+
config.llm.model = process.env.LLM_MODEL;
|
|
1009
|
+
}
|
|
1010
|
+
return config;
|
|
1011
|
+
}
|
|
1012
|
+
function saveConfig(config) {
|
|
1013
|
+
if (!(0, import_fs.existsSync)(CONFIG_DIR)) {
|
|
1014
|
+
(0, import_fs.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
1015
|
+
}
|
|
1016
|
+
(0, import_fs.writeFileSync)(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
1017
|
+
}
|
|
1018
|
+
function getLLMConfig(config) {
|
|
1019
|
+
const defaults = {
|
|
1020
|
+
provider: "rainfall",
|
|
1021
|
+
apiKey: config.apiKey || "",
|
|
1022
|
+
baseUrl: config.baseUrl || "https://api.rainfall.com",
|
|
1023
|
+
model: "llama-3.3-70b-versatile",
|
|
1024
|
+
options: {}
|
|
1025
|
+
};
|
|
1026
|
+
return { ...defaults, ...config.llm };
|
|
1027
|
+
}
|
|
1028
|
+
function isLocalProvider(config) {
|
|
1029
|
+
return config.llm?.provider === "ollama" || config.llm?.provider === "local";
|
|
1030
|
+
}
|
|
1031
|
+
function getProviderBaseUrl(config) {
|
|
1032
|
+
const provider = config.llm?.provider || "rainfall";
|
|
1033
|
+
switch (provider) {
|
|
1034
|
+
case "openai":
|
|
1035
|
+
return config.llm?.baseUrl || "https://api.openai.com/v1";
|
|
1036
|
+
case "anthropic":
|
|
1037
|
+
return config.llm?.baseUrl || "https://api.anthropic.com/v1";
|
|
1038
|
+
case "ollama":
|
|
1039
|
+
return config.llm?.baseUrl || "http://localhost:11434/v1";
|
|
1040
|
+
case "local":
|
|
1041
|
+
case "custom":
|
|
1042
|
+
return config.llm?.baseUrl || "http://localhost:1234/v1";
|
|
1043
|
+
case "rainfall":
|
|
1044
|
+
default:
|
|
1045
|
+
return config.baseUrl || "https://api.rainfall.com";
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
var import_fs, import_path, import_os, CONFIG_DIR, CONFIG_FILE;
|
|
1049
|
+
var init_config = __esm({
|
|
1050
|
+
"src/cli/config.ts"() {
|
|
1051
|
+
"use strict";
|
|
1052
|
+
init_cjs_shims();
|
|
1053
|
+
import_fs = require("fs");
|
|
1054
|
+
import_path = require("path");
|
|
1055
|
+
import_os = require("os");
|
|
1056
|
+
CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".rainfall");
|
|
1057
|
+
CONFIG_FILE = (0, import_path.join)(CONFIG_DIR, "config.json");
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
// src/services/networked.ts
|
|
1062
|
+
var RainfallNetworkedExecutor;
|
|
1063
|
+
var init_networked = __esm({
|
|
1064
|
+
"src/services/networked.ts"() {
|
|
1065
|
+
"use strict";
|
|
1066
|
+
init_cjs_shims();
|
|
1067
|
+
RainfallNetworkedExecutor = class {
|
|
1068
|
+
rainfall;
|
|
1069
|
+
options;
|
|
1070
|
+
edgeNodeId;
|
|
1071
|
+
jobCallbacks = /* @__PURE__ */ new Map();
|
|
1072
|
+
resultPollingInterval;
|
|
1073
|
+
constructor(rainfall, options = {}) {
|
|
1074
|
+
this.rainfall = rainfall;
|
|
1075
|
+
this.options = {
|
|
1076
|
+
wsPort: 8765,
|
|
1077
|
+
httpPort: 8787,
|
|
1078
|
+
hostname: process.env.HOSTNAME || "local-daemon",
|
|
1079
|
+
capabilities: {
|
|
1080
|
+
localExec: true,
|
|
1081
|
+
fileWatch: true,
|
|
1082
|
+
passiveListen: true
|
|
1083
|
+
},
|
|
1084
|
+
...options
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Register this edge node with the Rainfall backend
|
|
1089
|
+
*/
|
|
1090
|
+
async registerEdgeNode() {
|
|
1091
|
+
const capabilities = this.buildCapabilitiesList();
|
|
1092
|
+
try {
|
|
1093
|
+
const result = await this.rainfall.executeTool("register-edge-node", {
|
|
1094
|
+
hostname: this.options.hostname,
|
|
1095
|
+
capabilities,
|
|
1096
|
+
wsPort: this.options.wsPort,
|
|
1097
|
+
httpPort: this.options.httpPort,
|
|
1098
|
+
version: "0.1.0"
|
|
1099
|
+
});
|
|
1100
|
+
this.edgeNodeId = result.edgeNodeId;
|
|
1101
|
+
console.log(`\u{1F310} Edge node registered with Rainfall as ${this.edgeNodeId}`);
|
|
1102
|
+
return this.edgeNodeId;
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
this.edgeNodeId = `edge-${this.options.hostname}-${Date.now()}`;
|
|
1105
|
+
console.log(`\u{1F310} Edge node running in local mode (ID: ${this.edgeNodeId})`);
|
|
1106
|
+
return this.edgeNodeId;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Unregister this edge node on shutdown
|
|
1111
|
+
*/
|
|
1112
|
+
async unregisterEdgeNode() {
|
|
1113
|
+
if (!this.edgeNodeId) return;
|
|
1114
|
+
try {
|
|
1115
|
+
await this.rainfall.executeTool("unregister-edge-node", {
|
|
1116
|
+
edgeNodeId: this.edgeNodeId
|
|
1117
|
+
});
|
|
1118
|
+
console.log(`\u{1F310} Edge node ${this.edgeNodeId} unregistered`);
|
|
1119
|
+
} catch {
|
|
1120
|
+
}
|
|
1121
|
+
if (this.resultPollingInterval) {
|
|
1122
|
+
clearInterval(this.resultPollingInterval);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Queue a tool execution for distributed processing
|
|
1127
|
+
* Non-blocking - returns immediately with a job ID
|
|
1128
|
+
*/
|
|
1129
|
+
async queueToolExecution(toolId, params, options = {}) {
|
|
1130
|
+
const executionMode = options.executionMode || "any";
|
|
1131
|
+
try {
|
|
1132
|
+
const result = await this.rainfall.executeTool("queue-job", {
|
|
1133
|
+
toolId,
|
|
1134
|
+
params,
|
|
1135
|
+
executionMode,
|
|
1136
|
+
requesterEdgeNodeId: this.edgeNodeId
|
|
1137
|
+
});
|
|
1138
|
+
if (options.callback) {
|
|
1139
|
+
this.jobCallbacks.set(result.jobId, options.callback);
|
|
1140
|
+
this.startResultPolling();
|
|
1141
|
+
}
|
|
1142
|
+
return result.jobId;
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
if (executionMode === "local-only" || executionMode === "any") {
|
|
1145
|
+
try {
|
|
1146
|
+
const result = await this.rainfall.executeTool(toolId, params);
|
|
1147
|
+
if (options.callback) {
|
|
1148
|
+
options.callback(result);
|
|
1149
|
+
}
|
|
1150
|
+
return `local-${Date.now()}`;
|
|
1151
|
+
} catch (execError) {
|
|
1152
|
+
if (options.callback) {
|
|
1153
|
+
options.callback(null, String(execError));
|
|
1154
|
+
}
|
|
1155
|
+
throw execError;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
throw error;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Get status of a queued job
|
|
1163
|
+
*/
|
|
1164
|
+
async getJobStatus(jobId) {
|
|
1165
|
+
try {
|
|
1166
|
+
const result = await this.rainfall.executeTool("get-job-status", {
|
|
1167
|
+
jobId
|
|
1168
|
+
});
|
|
1169
|
+
return result.job;
|
|
1170
|
+
} catch {
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Subscribe to job results via polling (WebSocket fallback)
|
|
1176
|
+
* In the future, this will use WebSocket push from ApresMoi
|
|
1177
|
+
*/
|
|
1178
|
+
async subscribeToResults(callback) {
|
|
1179
|
+
console.log("\u{1F4E1} Subscribed to job results via Rainfall (polling mode)");
|
|
1180
|
+
this.onResultReceived = callback;
|
|
1181
|
+
}
|
|
1182
|
+
onResultReceived;
|
|
1183
|
+
/**
|
|
1184
|
+
* Start polling for job results (fallback until WebSocket push is ready)
|
|
1185
|
+
*/
|
|
1186
|
+
startResultPolling() {
|
|
1187
|
+
if (this.resultPollingInterval) return;
|
|
1188
|
+
this.resultPollingInterval = setInterval(async () => {
|
|
1189
|
+
for (const [jobId, callback] of this.jobCallbacks) {
|
|
1190
|
+
try {
|
|
1191
|
+
const job = await this.getJobStatus(jobId);
|
|
1192
|
+
if (job?.status === "completed" || job?.status === "failed") {
|
|
1193
|
+
callback(job.result, job.error);
|
|
1194
|
+
this.jobCallbacks.delete(jobId);
|
|
1195
|
+
if (this.onResultReceived) {
|
|
1196
|
+
this.onResultReceived(jobId, job.result, job.error);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
} catch {
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
if (this.jobCallbacks.size === 0 && this.resultPollingInterval) {
|
|
1203
|
+
clearInterval(this.resultPollingInterval);
|
|
1204
|
+
this.resultPollingInterval = void 0;
|
|
1205
|
+
}
|
|
1206
|
+
}, 2e3);
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Claim a job for execution on this edge node
|
|
1210
|
+
*/
|
|
1211
|
+
async claimJob() {
|
|
1212
|
+
try {
|
|
1213
|
+
const result = await this.rainfall.executeTool("claim-job", {
|
|
1214
|
+
edgeNodeId: this.edgeNodeId,
|
|
1215
|
+
capabilities: this.buildCapabilitiesList()
|
|
1216
|
+
});
|
|
1217
|
+
return result.job;
|
|
1218
|
+
} catch {
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Submit job result after execution
|
|
1224
|
+
*/
|
|
1225
|
+
async submitJobResult(jobId, result, error) {
|
|
1226
|
+
try {
|
|
1227
|
+
await this.rainfall.executeTool("submit-job-result", {
|
|
1228
|
+
jobId,
|
|
1229
|
+
edgeNodeId: this.edgeNodeId,
|
|
1230
|
+
result,
|
|
1231
|
+
error
|
|
1232
|
+
});
|
|
1233
|
+
} catch {
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Get this edge node's ID
|
|
1238
|
+
*/
|
|
1239
|
+
getEdgeNodeId() {
|
|
1240
|
+
return this.edgeNodeId;
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Build capabilities list from options
|
|
1244
|
+
*/
|
|
1245
|
+
buildCapabilitiesList() {
|
|
1246
|
+
const caps = this.options.capabilities || {};
|
|
1247
|
+
const list = [];
|
|
1248
|
+
if (caps.localExec) list.push("local-exec");
|
|
1249
|
+
if (caps.fileWatch) list.push("file-watch");
|
|
1250
|
+
if (caps.passiveListen) list.push("passive-listen");
|
|
1251
|
+
if (caps.browser) list.push("browser");
|
|
1252
|
+
if (caps.custom) list.push(...caps.custom);
|
|
1253
|
+
return list;
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
// src/services/context.ts
|
|
1260
|
+
var RainfallDaemonContext;
|
|
1261
|
+
var init_context = __esm({
|
|
1262
|
+
"src/services/context.ts"() {
|
|
1263
|
+
"use strict";
|
|
1264
|
+
init_cjs_shims();
|
|
1265
|
+
RainfallDaemonContext = class {
|
|
1266
|
+
rainfall;
|
|
1267
|
+
options;
|
|
1268
|
+
localMemories = /* @__PURE__ */ new Map();
|
|
1269
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1270
|
+
executionHistory = [];
|
|
1271
|
+
currentSessionId;
|
|
1272
|
+
constructor(rainfall, options = {}) {
|
|
1273
|
+
this.rainfall = rainfall;
|
|
1274
|
+
this.options = {
|
|
1275
|
+
maxLocalMemories: 1e3,
|
|
1276
|
+
maxMessageHistory: 100,
|
|
1277
|
+
maxExecutionHistory: 500,
|
|
1278
|
+
sessionTtl: 24 * 60 * 60 * 1e3,
|
|
1279
|
+
// 24 hours
|
|
1280
|
+
...options
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Initialize the context - load recent memories from cloud
|
|
1285
|
+
*/
|
|
1286
|
+
async initialize() {
|
|
1287
|
+
try {
|
|
1288
|
+
const recentMemories = await this.rainfall.memory.recall({
|
|
1289
|
+
query: "daemon:context",
|
|
1290
|
+
topK: this.options.maxLocalMemories
|
|
1291
|
+
});
|
|
1292
|
+
for (const memory of recentMemories) {
|
|
1293
|
+
this.localMemories.set(memory.id, {
|
|
1294
|
+
id: memory.id,
|
|
1295
|
+
content: memory.content,
|
|
1296
|
+
keywords: memory.keywords || [],
|
|
1297
|
+
timestamp: memory.timestamp,
|
|
1298
|
+
source: memory.source,
|
|
1299
|
+
metadata: memory.metadata
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
console.log(`\u{1F9E0} Loaded ${this.localMemories.size} memories into context`);
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
console.warn("\u26A0\uFE0F Could not sync memories:", error instanceof Error ? error.message : error);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Create or get a session
|
|
1309
|
+
*/
|
|
1310
|
+
getSession(sessionId) {
|
|
1311
|
+
if (sessionId && this.sessions.has(sessionId)) {
|
|
1312
|
+
const session = this.sessions.get(sessionId);
|
|
1313
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1314
|
+
return session;
|
|
1315
|
+
}
|
|
1316
|
+
const newSession = {
|
|
1317
|
+
id: sessionId || `session-${Date.now()}`,
|
|
1318
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1319
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1320
|
+
variables: {},
|
|
1321
|
+
messageHistory: []
|
|
1322
|
+
};
|
|
1323
|
+
this.sessions.set(newSession.id, newSession);
|
|
1324
|
+
this.currentSessionId = newSession.id;
|
|
1325
|
+
return newSession;
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Get the current active session
|
|
1329
|
+
*/
|
|
1330
|
+
getCurrentSession() {
|
|
1331
|
+
if (this.currentSessionId) {
|
|
1332
|
+
return this.sessions.get(this.currentSessionId);
|
|
1333
|
+
}
|
|
1334
|
+
return void 0;
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Set the current active session
|
|
1338
|
+
*/
|
|
1339
|
+
setCurrentSession(sessionId) {
|
|
1340
|
+
if (this.sessions.has(sessionId)) {
|
|
1341
|
+
this.currentSessionId = sessionId;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Add a message to the current session history
|
|
1346
|
+
*/
|
|
1347
|
+
addMessage(role, content) {
|
|
1348
|
+
const session = this.getCurrentSession();
|
|
1349
|
+
if (!session) return;
|
|
1350
|
+
session.messageHistory.push({
|
|
1351
|
+
role,
|
|
1352
|
+
content,
|
|
1353
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1354
|
+
});
|
|
1355
|
+
if (session.messageHistory.length > this.options.maxMessageHistory) {
|
|
1356
|
+
session.messageHistory = session.messageHistory.slice(-this.options.maxMessageHistory);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Store a memory (local + cloud sync)
|
|
1361
|
+
*/
|
|
1362
|
+
async storeMemory(content, options = {}) {
|
|
1363
|
+
const id = `mem-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1364
|
+
const entry = {
|
|
1365
|
+
id,
|
|
1366
|
+
content,
|
|
1367
|
+
keywords: options.keywords || [],
|
|
1368
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1369
|
+
source: options.source || "daemon",
|
|
1370
|
+
metadata: options.metadata
|
|
1371
|
+
};
|
|
1372
|
+
this.localMemories.set(id, entry);
|
|
1373
|
+
try {
|
|
1374
|
+
await this.rainfall.memory.create({
|
|
1375
|
+
content,
|
|
1376
|
+
keywords: [...options.keywords || [], "daemon:context"],
|
|
1377
|
+
metadata: {
|
|
1378
|
+
...options.metadata,
|
|
1379
|
+
daemonMemoryId: id,
|
|
1380
|
+
source: options.source || "daemon"
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
console.warn("\u26A0\uFE0F Could not sync memory to cloud:", error instanceof Error ? error.message : error);
|
|
1385
|
+
}
|
|
1386
|
+
this.trimLocalMemories();
|
|
1387
|
+
return id;
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Recall memories by query
|
|
1391
|
+
*/
|
|
1392
|
+
async recallMemories(query, topK = 5) {
|
|
1393
|
+
const localResults = Array.from(this.localMemories.values()).filter(
|
|
1394
|
+
(m) => m.content.toLowerCase().includes(query.toLowerCase()) || m.keywords.some((k) => k.toLowerCase().includes(query.toLowerCase()))
|
|
1395
|
+
).slice(0, topK);
|
|
1396
|
+
try {
|
|
1397
|
+
const cloudResults = await this.rainfall.memory.recall({ query, topK });
|
|
1398
|
+
const seen = new Set(localResults.map((r) => r.id));
|
|
1399
|
+
for (const mem of cloudResults) {
|
|
1400
|
+
if (!seen.has(mem.id)) {
|
|
1401
|
+
localResults.push({
|
|
1402
|
+
id: mem.id,
|
|
1403
|
+
content: mem.content,
|
|
1404
|
+
keywords: mem.keywords || [],
|
|
1405
|
+
timestamp: mem.timestamp,
|
|
1406
|
+
source: mem.source,
|
|
1407
|
+
metadata: mem.metadata
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
} catch {
|
|
1412
|
+
}
|
|
1413
|
+
return localResults.slice(0, topK);
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Set a session variable
|
|
1417
|
+
*/
|
|
1418
|
+
setVariable(key, value) {
|
|
1419
|
+
const session = this.getCurrentSession();
|
|
1420
|
+
if (session) {
|
|
1421
|
+
session.variables[key] = value;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Get a session variable
|
|
1426
|
+
*/
|
|
1427
|
+
getVariable(key) {
|
|
1428
|
+
const session = this.getCurrentSession();
|
|
1429
|
+
return session?.variables[key];
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Record a tool execution
|
|
1433
|
+
*/
|
|
1434
|
+
recordExecution(toolId, params, result, options = { duration: 0 }) {
|
|
1435
|
+
const record = {
|
|
1436
|
+
id: `exec-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
1437
|
+
toolId,
|
|
1438
|
+
params,
|
|
1439
|
+
result,
|
|
1440
|
+
error: options.error,
|
|
1441
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1442
|
+
duration: options.duration,
|
|
1443
|
+
edgeNodeId: options.edgeNodeId
|
|
1444
|
+
};
|
|
1445
|
+
this.executionHistory.push(record);
|
|
1446
|
+
if (this.executionHistory.length > this.options.maxExecutionHistory) {
|
|
1447
|
+
this.executionHistory = this.executionHistory.slice(-this.options.maxExecutionHistory);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Get recent execution history
|
|
1452
|
+
*/
|
|
1453
|
+
getExecutionHistory(limit = 10) {
|
|
1454
|
+
return this.executionHistory.slice(-limit).reverse();
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Get execution statistics
|
|
1458
|
+
*/
|
|
1459
|
+
getExecutionStats() {
|
|
1460
|
+
const stats = {
|
|
1461
|
+
total: this.executionHistory.length,
|
|
1462
|
+
successful: 0,
|
|
1463
|
+
failed: 0,
|
|
1464
|
+
averageDuration: 0,
|
|
1465
|
+
byTool: {}
|
|
1466
|
+
};
|
|
1467
|
+
let totalDuration = 0;
|
|
1468
|
+
for (const exec of this.executionHistory) {
|
|
1469
|
+
if (exec.error) {
|
|
1470
|
+
stats.failed++;
|
|
1471
|
+
} else {
|
|
1472
|
+
stats.successful++;
|
|
1473
|
+
}
|
|
1474
|
+
totalDuration += exec.duration;
|
|
1475
|
+
stats.byTool[exec.toolId] = (stats.byTool[exec.toolId] || 0) + 1;
|
|
1476
|
+
}
|
|
1477
|
+
stats.averageDuration = stats.total > 0 ? totalDuration / stats.total : 0;
|
|
1478
|
+
return stats;
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Clear old sessions based on TTL
|
|
1482
|
+
*/
|
|
1483
|
+
cleanupSessions() {
|
|
1484
|
+
const now = Date.now();
|
|
1485
|
+
const ttl = this.options.sessionTtl;
|
|
1486
|
+
for (const [id, session] of this.sessions) {
|
|
1487
|
+
const lastActivity = new Date(session.lastActivity).getTime();
|
|
1488
|
+
if (now - lastActivity > ttl) {
|
|
1489
|
+
this.sessions.delete(id);
|
|
1490
|
+
if (this.currentSessionId === id) {
|
|
1491
|
+
this.currentSessionId = void 0;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Get context summary for debugging
|
|
1498
|
+
*/
|
|
1499
|
+
getStatus() {
|
|
1500
|
+
return {
|
|
1501
|
+
memoriesCached: this.localMemories.size,
|
|
1502
|
+
activeSessions: this.sessions.size,
|
|
1503
|
+
currentSession: this.currentSessionId,
|
|
1504
|
+
executionHistorySize: this.executionHistory.length
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
trimLocalMemories() {
|
|
1508
|
+
if (this.localMemories.size <= this.options.maxLocalMemories) return;
|
|
1509
|
+
const entries = Array.from(this.localMemories.entries()).sort((a, b) => new Date(a[1].timestamp).getTime() - new Date(b[1].timestamp).getTime());
|
|
1510
|
+
const toRemove = entries.slice(0, entries.length - this.options.maxLocalMemories);
|
|
1511
|
+
for (const [id] of toRemove) {
|
|
1512
|
+
this.localMemories.delete(id);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
// src/services/listeners.ts
|
|
1520
|
+
var RainfallListenerRegistry;
|
|
1521
|
+
var init_listeners = __esm({
|
|
1522
|
+
"src/services/listeners.ts"() {
|
|
1523
|
+
"use strict";
|
|
1524
|
+
init_cjs_shims();
|
|
1525
|
+
RainfallListenerRegistry = class {
|
|
1526
|
+
rainfall;
|
|
1527
|
+
context;
|
|
1528
|
+
executor;
|
|
1529
|
+
watchers = /* @__PURE__ */ new Map();
|
|
1530
|
+
cronIntervals = /* @__PURE__ */ new Map();
|
|
1531
|
+
eventHistory = [];
|
|
1532
|
+
maxEventHistory = 100;
|
|
1533
|
+
constructor(rainfall, context, executor) {
|
|
1534
|
+
this.rainfall = rainfall;
|
|
1535
|
+
this.context = context;
|
|
1536
|
+
this.executor = executor;
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Register a file watcher
|
|
1540
|
+
* Note: Actual file watching requires fs.watch or chokidar
|
|
1541
|
+
* This is the registry - actual watching is done by the daemon
|
|
1542
|
+
*/
|
|
1543
|
+
async registerFileWatcher(config) {
|
|
1544
|
+
console.log(`\u{1F441}\uFE0F Registering file watcher: ${config.name} (${config.watchPath})`);
|
|
1545
|
+
const existing = Array.from(this.watchers.keys());
|
|
1546
|
+
if (existing.includes(config.id)) {
|
|
1547
|
+
await this.unregisterFileWatcher(config.id);
|
|
1548
|
+
}
|
|
1549
|
+
this.watchers.set(config.id, {
|
|
1550
|
+
stop: () => {
|
|
1551
|
+
console.log(`\u{1F441}\uFE0F Stopped file watcher: ${config.name}`);
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
await this.context.storeMemory(`File watcher registered: ${config.name}`, {
|
|
1555
|
+
keywords: ["listener", "file-watcher", config.name],
|
|
1556
|
+
metadata: { config }
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Unregister a file watcher
|
|
1561
|
+
*/
|
|
1562
|
+
async unregisterFileWatcher(id) {
|
|
1563
|
+
const watcher = this.watchers.get(id);
|
|
1564
|
+
if (watcher) {
|
|
1565
|
+
watcher.stop();
|
|
1566
|
+
this.watchers.delete(id);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Register a cron trigger
|
|
1571
|
+
*/
|
|
1572
|
+
async registerCronTrigger(config) {
|
|
1573
|
+
console.log(`\u23F0 Registering cron trigger: ${config.name} (${config.cron})`);
|
|
1574
|
+
if (this.cronIntervals.has(config.id)) {
|
|
1575
|
+
clearInterval(this.cronIntervals.get(config.id));
|
|
1576
|
+
this.cronIntervals.delete(config.id);
|
|
1577
|
+
}
|
|
1578
|
+
const interval = this.parseCronToMs(config.cron);
|
|
1579
|
+
if (interval) {
|
|
1580
|
+
const intervalId = setInterval(async () => {
|
|
1581
|
+
await this.handleCronTick(config);
|
|
1582
|
+
}, interval);
|
|
1583
|
+
this.cronIntervals.set(config.id, intervalId);
|
|
1584
|
+
}
|
|
1585
|
+
await this.context.storeMemory(`Cron trigger registered: ${config.name}`, {
|
|
1586
|
+
keywords: ["listener", "cron", config.name],
|
|
1587
|
+
metadata: { config }
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Unregister a cron trigger
|
|
1592
|
+
*/
|
|
1593
|
+
unregisterCronTrigger(id) {
|
|
1594
|
+
const interval = this.cronIntervals.get(id);
|
|
1595
|
+
if (interval) {
|
|
1596
|
+
clearInterval(interval);
|
|
1597
|
+
this.cronIntervals.delete(id);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Handle a file event
|
|
1602
|
+
*/
|
|
1603
|
+
async handleFileEvent(watcherId, eventType, filePath) {
|
|
1604
|
+
const event = {
|
|
1605
|
+
id: `evt-${Date.now()}`,
|
|
1606
|
+
type: "file",
|
|
1607
|
+
source: watcherId,
|
|
1608
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1609
|
+
data: { eventType, filePath }
|
|
1610
|
+
};
|
|
1611
|
+
this.recordEvent(event);
|
|
1612
|
+
console.log(`\u{1F4C1} File event: ${eventType} ${filePath}`);
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Handle a cron tick
|
|
1616
|
+
*/
|
|
1617
|
+
async handleCronTick(config) {
|
|
1618
|
+
const event = {
|
|
1619
|
+
id: `evt-${Date.now()}`,
|
|
1620
|
+
type: "cron",
|
|
1621
|
+
source: config.id,
|
|
1622
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1623
|
+
data: { cron: config.cron }
|
|
1624
|
+
};
|
|
1625
|
+
this.recordEvent(event);
|
|
1626
|
+
console.log(`\u23F0 Cron tick: ${config.name}`);
|
|
1627
|
+
for (const step of config.workflow) {
|
|
1628
|
+
try {
|
|
1629
|
+
await this.executor.queueToolExecution(step.toolId, {
|
|
1630
|
+
...step.params,
|
|
1631
|
+
_event: event
|
|
1632
|
+
});
|
|
1633
|
+
} catch (error) {
|
|
1634
|
+
console.error(`\u274C Workflow step failed: ${step.toolId}`, error);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Trigger a manual event (for testing or programmatic triggers)
|
|
1640
|
+
*/
|
|
1641
|
+
async triggerManual(name, data = {}) {
|
|
1642
|
+
const event = {
|
|
1643
|
+
id: `evt-${Date.now()}`,
|
|
1644
|
+
type: "manual",
|
|
1645
|
+
source: name,
|
|
1646
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1647
|
+
data
|
|
1648
|
+
};
|
|
1649
|
+
this.recordEvent(event);
|
|
1650
|
+
console.log(`\u{1F446} Manual trigger: ${name}`);
|
|
1651
|
+
await this.context.storeMemory(`Manual trigger fired: ${name}`, {
|
|
1652
|
+
keywords: ["trigger", "manual", name],
|
|
1653
|
+
metadata: { event }
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Get recent events
|
|
1658
|
+
*/
|
|
1659
|
+
getRecentEvents(limit = 10) {
|
|
1660
|
+
return this.eventHistory.slice(-limit).reverse();
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Get active listeners status
|
|
1664
|
+
*/
|
|
1665
|
+
getStatus() {
|
|
1666
|
+
return {
|
|
1667
|
+
fileWatchers: this.watchers.size,
|
|
1668
|
+
cronTriggers: this.cronIntervals.size,
|
|
1669
|
+
recentEvents: this.eventHistory.length
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Stop all listeners
|
|
1674
|
+
*/
|
|
1675
|
+
async stopAll() {
|
|
1676
|
+
for (const [id] of this.watchers) {
|
|
1677
|
+
await this.unregisterFileWatcher(id);
|
|
1678
|
+
}
|
|
1679
|
+
for (const [id] of this.cronIntervals) {
|
|
1680
|
+
this.unregisterCronTrigger(id);
|
|
1681
|
+
}
|
|
1682
|
+
console.log("\u{1F6D1} All listeners stopped");
|
|
1683
|
+
}
|
|
1684
|
+
recordEvent(event) {
|
|
1685
|
+
this.eventHistory.push(event);
|
|
1686
|
+
if (this.eventHistory.length > this.maxEventHistory) {
|
|
1687
|
+
this.eventHistory = this.eventHistory.slice(-this.maxEventHistory);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Simple cron parser - converts basic cron expressions to milliseconds
|
|
1692
|
+
* Supports: @hourly, @daily, @weekly, and simple intervals like every N minutes
|
|
1693
|
+
*/
|
|
1694
|
+
parseCronToMs(cron) {
|
|
1695
|
+
switch (cron) {
|
|
1696
|
+
case "@hourly":
|
|
1697
|
+
return 60 * 60 * 1e3;
|
|
1698
|
+
case "@daily":
|
|
1699
|
+
return 24 * 60 * 60 * 1e3;
|
|
1700
|
+
case "@weekly":
|
|
1701
|
+
return 7 * 24 * 60 * 60 * 1e3;
|
|
1702
|
+
case "@minutely":
|
|
1703
|
+
return 60 * 1e3;
|
|
1704
|
+
}
|
|
1705
|
+
const match = cron.match(/^\*\/(\d+)\s/);
|
|
1706
|
+
if (match) {
|
|
1707
|
+
const minutes = parseInt(match[1], 10);
|
|
1708
|
+
if (minutes > 0 && minutes <= 60) {
|
|
1709
|
+
return minutes * 60 * 1e3;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
console.warn(`\u26A0\uFE0F Unrecognized cron pattern "${cron}", using 1 minute interval`);
|
|
1713
|
+
return 60 * 1e3;
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
// src/services/mcp-proxy.ts
|
|
1720
|
+
var import_ws, import_client2, import_stdio, import_streamableHttp, import_types, MCPProxyHub;
|
|
1721
|
+
var init_mcp_proxy = __esm({
|
|
1722
|
+
"src/services/mcp-proxy.ts"() {
|
|
1723
|
+
"use strict";
|
|
1724
|
+
init_cjs_shims();
|
|
1725
|
+
import_ws = require("ws");
|
|
1726
|
+
import_client2 = require("@modelcontextprotocol/sdk/client/index.js");
|
|
1727
|
+
import_stdio = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
1728
|
+
import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
1729
|
+
import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
1730
|
+
MCPProxyHub = class {
|
|
1731
|
+
clients = /* @__PURE__ */ new Map();
|
|
1732
|
+
options;
|
|
1733
|
+
refreshTimer;
|
|
1734
|
+
reconnectTimeouts = /* @__PURE__ */ new Map();
|
|
1735
|
+
requestId = 0;
|
|
1736
|
+
constructor(options = {}) {
|
|
1737
|
+
this.options = {
|
|
1738
|
+
debug: options.debug ?? false,
|
|
1739
|
+
autoReconnect: options.autoReconnect ?? true,
|
|
1740
|
+
reconnectDelay: options.reconnectDelay ?? 5e3,
|
|
1741
|
+
toolTimeout: options.toolTimeout ?? 3e4,
|
|
1742
|
+
refreshInterval: options.refreshInterval ?? 3e4
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Initialize the MCP proxy hub
|
|
1747
|
+
*/
|
|
1748
|
+
async initialize() {
|
|
1749
|
+
this.log("\u{1F50C} Initializing MCP Proxy Hub...");
|
|
1750
|
+
this.startRefreshTimer();
|
|
1751
|
+
this.log("\u2705 MCP Proxy Hub initialized");
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Shutdown the MCP proxy hub and disconnect all clients
|
|
1755
|
+
*/
|
|
1756
|
+
async shutdown() {
|
|
1757
|
+
this.log("\u{1F6D1} Shutting down MCP Proxy Hub...");
|
|
1758
|
+
if (this.refreshTimer) {
|
|
1759
|
+
clearInterval(this.refreshTimer);
|
|
1760
|
+
this.refreshTimer = void 0;
|
|
1761
|
+
}
|
|
1762
|
+
for (const timeout of this.reconnectTimeouts.values()) {
|
|
1763
|
+
clearTimeout(timeout);
|
|
1764
|
+
}
|
|
1765
|
+
this.reconnectTimeouts.clear();
|
|
1766
|
+
const disconnectPromises = Array.from(this.clients.entries()).map(
|
|
1767
|
+
async ([name, client]) => {
|
|
1768
|
+
try {
|
|
1769
|
+
await this.disconnectClient(name);
|
|
1770
|
+
} catch (error) {
|
|
1771
|
+
this.log(`Error disconnecting ${name}:`, error);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
);
|
|
1775
|
+
await Promise.allSettled(disconnectPromises);
|
|
1776
|
+
this.clients.clear();
|
|
1777
|
+
this.log("\u{1F44B} MCP Proxy Hub shut down");
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Connect to an MCP server
|
|
1781
|
+
*/
|
|
1782
|
+
async connectClient(config) {
|
|
1783
|
+
const { name, transport } = config;
|
|
1784
|
+
if (this.clients.has(name)) {
|
|
1785
|
+
this.log(`Reconnecting client: ${name}`);
|
|
1786
|
+
await this.disconnectClient(name);
|
|
1787
|
+
}
|
|
1788
|
+
this.log(`Connecting to MCP server: ${name} (${transport})...`);
|
|
1789
|
+
try {
|
|
1790
|
+
const client = new import_client2.Client(
|
|
1791
|
+
{
|
|
1792
|
+
name: `rainfall-daemon-${name}`,
|
|
1793
|
+
version: "0.2.0"
|
|
1794
|
+
},
|
|
1795
|
+
{
|
|
1796
|
+
capabilities: {}
|
|
1797
|
+
}
|
|
1798
|
+
);
|
|
1799
|
+
let lastErrorTime = 0;
|
|
1800
|
+
client.onerror = (error) => {
|
|
1801
|
+
const now = Date.now();
|
|
1802
|
+
if (now - lastErrorTime > 5e3) {
|
|
1803
|
+
this.log(`MCP Server Error (${name}):`, error.message);
|
|
1804
|
+
lastErrorTime = now;
|
|
1805
|
+
}
|
|
1806
|
+
if (this.options.autoReconnect) {
|
|
1807
|
+
this.scheduleReconnect(name, config);
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
let transportInstance;
|
|
1811
|
+
if (transport === "stdio" && config.command) {
|
|
1812
|
+
const env = {};
|
|
1813
|
+
for (const [key, value] of Object.entries({ ...process.env, ...config.env })) {
|
|
1814
|
+
if (value !== void 0) {
|
|
1815
|
+
env[key] = value;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
transportInstance = new import_stdio.StdioClientTransport({
|
|
1819
|
+
command: config.command,
|
|
1820
|
+
args: config.args,
|
|
1821
|
+
env
|
|
1822
|
+
});
|
|
1823
|
+
} else if (transport === "http" && config.url) {
|
|
1824
|
+
transportInstance = new import_streamableHttp.StreamableHTTPClientTransport(
|
|
1825
|
+
new URL(config.url),
|
|
1826
|
+
{
|
|
1827
|
+
requestInit: {
|
|
1828
|
+
headers: config.headers
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
);
|
|
1832
|
+
} else if (transport === "websocket" && config.url) {
|
|
1833
|
+
transportInstance = new import_ws.WebSocket(config.url);
|
|
1834
|
+
await new Promise((resolve, reject) => {
|
|
1835
|
+
transportInstance.on("open", () => resolve());
|
|
1836
|
+
transportInstance.on("error", reject);
|
|
1837
|
+
setTimeout(() => reject(new Error("WebSocket connection timeout")), 1e4);
|
|
1838
|
+
});
|
|
1839
|
+
} else {
|
|
1840
|
+
throw new Error(`Invalid transport configuration for ${name}`);
|
|
1841
|
+
}
|
|
1842
|
+
await client.connect(transportInstance);
|
|
1843
|
+
const toolsResult = await client.request(
|
|
1844
|
+
{
|
|
1845
|
+
method: "tools/list",
|
|
1846
|
+
params: {}
|
|
1847
|
+
},
|
|
1848
|
+
import_types.ListToolsResultSchema
|
|
1849
|
+
);
|
|
1850
|
+
const tools = toolsResult.tools.map((tool) => ({
|
|
1851
|
+
name: tool.name,
|
|
1852
|
+
description: tool.description || "",
|
|
1853
|
+
inputSchema: tool.inputSchema,
|
|
1854
|
+
serverName: name
|
|
1855
|
+
}));
|
|
1856
|
+
const clientInfo = {
|
|
1857
|
+
name,
|
|
1858
|
+
client,
|
|
1859
|
+
transport: transportInstance,
|
|
1860
|
+
transportType: transport,
|
|
1861
|
+
tools,
|
|
1862
|
+
connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1863
|
+
lastUsed: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1864
|
+
config,
|
|
1865
|
+
status: "connected"
|
|
1866
|
+
};
|
|
1867
|
+
this.clients.set(name, clientInfo);
|
|
1868
|
+
this.log(`\u2705 Connected to ${name} (${tools.length} tools)`);
|
|
1869
|
+
this.printAvailableTools(name, tools);
|
|
1870
|
+
return name;
|
|
1871
|
+
} catch (error) {
|
|
1872
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1873
|
+
this.log(`\u274C Failed to connect to ${name}:`, errorMessage);
|
|
1874
|
+
if (this.options.autoReconnect) {
|
|
1875
|
+
this.scheduleReconnect(name, config);
|
|
1876
|
+
}
|
|
1877
|
+
throw error;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Disconnect a specific MCP client
|
|
1882
|
+
*/
|
|
1883
|
+
async disconnectClient(name) {
|
|
1884
|
+
const client = this.clients.get(name);
|
|
1885
|
+
if (!client) return;
|
|
1886
|
+
const timeout = this.reconnectTimeouts.get(name);
|
|
1887
|
+
if (timeout) {
|
|
1888
|
+
clearTimeout(timeout);
|
|
1889
|
+
this.reconnectTimeouts.delete(name);
|
|
1890
|
+
}
|
|
1891
|
+
try {
|
|
1892
|
+
await client.client.close();
|
|
1893
|
+
if ("close" in client.transport && typeof client.transport.close === "function") {
|
|
1894
|
+
await client.transport.close();
|
|
1895
|
+
}
|
|
1896
|
+
} catch (error) {
|
|
1897
|
+
this.log(`Error closing client ${name}:`, error);
|
|
1898
|
+
}
|
|
1899
|
+
this.clients.delete(name);
|
|
1900
|
+
this.log(`Disconnected from ${name}`);
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Schedule a reconnection attempt
|
|
1904
|
+
*/
|
|
1905
|
+
scheduleReconnect(name, config) {
|
|
1906
|
+
if (this.reconnectTimeouts.has(name)) return;
|
|
1907
|
+
const timeout = setTimeout(async () => {
|
|
1908
|
+
this.reconnectTimeouts.delete(name);
|
|
1909
|
+
this.log(`Attempting to reconnect to ${name}...`);
|
|
1910
|
+
try {
|
|
1911
|
+
await this.connectClient(config);
|
|
1912
|
+
} catch (error) {
|
|
1913
|
+
this.log(`Reconnection failed for ${name}`);
|
|
1914
|
+
}
|
|
1915
|
+
}, this.options.reconnectDelay);
|
|
1916
|
+
this.reconnectTimeouts.set(name, timeout);
|
|
1917
|
+
}
|
|
1918
|
+
/**
|
|
1919
|
+
* Call a tool on the appropriate MCP client
|
|
1920
|
+
*/
|
|
1921
|
+
async callTool(toolName, args, options = {}) {
|
|
1922
|
+
const timeout = options.timeout ?? this.options.toolTimeout;
|
|
1923
|
+
let clientInfo;
|
|
1924
|
+
let actualToolName = toolName;
|
|
1925
|
+
if (options.namespace) {
|
|
1926
|
+
clientInfo = this.clients.get(options.namespace);
|
|
1927
|
+
if (!clientInfo) {
|
|
1928
|
+
throw new Error(`Namespace '${options.namespace}' not found`);
|
|
1929
|
+
}
|
|
1930
|
+
const prefix = `${options.namespace}-`;
|
|
1931
|
+
if (actualToolName.startsWith(prefix)) {
|
|
1932
|
+
actualToolName = actualToolName.slice(prefix.length);
|
|
1933
|
+
}
|
|
1934
|
+
if (!clientInfo.tools.some((t) => t.name === actualToolName)) {
|
|
1935
|
+
throw new Error(`Tool '${actualToolName}' not found in namespace '${options.namespace}'`);
|
|
1936
|
+
}
|
|
1937
|
+
} else {
|
|
1938
|
+
for (const [, info] of this.clients) {
|
|
1939
|
+
const tool = info.tools.find((t) => t.name === toolName);
|
|
1940
|
+
if (tool) {
|
|
1941
|
+
clientInfo = info;
|
|
1942
|
+
break;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
if (!clientInfo) {
|
|
1947
|
+
throw new Error(`Tool '${toolName}' not found on any connected MCP server`);
|
|
1948
|
+
}
|
|
1949
|
+
const requestId = `req_${++this.requestId}`;
|
|
1950
|
+
clientInfo.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
|
|
1951
|
+
try {
|
|
1952
|
+
this.log(`[${requestId}] Calling '${actualToolName}' on '${clientInfo.name}'`);
|
|
1953
|
+
const result = await Promise.race([
|
|
1954
|
+
clientInfo.client.request(
|
|
1955
|
+
{
|
|
1956
|
+
method: "tools/call",
|
|
1957
|
+
params: {
|
|
1958
|
+
name: actualToolName,
|
|
1959
|
+
arguments: args
|
|
1960
|
+
}
|
|
1961
|
+
},
|
|
1962
|
+
import_types.CallToolResultSchema
|
|
1963
|
+
),
|
|
1964
|
+
new Promise(
|
|
1965
|
+
(_, reject) => setTimeout(() => reject(new Error(`Tool call timeout after ${timeout}ms`)), timeout)
|
|
1966
|
+
)
|
|
1967
|
+
]);
|
|
1968
|
+
this.log(`[${requestId}] Completed successfully`);
|
|
1969
|
+
return this.formatToolResult(result);
|
|
1970
|
+
} catch (error) {
|
|
1971
|
+
this.log(`[${requestId}] Failed:`, error instanceof Error ? error.message : error);
|
|
1972
|
+
if (error instanceof import_types.McpError) {
|
|
1973
|
+
throw new Error(`MCP Error (${toolName}): ${error.message} (code: ${error.code})`);
|
|
1974
|
+
}
|
|
1975
|
+
throw error;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Format MCP tool result for consistent output
|
|
1980
|
+
*/
|
|
1981
|
+
formatToolResult(result) {
|
|
1982
|
+
if (!result || !result.content) {
|
|
1983
|
+
return "";
|
|
1984
|
+
}
|
|
1985
|
+
return result.content.map((item) => {
|
|
1986
|
+
if (item.type === "text") {
|
|
1987
|
+
return item.text || "";
|
|
1988
|
+
} else if (item.type === "resource") {
|
|
1989
|
+
return `[Resource: ${item.resource?.uri || "unknown"}]`;
|
|
1990
|
+
} else if (item.type === "image") {
|
|
1991
|
+
return `[Image: ${item.mimeType || "unknown"}]`;
|
|
1992
|
+
} else if (item.type === "audio") {
|
|
1993
|
+
return `[Audio: ${item.mimeType || "unknown"}]`;
|
|
1994
|
+
} else {
|
|
1995
|
+
return JSON.stringify(item);
|
|
1996
|
+
}
|
|
1997
|
+
}).join("\n");
|
|
1998
|
+
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Get all tools from all connected MCP clients
|
|
2001
|
+
* Optionally with namespace prefix
|
|
2002
|
+
*/
|
|
2003
|
+
getAllTools(options = {}) {
|
|
2004
|
+
const allTools = [];
|
|
2005
|
+
for (const [clientName, client] of this.clients) {
|
|
2006
|
+
for (const tool of client.tools) {
|
|
2007
|
+
if (options.namespacePrefix) {
|
|
2008
|
+
allTools.push({
|
|
2009
|
+
...tool,
|
|
2010
|
+
name: `${clientName}-${tool.name}`
|
|
2011
|
+
});
|
|
2012
|
+
} else {
|
|
2013
|
+
allTools.push(tool);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
return allTools;
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Get tools from a specific client
|
|
2021
|
+
*/
|
|
2022
|
+
getClientTools(clientName) {
|
|
2023
|
+
const client = this.clients.get(clientName);
|
|
2024
|
+
return client?.tools || [];
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Get list of connected MCP clients
|
|
2028
|
+
*/
|
|
2029
|
+
listClients() {
|
|
2030
|
+
return Array.from(this.clients.entries()).map(([name, info]) => ({
|
|
2031
|
+
name,
|
|
2032
|
+
status: info.status,
|
|
2033
|
+
toolCount: info.tools.length,
|
|
2034
|
+
connectedAt: info.connectedAt,
|
|
2035
|
+
lastUsed: info.lastUsed,
|
|
2036
|
+
transportType: info.transportType
|
|
2037
|
+
}));
|
|
2038
|
+
}
|
|
2039
|
+
/**
|
|
2040
|
+
* Get client info by name
|
|
2041
|
+
*/
|
|
2042
|
+
getClient(name) {
|
|
2043
|
+
return this.clients.get(name);
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Refresh tool lists from all connected clients
|
|
2047
|
+
*/
|
|
2048
|
+
async refreshTools() {
|
|
2049
|
+
for (const [name, client] of this.clients) {
|
|
2050
|
+
try {
|
|
2051
|
+
const toolsResult = await client.client.request(
|
|
2052
|
+
{
|
|
2053
|
+
method: "tools/list",
|
|
2054
|
+
params: {}
|
|
2055
|
+
},
|
|
2056
|
+
import_types.ListToolsResultSchema
|
|
2057
|
+
);
|
|
2058
|
+
client.tools = toolsResult.tools.map((tool) => ({
|
|
2059
|
+
name: tool.name,
|
|
2060
|
+
description: tool.description || "",
|
|
2061
|
+
inputSchema: tool.inputSchema,
|
|
2062
|
+
serverName: name
|
|
2063
|
+
}));
|
|
2064
|
+
this.log(`Refreshed ${name}: ${client.tools.length} tools`);
|
|
2065
|
+
} catch (error) {
|
|
2066
|
+
this.log(`Failed to refresh tools for ${name}:`, error);
|
|
2067
|
+
client.status = "error";
|
|
2068
|
+
client.error = error instanceof Error ? error.message : String(error);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
/**
|
|
2073
|
+
* List resources from a specific client or all clients
|
|
2074
|
+
*/
|
|
2075
|
+
async listResources(clientName) {
|
|
2076
|
+
const results = [];
|
|
2077
|
+
const clients = clientName ? [clientName] : Array.from(this.clients.keys());
|
|
2078
|
+
for (const name of clients) {
|
|
2079
|
+
const client = this.clients.get(name);
|
|
2080
|
+
if (!client) continue;
|
|
2081
|
+
try {
|
|
2082
|
+
const result = await client.client.request(
|
|
2083
|
+
{
|
|
2084
|
+
method: "resources/list",
|
|
2085
|
+
params: {}
|
|
2086
|
+
},
|
|
2087
|
+
import_types.ListResourcesResultSchema
|
|
2088
|
+
);
|
|
2089
|
+
results.push({
|
|
2090
|
+
clientName: name,
|
|
2091
|
+
resources: result.resources
|
|
2092
|
+
});
|
|
2093
|
+
} catch (error) {
|
|
2094
|
+
this.log(`Failed to list resources for ${name}:`, error);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
return results;
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Read a resource from a specific client
|
|
2101
|
+
*/
|
|
2102
|
+
async readResource(uri, clientName) {
|
|
2103
|
+
if (clientName) {
|
|
2104
|
+
const client = this.clients.get(clientName);
|
|
2105
|
+
if (!client) {
|
|
2106
|
+
throw new Error(`Client '${clientName}' not found`);
|
|
2107
|
+
}
|
|
2108
|
+
const result = await client.client.request(
|
|
2109
|
+
{
|
|
2110
|
+
method: "resources/read",
|
|
2111
|
+
params: { uri }
|
|
2112
|
+
},
|
|
2113
|
+
import_types.ReadResourceResultSchema
|
|
2114
|
+
);
|
|
2115
|
+
return result;
|
|
2116
|
+
} else {
|
|
2117
|
+
for (const [name, client] of this.clients) {
|
|
2118
|
+
try {
|
|
2119
|
+
const result = await client.client.request(
|
|
2120
|
+
{
|
|
2121
|
+
method: "resources/read",
|
|
2122
|
+
params: { uri }
|
|
2123
|
+
},
|
|
2124
|
+
import_types.ReadResourceResultSchema
|
|
2125
|
+
);
|
|
2126
|
+
return { clientName: name, ...result };
|
|
2127
|
+
} catch {
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
throw new Error(`Resource '${uri}' not found on any client`);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* List prompts from a specific client or all clients
|
|
2135
|
+
*/
|
|
2136
|
+
async listPrompts(clientName) {
|
|
2137
|
+
const results = [];
|
|
2138
|
+
const clients = clientName ? [clientName] : Array.from(this.clients.keys());
|
|
2139
|
+
for (const name of clients) {
|
|
2140
|
+
const client = this.clients.get(name);
|
|
2141
|
+
if (!client) continue;
|
|
2142
|
+
try {
|
|
2143
|
+
const result = await client.client.request(
|
|
2144
|
+
{
|
|
2145
|
+
method: "prompts/list",
|
|
2146
|
+
params: {}
|
|
2147
|
+
},
|
|
2148
|
+
import_types.ListPromptsResultSchema
|
|
2149
|
+
);
|
|
2150
|
+
results.push({
|
|
2151
|
+
clientName: name,
|
|
2152
|
+
prompts: result.prompts
|
|
2153
|
+
});
|
|
2154
|
+
} catch (error) {
|
|
2155
|
+
this.log(`Failed to list prompts for ${name}:`, error);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
return results;
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Get a prompt from a specific client
|
|
2162
|
+
*/
|
|
2163
|
+
async getPrompt(name, args, clientName) {
|
|
2164
|
+
if (clientName) {
|
|
2165
|
+
const client = this.clients.get(clientName);
|
|
2166
|
+
if (!client) {
|
|
2167
|
+
throw new Error(`Client '${clientName}' not found`);
|
|
2168
|
+
}
|
|
2169
|
+
const result = await client.client.request(
|
|
2170
|
+
{
|
|
2171
|
+
method: "prompts/get",
|
|
2172
|
+
params: { name, arguments: args }
|
|
2173
|
+
},
|
|
2174
|
+
import_types.GetPromptResultSchema
|
|
2175
|
+
);
|
|
2176
|
+
return result;
|
|
2177
|
+
} else {
|
|
2178
|
+
for (const [cName, client] of this.clients) {
|
|
2179
|
+
try {
|
|
2180
|
+
const result = await client.client.request(
|
|
2181
|
+
{
|
|
2182
|
+
method: "prompts/get",
|
|
2183
|
+
params: { name, arguments: args }
|
|
2184
|
+
},
|
|
2185
|
+
import_types.GetPromptResultSchema
|
|
2186
|
+
);
|
|
2187
|
+
return { clientName: cName, ...result };
|
|
2188
|
+
} catch {
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
throw new Error(`Prompt '${name}' not found on any client`);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
/**
|
|
2195
|
+
* Health check for all connected clients
|
|
2196
|
+
*/
|
|
2197
|
+
async healthCheck() {
|
|
2198
|
+
const results = /* @__PURE__ */ new Map();
|
|
2199
|
+
for (const [name, client] of this.clients) {
|
|
2200
|
+
try {
|
|
2201
|
+
const startTime = Date.now();
|
|
2202
|
+
await client.client.request(
|
|
2203
|
+
{
|
|
2204
|
+
method: "tools/list",
|
|
2205
|
+
params: {}
|
|
2206
|
+
},
|
|
2207
|
+
import_types.ListToolsResultSchema
|
|
2208
|
+
);
|
|
2209
|
+
results.set(name, {
|
|
2210
|
+
status: "healthy",
|
|
2211
|
+
responseTime: Date.now() - startTime
|
|
2212
|
+
});
|
|
2213
|
+
} catch (error) {
|
|
2214
|
+
results.set(name, {
|
|
2215
|
+
status: "unhealthy",
|
|
2216
|
+
responseTime: 0,
|
|
2217
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2218
|
+
});
|
|
2219
|
+
if (this.options.autoReconnect) {
|
|
2220
|
+
this.scheduleReconnect(name, client.config);
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
return results;
|
|
2225
|
+
}
|
|
2226
|
+
/**
|
|
2227
|
+
* Start the automatic refresh timer
|
|
2228
|
+
*/
|
|
2229
|
+
startRefreshTimer() {
|
|
2230
|
+
if (this.refreshTimer) {
|
|
2231
|
+
clearInterval(this.refreshTimer);
|
|
2232
|
+
}
|
|
2233
|
+
if (this.options.refreshInterval > 0) {
|
|
2234
|
+
this.refreshTimer = setInterval(async () => {
|
|
2235
|
+
try {
|
|
2236
|
+
await this.refreshTools();
|
|
2237
|
+
} catch (error) {
|
|
2238
|
+
this.log("Auto-refresh failed:", error);
|
|
2239
|
+
}
|
|
2240
|
+
}, this.options.refreshInterval);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Print available tools for a client
|
|
2245
|
+
*/
|
|
2246
|
+
printAvailableTools(clientName, tools) {
|
|
2247
|
+
if (tools.length === 0) {
|
|
2248
|
+
this.log(` No tools available from ${clientName}`);
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
this.log(`
|
|
2252
|
+
--- ${clientName} Tools (${tools.length}) ---`);
|
|
2253
|
+
for (const tool of tools) {
|
|
2254
|
+
this.log(` \u2022 ${tool.name}: ${tool.description.slice(0, 60)}${tool.description.length > 60 ? "..." : ""}`);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
/**
|
|
2258
|
+
* Debug logging
|
|
2259
|
+
*/
|
|
2260
|
+
log(...args) {
|
|
2261
|
+
if (this.options.debug) {
|
|
2262
|
+
console.log("[MCP-Proxy]", ...args);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* Get statistics about the MCP proxy hub
|
|
2267
|
+
*/
|
|
2268
|
+
getStats() {
|
|
2269
|
+
const clients = Array.from(this.clients.entries()).map(([name, info]) => ({
|
|
2270
|
+
name,
|
|
2271
|
+
toolCount: info.tools.length,
|
|
2272
|
+
status: info.status,
|
|
2273
|
+
transportType: info.transportType
|
|
2274
|
+
}));
|
|
2275
|
+
return {
|
|
2276
|
+
totalClients: this.clients.size,
|
|
2277
|
+
totalTools: clients.reduce((sum, c) => sum + c.toolCount, 0),
|
|
2278
|
+
clients
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
});
|
|
2284
|
+
|
|
2285
|
+
// src/daemon/index.ts
|
|
2286
|
+
var daemon_exports = {};
|
|
2287
|
+
__export(daemon_exports, {
|
|
2288
|
+
MCPProxyHub: () => MCPProxyHub,
|
|
2289
|
+
RainfallDaemon: () => RainfallDaemon,
|
|
2290
|
+
getDaemonInstance: () => getDaemonInstance,
|
|
2291
|
+
getDaemonStatus: () => getDaemonStatus,
|
|
2292
|
+
startDaemon: () => startDaemon,
|
|
2293
|
+
stopDaemon: () => stopDaemon
|
|
2294
|
+
});
|
|
2295
|
+
async function startDaemon(config = {}) {
|
|
2296
|
+
if (daemonInstance) {
|
|
2297
|
+
console.log("Daemon already running");
|
|
2298
|
+
return daemonInstance;
|
|
2299
|
+
}
|
|
2300
|
+
daemonInstance = new RainfallDaemon(config);
|
|
2301
|
+
await daemonInstance.start();
|
|
2302
|
+
return daemonInstance;
|
|
2303
|
+
}
|
|
2304
|
+
async function stopDaemon() {
|
|
2305
|
+
if (!daemonInstance) {
|
|
2306
|
+
console.log("Daemon not running");
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
await daemonInstance.stop();
|
|
2310
|
+
daemonInstance = null;
|
|
2311
|
+
}
|
|
2312
|
+
function getDaemonStatus() {
|
|
2313
|
+
if (!daemonInstance) {
|
|
2314
|
+
return null;
|
|
2315
|
+
}
|
|
2316
|
+
return daemonInstance.getStatus();
|
|
2317
|
+
}
|
|
2318
|
+
function getDaemonInstance() {
|
|
2319
|
+
return daemonInstance;
|
|
2320
|
+
}
|
|
2321
|
+
var import_ws2, import_express, RainfallDaemon, daemonInstance;
|
|
2322
|
+
var init_daemon = __esm({
|
|
2323
|
+
"src/daemon/index.ts"() {
|
|
2324
|
+
"use strict";
|
|
2325
|
+
init_cjs_shims();
|
|
2326
|
+
import_ws2 = require("ws");
|
|
2327
|
+
import_express = __toESM(require("express"));
|
|
2328
|
+
init_sdk();
|
|
2329
|
+
init_networked();
|
|
2330
|
+
init_context();
|
|
2331
|
+
init_listeners();
|
|
2332
|
+
init_mcp_proxy();
|
|
2333
|
+
init_mcp_proxy();
|
|
2334
|
+
RainfallDaemon = class {
|
|
2335
|
+
wss;
|
|
2336
|
+
openaiApp;
|
|
2337
|
+
rainfall;
|
|
2338
|
+
port;
|
|
2339
|
+
openaiPort;
|
|
2340
|
+
rainfallConfig;
|
|
2341
|
+
tools = [];
|
|
2342
|
+
toolSchemas = /* @__PURE__ */ new Map();
|
|
2343
|
+
clients = /* @__PURE__ */ new Set();
|
|
2344
|
+
debug;
|
|
2345
|
+
// New services
|
|
2346
|
+
networkedExecutor;
|
|
2347
|
+
context;
|
|
2348
|
+
listeners;
|
|
2349
|
+
mcpProxy;
|
|
2350
|
+
enableMcpProxy;
|
|
2351
|
+
mcpNamespacePrefix;
|
|
2352
|
+
constructor(config = {}) {
|
|
2353
|
+
this.port = config.port || 8765;
|
|
2354
|
+
this.openaiPort = config.openaiPort || 8787;
|
|
2355
|
+
this.rainfallConfig = config.rainfallConfig;
|
|
2356
|
+
this.debug = config.debug || false;
|
|
2357
|
+
this.enableMcpProxy = config.enableMcpProxy ?? true;
|
|
2358
|
+
this.mcpNamespacePrefix = config.mcpNamespacePrefix ?? true;
|
|
2359
|
+
this.openaiApp = (0, import_express.default)();
|
|
2360
|
+
this.openaiApp.use(import_express.default.json());
|
|
2361
|
+
}
|
|
2362
|
+
async start() {
|
|
2363
|
+
this.log("\u{1F327}\uFE0F Rainfall Daemon starting...");
|
|
2364
|
+
await this.initializeRainfall();
|
|
2365
|
+
if (!this.rainfall) {
|
|
2366
|
+
throw new Error("Failed to initialize Rainfall SDK");
|
|
2367
|
+
}
|
|
2368
|
+
this.context = new RainfallDaemonContext(this.rainfall, {
|
|
2369
|
+
maxLocalMemories: 1e3,
|
|
2370
|
+
maxMessageHistory: 100,
|
|
2371
|
+
...this.rainfallConfig
|
|
2372
|
+
});
|
|
2373
|
+
await this.context.initialize();
|
|
2374
|
+
this.networkedExecutor = new RainfallNetworkedExecutor(this.rainfall, {
|
|
2375
|
+
wsPort: this.port,
|
|
2376
|
+
httpPort: this.openaiPort,
|
|
2377
|
+
hostname: process.env.HOSTNAME || "local-daemon",
|
|
2378
|
+
capabilities: {
|
|
2379
|
+
localExec: true,
|
|
2380
|
+
fileWatch: true,
|
|
2381
|
+
passiveListen: true
|
|
2382
|
+
}
|
|
2383
|
+
});
|
|
2384
|
+
await this.networkedExecutor.registerEdgeNode();
|
|
2385
|
+
await this.networkedExecutor.subscribeToResults((jobId, result, error) => {
|
|
2386
|
+
this.log(`\u{1F4EC} Job ${jobId} ${error ? "failed" : "completed"}`, error || result);
|
|
2387
|
+
});
|
|
2388
|
+
this.listeners = new RainfallListenerRegistry(
|
|
2389
|
+
this.rainfall,
|
|
2390
|
+
this.context,
|
|
2391
|
+
this.networkedExecutor
|
|
2392
|
+
);
|
|
2393
|
+
await this.loadTools();
|
|
2394
|
+
if (this.enableMcpProxy) {
|
|
2395
|
+
this.mcpProxy = new MCPProxyHub({ debug: this.debug });
|
|
2396
|
+
await this.mcpProxy.initialize();
|
|
2397
|
+
if (this.rainfallConfig?.mcpClients) {
|
|
2398
|
+
for (const clientConfig of this.rainfallConfig.mcpClients) {
|
|
2399
|
+
try {
|
|
2400
|
+
await this.mcpProxy.connectClient(clientConfig);
|
|
2401
|
+
} catch (error) {
|
|
2402
|
+
this.log(`Failed to connect MCP client ${clientConfig.name}:`, error);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
await this.startWebSocketServer();
|
|
2408
|
+
await this.startOpenAIProxy();
|
|
2409
|
+
console.log(`\u{1F680} Rainfall daemon running`);
|
|
2410
|
+
console.log(` WebSocket (MCP): ws://localhost:${this.port}`);
|
|
2411
|
+
console.log(` OpenAI API: http://localhost:${this.openaiPort}/v1/chat/completions`);
|
|
2412
|
+
console.log(` Health Check: http://localhost:${this.openaiPort}/health`);
|
|
2413
|
+
console.log(` Edge Node ID: ${this.networkedExecutor.getEdgeNodeId() || "local"}`);
|
|
2414
|
+
console.log(` Tools loaded: ${this.tools.length}`);
|
|
2415
|
+
console.log(` Press Ctrl+C to stop`);
|
|
2416
|
+
process.on("SIGINT", () => this.stop());
|
|
2417
|
+
process.on("SIGTERM", () => this.stop());
|
|
2418
|
+
}
|
|
2419
|
+
async stop() {
|
|
2420
|
+
this.log("\u{1F6D1} Shutting down Rainfall daemon...");
|
|
2421
|
+
if (this.listeners) {
|
|
2422
|
+
await this.listeners.stopAll();
|
|
2423
|
+
}
|
|
2424
|
+
if (this.networkedExecutor) {
|
|
2425
|
+
await this.networkedExecutor.unregisterEdgeNode();
|
|
2426
|
+
}
|
|
2427
|
+
if (this.mcpProxy) {
|
|
2428
|
+
await this.mcpProxy.shutdown();
|
|
2429
|
+
this.mcpProxy = void 0;
|
|
2430
|
+
}
|
|
2431
|
+
for (const client of this.clients) {
|
|
2432
|
+
client.close();
|
|
2433
|
+
}
|
|
2434
|
+
this.clients.clear();
|
|
2435
|
+
if (this.wss) {
|
|
2436
|
+
this.wss.close();
|
|
2437
|
+
this.wss = void 0;
|
|
2438
|
+
}
|
|
2439
|
+
console.log("\u{1F44B} Rainfall daemon stopped");
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* Get the networked executor for distributed job management
|
|
2443
|
+
*/
|
|
2444
|
+
getNetworkedExecutor() {
|
|
2445
|
+
return this.networkedExecutor;
|
|
2446
|
+
}
|
|
2447
|
+
/**
|
|
2448
|
+
* Get the context for memory/session management
|
|
2449
|
+
*/
|
|
2450
|
+
getContext() {
|
|
2451
|
+
return this.context;
|
|
2452
|
+
}
|
|
2453
|
+
/**
|
|
2454
|
+
* Get the listener registry for passive triggers
|
|
2455
|
+
*/
|
|
2456
|
+
getListenerRegistry() {
|
|
2457
|
+
return this.listeners;
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Get the MCP Proxy Hub for managing external MCP clients
|
|
2461
|
+
*/
|
|
2462
|
+
getMCPProxy() {
|
|
2463
|
+
return this.mcpProxy;
|
|
2464
|
+
}
|
|
2465
|
+
/**
|
|
2466
|
+
* Connect an MCP client dynamically
|
|
2467
|
+
*/
|
|
2468
|
+
async connectMCPClient(config) {
|
|
2469
|
+
if (!this.mcpProxy) {
|
|
2470
|
+
throw new Error("MCP Proxy Hub is not enabled");
|
|
2471
|
+
}
|
|
2472
|
+
return this.mcpProxy.connectClient(config);
|
|
2473
|
+
}
|
|
2474
|
+
/**
|
|
2475
|
+
* Disconnect an MCP client
|
|
2476
|
+
*/
|
|
2477
|
+
async disconnectMCPClient(name) {
|
|
2478
|
+
if (!this.mcpProxy) {
|
|
2479
|
+
throw new Error("MCP Proxy Hub is not enabled");
|
|
2480
|
+
}
|
|
2481
|
+
return this.mcpProxy.disconnectClient(name);
|
|
2482
|
+
}
|
|
2483
|
+
async initializeRainfall() {
|
|
2484
|
+
if (this.rainfallConfig?.apiKey) {
|
|
2485
|
+
this.rainfall = new Rainfall(this.rainfallConfig);
|
|
2486
|
+
} else {
|
|
2487
|
+
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
2488
|
+
const config = loadConfig2();
|
|
2489
|
+
if (config.apiKey) {
|
|
2490
|
+
this.rainfall = new Rainfall({
|
|
2491
|
+
apiKey: config.apiKey,
|
|
2492
|
+
baseUrl: config.baseUrl
|
|
2493
|
+
});
|
|
2494
|
+
} else {
|
|
2495
|
+
throw new Error("No API key configured. Run: rainfall auth login <api-key>");
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
async loadTools() {
|
|
2500
|
+
if (!this.rainfall) return;
|
|
2501
|
+
try {
|
|
2502
|
+
this.tools = await this.rainfall.listTools();
|
|
2503
|
+
this.log(`\u{1F4E6} Loaded ${this.tools.length} tools`);
|
|
2504
|
+
} catch (error) {
|
|
2505
|
+
console.warn("\u26A0\uFE0F Failed to load tools:", error instanceof Error ? error.message : error);
|
|
2506
|
+
this.tools = [];
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
async getToolSchema(toolId) {
|
|
2510
|
+
if (this.toolSchemas.has(toolId)) {
|
|
2511
|
+
return this.toolSchemas.get(toolId);
|
|
2512
|
+
}
|
|
2513
|
+
if (!this.rainfall) return null;
|
|
2514
|
+
try {
|
|
2515
|
+
const schema = await this.rainfall.getToolSchema(toolId);
|
|
2516
|
+
this.toolSchemas.set(toolId, schema);
|
|
2517
|
+
return schema;
|
|
2518
|
+
} catch {
|
|
2519
|
+
return null;
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
async startWebSocketServer() {
|
|
2523
|
+
this.wss = new import_ws2.WebSocketServer({ port: this.port });
|
|
2524
|
+
this.wss.on("connection", (ws) => {
|
|
2525
|
+
this.log("\u{1F7E2} MCP client connected");
|
|
2526
|
+
this.clients.add(ws);
|
|
2527
|
+
ws.on("message", async (data) => {
|
|
2528
|
+
try {
|
|
2529
|
+
const message = JSON.parse(data.toString());
|
|
2530
|
+
const response = await this.handleMCPMessage(message);
|
|
2531
|
+
ws.send(JSON.stringify(response));
|
|
2532
|
+
} catch (error) {
|
|
2533
|
+
const errorResponse = {
|
|
2534
|
+
jsonrpc: "2.0",
|
|
2535
|
+
id: void 0,
|
|
2536
|
+
error: {
|
|
2537
|
+
code: -32700,
|
|
2538
|
+
message: error instanceof Error ? error.message : "Parse error"
|
|
2539
|
+
}
|
|
2540
|
+
};
|
|
2541
|
+
ws.send(JSON.stringify(errorResponse));
|
|
2542
|
+
}
|
|
2543
|
+
});
|
|
2544
|
+
ws.on("close", () => {
|
|
2545
|
+
this.log("\u{1F534} MCP client disconnected");
|
|
2546
|
+
this.clients.delete(ws);
|
|
2547
|
+
});
|
|
2548
|
+
ws.on("error", (error) => {
|
|
2549
|
+
console.error("WebSocket error:", error);
|
|
2550
|
+
this.clients.delete(ws);
|
|
2551
|
+
});
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
async handleMCPMessage(message) {
|
|
2555
|
+
const { id, method, params } = message;
|
|
2556
|
+
switch (method) {
|
|
2557
|
+
case "initialize":
|
|
2558
|
+
return {
|
|
2559
|
+
jsonrpc: "2.0",
|
|
2560
|
+
id,
|
|
2561
|
+
result: {
|
|
2562
|
+
protocolVersion: "2024-11-05",
|
|
2563
|
+
capabilities: {
|
|
2564
|
+
tools: { listChanged: true }
|
|
2565
|
+
},
|
|
2566
|
+
serverInfo: {
|
|
2567
|
+
name: "rainfall-daemon",
|
|
2568
|
+
version: "0.1.0"
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
};
|
|
2572
|
+
case "tools/list":
|
|
2573
|
+
return {
|
|
2574
|
+
jsonrpc: "2.0",
|
|
2575
|
+
id,
|
|
2576
|
+
result: {
|
|
2577
|
+
tools: await this.getMCPTools()
|
|
2578
|
+
}
|
|
2579
|
+
};
|
|
2580
|
+
case "tools/call": {
|
|
2581
|
+
const toolName = params?.name;
|
|
2582
|
+
const toolParams = params?.arguments;
|
|
2583
|
+
try {
|
|
2584
|
+
const startTime = Date.now();
|
|
2585
|
+
const result = await this.executeToolWithMCP(toolName, toolParams);
|
|
2586
|
+
const duration = Date.now() - startTime;
|
|
2587
|
+
if (this.context) {
|
|
2588
|
+
this.context.recordExecution(toolName, toolParams || {}, result, { duration });
|
|
2589
|
+
}
|
|
2590
|
+
return {
|
|
2591
|
+
jsonrpc: "2.0",
|
|
2592
|
+
id,
|
|
2593
|
+
result: {
|
|
2594
|
+
content: [
|
|
2595
|
+
{
|
|
2596
|
+
type: "text",
|
|
2597
|
+
text: typeof result === "string" ? result : JSON.stringify(result, null, 2)
|
|
2598
|
+
}
|
|
2599
|
+
]
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2602
|
+
} catch (error) {
|
|
2603
|
+
const errorMessage = error instanceof Error ? error.message : "Tool execution failed";
|
|
2604
|
+
if (this.context) {
|
|
2605
|
+
this.context.recordExecution(toolName, toolParams || {}, null, {
|
|
2606
|
+
error: errorMessage,
|
|
2607
|
+
duration: 0
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
return {
|
|
2611
|
+
jsonrpc: "2.0",
|
|
2612
|
+
id,
|
|
2613
|
+
error: {
|
|
2614
|
+
code: -32603,
|
|
2615
|
+
message: errorMessage
|
|
2616
|
+
}
|
|
2617
|
+
};
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
case "ping":
|
|
2621
|
+
return {
|
|
2622
|
+
jsonrpc: "2.0",
|
|
2623
|
+
id,
|
|
2624
|
+
result: {}
|
|
2625
|
+
};
|
|
2626
|
+
default:
|
|
2627
|
+
return {
|
|
2628
|
+
jsonrpc: "2.0",
|
|
2629
|
+
id,
|
|
2630
|
+
error: {
|
|
2631
|
+
code: -32601,
|
|
2632
|
+
message: `Method not found: ${method}`
|
|
2633
|
+
}
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
async getMCPTools() {
|
|
2638
|
+
const mcpTools = [];
|
|
2639
|
+
for (const tool of this.tools) {
|
|
2640
|
+
const schema = await this.getToolSchema(tool.id);
|
|
2641
|
+
if (schema) {
|
|
2642
|
+
const toolSchema = schema;
|
|
2643
|
+
mcpTools.push({
|
|
2644
|
+
name: tool.id,
|
|
2645
|
+
description: toolSchema.description || tool.description,
|
|
2646
|
+
inputSchema: toolSchema.parameters || { type: "object", properties: {} }
|
|
2647
|
+
});
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
if (this.mcpProxy) {
|
|
2651
|
+
const proxyTools = this.mcpProxy.getAllTools({ namespacePrefix: this.mcpNamespacePrefix });
|
|
2652
|
+
for (const tool of proxyTools) {
|
|
2653
|
+
mcpTools.push({
|
|
2654
|
+
name: this.mcpNamespacePrefix ? `${tool.serverName}-${tool.name}` : tool.name,
|
|
2655
|
+
description: tool.description,
|
|
2656
|
+
inputSchema: tool.inputSchema || { type: "object", properties: {} }
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
return mcpTools;
|
|
2661
|
+
}
|
|
2662
|
+
async executeTool(toolId, params) {
|
|
2663
|
+
if (!this.rainfall) {
|
|
2664
|
+
throw new Error("Rainfall SDK not initialized");
|
|
2665
|
+
}
|
|
2666
|
+
return this.rainfall.executeTool(toolId, params);
|
|
2667
|
+
}
|
|
2668
|
+
/**
|
|
2669
|
+
* Execute a tool, trying MCP proxy first, then falling back to Rainfall tools
|
|
2670
|
+
*/
|
|
2671
|
+
async executeToolWithMCP(toolName, params) {
|
|
2672
|
+
if (this.mcpProxy) {
|
|
2673
|
+
try {
|
|
2674
|
+
if (this.mcpNamespacePrefix && toolName.includes("-")) {
|
|
2675
|
+
const namespace = toolName.split("-")[0];
|
|
2676
|
+
const actualToolName = toolName.slice(namespace.length + 1);
|
|
2677
|
+
if (this.mcpProxy.getClient(namespace)) {
|
|
2678
|
+
return await this.mcpProxy.callTool(toolName, params || {}, {
|
|
2679
|
+
namespace
|
|
2680
|
+
});
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
return await this.mcpProxy.callTool(toolName, params || {});
|
|
2684
|
+
} catch (error) {
|
|
2685
|
+
if (error instanceof Error && !error.message.includes("not found")) {
|
|
2686
|
+
throw error;
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
return this.executeTool(toolName, params);
|
|
2691
|
+
}
|
|
2692
|
+
async startOpenAIProxy() {
|
|
2693
|
+
this.openaiApp.get("/v1/models", async (_req, res) => {
|
|
2694
|
+
try {
|
|
2695
|
+
if (this.rainfall) {
|
|
2696
|
+
const models = await this.rainfall.listModels();
|
|
2697
|
+
res.json({
|
|
2698
|
+
object: "list",
|
|
2699
|
+
data: models.map((m) => ({
|
|
2700
|
+
id: m.id,
|
|
2701
|
+
object: "model",
|
|
2702
|
+
created: Math.floor(Date.now() / 1e3),
|
|
2703
|
+
owned_by: "rainfall"
|
|
2704
|
+
}))
|
|
2705
|
+
});
|
|
2706
|
+
} else {
|
|
2707
|
+
res.json({
|
|
2708
|
+
object: "list",
|
|
2709
|
+
data: [
|
|
2710
|
+
{ id: "llama-3.3-70b-versatile", object: "model", created: Date.now(), owned_by: "groq" },
|
|
2711
|
+
{ id: "gpt-4o", object: "model", created: Date.now(), owned_by: "openai" },
|
|
2712
|
+
{ id: "claude-3-5-sonnet", object: "model", created: Date.now(), owned_by: "anthropic" },
|
|
2713
|
+
{ id: "gemini-2.0-flash-exp", object: "model", created: Date.now(), owned_by: "gemini" }
|
|
2714
|
+
]
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2717
|
+
} catch (error) {
|
|
2718
|
+
res.status(500).json({ error: "Failed to fetch models" });
|
|
2719
|
+
}
|
|
2720
|
+
});
|
|
2721
|
+
this.openaiApp.post("/v1/chat/completions", async (req, res) => {
|
|
2722
|
+
const body = req.body;
|
|
2723
|
+
if (!body.messages || !Array.isArray(body.messages)) {
|
|
2724
|
+
res.status(400).json({
|
|
2725
|
+
error: {
|
|
2726
|
+
message: "Missing required field: messages",
|
|
2727
|
+
type: "invalid_request_error"
|
|
2728
|
+
}
|
|
2729
|
+
});
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
if (!this.rainfall) {
|
|
2733
|
+
res.status(503).json({
|
|
2734
|
+
error: {
|
|
2735
|
+
message: "Rainfall SDK not initialized",
|
|
2736
|
+
type: "service_unavailable"
|
|
2737
|
+
}
|
|
2738
|
+
});
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
try {
|
|
2742
|
+
const me = await this.rainfall.getMe();
|
|
2743
|
+
const subscriberId = me.id;
|
|
2744
|
+
const localToolMap = await this.buildLocalToolMap();
|
|
2745
|
+
let allTools = [];
|
|
2746
|
+
if (body.tools && body.tools.length > 0) {
|
|
2747
|
+
allTools = body.tools;
|
|
2748
|
+
} else if (body.tool_choice) {
|
|
2749
|
+
const openaiTools = await this.getOpenAITools();
|
|
2750
|
+
allTools = openaiTools;
|
|
2751
|
+
}
|
|
2752
|
+
let messages = [...body.messages];
|
|
2753
|
+
const maxToolIterations = 10;
|
|
2754
|
+
let toolIterations = 0;
|
|
2755
|
+
while (toolIterations < maxToolIterations) {
|
|
2756
|
+
toolIterations++;
|
|
2757
|
+
const llmResponse = await this.callLLM({
|
|
2758
|
+
subscriberId,
|
|
2759
|
+
model: body.model,
|
|
2760
|
+
messages,
|
|
2761
|
+
tools: allTools.length > 0 ? allTools : void 0,
|
|
2762
|
+
tool_choice: body.tool_choice,
|
|
2763
|
+
temperature: body.temperature,
|
|
2764
|
+
max_tokens: body.max_tokens,
|
|
2765
|
+
stream: false,
|
|
2766
|
+
// Always non-streaming for tool loop
|
|
2767
|
+
tool_priority: body.tool_priority,
|
|
2768
|
+
enable_stacked: body.enable_stacked
|
|
2769
|
+
});
|
|
2770
|
+
const choice = llmResponse.choices?.[0];
|
|
2771
|
+
let toolCalls = choice?.message?.tool_calls || [];
|
|
2772
|
+
const content = choice?.message?.content || "";
|
|
2773
|
+
const reasoningContent = choice?.message?.reasoning_content || "";
|
|
2774
|
+
const fullContent = content + " " + reasoningContent;
|
|
2775
|
+
const xmlToolCalls = this.parseXMLToolCalls(fullContent);
|
|
2776
|
+
if (xmlToolCalls.length > 0) {
|
|
2777
|
+
this.log(`\u{1F4CB} Parsed ${xmlToolCalls.length} XML tool calls from content`);
|
|
2778
|
+
toolCalls = xmlToolCalls;
|
|
2779
|
+
}
|
|
2780
|
+
if (!toolCalls || toolCalls.length === 0) {
|
|
2781
|
+
if (body.stream) {
|
|
2782
|
+
await this.streamResponse(res, llmResponse);
|
|
2783
|
+
} else {
|
|
2784
|
+
res.json(llmResponse);
|
|
2785
|
+
}
|
|
2786
|
+
this.updateContext(body.messages, llmResponse);
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
messages.push({
|
|
2790
|
+
role: "assistant",
|
|
2791
|
+
content: choice?.message?.content || "",
|
|
2792
|
+
tool_calls: toolCalls
|
|
2793
|
+
});
|
|
2794
|
+
for (const toolCall of toolCalls) {
|
|
2795
|
+
const toolName = toolCall.function?.name;
|
|
2796
|
+
const toolArgsStr = toolCall.function?.arguments || "{}";
|
|
2797
|
+
if (!toolName) continue;
|
|
2798
|
+
this.log(`\u{1F527} Tool call: ${toolName}`);
|
|
2799
|
+
let toolResult;
|
|
2800
|
+
let toolError;
|
|
2801
|
+
try {
|
|
2802
|
+
const localTool = this.findLocalTool(toolName, localToolMap);
|
|
2803
|
+
if (localTool) {
|
|
2804
|
+
this.log(` \u2192 Executing locally`);
|
|
2805
|
+
const args = JSON.parse(toolArgsStr);
|
|
2806
|
+
toolResult = await this.executeLocalTool(localTool.id, args);
|
|
2807
|
+
} else if (this.mcpProxy) {
|
|
2808
|
+
this.log(` \u2192 Trying MCP proxy`);
|
|
2809
|
+
const args = JSON.parse(toolArgsStr);
|
|
2810
|
+
toolResult = await this.executeToolWithMCP(toolName.replace(/_/g, "-"), args);
|
|
2811
|
+
} else {
|
|
2812
|
+
const shouldExecuteLocal = body.tool_priority === "local" || body.tool_priority === "stacked";
|
|
2813
|
+
if (shouldExecuteLocal) {
|
|
2814
|
+
try {
|
|
2815
|
+
const args = JSON.parse(toolArgsStr);
|
|
2816
|
+
toolResult = await this.rainfall.executeTool(toolName.replace(/_/g, "-"), args);
|
|
2817
|
+
} catch {
|
|
2818
|
+
toolResult = { _pending: true, tool: toolName, args: toolArgsStr };
|
|
2819
|
+
}
|
|
2820
|
+
} else {
|
|
2821
|
+
toolResult = { _pending: true, tool: toolName, args: toolArgsStr };
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
} catch (error) {
|
|
2825
|
+
toolError = error instanceof Error ? error.message : String(error);
|
|
2826
|
+
this.log(` \u2192 Error: ${toolError}`);
|
|
2827
|
+
}
|
|
2828
|
+
messages.push({
|
|
2829
|
+
role: "tool",
|
|
2830
|
+
content: toolError ? JSON.stringify({ error: toolError }) : typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult),
|
|
2831
|
+
tool_call_id: toolCall.id
|
|
2832
|
+
});
|
|
2833
|
+
if (this.context) {
|
|
2834
|
+
this.context.recordExecution(
|
|
2835
|
+
toolName,
|
|
2836
|
+
JSON.parse(toolArgsStr || "{}"),
|
|
2837
|
+
toolResult,
|
|
2838
|
+
{ error: toolError, duration: 0 }
|
|
2839
|
+
);
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
res.status(500).json({
|
|
2844
|
+
error: {
|
|
2845
|
+
message: "Maximum tool execution iterations reached",
|
|
2846
|
+
type: "tool_execution_error"
|
|
2847
|
+
}
|
|
2848
|
+
});
|
|
2849
|
+
} catch (error) {
|
|
2850
|
+
this.log("Chat completions error:", error);
|
|
2851
|
+
res.status(500).json({
|
|
2852
|
+
error: {
|
|
2853
|
+
message: error instanceof Error ? error.message : "Internal server error",
|
|
2854
|
+
type: "internal_error"
|
|
2855
|
+
}
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
});
|
|
2859
|
+
this.openaiApp.get("/health", (_req, res) => {
|
|
2860
|
+
const mcpStats = this.mcpProxy?.getStats();
|
|
2861
|
+
res.json({
|
|
2862
|
+
status: "ok",
|
|
2863
|
+
daemon: "rainfall",
|
|
2864
|
+
version: "0.2.0",
|
|
2865
|
+
tools_loaded: this.tools.length,
|
|
2866
|
+
mcp_clients: mcpStats?.totalClients || 0,
|
|
2867
|
+
mcp_tools: mcpStats?.totalTools || 0,
|
|
2868
|
+
edge_node_id: this.networkedExecutor?.getEdgeNodeId(),
|
|
2869
|
+
clients_connected: this.clients.size
|
|
2870
|
+
});
|
|
2871
|
+
});
|
|
2872
|
+
this.openaiApp.get("/v1/mcp/clients", (_req, res) => {
|
|
2873
|
+
if (!this.mcpProxy) {
|
|
2874
|
+
res.status(503).json({ error: "MCP proxy not enabled" });
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
res.json(this.mcpProxy.listClients());
|
|
2878
|
+
});
|
|
2879
|
+
this.openaiApp.post("/v1/mcp/connect", async (req, res) => {
|
|
2880
|
+
if (!this.mcpProxy) {
|
|
2881
|
+
res.status(503).json({ error: "MCP proxy not enabled" });
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
try {
|
|
2885
|
+
const name = await this.mcpProxy.connectClient(req.body);
|
|
2886
|
+
res.json({ success: true, client: name });
|
|
2887
|
+
} catch (error) {
|
|
2888
|
+
res.status(500).json({
|
|
2889
|
+
error: error instanceof Error ? error.message : "Failed to connect MCP client"
|
|
2890
|
+
});
|
|
2891
|
+
}
|
|
2892
|
+
});
|
|
2893
|
+
this.openaiApp.post("/v1/mcp/disconnect", async (req, res) => {
|
|
2894
|
+
if (!this.mcpProxy) {
|
|
2895
|
+
res.status(503).json({ error: "MCP proxy not enabled" });
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2898
|
+
const { name } = req.body;
|
|
2899
|
+
if (!name) {
|
|
2900
|
+
res.status(400).json({ error: "Missing required field: name" });
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
await this.mcpProxy.disconnectClient(name);
|
|
2904
|
+
res.json({ success: true });
|
|
2905
|
+
});
|
|
2906
|
+
this.openaiApp.get("/status", (_req, res) => {
|
|
2907
|
+
res.json(this.getStatus());
|
|
2908
|
+
});
|
|
2909
|
+
this.openaiApp.post("/v1/queue", async (req, res) => {
|
|
2910
|
+
const { tool_id, params, execution_mode = "any" } = req.body;
|
|
2911
|
+
if (!tool_id) {
|
|
2912
|
+
res.status(400).json({ error: "Missing required field: tool_id" });
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
if (!this.networkedExecutor) {
|
|
2916
|
+
res.status(503).json({ error: "Networked executor not available" });
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2919
|
+
try {
|
|
2920
|
+
const jobId = await this.networkedExecutor.queueToolExecution(
|
|
2921
|
+
tool_id,
|
|
2922
|
+
params || {},
|
|
2923
|
+
{ executionMode: execution_mode }
|
|
2924
|
+
);
|
|
2925
|
+
res.json({ job_id: jobId, status: "queued" });
|
|
2926
|
+
} catch (error) {
|
|
2927
|
+
res.status(500).json({
|
|
2928
|
+
error: error instanceof Error ? error.message : "Failed to queue job"
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
2931
|
+
});
|
|
2932
|
+
return new Promise((resolve) => {
|
|
2933
|
+
this.openaiApp.listen(this.openaiPort, () => {
|
|
2934
|
+
resolve();
|
|
2935
|
+
});
|
|
2936
|
+
});
|
|
2937
|
+
}
|
|
2938
|
+
/**
|
|
2939
|
+
* Build a map of local Rainfall tools for quick lookup
|
|
2940
|
+
* Maps OpenAI-style underscore names to Rainfall tool IDs
|
|
2941
|
+
*/
|
|
2942
|
+
async buildLocalToolMap() {
|
|
2943
|
+
const map = /* @__PURE__ */ new Map();
|
|
2944
|
+
for (const tool of this.tools) {
|
|
2945
|
+
const openAiName = tool.id.replace(/-/g, "_");
|
|
2946
|
+
map.set(openAiName, {
|
|
2947
|
+
id: tool.id,
|
|
2948
|
+
name: openAiName,
|
|
2949
|
+
description: tool.description
|
|
2950
|
+
});
|
|
2951
|
+
map.set(tool.id, {
|
|
2952
|
+
id: tool.id,
|
|
2953
|
+
name: openAiName,
|
|
2954
|
+
description: tool.description
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
return map;
|
|
2958
|
+
}
|
|
2959
|
+
/**
|
|
2960
|
+
* Find a local Rainfall tool by name (OpenAI underscore format or original)
|
|
2961
|
+
*/
|
|
2962
|
+
findLocalTool(toolName, localToolMap) {
|
|
2963
|
+
if (localToolMap.has(toolName)) {
|
|
2964
|
+
return localToolMap.get(toolName);
|
|
2965
|
+
}
|
|
2966
|
+
const dashedName = toolName.replace(/_/g, "-");
|
|
2967
|
+
if (localToolMap.has(dashedName)) {
|
|
2968
|
+
return localToolMap.get(dashedName);
|
|
2969
|
+
}
|
|
2970
|
+
return void 0;
|
|
2971
|
+
}
|
|
2972
|
+
/**
|
|
2973
|
+
* Execute a local Rainfall tool
|
|
2974
|
+
*/
|
|
2975
|
+
async executeLocalTool(toolId, args) {
|
|
2976
|
+
if (!this.rainfall) {
|
|
2977
|
+
throw new Error("Rainfall SDK not initialized");
|
|
2978
|
+
}
|
|
2979
|
+
const startTime = Date.now();
|
|
2980
|
+
try {
|
|
2981
|
+
const result = await this.rainfall.executeTool(toolId, args);
|
|
2982
|
+
const duration = Date.now() - startTime;
|
|
2983
|
+
this.log(` \u2713 Completed in ${duration}ms`);
|
|
2984
|
+
return result;
|
|
2985
|
+
} catch (error) {
|
|
2986
|
+
const duration = Date.now() - startTime;
|
|
2987
|
+
this.log(` \u2717 Failed after ${duration}ms`);
|
|
2988
|
+
throw error;
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
/**
|
|
2992
|
+
* Parse XML-style tool calls from model output
|
|
2993
|
+
* Handles formats like: <function=name><parameter=key>value</parameter></function>
|
|
2994
|
+
*/
|
|
2995
|
+
parseXMLToolCalls(content) {
|
|
2996
|
+
const toolCalls = [];
|
|
2997
|
+
const functionRegex = /<function=([^>]+)>([\s\S]*?)<\/function>/gi;
|
|
2998
|
+
let match;
|
|
2999
|
+
while ((match = functionRegex.exec(content)) !== null) {
|
|
3000
|
+
const functionName = match[1].trim();
|
|
3001
|
+
const paramsBlock = match[2];
|
|
3002
|
+
const params = {};
|
|
3003
|
+
const paramRegex = /<parameter=([^>]+)>([\s\S]*?)<\/parameter>/gi;
|
|
3004
|
+
let paramMatch;
|
|
3005
|
+
while ((paramMatch = paramRegex.exec(paramsBlock)) !== null) {
|
|
3006
|
+
const paramName = paramMatch[1].trim();
|
|
3007
|
+
const paramValue = paramMatch[2].trim();
|
|
3008
|
+
params[paramName] = paramValue;
|
|
3009
|
+
}
|
|
3010
|
+
toolCalls.push({
|
|
3011
|
+
id: `xml-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
3012
|
+
type: "function",
|
|
3013
|
+
function: {
|
|
3014
|
+
name: functionName,
|
|
3015
|
+
arguments: JSON.stringify(params)
|
|
3016
|
+
}
|
|
3017
|
+
});
|
|
3018
|
+
this.log(`\u{1F4CB} Parsed XML tool call: ${functionName}(${JSON.stringify(params)})`);
|
|
3019
|
+
}
|
|
3020
|
+
return toolCalls;
|
|
3021
|
+
}
|
|
3022
|
+
/**
|
|
3023
|
+
* Call the LLM via Rainfall backend, LM Studio, RunPod, or other providers
|
|
3024
|
+
*
|
|
3025
|
+
* Provider priority:
|
|
3026
|
+
* 1. Config file (llm.provider, llm.baseUrl)
|
|
3027
|
+
* 2. Environment variables (OPENAI_API_KEY, OLLAMA_HOST, etc.)
|
|
3028
|
+
* 3. Default to Rainfall (credits-based)
|
|
3029
|
+
*/
|
|
3030
|
+
async callLLM(params) {
|
|
3031
|
+
if (!this.rainfall) {
|
|
3032
|
+
throw new Error("Rainfall SDK not initialized");
|
|
3033
|
+
}
|
|
3034
|
+
const { loadConfig: loadConfig2, getProviderBaseUrl: getProviderBaseUrl2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
3035
|
+
const config = loadConfig2();
|
|
3036
|
+
const provider = config.llm?.provider || "rainfall";
|
|
3037
|
+
switch (provider) {
|
|
3038
|
+
case "local":
|
|
3039
|
+
case "ollama":
|
|
3040
|
+
case "custom":
|
|
3041
|
+
return this.callLocalLLM(params, config);
|
|
3042
|
+
case "openai":
|
|
3043
|
+
case "anthropic":
|
|
3044
|
+
return this.callExternalLLM(params, config, provider);
|
|
3045
|
+
case "rainfall":
|
|
3046
|
+
default:
|
|
3047
|
+
return this.rainfall.chatCompletions({
|
|
3048
|
+
subscriber_id: params.subscriberId,
|
|
3049
|
+
model: params.model,
|
|
3050
|
+
messages: params.messages,
|
|
3051
|
+
stream: params.stream || false,
|
|
3052
|
+
temperature: params.temperature,
|
|
3053
|
+
max_tokens: params.max_tokens,
|
|
3054
|
+
tools: params.tools,
|
|
3055
|
+
tool_choice: params.tool_choice,
|
|
3056
|
+
tool_priority: params.tool_priority,
|
|
3057
|
+
enable_stacked: params.enable_stacked
|
|
3058
|
+
});
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
/**
|
|
3062
|
+
* Call external LLM provider (OpenAI, Anthropic) via their OpenAI-compatible APIs
|
|
3063
|
+
*/
|
|
3064
|
+
async callExternalLLM(params, config, provider) {
|
|
3065
|
+
const { getProviderBaseUrl: getProviderBaseUrl2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
3066
|
+
const baseUrl = config.llm?.baseUrl || getProviderBaseUrl2({ llm: { provider } });
|
|
3067
|
+
const apiKey = config.llm?.apiKey;
|
|
3068
|
+
if (!apiKey) {
|
|
3069
|
+
throw new Error(`${provider} API key not configured. Set via: rainfall config set llm.apiKey <key>`);
|
|
3070
|
+
}
|
|
3071
|
+
const model = params.model || config.llm?.model || (provider === "anthropic" ? "claude-3-5-sonnet-20241022" : "gpt-4o");
|
|
3072
|
+
const url = `${baseUrl}/chat/completions`;
|
|
3073
|
+
const response = await fetch(url, {
|
|
3074
|
+
method: "POST",
|
|
3075
|
+
headers: {
|
|
3076
|
+
"Content-Type": "application/json",
|
|
3077
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
3078
|
+
"User-Agent": "Rainfall-DevKit/1.0"
|
|
3079
|
+
},
|
|
3080
|
+
body: JSON.stringify({
|
|
3081
|
+
model,
|
|
3082
|
+
messages: params.messages,
|
|
3083
|
+
tools: params.tools,
|
|
3084
|
+
tool_choice: params.tool_choice,
|
|
3085
|
+
temperature: params.temperature,
|
|
3086
|
+
max_tokens: params.max_tokens,
|
|
3087
|
+
stream: false
|
|
3088
|
+
// Tool loop requires non-streaming
|
|
3089
|
+
})
|
|
3090
|
+
});
|
|
3091
|
+
if (!response.ok) {
|
|
3092
|
+
const error = await response.text();
|
|
3093
|
+
throw new Error(`${provider} API error: ${error}`);
|
|
3094
|
+
}
|
|
3095
|
+
return response.json();
|
|
3096
|
+
}
|
|
3097
|
+
/**
|
|
3098
|
+
* Call a local LLM (LM Studio, Ollama, etc.)
|
|
3099
|
+
*/
|
|
3100
|
+
async callLocalLLM(params, config) {
|
|
3101
|
+
const baseUrl = config.llm?.baseUrl || "http://localhost:1234/v1";
|
|
3102
|
+
const apiKey = config.llm?.apiKey || "not-needed";
|
|
3103
|
+
const model = params.model || config.llm?.model || "local-model";
|
|
3104
|
+
const url = `${baseUrl}/chat/completions`;
|
|
3105
|
+
const response = await fetch(url, {
|
|
3106
|
+
method: "POST",
|
|
3107
|
+
headers: {
|
|
3108
|
+
"Content-Type": "application/json",
|
|
3109
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
3110
|
+
"User-Agent": "Rainfall-DevKit/1.0"
|
|
3111
|
+
},
|
|
3112
|
+
body: JSON.stringify({
|
|
3113
|
+
model,
|
|
3114
|
+
messages: params.messages,
|
|
3115
|
+
tools: params.tools,
|
|
3116
|
+
tool_choice: params.tool_choice,
|
|
3117
|
+
temperature: params.temperature,
|
|
3118
|
+
max_tokens: params.max_tokens,
|
|
3119
|
+
stream: false
|
|
3120
|
+
// Tool loop requires non-streaming
|
|
3121
|
+
})
|
|
3122
|
+
});
|
|
3123
|
+
if (!response.ok) {
|
|
3124
|
+
const error = await response.text();
|
|
3125
|
+
throw new Error(`Local LLM error: ${error}`);
|
|
3126
|
+
}
|
|
3127
|
+
return response.json();
|
|
3128
|
+
}
|
|
3129
|
+
/**
|
|
3130
|
+
* Stream a response to the client (converts non-streaming to SSE format)
|
|
3131
|
+
*/
|
|
3132
|
+
async streamResponse(res, response) {
|
|
3133
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
3134
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
3135
|
+
res.setHeader("Connection", "keep-alive");
|
|
3136
|
+
const message = response.choices?.[0]?.message;
|
|
3137
|
+
const id = response.id || `chatcmpl-${Date.now()}`;
|
|
3138
|
+
const model = response.model || "unknown";
|
|
3139
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
3140
|
+
res.write(`data: ${JSON.stringify({
|
|
3141
|
+
id,
|
|
3142
|
+
object: "chat.completion.chunk",
|
|
3143
|
+
created,
|
|
3144
|
+
model,
|
|
3145
|
+
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }]
|
|
3146
|
+
})}
|
|
489
3147
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
vision: (params) => client.executeTool("llama-scout-vision", { image: params.imageBase64, prompt: params.prompt }),
|
|
503
|
-
chat: (params) => client.executeTool("xai-chat-completions", params),
|
|
504
|
-
complete: (params) => client.executeTool("fim", params),
|
|
505
|
-
classify: (params) => client.executeTool("jina-document-classifier", params),
|
|
506
|
-
segment: (params) => client.executeTool("jina-text-segmenter", params)
|
|
507
|
-
};
|
|
508
|
-
}
|
|
3148
|
+
`);
|
|
3149
|
+
const content = message?.content || "";
|
|
3150
|
+
const chunkSize = 10;
|
|
3151
|
+
for (let i = 0; i < content.length; i += chunkSize) {
|
|
3152
|
+
const chunk = content.slice(i, i + chunkSize);
|
|
3153
|
+
res.write(`data: ${JSON.stringify({
|
|
3154
|
+
id,
|
|
3155
|
+
object: "chat.completion.chunk",
|
|
3156
|
+
created,
|
|
3157
|
+
model,
|
|
3158
|
+
choices: [{ index: 0, delta: { content: chunk }, finish_reason: null }]
|
|
3159
|
+
})}
|
|
509
3160
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
execute: (params) => client.executeTool("execute-saved-script", params),
|
|
520
|
-
list: () => client.executeTool("list-saved-scripts", {}),
|
|
521
|
-
update: (params) => client.executeTool("update-saved-script", params),
|
|
522
|
-
delete: (params) => client.executeTool("delete-saved-script", params)
|
|
523
|
-
},
|
|
524
|
-
similarity: {
|
|
525
|
-
search: (params) => client.executeTool("duck-db-similarity-search", params),
|
|
526
|
-
duckDbSearch: (params) => client.executeTool("duck-db-similarity-search", params)
|
|
527
|
-
}
|
|
528
|
-
};
|
|
529
|
-
}
|
|
3161
|
+
`);
|
|
3162
|
+
}
|
|
3163
|
+
res.write(`data: ${JSON.stringify({
|
|
3164
|
+
id,
|
|
3165
|
+
object: "chat.completion.chunk",
|
|
3166
|
+
created,
|
|
3167
|
+
model,
|
|
3168
|
+
choices: [{ index: 0, delta: {}, finish_reason: "stop" }]
|
|
3169
|
+
})}
|
|
530
3170
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
3171
|
+
`);
|
|
3172
|
+
res.write("data: [DONE]\n\n");
|
|
3173
|
+
res.end();
|
|
3174
|
+
}
|
|
3175
|
+
/**
|
|
3176
|
+
* Update context with conversation history
|
|
3177
|
+
*/
|
|
3178
|
+
updateContext(originalMessages, response) {
|
|
3179
|
+
if (!this.context) return;
|
|
3180
|
+
const lastUserMessage = originalMessages.filter((m) => m.role === "user").pop();
|
|
3181
|
+
if (lastUserMessage) {
|
|
3182
|
+
this.context.addMessage("user", lastUserMessage.content);
|
|
3183
|
+
}
|
|
3184
|
+
const assistantContent = response.choices?.[0]?.message?.content;
|
|
3185
|
+
if (assistantContent) {
|
|
3186
|
+
this.context.addMessage("assistant", assistantContent);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
async getOpenAITools() {
|
|
3190
|
+
const tools = [];
|
|
3191
|
+
for (const tool of this.tools.slice(0, 100)) {
|
|
3192
|
+
const schema = await this.getToolSchema(tool.id);
|
|
3193
|
+
if (schema) {
|
|
3194
|
+
const toolSchema = schema;
|
|
3195
|
+
let parameters = { type: "object", properties: {}, required: [] };
|
|
3196
|
+
if (toolSchema.parameters && typeof toolSchema.parameters === "object") {
|
|
3197
|
+
const rawParams = toolSchema.parameters;
|
|
3198
|
+
parameters = {
|
|
3199
|
+
type: rawParams.type || "object",
|
|
3200
|
+
properties: rawParams.properties || {},
|
|
3201
|
+
required: rawParams.required || []
|
|
3202
|
+
};
|
|
3203
|
+
}
|
|
3204
|
+
tools.push({
|
|
3205
|
+
type: "function",
|
|
3206
|
+
function: {
|
|
3207
|
+
name: tool.id.replace(/-/g, "_"),
|
|
3208
|
+
// OpenAI requires underscore names
|
|
3209
|
+
description: toolSchema.description || tool.description,
|
|
3210
|
+
parameters
|
|
3211
|
+
}
|
|
3212
|
+
});
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
if (this.mcpProxy) {
|
|
3216
|
+
const proxyTools = this.mcpProxy.getAllTools({ namespacePrefix: this.mcpNamespacePrefix });
|
|
3217
|
+
for (const tool of proxyTools.slice(0, 28)) {
|
|
3218
|
+
const inputSchema = tool.inputSchema || {};
|
|
3219
|
+
tools.push({
|
|
3220
|
+
type: "function",
|
|
3221
|
+
function: {
|
|
3222
|
+
name: this.mcpNamespacePrefix ? `${tool.serverName}_${tool.name}`.replace(/-/g, "_") : tool.name.replace(/-/g, "_"),
|
|
3223
|
+
description: `[${tool.serverName}] ${tool.description}`,
|
|
3224
|
+
parameters: {
|
|
3225
|
+
type: "object",
|
|
3226
|
+
properties: inputSchema.properties || {},
|
|
3227
|
+
required: inputSchema.required || []
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
});
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
return tools;
|
|
3234
|
+
}
|
|
3235
|
+
buildResponseContent() {
|
|
3236
|
+
const edgeNodeId = this.networkedExecutor?.getEdgeNodeId();
|
|
3237
|
+
const toolCount = this.tools.length;
|
|
3238
|
+
return `Rainfall daemon online. Edge node: ${edgeNodeId || "local"}. ${toolCount} tools available. What would you like to execute locally or in the cloud?`;
|
|
3239
|
+
}
|
|
3240
|
+
getStatus() {
|
|
3241
|
+
return {
|
|
3242
|
+
running: !!this.wss,
|
|
3243
|
+
port: this.port,
|
|
3244
|
+
openaiPort: this.openaiPort,
|
|
3245
|
+
toolsLoaded: this.tools.length,
|
|
3246
|
+
clientsConnected: this.clients.size,
|
|
3247
|
+
edgeNodeId: this.networkedExecutor?.getEdgeNodeId(),
|
|
3248
|
+
context: this.context?.getStatus() || {
|
|
3249
|
+
memoriesCached: 0,
|
|
3250
|
+
activeSessions: 0,
|
|
3251
|
+
executionHistorySize: 0
|
|
3252
|
+
},
|
|
3253
|
+
listeners: this.listeners?.getStatus() || {
|
|
3254
|
+
fileWatchers: 0,
|
|
3255
|
+
cronTriggers: 0,
|
|
3256
|
+
recentEvents: 0
|
|
3257
|
+
}
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3260
|
+
log(...args) {
|
|
3261
|
+
if (this.debug) {
|
|
3262
|
+
console.log(...args);
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
};
|
|
3266
|
+
daemonInstance = null;
|
|
3267
|
+
}
|
|
3268
|
+
});
|
|
548
3269
|
|
|
549
|
-
// src/
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
3270
|
+
// src/cli/index.ts
|
|
3271
|
+
init_cjs_shims();
|
|
3272
|
+
var import_fs2 = require("fs");
|
|
3273
|
+
var import_path2 = require("path");
|
|
3274
|
+
var import_url = require("url");
|
|
3275
|
+
init_sdk();
|
|
3276
|
+
init_config();
|
|
3277
|
+
var import_child_process = require("child_process");
|
|
3278
|
+
|
|
3279
|
+
// src/security/edge-node.ts
|
|
3280
|
+
init_cjs_shims();
|
|
3281
|
+
var sodium = __toESM(require("libsodium-wrappers"));
|
|
3282
|
+
var EdgeNodeSecurity = class {
|
|
3283
|
+
sodiumReady;
|
|
3284
|
+
backendSecret;
|
|
3285
|
+
keyPair;
|
|
3286
|
+
constructor(options = {}) {
|
|
3287
|
+
this.sodiumReady = sodium.ready;
|
|
3288
|
+
this.backendSecret = options.backendSecret;
|
|
3289
|
+
this.keyPair = options.keyPair;
|
|
561
3290
|
}
|
|
562
3291
|
/**
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
* @example
|
|
566
|
-
* ```typescript
|
|
567
|
-
* // GitHub
|
|
568
|
-
* await rainfall.integrations.github.issues.create({
|
|
569
|
-
* owner: 'facebook',
|
|
570
|
-
* repo: 'react',
|
|
571
|
-
* title: 'Bug report'
|
|
572
|
-
* });
|
|
573
|
-
*
|
|
574
|
-
* // Slack
|
|
575
|
-
* await rainfall.integrations.slack.messages.send({
|
|
576
|
-
* channelId: 'C123456',
|
|
577
|
-
* text: 'Hello team!'
|
|
578
|
-
* });
|
|
579
|
-
*
|
|
580
|
-
* // Linear
|
|
581
|
-
* const issues = await rainfall.integrations.linear.issues.list();
|
|
582
|
-
* ```
|
|
3292
|
+
* Initialize libsodium
|
|
583
3293
|
*/
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
this._integrations = createIntegrations(this.client);
|
|
587
|
-
}
|
|
588
|
-
return this._integrations;
|
|
3294
|
+
async initialize() {
|
|
3295
|
+
await this.sodiumReady;
|
|
589
3296
|
}
|
|
3297
|
+
// ============================================================================
|
|
3298
|
+
// JWT Token Management
|
|
3299
|
+
// ============================================================================
|
|
590
3300
|
/**
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
* @example
|
|
594
|
-
* ```typescript
|
|
595
|
-
* // Store a memory
|
|
596
|
-
* await rainfall.memory.create({
|
|
597
|
-
* content: 'User prefers dark mode',
|
|
598
|
-
* keywords: ['preference', 'ui']
|
|
599
|
-
* });
|
|
600
|
-
*
|
|
601
|
-
* // Recall similar memories
|
|
602
|
-
* const memories = await rainfall.memory.recall({
|
|
603
|
-
* query: 'user preferences',
|
|
604
|
-
* topK: 5
|
|
605
|
-
* });
|
|
606
|
-
* ```
|
|
3301
|
+
* Generate a JWT token for an edge node
|
|
3302
|
+
* Note: In production, this is done by the backend. This is for testing.
|
|
607
3303
|
*/
|
|
608
|
-
|
|
609
|
-
if (!this.
|
|
610
|
-
|
|
3304
|
+
generateJWT(edgeNodeId, subscriberId, expiresInDays = 30) {
|
|
3305
|
+
if (!this.backendSecret) {
|
|
3306
|
+
throw new Error("Backend secret not configured");
|
|
611
3307
|
}
|
|
612
|
-
|
|
3308
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
3309
|
+
const exp = now + expiresInDays * 24 * 60 * 60;
|
|
3310
|
+
const jti = this.generateTokenId();
|
|
3311
|
+
const payload = {
|
|
3312
|
+
sub: edgeNodeId,
|
|
3313
|
+
iss: "rainfall-backend",
|
|
3314
|
+
iat: now,
|
|
3315
|
+
exp,
|
|
3316
|
+
jti,
|
|
3317
|
+
scope: ["edge:heartbeat", "edge:claim", "edge:submit", "edge:queue"]
|
|
3318
|
+
};
|
|
3319
|
+
const header = { alg: "HS256", typ: "JWT" };
|
|
3320
|
+
const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
|
|
3321
|
+
const encodedPayload = this.base64UrlEncode(JSON.stringify(payload));
|
|
3322
|
+
const signature = this.hmacSha256(
|
|
3323
|
+
`${encodedHeader}.${encodedPayload}`,
|
|
3324
|
+
this.backendSecret
|
|
3325
|
+
);
|
|
3326
|
+
const encodedSignature = this.base64UrlEncode(signature);
|
|
3327
|
+
return `${encodedHeader}.${encodedPayload}.${encodedSignature}`;
|
|
613
3328
|
}
|
|
614
3329
|
/**
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
* @example
|
|
618
|
-
* ```typescript
|
|
619
|
-
* // Search news
|
|
620
|
-
* const articles = await rainfall.articles.search({
|
|
621
|
-
* query: 'artificial intelligence'
|
|
622
|
-
* });
|
|
623
|
-
*
|
|
624
|
-
* // Create from URL
|
|
625
|
-
* const article = await rainfall.articles.createFromUrl({
|
|
626
|
-
* url: 'https://example.com/article'
|
|
627
|
-
* });
|
|
628
|
-
*
|
|
629
|
-
* // Summarize
|
|
630
|
-
* const summary = await rainfall.articles.summarize({
|
|
631
|
-
* text: article.content
|
|
632
|
-
* });
|
|
633
|
-
* ```
|
|
3330
|
+
* Validate a JWT token
|
|
634
3331
|
*/
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
3332
|
+
validateJWT(token) {
|
|
3333
|
+
const parts = token.split(".");
|
|
3334
|
+
if (parts.length !== 3) {
|
|
3335
|
+
throw new Error("Invalid JWT format");
|
|
638
3336
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
* html,
|
|
658
|
-
* selector: 'a[href]'
|
|
659
|
-
* });
|
|
660
|
-
* ```
|
|
661
|
-
*/
|
|
662
|
-
get web() {
|
|
663
|
-
if (!this._web) {
|
|
664
|
-
this._web = createWeb(this.client);
|
|
3337
|
+
const [encodedHeader, encodedPayload, encodedSignature] = parts;
|
|
3338
|
+
if (this.backendSecret) {
|
|
3339
|
+
const expectedSignature = this.hmacSha256(
|
|
3340
|
+
`${encodedHeader}.${encodedPayload}`,
|
|
3341
|
+
this.backendSecret
|
|
3342
|
+
);
|
|
3343
|
+
const expectedEncoded = this.base64UrlEncode(expectedSignature);
|
|
3344
|
+
if (!this.timingSafeEqual(encodedSignature, expectedEncoded)) {
|
|
3345
|
+
throw new Error("Invalid JWT signature");
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
const payload = JSON.parse(this.base64UrlDecode(encodedPayload));
|
|
3349
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
3350
|
+
if (payload.exp < now) {
|
|
3351
|
+
throw new Error("JWT token expired");
|
|
3352
|
+
}
|
|
3353
|
+
if (payload.iss !== "rainfall-backend") {
|
|
3354
|
+
throw new Error("Invalid JWT issuer");
|
|
665
3355
|
}
|
|
666
|
-
return
|
|
3356
|
+
return {
|
|
3357
|
+
edgeNodeId: payload.sub,
|
|
3358
|
+
subscriberId: payload.sub,
|
|
3359
|
+
// Same as edge node ID for now
|
|
3360
|
+
scopes: payload.scope,
|
|
3361
|
+
expiresAt: payload.exp
|
|
3362
|
+
};
|
|
667
3363
|
}
|
|
668
3364
|
/**
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
* @example
|
|
672
|
-
* ```typescript
|
|
673
|
-
* // Generate embeddings
|
|
674
|
-
* const embedding = await rainfall.ai.embeddings.document({
|
|
675
|
-
* text: 'Hello world'
|
|
676
|
-
* });
|
|
677
|
-
*
|
|
678
|
-
* // Generate image
|
|
679
|
-
* const image = await rainfall.ai.image.generate({
|
|
680
|
-
* prompt: 'A serene mountain landscape'
|
|
681
|
-
* });
|
|
682
|
-
*
|
|
683
|
-
* // OCR
|
|
684
|
-
* const text = await rainfall.ai.ocr({ imageBase64: '...' });
|
|
685
|
-
*
|
|
686
|
-
* // Chat
|
|
687
|
-
* const response = await rainfall.ai.chat({
|
|
688
|
-
* messages: [{ role: 'user', content: 'Hello!' }]
|
|
689
|
-
* });
|
|
690
|
-
* ```
|
|
3365
|
+
* Extract bearer token from Authorization header
|
|
691
3366
|
*/
|
|
692
|
-
|
|
693
|
-
if (!
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
return this._ai;
|
|
3367
|
+
extractBearerToken(authHeader) {
|
|
3368
|
+
if (!authHeader) return null;
|
|
3369
|
+
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
3370
|
+
return match ? match[1] : null;
|
|
697
3371
|
}
|
|
3372
|
+
// ============================================================================
|
|
3373
|
+
// ACL Enforcement
|
|
3374
|
+
// ============================================================================
|
|
698
3375
|
/**
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
* @example
|
|
702
|
-
* ```typescript
|
|
703
|
-
* // Query CSV with SQL
|
|
704
|
-
* const results = await rainfall.data.csv.query({
|
|
705
|
-
* sql: 'SELECT * FROM data WHERE value > 100'
|
|
706
|
-
* });
|
|
707
|
-
*
|
|
708
|
-
* // Execute saved script
|
|
709
|
-
* const result = await rainfall.data.scripts.execute({
|
|
710
|
-
* name: 'my-script',
|
|
711
|
-
* params: { input: 'data' }
|
|
712
|
-
* });
|
|
713
|
-
* ```
|
|
3376
|
+
* Check if an edge node is allowed to perform an action on a job
|
|
3377
|
+
* Rule: Edge nodes can only access jobs for their own subscriber
|
|
714
3378
|
*/
|
|
715
|
-
|
|
716
|
-
if (
|
|
717
|
-
|
|
3379
|
+
checkACL(check) {
|
|
3380
|
+
if (check.subscriberId !== check.jobSubscriberId) {
|
|
3381
|
+
return {
|
|
3382
|
+
allowed: false,
|
|
3383
|
+
reason: `Edge node ${check.edgeNodeId} cannot access jobs from subscriber ${check.jobSubscriberId}`
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3386
|
+
const allowedActions = ["heartbeat", "claim", "submit", "queue"];
|
|
3387
|
+
if (!allowedActions.includes(check.action)) {
|
|
3388
|
+
return {
|
|
3389
|
+
allowed: false,
|
|
3390
|
+
reason: `Unknown action: ${check.action}`
|
|
3391
|
+
};
|
|
718
3392
|
}
|
|
719
|
-
return
|
|
3393
|
+
return { allowed: true };
|
|
720
3394
|
}
|
|
721
3395
|
/**
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
* @example
|
|
725
|
-
* ```typescript
|
|
726
|
-
* // Generate diagram
|
|
727
|
-
* const diagram = await rainfall.utils.mermaid({
|
|
728
|
-
* diagram: 'graph TD; A-->B;'
|
|
729
|
-
* });
|
|
730
|
-
*
|
|
731
|
-
* // Convert document
|
|
732
|
-
* const pdf = await rainfall.utils.documentConvert({
|
|
733
|
-
* document: markdownContent,
|
|
734
|
-
* mimeType: 'text/markdown',
|
|
735
|
-
* format: 'pdf'
|
|
736
|
-
* });
|
|
737
|
-
*
|
|
738
|
-
* // Extract JSON from text
|
|
739
|
-
* const json = await rainfall.utils.jsonExtract({
|
|
740
|
-
* text: 'Here is some data: {"key": "value"}'
|
|
741
|
-
* });
|
|
742
|
-
* ```
|
|
3396
|
+
* Middleware-style ACL check for job operations
|
|
743
3397
|
*/
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
3398
|
+
requireSameSubscriber(edgeNodeSubscriberId, jobSubscriberId, operation) {
|
|
3399
|
+
const result = this.checkACL({
|
|
3400
|
+
edgeNodeId: edgeNodeSubscriberId,
|
|
3401
|
+
subscriberId: edgeNodeSubscriberId,
|
|
3402
|
+
jobSubscriberId,
|
|
3403
|
+
action: operation
|
|
3404
|
+
});
|
|
3405
|
+
if (!result.allowed) {
|
|
3406
|
+
throw new Error(result.reason);
|
|
747
3407
|
}
|
|
748
|
-
return this._utils;
|
|
749
3408
|
}
|
|
3409
|
+
// ============================================================================
|
|
3410
|
+
// Encryption (Libsodium)
|
|
3411
|
+
// ============================================================================
|
|
750
3412
|
/**
|
|
751
|
-
*
|
|
3413
|
+
* Generate a new Ed25519 key pair for an edge node
|
|
752
3414
|
*/
|
|
753
|
-
|
|
754
|
-
|
|
3415
|
+
async generateKeyPair() {
|
|
3416
|
+
await this.sodiumReady;
|
|
3417
|
+
const keyPair = sodium.crypto_box_keypair();
|
|
3418
|
+
return {
|
|
3419
|
+
publicKey: this.bytesToBase64(keyPair.publicKey),
|
|
3420
|
+
privateKey: this.bytesToBase64(keyPair.privateKey)
|
|
3421
|
+
};
|
|
755
3422
|
}
|
|
756
3423
|
/**
|
|
757
|
-
*
|
|
3424
|
+
* Encrypt job parameters for a target edge node using its public key
|
|
758
3425
|
*/
|
|
759
|
-
async
|
|
760
|
-
|
|
3426
|
+
async encryptForEdgeNode(plaintext, targetPublicKeyBase64) {
|
|
3427
|
+
await this.sodiumReady;
|
|
3428
|
+
if (!this.keyPair) {
|
|
3429
|
+
throw new Error("Local key pair not configured");
|
|
3430
|
+
}
|
|
3431
|
+
const targetPublicKey = this.base64ToBytes(targetPublicKeyBase64);
|
|
3432
|
+
const ephemeralKeyPair = sodium.crypto_box_keypair();
|
|
3433
|
+
const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES);
|
|
3434
|
+
const message = new TextEncoder().encode(plaintext);
|
|
3435
|
+
const ciphertext = sodium.crypto_box_easy(
|
|
3436
|
+
message,
|
|
3437
|
+
nonce,
|
|
3438
|
+
targetPublicKey,
|
|
3439
|
+
ephemeralKeyPair.privateKey
|
|
3440
|
+
);
|
|
3441
|
+
return {
|
|
3442
|
+
ciphertext: this.bytesToBase64(ciphertext),
|
|
3443
|
+
nonce: this.bytesToBase64(nonce),
|
|
3444
|
+
ephemeralPublicKey: this.bytesToBase64(ephemeralKeyPair.publicKey)
|
|
3445
|
+
};
|
|
761
3446
|
}
|
|
762
3447
|
/**
|
|
763
|
-
*
|
|
3448
|
+
* Decrypt job parameters received from the backend
|
|
764
3449
|
*/
|
|
765
|
-
async
|
|
766
|
-
|
|
3450
|
+
async decryptFromBackend(encrypted) {
|
|
3451
|
+
await this.sodiumReady;
|
|
3452
|
+
if (!this.keyPair) {
|
|
3453
|
+
throw new Error("Local key pair not configured");
|
|
3454
|
+
}
|
|
3455
|
+
const privateKey = this.base64ToBytes(this.keyPair.privateKey);
|
|
3456
|
+
const ephemeralPublicKey = this.base64ToBytes(encrypted.ephemeralPublicKey);
|
|
3457
|
+
const nonce = this.base64ToBytes(encrypted.nonce);
|
|
3458
|
+
const ciphertext = this.base64ToBytes(encrypted.ciphertext);
|
|
3459
|
+
const decrypted = sodium.crypto_box_open_easy(
|
|
3460
|
+
ciphertext,
|
|
3461
|
+
nonce,
|
|
3462
|
+
ephemeralPublicKey,
|
|
3463
|
+
privateKey
|
|
3464
|
+
);
|
|
3465
|
+
if (!decrypted) {
|
|
3466
|
+
throw new Error("Decryption failed - invalid ciphertext or keys");
|
|
3467
|
+
}
|
|
3468
|
+
return new TextDecoder().decode(decrypted);
|
|
767
3469
|
}
|
|
768
3470
|
/**
|
|
769
|
-
*
|
|
3471
|
+
* Encrypt job parameters for local storage (using secretbox)
|
|
770
3472
|
*/
|
|
771
|
-
async
|
|
772
|
-
|
|
3473
|
+
async encryptLocal(plaintext, key) {
|
|
3474
|
+
await this.sodiumReady;
|
|
3475
|
+
const keyBytes = this.deriveKey(key);
|
|
3476
|
+
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
|
3477
|
+
const message = new TextEncoder().encode(plaintext);
|
|
3478
|
+
const ciphertext = sodium.crypto_secretbox_easy(message, nonce, keyBytes);
|
|
3479
|
+
return {
|
|
3480
|
+
ciphertext: this.bytesToBase64(ciphertext),
|
|
3481
|
+
nonce: this.bytesToBase64(nonce)
|
|
3482
|
+
};
|
|
773
3483
|
}
|
|
774
3484
|
/**
|
|
775
|
-
*
|
|
3485
|
+
* Decrypt locally stored job parameters
|
|
776
3486
|
*/
|
|
777
|
-
async
|
|
778
|
-
|
|
3487
|
+
async decryptLocal(encrypted, key) {
|
|
3488
|
+
await this.sodiumReady;
|
|
3489
|
+
const keyBytes = this.deriveKey(key);
|
|
3490
|
+
const nonce = this.base64ToBytes(encrypted.nonce);
|
|
3491
|
+
const ciphertext = this.base64ToBytes(encrypted.ciphertext);
|
|
3492
|
+
const decrypted = sodium.crypto_secretbox_open_easy(ciphertext, nonce, keyBytes);
|
|
3493
|
+
if (!decrypted) {
|
|
3494
|
+
throw new Error("Local decryption failed");
|
|
3495
|
+
}
|
|
3496
|
+
return new TextDecoder().decode(decrypted);
|
|
779
3497
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
return
|
|
3498
|
+
// ============================================================================
|
|
3499
|
+
// Utility Methods
|
|
3500
|
+
// ============================================================================
|
|
3501
|
+
generateTokenId() {
|
|
3502
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
785
3503
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
// src/cli/index.ts
|
|
789
|
-
var CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".rainfall");
|
|
790
|
-
var CONFIG_FILE = (0, import_path.join)(CONFIG_DIR, "config.json");
|
|
791
|
-
function loadConfig() {
|
|
792
|
-
if (!(0, import_fs.existsSync)(CONFIG_FILE)) {
|
|
793
|
-
return {};
|
|
3504
|
+
base64UrlEncode(str) {
|
|
3505
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
794
3506
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
return
|
|
3507
|
+
base64UrlDecode(str) {
|
|
3508
|
+
const padding = "=".repeat((4 - str.length % 4) % 4);
|
|
3509
|
+
const base64 = str.replace(/-/g, "+").replace(/_/g, "/") + padding;
|
|
3510
|
+
return atob(base64);
|
|
799
3511
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
3512
|
+
hmacSha256(message, secret) {
|
|
3513
|
+
const key = new TextEncoder().encode(secret);
|
|
3514
|
+
const msg = new TextEncoder().encode(message);
|
|
3515
|
+
const hash = sodium.crypto_auth(msg, key);
|
|
3516
|
+
return this.bytesToBase64(hash);
|
|
804
3517
|
}
|
|
805
|
-
(
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
3518
|
+
timingSafeEqual(a, b) {
|
|
3519
|
+
if (a.length !== b.length) return false;
|
|
3520
|
+
let result = 0;
|
|
3521
|
+
for (let i = 0; i < a.length; i++) {
|
|
3522
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
3523
|
+
}
|
|
3524
|
+
return result === 0;
|
|
812
3525
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
}
|
|
3526
|
+
bytesToBase64(bytes) {
|
|
3527
|
+
const binString = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
|
|
3528
|
+
return btoa(binString);
|
|
3529
|
+
}
|
|
3530
|
+
base64ToBytes(base64) {
|
|
3531
|
+
const binString = atob(base64);
|
|
3532
|
+
return Uint8Array.from(binString, (m) => m.charCodeAt(0));
|
|
3533
|
+
}
|
|
3534
|
+
deriveKey(password) {
|
|
3535
|
+
const passwordBytes = new TextEncoder().encode(password);
|
|
3536
|
+
return sodium.crypto_generichash(32, passwordBytes, null);
|
|
3537
|
+
}
|
|
3538
|
+
};
|
|
3539
|
+
async function createEdgeNodeSecurity(options = {}) {
|
|
3540
|
+
const security = new EdgeNodeSecurity(options);
|
|
3541
|
+
await security.initialize();
|
|
3542
|
+
return security;
|
|
817
3543
|
}
|
|
3544
|
+
|
|
3545
|
+
// src/cli/index.ts
|
|
818
3546
|
function printHelp() {
|
|
819
3547
|
console.log(`
|
|
820
3548
|
Rainfall CLI - 200+ tools, one key
|
|
@@ -832,28 +3560,72 @@ Commands:
|
|
|
832
3560
|
tools search <query> Search for tools
|
|
833
3561
|
|
|
834
3562
|
run <tool> [options] Execute a tool
|
|
3563
|
+
|
|
3564
|
+
daemon start Start the Rainfall daemon
|
|
3565
|
+
daemon stop Stop the Rainfall daemon
|
|
3566
|
+
daemon restart Restart the Rainfall daemon
|
|
3567
|
+
daemon status Check daemon status
|
|
3568
|
+
|
|
3569
|
+
workflow new Create a new workflow (interactive)
|
|
3570
|
+
workflow run <workflow> Run a saved workflow
|
|
835
3571
|
|
|
836
3572
|
me Show account info and usage
|
|
837
3573
|
|
|
838
3574
|
config get [key] Get configuration value
|
|
839
3575
|
config set <key> <value> Set configuration value
|
|
3576
|
+
config llm Show LLM configuration
|
|
3577
|
+
|
|
3578
|
+
edge generate-keys Generate key pair for edge node encryption
|
|
3579
|
+
edge status Show edge node security status
|
|
3580
|
+
|
|
3581
|
+
version Show version information
|
|
3582
|
+
upgrade Upgrade to the latest version
|
|
840
3583
|
|
|
841
3584
|
help Show this help message
|
|
842
3585
|
|
|
3586
|
+
Configuration keys:
|
|
3587
|
+
llm.provider LLM provider (rainfall|openai|anthropic|ollama|local)
|
|
3588
|
+
llm.baseUrl Base URL for the LLM API
|
|
3589
|
+
llm.apiKey API key for the LLM provider
|
|
3590
|
+
llm.model Default model to use
|
|
3591
|
+
|
|
843
3592
|
Options for 'run':
|
|
844
3593
|
--params, -p <json> Tool parameters as JSON
|
|
845
3594
|
--file, -f <path> Read parameters from file
|
|
846
3595
|
--raw Output raw JSON
|
|
3596
|
+
--<key> <value> Pass individual parameters (e.g., --query "AI news")
|
|
3597
|
+
|
|
3598
|
+
Options for 'daemon start':
|
|
3599
|
+
--port <port> WebSocket port (default: 8765)
|
|
3600
|
+
--openai-port <port> OpenAI API port (default: 8787)
|
|
3601
|
+
--mcp-proxy Enable MCP proxy hub (default: enabled)
|
|
3602
|
+
--no-mcp-proxy Disable MCP proxy hub
|
|
3603
|
+
--secure Enable edge node security (JWT, ACLs, encryption)
|
|
3604
|
+
--debug Enable verbose debug logging
|
|
847
3605
|
|
|
848
3606
|
Examples:
|
|
849
3607
|
rainfall auth login
|
|
850
3608
|
rainfall tools list
|
|
851
3609
|
rainfall tools describe github-create-issue
|
|
852
3610
|
rainfall run exa-web-search -p '{"query": "AI news"}'
|
|
3611
|
+
rainfall run exa-web-search --query "AI news"
|
|
3612
|
+
rainfall run github-create-issue --owner facebook --repo react --title "Bug"
|
|
853
3613
|
rainfall run article-summarize -f ./article.json
|
|
3614
|
+
rainfall daemon start
|
|
854
3615
|
echo '{"query": "hello"}' | rainfall run exa-web-search
|
|
855
3616
|
`);
|
|
856
3617
|
}
|
|
3618
|
+
function getRainfall() {
|
|
3619
|
+
const config = loadConfig();
|
|
3620
|
+
if (!config.apiKey) {
|
|
3621
|
+
console.error("Error: No API key configured. Run: rainfall auth login");
|
|
3622
|
+
process.exit(1);
|
|
3623
|
+
}
|
|
3624
|
+
return new Rainfall({
|
|
3625
|
+
apiKey: config.apiKey,
|
|
3626
|
+
baseUrl: config.baseUrl
|
|
3627
|
+
});
|
|
3628
|
+
}
|
|
857
3629
|
async function authLogin(args) {
|
|
858
3630
|
const apiKey = args[0] || process.env.RAINFALL_API_KEY;
|
|
859
3631
|
if (!apiKey) {
|
|
@@ -1018,16 +3790,20 @@ Options:
|
|
|
1018
3790
|
-p, --params <json> Tool parameters as JSON string
|
|
1019
3791
|
-f, --file <path> Read parameters from JSON file
|
|
1020
3792
|
--raw Output raw JSON (no formatting)
|
|
3793
|
+
--<key> <value> Pass individual parameters (e.g., --query "AI news")
|
|
1021
3794
|
|
|
1022
3795
|
Examples:
|
|
1023
3796
|
rainfall run figma-users-getMe
|
|
1024
3797
|
rainfall run exa-web-search -p '{"query": "AI news"}'
|
|
3798
|
+
rainfall run exa-web-search --query "AI news"
|
|
3799
|
+
rainfall run github-create-issue --owner facebook --repo react --title "Bug"
|
|
1025
3800
|
rainfall run github-create-issue -f ./issue.json
|
|
1026
3801
|
echo '{"query": "hello"}' | rainfall run exa-web-search
|
|
1027
3802
|
`);
|
|
1028
3803
|
return;
|
|
1029
3804
|
}
|
|
1030
3805
|
let params = {};
|
|
3806
|
+
const rawArgs = [];
|
|
1031
3807
|
for (let i = 1; i < args.length; i++) {
|
|
1032
3808
|
const arg = args[i];
|
|
1033
3809
|
if (arg === "--params" || arg === "-p") {
|
|
@@ -1049,12 +3825,26 @@ Examples:
|
|
|
1049
3825
|
process.exit(1);
|
|
1050
3826
|
}
|
|
1051
3827
|
try {
|
|
1052
|
-
params = JSON.parse((0,
|
|
3828
|
+
params = JSON.parse((0, import_fs2.readFileSync)(filePath, "utf8"));
|
|
1053
3829
|
} catch {
|
|
1054
3830
|
console.error(`Error: Could not read or parse file: ${filePath}`);
|
|
1055
3831
|
process.exit(1);
|
|
1056
3832
|
}
|
|
1057
3833
|
} else if (arg === "--raw") {
|
|
3834
|
+
} else if (arg.startsWith("--")) {
|
|
3835
|
+
const key = arg.slice(2);
|
|
3836
|
+
const value = args[++i];
|
|
3837
|
+
if (value === void 0) {
|
|
3838
|
+
console.error(`Error: ${arg} requires a value`);
|
|
3839
|
+
process.exit(1);
|
|
3840
|
+
}
|
|
3841
|
+
try {
|
|
3842
|
+
params[key] = JSON.parse(value);
|
|
3843
|
+
} catch {
|
|
3844
|
+
params[key] = value;
|
|
3845
|
+
}
|
|
3846
|
+
} else {
|
|
3847
|
+
rawArgs.push(arg);
|
|
1058
3848
|
}
|
|
1059
3849
|
}
|
|
1060
3850
|
if (!process.stdin.isTTY) {
|
|
@@ -1092,6 +3882,20 @@ Examples:
|
|
|
1092
3882
|
}
|
|
1093
3883
|
}
|
|
1094
3884
|
const rainfall = getRainfall();
|
|
3885
|
+
if (rawArgs.length === 1 && Object.keys(params).length === 0) {
|
|
3886
|
+
try {
|
|
3887
|
+
const schema = await rainfall.getToolSchema(toolId);
|
|
3888
|
+
if (schema.parameters && typeof schema.parameters === "object") {
|
|
3889
|
+
const paramEntries = Object.entries(schema.parameters);
|
|
3890
|
+
const requiredParams = paramEntries.filter(([, p]) => !p.optional);
|
|
3891
|
+
if (requiredParams.length === 1) {
|
|
3892
|
+
const [paramName] = requiredParams[0];
|
|
3893
|
+
params = { [paramName]: rawArgs[0] };
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
} catch {
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
1095
3899
|
try {
|
|
1096
3900
|
const result = await rainfall.executeTool(toolId, params);
|
|
1097
3901
|
if (args.includes("--raw")) {
|
|
@@ -1118,7 +3922,16 @@ function configGet(args) {
|
|
|
1118
3922
|
const key = args[0];
|
|
1119
3923
|
const config = loadConfig();
|
|
1120
3924
|
if (key) {
|
|
1121
|
-
|
|
3925
|
+
const parts = key.split(".");
|
|
3926
|
+
let value = config;
|
|
3927
|
+
for (const part of parts) {
|
|
3928
|
+
value = value?.[part];
|
|
3929
|
+
}
|
|
3930
|
+
if (typeof value === "object" && value !== null) {
|
|
3931
|
+
console.log(JSON.stringify(value, null, 2));
|
|
3932
|
+
} else {
|
|
3933
|
+
console.log(value ?? "");
|
|
3934
|
+
}
|
|
1122
3935
|
} else {
|
|
1123
3936
|
console.log(JSON.stringify(config, null, 2));
|
|
1124
3937
|
}
|
|
@@ -1129,13 +3942,306 @@ function configSet(args) {
|
|
|
1129
3942
|
if (!key || !value) {
|
|
1130
3943
|
console.error("Error: Both key and value required");
|
|
1131
3944
|
console.error("\nUsage: rainfall config set <key> <value>");
|
|
3945
|
+
console.error("\nExamples:");
|
|
3946
|
+
console.error(" rainfall config set llm.provider local");
|
|
3947
|
+
console.error(" rainfall config set llm.baseUrl http://localhost:1234/v1");
|
|
3948
|
+
console.error(" rainfall config set llm.model llama-3.3-70b-versatile");
|
|
1132
3949
|
process.exit(1);
|
|
1133
3950
|
}
|
|
1134
3951
|
const config = loadConfig();
|
|
1135
|
-
|
|
3952
|
+
const parts = key.split(".");
|
|
3953
|
+
if (parts.length === 1) {
|
|
3954
|
+
config[key] = value;
|
|
3955
|
+
} else {
|
|
3956
|
+
let target = config;
|
|
3957
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
3958
|
+
if (!target[parts[i]] || typeof target[parts[i]] !== "object") {
|
|
3959
|
+
target[parts[i]] = {};
|
|
3960
|
+
}
|
|
3961
|
+
target = target[parts[i]];
|
|
3962
|
+
}
|
|
3963
|
+
target[parts[parts.length - 1]] = value;
|
|
3964
|
+
}
|
|
1136
3965
|
saveConfig(config);
|
|
1137
3966
|
console.log(`\u2713 Set ${key} = ${value}`);
|
|
1138
3967
|
}
|
|
3968
|
+
function configLLM() {
|
|
3969
|
+
const config = loadConfig();
|
|
3970
|
+
const llm = config.llm || { provider: "rainfall" };
|
|
3971
|
+
console.log("LLM Configuration:");
|
|
3972
|
+
console.log(` Provider: ${llm.provider}`);
|
|
3973
|
+
console.log(` Base URL: ${llm.baseUrl || "(default)"}`);
|
|
3974
|
+
console.log(` Model: ${llm.model || "(default)"}`);
|
|
3975
|
+
console.log(` API Key: ${llm.apiKey ? "****" + llm.apiKey.slice(-4) : "(none)"}`);
|
|
3976
|
+
console.log();
|
|
3977
|
+
console.log("Providers:");
|
|
3978
|
+
console.log(" rainfall - Use Rainfall backend (default, uses your credits)");
|
|
3979
|
+
console.log(" openai - Use OpenAI API directly");
|
|
3980
|
+
console.log(" anthropic - Use Anthropic API directly");
|
|
3981
|
+
console.log(" ollama - Use local Ollama instance");
|
|
3982
|
+
console.log(" local - Use any OpenAI-compatible endpoint (LM Studio, etc.)");
|
|
3983
|
+
console.log(" custom - Use any custom OpenAI-compatible endpoint (RunPod, etc.)");
|
|
3984
|
+
console.log();
|
|
3985
|
+
console.log("Examples:");
|
|
3986
|
+
console.log(" rainfall config set llm.provider local");
|
|
3987
|
+
console.log(" rainfall config set llm.baseUrl http://localhost:1234/v1");
|
|
3988
|
+
console.log(" rainfall config set llm.provider custom");
|
|
3989
|
+
console.log(" rainfall config set llm.baseUrl https://your-runpod-endpoint.runpod.net/v1");
|
|
3990
|
+
console.log(" rainfall config set llm.provider openai");
|
|
3991
|
+
console.log(" rainfall config set llm.apiKey sk-...");
|
|
3992
|
+
}
|
|
3993
|
+
function getPackageJson() {
|
|
3994
|
+
try {
|
|
3995
|
+
const __filename2 = (0, import_url.fileURLToPath)(importMetaUrl);
|
|
3996
|
+
const __dirname = (0, import_path2.dirname)(__filename2);
|
|
3997
|
+
const packagePath = (0, import_path2.join)(__dirname, "..", "..", "package.json");
|
|
3998
|
+
const content = (0, import_fs2.readFileSync)(packagePath, "utf8");
|
|
3999
|
+
return JSON.parse(content);
|
|
4000
|
+
} catch {
|
|
4001
|
+
return { version: "unknown", name: "@rainfall-devkit/sdk" };
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
function showVersion() {
|
|
4005
|
+
const pkg = getPackageJson();
|
|
4006
|
+
console.log(`${pkg.name} v${pkg.version}`);
|
|
4007
|
+
}
|
|
4008
|
+
async function upgrade() {
|
|
4009
|
+
const pkg = getPackageJson();
|
|
4010
|
+
console.log(`Upgrading ${pkg.name}...`);
|
|
4011
|
+
const execPath = process.argv[0];
|
|
4012
|
+
const isBun = execPath.includes("bun");
|
|
4013
|
+
let command;
|
|
4014
|
+
let args;
|
|
4015
|
+
if (isBun) {
|
|
4016
|
+
command = "bun";
|
|
4017
|
+
args = ["add", "-g", `${pkg.name}@latest`];
|
|
4018
|
+
} else {
|
|
4019
|
+
command = "npm";
|
|
4020
|
+
args = ["i", "-g", `${pkg.name}@latest`];
|
|
4021
|
+
}
|
|
4022
|
+
console.log(`Running: ${command} ${args.join(" ")}`);
|
|
4023
|
+
console.log();
|
|
4024
|
+
return new Promise((resolve, reject) => {
|
|
4025
|
+
const child = (0, import_child_process.spawn)(command, args, {
|
|
4026
|
+
stdio: "inherit",
|
|
4027
|
+
shell: true
|
|
4028
|
+
});
|
|
4029
|
+
child.on("close", (code) => {
|
|
4030
|
+
if (code === 0) {
|
|
4031
|
+
console.log();
|
|
4032
|
+
console.log("\u2713 Upgrade complete");
|
|
4033
|
+
resolve();
|
|
4034
|
+
} else {
|
|
4035
|
+
reject(new Error(`Upgrade failed with exit code ${code}`));
|
|
4036
|
+
}
|
|
4037
|
+
});
|
|
4038
|
+
child.on("error", (err) => {
|
|
4039
|
+
reject(err);
|
|
4040
|
+
});
|
|
4041
|
+
});
|
|
4042
|
+
}
|
|
4043
|
+
async function daemonStart(args) {
|
|
4044
|
+
let port;
|
|
4045
|
+
let openaiPort;
|
|
4046
|
+
let debug = false;
|
|
4047
|
+
let enableMcpProxy = true;
|
|
4048
|
+
for (let i = 0; i < args.length; i++) {
|
|
4049
|
+
const arg = args[i];
|
|
4050
|
+
if (arg === "--port") {
|
|
4051
|
+
const val = parseInt(args[++i], 10);
|
|
4052
|
+
if (!isNaN(val)) port = val;
|
|
4053
|
+
} else if (arg === "--openai-port") {
|
|
4054
|
+
const val = parseInt(args[++i], 10);
|
|
4055
|
+
if (!isNaN(val)) openaiPort = val;
|
|
4056
|
+
} else if (arg === "--debug") {
|
|
4057
|
+
debug = true;
|
|
4058
|
+
} else if (arg === "--mcp-proxy") {
|
|
4059
|
+
enableMcpProxy = true;
|
|
4060
|
+
} else if (arg === "--no-mcp-proxy") {
|
|
4061
|
+
enableMcpProxy = false;
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
const { startDaemon: startDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
4065
|
+
try {
|
|
4066
|
+
await startDaemon2({ port, openaiPort, debug, enableMcpProxy });
|
|
4067
|
+
process.on("SIGINT", async () => {
|
|
4068
|
+
console.log("\n");
|
|
4069
|
+
const { stopDaemon: stopDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
4070
|
+
await stopDaemon2();
|
|
4071
|
+
process.exit(0);
|
|
4072
|
+
});
|
|
4073
|
+
process.on("SIGTERM", async () => {
|
|
4074
|
+
const { stopDaemon: stopDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
4075
|
+
await stopDaemon2();
|
|
4076
|
+
process.exit(0);
|
|
4077
|
+
});
|
|
4078
|
+
} catch (error) {
|
|
4079
|
+
console.error("Failed to start daemon:", error instanceof Error ? error.message : error);
|
|
4080
|
+
process.exit(1);
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
async function daemonStop() {
|
|
4084
|
+
const { stopDaemon: stopDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
4085
|
+
await stopDaemon2();
|
|
4086
|
+
}
|
|
4087
|
+
async function daemonRestart(args) {
|
|
4088
|
+
const { stopDaemon: stopDaemon2, startDaemon: startDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
4089
|
+
let port;
|
|
4090
|
+
let openaiPort;
|
|
4091
|
+
let debug = false;
|
|
4092
|
+
for (let i = 0; i < args.length; i++) {
|
|
4093
|
+
const arg = args[i];
|
|
4094
|
+
if (arg === "--port") {
|
|
4095
|
+
const val = parseInt(args[++i], 10);
|
|
4096
|
+
if (!isNaN(val)) port = val;
|
|
4097
|
+
} else if (arg === "--openai-port") {
|
|
4098
|
+
const val = parseInt(args[++i], 10);
|
|
4099
|
+
if (!isNaN(val)) openaiPort = val;
|
|
4100
|
+
} else if (arg === "--debug") {
|
|
4101
|
+
debug = true;
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
console.log("\u{1F504} Restarting daemon...");
|
|
4105
|
+
try {
|
|
4106
|
+
await stopDaemon2();
|
|
4107
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
4108
|
+
await startDaemon2({ port, openaiPort, debug });
|
|
4109
|
+
process.on("SIGINT", async () => {
|
|
4110
|
+
console.log("\n");
|
|
4111
|
+
const { stopDaemon: stop } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
4112
|
+
await stop();
|
|
4113
|
+
process.exit(0);
|
|
4114
|
+
});
|
|
4115
|
+
process.on("SIGTERM", async () => {
|
|
4116
|
+
const { stopDaemon: stop } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
4117
|
+
await stop();
|
|
4118
|
+
process.exit(0);
|
|
4119
|
+
});
|
|
4120
|
+
} catch (error) {
|
|
4121
|
+
console.error("Failed to restart daemon:", error instanceof Error ? error.message : error);
|
|
4122
|
+
process.exit(1);
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
async function daemonStatus() {
|
|
4126
|
+
const { getDaemonStatus: getDaemonStatus2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
4127
|
+
const status = getDaemonStatus2();
|
|
4128
|
+
if (!status) {
|
|
4129
|
+
console.log("Daemon not running");
|
|
4130
|
+
console.log("Run: rainfall daemon start");
|
|
4131
|
+
return;
|
|
4132
|
+
}
|
|
4133
|
+
console.log("Daemon status:");
|
|
4134
|
+
console.log(` Running: ${status.running ? "yes" : "no"}`);
|
|
4135
|
+
console.log(` WebSocket port: ${status.port}`);
|
|
4136
|
+
console.log(` OpenAI API port: ${status.openaiPort}`);
|
|
4137
|
+
console.log(` Tools loaded: ${status.toolsLoaded}`);
|
|
4138
|
+
console.log(` MCP clients: ${status.mcpClients || 0}`);
|
|
4139
|
+
console.log(` MCP tools: ${status.mcpTools || 0}`);
|
|
4140
|
+
console.log(` Clients connected: ${status.clientsConnected}`);
|
|
4141
|
+
console.log(` Edge Node ID: ${status.edgeNodeId || "local"}`);
|
|
4142
|
+
console.log();
|
|
4143
|
+
console.log("Context:");
|
|
4144
|
+
console.log(` Memories cached: ${status.context.memoriesCached}`);
|
|
4145
|
+
console.log(` Active sessions: ${status.context.activeSessions}`);
|
|
4146
|
+
console.log(` Current session: ${status.context.currentSession || "none"}`);
|
|
4147
|
+
console.log(` Execution history: ${status.context.executionHistorySize}`);
|
|
4148
|
+
console.log();
|
|
4149
|
+
console.log("Listeners:");
|
|
4150
|
+
console.log(` File watchers: ${status.listeners.fileWatchers}`);
|
|
4151
|
+
console.log(` Cron triggers: ${status.listeners.cronTriggers}`);
|
|
4152
|
+
console.log(` Recent events: ${status.listeners.recentEvents}`);
|
|
4153
|
+
}
|
|
4154
|
+
async function workflowNew() {
|
|
4155
|
+
console.log("\u{1F6A7} Interactive workflow creation coming soon!");
|
|
4156
|
+
console.log();
|
|
4157
|
+
console.log("For now, create workflows using the SDK:");
|
|
4158
|
+
console.log(' import { createFileWatcherWorkflow } from "@rainfall-devkit/sdk/daemon";');
|
|
4159
|
+
console.log();
|
|
4160
|
+
console.log("Example:");
|
|
4161
|
+
console.log(` const workflow = createFileWatcherWorkflow('pdf-processor', '~/Downloads', {`);
|
|
4162
|
+
console.log(` pattern: '*.pdf',`);
|
|
4163
|
+
console.log(` events: ['create'],`);
|
|
4164
|
+
console.log(` workflow: [`);
|
|
4165
|
+
console.log(` { toolId: 'ocr-pdf', params: {} },`);
|
|
4166
|
+
console.log(` { toolId: 'notion-create-page', params: { parent: '...' } },`);
|
|
4167
|
+
console.log(` ],`);
|
|
4168
|
+
console.log(` });`);
|
|
4169
|
+
}
|
|
4170
|
+
async function workflowRun(args) {
|
|
4171
|
+
const workflowId = args[0];
|
|
4172
|
+
if (!workflowId) {
|
|
4173
|
+
console.error("Error: Workflow ID required");
|
|
4174
|
+
console.error("\nUsage: rainfall workflow run <workflow-id>");
|
|
4175
|
+
process.exit(1);
|
|
4176
|
+
}
|
|
4177
|
+
console.log(`\u{1F6A7} Running workflow: ${workflowId}`);
|
|
4178
|
+
console.log("Workflow execution coming soon!");
|
|
4179
|
+
}
|
|
4180
|
+
async function edgeGenerateKeys() {
|
|
4181
|
+
console.log("\u{1F510} Generating edge node key pair...\n");
|
|
4182
|
+
try {
|
|
4183
|
+
const security = await createEdgeNodeSecurity();
|
|
4184
|
+
const keyPair = await security.generateKeyPair();
|
|
4185
|
+
const configDir = getConfigDir();
|
|
4186
|
+
const keysDir = (0, import_path2.join)(configDir, "keys");
|
|
4187
|
+
if (!(0, import_fs2.existsSync)(keysDir)) {
|
|
4188
|
+
(0, import_fs2.mkdirSync)(keysDir, { recursive: true });
|
|
4189
|
+
}
|
|
4190
|
+
const publicKeyPath = (0, import_path2.join)(keysDir, "edge-node.pub");
|
|
4191
|
+
const privateKeyPath = (0, import_path2.join)(keysDir, "edge-node.key");
|
|
4192
|
+
(0, import_fs2.writeFileSync)(publicKeyPath, keyPair.publicKey, { mode: 420 });
|
|
4193
|
+
(0, import_fs2.writeFileSync)(privateKeyPath, keyPair.privateKey, { mode: 384 });
|
|
4194
|
+
console.log("\u2705 Key pair generated successfully!\n");
|
|
4195
|
+
console.log("Public key:", keyPair.publicKey);
|
|
4196
|
+
console.log("\nKey files saved to:");
|
|
4197
|
+
console.log(" Public:", publicKeyPath);
|
|
4198
|
+
console.log(" Private:", privateKeyPath);
|
|
4199
|
+
console.log("\n\u{1F4CB} To register this edge node:");
|
|
4200
|
+
console.log(" 1. Copy the public key above");
|
|
4201
|
+
console.log(" 2. Register with: rainfall edge register <public-key>");
|
|
4202
|
+
console.log(" 3. The backend will return an edgeNodeSecret (JWT)");
|
|
4203
|
+
console.log(" 4. Store the secret securely - it expires in 30 days");
|
|
4204
|
+
} catch (error) {
|
|
4205
|
+
console.error("\u274C Failed to generate keys:", error instanceof Error ? error.message : error);
|
|
4206
|
+
process.exit(1);
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
async function edgeStatus() {
|
|
4210
|
+
const configDir = getConfigDir();
|
|
4211
|
+
const keysDir = (0, import_path2.join)(configDir, "keys");
|
|
4212
|
+
const publicKeyPath = (0, import_path2.join)(keysDir, "edge-node.pub");
|
|
4213
|
+
const privateKeyPath = (0, import_path2.join)(keysDir, "edge-node.key");
|
|
4214
|
+
console.log("\u{1F510} Edge Node Security Status\n");
|
|
4215
|
+
const hasPublicKey = (0, import_fs2.existsSync)(publicKeyPath);
|
|
4216
|
+
const hasPrivateKey = (0, import_fs2.existsSync)(privateKeyPath);
|
|
4217
|
+
console.log("Key Pair:");
|
|
4218
|
+
console.log(" Public key:", hasPublicKey ? "\u2705 Present" : "\u274C Missing");
|
|
4219
|
+
console.log(" Private key:", hasPrivateKey ? "\u2705 Present" : "\u274C Missing");
|
|
4220
|
+
if (hasPublicKey) {
|
|
4221
|
+
const publicKey = (0, import_fs2.readFileSync)(publicKeyPath, "utf-8");
|
|
4222
|
+
console.log("\nPublic Key:");
|
|
4223
|
+
console.log(" " + publicKey.substring(0, 50) + "...");
|
|
4224
|
+
}
|
|
4225
|
+
const config = loadConfig();
|
|
4226
|
+
if (config.edgeNodeId) {
|
|
4227
|
+
console.log("\nRegistration:");
|
|
4228
|
+
console.log(" Edge Node ID:", config.edgeNodeId);
|
|
4229
|
+
}
|
|
4230
|
+
if (config.edgeNodeSecret) {
|
|
4231
|
+
console.log(" JWT Secret: \u2705 Present (expires: check with backend)");
|
|
4232
|
+
} else {
|
|
4233
|
+
console.log(" JWT Secret: \u274C Not configured");
|
|
4234
|
+
}
|
|
4235
|
+
console.log("\n\u{1F4DA} Next steps:");
|
|
4236
|
+
if (!hasPublicKey) {
|
|
4237
|
+
console.log(" 1. Run: rainfall edge generate-keys");
|
|
4238
|
+
} else if (!config.edgeNodeSecret) {
|
|
4239
|
+
console.log(" 1. Register your edge node with the backend");
|
|
4240
|
+
console.log(" 2. Store the returned edgeNodeSecret in config");
|
|
4241
|
+
} else {
|
|
4242
|
+
console.log(" Edge node is configured and ready for secure operation");
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
1139
4245
|
async function main() {
|
|
1140
4246
|
const args = process.argv.slice(2);
|
|
1141
4247
|
const command = args[0];
|
|
@@ -1179,6 +4285,40 @@ async function main() {
|
|
|
1179
4285
|
case "run":
|
|
1180
4286
|
await runTool(args.slice(1));
|
|
1181
4287
|
break;
|
|
4288
|
+
case "daemon":
|
|
4289
|
+
switch (subcommand) {
|
|
4290
|
+
case "start":
|
|
4291
|
+
await daemonStart(rest);
|
|
4292
|
+
break;
|
|
4293
|
+
case "stop":
|
|
4294
|
+
await daemonStop();
|
|
4295
|
+
break;
|
|
4296
|
+
case "restart":
|
|
4297
|
+
await daemonRestart(rest);
|
|
4298
|
+
break;
|
|
4299
|
+
case "status":
|
|
4300
|
+
await daemonStatus();
|
|
4301
|
+
break;
|
|
4302
|
+
default:
|
|
4303
|
+
console.error("Error: Unknown daemon subcommand");
|
|
4304
|
+
console.error("\nUsage: rainfall daemon <start|stop|restart|status>");
|
|
4305
|
+
process.exit(1);
|
|
4306
|
+
}
|
|
4307
|
+
break;
|
|
4308
|
+
case "workflow":
|
|
4309
|
+
switch (subcommand) {
|
|
4310
|
+
case "new":
|
|
4311
|
+
await workflowNew();
|
|
4312
|
+
break;
|
|
4313
|
+
case "run":
|
|
4314
|
+
await workflowRun(rest);
|
|
4315
|
+
break;
|
|
4316
|
+
default:
|
|
4317
|
+
console.error("Error: Unknown workflow subcommand");
|
|
4318
|
+
console.error("\nUsage: rainfall workflow <new|run>");
|
|
4319
|
+
process.exit(1);
|
|
4320
|
+
}
|
|
4321
|
+
break;
|
|
1182
4322
|
case "me":
|
|
1183
4323
|
await showMe();
|
|
1184
4324
|
break;
|
|
@@ -1190,9 +4330,34 @@ async function main() {
|
|
|
1190
4330
|
case "set":
|
|
1191
4331
|
configSet(rest);
|
|
1192
4332
|
break;
|
|
4333
|
+
case "llm":
|
|
4334
|
+
configLLM();
|
|
4335
|
+
break;
|
|
1193
4336
|
default:
|
|
1194
4337
|
console.error("Error: Unknown config subcommand");
|
|
1195
|
-
console.error("\nUsage: rainfall config <get|set>");
|
|
4338
|
+
console.error("\nUsage: rainfall config <get|set|llm>");
|
|
4339
|
+
process.exit(1);
|
|
4340
|
+
}
|
|
4341
|
+
break;
|
|
4342
|
+
case "version":
|
|
4343
|
+
case "--version":
|
|
4344
|
+
case "-v":
|
|
4345
|
+
showVersion();
|
|
4346
|
+
break;
|
|
4347
|
+
case "upgrade":
|
|
4348
|
+
await upgrade();
|
|
4349
|
+
break;
|
|
4350
|
+
case "edge":
|
|
4351
|
+
switch (subcommand) {
|
|
4352
|
+
case "generate-keys":
|
|
4353
|
+
await edgeGenerateKeys();
|
|
4354
|
+
break;
|
|
4355
|
+
case "status":
|
|
4356
|
+
await edgeStatus();
|
|
4357
|
+
break;
|
|
4358
|
+
default:
|
|
4359
|
+
console.error("Error: Unknown edge subcommand");
|
|
4360
|
+
console.error("\nUsage: rainfall edge <generate-keys|status>");
|
|
1196
4361
|
process.exit(1);
|
|
1197
4362
|
}
|
|
1198
4363
|
break;
|