@gugacoder/agentic-sdk 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/dist/agent.d.ts +2 -0
  2. package/dist/agent.js +463 -0
  3. package/dist/context/compaction.d.ts +27 -0
  4. package/dist/context/compaction.js +219 -0
  5. package/dist/context/models.d.ts +6 -0
  6. package/dist/context/models.js +41 -0
  7. package/dist/context/tokenizer.d.ts +5 -0
  8. package/dist/context/tokenizer.js +11 -0
  9. package/dist/context/usage.d.ts +11 -0
  10. package/dist/context/usage.js +49 -0
  11. package/dist/display-schemas.d.ts +1865 -0
  12. package/dist/display-schemas.js +219 -0
  13. package/dist/index.d.ts +38 -0
  14. package/dist/index.js +28 -0
  15. package/dist/middleware/logging.d.ts +2 -0
  16. package/dist/middleware/logging.js +32 -0
  17. package/dist/prompts/assembly.d.ts +13 -0
  18. package/dist/prompts/assembly.js +229 -0
  19. package/dist/providers.d.ts +19 -0
  20. package/dist/providers.js +44 -0
  21. package/dist/proxy.d.ts +2 -0
  22. package/dist/proxy.js +103 -0
  23. package/dist/schemas.d.ts +228 -0
  24. package/dist/schemas.js +51 -0
  25. package/dist/session.d.ts +7 -0
  26. package/dist/session.js +102 -0
  27. package/dist/structured.d.ts +18 -0
  28. package/dist/structured.js +38 -0
  29. package/dist/tool-repair.d.ts +21 -0
  30. package/dist/tool-repair.js +72 -0
  31. package/dist/tools/api-spec.d.ts +4 -0
  32. package/dist/tools/api-spec.js +123 -0
  33. package/dist/tools/apply-patch.d.ts +484 -0
  34. package/dist/tools/apply-patch.js +157 -0
  35. package/dist/tools/ask-user.d.ts +14 -0
  36. package/dist/tools/ask-user.js +27 -0
  37. package/dist/tools/bash.d.ts +550 -0
  38. package/dist/tools/bash.js +43 -0
  39. package/dist/tools/batch.d.ts +13 -0
  40. package/dist/tools/batch.js +84 -0
  41. package/dist/tools/brave-search.d.ts +6 -0
  42. package/dist/tools/brave-search.js +19 -0
  43. package/dist/tools/code-search.d.ts +20 -0
  44. package/dist/tools/code-search.js +42 -0
  45. package/dist/tools/diagnostics.d.ts +4 -0
  46. package/dist/tools/diagnostics.js +69 -0
  47. package/dist/tools/display.d.ts +483 -0
  48. package/dist/tools/display.js +77 -0
  49. package/dist/tools/edit.d.ts +682 -0
  50. package/dist/tools/edit.js +47 -0
  51. package/dist/tools/glob.d.ts +4 -0
  52. package/dist/tools/glob.js +42 -0
  53. package/dist/tools/grep.d.ts +6 -0
  54. package/dist/tools/grep.js +69 -0
  55. package/dist/tools/http-request.d.ts +7 -0
  56. package/dist/tools/http-request.js +98 -0
  57. package/dist/tools/index.d.ts +1611 -0
  58. package/dist/tools/index.js +46 -0
  59. package/dist/tools/job-tools.d.ts +24 -0
  60. package/dist/tools/job-tools.js +67 -0
  61. package/dist/tools/list-dir.d.ts +5 -0
  62. package/dist/tools/list-dir.js +79 -0
  63. package/dist/tools/multi-edit.d.ts +814 -0
  64. package/dist/tools/multi-edit.js +57 -0
  65. package/dist/tools/read.d.ts +5 -0
  66. package/dist/tools/read.js +33 -0
  67. package/dist/tools/task.d.ts +21 -0
  68. package/dist/tools/task.js +51 -0
  69. package/dist/tools/todo.d.ts +14 -0
  70. package/dist/tools/todo.js +60 -0
  71. package/dist/tools/web-fetch.d.ts +4 -0
  72. package/dist/tools/web-fetch.js +126 -0
  73. package/dist/tools/web-search.d.ts +22 -0
  74. package/dist/tools/web-search.js +48 -0
  75. package/dist/tools/write.d.ts +550 -0
  76. package/dist/tools/write.js +30 -0
  77. package/dist/types.d.ts +201 -0
  78. package/dist/types.js +1 -0
  79. package/package.json +43 -0
  80. package/src/agent.ts +520 -0
  81. package/src/context/compaction.ts +265 -0
  82. package/src/context/models.ts +42 -0
  83. package/src/context/tokenizer.ts +12 -0
  84. package/src/context/usage.ts +65 -0
  85. package/src/display-schemas.ts +276 -0
  86. package/src/index.ts +43 -0
  87. package/src/middleware/logging.ts +37 -0
  88. package/src/prompts/assembly.ts +263 -0
  89. package/src/prompts/identity.md +10 -0
  90. package/src/prompts/patterns.md +7 -0
  91. package/src/prompts/safety.md +7 -0
  92. package/src/prompts/tool-guide.md +9 -0
  93. package/src/prompts/tools/bash.md +7 -0
  94. package/src/prompts/tools/edit.md +7 -0
  95. package/src/prompts/tools/glob.md +7 -0
  96. package/src/prompts/tools/grep.md +7 -0
  97. package/src/prompts/tools/read.md +7 -0
  98. package/src/prompts/tools/write.md +7 -0
  99. package/src/providers.ts +58 -0
  100. package/src/proxy.ts +101 -0
  101. package/src/schemas.ts +58 -0
  102. package/src/session.ts +110 -0
  103. package/src/structured.ts +65 -0
  104. package/src/tool-repair.ts +92 -0
  105. package/src/tools/api-spec.ts +158 -0
  106. package/src/tools/apply-patch.ts +188 -0
  107. package/src/tools/ask-user.ts +40 -0
  108. package/src/tools/bash.ts +51 -0
  109. package/src/tools/batch.ts +103 -0
  110. package/src/tools/brave-search.ts +24 -0
  111. package/src/tools/code-search.ts +69 -0
  112. package/src/tools/diagnostics.ts +93 -0
  113. package/src/tools/display.ts +105 -0
  114. package/src/tools/edit.ts +55 -0
  115. package/src/tools/glob.ts +46 -0
  116. package/src/tools/grep.ts +68 -0
  117. package/src/tools/http-request.ts +103 -0
  118. package/src/tools/index.ts +48 -0
  119. package/src/tools/job-tools.ts +84 -0
  120. package/src/tools/list-dir.ts +102 -0
  121. package/src/tools/multi-edit.ts +65 -0
  122. package/src/tools/read.ts +40 -0
  123. package/src/tools/task.ts +71 -0
  124. package/src/tools/todo.ts +82 -0
  125. package/src/tools/web-fetch.ts +155 -0
  126. package/src/tools/web-search.ts +75 -0
  127. package/src/tools/write.ts +34 -0
  128. package/src/types.ts +145 -0
  129. package/tsconfig.json +17 -0
package/src/session.ts ADDED
@@ -0,0 +1,110 @@
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import { join, dirname, basename } from "node:path";
3
+ import type { ModelMessage } from "ai";
4
+
5
+ function sessionPath(dir: string): string {
6
+ return join(dir, "messages.jsonl");
7
+ }
8
+
9
+ export async function resolveRefs(
10
+ content: unknown[],
11
+ attachmentsDir: string
12
+ ): Promise<unknown[]> {
13
+ const resolved: unknown[] = [];
14
+ for (const part of content) {
15
+ if (typeof part !== "object" || part === null) {
16
+ resolved.push(part);
17
+ continue;
18
+ }
19
+ const p = part as Record<string, unknown>;
20
+ if (p._ref && (p.type === "image" || p.type === "file")) {
21
+ const filename = basename(p._ref as string);
22
+ const filePath = join(attachmentsDir, filename);
23
+ try {
24
+ const buffer = await readFile(filePath);
25
+ const base64 = buffer.toString("base64");
26
+ if (p.type === "image") {
27
+ resolved.push({ type: "image", image: base64, mimeType: p.mimeType, _ref: p._ref });
28
+ } else {
29
+ resolved.push({ type: "file", data: base64, mimeType: p.mimeType, _ref: p._ref });
30
+ }
31
+ } catch {
32
+ resolved.push({ type: "text", text: `[arquivo removido: ${filename}]` });
33
+ }
34
+ } else {
35
+ resolved.push(part);
36
+ }
37
+ }
38
+ return resolved;
39
+ }
40
+
41
+ export async function loadSession(dir: string): Promise<ModelMessage[]> {
42
+ try {
43
+ const content = await readFile(sessionPath(dir), "utf-8");
44
+ const messages = content
45
+ .split("\n")
46
+ .filter((line) => line.trim())
47
+ .map((line) => {
48
+ const { _meta, ...msg } = JSON.parse(line);
49
+ return msg as ModelMessage;
50
+ });
51
+
52
+ const attachmentsDir = join(dir, "attachments");
53
+ const result: ModelMessage[] = [];
54
+ for (const msg of messages) {
55
+ if (Array.isArray(msg.content)) {
56
+ const resolvedContent = await resolveRefs(msg.content, attachmentsDir);
57
+ result.push({ ...msg, content: resolvedContent } as ModelMessage);
58
+ } else {
59
+ result.push(msg as ModelMessage);
60
+ }
61
+ }
62
+ return result;
63
+ } catch {
64
+ return [];
65
+ }
66
+ }
67
+
68
+ export function filterOldMedia(messages: ModelMessage[], lastUserIndex: number): ModelMessage[] {
69
+ return (messages as any[]).map((msg: any, i: number) => {
70
+ if (msg.role !== "user" || i === lastUserIndex) {
71
+ return msg;
72
+ }
73
+ if (!Array.isArray(msg.content)) {
74
+ return msg;
75
+ }
76
+ const filtered = (msg.content as unknown[]).map((part) => {
77
+ if (typeof part !== "object" || part === null) return part;
78
+ const p = part as Record<string, unknown>;
79
+ if (p.type === "image") {
80
+ const name = (p._ref as string | undefined) ?? "imagem";
81
+ return { type: "text", text: `[imagem enviada: ${name}]` };
82
+ }
83
+ if (p.type === "file" && p.data !== undefined) {
84
+ const name = (p._ref as string | undefined) ?? "arquivo";
85
+ return { type: "text", text: `[arquivo enviado: ${name}]` };
86
+ }
87
+ return part;
88
+ });
89
+ return { ...msg, content: filtered as ModelMessage["content"] };
90
+ });
91
+ }
92
+
93
+ export async function saveSession(
94
+ dir: string,
95
+ messages: (ModelMessage & { _meta?: Record<string, unknown> })[]
96
+ ): Promise<void> {
97
+ const filePath = sessionPath(dir);
98
+ await mkdir(dirname(filePath), { recursive: true });
99
+ const ts = new Date().toISOString();
100
+ const jsonl =
101
+ messages
102
+ .map((m) => {
103
+ if (!(m as any)._meta) {
104
+ return JSON.stringify({ ...m, _meta: { ts } });
105
+ }
106
+ return JSON.stringify(m);
107
+ })
108
+ .join("\n") + "\n";
109
+ await writeFile(filePath, jsonl, "utf-8");
110
+ }
@@ -0,0 +1,65 @@
1
+ import { generateText, streamObject, Output } from "ai";
2
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
3
+ import type { z } from "zod";
4
+ import type { createAiProviderRegistry } from "./providers.js";
5
+
6
+ export interface AiObjectOptions<T extends z.ZodType> {
7
+ model: string;
8
+ apiKey: string;
9
+ /** Provider name (e.g. "openrouter", "groq"). Default: "openrouter" */
10
+ provider?: string;
11
+ /** Base URL override for the provider */
12
+ baseURL?: string;
13
+ schema: T;
14
+ system?: string;
15
+ prompt: string;
16
+ maxTokens?: number;
17
+ /** Provider registry para reuso. Se nao fornecido, cria um internamente (backward compat). */
18
+ providers?: ReturnType<typeof createAiProviderRegistry>;
19
+ }
20
+
21
+ export async function aiGenerateObject<T extends z.ZodType>(
22
+ options: AiObjectOptions<T>
23
+ ): Promise<z.infer<T>> {
24
+ const model = options.providers
25
+ ? options.providers.model(options.model, options.provider)
26
+ : createOpenAICompatible({
27
+ name: options.provider ?? "openrouter",
28
+ baseURL: options.baseURL ?? "https://openrouter.ai/api/v1",
29
+ apiKey: options.apiKey,
30
+ })(options.model);
31
+
32
+ const result = await generateText({
33
+ model,
34
+ output: Output.object({ schema: options.schema }),
35
+ system: options.system,
36
+ prompt: options.prompt,
37
+ maxOutputTokens: options.maxTokens,
38
+ });
39
+
40
+ return result.output!;
41
+ }
42
+
43
+ export async function* aiStreamObject<T extends z.ZodType>(
44
+ options: AiObjectOptions<T>
45
+ ): AsyncGenerator<Partial<z.infer<T>>> {
46
+ const model = options.providers
47
+ ? options.providers.model(options.model, options.provider)
48
+ : createOpenAICompatible({
49
+ name: options.provider ?? "openrouter",
50
+ baseURL: options.baseURL ?? "https://openrouter.ai/api/v1",
51
+ apiKey: options.apiKey,
52
+ })(options.model);
53
+
54
+ const result = streamObject({
55
+ model,
56
+ schema: options.schema,
57
+ system: options.system,
58
+ prompt: options.prompt,
59
+ maxOutputTokens: options.maxTokens,
60
+ });
61
+
62
+ for await (const partial of result.partialObjectStream) {
63
+ yield partial;
64
+ }
65
+ }
@@ -0,0 +1,92 @@
1
+ import { generateText } from "ai";
2
+ import type { LanguageModel } from "ai";
3
+ import type { LanguageModelV3ToolCall } from "@ai-sdk/provider";
4
+
5
+ export interface RepairContext {
6
+ model: LanguageModel;
7
+ maxAttempts: number;
8
+ }
9
+
10
+ /**
11
+ * Try to fix tool name by case-insensitive matching against available tools.
12
+ * Returns the correct name if found, or null.
13
+ */
14
+ function fixToolName(toolName: string, tools: Record<string, unknown>): string | null {
15
+ if (toolName in tools) return toolName; // already correct
16
+ const lower = toolName.toLowerCase();
17
+ for (const name of Object.keys(tools)) {
18
+ if (name.toLowerCase() === lower) return name;
19
+ }
20
+ return null;
21
+ }
22
+
23
+ /**
24
+ * Cria um handler de reparo que pede ao modelo para corrigir a tool call.
25
+ * Primeiro tenta corrigir o nome da tool (case mismatch), depois os args.
26
+ * Tenta N vezes. Se todas falharem, retorna null (deixa o erro original propagar).
27
+ */
28
+ export function createToolCallRepairHandler(ctx: RepairContext) {
29
+ const attempts = new Map<string, number>();
30
+
31
+ return async (options: {
32
+ toolCall: LanguageModelV3ToolCall;
33
+ tools: Record<string, unknown>;
34
+ inputSchema: (opts: { toolName: string }) => unknown;
35
+ error: Error;
36
+ system?: unknown;
37
+ messages?: unknown;
38
+ }): Promise<LanguageModelV3ToolCall | null> => {
39
+ const { toolCall, tools, inputSchema, error } = options;
40
+
41
+ // Fix tool name case mismatch (e.g. "Email_send" → "email_send")
42
+ const correctedName = fixToolName(toolCall.toolName, tools);
43
+ if (correctedName && correctedName !== toolCall.toolName) {
44
+ return {
45
+ type: "tool-call" as const,
46
+ toolCallId: toolCall.toolCallId,
47
+ toolName: correctedName,
48
+ input: toolCall.input,
49
+ };
50
+ }
51
+
52
+ const key = `${toolCall.toolName}:${toolCall.input}`;
53
+ const current = attempts.get(key) ?? 0;
54
+
55
+ if (current >= ctx.maxAttempts) {
56
+ return null; // desiste — erro original propaga
57
+ }
58
+ attempts.set(key, current + 1);
59
+
60
+ // If tool name is completely wrong, can't repair args
61
+ if (!correctedName) return null;
62
+
63
+ try {
64
+ const schema = await Promise.resolve(inputSchema({ toolName: toolCall.toolName }));
65
+
66
+ const result = await generateText({
67
+ model: ctx.model,
68
+ system: [
69
+ "You generated an invalid tool call. Fix the JSON arguments to match the schema.",
70
+ "Return ONLY the corrected JSON object — no explanation, no markdown.",
71
+ ].join("\n"),
72
+ prompt: [
73
+ `Tool: ${toolCall.toolName}`,
74
+ `Schema: ${JSON.stringify(schema)}`,
75
+ `Invalid args: ${toolCall.input}`,
76
+ `Error: ${error.message}`,
77
+ ].join("\n"),
78
+ maxOutputTokens: 1000,
79
+ });
80
+
81
+ const repaired = (result.text ?? toolCall.input).trim();
82
+ return {
83
+ type: "tool-call" as const,
84
+ toolCallId: toolCall.toolCallId,
85
+ toolName: toolCall.toolName,
86
+ input: repaired,
87
+ };
88
+ } catch {
89
+ return null; // reparo falhou — erro original propaga
90
+ }
91
+ };
92
+ }
@@ -0,0 +1,158 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+
4
+ const MAX_SPEC_SIZE = 512 * 1024; // 512KB
5
+
6
+ interface EndpointInfo {
7
+ method: string;
8
+ path: string;
9
+ summary?: string;
10
+ parameters?: Array<{ name: string; in: string; required?: boolean; type?: string }>;
11
+ requestBody?: string;
12
+ responses?: Record<string, string>;
13
+ }
14
+
15
+ interface SpecSummary {
16
+ title: string;
17
+ version: string;
18
+ baseUrl: string;
19
+ auth: string[];
20
+ endpoints: EndpointInfo[];
21
+ }
22
+
23
+ function extractEndpoints(spec: any): EndpointInfo[] {
24
+ const endpoints: EndpointInfo[] = [];
25
+ const paths = spec.paths ?? {};
26
+
27
+ for (const [path, methods] of Object.entries(paths)) {
28
+ if (typeof methods !== "object" || methods === null) continue;
29
+
30
+ for (const [method, op] of Object.entries(methods as Record<string, any>)) {
31
+ if (["get", "post", "put", "patch", "delete", "head", "options"].indexOf(method) === -1) continue;
32
+
33
+ const endpoint: EndpointInfo = {
34
+ method: method.toUpperCase(),
35
+ path,
36
+ summary: op.summary ?? op.description?.slice(0, 120),
37
+ };
38
+
39
+ // Parameters
40
+ if (Array.isArray(op.parameters) && op.parameters.length > 0) {
41
+ endpoint.parameters = op.parameters.map((p: any) => ({
42
+ name: p.name,
43
+ in: p.in,
44
+ required: p.required,
45
+ type: p.schema?.type ?? p.type,
46
+ }));
47
+ }
48
+
49
+ // Request body
50
+ if (op.requestBody) {
51
+ const content = op.requestBody.content;
52
+ if (content) {
53
+ const mediaType = Object.keys(content)[0];
54
+ endpoint.requestBody = mediaType;
55
+ }
56
+ }
57
+
58
+ // Responses
59
+ if (op.responses) {
60
+ endpoint.responses = {};
61
+ for (const [code, resp] of Object.entries(op.responses as Record<string, any>)) {
62
+ endpoint.responses[code] = resp.description?.slice(0, 80) ?? "";
63
+ }
64
+ }
65
+
66
+ endpoints.push(endpoint);
67
+ }
68
+ }
69
+
70
+ return endpoints;
71
+ }
72
+
73
+ function extractAuth(spec: any): string[] {
74
+ const auth: string[] = [];
75
+ const schemes = spec.components?.securitySchemes ?? spec.securityDefinitions ?? {};
76
+
77
+ for (const [name, scheme] of Object.entries(schemes as Record<string, any>)) {
78
+ const type = scheme.type ?? "unknown";
79
+ const loc = scheme.in ? ` (in ${scheme.in})` : "";
80
+ const flow = scheme.flows ? ` [${Object.keys(scheme.flows).join(", ")}]` : "";
81
+ auth.push(`${name}: ${type}${loc}${flow}`);
82
+ }
83
+
84
+ return auth;
85
+ }
86
+
87
+ function extractBaseUrl(spec: any): string {
88
+ // OpenAPI 3.x
89
+ if (Array.isArray(spec.servers) && spec.servers.length > 0) {
90
+ return spec.servers[0].url;
91
+ }
92
+ // Swagger 2.x
93
+ if (spec.host) {
94
+ const scheme = spec.schemes?.[0] ?? "https";
95
+ const basePath = spec.basePath ?? "";
96
+ return `${scheme}://${spec.host}${basePath}`;
97
+ }
98
+ return "unknown";
99
+ }
100
+
101
+ export const apiSpecTool = tool({
102
+ description:
103
+ "Fetches and parses an API specification (OpenAPI/Swagger JSON or YAML). Returns a structured summary of available endpoints, methods, parameters, and authentication requirements.",
104
+ inputSchema: z.object({
105
+ url: z
106
+ .string()
107
+ .describe("URL to the OpenAPI/Swagger spec (JSON or YAML)"),
108
+ format: z
109
+ .enum(["openapi", "auto"])
110
+ .optional()
111
+ .default("auto")
112
+ .describe("Spec format (default: auto-detect)"),
113
+ }),
114
+ execute: async ({ url }) => {
115
+ try {
116
+ const controller = new AbortController();
117
+ const timer = setTimeout(() => controller.abort(), 30_000);
118
+
119
+ const response = await fetch(url, {
120
+ signal: controller.signal,
121
+ headers: { Accept: "application/json, application/yaml, text/yaml, */*" },
122
+ });
123
+ clearTimeout(timer);
124
+
125
+ if (!response.ok) {
126
+ return `Error: HTTP ${response.status} ${response.statusText}`;
127
+ }
128
+
129
+ const text = await response.text();
130
+ if (text.length > MAX_SPEC_SIZE) {
131
+ return `Error: Spec too large (${text.length} bytes, max ${MAX_SPEC_SIZE})`;
132
+ }
133
+
134
+ // Parse JSON (YAML support would require a dependency — JSON covers most use cases)
135
+ let spec: any;
136
+ try {
137
+ spec = JSON.parse(text);
138
+ } catch {
139
+ return "Error: Could not parse spec as JSON. Provide a JSON-format OpenAPI/Swagger spec URL.";
140
+ }
141
+
142
+ const summary: SpecSummary = {
143
+ title: spec.info?.title ?? "Untitled API",
144
+ version: spec.info?.version ?? "unknown",
145
+ baseUrl: extractBaseUrl(spec),
146
+ auth: extractAuth(spec),
147
+ endpoints: extractEndpoints(spec),
148
+ };
149
+
150
+ return JSON.stringify(summary, null, 2);
151
+ } catch (err: any) {
152
+ if (err.name === "AbortError") {
153
+ return "Error: Request timed out after 30s";
154
+ }
155
+ return `Error: ${err.message}`;
156
+ }
157
+ },
158
+ });
@@ -0,0 +1,188 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { readFile, writeFile, unlink, mkdir } from "node:fs/promises";
4
+ import { dirname } from "node:path";
5
+
6
+ interface PatchOperation {
7
+ type: "add" | "update" | "delete";
8
+ path: string;
9
+ lines: string[];
10
+ }
11
+
12
+ function parsePatch(patch: string): PatchOperation[] {
13
+ const lines = patch.split("\n");
14
+ const operations: PatchOperation[] = [];
15
+ let current: PatchOperation | null = null;
16
+ let started = false;
17
+
18
+ for (const line of lines) {
19
+ if (line.trim() === "*** Begin Patch") {
20
+ started = true;
21
+ continue;
22
+ }
23
+
24
+ if (line.trim() === "*** End Patch") {
25
+ if (current) operations.push(current);
26
+ break;
27
+ }
28
+
29
+ if (!started) continue;
30
+
31
+ if (line.startsWith("*** Add File: ")) {
32
+ if (current) operations.push(current);
33
+ current = { type: "add", path: line.slice("*** Add File: ".length).trim(), lines: [] };
34
+ continue;
35
+ }
36
+
37
+ if (line.startsWith("*** Update File: ")) {
38
+ if (current) operations.push(current);
39
+ current = { type: "update", path: line.slice("*** Update File: ".length).trim(), lines: [] };
40
+ continue;
41
+ }
42
+
43
+ if (line.startsWith("*** Delete File: ")) {
44
+ if (current) operations.push(current);
45
+ current = { type: "delete", path: line.slice("*** Delete File: ".length).trim(), lines: [] };
46
+ continue;
47
+ }
48
+
49
+ if (current) {
50
+ current.lines.push(line);
51
+ }
52
+ }
53
+
54
+ // Handle case where End Patch is missing but we have a pending operation
55
+ if (current && !operations.includes(current)) {
56
+ operations.push(current);
57
+ }
58
+
59
+ return operations;
60
+ }
61
+
62
+ function applyUpdate(original: string, patchLines: string[]): string {
63
+ const originalLines = original.split("\n");
64
+ const result: string[] = [];
65
+ let originalIdx = 0;
66
+
67
+ let i = 0;
68
+ while (i < patchLines.length) {
69
+ const line = patchLines[i];
70
+
71
+ if (line.startsWith("@@")) {
72
+ // Context marker — find the context line in the original to sync position
73
+ const contextText = line.slice(3); // skip "@@ "
74
+ // Advance in original until we find a line matching the context
75
+ while (originalIdx < originalLines.length) {
76
+ if (originalLines[originalIdx] === contextText) {
77
+ break;
78
+ }
79
+ result.push(originalLines[originalIdx]);
80
+ originalIdx++;
81
+ }
82
+ // Push the context line itself
83
+ if (originalIdx < originalLines.length) {
84
+ result.push(originalLines[originalIdx]);
85
+ originalIdx++;
86
+ }
87
+ i++;
88
+ continue;
89
+ }
90
+
91
+ if (line.startsWith("-")) {
92
+ // Remove line — skip it in original
93
+ originalIdx++;
94
+ i++;
95
+ continue;
96
+ }
97
+
98
+ if (line.startsWith("+")) {
99
+ // Add line
100
+ result.push(line.slice(1));
101
+ i++;
102
+ continue;
103
+ }
104
+
105
+ // Unrecognized line in patch — treat as context (copy from original)
106
+ if (line.startsWith(" ")) {
107
+ result.push(originalLines[originalIdx] ?? line.slice(1));
108
+ originalIdx++;
109
+ }
110
+ i++;
111
+ }
112
+
113
+ // Copy remaining original lines
114
+ while (originalIdx < originalLines.length) {
115
+ result.push(originalLines[originalIdx]);
116
+ originalIdx++;
117
+ }
118
+
119
+ return result.join("\n");
120
+ }
121
+
122
+ export function createApplyPatchTool(opts?: { autoApprove?: boolean }) {
123
+ const baseTool = tool({
124
+ description:
125
+ "Applies a multi-file patch in envelope format (Begin Patch / End Patch). Supports Add File (create new), Update File (apply diff with @@ context and +/- lines), and Delete File operations.",
126
+ inputSchema: z.object({
127
+ patch: z
128
+ .string()
129
+ .describe(
130
+ 'The patch text in envelope format: "*** Begin Patch" / "*** End Patch" with "*** Add File: path", "*** Update File: path", "*** Delete File: path" sections'
131
+ ),
132
+ }),
133
+ execute: async ({ patch }) => {
134
+ try {
135
+ const operations = parsePatch(patch);
136
+
137
+ if (operations.length === 0) {
138
+ return "Error: no valid operations found in patch. Ensure the patch uses the *** Begin Patch / *** End Patch envelope format.";
139
+ }
140
+
141
+ const results: string[] = [];
142
+
143
+ for (const op of operations) {
144
+ switch (op.type) {
145
+ case "add": {
146
+ const content = op.lines
147
+ .filter((l) => l.startsWith("+"))
148
+ .map((l) => l.slice(1))
149
+ .join("\n");
150
+ await mkdir(dirname(op.path), { recursive: true });
151
+ await writeFile(op.path, content, "utf-8");
152
+ results.push(`Added: ${op.path}`);
153
+ break;
154
+ }
155
+
156
+ case "update": {
157
+ const original = await readFile(op.path, "utf-8");
158
+ const updated = applyUpdate(original, op.lines);
159
+ await writeFile(op.path, updated, "utf-8");
160
+ results.push(`Updated: ${op.path}`);
161
+ break;
162
+ }
163
+
164
+ case "delete": {
165
+ await unlink(op.path);
166
+ results.push(`Deleted: ${op.path}`);
167
+ break;
168
+ }
169
+ }
170
+ }
171
+
172
+ return `Patch applied successfully:\n${results.join("\n")}`;
173
+ } catch (err: any) {
174
+ return `Error applying patch: ${err.message}`;
175
+ }
176
+ },
177
+ });
178
+
179
+ if (opts?.autoApprove === false) {
180
+ return Object.assign(baseTool, {
181
+ needsApproval: async () => true as const,
182
+ });
183
+ }
184
+
185
+ return baseTool;
186
+ }
187
+
188
+ export const applyPatchTool = createApplyPatchTool();
@@ -0,0 +1,40 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+
4
+ /**
5
+ * Callback type for the AskUser tool.
6
+ * The consumer (CLI, API, UI) implements this to present the question
7
+ * to the user and return their answer.
8
+ */
9
+ export type AskUserCallback = (
10
+ question: string,
11
+ options?: string[]
12
+ ) => Promise<string>;
13
+
14
+ /**
15
+ * Factory that creates the AskUser tool with an injected callback.
16
+ * If no callback is provided, returns a tool that explains no handler is configured.
17
+ */
18
+ export function createAskUserTool(onAskUser?: AskUserCallback) {
19
+ return tool({
20
+ description:
21
+ "Ask the user a question during execution. Use this to request clarifications, preferences, or decisions before proceeding. The user will see the question and can respond.",
22
+ inputSchema: z.object({
23
+ question: z
24
+ .string()
25
+ .describe("The question to ask the user"),
26
+ options: z
27
+ .array(z.string())
28
+ .optional()
29
+ .describe("Optional list of answer choices to present to the user"),
30
+ }),
31
+ execute: async ({ question, options }) => {
32
+ if (!onAskUser) {
33
+ return "AskUser handler not configured. The consuming application must provide an onAskUser callback in AiAgentOptions to enable user interaction.";
34
+ }
35
+
36
+ const answer = await onAskUser(question, options);
37
+ return answer;
38
+ },
39
+ });
40
+ }
@@ -0,0 +1,51 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { exec } from "node:child_process";
4
+
5
+ const MAX_OUTPUT = 30_000;
6
+ const DEFAULT_TIMEOUT = 120_000;
7
+
8
+ export function createBashTool(opts?: { autoApprove?: boolean }) {
9
+ const baseTool = tool({
10
+ description:
11
+ "Executes a bash command and returns stdout/stderr. Timeout defaults to 120s.",
12
+ inputSchema: z.object({
13
+ command: z.string().describe("The bash command to execute"),
14
+ timeout: z
15
+ .number()
16
+ .optional()
17
+ .describe("Timeout in milliseconds (max 600000)"),
18
+ }),
19
+ execute: async ({ command, timeout }) => {
20
+ const ms = Math.min(timeout ?? DEFAULT_TIMEOUT, 600_000);
21
+
22
+ return new Promise<string>((resolve) => {
23
+ exec(command, { timeout: ms, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
24
+ let output = "";
25
+
26
+ if (stdout) output += stdout;
27
+ if (stderr) output += (output ? "\n" : "") + stderr;
28
+ if (err && !stdout && !stderr) {
29
+ output = `Error: ${err.message}`;
30
+ }
31
+
32
+ if (output.length > MAX_OUTPUT) {
33
+ output = output.slice(0, MAX_OUTPUT) + "\n...[truncated]";
34
+ }
35
+
36
+ resolve(output || "(no output)");
37
+ });
38
+ });
39
+ },
40
+ });
41
+
42
+ if (opts?.autoApprove === false) {
43
+ return Object.assign(baseTool, {
44
+ needsApproval: async () => true as const,
45
+ });
46
+ }
47
+
48
+ return baseTool;
49
+ }
50
+
51
+ export const bashTool = createBashTool();