fourmis-agents-sdk 0.1.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/LICENSE +21 -0
- package/README.md +309 -0
- package/dist/agent-loop.d.ts +36 -0
- package/dist/agent-loop.d.ts.map +1 -0
- package/dist/agent-loop.js +387 -0
- package/dist/agents/index.d.ts +8 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +2309 -0
- package/dist/agents/task-manager.d.ts +14 -0
- package/dist/agents/task-manager.d.ts.map +1 -0
- package/dist/agents/task-manager.js +67 -0
- package/dist/agents/tools.d.ts +24 -0
- package/dist/agents/tools.d.ts.map +1 -0
- package/dist/agents/tools.js +2257 -0
- package/dist/agents/types.d.ts +22 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +1 -0
- package/dist/api.d.ts +35 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +2983 -0
- package/dist/auth/login-openai.d.ts +10 -0
- package/dist/auth/login-openai.d.ts.map +1 -0
- package/dist/auth/login-openai.js +316 -0
- package/dist/auth/openai-oauth.d.ts +45 -0
- package/dist/auth/openai-oauth.d.ts.map +1 -0
- package/dist/auth/openai-oauth.js +308 -0
- package/dist/hooks.d.ts +71 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +87 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3025 -0
- package/dist/mcp/client.d.ts +24 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +176 -0
- package/dist/mcp/index.d.ts +8 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +203 -0
- package/dist/mcp/server.d.ts +25 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +42 -0
- package/dist/mcp/types.d.ts +47 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +1 -0
- package/dist/permissions.d.ts +29 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +157 -0
- package/dist/providers/anthropic.d.ts +26 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +382 -0
- package/dist/providers/openai.d.ts +42 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +871 -0
- package/dist/providers/registry.d.ts +11 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +1118 -0
- package/dist/providers/types.d.ts +79 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +1 -0
- package/dist/query.d.ts +9 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +36 -0
- package/dist/settings.d.ts +28 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +143 -0
- package/dist/tools/bash.d.ts +6 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +88 -0
- package/dist/tools/edit.d.ts +6 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +108 -0
- package/dist/tools/glob.d.ts +6 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +70 -0
- package/dist/tools/grep.d.ts +7 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/grep.js +183 -0
- package/dist/tools/index.d.ts +18 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +595 -0
- package/dist/tools/mcp-resources.d.ts +8 -0
- package/dist/tools/mcp-resources.d.ts.map +1 -0
- package/dist/tools/mcp-resources.js +87 -0
- package/dist/tools/presets.d.ts +6 -0
- package/dist/tools/presets.d.ts.map +1 -0
- package/dist/tools/presets.js +32 -0
- package/dist/tools/read.d.ts +6 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +81 -0
- package/dist/tools/registry.d.ts +31 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +52 -0
- package/dist/tools/write.d.ts +6 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +62 -0
- package/dist/types.d.ts +201 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +39 -0
- package/dist/utils/cost.d.ts +35 -0
- package/dist/utils/cost.d.ts.map +1 -0
- package/dist/utils/cost.js +176 -0
- package/dist/utils/system-prompt.d.ts +11 -0
- package/dist/utils/system-prompt.d.ts.map +1 -0
- package/dist/utils/system-prompt.js +89 -0
- package/package.json +66 -0
|
@@ -0,0 +1,2309 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true,
|
|
8
|
+
configurable: true,
|
|
9
|
+
set: (newValue) => all[name] = () => newValue
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
13
|
+
var __require = import.meta.require;
|
|
14
|
+
|
|
15
|
+
// src/tools/mcp-resources.ts
|
|
16
|
+
var exports_mcp_resources = {};
|
|
17
|
+
__export(exports_mcp_resources, {
|
|
18
|
+
createReadMcpResourceTool: () => createReadMcpResourceTool,
|
|
19
|
+
createListMcpResourcesTool: () => createListMcpResourcesTool
|
|
20
|
+
});
|
|
21
|
+
function createListMcpResourcesTool(mcpClient) {
|
|
22
|
+
return {
|
|
23
|
+
name: "mcp__list_resources",
|
|
24
|
+
description: "List available resources from MCP servers.",
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
server: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Optional server name to filter by. If omitted, lists resources from all servers."
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
async execute(input) {
|
|
35
|
+
const { server } = input ?? {};
|
|
36
|
+
try {
|
|
37
|
+
const resources = await mcpClient.listResources(server);
|
|
38
|
+
if (resources.length === 0) {
|
|
39
|
+
return { content: "No resources available." };
|
|
40
|
+
}
|
|
41
|
+
const lines = resources.map((r) => `[${r.server}] ${r.uri} - ${r.name}${r.description ? `: ${r.description}` : ""}`);
|
|
42
|
+
return { content: lines.join(`
|
|
43
|
+
`) };
|
|
44
|
+
} catch (err) {
|
|
45
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
46
|
+
return { content: `Error listing resources: ${message}`, isError: true };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function createReadMcpResourceTool(mcpClient) {
|
|
52
|
+
return {
|
|
53
|
+
name: "mcp__read_resource",
|
|
54
|
+
description: "Read a specific resource from an MCP server by URI.",
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: {
|
|
58
|
+
server: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "The MCP server name that hosts the resource."
|
|
61
|
+
},
|
|
62
|
+
uri: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "The resource URI to read."
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
required: ["server", "uri"]
|
|
68
|
+
},
|
|
69
|
+
async execute(input) {
|
|
70
|
+
const { server, uri } = input;
|
|
71
|
+
if (!server || !uri) {
|
|
72
|
+
return { content: "Both 'server' and 'uri' are required.", isError: true };
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const content = await mcpClient.readResource(server, uri);
|
|
76
|
+
return { content };
|
|
77
|
+
} catch (err) {
|
|
78
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
79
|
+
return { content: `Error reading resource: ${message}`, isError: true };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/auth/openai-oauth.ts
|
|
86
|
+
var exports_openai_oauth = {};
|
|
87
|
+
__export(exports_openai_oauth, {
|
|
88
|
+
login: () => login,
|
|
89
|
+
loadTokens: () => loadTokens,
|
|
90
|
+
isLoggedIn: () => isLoggedIn,
|
|
91
|
+
getValidToken: () => getValidToken,
|
|
92
|
+
generateCodeVerifier: () => generateCodeVerifier,
|
|
93
|
+
generateCodeChallenge: () => generateCodeChallenge,
|
|
94
|
+
extractAccountId: () => extractAccountId,
|
|
95
|
+
decodeJwtPayload: () => decodeJwtPayload
|
|
96
|
+
});
|
|
97
|
+
import { randomBytes, createHash } from "crypto";
|
|
98
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
99
|
+
import { join } from "path";
|
|
100
|
+
function tokenDir() {
|
|
101
|
+
return join(process.env.HOME ?? "/root", ".fourmis");
|
|
102
|
+
}
|
|
103
|
+
function tokenPath() {
|
|
104
|
+
return join(tokenDir(), "openai-auth.json");
|
|
105
|
+
}
|
|
106
|
+
function codexFallbackPath() {
|
|
107
|
+
return join(process.env.HOME ?? "/root", ".codex", "auth.json");
|
|
108
|
+
}
|
|
109
|
+
function loadTokens() {
|
|
110
|
+
for (const p of [tokenPath(), codexFallbackPath()]) {
|
|
111
|
+
try {
|
|
112
|
+
const raw = readFileSync(p, "utf-8");
|
|
113
|
+
const data = JSON.parse(raw);
|
|
114
|
+
if (data.access_token && data.account_id) {
|
|
115
|
+
return data;
|
|
116
|
+
}
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
function saveTokens(tokens) {
|
|
122
|
+
const dir = tokenDir();
|
|
123
|
+
if (!existsSync(dir)) {
|
|
124
|
+
mkdirSync(dir, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
writeFileSync(tokenPath(), JSON.stringify(tokens, null, 2), { mode: 384 });
|
|
127
|
+
}
|
|
128
|
+
function generateCodeVerifier() {
|
|
129
|
+
return randomBytes(64).toString("base64url");
|
|
130
|
+
}
|
|
131
|
+
function generateCodeChallenge(verifier) {
|
|
132
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
133
|
+
}
|
|
134
|
+
function decodeJwtPayload(token) {
|
|
135
|
+
const parts = token.split(".");
|
|
136
|
+
if (parts.length < 2)
|
|
137
|
+
throw new Error("Invalid JWT");
|
|
138
|
+
const payload = Buffer.from(parts[1], "base64url").toString("utf-8");
|
|
139
|
+
return JSON.parse(payload);
|
|
140
|
+
}
|
|
141
|
+
function extractAccountId(accessToken) {
|
|
142
|
+
const payload = decodeJwtPayload(accessToken);
|
|
143
|
+
const authClaim = payload["https://api.openai.com/auth"];
|
|
144
|
+
if (!authClaim?.chatgpt_account_id) {
|
|
145
|
+
throw new Error("JWT missing chatgpt_account_id claim");
|
|
146
|
+
}
|
|
147
|
+
return authClaim.chatgpt_account_id;
|
|
148
|
+
}
|
|
149
|
+
async function exchangeCode(code, codeVerifier) {
|
|
150
|
+
const res = await fetch(TOKEN_URL, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: { "Content-Type": "application/json" },
|
|
153
|
+
body: JSON.stringify({
|
|
154
|
+
grant_type: "authorization_code",
|
|
155
|
+
client_id: CLIENT_ID,
|
|
156
|
+
code,
|
|
157
|
+
redirect_uri: REDIRECT_URI,
|
|
158
|
+
code_verifier: codeVerifier
|
|
159
|
+
})
|
|
160
|
+
});
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
const text = await res.text();
|
|
163
|
+
throw new Error(`Token exchange failed (${res.status}): ${text}`);
|
|
164
|
+
}
|
|
165
|
+
return res.json();
|
|
166
|
+
}
|
|
167
|
+
async function refreshAccessToken(refreshToken) {
|
|
168
|
+
const res = await fetch(TOKEN_URL, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: { "Content-Type": "application/json" },
|
|
171
|
+
body: JSON.stringify({
|
|
172
|
+
grant_type: "refresh_token",
|
|
173
|
+
client_id: CLIENT_ID,
|
|
174
|
+
refresh_token: refreshToken
|
|
175
|
+
})
|
|
176
|
+
});
|
|
177
|
+
if (!res.ok) {
|
|
178
|
+
const text = await res.text();
|
|
179
|
+
throw new Error(`Token refresh failed (${res.status}): ${text}`);
|
|
180
|
+
}
|
|
181
|
+
return res.json();
|
|
182
|
+
}
|
|
183
|
+
async function getValidToken() {
|
|
184
|
+
const tokens = loadTokens();
|
|
185
|
+
if (!tokens)
|
|
186
|
+
return null;
|
|
187
|
+
if (tokens.expires_at <= Date.now() + 300000) {
|
|
188
|
+
try {
|
|
189
|
+
const fresh = await refreshAccessToken(tokens.refresh_token);
|
|
190
|
+
const accountId = extractAccountId(fresh.access_token);
|
|
191
|
+
const updated = {
|
|
192
|
+
access_token: fresh.access_token,
|
|
193
|
+
refresh_token: fresh.refresh_token ?? tokens.refresh_token,
|
|
194
|
+
expires_at: Date.now() + fresh.expires_in * 1000,
|
|
195
|
+
account_id: accountId
|
|
196
|
+
};
|
|
197
|
+
saveTokens(updated);
|
|
198
|
+
return { accessToken: updated.access_token, accountId };
|
|
199
|
+
} catch {
|
|
200
|
+
return { accessToken: tokens.access_token, accountId: tokens.account_id };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { accessToken: tokens.access_token, accountId: tokens.account_id };
|
|
204
|
+
}
|
|
205
|
+
function isLoggedIn() {
|
|
206
|
+
return loadTokens() !== null;
|
|
207
|
+
}
|
|
208
|
+
async function login(opts) {
|
|
209
|
+
const codeVerifier = generateCodeVerifier();
|
|
210
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
211
|
+
const state = randomBytes(16).toString("hex");
|
|
212
|
+
const authUrl = new URL(AUTHORIZE_URL);
|
|
213
|
+
authUrl.searchParams.set("client_id", CLIENT_ID);
|
|
214
|
+
authUrl.searchParams.set("response_type", "code");
|
|
215
|
+
authUrl.searchParams.set("redirect_uri", REDIRECT_URI);
|
|
216
|
+
authUrl.searchParams.set("scope", SCOPE);
|
|
217
|
+
authUrl.searchParams.set("state", state);
|
|
218
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
219
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
220
|
+
authUrl.searchParams.set("audience", "https://api.openai.com/v1");
|
|
221
|
+
const forceHeadless = opts?.headless ?? false;
|
|
222
|
+
if (!forceHeadless) {
|
|
223
|
+
let serverWorked = false;
|
|
224
|
+
try {
|
|
225
|
+
const result = await loginWithServer(authUrl.toString(), state, codeVerifier);
|
|
226
|
+
return result;
|
|
227
|
+
} catch {}
|
|
228
|
+
}
|
|
229
|
+
return loginHeadless(authUrl.toString(), state, codeVerifier);
|
|
230
|
+
}
|
|
231
|
+
async function loginWithServer(authUrlStr, state, codeVerifier) {
|
|
232
|
+
let authCode = null;
|
|
233
|
+
let authError = null;
|
|
234
|
+
let resolveCallback;
|
|
235
|
+
const callbackPromise = new Promise((resolve) => {
|
|
236
|
+
resolveCallback = resolve;
|
|
237
|
+
});
|
|
238
|
+
const server = Bun.serve({
|
|
239
|
+
port: CALLBACK_PORT,
|
|
240
|
+
fetch(req) {
|
|
241
|
+
const url = new URL(req.url);
|
|
242
|
+
if (url.pathname === "/auth/callback") {
|
|
243
|
+
const receivedState = url.searchParams.get("state");
|
|
244
|
+
const code = url.searchParams.get("code");
|
|
245
|
+
const error = url.searchParams.get("error");
|
|
246
|
+
if (error) {
|
|
247
|
+
authError = error;
|
|
248
|
+
} else if (receivedState !== state) {
|
|
249
|
+
authError = "State mismatch";
|
|
250
|
+
} else if (code) {
|
|
251
|
+
authCode = code;
|
|
252
|
+
} else {
|
|
253
|
+
authError = "No code received";
|
|
254
|
+
}
|
|
255
|
+
resolveCallback();
|
|
256
|
+
return new Response("<html><body><h1>Authentication complete</h1><p>You can close this window.</p></body></html>", { headers: { "Content-Type": "text/html" } });
|
|
257
|
+
}
|
|
258
|
+
return new Response("Not found", { status: 404 });
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
try {
|
|
262
|
+
await openBrowser(authUrlStr);
|
|
263
|
+
} catch {
|
|
264
|
+
server.stop(true);
|
|
265
|
+
throw new Error("Cannot open browser");
|
|
266
|
+
}
|
|
267
|
+
console.log("Waiting for authentication...");
|
|
268
|
+
const timeout = setTimeout(() => {
|
|
269
|
+
authError = "Timeout waiting for callback (120s)";
|
|
270
|
+
resolveCallback();
|
|
271
|
+
}, 120000);
|
|
272
|
+
await callbackPromise;
|
|
273
|
+
clearTimeout(timeout);
|
|
274
|
+
server.stop(true);
|
|
275
|
+
if (authError) {
|
|
276
|
+
return { success: false, error: authError };
|
|
277
|
+
}
|
|
278
|
+
if (!authCode) {
|
|
279
|
+
return { success: false, error: "No authorization code received" };
|
|
280
|
+
}
|
|
281
|
+
return finishLogin(authCode, codeVerifier);
|
|
282
|
+
}
|
|
283
|
+
async function loginHeadless(authUrlStr, state, codeVerifier) {
|
|
284
|
+
console.log(`
|
|
285
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510`);
|
|
286
|
+
console.log("\u2502 Headless login (VPS / SSH mode) \u2502");
|
|
287
|
+
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
288
|
+
console.log(`
|
|
289
|
+
1. Open this URL in your local browser:
|
|
290
|
+
`);
|
|
291
|
+
console.log(` ${authUrlStr}
|
|
292
|
+
`);
|
|
293
|
+
console.log("2. Log in with your ChatGPT account.");
|
|
294
|
+
console.log("3. After login, your browser will try to redirect to");
|
|
295
|
+
console.log(" localhost:1455 \u2014 this will FAIL (that's expected).");
|
|
296
|
+
console.log("4. Copy the FULL URL from the browser's address bar");
|
|
297
|
+
console.log(" (it starts with http://localhost:1455/auth/callback?code=...)");
|
|
298
|
+
console.log(`5. Paste it here and press Enter:
|
|
299
|
+
`);
|
|
300
|
+
process.stdout.write("> ");
|
|
301
|
+
const pastedUrl = await readLine();
|
|
302
|
+
if (!pastedUrl.trim()) {
|
|
303
|
+
return { success: false, error: "No URL pasted" };
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
const url = new URL(pastedUrl.trim());
|
|
307
|
+
const code = url.searchParams.get("code");
|
|
308
|
+
const receivedState = url.searchParams.get("state");
|
|
309
|
+
const error = url.searchParams.get("error");
|
|
310
|
+
if (error) {
|
|
311
|
+
return { success: false, error: `OAuth error: ${error}` };
|
|
312
|
+
}
|
|
313
|
+
if (receivedState !== state) {
|
|
314
|
+
return { success: false, error: "State mismatch \u2014 did you use the right URL?" };
|
|
315
|
+
}
|
|
316
|
+
if (!code) {
|
|
317
|
+
return { success: false, error: "No authorization code in URL" };
|
|
318
|
+
}
|
|
319
|
+
return finishLogin(code, codeVerifier);
|
|
320
|
+
} catch (err) {
|
|
321
|
+
return { success: false, error: `Invalid URL: ${err.message}` };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function finishLogin(authCode, codeVerifier) {
|
|
325
|
+
try {
|
|
326
|
+
const tokenResponse = await exchangeCode(authCode, codeVerifier);
|
|
327
|
+
const accountId = extractAccountId(tokenResponse.access_token);
|
|
328
|
+
const stored = {
|
|
329
|
+
access_token: tokenResponse.access_token,
|
|
330
|
+
refresh_token: tokenResponse.refresh_token,
|
|
331
|
+
expires_at: Date.now() + tokenResponse.expires_in * 1000,
|
|
332
|
+
account_id: accountId
|
|
333
|
+
};
|
|
334
|
+
saveTokens(stored);
|
|
335
|
+
return { success: true, accountId };
|
|
336
|
+
} catch (err) {
|
|
337
|
+
return { success: false, error: err.message };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function readLine() {
|
|
341
|
+
return new Promise((resolve) => {
|
|
342
|
+
let data = "";
|
|
343
|
+
process.stdin.setEncoding("utf-8");
|
|
344
|
+
process.stdin.resume();
|
|
345
|
+
process.stdin.on("data", (chunk) => {
|
|
346
|
+
data += chunk;
|
|
347
|
+
if (data.includes(`
|
|
348
|
+
`)) {
|
|
349
|
+
process.stdin.pause();
|
|
350
|
+
resolve(data.split(`
|
|
351
|
+
`)[0]);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
async function openBrowser(url) {
|
|
357
|
+
const platform = process.platform;
|
|
358
|
+
const cmd = platform === "darwin" ? ["open", url] : platform === "win32" ? ["cmd", "/c", "start", url] : ["xdg-open", url];
|
|
359
|
+
const proc = Bun.spawn(cmd, { stdout: "ignore", stderr: "ignore" });
|
|
360
|
+
await proc.exited;
|
|
361
|
+
if (proc.exitCode !== 0) {
|
|
362
|
+
throw new Error(`Failed to open browser (exit code ${proc.exitCode})`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
var CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann", AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize", TOKEN_URL = "https://auth.openai.com/oauth/token", REDIRECT_URI = "http://localhost:1455/auth/callback", SCOPE = "openid profile email offline_access", CALLBACK_PORT = 1455;
|
|
366
|
+
var init_openai_oauth = () => {};
|
|
367
|
+
|
|
368
|
+
// src/types.ts
|
|
369
|
+
function uuid() {
|
|
370
|
+
return crypto.randomUUID();
|
|
371
|
+
}
|
|
372
|
+
function emptyTokenUsage() {
|
|
373
|
+
return {
|
|
374
|
+
inputTokens: 0,
|
|
375
|
+
outputTokens: 0,
|
|
376
|
+
cacheReadInputTokens: 0,
|
|
377
|
+
cacheCreationInputTokens: 0
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function mergeUsage(a, b) {
|
|
381
|
+
return {
|
|
382
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
383
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
384
|
+
cacheReadInputTokens: a.cacheReadInputTokens + b.cacheReadInputTokens,
|
|
385
|
+
cacheCreationInputTokens: a.cacheCreationInputTokens + b.cacheCreationInputTokens
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/agent-loop.ts
|
|
390
|
+
async function* agentLoop(prompt, options) {
|
|
391
|
+
const {
|
|
392
|
+
provider,
|
|
393
|
+
model,
|
|
394
|
+
systemPrompt,
|
|
395
|
+
tools,
|
|
396
|
+
permissions,
|
|
397
|
+
cwd,
|
|
398
|
+
sessionId,
|
|
399
|
+
maxTurns,
|
|
400
|
+
maxBudgetUsd,
|
|
401
|
+
includeStreamEvents,
|
|
402
|
+
signal,
|
|
403
|
+
env,
|
|
404
|
+
debug,
|
|
405
|
+
hooks,
|
|
406
|
+
mcpClient
|
|
407
|
+
} = options;
|
|
408
|
+
const startTime = Date.now();
|
|
409
|
+
let apiTimeMs = 0;
|
|
410
|
+
let turns = 0;
|
|
411
|
+
let totalUsage = emptyTokenUsage();
|
|
412
|
+
let costUsd = 0;
|
|
413
|
+
const modelUsage = {};
|
|
414
|
+
if (mcpClient) {
|
|
415
|
+
await mcpClient.connectAll();
|
|
416
|
+
for (const tool of mcpClient.getTools()) {
|
|
417
|
+
tools.register(tool);
|
|
418
|
+
}
|
|
419
|
+
const { createListMcpResourcesTool: createListMcpResourcesTool2, createReadMcpResourceTool: createReadMcpResourceTool2 } = await Promise.resolve().then(() => exports_mcp_resources);
|
|
420
|
+
tools.register(createListMcpResourcesTool2(mcpClient));
|
|
421
|
+
tools.register(createReadMcpResourceTool2(mcpClient));
|
|
422
|
+
}
|
|
423
|
+
const messages = [
|
|
424
|
+
{ role: "user", content: prompt }
|
|
425
|
+
];
|
|
426
|
+
yield {
|
|
427
|
+
type: "init",
|
|
428
|
+
sessionId,
|
|
429
|
+
model,
|
|
430
|
+
provider: provider.name,
|
|
431
|
+
tools: tools.list(),
|
|
432
|
+
cwd,
|
|
433
|
+
uuid: uuid()
|
|
434
|
+
};
|
|
435
|
+
if (hooks) {
|
|
436
|
+
await hooks.fire("SessionStart", { event: "SessionStart", session_id: sessionId }, undefined, { signal });
|
|
437
|
+
}
|
|
438
|
+
while (true) {
|
|
439
|
+
if (signal.aborted) {
|
|
440
|
+
yield makeError("error_execution", ["Aborted"], turns, costUsd, sessionId, startTime);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (turns >= maxTurns) {
|
|
444
|
+
yield makeError("error_max_turns", [`Reached maximum turns (${maxTurns})`], turns, costUsd, sessionId, startTime);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (maxBudgetUsd > 0 && costUsd >= maxBudgetUsd) {
|
|
448
|
+
yield makeError("error_max_budget", [`Reached budget limit ($${maxBudgetUsd})`], turns, costUsd, sessionId, startTime);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const toolDefs = tools.getDefinitions();
|
|
452
|
+
const apiStart = Date.now();
|
|
453
|
+
let assistantTextParts = [];
|
|
454
|
+
let toolCalls = [];
|
|
455
|
+
let turnUsage = emptyTokenUsage();
|
|
456
|
+
try {
|
|
457
|
+
const chunks = provider.chat({
|
|
458
|
+
model,
|
|
459
|
+
messages,
|
|
460
|
+
tools: toolDefs.length > 0 ? toolDefs : undefined,
|
|
461
|
+
systemPrompt,
|
|
462
|
+
signal
|
|
463
|
+
});
|
|
464
|
+
for await (const chunk of chunks) {
|
|
465
|
+
switch (chunk.type) {
|
|
466
|
+
case "text_delta":
|
|
467
|
+
assistantTextParts.push(chunk.text);
|
|
468
|
+
if (includeStreamEvents) {
|
|
469
|
+
yield { type: "stream", subtype: "text_delta", text: chunk.text, uuid: uuid() };
|
|
470
|
+
}
|
|
471
|
+
break;
|
|
472
|
+
case "thinking_delta":
|
|
473
|
+
if (includeStreamEvents) {
|
|
474
|
+
yield { type: "stream", subtype: "thinking_delta", text: chunk.text, uuid: uuid() };
|
|
475
|
+
}
|
|
476
|
+
break;
|
|
477
|
+
case "tool_call":
|
|
478
|
+
toolCalls.push({ id: chunk.id, name: chunk.name, input: chunk.input });
|
|
479
|
+
break;
|
|
480
|
+
case "usage":
|
|
481
|
+
turnUsage = mergeUsage(turnUsage, chunk.usage);
|
|
482
|
+
break;
|
|
483
|
+
case "done":
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} catch (err) {
|
|
488
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
489
|
+
yield makeError("error_execution", [`API error: ${message}`], turns, costUsd, sessionId, startTime);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
apiTimeMs += Date.now() - apiStart;
|
|
493
|
+
turns++;
|
|
494
|
+
totalUsage = mergeUsage(totalUsage, turnUsage);
|
|
495
|
+
const turnCost = provider.calculateCost(model, turnUsage);
|
|
496
|
+
costUsd += turnCost;
|
|
497
|
+
if (!modelUsage[model]) {
|
|
498
|
+
modelUsage[model] = {
|
|
499
|
+
inputTokens: 0,
|
|
500
|
+
outputTokens: 0,
|
|
501
|
+
cacheReadInputTokens: 0,
|
|
502
|
+
cacheCreationInputTokens: 0,
|
|
503
|
+
totalCostUsd: 0
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
modelUsage[model].inputTokens += turnUsage.inputTokens;
|
|
507
|
+
modelUsage[model].outputTokens += turnUsage.outputTokens;
|
|
508
|
+
modelUsage[model].cacheReadInputTokens += turnUsage.cacheReadInputTokens;
|
|
509
|
+
modelUsage[model].cacheCreationInputTokens += turnUsage.cacheCreationInputTokens;
|
|
510
|
+
modelUsage[model].totalCostUsd += turnCost;
|
|
511
|
+
const assistantText = assistantTextParts.join("");
|
|
512
|
+
const assistantContent = [];
|
|
513
|
+
if (assistantText) {
|
|
514
|
+
assistantContent.push({ type: "text", text: assistantText });
|
|
515
|
+
}
|
|
516
|
+
for (const call of toolCalls) {
|
|
517
|
+
assistantContent.push({
|
|
518
|
+
type: "tool_use",
|
|
519
|
+
id: call.id,
|
|
520
|
+
name: call.name,
|
|
521
|
+
input: call.input
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
messages.push({ role: "assistant", content: assistantContent });
|
|
525
|
+
if (assistantText) {
|
|
526
|
+
yield { type: "text", text: assistantText, uuid: uuid() };
|
|
527
|
+
}
|
|
528
|
+
if (toolCalls.length === 0) {
|
|
529
|
+
if (hooks) {
|
|
530
|
+
await hooks.fire("Stop", { event: "Stop", session_id: sessionId, text: assistantText || undefined }, undefined, { signal });
|
|
531
|
+
}
|
|
532
|
+
if (hooks) {
|
|
533
|
+
await hooks.fire("SessionEnd", { event: "SessionEnd", session_id: sessionId }, undefined, { signal });
|
|
534
|
+
}
|
|
535
|
+
yield {
|
|
536
|
+
type: "result",
|
|
537
|
+
subtype: "success",
|
|
538
|
+
text: assistantText || null,
|
|
539
|
+
turns,
|
|
540
|
+
costUsd,
|
|
541
|
+
durationMs: Date.now() - startTime,
|
|
542
|
+
durationApiMs: apiTimeMs,
|
|
543
|
+
sessionId,
|
|
544
|
+
usage: totalUsage,
|
|
545
|
+
modelUsage,
|
|
546
|
+
uuid: uuid()
|
|
547
|
+
};
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const toolResults = [];
|
|
551
|
+
for (const call of toolCalls) {
|
|
552
|
+
let hookDenied = false;
|
|
553
|
+
let hookUpdatedInput;
|
|
554
|
+
if (hooks) {
|
|
555
|
+
const hookResult = await hooks.fire("PreToolUse", { event: "PreToolUse", tool_name: call.name, tool_input: call.input, session_id: sessionId }, call.id, { signal });
|
|
556
|
+
if (hookResult) {
|
|
557
|
+
if (hookResult.permissionDecision === "deny") {
|
|
558
|
+
hookDenied = true;
|
|
559
|
+
}
|
|
560
|
+
if (hookResult.updatedInput !== undefined) {
|
|
561
|
+
hookUpdatedInput = hookResult.updatedInput;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (hookDenied) {
|
|
566
|
+
const denyContent = "Denied by hook";
|
|
567
|
+
yield {
|
|
568
|
+
type: "tool_result",
|
|
569
|
+
id: call.id,
|
|
570
|
+
name: call.name,
|
|
571
|
+
content: denyContent,
|
|
572
|
+
isError: true,
|
|
573
|
+
uuid: uuid()
|
|
574
|
+
};
|
|
575
|
+
toolResults.push({
|
|
576
|
+
type: "tool_result",
|
|
577
|
+
tool_use_id: call.id,
|
|
578
|
+
content: denyContent,
|
|
579
|
+
is_error: true
|
|
580
|
+
});
|
|
581
|
+
if (hooks) {
|
|
582
|
+
await hooks.fire("PostToolUseFailure", { event: "PostToolUseFailure", tool_name: call.name, tool_result: denyContent, tool_error: true, session_id: sessionId }, call.id, { signal });
|
|
583
|
+
}
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const inputAfterHook = hookUpdatedInput !== undefined ? hookUpdatedInput : call.input;
|
|
587
|
+
const permResult = await permissions.check(call.name, inputAfterHook ?? {}, { signal, toolUseId: call.id });
|
|
588
|
+
if (permResult.behavior === "deny") {
|
|
589
|
+
const denyContent = `Permission denied: ${permResult.message}`;
|
|
590
|
+
yield {
|
|
591
|
+
type: "tool_result",
|
|
592
|
+
id: call.id,
|
|
593
|
+
name: call.name,
|
|
594
|
+
content: denyContent,
|
|
595
|
+
isError: true,
|
|
596
|
+
uuid: uuid()
|
|
597
|
+
};
|
|
598
|
+
toolResults.push({
|
|
599
|
+
type: "tool_result",
|
|
600
|
+
tool_use_id: call.id,
|
|
601
|
+
content: denyContent,
|
|
602
|
+
is_error: true
|
|
603
|
+
});
|
|
604
|
+
if (hooks) {
|
|
605
|
+
await hooks.fire("PostToolUseFailure", { event: "PostToolUseFailure", tool_name: call.name, tool_result: denyContent, tool_error: true, session_id: sessionId }, call.id, { signal });
|
|
606
|
+
}
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const toolInput = permResult.behavior === "allow" && permResult.updatedInput ? permResult.updatedInput : inputAfterHook;
|
|
610
|
+
yield {
|
|
611
|
+
type: "tool_use",
|
|
612
|
+
id: call.id,
|
|
613
|
+
name: call.name,
|
|
614
|
+
input: toolInput,
|
|
615
|
+
uuid: uuid()
|
|
616
|
+
};
|
|
617
|
+
const toolCtx = {
|
|
618
|
+
cwd,
|
|
619
|
+
signal,
|
|
620
|
+
sessionId,
|
|
621
|
+
env
|
|
622
|
+
};
|
|
623
|
+
const result = await tools.execute(call.name, toolInput, toolCtx);
|
|
624
|
+
if (debug) {
|
|
625
|
+
console.error(`[debug] Tool ${call.name}: ${result.isError ? "ERROR" : "OK"} (${result.content.length} chars)`);
|
|
626
|
+
}
|
|
627
|
+
if (hooks) {
|
|
628
|
+
if (result.isError) {
|
|
629
|
+
await hooks.fire("PostToolUseFailure", { event: "PostToolUseFailure", tool_name: call.name, tool_result: result.content, tool_error: true, session_id: sessionId }, call.id, { signal });
|
|
630
|
+
} else {
|
|
631
|
+
const postResult = await hooks.fire("PostToolUse", { event: "PostToolUse", tool_name: call.name, tool_result: result.content, session_id: sessionId }, call.id, { signal });
|
|
632
|
+
if (postResult?.additionalContext) {
|
|
633
|
+
result.content += `
|
|
634
|
+
${postResult.additionalContext}`;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
yield {
|
|
639
|
+
type: "tool_result",
|
|
640
|
+
id: call.id,
|
|
641
|
+
name: call.name,
|
|
642
|
+
content: result.content,
|
|
643
|
+
isError: result.isError,
|
|
644
|
+
uuid: uuid()
|
|
645
|
+
};
|
|
646
|
+
toolResults.push({
|
|
647
|
+
type: "tool_result",
|
|
648
|
+
tool_use_id: call.id,
|
|
649
|
+
content: result.content,
|
|
650
|
+
is_error: result.isError
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
messages.push({ role: "user", content: toolResults });
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function makeError(subtype, errors, turns, costUsd, sessionId, startTime) {
|
|
657
|
+
return {
|
|
658
|
+
type: "result",
|
|
659
|
+
subtype,
|
|
660
|
+
errors,
|
|
661
|
+
turns,
|
|
662
|
+
costUsd,
|
|
663
|
+
durationMs: Date.now() - startTime,
|
|
664
|
+
sessionId,
|
|
665
|
+
uuid: uuid()
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// src/utils/cost.ts
|
|
670
|
+
var ANTHROPIC_PRICING = {
|
|
671
|
+
"claude-opus-4-6": {
|
|
672
|
+
inputPerMillion: 15,
|
|
673
|
+
outputPerMillion: 75,
|
|
674
|
+
cacheReadPerMillion: 1.5,
|
|
675
|
+
cacheWritePerMillion: 18.75
|
|
676
|
+
},
|
|
677
|
+
"claude-sonnet-4-5-20250929": {
|
|
678
|
+
inputPerMillion: 3,
|
|
679
|
+
outputPerMillion: 15,
|
|
680
|
+
cacheReadPerMillion: 0.3,
|
|
681
|
+
cacheWritePerMillion: 3.75
|
|
682
|
+
},
|
|
683
|
+
"claude-haiku-4-5-20251001": {
|
|
684
|
+
inputPerMillion: 0.8,
|
|
685
|
+
outputPerMillion: 4,
|
|
686
|
+
cacheReadPerMillion: 0.08,
|
|
687
|
+
cacheWritePerMillion: 1
|
|
688
|
+
},
|
|
689
|
+
"claude-3-5-sonnet-20241022": {
|
|
690
|
+
inputPerMillion: 3,
|
|
691
|
+
outputPerMillion: 15,
|
|
692
|
+
cacheReadPerMillion: 0.3,
|
|
693
|
+
cacheWritePerMillion: 3.75
|
|
694
|
+
},
|
|
695
|
+
"claude-3-5-haiku-20241022": {
|
|
696
|
+
inputPerMillion: 0.8,
|
|
697
|
+
outputPerMillion: 4,
|
|
698
|
+
cacheReadPerMillion: 0.08,
|
|
699
|
+
cacheWritePerMillion: 1
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
var ANTHROPIC_CONTEXT_WINDOWS = {
|
|
703
|
+
"claude-opus-4-6": 200000,
|
|
704
|
+
"claude-sonnet-4-5-20250929": 200000,
|
|
705
|
+
"claude-haiku-4-5-20251001": 200000,
|
|
706
|
+
"claude-3-5-sonnet-20241022": 200000,
|
|
707
|
+
"claude-3-5-haiku-20241022": 200000
|
|
708
|
+
};
|
|
709
|
+
var ANTHROPIC_MAX_OUTPUT = {
|
|
710
|
+
"claude-opus-4-6": 32000,
|
|
711
|
+
"claude-sonnet-4-5-20250929": 16384,
|
|
712
|
+
"claude-haiku-4-5-20251001": 8192,
|
|
713
|
+
"claude-3-5-sonnet-20241022": 8192,
|
|
714
|
+
"claude-3-5-haiku-20241022": 8192
|
|
715
|
+
};
|
|
716
|
+
function calculateAnthropicCost(model, usage) {
|
|
717
|
+
const pricing = findPricing(model);
|
|
718
|
+
if (!pricing)
|
|
719
|
+
return 0;
|
|
720
|
+
const inputCost = usage.inputTokens / 1e6 * pricing.inputPerMillion;
|
|
721
|
+
const outputCost = usage.outputTokens / 1e6 * pricing.outputPerMillion;
|
|
722
|
+
const cacheReadCost = usage.cacheReadInputTokens / 1e6 * (pricing.cacheReadPerMillion ?? pricing.inputPerMillion);
|
|
723
|
+
const cacheWriteCost = usage.cacheCreationInputTokens / 1e6 * (pricing.cacheWritePerMillion ?? pricing.inputPerMillion);
|
|
724
|
+
return inputCost + outputCost + cacheReadCost + cacheWriteCost;
|
|
725
|
+
}
|
|
726
|
+
function findPricing(model) {
|
|
727
|
+
if (ANTHROPIC_PRICING[model])
|
|
728
|
+
return ANTHROPIC_PRICING[model];
|
|
729
|
+
for (const [key, pricing] of Object.entries(ANTHROPIC_PRICING)) {
|
|
730
|
+
if (key.startsWith(model) || model.startsWith(key.split("-2025")[0])) {
|
|
731
|
+
return pricing;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
var OPENAI_PRICING = {
|
|
737
|
+
"gpt-4.1": {
|
|
738
|
+
inputPerMillion: 2,
|
|
739
|
+
outputPerMillion: 8,
|
|
740
|
+
cacheReadPerMillion: 0.5
|
|
741
|
+
},
|
|
742
|
+
"gpt-4.1-mini": {
|
|
743
|
+
inputPerMillion: 0.4,
|
|
744
|
+
outputPerMillion: 1.6,
|
|
745
|
+
cacheReadPerMillion: 0.1
|
|
746
|
+
},
|
|
747
|
+
"gpt-4.1-nano": {
|
|
748
|
+
inputPerMillion: 0.1,
|
|
749
|
+
outputPerMillion: 0.4,
|
|
750
|
+
cacheReadPerMillion: 0.025
|
|
751
|
+
},
|
|
752
|
+
"gpt-4o": {
|
|
753
|
+
inputPerMillion: 2.5,
|
|
754
|
+
outputPerMillion: 10,
|
|
755
|
+
cacheReadPerMillion: 1.25
|
|
756
|
+
},
|
|
757
|
+
"gpt-4o-mini": {
|
|
758
|
+
inputPerMillion: 0.15,
|
|
759
|
+
outputPerMillion: 0.6,
|
|
760
|
+
cacheReadPerMillion: 0.075
|
|
761
|
+
},
|
|
762
|
+
o3: {
|
|
763
|
+
inputPerMillion: 2,
|
|
764
|
+
outputPerMillion: 8,
|
|
765
|
+
cacheReadPerMillion: 0.5
|
|
766
|
+
},
|
|
767
|
+
"o3-mini": {
|
|
768
|
+
inputPerMillion: 1.1,
|
|
769
|
+
outputPerMillion: 4.4,
|
|
770
|
+
cacheReadPerMillion: 0.275
|
|
771
|
+
},
|
|
772
|
+
"o4-mini": {
|
|
773
|
+
inputPerMillion: 1.1,
|
|
774
|
+
outputPerMillion: 4.4,
|
|
775
|
+
cacheReadPerMillion: 0.275
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
var OPENAI_CONTEXT_WINDOWS = {
|
|
779
|
+
"gpt-4.1": 1047576,
|
|
780
|
+
"gpt-4.1-mini": 1047576,
|
|
781
|
+
"gpt-4.1-nano": 1047576,
|
|
782
|
+
"gpt-4o": 128000,
|
|
783
|
+
"gpt-4o-mini": 128000,
|
|
784
|
+
o3: 200000,
|
|
785
|
+
"o3-mini": 200000,
|
|
786
|
+
"o4-mini": 200000
|
|
787
|
+
};
|
|
788
|
+
var OPENAI_MAX_OUTPUT = {
|
|
789
|
+
"gpt-4.1": 32768,
|
|
790
|
+
"gpt-4.1-mini": 32768,
|
|
791
|
+
"gpt-4.1-nano": 32768,
|
|
792
|
+
"gpt-4o": 16384,
|
|
793
|
+
"gpt-4o-mini": 16384,
|
|
794
|
+
o3: 1e5,
|
|
795
|
+
"o3-mini": 1e5,
|
|
796
|
+
"o4-mini": 1e5
|
|
797
|
+
};
|
|
798
|
+
function calculateOpenAICost(model, usage) {
|
|
799
|
+
const pricing = findOpenAIPricing(model);
|
|
800
|
+
if (!pricing)
|
|
801
|
+
return 0;
|
|
802
|
+
const inputCost = usage.inputTokens / 1e6 * pricing.inputPerMillion;
|
|
803
|
+
const outputCost = usage.outputTokens / 1e6 * pricing.outputPerMillion;
|
|
804
|
+
const cacheReadCost = usage.cacheReadInputTokens / 1e6 * (pricing.cacheReadPerMillion ?? pricing.inputPerMillion);
|
|
805
|
+
return inputCost + outputCost + cacheReadCost;
|
|
806
|
+
}
|
|
807
|
+
function findOpenAIPricing(model) {
|
|
808
|
+
if (OPENAI_PRICING[model])
|
|
809
|
+
return OPENAI_PRICING[model];
|
|
810
|
+
let bestKey = "";
|
|
811
|
+
let bestPricing;
|
|
812
|
+
for (const [key, pricing] of Object.entries(OPENAI_PRICING)) {
|
|
813
|
+
if (model.startsWith(key) && key.length > bestKey.length) {
|
|
814
|
+
bestKey = key;
|
|
815
|
+
bestPricing = pricing;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return bestPricing;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// src/providers/anthropic.ts
|
|
822
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
823
|
+
function isOAuthToken(key) {
|
|
824
|
+
return key.includes("sk-ant-oat");
|
|
825
|
+
}
|
|
826
|
+
var CLAUDE_CODE_VERSION = "2.1.2";
|
|
827
|
+
|
|
828
|
+
class AnthropicAdapter {
|
|
829
|
+
name = "anthropic";
|
|
830
|
+
client;
|
|
831
|
+
oauthMode;
|
|
832
|
+
constructor(options) {
|
|
833
|
+
const key = options?.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
|
|
834
|
+
this.oauthMode = isOAuthToken(key);
|
|
835
|
+
if (this.oauthMode) {
|
|
836
|
+
this.client = new Anthropic({
|
|
837
|
+
apiKey: null,
|
|
838
|
+
authToken: key,
|
|
839
|
+
baseURL: options?.baseUrl,
|
|
840
|
+
defaultHeaders: {
|
|
841
|
+
"anthropic-beta": "claude-code-20250219,oauth-2025-04-20,fine-grained-tool-streaming-2025-05-14",
|
|
842
|
+
"user-agent": `claude-cli/${CLAUDE_CODE_VERSION} (external, cli)`,
|
|
843
|
+
"x-app": "cli"
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
} else {
|
|
847
|
+
this.client = new Anthropic({
|
|
848
|
+
apiKey: key || undefined,
|
|
849
|
+
baseURL: options?.baseUrl
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
async* chat(request) {
|
|
854
|
+
const messages = this.convertMessages(request.messages);
|
|
855
|
+
const tools = request.tools ? this.convertTools(request.tools) : undefined;
|
|
856
|
+
const maxTokens = request.maxTokens ?? ANTHROPIC_MAX_OUTPUT[request.model] ?? 16384;
|
|
857
|
+
const params = {
|
|
858
|
+
model: request.model,
|
|
859
|
+
messages,
|
|
860
|
+
max_tokens: maxTokens,
|
|
861
|
+
stream: true
|
|
862
|
+
};
|
|
863
|
+
if (this.oauthMode) {
|
|
864
|
+
const systemBlocks = [
|
|
865
|
+
{ type: "text", text: "You are Claude Code, Anthropic's official CLI for Claude." }
|
|
866
|
+
];
|
|
867
|
+
if (request.systemPrompt) {
|
|
868
|
+
systemBlocks.push({ type: "text", text: request.systemPrompt });
|
|
869
|
+
}
|
|
870
|
+
params.system = systemBlocks;
|
|
871
|
+
} else if (request.systemPrompt) {
|
|
872
|
+
params.system = request.systemPrompt;
|
|
873
|
+
}
|
|
874
|
+
if (tools && tools.length > 0) {
|
|
875
|
+
params.tools = tools;
|
|
876
|
+
}
|
|
877
|
+
if (request.temperature !== undefined) {
|
|
878
|
+
params.temperature = request.temperature;
|
|
879
|
+
}
|
|
880
|
+
const stream = this.client.messages.stream(params, {
|
|
881
|
+
signal: request.signal
|
|
882
|
+
});
|
|
883
|
+
const toolInputBuffers = new Map;
|
|
884
|
+
for await (const event of stream) {
|
|
885
|
+
switch (event.type) {
|
|
886
|
+
case "content_block_start": {
|
|
887
|
+
const block = event.content_block;
|
|
888
|
+
if (block.type === "tool_use") {
|
|
889
|
+
toolInputBuffers.set(event.index, {
|
|
890
|
+
id: block.id,
|
|
891
|
+
name: block.name,
|
|
892
|
+
json: ""
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
case "content_block_delta": {
|
|
898
|
+
const delta = event.delta;
|
|
899
|
+
if (delta.type === "text_delta") {
|
|
900
|
+
yield { type: "text_delta", text: delta.text };
|
|
901
|
+
} else if (delta.type === "input_json_delta") {
|
|
902
|
+
const buffer = toolInputBuffers.get(event.index);
|
|
903
|
+
if (buffer) {
|
|
904
|
+
buffer.json += delta.partial_json;
|
|
905
|
+
}
|
|
906
|
+
} else if (delta.type === "thinking_delta" && "thinking" in delta) {
|
|
907
|
+
yield { type: "thinking_delta", text: delta.thinking };
|
|
908
|
+
}
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
case "content_block_stop": {
|
|
912
|
+
const buffer = toolInputBuffers.get(event.index);
|
|
913
|
+
if (buffer) {
|
|
914
|
+
let input;
|
|
915
|
+
try {
|
|
916
|
+
input = buffer.json ? JSON.parse(buffer.json) : {};
|
|
917
|
+
} catch {
|
|
918
|
+
input = {};
|
|
919
|
+
}
|
|
920
|
+
yield {
|
|
921
|
+
type: "tool_call",
|
|
922
|
+
id: buffer.id,
|
|
923
|
+
name: buffer.name,
|
|
924
|
+
input
|
|
925
|
+
};
|
|
926
|
+
toolInputBuffers.delete(event.index);
|
|
927
|
+
}
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
case "message_delta": {
|
|
931
|
+
const stopReason = this.mapStopReason(event.delta.stop_reason);
|
|
932
|
+
if (event.usage) {
|
|
933
|
+
yield {
|
|
934
|
+
type: "usage",
|
|
935
|
+
usage: {
|
|
936
|
+
inputTokens: 0,
|
|
937
|
+
outputTokens: event.usage.output_tokens ?? 0,
|
|
938
|
+
cacheReadInputTokens: 0,
|
|
939
|
+
cacheCreationInputTokens: 0
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
yield { type: "done", stopReason };
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
case "message_start": {
|
|
947
|
+
if (event.message.usage) {
|
|
948
|
+
yield {
|
|
949
|
+
type: "usage",
|
|
950
|
+
usage: {
|
|
951
|
+
inputTokens: event.message.usage.input_tokens ?? 0,
|
|
952
|
+
outputTokens: event.message.usage.output_tokens ?? 0,
|
|
953
|
+
cacheReadInputTokens: event.message.usage.cache_read_input_tokens ?? 0,
|
|
954
|
+
cacheCreationInputTokens: event.message.usage.cache_creation_input_tokens ?? 0
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
calculateCost(model, usage) {
|
|
964
|
+
return calculateAnthropicCost(model, usage);
|
|
965
|
+
}
|
|
966
|
+
getContextWindow(model) {
|
|
967
|
+
return ANTHROPIC_CONTEXT_WINDOWS[model] ?? 200000;
|
|
968
|
+
}
|
|
969
|
+
supportsFeature(feature) {
|
|
970
|
+
switch (feature) {
|
|
971
|
+
case "streaming":
|
|
972
|
+
case "tool_calling":
|
|
973
|
+
case "image_input":
|
|
974
|
+
case "pdf_input":
|
|
975
|
+
case "thinking":
|
|
976
|
+
case "structured_output":
|
|
977
|
+
return true;
|
|
978
|
+
default:
|
|
979
|
+
return false;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
convertMessages(messages) {
|
|
983
|
+
return messages.map((msg) => {
|
|
984
|
+
if (typeof msg.content === "string") {
|
|
985
|
+
return { role: msg.role, content: msg.content };
|
|
986
|
+
}
|
|
987
|
+
const content = msg.content.map((block) => {
|
|
988
|
+
switch (block.type) {
|
|
989
|
+
case "text":
|
|
990
|
+
return { type: "text", text: block.text };
|
|
991
|
+
case "tool_use":
|
|
992
|
+
return {
|
|
993
|
+
type: "tool_use",
|
|
994
|
+
id: block.id,
|
|
995
|
+
name: block.name,
|
|
996
|
+
input: block.input
|
|
997
|
+
};
|
|
998
|
+
case "tool_result":
|
|
999
|
+
return {
|
|
1000
|
+
type: "tool_result",
|
|
1001
|
+
tool_use_id: block.tool_use_id,
|
|
1002
|
+
content: block.content,
|
|
1003
|
+
is_error: block.is_error
|
|
1004
|
+
};
|
|
1005
|
+
default:
|
|
1006
|
+
return { type: "text", text: String(block) };
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
return { role: msg.role, content };
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
convertTools(tools) {
|
|
1013
|
+
return tools.map((tool) => ({
|
|
1014
|
+
name: tool.name,
|
|
1015
|
+
description: tool.description,
|
|
1016
|
+
input_schema: tool.inputSchema
|
|
1017
|
+
}));
|
|
1018
|
+
}
|
|
1019
|
+
mapStopReason(reason) {
|
|
1020
|
+
switch (reason) {
|
|
1021
|
+
case "end_turn":
|
|
1022
|
+
return "end_turn";
|
|
1023
|
+
case "tool_use":
|
|
1024
|
+
return "tool_use";
|
|
1025
|
+
case "max_tokens":
|
|
1026
|
+
return "max_tokens";
|
|
1027
|
+
case "stop_sequence":
|
|
1028
|
+
return "stop_sequence";
|
|
1029
|
+
default:
|
|
1030
|
+
return "end_turn";
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/providers/openai.ts
|
|
1036
|
+
import OpenAI from "openai";
|
|
1037
|
+
var CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex";
|
|
1038
|
+
var CODEX_DEFAULT_MODEL = "gpt-5.3-codex";
|
|
1039
|
+
var CODEX_MODELS = new Set([
|
|
1040
|
+
"gpt-5.3-codex",
|
|
1041
|
+
"gpt-5.2-codex",
|
|
1042
|
+
"gpt-5.1-codex-mini",
|
|
1043
|
+
"gpt-5.1-codex-max",
|
|
1044
|
+
"gpt-5.1-codex",
|
|
1045
|
+
"gpt-5-codex",
|
|
1046
|
+
"gpt-5-codex-mini"
|
|
1047
|
+
]);
|
|
1048
|
+
|
|
1049
|
+
class OpenAIAdapter {
|
|
1050
|
+
name = "openai";
|
|
1051
|
+
client;
|
|
1052
|
+
codexMode;
|
|
1053
|
+
accountId;
|
|
1054
|
+
currentAccessToken;
|
|
1055
|
+
constructor(options) {
|
|
1056
|
+
const key = options?.apiKey ?? process.env.OPENAI_API_KEY;
|
|
1057
|
+
if (key) {
|
|
1058
|
+
this.codexMode = false;
|
|
1059
|
+
this.client = new OpenAI({
|
|
1060
|
+
apiKey: key,
|
|
1061
|
+
baseURL: options?.baseUrl
|
|
1062
|
+
});
|
|
1063
|
+
} else {
|
|
1064
|
+
const tokens = loadTokensSync();
|
|
1065
|
+
if (tokens) {
|
|
1066
|
+
this.codexMode = true;
|
|
1067
|
+
this.accountId = tokens.account_id;
|
|
1068
|
+
this.currentAccessToken = tokens.access_token;
|
|
1069
|
+
this.client = new OpenAI({
|
|
1070
|
+
apiKey: tokens.access_token,
|
|
1071
|
+
baseURL: options?.baseUrl ?? CODEX_BASE_URL,
|
|
1072
|
+
defaultHeaders: {
|
|
1073
|
+
"chatgpt-account-id": tokens.account_id,
|
|
1074
|
+
originator: "codex_cli_rs"
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
} else {
|
|
1078
|
+
this.codexMode = false;
|
|
1079
|
+
this.client = new OpenAI({
|
|
1080
|
+
apiKey: undefined,
|
|
1081
|
+
baseURL: options?.baseUrl
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
async* chat(request) {
|
|
1087
|
+
if (this.codexMode) {
|
|
1088
|
+
yield* this.chatResponses(request);
|
|
1089
|
+
} else {
|
|
1090
|
+
yield* this.chatCompletions(request);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
calculateCost(model, usage) {
|
|
1094
|
+
return calculateOpenAICost(model, usage);
|
|
1095
|
+
}
|
|
1096
|
+
getContextWindow(model) {
|
|
1097
|
+
return OPENAI_CONTEXT_WINDOWS[model] ?? 128000;
|
|
1098
|
+
}
|
|
1099
|
+
supportsFeature(feature) {
|
|
1100
|
+
switch (feature) {
|
|
1101
|
+
case "streaming":
|
|
1102
|
+
case "tool_calling":
|
|
1103
|
+
case "image_input":
|
|
1104
|
+
case "structured_output":
|
|
1105
|
+
return true;
|
|
1106
|
+
case "thinking":
|
|
1107
|
+
case "pdf_input":
|
|
1108
|
+
return false;
|
|
1109
|
+
default:
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
async* chatCompletions(request) {
|
|
1114
|
+
const messages = this.convertMessages(request.messages, request.systemPrompt);
|
|
1115
|
+
const tools = request.tools ? this.convertTools(request.tools) : undefined;
|
|
1116
|
+
const maxTokens = request.maxTokens ?? OPENAI_MAX_OUTPUT[request.model] ?? 16384;
|
|
1117
|
+
const params = {
|
|
1118
|
+
model: request.model,
|
|
1119
|
+
messages,
|
|
1120
|
+
max_completion_tokens: maxTokens,
|
|
1121
|
+
stream: true,
|
|
1122
|
+
stream_options: { include_usage: true }
|
|
1123
|
+
};
|
|
1124
|
+
if (tools && tools.length > 0) {
|
|
1125
|
+
params.tools = tools;
|
|
1126
|
+
}
|
|
1127
|
+
if (request.temperature !== undefined) {
|
|
1128
|
+
params.temperature = request.temperature;
|
|
1129
|
+
}
|
|
1130
|
+
const stream = await this.client.chat.completions.create(params, {
|
|
1131
|
+
signal: request.signal ?? undefined
|
|
1132
|
+
});
|
|
1133
|
+
const toolCallBuffers = new Map;
|
|
1134
|
+
let finishReason = null;
|
|
1135
|
+
for await (const chunk of stream) {
|
|
1136
|
+
if (chunk.usage) {
|
|
1137
|
+
const cached = chunk.usage.prompt_tokens_details?.cached_tokens ?? 0;
|
|
1138
|
+
yield {
|
|
1139
|
+
type: "usage",
|
|
1140
|
+
usage: {
|
|
1141
|
+
inputTokens: chunk.usage.prompt_tokens - cached,
|
|
1142
|
+
outputTokens: chunk.usage.completion_tokens,
|
|
1143
|
+
cacheReadInputTokens: cached,
|
|
1144
|
+
cacheCreationInputTokens: 0
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
const choice = chunk.choices?.[0];
|
|
1149
|
+
if (!choice)
|
|
1150
|
+
continue;
|
|
1151
|
+
if (choice.finish_reason) {
|
|
1152
|
+
finishReason = choice.finish_reason;
|
|
1153
|
+
}
|
|
1154
|
+
const delta = choice.delta;
|
|
1155
|
+
if (delta.content) {
|
|
1156
|
+
yield { type: "text_delta", text: delta.content };
|
|
1157
|
+
}
|
|
1158
|
+
if (delta.tool_calls) {
|
|
1159
|
+
for (const tc of delta.tool_calls) {
|
|
1160
|
+
const idx = tc.index;
|
|
1161
|
+
if (!toolCallBuffers.has(idx)) {
|
|
1162
|
+
toolCallBuffers.set(idx, {
|
|
1163
|
+
id: tc.id ?? "",
|
|
1164
|
+
name: tc.function?.name ?? "",
|
|
1165
|
+
args: ""
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
const buf = toolCallBuffers.get(idx);
|
|
1169
|
+
if (tc.id)
|
|
1170
|
+
buf.id = tc.id;
|
|
1171
|
+
if (tc.function?.name)
|
|
1172
|
+
buf.name = tc.function.name;
|
|
1173
|
+
if (tc.function?.arguments)
|
|
1174
|
+
buf.args += tc.function.arguments;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
for (const [, buf] of toolCallBuffers) {
|
|
1179
|
+
let input;
|
|
1180
|
+
try {
|
|
1181
|
+
input = buf.args ? JSON.parse(buf.args) : {};
|
|
1182
|
+
} catch {
|
|
1183
|
+
input = {};
|
|
1184
|
+
}
|
|
1185
|
+
yield {
|
|
1186
|
+
type: "tool_call",
|
|
1187
|
+
id: buf.id,
|
|
1188
|
+
name: buf.name,
|
|
1189
|
+
input
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
yield { type: "done", stopReason: this.mapStopReason(finishReason) };
|
|
1193
|
+
}
|
|
1194
|
+
async* chatResponses(request) {
|
|
1195
|
+
await this.refreshTokenIfNeeded();
|
|
1196
|
+
const input = this.convertMessagesForResponses(request.messages);
|
|
1197
|
+
const tools = request.tools ? this.convertToolsForResponses(request.tools) : undefined;
|
|
1198
|
+
const model = CODEX_MODELS.has(request.model) ? request.model : CODEX_DEFAULT_MODEL;
|
|
1199
|
+
const params = {
|
|
1200
|
+
model,
|
|
1201
|
+
input,
|
|
1202
|
+
instructions: request.systemPrompt || "You are a helpful coding assistant.",
|
|
1203
|
+
store: false,
|
|
1204
|
+
stream: true
|
|
1205
|
+
};
|
|
1206
|
+
if (tools && tools.length > 0) {
|
|
1207
|
+
params.tools = tools;
|
|
1208
|
+
}
|
|
1209
|
+
if (request.temperature !== undefined) {
|
|
1210
|
+
params.temperature = request.temperature;
|
|
1211
|
+
}
|
|
1212
|
+
const stream = await this.client.responses.create(params, {
|
|
1213
|
+
signal: request.signal ?? undefined
|
|
1214
|
+
});
|
|
1215
|
+
for await (const event of stream) {
|
|
1216
|
+
switch (event.type) {
|
|
1217
|
+
case "response.output_text.delta":
|
|
1218
|
+
yield { type: "text_delta", text: event.delta };
|
|
1219
|
+
break;
|
|
1220
|
+
case "response.output_item.done": {
|
|
1221
|
+
const item = event.item;
|
|
1222
|
+
if (item?.type === "function_call") {
|
|
1223
|
+
let parsedInput;
|
|
1224
|
+
try {
|
|
1225
|
+
parsedInput = item.arguments ? JSON.parse(item.arguments) : {};
|
|
1226
|
+
} catch {
|
|
1227
|
+
parsedInput = {};
|
|
1228
|
+
}
|
|
1229
|
+
yield {
|
|
1230
|
+
type: "tool_call",
|
|
1231
|
+
id: item.call_id,
|
|
1232
|
+
name: item.name,
|
|
1233
|
+
input: parsedInput
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
break;
|
|
1237
|
+
}
|
|
1238
|
+
case "response.completed": {
|
|
1239
|
+
const resp = event.response;
|
|
1240
|
+
if (resp?.usage) {
|
|
1241
|
+
const u = resp.usage;
|
|
1242
|
+
const cached = u.input_tokens_details?.cached_tokens ?? 0;
|
|
1243
|
+
yield {
|
|
1244
|
+
type: "usage",
|
|
1245
|
+
usage: {
|
|
1246
|
+
inputTokens: u.input_tokens - cached,
|
|
1247
|
+
outputTokens: u.output_tokens,
|
|
1248
|
+
cacheReadInputTokens: cached,
|
|
1249
|
+
cacheCreationInputTokens: 0
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
yield { type: "done", stopReason: this.mapResponseStatus(resp) };
|
|
1254
|
+
break;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
async refreshTokenIfNeeded() {
|
|
1260
|
+
if (!this.codexMode)
|
|
1261
|
+
return;
|
|
1262
|
+
try {
|
|
1263
|
+
const { getValidToken: getValidToken2 } = await Promise.resolve().then(() => (init_openai_oauth(), exports_openai_oauth));
|
|
1264
|
+
const tokens = await getValidToken2();
|
|
1265
|
+
if (tokens && tokens.accessToken !== this.currentAccessToken) {
|
|
1266
|
+
this.currentAccessToken = tokens.accessToken;
|
|
1267
|
+
this.client = new OpenAI({
|
|
1268
|
+
apiKey: tokens.accessToken,
|
|
1269
|
+
baseURL: CODEX_BASE_URL,
|
|
1270
|
+
defaultHeaders: {
|
|
1271
|
+
"chatgpt-account-id": tokens.accountId,
|
|
1272
|
+
originator: "codex_cli_rs"
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
} catch {}
|
|
1277
|
+
}
|
|
1278
|
+
convertMessages(messages, systemPrompt) {
|
|
1279
|
+
const result = [];
|
|
1280
|
+
if (systemPrompt) {
|
|
1281
|
+
result.push({ role: "developer", content: systemPrompt });
|
|
1282
|
+
}
|
|
1283
|
+
for (const msg of messages) {
|
|
1284
|
+
if (typeof msg.content === "string") {
|
|
1285
|
+
result.push({ role: msg.role, content: msg.content });
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
if (msg.role === "assistant") {
|
|
1289
|
+
const textParts = [];
|
|
1290
|
+
const toolCalls = [];
|
|
1291
|
+
for (const block of msg.content) {
|
|
1292
|
+
if (block.type === "text") {
|
|
1293
|
+
textParts.push(block.text);
|
|
1294
|
+
} else if (block.type === "tool_use") {
|
|
1295
|
+
toolCalls.push({
|
|
1296
|
+
id: block.id,
|
|
1297
|
+
type: "function",
|
|
1298
|
+
function: {
|
|
1299
|
+
name: block.name,
|
|
1300
|
+
arguments: JSON.stringify(block.input)
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
const assistantMsg = {
|
|
1306
|
+
role: "assistant",
|
|
1307
|
+
content: textParts.length > 0 ? textParts.join("") : null
|
|
1308
|
+
};
|
|
1309
|
+
if (toolCalls.length > 0) {
|
|
1310
|
+
assistantMsg.tool_calls = toolCalls;
|
|
1311
|
+
}
|
|
1312
|
+
result.push(assistantMsg);
|
|
1313
|
+
} else {
|
|
1314
|
+
const toolResults = [];
|
|
1315
|
+
const textParts = [];
|
|
1316
|
+
for (const block of msg.content) {
|
|
1317
|
+
if (block.type === "tool_result") {
|
|
1318
|
+
toolResults.push(block);
|
|
1319
|
+
} else if (block.type === "text") {
|
|
1320
|
+
textParts.push(block.text);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
for (const tr of toolResults) {
|
|
1324
|
+
result.push({
|
|
1325
|
+
role: "tool",
|
|
1326
|
+
tool_call_id: tr.tool_use_id,
|
|
1327
|
+
content: tr.content
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
if (textParts.length > 0) {
|
|
1331
|
+
result.push({ role: "user", content: textParts.join("") });
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
return result;
|
|
1336
|
+
}
|
|
1337
|
+
convertMessagesForResponses(messages) {
|
|
1338
|
+
const result = [];
|
|
1339
|
+
for (const msg of messages) {
|
|
1340
|
+
if (typeof msg.content === "string") {
|
|
1341
|
+
result.push({ role: msg.role, content: msg.content });
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
if (msg.role === "assistant") {
|
|
1345
|
+
const textParts = [];
|
|
1346
|
+
const toolUses = [];
|
|
1347
|
+
for (const block of msg.content) {
|
|
1348
|
+
if (block.type === "text") {
|
|
1349
|
+
textParts.push(block.text);
|
|
1350
|
+
} else if (block.type === "tool_use") {
|
|
1351
|
+
toolUses.push(block);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
if (textParts.length > 0) {
|
|
1355
|
+
result.push({
|
|
1356
|
+
role: "assistant",
|
|
1357
|
+
content: [{ type: "output_text", text: textParts.join("") }]
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
for (const tu of toolUses) {
|
|
1361
|
+
result.push({
|
|
1362
|
+
type: "function_call",
|
|
1363
|
+
call_id: tu.id,
|
|
1364
|
+
name: tu.name,
|
|
1365
|
+
arguments: JSON.stringify(tu.input)
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
} else if (msg.role === "user") {
|
|
1369
|
+
const textParts = [];
|
|
1370
|
+
const toolResults = [];
|
|
1371
|
+
for (const block of msg.content) {
|
|
1372
|
+
if (block.type === "tool_result") {
|
|
1373
|
+
toolResults.push(block);
|
|
1374
|
+
} else if (block.type === "text") {
|
|
1375
|
+
textParts.push(block.text);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
for (const tr of toolResults) {
|
|
1379
|
+
result.push({
|
|
1380
|
+
type: "function_call_output",
|
|
1381
|
+
call_id: tr.tool_use_id,
|
|
1382
|
+
output: tr.content
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
if (textParts.length > 0) {
|
|
1386
|
+
result.push({ role: "user", content: textParts.join("") });
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return result;
|
|
1391
|
+
}
|
|
1392
|
+
convertTools(tools) {
|
|
1393
|
+
return tools.map((tool) => ({
|
|
1394
|
+
type: "function",
|
|
1395
|
+
function: {
|
|
1396
|
+
name: tool.name,
|
|
1397
|
+
description: tool.description,
|
|
1398
|
+
parameters: tool.inputSchema
|
|
1399
|
+
}
|
|
1400
|
+
}));
|
|
1401
|
+
}
|
|
1402
|
+
convertToolsForResponses(tools) {
|
|
1403
|
+
return tools.map((tool) => ({
|
|
1404
|
+
type: "function",
|
|
1405
|
+
name: tool.name,
|
|
1406
|
+
description: tool.description,
|
|
1407
|
+
parameters: tool.inputSchema,
|
|
1408
|
+
strict: false
|
|
1409
|
+
}));
|
|
1410
|
+
}
|
|
1411
|
+
mapStopReason(reason) {
|
|
1412
|
+
switch (reason) {
|
|
1413
|
+
case "stop":
|
|
1414
|
+
return "end_turn";
|
|
1415
|
+
case "tool_calls":
|
|
1416
|
+
return "tool_use";
|
|
1417
|
+
case "length":
|
|
1418
|
+
return "max_tokens";
|
|
1419
|
+
default:
|
|
1420
|
+
return "end_turn";
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
mapResponseStatus(response) {
|
|
1424
|
+
if (!response)
|
|
1425
|
+
return "end_turn";
|
|
1426
|
+
const hasToolCalls = response.output?.some?.((item) => item.type === "function_call");
|
|
1427
|
+
if (hasToolCalls)
|
|
1428
|
+
return "tool_use";
|
|
1429
|
+
if (response.status === "incomplete") {
|
|
1430
|
+
const reason = response.incomplete_details?.reason;
|
|
1431
|
+
if (reason === "max_output_tokens")
|
|
1432
|
+
return "max_tokens";
|
|
1433
|
+
}
|
|
1434
|
+
return "end_turn";
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
function loadTokensSync() {
|
|
1438
|
+
const paths = [
|
|
1439
|
+
`${process.env.HOME}/.fourmis/openai-auth.json`,
|
|
1440
|
+
`${process.env.HOME}/.codex/auth.json`
|
|
1441
|
+
];
|
|
1442
|
+
for (const p of paths) {
|
|
1443
|
+
try {
|
|
1444
|
+
const fs = __require("fs");
|
|
1445
|
+
const raw = fs.readFileSync(p, "utf-8");
|
|
1446
|
+
const data = JSON.parse(raw);
|
|
1447
|
+
if (data.access_token && data.account_id) {
|
|
1448
|
+
return data;
|
|
1449
|
+
}
|
|
1450
|
+
} catch {}
|
|
1451
|
+
}
|
|
1452
|
+
return null;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// src/providers/registry.ts
|
|
1456
|
+
var providers = new Map;
|
|
1457
|
+
function registerProvider(name, adapter) {
|
|
1458
|
+
providers.set(name, adapter);
|
|
1459
|
+
}
|
|
1460
|
+
function getProvider(name, options) {
|
|
1461
|
+
if (!options?.apiKey && !options?.baseUrl) {
|
|
1462
|
+
const existing = providers.get(name);
|
|
1463
|
+
if (existing)
|
|
1464
|
+
return existing;
|
|
1465
|
+
}
|
|
1466
|
+
if (name === "anthropic") {
|
|
1467
|
+
const adapter = new AnthropicAdapter(options);
|
|
1468
|
+
if (!options?.apiKey && !options?.baseUrl) {
|
|
1469
|
+
providers.set(name, adapter);
|
|
1470
|
+
}
|
|
1471
|
+
return adapter;
|
|
1472
|
+
}
|
|
1473
|
+
if (name === "openai") {
|
|
1474
|
+
const adapter = new OpenAIAdapter(options);
|
|
1475
|
+
if (!options?.apiKey && !options?.baseUrl) {
|
|
1476
|
+
providers.set(name, adapter);
|
|
1477
|
+
}
|
|
1478
|
+
return adapter;
|
|
1479
|
+
}
|
|
1480
|
+
throw new Error(`Unknown provider: "${name}". Register it with registerProvider() first.`);
|
|
1481
|
+
}
|
|
1482
|
+
function listProviders() {
|
|
1483
|
+
return [...providers.keys()];
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// src/tools/registry.ts
|
|
1487
|
+
class ToolRegistry {
|
|
1488
|
+
tools = new Map;
|
|
1489
|
+
register(tool) {
|
|
1490
|
+
this.tools.set(tool.name, tool);
|
|
1491
|
+
}
|
|
1492
|
+
get(name) {
|
|
1493
|
+
return this.tools.get(name);
|
|
1494
|
+
}
|
|
1495
|
+
has(name) {
|
|
1496
|
+
return this.tools.has(name);
|
|
1497
|
+
}
|
|
1498
|
+
getDefinitions() {
|
|
1499
|
+
return [...this.tools.values()].map((tool) => ({
|
|
1500
|
+
name: tool.name,
|
|
1501
|
+
description: tool.description,
|
|
1502
|
+
inputSchema: tool.inputSchema
|
|
1503
|
+
}));
|
|
1504
|
+
}
|
|
1505
|
+
async execute(name, input, ctx) {
|
|
1506
|
+
const tool = this.tools.get(name);
|
|
1507
|
+
if (!tool) {
|
|
1508
|
+
return { content: `Unknown tool: ${name}`, isError: true };
|
|
1509
|
+
}
|
|
1510
|
+
try {
|
|
1511
|
+
return await tool.execute(input, ctx);
|
|
1512
|
+
} catch (err) {
|
|
1513
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1514
|
+
return { content: `Tool error: ${message}`, isError: true };
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
list() {
|
|
1518
|
+
return [...this.tools.keys()];
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// src/tools/presets.ts
|
|
1523
|
+
var PRESETS = {
|
|
1524
|
+
coding: ["Bash", "Read", "Write", "Edit", "Glob", "Grep"],
|
|
1525
|
+
readonly: ["Read", "Glob", "Grep"],
|
|
1526
|
+
minimal: ["Read", "Write", "Edit", "Glob", "Grep"]
|
|
1527
|
+
};
|
|
1528
|
+
function resolveToolNames(tools) {
|
|
1529
|
+
if (!tools)
|
|
1530
|
+
return PRESETS.coding;
|
|
1531
|
+
if (typeof tools === "string") {
|
|
1532
|
+
return PRESETS[tools] ?? [tools];
|
|
1533
|
+
}
|
|
1534
|
+
return tools;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// src/tools/bash.ts
|
|
1538
|
+
var DEFAULT_TIMEOUT = 120000;
|
|
1539
|
+
var MAX_TIMEOUT = 600000;
|
|
1540
|
+
var MAX_OUTPUT_LENGTH = 30000;
|
|
1541
|
+
var BashTool = {
|
|
1542
|
+
name: "Bash",
|
|
1543
|
+
description: "Executes a bash command. Use for system operations, git commands, running scripts, and other terminal tasks. " + "Working directory persists between calls. Commands timeout after 120s by default (max 600s).",
|
|
1544
|
+
inputSchema: {
|
|
1545
|
+
type: "object",
|
|
1546
|
+
properties: {
|
|
1547
|
+
command: {
|
|
1548
|
+
type: "string",
|
|
1549
|
+
description: "The bash command to execute"
|
|
1550
|
+
},
|
|
1551
|
+
description: {
|
|
1552
|
+
type: "string",
|
|
1553
|
+
description: "Brief description of what this command does"
|
|
1554
|
+
},
|
|
1555
|
+
timeout: {
|
|
1556
|
+
type: "number",
|
|
1557
|
+
description: "Timeout in milliseconds (max 600000)"
|
|
1558
|
+
}
|
|
1559
|
+
},
|
|
1560
|
+
required: ["command"]
|
|
1561
|
+
},
|
|
1562
|
+
async execute(input, ctx) {
|
|
1563
|
+
const { command, timeout: timeoutMs, description } = input;
|
|
1564
|
+
if (!command || typeof command !== "string") {
|
|
1565
|
+
return { content: "Error: command is required", isError: true };
|
|
1566
|
+
}
|
|
1567
|
+
const timeout = Math.min(timeoutMs ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
|
|
1568
|
+
try {
|
|
1569
|
+
const proc = Bun.spawn(["bash", "-c", command], {
|
|
1570
|
+
cwd: ctx.cwd,
|
|
1571
|
+
stdout: "pipe",
|
|
1572
|
+
stderr: "pipe",
|
|
1573
|
+
env: { ...process.env, ...ctx.env }
|
|
1574
|
+
});
|
|
1575
|
+
const timeoutId = setTimeout(() => {
|
|
1576
|
+
proc.kill();
|
|
1577
|
+
}, timeout);
|
|
1578
|
+
const [stdout, stderr] = await Promise.all([
|
|
1579
|
+
new Response(proc.stdout).text(),
|
|
1580
|
+
new Response(proc.stderr).text()
|
|
1581
|
+
]);
|
|
1582
|
+
const exitCode = await proc.exited;
|
|
1583
|
+
clearTimeout(timeoutId);
|
|
1584
|
+
let output = "";
|
|
1585
|
+
if (stdout)
|
|
1586
|
+
output += stdout;
|
|
1587
|
+
if (stderr)
|
|
1588
|
+
output += (output ? `
|
|
1589
|
+
` : "") + stderr;
|
|
1590
|
+
if (output.length > MAX_OUTPUT_LENGTH) {
|
|
1591
|
+
output = output.slice(0, MAX_OUTPUT_LENGTH) + `
|
|
1592
|
+
... (output truncated)`;
|
|
1593
|
+
}
|
|
1594
|
+
if (!output) {
|
|
1595
|
+
output = exitCode === 0 ? "(no output)" : `Command failed with exit code ${exitCode}`;
|
|
1596
|
+
}
|
|
1597
|
+
return {
|
|
1598
|
+
content: output,
|
|
1599
|
+
isError: exitCode !== 0 ? true : undefined,
|
|
1600
|
+
metadata: { exitCode }
|
|
1601
|
+
};
|
|
1602
|
+
} catch (err) {
|
|
1603
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1604
|
+
return { content: `Error executing command: ${message}`, isError: true };
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
|
|
1609
|
+
// src/tools/read.ts
|
|
1610
|
+
var MAX_LINE_LENGTH = 2000;
|
|
1611
|
+
var DEFAULT_LINE_LIMIT = 2000;
|
|
1612
|
+
var ReadTool = {
|
|
1613
|
+
name: "Read",
|
|
1614
|
+
description: "Reads a file from the filesystem. Returns content with line numbers (cat -n format). " + "Supports offset/limit for large files. Lines longer than 2000 chars are truncated.",
|
|
1615
|
+
inputSchema: {
|
|
1616
|
+
type: "object",
|
|
1617
|
+
properties: {
|
|
1618
|
+
file_path: {
|
|
1619
|
+
type: "string",
|
|
1620
|
+
description: "Absolute path to the file to read"
|
|
1621
|
+
},
|
|
1622
|
+
offset: {
|
|
1623
|
+
type: "number",
|
|
1624
|
+
description: "Line number to start reading from (1-based)"
|
|
1625
|
+
},
|
|
1626
|
+
limit: {
|
|
1627
|
+
type: "number",
|
|
1628
|
+
description: "Number of lines to read"
|
|
1629
|
+
}
|
|
1630
|
+
},
|
|
1631
|
+
required: ["file_path"]
|
|
1632
|
+
},
|
|
1633
|
+
async execute(input, ctx) {
|
|
1634
|
+
const { file_path, offset, limit } = input;
|
|
1635
|
+
if (!file_path) {
|
|
1636
|
+
return { content: "Error: file_path is required", isError: true };
|
|
1637
|
+
}
|
|
1638
|
+
const resolvedPath = resolvePath(file_path, ctx.cwd);
|
|
1639
|
+
try {
|
|
1640
|
+
const file = Bun.file(resolvedPath);
|
|
1641
|
+
const exists = await file.exists();
|
|
1642
|
+
if (!exists) {
|
|
1643
|
+
return { content: `Error: File not found: ${resolvedPath}`, isError: true };
|
|
1644
|
+
}
|
|
1645
|
+
const text = await file.text();
|
|
1646
|
+
const lines = text.split(`
|
|
1647
|
+
`);
|
|
1648
|
+
const startLine = Math.max(1, offset ?? 1);
|
|
1649
|
+
const lineLimit = limit ?? DEFAULT_LINE_LIMIT;
|
|
1650
|
+
const endLine = Math.min(lines.length, startLine + lineLimit - 1);
|
|
1651
|
+
const numberedLines = [];
|
|
1652
|
+
for (let i = startLine - 1;i < endLine; i++) {
|
|
1653
|
+
let line = lines[i];
|
|
1654
|
+
if (line.length > MAX_LINE_LENGTH) {
|
|
1655
|
+
line = line.slice(0, MAX_LINE_LENGTH) + "... (truncated)";
|
|
1656
|
+
}
|
|
1657
|
+
const lineNum = String(i + 1).padStart(6, " ");
|
|
1658
|
+
numberedLines.push(`${lineNum} ${line}`);
|
|
1659
|
+
}
|
|
1660
|
+
return { content: numberedLines.join(`
|
|
1661
|
+
`) };
|
|
1662
|
+
} catch (err) {
|
|
1663
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1664
|
+
return { content: `Error reading file: ${message}`, isError: true };
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
function resolvePath(filePath, cwd) {
|
|
1669
|
+
if (filePath.startsWith("/"))
|
|
1670
|
+
return filePath;
|
|
1671
|
+
return `${cwd}/${filePath}`;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// src/tools/write.ts
|
|
1675
|
+
import { mkdir } from "fs/promises";
|
|
1676
|
+
import { dirname } from "path";
|
|
1677
|
+
var WriteTool = {
|
|
1678
|
+
name: "Write",
|
|
1679
|
+
description: "Writes content to a file. Creates parent directories if needed. " + "Overwrites existing files.",
|
|
1680
|
+
inputSchema: {
|
|
1681
|
+
type: "object",
|
|
1682
|
+
properties: {
|
|
1683
|
+
file_path: {
|
|
1684
|
+
type: "string",
|
|
1685
|
+
description: "Absolute path to the file to write"
|
|
1686
|
+
},
|
|
1687
|
+
content: {
|
|
1688
|
+
type: "string",
|
|
1689
|
+
description: "The content to write to the file"
|
|
1690
|
+
}
|
|
1691
|
+
},
|
|
1692
|
+
required: ["file_path", "content"]
|
|
1693
|
+
},
|
|
1694
|
+
async execute(input, ctx) {
|
|
1695
|
+
const { file_path, content } = input;
|
|
1696
|
+
if (!file_path) {
|
|
1697
|
+
return { content: "Error: file_path is required", isError: true };
|
|
1698
|
+
}
|
|
1699
|
+
if (content === undefined || content === null) {
|
|
1700
|
+
return { content: "Error: content is required", isError: true };
|
|
1701
|
+
}
|
|
1702
|
+
const resolvedPath = file_path.startsWith("/") ? file_path : `${ctx.cwd}/${file_path}`;
|
|
1703
|
+
try {
|
|
1704
|
+
const dir = dirname(resolvedPath);
|
|
1705
|
+
await mkdir(dir, { recursive: true });
|
|
1706
|
+
await Bun.write(resolvedPath, content);
|
|
1707
|
+
const lines = content.split(`
|
|
1708
|
+
`).length;
|
|
1709
|
+
return {
|
|
1710
|
+
content: `Successfully wrote ${lines} lines to ${resolvedPath}`,
|
|
1711
|
+
metadata: { path: resolvedPath, lines }
|
|
1712
|
+
};
|
|
1713
|
+
} catch (err) {
|
|
1714
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1715
|
+
return { content: `Error writing file: ${message}`, isError: true };
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
// src/tools/edit.ts
|
|
1721
|
+
var EditTool = {
|
|
1722
|
+
name: "Edit",
|
|
1723
|
+
description: "Performs exact string replacements in files. The old_string must be unique in the file " + "unless replace_all is true. Use this for precise edits to existing files.",
|
|
1724
|
+
inputSchema: {
|
|
1725
|
+
type: "object",
|
|
1726
|
+
properties: {
|
|
1727
|
+
file_path: {
|
|
1728
|
+
type: "string",
|
|
1729
|
+
description: "Absolute path to the file to edit"
|
|
1730
|
+
},
|
|
1731
|
+
old_string: {
|
|
1732
|
+
type: "string",
|
|
1733
|
+
description: "The exact text to find and replace"
|
|
1734
|
+
},
|
|
1735
|
+
new_string: {
|
|
1736
|
+
type: "string",
|
|
1737
|
+
description: "The replacement text"
|
|
1738
|
+
},
|
|
1739
|
+
replace_all: {
|
|
1740
|
+
type: "boolean",
|
|
1741
|
+
description: "Replace all occurrences (default: false)",
|
|
1742
|
+
default: false
|
|
1743
|
+
}
|
|
1744
|
+
},
|
|
1745
|
+
required: ["file_path", "old_string", "new_string"]
|
|
1746
|
+
},
|
|
1747
|
+
async execute(input, ctx) {
|
|
1748
|
+
const { file_path, old_string, new_string, replace_all = false } = input;
|
|
1749
|
+
if (!file_path) {
|
|
1750
|
+
return { content: "Error: file_path is required", isError: true };
|
|
1751
|
+
}
|
|
1752
|
+
if (old_string === undefined) {
|
|
1753
|
+
return { content: "Error: old_string is required", isError: true };
|
|
1754
|
+
}
|
|
1755
|
+
if (new_string === undefined) {
|
|
1756
|
+
return { content: "Error: new_string is required", isError: true };
|
|
1757
|
+
}
|
|
1758
|
+
if (old_string === new_string) {
|
|
1759
|
+
return { content: "Error: old_string and new_string are identical", isError: true };
|
|
1760
|
+
}
|
|
1761
|
+
const resolvedPath = file_path.startsWith("/") ? file_path : `${ctx.cwd}/${file_path}`;
|
|
1762
|
+
try {
|
|
1763
|
+
const file = Bun.file(resolvedPath);
|
|
1764
|
+
const exists = await file.exists();
|
|
1765
|
+
if (!exists) {
|
|
1766
|
+
return { content: `Error: File not found: ${resolvedPath}`, isError: true };
|
|
1767
|
+
}
|
|
1768
|
+
const content = await file.text();
|
|
1769
|
+
let count = 0;
|
|
1770
|
+
let searchFrom = 0;
|
|
1771
|
+
while (true) {
|
|
1772
|
+
const idx = content.indexOf(old_string, searchFrom);
|
|
1773
|
+
if (idx === -1)
|
|
1774
|
+
break;
|
|
1775
|
+
count++;
|
|
1776
|
+
searchFrom = idx + old_string.length;
|
|
1777
|
+
}
|
|
1778
|
+
if (count === 0) {
|
|
1779
|
+
const preview = old_string.length > 100 ? old_string.slice(0, 100) + "..." : old_string;
|
|
1780
|
+
return {
|
|
1781
|
+
content: `Error: old_string not found in ${resolvedPath}. Searched for:
|
|
1782
|
+
${preview}`,
|
|
1783
|
+
isError: true
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
if (count > 1 && !replace_all) {
|
|
1787
|
+
return {
|
|
1788
|
+
content: `Error: old_string appears ${count} times in the file. Use replace_all: true to replace all occurrences, or provide a longer string with more context to make it unique.`,
|
|
1789
|
+
isError: true
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
let newContent;
|
|
1793
|
+
if (replace_all) {
|
|
1794
|
+
newContent = content.replaceAll(old_string, new_string);
|
|
1795
|
+
} else {
|
|
1796
|
+
const idx = content.indexOf(old_string);
|
|
1797
|
+
newContent = content.slice(0, idx) + new_string + content.slice(idx + old_string.length);
|
|
1798
|
+
}
|
|
1799
|
+
await Bun.write(resolvedPath, newContent);
|
|
1800
|
+
const replacements = replace_all ? count : 1;
|
|
1801
|
+
return {
|
|
1802
|
+
content: `Successfully replaced ${replacements} occurrence${replacements > 1 ? "s" : ""} in ${resolvedPath}`,
|
|
1803
|
+
metadata: { path: resolvedPath, replacements }
|
|
1804
|
+
};
|
|
1805
|
+
} catch (err) {
|
|
1806
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1807
|
+
return { content: `Error editing file: ${message}`, isError: true };
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
|
|
1812
|
+
// src/tools/glob.ts
|
|
1813
|
+
var {Glob } = globalThis.Bun;
|
|
1814
|
+
var GlobTool = {
|
|
1815
|
+
name: "Glob",
|
|
1816
|
+
description: "Fast file pattern matching. Supports glob patterns like '**/*.ts' or 'src/**/*.tsx'. " + "Returns matching file paths sorted by modification time.",
|
|
1817
|
+
inputSchema: {
|
|
1818
|
+
type: "object",
|
|
1819
|
+
properties: {
|
|
1820
|
+
pattern: {
|
|
1821
|
+
type: "string",
|
|
1822
|
+
description: "Glob pattern to match files against"
|
|
1823
|
+
},
|
|
1824
|
+
path: {
|
|
1825
|
+
type: "string",
|
|
1826
|
+
description: "Directory to search in (defaults to cwd)"
|
|
1827
|
+
}
|
|
1828
|
+
},
|
|
1829
|
+
required: ["pattern"]
|
|
1830
|
+
},
|
|
1831
|
+
async execute(input, ctx) {
|
|
1832
|
+
const { pattern, path } = input;
|
|
1833
|
+
if (!pattern) {
|
|
1834
|
+
return { content: "Error: pattern is required", isError: true };
|
|
1835
|
+
}
|
|
1836
|
+
const searchDir = path ? path.startsWith("/") ? path : `${ctx.cwd}/${path}` : ctx.cwd;
|
|
1837
|
+
try {
|
|
1838
|
+
const glob = new Glob(pattern);
|
|
1839
|
+
const matches = [];
|
|
1840
|
+
for await (const filePath of glob.scan({ cwd: searchDir, dot: false })) {
|
|
1841
|
+
try {
|
|
1842
|
+
const file = Bun.file(`${searchDir}/${filePath}`);
|
|
1843
|
+
const stat = await file.stat();
|
|
1844
|
+
matches.push({ path: filePath, mtime: stat?.mtime?.getTime() ?? 0 });
|
|
1845
|
+
} catch {
|
|
1846
|
+
matches.push({ path: filePath, mtime: 0 });
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
matches.sort((a, b) => b.mtime - a.mtime);
|
|
1850
|
+
if (matches.length === 0) {
|
|
1851
|
+
return { content: "No files matched the pattern." };
|
|
1852
|
+
}
|
|
1853
|
+
const result = matches.map((m) => m.path).join(`
|
|
1854
|
+
`);
|
|
1855
|
+
return {
|
|
1856
|
+
content: result,
|
|
1857
|
+
metadata: { count: matches.length }
|
|
1858
|
+
};
|
|
1859
|
+
} catch (err) {
|
|
1860
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1861
|
+
return { content: `Error: ${message}`, isError: true };
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
};
|
|
1865
|
+
|
|
1866
|
+
// src/tools/grep.ts
|
|
1867
|
+
var GrepTool = {
|
|
1868
|
+
name: "Grep",
|
|
1869
|
+
description: "Search file contents using regex patterns. Supports multiple output modes: " + "'content' (matching lines), 'files_with_matches' (file paths only, default), 'count' (match counts). " + "Supports context lines, case-insensitive search, glob filtering, and head_limit.",
|
|
1870
|
+
inputSchema: {
|
|
1871
|
+
type: "object",
|
|
1872
|
+
properties: {
|
|
1873
|
+
pattern: {
|
|
1874
|
+
type: "string",
|
|
1875
|
+
description: "Regex pattern to search for"
|
|
1876
|
+
},
|
|
1877
|
+
path: {
|
|
1878
|
+
type: "string",
|
|
1879
|
+
description: "File or directory to search in (defaults to cwd)"
|
|
1880
|
+
},
|
|
1881
|
+
glob: {
|
|
1882
|
+
type: "string",
|
|
1883
|
+
description: "Glob pattern to filter files (e.g., '*.ts')"
|
|
1884
|
+
},
|
|
1885
|
+
output_mode: {
|
|
1886
|
+
type: "string",
|
|
1887
|
+
enum: ["content", "files_with_matches", "count"],
|
|
1888
|
+
description: "Output mode (default: files_with_matches)"
|
|
1889
|
+
},
|
|
1890
|
+
"-i": {
|
|
1891
|
+
type: "boolean",
|
|
1892
|
+
description: "Case insensitive search"
|
|
1893
|
+
},
|
|
1894
|
+
"-n": {
|
|
1895
|
+
type: "boolean",
|
|
1896
|
+
description: "Show line numbers (for content mode)"
|
|
1897
|
+
},
|
|
1898
|
+
"-A": {
|
|
1899
|
+
type: "number",
|
|
1900
|
+
description: "Lines to show after each match"
|
|
1901
|
+
},
|
|
1902
|
+
"-B": {
|
|
1903
|
+
type: "number",
|
|
1904
|
+
description: "Lines to show before each match"
|
|
1905
|
+
},
|
|
1906
|
+
"-C": {
|
|
1907
|
+
type: "number",
|
|
1908
|
+
description: "Context lines before and after each match"
|
|
1909
|
+
},
|
|
1910
|
+
head_limit: {
|
|
1911
|
+
type: "number",
|
|
1912
|
+
description: "Limit output to first N entries"
|
|
1913
|
+
}
|
|
1914
|
+
},
|
|
1915
|
+
required: ["pattern"]
|
|
1916
|
+
},
|
|
1917
|
+
async execute(input, ctx) {
|
|
1918
|
+
const opts = input;
|
|
1919
|
+
if (!opts.pattern) {
|
|
1920
|
+
return { content: "Error: pattern is required", isError: true };
|
|
1921
|
+
}
|
|
1922
|
+
const searchPath = opts.path ? opts.path.startsWith("/") ? opts.path : `${ctx.cwd}/${opts.path}` : ctx.cwd;
|
|
1923
|
+
const mode = opts.output_mode ?? "files_with_matches";
|
|
1924
|
+
try {
|
|
1925
|
+
return await runRipgrep(opts, searchPath, mode, ctx);
|
|
1926
|
+
} catch {
|
|
1927
|
+
return await runJsGrep(opts, searchPath, mode, ctx);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
};
|
|
1931
|
+
async function runRipgrep(opts, searchPath, mode, ctx) {
|
|
1932
|
+
const args = ["rg"];
|
|
1933
|
+
if (mode === "files_with_matches") {
|
|
1934
|
+
args.push("-l");
|
|
1935
|
+
} else if (mode === "count") {
|
|
1936
|
+
args.push("-c");
|
|
1937
|
+
}
|
|
1938
|
+
if (opts["-i"])
|
|
1939
|
+
args.push("-i");
|
|
1940
|
+
if (opts["-n"] !== false && mode === "content")
|
|
1941
|
+
args.push("-n");
|
|
1942
|
+
if (opts["-A"])
|
|
1943
|
+
args.push("-A", String(opts["-A"]));
|
|
1944
|
+
if (opts["-B"])
|
|
1945
|
+
args.push("-B", String(opts["-B"]));
|
|
1946
|
+
if (opts["-C"])
|
|
1947
|
+
args.push("-C", String(opts["-C"]));
|
|
1948
|
+
if (opts.glob)
|
|
1949
|
+
args.push("--glob", opts.glob);
|
|
1950
|
+
args.push("--", opts.pattern, searchPath);
|
|
1951
|
+
const proc = Bun.spawn(args, {
|
|
1952
|
+
stdout: "pipe",
|
|
1953
|
+
stderr: "pipe",
|
|
1954
|
+
env: { ...process.env, ...ctx.env }
|
|
1955
|
+
});
|
|
1956
|
+
const stdout = await new Response(proc.stdout).text();
|
|
1957
|
+
const stderr = await new Response(proc.stderr).text();
|
|
1958
|
+
const exitCode = await proc.exited;
|
|
1959
|
+
if (exitCode === 2) {
|
|
1960
|
+
throw new Error(stderr || "ripgrep error");
|
|
1961
|
+
}
|
|
1962
|
+
let output = stdout.trim();
|
|
1963
|
+
if (opts.head_limit && output) {
|
|
1964
|
+
const lines = output.split(`
|
|
1965
|
+
`);
|
|
1966
|
+
output = lines.slice(0, opts.head_limit).join(`
|
|
1967
|
+
`);
|
|
1968
|
+
}
|
|
1969
|
+
return { content: output || "No matches found." };
|
|
1970
|
+
}
|
|
1971
|
+
async function runJsGrep(opts, searchPath, mode, ctx) {
|
|
1972
|
+
const flags = opts["-i"] ? "gi" : "g";
|
|
1973
|
+
let regex;
|
|
1974
|
+
try {
|
|
1975
|
+
regex = new RegExp(opts.pattern, flags);
|
|
1976
|
+
} catch (err) {
|
|
1977
|
+
return { content: `Invalid regex: ${opts.pattern}`, isError: true };
|
|
1978
|
+
}
|
|
1979
|
+
const files = await collectFiles(searchPath, opts.glob);
|
|
1980
|
+
const results = [];
|
|
1981
|
+
let totalCount = 0;
|
|
1982
|
+
for (const filePath of files) {
|
|
1983
|
+
try {
|
|
1984
|
+
const content = await Bun.file(filePath).text();
|
|
1985
|
+
const lines = content.split(`
|
|
1986
|
+
`);
|
|
1987
|
+
const matchedLines = [];
|
|
1988
|
+
for (let i = 0;i < lines.length; i++) {
|
|
1989
|
+
if (regex.test(lines[i])) {
|
|
1990
|
+
matchedLines.push({ num: i + 1, line: lines[i] });
|
|
1991
|
+
regex.lastIndex = 0;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
if (matchedLines.length === 0)
|
|
1995
|
+
continue;
|
|
1996
|
+
totalCount += matchedLines.length;
|
|
1997
|
+
const relativePath = filePath.startsWith(ctx.cwd) ? filePath.slice(ctx.cwd.length + 1) : filePath;
|
|
1998
|
+
if (mode === "files_with_matches") {
|
|
1999
|
+
results.push(relativePath);
|
|
2000
|
+
} else if (mode === "count") {
|
|
2001
|
+
results.push(`${relativePath}:${matchedLines.length}`);
|
|
2002
|
+
} else {
|
|
2003
|
+
for (const { num, line } of matchedLines) {
|
|
2004
|
+
results.push(`${relativePath}:${num}:${line}`);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
} catch {}
|
|
2008
|
+
if (opts.head_limit && results.length >= opts.head_limit) {
|
|
2009
|
+
break;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
let output = results.join(`
|
|
2013
|
+
`);
|
|
2014
|
+
if (opts.head_limit) {
|
|
2015
|
+
const entries = output.split(`
|
|
2016
|
+
`).slice(0, opts.head_limit);
|
|
2017
|
+
output = entries.join(`
|
|
2018
|
+
`);
|
|
2019
|
+
}
|
|
2020
|
+
return { content: output || "No matches found." };
|
|
2021
|
+
}
|
|
2022
|
+
async function collectFiles(dir, globPattern) {
|
|
2023
|
+
const { Glob: Glob2 } = await Promise.resolve(globalThis.Bun);
|
|
2024
|
+
const pattern = globPattern ?? "**/*";
|
|
2025
|
+
const glob = new Glob2(pattern);
|
|
2026
|
+
const files = [];
|
|
2027
|
+
for await (const path of glob.scan({ cwd: dir, dot: false, onlyFiles: true })) {
|
|
2028
|
+
files.push(`${dir}/${path}`);
|
|
2029
|
+
}
|
|
2030
|
+
return files;
|
|
2031
|
+
}
|
|
2032
|
+
// src/tools/index.ts
|
|
2033
|
+
var ALL_TOOLS = {
|
|
2034
|
+
Bash: BashTool,
|
|
2035
|
+
Read: ReadTool,
|
|
2036
|
+
Write: WriteTool,
|
|
2037
|
+
Edit: EditTool,
|
|
2038
|
+
Glob: GlobTool,
|
|
2039
|
+
Grep: GrepTool
|
|
2040
|
+
};
|
|
2041
|
+
function buildToolRegistry(toolNames, allowedTools, disallowedTools) {
|
|
2042
|
+
const registry = new ToolRegistry;
|
|
2043
|
+
for (const name of toolNames) {
|
|
2044
|
+
if (allowedTools && !allowedTools.includes(name))
|
|
2045
|
+
continue;
|
|
2046
|
+
if (disallowedTools?.includes(name))
|
|
2047
|
+
continue;
|
|
2048
|
+
const tool = ALL_TOOLS[name];
|
|
2049
|
+
if (tool) {
|
|
2050
|
+
registry.register(tool);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
return registry;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// src/agents/tools.ts
|
|
2057
|
+
function createTaskTool(ctx) {
|
|
2058
|
+
return {
|
|
2059
|
+
name: "Task",
|
|
2060
|
+
description: "Launch a subagent to handle a task. Specify the agent type and a prompt describing what to do.",
|
|
2061
|
+
inputSchema: {
|
|
2062
|
+
type: "object",
|
|
2063
|
+
properties: {
|
|
2064
|
+
description: {
|
|
2065
|
+
type: "string",
|
|
2066
|
+
description: "A short description of the task (3-5 words)."
|
|
2067
|
+
},
|
|
2068
|
+
prompt: {
|
|
2069
|
+
type: "string",
|
|
2070
|
+
description: "The detailed task prompt for the subagent."
|
|
2071
|
+
},
|
|
2072
|
+
subagent_type: {
|
|
2073
|
+
type: "string",
|
|
2074
|
+
description: "The type of agent to use. Must match a registered agent definition."
|
|
2075
|
+
},
|
|
2076
|
+
run_in_background: {
|
|
2077
|
+
type: "boolean",
|
|
2078
|
+
description: "If true, run the task in the background and return a task ID."
|
|
2079
|
+
},
|
|
2080
|
+
max_turns: {
|
|
2081
|
+
type: "number",
|
|
2082
|
+
description: "Maximum number of turns for the subagent."
|
|
2083
|
+
}
|
|
2084
|
+
},
|
|
2085
|
+
required: ["prompt", "subagent_type"]
|
|
2086
|
+
},
|
|
2087
|
+
async execute(input, toolCtx) {
|
|
2088
|
+
const {
|
|
2089
|
+
prompt,
|
|
2090
|
+
subagent_type,
|
|
2091
|
+
run_in_background,
|
|
2092
|
+
max_turns
|
|
2093
|
+
} = input;
|
|
2094
|
+
const agentDef = ctx.agents[subagent_type];
|
|
2095
|
+
if (!agentDef) {
|
|
2096
|
+
const available = Object.keys(ctx.agents).join(", ");
|
|
2097
|
+
return {
|
|
2098
|
+
content: `Unknown agent type "${subagent_type}". Available: ${available || "none"}`,
|
|
2099
|
+
isError: true
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
if (ctx.parentHooks) {
|
|
2103
|
+
await ctx.parentHooks.fire("SubagentStart", { event: "SubagentStart", agent_type: subagent_type, session_id: toolCtx.sessionId }, undefined, { signal: toolCtx.signal });
|
|
2104
|
+
}
|
|
2105
|
+
const provider = agentDef.provider ? getProvider(agentDef.provider) : ctx.parentProvider;
|
|
2106
|
+
const model = agentDef.model ?? ctx.parentModel;
|
|
2107
|
+
let subTools;
|
|
2108
|
+
if (agentDef.tools) {
|
|
2109
|
+
subTools = buildToolRegistry(agentDef.tools);
|
|
2110
|
+
} else {
|
|
2111
|
+
subTools = buildToolRegistry(resolveToolNames("coding"));
|
|
2112
|
+
}
|
|
2113
|
+
const maxTurns = max_turns ?? agentDef.maxTurns ?? 10;
|
|
2114
|
+
const sessionId = uuid();
|
|
2115
|
+
const abortController = new AbortController;
|
|
2116
|
+
if (toolCtx.signal) {
|
|
2117
|
+
toolCtx.signal.addEventListener("abort", () => abortController.abort(), { once: true });
|
|
2118
|
+
}
|
|
2119
|
+
const systemPrompt = `${agentDef.prompt}
|
|
2120
|
+
|
|
2121
|
+
You are a subagent of type "${subagent_type}". ${agentDef.description}`;
|
|
2122
|
+
const runAgent = async () => {
|
|
2123
|
+
const messages = [];
|
|
2124
|
+
let resultText = "";
|
|
2125
|
+
for await (const msg of agentLoop(prompt, {
|
|
2126
|
+
provider,
|
|
2127
|
+
model,
|
|
2128
|
+
systemPrompt,
|
|
2129
|
+
tools: subTools,
|
|
2130
|
+
permissions: ctx.parentPermissions,
|
|
2131
|
+
cwd: ctx.parentCwd,
|
|
2132
|
+
sessionId,
|
|
2133
|
+
maxTurns,
|
|
2134
|
+
maxBudgetUsd: 5,
|
|
2135
|
+
includeStreamEvents: false,
|
|
2136
|
+
signal: abortController.signal,
|
|
2137
|
+
env: ctx.parentEnv,
|
|
2138
|
+
debug: ctx.parentDebug,
|
|
2139
|
+
hooks: ctx.parentHooks
|
|
2140
|
+
})) {
|
|
2141
|
+
messages.push(msg);
|
|
2142
|
+
if (msg.type === "text") {
|
|
2143
|
+
resultText += msg.text;
|
|
2144
|
+
}
|
|
2145
|
+
if (msg.type === "result" && msg.subtype === "success") {
|
|
2146
|
+
resultText = msg.text ?? resultText;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
return resultText || "Subagent completed with no text output.";
|
|
2150
|
+
};
|
|
2151
|
+
if (run_in_background) {
|
|
2152
|
+
const taskId = uuid();
|
|
2153
|
+
const task = {
|
|
2154
|
+
id: taskId,
|
|
2155
|
+
agentType: subagent_type,
|
|
2156
|
+
status: "running",
|
|
2157
|
+
abortController,
|
|
2158
|
+
promise: (async () => {
|
|
2159
|
+
try {
|
|
2160
|
+
const result = await runAgent();
|
|
2161
|
+
const t = ctx.taskManager.get(taskId);
|
|
2162
|
+
if (t && t.status === "running") {
|
|
2163
|
+
t.status = "completed";
|
|
2164
|
+
t.result = result;
|
|
2165
|
+
}
|
|
2166
|
+
} catch (err) {
|
|
2167
|
+
const t = ctx.taskManager.get(taskId);
|
|
2168
|
+
if (t && t.status === "running") {
|
|
2169
|
+
t.status = "failed";
|
|
2170
|
+
t.error = err instanceof Error ? err.message : String(err);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
})()
|
|
2174
|
+
};
|
|
2175
|
+
ctx.taskManager.register(task);
|
|
2176
|
+
return {
|
|
2177
|
+
content: `Background task started with ID: ${taskId}. Use TaskOutput to check results.`
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
try {
|
|
2181
|
+
const result = await runAgent();
|
|
2182
|
+
if (ctx.parentHooks) {
|
|
2183
|
+
await ctx.parentHooks.fire("SubagentStop", { event: "SubagentStop", agent_type: subagent_type, session_id: toolCtx.sessionId }, undefined, { signal: toolCtx.signal });
|
|
2184
|
+
}
|
|
2185
|
+
return { content: result };
|
|
2186
|
+
} catch (err) {
|
|
2187
|
+
if (ctx.parentHooks) {
|
|
2188
|
+
await ctx.parentHooks.fire("SubagentStop", { event: "SubagentStop", agent_type: subagent_type, session_id: toolCtx.sessionId }, undefined, { signal: toolCtx.signal });
|
|
2189
|
+
}
|
|
2190
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2191
|
+
return { content: `Subagent error: ${message}`, isError: true };
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
function createTaskOutputTool(taskManager) {
|
|
2197
|
+
return {
|
|
2198
|
+
name: "TaskOutput",
|
|
2199
|
+
description: "Get the output from a background task.",
|
|
2200
|
+
inputSchema: {
|
|
2201
|
+
type: "object",
|
|
2202
|
+
properties: {
|
|
2203
|
+
task_id: {
|
|
2204
|
+
type: "string",
|
|
2205
|
+
description: "The ID of the background task."
|
|
2206
|
+
},
|
|
2207
|
+
block: {
|
|
2208
|
+
type: "boolean",
|
|
2209
|
+
description: "Whether to wait for the task to complete. Default: true."
|
|
2210
|
+
},
|
|
2211
|
+
timeout: {
|
|
2212
|
+
type: "number",
|
|
2213
|
+
description: "Max wait time in milliseconds. Default: 30000."
|
|
2214
|
+
}
|
|
2215
|
+
},
|
|
2216
|
+
required: ["task_id"]
|
|
2217
|
+
},
|
|
2218
|
+
async execute(input) {
|
|
2219
|
+
const { task_id, block = true, timeout = 30000 } = input;
|
|
2220
|
+
const output = await taskManager.getOutput(task_id, block, timeout);
|
|
2221
|
+
return { content: output };
|
|
2222
|
+
}
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
function createTaskStopTool(taskManager) {
|
|
2226
|
+
return {
|
|
2227
|
+
name: "TaskStop",
|
|
2228
|
+
description: "Stop a running background task.",
|
|
2229
|
+
inputSchema: {
|
|
2230
|
+
type: "object",
|
|
2231
|
+
properties: {
|
|
2232
|
+
task_id: {
|
|
2233
|
+
type: "string",
|
|
2234
|
+
description: "The ID of the background task to stop."
|
|
2235
|
+
}
|
|
2236
|
+
},
|
|
2237
|
+
required: ["task_id"]
|
|
2238
|
+
},
|
|
2239
|
+
async execute(input) {
|
|
2240
|
+
const { task_id } = input;
|
|
2241
|
+
const stopped = taskManager.stop(task_id);
|
|
2242
|
+
if (stopped) {
|
|
2243
|
+
return { content: `Task "${task_id}" has been stopped.` };
|
|
2244
|
+
}
|
|
2245
|
+
const task = taskManager.get(task_id);
|
|
2246
|
+
if (!task) {
|
|
2247
|
+
return { content: `Task "${task_id}" not found.`, isError: true };
|
|
2248
|
+
}
|
|
2249
|
+
return { content: `Task "${task_id}" is already ${task.status}.` };
|
|
2250
|
+
}
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
// src/agents/task-manager.ts
|
|
2255
|
+
class TaskManager {
|
|
2256
|
+
tasks = new Map;
|
|
2257
|
+
register(task) {
|
|
2258
|
+
this.tasks.set(task.id, task);
|
|
2259
|
+
}
|
|
2260
|
+
get(id) {
|
|
2261
|
+
return this.tasks.get(id);
|
|
2262
|
+
}
|
|
2263
|
+
stop(id) {
|
|
2264
|
+
const task = this.tasks.get(id);
|
|
2265
|
+
if (!task)
|
|
2266
|
+
return false;
|
|
2267
|
+
if (task.status !== "running")
|
|
2268
|
+
return false;
|
|
2269
|
+
task.abortController.abort();
|
|
2270
|
+
task.status = "stopped";
|
|
2271
|
+
return true;
|
|
2272
|
+
}
|
|
2273
|
+
async getOutput(id, block, timeoutMs) {
|
|
2274
|
+
const task = this.tasks.get(id);
|
|
2275
|
+
if (!task) {
|
|
2276
|
+
return `Task "${id}" not found.`;
|
|
2277
|
+
}
|
|
2278
|
+
if (task.status !== "running") {
|
|
2279
|
+
return this.formatOutput(task);
|
|
2280
|
+
}
|
|
2281
|
+
if (!block) {
|
|
2282
|
+
return `Task "${id}" is still running.`;
|
|
2283
|
+
}
|
|
2284
|
+
const timeoutPromise = new Promise((resolve) => setTimeout(resolve, timeoutMs));
|
|
2285
|
+
await Promise.race([task.promise, timeoutPromise]);
|
|
2286
|
+
if (task.status === "running") {
|
|
2287
|
+
return `Task "${id}" is still running (timed out after ${timeoutMs}ms).`;
|
|
2288
|
+
}
|
|
2289
|
+
return this.formatOutput(task);
|
|
2290
|
+
}
|
|
2291
|
+
list() {
|
|
2292
|
+
return [...this.tasks.values()];
|
|
2293
|
+
}
|
|
2294
|
+
formatOutput(task) {
|
|
2295
|
+
if (task.status === "failed") {
|
|
2296
|
+
return `Task "${task.id}" failed: ${task.error ?? "unknown error"}`;
|
|
2297
|
+
}
|
|
2298
|
+
if (task.status === "stopped") {
|
|
2299
|
+
return `Task "${task.id}" was stopped.`;
|
|
2300
|
+
}
|
|
2301
|
+
return task.result ?? `Task "${task.id}" completed with no output.`;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
export {
|
|
2305
|
+
createTaskTool,
|
|
2306
|
+
createTaskStopTool,
|
|
2307
|
+
createTaskOutputTool,
|
|
2308
|
+
TaskManager
|
|
2309
|
+
};
|