formagent-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4539 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
+
5
+ // src/cli/cli.ts
6
+ import * as readline from "node:readline";
7
+ import { homedir as homedir4 } from "node:os";
8
+ import { join as join7 } from "node:path";
9
+ import { existsSync as existsSync10 } from "node:fs";
10
+
11
+ // src/utils/id.ts
12
+ var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
13
+ function generateId(prefix, length = 12) {
14
+ const randomPart = generateRandomString(length);
15
+ return prefix ? `${prefix}_${randomPart}` : randomPart;
16
+ }
17
+ function generateRandomString(length) {
18
+ const bytes = new Uint8Array(length);
19
+ crypto.getRandomValues(bytes);
20
+ let result = "";
21
+ for (let i = 0;i < length; i++) {
22
+ result += ALPHABET[bytes[i] % ALPHABET.length];
23
+ }
24
+ return result;
25
+ }
26
+ function generateSessionId() {
27
+ return generateId("sess", 16);
28
+ }
29
+ function generateMessageId() {
30
+ return generateId("msg", 20);
31
+ }
32
+
33
+ // src/utils/events.ts
34
+ import { EventEmitter } from "events";
35
+
36
+ class TypedEventEmitter {
37
+ emitter;
38
+ constructor() {
39
+ this.emitter = new EventEmitter;
40
+ }
41
+ on(event, listener) {
42
+ this.emitter.on(event, listener);
43
+ return this;
44
+ }
45
+ once(event, listener) {
46
+ this.emitter.once(event, listener);
47
+ return this;
48
+ }
49
+ off(event, listener) {
50
+ this.emitter.off(event, listener);
51
+ return this;
52
+ }
53
+ emit(event, ...args) {
54
+ return this.emitter.emit(event, ...args);
55
+ }
56
+ removeAllListeners(event) {
57
+ if (event) {
58
+ this.emitter.removeAllListeners(event);
59
+ } else {
60
+ this.emitter.removeAllListeners();
61
+ }
62
+ return this;
63
+ }
64
+ listenerCount(event) {
65
+ return this.emitter.listenerCount(event);
66
+ }
67
+ listeners(event) {
68
+ return this.emitter.listeners(event);
69
+ }
70
+ setMaxListeners(n) {
71
+ this.emitter.setMaxListeners(n);
72
+ return this;
73
+ }
74
+ getMaxListeners() {
75
+ return this.emitter.getMaxListeners();
76
+ }
77
+ }
78
+
79
+ // src/hooks/manager.ts
80
+ class HookTimeoutError extends Error {
81
+ constructor(hookName, timeout) {
82
+ super(`Hook "${hookName}" timed out after ${timeout}ms`);
83
+ this.name = "HookTimeoutError";
84
+ }
85
+ }
86
+
87
+ class HooksManager {
88
+ config;
89
+ sessionId;
90
+ cwd;
91
+ constructor(config, sessionId, cwd = process.cwd()) {
92
+ this.config = config;
93
+ this.sessionId = sessionId;
94
+ this.cwd = cwd;
95
+ }
96
+ async runPreToolUse(toolName, toolInput, toolUseId, abortSignal) {
97
+ const matchers = this.config.PreToolUse ?? [];
98
+ const matchingHooks = this.getMatchingHooks(matchers, toolName);
99
+ if (matchingHooks.length === 0) {
100
+ return {
101
+ allowed: true,
102
+ decision: "allow",
103
+ continue: true
104
+ };
105
+ }
106
+ const input = {
107
+ hook_event_name: "PreToolUse",
108
+ session_id: this.sessionId,
109
+ cwd: this.cwd,
110
+ tool_name: toolName,
111
+ tool_input: toolInput
112
+ };
113
+ const context = { signal: abortSignal };
114
+ let finalDecision = "allow";
115
+ let finalReason;
116
+ let finalUpdatedInput;
117
+ let systemMessage;
118
+ let shouldContinue = true;
119
+ let stopReason;
120
+ for (const { hook, timeout } of matchingHooks) {
121
+ try {
122
+ const result = await this.runHookWithTimeout(hook, input, toolUseId, context, timeout);
123
+ if (result.continue === false) {
124
+ shouldContinue = false;
125
+ stopReason = result.stopReason;
126
+ break;
127
+ }
128
+ if (result.systemMessage) {
129
+ systemMessage = result.systemMessage;
130
+ }
131
+ if (result.hookSpecificOutput) {
132
+ const output = result.hookSpecificOutput;
133
+ if (output.hookEventName === "PreToolUse") {
134
+ if (output.permissionDecision === "deny") {
135
+ finalDecision = "deny";
136
+ finalReason = output.permissionDecisionReason;
137
+ } else if (output.permissionDecision === "ask" && finalDecision !== "deny") {
138
+ finalDecision = "ask";
139
+ finalReason = output.permissionDecisionReason;
140
+ } else if (output.permissionDecision === "allow" && output.updatedInput) {
141
+ finalUpdatedInput = output.updatedInput;
142
+ }
143
+ }
144
+ }
145
+ } catch (error) {
146
+ console.error(`Hook error: ${error}`);
147
+ }
148
+ }
149
+ return {
150
+ allowed: finalDecision === "allow",
151
+ decision: finalDecision,
152
+ reason: finalReason,
153
+ updatedInput: finalUpdatedInput,
154
+ systemMessage,
155
+ continue: shouldContinue,
156
+ stopReason
157
+ };
158
+ }
159
+ async runPostToolUse(toolName, toolInput, toolResponse, toolUseId, abortSignal) {
160
+ const matchers = this.config.PostToolUse ?? [];
161
+ const matchingHooks = this.getMatchingHooks(matchers, toolName);
162
+ if (matchingHooks.length === 0) {
163
+ return { continue: true };
164
+ }
165
+ const input = {
166
+ hook_event_name: "PostToolUse",
167
+ session_id: this.sessionId,
168
+ cwd: this.cwd,
169
+ tool_name: toolName,
170
+ tool_input: toolInput,
171
+ tool_response: toolResponse
172
+ };
173
+ const context = { signal: abortSignal };
174
+ let additionalContext;
175
+ let systemMessage;
176
+ let shouldContinue = true;
177
+ let stopReason;
178
+ for (const { hook, timeout } of matchingHooks) {
179
+ try {
180
+ const result = await this.runHookWithTimeout(hook, input, toolUseId, context, timeout);
181
+ if (result.continue === false) {
182
+ shouldContinue = false;
183
+ stopReason = result.stopReason;
184
+ break;
185
+ }
186
+ if (result.systemMessage) {
187
+ systemMessage = result.systemMessage;
188
+ }
189
+ if (result.hookSpecificOutput?.hookEventName === "PostToolUse") {
190
+ if (result.hookSpecificOutput.additionalContext) {
191
+ additionalContext = result.hookSpecificOutput.additionalContext;
192
+ }
193
+ }
194
+ } catch (error) {
195
+ console.error(`Hook error: ${error}`);
196
+ }
197
+ }
198
+ return {
199
+ additionalContext,
200
+ systemMessage,
201
+ continue: shouldContinue,
202
+ stopReason
203
+ };
204
+ }
205
+ async runUserPromptSubmit(prompt, abortSignal) {
206
+ const matchers = this.config.UserPromptSubmit ?? [];
207
+ if (matchers.length === 0) {
208
+ return { continue: true };
209
+ }
210
+ const input = {
211
+ hook_event_name: "UserPromptSubmit",
212
+ session_id: this.sessionId,
213
+ cwd: this.cwd,
214
+ prompt
215
+ };
216
+ const context = { signal: abortSignal };
217
+ let additionalContext;
218
+ let systemMessage;
219
+ let shouldContinue = true;
220
+ let stopReason;
221
+ for (const matcher of matchers) {
222
+ for (const hook of matcher.hooks) {
223
+ try {
224
+ const result = await this.runHookWithTimeout(hook, input, null, context, matcher.timeout ?? 60);
225
+ if (result.continue === false) {
226
+ shouldContinue = false;
227
+ stopReason = result.stopReason;
228
+ break;
229
+ }
230
+ if (result.systemMessage) {
231
+ systemMessage = result.systemMessage;
232
+ }
233
+ if (result.hookSpecificOutput?.hookEventName === "UserPromptSubmit") {
234
+ if (result.hookSpecificOutput.additionalContext) {
235
+ additionalContext = result.hookSpecificOutput.additionalContext;
236
+ }
237
+ }
238
+ } catch (error) {
239
+ console.error(`Hook error: ${error}`);
240
+ }
241
+ }
242
+ if (!shouldContinue)
243
+ break;
244
+ }
245
+ return {
246
+ additionalContext,
247
+ systemMessage,
248
+ continue: shouldContinue,
249
+ stopReason
250
+ };
251
+ }
252
+ async runStop(abortSignal) {
253
+ const matchers = this.config.Stop ?? [];
254
+ if (matchers.length === 0) {
255
+ return;
256
+ }
257
+ const input = {
258
+ hook_event_name: "Stop",
259
+ session_id: this.sessionId,
260
+ cwd: this.cwd,
261
+ stop_hook_active: true
262
+ };
263
+ const context = { signal: abortSignal };
264
+ for (const matcher of matchers) {
265
+ for (const hook of matcher.hooks) {
266
+ try {
267
+ await this.runHookWithTimeout(hook, input, null, context, matcher.timeout ?? 60);
268
+ } catch (error) {
269
+ console.error(`Hook error: ${error}`);
270
+ }
271
+ }
272
+ }
273
+ }
274
+ getMatchingHooks(matchers, toolName) {
275
+ const result = [];
276
+ for (const matcher of matchers) {
277
+ if (!matcher.matcher) {
278
+ for (const hook of matcher.hooks) {
279
+ result.push({ hook, timeout: (matcher.timeout ?? 60) * 1000 });
280
+ }
281
+ continue;
282
+ }
283
+ try {
284
+ const regex = new RegExp(matcher.matcher);
285
+ if (regex.test(toolName)) {
286
+ for (const hook of matcher.hooks) {
287
+ result.push({ hook, timeout: (matcher.timeout ?? 60) * 1000 });
288
+ }
289
+ }
290
+ } catch {
291
+ console.warn(`Invalid hook matcher regex: ${matcher.matcher}`);
292
+ }
293
+ }
294
+ return result;
295
+ }
296
+ async runHookWithTimeout(hook, input, toolUseId, context, timeoutMs) {
297
+ return new Promise((resolve, reject) => {
298
+ const timer = setTimeout(() => {
299
+ reject(new HookTimeoutError(input.hook_event_name, timeoutMs));
300
+ }, timeoutMs);
301
+ hook(input, toolUseId, context).then((result) => {
302
+ clearTimeout(timer);
303
+ resolve(result);
304
+ }).catch((error) => {
305
+ clearTimeout(timer);
306
+ reject(error);
307
+ });
308
+ });
309
+ }
310
+ }
311
+
312
+ // src/skills/loader.ts
313
+ import { existsSync, readdirSync, statSync } from "node:fs";
314
+ import { readFile } from "node:fs/promises";
315
+ import { join, dirname, basename } from "node:path";
316
+ import { homedir } from "node:os";
317
+
318
+ // src/utils/frontmatter.ts
319
+ function parseFrontmatter(input) {
320
+ const trimmed = input.trim();
321
+ if (!trimmed.startsWith("---")) {
322
+ return {
323
+ data: {},
324
+ content: input
325
+ };
326
+ }
327
+ const endIndex = trimmed.indexOf(`
328
+ ---`, 3);
329
+ if (endIndex === -1) {
330
+ return {
331
+ data: {},
332
+ content: input
333
+ };
334
+ }
335
+ const raw = trimmed.slice(4, endIndex).trim();
336
+ const content = trimmed.slice(endIndex + 4).trim();
337
+ const data = parseYamlLike(raw);
338
+ return {
339
+ data,
340
+ content,
341
+ raw
342
+ };
343
+ }
344
+ function parseYamlLike(yaml) {
345
+ const result = {};
346
+ const lines = yaml.split(`
347
+ `);
348
+ let currentKey = null;
349
+ let currentArray = null;
350
+ for (const line of lines) {
351
+ const trimmedLine = line.trim();
352
+ if (!trimmedLine || trimmedLine.startsWith("#")) {
353
+ continue;
354
+ }
355
+ if (trimmedLine.startsWith("- ") && currentKey && currentArray) {
356
+ const value = parseValue(trimmedLine.slice(2).trim());
357
+ currentArray.push(value);
358
+ continue;
359
+ }
360
+ const colonIndex = trimmedLine.indexOf(":");
361
+ if (colonIndex === -1) {
362
+ continue;
363
+ }
364
+ const key = trimmedLine.slice(0, colonIndex).trim();
365
+ const rawValue = trimmedLine.slice(colonIndex + 1).trim();
366
+ if (!rawValue) {
367
+ currentKey = key;
368
+ currentArray = [];
369
+ result[key] = currentArray;
370
+ continue;
371
+ }
372
+ currentKey = null;
373
+ currentArray = null;
374
+ result[key] = parseValue(rawValue);
375
+ }
376
+ return result;
377
+ }
378
+ function parseValue(raw) {
379
+ const trimmed = raw.trim();
380
+ if (!trimmed) {
381
+ return "";
382
+ }
383
+ if (trimmed === "null" || trimmed === "~") {
384
+ return null;
385
+ }
386
+ if (trimmed === "true") {
387
+ return true;
388
+ }
389
+ if (trimmed === "false") {
390
+ return false;
391
+ }
392
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
393
+ const inner = trimmed.slice(1, -1).trim();
394
+ if (!inner) {
395
+ return [];
396
+ }
397
+ return inner.split(",").map((item) => parseValue(item.trim()));
398
+ }
399
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
400
+ return trimmed.slice(1, -1);
401
+ }
402
+ const num = Number(trimmed);
403
+ if (!isNaN(num) && trimmed !== "") {
404
+ return num;
405
+ }
406
+ return trimmed;
407
+ }
408
+
409
+ // src/skills/loader.ts
410
+ var SKILL_FILE_NAME = "SKILL.md";
411
+ var USER_SKILLS_DIR = ".claude/skills";
412
+ var PROJECT_SKILLS_DIR = ".claude/skills";
413
+
414
+ class SkillLoader {
415
+ skills = new Map;
416
+ discoveredPaths = new Set;
417
+ projectRoot;
418
+ constructor(projectRoot) {
419
+ this.projectRoot = projectRoot;
420
+ }
421
+ async discover(options = {}) {
422
+ const {
423
+ directories = [],
424
+ includeUserSkills = true,
425
+ includeProjectSkills = true,
426
+ maxDepth = 2
427
+ } = options;
428
+ const allDirs = [...directories];
429
+ if (includeUserSkills) {
430
+ const userSkillsDir = join(homedir(), USER_SKILLS_DIR);
431
+ if (existsSync(userSkillsDir)) {
432
+ allDirs.push(userSkillsDir);
433
+ }
434
+ }
435
+ if (includeProjectSkills && this.projectRoot) {
436
+ const projectSkillsDir = join(this.projectRoot, PROJECT_SKILLS_DIR);
437
+ if (existsSync(projectSkillsDir)) {
438
+ allDirs.push(projectSkillsDir);
439
+ }
440
+ }
441
+ const skills = [];
442
+ for (const dir of allDirs) {
443
+ const dirSkills = await this.scanDirectory(dir, maxDepth);
444
+ skills.push(...dirSkills);
445
+ }
446
+ return skills;
447
+ }
448
+ async load(skillId) {
449
+ if (this.skills.has(skillId)) {
450
+ return this.skills.get(skillId);
451
+ }
452
+ for (const path of this.discoveredPaths) {
453
+ const skill = await this.parseSkillFile(path);
454
+ if (skill && skill.id === skillId) {
455
+ this.skills.set(skill.id, skill);
456
+ return skill;
457
+ }
458
+ }
459
+ return;
460
+ }
461
+ async search(query) {
462
+ if (!query) {
463
+ return this.getAll();
464
+ }
465
+ const lowerQuery = query.toLowerCase();
466
+ return this.getAll().filter((skill) => {
467
+ if (skill.name.toLowerCase().includes(lowerQuery)) {
468
+ return true;
469
+ }
470
+ if (skill.description.toLowerCase().includes(lowerQuery)) {
471
+ return true;
472
+ }
473
+ if (skill.id.toLowerCase().includes(lowerQuery)) {
474
+ return true;
475
+ }
476
+ if (skill.metadata?.tags?.some((tag) => tag.toLowerCase().includes(lowerQuery))) {
477
+ return true;
478
+ }
479
+ if (skill.triggers?.some((trigger) => trigger.toLowerCase().includes(lowerQuery))) {
480
+ return true;
481
+ }
482
+ return false;
483
+ });
484
+ }
485
+ register(skill) {
486
+ this.skills.set(skill.id, skill);
487
+ }
488
+ unregister(skillId) {
489
+ this.skills.delete(skillId);
490
+ }
491
+ getAll() {
492
+ return Array.from(this.skills.values());
493
+ }
494
+ async checkActivation(message, context) {
495
+ const lowerMessage = message.toLowerCase();
496
+ const matchingSkills = [];
497
+ let matchedTrigger;
498
+ for (const skill of this.skills.values()) {
499
+ if (skill.triggers) {
500
+ for (const trigger of skill.triggers) {
501
+ if (this.matchesTrigger(lowerMessage, trigger)) {
502
+ matchingSkills.push(skill);
503
+ matchedTrigger = trigger;
504
+ break;
505
+ }
506
+ }
507
+ }
508
+ }
509
+ if (matchingSkills.length === 0) {
510
+ return {
511
+ shouldActivate: false,
512
+ skills: []
513
+ };
514
+ }
515
+ return {
516
+ shouldActivate: true,
517
+ skills: matchingSkills,
518
+ systemPromptAddition: this.getSystemPromptContent(matchingSkills.map((s) => s.id))
519
+ };
520
+ }
521
+ getSystemPromptContent(skillIds) {
522
+ const parts = [];
523
+ for (const skillId of skillIds) {
524
+ const skill = this.skills.get(skillId);
525
+ if (skill) {
526
+ parts.push(`## Skill: ${skill.name}`);
527
+ parts.push(skill.content);
528
+ }
529
+ }
530
+ return parts.join(`
531
+
532
+ `);
533
+ }
534
+ async refresh() {
535
+ this.skills.clear();
536
+ await this.discover();
537
+ }
538
+ clear() {
539
+ this.skills.clear();
540
+ this.discoveredPaths.clear();
541
+ }
542
+ async scanDirectory(dir, maxDepth, currentDepth = 0) {
543
+ if (currentDepth > maxDepth) {
544
+ return [];
545
+ }
546
+ if (!existsSync(dir)) {
547
+ return [];
548
+ }
549
+ const skills = [];
550
+ try {
551
+ const entries = readdirSync(dir);
552
+ for (const entry of entries) {
553
+ const fullPath = join(dir, entry);
554
+ const stat = statSync(fullPath);
555
+ if (stat.isDirectory()) {
556
+ const skillFile = join(fullPath, SKILL_FILE_NAME);
557
+ if (existsSync(skillFile)) {
558
+ if (!this.discoveredPaths.has(skillFile)) {
559
+ const skill = await this.parseSkillFile(skillFile);
560
+ if (skill) {
561
+ this.skills.set(skill.id, skill);
562
+ this.discoveredPaths.add(skillFile);
563
+ skills.push(skill);
564
+ }
565
+ }
566
+ }
567
+ const subSkills = await this.scanDirectory(fullPath, maxDepth, currentDepth + 1);
568
+ skills.push(...subSkills);
569
+ } else if (entry === SKILL_FILE_NAME) {
570
+ if (!this.discoveredPaths.has(fullPath)) {
571
+ const skill = await this.parseSkillFile(fullPath);
572
+ if (skill) {
573
+ this.skills.set(skill.id, skill);
574
+ this.discoveredPaths.add(fullPath);
575
+ skills.push(skill);
576
+ }
577
+ }
578
+ }
579
+ }
580
+ } catch {}
581
+ return skills;
582
+ }
583
+ async parseSkillFile(filePath) {
584
+ try {
585
+ const content = await readFile(filePath, "utf-8");
586
+ const { data: frontmatter, content: body } = parseFrontmatter(content);
587
+ const dirName = basename(dirname(filePath));
588
+ const id = frontmatter.id || dirName;
589
+ const metadata = {
590
+ version: frontmatter.version,
591
+ author: frontmatter.author,
592
+ tags: frontmatter.tags,
593
+ dependencies: frontmatter.dependencies,
594
+ triggers: frontmatter.triggers,
595
+ tools: frontmatter.tools
596
+ };
597
+ const skill = {
598
+ id,
599
+ name: frontmatter.name || id,
600
+ description: frontmatter.description || "",
601
+ content: body,
602
+ triggers: frontmatter.triggers,
603
+ tools: frontmatter.tools,
604
+ metadata,
605
+ filePath
606
+ };
607
+ return skill;
608
+ } catch {
609
+ return;
610
+ }
611
+ }
612
+ matchesTrigger(message, trigger) {
613
+ const lowerTrigger = trigger.toLowerCase();
614
+ if (message.includes(lowerTrigger)) {
615
+ return true;
616
+ }
617
+ if (trigger.startsWith("/") && trigger.endsWith("/")) {
618
+ try {
619
+ const regex = new RegExp(trigger.slice(1, -1), "i");
620
+ return regex.test(message);
621
+ } catch {
622
+ return false;
623
+ }
624
+ }
625
+ if (!lowerTrigger.includes(" ")) {
626
+ const wordBoundary = new RegExp(`\\b${lowerTrigger}\\b`, "i");
627
+ return wordBoundary.test(message);
628
+ }
629
+ return false;
630
+ }
631
+ }
632
+ var defaultSkillLoader = new SkillLoader;
633
+
634
+ // src/tools/skill.ts
635
+ import { existsSync as existsSync2 } from "node:fs";
636
+ import { join as join2 } from "node:path";
637
+ import { homedir as homedir2 } from "node:os";
638
+ var DEFAULT_USER_SKILLS_PATH = join2(homedir2(), ".claude/skills");
639
+ var skillToolSchema = {
640
+ type: "object",
641
+ properties: {
642
+ action: {
643
+ type: "string",
644
+ enum: ["list", "invoke"],
645
+ description: "Action to perform: 'list' to show available skills, 'invoke' to use a skill"
646
+ },
647
+ skill_name: {
648
+ type: "string",
649
+ description: "Name or ID of the skill to invoke (required when action is 'invoke')"
650
+ },
651
+ query: {
652
+ type: "string",
653
+ description: "Optional search query to filter skills (used with 'list' action)"
654
+ }
655
+ },
656
+ required: ["action"]
657
+ };
658
+ function createSkillTool(config = {}) {
659
+ const loader = new SkillLoader(config.cwd);
660
+ let initialized = false;
661
+ const resolvePaths = () => {
662
+ const paths = [];
663
+ if (config.settingSources && config.settingSources.length > 0) {
664
+ for (const source of config.settingSources) {
665
+ const resolvedPath = source.startsWith("~") ? join2(homedir2(), source.slice(1)) : source;
666
+ if (existsSync2(resolvedPath)) {
667
+ paths.push(resolvedPath);
668
+ }
669
+ }
670
+ } else {
671
+ if (existsSync2(DEFAULT_USER_SKILLS_PATH)) {
672
+ paths.push(DEFAULT_USER_SKILLS_PATH);
673
+ }
674
+ }
675
+ return paths;
676
+ };
677
+ const initialize = async () => {
678
+ if (initialized)
679
+ return;
680
+ const paths = resolvePaths();
681
+ await loader.discover({
682
+ directories: paths,
683
+ includeUserSkills: false,
684
+ includeProjectSkills: false,
685
+ maxDepth: 3
686
+ });
687
+ initialized = true;
688
+ };
689
+ return {
690
+ name: "Skill",
691
+ description: `Discover and use specialized skills that extend Claude's capabilities.
692
+
693
+ Actions:
694
+ - "list": Show available skills with their descriptions and triggers
695
+ - "invoke": Activate a skill by name to get specialized instructions
696
+
697
+ When you invoke a skill, you will receive detailed instructions and guidelines that you should follow for the current task.
698
+
699
+ Skills are discovered from configured directories and provide domain-specific expertise.`,
700
+ inputSchema: skillToolSchema,
701
+ execute: async (rawInput, _context) => {
702
+ await initialize();
703
+ const input = rawInput;
704
+ const { action, skill_name, query } = input;
705
+ if (action === "list") {
706
+ return listSkills(loader, query);
707
+ }
708
+ if (action === "invoke") {
709
+ if (!skill_name) {
710
+ return {
711
+ content: "Error: skill_name is required when action is 'invoke'",
712
+ isError: true
713
+ };
714
+ }
715
+ return invokeSkill(loader, skill_name);
716
+ }
717
+ return {
718
+ content: `Error: Unknown action "${action}". Use "list" or "invoke".`,
719
+ isError: true
720
+ };
721
+ }
722
+ };
723
+ }
724
+ async function listSkills(loader, query) {
725
+ const skills = query ? await loader.search(query) : loader.getAll();
726
+ if (skills.length === 0) {
727
+ return {
728
+ content: query ? `No skills found matching "${query}".` : "No skills available. Skills are loaded from configured settingSources directories."
729
+ };
730
+ }
731
+ const skillList = skills.map((skill) => {
732
+ const triggers = skill.triggers?.slice(0, 3).join(", ") || "none";
733
+ return `- **${skill.name}** (${skill.id})
734
+ Description: ${skill.description || "No description"}
735
+ Triggers: ${triggers}`;
736
+ });
737
+ return {
738
+ content: `# Available Skills
739
+
740
+ ${skillList.join(`
741
+
742
+ `)}
743
+
744
+ To use a skill, invoke the Skill tool with action="invoke" and skill_name="<skill-id>".`
745
+ };
746
+ }
747
+ async function invokeSkill(loader, skillName) {
748
+ let skill = await loader.load(skillName);
749
+ if (!skill) {
750
+ const matches = await loader.search(skillName);
751
+ if (matches.length === 1) {
752
+ skill = matches[0];
753
+ } else if (matches.length > 1) {
754
+ const names = matches.map((s) => `${s.name} (${s.id})`).join(", ");
755
+ return {
756
+ content: `Multiple skills match "${skillName}": ${names}. Please specify the exact skill ID.`,
757
+ isError: true
758
+ };
759
+ }
760
+ }
761
+ if (!skill) {
762
+ return {
763
+ content: `Skill "${skillName}" not found. Use action="list" to see available skills.`,
764
+ isError: true
765
+ };
766
+ }
767
+ return {
768
+ content: formatSkillContent(skill),
769
+ metadata: {
770
+ skillId: skill.id,
771
+ skillName: skill.name,
772
+ activated: true
773
+ }
774
+ };
775
+ }
776
+ function formatSkillContent(skill) {
777
+ const parts = [];
778
+ parts.push(`# Skill Activated: ${skill.name}`);
779
+ parts.push("");
780
+ if (skill.description) {
781
+ parts.push(`**Description:** ${skill.description}`);
782
+ parts.push("");
783
+ }
784
+ if (skill.tools && skill.tools.length > 0) {
785
+ parts.push(`**Recommended Tools:** ${skill.tools.join(", ")}`);
786
+ parts.push("");
787
+ }
788
+ parts.push("## Instructions");
789
+ parts.push("");
790
+ parts.push(skill.content);
791
+ return parts.join(`
792
+ `);
793
+ }
794
+ var skillTool = createSkillTool();
795
+
796
+ // src/prompt/presets.ts
797
+ var CLI_AGENT_PRESET = `You are FormAgent, a highly capable AI assistant running as a CLI tool.
798
+ You help users with software engineering tasks including coding, debugging, refactoring, and more.
799
+
800
+ ## Tone and Style
801
+
802
+ Be concise, direct, and to the point. Match detail level to task complexity.
803
+ - Brief responses are preferred (generally <4 lines for simple tasks)
804
+ - Minimize preamble/postamble - don't explain what you're about to do or summarize what you did
805
+ - Answer questions directly without unnecessary elaboration
806
+ - Only provide detailed explanations when the task is complex or when asked
807
+
808
+ Examples of appropriate brevity:
809
+ - "What is 2+2?" → "4"
810
+ - "Is this a valid JSON?" → "Yes" or "No, missing closing brace on line 3"
811
+ - "What command lists files?" → "ls"
812
+
813
+ ## Proactiveness
814
+
815
+ Strike a balance between being helpful and not surprising the user:
816
+ - When asked to do something, take appropriate action and follow-up actions
817
+ - When asked HOW to do something, explain first before taking action
818
+ - Don't take unexpected actions without being asked
819
+ - If a task is ambiguous, ask for clarification rather than guessing
820
+
821
+ ## Professional Objectivity
822
+
823
+ Prioritize technical accuracy over validation:
824
+ - Focus on facts and problem-solving
825
+ - Provide direct, objective technical information
826
+ - Disagree respectfully when the user's approach has issues
827
+ - Investigate to find truth rather than confirming beliefs
828
+ - Avoid unnecessary praise or emotional validation
829
+
830
+ ## Task Management
831
+
832
+ Use the TodoWrite tool proactively for complex tasks:
833
+ - Create todos when a task has 3+ steps or touches multiple files
834
+ - Break complex tasks into specific, actionable items
835
+ - Mark todos as in_progress BEFORE starting work (only ONE at a time)
836
+ - Mark todos as completed IMMEDIATELY after finishing (don't batch)
837
+ - Only mark complete when FULLY accomplished (not when tests fail or errors occur)
838
+
839
+ When NOT to use todos:
840
+ - Single, straightforward tasks
841
+ - Trivial operations that take <3 steps
842
+ - Purely informational requests
843
+
844
+ ## Tool Usage
845
+
846
+ Use the right tool for each task:
847
+ - Read: Read files (NOT cat/head/tail)
848
+ - Write: Create new files
849
+ - Edit: Modify existing files (NOT sed/awk)
850
+ - Glob: Find files by pattern (NOT find/ls)
851
+ - Grep: Search file contents (NOT grep/rg command)
852
+ - Bash: Execute shell commands (for git, npm, build tools, etc.)
853
+ - WebFetch: Retrieve web content
854
+ - Skill: Invoke specialized skills for domain-specific tasks
855
+
856
+ Best practices:
857
+ - Always Read a file before editing it
858
+ - Verify edits by reading the file after changes
859
+ - Use specialized tools instead of Bash when available
860
+ - Batch independent operations in parallel when possible
861
+
862
+ ## Code Quality Standards
863
+
864
+ - Follow existing project conventions and patterns
865
+ - Write clean, readable, well-documented code
866
+ - Consider edge cases and error handling
867
+ - Test changes when appropriate
868
+ - Don't over-engineer - only add what's asked for
869
+
870
+ ## Git Workflow (when using Bash for git)
871
+
872
+ Safety rules:
873
+ - NEVER update git config
874
+ - NEVER run destructive commands (push --force, hard reset) unless explicitly asked
875
+ - NEVER skip hooks (--no-verify) unless explicitly asked
876
+ - NEVER force push to main/master - warn the user if requested
877
+ - Only commit when explicitly asked
878
+
879
+ When committing:
880
+ - Run git status and git diff first to understand changes
881
+ - Write concise commit messages focused on "why" not "what"
882
+ - Use conventional commit format when the project uses it
883
+
884
+ ## Code References
885
+
886
+ When referencing code locations, use the format: \`file_path:line_number\`
887
+ Example: "The error handler is in src/utils/error.ts:42"
888
+
889
+ ## Error Handling
890
+
891
+ When something fails:
892
+ - Investigate the root cause, don't just retry
893
+ - Never disable/skip tests to make things pass
894
+ - Provide actionable suggestions for fixing issues
895
+ - If blocked, explain what's needed to proceed
896
+
897
+ ## Security
898
+
899
+ - Assist with defensive security tasks only
900
+ - Refuse to create, modify, or improve potentially malicious code
901
+ - Don't generate or guess URLs unless confident they're for programming help
902
+ - Warn about potential security issues in code
903
+ `;
904
+ var SDK_DEFAULT_PRESET = `You are FormAgent, a helpful AI assistant for software development tasks.
905
+
906
+ ## Core Behavior
907
+
908
+ - Be concise and direct in responses
909
+ - Match detail level to task complexity
910
+ - Use available tools to accomplish tasks efficiently
911
+ - Follow existing code patterns and conventions
912
+
913
+ ## Response Style
914
+
915
+ - Brief responses preferred for simple tasks
916
+ - Avoid unnecessary preamble or postamble
917
+ - Only explain in detail when the task is complex or when asked
918
+ - Focus on actionable information
919
+
920
+ ## Tool Usage
921
+
922
+ Use the right tool for each task:
923
+ - Read: Read file contents
924
+ - Write: Create new files
925
+ - Edit: Modify existing files
926
+ - Glob: Find files by pattern
927
+ - Grep: Search file contents
928
+
929
+ Best practices:
930
+ - Read a file before editing it
931
+ - Verify changes after editing
932
+ - Use specialized tools when available
933
+
934
+ ## Code Quality
935
+
936
+ - Follow existing project conventions
937
+ - Write clean, readable code
938
+ - Consider error handling
939
+ - Don't over-engineer - only add what's asked for
940
+
941
+ ## Task Management
942
+
943
+ For complex multi-step tasks:
944
+ - Use TodoWrite to track progress
945
+ - Break tasks into specific items
946
+ - Mark items complete as you finish them
947
+
948
+ ## Professional Objectivity
949
+
950
+ - Prioritize technical accuracy
951
+ - Provide direct, objective information
952
+ - Point out issues respectfully when needed
953
+ `;
954
+ var MINIMAL_PRESET = `You are a helpful AI assistant. Use available tools to accomplish tasks efficiently.`;
955
+ function generateEnvContext(options) {
956
+ const {
957
+ cwd = process.cwd(),
958
+ isGitRepo = false,
959
+ platform = process.platform,
960
+ osVersion,
961
+ date = new Date,
962
+ shell
963
+ } = options;
964
+ const lines = [
965
+ "<env>",
966
+ `Working directory: ${cwd}`,
967
+ `Is directory a git repo: ${isGitRepo ? "Yes" : "No"}`,
968
+ `Platform: ${platform}`
969
+ ];
970
+ if (osVersion) {
971
+ lines.push(`OS Version: ${osVersion}`);
972
+ }
973
+ if (shell) {
974
+ lines.push(`Shell: ${shell}`);
975
+ }
976
+ lines.push(`Today's date: ${date.toISOString().split("T")[0]}`);
977
+ lines.push("</env>");
978
+ return lines.join(`
979
+ `);
980
+ }
981
+ var BUILT_IN_PRESETS = {
982
+ claude_code: CLI_AGENT_PRESET,
983
+ default: SDK_DEFAULT_PRESET,
984
+ minimal: MINIMAL_PRESET
985
+ };
986
+ function getBuiltInPreset(name) {
987
+ return BUILT_IN_PRESETS[name];
988
+ }
989
+
990
+ // src/prompt/builder.ts
991
+ class SystemPromptBuilderImpl {
992
+ customPresets = new Map;
993
+ async build(config, context) {
994
+ const parts = [];
995
+ if (config.prepend) {
996
+ parts.push(config.prepend.trim());
997
+ }
998
+ const baseContent = this.getBaseContent(config);
999
+ if (baseContent) {
1000
+ parts.push(baseContent.trim());
1001
+ }
1002
+ const contextContent = this.buildContextContent(context);
1003
+ if (contextContent) {
1004
+ parts.push(contextContent.trim());
1005
+ }
1006
+ if (config.append) {
1007
+ parts.push(config.append.trim());
1008
+ }
1009
+ return parts.filter(Boolean).join(`
1010
+
1011
+ `);
1012
+ }
1013
+ getPreset(preset) {
1014
+ if (preset === "custom") {
1015
+ return "";
1016
+ }
1017
+ const customPreset = this.customPresets.get(preset);
1018
+ if (customPreset) {
1019
+ return customPreset;
1020
+ }
1021
+ return getBuiltInPreset(preset);
1022
+ }
1023
+ registerPreset(name, content) {
1024
+ this.customPresets.set(name, content);
1025
+ }
1026
+ listPresets() {
1027
+ const presets = [];
1028
+ for (const [name, content] of Object.entries(BUILT_IN_PRESETS)) {
1029
+ presets.push({
1030
+ name,
1031
+ description: this.getPresetDescription(name),
1032
+ length: content.length,
1033
+ builtIn: true
1034
+ });
1035
+ }
1036
+ for (const [name, content] of this.customPresets.entries()) {
1037
+ presets.push({
1038
+ name,
1039
+ description: `Custom preset: ${name}`,
1040
+ length: content.length,
1041
+ builtIn: false
1042
+ });
1043
+ }
1044
+ return presets;
1045
+ }
1046
+ getBaseContent(config) {
1047
+ if (config.custom) {
1048
+ return config.custom;
1049
+ }
1050
+ if (config.preset) {
1051
+ return this.getPreset(config.preset);
1052
+ }
1053
+ return this.getPreset("default");
1054
+ }
1055
+ buildContextContent(context) {
1056
+ if (!context) {
1057
+ return "";
1058
+ }
1059
+ const parts = [];
1060
+ if (context.environment || context.cwd || context.timestamp) {
1061
+ const envParts = [];
1062
+ if (context.cwd) {
1063
+ envParts.push(`Working directory: ${context.cwd}`);
1064
+ }
1065
+ if (context.environment?.platform) {
1066
+ envParts.push(`Platform: ${context.environment.platform}`);
1067
+ }
1068
+ if (context.environment?.shell) {
1069
+ envParts.push(`Shell: ${context.environment.shell}`);
1070
+ }
1071
+ if (context.timestamp) {
1072
+ const date = new Date(context.timestamp);
1073
+ envParts.push(`Current date: ${date.toISOString().split("T")[0]}`);
1074
+ }
1075
+ if (envParts.length > 0) {
1076
+ parts.push(`## Environment
1077
+ ${envParts.join(`
1078
+ `)}`);
1079
+ }
1080
+ }
1081
+ if (context.toolNames && context.toolNames.length > 0) {
1082
+ parts.push(`## Available Tools
1083
+ ${context.toolNames.join(", ")}`);
1084
+ }
1085
+ if (context.skillNames && context.skillNames.length > 0) {
1086
+ parts.push(`## Available Skills
1087
+ ${context.skillNames.join(", ")}`);
1088
+ }
1089
+ if (context.user?.name) {
1090
+ parts.push(`## User
1091
+ Name: ${context.user.name}`);
1092
+ }
1093
+ return parts.join(`
1094
+
1095
+ `);
1096
+ }
1097
+ getPresetDescription(preset) {
1098
+ switch (preset) {
1099
+ case "claude_code":
1100
+ return "Full-featured Claude Code agent behavior";
1101
+ case "default":
1102
+ return "Balanced preset for general use";
1103
+ case "minimal":
1104
+ return "Minimal instructions for simple tasks";
1105
+ case "custom":
1106
+ return "User-defined custom prompt";
1107
+ default:
1108
+ return `Preset: ${preset}`;
1109
+ }
1110
+ }
1111
+ }
1112
+ var defaultSystemPromptBuilder = new SystemPromptBuilderImpl;
1113
+ // src/prompt/claude-md.ts
1114
+ import { existsSync as existsSync3 } from "node:fs";
1115
+ import { readFile as readFile2 } from "node:fs/promises";
1116
+ import { join as join3, dirname as dirname2 } from "node:path";
1117
+ import { homedir as homedir3 } from "node:os";
1118
+ var CLAUDE_MD_FILENAME = "CLAUDE.md";
1119
+ var USER_CLAUDE_DIR = ".claude";
1120
+
1121
+ class ClaudeMdLoaderImpl {
1122
+ async loadProjectClaudeMd(cwd) {
1123
+ const filePath = await this.findProjectClaudeMd(cwd);
1124
+ if (!filePath) {
1125
+ return;
1126
+ }
1127
+ return this.loadFile(filePath, "project");
1128
+ }
1129
+ async loadUserClaudeMd() {
1130
+ const filePath = join3(homedir3(), USER_CLAUDE_DIR, CLAUDE_MD_FILENAME);
1131
+ if (!existsSync3(filePath)) {
1132
+ return;
1133
+ }
1134
+ return this.loadFile(filePath, "user");
1135
+ }
1136
+ async loadAll(config, cwd) {
1137
+ const contents = [];
1138
+ if (config.user !== false) {
1139
+ const userMd = await this.loadUserClaudeMd();
1140
+ if (userMd) {
1141
+ contents.push(userMd);
1142
+ }
1143
+ }
1144
+ if (config.project !== false && cwd) {
1145
+ const projectMd = await this.loadProjectClaudeMd(cwd);
1146
+ if (projectMd) {
1147
+ contents.push(projectMd);
1148
+ }
1149
+ }
1150
+ if (config.additionalPaths) {
1151
+ for (const path of config.additionalPaths) {
1152
+ if (existsSync3(path)) {
1153
+ const content = await this.loadFile(path, "project");
1154
+ if (content) {
1155
+ contents.push(content);
1156
+ }
1157
+ }
1158
+ }
1159
+ }
1160
+ return contents;
1161
+ }
1162
+ merge(contents) {
1163
+ if (contents.length === 0) {
1164
+ return "";
1165
+ }
1166
+ const parts = [];
1167
+ for (const content of contents) {
1168
+ if (content.raw.trim()) {
1169
+ parts.push(`<!-- Source: ${content.filePath} (${content.type}) -->`);
1170
+ parts.push(content.raw.trim());
1171
+ }
1172
+ }
1173
+ return parts.join(`
1174
+
1175
+ `);
1176
+ }
1177
+ async findProjectClaudeMd(startDir) {
1178
+ let currentDir = startDir;
1179
+ while (currentDir !== "/") {
1180
+ const claudeMdPath = join3(currentDir, CLAUDE_MD_FILENAME);
1181
+ if (existsSync3(claudeMdPath)) {
1182
+ return claudeMdPath;
1183
+ }
1184
+ const gitPath = join3(currentDir, ".git");
1185
+ if (existsSync3(gitPath)) {
1186
+ break;
1187
+ }
1188
+ const parentDir = dirname2(currentDir);
1189
+ if (parentDir === currentDir) {
1190
+ break;
1191
+ }
1192
+ currentDir = parentDir;
1193
+ }
1194
+ return;
1195
+ }
1196
+ async loadFile(filePath, type) {
1197
+ try {
1198
+ const raw = await readFile2(filePath, "utf-8");
1199
+ const sections = this.parseSections(raw);
1200
+ return {
1201
+ raw,
1202
+ sections,
1203
+ filePath,
1204
+ type
1205
+ };
1206
+ } catch {
1207
+ return;
1208
+ }
1209
+ }
1210
+ parseSections(content) {
1211
+ const sections = [];
1212
+ const lines = content.split(`
1213
+ `);
1214
+ let currentSection = null;
1215
+ let contentLines = [];
1216
+ for (const line of lines) {
1217
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
1218
+ if (headingMatch) {
1219
+ if (currentSection) {
1220
+ currentSection.content = contentLines.join(`
1221
+ `).trim();
1222
+ sections.push(currentSection);
1223
+ }
1224
+ currentSection = {
1225
+ heading: headingMatch[2],
1226
+ level: headingMatch[1].length,
1227
+ content: ""
1228
+ };
1229
+ contentLines = [];
1230
+ } else if (currentSection) {
1231
+ contentLines.push(line);
1232
+ }
1233
+ }
1234
+ if (currentSection) {
1235
+ currentSection.content = contentLines.join(`
1236
+ `).trim();
1237
+ sections.push(currentSection);
1238
+ }
1239
+ return sections;
1240
+ }
1241
+ }
1242
+ var defaultClaudeMdLoader = new ClaudeMdLoaderImpl;
1243
+ // src/session/session.ts
1244
+ class SessionImpl {
1245
+ id;
1246
+ config;
1247
+ _state;
1248
+ provider;
1249
+ tools;
1250
+ emitter;
1251
+ pendingMessage = null;
1252
+ isReceiving = false;
1253
+ abortController = null;
1254
+ closed = false;
1255
+ hooksManager = null;
1256
+ maxTurns;
1257
+ constructor(id, config, provider, state) {
1258
+ this.id = id;
1259
+ this.config = config;
1260
+ this.provider = provider;
1261
+ this.tools = new Map;
1262
+ this.emitter = new TypedEventEmitter;
1263
+ this._state = {
1264
+ id,
1265
+ messages: state?.messages ?? [],
1266
+ usage: state?.usage ?? {
1267
+ input_tokens: 0,
1268
+ output_tokens: 0
1269
+ },
1270
+ metadata: state?.metadata ?? {},
1271
+ createdAt: state?.createdAt ?? Date.now(),
1272
+ updatedAt: Date.now(),
1273
+ parentId: state?.parentId
1274
+ };
1275
+ this.maxTurns = config.maxTurns;
1276
+ if (config.tools) {
1277
+ for (const tool of config.tools) {
1278
+ this.tools.set(tool.name, tool);
1279
+ }
1280
+ }
1281
+ if (config.settingSources && config.settingSources.length > 0) {
1282
+ const skillTool2 = createSkillTool({
1283
+ settingSources: config.settingSources,
1284
+ cwd: config.cwd
1285
+ });
1286
+ this.tools.set(skillTool2.name, skillTool2);
1287
+ }
1288
+ this.applyAllowedToolsFilter();
1289
+ if (config.hooks) {
1290
+ this.hooksManager = new HooksManager(config.hooks, id, config.cwd ?? process.cwd());
1291
+ }
1292
+ }
1293
+ get state() {
1294
+ return { ...this._state };
1295
+ }
1296
+ async send(message, options) {
1297
+ if (this.closed) {
1298
+ throw new Error("Session is closed");
1299
+ }
1300
+ if (this.isReceiving) {
1301
+ throw new Error("Cannot send while receiving");
1302
+ }
1303
+ const normalizedMessage = typeof message === "string" ? {
1304
+ id: generateMessageId(),
1305
+ role: "user",
1306
+ content: message
1307
+ } : {
1308
+ ...message,
1309
+ id: message.id ?? generateMessageId()
1310
+ };
1311
+ this.pendingMessage = normalizedMessage;
1312
+ this._state.messages.push(normalizedMessage);
1313
+ this._state.updatedAt = Date.now();
1314
+ }
1315
+ async* receive(options) {
1316
+ if (this.closed) {
1317
+ throw new Error("Session is closed");
1318
+ }
1319
+ if (this.isReceiving) {
1320
+ throw new Error("Already receiving");
1321
+ }
1322
+ if (!this.pendingMessage && !options?.continue) {
1323
+ throw new Error("No pending message to process");
1324
+ }
1325
+ if (this.maxTurns !== undefined) {
1326
+ const assistantCount = this._state.messages.filter((m) => m.role === "assistant").length;
1327
+ if (assistantCount >= this.maxTurns) {
1328
+ this.pendingMessage = null;
1329
+ yield {
1330
+ type: "stop",
1331
+ stop_reason: "max_turns",
1332
+ usage: this._state.usage
1333
+ };
1334
+ return;
1335
+ }
1336
+ }
1337
+ this.isReceiving = true;
1338
+ this.abortController = new AbortController;
1339
+ const abortSignal = options?.abortSignal ? this.combineAbortSignals(options.abortSignal, this.abortController.signal) : this.abortController.signal;
1340
+ try {
1341
+ const request = await this.buildRequest();
1342
+ const streamOptions = {
1343
+ onText: (text) => {
1344
+ const event = { type: "text", text };
1345
+ this.emitter.emit("text", event);
1346
+ },
1347
+ onToolUse: (toolUse) => {
1348
+ const event = {
1349
+ type: "tool_use",
1350
+ id: toolUse.id,
1351
+ name: toolUse.name,
1352
+ input: toolUse.input
1353
+ };
1354
+ this.emitter.emit("tool_use", event);
1355
+ }
1356
+ };
1357
+ const stream = await this.provider.stream(request, streamOptions);
1358
+ const content = [];
1359
+ let currentText = "";
1360
+ let currentToolUse = null;
1361
+ let stopReason = "end_turn";
1362
+ let usage = { input_tokens: 0, output_tokens: 0 };
1363
+ for await (const event of stream) {
1364
+ if (abortSignal.aborted) {
1365
+ break;
1366
+ }
1367
+ if (event.type === "content_block_start") {
1368
+ if (event.content_block.type === "text") {
1369
+ currentText = "";
1370
+ } else if (event.content_block.type === "tool_use") {
1371
+ currentToolUse = {
1372
+ id: event.content_block.id,
1373
+ name: event.content_block.name,
1374
+ input: ""
1375
+ };
1376
+ }
1377
+ } else if (event.type === "content_block_delta") {
1378
+ if (event.delta.type === "text_delta" && event.delta.text) {
1379
+ currentText += event.delta.text;
1380
+ yield { type: "text", text: event.delta.text };
1381
+ } else if (event.delta.type === "input_json_delta" && currentToolUse) {
1382
+ currentToolUse.input += event.delta.partial_json || "";
1383
+ }
1384
+ } else if (event.type === "content_block_stop") {
1385
+ if (currentText) {
1386
+ content.push({ type: "text", text: currentText });
1387
+ currentText = "";
1388
+ }
1389
+ if (currentToolUse) {
1390
+ let parsedInput = {};
1391
+ try {
1392
+ parsedInput = JSON.parse(currentToolUse.input || "{}");
1393
+ } catch {}
1394
+ yield {
1395
+ type: "tool_use",
1396
+ id: currentToolUse.id,
1397
+ name: currentToolUse.name,
1398
+ input: parsedInput
1399
+ };
1400
+ content.push({
1401
+ type: "tool_use",
1402
+ id: currentToolUse.id,
1403
+ name: currentToolUse.name,
1404
+ input: parsedInput
1405
+ });
1406
+ currentToolUse = null;
1407
+ }
1408
+ } else if (event.type === "message_delta") {
1409
+ stopReason = event.delta?.stop_reason;
1410
+ if (event.usage?.output_tokens !== undefined) {
1411
+ usage = {
1412
+ ...usage,
1413
+ output_tokens: event.usage.output_tokens
1414
+ };
1415
+ }
1416
+ if (event.usage?.input_tokens !== undefined) {
1417
+ usage = {
1418
+ ...usage,
1419
+ input_tokens: event.usage.input_tokens
1420
+ };
1421
+ }
1422
+ } else if (event.type === "message_start") {
1423
+ if (event.message?.usage?.input_tokens !== undefined) {
1424
+ usage = {
1425
+ ...usage,
1426
+ input_tokens: event.message.usage.input_tokens
1427
+ };
1428
+ }
1429
+ } else if (event.type === "message_stop") {}
1430
+ }
1431
+ if (currentText) {
1432
+ content.push({ type: "text", text: currentText });
1433
+ currentText = "";
1434
+ }
1435
+ if (currentToolUse) {
1436
+ let parsedInput = {};
1437
+ try {
1438
+ parsedInput = JSON.parse(currentToolUse.input || "{}");
1439
+ } catch {}
1440
+ yield {
1441
+ type: "tool_use",
1442
+ id: currentToolUse.id,
1443
+ name: currentToolUse.name,
1444
+ input: parsedInput
1445
+ };
1446
+ content.push({
1447
+ type: "tool_use",
1448
+ id: currentToolUse.id,
1449
+ name: currentToolUse.name,
1450
+ input: parsedInput
1451
+ });
1452
+ currentToolUse = null;
1453
+ }
1454
+ const assistantContent = content.filter((b) => b.type !== "tool_result");
1455
+ const assistantMessage = {
1456
+ id: generateMessageId(),
1457
+ role: "assistant",
1458
+ content: assistantContent,
1459
+ stop_reason: stopReason,
1460
+ usage
1461
+ };
1462
+ this._state.messages.push(assistantMessage);
1463
+ this._state.usage = {
1464
+ input_tokens: this._state.usage.input_tokens + usage.input_tokens,
1465
+ output_tokens: this._state.usage.output_tokens + usage.output_tokens
1466
+ };
1467
+ this._state.updatedAt = Date.now();
1468
+ yield { type: "message", message: assistantMessage };
1469
+ const toolUseBlocks = assistantContent.filter((b) => b.type === "tool_use");
1470
+ if (toolUseBlocks.length > 0) {
1471
+ if (this.maxTurns !== undefined) {
1472
+ const assistantCountNow = this._state.messages.filter((m) => m.role === "assistant").length;
1473
+ if (assistantCountNow >= this.maxTurns) {
1474
+ yield {
1475
+ type: "stop",
1476
+ stop_reason: "max_turns",
1477
+ usage: this._state.usage
1478
+ };
1479
+ this.pendingMessage = null;
1480
+ return;
1481
+ }
1482
+ }
1483
+ const toolResults = [];
1484
+ for (const block of toolUseBlocks) {
1485
+ if (block.type === "tool_use") {
1486
+ const toolResult = await this.executeToolCall(block, abortSignal);
1487
+ yield toolResult;
1488
+ if (toolResult.type === "tool_result") {
1489
+ toolResults.push({
1490
+ type: "tool_result",
1491
+ tool_use_id: block.id,
1492
+ content: typeof toolResult.content === "string" ? toolResult.content : "",
1493
+ is_error: toolResult.is_error
1494
+ });
1495
+ }
1496
+ }
1497
+ }
1498
+ const toolResultMessage = {
1499
+ id: generateMessageId(),
1500
+ role: "user",
1501
+ content: toolResults
1502
+ };
1503
+ this._state.messages.push(toolResultMessage);
1504
+ for await (const event of this.continueConversation(abortSignal)) {
1505
+ yield event;
1506
+ }
1507
+ } else {
1508
+ yield {
1509
+ type: "stop",
1510
+ stop_reason: stopReason,
1511
+ usage: this._state.usage
1512
+ };
1513
+ }
1514
+ this.pendingMessage = null;
1515
+ } catch (error) {
1516
+ const errorEvent = {
1517
+ type: "error",
1518
+ error: error instanceof Error ? error : new Error(String(error))
1519
+ };
1520
+ this.emitter.emit("error", errorEvent);
1521
+ yield errorEvent;
1522
+ } finally {
1523
+ this.isReceiving = false;
1524
+ this.abortController = null;
1525
+ }
1526
+ }
1527
+ getMessages() {
1528
+ return [...this._state.messages];
1529
+ }
1530
+ getUsage() {
1531
+ return { ...this._state.usage };
1532
+ }
1533
+ async close() {
1534
+ if (this.closed) {
1535
+ return;
1536
+ }
1537
+ this.abortController?.abort();
1538
+ this.emitter.removeAllListeners();
1539
+ this.tools.clear();
1540
+ this.pendingMessage = null;
1541
+ this.closed = true;
1542
+ }
1543
+ async[Symbol.asyncDispose]() {
1544
+ await this.close();
1545
+ }
1546
+ async buildRequest() {
1547
+ const messages = this._state.messages.map((msg) => ({
1548
+ role: msg.role,
1549
+ content: msg.content
1550
+ }));
1551
+ const tools = Array.from(this.tools.values());
1552
+ const model = typeof this.config.model === "string" ? this.config.model : this.config.model?.model ?? "claude-sonnet-4-20250514";
1553
+ const maxTokens = typeof this.config.model === "string" ? 4096 : this.config.model?.maxTokens ?? 4096;
1554
+ const systemPrompt = await this.buildSystemPrompt(tools.map((t) => t.name));
1555
+ return {
1556
+ messages,
1557
+ tools,
1558
+ config: {
1559
+ ...(typeof this.config.model === "string" ? {} : this.config.model) ?? {},
1560
+ model,
1561
+ maxTokens
1562
+ },
1563
+ systemPrompt,
1564
+ abortSignal: this.abortController?.signal
1565
+ };
1566
+ }
1567
+ async executeToolCall(block, abortSignal) {
1568
+ let toolInput = block.input;
1569
+ let systemMessage;
1570
+ if (this.hooksManager) {
1571
+ const preResult = await this.hooksManager.runPreToolUse(block.name, block.input, block.id, abortSignal);
1572
+ if (!preResult.continue) {
1573
+ return {
1574
+ type: "tool_result",
1575
+ tool_use_id: block.id,
1576
+ content: preResult.stopReason ?? "Execution stopped by hook",
1577
+ is_error: true,
1578
+ _hookSystemMessage: preResult.systemMessage
1579
+ };
1580
+ }
1581
+ if (!preResult.allowed) {
1582
+ return {
1583
+ type: "tool_result",
1584
+ tool_use_id: block.id,
1585
+ content: preResult.reason ?? `Tool "${block.name}" was denied by hook`,
1586
+ is_error: true,
1587
+ _hookSystemMessage: preResult.systemMessage
1588
+ };
1589
+ }
1590
+ if (preResult.updatedInput) {
1591
+ toolInput = preResult.updatedInput;
1592
+ }
1593
+ systemMessage = preResult.systemMessage;
1594
+ }
1595
+ const tool = this.tools.get(block.name);
1596
+ if (!tool) {
1597
+ return {
1598
+ type: "tool_result",
1599
+ tool_use_id: block.id,
1600
+ content: `Error: Tool "${block.name}" not found`,
1601
+ is_error: true,
1602
+ _hookSystemMessage: systemMessage
1603
+ };
1604
+ }
1605
+ const context = {
1606
+ sessionId: this.id,
1607
+ abortSignal
1608
+ };
1609
+ let result;
1610
+ let toolResponse;
1611
+ try {
1612
+ const toolResult = await tool.execute(toolInput, context);
1613
+ const content = typeof toolResult.content === "string" ? toolResult.content : JSON.stringify(toolResult.content);
1614
+ toolResponse = toolResult;
1615
+ result = {
1616
+ type: "tool_result",
1617
+ tool_use_id: block.id,
1618
+ content,
1619
+ is_error: toolResult.isError
1620
+ };
1621
+ } catch (error) {
1622
+ toolResponse = { error: error instanceof Error ? error.message : String(error) };
1623
+ result = {
1624
+ type: "tool_result",
1625
+ tool_use_id: block.id,
1626
+ content: `Error: ${error instanceof Error ? error.message : String(error)}`,
1627
+ is_error: true
1628
+ };
1629
+ }
1630
+ if (this.hooksManager) {
1631
+ const postResult = await this.hooksManager.runPostToolUse(block.name, toolInput, toolResponse, block.id, abortSignal);
1632
+ if (postResult.systemMessage) {
1633
+ systemMessage = postResult.systemMessage;
1634
+ }
1635
+ if (postResult.additionalContext && result.type === "tool_result") {
1636
+ result = {
1637
+ ...result,
1638
+ content: `${result.content}
1639
+
1640
+ ${postResult.additionalContext}`
1641
+ };
1642
+ }
1643
+ }
1644
+ return {
1645
+ ...result,
1646
+ _hookSystemMessage: systemMessage
1647
+ };
1648
+ }
1649
+ async* continueConversation(abortSignal) {
1650
+ if (this.maxTurns !== undefined) {
1651
+ const assistantCount = this._state.messages.filter((m) => m.role === "assistant").length;
1652
+ if (assistantCount >= this.maxTurns) {
1653
+ yield {
1654
+ type: "stop",
1655
+ stop_reason: "max_turns",
1656
+ usage: this._state.usage
1657
+ };
1658
+ return;
1659
+ }
1660
+ }
1661
+ const request = await this.buildRequest();
1662
+ const stream = await this.provider.stream(request, {});
1663
+ const content = [];
1664
+ let currentText = "";
1665
+ let currentToolUse = null;
1666
+ let stopReason = "end_turn";
1667
+ let usage = { input_tokens: 0, output_tokens: 0 };
1668
+ for await (const event of stream) {
1669
+ if (abortSignal.aborted) {
1670
+ break;
1671
+ }
1672
+ if (event.type === "content_block_start") {
1673
+ if (event.content_block.type === "text") {
1674
+ currentText = "";
1675
+ } else if (event.content_block.type === "tool_use") {
1676
+ currentToolUse = {
1677
+ id: event.content_block.id,
1678
+ name: event.content_block.name,
1679
+ input: ""
1680
+ };
1681
+ }
1682
+ } else if (event.type === "content_block_delta") {
1683
+ if (event.delta.type === "text_delta" && event.delta.text) {
1684
+ currentText += event.delta.text;
1685
+ yield { type: "text", text: event.delta.text };
1686
+ } else if (event.delta.type === "input_json_delta" && currentToolUse) {
1687
+ currentToolUse.input += event.delta.partial_json || "";
1688
+ }
1689
+ } else if (event.type === "content_block_stop") {
1690
+ if (currentText) {
1691
+ content.push({ type: "text", text: currentText });
1692
+ currentText = "";
1693
+ }
1694
+ if (currentToolUse) {
1695
+ let parsedInput = {};
1696
+ try {
1697
+ parsedInput = JSON.parse(currentToolUse.input || "{}");
1698
+ } catch {}
1699
+ yield {
1700
+ type: "tool_use",
1701
+ id: currentToolUse.id,
1702
+ name: currentToolUse.name,
1703
+ input: parsedInput
1704
+ };
1705
+ content.push({
1706
+ type: "tool_use",
1707
+ id: currentToolUse.id,
1708
+ name: currentToolUse.name,
1709
+ input: parsedInput
1710
+ });
1711
+ currentToolUse = null;
1712
+ }
1713
+ } else if (event.type === "message_delta") {
1714
+ stopReason = event.delta?.stop_reason;
1715
+ if (event.usage?.output_tokens !== undefined) {
1716
+ usage = { ...usage, output_tokens: event.usage.output_tokens };
1717
+ }
1718
+ if (event.usage?.input_tokens !== undefined) {
1719
+ usage = { ...usage, input_tokens: event.usage.input_tokens };
1720
+ }
1721
+ } else if (event.type === "message_start") {
1722
+ if (event.message?.usage?.input_tokens !== undefined) {
1723
+ usage = { ...usage, input_tokens: event.message.usage.input_tokens };
1724
+ }
1725
+ }
1726
+ }
1727
+ if (currentText) {
1728
+ content.push({ type: "text", text: currentText });
1729
+ currentText = "";
1730
+ }
1731
+ if (currentToolUse) {
1732
+ let parsedInput = {};
1733
+ try {
1734
+ parsedInput = JSON.parse(currentToolUse.input || "{}");
1735
+ } catch {}
1736
+ yield {
1737
+ type: "tool_use",
1738
+ id: currentToolUse.id,
1739
+ name: currentToolUse.name,
1740
+ input: parsedInput
1741
+ };
1742
+ content.push({
1743
+ type: "tool_use",
1744
+ id: currentToolUse.id,
1745
+ name: currentToolUse.name,
1746
+ input: parsedInput
1747
+ });
1748
+ currentToolUse = null;
1749
+ }
1750
+ const assistantMessage = {
1751
+ id: generateMessageId(),
1752
+ role: "assistant",
1753
+ content,
1754
+ stop_reason: stopReason,
1755
+ usage
1756
+ };
1757
+ this._state.messages.push(assistantMessage);
1758
+ this._state.usage = {
1759
+ input_tokens: this._state.usage.input_tokens + usage.input_tokens,
1760
+ output_tokens: this._state.usage.output_tokens + usage.output_tokens
1761
+ };
1762
+ this._state.updatedAt = Date.now();
1763
+ yield { type: "message", message: assistantMessage };
1764
+ const toolUseBlocks = content.filter((b) => b.type === "tool_use");
1765
+ if (toolUseBlocks.length > 0) {
1766
+ const toolResults = [];
1767
+ for (const block of toolUseBlocks) {
1768
+ if (block.type === "tool_use") {
1769
+ const toolResult = await this.executeToolCall(block, abortSignal);
1770
+ yield toolResult;
1771
+ if (toolResult.type === "tool_result") {
1772
+ toolResults.push({
1773
+ type: "tool_result",
1774
+ tool_use_id: block.id,
1775
+ content: typeof toolResult.content === "string" ? toolResult.content : "",
1776
+ is_error: toolResult.is_error
1777
+ });
1778
+ }
1779
+ }
1780
+ }
1781
+ const toolResultMessage = {
1782
+ id: generateMessageId(),
1783
+ role: "user",
1784
+ content: toolResults
1785
+ };
1786
+ this._state.messages.push(toolResultMessage);
1787
+ for await (const event of this.continueConversation(abortSignal)) {
1788
+ yield event;
1789
+ }
1790
+ } else {
1791
+ yield {
1792
+ type: "stop",
1793
+ stop_reason: stopReason,
1794
+ usage: this._state.usage
1795
+ };
1796
+ }
1797
+ }
1798
+ combineAbortSignals(...signals) {
1799
+ const controller = new AbortController;
1800
+ for (const signal of signals) {
1801
+ if (signal.aborted) {
1802
+ controller.abort();
1803
+ break;
1804
+ }
1805
+ signal.addEventListener("abort", () => controller.abort(), { once: true });
1806
+ }
1807
+ return controller.signal;
1808
+ }
1809
+ applyAllowedToolsFilter() {
1810
+ const spec = this.config.allowedTools;
1811
+ if (!spec)
1812
+ return;
1813
+ const patternsFromList = (list) => (list ?? []).map((p) => p.trim()).filter(Boolean);
1814
+ const wildcardToRegex = (pattern) => {
1815
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
1816
+ return new RegExp(`^${escaped}$`);
1817
+ };
1818
+ const matchesAny = (name, patterns) => patterns.some((p) => wildcardToRegex(p).test(name));
1819
+ let allow = null;
1820
+ let deny = [];
1821
+ if (Array.isArray(spec)) {
1822
+ allow = patternsFromList(spec);
1823
+ } else {
1824
+ allow = spec.allow ? patternsFromList(spec.allow) : null;
1825
+ deny = patternsFromList(spec.deny);
1826
+ }
1827
+ for (const name of Array.from(this.tools.keys())) {
1828
+ if (deny.length && matchesAny(name, deny)) {
1829
+ this.tools.delete(name);
1830
+ continue;
1831
+ }
1832
+ if (allow && allow.length && !matchesAny(name, allow)) {
1833
+ this.tools.delete(name);
1834
+ }
1835
+ }
1836
+ }
1837
+ async buildSystemPrompt(toolNames) {
1838
+ if (typeof this.config.systemPrompt === "string") {
1839
+ return this.config.systemPrompt;
1840
+ }
1841
+ if (!this.config.systemPrompt) {
1842
+ return;
1843
+ }
1844
+ const cwd = this.config.cwd ?? process.cwd();
1845
+ const built = await defaultSystemPromptBuilder.build(this.config.systemPrompt, {
1846
+ cwd,
1847
+ toolNames,
1848
+ timestamp: Date.now(),
1849
+ environment: {
1850
+ platform: process.platform,
1851
+ shell: process.env.SHELL
1852
+ }
1853
+ });
1854
+ if (!this.config.systemPrompt.settingSources) {
1855
+ return built || undefined;
1856
+ }
1857
+ const contents = await defaultClaudeMdLoader.loadAll(this.config.systemPrompt.settingSources, cwd);
1858
+ const merged = defaultClaudeMdLoader.merge(contents).trim();
1859
+ const parts = [built?.trim(), merged].filter((p) => p && p.length > 0);
1860
+ return parts.length ? parts.join(`
1861
+
1862
+ `) : undefined;
1863
+ }
1864
+ }
1865
+ function createSessionImpl(config, provider, state) {
1866
+ const id = state?.id ?? generateSessionId();
1867
+ return new SessionImpl(id, config, provider, state);
1868
+ }
1869
+
1870
+ // src/session/storage.ts
1871
+ class MemorySessionStorage {
1872
+ sessions = new Map;
1873
+ async save(state) {
1874
+ const cloned = JSON.parse(JSON.stringify(state));
1875
+ this.sessions.set(state.id, cloned);
1876
+ }
1877
+ async load(sessionId) {
1878
+ const state = this.sessions.get(sessionId);
1879
+ if (!state) {
1880
+ return;
1881
+ }
1882
+ return JSON.parse(JSON.stringify(state));
1883
+ }
1884
+ async delete(sessionId) {
1885
+ this.sessions.delete(sessionId);
1886
+ }
1887
+ async list() {
1888
+ return Array.from(this.sessions.keys());
1889
+ }
1890
+ clear() {
1891
+ this.sessions.clear();
1892
+ }
1893
+ size() {
1894
+ return this.sessions.size;
1895
+ }
1896
+ }
1897
+
1898
+ // src/session/manager.ts
1899
+ class SessionManagerImpl {
1900
+ sessions = new Map;
1901
+ storage;
1902
+ provider;
1903
+ defaultConfig;
1904
+ constructor(options) {
1905
+ this.storage = options.storage ?? new MemorySessionStorage;
1906
+ this.provider = options.provider;
1907
+ this.defaultConfig = options.defaultConfig ?? {};
1908
+ }
1909
+ async create(config) {
1910
+ const mergedConfig = {
1911
+ ...this.defaultConfig,
1912
+ ...config
1913
+ };
1914
+ if (config?.resume) {
1915
+ return this.resume(config.resume, mergedConfig);
1916
+ }
1917
+ if (config?.fork) {
1918
+ return this.fork(config.fork, mergedConfig);
1919
+ }
1920
+ const session = createSessionImpl(mergedConfig, this.provider);
1921
+ this.sessions.set(session.id, session);
1922
+ await this.storage.save(session.state);
1923
+ return session;
1924
+ }
1925
+ async resume(sessionId, config) {
1926
+ const existingSession = this.sessions.get(sessionId);
1927
+ if (existingSession) {
1928
+ return existingSession;
1929
+ }
1930
+ const state = await this.storage.load(sessionId);
1931
+ if (!state) {
1932
+ throw new Error(`Session not found: ${sessionId}`);
1933
+ }
1934
+ const mergedConfig = {
1935
+ ...this.defaultConfig,
1936
+ ...config
1937
+ };
1938
+ const session = createSessionImpl(mergedConfig, this.provider, state);
1939
+ this.sessions.set(session.id, session);
1940
+ return session;
1941
+ }
1942
+ async fork(sessionId, config) {
1943
+ const originalState = await this.storage.load(sessionId);
1944
+ if (!originalState) {
1945
+ throw new Error(`Session not found: ${sessionId}`);
1946
+ }
1947
+ const mergedConfig = {
1948
+ ...this.defaultConfig,
1949
+ ...config
1950
+ };
1951
+ const newId = generateSessionId();
1952
+ const forkedState = {
1953
+ ...originalState,
1954
+ id: newId,
1955
+ parentId: sessionId,
1956
+ createdAt: Date.now(),
1957
+ updatedAt: Date.now(),
1958
+ messages: JSON.parse(JSON.stringify(originalState.messages))
1959
+ };
1960
+ const session = createSessionImpl(mergedConfig, this.provider, forkedState);
1961
+ this.sessions.set(session.id, session);
1962
+ await this.storage.save(session.state);
1963
+ return session;
1964
+ }
1965
+ get(sessionId) {
1966
+ return this.sessions.get(sessionId);
1967
+ }
1968
+ list() {
1969
+ return Array.from(this.sessions.keys());
1970
+ }
1971
+ async close(sessionId) {
1972
+ const session = this.sessions.get(sessionId);
1973
+ if (!session) {
1974
+ return;
1975
+ }
1976
+ await this.storage.save(session.state);
1977
+ await session.close();
1978
+ this.sessions.delete(sessionId);
1979
+ }
1980
+ async closeAll() {
1981
+ const sessionIds = Array.from(this.sessions.keys());
1982
+ await Promise.all(sessionIds.map((id) => this.close(id)));
1983
+ }
1984
+ getStorage() {
1985
+ return this.storage;
1986
+ }
1987
+ getProvider() {
1988
+ return this.provider;
1989
+ }
1990
+ }
1991
+
1992
+ // src/llm/anthropic.ts
1993
+ class AnthropicProvider {
1994
+ id = "anthropic";
1995
+ name = "Anthropic";
1996
+ supportedModels = [
1997
+ /^claude-sonnet-4/,
1998
+ /^claude-opus-4/,
1999
+ /^claude-3/,
2000
+ /^claude-2/,
2001
+ /^claude-instant/
2002
+ ];
2003
+ config;
2004
+ constructor(config = {}) {
2005
+ const apiKey = config.apiKey ?? process.env.ANTHROPIC_API_KEY;
2006
+ if (!apiKey) {
2007
+ throw new Error("Anthropic API key is required. Set ANTHROPIC_API_KEY environment variable or pass apiKey in config.");
2008
+ }
2009
+ this.config = {
2010
+ apiKey,
2011
+ baseUrl: config.baseUrl ?? process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com",
2012
+ apiVersion: config.apiVersion ?? "2023-06-01",
2013
+ defaultMaxTokens: config.defaultMaxTokens ?? 4096
2014
+ };
2015
+ }
2016
+ supportsModel(model) {
2017
+ return this.supportedModels.some((pattern) => pattern.test(model));
2018
+ }
2019
+ async complete(request) {
2020
+ const anthropicRequest = this.buildRequest(request, false);
2021
+ const response = await fetch(`${this.config.baseUrl}/v1/messages`, {
2022
+ method: "POST",
2023
+ headers: this.getHeaders(),
2024
+ body: JSON.stringify(anthropicRequest),
2025
+ signal: request.abortSignal
2026
+ });
2027
+ if (!response.ok) {
2028
+ const error = await response.text();
2029
+ throw new Error(`Anthropic API error: ${response.status} ${error}`);
2030
+ }
2031
+ const data = await response.json();
2032
+ return this.convertResponse(data);
2033
+ }
2034
+ async stream(request, options) {
2035
+ const anthropicRequest = this.buildRequest(request, true);
2036
+ const response = await fetch(`${this.config.baseUrl}/v1/messages`, {
2037
+ method: "POST",
2038
+ headers: this.getHeaders(),
2039
+ body: JSON.stringify(anthropicRequest),
2040
+ signal: request.abortSignal
2041
+ });
2042
+ if (!response.ok) {
2043
+ const error = await response.text();
2044
+ throw new Error(`Anthropic API error: ${response.status} ${error}`);
2045
+ }
2046
+ return this.createStreamIterator(response.body, options);
2047
+ }
2048
+ buildRequest(request, stream) {
2049
+ const messages = this.convertMessages(request.messages);
2050
+ const tools = request.tools ? this.convertTools(request.tools) : undefined;
2051
+ return {
2052
+ model: request.config.model,
2053
+ messages,
2054
+ system: request.systemPrompt,
2055
+ max_tokens: request.config.maxTokens ?? this.config.defaultMaxTokens,
2056
+ temperature: request.config.temperature,
2057
+ top_p: request.config.topP,
2058
+ top_k: request.config.topK,
2059
+ stop_sequences: request.config.stopSequences,
2060
+ stream,
2061
+ tools
2062
+ };
2063
+ }
2064
+ convertMessages(messages) {
2065
+ return messages.filter((msg) => msg.role !== "system").map((msg) => {
2066
+ if (typeof msg.content === "string") {
2067
+ return {
2068
+ role: msg.role,
2069
+ content: msg.content
2070
+ };
2071
+ }
2072
+ const content = [];
2073
+ for (const block of msg.content) {
2074
+ if (block.type === "text") {
2075
+ content.push({ type: "text", text: block.text });
2076
+ } else if (block.type === "image") {
2077
+ if (block.source.type === "base64") {
2078
+ content.push({
2079
+ type: "image",
2080
+ source: {
2081
+ type: "base64",
2082
+ media_type: block.source.media_type,
2083
+ data: block.source.data
2084
+ }
2085
+ });
2086
+ }
2087
+ } else if (block.type === "tool_use") {
2088
+ content.push({
2089
+ type: "tool_use",
2090
+ id: block.id,
2091
+ name: block.name,
2092
+ input: block.input
2093
+ });
2094
+ } else if (block.type === "tool_result") {
2095
+ content.push({
2096
+ type: "tool_result",
2097
+ tool_use_id: block.tool_use_id,
2098
+ content: typeof block.content === "string" ? block.content : undefined,
2099
+ is_error: block.is_error
2100
+ });
2101
+ }
2102
+ }
2103
+ return {
2104
+ role: msg.role,
2105
+ content
2106
+ };
2107
+ });
2108
+ }
2109
+ convertTools(tools) {
2110
+ return tools.map((tool) => ({
2111
+ name: tool.name,
2112
+ description: tool.description,
2113
+ input_schema: tool.inputSchema
2114
+ }));
2115
+ }
2116
+ convertResponse(data) {
2117
+ const content = data.content.map((block) => {
2118
+ if (block.type === "text") {
2119
+ return { type: "text", text: block.text };
2120
+ }
2121
+ if (block.type === "tool_use") {
2122
+ return {
2123
+ type: "tool_use",
2124
+ id: block.id,
2125
+ name: block.name,
2126
+ input: block.input
2127
+ };
2128
+ }
2129
+ return { type: "text", text: "" };
2130
+ });
2131
+ return {
2132
+ id: data.id,
2133
+ model: data.model,
2134
+ content,
2135
+ stopReason: data.stop_reason,
2136
+ stopSequence: data.stop_sequence,
2137
+ usage: {
2138
+ input_tokens: data.usage.input_tokens,
2139
+ output_tokens: data.usage.output_tokens,
2140
+ cache_creation_input_tokens: data.usage.cache_creation_input_tokens,
2141
+ cache_read_input_tokens: data.usage.cache_read_input_tokens
2142
+ }
2143
+ };
2144
+ }
2145
+ createStreamIterator(body, options) {
2146
+ const self = this;
2147
+ return {
2148
+ async* [Symbol.asyncIterator]() {
2149
+ const reader = body.getReader();
2150
+ const decoder = new TextDecoder;
2151
+ let buffer = "";
2152
+ try {
2153
+ while (true) {
2154
+ const { done, value } = await reader.read();
2155
+ if (done)
2156
+ break;
2157
+ buffer += decoder.decode(value, { stream: true });
2158
+ const lines = buffer.split(`
2159
+ `);
2160
+ buffer = lines.pop() || "";
2161
+ for (const line of lines) {
2162
+ if (line.startsWith("data: ")) {
2163
+ const data = line.slice(6).trim();
2164
+ if (!data || data === "[DONE]")
2165
+ continue;
2166
+ try {
2167
+ const event = JSON.parse(data);
2168
+ const streamEvent = self.parseStreamEvent(event);
2169
+ if (streamEvent) {
2170
+ if (streamEvent.type === "content_block_delta" && streamEvent.delta.type === "text_delta") {
2171
+ options?.onText?.(streamEvent.delta.text);
2172
+ }
2173
+ options?.onEvent?.(streamEvent);
2174
+ yield streamEvent;
2175
+ }
2176
+ } catch {}
2177
+ }
2178
+ }
2179
+ }
2180
+ } finally {
2181
+ reader.releaseLock();
2182
+ }
2183
+ }
2184
+ };
2185
+ }
2186
+ parseStreamEvent(event) {
2187
+ switch (event.type) {
2188
+ case "message_start":
2189
+ return {
2190
+ type: "message_start",
2191
+ message: {
2192
+ id: event.message.id,
2193
+ type: "message",
2194
+ role: "assistant",
2195
+ content: [],
2196
+ model: event.message.model,
2197
+ stop_reason: null,
2198
+ stop_sequence: null,
2199
+ usage: event.message.usage
2200
+ }
2201
+ };
2202
+ case "content_block_start":
2203
+ return {
2204
+ type: "content_block_start",
2205
+ index: event.index,
2206
+ content_block: event.content_block
2207
+ };
2208
+ case "content_block_delta":
2209
+ return {
2210
+ type: "content_block_delta",
2211
+ index: event.index,
2212
+ delta: event.delta
2213
+ };
2214
+ case "content_block_stop":
2215
+ return {
2216
+ type: "content_block_stop",
2217
+ index: event.index
2218
+ };
2219
+ case "message_delta":
2220
+ return {
2221
+ type: "message_delta",
2222
+ delta: {
2223
+ stop_reason: event.delta.stop_reason,
2224
+ stop_sequence: event.delta.stop_sequence
2225
+ },
2226
+ usage: {
2227
+ output_tokens: event.usage.output_tokens
2228
+ }
2229
+ };
2230
+ case "message_stop":
2231
+ return {
2232
+ type: "message_stop"
2233
+ };
2234
+ case "error":
2235
+ return {
2236
+ type: "error",
2237
+ error: {
2238
+ type: event.error.type,
2239
+ message: event.error.message
2240
+ }
2241
+ };
2242
+ default:
2243
+ return null;
2244
+ }
2245
+ }
2246
+ getHeaders() {
2247
+ return {
2248
+ "Content-Type": "application/json",
2249
+ "x-api-key": this.config.apiKey,
2250
+ "anthropic-version": this.config.apiVersion
2251
+ };
2252
+ }
2253
+ }
2254
+
2255
+ // src/llm/openai.ts
2256
+ class OpenAIProvider {
2257
+ id = "openai";
2258
+ name = "OpenAI";
2259
+ supportedModels = [
2260
+ /^gpt-4/,
2261
+ /^gpt-3\.5/,
2262
+ /^o1/,
2263
+ /^chatgpt/
2264
+ ];
2265
+ config;
2266
+ constructor(config = {}) {
2267
+ const apiKey = config.apiKey ?? process.env.OPENAI_API_KEY;
2268
+ if (!apiKey) {
2269
+ throw new Error("OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass apiKey in config.");
2270
+ }
2271
+ this.config = {
2272
+ apiKey,
2273
+ baseUrl: config.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1",
2274
+ organization: config.organization,
2275
+ defaultMaxTokens: config.defaultMaxTokens ?? 4096
2276
+ };
2277
+ }
2278
+ supportsModel(model) {
2279
+ return this.supportedModels.some((pattern) => pattern.test(model));
2280
+ }
2281
+ async complete(request) {
2282
+ const openaiRequest = this.buildRequest(request, false);
2283
+ const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
2284
+ method: "POST",
2285
+ headers: this.getHeaders(),
2286
+ body: JSON.stringify(openaiRequest),
2287
+ signal: request.abortSignal
2288
+ });
2289
+ if (!response.ok) {
2290
+ const error = await response.text();
2291
+ throw new Error(`OpenAI API error: ${response.status} ${error}`);
2292
+ }
2293
+ const data = await response.json();
2294
+ return this.convertResponse(data);
2295
+ }
2296
+ async stream(request, options) {
2297
+ const openaiRequest = this.buildRequest(request, true);
2298
+ const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
2299
+ method: "POST",
2300
+ headers: this.getHeaders(),
2301
+ body: JSON.stringify(openaiRequest),
2302
+ signal: request.abortSignal
2303
+ });
2304
+ if (!response.ok) {
2305
+ const error = await response.text();
2306
+ throw new Error(`OpenAI API error: ${response.status} ${error}`);
2307
+ }
2308
+ return this.createStreamIterator(response.body, options);
2309
+ }
2310
+ buildRequest(request, stream) {
2311
+ const messages = this.convertMessages(request.messages, request.systemPrompt);
2312
+ const tools = request.tools ? this.convertTools(request.tools) : undefined;
2313
+ return {
2314
+ model: request.config.model,
2315
+ messages,
2316
+ max_tokens: request.config.maxTokens ?? this.config.defaultMaxTokens,
2317
+ temperature: request.config.temperature,
2318
+ top_p: request.config.topP,
2319
+ stop: request.config.stopSequences,
2320
+ stream,
2321
+ stream_options: stream ? { include_usage: true } : undefined,
2322
+ tools
2323
+ };
2324
+ }
2325
+ convertMessages(messages, systemPrompt) {
2326
+ const result = [];
2327
+ if (systemPrompt) {
2328
+ result.push({
2329
+ role: "system",
2330
+ content: systemPrompt
2331
+ });
2332
+ }
2333
+ for (const msg of messages) {
2334
+ if (msg.role === "system") {
2335
+ result.push({
2336
+ role: "system",
2337
+ content: typeof msg.content === "string" ? msg.content : ""
2338
+ });
2339
+ continue;
2340
+ }
2341
+ if (typeof msg.content === "string") {
2342
+ result.push({
2343
+ role: msg.role,
2344
+ content: msg.content
2345
+ });
2346
+ continue;
2347
+ }
2348
+ const content = [];
2349
+ const toolCalls = [];
2350
+ for (const block of msg.content) {
2351
+ if (block.type === "text") {
2352
+ content.push({ type: "text", text: block.text });
2353
+ } else if (block.type === "image") {
2354
+ if (block.source.type === "base64") {
2355
+ content.push({
2356
+ type: "image_url",
2357
+ image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
2358
+ });
2359
+ } else if (block.source.type === "url") {
2360
+ content.push({
2361
+ type: "image_url",
2362
+ image_url: { url: block.source.url }
2363
+ });
2364
+ }
2365
+ } else if (block.type === "tool_use") {
2366
+ toolCalls.push({
2367
+ id: block.id,
2368
+ type: "function",
2369
+ function: {
2370
+ name: block.name,
2371
+ arguments: JSON.stringify(block.input)
2372
+ }
2373
+ });
2374
+ } else if (block.type === "tool_result") {
2375
+ result.push({
2376
+ role: "tool",
2377
+ tool_call_id: block.tool_use_id,
2378
+ content: typeof block.content === "string" ? block.content : JSON.stringify(block.content)
2379
+ });
2380
+ continue;
2381
+ }
2382
+ }
2383
+ if (content.length > 0 || toolCalls.length > 0) {
2384
+ const message = {
2385
+ role: msg.role,
2386
+ content: content.length === 1 && content[0].type === "text" ? content[0].text : content
2387
+ };
2388
+ if (toolCalls.length > 0) {
2389
+ message.tool_calls = toolCalls;
2390
+ }
2391
+ result.push(message);
2392
+ }
2393
+ }
2394
+ return result;
2395
+ }
2396
+ convertTools(tools) {
2397
+ return tools.map((tool) => ({
2398
+ type: "function",
2399
+ function: {
2400
+ name: tool.name,
2401
+ description: tool.description,
2402
+ parameters: tool.inputSchema
2403
+ }
2404
+ }));
2405
+ }
2406
+ convertResponse(data) {
2407
+ const choice = data.choices[0];
2408
+ const content = [];
2409
+ if (choice.message.content) {
2410
+ content.push({ type: "text", text: choice.message.content });
2411
+ }
2412
+ if (choice.message.tool_calls) {
2413
+ for (const toolCall of choice.message.tool_calls) {
2414
+ content.push({
2415
+ type: "tool_use",
2416
+ id: toolCall.id,
2417
+ name: toolCall.function.name,
2418
+ input: JSON.parse(toolCall.function.arguments)
2419
+ });
2420
+ }
2421
+ }
2422
+ return {
2423
+ id: data.id,
2424
+ model: data.model,
2425
+ content,
2426
+ stopReason: this.convertStopReason(choice.finish_reason),
2427
+ stopSequence: null,
2428
+ usage: {
2429
+ input_tokens: data.usage.prompt_tokens,
2430
+ output_tokens: data.usage.completion_tokens
2431
+ }
2432
+ };
2433
+ }
2434
+ convertStopReason(reason) {
2435
+ switch (reason) {
2436
+ case "stop":
2437
+ return "end_turn";
2438
+ case "length":
2439
+ return "max_tokens";
2440
+ case "tool_calls":
2441
+ return "tool_use";
2442
+ case "content_filter":
2443
+ return "stop_sequence";
2444
+ default:
2445
+ return "end_turn";
2446
+ }
2447
+ }
2448
+ createStreamIterator(body, options) {
2449
+ const self = this;
2450
+ return {
2451
+ async* [Symbol.asyncIterator]() {
2452
+ const reader = body.getReader();
2453
+ const decoder = new TextDecoder;
2454
+ let buffer = "";
2455
+ const toolCalls = new Map;
2456
+ let emittedMessageStart = false;
2457
+ let textBlockStarted = false;
2458
+ let finished = false;
2459
+ try {
2460
+ while (true) {
2461
+ const { done, value } = await reader.read();
2462
+ if (done)
2463
+ break;
2464
+ buffer += decoder.decode(value, { stream: true });
2465
+ const lines = buffer.split(`
2466
+ `);
2467
+ buffer = lines.pop() || "";
2468
+ for (const line of lines) {
2469
+ if (line.startsWith("data: ")) {
2470
+ const data = line.slice(6).trim();
2471
+ if (!data)
2472
+ continue;
2473
+ if (data === "[DONE]") {
2474
+ if (!finished) {
2475
+ const stopEvent = { type: "message_stop" };
2476
+ options?.onEvent?.(stopEvent);
2477
+ yield stopEvent;
2478
+ }
2479
+ finished = true;
2480
+ continue;
2481
+ }
2482
+ try {
2483
+ const json = JSON.parse(data);
2484
+ const delta = json.choices?.[0]?.delta;
2485
+ const finishReason = json.choices?.[0]?.finish_reason;
2486
+ if (!emittedMessageStart) {
2487
+ emittedMessageStart = true;
2488
+ const startEvent = {
2489
+ type: "message_start",
2490
+ message: {
2491
+ id: json.id ?? "",
2492
+ type: "message",
2493
+ role: "assistant",
2494
+ content: [],
2495
+ model: json.model ?? "",
2496
+ stop_reason: null,
2497
+ stop_sequence: null,
2498
+ usage: { input_tokens: 0, output_tokens: 0 }
2499
+ }
2500
+ };
2501
+ options?.onEvent?.(startEvent);
2502
+ yield startEvent;
2503
+ }
2504
+ if (delta?.content) {
2505
+ if (!textBlockStarted) {
2506
+ textBlockStarted = true;
2507
+ const startText = {
2508
+ type: "content_block_start",
2509
+ index: 0,
2510
+ content_block: { type: "text", text: "" }
2511
+ };
2512
+ options?.onEvent?.(startText);
2513
+ yield startText;
2514
+ }
2515
+ const textEvent = {
2516
+ type: "content_block_delta",
2517
+ index: 0,
2518
+ delta: {
2519
+ type: "text_delta",
2520
+ text: delta.content
2521
+ }
2522
+ };
2523
+ options?.onText?.(delta.content);
2524
+ options?.onEvent?.(textEvent);
2525
+ yield textEvent;
2526
+ }
2527
+ if (delta?.tool_calls) {
2528
+ for (const tc of delta.tool_calls) {
2529
+ const index = tc.index;
2530
+ const blockIndex = 1 + index;
2531
+ if (!toolCalls.has(index)) {
2532
+ toolCalls.set(index, {
2533
+ id: tc.id || "",
2534
+ name: tc.function?.name || "",
2535
+ arguments: tc.function?.arguments || ""
2536
+ });
2537
+ const startEvent = {
2538
+ type: "content_block_start",
2539
+ index: blockIndex,
2540
+ content_block: {
2541
+ type: "tool_use",
2542
+ id: tc.id || "",
2543
+ name: tc.function?.name || "",
2544
+ input: {}
2545
+ }
2546
+ };
2547
+ options?.onEvent?.(startEvent);
2548
+ yield startEvent;
2549
+ } else {
2550
+ const existing = toolCalls.get(index);
2551
+ if (tc.id)
2552
+ existing.id = tc.id;
2553
+ if (tc.function?.name)
2554
+ existing.name = tc.function.name;
2555
+ if (tc.function?.arguments)
2556
+ existing.arguments += tc.function.arguments;
2557
+ }
2558
+ if (tc.function?.arguments) {
2559
+ const deltaEvent = {
2560
+ type: "content_block_delta",
2561
+ index: blockIndex,
2562
+ delta: {
2563
+ type: "input_json_delta",
2564
+ partial_json: tc.function.arguments
2565
+ }
2566
+ };
2567
+ options?.onEvent?.(deltaEvent);
2568
+ yield deltaEvent;
2569
+ }
2570
+ }
2571
+ }
2572
+ if (finishReason) {
2573
+ finished = true;
2574
+ if (textBlockStarted) {
2575
+ const stopText = { type: "content_block_stop", index: 0 };
2576
+ options?.onEvent?.(stopText);
2577
+ yield stopText;
2578
+ }
2579
+ for (const [index, tc] of toolCalls) {
2580
+ const stopEvent2 = {
2581
+ type: "content_block_stop",
2582
+ index: 1 + index
2583
+ };
2584
+ options?.onEvent?.(stopEvent2);
2585
+ yield stopEvent2;
2586
+ try {
2587
+ const input = JSON.parse(tc.arguments);
2588
+ options?.onToolUse?.({ id: tc.id, name: tc.name, input });
2589
+ } catch {}
2590
+ }
2591
+ const messageDelta = {
2592
+ type: "message_delta",
2593
+ delta: {
2594
+ stop_reason: self.convertStopReason(finishReason),
2595
+ stop_sequence: null
2596
+ },
2597
+ usage: {
2598
+ output_tokens: json.usage?.completion_tokens ?? 0,
2599
+ input_tokens: json.usage?.prompt_tokens ?? 0
2600
+ }
2601
+ };
2602
+ options?.onEvent?.(messageDelta);
2603
+ yield messageDelta;
2604
+ const stopEvent = { type: "message_stop" };
2605
+ options?.onEvent?.(stopEvent);
2606
+ yield stopEvent;
2607
+ }
2608
+ } catch {}
2609
+ }
2610
+ }
2611
+ }
2612
+ } finally {
2613
+ reader.releaseLock();
2614
+ }
2615
+ }
2616
+ };
2617
+ }
2618
+ getHeaders() {
2619
+ const headers = {
2620
+ "Content-Type": "application/json",
2621
+ Authorization: `Bearer ${this.config.apiKey}`
2622
+ };
2623
+ if (this.config.organization) {
2624
+ headers["OpenAI-Organization"] = this.config.organization;
2625
+ }
2626
+ return headers;
2627
+ }
2628
+ }
2629
+
2630
+ // src/api.ts
2631
+ var globalManager = null;
2632
+ var defaultProvider = null;
2633
+ function getGlobalManager() {
2634
+ if (!globalManager) {
2635
+ if (!defaultProvider) {
2636
+ if (process.env.ANTHROPIC_API_KEY) {
2637
+ defaultProvider = new AnthropicProvider;
2638
+ } else if (process.env.OPENAI_API_KEY) {
2639
+ defaultProvider = new OpenAIProvider({
2640
+ apiKey: process.env.OPENAI_API_KEY,
2641
+ baseUrl: process.env.OPENAI_BASE_URL
2642
+ });
2643
+ } else {
2644
+ 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.");
2645
+ }
2646
+ }
2647
+ globalManager = new SessionManagerImpl({
2648
+ provider: defaultProvider,
2649
+ storage: new MemorySessionStorage
2650
+ });
2651
+ }
2652
+ return globalManager;
2653
+ }
2654
+ async function createSession(options) {
2655
+ if (options?.provider) {
2656
+ const customManager = new SessionManagerImpl({
2657
+ provider: options.provider,
2658
+ storage: new MemorySessionStorage
2659
+ });
2660
+ return customManager.create(options);
2661
+ }
2662
+ const manager = getGlobalManager();
2663
+ return manager.create(options);
2664
+ }
2665
+
2666
+ // src/tools/registry.ts
2667
+ class ToolRegistry {
2668
+ tools = new Map;
2669
+ register(tool) {
2670
+ this.tools.set(tool.id, tool);
2671
+ }
2672
+ unregister(toolId) {
2673
+ this.tools.delete(toolId);
2674
+ }
2675
+ get(toolId) {
2676
+ return this.tools.get(toolId);
2677
+ }
2678
+ getAll() {
2679
+ return Array.from(this.tools.values());
2680
+ }
2681
+ clear() {
2682
+ this.tools.clear();
2683
+ }
2684
+ async execute(toolId, input, context) {
2685
+ const tool = this.tools.get(toolId);
2686
+ if (!tool) {
2687
+ throw new Error(`Tool not found: ${toolId}`);
2688
+ }
2689
+ context.notify?.({ type: "start", toolId, toolName: tool.name, input });
2690
+ try {
2691
+ const result = await tool.execute(input, context);
2692
+ context.notify?.({ type: "result", toolId, result });
2693
+ return result;
2694
+ } catch (error) {
2695
+ const errorMessage = error instanceof Error ? error.message : String(error);
2696
+ context.notify?.({ type: "error", toolId, error: errorMessage });
2697
+ throw error;
2698
+ }
2699
+ }
2700
+ }
2701
+ // src/tools/mcp.ts
2702
+ var MCP_NAMESPACE_SEPARATOR = "__";
2703
+ function createMCPToolName(serverName, toolName) {
2704
+ return `mcp${MCP_NAMESPACE_SEPARATOR}${serverName}${MCP_NAMESPACE_SEPARATOR}${toolName}`;
2705
+ }
2706
+ function parseMCPToolName(namespacedName) {
2707
+ const parts = namespacedName.split(MCP_NAMESPACE_SEPARATOR);
2708
+ if (parts.length < 3 || parts[0] !== "mcp") {
2709
+ return null;
2710
+ }
2711
+ return {
2712
+ serverName: parts[1],
2713
+ toolName: parts.slice(2).join(MCP_NAMESPACE_SEPARATOR)
2714
+ };
2715
+ }
2716
+ function isMCPTool(name) {
2717
+ return name.startsWith(`mcp${MCP_NAMESPACE_SEPARATOR}`);
2718
+ }
2719
+
2720
+ class MCPServerWrapper {
2721
+ server;
2722
+ toolCache = new Map;
2723
+ constructor(server) {
2724
+ this.server = server;
2725
+ }
2726
+ get name() {
2727
+ return this.server.name;
2728
+ }
2729
+ async getTools() {
2730
+ const mcpTools = await this.server.listTools();
2731
+ const tools = [];
2732
+ for (const mcpTool of mcpTools) {
2733
+ this.toolCache.set(mcpTool.name, mcpTool);
2734
+ const namespacedName = createMCPToolName(this.server.name, mcpTool.name);
2735
+ tools.push({
2736
+ name: namespacedName,
2737
+ description: `[MCP: ${this.server.name}] ${mcpTool.description}`,
2738
+ inputSchema: mcpTool.inputSchema,
2739
+ execute: async (input, _context) => {
2740
+ return this.executeTool(mcpTool.name, input);
2741
+ }
2742
+ });
2743
+ }
2744
+ return tools;
2745
+ }
2746
+ async executeTool(toolName, input) {
2747
+ const result = await this.server.callTool(toolName, input);
2748
+ return this.convertResult(result);
2749
+ }
2750
+ async executeNamespacedTool(namespacedName, input) {
2751
+ const parsed = parseMCPToolName(namespacedName);
2752
+ if (!parsed || parsed.serverName !== this.server.name) {
2753
+ throw new Error(`Tool ${namespacedName} is not from server ${this.server.name}`);
2754
+ }
2755
+ return this.executeTool(parsed.toolName, input);
2756
+ }
2757
+ async close() {
2758
+ await this.server.close();
2759
+ this.toolCache.clear();
2760
+ }
2761
+ convertResult(result) {
2762
+ const content = result.content.map((item) => {
2763
+ if (item.type === "text") {
2764
+ return item.text;
2765
+ }
2766
+ if (item.type === "image") {
2767
+ return `[Image: ${item.mimeType}]`;
2768
+ }
2769
+ return "";
2770
+ }).join(`
2771
+ `);
2772
+ return {
2773
+ content,
2774
+ isError: result.isError
2775
+ };
2776
+ }
2777
+ }
2778
+ class MCPServerManager {
2779
+ servers = new Map;
2780
+ async register(server) {
2781
+ const wrapper = new MCPServerWrapper(server);
2782
+ this.servers.set(server.name, wrapper);
2783
+ return wrapper;
2784
+ }
2785
+ async unregister(serverName) {
2786
+ const wrapper = this.servers.get(serverName);
2787
+ if (wrapper) {
2788
+ await wrapper.close();
2789
+ this.servers.delete(serverName);
2790
+ }
2791
+ }
2792
+ getServers() {
2793
+ return Array.from(this.servers.values());
2794
+ }
2795
+ getServer(name) {
2796
+ return this.servers.get(name);
2797
+ }
2798
+ async getAllTools() {
2799
+ const allTools = [];
2800
+ for (const wrapper of this.servers.values()) {
2801
+ const tools = await wrapper.getTools();
2802
+ allTools.push(...tools);
2803
+ }
2804
+ return allTools;
2805
+ }
2806
+ async closeAll() {
2807
+ for (const wrapper of this.servers.values()) {
2808
+ await wrapper.close();
2809
+ }
2810
+ this.servers.clear();
2811
+ }
2812
+ }
2813
+ var defaultMCPServerManager = new MCPServerManager;
2814
+ // src/tools/manager.ts
2815
+ class ToolManager {
2816
+ tools = new Map;
2817
+ mcpServers = new Map;
2818
+ allowedTools;
2819
+ onToolEvent;
2820
+ constructor(options = {}) {
2821
+ this.allowedTools = options.allowedTools;
2822
+ this.onToolEvent = options.onToolEvent;
2823
+ }
2824
+ register(tool) {
2825
+ this.tools.set(tool.name, tool);
2826
+ }
2827
+ unregister(name) {
2828
+ this.tools.delete(name);
2829
+ }
2830
+ get(name) {
2831
+ const tool = this.tools.get(name);
2832
+ if (tool) {
2833
+ return tool;
2834
+ }
2835
+ if (isMCPTool(name)) {
2836
+ const parsed = parseMCPToolName(name);
2837
+ if (parsed) {
2838
+ const wrapper = this.mcpServers.get(parsed.serverName);
2839
+ if (wrapper) {
2840
+ return this.createMCPToolProxy(name, wrapper);
2841
+ }
2842
+ }
2843
+ }
2844
+ return;
2845
+ }
2846
+ getAll() {
2847
+ const allTools = [];
2848
+ for (const tool of this.tools.values()) {
2849
+ if (this.isToolAllowed(tool)) {
2850
+ allTools.push(tool);
2851
+ }
2852
+ }
2853
+ for (const wrapper of this.mcpServers.values()) {
2854
+ const mcpTools = Array.from(this.tools.values()).filter((t) => isMCPTool(t.name) && t.name.includes(`__${wrapper.name}__`));
2855
+ }
2856
+ return allTools;
2857
+ }
2858
+ has(name) {
2859
+ return this.get(name) !== undefined;
2860
+ }
2861
+ clear() {
2862
+ this.tools.clear();
2863
+ }
2864
+ async registerMCPServer(server) {
2865
+ const wrapper = new MCPServerWrapper(server);
2866
+ this.mcpServers.set(server.name, wrapper);
2867
+ const tools = await wrapper.getTools();
2868
+ for (const tool of tools) {
2869
+ this.tools.set(tool.name, tool);
2870
+ }
2871
+ return tools.length;
2872
+ }
2873
+ async unregisterMCPServer(serverName) {
2874
+ const wrapper = this.mcpServers.get(serverName);
2875
+ if (wrapper) {
2876
+ for (const [name, _tool] of this.tools) {
2877
+ if (isMCPTool(name)) {
2878
+ const parsed = parseMCPToolName(name);
2879
+ if (parsed?.serverName === serverName) {
2880
+ this.tools.delete(name);
2881
+ }
2882
+ }
2883
+ }
2884
+ await wrapper.close();
2885
+ this.mcpServers.delete(serverName);
2886
+ }
2887
+ }
2888
+ getMCPServers() {
2889
+ return Array.from(this.mcpServers.keys());
2890
+ }
2891
+ async execute(name, input, context) {
2892
+ const tool = this.get(name);
2893
+ if (!tool) {
2894
+ throw new Error(`Tool not found: ${name}`);
2895
+ }
2896
+ if (!this.isToolAllowed(tool)) {
2897
+ throw new Error(`Tool not allowed: ${name}`);
2898
+ }
2899
+ const toolId = generateId("tool");
2900
+ this.emitEvent({
2901
+ type: "tool_start",
2902
+ toolId,
2903
+ toolName: name,
2904
+ input
2905
+ });
2906
+ try {
2907
+ const result = await tool.execute(input, context);
2908
+ this.emitEvent({
2909
+ type: "tool_result",
2910
+ toolId,
2911
+ output: result
2912
+ });
2913
+ return result;
2914
+ } catch (error) {
2915
+ this.emitEvent({
2916
+ type: "tool_error",
2917
+ toolId,
2918
+ error: error instanceof Error ? error : new Error(String(error))
2919
+ });
2920
+ throw error;
2921
+ }
2922
+ }
2923
+ setAllowedTools(allowedTools) {
2924
+ this.allowedTools = allowedTools;
2925
+ }
2926
+ getFilteredTools() {
2927
+ return this.getAll().filter((tool) => this.isToolAllowed(tool));
2928
+ }
2929
+ isToolAllowed(tool) {
2930
+ if (!this.allowedTools) {
2931
+ return true;
2932
+ }
2933
+ if (typeof this.allowedTools === "function") {
2934
+ return this.allowedTools(tool);
2935
+ }
2936
+ for (const pattern of this.allowedTools) {
2937
+ if (this.matchesPattern(tool.name, pattern)) {
2938
+ return true;
2939
+ }
2940
+ }
2941
+ return false;
2942
+ }
2943
+ matchesPattern(name, pattern) {
2944
+ if (pattern === "*") {
2945
+ return true;
2946
+ }
2947
+ if (pattern.includes("*")) {
2948
+ const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
2949
+ return new RegExp(`^${regexPattern}$`).test(name);
2950
+ }
2951
+ return name === pattern;
2952
+ }
2953
+ createMCPToolProxy(name, wrapper) {
2954
+ const parsed = parseMCPToolName(name);
2955
+ if (!parsed) {
2956
+ return;
2957
+ }
2958
+ return {
2959
+ name,
2960
+ description: `MCP tool: ${parsed.toolName} from ${parsed.serverName}`,
2961
+ inputSchema: { type: "object", properties: {} },
2962
+ execute: async (input, _context) => {
2963
+ return wrapper.executeNamespacedTool(name, input);
2964
+ }
2965
+ };
2966
+ }
2967
+ emitEvent(event) {
2968
+ this.onToolEvent?.(event);
2969
+ }
2970
+ }
2971
+ var defaultToolManager = new ToolManager;
2972
+ // src/tools/builtin/bash.ts
2973
+ import { spawn } from "node:child_process";
2974
+
2975
+ // src/tools/builtin/path-guard.ts
2976
+ import { isAbsolute, normalize, resolve, sep } from "node:path";
2977
+ function withTrailingSep(p) {
2978
+ const n = normalize(p);
2979
+ return n.endsWith(sep) ? n : n + sep;
2980
+ }
2981
+ function isSubpath(parentDir, childPath) {
2982
+ const parent = withTrailingSep(resolve(parentDir));
2983
+ const child = resolve(childPath);
2984
+ return child === parent.slice(0, -1) || child.startsWith(parent);
2985
+ }
2986
+ function getDefaultAllowedPaths(options) {
2987
+ const cwd = options.cwd ?? process.cwd();
2988
+ return [cwd];
2989
+ }
2990
+ function validateAbsolutePath(filePath) {
2991
+ if (!isAbsolute(filePath)) {
2992
+ return { ok: false, error: `Invalid path: ${filePath}. Must be an absolute path.` };
2993
+ }
2994
+ return { ok: true, resolved: resolve(filePath) };
2995
+ }
2996
+ function checkPathAccess(filePath, options, kind = "file") {
2997
+ const abs = validateAbsolutePath(filePath);
2998
+ if (!abs.ok)
2999
+ return abs;
3000
+ const resolved = abs.resolved;
3001
+ const blockedPaths = options.blockedPaths ?? [];
3002
+ for (const blocked of blockedPaths) {
3003
+ if (isSubpath(blocked, resolved)) {
3004
+ return { ok: false, error: `Path is blocked: ${resolved}` };
3005
+ }
3006
+ }
3007
+ const allowedPaths = options.allowedPaths ?? getDefaultAllowedPaths(options);
3008
+ const allowed = allowedPaths.some((allowedPath) => isSubpath(allowedPath, resolved));
3009
+ if (!allowed) {
3010
+ const base = options.cwd ?? process.cwd();
3011
+ return {
3012
+ ok: false,
3013
+ error: `Access denied: ${kind} path is outside allowedPaths.
3014
+ Path: ${resolved}
3015
+ Allowed: ${allowedPaths.join(", ")}
3016
+ Hint: configure tools with createBuiltinTools({ allowedPaths: ["${base}"] })`
3017
+ };
3018
+ }
3019
+ return { ok: true, resolved };
3020
+ }
3021
+ function checkDirAccess(dirPath, options) {
3022
+ return checkPathAccess(dirPath, options, "dir");
3023
+ }
3024
+
3025
+ // src/tools/builtin/bash.ts
3026
+ var DEFAULT_TIMEOUT = 120000;
3027
+ var MAX_OUTPUT_LENGTH = 1e5;
3028
+ var DEFAULT_BLOCKED_PATTERNS = [
3029
+ "\\bsudo\\b",
3030
+ "\\bmkfs\\b",
3031
+ "\\bdd\\s+if=",
3032
+ "\\brm\\s+-rf\\s+/(\\s|$)",
3033
+ "\\bshutdown\\b",
3034
+ "\\breboot\\b",
3035
+ "\\bpoweroff\\b"
3036
+ ];
3037
+ function createBashTool(options = {}) {
3038
+ const defaultCwd = options.cwd ?? process.cwd();
3039
+ const defaultTimeout = options.defaultTimeout ?? DEFAULT_TIMEOUT;
3040
+ const blockedPatterns = [
3041
+ ...DEFAULT_BLOCKED_PATTERNS,
3042
+ ...options.blockedCommandPatterns ?? []
3043
+ ].map((p) => {
3044
+ try {
3045
+ return new RegExp(p, "i");
3046
+ } catch {
3047
+ return null;
3048
+ }
3049
+ }).filter((r) => r !== null);
3050
+ return {
3051
+ name: "Bash",
3052
+ description: `Execute bash commands in a persistent shell session. Use for git, npm, docker, and other CLI operations. Avoid using for file operations (use Read/Write/Edit instead).`,
3053
+ inputSchema: {
3054
+ type: "object",
3055
+ properties: {
3056
+ command: {
3057
+ type: "string",
3058
+ description: "The bash command to execute"
3059
+ },
3060
+ cwd: {
3061
+ type: "string",
3062
+ description: "Working directory for the command"
3063
+ },
3064
+ timeout: {
3065
+ type: "number",
3066
+ description: "Timeout in milliseconds (max 600000)"
3067
+ },
3068
+ description: {
3069
+ type: "string",
3070
+ description: "Brief description of what this command does"
3071
+ }
3072
+ },
3073
+ required: ["command"]
3074
+ },
3075
+ execute: async (rawInput, _context) => {
3076
+ const input = rawInput;
3077
+ const { command, cwd = defaultCwd, timeout = defaultTimeout } = input;
3078
+ const cwdAccess = checkDirAccess(cwd, options);
3079
+ if (!cwdAccess.ok) {
3080
+ return { content: cwdAccess.error, isError: true };
3081
+ }
3082
+ if (!options.allowDangerous) {
3083
+ for (const pattern of blockedPatterns) {
3084
+ if (pattern.test(command)) {
3085
+ return {
3086
+ content: `Command blocked by policy (allowDangerous=false): matched /${pattern.source}/`,
3087
+ isError: true
3088
+ };
3089
+ }
3090
+ }
3091
+ }
3092
+ const actualTimeout = Math.min(timeout, 600000);
3093
+ return new Promise((resolve2) => {
3094
+ let stdout = "";
3095
+ let stderr = "";
3096
+ let killed = false;
3097
+ const proc = spawn("bash", ["-c", command], {
3098
+ cwd: cwdAccess.resolved,
3099
+ env: process.env,
3100
+ shell: false
3101
+ });
3102
+ const timer = setTimeout(() => {
3103
+ killed = true;
3104
+ proc.kill("SIGTERM");
3105
+ setTimeout(() => proc.kill("SIGKILL"), 1000);
3106
+ }, actualTimeout);
3107
+ proc.stdout?.on("data", (data) => {
3108
+ stdout += data.toString();
3109
+ if (stdout.length > MAX_OUTPUT_LENGTH) {
3110
+ stdout = stdout.slice(0, MAX_OUTPUT_LENGTH) + `
3111
+ ... (output truncated)`;
3112
+ proc.kill("SIGTERM");
3113
+ }
3114
+ });
3115
+ proc.stderr?.on("data", (data) => {
3116
+ stderr += data.toString();
3117
+ if (stderr.length > MAX_OUTPUT_LENGTH) {
3118
+ stderr = stderr.slice(0, MAX_OUTPUT_LENGTH) + `
3119
+ ... (output truncated)`;
3120
+ }
3121
+ });
3122
+ proc.on("close", (code) => {
3123
+ clearTimeout(timer);
3124
+ if (killed) {
3125
+ resolve2({
3126
+ content: `Command timed out after ${actualTimeout}ms
3127
+
3128
+ Partial output:
3129
+ ${stdout}
3130
+
3131
+ Stderr:
3132
+ ${stderr}`,
3133
+ isError: true
3134
+ });
3135
+ return;
3136
+ }
3137
+ const output = stdout + (stderr ? `
3138
+ Stderr:
3139
+ ${stderr}` : "");
3140
+ if (code !== 0) {
3141
+ resolve2({
3142
+ content: `Command failed with exit code ${code}
3143
+
3144
+ ${output}`,
3145
+ isError: true
3146
+ });
3147
+ } else {
3148
+ resolve2({
3149
+ content: output || "(no output)"
3150
+ });
3151
+ }
3152
+ });
3153
+ proc.on("error", (error) => {
3154
+ clearTimeout(timer);
3155
+ resolve2({
3156
+ content: `Failed to execute command: ${error.message}`,
3157
+ isError: true
3158
+ });
3159
+ });
3160
+ });
3161
+ }
3162
+ };
3163
+ }
3164
+ var BashTool = createBashTool();
3165
+ // src/tools/builtin/read.ts
3166
+ import { readFile as readFile3, stat } from "node:fs/promises";
3167
+ import { existsSync as existsSync4 } from "node:fs";
3168
+ import { extname } from "node:path";
3169
+ var DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
3170
+ var DEFAULT_LINE_LIMIT = 2000;
3171
+ var MAX_LINE_LENGTH = 2000;
3172
+ var BINARY_EXTENSIONS = new Set([
3173
+ ".png",
3174
+ ".jpg",
3175
+ ".jpeg",
3176
+ ".gif",
3177
+ ".bmp",
3178
+ ".ico",
3179
+ ".webp",
3180
+ ".mp3",
3181
+ ".mp4",
3182
+ ".wav",
3183
+ ".avi",
3184
+ ".mov",
3185
+ ".mkv",
3186
+ ".zip",
3187
+ ".tar",
3188
+ ".gz",
3189
+ ".rar",
3190
+ ".7z",
3191
+ ".exe",
3192
+ ".dll",
3193
+ ".so",
3194
+ ".dylib",
3195
+ ".pdf",
3196
+ ".doc",
3197
+ ".docx",
3198
+ ".xls",
3199
+ ".xlsx",
3200
+ ".woff",
3201
+ ".woff2",
3202
+ ".ttf",
3203
+ ".otf",
3204
+ ".eot"
3205
+ ]);
3206
+ function createReadTool(options = {}) {
3207
+ const maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
3208
+ return {
3209
+ name: "Read",
3210
+ description: `Read file contents. Supports text files with optional line range. Returns line numbers in output. For images/PDFs, returns metadata only.`,
3211
+ inputSchema: {
3212
+ type: "object",
3213
+ properties: {
3214
+ file_path: {
3215
+ type: "string",
3216
+ description: "Absolute path to the file to read"
3217
+ },
3218
+ offset: {
3219
+ type: "number",
3220
+ description: "Line number to start reading from (1-indexed)"
3221
+ },
3222
+ limit: {
3223
+ type: "number",
3224
+ description: "Number of lines to read (default: 2000)"
3225
+ }
3226
+ },
3227
+ required: ["file_path"]
3228
+ },
3229
+ execute: async (rawInput, _context) => {
3230
+ const input = rawInput;
3231
+ const { file_path, offset = 1, limit = DEFAULT_LINE_LIMIT } = input;
3232
+ const access = checkPathAccess(file_path, options, "file");
3233
+ if (!access.ok) {
3234
+ return { content: access.error, isError: true };
3235
+ }
3236
+ if (!existsSync4(access.resolved)) {
3237
+ return {
3238
+ content: `File not found: ${access.resolved}`,
3239
+ isError: true
3240
+ };
3241
+ }
3242
+ const fileStat = await stat(access.resolved);
3243
+ if (fileStat.size > maxFileSize) {
3244
+ return {
3245
+ content: `File too large: ${fileStat.size} bytes (max: ${maxFileSize} bytes)`,
3246
+ isError: true
3247
+ };
3248
+ }
3249
+ const ext = extname(access.resolved).toLowerCase();
3250
+ if (BINARY_EXTENSIONS.has(ext)) {
3251
+ return {
3252
+ content: `Binary file: ${access.resolved}
3253
+ Size: ${fileStat.size} bytes
3254
+ Type: ${ext}`
3255
+ };
3256
+ }
3257
+ try {
3258
+ const content = await readFile3(access.resolved, "utf-8");
3259
+ const lines = content.split(`
3260
+ `);
3261
+ const startLine = Math.max(1, offset);
3262
+ const endLine = Math.min(lines.length, startLine + limit - 1);
3263
+ const outputLines = [];
3264
+ for (let i = startLine - 1;i < endLine; i++) {
3265
+ const lineNum = i + 1;
3266
+ let line = lines[i];
3267
+ if (line.length > MAX_LINE_LENGTH) {
3268
+ line = line.slice(0, MAX_LINE_LENGTH) + "...";
3269
+ }
3270
+ const padding = String(endLine).length;
3271
+ outputLines.push(`${String(lineNum).padStart(padding)}→${line}`);
3272
+ }
3273
+ let header = "";
3274
+ if (startLine > 1 || endLine < lines.length) {
3275
+ header = `[Lines ${startLine}-${endLine} of ${lines.length}]
3276
+
3277
+ `;
3278
+ }
3279
+ return {
3280
+ content: header + outputLines.join(`
3281
+ `)
3282
+ };
3283
+ } catch (error) {
3284
+ return {
3285
+ content: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
3286
+ isError: true
3287
+ };
3288
+ }
3289
+ }
3290
+ };
3291
+ }
3292
+ var ReadTool = createReadTool();
3293
+ // src/tools/builtin/write.ts
3294
+ import { writeFile, mkdir } from "node:fs/promises";
3295
+ import { existsSync as existsSync5 } from "node:fs";
3296
+ import { dirname as dirname3 } from "node:path";
3297
+ function createWriteTool(options = {}) {
3298
+ return {
3299
+ name: "Write",
3300
+ description: `Write content to a file. Creates parent directories if needed. Overwrites existing files. Use Edit tool for modifying existing files.`,
3301
+ inputSchema: {
3302
+ type: "object",
3303
+ properties: {
3304
+ file_path: {
3305
+ type: "string",
3306
+ description: "Absolute path to the file to write"
3307
+ },
3308
+ content: {
3309
+ type: "string",
3310
+ description: "Content to write to the file"
3311
+ }
3312
+ },
3313
+ required: ["file_path", "content"]
3314
+ },
3315
+ execute: async (rawInput, _context) => {
3316
+ const input = rawInput;
3317
+ const { file_path, content } = input;
3318
+ const access = checkPathAccess(file_path, options, "file");
3319
+ if (!access.ok) {
3320
+ return { content: access.error, isError: true };
3321
+ }
3322
+ try {
3323
+ const dir = dirname3(access.resolved);
3324
+ if (!existsSync5(dir)) {
3325
+ await mkdir(dir, { recursive: true });
3326
+ }
3327
+ await writeFile(access.resolved, content, "utf-8");
3328
+ const lines = content.split(`
3329
+ `).length;
3330
+ const bytes = Buffer.byteLength(content, "utf-8");
3331
+ return {
3332
+ content: `Successfully wrote ${bytes} bytes (${lines} lines) to ${access.resolved}`
3333
+ };
3334
+ } catch (error) {
3335
+ return {
3336
+ content: `Failed to write file: ${error instanceof Error ? error.message : String(error)}`,
3337
+ isError: true
3338
+ };
3339
+ }
3340
+ }
3341
+ };
3342
+ }
3343
+ var WriteTool = createWriteTool();
3344
+ // src/tools/builtin/edit.ts
3345
+ import { readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
3346
+ import { existsSync as existsSync6 } from "node:fs";
3347
+ function createEditTool(options = {}) {
3348
+ return {
3349
+ name: "Edit",
3350
+ 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.`,
3351
+ inputSchema: {
3352
+ type: "object",
3353
+ properties: {
3354
+ file_path: {
3355
+ type: "string",
3356
+ description: "Absolute path to the file to edit"
3357
+ },
3358
+ old_string: {
3359
+ type: "string",
3360
+ description: "Text to find and replace"
3361
+ },
3362
+ new_string: {
3363
+ type: "string",
3364
+ description: "Replacement text"
3365
+ },
3366
+ replace_all: {
3367
+ type: "boolean",
3368
+ description: "Replace all occurrences (default: false)",
3369
+ default: false
3370
+ }
3371
+ },
3372
+ required: ["file_path", "old_string", "new_string"]
3373
+ },
3374
+ execute: async (rawInput, _context) => {
3375
+ const input = rawInput;
3376
+ const { file_path, old_string, new_string, replace_all = false } = input;
3377
+ const access = checkPathAccess(file_path, options, "file");
3378
+ if (!access.ok) {
3379
+ return { content: access.error, isError: true };
3380
+ }
3381
+ if (!existsSync6(access.resolved)) {
3382
+ return {
3383
+ content: `File not found: ${access.resolved}`,
3384
+ isError: true
3385
+ };
3386
+ }
3387
+ if (old_string === new_string) {
3388
+ return {
3389
+ content: `old_string and new_string are identical. No changes needed.`,
3390
+ isError: true
3391
+ };
3392
+ }
3393
+ try {
3394
+ const content = await readFile4(access.resolved, "utf-8");
3395
+ const occurrences = content.split(old_string).length - 1;
3396
+ if (occurrences === 0) {
3397
+ return {
3398
+ content: `Text not found in file: "${old_string.slice(0, 100)}${old_string.length > 100 ? "..." : ""}"`,
3399
+ isError: true
3400
+ };
3401
+ }
3402
+ if (!replace_all && occurrences > 1) {
3403
+ return {
3404
+ content: `Found ${occurrences} occurrences of the text. Use replace_all: true to replace all, or provide a more unique string.`,
3405
+ isError: true
3406
+ };
3407
+ }
3408
+ let newContent;
3409
+ let replacedCount;
3410
+ if (replace_all) {
3411
+ newContent = content.split(old_string).join(new_string);
3412
+ replacedCount = occurrences;
3413
+ } else {
3414
+ newContent = content.replace(old_string, new_string);
3415
+ replacedCount = 1;
3416
+ }
3417
+ await writeFile2(access.resolved, newContent, "utf-8");
3418
+ return {
3419
+ content: `Successfully replaced ${replacedCount} occurrence${replacedCount > 1 ? "s" : ""} in ${access.resolved}`
3420
+ };
3421
+ } catch (error) {
3422
+ return {
3423
+ content: `Failed to edit file: ${error instanceof Error ? error.message : String(error)}`,
3424
+ isError: true
3425
+ };
3426
+ }
3427
+ }
3428
+ };
3429
+ }
3430
+ var EditTool = createEditTool();
3431
+ // src/tools/builtin/glob.ts
3432
+ import { readdir, stat as stat2 } from "node:fs/promises";
3433
+ import { existsSync as existsSync7 } from "node:fs";
3434
+ import { join as join4, relative } from "node:path";
3435
+ var MAX_RESULTS = 1000;
3436
+ function matchGlob(pattern, path) {
3437
+ const regexPattern = pattern.replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/{{GLOBSTAR}}/g, ".*").replace(/\./g, "\\.");
3438
+ const regex = new RegExp(`^${regexPattern}$`);
3439
+ return regex.test(path);
3440
+ }
3441
+ async function scanDir(dir, pattern, results, baseDir, maxDepth = 10, currentDepth = 0) {
3442
+ if (currentDepth > maxDepth || results.length >= MAX_RESULTS) {
3443
+ return;
3444
+ }
3445
+ if (!existsSync7(dir)) {
3446
+ return;
3447
+ }
3448
+ try {
3449
+ const entries = await readdir(dir, { withFileTypes: true });
3450
+ for (const entry of entries) {
3451
+ if (results.length >= MAX_RESULTS)
3452
+ break;
3453
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
3454
+ continue;
3455
+ }
3456
+ const fullPath = join4(dir, entry.name);
3457
+ const relativePath = relative(baseDir, fullPath);
3458
+ if (entry.isDirectory()) {
3459
+ if (matchGlob(pattern, relativePath) || matchGlob(pattern, relativePath + "/")) {
3460
+ results.push(fullPath);
3461
+ }
3462
+ await scanDir(fullPath, pattern, results, baseDir, maxDepth, currentDepth + 1);
3463
+ } else if (entry.isFile()) {
3464
+ if (matchGlob(pattern, relativePath)) {
3465
+ results.push(fullPath);
3466
+ }
3467
+ }
3468
+ }
3469
+ } catch {}
3470
+ }
3471
+ function createGlobTool(options = {}) {
3472
+ const defaultCwd = options.cwd ?? process.cwd();
3473
+ return {
3474
+ name: "Glob",
3475
+ description: `Find files matching a glob pattern. Supports ** for recursive matching, * for single directory, ? for single character. Returns file paths sorted by modification time.`,
3476
+ inputSchema: {
3477
+ type: "object",
3478
+ properties: {
3479
+ pattern: {
3480
+ type: "string",
3481
+ description: 'Glob pattern to match (e.g., "**/*.ts", "src/**/*.js")'
3482
+ },
3483
+ path: {
3484
+ type: "string",
3485
+ description: "Directory to search in (default: current directory)"
3486
+ }
3487
+ },
3488
+ required: ["pattern"]
3489
+ },
3490
+ execute: async (rawInput, _context) => {
3491
+ const input = rawInput;
3492
+ const { pattern, path = defaultCwd } = input;
3493
+ const access = checkDirAccess(path, options);
3494
+ if (!access.ok) {
3495
+ return { content: access.error, isError: true };
3496
+ }
3497
+ if (!existsSync7(access.resolved)) {
3498
+ return {
3499
+ content: `Directory not found: ${access.resolved}`,
3500
+ isError: true
3501
+ };
3502
+ }
3503
+ try {
3504
+ const results = [];
3505
+ await scanDir(access.resolved, pattern, results, access.resolved);
3506
+ if (results.length === 0) {
3507
+ return {
3508
+ content: `No files found matching pattern: ${pattern}`
3509
+ };
3510
+ }
3511
+ const filesWithStats = await Promise.all(results.map(async (file) => {
3512
+ try {
3513
+ const stats = await stat2(file);
3514
+ return { file, mtime: stats.mtime.getTime() };
3515
+ } catch {
3516
+ return { file, mtime: 0 };
3517
+ }
3518
+ }));
3519
+ filesWithStats.sort((a, b) => b.mtime - a.mtime);
3520
+ const output = filesWithStats.map((f) => f.file).join(`
3521
+ `);
3522
+ const truncated = results.length >= MAX_RESULTS ? `
3523
+
3524
+ (Results truncated at ${MAX_RESULTS} files)` : "";
3525
+ return {
3526
+ content: `Found ${results.length} files:
3527
+
3528
+ ${output}${truncated}`
3529
+ };
3530
+ } catch (error) {
3531
+ return {
3532
+ content: `Failed to search: ${error instanceof Error ? error.message : String(error)}`,
3533
+ isError: true
3534
+ };
3535
+ }
3536
+ }
3537
+ };
3538
+ }
3539
+ var GlobTool = createGlobTool();
3540
+ // src/tools/builtin/grep.ts
3541
+ import { readFile as readFile5, readdir as readdir2, stat as stat3 } from "node:fs/promises";
3542
+ import { existsSync as existsSync8 } from "node:fs";
3543
+ import { join as join5, extname as extname2 } from "node:path";
3544
+ var MAX_RESULTS2 = 500;
3545
+ var MAX_LINE_LENGTH2 = 500;
3546
+ var BINARY_EXTENSIONS2 = new Set([
3547
+ ".png",
3548
+ ".jpg",
3549
+ ".jpeg",
3550
+ ".gif",
3551
+ ".bmp",
3552
+ ".ico",
3553
+ ".webp",
3554
+ ".mp3",
3555
+ ".mp4",
3556
+ ".wav",
3557
+ ".avi",
3558
+ ".mov",
3559
+ ".mkv",
3560
+ ".zip",
3561
+ ".tar",
3562
+ ".gz",
3563
+ ".rar",
3564
+ ".7z",
3565
+ ".exe",
3566
+ ".dll",
3567
+ ".so",
3568
+ ".dylib",
3569
+ ".pdf",
3570
+ ".doc",
3571
+ ".docx",
3572
+ ".xls",
3573
+ ".xlsx",
3574
+ ".woff",
3575
+ ".woff2",
3576
+ ".ttf",
3577
+ ".otf",
3578
+ ".eot",
3579
+ ".lock"
3580
+ ]);
3581
+ function matchGlob2(pattern, filename) {
3582
+ const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/\./g, "\\.");
3583
+ return new RegExp(`^${regexPattern}$`).test(filename);
3584
+ }
3585
+ async function searchFile(filePath, regex, before, after) {
3586
+ const matches = [];
3587
+ try {
3588
+ const content = await readFile5(filePath, "utf-8");
3589
+ const lines = content.split(`
3590
+ `);
3591
+ for (let i = 0;i < lines.length; i++) {
3592
+ if (regex.test(lines[i])) {
3593
+ const match = {
3594
+ file: filePath,
3595
+ line: i + 1,
3596
+ content: lines[i].slice(0, MAX_LINE_LENGTH2)
3597
+ };
3598
+ if (before > 0) {
3599
+ match.before = lines.slice(Math.max(0, i - before), i).map((l) => l.slice(0, MAX_LINE_LENGTH2));
3600
+ }
3601
+ if (after > 0) {
3602
+ match.after = lines.slice(i + 1, i + 1 + after).map((l) => l.slice(0, MAX_LINE_LENGTH2));
3603
+ }
3604
+ matches.push(match);
3605
+ if (matches.length >= MAX_RESULTS2) {
3606
+ break;
3607
+ }
3608
+ }
3609
+ }
3610
+ } catch {}
3611
+ return matches;
3612
+ }
3613
+ async function searchDir(dir, regex, glob, before, after, results, maxDepth = 10, currentDepth = 0) {
3614
+ if (currentDepth > maxDepth || results.length >= MAX_RESULTS2) {
3615
+ return;
3616
+ }
3617
+ try {
3618
+ const entries = await readdir2(dir, { withFileTypes: true });
3619
+ for (const entry of entries) {
3620
+ if (results.length >= MAX_RESULTS2)
3621
+ break;
3622
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
3623
+ continue;
3624
+ }
3625
+ const fullPath = join5(dir, entry.name);
3626
+ if (entry.isDirectory()) {
3627
+ await searchDir(fullPath, regex, glob, before, after, results, maxDepth, currentDepth + 1);
3628
+ } else if (entry.isFile()) {
3629
+ const ext = extname2(entry.name).toLowerCase();
3630
+ if (BINARY_EXTENSIONS2.has(ext)) {
3631
+ continue;
3632
+ }
3633
+ if (glob && !matchGlob2(glob, entry.name)) {
3634
+ continue;
3635
+ }
3636
+ const matches = await searchFile(fullPath, regex, before, after);
3637
+ results.push(...matches);
3638
+ }
3639
+ }
3640
+ } catch {}
3641
+ }
3642
+ function createGrepTool(options = {}) {
3643
+ const defaultCwd = options.cwd ?? process.cwd();
3644
+ return {
3645
+ name: "Grep",
3646
+ description: `Search file contents using regular expressions. Supports context lines before/after matches. Returns matching lines with file paths and line numbers.`,
3647
+ inputSchema: {
3648
+ type: "object",
3649
+ properties: {
3650
+ pattern: {
3651
+ type: "string",
3652
+ description: "Regular expression pattern to search for"
3653
+ },
3654
+ path: {
3655
+ type: "string",
3656
+ description: "File or directory to search in"
3657
+ },
3658
+ glob: {
3659
+ type: "string",
3660
+ description: 'Glob pattern to filter files (e.g., "*.ts")'
3661
+ },
3662
+ before: {
3663
+ type: "number",
3664
+ description: "Number of lines to show before each match"
3665
+ },
3666
+ after: {
3667
+ type: "number",
3668
+ description: "Number of lines to show after each match"
3669
+ },
3670
+ ignoreCase: {
3671
+ type: "boolean",
3672
+ description: "Case insensitive search"
3673
+ }
3674
+ },
3675
+ required: ["pattern"]
3676
+ },
3677
+ execute: async (rawInput, _context) => {
3678
+ const input = rawInput;
3679
+ const {
3680
+ pattern,
3681
+ path = defaultCwd,
3682
+ glob,
3683
+ before = 0,
3684
+ after = 0,
3685
+ ignoreCase = false
3686
+ } = input;
3687
+ const access = checkPathAccess(path, options, "dir");
3688
+ if (!access.ok) {
3689
+ return { content: access.error, isError: true };
3690
+ }
3691
+ if (!existsSync8(access.resolved)) {
3692
+ return {
3693
+ content: `Path not found: ${access.resolved}`,
3694
+ isError: true
3695
+ };
3696
+ }
3697
+ try {
3698
+ const flags = ignoreCase ? "gi" : "g";
3699
+ const regex = new RegExp(pattern, flags);
3700
+ const results = [];
3701
+ const pathStat = await stat3(access.resolved);
3702
+ if (pathStat.isFile()) {
3703
+ const matches = await searchFile(access.resolved, regex, before, after);
3704
+ results.push(...matches);
3705
+ } else if (pathStat.isDirectory()) {
3706
+ await searchDir(access.resolved, regex, glob, before, after, results);
3707
+ }
3708
+ if (results.length === 0) {
3709
+ return {
3710
+ content: `No matches found for pattern: ${pattern}`
3711
+ };
3712
+ }
3713
+ const output = [];
3714
+ for (const match of results) {
3715
+ if (match.before?.length) {
3716
+ for (let i = 0;i < match.before.length; i++) {
3717
+ const lineNum = match.line - match.before.length + i;
3718
+ output.push(`${match.file}:${lineNum}- ${match.before[i]}`);
3719
+ }
3720
+ }
3721
+ output.push(`${match.file}:${match.line}: ${match.content}`);
3722
+ if (match.after?.length) {
3723
+ for (let i = 0;i < match.after.length; i++) {
3724
+ const lineNum = match.line + i + 1;
3725
+ output.push(`${match.file}:${lineNum}+ ${match.after[i]}`);
3726
+ }
3727
+ }
3728
+ if (match.before?.length || match.after?.length) {
3729
+ output.push("--");
3730
+ }
3731
+ }
3732
+ const truncated = results.length >= MAX_RESULTS2 ? `
3733
+
3734
+ (Results truncated at ${MAX_RESULTS2} matches)` : "";
3735
+ return {
3736
+ content: `Found ${results.length} matches:
3737
+
3738
+ ${output.join(`
3739
+ `)}${truncated}`
3740
+ };
3741
+ } catch (error) {
3742
+ return {
3743
+ content: `Search failed: ${error instanceof Error ? error.message : String(error)}`,
3744
+ isError: true
3745
+ };
3746
+ }
3747
+ }
3748
+ };
3749
+ }
3750
+ var GrepTool = createGrepTool();
3751
+ // src/tools/builtin/webfetch.ts
3752
+ import { lookup } from "node:dns/promises";
3753
+ var MAX_CONTENT_LENGTH = 1e5;
3754
+ var FETCH_TIMEOUT = 30000;
3755
+ function isIpV4(host) {
3756
+ return /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
3757
+ }
3758
+ function isIpV6(host) {
3759
+ return /^[0-9a-fA-F:]+$/.test(host) && host.includes(":");
3760
+ }
3761
+ function isPrivateIpv4(ip) {
3762
+ const parts = ip.split(".").map((p) => Number(p));
3763
+ if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255))
3764
+ return true;
3765
+ const [a, b] = parts;
3766
+ if (a === 10)
3767
+ return true;
3768
+ if (a === 127)
3769
+ return true;
3770
+ if (a === 0)
3771
+ return true;
3772
+ if (a === 169 && b === 254)
3773
+ return true;
3774
+ if (a === 172 && b >= 16 && b <= 31)
3775
+ return true;
3776
+ if (a === 192 && b === 168)
3777
+ return true;
3778
+ if (a === 100 && b >= 64 && b <= 127)
3779
+ return true;
3780
+ if (a >= 224)
3781
+ return true;
3782
+ return false;
3783
+ }
3784
+ function isPrivateIpv6(ip) {
3785
+ const normalized = ip.toLowerCase();
3786
+ if (normalized === "::" || normalized === "::1")
3787
+ return true;
3788
+ if (normalized.startsWith("fe80:"))
3789
+ return true;
3790
+ if (normalized.startsWith("fc") || normalized.startsWith("fd"))
3791
+ return true;
3792
+ return false;
3793
+ }
3794
+ async function denyPrivateNetworkTargets(url, options) {
3795
+ if (options.allowPrivateNetwork)
3796
+ return null;
3797
+ const hostname = url.hostname.toLowerCase();
3798
+ if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local")) {
3799
+ return `Blocked by policy (allowPrivateNetwork=false): hostname "${hostname}" is local-only`;
3800
+ }
3801
+ if (isIpV4(hostname) && isPrivateIpv4(hostname)) {
3802
+ return `Blocked by policy (allowPrivateNetwork=false): private IPv4 target "${hostname}"`;
3803
+ }
3804
+ if (isIpV6(hostname) && isPrivateIpv6(hostname)) {
3805
+ return `Blocked by policy (allowPrivateNetwork=false): private IPv6 target "${hostname}"`;
3806
+ }
3807
+ const resolveHostnames = options.resolveHostnames ?? true;
3808
+ if (!resolveHostnames)
3809
+ return null;
3810
+ try {
3811
+ const addrs = await lookup(hostname, { all: true, verbatim: true });
3812
+ for (const addr of addrs) {
3813
+ if (addr.family === 4 && isPrivateIpv4(addr.address)) {
3814
+ return `Blocked by policy (allowPrivateNetwork=false): "${hostname}" resolves to private IPv4 "${addr.address}"`;
3815
+ }
3816
+ if (addr.family === 6 && isPrivateIpv6(addr.address)) {
3817
+ return `Blocked by policy (allowPrivateNetwork=false): "${hostname}" resolves to private IPv6 "${addr.address}"`;
3818
+ }
3819
+ }
3820
+ } catch (e) {
3821
+ return `DNS resolution failed for "${hostname}" (allowPrivateNetwork=false): ${e instanceof Error ? e.message : String(e)}`;
3822
+ }
3823
+ return null;
3824
+ }
3825
+ function htmlToMarkdown(html) {
3826
+ let md = html;
3827
+ md = md.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
3828
+ md = md.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
3829
+ md = md.replace(/<h1[^>]*>(.*?)<\/h1>/gi, `
3830
+ # $1
3831
+ `);
3832
+ md = md.replace(/<h2[^>]*>(.*?)<\/h2>/gi, `
3833
+ ## $1
3834
+ `);
3835
+ md = md.replace(/<h3[^>]*>(.*?)<\/h3>/gi, `
3836
+ ### $1
3837
+ `);
3838
+ md = md.replace(/<h4[^>]*>(.*?)<\/h4>/gi, `
3839
+ #### $1
3840
+ `);
3841
+ md = md.replace(/<h5[^>]*>(.*?)<\/h5>/gi, `
3842
+ ##### $1
3843
+ `);
3844
+ md = md.replace(/<h6[^>]*>(.*?)<\/h6>/gi, `
3845
+ ###### $1
3846
+ `);
3847
+ md = md.replace(/<p[^>]*>(.*?)<\/p>/gi, `
3848
+ $1
3849
+ `);
3850
+ md = md.replace(/<a[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi, "[$2]($1)");
3851
+ md = md.replace(/<(strong|b)[^>]*>(.*?)<\/\1>/gi, "**$2**");
3852
+ md = md.replace(/<(em|i)[^>]*>(.*?)<\/\1>/gi, "*$2*");
3853
+ md = md.replace(/<code[^>]*>(.*?)<\/code>/gi, "`$1`");
3854
+ md = md.replace(/<pre[^>]*>(.*?)<\/pre>/gis, "\n```\n$1\n```\n");
3855
+ md = md.replace(/<li[^>]*>(.*?)<\/li>/gi, `- $1
3856
+ `);
3857
+ md = md.replace(/<\/?[ou]l[^>]*>/gi, `
3858
+ `);
3859
+ md = md.replace(/<br\s*\/?>/gi, `
3860
+ `);
3861
+ md = md.replace(/<[^>]+>/g, "");
3862
+ md = md.replace(/&nbsp;/g, " ");
3863
+ md = md.replace(/&amp;/g, "&");
3864
+ md = md.replace(/&lt;/g, "<");
3865
+ md = md.replace(/&gt;/g, ">");
3866
+ md = md.replace(/&quot;/g, '"');
3867
+ md = md.replace(/\n{3,}/g, `
3868
+
3869
+ `);
3870
+ md = md.trim();
3871
+ return md;
3872
+ }
3873
+ function createWebFetchTool(options = {}) {
3874
+ return {
3875
+ name: "WebFetch",
3876
+ description: `Fetch content from a URL. Converts HTML to markdown for readability. Use for retrieving web pages, documentation, or API responses.`,
3877
+ inputSchema: {
3878
+ type: "object",
3879
+ properties: {
3880
+ url: {
3881
+ type: "string",
3882
+ description: "URL to fetch"
3883
+ },
3884
+ prompt: {
3885
+ type: "string",
3886
+ description: "Optional prompt to describe what information to extract"
3887
+ }
3888
+ },
3889
+ required: ["url"]
3890
+ },
3891
+ execute: async (rawInput, _context) => {
3892
+ const input = rawInput;
3893
+ const { url, prompt } = input;
3894
+ let parsedUrl;
3895
+ try {
3896
+ parsedUrl = new URL(url);
3897
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
3898
+ return {
3899
+ content: `Invalid URL protocol: ${parsedUrl.protocol}. Only http/https are allowed.`,
3900
+ isError: true
3901
+ };
3902
+ }
3903
+ if (parsedUrl.protocol === "http:")
3904
+ parsedUrl.protocol = "https:";
3905
+ } catch {
3906
+ return {
3907
+ content: `Invalid URL: ${url}`,
3908
+ isError: true
3909
+ };
3910
+ }
3911
+ const ssrfDeny = await denyPrivateNetworkTargets(parsedUrl, options);
3912
+ if (ssrfDeny) {
3913
+ return { content: ssrfDeny, isError: true };
3914
+ }
3915
+ try {
3916
+ const controller = new AbortController;
3917
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
3918
+ const response = await fetch(parsedUrl.toString(), {
3919
+ signal: controller.signal,
3920
+ headers: {
3921
+ "User-Agent": "Mozilla/5.0 (compatible; OpenCode-Agent/1.0)",
3922
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
3923
+ }
3924
+ });
3925
+ clearTimeout(timeout);
3926
+ if (!response.ok) {
3927
+ return {
3928
+ content: `HTTP error: ${response.status} ${response.statusText}`,
3929
+ isError: true
3930
+ };
3931
+ }
3932
+ const finalUrl = new URL(response.url);
3933
+ if (finalUrl.host !== parsedUrl.host) {
3934
+ return {
3935
+ content: `Redirected to different host: ${response.url}
3936
+
3937
+ Please fetch the new URL if you want to continue.`
3938
+ };
3939
+ }
3940
+ const ssrfDenyAfter = await denyPrivateNetworkTargets(finalUrl, options);
3941
+ if (ssrfDenyAfter) {
3942
+ return { content: `Redirect target denied: ${ssrfDenyAfter}`, isError: true };
3943
+ }
3944
+ const contentType = response.headers.get("content-type") || "";
3945
+ let content = await response.text();
3946
+ if (content.length > MAX_CONTENT_LENGTH) {
3947
+ content = content.slice(0, MAX_CONTENT_LENGTH) + `
3948
+
3949
+ ... (content truncated)`;
3950
+ }
3951
+ if (contentType.includes("text/html")) {
3952
+ content = htmlToMarkdown(content);
3953
+ }
3954
+ let output = `URL: ${response.url}
3955
+ `;
3956
+ output += `Content-Type: ${contentType}
3957
+
3958
+ `;
3959
+ if (prompt) {
3960
+ output += `Requested: ${prompt}
3961
+
3962
+ `;
3963
+ }
3964
+ output += content;
3965
+ return {
3966
+ content: output
3967
+ };
3968
+ } catch (error) {
3969
+ if (error instanceof Error && error.name === "AbortError") {
3970
+ return {
3971
+ content: `Request timed out after ${FETCH_TIMEOUT}ms`,
3972
+ isError: true
3973
+ };
3974
+ }
3975
+ return {
3976
+ content: `Failed to fetch: ${error instanceof Error ? error.message : String(error)}`,
3977
+ isError: true
3978
+ };
3979
+ }
3980
+ }
3981
+ };
3982
+ }
3983
+ var WebFetchTool = createWebFetchTool();
3984
+ // src/tools/builtin/todo.ts
3985
+ var globalTodos = [];
3986
+ var onTodoChange = null;
3987
+ function setTodoChangeCallback(callback) {
3988
+ onTodoChange = callback;
3989
+ }
3990
+ function getTodos() {
3991
+ return [...globalTodos];
3992
+ }
3993
+ function clearTodos() {
3994
+ globalTodos = [];
3995
+ onTodoChange?.(globalTodos);
3996
+ }
3997
+ function createTodoWriteTool(options = {}) {
3998
+ return {
3999
+ name: "TodoWrite",
4000
+ description: `Manage a task list to track progress on complex tasks. Use to plan work, track completed items, and show progress to the user.`,
4001
+ inputSchema: {
4002
+ type: "object",
4003
+ properties: {
4004
+ todos: {
4005
+ type: "array",
4006
+ description: "Updated todo list",
4007
+ items: {
4008
+ type: "object",
4009
+ properties: {
4010
+ content: {
4011
+ type: "string",
4012
+ description: "Task description (imperative form, e.g., 'Run tests')"
4013
+ },
4014
+ status: {
4015
+ type: "string",
4016
+ enum: ["pending", "in_progress", "completed"],
4017
+ description: "Task status"
4018
+ },
4019
+ activeForm: {
4020
+ type: "string",
4021
+ description: "Present continuous form (e.g., 'Running tests')"
4022
+ }
4023
+ },
4024
+ required: ["content", "status", "activeForm"]
4025
+ }
4026
+ }
4027
+ },
4028
+ required: ["todos"]
4029
+ },
4030
+ execute: async (rawInput, _context) => {
4031
+ const input = rawInput;
4032
+ const { todos } = input;
4033
+ for (const todo of todos) {
4034
+ if (!todo.content || !todo.status || !todo.activeForm) {
4035
+ return {
4036
+ content: "Invalid todo item: missing required fields (content, status, activeForm)",
4037
+ isError: true
4038
+ };
4039
+ }
4040
+ if (!["pending", "in_progress", "completed"].includes(todo.status)) {
4041
+ return {
4042
+ content: `Invalid status: ${todo.status}. Must be pending, in_progress, or completed.`,
4043
+ isError: true
4044
+ };
4045
+ }
4046
+ }
4047
+ globalTodos = todos;
4048
+ onTodoChange?.(globalTodos);
4049
+ const completed = todos.filter((t) => t.status === "completed").length;
4050
+ const inProgress = todos.filter((t) => t.status === "in_progress").length;
4051
+ const pending = todos.filter((t) => t.status === "pending").length;
4052
+ const lines = [
4053
+ `Todo list updated (${completed}/${todos.length} completed)`,
4054
+ ""
4055
+ ];
4056
+ for (const todo of todos) {
4057
+ const icon = todo.status === "completed" ? "✓" : todo.status === "in_progress" ? "→" : "○";
4058
+ lines.push(`${icon} ${todo.content}`);
4059
+ }
4060
+ if (inProgress > 0) {
4061
+ lines.push("");
4062
+ lines.push(`Currently: ${todos.find((t) => t.status === "in_progress")?.activeForm}`);
4063
+ }
4064
+ return {
4065
+ content: lines.join(`
4066
+ `)
4067
+ };
4068
+ }
4069
+ };
4070
+ }
4071
+ var TodoWriteTool = createTodoWriteTool();
4072
+ // src/tools/builtin/index.ts
4073
+ var builtinTools = [
4074
+ BashTool,
4075
+ ReadTool,
4076
+ WriteTool,
4077
+ EditTool,
4078
+ GlobTool,
4079
+ GrepTool,
4080
+ WebFetchTool,
4081
+ TodoWriteTool
4082
+ ];
4083
+ // src/utils/env.ts
4084
+ import { existsSync as existsSync9, readFileSync } from "node:fs";
4085
+ import { join as join6 } from "node:path";
4086
+ function loadEnvOverride(cwd) {
4087
+ const dir = cwd || process.cwd();
4088
+ const envPath = join6(dir, ".env");
4089
+ if (!existsSync9(envPath)) {
4090
+ return;
4091
+ }
4092
+ try {
4093
+ const content = readFileSync(envPath, "utf-8");
4094
+ const lines = content.split(`
4095
+ `);
4096
+ for (const line of lines) {
4097
+ const trimmed = line.trim();
4098
+ if (!trimmed || trimmed.startsWith("#")) {
4099
+ continue;
4100
+ }
4101
+ const eqIndex = trimmed.indexOf("=");
4102
+ if (eqIndex === -1) {
4103
+ continue;
4104
+ }
4105
+ const key = trimmed.slice(0, eqIndex).trim();
4106
+ let value = trimmed.slice(eqIndex + 1).trim();
4107
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
4108
+ value = value.slice(1, -1);
4109
+ }
4110
+ process.env[key] = value;
4111
+ }
4112
+ } catch {}
4113
+ }
4114
+
4115
+ // src/cli/cli.ts
4116
+ loadEnvOverride();
4117
+ var VERSION = "0.1.0";
4118
+ var SKILLS_PATH = join7(homedir4(), ".claude");
4119
+ var colors = {
4120
+ reset: "\x1B[0m",
4121
+ bold: "\x1B[1m",
4122
+ dim: "\x1B[2m",
4123
+ red: "\x1B[31m",
4124
+ green: "\x1B[32m",
4125
+ yellow: "\x1B[33m",
4126
+ blue: "\x1B[34m",
4127
+ magenta: "\x1B[35m",
4128
+ cyan: "\x1B[36m",
4129
+ gray: "\x1B[90m"
4130
+ };
4131
+ var c = {
4132
+ bold: (s) => `${colors.bold}${s}${colors.reset}`,
4133
+ dim: (s) => `${colors.dim}${s}${colors.reset}`,
4134
+ red: (s) => `${colors.red}${s}${colors.reset}`,
4135
+ green: (s) => `${colors.green}${s}${colors.reset}`,
4136
+ yellow: (s) => `${colors.yellow}${s}${colors.reset}`,
4137
+ blue: (s) => `${colors.blue}${s}${colors.reset}`,
4138
+ magenta: (s) => `${colors.magenta}${s}${colors.reset}`,
4139
+ cyan: (s) => `${colors.cyan}${s}${colors.reset}`,
4140
+ gray: (s) => `${colors.gray}${s}${colors.reset}`
4141
+ };
4142
+ var session = null;
4143
+ var totalInputTokens = 0;
4144
+ var totalOutputTokens = 0;
4145
+ var messageCount = 0;
4146
+ function isGitRepo(dir) {
4147
+ return existsSync10(join7(dir, ".git"));
4148
+ }
4149
+ function getOsVersion() {
4150
+ try {
4151
+ return __require("node:os").release();
4152
+ } catch {
4153
+ return "";
4154
+ }
4155
+ }
4156
+ function buildSystemPrompt() {
4157
+ const cwd = process.cwd();
4158
+ const envContext = generateEnvContext({
4159
+ cwd,
4160
+ isGitRepo: isGitRepo(cwd),
4161
+ platform: process.platform,
4162
+ osVersion: getOsVersion(),
4163
+ date: new Date,
4164
+ shell: process.env.SHELL
4165
+ });
4166
+ return `${CLI_AGENT_PRESET}
4167
+
4168
+ ${envContext}
4169
+ `;
4170
+ }
4171
+ function getAllTools() {
4172
+ const skillTool2 = createSkillTool({
4173
+ settingSources: [SKILLS_PATH],
4174
+ cwd: process.cwd()
4175
+ });
4176
+ return [...builtinTools, skillTool2];
4177
+ }
4178
+ function printHelp() {
4179
+ console.log(`
4180
+ ${c.bold("FormAgent CLI")} - Interactive AI Agent
4181
+
4182
+ ${c.bold("Usage:")}
4183
+ ${c.cyan("npx formagent")} Start interactive mode
4184
+ ${c.cyan("npx formagent <question>")} Quick query mode
4185
+ ${c.cyan("npx formagent --help")} Show this help
4186
+ ${c.cyan("npx formagent --version")} Show version
4187
+
4188
+ ${c.bold("Interactive Commands:")}
4189
+ ${c.cyan("/help")} Show available commands
4190
+ ${c.cyan("/clear")} Clear conversation history
4191
+ ${c.cyan("/tools")} List available tools
4192
+ ${c.cyan("/skills")} List available skills
4193
+ ${c.cyan("/todos")} Show current todo list
4194
+ ${c.cyan("/usage")} Show token usage statistics
4195
+ ${c.cyan("/exit")} Exit the CLI
4196
+
4197
+ ${c.bold("Environment:")}
4198
+ ${c.cyan("ANTHROPIC_API_KEY")} Anthropic API key (for Claude models)
4199
+ ${c.cyan("ANTHROPIC_MODEL")} Optional. Claude model (default: claude-sonnet-4-20250514)
4200
+ ${c.cyan("OPENAI_API_KEY")} OpenAI API key (for GPT models)
4201
+ ${c.cyan("OPENAI_MODEL")} Optional. OpenAI model (default: gpt-4o)
4202
+ ${c.cyan("OPENAI_BASE_URL")} Optional. Custom OpenAI-compatible API URL
4203
+
4204
+ ${c.bold("Examples:")}
4205
+ ${c.dim("# Start interactive mode")}
4206
+ npx formagent
4207
+
4208
+ ${c.dim("# Quick query")}
4209
+ npx formagent "What is the capital of France?"
4210
+
4211
+ ${c.dim("# Multi-word query")}
4212
+ npx formagent "Explain how async/await works in JavaScript"
4213
+ `);
4214
+ }
4215
+ function printVersion() {
4216
+ console.log(`formagent-sdk v${VERSION}`);
4217
+ }
4218
+ function printBanner() {
4219
+ const model = getDefaultModel();
4220
+ console.log();
4221
+ console.log(c.cyan("╔═══════════════════════════════════════════════════════════╗"));
4222
+ console.log(c.cyan("║") + c.bold(" FormAgent CLI v" + VERSION + " ") + c.cyan("║"));
4223
+ console.log(c.cyan("║") + c.dim(" AI Agent Framework ") + c.cyan("║"));
4224
+ console.log(c.cyan("╚═══════════════════════════════════════════════════════════╝"));
4225
+ console.log();
4226
+ console.log(c.dim(" Model: ") + c.green(model));
4227
+ console.log(c.dim(" Type your message and press Enter to chat."));
4228
+ console.log(c.dim(" Use /help for commands, /exit to quit."));
4229
+ console.log();
4230
+ }
4231
+ function printInteractiveHelp() {
4232
+ console.log();
4233
+ console.log(c.bold("Available Commands:"));
4234
+ console.log();
4235
+ console.log(` ${c.cyan("/help")} Show this help message`);
4236
+ console.log(` ${c.cyan("/clear")} Clear conversation history`);
4237
+ console.log(` ${c.cyan("/tools")} List available tools`);
4238
+ console.log(` ${c.cyan("/skills")} List available skills`);
4239
+ console.log(` ${c.cyan("/todos")} Show current todo list`);
4240
+ console.log(` ${c.cyan("/usage")} Show token usage statistics`);
4241
+ console.log(` ${c.cyan("/exit")} Exit the CLI`);
4242
+ console.log();
4243
+ }
4244
+ function printTools() {
4245
+ const tools = getAllTools();
4246
+ console.log();
4247
+ console.log(c.bold("Available Tools:"));
4248
+ console.log();
4249
+ for (const tool2 of tools) {
4250
+ console.log(` ${c.green("●")} ${c.bold(tool2.name)}`);
4251
+ const desc = tool2.description?.split(`
4252
+ `)[0] || "";
4253
+ console.log(` ${c.dim(desc.slice(0, 70))}${desc.length > 70 ? "..." : ""}`);
4254
+ }
4255
+ console.log();
4256
+ }
4257
+ async function printSkills() {
4258
+ console.log();
4259
+ console.log(c.bold("Available Skills:"));
4260
+ console.log(c.dim(` (from ${SKILLS_PATH})`));
4261
+ console.log();
4262
+ const loader = new SkillLoader;
4263
+ const skills = await loader.discover({
4264
+ directories: [SKILLS_PATH],
4265
+ includeUserSkills: false,
4266
+ includeProjectSkills: false,
4267
+ maxDepth: 3
4268
+ });
4269
+ if (skills.length === 0) {
4270
+ console.log(c.dim(" No skills found."));
4271
+ } else {
4272
+ for (const skill of skills) {
4273
+ const triggers = skill.triggers?.slice(0, 3).join(", ") || "none";
4274
+ console.log(` ${c.magenta("◆")} ${c.bold(skill.name)} ${c.dim(`[${skill.id}]`)}`);
4275
+ if (skill.description) {
4276
+ console.log(` ${c.dim(skill.description.slice(0, 60))}...`);
4277
+ }
4278
+ console.log(` ${c.dim("Triggers:")} ${triggers}`);
4279
+ }
4280
+ }
4281
+ console.log();
4282
+ }
4283
+ function printTodos() {
4284
+ const todos = getTodos();
4285
+ console.log();
4286
+ if (todos.length === 0) {
4287
+ console.log(c.dim(" No todos."));
4288
+ } else {
4289
+ console.log(c.bold("Current Todos:"));
4290
+ console.log();
4291
+ for (const todo of todos) {
4292
+ const icon = todo.status === "completed" ? c.green("✓") : todo.status === "in_progress" ? c.yellow("→") : c.dim("○");
4293
+ console.log(` ${icon} ${todo.content}`);
4294
+ }
4295
+ }
4296
+ console.log();
4297
+ }
4298
+ function printUsage() {
4299
+ console.log();
4300
+ console.log(c.bold("Token Usage:"));
4301
+ console.log();
4302
+ console.log(` ${c.cyan("Messages:")} ${messageCount}`);
4303
+ console.log(` ${c.cyan("Input tokens:")} ${totalInputTokens.toLocaleString()}`);
4304
+ console.log(` ${c.cyan("Output tokens:")} ${totalOutputTokens.toLocaleString()}`);
4305
+ console.log(` ${c.cyan("Total tokens:")} ${(totalInputTokens + totalOutputTokens).toLocaleString()}`);
4306
+ const inputCost = totalInputTokens / 1e6 * 3;
4307
+ const outputCost = totalOutputTokens / 1e6 * 15;
4308
+ console.log(` ${c.cyan("Est. cost:")} $${(inputCost + outputCost).toFixed(4)}`);
4309
+ console.log();
4310
+ }
4311
+ function formatToolInput(name, input) {
4312
+ switch (name) {
4313
+ case "Bash":
4314
+ return String(input.command || "");
4315
+ case "Read":
4316
+ return String(input.file_path || "");
4317
+ case "Write":
4318
+ return `${input.file_path} (${String(input.content || "").length} chars)`;
4319
+ case "Edit":
4320
+ return String(input.file_path || "");
4321
+ case "Glob":
4322
+ return `${input.pattern}${input.path ? ` in ${input.path}` : ""}`;
4323
+ case "Grep":
4324
+ return `/${input.pattern}/${input.path ? ` in ${input.path}` : ""}`;
4325
+ case "WebFetch":
4326
+ return String(input.url || "");
4327
+ case "TodoWrite":
4328
+ return `${input.todos?.length || 0} items`;
4329
+ case "Skill":
4330
+ if (input.action === "list") {
4331
+ return input.query ? `list (query: ${input.query})` : "list";
4332
+ }
4333
+ return `invoke ${input.skill_name || ""}`;
4334
+ default:
4335
+ return JSON.stringify(input).slice(0, 50);
4336
+ }
4337
+ }
4338
+ function getDefaultModel() {
4339
+ if (process.env.ANTHROPIC_API_KEY) {
4340
+ return process.env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514";
4341
+ }
4342
+ if (process.env.OPENAI_API_KEY) {
4343
+ return process.env.OPENAI_MODEL || "gpt-4o";
4344
+ }
4345
+ return "claude-sonnet-4-20250514";
4346
+ }
4347
+ async function getSession() {
4348
+ if (!session) {
4349
+ session = await createSession({
4350
+ model: getDefaultModel(),
4351
+ tools: getAllTools(),
4352
+ systemPrompt: buildSystemPrompt()
4353
+ });
4354
+ }
4355
+ return session;
4356
+ }
4357
+ async function processStream(sess) {
4358
+ let hasText = false;
4359
+ for await (const event of sess.receive()) {
4360
+ switch (event.type) {
4361
+ case "text":
4362
+ if (!hasText) {
4363
+ process.stdout.write(`
4364
+ `);
4365
+ hasText = true;
4366
+ }
4367
+ process.stdout.write(event.text);
4368
+ break;
4369
+ case "tool_use":
4370
+ const inputDisplay = formatToolInput(event.name, event.input);
4371
+ console.log(`
4372
+ ${c.yellow("⚡")} ${c.bold(event.name)} ${c.dim(inputDisplay)}`);
4373
+ break;
4374
+ case "tool_result":
4375
+ const content = typeof event.content === "string" ? event.content : JSON.stringify(event.content);
4376
+ if (event.is_error) {
4377
+ console.log(`${c.red("✗")} ${c.red("Error:")} ${content?.slice(0, 100)}`);
4378
+ } else {
4379
+ const preview = content?.split(`
4380
+ `)[0]?.slice(0, 80) || "";
4381
+ console.log(`${c.green("✓")} ${c.dim(preview)}${content && content.length > 80 ? "..." : ""}`);
4382
+ }
4383
+ break;
4384
+ case "error":
4385
+ console.log(`
4386
+ ${c.red("Error:")} ${event.error}`);
4387
+ break;
4388
+ case "stop":
4389
+ if (event.usage) {
4390
+ totalInputTokens += event.usage.input_tokens || 0;
4391
+ totalOutputTokens += event.usage.output_tokens || 0;
4392
+ }
4393
+ if (hasText) {
4394
+ console.log(`
4395
+ `);
4396
+ }
4397
+ break;
4398
+ }
4399
+ }
4400
+ messageCount++;
4401
+ }
4402
+ async function handleInput(input) {
4403
+ const trimmed = input.trim();
4404
+ if (!trimmed) {
4405
+ return true;
4406
+ }
4407
+ if (trimmed.startsWith("/")) {
4408
+ const cmd = trimmed.toLowerCase();
4409
+ switch (cmd) {
4410
+ case "/help":
4411
+ printInteractiveHelp();
4412
+ return true;
4413
+ case "/clear":
4414
+ if (session) {
4415
+ await session.close();
4416
+ session = null;
4417
+ }
4418
+ clearTodos();
4419
+ totalInputTokens = 0;
4420
+ totalOutputTokens = 0;
4421
+ messageCount = 0;
4422
+ console.log(c.green(`
4423
+ ✓ Conversation cleared.
4424
+ `));
4425
+ return true;
4426
+ case "/tools":
4427
+ printTools();
4428
+ return true;
4429
+ case "/skills":
4430
+ await printSkills();
4431
+ return true;
4432
+ case "/todos":
4433
+ printTodos();
4434
+ return true;
4435
+ case "/usage":
4436
+ printUsage();
4437
+ return true;
4438
+ case "/exit":
4439
+ case "/quit":
4440
+ case "/q":
4441
+ return false;
4442
+ default:
4443
+ console.log(c.yellow(`
4444
+ Unknown command: ${cmd}. Type /help for available commands.
4445
+ `));
4446
+ return true;
4447
+ }
4448
+ }
4449
+ try {
4450
+ const sess = await getSession();
4451
+ await sess.send(trimmed);
4452
+ await processStream(sess);
4453
+ } catch (error) {
4454
+ console.log(c.red(`
4455
+ Error: ${error instanceof Error ? error.message : String(error)}
4456
+ `));
4457
+ }
4458
+ return true;
4459
+ }
4460
+ async function runQuickQuery(query) {
4461
+ if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
4462
+ console.error(c.red("Error: No API key found"));
4463
+ console.error(c.dim("Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable"));
4464
+ process.exit(1);
4465
+ }
4466
+ try {
4467
+ const sess = await getSession();
4468
+ await sess.send(query);
4469
+ await processStream(sess);
4470
+ await sess.close();
4471
+ } catch (error) {
4472
+ console.error(c.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
4473
+ process.exit(1);
4474
+ }
4475
+ }
4476
+ async function runInteractive() {
4477
+ if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
4478
+ console.error(c.red("Error: No API key found"));
4479
+ console.error(c.dim("Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable"));
4480
+ process.exit(1);
4481
+ }
4482
+ setTodoChangeCallback(() => {});
4483
+ printBanner();
4484
+ const rl = readline.createInterface({
4485
+ input: process.stdin,
4486
+ output: process.stdout,
4487
+ terminal: true
4488
+ });
4489
+ const prompt = () => {
4490
+ rl.question(c.cyan("❯ "), async (input) => {
4491
+ const shouldContinue = await handleInput(input);
4492
+ if (shouldContinue) {
4493
+ prompt();
4494
+ } else {
4495
+ console.log(c.dim(`
4496
+ Goodbye!
4497
+ `));
4498
+ if (session) {
4499
+ await session.close();
4500
+ }
4501
+ rl.close();
4502
+ process.exit(0);
4503
+ }
4504
+ });
4505
+ };
4506
+ rl.on("SIGINT", async () => {
4507
+ console.log(c.dim(`
4508
+
4509
+ Interrupted. Goodbye!
4510
+ `));
4511
+ if (session) {
4512
+ await session.close();
4513
+ }
4514
+ process.exit(0);
4515
+ });
4516
+ prompt();
4517
+ }
4518
+ async function runCLI(args) {
4519
+ if (args.includes("--help") || args.includes("-h")) {
4520
+ printHelp();
4521
+ return;
4522
+ }
4523
+ if (args.includes("--version") || args.includes("-v")) {
4524
+ printVersion();
4525
+ return;
4526
+ }
4527
+ const query = args.filter((arg) => !arg.startsWith("-")).join(" ").trim();
4528
+ if (query) {
4529
+ await runQuickQuery(query);
4530
+ } else {
4531
+ await runInteractive();
4532
+ }
4533
+ }
4534
+
4535
+ // src/cli/index.ts
4536
+ runCLI(process.argv.slice(2)).catch((error) => {
4537
+ console.error(`\x1B[31mFatal error: ${error.message}\x1B[0m`);
4538
+ process.exit(1);
4539
+ });