@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.
- package/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/package.json +4 -7
- package/src/cli/commands/ci-init.ts +12 -1
- package/src/cli/commands/coverage.ts +21 -1
- package/src/cli/commands/db.ts +121 -0
- package/src/cli/commands/describe.ts +60 -0
- package/src/cli/commands/generate.ts +127 -0
- package/src/cli/commands/guide.ts +127 -0
- package/src/cli/commands/init.ts +50 -77
- package/src/cli/commands/request.ts +57 -0
- package/src/cli/commands/run.ts +53 -10
- package/src/cli/commands/serve.ts +62 -3
- package/src/cli/commands/validate.ts +18 -2
- package/src/cli/index.ts +213 -215
- package/src/cli/json-envelope.ts +19 -0
- package/src/core/diagnostics/db-analysis.ts +351 -0
- package/src/core/diagnostics/failure-hints.ts +1 -0
- package/src/core/generator/data-factory.ts +19 -8
- package/src/core/generator/describe.ts +250 -0
- package/src/core/generator/guide-builder.ts +20 -0
- package/src/core/generator/index.ts +0 -3
- package/src/core/generator/suite-generator.ts +133 -20
- package/src/core/runner/executor.ts +1 -0
- package/src/core/runner/send-request.ts +94 -0
- package/src/core/runner/types.ts +1 -0
- package/src/db/queries.ts +4 -2
- package/src/db/schema.ts +11 -3
- package/src/mcp/descriptions.ts +0 -24
- package/src/mcp/server.ts +1 -8
- package/src/mcp/tools/describe-endpoint.ts +3 -218
- package/src/mcp/tools/query-db.ts +6 -222
- package/src/mcp/tools/run-tests.ts +1 -0
- package/src/mcp/tools/send-request.ts +15 -61
- package/src/web/views/suites-tab.ts +1 -1
- package/src/cli/commands/add-api.ts +0 -53
- package/src/cli/commands/ai-generate.ts +0 -106
- package/src/cli/commands/chat.ts +0 -43
- package/src/cli/commands/collections.ts +0 -41
- package/src/cli/commands/compare.ts +0 -129
- package/src/cli/commands/doctor.ts +0 -127
- package/src/cli/commands/runs.ts +0 -108
- package/src/cli/commands/update.ts +0 -142
- package/src/core/agent/agent-loop.ts +0 -116
- package/src/core/agent/context-manager.ts +0 -41
- package/src/core/agent/system-prompt.ts +0 -27
- package/src/core/agent/tools/diagnose-failure.ts +0 -51
- package/src/core/agent/tools/index.ts +0 -42
- package/src/core/agent/tools/query-results.ts +0 -40
- package/src/core/agent/tools/run-tests.ts +0 -38
- package/src/core/agent/tools/send-request.ts +0 -44
- package/src/core/agent/types.ts +0 -22
- package/src/core/generator/ai/ai-generator.ts +0 -61
- package/src/core/generator/ai/llm-client.ts +0 -159
- package/src/core/generator/ai/output-parser.ts +0 -307
- package/src/core/generator/ai/prompt-builder.ts +0 -153
- package/src/core/generator/ai/types.ts +0 -56
- package/src/mcp/tools/generate-and-save.ts +0 -202
- package/src/mcp/tools/save-test-suite.ts +0 -218
- package/src/mcp/tools/set-work-dir.ts +0 -35
- package/src/tui/chat-ui.ts +0 -150
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
-
import {
|
|
5
|
-
readOpenApiSpec,
|
|
6
|
-
extractEndpoints,
|
|
7
|
-
extractSecuritySchemes,
|
|
8
|
-
scanCoveredEndpoints,
|
|
9
|
-
filterUncoveredEndpoints,
|
|
10
|
-
serializeSuite,
|
|
11
|
-
generateSuites,
|
|
12
|
-
findUnresolvedVars,
|
|
13
|
-
} from "../../core/generator/index.ts";
|
|
14
|
-
import { loadEnvironment } from "../../core/parser/variables.ts";
|
|
15
|
-
import { compressEndpointsWithSchemas, buildGenerationGuide } from "../../core/generator/guide-builder.ts";
|
|
16
|
-
import { findCollectionBySpec } from "../../db/queries.ts";
|
|
17
|
-
import { planChunks, filterByTag } from "../../core/generator/chunker.ts";
|
|
18
|
-
import { TOOL_DESCRIPTIONS } from "../descriptions.js";
|
|
19
|
-
import { validateAndSave } from "./save-test-suite.ts";
|
|
20
|
-
|
|
21
|
-
export function registerGenerateAndSaveTool(server: McpServer) {
|
|
22
|
-
server.registerTool("generate_and_save", {
|
|
23
|
-
description: TOOL_DESCRIPTIONS.generate_and_save,
|
|
24
|
-
inputSchema: {
|
|
25
|
-
specPath: z.string().describe("Path or URL to OpenAPI spec file"),
|
|
26
|
-
outputDir: z.optional(z.string()).describe("Directory for saving test files (default: ./tests/)"),
|
|
27
|
-
tag: z.optional(z.string()).describe("Generate tests only for this tag's endpoints"),
|
|
28
|
-
methodFilter: z.optional(z.array(z.string())).describe("Only include endpoints with these HTTP methods (e.g. [\"GET\"] for smoke tests)"),
|
|
29
|
-
testsDir: z.optional(z.string()).describe("Path to existing tests directory — filters to uncovered endpoints only"),
|
|
30
|
-
overwrite: z.optional(z.boolean()).describe("Hint for save_test_suites overwrite behavior (default: false)"),
|
|
31
|
-
includeFormat: z.optional(z.boolean()).describe("Include YAML format reference (default: true, set false for subsequent tag chunks)"),
|
|
32
|
-
mode: z.optional(z.enum(["generate", "guide"])).describe(
|
|
33
|
-
"'generate' creates and saves YAML test files deterministically (default), 'guide' returns text for LLM-crafted tests"
|
|
34
|
-
),
|
|
35
|
-
},
|
|
36
|
-
}, async ({ specPath, outputDir, tag, methodFilter, testsDir, overwrite, includeFormat, mode }) => {
|
|
37
|
-
try {
|
|
38
|
-
const doc = await readOpenApiSpec(specPath);
|
|
39
|
-
let endpoints = extractEndpoints(doc);
|
|
40
|
-
const securitySchemes = extractSecuritySchemes(doc);
|
|
41
|
-
const baseUrl = ((doc as any).servers?.[0]?.url) as string | undefined;
|
|
42
|
-
const title = (doc as any).info?.title as string | undefined;
|
|
43
|
-
let effectiveOutputDir = outputDir;
|
|
44
|
-
if (!effectiveOutputDir) {
|
|
45
|
-
const collection = findCollectionBySpec(specPath);
|
|
46
|
-
effectiveOutputDir = collection?.test_path ?? "./tests/";
|
|
47
|
-
}
|
|
48
|
-
const effectiveMode = mode ?? "generate";
|
|
49
|
-
|
|
50
|
-
// Apply method filter
|
|
51
|
-
if (methodFilter && methodFilter.length > 0) {
|
|
52
|
-
const methods = methodFilter.map(m => m.toUpperCase());
|
|
53
|
-
endpoints = endpoints.filter(ep => methods.includes(ep.method.toUpperCase()));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Coverage filtering
|
|
57
|
-
let coverageInfo: { covered: number; total: number; percentage: number } | undefined;
|
|
58
|
-
if (testsDir) {
|
|
59
|
-
const totalBefore = endpoints.length;
|
|
60
|
-
const covered = await scanCoveredEndpoints(testsDir);
|
|
61
|
-
const uncovered = filterUncoveredEndpoints(endpoints, covered);
|
|
62
|
-
const coveredCount = totalBefore - uncovered.length;
|
|
63
|
-
const percentage = totalBefore > 0 ? Math.round((coveredCount / totalBefore) * 100) : 100;
|
|
64
|
-
coverageInfo = { covered: coveredCount, total: totalBefore, percentage };
|
|
65
|
-
endpoints = uncovered;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (endpoints.length === 0) {
|
|
69
|
-
const msg = testsDir
|
|
70
|
-
? { fullyCovered: true, ...coverageInfo }
|
|
71
|
-
: { error: "No endpoints found in the spec" };
|
|
72
|
-
return {
|
|
73
|
-
content: [{ type: "text" as const, text: JSON.stringify(msg, null, 2) }],
|
|
74
|
-
isError: !testsDir,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Tag filtering
|
|
79
|
-
if (tag) {
|
|
80
|
-
endpoints = filterByTag(endpoints, tag);
|
|
81
|
-
if (endpoints.length === 0) {
|
|
82
|
-
return {
|
|
83
|
-
content: [{ type: "text" as const, text: JSON.stringify({ error: `No endpoints found for tag "${tag}"` }, null, 2) }],
|
|
84
|
-
isError: true,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const plan = planChunks(endpoints);
|
|
90
|
-
|
|
91
|
-
// Plan mode: large API without specific tag
|
|
92
|
-
if (plan.needsChunking && !tag) {
|
|
93
|
-
const result: Record<string, unknown> = {
|
|
94
|
-
mode: "plan",
|
|
95
|
-
title: title ?? "API",
|
|
96
|
-
totalEndpoints: plan.totalEndpoints,
|
|
97
|
-
chunks: plan.chunks,
|
|
98
|
-
instruction:
|
|
99
|
-
`This API has ${plan.totalEndpoints} endpoints across ${plan.chunks.length} tags. ` +
|
|
100
|
-
`Call generate_and_save with tag parameter for each chunk sequentially. ` +
|
|
101
|
-
(effectiveMode === "guide"
|
|
102
|
-
? `Pass includeFormat: false for subsequent chunks to save tokens. `
|
|
103
|
-
: "") +
|
|
104
|
-
`Example: generate_and_save(specPath: '${specPath}', tag: '${plan.chunks[0]!.tag}'` +
|
|
105
|
-
(effectiveMode === "guide" ? `, mode: 'guide'` : "") + `)`,
|
|
106
|
-
};
|
|
107
|
-
if (coverageInfo) {
|
|
108
|
-
result.coverage = coverageInfo;
|
|
109
|
-
}
|
|
110
|
-
return {
|
|
111
|
-
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ── Generate mode: deterministic YAML generation ──
|
|
116
|
-
if (effectiveMode === "generate") {
|
|
117
|
-
const suites = generateSuites({ endpoints, securitySchemes });
|
|
118
|
-
|
|
119
|
-
const files: Array<{
|
|
120
|
-
saved: boolean;
|
|
121
|
-
filePath: string;
|
|
122
|
-
tests: number;
|
|
123
|
-
error?: string;
|
|
124
|
-
}> = [];
|
|
125
|
-
|
|
126
|
-
for (const suite of suites) {
|
|
127
|
-
const yaml = serializeSuite(suite);
|
|
128
|
-
const fileName = (suite.fileStem ?? suite.name) + ".yaml";
|
|
129
|
-
const filePath = join(effectiveOutputDir, fileName);
|
|
130
|
-
|
|
131
|
-
const { result: saveResult } = await validateAndSave(filePath, yaml, overwrite ?? false);
|
|
132
|
-
files.push({
|
|
133
|
-
saved: saveResult.saved,
|
|
134
|
-
filePath: saveResult.filePath ?? filePath,
|
|
135
|
-
tests: suite.tests.length,
|
|
136
|
-
...(saveResult.error ? { error: saveResult.error } : {}),
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const warnings: string[] = [];
|
|
141
|
-
const env = await loadEnvironment(undefined, effectiveOutputDir);
|
|
142
|
-
const envKeys = new Set(Object.keys(env));
|
|
143
|
-
for (const suite of suites) {
|
|
144
|
-
const unresolved = findUnresolvedVars(suite, envKeys);
|
|
145
|
-
if (unresolved.length > 0)
|
|
146
|
-
warnings.push(`${suite.fileStem ?? suite.name}.yaml: unresolved [${unresolved.join(", ")}]`);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const response: Record<string, unknown> = {
|
|
150
|
-
mode: "generate",
|
|
151
|
-
suitesGenerated: suites.length,
|
|
152
|
-
files,
|
|
153
|
-
...(warnings.length > 0 ? { warnings } : {}),
|
|
154
|
-
hint: files.some(f => !f.saved)
|
|
155
|
-
? "Some files were not saved (already exist?). Use overwrite: true to replace."
|
|
156
|
-
: "Files saved. Run run_tests to verify. Use mode: 'guide' for LLM-crafted tests with more detail.",
|
|
157
|
-
};
|
|
158
|
-
if (coverageInfo) {
|
|
159
|
-
response.coverage = coverageInfo;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }],
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ── Guide mode: text-based generation guide ──
|
|
168
|
-
const coverageHeader = coverageInfo
|
|
169
|
-
? `## Coverage: ${coverageInfo.covered}/${coverageInfo.total} endpoints covered (${coverageInfo.percentage}%). Generating tests for ${endpoints.length} uncovered endpoints:`
|
|
170
|
-
: undefined;
|
|
171
|
-
|
|
172
|
-
const apiContext = compressEndpointsWithSchemas(endpoints, securitySchemes);
|
|
173
|
-
const guide = buildGenerationGuide({
|
|
174
|
-
title: tag ? `${title ?? "API"} — tag: ${tag}` : (title ?? "API"),
|
|
175
|
-
baseUrl,
|
|
176
|
-
apiContext,
|
|
177
|
-
outputDir: effectiveOutputDir,
|
|
178
|
-
securitySchemes,
|
|
179
|
-
endpointCount: endpoints.length,
|
|
180
|
-
coverageHeader,
|
|
181
|
-
includeFormat: includeFormat ?? true,
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const saveInstructions = `
|
|
185
|
-
---
|
|
186
|
-
## Save Instructions
|
|
187
|
-
- Output directory: ${effectiveOutputDir}
|
|
188
|
-
- Use \`save_test_suites\` to save all generated files in one call
|
|
189
|
-
- Overwrite: ${overwrite ? "true" : "false (set overwrite: true in save_test_suites to replace existing files)"}
|
|
190
|
-
- After saving, run \`run_tests\` to verify`;
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
content: [{ type: "text" as const, text: guide + saveInstructions }],
|
|
194
|
-
};
|
|
195
|
-
} catch (err) {
|
|
196
|
-
return {
|
|
197
|
-
content: [{ type: "text" as const, text: JSON.stringify({ error: (err as Error).message }, null, 2) }],
|
|
198
|
-
isError: true,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { validateSuite } from "../../core/parser/schema.ts";
|
|
4
|
-
import { join, dirname } from "node:path";
|
|
5
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
6
|
-
import YAML from "yaml";
|
|
7
|
-
import { TOOL_DESCRIPTIONS } from "../descriptions.js";
|
|
8
|
-
|
|
9
|
-
export interface SaveResult {
|
|
10
|
-
saved: boolean;
|
|
11
|
-
filePath?: string;
|
|
12
|
-
suite?: { name: unknown; tests: number; base_url: unknown };
|
|
13
|
-
hint?: string;
|
|
14
|
-
coverage?: Record<string, unknown>;
|
|
15
|
-
error?: string;
|
|
16
|
-
detected?: string[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function validateAndSave(
|
|
20
|
-
filePath: string,
|
|
21
|
-
content: string,
|
|
22
|
-
overwrite: boolean | undefined,
|
|
23
|
-
dbPath?: string,
|
|
24
|
-
): Promise<{ result: SaveResult; isError: boolean }> {
|
|
25
|
-
// Parse YAML
|
|
26
|
-
let parsed: unknown;
|
|
27
|
-
try {
|
|
28
|
-
parsed = YAML.parse(content);
|
|
29
|
-
} catch (err) {
|
|
30
|
-
return {
|
|
31
|
-
result: {
|
|
32
|
-
saved: false,
|
|
33
|
-
error: `YAML parse error: ${(err as Error).message}`,
|
|
34
|
-
hint: "Check YAML syntax — indentation, colons, and quoting",
|
|
35
|
-
},
|
|
36
|
-
isError: true,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Validate against test suite schema
|
|
41
|
-
try {
|
|
42
|
-
validateSuite(parsed);
|
|
43
|
-
} catch (err) {
|
|
44
|
-
const message = (err as Error).message;
|
|
45
|
-
let hint = "Check the test suite structure matches the expected format";
|
|
46
|
-
if (message.includes("status")) {
|
|
47
|
-
hint = "Status codes must be numbers, not strings (e.g. status: 200, not status: \"200\")";
|
|
48
|
-
} else if (message.includes("exists")) {
|
|
49
|
-
hint = "exists must be boolean true/false, not string \"true\"/\"false\"";
|
|
50
|
-
} else if (message.includes("tests")) {
|
|
51
|
-
hint = "Suite must have a 'tests' array with at least one test step";
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
result: { saved: false, error: `Validation: ${message}`, hint },
|
|
55
|
-
isError: true,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Detect hardcoded credentials — long opaque strings in auth headers
|
|
60
|
-
const credentialPattern = /Authorization\s*:\s*["']?(Basic|Bearer)\s+([A-Za-z0-9+/=_\-]{20,})["']?/g;
|
|
61
|
-
const credMatches = [...content.matchAll(credentialPattern)];
|
|
62
|
-
const suspiciousCredentials = credMatches.filter(m => {
|
|
63
|
-
const value = m[2]!;
|
|
64
|
-
return !value.startsWith("{{") && !value.endsWith("}}");
|
|
65
|
-
});
|
|
66
|
-
if (suspiciousCredentials.length > 0) {
|
|
67
|
-
return {
|
|
68
|
-
result: {
|
|
69
|
-
saved: false,
|
|
70
|
-
error: "Hardcoded credentials detected in Authorization header(s)",
|
|
71
|
-
hint: "Never put literal API keys or tokens in YAML files. Store them in the .env.yaml file in the API directory and reference as {{api_key}} in headers.",
|
|
72
|
-
detected: suspiciousCredentials.map(m => `${m[1]} <redacted>`),
|
|
73
|
-
},
|
|
74
|
-
isError: true,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Resolve path
|
|
79
|
-
const resolvedPath = filePath.startsWith("/") || /^[a-zA-Z]:/.test(filePath)
|
|
80
|
-
? filePath
|
|
81
|
-
: join(process.cwd(), filePath);
|
|
82
|
-
|
|
83
|
-
// Check existing file
|
|
84
|
-
if (!overwrite && existsSync(resolvedPath)) {
|
|
85
|
-
return {
|
|
86
|
-
result: {
|
|
87
|
-
saved: false,
|
|
88
|
-
error: `File already exists: ${resolvedPath}`,
|
|
89
|
-
hint: "Use overwrite: true to replace the existing file",
|
|
90
|
-
},
|
|
91
|
-
isError: true,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Create directories
|
|
96
|
-
const dir = dirname(resolvedPath);
|
|
97
|
-
if (!existsSync(dir)) {
|
|
98
|
-
mkdirSync(dir, { recursive: true });
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Write original YAML content (preserve formatting/comments)
|
|
102
|
-
await Bun.write(resolvedPath, content);
|
|
103
|
-
|
|
104
|
-
// Extract summary info
|
|
105
|
-
const suite = parsed as Record<string, unknown>;
|
|
106
|
-
const tests = (suite.tests as unknown[]) ?? [];
|
|
107
|
-
|
|
108
|
-
const result: SaveResult = {
|
|
109
|
-
saved: true,
|
|
110
|
-
filePath: resolvedPath,
|
|
111
|
-
suite: {
|
|
112
|
-
name: suite.name,
|
|
113
|
-
tests: tests.length,
|
|
114
|
-
base_url: suite.base_url ?? null,
|
|
115
|
-
},
|
|
116
|
-
hint: "After tests are ready, ask the user if they want to set up CI/CD with ci_init to run tests automatically on push.",
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
// Attempt to compute coverage hint
|
|
120
|
-
try {
|
|
121
|
-
const testDir = dirname(resolvedPath);
|
|
122
|
-
const { findCollectionByTestPath } = await import("../../db/queries.ts");
|
|
123
|
-
const { getDb } = await import("../../db/schema.ts");
|
|
124
|
-
getDb(dbPath);
|
|
125
|
-
const collection = findCollectionByTestPath(testDir);
|
|
126
|
-
if (collection?.openapi_spec) {
|
|
127
|
-
const { readOpenApiSpec, extractEndpoints } = await import("../../core/generator/openapi-reader.ts");
|
|
128
|
-
const { scanCoveredEndpoints, filterUncoveredEndpoints } = await import("../../core/generator/coverage-scanner.ts");
|
|
129
|
-
|
|
130
|
-
const doc = await readOpenApiSpec(collection.openapi_spec);
|
|
131
|
-
const allEndpoints = extractEndpoints(doc);
|
|
132
|
-
const covered = await scanCoveredEndpoints(testDir);
|
|
133
|
-
const uncovered = filterUncoveredEndpoints(allEndpoints, covered);
|
|
134
|
-
|
|
135
|
-
const total = allEndpoints.length;
|
|
136
|
-
const coveredCount = total - uncovered.length;
|
|
137
|
-
const percentage = total > 0 ? Math.round((coveredCount / total) * 100) : 0;
|
|
138
|
-
|
|
139
|
-
const coverage: Record<string, unknown> = { percentage, covered: coveredCount, total, uncoveredCount: uncovered.length };
|
|
140
|
-
if (percentage < 80 && uncovered.length > 0) {
|
|
141
|
-
coverage.suggestion = `Use generate_and_save with testsDir to cover ${uncovered.length} remaining endpoint${uncovered.length > 1 ? "s" : ""}`;
|
|
142
|
-
}
|
|
143
|
-
result.coverage = coverage;
|
|
144
|
-
}
|
|
145
|
-
} catch { /* silently skip coverage if unavailable */ }
|
|
146
|
-
|
|
147
|
-
return { result, isError: false };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function registerSaveTestSuiteTool(server: McpServer, dbPath?: string) {
|
|
151
|
-
server.registerTool("save_test_suite", {
|
|
152
|
-
description: TOOL_DESCRIPTIONS.save_test_suite,
|
|
153
|
-
inputSchema: {
|
|
154
|
-
filePath: z.string().describe("Path for saving the YAML test file (e.g. apis/petstore/tests/pets-crud.yaml)"),
|
|
155
|
-
content: z.string().describe("YAML content of the test suite"),
|
|
156
|
-
overwrite: z.optional(z.boolean()).describe("Overwrite existing file (default: false)"),
|
|
157
|
-
},
|
|
158
|
-
}, async ({ filePath, content, overwrite }) => {
|
|
159
|
-
try {
|
|
160
|
-
const { result, isError } = await validateAndSave(filePath, content, overwrite, dbPath);
|
|
161
|
-
return {
|
|
162
|
-
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
163
|
-
...(isError ? { isError: true } : {}),
|
|
164
|
-
};
|
|
165
|
-
} catch (err) {
|
|
166
|
-
return {
|
|
167
|
-
content: [{ type: "text" as const, text: JSON.stringify({
|
|
168
|
-
saved: false,
|
|
169
|
-
error: (err as Error).message,
|
|
170
|
-
}, null, 2) }],
|
|
171
|
-
isError: true,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function registerSaveTestSuitesTool(server: McpServer, dbPath?: string) {
|
|
178
|
-
server.registerTool("save_test_suites", {
|
|
179
|
-
description: TOOL_DESCRIPTIONS.save_test_suites,
|
|
180
|
-
inputSchema: {
|
|
181
|
-
files: z.array(z.object({
|
|
182
|
-
filePath: z.string().describe("Path for saving the YAML test file"),
|
|
183
|
-
content: z.string().describe("YAML content of the test suite"),
|
|
184
|
-
})).describe("Array of files to save"),
|
|
185
|
-
overwrite: z.optional(z.boolean()).describe("Overwrite existing files (default: false)"),
|
|
186
|
-
},
|
|
187
|
-
}, async ({ files, overwrite }) => {
|
|
188
|
-
try {
|
|
189
|
-
const results: Array<SaveResult & { filePath: string; inputPath: string }> = [];
|
|
190
|
-
let hasErrors = false;
|
|
191
|
-
|
|
192
|
-
for (const file of files) {
|
|
193
|
-
const { result, isError } = await validateAndSave(file.filePath, file.content, overwrite, dbPath);
|
|
194
|
-
results.push({ ...result, inputPath: file.filePath, filePath: result.filePath ?? file.filePath });
|
|
195
|
-
if (isError) hasErrors = true;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const summary = {
|
|
199
|
-
total: files.length,
|
|
200
|
-
saved: results.filter(r => r.saved).length,
|
|
201
|
-
failed: results.filter(r => !r.saved).length,
|
|
202
|
-
files: results,
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
content: [{ type: "text" as const, text: JSON.stringify(summary, null, 2) }],
|
|
207
|
-
...(hasErrors ? { isError: true } : {}),
|
|
208
|
-
};
|
|
209
|
-
} catch (err) {
|
|
210
|
-
return {
|
|
211
|
-
content: [{ type: "text" as const, text: JSON.stringify({
|
|
212
|
-
error: (err as Error).message,
|
|
213
|
-
}, null, 2) }],
|
|
214
|
-
isError: true,
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { resolve, join } from "node:path";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
import { resetDb } from "../../db/schema.ts";
|
|
6
|
-
import { TOOL_DESCRIPTIONS } from "../descriptions.js";
|
|
7
|
-
|
|
8
|
-
export function registerSetWorkDirTool(server: McpServer) {
|
|
9
|
-
server.registerTool("set_work_dir", {
|
|
10
|
-
description: TOOL_DESCRIPTIONS.set_work_dir,
|
|
11
|
-
inputSchema: {
|
|
12
|
-
workDir: z.string().describe(
|
|
13
|
-
"Absolute path to project root (e.g. /home/user/myproject or C:/Users/user/myproject)"
|
|
14
|
-
),
|
|
15
|
-
},
|
|
16
|
-
}, async ({ workDir }) => {
|
|
17
|
-
const resolved = resolve(workDir);
|
|
18
|
-
if (!existsSync(resolved)) {
|
|
19
|
-
return {
|
|
20
|
-
content: [{ type: "text" as const, text: JSON.stringify({ error: `Directory not found: ${resolved}` }, null, 2) }],
|
|
21
|
-
isError: true,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
process.chdir(resolved);
|
|
25
|
-
resetDb();
|
|
26
|
-
const dbPath = join(resolved, "zond.db");
|
|
27
|
-
return {
|
|
28
|
-
content: [{ type: "text" as const, text: JSON.stringify({
|
|
29
|
-
workDir: resolved,
|
|
30
|
-
zond_db: dbPath,
|
|
31
|
-
hint: "Working directory set. All relative paths and zond.db will now resolve from this directory.",
|
|
32
|
-
}, null, 2) }],
|
|
33
|
-
};
|
|
34
|
-
});
|
|
35
|
-
}
|
package/src/tui/chat-ui.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { createInterface } from "readline";
|
|
2
|
-
import { runAgentTurn } from "../core/agent/agent-loop.ts";
|
|
3
|
-
import { trimContext } from "../core/agent/context-manager.ts";
|
|
4
|
-
import type { AgentConfig, ToolEvent } from "../core/agent/types.ts";
|
|
5
|
-
import type { ModelMessage } from "ai";
|
|
6
|
-
|
|
7
|
-
// ── ANSI helpers ──
|
|
8
|
-
const RESET = "\x1b[0m";
|
|
9
|
-
const BOLD = "\x1b[1m";
|
|
10
|
-
const DIM = "\x1b[2m";
|
|
11
|
-
const CYAN = "\x1b[36m";
|
|
12
|
-
const GREEN = "\x1b[32m";
|
|
13
|
-
const YELLOW = "\x1b[33m";
|
|
14
|
-
const MAGENTA = "\x1b[35m";
|
|
15
|
-
|
|
16
|
-
// Mouse reporting escape sequences
|
|
17
|
-
const DISABLE_MOUSE = "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l";
|
|
18
|
-
// Regex to strip mouse escape sequences from input
|
|
19
|
-
// Covers SGR extended, X10, urxvt mouse protocols
|
|
20
|
-
const MOUSE_SEQ_RE = /\x1b\[\<[\d;]*[mM]|\x1b\[M[\s\S]{3}|\x1b\[\d+;\d+;\d+M/g;
|
|
21
|
-
|
|
22
|
-
function printToolEvent(event: ToolEvent) {
|
|
23
|
-
const args = Object.entries(event.args)
|
|
24
|
-
.filter(([, v]) => v !== undefined)
|
|
25
|
-
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
26
|
-
.slice(0, 3)
|
|
27
|
-
.join(", ");
|
|
28
|
-
|
|
29
|
-
const result = event.result as Record<string, unknown> | null;
|
|
30
|
-
let summary = "done";
|
|
31
|
-
if (result) {
|
|
32
|
-
if ("error" in result) summary = `error: ${result.error}`;
|
|
33
|
-
else if ("status" in result) summary = String(result.status);
|
|
34
|
-
else if ("valid" in result) summary = result.valid ? "valid" : "invalid";
|
|
35
|
-
else if ("runId" in result) summary = `run #${result.runId}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
process.stdout.write(` ${MAGENTA}↳ ${event.toolName}${RESET}${DIM}(${args})${RESET} → ${summary}\n`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export async function startChatUI(config: AgentConfig): Promise<void> {
|
|
42
|
-
const { provider } = config.provider;
|
|
43
|
-
const model = config.provider.model;
|
|
44
|
-
const safeLabel = config.safeMode ? ` ${YELLOW}[SAFE]${RESET}` : "";
|
|
45
|
-
|
|
46
|
-
// Disable mouse reporting that may be left over from other TUI apps
|
|
47
|
-
process.stdout.write(DISABLE_MOUSE);
|
|
48
|
-
|
|
49
|
-
console.log(`\n${BOLD}${CYAN}zond chat${RESET} — ${provider}/${model}${safeLabel}`);
|
|
50
|
-
console.log(`${DIM}Commands: /clear, /tokens, /quit | Ctrl+C to exit${RESET}\n`);
|
|
51
|
-
|
|
52
|
-
const messages: ModelMessage[] = [];
|
|
53
|
-
let totalIn = 0;
|
|
54
|
-
let totalOut = 0;
|
|
55
|
-
let busy = false;
|
|
56
|
-
|
|
57
|
-
const rl = createInterface({
|
|
58
|
-
input: process.stdin,
|
|
59
|
-
output: process.stdout,
|
|
60
|
-
prompt: `${BOLD}${CYAN}> ${RESET}`,
|
|
61
|
-
terminal: true,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
rl.prompt();
|
|
65
|
-
|
|
66
|
-
rl.on("line", async (raw: string) => {
|
|
67
|
-
// Strip mouse escape sequences that leak into input
|
|
68
|
-
const text = raw.replace(MOUSE_SEQ_RE, "").trim();
|
|
69
|
-
|
|
70
|
-
if (!text || busy) {
|
|
71
|
-
if (!busy) rl.prompt();
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (text === "/quit" || text === "/exit") {
|
|
76
|
-
rl.close();
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (text === "/clear") {
|
|
81
|
-
messages.length = 0;
|
|
82
|
-
totalIn = 0;
|
|
83
|
-
totalOut = 0;
|
|
84
|
-
console.log(`${DIM}Conversation cleared.${RESET}\n`);
|
|
85
|
-
rl.prompt();
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (text === "/tokens") {
|
|
90
|
-
console.log(`${DIM}Tokens: ${totalIn} in / ${totalOut} out${RESET}\n`);
|
|
91
|
-
rl.prompt();
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
busy = true;
|
|
96
|
-
messages.push({ role: "user", content: text });
|
|
97
|
-
|
|
98
|
-
// Trim context if conversation is long
|
|
99
|
-
const trimmed = trimContext(
|
|
100
|
-
messages.map((m) => ({
|
|
101
|
-
role: m.role as "user" | "assistant",
|
|
102
|
-
content: typeof m.content === "string" ? m.content : "",
|
|
103
|
-
})),
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
process.stdout.write(`\n${DIM}thinking...${RESET}`);
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
const result = await runAgentTurn(
|
|
110
|
-
trimmed.map((m) => ({ role: m.role, content: m.content })),
|
|
111
|
-
config,
|
|
112
|
-
(event) => {
|
|
113
|
-
process.stdout.write(`\r\x1b[K`);
|
|
114
|
-
printToolEvent(event);
|
|
115
|
-
process.stdout.write(`${DIM}thinking...${RESET}`);
|
|
116
|
-
},
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
process.stdout.write(`\r\x1b[K`);
|
|
120
|
-
|
|
121
|
-
const responseText = result.text || "(no response)";
|
|
122
|
-
console.log(`\n${GREEN}${BOLD}AI${RESET} ${responseText}\n`);
|
|
123
|
-
|
|
124
|
-
totalIn += result.inputTokens;
|
|
125
|
-
totalOut += result.outputTokens;
|
|
126
|
-
messages.push({ role: "assistant", content: responseText });
|
|
127
|
-
} catch (err) {
|
|
128
|
-
process.stdout.write(`\r\x1b[K`);
|
|
129
|
-
console.log(`\n${YELLOW}Error: ${(err as Error).message}${RESET}\n`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
busy = false;
|
|
133
|
-
rl.prompt();
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
rl.on("close", () => {
|
|
137
|
-
// Disable mouse reporting on exit too
|
|
138
|
-
process.stdout.write(DISABLE_MOUSE);
|
|
139
|
-
console.log(`\n${DIM}Bye! (${totalIn} in / ${totalOut} out tokens)${RESET}`);
|
|
140
|
-
process.exit(0);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// Also disable mouse on SIGINT
|
|
144
|
-
process.on("SIGINT", () => {
|
|
145
|
-
process.stdout.write(DISABLE_MOUSE);
|
|
146
|
-
rl.close();
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
await new Promise<void>(() => {});
|
|
150
|
-
}
|