formagent-sdk 0.1.3 → 0.3.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/dist/cli/index.js +3118 -746
- package/dist/index.js +2445 -106
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -5,8 +5,9 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
5
5
|
// src/cli/cli.ts
|
|
6
6
|
import * as readline from "node:readline";
|
|
7
7
|
import { homedir as homedir4 } from "node:os";
|
|
8
|
-
import { join as
|
|
8
|
+
import { join as join8 } from "node:path";
|
|
9
9
|
import { existsSync as existsSync10 } from "node:fs";
|
|
10
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
10
11
|
|
|
11
12
|
// src/utils/id.ts
|
|
12
13
|
var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
@@ -76,6 +77,113 @@ class TypedEventEmitter {
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
// src/utils/truncation.ts
|
|
81
|
+
import * as fs from "node:fs/promises";
|
|
82
|
+
import * as path from "node:path";
|
|
83
|
+
import * as os from "node:os";
|
|
84
|
+
var TRUNCATION_DEFAULTS = {
|
|
85
|
+
MAX_LINES: 2000,
|
|
86
|
+
MAX_BYTES: 50 * 1024,
|
|
87
|
+
RETENTION_MS: 7 * 24 * 60 * 60 * 1000
|
|
88
|
+
};
|
|
89
|
+
function getTempDir(config) {
|
|
90
|
+
return config?.tempDir ?? path.join(os.tmpdir(), "formagent-sdk-output");
|
|
91
|
+
}
|
|
92
|
+
function generateOutputFilename() {
|
|
93
|
+
const timestamp = Date.now();
|
|
94
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
95
|
+
return `tool_${timestamp}_${random}.txt`;
|
|
96
|
+
}
|
|
97
|
+
async function ensureTempDir(dir) {
|
|
98
|
+
try {
|
|
99
|
+
await fs.mkdir(dir, { recursive: true });
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
async function truncateOutput(text, config = {}) {
|
|
103
|
+
const maxLines = config.maxLines ?? TRUNCATION_DEFAULTS.MAX_LINES;
|
|
104
|
+
const maxBytes = config.maxBytes ?? TRUNCATION_DEFAULTS.MAX_BYTES;
|
|
105
|
+
const direction = config.direction ?? "head";
|
|
106
|
+
const saveToFile = config.saveToFile ?? true;
|
|
107
|
+
const lines = text.split(`
|
|
108
|
+
`);
|
|
109
|
+
const totalBytes = Buffer.byteLength(text, "utf-8");
|
|
110
|
+
if (lines.length <= maxLines && totalBytes <= maxBytes) {
|
|
111
|
+
return {
|
|
112
|
+
content: text,
|
|
113
|
+
truncated: false,
|
|
114
|
+
originalBytes: totalBytes,
|
|
115
|
+
originalLines: lines.length
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const out = [];
|
|
119
|
+
let bytes = 0;
|
|
120
|
+
let hitBytes = false;
|
|
121
|
+
if (direction === "head") {
|
|
122
|
+
for (let i = 0;i < lines.length && i < maxLines; i++) {
|
|
123
|
+
const lineBytes = Buffer.byteLength(lines[i], "utf-8") + (i > 0 ? 1 : 0);
|
|
124
|
+
if (bytes + lineBytes > maxBytes) {
|
|
125
|
+
hitBytes = true;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
out.push(lines[i]);
|
|
129
|
+
bytes += lineBytes;
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
for (let i = lines.length - 1;i >= 0 && out.length < maxLines; i--) {
|
|
133
|
+
const lineBytes = Buffer.byteLength(lines[i], "utf-8") + (out.length > 0 ? 1 : 0);
|
|
134
|
+
if (bytes + lineBytes > maxBytes) {
|
|
135
|
+
hitBytes = true;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
out.unshift(lines[i]);
|
|
139
|
+
bytes += lineBytes;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const removed = hitBytes ? totalBytes - bytes : lines.length - out.length;
|
|
143
|
+
const unit = hitBytes ? "bytes" : "lines";
|
|
144
|
+
const preview = out.join(`
|
|
145
|
+
`);
|
|
146
|
+
let outputPath;
|
|
147
|
+
if (saveToFile) {
|
|
148
|
+
const dir = getTempDir(config);
|
|
149
|
+
await ensureTempDir(dir);
|
|
150
|
+
outputPath = path.join(dir, generateOutputFilename());
|
|
151
|
+
await fs.writeFile(outputPath, text, "utf-8");
|
|
152
|
+
}
|
|
153
|
+
const hint = outputPath ? `Full output saved to: ${outputPath}
|
|
154
|
+
Use Read tool with offset/limit to view specific sections, or Grep to search the content.` : "Output was truncated. Consider using more specific queries.";
|
|
155
|
+
const message = direction === "head" ? `${preview}
|
|
156
|
+
|
|
157
|
+
...${removed} ${unit} truncated...
|
|
158
|
+
|
|
159
|
+
${hint}` : `...${removed} ${unit} truncated...
|
|
160
|
+
|
|
161
|
+
${hint}
|
|
162
|
+
|
|
163
|
+
${preview}`;
|
|
164
|
+
return {
|
|
165
|
+
content: message,
|
|
166
|
+
truncated: true,
|
|
167
|
+
outputPath,
|
|
168
|
+
originalBytes: totalBytes,
|
|
169
|
+
originalLines: lines.length,
|
|
170
|
+
truncatedBytes: bytes,
|
|
171
|
+
truncatedLines: out.length
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
async function truncateToolOutput(output, config) {
|
|
175
|
+
const result = await truncateOutput(output, config);
|
|
176
|
+
return result.content;
|
|
177
|
+
}
|
|
178
|
+
function needsTruncation(text, config = {}) {
|
|
179
|
+
const maxLines = config.maxLines ?? TRUNCATION_DEFAULTS.MAX_LINES;
|
|
180
|
+
const maxBytes = config.maxBytes ?? TRUNCATION_DEFAULTS.MAX_BYTES;
|
|
181
|
+
const lines = text.split(`
|
|
182
|
+
`);
|
|
183
|
+
const bytes = Buffer.byteLength(text, "utf-8");
|
|
184
|
+
return lines.length > maxLines || bytes > maxBytes;
|
|
185
|
+
}
|
|
186
|
+
|
|
79
187
|
// src/hooks/manager.ts
|
|
80
188
|
class HookTimeoutError extends Error {
|
|
81
189
|
constructor(hookName, timeout) {
|
|
@@ -312,7 +420,7 @@ class HooksManager {
|
|
|
312
420
|
// src/skills/loader.ts
|
|
313
421
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
314
422
|
import { readFile } from "node:fs/promises";
|
|
315
|
-
import { join, dirname, basename } from "node:path";
|
|
423
|
+
import { join as join2, dirname, basename } from "node:path";
|
|
316
424
|
import { homedir } from "node:os";
|
|
317
425
|
|
|
318
426
|
// src/utils/frontmatter.ts
|
|
@@ -427,13 +535,13 @@ class SkillLoader {
|
|
|
427
535
|
} = options;
|
|
428
536
|
const allDirs = [...directories];
|
|
429
537
|
if (includeUserSkills) {
|
|
430
|
-
const userSkillsDir =
|
|
538
|
+
const userSkillsDir = join2(homedir(), USER_SKILLS_DIR);
|
|
431
539
|
if (existsSync(userSkillsDir)) {
|
|
432
540
|
allDirs.push(userSkillsDir);
|
|
433
541
|
}
|
|
434
542
|
}
|
|
435
543
|
if (includeProjectSkills && this.projectRoot) {
|
|
436
|
-
const projectSkillsDir =
|
|
544
|
+
const projectSkillsDir = join2(this.projectRoot, PROJECT_SKILLS_DIR);
|
|
437
545
|
if (existsSync(projectSkillsDir)) {
|
|
438
546
|
allDirs.push(projectSkillsDir);
|
|
439
547
|
}
|
|
@@ -449,8 +557,8 @@ class SkillLoader {
|
|
|
449
557
|
if (this.skills.has(skillId)) {
|
|
450
558
|
return this.skills.get(skillId);
|
|
451
559
|
}
|
|
452
|
-
for (const
|
|
453
|
-
const skill = await this.parseSkillFile(
|
|
560
|
+
for (const path2 of this.discoveredPaths) {
|
|
561
|
+
const skill = await this.parseSkillFile(path2);
|
|
454
562
|
if (skill && skill.id === skillId) {
|
|
455
563
|
this.skills.set(skill.id, skill);
|
|
456
564
|
return skill;
|
|
@@ -550,10 +658,10 @@ class SkillLoader {
|
|
|
550
658
|
try {
|
|
551
659
|
const entries = readdirSync(dir);
|
|
552
660
|
for (const entry of entries) {
|
|
553
|
-
const fullPath =
|
|
661
|
+
const fullPath = join2(dir, entry);
|
|
554
662
|
const stat = statSync(fullPath);
|
|
555
663
|
if (stat.isDirectory()) {
|
|
556
|
-
const skillFile =
|
|
664
|
+
const skillFile = join2(fullPath, SKILL_FILE_NAME);
|
|
557
665
|
if (existsSync(skillFile)) {
|
|
558
666
|
if (!this.discoveredPaths.has(skillFile)) {
|
|
559
667
|
const skill = await this.parseSkillFile(skillFile);
|
|
@@ -633,9 +741,9 @@ var defaultSkillLoader = new SkillLoader;
|
|
|
633
741
|
|
|
634
742
|
// src/tools/skill.ts
|
|
635
743
|
import { existsSync as existsSync2 } from "node:fs";
|
|
636
|
-
import { join as
|
|
744
|
+
import { join as join3 } from "node:path";
|
|
637
745
|
import { homedir as homedir2 } from "node:os";
|
|
638
|
-
var DEFAULT_USER_SKILLS_PATH =
|
|
746
|
+
var DEFAULT_USER_SKILLS_PATH = join3(homedir2(), ".claude/skills");
|
|
639
747
|
var skillToolSchema = {
|
|
640
748
|
type: "object",
|
|
641
749
|
properties: {
|
|
@@ -662,7 +770,7 @@ function createSkillTool(config = {}) {
|
|
|
662
770
|
const paths = [];
|
|
663
771
|
if (config.settingSources && config.settingSources.length > 0) {
|
|
664
772
|
for (const source of config.settingSources) {
|
|
665
|
-
const resolvedPath = source.startsWith("~") ?
|
|
773
|
+
const resolvedPath = source.startsWith("~") ? join3(homedir2(), source.slice(1)) : source;
|
|
666
774
|
if (existsSync2(resolvedPath)) {
|
|
667
775
|
paths.push(resolvedPath);
|
|
668
776
|
}
|
|
@@ -1113,7 +1221,7 @@ var defaultSystemPromptBuilder = new SystemPromptBuilderImpl;
|
|
|
1113
1221
|
// src/prompt/claude-md.ts
|
|
1114
1222
|
import { existsSync as existsSync3 } from "node:fs";
|
|
1115
1223
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
1116
|
-
import { join as
|
|
1224
|
+
import { join as join4, dirname as dirname2 } from "node:path";
|
|
1117
1225
|
import { homedir as homedir3 } from "node:os";
|
|
1118
1226
|
var CLAUDE_MD_FILENAME = "CLAUDE.md";
|
|
1119
1227
|
var USER_CLAUDE_DIR = ".claude";
|
|
@@ -1127,7 +1235,7 @@ class ClaudeMdLoaderImpl {
|
|
|
1127
1235
|
return this.loadFile(filePath, "project");
|
|
1128
1236
|
}
|
|
1129
1237
|
async loadUserClaudeMd() {
|
|
1130
|
-
const filePath =
|
|
1238
|
+
const filePath = join4(homedir3(), USER_CLAUDE_DIR, CLAUDE_MD_FILENAME);
|
|
1131
1239
|
if (!existsSync3(filePath)) {
|
|
1132
1240
|
return;
|
|
1133
1241
|
}
|
|
@@ -1148,9 +1256,9 @@ class ClaudeMdLoaderImpl {
|
|
|
1148
1256
|
}
|
|
1149
1257
|
}
|
|
1150
1258
|
if (config.additionalPaths) {
|
|
1151
|
-
for (const
|
|
1152
|
-
if (existsSync3(
|
|
1153
|
-
const content = await this.loadFile(
|
|
1259
|
+
for (const path2 of config.additionalPaths) {
|
|
1260
|
+
if (existsSync3(path2)) {
|
|
1261
|
+
const content = await this.loadFile(path2, "project");
|
|
1154
1262
|
if (content) {
|
|
1155
1263
|
contents.push(content);
|
|
1156
1264
|
}
|
|
@@ -1177,11 +1285,11 @@ class ClaudeMdLoaderImpl {
|
|
|
1177
1285
|
async findProjectClaudeMd(startDir) {
|
|
1178
1286
|
let currentDir = startDir;
|
|
1179
1287
|
while (currentDir !== "/") {
|
|
1180
|
-
const claudeMdPath =
|
|
1288
|
+
const claudeMdPath = join4(currentDir, CLAUDE_MD_FILENAME);
|
|
1181
1289
|
if (existsSync3(claudeMdPath)) {
|
|
1182
1290
|
return claudeMdPath;
|
|
1183
1291
|
}
|
|
1184
|
-
const gitPath =
|
|
1292
|
+
const gitPath = join4(currentDir, ".git");
|
|
1185
1293
|
if (existsSync3(gitPath)) {
|
|
1186
1294
|
break;
|
|
1187
1295
|
}
|
|
@@ -1247,6 +1355,7 @@ class SessionImpl {
|
|
|
1247
1355
|
_state;
|
|
1248
1356
|
provider;
|
|
1249
1357
|
tools;
|
|
1358
|
+
toolNameLookup = new Map;
|
|
1250
1359
|
emitter;
|
|
1251
1360
|
pendingMessage = null;
|
|
1252
1361
|
isReceiving = false;
|
|
@@ -1254,6 +1363,7 @@ class SessionImpl {
|
|
|
1254
1363
|
closed = false;
|
|
1255
1364
|
hooksManager = null;
|
|
1256
1365
|
maxTurns;
|
|
1366
|
+
enableToolRepair = true;
|
|
1257
1367
|
constructor(id, config, provider, state) {
|
|
1258
1368
|
this.id = id;
|
|
1259
1369
|
this.config = config;
|
|
@@ -1276,6 +1386,7 @@ class SessionImpl {
|
|
|
1276
1386
|
if (config.tools) {
|
|
1277
1387
|
for (const tool of config.tools) {
|
|
1278
1388
|
this.tools.set(tool.name, tool);
|
|
1389
|
+
this.toolNameLookup.set(tool.name.toLowerCase(), tool.name);
|
|
1279
1390
|
}
|
|
1280
1391
|
}
|
|
1281
1392
|
if (config.settingSources && config.settingSources.length > 0) {
|
|
@@ -1284,6 +1395,7 @@ class SessionImpl {
|
|
|
1284
1395
|
cwd: config.cwd
|
|
1285
1396
|
});
|
|
1286
1397
|
this.tools.set(skillTool2.name, skillTool2);
|
|
1398
|
+
this.toolNameLookup.set(skillTool2.name.toLowerCase(), skillTool2.name);
|
|
1287
1399
|
}
|
|
1288
1400
|
this.applyAllowedToolsFilter();
|
|
1289
1401
|
if (config.hooks) {
|
|
@@ -1567,8 +1679,18 @@ class SessionImpl {
|
|
|
1567
1679
|
async executeToolCall(block, abortSignal) {
|
|
1568
1680
|
let toolInput = block.input;
|
|
1569
1681
|
let systemMessage;
|
|
1682
|
+
let tool = this.tools.get(block.name);
|
|
1683
|
+
let effectiveToolName = block.name;
|
|
1684
|
+
if (!tool && this.enableToolRepair) {
|
|
1685
|
+
const lowerName = block.name.toLowerCase();
|
|
1686
|
+
const originalName = this.toolNameLookup.get(lowerName);
|
|
1687
|
+
if (originalName) {
|
|
1688
|
+
tool = this.tools.get(originalName);
|
|
1689
|
+
effectiveToolName = originalName;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1570
1692
|
if (this.hooksManager) {
|
|
1571
|
-
const preResult = await this.hooksManager.runPreToolUse(
|
|
1693
|
+
const preResult = await this.hooksManager.runPreToolUse(effectiveToolName, block.input, block.id, abortSignal);
|
|
1572
1694
|
if (!preResult.continue) {
|
|
1573
1695
|
return {
|
|
1574
1696
|
type: "tool_result",
|
|
@@ -1582,7 +1704,7 @@ class SessionImpl {
|
|
|
1582
1704
|
return {
|
|
1583
1705
|
type: "tool_result",
|
|
1584
1706
|
tool_use_id: block.id,
|
|
1585
|
-
content: preResult.reason ?? `Tool "${
|
|
1707
|
+
content: preResult.reason ?? `Tool "${effectiveToolName}" was denied by hook`,
|
|
1586
1708
|
is_error: true,
|
|
1587
1709
|
_hookSystemMessage: preResult.systemMessage
|
|
1588
1710
|
};
|
|
@@ -1592,12 +1714,13 @@ class SessionImpl {
|
|
|
1592
1714
|
}
|
|
1593
1715
|
systemMessage = preResult.systemMessage;
|
|
1594
1716
|
}
|
|
1595
|
-
const tool = this.tools.get(block.name);
|
|
1596
1717
|
if (!tool) {
|
|
1718
|
+
const availableTools = Array.from(this.tools.keys()).slice(0, 10);
|
|
1719
|
+
const suffix = this.tools.size > 10 ? ` (and ${this.tools.size - 10} more)` : "";
|
|
1597
1720
|
return {
|
|
1598
1721
|
type: "tool_result",
|
|
1599
1722
|
tool_use_id: block.id,
|
|
1600
|
-
content: `Error: Tool "${block.name}" not found`,
|
|
1723
|
+
content: `Error: Tool "${block.name}" not found. Available tools: ${availableTools.join(", ")}${suffix}`,
|
|
1601
1724
|
is_error: true,
|
|
1602
1725
|
_hookSystemMessage: systemMessage
|
|
1603
1726
|
};
|
|
@@ -1610,7 +1733,10 @@ class SessionImpl {
|
|
|
1610
1733
|
let toolResponse;
|
|
1611
1734
|
try {
|
|
1612
1735
|
const toolResult = await tool.execute(toolInput, context);
|
|
1613
|
-
|
|
1736
|
+
let content = typeof toolResult.content === "string" ? toolResult.content : JSON.stringify(toolResult.content);
|
|
1737
|
+
if (needsTruncation(content)) {
|
|
1738
|
+
content = await truncateToolOutput(content);
|
|
1739
|
+
}
|
|
1614
1740
|
toolResponse = toolResult;
|
|
1615
1741
|
result = {
|
|
1616
1742
|
type: "tool_result",
|
|
@@ -1628,7 +1754,7 @@ class SessionImpl {
|
|
|
1628
1754
|
};
|
|
1629
1755
|
}
|
|
1630
1756
|
if (this.hooksManager) {
|
|
1631
|
-
const postResult = await this.hooksManager.runPostToolUse(
|
|
1757
|
+
const postResult = await this.hooksManager.runPostToolUse(effectiveToolName, toolInput, toolResponse, block.id, abortSignal);
|
|
1632
1758
|
if (postResult.systemMessage) {
|
|
1633
1759
|
systemMessage = postResult.systemMessage;
|
|
1634
1760
|
}
|
|
@@ -2270,7 +2396,7 @@ class OpenAIProvider {
|
|
|
2270
2396
|
}
|
|
2271
2397
|
this.config = {
|
|
2272
2398
|
apiKey,
|
|
2273
|
-
baseUrl: config.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1",
|
|
2399
|
+
baseUrl: this.normalizeBaseUrl(config.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1"),
|
|
2274
2400
|
organization: config.organization,
|
|
2275
2401
|
defaultMaxTokens: config.defaultMaxTokens ?? 4096
|
|
2276
2402
|
};
|
|
@@ -2279,8 +2405,23 @@ class OpenAIProvider {
|
|
|
2279
2405
|
return this.supportedModels.some((pattern) => pattern.test(model));
|
|
2280
2406
|
}
|
|
2281
2407
|
async complete(request) {
|
|
2408
|
+
if (this.usesResponsesApi(request.config.model)) {
|
|
2409
|
+
const openaiRequest2 = this.buildResponsesRequest(request, false);
|
|
2410
|
+
const response2 = await fetch(`${this.config.baseUrl}/responses`, {
|
|
2411
|
+
method: "POST",
|
|
2412
|
+
headers: this.getHeaders(),
|
|
2413
|
+
body: JSON.stringify(openaiRequest2),
|
|
2414
|
+
signal: request.abortSignal
|
|
2415
|
+
});
|
|
2416
|
+
if (!response2.ok) {
|
|
2417
|
+
const error = await response2.text();
|
|
2418
|
+
throw new Error(`OpenAI API error: ${response2.status} ${error}`);
|
|
2419
|
+
}
|
|
2420
|
+
const data2 = await response2.json();
|
|
2421
|
+
return this.convertResponsesResponse(data2);
|
|
2422
|
+
}
|
|
2282
2423
|
const openaiRequest = this.buildRequest(request, false);
|
|
2283
|
-
|
|
2424
|
+
let response = await fetch(`${this.config.baseUrl}/chat/completions`, {
|
|
2284
2425
|
method: "POST",
|
|
2285
2426
|
headers: this.getHeaders(),
|
|
2286
2427
|
body: JSON.stringify(openaiRequest),
|
|
@@ -2288,14 +2429,43 @@ class OpenAIProvider {
|
|
|
2288
2429
|
});
|
|
2289
2430
|
if (!response.ok) {
|
|
2290
2431
|
const error = await response.text();
|
|
2432
|
+
if (this.shouldFallbackToResponses(response.status, error)) {
|
|
2433
|
+
const fallbackRequest = this.buildResponsesRequest(request, false);
|
|
2434
|
+
response = await fetch(`${this.config.baseUrl}/responses`, {
|
|
2435
|
+
method: "POST",
|
|
2436
|
+
headers: this.getHeaders(),
|
|
2437
|
+
body: JSON.stringify(fallbackRequest),
|
|
2438
|
+
signal: request.abortSignal
|
|
2439
|
+
});
|
|
2440
|
+
if (!response.ok) {
|
|
2441
|
+
const fallbackError = await response.text();
|
|
2442
|
+
throw new Error(`OpenAI API error: ${response.status} ${fallbackError}`);
|
|
2443
|
+
}
|
|
2444
|
+
const data2 = await response.json();
|
|
2445
|
+
return this.convertResponsesResponse(data2);
|
|
2446
|
+
}
|
|
2291
2447
|
throw new Error(`OpenAI API error: ${response.status} ${error}`);
|
|
2292
2448
|
}
|
|
2293
2449
|
const data = await response.json();
|
|
2294
2450
|
return this.convertResponse(data);
|
|
2295
2451
|
}
|
|
2296
2452
|
async stream(request, options) {
|
|
2453
|
+
if (this.usesResponsesApi(request.config.model)) {
|
|
2454
|
+
const openaiRequest2 = this.buildResponsesRequest(request, true);
|
|
2455
|
+
const response2 = await fetch(`${this.config.baseUrl}/responses`, {
|
|
2456
|
+
method: "POST",
|
|
2457
|
+
headers: this.getHeaders(),
|
|
2458
|
+
body: JSON.stringify(openaiRequest2),
|
|
2459
|
+
signal: request.abortSignal
|
|
2460
|
+
});
|
|
2461
|
+
if (!response2.ok) {
|
|
2462
|
+
const error = await response2.text();
|
|
2463
|
+
throw new Error(`OpenAI API error: ${response2.status} ${error}`);
|
|
2464
|
+
}
|
|
2465
|
+
return this.createResponsesStreamIterator(response2.body, options);
|
|
2466
|
+
}
|
|
2297
2467
|
const openaiRequest = this.buildRequest(request, true);
|
|
2298
|
-
|
|
2468
|
+
let response = await fetch(`${this.config.baseUrl}/chat/completions`, {
|
|
2299
2469
|
method: "POST",
|
|
2300
2470
|
headers: this.getHeaders(),
|
|
2301
2471
|
body: JSON.stringify(openaiRequest),
|
|
@@ -2303,6 +2473,20 @@ class OpenAIProvider {
|
|
|
2303
2473
|
});
|
|
2304
2474
|
if (!response.ok) {
|
|
2305
2475
|
const error = await response.text();
|
|
2476
|
+
if (this.shouldFallbackToResponses(response.status, error)) {
|
|
2477
|
+
const fallbackRequest = this.buildResponsesRequest(request, true);
|
|
2478
|
+
response = await fetch(`${this.config.baseUrl}/responses`, {
|
|
2479
|
+
method: "POST",
|
|
2480
|
+
headers: this.getHeaders(),
|
|
2481
|
+
body: JSON.stringify(fallbackRequest),
|
|
2482
|
+
signal: request.abortSignal
|
|
2483
|
+
});
|
|
2484
|
+
if (!response.ok) {
|
|
2485
|
+
const fallbackError = await response.text();
|
|
2486
|
+
throw new Error(`OpenAI API error: ${response.status} ${fallbackError}`);
|
|
2487
|
+
}
|
|
2488
|
+
return this.createResponsesStreamIterator(response.body, options);
|
|
2489
|
+
}
|
|
2306
2490
|
throw new Error(`OpenAI API error: ${response.status} ${error}`);
|
|
2307
2491
|
}
|
|
2308
2492
|
return this.createStreamIterator(response.body, options);
|
|
@@ -2310,10 +2494,10 @@ class OpenAIProvider {
|
|
|
2310
2494
|
buildRequest(request, stream) {
|
|
2311
2495
|
const messages = this.convertMessages(request.messages, request.systemPrompt);
|
|
2312
2496
|
const tools = request.tools ? this.convertTools(request.tools) : undefined;
|
|
2313
|
-
|
|
2497
|
+
const maxTokens = request.config.maxTokens ?? this.config.defaultMaxTokens;
|
|
2498
|
+
const openaiRequest = {
|
|
2314
2499
|
model: request.config.model,
|
|
2315
2500
|
messages,
|
|
2316
|
-
max_tokens: request.config.maxTokens ?? this.config.defaultMaxTokens,
|
|
2317
2501
|
temperature: request.config.temperature,
|
|
2318
2502
|
top_p: request.config.topP,
|
|
2319
2503
|
stop: request.config.stopSequences,
|
|
@@ -2321,6 +2505,162 @@ class OpenAIProvider {
|
|
|
2321
2505
|
stream_options: stream ? { include_usage: true } : undefined,
|
|
2322
2506
|
tools
|
|
2323
2507
|
};
|
|
2508
|
+
if (this.usesMaxCompletionTokens(request.config.model)) {
|
|
2509
|
+
openaiRequest.max_completion_tokens = maxTokens;
|
|
2510
|
+
} else {
|
|
2511
|
+
openaiRequest.max_tokens = maxTokens;
|
|
2512
|
+
}
|
|
2513
|
+
return openaiRequest;
|
|
2514
|
+
}
|
|
2515
|
+
buildResponsesRequest(request, stream) {
|
|
2516
|
+
const input = this.convertResponsesInput(request.messages, request.systemPrompt);
|
|
2517
|
+
const tools = request.tools ? this.convertResponsesTools(request.tools) : undefined;
|
|
2518
|
+
return {
|
|
2519
|
+
model: request.config.model,
|
|
2520
|
+
input,
|
|
2521
|
+
max_output_tokens: request.config.maxTokens ?? this.config.defaultMaxTokens,
|
|
2522
|
+
temperature: request.config.temperature,
|
|
2523
|
+
top_p: request.config.topP,
|
|
2524
|
+
stop: request.config.stopSequences,
|
|
2525
|
+
stream,
|
|
2526
|
+
tools
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
usesMaxCompletionTokens(model) {
|
|
2530
|
+
return /^gpt-5/.test(model) || /^o1/.test(model);
|
|
2531
|
+
}
|
|
2532
|
+
usesResponsesApi(model) {
|
|
2533
|
+
return /^gpt-5/.test(model) || /^o1/.test(model);
|
|
2534
|
+
}
|
|
2535
|
+
shouldFallbackToResponses(status, errorText) {
|
|
2536
|
+
if (status !== 404) {
|
|
2537
|
+
return false;
|
|
2538
|
+
}
|
|
2539
|
+
const normalized = errorText.toLowerCase();
|
|
2540
|
+
return normalized.includes("/chat/completions") && normalized.includes("not found");
|
|
2541
|
+
}
|
|
2542
|
+
convertResponsesInput(messages, systemPrompt) {
|
|
2543
|
+
const input = [];
|
|
2544
|
+
if (systemPrompt) {
|
|
2545
|
+
input.push({ role: "system", content: systemPrompt });
|
|
2546
|
+
}
|
|
2547
|
+
for (const msg of messages) {
|
|
2548
|
+
if (msg.role === "system") {
|
|
2549
|
+
input.push({
|
|
2550
|
+
role: "system",
|
|
2551
|
+
content: typeof msg.content === "string" ? msg.content : ""
|
|
2552
|
+
});
|
|
2553
|
+
continue;
|
|
2554
|
+
}
|
|
2555
|
+
if (typeof msg.content === "string") {
|
|
2556
|
+
if (msg.role === "user") {
|
|
2557
|
+
input.push({
|
|
2558
|
+
role: "user",
|
|
2559
|
+
content: [{ type: "input_text", text: msg.content }]
|
|
2560
|
+
});
|
|
2561
|
+
} else {
|
|
2562
|
+
input.push({
|
|
2563
|
+
role: "assistant",
|
|
2564
|
+
content: [{ type: "output_text", text: msg.content }]
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
continue;
|
|
2568
|
+
}
|
|
2569
|
+
const userContent = [];
|
|
2570
|
+
const assistantContent = [];
|
|
2571
|
+
for (const block of msg.content) {
|
|
2572
|
+
if (block.type === "text") {
|
|
2573
|
+
if (msg.role === "user") {
|
|
2574
|
+
userContent.push({ type: "input_text", text: block.text });
|
|
2575
|
+
} else if (msg.role === "assistant") {
|
|
2576
|
+
assistantContent.push({ type: "output_text", text: block.text });
|
|
2577
|
+
}
|
|
2578
|
+
} else if (block.type === "image" && msg.role === "user") {
|
|
2579
|
+
if (block.source.type === "base64") {
|
|
2580
|
+
userContent.push({
|
|
2581
|
+
type: "input_image",
|
|
2582
|
+
image_url: `data:${block.source.media_type};base64,${block.source.data}`
|
|
2583
|
+
});
|
|
2584
|
+
} else if (block.source.type === "url") {
|
|
2585
|
+
userContent.push({
|
|
2586
|
+
type: "input_image",
|
|
2587
|
+
image_url: block.source.url
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
} else if (block.type === "tool_use") {
|
|
2591
|
+
input.push({
|
|
2592
|
+
type: "function_call",
|
|
2593
|
+
call_id: block.id,
|
|
2594
|
+
name: block.name,
|
|
2595
|
+
arguments: JSON.stringify(block.input)
|
|
2596
|
+
});
|
|
2597
|
+
} else if (block.type === "tool_result") {
|
|
2598
|
+
input.push({
|
|
2599
|
+
type: "function_call_output",
|
|
2600
|
+
call_id: block.tool_use_id,
|
|
2601
|
+
output: typeof block.content === "string" ? block.content : JSON.stringify(block.content)
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
if (msg.role === "user" && userContent.length > 0) {
|
|
2606
|
+
input.push({ role: "user", content: userContent });
|
|
2607
|
+
} else if (msg.role === "assistant" && assistantContent.length > 0) {
|
|
2608
|
+
input.push({ role: "assistant", content: assistantContent });
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
return input;
|
|
2612
|
+
}
|
|
2613
|
+
convertResponsesResponse(data) {
|
|
2614
|
+
const content = [];
|
|
2615
|
+
for (const item of data.output ?? []) {
|
|
2616
|
+
if (item.type === "message" && item.content) {
|
|
2617
|
+
for (const part of item.content) {
|
|
2618
|
+
if (part.type === "output_text") {
|
|
2619
|
+
content.push({ type: "text", text: part.text });
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
} else if (item.type === "function_call" && item.call_id && item.name) {
|
|
2623
|
+
content.push({
|
|
2624
|
+
type: "tool_use",
|
|
2625
|
+
id: item.call_id,
|
|
2626
|
+
name: item.name,
|
|
2627
|
+
input: item.arguments ? JSON.parse(item.arguments) : {}
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
return {
|
|
2632
|
+
id: data.id,
|
|
2633
|
+
model: data.model,
|
|
2634
|
+
content,
|
|
2635
|
+
stopReason: "end_turn",
|
|
2636
|
+
stopSequence: null,
|
|
2637
|
+
usage: {
|
|
2638
|
+
input_tokens: data.usage?.input_tokens ?? 0,
|
|
2639
|
+
output_tokens: data.usage?.output_tokens ?? 0
|
|
2640
|
+
}
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2643
|
+
normalizeBaseUrl(baseUrl) {
|
|
2644
|
+
const trimmed = baseUrl.replace(/\/+$/, "");
|
|
2645
|
+
try {
|
|
2646
|
+
const url = new URL(trimmed);
|
|
2647
|
+
const path2 = url.pathname.replace(/\/+$/, "");
|
|
2648
|
+
if (path2 === "" || path2 === "/") {
|
|
2649
|
+
url.pathname = "/v1";
|
|
2650
|
+
return url.toString().replace(/\/+$/, "");
|
|
2651
|
+
}
|
|
2652
|
+
if (path2.endsWith("/openai")) {
|
|
2653
|
+
url.pathname = `${path2}/v1`;
|
|
2654
|
+
return url.toString().replace(/\/+$/, "");
|
|
2655
|
+
}
|
|
2656
|
+
if (!/\/v\d/.test(path2)) {
|
|
2657
|
+
url.pathname = `${path2}/v1`;
|
|
2658
|
+
return url.toString().replace(/\/+$/, "");
|
|
2659
|
+
}
|
|
2660
|
+
return url.toString().replace(/\/+$/, "");
|
|
2661
|
+
} catch {
|
|
2662
|
+
return trimmed;
|
|
2663
|
+
}
|
|
2324
2664
|
}
|
|
2325
2665
|
convertMessages(messages, systemPrompt) {
|
|
2326
2666
|
const result = [];
|
|
@@ -2403,6 +2743,14 @@ class OpenAIProvider {
|
|
|
2403
2743
|
}
|
|
2404
2744
|
}));
|
|
2405
2745
|
}
|
|
2746
|
+
convertResponsesTools(tools) {
|
|
2747
|
+
return tools.map((tool) => ({
|
|
2748
|
+
type: "function",
|
|
2749
|
+
name: tool.name,
|
|
2750
|
+
description: tool.description,
|
|
2751
|
+
parameters: tool.inputSchema
|
|
2752
|
+
}));
|
|
2753
|
+
}
|
|
2406
2754
|
convertResponse(data) {
|
|
2407
2755
|
const choice = data.choices[0];
|
|
2408
2756
|
const content = [];
|
|
@@ -2615,6 +2963,221 @@ class OpenAIProvider {
|
|
|
2615
2963
|
}
|
|
2616
2964
|
};
|
|
2617
2965
|
}
|
|
2966
|
+
createResponsesStreamIterator(body, options) {
|
|
2967
|
+
const self = this;
|
|
2968
|
+
return {
|
|
2969
|
+
async* [Symbol.asyncIterator]() {
|
|
2970
|
+
const reader = body.getReader();
|
|
2971
|
+
const decoder = new TextDecoder;
|
|
2972
|
+
let buffer = "";
|
|
2973
|
+
let emittedMessageStart = false;
|
|
2974
|
+
let textBlockStarted = false;
|
|
2975
|
+
let finished = false;
|
|
2976
|
+
const toolCalls = new Map;
|
|
2977
|
+
let nextToolBlockIndex = 1;
|
|
2978
|
+
const ensureMessageStart = (id, model) => {
|
|
2979
|
+
if (emittedMessageStart)
|
|
2980
|
+
return;
|
|
2981
|
+
emittedMessageStart = true;
|
|
2982
|
+
const startEvent = {
|
|
2983
|
+
type: "message_start",
|
|
2984
|
+
message: {
|
|
2985
|
+
id: id ?? "",
|
|
2986
|
+
type: "message",
|
|
2987
|
+
role: "assistant",
|
|
2988
|
+
content: [],
|
|
2989
|
+
model: model ?? "",
|
|
2990
|
+
stop_reason: null,
|
|
2991
|
+
stop_sequence: null,
|
|
2992
|
+
usage: { input_tokens: 0, output_tokens: 0 }
|
|
2993
|
+
}
|
|
2994
|
+
};
|
|
2995
|
+
options?.onEvent?.(startEvent);
|
|
2996
|
+
return startEvent;
|
|
2997
|
+
};
|
|
2998
|
+
try {
|
|
2999
|
+
while (true) {
|
|
3000
|
+
const { done, value } = await reader.read();
|
|
3001
|
+
if (done)
|
|
3002
|
+
break;
|
|
3003
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3004
|
+
const lines = buffer.split(`
|
|
3005
|
+
`);
|
|
3006
|
+
buffer = lines.pop() || "";
|
|
3007
|
+
for (const line of lines) {
|
|
3008
|
+
if (!line.startsWith("data: "))
|
|
3009
|
+
continue;
|
|
3010
|
+
const data = line.slice(6).trim();
|
|
3011
|
+
if (!data)
|
|
3012
|
+
continue;
|
|
3013
|
+
if (data === "[DONE]") {
|
|
3014
|
+
if (!finished) {
|
|
3015
|
+
const stopEvent = { type: "message_stop" };
|
|
3016
|
+
options?.onEvent?.(stopEvent);
|
|
3017
|
+
yield stopEvent;
|
|
3018
|
+
}
|
|
3019
|
+
finished = true;
|
|
3020
|
+
continue;
|
|
3021
|
+
}
|
|
3022
|
+
let payload;
|
|
3023
|
+
try {
|
|
3024
|
+
payload = JSON.parse(data);
|
|
3025
|
+
} catch {
|
|
3026
|
+
continue;
|
|
3027
|
+
}
|
|
3028
|
+
const type = payload?.type;
|
|
3029
|
+
if (type === "response.created") {
|
|
3030
|
+
const startEvent = ensureMessageStart(payload.response?.id, payload.response?.model);
|
|
3031
|
+
if (startEvent)
|
|
3032
|
+
yield startEvent;
|
|
3033
|
+
continue;
|
|
3034
|
+
}
|
|
3035
|
+
if (!emittedMessageStart) {
|
|
3036
|
+
const startEvent = ensureMessageStart(payload?.response?.id, payload?.response?.model);
|
|
3037
|
+
if (startEvent)
|
|
3038
|
+
yield startEvent;
|
|
3039
|
+
}
|
|
3040
|
+
if (type === "response.output_text.delta") {
|
|
3041
|
+
if (!textBlockStarted) {
|
|
3042
|
+
textBlockStarted = true;
|
|
3043
|
+
const startText = {
|
|
3044
|
+
type: "content_block_start",
|
|
3045
|
+
index: 0,
|
|
3046
|
+
content_block: { type: "text", text: "" }
|
|
3047
|
+
};
|
|
3048
|
+
options?.onEvent?.(startText);
|
|
3049
|
+
yield startText;
|
|
3050
|
+
}
|
|
3051
|
+
const textDelta = payload.delta ?? "";
|
|
3052
|
+
if (textDelta) {
|
|
3053
|
+
const textEvent = {
|
|
3054
|
+
type: "content_block_delta",
|
|
3055
|
+
index: 0,
|
|
3056
|
+
delta: { type: "text_delta", text: textDelta }
|
|
3057
|
+
};
|
|
3058
|
+
options?.onText?.(textDelta);
|
|
3059
|
+
options?.onEvent?.(textEvent);
|
|
3060
|
+
yield textEvent;
|
|
3061
|
+
}
|
|
3062
|
+
} else if (type === "response.output_item.added") {
|
|
3063
|
+
const item = payload.item;
|
|
3064
|
+
if (item?.type === "function_call") {
|
|
3065
|
+
const blockIndex = nextToolBlockIndex++;
|
|
3066
|
+
const callId = item.call_id ?? item.id ?? "";
|
|
3067
|
+
toolCalls.set(item.id, {
|
|
3068
|
+
callId,
|
|
3069
|
+
name: item.name ?? "",
|
|
3070
|
+
arguments: item.arguments ?? "",
|
|
3071
|
+
blockIndex,
|
|
3072
|
+
done: false
|
|
3073
|
+
});
|
|
3074
|
+
const startEvent = {
|
|
3075
|
+
type: "content_block_start",
|
|
3076
|
+
index: blockIndex,
|
|
3077
|
+
content_block: {
|
|
3078
|
+
type: "tool_use",
|
|
3079
|
+
id: callId,
|
|
3080
|
+
name: item.name ?? "",
|
|
3081
|
+
input: {}
|
|
3082
|
+
}
|
|
3083
|
+
};
|
|
3084
|
+
options?.onEvent?.(startEvent);
|
|
3085
|
+
yield startEvent;
|
|
3086
|
+
if (item.arguments) {
|
|
3087
|
+
const deltaEvent = {
|
|
3088
|
+
type: "content_block_delta",
|
|
3089
|
+
index: blockIndex,
|
|
3090
|
+
delta: {
|
|
3091
|
+
type: "input_json_delta",
|
|
3092
|
+
partial_json: item.arguments
|
|
3093
|
+
}
|
|
3094
|
+
};
|
|
3095
|
+
options?.onEvent?.(deltaEvent);
|
|
3096
|
+
yield deltaEvent;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
} else if (type === "response.function_call_arguments.delta") {
|
|
3100
|
+
const entry = toolCalls.get(payload.item_id);
|
|
3101
|
+
if (entry && payload.delta) {
|
|
3102
|
+
entry.arguments += payload.delta;
|
|
3103
|
+
const deltaEvent = {
|
|
3104
|
+
type: "content_block_delta",
|
|
3105
|
+
index: entry.blockIndex,
|
|
3106
|
+
delta: { type: "input_json_delta", partial_json: payload.delta }
|
|
3107
|
+
};
|
|
3108
|
+
options?.onEvent?.(deltaEvent);
|
|
3109
|
+
yield deltaEvent;
|
|
3110
|
+
}
|
|
3111
|
+
} else if (type === "response.output_item.done") {
|
|
3112
|
+
const item = payload.item;
|
|
3113
|
+
if (item?.type === "function_call") {
|
|
3114
|
+
const entry = toolCalls.get(item.id);
|
|
3115
|
+
if (entry && !entry.done) {
|
|
3116
|
+
entry.done = true;
|
|
3117
|
+
const stopEvent = {
|
|
3118
|
+
type: "content_block_stop",
|
|
3119
|
+
index: entry.blockIndex
|
|
3120
|
+
};
|
|
3121
|
+
options?.onEvent?.(stopEvent);
|
|
3122
|
+
yield stopEvent;
|
|
3123
|
+
try {
|
|
3124
|
+
const input = entry.arguments ? JSON.parse(entry.arguments) : {};
|
|
3125
|
+
options?.onToolUse?.({ id: entry.callId, name: entry.name, input });
|
|
3126
|
+
} catch {
|
|
3127
|
+
options?.onToolUse?.({ id: entry.callId, name: entry.name, input: {} });
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
} else if (type === "response.completed" || type === "response.incomplete") {
|
|
3132
|
+
finished = true;
|
|
3133
|
+
if (textBlockStarted) {
|
|
3134
|
+
const stopText = { type: "content_block_stop", index: 0 };
|
|
3135
|
+
options?.onEvent?.(stopText);
|
|
3136
|
+
yield stopText;
|
|
3137
|
+
}
|
|
3138
|
+
for (const entry of toolCalls.values()) {
|
|
3139
|
+
if (entry.done)
|
|
3140
|
+
continue;
|
|
3141
|
+
entry.done = true;
|
|
3142
|
+
const stopEvent2 = {
|
|
3143
|
+
type: "content_block_stop",
|
|
3144
|
+
index: entry.blockIndex
|
|
3145
|
+
};
|
|
3146
|
+
options?.onEvent?.(stopEvent2);
|
|
3147
|
+
yield stopEvent2;
|
|
3148
|
+
try {
|
|
3149
|
+
const input = entry.arguments ? JSON.parse(entry.arguments) : {};
|
|
3150
|
+
options?.onToolUse?.({ id: entry.callId, name: entry.name, input });
|
|
3151
|
+
} catch {
|
|
3152
|
+
options?.onToolUse?.({ id: entry.callId, name: entry.name, input: {} });
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
const finishReason = payload.response?.incomplete_details?.reason;
|
|
3156
|
+
const messageDelta = {
|
|
3157
|
+
type: "message_delta",
|
|
3158
|
+
delta: {
|
|
3159
|
+
stop_reason: self.convertResponsesStopReason(finishReason),
|
|
3160
|
+
stop_sequence: null
|
|
3161
|
+
},
|
|
3162
|
+
usage: {
|
|
3163
|
+
output_tokens: payload.response?.usage?.output_tokens ?? 0,
|
|
3164
|
+
input_tokens: payload.response?.usage?.input_tokens ?? 0
|
|
3165
|
+
}
|
|
3166
|
+
};
|
|
3167
|
+
options?.onEvent?.(messageDelta);
|
|
3168
|
+
yield messageDelta;
|
|
3169
|
+
const stopEvent = { type: "message_stop" };
|
|
3170
|
+
options?.onEvent?.(stopEvent);
|
|
3171
|
+
yield stopEvent;
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
} finally {
|
|
3176
|
+
reader.releaseLock();
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
2618
3181
|
getHeaders() {
|
|
2619
3182
|
const headers = {
|
|
2620
3183
|
"Content-Type": "application/json",
|
|
@@ -2625,71 +3188,675 @@ class OpenAIProvider {
|
|
|
2625
3188
|
}
|
|
2626
3189
|
return headers;
|
|
2627
3190
|
}
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
var globalManager = null;
|
|
2632
|
-
var defaultProvider = null;
|
|
2633
|
-
var defaultStorage = null;
|
|
2634
|
-
function getGlobalManager() {
|
|
2635
|
-
if (!globalManager) {
|
|
2636
|
-
if (!defaultProvider) {
|
|
2637
|
-
if (process.env.ANTHROPIC_API_KEY) {
|
|
2638
|
-
defaultProvider = new AnthropicProvider;
|
|
2639
|
-
} else if (process.env.OPENAI_API_KEY) {
|
|
2640
|
-
defaultProvider = new OpenAIProvider({
|
|
2641
|
-
apiKey: process.env.OPENAI_API_KEY,
|
|
2642
|
-
baseUrl: process.env.OPENAI_BASE_URL
|
|
2643
|
-
});
|
|
2644
|
-
} else {
|
|
2645
|
-
throw new Error("No default provider set. Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable, " + "call setDefaultProvider(), or pass a provider in the options.");
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
globalManager = new SessionManagerImpl({
|
|
2649
|
-
provider: defaultProvider,
|
|
2650
|
-
storage: defaultStorage ?? new MemorySessionStorage
|
|
2651
|
-
});
|
|
2652
|
-
}
|
|
2653
|
-
return globalManager;
|
|
2654
|
-
}
|
|
2655
|
-
async function createSession(options) {
|
|
2656
|
-
if (options?.provider || options?.sessionStorage) {
|
|
2657
|
-
let provider = options?.provider ?? defaultProvider;
|
|
2658
|
-
if (!provider) {
|
|
2659
|
-
if (process.env.ANTHROPIC_API_KEY) {
|
|
2660
|
-
provider = new AnthropicProvider;
|
|
2661
|
-
} else if (process.env.OPENAI_API_KEY) {
|
|
2662
|
-
provider = new OpenAIProvider({
|
|
2663
|
-
apiKey: process.env.OPENAI_API_KEY,
|
|
2664
|
-
baseUrl: process.env.OPENAI_BASE_URL
|
|
2665
|
-
});
|
|
2666
|
-
} else {
|
|
2667
|
-
throw new Error("No provider available. Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable, " + "call setDefaultProvider(), or pass a provider in the options.");
|
|
2668
|
-
}
|
|
3191
|
+
convertResponsesStopReason(reason) {
|
|
3192
|
+
if (reason === "max_output_tokens") {
|
|
3193
|
+
return "max_tokens";
|
|
2669
3194
|
}
|
|
2670
|
-
|
|
2671
|
-
provider,
|
|
2672
|
-
storage: options?.sessionStorage ?? defaultStorage ?? new MemorySessionStorage
|
|
2673
|
-
});
|
|
2674
|
-
return customManager.create(options);
|
|
3195
|
+
return "end_turn";
|
|
2675
3196
|
}
|
|
2676
|
-
const manager = getGlobalManager();
|
|
2677
|
-
return manager.create(options);
|
|
2678
3197
|
}
|
|
2679
3198
|
|
|
2680
|
-
// src/
|
|
2681
|
-
class
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
3199
|
+
// src/llm/gemini.ts
|
|
3200
|
+
class GeminiProvider {
|
|
3201
|
+
id = "gemini";
|
|
3202
|
+
name = "Gemini";
|
|
3203
|
+
supportedModels = [/^gemini-/, /^models\/gemini-/];
|
|
3204
|
+
config;
|
|
3205
|
+
constructor(config = {}) {
|
|
3206
|
+
const apiKey = config.apiKey ?? process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
|
|
3207
|
+
if (!apiKey) {
|
|
3208
|
+
throw new Error("Gemini API key is required. Set GEMINI_API_KEY/GOOGLE_API_KEY or pass apiKey in config.");
|
|
3209
|
+
}
|
|
3210
|
+
this.config = {
|
|
3211
|
+
apiKey,
|
|
3212
|
+
baseUrl: config.baseUrl ?? process.env.GEMINI_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta",
|
|
3213
|
+
defaultMaxTokens: config.defaultMaxTokens ?? 4096
|
|
3214
|
+
};
|
|
2688
3215
|
}
|
|
2689
|
-
|
|
2690
|
-
return this.
|
|
3216
|
+
supportsModel(model) {
|
|
3217
|
+
return this.supportedModels.some((pattern) => pattern.test(model));
|
|
2691
3218
|
}
|
|
2692
|
-
|
|
3219
|
+
async complete(request) {
|
|
3220
|
+
const geminiRequest = this.buildRequest(request);
|
|
3221
|
+
const url = this.buildUrl(this.getModelPath(request.config.model) + ":generateContent");
|
|
3222
|
+
const response = await fetch(url, {
|
|
3223
|
+
method: "POST",
|
|
3224
|
+
headers: this.getHeaders(),
|
|
3225
|
+
body: JSON.stringify(geminiRequest),
|
|
3226
|
+
signal: request.abortSignal
|
|
3227
|
+
});
|
|
3228
|
+
if (!response.ok) {
|
|
3229
|
+
const error = await response.text();
|
|
3230
|
+
throw new Error(`Gemini API error: ${response.status} ${error}`);
|
|
3231
|
+
}
|
|
3232
|
+
const data = await response.json();
|
|
3233
|
+
return this.convertResponse(data, request.config.model);
|
|
3234
|
+
}
|
|
3235
|
+
async stream(request, options) {
|
|
3236
|
+
const geminiRequest = this.buildRequest(request);
|
|
3237
|
+
const url = this.buildUrl(this.getModelPath(request.config.model) + ":streamGenerateContent", {
|
|
3238
|
+
alt: "sse"
|
|
3239
|
+
});
|
|
3240
|
+
const response = await fetch(url, {
|
|
3241
|
+
method: "POST",
|
|
3242
|
+
headers: this.getHeaders(),
|
|
3243
|
+
body: JSON.stringify(geminiRequest),
|
|
3244
|
+
signal: request.abortSignal
|
|
3245
|
+
});
|
|
3246
|
+
if (!response.ok) {
|
|
3247
|
+
const error = await response.text();
|
|
3248
|
+
throw new Error(`Gemini API error: ${response.status} ${error}`);
|
|
3249
|
+
}
|
|
3250
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
3251
|
+
if (!contentType.includes("text/event-stream")) {
|
|
3252
|
+
const data = await response.json();
|
|
3253
|
+
return this.createResponseIterator(data, options, request.config.model);
|
|
3254
|
+
}
|
|
3255
|
+
return this.createStreamIterator(response.body, options, request.config.model);
|
|
3256
|
+
}
|
|
3257
|
+
buildRequest(request) {
|
|
3258
|
+
const { contents, systemInstruction } = this.convertMessages(request.messages, request.systemPrompt);
|
|
3259
|
+
const tools = request.tools ? this.convertTools(request.tools) : undefined;
|
|
3260
|
+
return {
|
|
3261
|
+
contents,
|
|
3262
|
+
systemInstruction,
|
|
3263
|
+
generationConfig: {
|
|
3264
|
+
temperature: request.config.temperature,
|
|
3265
|
+
topP: request.config.topP,
|
|
3266
|
+
maxOutputTokens: request.config.maxTokens ?? this.config.defaultMaxTokens,
|
|
3267
|
+
stopSequences: request.config.stopSequences
|
|
3268
|
+
},
|
|
3269
|
+
tools,
|
|
3270
|
+
toolConfig: tools ? { functionCallingConfig: { mode: "AUTO" } } : undefined
|
|
3271
|
+
};
|
|
3272
|
+
}
|
|
3273
|
+
convertMessages(messages, systemPrompt) {
|
|
3274
|
+
const contents = [];
|
|
3275
|
+
const systemTexts = [];
|
|
3276
|
+
const toolNameById = new Map;
|
|
3277
|
+
for (const msg of messages) {
|
|
3278
|
+
if (typeof msg.content !== "string") {
|
|
3279
|
+
for (const block of msg.content) {
|
|
3280
|
+
if (block.type === "tool_use") {
|
|
3281
|
+
toolNameById.set(block.id, block.name);
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
if (systemPrompt) {
|
|
3287
|
+
systemTexts.push(systemPrompt);
|
|
3288
|
+
}
|
|
3289
|
+
for (const msg of messages) {
|
|
3290
|
+
if (msg.role === "system") {
|
|
3291
|
+
if (typeof msg.content === "string") {
|
|
3292
|
+
systemTexts.push(msg.content);
|
|
3293
|
+
} else {
|
|
3294
|
+
for (const block of msg.content) {
|
|
3295
|
+
if (block.type === "text") {
|
|
3296
|
+
systemTexts.push(block.text);
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
continue;
|
|
3301
|
+
}
|
|
3302
|
+
if (typeof msg.content === "string") {
|
|
3303
|
+
contents.push({
|
|
3304
|
+
role: msg.role === "assistant" ? "model" : "user",
|
|
3305
|
+
parts: [{ text: msg.content }]
|
|
3306
|
+
});
|
|
3307
|
+
continue;
|
|
3308
|
+
}
|
|
3309
|
+
const parts = [];
|
|
3310
|
+
const toolResponses = [];
|
|
3311
|
+
for (const block of msg.content) {
|
|
3312
|
+
if (block.type === "text") {
|
|
3313
|
+
parts.push({ text: block.text });
|
|
3314
|
+
} else if (block.type === "image") {
|
|
3315
|
+
if (block.source.type === "base64") {
|
|
3316
|
+
parts.push({
|
|
3317
|
+
inlineData: {
|
|
3318
|
+
mimeType: block.source.media_type ?? "image/jpeg",
|
|
3319
|
+
data: block.source.data ?? ""
|
|
3320
|
+
}
|
|
3321
|
+
});
|
|
3322
|
+
} else if (block.source.type === "url") {
|
|
3323
|
+
parts.push({
|
|
3324
|
+
fileData: {
|
|
3325
|
+
mimeType: block.source.media_type ?? "image/jpeg",
|
|
3326
|
+
fileUri: block.source.url ?? ""
|
|
3327
|
+
}
|
|
3328
|
+
});
|
|
3329
|
+
}
|
|
3330
|
+
} else if (block.type === "tool_result") {
|
|
3331
|
+
const toolName = toolNameById.get(block.tool_use_id) ?? "tool";
|
|
3332
|
+
const output = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
|
|
3333
|
+
toolResponses.push({
|
|
3334
|
+
functionResponse: {
|
|
3335
|
+
name: toolName,
|
|
3336
|
+
response: { output }
|
|
3337
|
+
}
|
|
3338
|
+
});
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
if (parts.length > 0) {
|
|
3342
|
+
contents.push({
|
|
3343
|
+
role: msg.role === "assistant" ? "model" : "user",
|
|
3344
|
+
parts
|
|
3345
|
+
});
|
|
3346
|
+
}
|
|
3347
|
+
if (toolResponses.length > 0) {
|
|
3348
|
+
contents.push({
|
|
3349
|
+
role: "user",
|
|
3350
|
+
parts: toolResponses
|
|
3351
|
+
});
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
const systemInstruction = systemTexts.length > 0 ? { parts: [{ text: systemTexts.join(`
|
|
3355
|
+
|
|
3356
|
+
`) }] } : undefined;
|
|
3357
|
+
return { contents, systemInstruction };
|
|
3358
|
+
}
|
|
3359
|
+
convertTools(tools) {
|
|
3360
|
+
return [
|
|
3361
|
+
{
|
|
3362
|
+
functionDeclarations: tools.map((tool) => ({
|
|
3363
|
+
name: tool.name,
|
|
3364
|
+
description: tool.description,
|
|
3365
|
+
parameters: this.sanitizeSchema(tool.inputSchema)
|
|
3366
|
+
}))
|
|
3367
|
+
}
|
|
3368
|
+
];
|
|
3369
|
+
}
|
|
3370
|
+
sanitizeSchema(schema) {
|
|
3371
|
+
const visited = new WeakMap;
|
|
3372
|
+
const scrub = (value) => {
|
|
3373
|
+
if (value === null || typeof value !== "object") {
|
|
3374
|
+
return value;
|
|
3375
|
+
}
|
|
3376
|
+
if (Array.isArray(value)) {
|
|
3377
|
+
return value.map((item) => scrub(item));
|
|
3378
|
+
}
|
|
3379
|
+
const existing = visited.get(value);
|
|
3380
|
+
if (existing) {
|
|
3381
|
+
return existing;
|
|
3382
|
+
}
|
|
3383
|
+
const result = {};
|
|
3384
|
+
visited.set(value, result);
|
|
3385
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
3386
|
+
if (key === "additionalProperties") {
|
|
3387
|
+
continue;
|
|
3388
|
+
}
|
|
3389
|
+
result[key] = scrub(inner);
|
|
3390
|
+
}
|
|
3391
|
+
return result;
|
|
3392
|
+
};
|
|
3393
|
+
return scrub(schema);
|
|
3394
|
+
}
|
|
3395
|
+
convertResponse(data, model) {
|
|
3396
|
+
const candidate = data.candidates?.[0];
|
|
3397
|
+
const content = [];
|
|
3398
|
+
const parts = candidate?.content?.parts ?? [];
|
|
3399
|
+
let toolIndex = 0;
|
|
3400
|
+
for (const part of parts) {
|
|
3401
|
+
if ("text" in part && part.text) {
|
|
3402
|
+
content.push({ type: "text", text: part.text });
|
|
3403
|
+
} else if ("functionCall" in part && part.functionCall) {
|
|
3404
|
+
const callId = `${part.functionCall.name}_${toolIndex++}`;
|
|
3405
|
+
content.push({
|
|
3406
|
+
type: "tool_use",
|
|
3407
|
+
id: callId,
|
|
3408
|
+
name: part.functionCall.name,
|
|
3409
|
+
input: part.functionCall.args ?? {}
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
return {
|
|
3414
|
+
id: "",
|
|
3415
|
+
model: data.model ?? model,
|
|
3416
|
+
content,
|
|
3417
|
+
stopReason: this.convertStopReason(candidate?.finishReason),
|
|
3418
|
+
stopSequence: null,
|
|
3419
|
+
usage: this.convertUsage(data.usageMetadata)
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
convertUsage(usage) {
|
|
3423
|
+
return {
|
|
3424
|
+
input_tokens: usage?.promptTokenCount ?? 0,
|
|
3425
|
+
output_tokens: usage?.candidatesTokenCount ?? 0
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
convertStopReason(reason) {
|
|
3429
|
+
switch (reason) {
|
|
3430
|
+
case "MAX_TOKENS":
|
|
3431
|
+
return "max_tokens";
|
|
3432
|
+
case "STOP":
|
|
3433
|
+
return "end_turn";
|
|
3434
|
+
default:
|
|
3435
|
+
return "end_turn";
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
createStreamIterator(body, options, model) {
|
|
3439
|
+
const self = this;
|
|
3440
|
+
return {
|
|
3441
|
+
async* [Symbol.asyncIterator]() {
|
|
3442
|
+
const reader = body.getReader();
|
|
3443
|
+
const decoder = new TextDecoder;
|
|
3444
|
+
let buffer = "";
|
|
3445
|
+
let emittedMessageStart = false;
|
|
3446
|
+
let textBlockStarted = false;
|
|
3447
|
+
let finished = false;
|
|
3448
|
+
let toolIndex = 0;
|
|
3449
|
+
let emittedAny = false;
|
|
3450
|
+
const emitMessageStart = (modelId) => {
|
|
3451
|
+
if (emittedMessageStart)
|
|
3452
|
+
return;
|
|
3453
|
+
emittedMessageStart = true;
|
|
3454
|
+
const startEvent = {
|
|
3455
|
+
type: "message_start",
|
|
3456
|
+
message: {
|
|
3457
|
+
id: "",
|
|
3458
|
+
type: "message",
|
|
3459
|
+
role: "assistant",
|
|
3460
|
+
content: [],
|
|
3461
|
+
model: modelId,
|
|
3462
|
+
stop_reason: null,
|
|
3463
|
+
stop_sequence: null,
|
|
3464
|
+
usage: { input_tokens: 0, output_tokens: 0 }
|
|
3465
|
+
}
|
|
3466
|
+
};
|
|
3467
|
+
options?.onEvent?.(startEvent);
|
|
3468
|
+
return startEvent;
|
|
3469
|
+
};
|
|
3470
|
+
try {
|
|
3471
|
+
while (true) {
|
|
3472
|
+
const { done, value } = await reader.read();
|
|
3473
|
+
if (done)
|
|
3474
|
+
break;
|
|
3475
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3476
|
+
const lines = buffer.split(`
|
|
3477
|
+
`);
|
|
3478
|
+
buffer = lines.pop() || "";
|
|
3479
|
+
for (const line of lines) {
|
|
3480
|
+
const trimmed = line.trim();
|
|
3481
|
+
if (!trimmed)
|
|
3482
|
+
continue;
|
|
3483
|
+
let jsonText = trimmed;
|
|
3484
|
+
if (trimmed.startsWith("data:")) {
|
|
3485
|
+
jsonText = trimmed.slice(5).trim();
|
|
3486
|
+
}
|
|
3487
|
+
let payload;
|
|
3488
|
+
try {
|
|
3489
|
+
payload = JSON.parse(jsonText);
|
|
3490
|
+
} catch {
|
|
3491
|
+
continue;
|
|
3492
|
+
}
|
|
3493
|
+
const startEvent = emitMessageStart(payload.model ?? model);
|
|
3494
|
+
if (startEvent) {
|
|
3495
|
+
yield startEvent;
|
|
3496
|
+
emittedAny = true;
|
|
3497
|
+
}
|
|
3498
|
+
const candidate = payload.candidates?.[0];
|
|
3499
|
+
const parts = candidate?.content?.parts ?? [];
|
|
3500
|
+
for (const part of parts) {
|
|
3501
|
+
if ("text" in part && part.text) {
|
|
3502
|
+
if (!textBlockStarted) {
|
|
3503
|
+
textBlockStarted = true;
|
|
3504
|
+
const startText = {
|
|
3505
|
+
type: "content_block_start",
|
|
3506
|
+
index: 0,
|
|
3507
|
+
content_block: { type: "text", text: "" }
|
|
3508
|
+
};
|
|
3509
|
+
options?.onEvent?.(startText);
|
|
3510
|
+
yield startText;
|
|
3511
|
+
emittedAny = true;
|
|
3512
|
+
}
|
|
3513
|
+
const textEvent = {
|
|
3514
|
+
type: "content_block_delta",
|
|
3515
|
+
index: 0,
|
|
3516
|
+
delta: { type: "text_delta", text: part.text }
|
|
3517
|
+
};
|
|
3518
|
+
options?.onText?.(part.text);
|
|
3519
|
+
options?.onEvent?.(textEvent);
|
|
3520
|
+
yield textEvent;
|
|
3521
|
+
emittedAny = true;
|
|
3522
|
+
} else if ("functionCall" in part && part.functionCall) {
|
|
3523
|
+
const callId = `${part.functionCall.name}_${toolIndex}`;
|
|
3524
|
+
const blockIndex = 1 + toolIndex;
|
|
3525
|
+
toolIndex += 1;
|
|
3526
|
+
const startTool = {
|
|
3527
|
+
type: "content_block_start",
|
|
3528
|
+
index: blockIndex,
|
|
3529
|
+
content_block: {
|
|
3530
|
+
type: "tool_use",
|
|
3531
|
+
id: callId,
|
|
3532
|
+
name: part.functionCall.name,
|
|
3533
|
+
input: {}
|
|
3534
|
+
}
|
|
3535
|
+
};
|
|
3536
|
+
options?.onEvent?.(startTool);
|
|
3537
|
+
yield startTool;
|
|
3538
|
+
emittedAny = true;
|
|
3539
|
+
const args = JSON.stringify(part.functionCall.args ?? {});
|
|
3540
|
+
if (args) {
|
|
3541
|
+
const deltaEvent = {
|
|
3542
|
+
type: "content_block_delta",
|
|
3543
|
+
index: blockIndex,
|
|
3544
|
+
delta: { type: "input_json_delta", partial_json: args }
|
|
3545
|
+
};
|
|
3546
|
+
options?.onEvent?.(deltaEvent);
|
|
3547
|
+
yield deltaEvent;
|
|
3548
|
+
emittedAny = true;
|
|
3549
|
+
}
|
|
3550
|
+
const stopTool = {
|
|
3551
|
+
type: "content_block_stop",
|
|
3552
|
+
index: blockIndex
|
|
3553
|
+
};
|
|
3554
|
+
options?.onEvent?.(stopTool);
|
|
3555
|
+
yield stopTool;
|
|
3556
|
+
emittedAny = true;
|
|
3557
|
+
options?.onToolUse?.({
|
|
3558
|
+
id: callId,
|
|
3559
|
+
name: part.functionCall.name,
|
|
3560
|
+
input: part.functionCall.args ?? {}
|
|
3561
|
+
});
|
|
3562
|
+
emittedAny = true;
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
if (candidate?.finishReason && !finished) {
|
|
3566
|
+
finished = true;
|
|
3567
|
+
if (textBlockStarted) {
|
|
3568
|
+
const stopText = { type: "content_block_stop", index: 0 };
|
|
3569
|
+
options?.onEvent?.(stopText);
|
|
3570
|
+
yield stopText;
|
|
3571
|
+
}
|
|
3572
|
+
const messageDelta = {
|
|
3573
|
+
type: "message_delta",
|
|
3574
|
+
delta: {
|
|
3575
|
+
stop_reason: self.convertStopReason(candidate.finishReason),
|
|
3576
|
+
stop_sequence: null
|
|
3577
|
+
},
|
|
3578
|
+
usage: self.convertUsage(payload.usageMetadata)
|
|
3579
|
+
};
|
|
3580
|
+
options?.onEvent?.(messageDelta);
|
|
3581
|
+
yield messageDelta;
|
|
3582
|
+
emittedAny = true;
|
|
3583
|
+
const stopEvent = { type: "message_stop" };
|
|
3584
|
+
options?.onEvent?.(stopEvent);
|
|
3585
|
+
yield stopEvent;
|
|
3586
|
+
emittedAny = true;
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
} finally {
|
|
3591
|
+
reader.releaseLock();
|
|
3592
|
+
}
|
|
3593
|
+
if (!emittedAny) {
|
|
3594
|
+
const trimmed = buffer.trim();
|
|
3595
|
+
if (trimmed) {
|
|
3596
|
+
try {
|
|
3597
|
+
const parsed = JSON.parse(trimmed);
|
|
3598
|
+
const responses = Array.isArray(parsed) ? parsed : [parsed];
|
|
3599
|
+
for (const payload of responses) {
|
|
3600
|
+
const startEvent = emitMessageStart(payload.model ?? model);
|
|
3601
|
+
if (startEvent) {
|
|
3602
|
+
yield startEvent;
|
|
3603
|
+
}
|
|
3604
|
+
const candidate = payload.candidates?.[0];
|
|
3605
|
+
const parts = candidate?.content?.parts ?? [];
|
|
3606
|
+
for (const part of parts) {
|
|
3607
|
+
if ("text" in part && part.text) {
|
|
3608
|
+
if (!textBlockStarted) {
|
|
3609
|
+
textBlockStarted = true;
|
|
3610
|
+
const startText = {
|
|
3611
|
+
type: "content_block_start",
|
|
3612
|
+
index: 0,
|
|
3613
|
+
content_block: { type: "text", text: "" }
|
|
3614
|
+
};
|
|
3615
|
+
options?.onEvent?.(startText);
|
|
3616
|
+
yield startText;
|
|
3617
|
+
}
|
|
3618
|
+
const textEvent = {
|
|
3619
|
+
type: "content_block_delta",
|
|
3620
|
+
index: 0,
|
|
3621
|
+
delta: { type: "text_delta", text: part.text }
|
|
3622
|
+
};
|
|
3623
|
+
options?.onText?.(part.text);
|
|
3624
|
+
options?.onEvent?.(textEvent);
|
|
3625
|
+
yield textEvent;
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
if (textBlockStarted) {
|
|
3629
|
+
const stopText = { type: "content_block_stop", index: 0 };
|
|
3630
|
+
options?.onEvent?.(stopText);
|
|
3631
|
+
yield stopText;
|
|
3632
|
+
}
|
|
3633
|
+
const messageDelta = {
|
|
3634
|
+
type: "message_delta",
|
|
3635
|
+
delta: {
|
|
3636
|
+
stop_reason: self.convertStopReason(candidate?.finishReason),
|
|
3637
|
+
stop_sequence: null
|
|
3638
|
+
},
|
|
3639
|
+
usage: self.convertUsage(payload.usageMetadata)
|
|
3640
|
+
};
|
|
3641
|
+
options?.onEvent?.(messageDelta);
|
|
3642
|
+
yield messageDelta;
|
|
3643
|
+
const stopEvent = { type: "message_stop" };
|
|
3644
|
+
options?.onEvent?.(stopEvent);
|
|
3645
|
+
yield stopEvent;
|
|
3646
|
+
}
|
|
3647
|
+
return;
|
|
3648
|
+
} catch {}
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
if (!finished) {
|
|
3652
|
+
if (textBlockStarted) {
|
|
3653
|
+
const stopText = { type: "content_block_stop", index: 0 };
|
|
3654
|
+
options?.onEvent?.(stopText);
|
|
3655
|
+
yield stopText;
|
|
3656
|
+
}
|
|
3657
|
+
const stopEvent = { type: "message_stop" };
|
|
3658
|
+
options?.onEvent?.(stopEvent);
|
|
3659
|
+
yield stopEvent;
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3664
|
+
createResponseIterator(data, options, model) {
|
|
3665
|
+
const responses = Array.isArray(data) ? data : [data];
|
|
3666
|
+
const self = this;
|
|
3667
|
+
return {
|
|
3668
|
+
async* [Symbol.asyncIterator]() {
|
|
3669
|
+
for (const payload of responses) {
|
|
3670
|
+
const candidate = payload.candidates?.[0];
|
|
3671
|
+
const parts = candidate?.content?.parts ?? [];
|
|
3672
|
+
let textIndex = 0;
|
|
3673
|
+
let toolIndex = 0;
|
|
3674
|
+
const startEvent = {
|
|
3675
|
+
type: "message_start",
|
|
3676
|
+
message: {
|
|
3677
|
+
id: "",
|
|
3678
|
+
type: "message",
|
|
3679
|
+
role: "assistant",
|
|
3680
|
+
content: [],
|
|
3681
|
+
model: payload.model ?? model,
|
|
3682
|
+
stop_reason: null,
|
|
3683
|
+
stop_sequence: null,
|
|
3684
|
+
usage: { input_tokens: 0, output_tokens: 0 }
|
|
3685
|
+
}
|
|
3686
|
+
};
|
|
3687
|
+
options?.onEvent?.(startEvent);
|
|
3688
|
+
yield startEvent;
|
|
3689
|
+
for (const part of parts) {
|
|
3690
|
+
if ("text" in part && part.text) {
|
|
3691
|
+
const startText = {
|
|
3692
|
+
type: "content_block_start",
|
|
3693
|
+
index: textIndex,
|
|
3694
|
+
content_block: { type: "text", text: "" }
|
|
3695
|
+
};
|
|
3696
|
+
options?.onEvent?.(startText);
|
|
3697
|
+
yield startText;
|
|
3698
|
+
const textEvent = {
|
|
3699
|
+
type: "content_block_delta",
|
|
3700
|
+
index: textIndex,
|
|
3701
|
+
delta: { type: "text_delta", text: part.text }
|
|
3702
|
+
};
|
|
3703
|
+
options?.onText?.(part.text);
|
|
3704
|
+
options?.onEvent?.(textEvent);
|
|
3705
|
+
yield textEvent;
|
|
3706
|
+
const stopText = { type: "content_block_stop", index: textIndex };
|
|
3707
|
+
options?.onEvent?.(stopText);
|
|
3708
|
+
yield stopText;
|
|
3709
|
+
textIndex += 1;
|
|
3710
|
+
} else if ("functionCall" in part && part.functionCall) {
|
|
3711
|
+
const callId = `${part.functionCall.name}_${toolIndex}`;
|
|
3712
|
+
const blockIndex = textIndex + toolIndex + 1;
|
|
3713
|
+
toolIndex += 1;
|
|
3714
|
+
const startTool = {
|
|
3715
|
+
type: "content_block_start",
|
|
3716
|
+
index: blockIndex,
|
|
3717
|
+
content_block: {
|
|
3718
|
+
type: "tool_use",
|
|
3719
|
+
id: callId,
|
|
3720
|
+
name: part.functionCall.name,
|
|
3721
|
+
input: {}
|
|
3722
|
+
}
|
|
3723
|
+
};
|
|
3724
|
+
options?.onEvent?.(startTool);
|
|
3725
|
+
yield startTool;
|
|
3726
|
+
const args = JSON.stringify(part.functionCall.args ?? {});
|
|
3727
|
+
if (args) {
|
|
3728
|
+
const deltaEvent = {
|
|
3729
|
+
type: "content_block_delta",
|
|
3730
|
+
index: blockIndex,
|
|
3731
|
+
delta: { type: "input_json_delta", partial_json: args }
|
|
3732
|
+
};
|
|
3733
|
+
options?.onEvent?.(deltaEvent);
|
|
3734
|
+
yield deltaEvent;
|
|
3735
|
+
}
|
|
3736
|
+
const stopTool = { type: "content_block_stop", index: blockIndex };
|
|
3737
|
+
options?.onEvent?.(stopTool);
|
|
3738
|
+
yield stopTool;
|
|
3739
|
+
options?.onToolUse?.({
|
|
3740
|
+
id: callId,
|
|
3741
|
+
name: part.functionCall.name,
|
|
3742
|
+
input: part.functionCall.args ?? {}
|
|
3743
|
+
});
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
const messageDelta = {
|
|
3747
|
+
type: "message_delta",
|
|
3748
|
+
delta: {
|
|
3749
|
+
stop_reason: self.convertStopReason(candidate?.finishReason),
|
|
3750
|
+
stop_sequence: null
|
|
3751
|
+
},
|
|
3752
|
+
usage: self.convertUsage(payload.usageMetadata)
|
|
3753
|
+
};
|
|
3754
|
+
options?.onEvent?.(messageDelta);
|
|
3755
|
+
yield messageDelta;
|
|
3756
|
+
const stopEvent = { type: "message_stop" };
|
|
3757
|
+
options?.onEvent?.(stopEvent);
|
|
3758
|
+
yield stopEvent;
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
};
|
|
3762
|
+
}
|
|
3763
|
+
getHeaders() {
|
|
3764
|
+
return {
|
|
3765
|
+
"Content-Type": "application/json",
|
|
3766
|
+
"x-goog-api-key": this.config.apiKey
|
|
3767
|
+
};
|
|
3768
|
+
}
|
|
3769
|
+
buildUrl(path2, params) {
|
|
3770
|
+
const base = this.config.baseUrl.replace(/\/+$/, "");
|
|
3771
|
+
const url = new URL(`${base}/${path2.replace(/^\/+/, "")}`);
|
|
3772
|
+
if (!url.searchParams.has("key")) {
|
|
3773
|
+
url.searchParams.set("key", this.config.apiKey);
|
|
3774
|
+
}
|
|
3775
|
+
if (params) {
|
|
3776
|
+
for (const [key, value] of Object.entries(params)) {
|
|
3777
|
+
url.searchParams.set(key, value);
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
return url.toString();
|
|
3781
|
+
}
|
|
3782
|
+
getModelPath(model) {
|
|
3783
|
+
return model.startsWith("models/") ? model : `models/${model}`;
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
// src/api.ts
|
|
3788
|
+
var globalManager = null;
|
|
3789
|
+
var defaultProvider = null;
|
|
3790
|
+
var defaultStorage = null;
|
|
3791
|
+
function getGlobalManager() {
|
|
3792
|
+
if (!globalManager) {
|
|
3793
|
+
if (!defaultProvider) {
|
|
3794
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
3795
|
+
defaultProvider = new AnthropicProvider;
|
|
3796
|
+
} else if (process.env.OPENAI_API_KEY) {
|
|
3797
|
+
defaultProvider = new OpenAIProvider({
|
|
3798
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
3799
|
+
baseUrl: process.env.OPENAI_BASE_URL
|
|
3800
|
+
});
|
|
3801
|
+
} else if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
|
|
3802
|
+
defaultProvider = new GeminiProvider({
|
|
3803
|
+
apiKey: process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY,
|
|
3804
|
+
baseUrl: process.env.GEMINI_BASE_URL
|
|
3805
|
+
});
|
|
3806
|
+
} else {
|
|
3807
|
+
throw new Error("No default provider set. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY environment variable, " + "call setDefaultProvider(), or pass a provider in the options.");
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
globalManager = new SessionManagerImpl({
|
|
3811
|
+
provider: defaultProvider,
|
|
3812
|
+
storage: defaultStorage ?? new MemorySessionStorage
|
|
3813
|
+
});
|
|
3814
|
+
}
|
|
3815
|
+
return globalManager;
|
|
3816
|
+
}
|
|
3817
|
+
async function createSession(options) {
|
|
3818
|
+
if (options?.provider || options?.sessionStorage) {
|
|
3819
|
+
let provider = options?.provider ?? defaultProvider;
|
|
3820
|
+
if (!provider) {
|
|
3821
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
3822
|
+
provider = new AnthropicProvider;
|
|
3823
|
+
} else if (process.env.OPENAI_API_KEY) {
|
|
3824
|
+
provider = new OpenAIProvider({
|
|
3825
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
3826
|
+
baseUrl: process.env.OPENAI_BASE_URL
|
|
3827
|
+
});
|
|
3828
|
+
} else if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
|
|
3829
|
+
provider = new GeminiProvider({
|
|
3830
|
+
apiKey: process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY,
|
|
3831
|
+
baseUrl: process.env.GEMINI_BASE_URL
|
|
3832
|
+
});
|
|
3833
|
+
} else {
|
|
3834
|
+
throw new Error("No provider available. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY environment variable, " + "call setDefaultProvider(), or pass a provider in the options.");
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
const customManager = new SessionManagerImpl({
|
|
3838
|
+
provider,
|
|
3839
|
+
storage: options?.sessionStorage ?? defaultStorage ?? new MemorySessionStorage
|
|
3840
|
+
});
|
|
3841
|
+
return customManager.create(options);
|
|
3842
|
+
}
|
|
3843
|
+
const manager = getGlobalManager();
|
|
3844
|
+
return manager.create(options);
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
// src/tools/registry.ts
|
|
3848
|
+
class ToolRegistry {
|
|
3849
|
+
tools = new Map;
|
|
3850
|
+
register(tool) {
|
|
3851
|
+
this.tools.set(tool.id, tool);
|
|
3852
|
+
}
|
|
3853
|
+
unregister(toolId) {
|
|
3854
|
+
this.tools.delete(toolId);
|
|
3855
|
+
}
|
|
3856
|
+
get(toolId) {
|
|
3857
|
+
return this.tools.get(toolId);
|
|
3858
|
+
}
|
|
3859
|
+
getAll() {
|
|
2693
3860
|
return Array.from(this.tools.values());
|
|
2694
3861
|
}
|
|
2695
3862
|
clear() {
|
|
@@ -2828,24 +3995,36 @@ var defaultMCPServerManager = new MCPServerManager;
|
|
|
2828
3995
|
// src/tools/manager.ts
|
|
2829
3996
|
class ToolManager {
|
|
2830
3997
|
tools = new Map;
|
|
3998
|
+
toolNameLookup = new Map;
|
|
2831
3999
|
mcpServers = new Map;
|
|
2832
4000
|
allowedTools;
|
|
2833
4001
|
onToolEvent;
|
|
4002
|
+
enableToolRepair;
|
|
2834
4003
|
constructor(options = {}) {
|
|
2835
4004
|
this.allowedTools = options.allowedTools;
|
|
2836
4005
|
this.onToolEvent = options.onToolEvent;
|
|
4006
|
+
this.enableToolRepair = options.enableToolRepair ?? true;
|
|
2837
4007
|
}
|
|
2838
4008
|
register(tool) {
|
|
2839
4009
|
this.tools.set(tool.name, tool);
|
|
4010
|
+
this.toolNameLookup.set(tool.name.toLowerCase(), tool.name);
|
|
2840
4011
|
}
|
|
2841
4012
|
unregister(name) {
|
|
2842
4013
|
this.tools.delete(name);
|
|
4014
|
+
this.toolNameLookup.delete(name.toLowerCase());
|
|
2843
4015
|
}
|
|
2844
4016
|
get(name) {
|
|
2845
4017
|
const tool = this.tools.get(name);
|
|
2846
4018
|
if (tool) {
|
|
2847
4019
|
return tool;
|
|
2848
4020
|
}
|
|
4021
|
+
if (this.enableToolRepair) {
|
|
4022
|
+
const lowerName = name.toLowerCase();
|
|
4023
|
+
const originalName = this.toolNameLookup.get(lowerName);
|
|
4024
|
+
if (originalName) {
|
|
4025
|
+
return this.tools.get(originalName);
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
2849
4028
|
if (isMCPTool(name)) {
|
|
2850
4029
|
const parsed = parseMCPToolName(name);
|
|
2851
4030
|
if (parsed) {
|
|
@@ -2857,6 +4036,34 @@ class ToolManager {
|
|
|
2857
4036
|
}
|
|
2858
4037
|
return;
|
|
2859
4038
|
}
|
|
4039
|
+
repairToolName(name) {
|
|
4040
|
+
if (this.tools.has(name)) {
|
|
4041
|
+
return { repaired: false, originalName: name };
|
|
4042
|
+
}
|
|
4043
|
+
const lowerName = name.toLowerCase();
|
|
4044
|
+
const originalName = this.toolNameLookup.get(lowerName);
|
|
4045
|
+
if (originalName && originalName !== name) {
|
|
4046
|
+
return {
|
|
4047
|
+
repaired: true,
|
|
4048
|
+
originalName: name,
|
|
4049
|
+
repairedName: originalName
|
|
4050
|
+
};
|
|
4051
|
+
}
|
|
4052
|
+
if (isMCPTool(name)) {
|
|
4053
|
+
const parsed = parseMCPToolName(name);
|
|
4054
|
+
if (parsed) {
|
|
4055
|
+
const wrapper = this.mcpServers.get(parsed.serverName);
|
|
4056
|
+
if (wrapper) {
|
|
4057
|
+
return { repaired: false, originalName: name };
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
return {
|
|
4062
|
+
repaired: false,
|
|
4063
|
+
originalName: name,
|
|
4064
|
+
error: `Tool "${name}" not found. Available tools: ${Array.from(this.tools.keys()).join(", ")}`
|
|
4065
|
+
};
|
|
4066
|
+
}
|
|
2860
4067
|
getAll() {
|
|
2861
4068
|
const allTools = [];
|
|
2862
4069
|
for (const tool of this.tools.values()) {
|
|
@@ -2874,6 +4081,7 @@ class ToolManager {
|
|
|
2874
4081
|
}
|
|
2875
4082
|
clear() {
|
|
2876
4083
|
this.tools.clear();
|
|
4084
|
+
this.toolNameLookup.clear();
|
|
2877
4085
|
}
|
|
2878
4086
|
async registerMCPServer(server) {
|
|
2879
4087
|
const wrapper = new MCPServerWrapper(server);
|
|
@@ -2881,6 +4089,7 @@ class ToolManager {
|
|
|
2881
4089
|
const tools = await wrapper.getTools();
|
|
2882
4090
|
for (const tool of tools) {
|
|
2883
4091
|
this.tools.set(tool.name, tool);
|
|
4092
|
+
this.toolNameLookup.set(tool.name.toLowerCase(), tool.name);
|
|
2884
4093
|
}
|
|
2885
4094
|
return tools.length;
|
|
2886
4095
|
}
|
|
@@ -2892,6 +4101,7 @@ class ToolManager {
|
|
|
2892
4101
|
const parsed = parseMCPToolName(name);
|
|
2893
4102
|
if (parsed?.serverName === serverName) {
|
|
2894
4103
|
this.tools.delete(name);
|
|
4104
|
+
this.toolNameLookup.delete(name.toLowerCase());
|
|
2895
4105
|
}
|
|
2896
4106
|
}
|
|
2897
4107
|
}
|
|
@@ -2903,9 +4113,23 @@ class ToolManager {
|
|
|
2903
4113
|
return Array.from(this.mcpServers.keys());
|
|
2904
4114
|
}
|
|
2905
4115
|
async execute(name, input, context) {
|
|
2906
|
-
|
|
4116
|
+
let effectiveName = name;
|
|
4117
|
+
let tool = this.tools.get(name);
|
|
4118
|
+
if (!tool && this.enableToolRepair) {
|
|
4119
|
+
const repairResult = this.repairToolName(name);
|
|
4120
|
+
if (repairResult.repaired && repairResult.repairedName) {
|
|
4121
|
+
effectiveName = repairResult.repairedName;
|
|
4122
|
+
tool = this.tools.get(effectiveName);
|
|
4123
|
+
console.debug(`[ToolManager] Repaired tool name: "${name}" -> "${effectiveName}"`);
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
if (!tool) {
|
|
4127
|
+
tool = this.get(name);
|
|
4128
|
+
}
|
|
2907
4129
|
if (!tool) {
|
|
2908
|
-
|
|
4130
|
+
const availableTools = Array.from(this.tools.keys()).slice(0, 10);
|
|
4131
|
+
const suffix = this.tools.size > 10 ? ` (and ${this.tools.size - 10} more)` : "";
|
|
4132
|
+
throw new Error(`Tool not found: "${name}". Available tools: ${availableTools.join(", ")}${suffix}`);
|
|
2909
4133
|
}
|
|
2910
4134
|
if (!this.isToolAllowed(tool)) {
|
|
2911
4135
|
throw new Error(`Tool not allowed: ${name}`);
|
|
@@ -2914,7 +4138,7 @@ class ToolManager {
|
|
|
2914
4138
|
this.emitEvent({
|
|
2915
4139
|
type: "tool_start",
|
|
2916
4140
|
toolId,
|
|
2917
|
-
toolName:
|
|
4141
|
+
toolName: effectiveName,
|
|
2918
4142
|
input
|
|
2919
4143
|
});
|
|
2920
4144
|
try {
|
|
@@ -3220,869 +4444,1618 @@ var BINARY_EXTENSIONS = new Set([
|
|
|
3220
4444
|
function createReadTool(options = {}) {
|
|
3221
4445
|
const maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
3222
4446
|
return {
|
|
3223
|
-
name: "Read",
|
|
3224
|
-
description: `Read file contents. Supports text files with optional line range. Returns line numbers in output. For images/PDFs, returns metadata only.`,
|
|
4447
|
+
name: "Read",
|
|
4448
|
+
description: `Read file contents. Supports text files with optional line range. Returns line numbers in output. For images/PDFs, returns metadata only.`,
|
|
4449
|
+
inputSchema: {
|
|
4450
|
+
type: "object",
|
|
4451
|
+
properties: {
|
|
4452
|
+
file_path: {
|
|
4453
|
+
type: "string",
|
|
4454
|
+
description: "Absolute path to the file to read"
|
|
4455
|
+
},
|
|
4456
|
+
offset: {
|
|
4457
|
+
type: "number",
|
|
4458
|
+
description: "Line number to start reading from (1-indexed)"
|
|
4459
|
+
},
|
|
4460
|
+
limit: {
|
|
4461
|
+
type: "number",
|
|
4462
|
+
description: "Number of lines to read (default: 2000)"
|
|
4463
|
+
}
|
|
4464
|
+
},
|
|
4465
|
+
required: ["file_path"]
|
|
4466
|
+
},
|
|
4467
|
+
execute: async (rawInput, _context) => {
|
|
4468
|
+
const input = rawInput;
|
|
4469
|
+
const { file_path, offset = 1, limit = DEFAULT_LINE_LIMIT } = input;
|
|
4470
|
+
const access = checkPathAccess(file_path, options, "file");
|
|
4471
|
+
if (!access.ok) {
|
|
4472
|
+
return { content: access.error, isError: true };
|
|
4473
|
+
}
|
|
4474
|
+
if (!existsSync4(access.resolved)) {
|
|
4475
|
+
return {
|
|
4476
|
+
content: `File not found: ${access.resolved}`,
|
|
4477
|
+
isError: true
|
|
4478
|
+
};
|
|
4479
|
+
}
|
|
4480
|
+
const fileStat = await stat(access.resolved);
|
|
4481
|
+
if (fileStat.size > maxFileSize) {
|
|
4482
|
+
return {
|
|
4483
|
+
content: `File too large: ${fileStat.size} bytes (max: ${maxFileSize} bytes)`,
|
|
4484
|
+
isError: true
|
|
4485
|
+
};
|
|
4486
|
+
}
|
|
4487
|
+
const ext = extname(access.resolved).toLowerCase();
|
|
4488
|
+
if (BINARY_EXTENSIONS.has(ext)) {
|
|
4489
|
+
return {
|
|
4490
|
+
content: `Binary file: ${access.resolved}
|
|
4491
|
+
Size: ${fileStat.size} bytes
|
|
4492
|
+
Type: ${ext}`
|
|
4493
|
+
};
|
|
4494
|
+
}
|
|
4495
|
+
try {
|
|
4496
|
+
const content = await readFile3(access.resolved, "utf-8");
|
|
4497
|
+
const lines = content.split(`
|
|
4498
|
+
`);
|
|
4499
|
+
const startLine = Math.max(1, offset);
|
|
4500
|
+
const endLine = Math.min(lines.length, startLine + limit - 1);
|
|
4501
|
+
const outputLines = [];
|
|
4502
|
+
for (let i = startLine - 1;i < endLine; i++) {
|
|
4503
|
+
const lineNum = i + 1;
|
|
4504
|
+
let line = lines[i];
|
|
4505
|
+
if (line.length > MAX_LINE_LENGTH) {
|
|
4506
|
+
line = line.slice(0, MAX_LINE_LENGTH) + "...";
|
|
4507
|
+
}
|
|
4508
|
+
const padding = String(endLine).length;
|
|
4509
|
+
outputLines.push(`${String(lineNum).padStart(padding)}→${line}`);
|
|
4510
|
+
}
|
|
4511
|
+
let header = "";
|
|
4512
|
+
if (startLine > 1 || endLine < lines.length) {
|
|
4513
|
+
header = `[Lines ${startLine}-${endLine} of ${lines.length}]
|
|
4514
|
+
|
|
4515
|
+
`;
|
|
4516
|
+
}
|
|
4517
|
+
return {
|
|
4518
|
+
content: header + outputLines.join(`
|
|
4519
|
+
`)
|
|
4520
|
+
};
|
|
4521
|
+
} catch (error) {
|
|
4522
|
+
return {
|
|
4523
|
+
content: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
|
|
4524
|
+
isError: true
|
|
4525
|
+
};
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
};
|
|
4529
|
+
}
|
|
4530
|
+
var ReadTool = createReadTool();
|
|
4531
|
+
// src/tools/builtin/write.ts
|
|
4532
|
+
import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
|
|
4533
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
4534
|
+
import { dirname as dirname3 } from "node:path";
|
|
4535
|
+
function createWriteTool(options = {}) {
|
|
4536
|
+
return {
|
|
4537
|
+
name: "Write",
|
|
4538
|
+
description: `Write content to a file. Creates parent directories if needed. Overwrites existing files. Use Edit tool for modifying existing files.`,
|
|
4539
|
+
inputSchema: {
|
|
4540
|
+
type: "object",
|
|
4541
|
+
properties: {
|
|
4542
|
+
file_path: {
|
|
4543
|
+
type: "string",
|
|
4544
|
+
description: "Absolute path to the file to write"
|
|
4545
|
+
},
|
|
4546
|
+
content: {
|
|
4547
|
+
type: "string",
|
|
4548
|
+
description: "Content to write to the file"
|
|
4549
|
+
}
|
|
4550
|
+
},
|
|
4551
|
+
required: ["file_path", "content"]
|
|
4552
|
+
},
|
|
4553
|
+
execute: async (rawInput, _context) => {
|
|
4554
|
+
const input = rawInput;
|
|
4555
|
+
const { file_path, content } = input;
|
|
4556
|
+
const access = checkPathAccess(file_path, options, "file");
|
|
4557
|
+
if (!access.ok) {
|
|
4558
|
+
return { content: access.error, isError: true };
|
|
4559
|
+
}
|
|
4560
|
+
try {
|
|
4561
|
+
const dir = dirname3(access.resolved);
|
|
4562
|
+
if (!existsSync5(dir)) {
|
|
4563
|
+
await mkdir2(dir, { recursive: true });
|
|
4564
|
+
}
|
|
4565
|
+
await writeFile2(access.resolved, content, "utf-8");
|
|
4566
|
+
const lines = content.split(`
|
|
4567
|
+
`).length;
|
|
4568
|
+
const bytes = Buffer.byteLength(content, "utf-8");
|
|
4569
|
+
return {
|
|
4570
|
+
content: `Successfully wrote ${bytes} bytes (${lines} lines) to ${access.resolved}`
|
|
4571
|
+
};
|
|
4572
|
+
} catch (error) {
|
|
4573
|
+
return {
|
|
4574
|
+
content: `Failed to write file: ${error instanceof Error ? error.message : String(error)}`,
|
|
4575
|
+
isError: true
|
|
4576
|
+
};
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
};
|
|
4580
|
+
}
|
|
4581
|
+
var WriteTool = createWriteTool();
|
|
4582
|
+
// src/tools/builtin/edit.ts
|
|
4583
|
+
import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
4584
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
4585
|
+
function createEditTool(options = {}) {
|
|
4586
|
+
return {
|
|
4587
|
+
name: "Edit",
|
|
4588
|
+
description: `Edit a file by replacing text. Finds old_string and replaces with new_string. The old_string must be unique in the file unless replace_all is true.`,
|
|
3225
4589
|
inputSchema: {
|
|
3226
4590
|
type: "object",
|
|
3227
4591
|
properties: {
|
|
3228
4592
|
file_path: {
|
|
3229
4593
|
type: "string",
|
|
3230
|
-
description: "Absolute path to the file to
|
|
4594
|
+
description: "Absolute path to the file to edit"
|
|
3231
4595
|
},
|
|
3232
|
-
|
|
3233
|
-
type: "
|
|
3234
|
-
description: "
|
|
4596
|
+
old_string: {
|
|
4597
|
+
type: "string",
|
|
4598
|
+
description: "Text to find and replace"
|
|
3235
4599
|
},
|
|
3236
|
-
|
|
3237
|
-
type: "
|
|
3238
|
-
description: "
|
|
4600
|
+
new_string: {
|
|
4601
|
+
type: "string",
|
|
4602
|
+
description: "Replacement text"
|
|
4603
|
+
},
|
|
4604
|
+
replace_all: {
|
|
4605
|
+
type: "boolean",
|
|
4606
|
+
description: "Replace all occurrences (default: false)",
|
|
4607
|
+
default: false
|
|
3239
4608
|
}
|
|
3240
4609
|
},
|
|
3241
|
-
required: ["file_path"]
|
|
4610
|
+
required: ["file_path", "old_string", "new_string"]
|
|
3242
4611
|
},
|
|
3243
4612
|
execute: async (rawInput, _context) => {
|
|
3244
4613
|
const input = rawInput;
|
|
3245
|
-
const { file_path,
|
|
4614
|
+
const { file_path, old_string, new_string, replace_all = false } = input;
|
|
3246
4615
|
const access = checkPathAccess(file_path, options, "file");
|
|
3247
4616
|
if (!access.ok) {
|
|
3248
4617
|
return { content: access.error, isError: true };
|
|
3249
4618
|
}
|
|
3250
|
-
if (!
|
|
4619
|
+
if (!existsSync6(access.resolved)) {
|
|
3251
4620
|
return {
|
|
3252
4621
|
content: `File not found: ${access.resolved}`,
|
|
3253
4622
|
isError: true
|
|
3254
4623
|
};
|
|
3255
4624
|
}
|
|
3256
|
-
|
|
3257
|
-
if (fileStat.size > maxFileSize) {
|
|
4625
|
+
if (old_string === new_string) {
|
|
3258
4626
|
return {
|
|
3259
|
-
content: `
|
|
4627
|
+
content: `old_string and new_string are identical. No changes needed.`,
|
|
3260
4628
|
isError: true
|
|
3261
4629
|
};
|
|
3262
4630
|
}
|
|
3263
|
-
const ext = extname(access.resolved).toLowerCase();
|
|
3264
|
-
if (BINARY_EXTENSIONS.has(ext)) {
|
|
3265
|
-
return {
|
|
3266
|
-
content: `Binary file: ${access.resolved}
|
|
3267
|
-
Size: ${fileStat.size} bytes
|
|
3268
|
-
Type: ${ext}`
|
|
3269
|
-
};
|
|
3270
|
-
}
|
|
3271
4631
|
try {
|
|
3272
|
-
const content = await
|
|
3273
|
-
const
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
const lineNum = i + 1;
|
|
3280
|
-
let line = lines[i];
|
|
3281
|
-
if (line.length > MAX_LINE_LENGTH) {
|
|
3282
|
-
line = line.slice(0, MAX_LINE_LENGTH) + "...";
|
|
3283
|
-
}
|
|
3284
|
-
const padding = String(endLine).length;
|
|
3285
|
-
outputLines.push(`${String(lineNum).padStart(padding)}→${line}`);
|
|
4632
|
+
const content = await readFile4(access.resolved, "utf-8");
|
|
4633
|
+
const occurrences = content.split(old_string).length - 1;
|
|
4634
|
+
if (occurrences === 0) {
|
|
4635
|
+
return {
|
|
4636
|
+
content: `Text not found in file: "${old_string.slice(0, 100)}${old_string.length > 100 ? "..." : ""}"`,
|
|
4637
|
+
isError: true
|
|
4638
|
+
};
|
|
3286
4639
|
}
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
4640
|
+
if (!replace_all && occurrences > 1) {
|
|
4641
|
+
return {
|
|
4642
|
+
content: `Found ${occurrences} occurrences of the text. Use replace_all: true to replace all, or provide a more unique string.`,
|
|
4643
|
+
isError: true
|
|
4644
|
+
};
|
|
4645
|
+
}
|
|
4646
|
+
let newContent;
|
|
4647
|
+
let replacedCount;
|
|
4648
|
+
if (replace_all) {
|
|
4649
|
+
newContent = content.split(old_string).join(new_string);
|
|
4650
|
+
replacedCount = occurrences;
|
|
4651
|
+
} else {
|
|
4652
|
+
newContent = content.replace(old_string, new_string);
|
|
4653
|
+
replacedCount = 1;
|
|
3292
4654
|
}
|
|
4655
|
+
await writeFile3(access.resolved, newContent, "utf-8");
|
|
3293
4656
|
return {
|
|
3294
|
-
content:
|
|
3295
|
-
`)
|
|
4657
|
+
content: `Successfully replaced ${replacedCount} occurrence${replacedCount > 1 ? "s" : ""} in ${access.resolved}`
|
|
3296
4658
|
};
|
|
3297
4659
|
} catch (error) {
|
|
3298
4660
|
return {
|
|
3299
|
-
content: `Failed to
|
|
4661
|
+
content: `Failed to edit file: ${error instanceof Error ? error.message : String(error)}`,
|
|
3300
4662
|
isError: true
|
|
3301
4663
|
};
|
|
3302
4664
|
}
|
|
3303
4665
|
}
|
|
3304
4666
|
};
|
|
3305
4667
|
}
|
|
3306
|
-
var
|
|
3307
|
-
// src/tools/builtin/
|
|
3308
|
-
import {
|
|
3309
|
-
import { existsSync as
|
|
3310
|
-
import {
|
|
3311
|
-
|
|
4668
|
+
var EditTool = createEditTool();
|
|
4669
|
+
// src/tools/builtin/glob.ts
|
|
4670
|
+
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
|
4671
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
4672
|
+
import { join as join5, relative } from "node:path";
|
|
4673
|
+
var MAX_RESULTS = 1000;
|
|
4674
|
+
function matchGlob(pattern, path2) {
|
|
4675
|
+
const regexPattern = pattern.replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/{{GLOBSTAR}}/g, ".*").replace(/\./g, "\\.");
|
|
4676
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
4677
|
+
return regex.test(path2);
|
|
4678
|
+
}
|
|
4679
|
+
async function scanDir(dir, pattern, results, baseDir, maxDepth = 10, currentDepth = 0) {
|
|
4680
|
+
if (currentDepth > maxDepth || results.length >= MAX_RESULTS) {
|
|
4681
|
+
return;
|
|
4682
|
+
}
|
|
4683
|
+
if (!existsSync7(dir)) {
|
|
4684
|
+
return;
|
|
4685
|
+
}
|
|
4686
|
+
try {
|
|
4687
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
4688
|
+
for (const entry of entries) {
|
|
4689
|
+
if (results.length >= MAX_RESULTS)
|
|
4690
|
+
break;
|
|
4691
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") {
|
|
4692
|
+
continue;
|
|
4693
|
+
}
|
|
4694
|
+
const fullPath = join5(dir, entry.name);
|
|
4695
|
+
const relativePath = relative(baseDir, fullPath);
|
|
4696
|
+
if (entry.isDirectory()) {
|
|
4697
|
+
if (matchGlob(pattern, relativePath) || matchGlob(pattern, relativePath + "/")) {
|
|
4698
|
+
results.push(fullPath);
|
|
4699
|
+
}
|
|
4700
|
+
await scanDir(fullPath, pattern, results, baseDir, maxDepth, currentDepth + 1);
|
|
4701
|
+
} else if (entry.isFile()) {
|
|
4702
|
+
if (matchGlob(pattern, relativePath)) {
|
|
4703
|
+
results.push(fullPath);
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
} catch {}
|
|
4708
|
+
}
|
|
4709
|
+
function createGlobTool(options = {}) {
|
|
4710
|
+
const defaultCwd = options.cwd ?? process.cwd();
|
|
3312
4711
|
return {
|
|
3313
|
-
name: "
|
|
3314
|
-
description: `
|
|
4712
|
+
name: "Glob",
|
|
4713
|
+
description: `Find files matching a glob pattern. Supports ** for recursive matching, * for single directory, ? for single character. Returns file paths sorted by modification time.`,
|
|
3315
4714
|
inputSchema: {
|
|
3316
4715
|
type: "object",
|
|
3317
4716
|
properties: {
|
|
3318
|
-
|
|
4717
|
+
pattern: {
|
|
3319
4718
|
type: "string",
|
|
3320
|
-
description:
|
|
4719
|
+
description: 'Glob pattern to match (e.g., "**/*.ts", "src/**/*.js")'
|
|
3321
4720
|
},
|
|
3322
|
-
|
|
4721
|
+
path: {
|
|
3323
4722
|
type: "string",
|
|
3324
|
-
description: "
|
|
4723
|
+
description: "Directory to search in (default: current directory)"
|
|
3325
4724
|
}
|
|
3326
4725
|
},
|
|
3327
|
-
required: ["
|
|
4726
|
+
required: ["pattern"]
|
|
3328
4727
|
},
|
|
3329
4728
|
execute: async (rawInput, _context) => {
|
|
3330
4729
|
const input = rawInput;
|
|
3331
|
-
const {
|
|
3332
|
-
const access =
|
|
4730
|
+
const { pattern, path: path2 = defaultCwd } = input;
|
|
4731
|
+
const access = checkDirAccess(path2, options);
|
|
3333
4732
|
if (!access.ok) {
|
|
3334
4733
|
return { content: access.error, isError: true };
|
|
3335
4734
|
}
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
4735
|
+
if (!existsSync7(access.resolved)) {
|
|
4736
|
+
return {
|
|
4737
|
+
content: `Directory not found: ${access.resolved}`,
|
|
4738
|
+
isError: true
|
|
4739
|
+
};
|
|
4740
|
+
}
|
|
4741
|
+
try {
|
|
4742
|
+
const results = [];
|
|
4743
|
+
await scanDir(access.resolved, pattern, results, access.resolved);
|
|
4744
|
+
if (results.length === 0) {
|
|
4745
|
+
return {
|
|
4746
|
+
content: `No files found matching pattern: ${pattern}`
|
|
4747
|
+
};
|
|
4748
|
+
}
|
|
4749
|
+
const filesWithStats = await Promise.all(results.map(async (file) => {
|
|
4750
|
+
try {
|
|
4751
|
+
const stats = await stat2(file);
|
|
4752
|
+
return { file, mtime: stats.mtime.getTime() };
|
|
4753
|
+
} catch {
|
|
4754
|
+
return { file, mtime: 0 };
|
|
4755
|
+
}
|
|
4756
|
+
}));
|
|
4757
|
+
filesWithStats.sort((a, b) => b.mtime - a.mtime);
|
|
4758
|
+
const output = filesWithStats.map((f) => f.file).join(`
|
|
4759
|
+
`);
|
|
4760
|
+
const truncated = results.length >= MAX_RESULTS ? `
|
|
4761
|
+
|
|
4762
|
+
(Results truncated at ${MAX_RESULTS} files)` : "";
|
|
4763
|
+
return {
|
|
4764
|
+
content: `Found ${results.length} files:
|
|
4765
|
+
|
|
4766
|
+
${output}${truncated}`
|
|
4767
|
+
};
|
|
4768
|
+
} catch (error) {
|
|
4769
|
+
return {
|
|
4770
|
+
content: `Failed to search: ${error instanceof Error ? error.message : String(error)}`,
|
|
4771
|
+
isError: true
|
|
4772
|
+
};
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
};
|
|
4776
|
+
}
|
|
4777
|
+
var GlobTool = createGlobTool();
|
|
4778
|
+
// src/tools/builtin/grep.ts
|
|
4779
|
+
import { readFile as readFile5, readdir as readdir3, stat as stat3 } from "node:fs/promises";
|
|
4780
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
4781
|
+
import { join as join6, extname as extname2 } from "node:path";
|
|
4782
|
+
var MAX_RESULTS2 = 500;
|
|
4783
|
+
var MAX_LINE_LENGTH2 = 500;
|
|
4784
|
+
var BINARY_EXTENSIONS2 = new Set([
|
|
4785
|
+
".png",
|
|
4786
|
+
".jpg",
|
|
4787
|
+
".jpeg",
|
|
4788
|
+
".gif",
|
|
4789
|
+
".bmp",
|
|
4790
|
+
".ico",
|
|
4791
|
+
".webp",
|
|
4792
|
+
".mp3",
|
|
4793
|
+
".mp4",
|
|
4794
|
+
".wav",
|
|
4795
|
+
".avi",
|
|
4796
|
+
".mov",
|
|
4797
|
+
".mkv",
|
|
4798
|
+
".zip",
|
|
4799
|
+
".tar",
|
|
4800
|
+
".gz",
|
|
4801
|
+
".rar",
|
|
4802
|
+
".7z",
|
|
4803
|
+
".exe",
|
|
4804
|
+
".dll",
|
|
4805
|
+
".so",
|
|
4806
|
+
".dylib",
|
|
4807
|
+
".pdf",
|
|
4808
|
+
".doc",
|
|
4809
|
+
".docx",
|
|
4810
|
+
".xls",
|
|
4811
|
+
".xlsx",
|
|
4812
|
+
".woff",
|
|
4813
|
+
".woff2",
|
|
4814
|
+
".ttf",
|
|
4815
|
+
".otf",
|
|
4816
|
+
".eot",
|
|
4817
|
+
".lock"
|
|
4818
|
+
]);
|
|
4819
|
+
function matchGlob2(pattern, filename) {
|
|
4820
|
+
const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/\./g, "\\.");
|
|
4821
|
+
return new RegExp(`^${regexPattern}$`).test(filename);
|
|
4822
|
+
}
|
|
4823
|
+
async function searchFile(filePath, regex, before, after) {
|
|
4824
|
+
const matches = [];
|
|
4825
|
+
try {
|
|
4826
|
+
const content = await readFile5(filePath, "utf-8");
|
|
4827
|
+
const lines = content.split(`
|
|
4828
|
+
`);
|
|
4829
|
+
for (let i = 0;i < lines.length; i++) {
|
|
4830
|
+
if (regex.test(lines[i])) {
|
|
4831
|
+
const match = {
|
|
4832
|
+
file: filePath,
|
|
4833
|
+
line: i + 1,
|
|
4834
|
+
content: lines[i].slice(0, MAX_LINE_LENGTH2)
|
|
4835
|
+
};
|
|
4836
|
+
if (before > 0) {
|
|
4837
|
+
match.before = lines.slice(Math.max(0, i - before), i).map((l) => l.slice(0, MAX_LINE_LENGTH2));
|
|
4838
|
+
}
|
|
4839
|
+
if (after > 0) {
|
|
4840
|
+
match.after = lines.slice(i + 1, i + 1 + after).map((l) => l.slice(0, MAX_LINE_LENGTH2));
|
|
4841
|
+
}
|
|
4842
|
+
matches.push(match);
|
|
4843
|
+
if (matches.length >= MAX_RESULTS2) {
|
|
4844
|
+
break;
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
}
|
|
4848
|
+
} catch {}
|
|
4849
|
+
return matches;
|
|
4850
|
+
}
|
|
4851
|
+
async function searchDir(dir, regex, glob, before, after, results, maxDepth = 10, currentDepth = 0) {
|
|
4852
|
+
if (currentDepth > maxDepth || results.length >= MAX_RESULTS2) {
|
|
4853
|
+
return;
|
|
4854
|
+
}
|
|
4855
|
+
try {
|
|
4856
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
4857
|
+
for (const entry of entries) {
|
|
4858
|
+
if (results.length >= MAX_RESULTS2)
|
|
4859
|
+
break;
|
|
4860
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") {
|
|
4861
|
+
continue;
|
|
4862
|
+
}
|
|
4863
|
+
const fullPath = join6(dir, entry.name);
|
|
4864
|
+
if (entry.isDirectory()) {
|
|
4865
|
+
await searchDir(fullPath, regex, glob, before, after, results, maxDepth, currentDepth + 1);
|
|
4866
|
+
} else if (entry.isFile()) {
|
|
4867
|
+
const ext = extname2(entry.name).toLowerCase();
|
|
4868
|
+
if (BINARY_EXTENSIONS2.has(ext)) {
|
|
4869
|
+
continue;
|
|
3340
4870
|
}
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
const
|
|
3345
|
-
|
|
3346
|
-
content: `Successfully wrote ${bytes} bytes (${lines} lines) to ${access.resolved}`
|
|
3347
|
-
};
|
|
3348
|
-
} catch (error) {
|
|
3349
|
-
return {
|
|
3350
|
-
content: `Failed to write file: ${error instanceof Error ? error.message : String(error)}`,
|
|
3351
|
-
isError: true
|
|
3352
|
-
};
|
|
4871
|
+
if (glob && !matchGlob2(glob, entry.name)) {
|
|
4872
|
+
continue;
|
|
4873
|
+
}
|
|
4874
|
+
const matches = await searchFile(fullPath, regex, before, after);
|
|
4875
|
+
results.push(...matches);
|
|
3353
4876
|
}
|
|
3354
4877
|
}
|
|
3355
|
-
}
|
|
4878
|
+
} catch {}
|
|
3356
4879
|
}
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
import { readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
|
|
3360
|
-
import { existsSync as existsSync6 } from "node:fs";
|
|
3361
|
-
function createEditTool(options = {}) {
|
|
4880
|
+
function createGrepTool(options = {}) {
|
|
4881
|
+
const defaultCwd = options.cwd ?? process.cwd();
|
|
3362
4882
|
return {
|
|
3363
|
-
name: "
|
|
3364
|
-
description: `
|
|
4883
|
+
name: "Grep",
|
|
4884
|
+
description: `Search file contents using regular expressions. Supports context lines before/after matches. Returns matching lines with file paths and line numbers.`,
|
|
3365
4885
|
inputSchema: {
|
|
3366
4886
|
type: "object",
|
|
3367
4887
|
properties: {
|
|
3368
|
-
|
|
4888
|
+
pattern: {
|
|
3369
4889
|
type: "string",
|
|
3370
|
-
description: "
|
|
4890
|
+
description: "Regular expression pattern to search for"
|
|
3371
4891
|
},
|
|
3372
|
-
|
|
4892
|
+
path: {
|
|
3373
4893
|
type: "string",
|
|
3374
|
-
description: "
|
|
4894
|
+
description: "File or directory to search in"
|
|
3375
4895
|
},
|
|
3376
|
-
|
|
4896
|
+
glob: {
|
|
3377
4897
|
type: "string",
|
|
3378
|
-
description:
|
|
4898
|
+
description: 'Glob pattern to filter files (e.g., "*.ts")'
|
|
3379
4899
|
},
|
|
3380
|
-
|
|
4900
|
+
before: {
|
|
4901
|
+
type: "number",
|
|
4902
|
+
description: "Number of lines to show before each match"
|
|
4903
|
+
},
|
|
4904
|
+
after: {
|
|
4905
|
+
type: "number",
|
|
4906
|
+
description: "Number of lines to show after each match"
|
|
4907
|
+
},
|
|
4908
|
+
ignoreCase: {
|
|
3381
4909
|
type: "boolean",
|
|
3382
|
-
description: "
|
|
3383
|
-
default: false
|
|
4910
|
+
description: "Case insensitive search"
|
|
3384
4911
|
}
|
|
3385
4912
|
},
|
|
3386
|
-
required: ["
|
|
4913
|
+
required: ["pattern"]
|
|
3387
4914
|
},
|
|
3388
4915
|
execute: async (rawInput, _context) => {
|
|
3389
4916
|
const input = rawInput;
|
|
3390
|
-
const {
|
|
3391
|
-
|
|
4917
|
+
const {
|
|
4918
|
+
pattern,
|
|
4919
|
+
path: path2 = defaultCwd,
|
|
4920
|
+
glob,
|
|
4921
|
+
before = 0,
|
|
4922
|
+
after = 0,
|
|
4923
|
+
ignoreCase = false
|
|
4924
|
+
} = input;
|
|
4925
|
+
const access = checkPathAccess(path2, options, "dir");
|
|
3392
4926
|
if (!access.ok) {
|
|
3393
4927
|
return { content: access.error, isError: true };
|
|
3394
4928
|
}
|
|
3395
|
-
if (!
|
|
3396
|
-
return {
|
|
3397
|
-
content: `File not found: ${access.resolved}`,
|
|
3398
|
-
isError: true
|
|
3399
|
-
};
|
|
3400
|
-
}
|
|
3401
|
-
if (old_string === new_string) {
|
|
4929
|
+
if (!existsSync8(access.resolved)) {
|
|
3402
4930
|
return {
|
|
3403
|
-
content: `
|
|
4931
|
+
content: `Path not found: ${access.resolved}`,
|
|
3404
4932
|
isError: true
|
|
3405
4933
|
};
|
|
3406
4934
|
}
|
|
3407
4935
|
try {
|
|
3408
|
-
const
|
|
3409
|
-
const
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
4936
|
+
const flags = ignoreCase ? "gi" : "g";
|
|
4937
|
+
const regex = new RegExp(pattern, flags);
|
|
4938
|
+
const results = [];
|
|
4939
|
+
const pathStat = await stat3(access.resolved);
|
|
4940
|
+
if (pathStat.isFile()) {
|
|
4941
|
+
const matches = await searchFile(access.resolved, regex, before, after);
|
|
4942
|
+
results.push(...matches);
|
|
4943
|
+
} else if (pathStat.isDirectory()) {
|
|
4944
|
+
await searchDir(access.resolved, regex, glob, before, after, results);
|
|
3415
4945
|
}
|
|
3416
|
-
if (
|
|
4946
|
+
if (results.length === 0) {
|
|
3417
4947
|
return {
|
|
3418
|
-
content: `
|
|
3419
|
-
isError: true
|
|
4948
|
+
content: `No matches found for pattern: ${pattern}`
|
|
3420
4949
|
};
|
|
3421
4950
|
}
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
4951
|
+
const output = [];
|
|
4952
|
+
for (const match of results) {
|
|
4953
|
+
if (match.before?.length) {
|
|
4954
|
+
for (let i = 0;i < match.before.length; i++) {
|
|
4955
|
+
const lineNum = match.line - match.before.length + i;
|
|
4956
|
+
output.push(`${match.file}:${lineNum}- ${match.before[i]}`);
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
output.push(`${match.file}:${match.line}: ${match.content}`);
|
|
4960
|
+
if (match.after?.length) {
|
|
4961
|
+
for (let i = 0;i < match.after.length; i++) {
|
|
4962
|
+
const lineNum = match.line + i + 1;
|
|
4963
|
+
output.push(`${match.file}:${lineNum}+ ${match.after[i]}`);
|
|
4964
|
+
}
|
|
4965
|
+
}
|
|
4966
|
+
if (match.before?.length || match.after?.length) {
|
|
4967
|
+
output.push("--");
|
|
4968
|
+
}
|
|
3430
4969
|
}
|
|
3431
|
-
|
|
4970
|
+
const truncated = results.length >= MAX_RESULTS2 ? `
|
|
4971
|
+
|
|
4972
|
+
(Results truncated at ${MAX_RESULTS2} matches)` : "";
|
|
3432
4973
|
return {
|
|
3433
|
-
content: `
|
|
4974
|
+
content: `Found ${results.length} matches:
|
|
4975
|
+
|
|
4976
|
+
${output.join(`
|
|
4977
|
+
`)}${truncated}`
|
|
3434
4978
|
};
|
|
3435
4979
|
} catch (error) {
|
|
3436
4980
|
return {
|
|
3437
|
-
content: `
|
|
4981
|
+
content: `Search failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
3438
4982
|
isError: true
|
|
3439
4983
|
};
|
|
3440
4984
|
}
|
|
3441
4985
|
}
|
|
3442
4986
|
};
|
|
3443
4987
|
}
|
|
3444
|
-
var
|
|
3445
|
-
// src/tools/builtin/
|
|
3446
|
-
import {
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
const regexPattern = pattern.replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/{{GLOBSTAR}}/g, ".*").replace(/\./g, "\\.");
|
|
3452
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
3453
|
-
return regex.test(path);
|
|
4988
|
+
var GrepTool = createGrepTool();
|
|
4989
|
+
// src/tools/builtin/webfetch.ts
|
|
4990
|
+
import { lookup } from "node:dns/promises";
|
|
4991
|
+
var MAX_CONTENT_LENGTH = 1e5;
|
|
4992
|
+
var FETCH_TIMEOUT = 30000;
|
|
4993
|
+
function isIpV4(host) {
|
|
4994
|
+
return /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
|
|
3454
4995
|
}
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
4996
|
+
function isIpV6(host) {
|
|
4997
|
+
return /^[0-9a-fA-F:]+$/.test(host) && host.includes(":");
|
|
4998
|
+
}
|
|
4999
|
+
function isPrivateIpv4(ip) {
|
|
5000
|
+
const parts = ip.split(".").map((p) => Number(p));
|
|
5001
|
+
if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255))
|
|
5002
|
+
return true;
|
|
5003
|
+
const [a, b] = parts;
|
|
5004
|
+
if (a === 10)
|
|
5005
|
+
return true;
|
|
5006
|
+
if (a === 127)
|
|
5007
|
+
return true;
|
|
5008
|
+
if (a === 0)
|
|
5009
|
+
return true;
|
|
5010
|
+
if (a === 169 && b === 254)
|
|
5011
|
+
return true;
|
|
5012
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
5013
|
+
return true;
|
|
5014
|
+
if (a === 192 && b === 168)
|
|
5015
|
+
return true;
|
|
5016
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
5017
|
+
return true;
|
|
5018
|
+
if (a >= 224)
|
|
5019
|
+
return true;
|
|
5020
|
+
return false;
|
|
5021
|
+
}
|
|
5022
|
+
function isPrivateIpv6(ip) {
|
|
5023
|
+
const normalized = ip.toLowerCase();
|
|
5024
|
+
if (normalized === "::" || normalized === "::1")
|
|
5025
|
+
return true;
|
|
5026
|
+
if (normalized.startsWith("fe80:"))
|
|
5027
|
+
return true;
|
|
5028
|
+
if (normalized.startsWith("fc") || normalized.startsWith("fd"))
|
|
5029
|
+
return true;
|
|
5030
|
+
return false;
|
|
5031
|
+
}
|
|
5032
|
+
async function denyPrivateNetworkTargets(url, options) {
|
|
5033
|
+
if (options.allowPrivateNetwork)
|
|
5034
|
+
return null;
|
|
5035
|
+
const hostname = url.hostname.toLowerCase();
|
|
5036
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local")) {
|
|
5037
|
+
return `Blocked by policy (allowPrivateNetwork=false): hostname "${hostname}" is local-only`;
|
|
3458
5038
|
}
|
|
3459
|
-
if (
|
|
3460
|
-
return
|
|
5039
|
+
if (isIpV4(hostname) && isPrivateIpv4(hostname)) {
|
|
5040
|
+
return `Blocked by policy (allowPrivateNetwork=false): private IPv4 target "${hostname}"`;
|
|
5041
|
+
}
|
|
5042
|
+
if (isIpV6(hostname) && isPrivateIpv6(hostname)) {
|
|
5043
|
+
return `Blocked by policy (allowPrivateNetwork=false): private IPv6 target "${hostname}"`;
|
|
3461
5044
|
}
|
|
5045
|
+
const resolveHostnames = options.resolveHostnames ?? true;
|
|
5046
|
+
if (!resolveHostnames)
|
|
5047
|
+
return null;
|
|
3462
5048
|
try {
|
|
3463
|
-
const
|
|
3464
|
-
for (const
|
|
3465
|
-
if (
|
|
3466
|
-
|
|
3467
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules") {
|
|
3468
|
-
continue;
|
|
5049
|
+
const addrs = await lookup(hostname, { all: true, verbatim: true });
|
|
5050
|
+
for (const addr of addrs) {
|
|
5051
|
+
if (addr.family === 4 && isPrivateIpv4(addr.address)) {
|
|
5052
|
+
return `Blocked by policy (allowPrivateNetwork=false): "${hostname}" resolves to private IPv4 "${addr.address}"`;
|
|
3469
5053
|
}
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
if (entry.isDirectory()) {
|
|
3473
|
-
if (matchGlob(pattern, relativePath) || matchGlob(pattern, relativePath + "/")) {
|
|
3474
|
-
results.push(fullPath);
|
|
3475
|
-
}
|
|
3476
|
-
await scanDir(fullPath, pattern, results, baseDir, maxDepth, currentDepth + 1);
|
|
3477
|
-
} else if (entry.isFile()) {
|
|
3478
|
-
if (matchGlob(pattern, relativePath)) {
|
|
3479
|
-
results.push(fullPath);
|
|
3480
|
-
}
|
|
5054
|
+
if (addr.family === 6 && isPrivateIpv6(addr.address)) {
|
|
5055
|
+
return `Blocked by policy (allowPrivateNetwork=false): "${hostname}" resolves to private IPv6 "${addr.address}"`;
|
|
3481
5056
|
}
|
|
3482
5057
|
}
|
|
3483
|
-
} catch {
|
|
5058
|
+
} catch (e) {
|
|
5059
|
+
return `DNS resolution failed for "${hostname}" (allowPrivateNetwork=false): ${e instanceof Error ? e.message : String(e)}`;
|
|
5060
|
+
}
|
|
5061
|
+
return null;
|
|
5062
|
+
}
|
|
5063
|
+
function htmlToMarkdown(html) {
|
|
5064
|
+
let md = html;
|
|
5065
|
+
md = md.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
|
|
5066
|
+
md = md.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
5067
|
+
md = md.replace(/<h1[^>]*>(.*?)<\/h1>/gi, `
|
|
5068
|
+
# $1
|
|
5069
|
+
`);
|
|
5070
|
+
md = md.replace(/<h2[^>]*>(.*?)<\/h2>/gi, `
|
|
5071
|
+
## $1
|
|
5072
|
+
`);
|
|
5073
|
+
md = md.replace(/<h3[^>]*>(.*?)<\/h3>/gi, `
|
|
5074
|
+
### $1
|
|
5075
|
+
`);
|
|
5076
|
+
md = md.replace(/<h4[^>]*>(.*?)<\/h4>/gi, `
|
|
5077
|
+
#### $1
|
|
5078
|
+
`);
|
|
5079
|
+
md = md.replace(/<h5[^>]*>(.*?)<\/h5>/gi, `
|
|
5080
|
+
##### $1
|
|
5081
|
+
`);
|
|
5082
|
+
md = md.replace(/<h6[^>]*>(.*?)<\/h6>/gi, `
|
|
5083
|
+
###### $1
|
|
5084
|
+
`);
|
|
5085
|
+
md = md.replace(/<p[^>]*>(.*?)<\/p>/gi, `
|
|
5086
|
+
$1
|
|
5087
|
+
`);
|
|
5088
|
+
md = md.replace(/<a[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi, "[$2]($1)");
|
|
5089
|
+
md = md.replace(/<(strong|b)[^>]*>(.*?)<\/\1>/gi, "**$2**");
|
|
5090
|
+
md = md.replace(/<(em|i)[^>]*>(.*?)<\/\1>/gi, "*$2*");
|
|
5091
|
+
md = md.replace(/<code[^>]*>(.*?)<\/code>/gi, "`$1`");
|
|
5092
|
+
md = md.replace(/<pre[^>]*>(.*?)<\/pre>/gis, "\n```\n$1\n```\n");
|
|
5093
|
+
md = md.replace(/<li[^>]*>(.*?)<\/li>/gi, `- $1
|
|
5094
|
+
`);
|
|
5095
|
+
md = md.replace(/<\/?[ou]l[^>]*>/gi, `
|
|
5096
|
+
`);
|
|
5097
|
+
md = md.replace(/<br\s*\/?>/gi, `
|
|
5098
|
+
`);
|
|
5099
|
+
md = md.replace(/<[^>]+>/g, "");
|
|
5100
|
+
md = md.replace(/ /g, " ");
|
|
5101
|
+
md = md.replace(/&/g, "&");
|
|
5102
|
+
md = md.replace(/</g, "<");
|
|
5103
|
+
md = md.replace(/>/g, ">");
|
|
5104
|
+
md = md.replace(/"/g, '"');
|
|
5105
|
+
md = md.replace(/\n{3,}/g, `
|
|
5106
|
+
|
|
5107
|
+
`);
|
|
5108
|
+
md = md.trim();
|
|
5109
|
+
return md;
|
|
3484
5110
|
}
|
|
3485
|
-
function
|
|
3486
|
-
const defaultCwd = options.cwd ?? process.cwd();
|
|
5111
|
+
function createWebFetchTool(options = {}) {
|
|
3487
5112
|
return {
|
|
3488
|
-
name: "
|
|
3489
|
-
description: `
|
|
5113
|
+
name: "WebFetch",
|
|
5114
|
+
description: `Fetch content from a URL. Converts HTML to markdown for readability. Use for retrieving web pages, documentation, or API responses.`,
|
|
3490
5115
|
inputSchema: {
|
|
3491
5116
|
type: "object",
|
|
3492
5117
|
properties: {
|
|
3493
|
-
|
|
5118
|
+
url: {
|
|
3494
5119
|
type: "string",
|
|
3495
|
-
description:
|
|
5120
|
+
description: "URL to fetch"
|
|
3496
5121
|
},
|
|
3497
|
-
|
|
5122
|
+
prompt: {
|
|
3498
5123
|
type: "string",
|
|
3499
|
-
description: "
|
|
5124
|
+
description: "Optional prompt to describe what information to extract"
|
|
3500
5125
|
}
|
|
3501
5126
|
},
|
|
3502
|
-
required: ["
|
|
5127
|
+
required: ["url"]
|
|
3503
5128
|
},
|
|
3504
5129
|
execute: async (rawInput, _context) => {
|
|
3505
5130
|
const input = rawInput;
|
|
3506
|
-
const {
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
5131
|
+
const { url, prompt } = input;
|
|
5132
|
+
let parsedUrl;
|
|
5133
|
+
try {
|
|
5134
|
+
parsedUrl = new URL(url);
|
|
5135
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
5136
|
+
return {
|
|
5137
|
+
content: `Invalid URL protocol: ${parsedUrl.protocol}. Only http/https are allowed.`,
|
|
5138
|
+
isError: true
|
|
5139
|
+
};
|
|
5140
|
+
}
|
|
5141
|
+
if (parsedUrl.protocol === "http:")
|
|
5142
|
+
parsedUrl.protocol = "https:";
|
|
5143
|
+
} catch {
|
|
3512
5144
|
return {
|
|
3513
|
-
content: `
|
|
5145
|
+
content: `Invalid URL: ${url}`,
|
|
3514
5146
|
isError: true
|
|
3515
5147
|
};
|
|
3516
5148
|
}
|
|
5149
|
+
const ssrfDeny = await denyPrivateNetworkTargets(parsedUrl, options);
|
|
5150
|
+
if (ssrfDeny) {
|
|
5151
|
+
return { content: ssrfDeny, isError: true };
|
|
5152
|
+
}
|
|
3517
5153
|
try {
|
|
3518
|
-
const
|
|
3519
|
-
|
|
3520
|
-
|
|
5154
|
+
const controller = new AbortController;
|
|
5155
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
|
5156
|
+
const response = await fetch(parsedUrl.toString(), {
|
|
5157
|
+
signal: controller.signal,
|
|
5158
|
+
headers: {
|
|
5159
|
+
"User-Agent": "Mozilla/5.0 (compatible; OpenCode-Agent/1.0)",
|
|
5160
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
|
5161
|
+
}
|
|
5162
|
+
});
|
|
5163
|
+
clearTimeout(timeout);
|
|
5164
|
+
if (!response.ok) {
|
|
3521
5165
|
return {
|
|
3522
|
-
content: `
|
|
5166
|
+
content: `HTTP error: ${response.status} ${response.statusText}`,
|
|
5167
|
+
isError: true
|
|
3523
5168
|
};
|
|
3524
5169
|
}
|
|
3525
|
-
const
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
} catch {
|
|
3530
|
-
return { file, mtime: 0 };
|
|
3531
|
-
}
|
|
3532
|
-
}));
|
|
3533
|
-
filesWithStats.sort((a, b) => b.mtime - a.mtime);
|
|
3534
|
-
const output = filesWithStats.map((f) => f.file).join(`
|
|
3535
|
-
`);
|
|
3536
|
-
const truncated = results.length >= MAX_RESULTS ? `
|
|
3537
|
-
|
|
3538
|
-
(Results truncated at ${MAX_RESULTS} files)` : "";
|
|
3539
|
-
return {
|
|
3540
|
-
content: `Found ${results.length} files:
|
|
5170
|
+
const finalUrl = new URL(response.url);
|
|
5171
|
+
if (finalUrl.host !== parsedUrl.host) {
|
|
5172
|
+
return {
|
|
5173
|
+
content: `Redirected to different host: ${response.url}
|
|
3541
5174
|
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
} catch (error) {
|
|
3545
|
-
return {
|
|
3546
|
-
content: `Failed to search: ${error instanceof Error ? error.message : String(error)}`,
|
|
3547
|
-
isError: true
|
|
3548
|
-
};
|
|
3549
|
-
}
|
|
3550
|
-
}
|
|
3551
|
-
};
|
|
3552
|
-
}
|
|
3553
|
-
var GlobTool = createGlobTool();
|
|
3554
|
-
// src/tools/builtin/grep.ts
|
|
3555
|
-
import { readFile as readFile5, readdir as readdir2, stat as stat3 } from "node:fs/promises";
|
|
3556
|
-
import { existsSync as existsSync8 } from "node:fs";
|
|
3557
|
-
import { join as join5, extname as extname2 } from "node:path";
|
|
3558
|
-
var MAX_RESULTS2 = 500;
|
|
3559
|
-
var MAX_LINE_LENGTH2 = 500;
|
|
3560
|
-
var BINARY_EXTENSIONS2 = new Set([
|
|
3561
|
-
".png",
|
|
3562
|
-
".jpg",
|
|
3563
|
-
".jpeg",
|
|
3564
|
-
".gif",
|
|
3565
|
-
".bmp",
|
|
3566
|
-
".ico",
|
|
3567
|
-
".webp",
|
|
3568
|
-
".mp3",
|
|
3569
|
-
".mp4",
|
|
3570
|
-
".wav",
|
|
3571
|
-
".avi",
|
|
3572
|
-
".mov",
|
|
3573
|
-
".mkv",
|
|
3574
|
-
".zip",
|
|
3575
|
-
".tar",
|
|
3576
|
-
".gz",
|
|
3577
|
-
".rar",
|
|
3578
|
-
".7z",
|
|
3579
|
-
".exe",
|
|
3580
|
-
".dll",
|
|
3581
|
-
".so",
|
|
3582
|
-
".dylib",
|
|
3583
|
-
".pdf",
|
|
3584
|
-
".doc",
|
|
3585
|
-
".docx",
|
|
3586
|
-
".xls",
|
|
3587
|
-
".xlsx",
|
|
3588
|
-
".woff",
|
|
3589
|
-
".woff2",
|
|
3590
|
-
".ttf",
|
|
3591
|
-
".otf",
|
|
3592
|
-
".eot",
|
|
3593
|
-
".lock"
|
|
3594
|
-
]);
|
|
3595
|
-
function matchGlob2(pattern, filename) {
|
|
3596
|
-
const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/\./g, "\\.");
|
|
3597
|
-
return new RegExp(`^${regexPattern}$`).test(filename);
|
|
3598
|
-
}
|
|
3599
|
-
async function searchFile(filePath, regex, before, after) {
|
|
3600
|
-
const matches = [];
|
|
3601
|
-
try {
|
|
3602
|
-
const content = await readFile5(filePath, "utf-8");
|
|
3603
|
-
const lines = content.split(`
|
|
3604
|
-
`);
|
|
3605
|
-
for (let i = 0;i < lines.length; i++) {
|
|
3606
|
-
if (regex.test(lines[i])) {
|
|
3607
|
-
const match = {
|
|
3608
|
-
file: filePath,
|
|
3609
|
-
line: i + 1,
|
|
3610
|
-
content: lines[i].slice(0, MAX_LINE_LENGTH2)
|
|
3611
|
-
};
|
|
3612
|
-
if (before > 0) {
|
|
3613
|
-
match.before = lines.slice(Math.max(0, i - before), i).map((l) => l.slice(0, MAX_LINE_LENGTH2));
|
|
5175
|
+
Please fetch the new URL if you want to continue.`
|
|
5176
|
+
};
|
|
3614
5177
|
}
|
|
3615
|
-
|
|
3616
|
-
|
|
5178
|
+
const ssrfDenyAfter = await denyPrivateNetworkTargets(finalUrl, options);
|
|
5179
|
+
if (ssrfDenyAfter) {
|
|
5180
|
+
return { content: `Redirect target denied: ${ssrfDenyAfter}`, isError: true };
|
|
3617
5181
|
}
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
5182
|
+
const contentType = response.headers.get("content-type") || "";
|
|
5183
|
+
let content = await response.text();
|
|
5184
|
+
if (content.length > MAX_CONTENT_LENGTH) {
|
|
5185
|
+
content = content.slice(0, MAX_CONTENT_LENGTH) + `
|
|
5186
|
+
|
|
5187
|
+
... (content truncated)`;
|
|
3621
5188
|
}
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
} catch {}
|
|
3625
|
-
return matches;
|
|
3626
|
-
}
|
|
3627
|
-
async function searchDir(dir, regex, glob, before, after, results, maxDepth = 10, currentDepth = 0) {
|
|
3628
|
-
if (currentDepth > maxDepth || results.length >= MAX_RESULTS2) {
|
|
3629
|
-
return;
|
|
3630
|
-
}
|
|
3631
|
-
try {
|
|
3632
|
-
const entries = await readdir2(dir, { withFileTypes: true });
|
|
3633
|
-
for (const entry of entries) {
|
|
3634
|
-
if (results.length >= MAX_RESULTS2)
|
|
3635
|
-
break;
|
|
3636
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules") {
|
|
3637
|
-
continue;
|
|
3638
|
-
}
|
|
3639
|
-
const fullPath = join5(dir, entry.name);
|
|
3640
|
-
if (entry.isDirectory()) {
|
|
3641
|
-
await searchDir(fullPath, regex, glob, before, after, results, maxDepth, currentDepth + 1);
|
|
3642
|
-
} else if (entry.isFile()) {
|
|
3643
|
-
const ext = extname2(entry.name).toLowerCase();
|
|
3644
|
-
if (BINARY_EXTENSIONS2.has(ext)) {
|
|
3645
|
-
continue;
|
|
5189
|
+
if (contentType.includes("text/html")) {
|
|
5190
|
+
content = htmlToMarkdown(content);
|
|
3646
5191
|
}
|
|
3647
|
-
|
|
3648
|
-
|
|
5192
|
+
let output = `URL: ${response.url}
|
|
5193
|
+
`;
|
|
5194
|
+
output += `Content-Type: ${contentType}
|
|
5195
|
+
|
|
5196
|
+
`;
|
|
5197
|
+
if (prompt) {
|
|
5198
|
+
output += `Requested: ${prompt}
|
|
5199
|
+
|
|
5200
|
+
`;
|
|
3649
5201
|
}
|
|
3650
|
-
|
|
3651
|
-
|
|
5202
|
+
output += content;
|
|
5203
|
+
return {
|
|
5204
|
+
content: output
|
|
5205
|
+
};
|
|
5206
|
+
} catch (error) {
|
|
5207
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
5208
|
+
return {
|
|
5209
|
+
content: `Request timed out after ${FETCH_TIMEOUT}ms`,
|
|
5210
|
+
isError: true
|
|
5211
|
+
};
|
|
5212
|
+
}
|
|
5213
|
+
return {
|
|
5214
|
+
content: `Failed to fetch: ${error instanceof Error ? error.message : String(error)}`,
|
|
5215
|
+
isError: true
|
|
5216
|
+
};
|
|
3652
5217
|
}
|
|
3653
5218
|
}
|
|
3654
|
-
}
|
|
5219
|
+
};
|
|
3655
5220
|
}
|
|
3656
|
-
|
|
3657
|
-
|
|
5221
|
+
var WebFetchTool = createWebFetchTool();
|
|
5222
|
+
// src/tools/builtin/websearch.ts
|
|
5223
|
+
var API_CONFIG = {
|
|
5224
|
+
BASE_URL: "https://mcp.exa.ai",
|
|
5225
|
+
ENDPOINTS: {
|
|
5226
|
+
SEARCH: "/mcp"
|
|
5227
|
+
},
|
|
5228
|
+
DEFAULT_NUM_RESULTS: 8,
|
|
5229
|
+
DEFAULT_CONTEXT_MAX_CHARS: 1e4,
|
|
5230
|
+
TIMEOUT_MS: 30000
|
|
5231
|
+
};
|
|
5232
|
+
var WEBSEARCH_DESCRIPTION = `Search the web for up-to-date information using Exa AI.
|
|
5233
|
+
|
|
5234
|
+
Use this tool when you need to:
|
|
5235
|
+
- Find current information, news, or recent events
|
|
5236
|
+
- Look up documentation or technical resources
|
|
5237
|
+
- Research topics beyond your knowledge cutoff
|
|
5238
|
+
- Verify facts or get multiple perspectives
|
|
5239
|
+
|
|
5240
|
+
The search returns curated, AI-optimized content with context.
|
|
5241
|
+
|
|
5242
|
+
Parameters:
|
|
5243
|
+
- query: The search query (required)
|
|
5244
|
+
- numResults: Number of results to return (default: 8, max: 20)
|
|
5245
|
+
- livecrawl: 'fallback' (default) or 'preferred' for fresh content
|
|
5246
|
+
- type: 'auto' (default), 'fast', or 'deep'
|
|
5247
|
+
- contextMaxCharacters: Max characters per result (default: 10000)
|
|
5248
|
+
|
|
5249
|
+
Best practices:
|
|
5250
|
+
- Use specific, detailed queries for better results
|
|
5251
|
+
- For technical topics, include relevant terms and context
|
|
5252
|
+
- Use 'deep' type for comprehensive research
|
|
5253
|
+
- Use 'fast' type for quick fact-checking`;
|
|
5254
|
+
function createWebSearchTool(options = {}) {
|
|
3658
5255
|
return {
|
|
3659
|
-
name: "
|
|
3660
|
-
description:
|
|
5256
|
+
name: "WebSearch",
|
|
5257
|
+
description: WEBSEARCH_DESCRIPTION,
|
|
3661
5258
|
inputSchema: {
|
|
3662
5259
|
type: "object",
|
|
3663
5260
|
properties: {
|
|
3664
|
-
|
|
5261
|
+
query: {
|
|
3665
5262
|
type: "string",
|
|
3666
|
-
description: "
|
|
5263
|
+
description: "Search query to find relevant information"
|
|
3667
5264
|
},
|
|
3668
|
-
|
|
3669
|
-
type: "
|
|
3670
|
-
description: "
|
|
5265
|
+
numResults: {
|
|
5266
|
+
type: "number",
|
|
5267
|
+
description: "Number of search results to return (default: 8, max: 20)"
|
|
3671
5268
|
},
|
|
3672
|
-
|
|
5269
|
+
livecrawl: {
|
|
3673
5270
|
type: "string",
|
|
3674
|
-
|
|
5271
|
+
enum: ["fallback", "preferred"],
|
|
5272
|
+
description: "Live crawl mode - 'fallback': use live crawling as backup, 'preferred': prioritize live crawling"
|
|
3675
5273
|
},
|
|
3676
|
-
|
|
3677
|
-
type: "
|
|
3678
|
-
|
|
5274
|
+
type: {
|
|
5275
|
+
type: "string",
|
|
5276
|
+
enum: ["auto", "fast", "deep"],
|
|
5277
|
+
description: "Search type - 'auto': balanced, 'fast': quick results, 'deep': comprehensive"
|
|
3679
5278
|
},
|
|
3680
|
-
|
|
5279
|
+
contextMaxCharacters: {
|
|
3681
5280
|
type: "number",
|
|
3682
|
-
description: "
|
|
3683
|
-
},
|
|
3684
|
-
ignoreCase: {
|
|
3685
|
-
type: "boolean",
|
|
3686
|
-
description: "Case insensitive search"
|
|
5281
|
+
description: "Maximum characters for context per result (default: 10000)"
|
|
3687
5282
|
}
|
|
3688
5283
|
},
|
|
3689
|
-
required: ["
|
|
5284
|
+
required: ["query"]
|
|
3690
5285
|
},
|
|
3691
|
-
execute: async (rawInput,
|
|
5286
|
+
execute: async (rawInput, context) => {
|
|
3692
5287
|
const input = rawInput;
|
|
3693
|
-
const {
|
|
3694
|
-
|
|
3695
|
-
path = defaultCwd,
|
|
3696
|
-
glob,
|
|
3697
|
-
before = 0,
|
|
3698
|
-
after = 0,
|
|
3699
|
-
ignoreCase = false
|
|
3700
|
-
} = input;
|
|
3701
|
-
const access = checkPathAccess(path, options, "dir");
|
|
3702
|
-
if (!access.ok) {
|
|
3703
|
-
return { content: access.error, isError: true };
|
|
3704
|
-
}
|
|
3705
|
-
if (!existsSync8(access.resolved)) {
|
|
5288
|
+
const { query, numResults, livecrawl, type, contextMaxCharacters } = input;
|
|
5289
|
+
if (!query || typeof query !== "string" || query.trim().length === 0) {
|
|
3706
5290
|
return {
|
|
3707
|
-
content:
|
|
5291
|
+
content: "Error: A non-empty search query is required.",
|
|
3708
5292
|
isError: true
|
|
3709
5293
|
};
|
|
3710
5294
|
}
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
5295
|
+
const clampedNumResults = Math.min(Math.max(numResults || API_CONFIG.DEFAULT_NUM_RESULTS, 1), 20);
|
|
5296
|
+
const searchRequest = {
|
|
5297
|
+
jsonrpc: "2.0",
|
|
5298
|
+
id: 1,
|
|
5299
|
+
method: "tools/call",
|
|
5300
|
+
params: {
|
|
5301
|
+
name: "web_search_exa",
|
|
5302
|
+
arguments: {
|
|
5303
|
+
query: query.trim(),
|
|
5304
|
+
numResults: clampedNumResults,
|
|
5305
|
+
livecrawl: livecrawl || "fallback",
|
|
5306
|
+
type: type || "auto",
|
|
5307
|
+
contextMaxCharacters: contextMaxCharacters || API_CONFIG.DEFAULT_CONTEXT_MAX_CHARS
|
|
5308
|
+
}
|
|
3721
5309
|
}
|
|
3722
|
-
|
|
5310
|
+
};
|
|
5311
|
+
const controller = new AbortController;
|
|
5312
|
+
const timeoutId = setTimeout(() => controller.abort(), API_CONFIG.TIMEOUT_MS);
|
|
5313
|
+
try {
|
|
5314
|
+
const signal = context.abortSignal ? AbortSignal.any([controller.signal, context.abortSignal]) : controller.signal;
|
|
5315
|
+
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SEARCH}`, {
|
|
5316
|
+
method: "POST",
|
|
5317
|
+
headers: {
|
|
5318
|
+
Accept: "application/json, text/event-stream",
|
|
5319
|
+
"Content-Type": "application/json",
|
|
5320
|
+
"User-Agent": "FormAgent-SDK/1.0"
|
|
5321
|
+
},
|
|
5322
|
+
body: JSON.stringify(searchRequest),
|
|
5323
|
+
signal
|
|
5324
|
+
});
|
|
5325
|
+
clearTimeout(timeoutId);
|
|
5326
|
+
if (!response.ok) {
|
|
5327
|
+
const errorText = await response.text();
|
|
3723
5328
|
return {
|
|
3724
|
-
content: `
|
|
5329
|
+
content: `Search error (${response.status}): ${errorText}`,
|
|
5330
|
+
isError: true
|
|
3725
5331
|
};
|
|
3726
5332
|
}
|
|
3727
|
-
const
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
5333
|
+
const responseText = await response.text();
|
|
5334
|
+
const lines = responseText.split(`
|
|
5335
|
+
`);
|
|
5336
|
+
for (const line of lines) {
|
|
5337
|
+
if (line.startsWith("data: ")) {
|
|
5338
|
+
try {
|
|
5339
|
+
const data = JSON.parse(line.substring(6));
|
|
5340
|
+
if (data.error) {
|
|
5341
|
+
return {
|
|
5342
|
+
content: `Search API error: ${data.error.message}`,
|
|
5343
|
+
isError: true
|
|
5344
|
+
};
|
|
5345
|
+
}
|
|
5346
|
+
if (data.result?.content && data.result.content.length > 0) {
|
|
5347
|
+
const resultText = data.result.content[0].text;
|
|
5348
|
+
let output = `## Web Search Results
|
|
5349
|
+
|
|
5350
|
+
`;
|
|
5351
|
+
output += `**Query:** ${query}
|
|
5352
|
+
`;
|
|
5353
|
+
output += `**Results:** ${clampedNumResults} requested
|
|
5354
|
+
|
|
5355
|
+
`;
|
|
5356
|
+
output += `---
|
|
5357
|
+
|
|
5358
|
+
`;
|
|
5359
|
+
output += resultText;
|
|
5360
|
+
return {
|
|
5361
|
+
content: output
|
|
5362
|
+
};
|
|
5363
|
+
}
|
|
5364
|
+
} catch (parseError) {
|
|
5365
|
+
continue;
|
|
3740
5366
|
}
|
|
3741
5367
|
}
|
|
3742
|
-
if (match.before?.length || match.after?.length) {
|
|
3743
|
-
output.push("--");
|
|
3744
|
-
}
|
|
3745
5368
|
}
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
(
|
|
5369
|
+
try {
|
|
5370
|
+
const data = JSON.parse(responseText);
|
|
5371
|
+
if (data.result?.content && data.result.content.length > 0) {
|
|
5372
|
+
return {
|
|
5373
|
+
content: data.result.content[0].text
|
|
5374
|
+
};
|
|
5375
|
+
}
|
|
5376
|
+
} catch {}
|
|
3749
5377
|
return {
|
|
3750
|
-
content: `
|
|
3751
|
-
|
|
3752
|
-
${output.join(`
|
|
3753
|
-
`)}${truncated}`
|
|
5378
|
+
content: `No search results found for query: "${query}". Please try a different or more specific query.`
|
|
3754
5379
|
};
|
|
3755
5380
|
} catch (error) {
|
|
5381
|
+
clearTimeout(timeoutId);
|
|
5382
|
+
if (error instanceof Error) {
|
|
5383
|
+
if (error.name === "AbortError") {
|
|
5384
|
+
return {
|
|
5385
|
+
content: `Search request timed out after ${API_CONFIG.TIMEOUT_MS}ms. Please try again or use a simpler query.`,
|
|
5386
|
+
isError: true
|
|
5387
|
+
};
|
|
5388
|
+
}
|
|
5389
|
+
return {
|
|
5390
|
+
content: `Search failed: ${error.message}`,
|
|
5391
|
+
isError: true
|
|
5392
|
+
};
|
|
5393
|
+
}
|
|
3756
5394
|
return {
|
|
3757
|
-
content: `Search failed: ${
|
|
5395
|
+
content: `Search failed: ${String(error)}`,
|
|
3758
5396
|
isError: true
|
|
3759
5397
|
};
|
|
3760
5398
|
}
|
|
3761
5399
|
}
|
|
3762
5400
|
};
|
|
3763
5401
|
}
|
|
3764
|
-
var
|
|
3765
|
-
// src/tools/builtin/
|
|
3766
|
-
|
|
3767
|
-
var
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
return /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
|
|
3771
|
-
}
|
|
3772
|
-
function isIpV6(host) {
|
|
3773
|
-
return /^[0-9a-fA-F:]+$/.test(host) && host.includes(":");
|
|
5402
|
+
var WebSearchTool = createWebSearchTool();
|
|
5403
|
+
// src/tools/builtin/todo.ts
|
|
5404
|
+
var globalTodos = [];
|
|
5405
|
+
var onTodoChange = null;
|
|
5406
|
+
function setTodoChangeCallback(callback) {
|
|
5407
|
+
onTodoChange = callback;
|
|
3774
5408
|
}
|
|
3775
|
-
function
|
|
3776
|
-
|
|
3777
|
-
if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255))
|
|
3778
|
-
return true;
|
|
3779
|
-
const [a, b] = parts;
|
|
3780
|
-
if (a === 10)
|
|
3781
|
-
return true;
|
|
3782
|
-
if (a === 127)
|
|
3783
|
-
return true;
|
|
3784
|
-
if (a === 0)
|
|
3785
|
-
return true;
|
|
3786
|
-
if (a === 169 && b === 254)
|
|
3787
|
-
return true;
|
|
3788
|
-
if (a === 172 && b >= 16 && b <= 31)
|
|
3789
|
-
return true;
|
|
3790
|
-
if (a === 192 && b === 168)
|
|
3791
|
-
return true;
|
|
3792
|
-
if (a === 100 && b >= 64 && b <= 127)
|
|
3793
|
-
return true;
|
|
3794
|
-
if (a >= 224)
|
|
3795
|
-
return true;
|
|
3796
|
-
return false;
|
|
5409
|
+
function getTodos() {
|
|
5410
|
+
return [...globalTodos];
|
|
3797
5411
|
}
|
|
3798
|
-
function
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
return true;
|
|
3802
|
-
if (normalized.startsWith("fe80:"))
|
|
3803
|
-
return true;
|
|
3804
|
-
if (normalized.startsWith("fc") || normalized.startsWith("fd"))
|
|
3805
|
-
return true;
|
|
3806
|
-
return false;
|
|
5412
|
+
function clearTodos() {
|
|
5413
|
+
globalTodos = [];
|
|
5414
|
+
onTodoChange?.(globalTodos);
|
|
3807
5415
|
}
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
5416
|
+
function createTodoWriteTool(options = {}) {
|
|
5417
|
+
return {
|
|
5418
|
+
name: "TodoWrite",
|
|
5419
|
+
description: `Manage a task list to track progress on complex tasks. Use to plan work, track completed items, and show progress to the user.`,
|
|
5420
|
+
inputSchema: {
|
|
5421
|
+
type: "object",
|
|
5422
|
+
properties: {
|
|
5423
|
+
todos: {
|
|
5424
|
+
type: "array",
|
|
5425
|
+
description: "Updated todo list",
|
|
5426
|
+
items: {
|
|
5427
|
+
type: "object",
|
|
5428
|
+
properties: {
|
|
5429
|
+
content: {
|
|
5430
|
+
type: "string",
|
|
5431
|
+
description: "Task description (imperative form, e.g., 'Run tests')"
|
|
5432
|
+
},
|
|
5433
|
+
status: {
|
|
5434
|
+
type: "string",
|
|
5435
|
+
enum: ["pending", "in_progress", "completed"],
|
|
5436
|
+
description: "Task status"
|
|
5437
|
+
},
|
|
5438
|
+
activeForm: {
|
|
5439
|
+
type: "string",
|
|
5440
|
+
description: "Present continuous form (e.g., 'Running tests')"
|
|
5441
|
+
}
|
|
5442
|
+
},
|
|
5443
|
+
required: ["content", "status", "activeForm"]
|
|
5444
|
+
}
|
|
5445
|
+
}
|
|
5446
|
+
},
|
|
5447
|
+
required: ["todos"]
|
|
5448
|
+
},
|
|
5449
|
+
execute: async (rawInput, _context) => {
|
|
5450
|
+
const input = rawInput;
|
|
5451
|
+
const { todos } = input;
|
|
5452
|
+
for (const todo of todos) {
|
|
5453
|
+
if (!todo.content || !todo.status || !todo.activeForm) {
|
|
5454
|
+
return {
|
|
5455
|
+
content: "Invalid todo item: missing required fields (content, status, activeForm)",
|
|
5456
|
+
isError: true
|
|
5457
|
+
};
|
|
5458
|
+
}
|
|
5459
|
+
if (!["pending", "in_progress", "completed"].includes(todo.status)) {
|
|
5460
|
+
return {
|
|
5461
|
+
content: `Invalid status: ${todo.status}. Must be pending, in_progress, or completed.`,
|
|
5462
|
+
isError: true
|
|
5463
|
+
};
|
|
5464
|
+
}
|
|
3829
5465
|
}
|
|
3830
|
-
|
|
3831
|
-
|
|
5466
|
+
globalTodos = todos;
|
|
5467
|
+
onTodoChange?.(globalTodos);
|
|
5468
|
+
const completed = todos.filter((t) => t.status === "completed").length;
|
|
5469
|
+
const inProgress = todos.filter((t) => t.status === "in_progress").length;
|
|
5470
|
+
const pending = todos.filter((t) => t.status === "pending").length;
|
|
5471
|
+
const lines = [
|
|
5472
|
+
`Todo list updated (${completed}/${todos.length} completed)`,
|
|
5473
|
+
""
|
|
5474
|
+
];
|
|
5475
|
+
for (const todo of todos) {
|
|
5476
|
+
const icon = todo.status === "completed" ? "✓" : todo.status === "in_progress" ? "→" : "○";
|
|
5477
|
+
lines.push(`${icon} ${todo.content}`);
|
|
5478
|
+
}
|
|
5479
|
+
if (inProgress > 0) {
|
|
5480
|
+
lines.push("");
|
|
5481
|
+
lines.push(`Currently: ${todos.find((t) => t.status === "in_progress")?.activeForm}`);
|
|
3832
5482
|
}
|
|
5483
|
+
return {
|
|
5484
|
+
content: lines.join(`
|
|
5485
|
+
`)
|
|
5486
|
+
};
|
|
3833
5487
|
}
|
|
3834
|
-
}
|
|
3835
|
-
return `DNS resolution failed for "${hostname}" (allowPrivateNetwork=false): ${e instanceof Error ? e.message : String(e)}`;
|
|
3836
|
-
}
|
|
3837
|
-
return null;
|
|
5488
|
+
};
|
|
3838
5489
|
}
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
md = md.replace(/<h1[^>]*>(.*?)<\/h1>/gi, `
|
|
3844
|
-
# $1
|
|
3845
|
-
`);
|
|
3846
|
-
md = md.replace(/<h2[^>]*>(.*?)<\/h2>/gi, `
|
|
3847
|
-
## $1
|
|
3848
|
-
`);
|
|
3849
|
-
md = md.replace(/<h3[^>]*>(.*?)<\/h3>/gi, `
|
|
3850
|
-
### $1
|
|
3851
|
-
`);
|
|
3852
|
-
md = md.replace(/<h4[^>]*>(.*?)<\/h4>/gi, `
|
|
3853
|
-
#### $1
|
|
3854
|
-
`);
|
|
3855
|
-
md = md.replace(/<h5[^>]*>(.*?)<\/h5>/gi, `
|
|
3856
|
-
##### $1
|
|
3857
|
-
`);
|
|
3858
|
-
md = md.replace(/<h6[^>]*>(.*?)<\/h6>/gi, `
|
|
3859
|
-
###### $1
|
|
3860
|
-
`);
|
|
3861
|
-
md = md.replace(/<p[^>]*>(.*?)<\/p>/gi, `
|
|
3862
|
-
$1
|
|
3863
|
-
`);
|
|
3864
|
-
md = md.replace(/<a[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi, "[$2]($1)");
|
|
3865
|
-
md = md.replace(/<(strong|b)[^>]*>(.*?)<\/\1>/gi, "**$2**");
|
|
3866
|
-
md = md.replace(/<(em|i)[^>]*>(.*?)<\/\1>/gi, "*$2*");
|
|
3867
|
-
md = md.replace(/<code[^>]*>(.*?)<\/code>/gi, "`$1`");
|
|
3868
|
-
md = md.replace(/<pre[^>]*>(.*?)<\/pre>/gis, "\n```\n$1\n```\n");
|
|
3869
|
-
md = md.replace(/<li[^>]*>(.*?)<\/li>/gi, `- $1
|
|
3870
|
-
`);
|
|
3871
|
-
md = md.replace(/<\/?[ou]l[^>]*>/gi, `
|
|
3872
|
-
`);
|
|
3873
|
-
md = md.replace(/<br\s*\/?>/gi, `
|
|
3874
|
-
`);
|
|
3875
|
-
md = md.replace(/<[^>]+>/g, "");
|
|
3876
|
-
md = md.replace(/ /g, " ");
|
|
3877
|
-
md = md.replace(/&/g, "&");
|
|
3878
|
-
md = md.replace(/</g, "<");
|
|
3879
|
-
md = md.replace(/>/g, ">");
|
|
3880
|
-
md = md.replace(/"/g, '"');
|
|
3881
|
-
md = md.replace(/\n{3,}/g, `
|
|
5490
|
+
var TodoWriteTool = createTodoWriteTool();
|
|
5491
|
+
// src/tools/builtin/askuser.ts
|
|
5492
|
+
var globalAskUserHandler = null;
|
|
5493
|
+
var ASKUSER_DESCRIPTION = `Ask the user questions to gather information, clarify requirements, or get decisions.
|
|
3882
5494
|
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
5495
|
+
Use this tool when you need to:
|
|
5496
|
+
- Gather user preferences or requirements
|
|
5497
|
+
- Clarify ambiguous instructions
|
|
5498
|
+
- Get decisions on implementation choices
|
|
5499
|
+
- Offer choices about what direction to take
|
|
5500
|
+
|
|
5501
|
+
Parameters:
|
|
5502
|
+
- questions: Array of questions (1-4 questions per call)
|
|
5503
|
+
- question: The question text (required)
|
|
5504
|
+
- header: Short label for display (optional)
|
|
5505
|
+
- options: Array of choices (optional)
|
|
5506
|
+
- label: Display text
|
|
5507
|
+
- description: Explanation of the option
|
|
5508
|
+
- multiSelect: Allow multiple selections (default: false)
|
|
5509
|
+
- defaultValue: Default if no response
|
|
5510
|
+
|
|
5511
|
+
Best practices:
|
|
5512
|
+
- Keep questions clear and concise
|
|
5513
|
+
- Provide options when there are known choices
|
|
5514
|
+
- Use multiSelect for non-mutually-exclusive options
|
|
5515
|
+
- Limit to 4 questions per call to avoid overwhelming the user`;
|
|
5516
|
+
function createAskUserTool(options = {}) {
|
|
3888
5517
|
return {
|
|
3889
|
-
name: "
|
|
3890
|
-
description:
|
|
5518
|
+
name: "AskUser",
|
|
5519
|
+
description: ASKUSER_DESCRIPTION,
|
|
3891
5520
|
inputSchema: {
|
|
3892
5521
|
type: "object",
|
|
3893
5522
|
properties: {
|
|
3894
|
-
|
|
3895
|
-
type: "
|
|
3896
|
-
description: "
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
5523
|
+
questions: {
|
|
5524
|
+
type: "array",
|
|
5525
|
+
description: "Questions to ask the user (1-4 questions)",
|
|
5526
|
+
items: {
|
|
5527
|
+
type: "object",
|
|
5528
|
+
properties: {
|
|
5529
|
+
question: {
|
|
5530
|
+
type: "string",
|
|
5531
|
+
description: "The question text to display"
|
|
5532
|
+
},
|
|
5533
|
+
header: {
|
|
5534
|
+
type: "string",
|
|
5535
|
+
description: "Short label for the question (max 12 chars)"
|
|
5536
|
+
},
|
|
5537
|
+
options: {
|
|
5538
|
+
type: "array",
|
|
5539
|
+
description: "Available choices (2-4 options)",
|
|
5540
|
+
items: {
|
|
5541
|
+
type: "object",
|
|
5542
|
+
properties: {
|
|
5543
|
+
label: {
|
|
5544
|
+
type: "string",
|
|
5545
|
+
description: "Display text for this option"
|
|
5546
|
+
},
|
|
5547
|
+
description: {
|
|
5548
|
+
type: "string",
|
|
5549
|
+
description: "Explanation of what this option means"
|
|
5550
|
+
}
|
|
5551
|
+
},
|
|
5552
|
+
required: ["label"]
|
|
5553
|
+
}
|
|
5554
|
+
},
|
|
5555
|
+
multiSelect: {
|
|
5556
|
+
type: "boolean",
|
|
5557
|
+
description: "Allow multiple selections (default: false)"
|
|
5558
|
+
},
|
|
5559
|
+
defaultValue: {
|
|
5560
|
+
type: "string",
|
|
5561
|
+
description: "Default value if user doesn't respond"
|
|
5562
|
+
}
|
|
5563
|
+
},
|
|
5564
|
+
required: ["question"]
|
|
5565
|
+
},
|
|
5566
|
+
minItems: 1,
|
|
5567
|
+
maxItems: 4
|
|
3901
5568
|
}
|
|
3902
5569
|
},
|
|
3903
|
-
required: ["
|
|
5570
|
+
required: ["questions"]
|
|
3904
5571
|
},
|
|
3905
|
-
execute: async (rawInput,
|
|
5572
|
+
execute: async (rawInput, context) => {
|
|
3906
5573
|
const input = rawInput;
|
|
3907
|
-
const {
|
|
3908
|
-
|
|
3909
|
-
try {
|
|
3910
|
-
parsedUrl = new URL(url);
|
|
3911
|
-
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
3912
|
-
return {
|
|
3913
|
-
content: `Invalid URL protocol: ${parsedUrl.protocol}. Only http/https are allowed.`,
|
|
3914
|
-
isError: true
|
|
3915
|
-
};
|
|
3916
|
-
}
|
|
3917
|
-
if (parsedUrl.protocol === "http:")
|
|
3918
|
-
parsedUrl.protocol = "https:";
|
|
3919
|
-
} catch {
|
|
5574
|
+
const { questions } = input;
|
|
5575
|
+
if (!questions || !Array.isArray(questions) || questions.length === 0) {
|
|
3920
5576
|
return {
|
|
3921
|
-
content:
|
|
5577
|
+
content: "Error: At least one question is required.",
|
|
3922
5578
|
isError: true
|
|
3923
5579
|
};
|
|
3924
5580
|
}
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
5581
|
+
if (questions.length > 4) {
|
|
5582
|
+
return {
|
|
5583
|
+
content: "Error: Maximum 4 questions per call. Please split into multiple calls.",
|
|
5584
|
+
isError: true
|
|
5585
|
+
};
|
|
5586
|
+
}
|
|
5587
|
+
if (!globalAskUserHandler) {
|
|
5588
|
+
return {
|
|
5589
|
+
content: `Error: AskUser handler not configured. The SDK user must call setAskUserHandler() to enable user interaction.
|
|
5590
|
+
|
|
5591
|
+
Questions that would have been asked:
|
|
5592
|
+
${questions.map((q, i) => `${i + 1}. ${q.question}${q.options ? ` [Options: ${q.options.map((o) => o.label).join(", ")}]` : ""}`).join(`
|
|
5593
|
+
`)}
|
|
5594
|
+
|
|
5595
|
+
To enable this tool, the SDK user should implement an AskUser handler.`,
|
|
5596
|
+
isError: true
|
|
5597
|
+
};
|
|
3928
5598
|
}
|
|
3929
5599
|
try {
|
|
3930
|
-
const
|
|
3931
|
-
const
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
"
|
|
3936
|
-
|
|
5600
|
+
const answers = await globalAskUserHandler(questions, context);
|
|
5601
|
+
const formatAnswer = (answer) => {
|
|
5602
|
+
if (answer === undefined || answer === null)
|
|
5603
|
+
return "(no answer)";
|
|
5604
|
+
if (Array.isArray(answer))
|
|
5605
|
+
return answer.join(", ") || "(no answer)";
|
|
5606
|
+
return answer || "(no answer)";
|
|
5607
|
+
};
|
|
5608
|
+
const formattedAnswers = questions.map((q, i) => `Q: "${q.question}"
|
|
5609
|
+
A: ${formatAnswer(answers[i])}`).join(`
|
|
5610
|
+
|
|
5611
|
+
`);
|
|
5612
|
+
return {
|
|
5613
|
+
content: `User has answered your questions:
|
|
5614
|
+
|
|
5615
|
+
${formattedAnswers}
|
|
5616
|
+
|
|
5617
|
+
You can now continue with the user's answers in mind.`,
|
|
5618
|
+
metadata: {
|
|
5619
|
+
questions: questions.map((q) => q.question),
|
|
5620
|
+
answers
|
|
3937
5621
|
}
|
|
3938
|
-
}
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
5622
|
+
};
|
|
5623
|
+
} catch (error) {
|
|
5624
|
+
return {
|
|
5625
|
+
content: `Error getting user response: ${error instanceof Error ? error.message : String(error)}`,
|
|
5626
|
+
isError: true
|
|
5627
|
+
};
|
|
5628
|
+
}
|
|
5629
|
+
}
|
|
5630
|
+
};
|
|
5631
|
+
}
|
|
5632
|
+
var AskUserTool = createAskUserTool();
|
|
5633
|
+
// src/tools/builtin/batch.ts
|
|
5634
|
+
var globalToolResolver = null;
|
|
5635
|
+
var DISALLOWED_TOOLS = new Set(["Batch", "batch", "AskUser", "askuser"]);
|
|
5636
|
+
var BATCH_DESCRIPTION = `Execute multiple tool calls in parallel for improved performance.
|
|
3950
5637
|
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
if (ssrfDenyAfter) {
|
|
3956
|
-
return { content: `Redirect target denied: ${ssrfDenyAfter}`, isError: true };
|
|
3957
|
-
}
|
|
3958
|
-
const contentType = response.headers.get("content-type") || "";
|
|
3959
|
-
let content = await response.text();
|
|
3960
|
-
if (content.length > MAX_CONTENT_LENGTH) {
|
|
3961
|
-
content = content.slice(0, MAX_CONTENT_LENGTH) + `
|
|
5638
|
+
Use this tool when you need to:
|
|
5639
|
+
- Read multiple files at once
|
|
5640
|
+
- Perform several independent operations
|
|
5641
|
+
- Speed up tasks that don't have dependencies
|
|
3962
5642
|
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
}
|
|
3968
|
-
let output = `URL: ${response.url}
|
|
3969
|
-
`;
|
|
3970
|
-
output += `Content-Type: ${contentType}
|
|
5643
|
+
Parameters:
|
|
5644
|
+
- tool_calls: Array of tool calls (1-10 calls per batch)
|
|
5645
|
+
- tool: Name of the tool to execute
|
|
5646
|
+
- parameters: Parameters for that tool
|
|
3971
5647
|
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
5648
|
+
Limitations:
|
|
5649
|
+
- Maximum 10 tool calls per batch
|
|
5650
|
+
- Cannot nest Batch calls (no batch within batch)
|
|
5651
|
+
- Cannot batch AskUser (requires sequential interaction)
|
|
5652
|
+
- All calls execute in parallel - don't batch dependent operations
|
|
3975
5653
|
|
|
3976
|
-
|
|
5654
|
+
Best practices:
|
|
5655
|
+
- Group independent operations (e.g., reading multiple unrelated files)
|
|
5656
|
+
- Don't batch operations that depend on each other's results
|
|
5657
|
+
- Use for bulk file reads, multiple greps, or parallel web fetches
|
|
5658
|
+
|
|
5659
|
+
Example:
|
|
5660
|
+
{
|
|
5661
|
+
"tool_calls": [
|
|
5662
|
+
{ "tool": "Read", "parameters": { "file_path": "/path/to/file1.ts" } },
|
|
5663
|
+
{ "tool": "Read", "parameters": { "file_path": "/path/to/file2.ts" } },
|
|
5664
|
+
{ "tool": "Grep", "parameters": { "pattern": "TODO", "path": "./src" } }
|
|
5665
|
+
]
|
|
5666
|
+
}`;
|
|
5667
|
+
function createBatchTool(options = {}, toolMap) {
|
|
5668
|
+
return {
|
|
5669
|
+
name: "Batch",
|
|
5670
|
+
description: BATCH_DESCRIPTION,
|
|
5671
|
+
inputSchema: {
|
|
5672
|
+
type: "object",
|
|
5673
|
+
properties: {
|
|
5674
|
+
tool_calls: {
|
|
5675
|
+
type: "array",
|
|
5676
|
+
description: "Array of tool calls to execute in parallel",
|
|
5677
|
+
items: {
|
|
5678
|
+
type: "object",
|
|
5679
|
+
properties: {
|
|
5680
|
+
tool: {
|
|
5681
|
+
type: "string",
|
|
5682
|
+
description: "Name of the tool to execute"
|
|
5683
|
+
},
|
|
5684
|
+
parameters: {
|
|
5685
|
+
type: "object",
|
|
5686
|
+
description: "Parameters for the tool",
|
|
5687
|
+
additionalProperties: true
|
|
5688
|
+
}
|
|
5689
|
+
},
|
|
5690
|
+
required: ["tool", "parameters"]
|
|
5691
|
+
},
|
|
5692
|
+
minItems: 1,
|
|
5693
|
+
maxItems: 10
|
|
3977
5694
|
}
|
|
3978
|
-
|
|
5695
|
+
},
|
|
5696
|
+
required: ["tool_calls"]
|
|
5697
|
+
},
|
|
5698
|
+
execute: async (rawInput, context) => {
|
|
5699
|
+
const input = rawInput;
|
|
5700
|
+
const { tool_calls } = input;
|
|
5701
|
+
if (!tool_calls || !Array.isArray(tool_calls) || tool_calls.length === 0) {
|
|
3979
5702
|
return {
|
|
3980
|
-
content:
|
|
5703
|
+
content: "Error: At least one tool call is required.",
|
|
5704
|
+
isError: true
|
|
3981
5705
|
};
|
|
3982
|
-
}
|
|
3983
|
-
|
|
5706
|
+
}
|
|
5707
|
+
const callsToExecute = tool_calls.slice(0, 10);
|
|
5708
|
+
const discardedCalls = tool_calls.slice(10);
|
|
5709
|
+
const executeCall = async (call) => {
|
|
5710
|
+
const startTime = Date.now();
|
|
5711
|
+
try {
|
|
5712
|
+
if (DISALLOWED_TOOLS.has(call.tool)) {
|
|
5713
|
+
return {
|
|
5714
|
+
success: false,
|
|
5715
|
+
tool: call.tool,
|
|
5716
|
+
error: `Tool '${call.tool}' cannot be used in batch. Disallowed: ${Array.from(DISALLOWED_TOOLS).join(", ")}`,
|
|
5717
|
+
duration: Date.now() - startTime
|
|
5718
|
+
};
|
|
5719
|
+
}
|
|
5720
|
+
let toolExecute;
|
|
5721
|
+
if (toolMap) {
|
|
5722
|
+
const tool = toolMap.get(call.tool);
|
|
5723
|
+
if (tool) {
|
|
5724
|
+
toolExecute = tool.execute;
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
if (!toolExecute && globalToolResolver) {
|
|
5728
|
+
toolExecute = globalToolResolver(call.tool);
|
|
5729
|
+
}
|
|
5730
|
+
if (!toolExecute) {
|
|
5731
|
+
const availableTools = toolMap ? Array.from(toolMap.keys()).filter((n) => !DISALLOWED_TOOLS.has(n)) : [];
|
|
5732
|
+
return {
|
|
5733
|
+
success: false,
|
|
5734
|
+
tool: call.tool,
|
|
5735
|
+
error: `Tool '${call.tool}' not found.${availableTools.length > 0 ? ` Available: ${availableTools.slice(0, 10).join(", ")}` : ""}`,
|
|
5736
|
+
duration: Date.now() - startTime
|
|
5737
|
+
};
|
|
5738
|
+
}
|
|
5739
|
+
const result = await toolExecute(call.parameters, context);
|
|
3984
5740
|
return {
|
|
3985
|
-
|
|
3986
|
-
|
|
5741
|
+
success: !result.isError,
|
|
5742
|
+
tool: call.tool,
|
|
5743
|
+
result,
|
|
5744
|
+
duration: Date.now() - startTime
|
|
5745
|
+
};
|
|
5746
|
+
} catch (error) {
|
|
5747
|
+
return {
|
|
5748
|
+
success: false,
|
|
5749
|
+
tool: call.tool,
|
|
5750
|
+
error: error instanceof Error ? error.message : String(error),
|
|
5751
|
+
duration: Date.now() - startTime
|
|
3987
5752
|
};
|
|
3988
5753
|
}
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
5754
|
+
};
|
|
5755
|
+
const results = await Promise.all(callsToExecute.map(executeCall));
|
|
5756
|
+
for (const call of discardedCalls) {
|
|
5757
|
+
results.push({
|
|
5758
|
+
success: false,
|
|
5759
|
+
tool: call.tool,
|
|
5760
|
+
error: "Exceeded maximum of 10 tool calls per batch",
|
|
5761
|
+
duration: 0
|
|
5762
|
+
});
|
|
5763
|
+
}
|
|
5764
|
+
const successCount = results.filter((r) => r.success).length;
|
|
5765
|
+
const failCount = results.length - successCount;
|
|
5766
|
+
const totalDuration = results.reduce((sum, r) => sum + (r.duration || 0), 0);
|
|
5767
|
+
const formatResult = (r, index) => {
|
|
5768
|
+
if (r.success && r.result) {
|
|
5769
|
+
const content = typeof r.result.content === "string" ? r.result.content.slice(0, 500) + (r.result.content.length > 500 ? "..." : "") : JSON.stringify(r.result.content).slice(0, 500);
|
|
5770
|
+
return `[${index + 1}] ${r.tool}: SUCCESS (${r.duration}ms)
|
|
5771
|
+
${content}`;
|
|
5772
|
+
} else {
|
|
5773
|
+
return `[${index + 1}] ${r.tool}: FAILED (${r.duration}ms)
|
|
5774
|
+
Error: ${r.error}`;
|
|
5775
|
+
}
|
|
5776
|
+
};
|
|
5777
|
+
const resultsOutput = results.map(formatResult).join(`
|
|
5778
|
+
|
|
5779
|
+
---
|
|
5780
|
+
|
|
5781
|
+
`);
|
|
5782
|
+
const summary = failCount > 0 ? `Batch execution: ${successCount}/${results.length} succeeded, ${failCount} failed (${totalDuration}ms total)` : `Batch execution: All ${successCount} tools succeeded (${totalDuration}ms total)`;
|
|
5783
|
+
return {
|
|
5784
|
+
content: `${summary}
|
|
5785
|
+
|
|
5786
|
+
${resultsOutput}`,
|
|
5787
|
+
metadata: {
|
|
5788
|
+
totalCalls: results.length,
|
|
5789
|
+
successful: successCount,
|
|
5790
|
+
failed: failCount,
|
|
5791
|
+
totalDuration,
|
|
5792
|
+
details: results.map((r) => ({
|
|
5793
|
+
tool: r.tool,
|
|
5794
|
+
success: r.success,
|
|
5795
|
+
duration: r.duration,
|
|
5796
|
+
error: r.error
|
|
5797
|
+
}))
|
|
5798
|
+
}
|
|
5799
|
+
};
|
|
5800
|
+
}
|
|
5801
|
+
};
|
|
5802
|
+
}
|
|
5803
|
+
var BatchTool = createBatchTool();
|
|
5804
|
+
// src/tools/builtin/httprequest.ts
|
|
5805
|
+
import { lookup as lookup2 } from "node:dns/promises";
|
|
5806
|
+
var DEFAULT_TIMEOUT2 = 30000;
|
|
5807
|
+
var MAX_RESPONSE_SIZE = 5 * 1024 * 1024;
|
|
5808
|
+
function isPrivateIp(ip) {
|
|
5809
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
|
|
5810
|
+
const parts = ip.split(".").map(Number);
|
|
5811
|
+
const [a, b] = parts;
|
|
5812
|
+
if (a === 10)
|
|
5813
|
+
return true;
|
|
5814
|
+
if (a === 127)
|
|
5815
|
+
return true;
|
|
5816
|
+
if (a === 0)
|
|
5817
|
+
return true;
|
|
5818
|
+
if (a === 169 && b === 254)
|
|
5819
|
+
return true;
|
|
5820
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
5821
|
+
return true;
|
|
5822
|
+
if (a === 192 && b === 168)
|
|
5823
|
+
return true;
|
|
5824
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
5825
|
+
return true;
|
|
5826
|
+
if (a >= 224)
|
|
5827
|
+
return true;
|
|
5828
|
+
}
|
|
5829
|
+
const normalized = ip.toLowerCase();
|
|
5830
|
+
if (normalized === "::" || normalized === "::1")
|
|
5831
|
+
return true;
|
|
5832
|
+
if (normalized.startsWith("fe80:"))
|
|
5833
|
+
return true;
|
|
5834
|
+
if (normalized.startsWith("fc") || normalized.startsWith("fd"))
|
|
5835
|
+
return true;
|
|
5836
|
+
return false;
|
|
5837
|
+
}
|
|
5838
|
+
async function validateUrl(url, options) {
|
|
5839
|
+
let parsedUrl;
|
|
5840
|
+
try {
|
|
5841
|
+
parsedUrl = new URL(url);
|
|
5842
|
+
} catch {
|
|
5843
|
+
return { valid: false, error: `Invalid URL: ${url}` };
|
|
5844
|
+
}
|
|
5845
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
5846
|
+
return { valid: false, error: `Invalid protocol: ${parsedUrl.protocol}. Only http/https allowed.` };
|
|
5847
|
+
}
|
|
5848
|
+
if (!options.allowPrivateNetwork) {
|
|
5849
|
+
const hostname = parsedUrl.hostname.toLowerCase();
|
|
5850
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local")) {
|
|
5851
|
+
return { valid: false, error: `Blocked: localhost/local hostnames not allowed` };
|
|
5852
|
+
}
|
|
5853
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname) && isPrivateIp(hostname)) {
|
|
5854
|
+
return { valid: false, error: `Blocked: private IP address ${hostname}` };
|
|
5855
|
+
}
|
|
5856
|
+
const resolveHostnames = options.resolveHostnames ?? true;
|
|
5857
|
+
if (resolveHostnames) {
|
|
5858
|
+
try {
|
|
5859
|
+
const addrs = await lookup2(hostname, { all: true, verbatim: true });
|
|
5860
|
+
for (const addr of addrs) {
|
|
5861
|
+
if (isPrivateIp(addr.address)) {
|
|
5862
|
+
return { valid: false, error: `Blocked: ${hostname} resolves to private IP ${addr.address}` };
|
|
5863
|
+
}
|
|
5864
|
+
}
|
|
5865
|
+
} catch (e) {
|
|
5866
|
+
return { valid: false, error: `DNS resolution failed for ${hostname}` };
|
|
3993
5867
|
}
|
|
3994
5868
|
}
|
|
3995
|
-
}
|
|
3996
|
-
}
|
|
3997
|
-
var WebFetchTool = createWebFetchTool();
|
|
3998
|
-
// src/tools/builtin/todo.ts
|
|
3999
|
-
var globalTodos = [];
|
|
4000
|
-
var onTodoChange = null;
|
|
4001
|
-
function setTodoChangeCallback(callback) {
|
|
4002
|
-
onTodoChange = callback;
|
|
4003
|
-
}
|
|
4004
|
-
function getTodos() {
|
|
4005
|
-
return [...globalTodos];
|
|
4006
|
-
}
|
|
4007
|
-
function clearTodos() {
|
|
4008
|
-
globalTodos = [];
|
|
4009
|
-
onTodoChange?.(globalTodos);
|
|
5869
|
+
}
|
|
5870
|
+
return { valid: true, parsedUrl };
|
|
4010
5871
|
}
|
|
4011
|
-
|
|
5872
|
+
var HTTPREQUEST_DESCRIPTION = `Make HTTP requests to APIs and web services.
|
|
5873
|
+
|
|
5874
|
+
Use this tool when you need to:
|
|
5875
|
+
- Call REST APIs (GET, POST, PUT, DELETE, etc.)
|
|
5876
|
+
- Send data to web services
|
|
5877
|
+
- Fetch JSON data from APIs
|
|
5878
|
+
- Interact with webhooks
|
|
5879
|
+
|
|
5880
|
+
Parameters:
|
|
5881
|
+
- method: HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
|
|
5882
|
+
- url: Full URL to request (required)
|
|
5883
|
+
- headers: Request headers as key-value pairs (optional)
|
|
5884
|
+
- body: Request body for POST/PUT/PATCH (optional, auto-serialized to JSON)
|
|
5885
|
+
- timeout: Request timeout in ms (default: 30000, max: 120000)
|
|
5886
|
+
- responseType: Expected response type - 'json', 'text', or 'binary' (default: auto-detect)
|
|
5887
|
+
|
|
5888
|
+
Security:
|
|
5889
|
+
- Private/local network access is blocked by default
|
|
5890
|
+
- Maximum response size: 5MB
|
|
5891
|
+
|
|
5892
|
+
Best practices:
|
|
5893
|
+
- Include appropriate Content-Type header for POST/PUT requests
|
|
5894
|
+
- Handle authentication via headers (Authorization, API keys, etc.)
|
|
5895
|
+
- Use responseType: 'json' when expecting JSON response
|
|
5896
|
+
|
|
5897
|
+
Example:
|
|
5898
|
+
{
|
|
5899
|
+
"method": "POST",
|
|
5900
|
+
"url": "https://api.example.com/data",
|
|
5901
|
+
"headers": {
|
|
5902
|
+
"Authorization": "Bearer token123",
|
|
5903
|
+
"Content-Type": "application/json"
|
|
5904
|
+
},
|
|
5905
|
+
"body": { "name": "test", "value": 42 }
|
|
5906
|
+
}`;
|
|
5907
|
+
function createHttpRequestTool(options = {}) {
|
|
4012
5908
|
return {
|
|
4013
|
-
name: "
|
|
4014
|
-
description:
|
|
5909
|
+
name: "HttpRequest",
|
|
5910
|
+
description: HTTPREQUEST_DESCRIPTION,
|
|
4015
5911
|
inputSchema: {
|
|
4016
5912
|
type: "object",
|
|
4017
5913
|
properties: {
|
|
4018
|
-
|
|
4019
|
-
type: "
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
5914
|
+
method: {
|
|
5915
|
+
type: "string",
|
|
5916
|
+
enum: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
|
|
5917
|
+
description: "HTTP method"
|
|
5918
|
+
},
|
|
5919
|
+
url: {
|
|
5920
|
+
type: "string",
|
|
5921
|
+
description: "URL to request"
|
|
5922
|
+
},
|
|
5923
|
+
headers: {
|
|
5924
|
+
type: "object",
|
|
5925
|
+
description: "Request headers",
|
|
5926
|
+
additionalProperties: { type: "string" }
|
|
5927
|
+
},
|
|
5928
|
+
body: {
|
|
5929
|
+
description: "Request body (auto-serialized to JSON if object)"
|
|
5930
|
+
},
|
|
5931
|
+
timeout: {
|
|
5932
|
+
type: "number",
|
|
5933
|
+
description: "Request timeout in milliseconds (default: 30000, max: 120000)"
|
|
5934
|
+
},
|
|
5935
|
+
responseType: {
|
|
5936
|
+
type: "string",
|
|
5937
|
+
enum: ["json", "text", "binary"],
|
|
5938
|
+
description: "Expected response type (default: auto-detect)"
|
|
4040
5939
|
}
|
|
4041
5940
|
},
|
|
4042
|
-
required: ["
|
|
5941
|
+
required: ["method", "url"]
|
|
4043
5942
|
},
|
|
4044
|
-
execute: async (rawInput,
|
|
5943
|
+
execute: async (rawInput, context) => {
|
|
4045
5944
|
const input = rawInput;
|
|
4046
|
-
const {
|
|
4047
|
-
|
|
4048
|
-
|
|
5945
|
+
const { method, url, headers = {}, body, timeout, responseType } = input;
|
|
5946
|
+
const validMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
5947
|
+
if (!validMethods.includes(method)) {
|
|
5948
|
+
return {
|
|
5949
|
+
content: `Invalid HTTP method: ${method}. Must be one of: ${validMethods.join(", ")}`,
|
|
5950
|
+
isError: true
|
|
5951
|
+
};
|
|
5952
|
+
}
|
|
5953
|
+
const urlValidation = await validateUrl(url, options);
|
|
5954
|
+
if (!urlValidation.valid) {
|
|
5955
|
+
return {
|
|
5956
|
+
content: urlValidation.error,
|
|
5957
|
+
isError: true
|
|
5958
|
+
};
|
|
5959
|
+
}
|
|
5960
|
+
const requestTimeout = Math.min(timeout ?? DEFAULT_TIMEOUT2, 120000);
|
|
5961
|
+
const controller = new AbortController;
|
|
5962
|
+
const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
|
|
5963
|
+
const signal = context.abortSignal ? AbortSignal.any([controller.signal, context.abortSignal]) : controller.signal;
|
|
5964
|
+
const requestHeaders = {
|
|
5965
|
+
"User-Agent": "FormAgent-SDK/1.0",
|
|
5966
|
+
...headers
|
|
5967
|
+
};
|
|
5968
|
+
let requestBody;
|
|
5969
|
+
if (body !== undefined && ["POST", "PUT", "PATCH"].includes(method)) {
|
|
5970
|
+
if (typeof body === "string") {
|
|
5971
|
+
requestBody = body;
|
|
5972
|
+
} else {
|
|
5973
|
+
requestBody = JSON.stringify(body);
|
|
5974
|
+
if (!requestHeaders["Content-Type"] && !requestHeaders["content-type"]) {
|
|
5975
|
+
requestHeaders["Content-Type"] = "application/json";
|
|
5976
|
+
}
|
|
5977
|
+
}
|
|
5978
|
+
}
|
|
5979
|
+
try {
|
|
5980
|
+
const response = await fetch(urlValidation.parsedUrl.toString(), {
|
|
5981
|
+
method,
|
|
5982
|
+
headers: requestHeaders,
|
|
5983
|
+
body: requestBody,
|
|
5984
|
+
signal
|
|
5985
|
+
});
|
|
5986
|
+
clearTimeout(timeoutId);
|
|
5987
|
+
const contentLength = response.headers.get("content-length");
|
|
5988
|
+
if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) {
|
|
4049
5989
|
return {
|
|
4050
|
-
content:
|
|
5990
|
+
content: `Response too large: ${contentLength} bytes (max: ${MAX_RESPONSE_SIZE})`,
|
|
4051
5991
|
isError: true
|
|
4052
5992
|
};
|
|
4053
5993
|
}
|
|
4054
|
-
|
|
5994
|
+
const contentType = response.headers.get("content-type") || "";
|
|
5995
|
+
let responseBody;
|
|
5996
|
+
let responseText;
|
|
5997
|
+
const effectiveResponseType = responseType || (contentType.includes("application/json") ? "json" : "text");
|
|
5998
|
+
if (effectiveResponseType === "json") {
|
|
5999
|
+
try {
|
|
6000
|
+
responseBody = await response.json();
|
|
6001
|
+
responseText = JSON.stringify(responseBody, null, 2);
|
|
6002
|
+
} catch {
|
|
6003
|
+
responseText = await response.text();
|
|
6004
|
+
responseBody = responseText;
|
|
6005
|
+
}
|
|
6006
|
+
} else if (effectiveResponseType === "binary") {
|
|
6007
|
+
const buffer = await response.arrayBuffer();
|
|
6008
|
+
responseText = `[Binary data: ${buffer.byteLength} bytes]`;
|
|
6009
|
+
responseBody = { type: "binary", size: buffer.byteLength };
|
|
6010
|
+
} else {
|
|
6011
|
+
responseText = await response.text();
|
|
6012
|
+
responseBody = responseText;
|
|
6013
|
+
}
|
|
6014
|
+
if (responseText.length > 1e5) {
|
|
6015
|
+
responseText = responseText.slice(0, 1e5) + `
|
|
6016
|
+
|
|
6017
|
+
... (truncated)`;
|
|
6018
|
+
}
|
|
6019
|
+
const statusEmoji = response.ok ? "✓" : "✗";
|
|
6020
|
+
const output = `${statusEmoji} ${method} ${url}
|
|
6021
|
+
Status: ${response.status} ${response.statusText}
|
|
6022
|
+
Content-Type: ${contentType}
|
|
6023
|
+
|
|
6024
|
+
Response:
|
|
6025
|
+
${responseText}`;
|
|
6026
|
+
return {
|
|
6027
|
+
content: output,
|
|
6028
|
+
isError: !response.ok,
|
|
6029
|
+
metadata: {
|
|
6030
|
+
status: response.status,
|
|
6031
|
+
statusText: response.statusText,
|
|
6032
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
6033
|
+
body: responseBody
|
|
6034
|
+
}
|
|
6035
|
+
};
|
|
6036
|
+
} catch (error) {
|
|
6037
|
+
clearTimeout(timeoutId);
|
|
6038
|
+
if (error instanceof Error) {
|
|
6039
|
+
if (error.name === "AbortError") {
|
|
6040
|
+
return {
|
|
6041
|
+
content: `Request timed out after ${requestTimeout}ms`,
|
|
6042
|
+
isError: true
|
|
6043
|
+
};
|
|
6044
|
+
}
|
|
4055
6045
|
return {
|
|
4056
|
-
content: `
|
|
6046
|
+
content: `Request failed: ${error.message}`,
|
|
4057
6047
|
isError: true
|
|
4058
6048
|
};
|
|
4059
6049
|
}
|
|
6050
|
+
return {
|
|
6051
|
+
content: `Request failed: ${String(error)}`,
|
|
6052
|
+
isError: true
|
|
6053
|
+
};
|
|
4060
6054
|
}
|
|
4061
|
-
globalTodos = todos;
|
|
4062
|
-
onTodoChange?.(globalTodos);
|
|
4063
|
-
const completed = todos.filter((t) => t.status === "completed").length;
|
|
4064
|
-
const inProgress = todos.filter((t) => t.status === "in_progress").length;
|
|
4065
|
-
const pending = todos.filter((t) => t.status === "pending").length;
|
|
4066
|
-
const lines = [
|
|
4067
|
-
`Todo list updated (${completed}/${todos.length} completed)`,
|
|
4068
|
-
""
|
|
4069
|
-
];
|
|
4070
|
-
for (const todo of todos) {
|
|
4071
|
-
const icon = todo.status === "completed" ? "✓" : todo.status === "in_progress" ? "→" : "○";
|
|
4072
|
-
lines.push(`${icon} ${todo.content}`);
|
|
4073
|
-
}
|
|
4074
|
-
if (inProgress > 0) {
|
|
4075
|
-
lines.push("");
|
|
4076
|
-
lines.push(`Currently: ${todos.find((t) => t.status === "in_progress")?.activeForm}`);
|
|
4077
|
-
}
|
|
4078
|
-
return {
|
|
4079
|
-
content: lines.join(`
|
|
4080
|
-
`)
|
|
4081
|
-
};
|
|
4082
6055
|
}
|
|
4083
6056
|
};
|
|
4084
6057
|
}
|
|
4085
|
-
var
|
|
6058
|
+
var HttpRequestTool = createHttpRequestTool();
|
|
4086
6059
|
// src/tools/builtin/index.ts
|
|
4087
6060
|
var builtinTools = [
|
|
4088
6061
|
BashTool,
|
|
@@ -4092,14 +6065,18 @@ var builtinTools = [
|
|
|
4092
6065
|
GlobTool,
|
|
4093
6066
|
GrepTool,
|
|
4094
6067
|
WebFetchTool,
|
|
4095
|
-
|
|
6068
|
+
WebSearchTool,
|
|
6069
|
+
HttpRequestTool,
|
|
6070
|
+
TodoWriteTool,
|
|
6071
|
+
AskUserTool,
|
|
6072
|
+
BatchTool
|
|
4096
6073
|
];
|
|
4097
6074
|
// src/utils/env.ts
|
|
4098
6075
|
import { existsSync as existsSync9, readFileSync } from "node:fs";
|
|
4099
|
-
import { join as
|
|
6076
|
+
import { join as join7 } from "node:path";
|
|
4100
6077
|
function loadEnvOverride(cwd) {
|
|
4101
6078
|
const dir = cwd || process.cwd();
|
|
4102
|
-
const envPath =
|
|
6079
|
+
const envPath = join7(dir, ".env");
|
|
4103
6080
|
if (!existsSync9(envPath)) {
|
|
4104
6081
|
return;
|
|
4105
6082
|
}
|
|
@@ -4128,8 +6105,18 @@ function loadEnvOverride(cwd) {
|
|
|
4128
6105
|
|
|
4129
6106
|
// src/cli/cli.ts
|
|
4130
6107
|
loadEnvOverride();
|
|
4131
|
-
|
|
4132
|
-
|
|
6108
|
+
function getCliVersion() {
|
|
6109
|
+
try {
|
|
6110
|
+
const pkgUrl = new URL("../../package.json", import.meta.url);
|
|
6111
|
+
const raw = readFileSync2(pkgUrl, "utf-8");
|
|
6112
|
+
const parsed = JSON.parse(raw);
|
|
6113
|
+
return parsed.version ?? "0.0.0";
|
|
6114
|
+
} catch {
|
|
6115
|
+
return "0.0.0";
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
6118
|
+
var VERSION = getCliVersion();
|
|
6119
|
+
var SKILLS_PATH = join8(homedir4(), ".claude");
|
|
4133
6120
|
var colors = {
|
|
4134
6121
|
reset: "\x1B[0m",
|
|
4135
6122
|
bold: "\x1B[1m",
|
|
@@ -4157,8 +6144,10 @@ var session = null;
|
|
|
4157
6144
|
var totalInputTokens = 0;
|
|
4158
6145
|
var totalOutputTokens = 0;
|
|
4159
6146
|
var messageCount = 0;
|
|
6147
|
+
var currentProviderId = null;
|
|
6148
|
+
var currentModelOverride = null;
|
|
4160
6149
|
function isGitRepo(dir) {
|
|
4161
|
-
return existsSync10(
|
|
6150
|
+
return existsSync10(join8(dir, ".git"));
|
|
4162
6151
|
}
|
|
4163
6152
|
function getOsVersion() {
|
|
4164
6153
|
try {
|
|
@@ -4204,15 +6193,20 @@ ${c.bold("Interactive Commands:")}
|
|
|
4204
6193
|
${c.cyan("/clear")} Clear conversation history
|
|
4205
6194
|
${c.cyan("/tools")} List available tools
|
|
4206
6195
|
${c.cyan("/skills")} List available skills
|
|
6196
|
+
${c.cyan("/models")} Show or switch provider/model
|
|
4207
6197
|
${c.cyan("/todos")} Show current todo list
|
|
4208
6198
|
${c.cyan("/usage")} Show token usage statistics
|
|
6199
|
+
${c.cyan("/debug")} Show debug info (prompt, model, env)
|
|
4209
6200
|
${c.cyan("/exit")} Exit the CLI
|
|
4210
6201
|
|
|
4211
6202
|
${c.bold("Environment:")}
|
|
4212
6203
|
${c.cyan("ANTHROPIC_API_KEY")} Anthropic API key (for Claude models)
|
|
4213
6204
|
${c.cyan("ANTHROPIC_MODEL")} Optional. Claude model (default: claude-sonnet-4-20250514)
|
|
6205
|
+
${c.cyan("GEMINI_API_KEY")} Gemini API key (for Gemini models)
|
|
6206
|
+
${c.cyan("GEMINI_MODEL")} Optional. Gemini model (default: gemini-1.5-pro)
|
|
6207
|
+
${c.cyan("GEMINI_BASE_URL")} Optional. Custom Gemini API base URL
|
|
4214
6208
|
${c.cyan("OPENAI_API_KEY")} OpenAI API key (for GPT models)
|
|
4215
|
-
${c.cyan("OPENAI_MODEL")} Optional. OpenAI model (default: gpt-
|
|
6209
|
+
${c.cyan("OPENAI_MODEL")} Optional. OpenAI model (default: gpt-5.2)
|
|
4216
6210
|
${c.cyan("OPENAI_BASE_URL")} Optional. Custom OpenAI-compatible API URL
|
|
4217
6211
|
|
|
4218
6212
|
${c.bold("Examples:")}
|
|
@@ -4230,7 +6224,7 @@ function printVersion() {
|
|
|
4230
6224
|
console.log(`formagent-sdk v${VERSION}`);
|
|
4231
6225
|
}
|
|
4232
6226
|
function printBanner() {
|
|
4233
|
-
const model =
|
|
6227
|
+
const model = getActiveModel();
|
|
4234
6228
|
console.log();
|
|
4235
6229
|
console.log(c.cyan("╔═══════════════════════════════════════════════════════════╗"));
|
|
4236
6230
|
console.log(c.cyan("║") + c.bold(" FormAgent CLI v" + VERSION + " ") + c.cyan("║"));
|
|
@@ -4238,6 +6232,7 @@ function printBanner() {
|
|
|
4238
6232
|
console.log(c.cyan("╚═══════════════════════════════════════════════════════════╝"));
|
|
4239
6233
|
console.log();
|
|
4240
6234
|
console.log(c.dim(" Model: ") + c.green(model));
|
|
6235
|
+
console.log(c.dim(" Provider: ") + c.green(getActiveProviderId() ?? "auto"));
|
|
4241
6236
|
console.log(c.dim(" Type your message and press Enter to chat."));
|
|
4242
6237
|
console.log(c.dim(" Use /help for commands, /exit to quit."));
|
|
4243
6238
|
console.log();
|
|
@@ -4250,8 +6245,10 @@ function printInteractiveHelp() {
|
|
|
4250
6245
|
console.log(` ${c.cyan("/clear")} Clear conversation history`);
|
|
4251
6246
|
console.log(` ${c.cyan("/tools")} List available tools`);
|
|
4252
6247
|
console.log(` ${c.cyan("/skills")} List available skills`);
|
|
6248
|
+
console.log(` ${c.cyan("/models")} Show or switch provider/model`);
|
|
4253
6249
|
console.log(` ${c.cyan("/todos")} Show current todo list`);
|
|
4254
6250
|
console.log(` ${c.cyan("/usage")} Show token usage statistics`);
|
|
6251
|
+
console.log(` ${c.cyan("/debug")} Show debug info (prompt, model, env)`);
|
|
4255
6252
|
console.log(` ${c.cyan("/exit")} Exit the CLI`);
|
|
4256
6253
|
console.log();
|
|
4257
6254
|
}
|
|
@@ -4322,6 +6319,298 @@ function printUsage() {
|
|
|
4322
6319
|
console.log(` ${c.cyan("Est. cost:")} $${(inputCost + outputCost).toFixed(4)}`);
|
|
4323
6320
|
console.log();
|
|
4324
6321
|
}
|
|
6322
|
+
async function resetSessionForModelChange() {
|
|
6323
|
+
if (session) {
|
|
6324
|
+
await session.close();
|
|
6325
|
+
session = null;
|
|
6326
|
+
}
|
|
6327
|
+
totalInputTokens = 0;
|
|
6328
|
+
totalOutputTokens = 0;
|
|
6329
|
+
messageCount = 0;
|
|
6330
|
+
}
|
|
6331
|
+
function printModelsHelp() {
|
|
6332
|
+
const provider = getActiveProviderId() ?? "auto";
|
|
6333
|
+
const model = getActiveModel();
|
|
6334
|
+
console.log();
|
|
6335
|
+
console.log(c.bold("Model Selection:"));
|
|
6336
|
+
console.log();
|
|
6337
|
+
console.log(` ${c.cyan("Current provider:")} ${provider}`);
|
|
6338
|
+
console.log(` ${c.cyan("Current model:")} ${model}`);
|
|
6339
|
+
console.log();
|
|
6340
|
+
console.log(c.bold("Usage:"));
|
|
6341
|
+
console.log(` ${c.cyan("/models")}`);
|
|
6342
|
+
console.log(c.dim(" List models for the active provider"));
|
|
6343
|
+
console.log(` ${c.cyan("/models")} openai gpt-5-mini`);
|
|
6344
|
+
console.log(` ${c.cyan("/models")} anthropic claude-sonnet-4-20250514`);
|
|
6345
|
+
console.log(` ${c.cyan("/models")} gemini gemini-1.5-pro`);
|
|
6346
|
+
console.log(` ${c.cyan("/models")} gpt-5.2`);
|
|
6347
|
+
console.log(` ${c.cyan("/models")} reset`);
|
|
6348
|
+
console.log();
|
|
6349
|
+
}
|
|
6350
|
+
async function handleModelsCommand(args) {
|
|
6351
|
+
if (args.length === 0) {
|
|
6352
|
+
await listModelsSummary();
|
|
6353
|
+
return;
|
|
6354
|
+
}
|
|
6355
|
+
if (args[0].toLowerCase() === "reset") {
|
|
6356
|
+
currentProviderId = null;
|
|
6357
|
+
currentModelOverride = null;
|
|
6358
|
+
await resetSessionForModelChange();
|
|
6359
|
+
console.log(c.green(`
|
|
6360
|
+
✓ Model selection reset to environment defaults.
|
|
6361
|
+
`));
|
|
6362
|
+
return;
|
|
6363
|
+
}
|
|
6364
|
+
if (args.length === 1) {
|
|
6365
|
+
const provider2 = parseProvider(args[0]);
|
|
6366
|
+
if (provider2) {
|
|
6367
|
+
currentProviderId = provider2;
|
|
6368
|
+
currentModelOverride = null;
|
|
6369
|
+
await resetSessionForModelChange();
|
|
6370
|
+
console.log(c.green(`
|
|
6371
|
+
✓ Provider set to ${provider2}. Model: ${getActiveModel()}.
|
|
6372
|
+
`));
|
|
6373
|
+
return;
|
|
6374
|
+
}
|
|
6375
|
+
currentModelOverride = args[0];
|
|
6376
|
+
currentProviderId = inferProviderFromModel(args[0]) ?? currentProviderId;
|
|
6377
|
+
await resetSessionForModelChange();
|
|
6378
|
+
console.log(c.green(`
|
|
6379
|
+
✓ Model set to ${currentModelOverride} (provider: ${getActiveProviderId() ?? "auto"}).
|
|
6380
|
+
`));
|
|
6381
|
+
return;
|
|
6382
|
+
}
|
|
6383
|
+
const provider = parseProvider(args[0]);
|
|
6384
|
+
if (!provider) {
|
|
6385
|
+
console.log(c.yellow(`
|
|
6386
|
+
Unknown provider: ${args[0]}. Use "openai", "anthropic", or "gemini".
|
|
6387
|
+
`));
|
|
6388
|
+
return;
|
|
6389
|
+
}
|
|
6390
|
+
const model = args.slice(1).join(" ");
|
|
6391
|
+
if (!model) {
|
|
6392
|
+
console.log(c.yellow(`
|
|
6393
|
+
Missing model name. Example: /models openai gpt-5-mini
|
|
6394
|
+
`));
|
|
6395
|
+
return;
|
|
6396
|
+
}
|
|
6397
|
+
currentProviderId = provider;
|
|
6398
|
+
currentModelOverride = model;
|
|
6399
|
+
await resetSessionForModelChange();
|
|
6400
|
+
console.log(c.green(`
|
|
6401
|
+
✓ Provider set to ${provider}, model set to ${model}.
|
|
6402
|
+
`));
|
|
6403
|
+
}
|
|
6404
|
+
function normalizeOpenAIBaseUrl(baseUrl) {
|
|
6405
|
+
const trimmed = baseUrl.replace(/\/+$/, "");
|
|
6406
|
+
if (trimmed.endsWith("/v1")) {
|
|
6407
|
+
return trimmed;
|
|
6408
|
+
}
|
|
6409
|
+
return `${trimmed}/v1`;
|
|
6410
|
+
}
|
|
6411
|
+
function getOpenAIApiType(baseUrl) {
|
|
6412
|
+
const normalized = baseUrl.toLowerCase();
|
|
6413
|
+
return normalized.includes("api.openai.com") ? "openai" : "openai-compatible";
|
|
6414
|
+
}
|
|
6415
|
+
function isGoogleGeminiBaseUrl(baseUrl) {
|
|
6416
|
+
const normalized = baseUrl.toLowerCase();
|
|
6417
|
+
return normalized.includes("generativelanguage.googleapis.com") || normalized.includes("/v1beta");
|
|
6418
|
+
}
|
|
6419
|
+
async function listAnthropicModels() {
|
|
6420
|
+
const baseUrlRaw = (process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com").replace(/\/+$/, "");
|
|
6421
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
6422
|
+
const baseUrl = baseUrlRaw.endsWith("/v1") ? baseUrlRaw : `${baseUrlRaw}/v1`;
|
|
6423
|
+
console.log(c.bold("Anthropic Models:"));
|
|
6424
|
+
console.log(c.dim(" API Type: anthropic (official)"));
|
|
6425
|
+
console.log(c.dim(` Base URL: ${baseUrl}`));
|
|
6426
|
+
if (!apiKey) {
|
|
6427
|
+
console.log(c.red(" ✗ ANTHROPIC_API_KEY not set"));
|
|
6428
|
+
console.log();
|
|
6429
|
+
return;
|
|
6430
|
+
}
|
|
6431
|
+
const res = await fetch(`${baseUrl}/models`, {
|
|
6432
|
+
headers: {
|
|
6433
|
+
"x-api-key": apiKey,
|
|
6434
|
+
"anthropic-version": "2023-06-01"
|
|
6435
|
+
}
|
|
6436
|
+
});
|
|
6437
|
+
if (!res.ok) {
|
|
6438
|
+
console.log(c.red(` ✗ Failed to fetch models (${res.status})`));
|
|
6439
|
+
console.log(c.dim(` URL: ${baseUrl}/models`));
|
|
6440
|
+
console.log();
|
|
6441
|
+
return;
|
|
6442
|
+
}
|
|
6443
|
+
const payload = await res.json();
|
|
6444
|
+
const items = payload.data ?? [];
|
|
6445
|
+
for (const item of items) {
|
|
6446
|
+
const name = item.display_name ? ` (${item.display_name})` : "";
|
|
6447
|
+
console.log(` ${c.green("●")} ${item.id}${name}`);
|
|
6448
|
+
}
|
|
6449
|
+
console.log();
|
|
6450
|
+
}
|
|
6451
|
+
async function listOpenAIModels() {
|
|
6452
|
+
const baseUrl = normalizeOpenAIBaseUrl(process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1");
|
|
6453
|
+
const apiFlavor = getOpenAIApiType(baseUrl);
|
|
6454
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
6455
|
+
console.log(c.bold("OpenAI Models:"));
|
|
6456
|
+
console.log(c.dim(` API Type: ${apiFlavor}`));
|
|
6457
|
+
console.log(c.dim(` Base URL: ${baseUrl}`));
|
|
6458
|
+
if (!apiKey) {
|
|
6459
|
+
console.log(c.red(" ✗ OPENAI_API_KEY not set"));
|
|
6460
|
+
console.log();
|
|
6461
|
+
return;
|
|
6462
|
+
}
|
|
6463
|
+
const res = await fetch(`${baseUrl}/models`, {
|
|
6464
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
6465
|
+
});
|
|
6466
|
+
if (!res.ok) {
|
|
6467
|
+
console.log(c.red(` ✗ Failed to fetch models (${res.status})`));
|
|
6468
|
+
console.log(c.dim(` URL: ${baseUrl}/models`));
|
|
6469
|
+
console.log();
|
|
6470
|
+
return;
|
|
6471
|
+
}
|
|
6472
|
+
const payload = await res.json();
|
|
6473
|
+
const items = payload.data ?? [];
|
|
6474
|
+
for (const item of items) {
|
|
6475
|
+
const owner = item.owned_by ? ` (${item.owned_by})` : "";
|
|
6476
|
+
console.log(` ${c.green("●")} ${item.id}${owner}`);
|
|
6477
|
+
}
|
|
6478
|
+
console.log();
|
|
6479
|
+
}
|
|
6480
|
+
async function listGeminiModels() {
|
|
6481
|
+
const baseUrlRaw = (process.env.GEMINI_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta").replace(/\/+$/, "");
|
|
6482
|
+
const apiKey = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
|
|
6483
|
+
console.log(c.bold("Gemini Models:"));
|
|
6484
|
+
console.log(c.dim(` Base URL: ${baseUrlRaw}`));
|
|
6485
|
+
if (!apiKey) {
|
|
6486
|
+
console.log(c.red(" ✗ GEMINI_API_KEY not set"));
|
|
6487
|
+
console.log();
|
|
6488
|
+
return;
|
|
6489
|
+
}
|
|
6490
|
+
if (isGoogleGeminiBaseUrl(baseUrlRaw)) {
|
|
6491
|
+
console.log(c.dim(" API Type: gemini"));
|
|
6492
|
+
const url = `${baseUrlRaw}/models`;
|
|
6493
|
+
const res2 = await fetch(url, {
|
|
6494
|
+
headers: { "x-goog-api-key": apiKey }
|
|
6495
|
+
});
|
|
6496
|
+
if (!res2.ok) {
|
|
6497
|
+
console.log(c.red(` ✗ Failed to fetch models (${res2.status})`));
|
|
6498
|
+
console.log(c.dim(` URL: ${url}`));
|
|
6499
|
+
console.log();
|
|
6500
|
+
return;
|
|
6501
|
+
}
|
|
6502
|
+
const payload2 = await res2.json();
|
|
6503
|
+
const items2 = payload2.models ?? [];
|
|
6504
|
+
for (const item of items2) {
|
|
6505
|
+
console.log(` ${c.green("●")} ${item.name}`);
|
|
6506
|
+
}
|
|
6507
|
+
console.log();
|
|
6508
|
+
return;
|
|
6509
|
+
}
|
|
6510
|
+
const openaiBase = normalizeOpenAIBaseUrl(baseUrlRaw);
|
|
6511
|
+
console.log(c.dim(" API Type: openai-compatible"));
|
|
6512
|
+
console.log(c.dim(" Auth: Bearer (GEMINI_API_KEY)"));
|
|
6513
|
+
const res = await fetch(`${openaiBase}/models`, {
|
|
6514
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
6515
|
+
});
|
|
6516
|
+
if (!res.ok) {
|
|
6517
|
+
console.log(c.red(` ✗ Failed to fetch models (${res.status})`));
|
|
6518
|
+
console.log(c.dim(` URL: ${openaiBase}/models`));
|
|
6519
|
+
console.log();
|
|
6520
|
+
return;
|
|
6521
|
+
}
|
|
6522
|
+
const payload = await res.json();
|
|
6523
|
+
const items = payload.data ?? [];
|
|
6524
|
+
for (const item of items) {
|
|
6525
|
+
const owner = item.owned_by ? ` (${item.owned_by})` : "";
|
|
6526
|
+
console.log(` ${c.green("●")} ${item.id}${owner}`);
|
|
6527
|
+
}
|
|
6528
|
+
console.log();
|
|
6529
|
+
}
|
|
6530
|
+
async function listModelsSummary() {
|
|
6531
|
+
const provider = getActiveProviderId();
|
|
6532
|
+
const apiType = provider ?? "auto";
|
|
6533
|
+
console.log();
|
|
6534
|
+
console.log(c.bold("Available Models:"));
|
|
6535
|
+
console.log(c.dim(` Active Provider: ${apiType}`));
|
|
6536
|
+
console.log();
|
|
6537
|
+
printModelsHelp();
|
|
6538
|
+
try {
|
|
6539
|
+
await listOpenAIModels();
|
|
6540
|
+
} catch (error) {
|
|
6541
|
+
console.log(c.red(` ✗ OpenAI: ${error instanceof Error ? error.message : String(error)}`));
|
|
6542
|
+
console.log();
|
|
6543
|
+
}
|
|
6544
|
+
try {
|
|
6545
|
+
await listGeminiModels();
|
|
6546
|
+
} catch (error) {
|
|
6547
|
+
console.log(c.red(` ✗ Gemini: ${error instanceof Error ? error.message : String(error)}`));
|
|
6548
|
+
console.log();
|
|
6549
|
+
}
|
|
6550
|
+
await listAnthropicModels();
|
|
6551
|
+
}
|
|
6552
|
+
function printDebug() {
|
|
6553
|
+
const model = getActiveModel();
|
|
6554
|
+
const tools = getAllTools();
|
|
6555
|
+
const systemPrompt = buildSystemPrompt();
|
|
6556
|
+
const cwd = process.cwd();
|
|
6557
|
+
console.log();
|
|
6558
|
+
console.log(c.bold("═══════════════════════════════════════════════════════════"));
|
|
6559
|
+
console.log(c.bold(" DEBUG INFORMATION "));
|
|
6560
|
+
console.log(c.bold("═══════════════════════════════════════════════════════════"));
|
|
6561
|
+
console.log();
|
|
6562
|
+
console.log(c.bold("Model:"));
|
|
6563
|
+
console.log(` ${c.cyan("Current:")} ${model}`);
|
|
6564
|
+
console.log(` ${c.cyan("Provider:")} ${getActiveProviderId() ?? "auto"}`);
|
|
6565
|
+
console.log(` ${c.cyan("Override:")} ${currentModelOverride ?? c.dim("(not set)")}`);
|
|
6566
|
+
console.log(` ${c.cyan("ANTHROPIC_MODEL:")} ${process.env.ANTHROPIC_MODEL || c.dim("(not set)")}`);
|
|
6567
|
+
console.log(` ${c.cyan("GEMINI_MODEL:")} ${process.env.GEMINI_MODEL || c.dim("(not set)")}`);
|
|
6568
|
+
console.log(` ${c.cyan("GEMINI_BASE_URL:")} ${process.env.GEMINI_BASE_URL || c.dim("(not set)")}`);
|
|
6569
|
+
console.log(` ${c.cyan("OPENAI_MODEL:")} ${process.env.OPENAI_MODEL || c.dim("(not set)")}`);
|
|
6570
|
+
console.log(` ${c.cyan("OPENAI_BASE_URL:")} ${process.env.OPENAI_BASE_URL || c.dim("(not set)")}`);
|
|
6571
|
+
console.log();
|
|
6572
|
+
console.log(c.bold("API Keys:"));
|
|
6573
|
+
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
6574
|
+
const geminiKey = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
|
|
6575
|
+
const openaiKey = process.env.OPENAI_API_KEY;
|
|
6576
|
+
console.log(` ${c.cyan("ANTHROPIC_API_KEY:")} ${anthropicKey ? c.green("✓ set") + c.dim(` (${anthropicKey.slice(0, 8)}...${anthropicKey.slice(-4)})`) : c.red("✗ not set")}`);
|
|
6577
|
+
console.log(` ${c.cyan("GEMINI_API_KEY:")} ${geminiKey ? c.green("✓ set") + c.dim(` (${geminiKey.slice(0, 8)}...${geminiKey.slice(-4)})`) : c.red("✗ not set")}`);
|
|
6578
|
+
console.log(` ${c.cyan("OPENAI_API_KEY:")} ${openaiKey ? c.green("✓ set") + c.dim(` (${openaiKey.slice(0, 8)}...${openaiKey.slice(-4)})`) : c.red("✗ not set")}`);
|
|
6579
|
+
console.log();
|
|
6580
|
+
console.log(c.bold("Environment:"));
|
|
6581
|
+
console.log(` ${c.cyan("Working dir:")} ${cwd}`);
|
|
6582
|
+
console.log(` ${c.cyan("Git repo:")} ${isGitRepo(cwd) ? c.green("Yes") : "No"}`);
|
|
6583
|
+
console.log(` ${c.cyan("Platform:")} ${process.platform}`);
|
|
6584
|
+
console.log(` ${c.cyan("OS Version:")} ${getOsVersion()}`);
|
|
6585
|
+
console.log(` ${c.cyan("Shell:")} ${process.env.SHELL || c.dim("(not set)")}`);
|
|
6586
|
+
console.log(` ${c.cyan("Skills path:")} ${SKILLS_PATH}`);
|
|
6587
|
+
console.log();
|
|
6588
|
+
console.log(c.bold("Tools:") + c.dim(` (${tools.length} total)`));
|
|
6589
|
+
const toolNames = tools.map((t) => t.name);
|
|
6590
|
+
console.log(` ${toolNames.join(", ")}`);
|
|
6591
|
+
console.log();
|
|
6592
|
+
console.log(c.bold("Session State:"));
|
|
6593
|
+
console.log(` ${c.cyan("Active:")} ${session ? c.green("Yes") : "No"}`);
|
|
6594
|
+
console.log(` ${c.cyan("Messages:")} ${messageCount}`);
|
|
6595
|
+
console.log(` ${c.cyan("Input tokens:")} ${totalInputTokens.toLocaleString()}`);
|
|
6596
|
+
console.log(` ${c.cyan("Output tokens:")} ${totalOutputTokens.toLocaleString()}`);
|
|
6597
|
+
console.log();
|
|
6598
|
+
console.log(c.bold("System Prompt:") + c.dim(` (${systemPrompt.length} chars)`));
|
|
6599
|
+
console.log(c.dim("─".repeat(60)));
|
|
6600
|
+
const promptLines = systemPrompt.split(`
|
|
6601
|
+
`);
|
|
6602
|
+
const maxLines = 50;
|
|
6603
|
+
for (let i = 0;i < Math.min(promptLines.length, maxLines); i++) {
|
|
6604
|
+
const lineNum = String(i + 1).padStart(3, " ");
|
|
6605
|
+
const line = promptLines[i].slice(0, 75);
|
|
6606
|
+
console.log(`${c.dim(lineNum + "│")} ${line}${promptLines[i].length > 75 ? c.dim("...") : ""}`);
|
|
6607
|
+
}
|
|
6608
|
+
if (promptLines.length > maxLines) {
|
|
6609
|
+
console.log(c.dim(` ... (${promptLines.length - maxLines} more lines)`));
|
|
6610
|
+
}
|
|
6611
|
+
console.log(c.dim("─".repeat(60)));
|
|
6612
|
+
console.log();
|
|
6613
|
+
}
|
|
4325
6614
|
function formatToolInput(name, input) {
|
|
4326
6615
|
switch (name) {
|
|
4327
6616
|
case "Bash":
|
|
@@ -4349,19 +6638,94 @@ function formatToolInput(name, input) {
|
|
|
4349
6638
|
return JSON.stringify(input).slice(0, 50);
|
|
4350
6639
|
}
|
|
4351
6640
|
}
|
|
4352
|
-
function
|
|
6641
|
+
function getDefaultProviderFromEnv() {
|
|
4353
6642
|
if (process.env.ANTHROPIC_API_KEY) {
|
|
4354
|
-
return
|
|
6643
|
+
return "anthropic";
|
|
4355
6644
|
}
|
|
4356
6645
|
if (process.env.OPENAI_API_KEY) {
|
|
4357
|
-
return
|
|
6646
|
+
return "openai";
|
|
6647
|
+
}
|
|
6648
|
+
if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
|
|
6649
|
+
return "gemini";
|
|
6650
|
+
}
|
|
6651
|
+
return null;
|
|
6652
|
+
}
|
|
6653
|
+
function inferProviderFromModel(model) {
|
|
6654
|
+
const normalized = model.toLowerCase();
|
|
6655
|
+
if (normalized.startsWith("claude")) {
|
|
6656
|
+
return "anthropic";
|
|
6657
|
+
}
|
|
6658
|
+
if (normalized.startsWith("gpt") || normalized.startsWith("o1") || normalized.startsWith("chatgpt")) {
|
|
6659
|
+
return "openai";
|
|
6660
|
+
}
|
|
6661
|
+
if (normalized.startsWith("gemini") || normalized.startsWith("models/gemini")) {
|
|
6662
|
+
return "gemini";
|
|
6663
|
+
}
|
|
6664
|
+
return null;
|
|
6665
|
+
}
|
|
6666
|
+
function getDefaultModelForProvider(providerId) {
|
|
6667
|
+
if (providerId === "anthropic") {
|
|
6668
|
+
return process.env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514";
|
|
6669
|
+
}
|
|
6670
|
+
if (providerId === "gemini") {
|
|
6671
|
+
return process.env.GEMINI_MODEL || "gemini-1.5-pro";
|
|
6672
|
+
}
|
|
6673
|
+
return process.env.OPENAI_MODEL || "gpt-5.2";
|
|
6674
|
+
}
|
|
6675
|
+
function getActiveProviderId() {
|
|
6676
|
+
if (currentProviderId) {
|
|
6677
|
+
return currentProviderId;
|
|
6678
|
+
}
|
|
6679
|
+
if (currentModelOverride) {
|
|
6680
|
+
return inferProviderFromModel(currentModelOverride);
|
|
6681
|
+
}
|
|
6682
|
+
return getDefaultProviderFromEnv();
|
|
6683
|
+
}
|
|
6684
|
+
function getActiveModel() {
|
|
6685
|
+
if (currentModelOverride) {
|
|
6686
|
+
return currentModelOverride;
|
|
6687
|
+
}
|
|
6688
|
+
const provider = getActiveProviderId();
|
|
6689
|
+
if (provider) {
|
|
6690
|
+
return getDefaultModelForProvider(provider);
|
|
4358
6691
|
}
|
|
4359
6692
|
return "claude-sonnet-4-20250514";
|
|
4360
6693
|
}
|
|
6694
|
+
function parseProvider(arg) {
|
|
6695
|
+
const normalized = arg.toLowerCase();
|
|
6696
|
+
if (normalized === "anthropic" || normalized === "claude") {
|
|
6697
|
+
return "anthropic";
|
|
6698
|
+
}
|
|
6699
|
+
if (normalized === "openai" || normalized === "gpt") {
|
|
6700
|
+
return "openai";
|
|
6701
|
+
}
|
|
6702
|
+
if (normalized === "gemini" || normalized === "google") {
|
|
6703
|
+
return "gemini";
|
|
6704
|
+
}
|
|
6705
|
+
return null;
|
|
6706
|
+
}
|
|
6707
|
+
function createProvider(providerId) {
|
|
6708
|
+
if (providerId === "anthropic") {
|
|
6709
|
+
return new AnthropicProvider;
|
|
6710
|
+
}
|
|
6711
|
+
if (providerId === "gemini") {
|
|
6712
|
+
return new GeminiProvider({
|
|
6713
|
+
apiKey: process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY,
|
|
6714
|
+
baseUrl: process.env.GEMINI_BASE_URL
|
|
6715
|
+
});
|
|
6716
|
+
}
|
|
6717
|
+
return new OpenAIProvider({
|
|
6718
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
6719
|
+
baseUrl: process.env.OPENAI_BASE_URL
|
|
6720
|
+
});
|
|
6721
|
+
}
|
|
4361
6722
|
async function getSession() {
|
|
4362
6723
|
if (!session) {
|
|
6724
|
+
const providerId = getActiveProviderId();
|
|
6725
|
+
const provider = providerId ? createProvider(providerId) : undefined;
|
|
4363
6726
|
session = await createSession({
|
|
4364
|
-
model:
|
|
6727
|
+
model: getActiveModel(),
|
|
6728
|
+
provider,
|
|
4365
6729
|
tools: getAllTools(),
|
|
4366
6730
|
systemPrompt: buildSystemPrompt()
|
|
4367
6731
|
});
|
|
@@ -4419,7 +6783,9 @@ async function handleInput(input) {
|
|
|
4419
6783
|
return true;
|
|
4420
6784
|
}
|
|
4421
6785
|
if (trimmed.startsWith("/")) {
|
|
4422
|
-
const
|
|
6786
|
+
const parts = trimmed.split(/\s+/);
|
|
6787
|
+
const cmd = parts[0].toLowerCase();
|
|
6788
|
+
const args = parts.slice(1);
|
|
4423
6789
|
switch (cmd) {
|
|
4424
6790
|
case "/help":
|
|
4425
6791
|
printInteractiveHelp();
|
|
@@ -4443,12 +6809,18 @@ async function handleInput(input) {
|
|
|
4443
6809
|
case "/skills":
|
|
4444
6810
|
await printSkills();
|
|
4445
6811
|
return true;
|
|
6812
|
+
case "/models":
|
|
6813
|
+
await handleModelsCommand(args);
|
|
6814
|
+
return true;
|
|
4446
6815
|
case "/todos":
|
|
4447
6816
|
printTodos();
|
|
4448
6817
|
return true;
|
|
4449
6818
|
case "/usage":
|
|
4450
6819
|
printUsage();
|
|
4451
6820
|
return true;
|
|
6821
|
+
case "/debug":
|
|
6822
|
+
printDebug();
|
|
6823
|
+
return true;
|
|
4452
6824
|
case "/exit":
|
|
4453
6825
|
case "/quit":
|
|
4454
6826
|
case "/q":
|
|
@@ -4472,9 +6844,9 @@ Error: ${error instanceof Error ? error.message : String(error)}
|
|
|
4472
6844
|
return true;
|
|
4473
6845
|
}
|
|
4474
6846
|
async function runQuickQuery(query) {
|
|
4475
|
-
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
|
|
6847
|
+
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY && !process.env.GEMINI_API_KEY && !process.env.GOOGLE_API_KEY) {
|
|
4476
6848
|
console.error(c.red("Error: No API key found"));
|
|
4477
|
-
console.error(c.dim("Set ANTHROPIC_API_KEY or
|
|
6849
|
+
console.error(c.dim("Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY environment variable"));
|
|
4478
6850
|
process.exit(1);
|
|
4479
6851
|
}
|
|
4480
6852
|
try {
|
|
@@ -4488,9 +6860,9 @@ async function runQuickQuery(query) {
|
|
|
4488
6860
|
}
|
|
4489
6861
|
}
|
|
4490
6862
|
async function runInteractive() {
|
|
4491
|
-
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
|
|
6863
|
+
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY && !process.env.GEMINI_API_KEY && !process.env.GOOGLE_API_KEY) {
|
|
4492
6864
|
console.error(c.red("Error: No API key found"));
|
|
4493
|
-
console.error(c.dim("Set ANTHROPIC_API_KEY or
|
|
6865
|
+
console.error(c.dim("Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY environment variable"));
|
|
4494
6866
|
process.exit(1);
|
|
4495
6867
|
}
|
|
4496
6868
|
setTodoChangeCallback(() => {});
|