roboport 0.0.1 → 0.2.0
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 +3 -2
- package/core/agent.d.ts +2 -1
- package/gateways/core.d.ts +27 -0
- package/gateways/index.d.ts +5 -0
- package/gateways/index.js +575 -0
- package/gateways/serve.d.ts +19 -0
- package/gateways/sources/telegram.d.ts +28 -0
- package/gateways/store.d.ts +9 -0
- package/harness/index.js +15 -6
- package/index.js +15 -6
- package/mcp/auth.d.ts +2 -0
- package/mcp/index.d.ts +4 -1
- package/mcp/index.js +814 -795
- package/models/index.js +15 -6
- package/package.json +9 -5
- package/skills/index.js +15 -6
- package/triggers/core.d.ts +1 -1
- package/triggers/index.js +10 -2
- package/triggers/sources/telegram.d.ts +9 -1
package/mcp/index.js
CHANGED
|
@@ -1,878 +1,889 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// src/
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
while (this.waiters.length > 0) {
|
|
37
|
-
const waiter = this.waiters.shift();
|
|
38
|
-
waiter?.({ value: undefined, done: true });
|
|
2
|
+
// src/mcp/oauth.ts
|
|
3
|
+
function base64UrlEncode(bytes) {
|
|
4
|
+
let str = "";
|
|
5
|
+
for (const byte of bytes)
|
|
6
|
+
str += String.fromCharCode(byte);
|
|
7
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
8
|
+
}
|
|
9
|
+
function randomBytes(length) {
|
|
10
|
+
const bytes = new Uint8Array(length);
|
|
11
|
+
crypto.getRandomValues(bytes);
|
|
12
|
+
return bytes;
|
|
13
|
+
}
|
|
14
|
+
async function sha256(input) {
|
|
15
|
+
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
16
|
+
return new Uint8Array(hash);
|
|
17
|
+
}
|
|
18
|
+
async function generatePkce() {
|
|
19
|
+
const verifier = base64UrlEncode(randomBytes(32));
|
|
20
|
+
const challenge = base64UrlEncode(await sha256(verifier));
|
|
21
|
+
return { verifier, challenge };
|
|
22
|
+
}
|
|
23
|
+
function generateState() {
|
|
24
|
+
return base64UrlEncode(randomBytes(16));
|
|
25
|
+
}
|
|
26
|
+
async function discover(serverUrl) {
|
|
27
|
+
const u = new URL(serverUrl);
|
|
28
|
+
const origin = `${u.protocol}//${u.host}`;
|
|
29
|
+
let authServer = origin;
|
|
30
|
+
try {
|
|
31
|
+
const res2 = await fetch(`${origin}/.well-known/oauth-protected-resource`);
|
|
32
|
+
if (res2.ok) {
|
|
33
|
+
const meta = await res2.json();
|
|
34
|
+
if (meta.authorization_servers?.[0])
|
|
35
|
+
authServer = meta.authorization_servers[0];
|
|
39
36
|
}
|
|
37
|
+
} catch {}
|
|
38
|
+
const asUrl = new URL(authServer);
|
|
39
|
+
const asOrigin = `${asUrl.protocol}//${asUrl.host}`;
|
|
40
|
+
const res = await fetch(`${asOrigin}/.well-known/oauth-authorization-server`);
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
throw new Error(`OAuth discovery failed for ${asOrigin}: ${res.status} ${await res.text()}`);
|
|
40
43
|
}
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
return await res.json();
|
|
45
|
+
}
|
|
46
|
+
async function registerClient(registrationEndpoint, redirectUri) {
|
|
47
|
+
const res = await fetch(registrationEndpoint, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: { "content-type": "application/json" },
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
client_name: "roboport",
|
|
52
|
+
redirect_uris: [redirectUri],
|
|
53
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
54
|
+
response_types: ["code"],
|
|
55
|
+
token_endpoint_auth_method: "none"
|
|
56
|
+
})
|
|
57
|
+
});
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
throw new Error(`OAuth client registration failed: ${res.status} ${await res.text()}`);
|
|
43
60
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
const data = await res.json();
|
|
62
|
+
return data.client_id;
|
|
63
|
+
}
|
|
64
|
+
function openBrowser(url) {
|
|
65
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
66
|
+
Bun.spawn([cmd, url], { stdout: "ignore", stderr: "ignore" });
|
|
67
|
+
}
|
|
68
|
+
function captureAuthorizationCode(port, expectedState, timeoutMs) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const server = Bun.serve({
|
|
71
|
+
port,
|
|
72
|
+
hostname: "127.0.0.1",
|
|
73
|
+
fetch(req) {
|
|
74
|
+
const u = new URL(req.url);
|
|
75
|
+
const error = u.searchParams.get("error");
|
|
76
|
+
if (error) {
|
|
77
|
+
const desc = u.searchParams.get("error_description") ?? "";
|
|
78
|
+
finish(() => reject(new Error(`OAuth error: ${error} ${desc}`)));
|
|
79
|
+
return new Response("Authentication failed. You can close this tab.", {
|
|
80
|
+
status: 400
|
|
59
81
|
});
|
|
60
82
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
};
|
|
83
|
+
const code = u.searchParams.get("code");
|
|
84
|
+
const state = u.searchParams.get("state");
|
|
85
|
+
if (!code || state !== expectedState) {
|
|
86
|
+
finish(() => reject(new Error("OAuth state mismatch or missing code.")));
|
|
87
|
+
return new Response("Invalid OAuth response.", { status: 400 });
|
|
88
|
+
}
|
|
89
|
+
finish(() => resolve(code));
|
|
90
|
+
return new Response("<html><body>Authenticated. You can close this tab.</body></html>", { headers: { "content-type": "text/html" } });
|
|
70
91
|
}
|
|
71
|
-
};
|
|
92
|
+
});
|
|
93
|
+
const timer = setTimeout(() => {
|
|
94
|
+
finish(() => reject(new Error("OAuth flow timed out.")));
|
|
95
|
+
}, timeoutMs);
|
|
96
|
+
function finish(action) {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
setTimeout(() => server.stop(true), 50);
|
|
99
|
+
action();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async function exchangeCode(opts) {
|
|
104
|
+
const body = new URLSearchParams({
|
|
105
|
+
grant_type: "authorization_code",
|
|
106
|
+
code: opts.code,
|
|
107
|
+
redirect_uri: opts.redirectUri,
|
|
108
|
+
client_id: opts.clientId,
|
|
109
|
+
code_verifier: opts.verifier
|
|
110
|
+
});
|
|
111
|
+
const res = await fetch(opts.tokenEndpoint, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
114
|
+
body
|
|
115
|
+
});
|
|
116
|
+
if (!res.ok) {
|
|
117
|
+
throw new Error(`OAuth token exchange failed: ${res.status} ${await res.text()}`);
|
|
72
118
|
}
|
|
73
|
-
|
|
74
|
-
|
|
119
|
+
return await res.json();
|
|
120
|
+
}
|
|
121
|
+
async function refreshTokens(opts) {
|
|
122
|
+
const body = new URLSearchParams({
|
|
123
|
+
grant_type: "refresh_token",
|
|
124
|
+
refresh_token: opts.refreshToken,
|
|
125
|
+
client_id: opts.clientId
|
|
126
|
+
});
|
|
127
|
+
const res = await fetch(opts.tokenEndpoint, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
130
|
+
body
|
|
131
|
+
});
|
|
132
|
+
if (!res.ok) {
|
|
133
|
+
throw new Error(`OAuth refresh failed: ${res.status} ${await res.text()}`);
|
|
75
134
|
}
|
|
135
|
+
return await res.json();
|
|
76
136
|
}
|
|
77
137
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
138
|
+
// src/mcp/storage.ts
|
|
139
|
+
import { chmod, mkdir, readFile, writeFile } from "fs/promises";
|
|
140
|
+
import { homedir } from "os";
|
|
141
|
+
import { dirname, join } from "path";
|
|
142
|
+
var DEFAULT_PATH = join(homedir(), ".roboport", "mcp-auth.json");
|
|
143
|
+
|
|
144
|
+
class FileStorage {
|
|
145
|
+
path;
|
|
146
|
+
cache;
|
|
147
|
+
constructor(path) {
|
|
148
|
+
this.path = path ?? DEFAULT_PATH;
|
|
84
149
|
}
|
|
85
|
-
|
|
86
|
-
|
|
150
|
+
async read() {
|
|
151
|
+
if (this.cache)
|
|
152
|
+
return this.cache;
|
|
153
|
+
try {
|
|
154
|
+
const raw = await readFile(this.path, "utf8");
|
|
155
|
+
this.cache = JSON.parse(raw);
|
|
156
|
+
} catch {
|
|
157
|
+
this.cache = {};
|
|
158
|
+
}
|
|
159
|
+
return this.cache;
|
|
87
160
|
}
|
|
88
|
-
|
|
89
|
-
|
|
161
|
+
async write(data) {
|
|
162
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
163
|
+
await writeFile(this.path, JSON.stringify(data, null, 2), "utf8");
|
|
164
|
+
await chmod(this.path, 384);
|
|
165
|
+
this.cache = data;
|
|
90
166
|
}
|
|
91
|
-
async
|
|
92
|
-
await this.
|
|
167
|
+
async load(key) {
|
|
168
|
+
const data = await this.read();
|
|
169
|
+
return data[key] ?? null;
|
|
93
170
|
}
|
|
94
|
-
async
|
|
95
|
-
await this.
|
|
171
|
+
async save(key, tokens) {
|
|
172
|
+
const data = await this.read();
|
|
173
|
+
data[key] = tokens;
|
|
174
|
+
await this.write(data);
|
|
175
|
+
}
|
|
176
|
+
async clear(key) {
|
|
177
|
+
const data = await this.read();
|
|
178
|
+
delete data[key];
|
|
179
|
+
await this.write(data);
|
|
96
180
|
}
|
|
97
181
|
}
|
|
98
182
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
class Tool {
|
|
106
|
-
name;
|
|
107
|
-
description;
|
|
108
|
-
inputSchema;
|
|
109
|
-
jsonSchema;
|
|
110
|
-
execute;
|
|
111
|
-
deferred;
|
|
112
|
-
constructor(init) {
|
|
113
|
-
this.name = init.name;
|
|
114
|
-
this.description = init.description;
|
|
115
|
-
this.deferred = init.deferred ?? false;
|
|
116
|
-
if ("inputSchema" in init) {
|
|
117
|
-
this.inputSchema = init.inputSchema;
|
|
118
|
-
this.execute = init.execute;
|
|
119
|
-
} else {
|
|
120
|
-
this.jsonSchema = init.jsonSchema;
|
|
121
|
-
this.execute = init.execute;
|
|
122
|
-
}
|
|
183
|
+
class MemoryStorage {
|
|
184
|
+
data = new Map;
|
|
185
|
+
async load(key) {
|
|
186
|
+
return this.data.get(key) ?? null;
|
|
123
187
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return this.jsonSchema;
|
|
127
|
-
if (this.inputSchema === undefined) {
|
|
128
|
-
throw new Error(`Tool "${this.name}" has neither inputSchema nor jsonSchema.`);
|
|
129
|
-
}
|
|
130
|
-
return z4.toJSONSchema(this.inputSchema);
|
|
188
|
+
async save(key, tokens) {
|
|
189
|
+
this.data.set(key, tokens);
|
|
131
190
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (!schema)
|
|
135
|
-
return input;
|
|
136
|
-
if (hasParseMethod(schema))
|
|
137
|
-
return schema.parse(input);
|
|
138
|
-
return z4.parse(schema, input);
|
|
191
|
+
async clear(key) {
|
|
192
|
+
this.data.delete(key);
|
|
139
193
|
}
|
|
140
194
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const tool = byName.get(name);
|
|
152
|
-
if (!tool) {
|
|
153
|
-
missing.push(name);
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
loadedNames.add(name);
|
|
157
|
-
loaded.push(tool);
|
|
158
|
-
}
|
|
159
|
-
return { loaded, missing };
|
|
160
|
-
}
|
|
161
|
-
};
|
|
195
|
+
|
|
196
|
+
// src/mcp/auth.ts
|
|
197
|
+
class BearerAuth {
|
|
198
|
+
token;
|
|
199
|
+
constructor(token) {
|
|
200
|
+
this.token = token;
|
|
201
|
+
}
|
|
202
|
+
async getHeader() {
|
|
203
|
+
return `Bearer ${this.token}`;
|
|
204
|
+
}
|
|
162
205
|
}
|
|
206
|
+
var DEFAULT_PORT = 33418;
|
|
207
|
+
var DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
163
208
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
this.
|
|
183
|
-
this.
|
|
184
|
-
this.tools = tools;
|
|
185
|
-
this.skills = skills;
|
|
186
|
-
this.mcp = mcp ?? [];
|
|
187
|
-
this.cwd = cwd;
|
|
209
|
+
class OAuthAuth {
|
|
210
|
+
serverUrl;
|
|
211
|
+
storage;
|
|
212
|
+
storageKey;
|
|
213
|
+
redirectPort;
|
|
214
|
+
scopes;
|
|
215
|
+
flowTimeoutMs;
|
|
216
|
+
clientId;
|
|
217
|
+
tokens;
|
|
218
|
+
loaded = false;
|
|
219
|
+
metadata;
|
|
220
|
+
inFlight;
|
|
221
|
+
constructor(opts) {
|
|
222
|
+
this.serverUrl = opts.serverUrl;
|
|
223
|
+
this.storage = opts.storage ?? new FileStorage;
|
|
224
|
+
this.storageKey = opts.storageKey;
|
|
225
|
+
this.redirectPort = opts.redirectPort ?? DEFAULT_PORT;
|
|
226
|
+
this.scopes = opts.scopes;
|
|
227
|
+
this.flowTimeoutMs = opts.flowTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
228
|
+
this.clientId = opts.clientId;
|
|
188
229
|
}
|
|
189
|
-
|
|
190
|
-
this.
|
|
230
|
+
async getHeader() {
|
|
231
|
+
await this.ensureTokens();
|
|
232
|
+
if (!this.tokens)
|
|
233
|
+
throw new Error("OAuth: no tokens after auth flow.");
|
|
234
|
+
return `Bearer ${this.tokens.accessToken}`;
|
|
191
235
|
}
|
|
192
|
-
async
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
this.unsubs.push(unsub);
|
|
236
|
+
async onUnauthorized() {
|
|
237
|
+
if (this.tokens?.refreshToken) {
|
|
238
|
+
try {
|
|
239
|
+
await this.refresh();
|
|
240
|
+
return;
|
|
241
|
+
} catch {}
|
|
201
242
|
}
|
|
243
|
+
this.tokens = undefined;
|
|
244
|
+
await this.storage.clear(this.storageKey);
|
|
245
|
+
await this.authorize();
|
|
202
246
|
}
|
|
203
|
-
async
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
buildSystem(allTools) {
|
|
209
|
-
let system = this.system;
|
|
210
|
-
if (this.skills.length > 0) {
|
|
211
|
-
const skillsList = this.skills.map((skill) => `- ${skill.name}: ${skill.description}`).join(`
|
|
212
|
-
`);
|
|
213
|
-
system = `${system}
|
|
214
|
-
|
|
215
|
-
# Skills
|
|
216
|
-
The following skills are available. When a task matches one, call the \`Skill\` tool with that skill's name to load its full content before proceeding.
|
|
217
|
-
|
|
218
|
-
${skillsList}`;
|
|
247
|
+
async ensureTokens() {
|
|
248
|
+
if (this.inFlight) {
|
|
249
|
+
await this.inFlight;
|
|
250
|
+
return;
|
|
219
251
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
252
|
+
this.inFlight = (async () => {
|
|
253
|
+
if (!this.loaded) {
|
|
254
|
+
this.tokens = await this.storage.load(this.storageKey) ?? undefined;
|
|
255
|
+
this.loaded = true;
|
|
256
|
+
}
|
|
257
|
+
if (!this.tokens) {
|
|
258
|
+
await this.authorize();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (this.isExpired(this.tokens)) {
|
|
262
|
+
if (this.tokens.refreshToken) {
|
|
263
|
+
try {
|
|
264
|
+
await this.refresh();
|
|
265
|
+
return;
|
|
266
|
+
} catch {}
|
|
267
|
+
}
|
|
268
|
+
await this.authorize();
|
|
269
|
+
}
|
|
270
|
+
})();
|
|
271
|
+
try {
|
|
272
|
+
await this.inFlight;
|
|
273
|
+
} finally {
|
|
274
|
+
this.inFlight = undefined;
|
|
229
275
|
}
|
|
230
|
-
return system;
|
|
231
276
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
277
|
+
isExpired(tokens) {
|
|
278
|
+
if (!tokens.expiresAt)
|
|
279
|
+
return false;
|
|
280
|
+
return Date.now() / 1000 >= tokens.expiresAt - 30;
|
|
281
|
+
}
|
|
282
|
+
async getMetadata() {
|
|
283
|
+
if (!this.metadata)
|
|
284
|
+
this.metadata = await discover(this.serverUrl);
|
|
285
|
+
return this.metadata;
|
|
286
|
+
}
|
|
287
|
+
async authorize() {
|
|
288
|
+
const meta = await this.getMetadata();
|
|
289
|
+
const redirectUri = this.tokens?.redirectUri ?? `http://127.0.0.1:${this.redirectPort}/callback`;
|
|
290
|
+
let clientId = this.tokens?.clientId ?? this.clientId;
|
|
291
|
+
if (!clientId) {
|
|
292
|
+
if (!meta.registration_endpoint) {
|
|
293
|
+
throw new Error("OAuth server does not support dynamic client registration; pass a clientId.");
|
|
249
294
|
}
|
|
295
|
+
clientId = await registerClient(meta.registration_endpoint, redirectUri);
|
|
296
|
+
}
|
|
297
|
+
const { verifier, challenge } = await generatePkce();
|
|
298
|
+
const state = generateState();
|
|
299
|
+
const port = parseInt(new URL(redirectUri).port, 10);
|
|
300
|
+
const codePromise = captureAuthorizationCode(port, state, this.flowTimeoutMs);
|
|
301
|
+
const authUrl = new URL(meta.authorization_endpoint);
|
|
302
|
+
authUrl.searchParams.set("response_type", "code");
|
|
303
|
+
authUrl.searchParams.set("client_id", clientId);
|
|
304
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
305
|
+
authUrl.searchParams.set("code_challenge", challenge);
|
|
306
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
307
|
+
authUrl.searchParams.set("state", state);
|
|
308
|
+
if (this.scopes?.length) {
|
|
309
|
+
authUrl.searchParams.set("scope", this.scopes.join(" "));
|
|
310
|
+
}
|
|
311
|
+
console.error(`[mcp:oauth] Opening browser for ${this.storageKey} authentication...`);
|
|
312
|
+
openBrowser(authUrl.toString());
|
|
313
|
+
const code = await codePromise;
|
|
314
|
+
const response = await exchangeCode({
|
|
315
|
+
tokenEndpoint: meta.token_endpoint,
|
|
316
|
+
code,
|
|
317
|
+
clientId,
|
|
318
|
+
redirectUri,
|
|
319
|
+
verifier
|
|
250
320
|
});
|
|
321
|
+
this.tokens = this.toTokenSet(response, clientId, redirectUri);
|
|
322
|
+
await this.storage.save(this.storageKey, this.tokens);
|
|
251
323
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const state = {
|
|
256
|
-
messages: initialMessages,
|
|
257
|
-
store: new Map
|
|
258
|
-
};
|
|
259
|
-
let activeTurn = null;
|
|
260
|
-
let mcpConnected = false;
|
|
261
|
-
let allTools = null;
|
|
262
|
-
let registry = null;
|
|
263
|
-
let ctx = null;
|
|
264
|
-
const ensureReady = async () => {
|
|
265
|
-
if (!allTools || !registry || !ctx) {
|
|
266
|
-
const mcpToolGroups = await Promise.all(this.mcp.map((mcp) => mcp.connect()));
|
|
267
|
-
mcpConnected = true;
|
|
268
|
-
allTools = [
|
|
269
|
-
...this.tools,
|
|
270
|
-
...mcpToolGroups.flat(),
|
|
271
|
-
...this.skills.length > 0 ? [this.buildSkillTool()] : []
|
|
272
|
-
];
|
|
273
|
-
registry = createRegistry(allTools);
|
|
274
|
-
ctx = {
|
|
275
|
-
complete: async (p) => {
|
|
276
|
-
const response = await this.model.createMessage({
|
|
277
|
-
messages: [{ role: "user", content: p }]
|
|
278
|
-
});
|
|
279
|
-
return response.content.filter((block) => block.type === "text").map((block) => block.text).join(`
|
|
280
|
-
`);
|
|
281
|
-
},
|
|
282
|
-
searchWeb: (query, opts) => this.model.searchWeb(query, opts),
|
|
283
|
-
session: state,
|
|
284
|
-
tools: registry,
|
|
285
|
-
cwd: sessionCwd
|
|
286
|
-
};
|
|
287
|
-
if (state.messages.length === 0 || state.messages[0]?.role !== "system") {
|
|
288
|
-
state.messages.unshift({
|
|
289
|
-
role: "system",
|
|
290
|
-
content: this.buildSystem(allTools)
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
return { tools: allTools, registry, ctx };
|
|
295
|
-
};
|
|
296
|
-
const internals = {
|
|
297
|
-
send: (prompt) => {
|
|
298
|
-
if (activeTurn !== null) {
|
|
299
|
-
throw new Error("Session.send() called while another turn is in flight.");
|
|
300
|
-
}
|
|
301
|
-
const turn = new Turn(async (turnCtx) => {
|
|
302
|
-
try {
|
|
303
|
-
const ready = await ensureReady();
|
|
304
|
-
state.messages.push(toUserMessage(prompt));
|
|
305
|
-
await runAgentLoop({
|
|
306
|
-
model: this.model,
|
|
307
|
-
state,
|
|
308
|
-
registry: ready.registry,
|
|
309
|
-
ctx: ready.ctx,
|
|
310
|
-
emit: turnCtx.emit,
|
|
311
|
-
signal: turnCtx.signal
|
|
312
|
-
});
|
|
313
|
-
return [...state.messages];
|
|
314
|
-
} finally {
|
|
315
|
-
activeTurn = null;
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
activeTurn = turn;
|
|
319
|
-
return turn;
|
|
320
|
-
},
|
|
321
|
-
close: async () => {
|
|
322
|
-
const pending = activeTurn;
|
|
323
|
-
if (pending) {
|
|
324
|
-
pending.abort("session closed");
|
|
325
|
-
await Promise.resolve(pending).catch(() => {});
|
|
326
|
-
}
|
|
327
|
-
if (mcpConnected) {
|
|
328
|
-
await Promise.all(this.mcp.map((mcp) => mcp.disconnect()));
|
|
329
|
-
mcpConnected = false;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
|
-
return new Session(internals, state);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
function toUserMessage(prompt) {
|
|
337
|
-
if (typeof prompt === "string")
|
|
338
|
-
return { role: "user", content: prompt };
|
|
339
|
-
return { role: "user", content: prompt };
|
|
340
|
-
}
|
|
341
|
-
async function runAgentLoop({
|
|
342
|
-
model,
|
|
343
|
-
state,
|
|
344
|
-
registry,
|
|
345
|
-
ctx,
|
|
346
|
-
emit,
|
|
347
|
-
signal
|
|
348
|
-
}) {
|
|
349
|
-
while (true) {
|
|
350
|
-
if (signal.aborted)
|
|
351
|
-
break;
|
|
352
|
-
const active = registry.loaded();
|
|
353
|
-
const toolByName = new Map(active.map((tool) => [tool.name, tool]));
|
|
354
|
-
emit({ type: "message-start" });
|
|
355
|
-
const assistantContent = [];
|
|
356
|
-
let stopReason = "end_turn";
|
|
357
|
-
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
358
|
-
try {
|
|
359
|
-
for await (const event of model.streamMessage({
|
|
360
|
-
messages: state.messages,
|
|
361
|
-
tools: active,
|
|
362
|
-
signal
|
|
363
|
-
})) {
|
|
364
|
-
switch (event.type) {
|
|
365
|
-
case "text-delta":
|
|
366
|
-
emit({ type: "text-delta", text: event.text });
|
|
367
|
-
break;
|
|
368
|
-
case "text-end":
|
|
369
|
-
assistantContent.push({ type: "text", text: event.text });
|
|
370
|
-
emit({ type: "text", text: event.text });
|
|
371
|
-
break;
|
|
372
|
-
case "thinking-delta":
|
|
373
|
-
emit({ type: "thinking-delta", text: event.text });
|
|
374
|
-
break;
|
|
375
|
-
case "thinking-end":
|
|
376
|
-
assistantContent.push({
|
|
377
|
-
type: "thinking",
|
|
378
|
-
text: event.text,
|
|
379
|
-
...event.signature !== undefined ? { signature: event.signature } : {},
|
|
380
|
-
...event.redactedData !== undefined ? { redactedData: event.redactedData } : {}
|
|
381
|
-
});
|
|
382
|
-
emit({
|
|
383
|
-
type: "thinking",
|
|
384
|
-
text: event.text,
|
|
385
|
-
...event.signature !== undefined ? { signature: event.signature } : {}
|
|
386
|
-
});
|
|
387
|
-
break;
|
|
388
|
-
case "tool-call":
|
|
389
|
-
assistantContent.push({
|
|
390
|
-
type: "tool-call",
|
|
391
|
-
toolCallId: event.toolCallId,
|
|
392
|
-
toolName: event.toolName,
|
|
393
|
-
input: event.input
|
|
394
|
-
});
|
|
395
|
-
emit({
|
|
396
|
-
type: "tool-call",
|
|
397
|
-
toolCallId: event.toolCallId,
|
|
398
|
-
toolName: event.toolName,
|
|
399
|
-
input: event.input
|
|
400
|
-
});
|
|
401
|
-
break;
|
|
402
|
-
case "message-end":
|
|
403
|
-
stopReason = event.stopReason;
|
|
404
|
-
usage = event.usage;
|
|
405
|
-
break;
|
|
406
|
-
default:
|
|
407
|
-
break;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
} catch (error) {
|
|
411
|
-
if (signal.aborted) {
|
|
412
|
-
state.messages.push({ role: "assistant", content: assistantContent });
|
|
413
|
-
break;
|
|
414
|
-
}
|
|
415
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
416
|
-
emit({ type: "error", error: err });
|
|
417
|
-
throw err;
|
|
418
|
-
}
|
|
419
|
-
state.messages.push({ role: "assistant", content: assistantContent });
|
|
420
|
-
emit({ type: "message-end", usage });
|
|
421
|
-
if (stopReason !== "tool_use") {
|
|
422
|
-
emit({ type: "turn-end" });
|
|
423
|
-
break;
|
|
424
|
-
}
|
|
425
|
-
const toolCalls = assistantContent.filter((block) => block.type === "tool-call");
|
|
426
|
-
const results = [];
|
|
427
|
-
for (const call of toolCalls) {
|
|
428
|
-
if (signal.aborted)
|
|
429
|
-
break;
|
|
430
|
-
const tool = toolByName.get(call.toolName);
|
|
431
|
-
const result = await runTool(tool, call, ctx);
|
|
432
|
-
results.push(result);
|
|
433
|
-
emit({
|
|
434
|
-
type: "tool-result",
|
|
435
|
-
toolCallId: result.toolCallId,
|
|
436
|
-
toolName: result.toolName,
|
|
437
|
-
output: result.output,
|
|
438
|
-
isError: typeof result.output === "string" ? result.output.startsWith("Error:") : false
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
state.messages.push({ role: "tool", content: results });
|
|
442
|
-
if (signal.aborted)
|
|
443
|
-
break;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
async function runTool(tool, call, ctx) {
|
|
447
|
-
if (!tool) {
|
|
448
|
-
return {
|
|
449
|
-
type: "tool-result",
|
|
450
|
-
toolCallId: call.toolCallId,
|
|
451
|
-
toolName: call.toolName,
|
|
452
|
-
output: `Error: tool "${call.toolName}" not found`
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
try {
|
|
456
|
-
const parsed = tool.parse(call.input);
|
|
457
|
-
const output = await tool.execute(parsed, ctx);
|
|
458
|
-
return {
|
|
459
|
-
type: "tool-result",
|
|
460
|
-
toolCallId: call.toolCallId,
|
|
461
|
-
toolName: call.toolName,
|
|
462
|
-
output
|
|
463
|
-
};
|
|
464
|
-
} catch (error) {
|
|
465
|
-
return {
|
|
466
|
-
type: "tool-result",
|
|
467
|
-
toolCallId: call.toolCallId,
|
|
468
|
-
toolName: call.toolName,
|
|
469
|
-
output: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// src/core/model.ts
|
|
475
|
-
class Model {
|
|
476
|
-
async createMessage(params) {
|
|
477
|
-
const content = [];
|
|
478
|
-
let id = "";
|
|
479
|
-
let stopReason = "end_turn";
|
|
480
|
-
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
481
|
-
for await (const event of this.streamMessage(params)) {
|
|
482
|
-
switch (event.type) {
|
|
483
|
-
case "text-end":
|
|
484
|
-
content.push({ type: "text", text: event.text });
|
|
485
|
-
break;
|
|
486
|
-
case "thinking-end":
|
|
487
|
-
content.push({
|
|
488
|
-
type: "thinking",
|
|
489
|
-
text: event.text,
|
|
490
|
-
...event.signature !== undefined ? { signature: event.signature } : {},
|
|
491
|
-
...event.redactedData !== undefined ? { redactedData: event.redactedData } : {}
|
|
492
|
-
});
|
|
493
|
-
break;
|
|
494
|
-
case "tool-call":
|
|
495
|
-
content.push({
|
|
496
|
-
type: "tool-call",
|
|
497
|
-
toolCallId: event.toolCallId,
|
|
498
|
-
toolName: event.toolName,
|
|
499
|
-
input: event.input
|
|
500
|
-
});
|
|
501
|
-
break;
|
|
502
|
-
case "message-end":
|
|
503
|
-
id = event.id;
|
|
504
|
-
stopReason = event.stopReason;
|
|
505
|
-
usage = event.usage;
|
|
506
|
-
break;
|
|
507
|
-
default:
|
|
508
|
-
break;
|
|
509
|
-
}
|
|
324
|
+
async refresh() {
|
|
325
|
+
if (!this.tokens?.refreshToken || !this.tokens.clientId) {
|
|
326
|
+
throw new Error("OAuth refresh: missing refresh token or client id.");
|
|
510
327
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
328
|
+
const meta = await this.getMetadata();
|
|
329
|
+
const response = await refreshTokens({
|
|
330
|
+
tokenEndpoint: meta.token_endpoint,
|
|
331
|
+
refreshToken: this.tokens.refreshToken,
|
|
332
|
+
clientId: this.tokens.clientId
|
|
333
|
+
});
|
|
334
|
+
this.tokens = this.toTokenSet(response, this.tokens.clientId, this.tokens.redirectUri, this.tokens.refreshToken);
|
|
335
|
+
await this.storage.save(this.storageKey, this.tokens);
|
|
336
|
+
}
|
|
337
|
+
toTokenSet(response, clientId, redirectUri, fallbackRefresh) {
|
|
338
|
+
const expiresAt = response.expires_in ? Math.floor(Date.now() / 1000) + response.expires_in : undefined;
|
|
339
|
+
return {
|
|
340
|
+
accessToken: response.access_token,
|
|
341
|
+
refreshToken: response.refresh_token ?? fallbackRefresh,
|
|
342
|
+
expiresAt,
|
|
343
|
+
clientId,
|
|
344
|
+
redirectUri
|
|
345
|
+
};
|
|
528
346
|
}
|
|
529
347
|
}
|
|
530
|
-
|
|
531
|
-
// src/
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
if (res2.ok) {
|
|
562
|
-
const meta = await res2.json();
|
|
563
|
-
if (meta.authorization_servers?.[0])
|
|
564
|
-
authServer = meta.authorization_servers[0];
|
|
348
|
+
|
|
349
|
+
// src/core/agent.ts
|
|
350
|
+
import { z } from "zod";
|
|
351
|
+
|
|
352
|
+
// src/core/session.ts
|
|
353
|
+
class Turn {
|
|
354
|
+
queue = [];
|
|
355
|
+
waiters = [];
|
|
356
|
+
ended = false;
|
|
357
|
+
iterated = false;
|
|
358
|
+
resultPromise;
|
|
359
|
+
abortController = new AbortController;
|
|
360
|
+
constructor(runner) {
|
|
361
|
+
this.resultPromise = runner({
|
|
362
|
+
emit: (event) => this.emit(event),
|
|
363
|
+
signal: this.abortController.signal
|
|
364
|
+
}).then((messages) => {
|
|
365
|
+
this.close();
|
|
366
|
+
return messages;
|
|
367
|
+
}, (error) => {
|
|
368
|
+
this.close();
|
|
369
|
+
throw error;
|
|
370
|
+
});
|
|
371
|
+
this.resultPromise.catch(() => {});
|
|
372
|
+
}
|
|
373
|
+
emit(event) {
|
|
374
|
+
const waiter = this.waiters.shift();
|
|
375
|
+
if (waiter) {
|
|
376
|
+
waiter({ value: event, done: false });
|
|
377
|
+
} else {
|
|
378
|
+
this.queue.push(event);
|
|
565
379
|
}
|
|
566
|
-
} catch {}
|
|
567
|
-
const asUrl = new URL(authServer);
|
|
568
|
-
const asOrigin = `${asUrl.protocol}//${asUrl.host}`;
|
|
569
|
-
const res = await fetch(`${asOrigin}/.well-known/oauth-authorization-server`);
|
|
570
|
-
if (!res.ok) {
|
|
571
|
-
throw new Error(`OAuth discovery failed for ${asOrigin}: ${res.status} ${await res.text()}`);
|
|
572
380
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
body: JSON.stringify({
|
|
580
|
-
client_name: "roboport",
|
|
581
|
-
redirect_uris: [redirectUri],
|
|
582
|
-
grant_types: ["authorization_code", "refresh_token"],
|
|
583
|
-
response_types: ["code"],
|
|
584
|
-
token_endpoint_auth_method: "none"
|
|
585
|
-
})
|
|
586
|
-
});
|
|
587
|
-
if (!res.ok) {
|
|
588
|
-
throw new Error(`OAuth client registration failed: ${res.status} ${await res.text()}`);
|
|
381
|
+
close() {
|
|
382
|
+
this.ended = true;
|
|
383
|
+
while (this.waiters.length > 0) {
|
|
384
|
+
const waiter = this.waiters.shift();
|
|
385
|
+
waiter?.({ value: undefined, done: true });
|
|
386
|
+
}
|
|
589
387
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const u = new URL(req.url);
|
|
604
|
-
const error = u.searchParams.get("error");
|
|
605
|
-
if (error) {
|
|
606
|
-
const desc = u.searchParams.get("error_description") ?? "";
|
|
607
|
-
finish(() => reject(new Error(`OAuth error: ${error} ${desc}`)));
|
|
608
|
-
return new Response("Authentication failed. You can close this tab.", {
|
|
609
|
-
status: 400
|
|
610
|
-
});
|
|
388
|
+
abort(reason) {
|
|
389
|
+
this.abortController.abort(reason);
|
|
390
|
+
}
|
|
391
|
+
[Symbol.asyncIterator]() {
|
|
392
|
+
if (this.iterated) {
|
|
393
|
+
throw new Error("Turn can only be iterated once.");
|
|
394
|
+
}
|
|
395
|
+
this.iterated = true;
|
|
396
|
+
return {
|
|
397
|
+
next: () => {
|
|
398
|
+
if (this.queue.length > 0) {
|
|
399
|
+
const value = this.queue.shift();
|
|
400
|
+
return Promise.resolve({ value, done: false });
|
|
611
401
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
402
|
+
if (this.ended) {
|
|
403
|
+
return Promise.resolve({
|
|
404
|
+
value: undefined,
|
|
405
|
+
done: true
|
|
406
|
+
});
|
|
617
407
|
}
|
|
618
|
-
|
|
619
|
-
|
|
408
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
409
|
+
},
|
|
410
|
+
return: async () => {
|
|
411
|
+
this.abort("iteration ended");
|
|
412
|
+
await this.resultPromise.catch(() => {});
|
|
413
|
+
return {
|
|
414
|
+
value: undefined,
|
|
415
|
+
done: true
|
|
416
|
+
};
|
|
620
417
|
}
|
|
621
|
-
}
|
|
622
|
-
const timer = setTimeout(() => {
|
|
623
|
-
finish(() => reject(new Error("OAuth flow timed out.")));
|
|
624
|
-
}, timeoutMs);
|
|
625
|
-
function finish(action) {
|
|
626
|
-
clearTimeout(timer);
|
|
627
|
-
setTimeout(() => server.stop(true), 50);
|
|
628
|
-
action();
|
|
629
|
-
}
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
async function exchangeCode(opts) {
|
|
633
|
-
const body = new URLSearchParams({
|
|
634
|
-
grant_type: "authorization_code",
|
|
635
|
-
code: opts.code,
|
|
636
|
-
redirect_uri: opts.redirectUri,
|
|
637
|
-
client_id: opts.clientId,
|
|
638
|
-
code_verifier: opts.verifier
|
|
639
|
-
});
|
|
640
|
-
const res = await fetch(opts.tokenEndpoint, {
|
|
641
|
-
method: "POST",
|
|
642
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
643
|
-
body
|
|
644
|
-
});
|
|
645
|
-
if (!res.ok) {
|
|
646
|
-
throw new Error(`OAuth token exchange failed: ${res.status} ${await res.text()}`);
|
|
418
|
+
};
|
|
647
419
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
async function refreshTokens(opts) {
|
|
651
|
-
const body = new URLSearchParams({
|
|
652
|
-
grant_type: "refresh_token",
|
|
653
|
-
refresh_token: opts.refreshToken,
|
|
654
|
-
client_id: opts.clientId
|
|
655
|
-
});
|
|
656
|
-
const res = await fetch(opts.tokenEndpoint, {
|
|
657
|
-
method: "POST",
|
|
658
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
659
|
-
body
|
|
660
|
-
});
|
|
661
|
-
if (!res.ok) {
|
|
662
|
-
throw new Error(`OAuth refresh failed: ${res.status} ${await res.text()}`);
|
|
420
|
+
then(onfulfilled, onrejected) {
|
|
421
|
+
return this.resultPromise.then(onfulfilled, onrejected);
|
|
663
422
|
}
|
|
664
|
-
return await res.json();
|
|
665
423
|
}
|
|
666
424
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
class FileStorage {
|
|
674
|
-
path;
|
|
675
|
-
cache;
|
|
676
|
-
constructor(path) {
|
|
677
|
-
this.path = path ?? DEFAULT_PATH;
|
|
678
|
-
}
|
|
679
|
-
async read() {
|
|
680
|
-
if (this.cache)
|
|
681
|
-
return this.cache;
|
|
682
|
-
try {
|
|
683
|
-
const raw = await readFile(this.path, "utf8");
|
|
684
|
-
this.cache = JSON.parse(raw);
|
|
685
|
-
} catch {
|
|
686
|
-
this.cache = {};
|
|
687
|
-
}
|
|
688
|
-
return this.cache;
|
|
425
|
+
class Session {
|
|
426
|
+
internals;
|
|
427
|
+
state;
|
|
428
|
+
constructor(internals, state) {
|
|
429
|
+
this.internals = internals;
|
|
430
|
+
this.state = state;
|
|
689
431
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
await writeFile(this.path, JSON.stringify(data, null, 2), "utf8");
|
|
693
|
-
await chmod(this.path, 384);
|
|
694
|
-
this.cache = data;
|
|
432
|
+
get messages() {
|
|
433
|
+
return this.state.messages;
|
|
695
434
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
return data[key] ?? null;
|
|
435
|
+
send(prompt) {
|
|
436
|
+
return this.internals.send(prompt);
|
|
699
437
|
}
|
|
700
|
-
async
|
|
701
|
-
|
|
702
|
-
data[key] = tokens;
|
|
703
|
-
await this.write(data);
|
|
438
|
+
async close() {
|
|
439
|
+
await this.internals.close();
|
|
704
440
|
}
|
|
705
|
-
async
|
|
706
|
-
|
|
707
|
-
delete data[key];
|
|
708
|
-
await this.write(data);
|
|
441
|
+
async[Symbol.asyncDispose]() {
|
|
442
|
+
await this.close();
|
|
709
443
|
}
|
|
710
444
|
}
|
|
711
445
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
446
|
+
// src/core/tool.ts
|
|
447
|
+
import * as z4 from "zod/v4/core";
|
|
448
|
+
function hasParseMethod(schema) {
|
|
449
|
+
return typeof schema === "object" && schema !== null && "parse" in schema && typeof schema.parse === "function";
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
class Tool {
|
|
453
|
+
name;
|
|
454
|
+
description;
|
|
455
|
+
inputSchema;
|
|
456
|
+
jsonSchema;
|
|
457
|
+
execute;
|
|
458
|
+
deferred;
|
|
459
|
+
constructor(init) {
|
|
460
|
+
this.name = init.name;
|
|
461
|
+
this.description = init.description;
|
|
462
|
+
this.deferred = init.deferred ?? false;
|
|
463
|
+
if ("inputSchema" in init) {
|
|
464
|
+
this.inputSchema = init.inputSchema;
|
|
465
|
+
this.execute = init.execute;
|
|
466
|
+
} else {
|
|
467
|
+
this.jsonSchema = init.jsonSchema;
|
|
468
|
+
this.execute = init.execute;
|
|
469
|
+
}
|
|
716
470
|
}
|
|
717
|
-
|
|
718
|
-
this.
|
|
471
|
+
toJsonSchema() {
|
|
472
|
+
if (this.jsonSchema !== undefined)
|
|
473
|
+
return this.jsonSchema;
|
|
474
|
+
if (this.inputSchema === undefined) {
|
|
475
|
+
throw new Error(`Tool "${this.name}" has neither inputSchema nor jsonSchema.`);
|
|
476
|
+
}
|
|
477
|
+
return z4.toJSONSchema(this.inputSchema);
|
|
719
478
|
}
|
|
720
|
-
|
|
721
|
-
this.
|
|
479
|
+
parse(input) {
|
|
480
|
+
const schema = this.inputSchema;
|
|
481
|
+
if (!schema)
|
|
482
|
+
return input;
|
|
483
|
+
if (hasParseMethod(schema))
|
|
484
|
+
return schema.parse(input);
|
|
485
|
+
return z4.parse(schema, input);
|
|
722
486
|
}
|
|
723
487
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
488
|
+
function createRegistry(tools) {
|
|
489
|
+
const byName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
490
|
+
const loadedNames = new Set(tools.filter((tool) => !tool.deferred).map((tool) => tool.name));
|
|
491
|
+
return {
|
|
492
|
+
loaded: () => tools.filter((tool) => loadedNames.has(tool.name)),
|
|
493
|
+
deferred: () => tools.filter((tool) => tool.deferred && !loadedNames.has(tool.name)),
|
|
494
|
+
load: (names) => {
|
|
495
|
+
const loaded = [];
|
|
496
|
+
const missing = [];
|
|
497
|
+
for (const name of names) {
|
|
498
|
+
const tool = byName.get(name);
|
|
499
|
+
if (!tool) {
|
|
500
|
+
missing.push(name);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
loadedNames.add(name);
|
|
504
|
+
loaded.push(tool);
|
|
505
|
+
}
|
|
506
|
+
return { loaded, missing };
|
|
507
|
+
}
|
|
508
|
+
};
|
|
734
509
|
}
|
|
735
|
-
var DEFAULT_PORT = 33418;
|
|
736
|
-
var DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
737
510
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
511
|
+
// src/core/agent.ts
|
|
512
|
+
class Agent {
|
|
513
|
+
model;
|
|
514
|
+
system;
|
|
515
|
+
tools;
|
|
516
|
+
skills;
|
|
517
|
+
mcp;
|
|
518
|
+
cwd;
|
|
519
|
+
registrations = [];
|
|
520
|
+
unsubs = [];
|
|
521
|
+
constructor({
|
|
522
|
+
model,
|
|
523
|
+
system,
|
|
524
|
+
tools,
|
|
525
|
+
skills,
|
|
526
|
+
mcp,
|
|
527
|
+
cwd
|
|
528
|
+
}) {
|
|
529
|
+
this.model = model;
|
|
530
|
+
this.system = system;
|
|
531
|
+
this.tools = tools;
|
|
532
|
+
this.skills = skills;
|
|
533
|
+
this.mcp = mcp ?? [];
|
|
534
|
+
this.cwd = cwd;
|
|
756
535
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
if (!this.tokens)
|
|
760
|
-
throw new Error("OAuth: no tokens after auth flow.");
|
|
761
|
-
return `Bearer ${this.tokens.accessToken}`;
|
|
536
|
+
on(trigger, handler) {
|
|
537
|
+
this.registrations.push({ trigger, handler });
|
|
762
538
|
}
|
|
763
|
-
async
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
539
|
+
async start() {
|
|
540
|
+
for (const { trigger, handler } of this.registrations) {
|
|
541
|
+
const unsub = await trigger.start((event) => {
|
|
542
|
+
Promise.resolve().then(() => handler(event)).catch((error) => {
|
|
543
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
544
|
+
console.error(`[roboport] trigger "${trigger.name}" handler failed: ${message}`);
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
this.unsubs.push(unsub);
|
|
769
548
|
}
|
|
770
|
-
this.tokens = undefined;
|
|
771
|
-
await this.storage.clear(this.storageKey);
|
|
772
|
-
await this.authorize();
|
|
773
549
|
}
|
|
774
|
-
async
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
550
|
+
async stop() {
|
|
551
|
+
const unsubs = this.unsubs;
|
|
552
|
+
this.unsubs = [];
|
|
553
|
+
await Promise.all(unsubs.map((u) => u()));
|
|
554
|
+
}
|
|
555
|
+
buildSystem(allTools, systemExtension) {
|
|
556
|
+
let system = this.system;
|
|
557
|
+
if (this.skills.length > 0) {
|
|
558
|
+
const skillsList = this.skills.map((skill) => `- ${skill.name}: ${skill.description}`).join(`
|
|
559
|
+
`);
|
|
560
|
+
system = `${system}
|
|
561
|
+
|
|
562
|
+
# Skills
|
|
563
|
+
The following skills are available. When a task matches one, call the \`Skill\` tool with that skill's name to load its full content before proceeding.
|
|
564
|
+
|
|
565
|
+
${skillsList}`;
|
|
778
566
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
567
|
+
const deferred = allTools.filter((tool) => tool.deferred);
|
|
568
|
+
if (deferred.length > 0) {
|
|
569
|
+
const list = deferred.map((tool) => `- ${tool.name}`).join(`
|
|
570
|
+
`);
|
|
571
|
+
system = `${system}
|
|
572
|
+
|
|
573
|
+
# Deferred tools
|
|
574
|
+
These tools are available but their schemas are not loaded. Use ToolSearch to load them before calling.
|
|
575
|
+
${list}`;
|
|
576
|
+
}
|
|
577
|
+
if (systemExtension) {
|
|
578
|
+
system = `${system}
|
|
579
|
+
|
|
580
|
+
${systemExtension}`;
|
|
581
|
+
}
|
|
582
|
+
return system;
|
|
583
|
+
}
|
|
584
|
+
buildSkillTool() {
|
|
585
|
+
const byName = new Map(this.skills.map((skill) => [skill.name, skill]));
|
|
586
|
+
return new Tool({
|
|
587
|
+
name: "Skill",
|
|
588
|
+
description: 'Load the full content of a skill listed under "# Skills" in the system prompt. Call this when you decide a listed skill applies to the current task; the returned content extends your instructions for the rest of the session.',
|
|
589
|
+
inputSchema: z.object({
|
|
590
|
+
skill: z.string().describe("Name of the skill to load (must match a listed skill).")
|
|
591
|
+
}),
|
|
592
|
+
execute: ({ skill }) => {
|
|
593
|
+
const found = byName.get(skill);
|
|
594
|
+
if (!found) {
|
|
595
|
+
const available = [...byName.keys()].join(", ");
|
|
596
|
+
throw new Error(`Skill "${skill}" not found. Available: ${available}`);
|
|
597
|
+
}
|
|
598
|
+
return `<skill name="${found.name}">
|
|
599
|
+
${found.content}
|
|
600
|
+
</skill>`;
|
|
783
601
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
session(init) {
|
|
605
|
+
const initialMessages = init?.messages ? [...init.messages] : [];
|
|
606
|
+
const sessionCwd = init?.cwd ?? this.cwd ?? process.cwd();
|
|
607
|
+
const systemExtension = init?.systemExtension;
|
|
608
|
+
const state = {
|
|
609
|
+
messages: initialMessages,
|
|
610
|
+
store: new Map
|
|
611
|
+
};
|
|
612
|
+
let activeTurn = null;
|
|
613
|
+
let mcpConnected = false;
|
|
614
|
+
let allTools = null;
|
|
615
|
+
let registry = null;
|
|
616
|
+
let ctx = null;
|
|
617
|
+
const ensureReady = async () => {
|
|
618
|
+
if (!allTools || !registry || !ctx) {
|
|
619
|
+
const mcpToolGroups = await Promise.all(this.mcp.map((mcp) => mcp.connect()));
|
|
620
|
+
mcpConnected = true;
|
|
621
|
+
allTools = [
|
|
622
|
+
...this.tools,
|
|
623
|
+
...mcpToolGroups.flat(),
|
|
624
|
+
...this.skills.length > 0 ? [this.buildSkillTool()] : []
|
|
625
|
+
];
|
|
626
|
+
registry = createRegistry(allTools);
|
|
627
|
+
ctx = {
|
|
628
|
+
complete: async (p) => {
|
|
629
|
+
const response = await this.model.createMessage({
|
|
630
|
+
messages: [{ role: "user", content: p }]
|
|
631
|
+
});
|
|
632
|
+
return response.content.filter((block) => block.type === "text").map((block) => block.text).join(`
|
|
633
|
+
`);
|
|
634
|
+
},
|
|
635
|
+
searchWeb: (query, opts) => this.model.searchWeb(query, opts),
|
|
636
|
+
session: state,
|
|
637
|
+
tools: registry,
|
|
638
|
+
cwd: sessionCwd
|
|
639
|
+
};
|
|
640
|
+
const systemMessage = {
|
|
641
|
+
role: "system",
|
|
642
|
+
content: this.buildSystem(allTools, systemExtension)
|
|
643
|
+
};
|
|
644
|
+
if (state.messages[0]?.role === "system") {
|
|
645
|
+
state.messages[0] = systemMessage;
|
|
646
|
+
} else {
|
|
647
|
+
state.messages.unshift(systemMessage);
|
|
648
|
+
}
|
|
787
649
|
}
|
|
788
|
-
|
|
789
|
-
|
|
650
|
+
return { tools: allTools, registry, ctx };
|
|
651
|
+
};
|
|
652
|
+
const internals = {
|
|
653
|
+
send: (prompt) => {
|
|
654
|
+
if (activeTurn !== null) {
|
|
655
|
+
throw new Error("Session.send() called while another turn is in flight.");
|
|
656
|
+
}
|
|
657
|
+
const turn = new Turn(async (turnCtx) => {
|
|
790
658
|
try {
|
|
791
|
-
await
|
|
792
|
-
|
|
793
|
-
|
|
659
|
+
const ready = await ensureReady();
|
|
660
|
+
state.messages.push(toUserMessage(prompt));
|
|
661
|
+
await runAgentLoop({
|
|
662
|
+
model: this.model,
|
|
663
|
+
state,
|
|
664
|
+
registry: ready.registry,
|
|
665
|
+
ctx: ready.ctx,
|
|
666
|
+
emit: turnCtx.emit,
|
|
667
|
+
signal: turnCtx.signal
|
|
668
|
+
});
|
|
669
|
+
return [...state.messages];
|
|
670
|
+
} finally {
|
|
671
|
+
activeTurn = null;
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
activeTurn = turn;
|
|
675
|
+
return turn;
|
|
676
|
+
},
|
|
677
|
+
close: async () => {
|
|
678
|
+
const pending = activeTurn;
|
|
679
|
+
if (pending) {
|
|
680
|
+
pending.abort("session closed");
|
|
681
|
+
await Promise.resolve(pending).catch(() => {});
|
|
682
|
+
}
|
|
683
|
+
if (mcpConnected) {
|
|
684
|
+
await Promise.all(this.mcp.map((mcp) => mcp.disconnect()));
|
|
685
|
+
mcpConnected = false;
|
|
794
686
|
}
|
|
795
|
-
await this.authorize();
|
|
796
687
|
}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
await this.inFlight;
|
|
800
|
-
} finally {
|
|
801
|
-
this.inFlight = undefined;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
isExpired(tokens) {
|
|
805
|
-
if (!tokens.expiresAt)
|
|
806
|
-
return false;
|
|
807
|
-
return Date.now() / 1000 >= tokens.expiresAt - 30;
|
|
808
|
-
}
|
|
809
|
-
async getMetadata() {
|
|
810
|
-
if (!this.metadata)
|
|
811
|
-
this.metadata = await discover(this.serverUrl);
|
|
812
|
-
return this.metadata;
|
|
688
|
+
};
|
|
689
|
+
return new Session(internals, state);
|
|
813
690
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
691
|
+
}
|
|
692
|
+
function toUserMessage(prompt) {
|
|
693
|
+
if (typeof prompt === "string")
|
|
694
|
+
return { role: "user", content: prompt };
|
|
695
|
+
return { role: "user", content: prompt };
|
|
696
|
+
}
|
|
697
|
+
async function runAgentLoop({
|
|
698
|
+
model,
|
|
699
|
+
state,
|
|
700
|
+
registry,
|
|
701
|
+
ctx,
|
|
702
|
+
emit,
|
|
703
|
+
signal
|
|
704
|
+
}) {
|
|
705
|
+
while (true) {
|
|
706
|
+
if (signal.aborted)
|
|
707
|
+
break;
|
|
708
|
+
const active = registry.loaded();
|
|
709
|
+
const toolByName = new Map(active.map((tool) => [tool.name, tool]));
|
|
710
|
+
emit({ type: "message-start" });
|
|
711
|
+
const assistantContent = [];
|
|
712
|
+
let stopReason = "end_turn";
|
|
713
|
+
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
714
|
+
try {
|
|
715
|
+
for await (const event of model.streamMessage({
|
|
716
|
+
messages: state.messages,
|
|
717
|
+
tools: active,
|
|
718
|
+
signal
|
|
719
|
+
})) {
|
|
720
|
+
switch (event.type) {
|
|
721
|
+
case "text-delta":
|
|
722
|
+
emit({ type: "text-delta", text: event.text });
|
|
723
|
+
break;
|
|
724
|
+
case "text-end":
|
|
725
|
+
assistantContent.push({ type: "text", text: event.text });
|
|
726
|
+
emit({ type: "text", text: event.text });
|
|
727
|
+
break;
|
|
728
|
+
case "thinking-delta":
|
|
729
|
+
emit({ type: "thinking-delta", text: event.text });
|
|
730
|
+
break;
|
|
731
|
+
case "thinking-end":
|
|
732
|
+
assistantContent.push({
|
|
733
|
+
type: "thinking",
|
|
734
|
+
text: event.text,
|
|
735
|
+
...event.signature !== undefined ? { signature: event.signature } : {},
|
|
736
|
+
...event.redactedData !== undefined ? { redactedData: event.redactedData } : {}
|
|
737
|
+
});
|
|
738
|
+
emit({
|
|
739
|
+
type: "thinking",
|
|
740
|
+
text: event.text,
|
|
741
|
+
...event.signature !== undefined ? { signature: event.signature } : {}
|
|
742
|
+
});
|
|
743
|
+
break;
|
|
744
|
+
case "tool-call":
|
|
745
|
+
assistantContent.push({
|
|
746
|
+
type: "tool-call",
|
|
747
|
+
toolCallId: event.toolCallId,
|
|
748
|
+
toolName: event.toolName,
|
|
749
|
+
input: event.input
|
|
750
|
+
});
|
|
751
|
+
emit({
|
|
752
|
+
type: "tool-call",
|
|
753
|
+
toolCallId: event.toolCallId,
|
|
754
|
+
toolName: event.toolName,
|
|
755
|
+
input: event.input
|
|
756
|
+
});
|
|
757
|
+
break;
|
|
758
|
+
case "message-end":
|
|
759
|
+
stopReason = event.stopReason;
|
|
760
|
+
usage = event.usage;
|
|
761
|
+
break;
|
|
762
|
+
default:
|
|
763
|
+
break;
|
|
764
|
+
}
|
|
821
765
|
}
|
|
822
|
-
|
|
766
|
+
} catch (error) {
|
|
767
|
+
if (signal.aborted) {
|
|
768
|
+
state.messages.push({ role: "assistant", content: assistantContent });
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
772
|
+
emit({ type: "error", error: err });
|
|
773
|
+
throw err;
|
|
823
774
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
authUrl.searchParams.set("response_type", "code");
|
|
830
|
-
authUrl.searchParams.set("client_id", clientId);
|
|
831
|
-
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
832
|
-
authUrl.searchParams.set("code_challenge", challenge);
|
|
833
|
-
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
834
|
-
authUrl.searchParams.set("state", state);
|
|
835
|
-
if (this.scopes?.length) {
|
|
836
|
-
authUrl.searchParams.set("scope", this.scopes.join(" "));
|
|
775
|
+
state.messages.push({ role: "assistant", content: assistantContent });
|
|
776
|
+
emit({ type: "message-end", usage });
|
|
777
|
+
if (stopReason !== "tool_use") {
|
|
778
|
+
emit({ type: "turn-end" });
|
|
779
|
+
break;
|
|
837
780
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
throw new Error("OAuth refresh: missing refresh token or client id.");
|
|
781
|
+
const toolCalls = assistantContent.filter((block) => block.type === "tool-call");
|
|
782
|
+
const results = [];
|
|
783
|
+
for (const call of toolCalls) {
|
|
784
|
+
if (signal.aborted)
|
|
785
|
+
break;
|
|
786
|
+
const tool = toolByName.get(call.toolName);
|
|
787
|
+
const result = await runTool(tool, call, ctx);
|
|
788
|
+
results.push(result);
|
|
789
|
+
emit({
|
|
790
|
+
type: "tool-result",
|
|
791
|
+
toolCallId: result.toolCallId,
|
|
792
|
+
toolName: result.toolName,
|
|
793
|
+
output: result.output,
|
|
794
|
+
isError: typeof result.output === "string" ? result.output.startsWith("Error:") : false
|
|
795
|
+
});
|
|
854
796
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
refreshToken: this.tokens.refreshToken,
|
|
859
|
-
clientId: this.tokens.clientId
|
|
860
|
-
});
|
|
861
|
-
this.tokens = this.toTokenSet(response, this.tokens.clientId, this.tokens.redirectUri, this.tokens.refreshToken);
|
|
862
|
-
await this.storage.save(this.storageKey, this.tokens);
|
|
797
|
+
state.messages.push({ role: "tool", content: results });
|
|
798
|
+
if (signal.aborted)
|
|
799
|
+
break;
|
|
863
800
|
}
|
|
864
|
-
|
|
865
|
-
|
|
801
|
+
}
|
|
802
|
+
async function runTool(tool, call, ctx) {
|
|
803
|
+
if (!tool) {
|
|
866
804
|
return {
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
805
|
+
type: "tool-result",
|
|
806
|
+
toolCallId: call.toolCallId,
|
|
807
|
+
toolName: call.toolName,
|
|
808
|
+
output: `Error: tool "${call.toolName}" not found`
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
try {
|
|
812
|
+
const parsed = tool.parse(call.input);
|
|
813
|
+
const output = await tool.execute(parsed, ctx);
|
|
814
|
+
return {
|
|
815
|
+
type: "tool-result",
|
|
816
|
+
toolCallId: call.toolCallId,
|
|
817
|
+
toolName: call.toolName,
|
|
818
|
+
output
|
|
819
|
+
};
|
|
820
|
+
} catch (error) {
|
|
821
|
+
return {
|
|
822
|
+
type: "tool-result",
|
|
823
|
+
toolCallId: call.toolCallId,
|
|
824
|
+
toolName: call.toolName,
|
|
825
|
+
output: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
872
826
|
};
|
|
873
827
|
}
|
|
874
828
|
}
|
|
875
829
|
|
|
830
|
+
// src/core/model.ts
|
|
831
|
+
class Model {
|
|
832
|
+
async createMessage(params) {
|
|
833
|
+
const content = [];
|
|
834
|
+
let id = "";
|
|
835
|
+
let stopReason = "end_turn";
|
|
836
|
+
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
837
|
+
for await (const event of this.streamMessage(params)) {
|
|
838
|
+
switch (event.type) {
|
|
839
|
+
case "text-end":
|
|
840
|
+
content.push({ type: "text", text: event.text });
|
|
841
|
+
break;
|
|
842
|
+
case "thinking-end":
|
|
843
|
+
content.push({
|
|
844
|
+
type: "thinking",
|
|
845
|
+
text: event.text,
|
|
846
|
+
...event.signature !== undefined ? { signature: event.signature } : {},
|
|
847
|
+
...event.redactedData !== undefined ? { redactedData: event.redactedData } : {}
|
|
848
|
+
});
|
|
849
|
+
break;
|
|
850
|
+
case "tool-call":
|
|
851
|
+
content.push({
|
|
852
|
+
type: "tool-call",
|
|
853
|
+
toolCallId: event.toolCallId,
|
|
854
|
+
toolName: event.toolName,
|
|
855
|
+
input: event.input
|
|
856
|
+
});
|
|
857
|
+
break;
|
|
858
|
+
case "message-end":
|
|
859
|
+
id = event.id;
|
|
860
|
+
stopReason = event.stopReason;
|
|
861
|
+
usage = event.usage;
|
|
862
|
+
break;
|
|
863
|
+
default:
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return { id, content, stopReason, usage };
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/core/skill.ts
|
|
872
|
+
class Skill {
|
|
873
|
+
name;
|
|
874
|
+
description;
|
|
875
|
+
content;
|
|
876
|
+
constructor({
|
|
877
|
+
name,
|
|
878
|
+
description,
|
|
879
|
+
content
|
|
880
|
+
}) {
|
|
881
|
+
this.name = name;
|
|
882
|
+
this.description = description;
|
|
883
|
+
this.content = content;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
876
887
|
// src/mcp/clients/grafana.ts
|
|
877
888
|
var EMPTY_OBJECT = {
|
|
878
889
|
type: "object",
|
|
@@ -1263,6 +1274,9 @@ class Mcp2 {
|
|
|
1263
1274
|
transport,
|
|
1264
1275
|
deferred
|
|
1265
1276
|
}) {
|
|
1277
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
1278
|
+
throw new Error(`Invalid MCP name "${name}": use only letters, digits, underscores, and hyphens.`);
|
|
1279
|
+
}
|
|
1266
1280
|
this.name = name;
|
|
1267
1281
|
this.transport = transport;
|
|
1268
1282
|
this.deferred = deferred ?? true;
|
|
@@ -1351,6 +1365,11 @@ class Mcp4 extends Mcp2 {
|
|
|
1351
1365
|
var tenderly_default = Mcp4;
|
|
1352
1366
|
export {
|
|
1353
1367
|
tenderly_default as Tenderly,
|
|
1368
|
+
OAuthAuth,
|
|
1369
|
+
MemoryStorage,
|
|
1370
|
+
Mcp2 as Mcp,
|
|
1354
1371
|
linear_default as Linear,
|
|
1355
|
-
grafana_default as Grafana
|
|
1372
|
+
grafana_default as Grafana,
|
|
1373
|
+
FileStorage,
|
|
1374
|
+
BearerAuth
|
|
1356
1375
|
};
|