@kirrosh/apitool 0.4.3

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 (191) hide show
  1. package/.github/workflows/ci.yml +27 -0
  2. package/.github/workflows/release.yml +97 -0
  3. package/.mcp.json +9 -0
  4. package/APITOOL.md +195 -0
  5. package/BACKLOG.md +62 -0
  6. package/CHANGELOG.md +88 -0
  7. package/LICENSE +21 -0
  8. package/README.md +105 -0
  9. package/bun.lock +291 -0
  10. package/docs/GLOSSARY.md +182 -0
  11. package/docs/INDEX.md +21 -0
  12. package/docs/agent.md +135 -0
  13. package/docs/archive/APITOOL-pre-M22.md +831 -0
  14. package/docs/archive/BACKLOG-AI-NATIVE.md +56 -0
  15. package/docs/archive/M1-M2-parser-runner.md +216 -0
  16. package/docs/archive/M4-M7-reporter-cli.md +179 -0
  17. package/docs/archive/M5-M7-storage-junit.md +300 -0
  18. package/docs/archive/M6-webui.md +339 -0
  19. package/docs/ci.md +274 -0
  20. package/docs/generation-issues.md +67 -0
  21. package/generated/.env.yaml +3 -0
  22. package/install.ps1 +80 -0
  23. package/install.sh +113 -0
  24. package/package.json +46 -0
  25. package/scripts/run-mocked-tests.ts +45 -0
  26. package/seed-demo.ts +53 -0
  27. package/self-tests/auth.yaml +18 -0
  28. package/self-tests/collections-crud.yaml +46 -0
  29. package/self-tests/environments-crud.yaml +48 -0
  30. package/self-tests/export.yaml +32 -0
  31. package/self-tests/runs.yaml +16 -0
  32. package/src/bun-types.d.ts +5 -0
  33. package/src/cli/commands/add-api.ts +51 -0
  34. package/src/cli/commands/ai-generate.ts +106 -0
  35. package/src/cli/commands/chat.ts +43 -0
  36. package/src/cli/commands/ci-init.ts +126 -0
  37. package/src/cli/commands/collections.ts +41 -0
  38. package/src/cli/commands/coverage.ts +65 -0
  39. package/src/cli/commands/doctor.ts +127 -0
  40. package/src/cli/commands/envs.ts +218 -0
  41. package/src/cli/commands/init.ts +84 -0
  42. package/src/cli/commands/mcp.ts +16 -0
  43. package/src/cli/commands/run.ts +137 -0
  44. package/src/cli/commands/runs.ts +108 -0
  45. package/src/cli/commands/serve.ts +22 -0
  46. package/src/cli/commands/update.ts +142 -0
  47. package/src/cli/commands/validate.ts +18 -0
  48. package/src/cli/index.ts +500 -0
  49. package/src/cli/output.ts +24 -0
  50. package/src/cli/runtime.ts +7 -0
  51. package/src/core/agent/agent-loop.ts +116 -0
  52. package/src/core/agent/context-manager.ts +41 -0
  53. package/src/core/agent/system-prompt.ts +33 -0
  54. package/src/core/agent/tools/diagnose-failure.ts +51 -0
  55. package/src/core/agent/tools/explore-api.ts +40 -0
  56. package/src/core/agent/tools/index.ts +48 -0
  57. package/src/core/agent/tools/manage-environment.ts +40 -0
  58. package/src/core/agent/tools/query-results.ts +40 -0
  59. package/src/core/agent/tools/run-tests.ts +38 -0
  60. package/src/core/agent/tools/send-request.ts +44 -0
  61. package/src/core/agent/tools/validate-tests.ts +23 -0
  62. package/src/core/agent/types.ts +22 -0
  63. package/src/core/generator/ai/ai-generator.ts +61 -0
  64. package/src/core/generator/ai/llm-client.ts +159 -0
  65. package/src/core/generator/ai/output-parser.ts +307 -0
  66. package/src/core/generator/ai/prompt-builder.ts +153 -0
  67. package/src/core/generator/ai/types.ts +56 -0
  68. package/src/core/generator/coverage-scanner.ts +87 -0
  69. package/src/core/generator/data-factory.ts +115 -0
  70. package/src/core/generator/index.ts +10 -0
  71. package/src/core/generator/openapi-reader.ts +142 -0
  72. package/src/core/generator/schema-utils.ts +52 -0
  73. package/src/core/generator/serializer.ts +189 -0
  74. package/src/core/generator/types.ts +47 -0
  75. package/src/core/parser/filter.ts +14 -0
  76. package/src/core/parser/index.ts +21 -0
  77. package/src/core/parser/schema.ts +175 -0
  78. package/src/core/parser/types.ts +50 -0
  79. package/src/core/parser/variables.ts +146 -0
  80. package/src/core/parser/yaml-parser.ts +85 -0
  81. package/src/core/reporter/console.ts +175 -0
  82. package/src/core/reporter/index.ts +23 -0
  83. package/src/core/reporter/json.ts +9 -0
  84. package/src/core/reporter/junit.ts +78 -0
  85. package/src/core/reporter/types.ts +12 -0
  86. package/src/core/runner/assertions.ts +172 -0
  87. package/src/core/runner/execute-run.ts +75 -0
  88. package/src/core/runner/executor.ts +150 -0
  89. package/src/core/runner/http-client.ts +69 -0
  90. package/src/core/runner/index.ts +12 -0
  91. package/src/core/runner/types.ts +48 -0
  92. package/src/core/setup-api.ts +97 -0
  93. package/src/core/utils.ts +9 -0
  94. package/src/db/queries.ts +868 -0
  95. package/src/db/schema.ts +215 -0
  96. package/src/mcp/server.ts +47 -0
  97. package/src/mcp/tools/ci-init.ts +57 -0
  98. package/src/mcp/tools/coverage-analysis.ts +58 -0
  99. package/src/mcp/tools/explore-api.ts +84 -0
  100. package/src/mcp/tools/generate-missing-tests.ts +80 -0
  101. package/src/mcp/tools/generate-tests-guide.ts +353 -0
  102. package/src/mcp/tools/manage-environment.ts +123 -0
  103. package/src/mcp/tools/manage-server.ts +87 -0
  104. package/src/mcp/tools/query-db.ts +141 -0
  105. package/src/mcp/tools/run-tests.ts +66 -0
  106. package/src/mcp/tools/save-test-suite.ts +164 -0
  107. package/src/mcp/tools/send-request.ts +53 -0
  108. package/src/mcp/tools/setup-api.ts +49 -0
  109. package/src/mcp/tools/validate-tests.ts +42 -0
  110. package/src/tui/chat-ui.ts +150 -0
  111. package/src/web/routes/api.ts +234 -0
  112. package/src/web/routes/dashboard.ts +348 -0
  113. package/src/web/routes/runs.ts +64 -0
  114. package/src/web/schemas.ts +121 -0
  115. package/src/web/server.ts +134 -0
  116. package/src/web/static/htmx.min.js +1 -0
  117. package/src/web/static/style.css +265 -0
  118. package/src/web/views/layout.ts +46 -0
  119. package/src/web/views/results.ts +209 -0
  120. package/tests/agent/agent-loop.test.ts +61 -0
  121. package/tests/agent/context-manager.test.ts +59 -0
  122. package/tests/agent/system-prompt.test.ts +42 -0
  123. package/tests/agent/tools/diagnose-failure.test.ts +85 -0
  124. package/tests/agent/tools/explore-api.test.ts +59 -0
  125. package/tests/agent/tools/manage-environment.test.ts +78 -0
  126. package/tests/agent/tools/query-results.test.ts +77 -0
  127. package/tests/agent/tools/run-tests.test.ts +89 -0
  128. package/tests/agent/tools/send-request.test.ts +78 -0
  129. package/tests/agent/tools/validate-tests.test.ts +59 -0
  130. package/tests/ai/ai-generator.integration.test.ts +131 -0
  131. package/tests/ai/llm-client.test.ts +145 -0
  132. package/tests/ai/output-parser.test.ts +132 -0
  133. package/tests/ai/prompt-builder.test.ts +67 -0
  134. package/tests/ai/types.test.ts +55 -0
  135. package/tests/cli/args.test.ts +63 -0
  136. package/tests/cli/chat.test.ts +38 -0
  137. package/tests/cli/ci-init.test.ts +112 -0
  138. package/tests/cli/commands.test.ts +316 -0
  139. package/tests/cli/coverage.test.ts +58 -0
  140. package/tests/cli/doctor.test.ts +39 -0
  141. package/tests/cli/envs.test.ts +181 -0
  142. package/tests/cli/init.test.ts +80 -0
  143. package/tests/cli/runs.test.ts +94 -0
  144. package/tests/cli/safe-run.test.ts +103 -0
  145. package/tests/cli/update.test.ts +32 -0
  146. package/tests/core/generator/schema-utils.test.ts +108 -0
  147. package/tests/core/parser/nested-assertions.test.ts +80 -0
  148. package/tests/core/runner/root-body-assertions.test.ts +70 -0
  149. package/tests/db/chat-queries.test.ts +88 -0
  150. package/tests/db/chat-schema.test.ts +37 -0
  151. package/tests/db/environments.test.ts +131 -0
  152. package/tests/db/queries.test.ts +409 -0
  153. package/tests/db/schema.test.ts +141 -0
  154. package/tests/fixtures/.env.yaml +3 -0
  155. package/tests/fixtures/auth-token-test.yaml +8 -0
  156. package/tests/fixtures/bail/suite-a.yaml +6 -0
  157. package/tests/fixtures/bail/suite-b.yaml +6 -0
  158. package/tests/fixtures/crud.yaml +35 -0
  159. package/tests/fixtures/invalid-missing-name.yaml +5 -0
  160. package/tests/fixtures/invalid-no-method.yaml +6 -0
  161. package/tests/fixtures/petstore-auth.json +295 -0
  162. package/tests/fixtures/petstore-simple.json +151 -0
  163. package/tests/fixtures/post-only.yaml +12 -0
  164. package/tests/fixtures/simple.yaml +6 -0
  165. package/tests/fixtures/valid/.env.yaml +1 -0
  166. package/tests/fixtures/valid/a.yaml +5 -0
  167. package/tests/fixtures/valid/b.yml +5 -0
  168. package/tests/generator/coverage-scanner.test.ts +129 -0
  169. package/tests/generator/data-factory.test.ts +133 -0
  170. package/tests/generator/openapi-reader.test.ts +131 -0
  171. package/tests/integration/auth-flow.test.ts +217 -0
  172. package/tests/mcp/coverage-analysis.test.ts +64 -0
  173. package/tests/mcp/explore-api-schemas.test.ts +105 -0
  174. package/tests/mcp/explore-api.test.ts +49 -0
  175. package/tests/mcp/generate-missing-tests.test.ts +69 -0
  176. package/tests/mcp/manage-environment.test.ts +89 -0
  177. package/tests/mcp/save-test-suite.test.ts +116 -0
  178. package/tests/mcp/send-request.test.ts +79 -0
  179. package/tests/mcp/setup-api.test.ts +106 -0
  180. package/tests/mcp/tools.test.ts +248 -0
  181. package/tests/parser/schema.test.ts +134 -0
  182. package/tests/parser/variables.test.ts +227 -0
  183. package/tests/parser/yaml-parser.test.ts +69 -0
  184. package/tests/reporter/console.test.ts +256 -0
  185. package/tests/reporter/json.test.ts +98 -0
  186. package/tests/reporter/junit.test.ts +284 -0
  187. package/tests/runner/assertions.test.ts +262 -0
  188. package/tests/runner/executor.test.ts +310 -0
  189. package/tests/runner/http-client.test.ts +138 -0
  190. package/tests/web/routes.test.ts +160 -0
  191. package/tsconfig.json +31 -0
@@ -0,0 +1,24 @@
1
+ const RESET = "\x1b[0m";
2
+ const RED = "\x1b[31m";
3
+ const GREEN = "\x1b[32m";
4
+ const YELLOW = "\x1b[33m";
5
+
6
+ function useColor(): boolean {
7
+ return process.stderr.isTTY ?? false;
8
+ }
9
+
10
+ export function printError(message: string): void {
11
+ const msg = useColor() ? `${RED}Error: ${message}${RESET}` : `Error: ${message}`;
12
+ process.stderr.write(msg + "\n");
13
+ }
14
+
15
+ export function printSuccess(message: string): void {
16
+ const color = process.stdout.isTTY ?? false;
17
+ const msg = color ? `${GREEN}${message}${RESET}` : message;
18
+ process.stdout.write(msg + "\n");
19
+ }
20
+
21
+ export function printWarning(message: string): void {
22
+ const msg = useColor() ? `${YELLOW}Warning: ${message}${RESET}` : `Warning: ${message}`;
23
+ process.stderr.write(msg + "\n");
24
+ }
@@ -0,0 +1,7 @@
1
+ export function isCompiledBinary(): boolean {
2
+ return process.argv[0] === "bun" && import.meta.path.includes("~BUN");
3
+ }
4
+
5
+ export function getRuntimeInfo(): string {
6
+ return isCompiledBinary() ? "standalone" : "bun";
7
+ }
@@ -0,0 +1,116 @@
1
+ // Suppress AI SDK v2 spec compatibility warnings for Ollama (cosmetic, tool calling works fine)
2
+ (globalThis as any).AI_SDK_LOG_WARNINGS = false;
3
+
4
+ import { generateText, stepCountIs } from "ai";
5
+ import { createOpenAI } from "@ai-sdk/openai";
6
+ import { createAnthropic } from "@ai-sdk/anthropic";
7
+ import { AGENT_SYSTEM_PROMPT } from "./system-prompt.ts";
8
+ import { buildAgentTools } from "./tools/index.ts";
9
+ import type { AgentConfig, AgentTurnResult, ToolEvent } from "./types.ts";
10
+ import type { ModelMessage } from "ai";
11
+
12
+ export function buildProvider(config: AgentConfig) {
13
+ const { provider } = config.provider;
14
+
15
+ if (provider === "anthropic") {
16
+ return createAnthropic({
17
+ apiKey: config.provider.apiKey,
18
+ baseURL: config.provider.baseUrl || undefined,
19
+ });
20
+ }
21
+
22
+ // openai, ollama, custom all use OpenAI-compatible API
23
+ return createOpenAI({
24
+ apiKey: config.provider.apiKey ?? "ollama",
25
+ baseURL: config.provider.baseUrl,
26
+ });
27
+ }
28
+
29
+ function buildModel(config: AgentConfig) {
30
+ const provider = buildProvider(config);
31
+ const { provider: providerType } = config.provider;
32
+
33
+ // For ollama/custom, use .chat() to avoid the responses API which they don't support.
34
+ if (providerType === "ollama" || providerType === "custom") {
35
+ return (provider as ReturnType<typeof createOpenAI>).chat(config.provider.model);
36
+ }
37
+
38
+ return provider(config.provider.model);
39
+ }
40
+
41
+ /**
42
+ * Prepare messages with system prompt.
43
+ * Some small/local models (e.g. qwen3 thinking mode via Ollama) break tool calling
44
+ * when a separate `system` message is present. For ollama/custom providers, we inject
45
+ * the system prompt into the first user message instead.
46
+ */
47
+ function prepareMessages(
48
+ messages: ModelMessage[],
49
+ config: AgentConfig,
50
+ ): { system?: string; messages: ModelMessage[] } {
51
+ const { provider } = config.provider;
52
+
53
+ if (provider === "ollama" || provider === "custom") {
54
+ // Inject system prompt into first user message to avoid breaking tool calling
55
+ const prepared = [...messages];
56
+ const firstUserIdx = prepared.findIndex(
57
+ (m) => m.role === "user" && typeof m.content === "string",
58
+ );
59
+
60
+ if (firstUserIdx >= 0) {
61
+ const msg = prepared[firstUserIdx] as { role: "user"; content: string };
62
+ prepared[firstUserIdx] = {
63
+ ...msg,
64
+ content: `[System instructions]\n${AGENT_SYSTEM_PROMPT}\n[End instructions]\n\n${msg.content}`,
65
+ };
66
+ }
67
+
68
+ return { messages: prepared };
69
+ }
70
+
71
+ // For OpenAI/Anthropic, use the standard system parameter
72
+ return { system: AGENT_SYSTEM_PROMPT, messages };
73
+ }
74
+
75
+ export async function runAgentTurn(
76
+ messages: ModelMessage[],
77
+ config: AgentConfig,
78
+ onToolEvent?: (event: ToolEvent) => void,
79
+ ): Promise<AgentTurnResult> {
80
+ const model = buildModel(config);
81
+ const tools = buildAgentTools(config);
82
+ const { system, messages: prepared } = prepareMessages(messages, config);
83
+ const toolEvents: ToolEvent[] = [];
84
+
85
+ const result = await generateText({
86
+ model,
87
+ system,
88
+ messages: prepared,
89
+ tools,
90
+ stopWhen: stepCountIs(config.maxSteps ?? 10),
91
+ maxOutputTokens: config.provider.maxTokens ?? 4096,
92
+ onStepFinish: ({ toolCalls, toolResults }) => {
93
+ if (toolCalls) {
94
+ for (let i = 0; i < toolCalls.length; i++) {
95
+ const call = toolCalls[i]!;
96
+ const toolResult = toolResults?.[i];
97
+ const event: ToolEvent = {
98
+ toolName: call.toolName,
99
+ args: ("input" in call ? call.input : {}) as Record<string, unknown>,
100
+ result: toolResult ?? null,
101
+ timestamp: new Date().toISOString(),
102
+ };
103
+ toolEvents.push(event);
104
+ onToolEvent?.(event);
105
+ }
106
+ }
107
+ },
108
+ });
109
+
110
+ return {
111
+ text: result.text,
112
+ toolEvents,
113
+ inputTokens: result.usage?.inputTokens ?? 0,
114
+ outputTokens: result.usage?.outputTokens ?? 0,
115
+ };
116
+ }
@@ -0,0 +1,41 @@
1
+ import type { CoreMessageFormat } from "../../db/queries.ts";
2
+
3
+ const MAX_MESSAGES = 20;
4
+ const KEEP_RECENT_TURNS = 6; // 6 turns = 12 messages (user + assistant pairs)
5
+ const KEEP_RECENT_MESSAGES = KEEP_RECENT_TURNS * 2;
6
+
7
+ export function trimContext(messages: CoreMessageFormat[]): CoreMessageFormat[] {
8
+ if (messages.length <= MAX_MESSAGES) {
9
+ return messages;
10
+ }
11
+
12
+ const oldMessages = messages.slice(0, messages.length - KEEP_RECENT_MESSAGES);
13
+ const recentMessages = messages.slice(messages.length - KEEP_RECENT_MESSAGES);
14
+
15
+ const summary = buildSummary(oldMessages);
16
+
17
+ // Use role "user" for the summary so that the conversation always starts with a user message.
18
+ // Some providers require conversations to begin with a user turn.
19
+ return [
20
+ { role: "user" as const, content: summary },
21
+ ...recentMessages,
22
+ ];
23
+ }
24
+
25
+ function buildSummary(messages: CoreMessageFormat[]): string {
26
+ const userMessages = messages.filter((m) => m.role === "user");
27
+ const topics = userMessages
28
+ .map((m) => m.content.slice(0, 80))
29
+ .slice(0, 5);
30
+
31
+ const topicList = topics.length > 0
32
+ ? topics.map((t) => `- ${t}`).join("\n")
33
+ : "- General conversation";
34
+
35
+ return `[Conversation summary — ${messages.length} earlier messages condensed]
36
+
37
+ Topics discussed:
38
+ ${topicList}
39
+
40
+ The conversation continues below with the most recent messages.`;
41
+ }
@@ -0,0 +1,33 @@
1
+ export const AGENT_SYSTEM_PROMPT = `You are an API testing assistant powered by apitool. You help users run, create, validate, and diagnose API tests.
2
+
3
+ You have access to the following tools:
4
+
5
+ - **run_tests**: Execute API test suites from YAML files or directories. Returns pass/fail summary with run ID.
6
+ - **validate_tests**: Validate YAML test files without executing them. Check syntax and structure.
7
+ - **query_results**: Query historical test run results and collections from the database.
8
+ - **manage_environment**: List, get, or set environment variables used during test execution.
9
+ - **diagnose_failure**: Analyze a failed test run to identify root causes and suggest fixes.
10
+
11
+ Tool usage examples:
12
+ - run_tests: { testPath: "tests/api.yaml" } or { testPath: "tests/", envName: "staging", safe: true }
13
+ - validate_tests: { testPath: "tests/api.yaml" }
14
+ - query_results: action must be "list_runs", "get_run" (requires runId), or "list_collections"
15
+ - List runs: { action: "list_runs", limit: 10 }
16
+ - Get run details: { action: "get_run", runId: 1 }
17
+ - List collections: { action: "list_collections" }
18
+ - manage_environment: action must be "list", "get" (requires name), or "set" (requires name + variables)
19
+ - List environments: { action: "list" }
20
+ - Get environment: { action: "get", name: "staging" }
21
+ - Set variables: { action: "set", name: "staging", variables: { "base_url": "https://api.example.com" } }
22
+ - diagnose_failure: { runId: 1 }
23
+
24
+ Guidelines:
25
+ - When asked to run tests, use the run_tests tool and report results clearly.
26
+ - When a test run has failures, proactively use diagnose_failure to analyze the issues.
27
+ - When asked about past results, use query_results to look up run history.
28
+ - Always provide actionable suggestions when tests fail.
29
+ - Be concise but thorough in your explanations.
30
+ - If a tool call fails with a validation error, re-read the tool schema and retry with corrected arguments.
31
+ - When in safe mode, only GET (read-only) tests will be executed.
32
+ - When using thinking/reasoning, keep your internal reasoning focused and share conclusions with the user.
33
+ `;
@@ -0,0 +1,51 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { getDb } from "../../../db/schema.ts";
4
+ import { getRunById, getResultsByRunId } from "../../../db/queries.ts";
5
+
6
+ export const diagnoseFailureTool = tool({
7
+ description: "Diagnose failures in a test run by analyzing failed steps and their errors",
8
+ inputSchema: z.object({
9
+ runId: z.number().describe("Run ID to diagnose"),
10
+ }),
11
+ execute: async (args) => {
12
+ try {
13
+ getDb();
14
+
15
+ const run = getRunById(args.runId);
16
+ if (!run) return { error: `Run ${args.runId} not found` };
17
+
18
+ const results = getResultsByRunId(args.runId);
19
+ const failures = results
20
+ .filter((r) => r.status === "fail" || r.status === "error")
21
+ .map((r) => ({
22
+ suite_name: r.suite_name,
23
+ test_name: r.test_name,
24
+ status: r.status,
25
+ error_message: r.error_message,
26
+ request_method: r.request_method,
27
+ request_url: r.request_url,
28
+ response_status: r.response_status,
29
+ assertions: r.assertions,
30
+ duration_ms: r.duration_ms,
31
+ }));
32
+
33
+ return {
34
+ run: {
35
+ id: run.id,
36
+ started_at: run.started_at,
37
+ environment: run.environment,
38
+ duration_ms: run.duration_ms,
39
+ },
40
+ summary: {
41
+ total: run.total,
42
+ passed: run.passed,
43
+ failed: run.failed,
44
+ },
45
+ failures,
46
+ };
47
+ } catch (err) {
48
+ return { error: (err as Error).message };
49
+ }
50
+ },
51
+ });
@@ -0,0 +1,40 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { readOpenApiSpec, extractEndpoints, extractSecuritySchemes } from "../../generator/index.ts";
4
+
5
+ export const exploreApiTool = tool({
6
+ description: "Explore an OpenAPI spec — list endpoints with method, path, and summary. Optionally filter by tag.",
7
+ inputSchema: z.object({
8
+ specPath: z.string().describe("Path to OpenAPI spec file (JSON or YAML)"),
9
+ tag: z.string().optional().describe("Filter endpoints by tag"),
10
+ }),
11
+ execute: async (args) => {
12
+ try {
13
+ const doc = await readOpenApiSpec(args.specPath);
14
+ const allEndpoints = extractEndpoints(doc);
15
+ const securitySchemes = extractSecuritySchemes(doc);
16
+ const servers = ((doc as any).servers ?? []) as Array<{ url: string }>;
17
+
18
+ const endpoints = args.tag
19
+ ? allEndpoints.filter(ep => ep.tags.includes(args.tag!))
20
+ : allEndpoints;
21
+
22
+ // Compact output — method + path + summary only
23
+ return {
24
+ title: (doc as any).info?.title,
25
+ version: (doc as any).info?.version,
26
+ servers: servers.map(s => s.url),
27
+ securitySchemes: securitySchemes.map(s => s.name),
28
+ totalEndpoints: allEndpoints.length,
29
+ ...(args.tag ? { filteredByTag: args.tag, matchingEndpoints: endpoints.length } : {}),
30
+ endpoints: endpoints.map(ep => ({
31
+ method: ep.method,
32
+ path: ep.path,
33
+ summary: ep.summary,
34
+ })),
35
+ };
36
+ } catch (err) {
37
+ return { error: (err as Error).message };
38
+ }
39
+ },
40
+ });
@@ -0,0 +1,48 @@
1
+ import { tool } from "ai";
2
+ import { runTestsTool } from "./run-tests.ts";
3
+ import { validateTestsTool } from "./validate-tests.ts";
4
+ import { queryResultsTool } from "./query-results.ts";
5
+ import { manageEnvironmentTool } from "./manage-environment.ts";
6
+ import { diagnoseFailureTool } from "./diagnose-failure.ts";
7
+ import { sendRequestTool } from "./send-request.ts";
8
+ import { exploreApiTool } from "./explore-api.ts";
9
+ import type { AgentConfig } from "../types.ts";
10
+
11
+ export function buildAgentTools(config: AgentConfig) {
12
+ // In safe mode, wrap run_tests to force safe=true
13
+ const run_tests = config.safeMode
14
+ ? tool({
15
+ description: runTestsTool.description,
16
+ inputSchema: runTestsTool.inputSchema,
17
+ execute: async (args, options) => {
18
+ return runTestsTool.execute!({ ...args, safe: true }, options);
19
+ },
20
+ })
21
+ : runTestsTool;
22
+
23
+ // In safe mode, wrap send_request to only allow GET
24
+ const send_request = config.safeMode
25
+ ? tool({
26
+ description: sendRequestTool.description,
27
+ inputSchema: sendRequestTool.inputSchema,
28
+ execute: async (args, options) => {
29
+ if (args.method !== "GET") {
30
+ return { error: "Safe mode: only GET requests are allowed" };
31
+ }
32
+ return sendRequestTool.execute!(args, options);
33
+ },
34
+ })
35
+ : sendRequestTool;
36
+
37
+ return {
38
+ run_tests,
39
+ validate_tests: validateTestsTool,
40
+ query_results: queryResultsTool,
41
+ manage_environment: manageEnvironmentTool,
42
+ diagnose_failure: diagnoseFailureTool,
43
+ send_request,
44
+ explore_api: exploreApiTool,
45
+ };
46
+ }
47
+
48
+ export { runTestsTool, validateTestsTool, queryResultsTool, manageEnvironmentTool, diagnoseFailureTool, sendRequestTool, exploreApiTool };
@@ -0,0 +1,40 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { getDb } from "../../../db/schema.ts";
4
+ import { listEnvironmentRecords, getEnvironment, upsertEnvironment } from "../../../db/queries.ts";
5
+
6
+ export const manageEnvironmentTool = tool({
7
+ description: "List, get, or set environment variables used for API test execution",
8
+ inputSchema: z.object({
9
+ action: z.enum(["list", "get", "set"]).describe("Action to perform"),
10
+ name: z.string().optional().describe("Environment name"),
11
+ variables: z.record(z.string(), z.string()).optional().describe("Variables to set (for set action)"),
12
+ }),
13
+ execute: async (args) => {
14
+ try {
15
+ getDb();
16
+
17
+ switch (args.action) {
18
+ case "list": {
19
+ const environments = listEnvironmentRecords();
20
+ return { environments };
21
+ }
22
+ case "get": {
23
+ if (!args.name) return { error: "name is required for get action" };
24
+ const variables = getEnvironment(args.name);
25
+ if (!variables) return { error: `Environment '${args.name}' not found` };
26
+ return { name: args.name, variables };
27
+ }
28
+ case "set": {
29
+ if (!args.name || !args.variables) return { error: "name and variables are required for set action" };
30
+ upsertEnvironment(args.name, args.variables);
31
+ return { success: true, name: args.name };
32
+ }
33
+ default:
34
+ return { error: `Unknown action: ${args.action}` };
35
+ }
36
+ } catch (err) {
37
+ return { error: (err as Error).message };
38
+ }
39
+ },
40
+ });
@@ -0,0 +1,40 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { getDb } from "../../../db/schema.ts";
4
+ import { listRuns, getRunById, getResultsByRunId, listCollections } from "../../../db/queries.ts";
5
+
6
+ export const queryResultsTool = tool({
7
+ description: "Query test run results and collections from the database",
8
+ inputSchema: z.object({
9
+ action: z.enum(["list_runs", "get_run", "list_collections"]).describe("Action to perform"),
10
+ runId: z.number().optional().describe("Run ID (for get_run action)"),
11
+ limit: z.number().optional().describe("Max results to return (default: 20)"),
12
+ }),
13
+ execute: async (args) => {
14
+ try {
15
+ getDb();
16
+
17
+ switch (args.action) {
18
+ case "list_runs": {
19
+ const runs = listRuns(args.limit ?? 20);
20
+ return { runs };
21
+ }
22
+ case "get_run": {
23
+ if (args.runId == null) return { error: "runId is required for get_run action" };
24
+ const run = getRunById(args.runId);
25
+ if (!run) return { error: `Run ${args.runId} not found` };
26
+ const results = getResultsByRunId(args.runId);
27
+ return { run, results };
28
+ }
29
+ case "list_collections": {
30
+ const collections = listCollections();
31
+ return { collections };
32
+ }
33
+ default:
34
+ return { error: `Unknown action: ${args.action}` };
35
+ }
36
+ } catch (err) {
37
+ return { error: (err as Error).message };
38
+ }
39
+ },
40
+ });
@@ -0,0 +1,38 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { executeRun } from "../../runner/execute-run.ts";
4
+
5
+ export const runTestsTool = tool({
6
+ description: "Run API test suites from a YAML file or directory and return results summary",
7
+ inputSchema: z.object({
8
+ testPath: z.string().describe("Path to test YAML file or directory"),
9
+ envName: z.string().optional().describe("Environment name (loads .env.<name>.yaml)"),
10
+ safe: z.boolean().optional().describe("Run only GET tests (read-only, safe mode)"),
11
+ }),
12
+ execute: async (args) => {
13
+ try {
14
+ const { runId, results } = await executeRun({
15
+ testPath: args.testPath,
16
+ envName: args.envName,
17
+ safe: args.safe,
18
+ trigger: "agent",
19
+ });
20
+
21
+ const total = results.reduce((s, r) => s + r.total, 0);
22
+ const passed = results.reduce((s, r) => s + r.passed, 0);
23
+ const failed = results.reduce((s, r) => s + r.failed, 0);
24
+ const skipped = results.reduce((s, r) => s + r.skipped, 0);
25
+
26
+ return {
27
+ runId,
28
+ total,
29
+ passed,
30
+ failed,
31
+ skipped,
32
+ status: failed > 0 ? "has_failures" : "all_passed",
33
+ };
34
+ } catch (err) {
35
+ return { error: (err as Error).message };
36
+ }
37
+ },
38
+ });
@@ -0,0 +1,44 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { executeRequest } from "../../runner/http-client.ts";
4
+ import { loadEnvironment, substituteString, substituteDeep } from "../../parser/variables.ts";
5
+
6
+ export const sendRequestTool = tool({
7
+ description: "Send an ad-hoc HTTP request. Supports variable interpolation from environments (e.g. {{base_url}}).",
8
+ inputSchema: z.object({
9
+ method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
10
+ url: z.string().describe("Request URL (supports {{variable}} interpolation)"),
11
+ headers: z.record(z.string(), z.string()).optional().describe("Request headers"),
12
+ body: z.string().optional().describe("Request body (JSON string)"),
13
+ timeout: z.number().int().positive().optional().describe("Request timeout in ms"),
14
+ envName: z.string().optional().describe("Environment name for variable interpolation"),
15
+ }),
16
+ execute: async (args) => {
17
+ try {
18
+ const vars = await loadEnvironment(args.envName);
19
+
20
+ const resolvedUrl = substituteString(args.url, vars) as string;
21
+ const resolvedHeaders = args.headers ? substituteDeep(args.headers, vars) : {};
22
+ const resolvedBody = args.body ? substituteString(args.body, vars) as string : undefined;
23
+
24
+ const response = await executeRequest(
25
+ {
26
+ method: args.method,
27
+ url: resolvedUrl,
28
+ headers: resolvedHeaders,
29
+ body: resolvedBody,
30
+ },
31
+ args.timeout ? { timeout: args.timeout } : undefined,
32
+ );
33
+
34
+ // Compact output for agent — skip response headers
35
+ return {
36
+ status: response.status,
37
+ body: response.body_parsed ?? response.body,
38
+ duration_ms: response.duration_ms,
39
+ };
40
+ } catch (err) {
41
+ return { error: (err as Error).message };
42
+ }
43
+ },
44
+ });
@@ -0,0 +1,23 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { parse } from "../../parser/yaml-parser.ts";
4
+
5
+ export const validateTestsTool = tool({
6
+ description: "Validate YAML test files without running them. Returns parsed suite info or validation errors.",
7
+ inputSchema: z.object({
8
+ testPath: z.string().describe("Path to test YAML file or directory"),
9
+ }),
10
+ execute: async (args) => {
11
+ try {
12
+ const suites = await parse(args.testPath);
13
+ return {
14
+ valid: true,
15
+ suiteCount: suites.length,
16
+ totalTests: suites.reduce((s, suite) => s + suite.tests.length, 0),
17
+ suites: suites.map((s) => ({ name: s.name, testCount: s.tests.length })),
18
+ };
19
+ } catch (err) {
20
+ return { valid: false, error: (err as Error).message };
21
+ }
22
+ },
23
+ });
@@ -0,0 +1,22 @@
1
+ import type { AIProviderConfig } from "../generator/ai/types.ts";
2
+
3
+ export interface AgentConfig {
4
+ provider: AIProviderConfig;
5
+ safeMode?: boolean;
6
+ dbPath?: string;
7
+ maxSteps?: number;
8
+ }
9
+
10
+ export interface ToolEvent {
11
+ toolName: string;
12
+ args: Record<string, unknown>;
13
+ result: unknown;
14
+ timestamp: string;
15
+ }
16
+
17
+ export interface AgentTurnResult {
18
+ text: string;
19
+ toolEvents: ToolEvent[];
20
+ inputTokens: number;
21
+ outputTokens: number;
22
+ }
@@ -0,0 +1,61 @@
1
+ import type { AIGenerateOptions, AIGenerateResult } from "./types.ts";
2
+ import { readOpenApiSpec, extractEndpoints, extractSecuritySchemes } from "../openapi-reader.ts";
3
+ import { buildMessages } from "./prompt-builder.ts";
4
+ import { chatCompletion } from "./llm-client.ts";
5
+ import { parseAIResponse } from "./output-parser.ts";
6
+
7
+ export async function generateWithAI(options: AIGenerateOptions): Promise<AIGenerateResult> {
8
+ // 1. Read OpenAPI spec
9
+ const doc = await readOpenApiSpec(options.specPath);
10
+
11
+ // 2. Extract endpoints + security schemes
12
+ let endpoints = extractEndpoints(doc);
13
+ if (endpoints.length === 0) {
14
+ throw new Error("No endpoints found in the OpenAPI spec");
15
+ }
16
+ const securitySchemes = extractSecuritySchemes(doc);
17
+
18
+ // Filter to single endpoint if requested
19
+ if (options.filterEndpoint) {
20
+ const { method, path } = options.filterEndpoint;
21
+ const filtered = endpoints.filter(
22
+ (ep) => ep.method === method.toUpperCase() && ep.path === path,
23
+ );
24
+ if (filtered.length === 0) {
25
+ throw new Error(`Endpoint ${method} ${path} not found in spec`);
26
+ }
27
+ endpoints = filtered;
28
+ }
29
+
30
+ // Determine base URL: explicit option, or from spec servers[0]
31
+ const baseUrl = options.baseUrl ?? (doc as any).servers?.[0]?.url as string | undefined;
32
+
33
+ // 3. Build prompt
34
+ const messages = buildMessages(endpoints, securitySchemes, options.prompt, baseUrl);
35
+
36
+ // 4. Call LLM
37
+ const startTime = Date.now();
38
+ const llmResult = await chatCompletion(options.provider, messages);
39
+ const durationMs = Date.now() - startTime;
40
+
41
+ // 5. Parse + validate output
42
+ const parsed = parseAIResponse(llmResult.content);
43
+
44
+ if (parsed.suites.length === 0) {
45
+ const errorDetail = parsed.errors.length > 0
46
+ ? parsed.errors.join("; ")
47
+ : "No valid suites in response";
48
+ throw new Error(`AI generation failed: ${errorDetail}`);
49
+ }
50
+
51
+ // If there are validation errors but we still got suites, include them as warnings
52
+ const yaml = parsed.yaml;
53
+
54
+ return {
55
+ yaml,
56
+ rawResponse: llmResult.content,
57
+ promptTokens: llmResult.usage.promptTokens,
58
+ completionTokens: llmResult.usage.completionTokens,
59
+ model: options.provider.model,
60
+ };
61
+ }