@kirrosh/zond 0.13.0 → 0.16.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 (61) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -1
  3. package/package.json +4 -7
  4. package/src/cli/commands/ci-init.ts +12 -1
  5. package/src/cli/commands/coverage.ts +21 -1
  6. package/src/cli/commands/db.ts +121 -0
  7. package/src/cli/commands/describe.ts +60 -0
  8. package/src/cli/commands/generate.ts +127 -0
  9. package/src/cli/commands/guide.ts +127 -0
  10. package/src/cli/commands/init.ts +50 -77
  11. package/src/cli/commands/request.ts +57 -0
  12. package/src/cli/commands/run.ts +53 -10
  13. package/src/cli/commands/serve.ts +62 -3
  14. package/src/cli/commands/validate.ts +18 -2
  15. package/src/cli/index.ts +213 -215
  16. package/src/cli/json-envelope.ts +19 -0
  17. package/src/core/diagnostics/db-analysis.ts +351 -0
  18. package/src/core/diagnostics/failure-hints.ts +1 -0
  19. package/src/core/generator/data-factory.ts +19 -8
  20. package/src/core/generator/describe.ts +250 -0
  21. package/src/core/generator/guide-builder.ts +20 -0
  22. package/src/core/generator/index.ts +0 -3
  23. package/src/core/generator/suite-generator.ts +133 -20
  24. package/src/core/runner/executor.ts +1 -0
  25. package/src/core/runner/send-request.ts +94 -0
  26. package/src/core/runner/types.ts +1 -0
  27. package/src/db/queries.ts +4 -2
  28. package/src/db/schema.ts +11 -3
  29. package/src/mcp/descriptions.ts +0 -24
  30. package/src/mcp/server.ts +1 -8
  31. package/src/mcp/tools/describe-endpoint.ts +3 -218
  32. package/src/mcp/tools/query-db.ts +6 -222
  33. package/src/mcp/tools/run-tests.ts +1 -0
  34. package/src/mcp/tools/send-request.ts +15 -61
  35. package/src/web/views/suites-tab.ts +1 -1
  36. package/src/cli/commands/add-api.ts +0 -53
  37. package/src/cli/commands/ai-generate.ts +0 -106
  38. package/src/cli/commands/chat.ts +0 -43
  39. package/src/cli/commands/collections.ts +0 -41
  40. package/src/cli/commands/compare.ts +0 -129
  41. package/src/cli/commands/doctor.ts +0 -127
  42. package/src/cli/commands/runs.ts +0 -108
  43. package/src/cli/commands/update.ts +0 -142
  44. package/src/core/agent/agent-loop.ts +0 -116
  45. package/src/core/agent/context-manager.ts +0 -41
  46. package/src/core/agent/system-prompt.ts +0 -27
  47. package/src/core/agent/tools/diagnose-failure.ts +0 -51
  48. package/src/core/agent/tools/index.ts +0 -42
  49. package/src/core/agent/tools/query-results.ts +0 -40
  50. package/src/core/agent/tools/run-tests.ts +0 -38
  51. package/src/core/agent/tools/send-request.ts +0 -44
  52. package/src/core/agent/types.ts +0 -22
  53. package/src/core/generator/ai/ai-generator.ts +0 -61
  54. package/src/core/generator/ai/llm-client.ts +0 -159
  55. package/src/core/generator/ai/output-parser.ts +0 -307
  56. package/src/core/generator/ai/prompt-builder.ts +0 -153
  57. package/src/core/generator/ai/types.ts +0 -56
  58. package/src/mcp/tools/generate-and-save.ts +0 -202
  59. package/src/mcp/tools/save-test-suite.ts +0 -218
  60. package/src/mcp/tools/set-work-dir.ts +0 -35
  61. package/src/tui/chat-ui.ts +0 -150
@@ -1,51 +0,0 @@
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
- });
@@ -1,42 +0,0 @@
1
- import { tool } from "ai";
2
- import { runTestsTool } from "./run-tests.ts";
3
- import { queryResultsTool } from "./query-results.ts";
4
- import { diagnoseFailureTool } from "./diagnose-failure.ts";
5
- import { sendRequestTool } from "./send-request.ts";
6
- import type { AgentConfig } from "../types.ts";
7
-
8
- export function buildAgentTools(config: AgentConfig) {
9
- // In safe mode, wrap run_tests to force safe=true
10
- const run_tests = config.safeMode
11
- ? tool({
12
- description: runTestsTool.description,
13
- inputSchema: runTestsTool.inputSchema,
14
- execute: async (args, options) => {
15
- return runTestsTool.execute!({ ...args, safe: true }, options);
16
- },
17
- })
18
- : runTestsTool;
19
-
20
- // In safe mode, wrap send_request to only allow GET
21
- const send_request = config.safeMode
22
- ? tool({
23
- description: sendRequestTool.description,
24
- inputSchema: sendRequestTool.inputSchema,
25
- execute: async (args, options) => {
26
- if (args.method !== "GET") {
27
- return { error: "Safe mode: only GET requests are allowed" };
28
- }
29
- return sendRequestTool.execute!(args, options);
30
- },
31
- })
32
- : sendRequestTool;
33
-
34
- return {
35
- run_tests,
36
- query_results: queryResultsTool,
37
- diagnose_failure: diagnoseFailureTool,
38
- send_request,
39
- };
40
- }
41
-
42
- export { runTestsTool, queryResultsTool, diagnoseFailureTool, sendRequestTool };
@@ -1,40 +0,0 @@
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
- });
@@ -1,38 +0,0 @@
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
- });
@@ -1,44 +0,0 @@
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
- });
@@ -1,22 +0,0 @@
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
- }
@@ -1,61 +0,0 @@
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
- }
@@ -1,159 +0,0 @@
1
- import type { AIProviderConfig } from "./types.ts";
2
-
3
- export interface ChatMessage {
4
- role: "system" | "user" | "assistant";
5
- content: string;
6
- }
7
-
8
- export interface ChatCompletionResult {
9
- content: string;
10
- usage: {
11
- promptTokens?: number;
12
- completionTokens?: number;
13
- };
14
- }
15
-
16
- export async function chatCompletion(
17
- config: AIProviderConfig,
18
- messages: ChatMessage[],
19
- ): Promise<ChatCompletionResult> {
20
- if (config.provider === "anthropic") {
21
- return callAnthropic(config, messages);
22
- }
23
- return callOpenAICompatible(config, messages);
24
- }
25
-
26
- async function callOpenAICompatible(
27
- config: AIProviderConfig,
28
- messages: ChatMessage[],
29
- ): Promise<ChatCompletionResult> {
30
- const url = `${config.baseUrl.replace(/\/+$/, "")}/chat/completions`;
31
-
32
- // For ollama/custom providers, inject system prompt into first user message
33
- // to avoid issues with thinking models (e.g. qwen3) that break with separate system messages
34
- let apiMessages: Array<{ role: string; content: string }>;
35
- if (config.provider === "ollama" || config.provider === "custom") {
36
- const systemMsgs = messages.filter((m) => m.role === "system");
37
- const nonSystem = messages.filter((m) => m.role !== "system");
38
- if (systemMsgs.length > 0 && nonSystem.length > 0) {
39
- const systemText = systemMsgs.map((m) => m.content).join("\n\n");
40
- apiMessages = nonSystem.map((m, i) =>
41
- i === 0 ? { role: m.role, content: `${systemText}\n\n${m.content}` } : { role: m.role, content: m.content }
42
- );
43
- } else {
44
- apiMessages = messages.map((m) => ({ role: m.role, content: m.content }));
45
- }
46
- } else {
47
- apiMessages = messages.map((m) => ({ role: m.role, content: m.content }));
48
- }
49
-
50
- const body: Record<string, unknown> = {
51
- model: config.model,
52
- messages: apiMessages,
53
- temperature: config.temperature ?? 0.2,
54
- max_tokens: config.maxTokens ?? 4096,
55
- };
56
-
57
- // Request JSON output where supported (OpenAI, newer Ollama models)
58
- if (config.provider === "openai") {
59
- body.response_format = { type: "json_object" };
60
- }
61
-
62
- const headers: Record<string, string> = {
63
- "Content-Type": "application/json",
64
- };
65
- if (config.apiKey) {
66
- headers["Authorization"] = `Bearer ${config.apiKey}`;
67
- }
68
-
69
- const resp = await fetch(url, {
70
- method: "POST",
71
- headers,
72
- body: JSON.stringify(body),
73
- });
74
-
75
- if (!resp.ok) {
76
- const text = await resp.text();
77
- throw new Error(`LLM request failed (${resp.status}): ${text}`);
78
- }
79
-
80
- const data = (await resp.json()) as {
81
- choices: Array<{ message: { content: string; reasoning?: string } }>;
82
- usage?: { prompt_tokens?: number; completion_tokens?: number };
83
- };
84
-
85
- const msg = data.choices?.[0]?.message;
86
- // Thinking models (e.g. qwen3) may put output in `reasoning` with empty `content`
87
- const content = msg?.content || msg?.reasoning || "";
88
- return {
89
- content,
90
- usage: {
91
- promptTokens: data.usage?.prompt_tokens,
92
- completionTokens: data.usage?.completion_tokens,
93
- },
94
- };
95
- }
96
-
97
- async function callAnthropic(
98
- config: AIProviderConfig,
99
- messages: ChatMessage[],
100
- ): Promise<ChatCompletionResult> {
101
- const url = `${config.baseUrl.replace(/\/+$/, "")}/v1/messages`;
102
-
103
- // Separate system prompt from user/assistant messages
104
- const systemMessages = messages.filter((m) => m.role === "system");
105
- const nonSystemMessages = messages.filter((m) => m.role !== "system");
106
-
107
- const systemText = systemMessages.map((m) => m.content).join("\n\n");
108
-
109
- const body: Record<string, unknown> = {
110
- model: config.model,
111
- max_tokens: config.maxTokens ?? 4096,
112
- temperature: config.temperature ?? 0.2,
113
- messages: nonSystemMessages.map((m) => ({
114
- role: m.role,
115
- content: m.content,
116
- })),
117
- };
118
-
119
- if (systemText) {
120
- body.system = systemText;
121
- }
122
-
123
- const headers: Record<string, string> = {
124
- "Content-Type": "application/json",
125
- "anthropic-version": "2023-06-01",
126
- };
127
- if (config.apiKey) {
128
- headers["x-api-key"] = config.apiKey;
129
- }
130
-
131
- const resp = await fetch(url, {
132
- method: "POST",
133
- headers,
134
- body: JSON.stringify(body),
135
- });
136
-
137
- if (!resp.ok) {
138
- const text = await resp.text();
139
- throw new Error(`Anthropic request failed (${resp.status}): ${text}`);
140
- }
141
-
142
- const data = (await resp.json()) as {
143
- content: Array<{ type: string; text: string }>;
144
- usage?: { input_tokens?: number; output_tokens?: number };
145
- };
146
-
147
- const content = data.content
148
- ?.filter((b) => b.type === "text")
149
- .map((b) => b.text)
150
- .join("") ?? "";
151
-
152
- return {
153
- content,
154
- usage: {
155
- promptTokens: data.usage?.input_tokens,
156
- completionTokens: data.usage?.output_tokens,
157
- },
158
- };
159
- }