@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,129 +0,0 @@
|
|
|
1
|
-
import { getDb } from "../../db/schema.ts";
|
|
2
|
-
import { getRunById, getResultsByRunId } from "../../db/queries.ts";
|
|
3
|
-
import { printError } from "../output.ts";
|
|
4
|
-
|
|
5
|
-
const RESET = "\x1b[0m";
|
|
6
|
-
const GREEN = "\x1b[32m";
|
|
7
|
-
const RED = "\x1b[31m";
|
|
8
|
-
const YELLOW = "\x1b[33m";
|
|
9
|
-
const BOLD = "\x1b[1m";
|
|
10
|
-
|
|
11
|
-
function useColor(): boolean {
|
|
12
|
-
return process.stdout.isTTY ?? false;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface CompareOptions {
|
|
16
|
-
runA: number;
|
|
17
|
-
runB: number;
|
|
18
|
-
dbPath?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function compareCommand(options: CompareOptions): Promise<number> {
|
|
22
|
-
const { runA, runB, dbPath } = options;
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
getDb(dbPath);
|
|
26
|
-
|
|
27
|
-
const runARecord = getRunById(runA);
|
|
28
|
-
const runBRecord = getRunById(runB);
|
|
29
|
-
|
|
30
|
-
if (!runARecord) {
|
|
31
|
-
printError(`Run #${runA} not found`);
|
|
32
|
-
return 2;
|
|
33
|
-
}
|
|
34
|
-
if (!runBRecord) {
|
|
35
|
-
printError(`Run #${runB} not found`);
|
|
36
|
-
return 2;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const resultsA = getResultsByRunId(runA);
|
|
40
|
-
const resultsB = getResultsByRunId(runB);
|
|
41
|
-
|
|
42
|
-
// Build lookup maps: "suite_name::test_name" → status
|
|
43
|
-
const mapA = new Map<string, string>();
|
|
44
|
-
const mapB = new Map<string, string>();
|
|
45
|
-
|
|
46
|
-
for (const r of resultsA) {
|
|
47
|
-
mapA.set(`${r.suite_name}::${r.test_name}`, r.status);
|
|
48
|
-
}
|
|
49
|
-
for (const r of resultsB) {
|
|
50
|
-
mapB.set(`${r.suite_name}::${r.test_name}`, r.status);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const regressions: Array<{ suite: string; test: string; before: string; after: string }> = [];
|
|
54
|
-
const fixes: Array<{ suite: string; test: string; before: string; after: string }> = [];
|
|
55
|
-
const unchanged: number[] = [];
|
|
56
|
-
let newTests = 0;
|
|
57
|
-
let removedTests = 0;
|
|
58
|
-
|
|
59
|
-
// Check all keys from B (current run)
|
|
60
|
-
for (const [key, statusB] of mapB) {
|
|
61
|
-
const statusA = mapA.get(key);
|
|
62
|
-
if (statusA === undefined) {
|
|
63
|
-
newTests++;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
const wasPass = statusA === "pass";
|
|
67
|
-
const isPass = statusB === "pass";
|
|
68
|
-
const wasFail = statusA === "fail" || statusA === "error";
|
|
69
|
-
const isFail = statusB === "fail" || statusB === "error";
|
|
70
|
-
|
|
71
|
-
const [suite, test] = key.split("::") as [string, string];
|
|
72
|
-
|
|
73
|
-
if (wasPass && isFail) {
|
|
74
|
-
regressions.push({ suite, test, before: statusA, after: statusB });
|
|
75
|
-
} else if (wasFail && isPass) {
|
|
76
|
-
fixes.push({ suite, test, before: statusA, after: statusB });
|
|
77
|
-
} else {
|
|
78
|
-
unchanged.push(1);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Count removed tests
|
|
83
|
-
for (const key of mapA.keys()) {
|
|
84
|
-
if (!mapB.has(key)) removedTests++;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const color = useColor();
|
|
88
|
-
|
|
89
|
-
// Header
|
|
90
|
-
console.log(`\nComparing run #${runA} (${runARecord.started_at.slice(0, 19)}) → run #${runB} (${runBRecord.started_at.slice(0, 19)})\n`);
|
|
91
|
-
|
|
92
|
-
// Summary line
|
|
93
|
-
const parts = [
|
|
94
|
-
`${color ? BOLD : ""}${regressions.length} regressions${color ? RESET : ""}`,
|
|
95
|
-
`${fixes.length} fixes`,
|
|
96
|
-
`${unchanged.length} unchanged`,
|
|
97
|
-
];
|
|
98
|
-
if (newTests > 0) parts.push(`${newTests} new`);
|
|
99
|
-
if (removedTests > 0) parts.push(`${removedTests} removed`);
|
|
100
|
-
console.log(parts.join(" | ") + "\n");
|
|
101
|
-
|
|
102
|
-
// Regressions
|
|
103
|
-
if (regressions.length > 0) {
|
|
104
|
-
console.log(`${color ? RED + BOLD : ""}Regressions (pass → fail):${color ? RESET : ""}`);
|
|
105
|
-
for (const r of regressions) {
|
|
106
|
-
console.log(` ${color ? RED : ""}✗${color ? RESET : ""} [${r.suite}] ${r.test} (${r.before} → ${r.after})`);
|
|
107
|
-
}
|
|
108
|
-
console.log("");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Fixes
|
|
112
|
-
if (fixes.length > 0) {
|
|
113
|
-
console.log(`${color ? GREEN : ""}Fixes (fail → pass):${color ? RESET : ""}`);
|
|
114
|
-
for (const f of fixes) {
|
|
115
|
-
console.log(` ${color ? GREEN : ""}✓${color ? RESET : ""} [${f.suite}] ${f.test} (${f.before} → ${f.after})`);
|
|
116
|
-
}
|
|
117
|
-
console.log("");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (regressions.length === 0 && fixes.length === 0) {
|
|
121
|
-
console.log(`${color ? GREEN : ""}No regressions detected.${color ? RESET : ""}`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return regressions.length > 0 ? 1 : 0;
|
|
125
|
-
} catch (err) {
|
|
126
|
-
printError(err instanceof Error ? err.message : String(err));
|
|
127
|
-
return 2;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "fs";
|
|
2
|
-
import { resolve } from "path";
|
|
3
|
-
import { getDb, closeDb } from "../../db/schema.ts";
|
|
4
|
-
|
|
5
|
-
export interface DoctorOptions {
|
|
6
|
-
dbPath?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface Check {
|
|
10
|
-
label: string;
|
|
11
|
-
ok: boolean;
|
|
12
|
-
detail: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export async function doctorCommand(options: DoctorOptions): Promise<number> {
|
|
16
|
-
const checks: Check[] = [];
|
|
17
|
-
|
|
18
|
-
// 1. Database
|
|
19
|
-
checks.push(checkDatabase(options.dbPath));
|
|
20
|
-
|
|
21
|
-
// 2. Test files
|
|
22
|
-
checks.push(checkTestFiles());
|
|
23
|
-
|
|
24
|
-
// 3. OpenAPI spec
|
|
25
|
-
checks.push(checkOpenApiSpec());
|
|
26
|
-
|
|
27
|
-
// 4. Environment files
|
|
28
|
-
checks.push(checkEnvFiles());
|
|
29
|
-
|
|
30
|
-
// 5. Ollama
|
|
31
|
-
checks.push(await checkOllama());
|
|
32
|
-
|
|
33
|
-
// Print results
|
|
34
|
-
console.log("\nzond doctor\n");
|
|
35
|
-
|
|
36
|
-
let hasFailure = false;
|
|
37
|
-
for (const check of checks) {
|
|
38
|
-
const icon = check.ok ? "\u2713" : "\u2717";
|
|
39
|
-
console.log(` ${icon} ${check.label}: ${check.detail}`);
|
|
40
|
-
if (!check.ok) hasFailure = true;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
console.log("");
|
|
44
|
-
if (hasFailure) {
|
|
45
|
-
console.log("Some checks failed. See details above.");
|
|
46
|
-
} else {
|
|
47
|
-
console.log("All checks passed.");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return hasFailure ? 1 : 0;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function checkDatabase(dbPath?: string): Check {
|
|
54
|
-
const path = dbPath ? resolve(dbPath) : resolve(process.cwd(), "zond.db");
|
|
55
|
-
try {
|
|
56
|
-
const db = getDb(path);
|
|
57
|
-
const runs = (db.query("SELECT COUNT(*) as cnt FROM runs").get() as { cnt: number }).cnt;
|
|
58
|
-
const envs = (db.query("SELECT COUNT(*) as cnt FROM environments").get() as { cnt: number }).cnt;
|
|
59
|
-
closeDb();
|
|
60
|
-
return { label: "Database", ok: true, detail: `${path} (${runs} runs, ${envs} environments)` };
|
|
61
|
-
} catch (err) {
|
|
62
|
-
return { label: "Database", ok: false, detail: `Cannot open ${path}: ${(err as Error).message}` };
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function checkTestFiles(): Check {
|
|
67
|
-
const dirs = [".", "tests", "test"];
|
|
68
|
-
const found: string[] = [];
|
|
69
|
-
|
|
70
|
-
for (const dir of dirs) {
|
|
71
|
-
const full = resolve(process.cwd(), dir);
|
|
72
|
-
if (!existsSync(full)) continue;
|
|
73
|
-
try {
|
|
74
|
-
const glob = new Bun.Glob("**/*.yaml");
|
|
75
|
-
for (const file of glob.scanSync({ cwd: full, absolute: false })) {
|
|
76
|
-
if (!file.startsWith(".env.")) {
|
|
77
|
-
found.push(`${dir}/${file}`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
} catch { /* ignore */ }
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (found.length > 0) {
|
|
84
|
-
return { label: "Test files", ok: true, detail: `${found.length} YAML file(s) found` };
|
|
85
|
-
}
|
|
86
|
-
return { label: "Test files", ok: false, detail: "No YAML test files found in cwd or tests/" };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function checkOpenApiSpec(): Check {
|
|
90
|
-
const candidates = ["openapi.yaml", "openapi.json", "openapi.yml", "swagger.yaml", "swagger.json"];
|
|
91
|
-
for (const name of candidates) {
|
|
92
|
-
const full = resolve(process.cwd(), name);
|
|
93
|
-
if (existsSync(full)) {
|
|
94
|
-
return { label: "OpenAPI spec", ok: true, detail: name };
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return { label: "OpenAPI spec", ok: false, detail: "No openapi.yaml/json found (optional)" };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function checkEnvFiles(): Check {
|
|
101
|
-
const found: string[] = [];
|
|
102
|
-
try {
|
|
103
|
-
const glob = new Bun.Glob(".env.*.yaml");
|
|
104
|
-
for (const file of glob.scanSync({ cwd: process.cwd(), absolute: false })) {
|
|
105
|
-
found.push(file);
|
|
106
|
-
}
|
|
107
|
-
} catch { /* ignore */ }
|
|
108
|
-
|
|
109
|
-
if (found.length > 0) {
|
|
110
|
-
return { label: "Environment files", ok: true, detail: found.join(", ") };
|
|
111
|
-
}
|
|
112
|
-
return { label: "Environment files", ok: false, detail: "No .env.*.yaml files found (optional)" };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function checkOllama(): Promise<Check> {
|
|
116
|
-
try {
|
|
117
|
-
const res = await fetch("http://localhost:11434/api/tags", { signal: AbortSignal.timeout(3000) });
|
|
118
|
-
if (res.ok) {
|
|
119
|
-
const data = await res.json() as { models?: { name: string }[] };
|
|
120
|
-
const count = data.models?.length ?? 0;
|
|
121
|
-
return { label: "Ollama", ok: true, detail: `Running (${count} model(s) available)` };
|
|
122
|
-
}
|
|
123
|
-
return { label: "Ollama", ok: false, detail: `Responded with status ${res.status}` };
|
|
124
|
-
} catch {
|
|
125
|
-
return { label: "Ollama", ok: false, detail: "Not reachable at localhost:11434 (optional, needed for chat)" };
|
|
126
|
-
}
|
|
127
|
-
}
|
package/src/cli/commands/runs.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { getDb } from "../../db/schema.ts";
|
|
2
|
-
import { listRuns, getRunById, getResultsByRunId } from "../../db/queries.ts";
|
|
3
|
-
import { printError } from "../output.ts";
|
|
4
|
-
|
|
5
|
-
export interface RunsOptions {
|
|
6
|
-
runId?: number;
|
|
7
|
-
limit?: number;
|
|
8
|
-
dbPath?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const RESET = "\x1b[0m";
|
|
12
|
-
const GREEN = "\x1b[32m";
|
|
13
|
-
const RED = "\x1b[31m";
|
|
14
|
-
const YELLOW = "\x1b[33m";
|
|
15
|
-
|
|
16
|
-
function useColor(): boolean {
|
|
17
|
-
return process.stdout.isTTY ?? false;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function statusIcon(passed: number, failed: number): string {
|
|
21
|
-
const color = useColor();
|
|
22
|
-
if (failed === 0) return color ? `${GREEN}PASS${RESET}` : "PASS";
|
|
23
|
-
return color ? `${RED}FAIL${RESET}` : "FAIL";
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function runsCommand(options: RunsOptions): number {
|
|
27
|
-
const { runId, limit = 20, dbPath } = options;
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
getDb(dbPath);
|
|
31
|
-
} catch (err) {
|
|
32
|
-
printError(`Failed to open database: ${(err as Error).message}`);
|
|
33
|
-
return 2;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (runId !== undefined) {
|
|
37
|
-
return showRunDetail(runId);
|
|
38
|
-
}
|
|
39
|
-
return showRunList(limit);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function showRunList(limit: number): number {
|
|
43
|
-
const runs = listRuns(limit);
|
|
44
|
-
|
|
45
|
-
if (runs.length === 0) {
|
|
46
|
-
console.log("No runs found.");
|
|
47
|
-
return 0;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Print table
|
|
51
|
-
const header = "ID STATUS TOTAL PASS FAIL ENV DURATION STARTED";
|
|
52
|
-
console.log(header);
|
|
53
|
-
console.log("-".repeat(header.length));
|
|
54
|
-
|
|
55
|
-
for (const run of runs) {
|
|
56
|
-
const status = statusIcon(run.passed, run.failed);
|
|
57
|
-
const env = (run.environment ?? "-").slice(0, 10).padEnd(10);
|
|
58
|
-
const duration = run.duration_ms != null ? `${run.duration_ms}ms` : "-";
|
|
59
|
-
const started = run.started_at.slice(0, 19).replace("T", " ");
|
|
60
|
-
console.log(
|
|
61
|
-
`${String(run.id).padEnd(6)} ${status.padEnd(useColor() ? 14 : 6)} ${String(run.total).padEnd(5)} ${String(run.passed).padEnd(4)} ${String(run.failed).padEnd(4)} ${env} ${duration.padEnd(8)} ${started}`,
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return 0;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function showRunDetail(runId: number): number {
|
|
69
|
-
const run = getRunById(runId);
|
|
70
|
-
if (!run) {
|
|
71
|
-
printError(`Run #${runId} not found`);
|
|
72
|
-
return 1;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const color = useColor();
|
|
76
|
-
|
|
77
|
-
console.log(`Run #${run.id}`);
|
|
78
|
-
console.log(` Started: ${run.started_at}`);
|
|
79
|
-
if (run.finished_at) console.log(` Finished: ${run.finished_at}`);
|
|
80
|
-
if (run.environment) console.log(` Environment: ${run.environment}`);
|
|
81
|
-
if (run.duration_ms != null) console.log(` Duration: ${run.duration_ms}ms`);
|
|
82
|
-
console.log(` Total: ${run.total} Passed: ${run.passed} Failed: ${run.failed} Skipped: ${run.skipped}`);
|
|
83
|
-
|
|
84
|
-
const results = getResultsByRunId(runId);
|
|
85
|
-
if (results.length === 0) {
|
|
86
|
-
console.log("\nNo step results recorded.");
|
|
87
|
-
return 0;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
console.log("\nSteps:");
|
|
91
|
-
for (const r of results) {
|
|
92
|
-
let statusStr: string;
|
|
93
|
-
if (r.status === "pass") {
|
|
94
|
-
statusStr = color ? `${GREEN}PASS${RESET}` : "PASS";
|
|
95
|
-
} else if (r.status === "fail" || r.status === "error") {
|
|
96
|
-
statusStr = color ? `${RED}${r.status.toUpperCase()}${RESET}` : r.status.toUpperCase();
|
|
97
|
-
} else {
|
|
98
|
-
statusStr = color ? `${YELLOW}SKIP${RESET}` : "SKIP";
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
console.log(` ${statusStr} ${r.test_name} (${r.duration_ms}ms)`);
|
|
102
|
-
if (r.error_message) {
|
|
103
|
-
console.log(` ${color ? RED : ""}${r.error_message}${color ? RESET : ""}`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return 0;
|
|
108
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { tmpdir } from "os";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import { existsSync, unlinkSync, renameSync, copyFileSync, chmodSync } from "fs";
|
|
4
|
-
import { VERSION } from "../index.ts";
|
|
5
|
-
import { isCompiledBinary } from "../runtime.ts";
|
|
6
|
-
|
|
7
|
-
export interface UpdateCommandOptions {
|
|
8
|
-
force?: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function detectTarget(): { target: string; archive: "tar.gz" | "zip" } {
|
|
12
|
-
const platform = process.platform;
|
|
13
|
-
const arch = process.arch;
|
|
14
|
-
|
|
15
|
-
const os = platform === "win32" ? "win" : platform;
|
|
16
|
-
const archSuffix = arch === "arm64" ? "arm64" : "x64";
|
|
17
|
-
const target = `${os}-${archSuffix}`;
|
|
18
|
-
const archive = platform === "win32" ? "zip" : ("tar.gz" as const);
|
|
19
|
-
|
|
20
|
-
return { target, archive };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function parseVersion(tag: string): string {
|
|
24
|
-
return tag.replace(/^v/, "");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function compareVersions(a: string, b: string): number {
|
|
28
|
-
const pa = a.split(".").map(Number);
|
|
29
|
-
const pb = b.split(".").map(Number);
|
|
30
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
31
|
-
const na = pa[i] ?? 0;
|
|
32
|
-
const nb = pb[i] ?? 0;
|
|
33
|
-
if (na !== nb) return na - nb;
|
|
34
|
-
}
|
|
35
|
-
return 0;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function updateCommand(options: UpdateCommandOptions): Promise<number> {
|
|
39
|
-
if (!isCompiledBinary()) {
|
|
40
|
-
console.log("Running from source — use git pull to update.");
|
|
41
|
-
return 0;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
console.log("Checking for updates...");
|
|
45
|
-
|
|
46
|
-
// Fetch latest release
|
|
47
|
-
const res = await fetch("https://api.github.com/repos/kirrosh/zond/releases/latest", {
|
|
48
|
-
headers: { "User-Agent": "zond-updater" },
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
if (!res.ok) {
|
|
52
|
-
console.error(`Failed to check for updates: HTTP ${res.status}`);
|
|
53
|
-
return 1;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const release = (await res.json()) as { tag_name: string };
|
|
57
|
-
const latestVersion = parseVersion(release.tag_name);
|
|
58
|
-
const currentVersion = VERSION;
|
|
59
|
-
|
|
60
|
-
if (!options.force && compareVersions(currentVersion, latestVersion) >= 0) {
|
|
61
|
-
console.log(`Already up to date (v${currentVersion}).`);
|
|
62
|
-
return 0;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
console.log(`Updating v${currentVersion} → v${latestVersion}...`);
|
|
66
|
-
|
|
67
|
-
const { target, archive } = detectTarget();
|
|
68
|
-
const assetName = `zond-${target}.${archive}`;
|
|
69
|
-
const downloadUrl = `https://github.com/kirrosh/zond/releases/download/${release.tag_name}/${assetName}`;
|
|
70
|
-
|
|
71
|
-
// Download artifact
|
|
72
|
-
const dlRes = await fetch(downloadUrl);
|
|
73
|
-
if (!dlRes.ok) {
|
|
74
|
-
console.error(`Failed to download ${assetName}: HTTP ${dlRes.status}`);
|
|
75
|
-
return 1;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const tempDir = tmpdir();
|
|
79
|
-
const archivePath = join(tempDir, assetName);
|
|
80
|
-
const archiveBytes = new Uint8Array(await dlRes.arrayBuffer());
|
|
81
|
-
await Bun.write(archivePath, archiveBytes);
|
|
82
|
-
|
|
83
|
-
// Extract
|
|
84
|
-
const extractDir = join(tempDir, `zond-update-${Date.now()}`);
|
|
85
|
-
const mkdirResult = Bun.spawnSync(["mkdir", "-p", extractDir]);
|
|
86
|
-
if (mkdirResult.exitCode !== 0) {
|
|
87
|
-
// Fallback for Windows
|
|
88
|
-
const { mkdirSync } = await import("fs");
|
|
89
|
-
mkdirSync(extractDir, { recursive: true });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (archive === "tar.gz") {
|
|
93
|
-
const tar = Bun.spawnSync(["tar", "-xzf", archivePath, "-C", extractDir]);
|
|
94
|
-
if (tar.exitCode !== 0) {
|
|
95
|
-
console.error("Failed to extract archive.");
|
|
96
|
-
return 1;
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
// Windows zip — use tar (Windows 10+ includes bsdtar)
|
|
100
|
-
const tar = Bun.spawnSync(["tar", "-xf", archivePath, "-C", extractDir]);
|
|
101
|
-
if (tar.exitCode !== 0) {
|
|
102
|
-
console.error("Failed to extract archive.");
|
|
103
|
-
return 1;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Find extracted binary
|
|
108
|
-
const binaryName = process.platform === "win32" ? "zond.exe" : "zond";
|
|
109
|
-
const newBinary = join(extractDir, binaryName);
|
|
110
|
-
|
|
111
|
-
if (!existsSync(newBinary)) {
|
|
112
|
-
console.error(`Expected binary not found: ${newBinary}`);
|
|
113
|
-
return 1;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Replace current binary
|
|
117
|
-
const currentBinary = process.execPath;
|
|
118
|
-
|
|
119
|
-
if (process.platform === "win32") {
|
|
120
|
-
// Windows: can't overwrite running exe — rename current to .old, copy new
|
|
121
|
-
const oldPath = currentBinary + ".old";
|
|
122
|
-
try {
|
|
123
|
-
if (existsSync(oldPath)) unlinkSync(oldPath);
|
|
124
|
-
} catch { /* ignore */ }
|
|
125
|
-
renameSync(currentBinary, oldPath);
|
|
126
|
-
copyFileSync(newBinary, currentBinary);
|
|
127
|
-
} else {
|
|
128
|
-
// Unix: rename new over current (atomic on same filesystem, but we copy across)
|
|
129
|
-
unlinkSync(currentBinary);
|
|
130
|
-
copyFileSync(newBinary, currentBinary);
|
|
131
|
-
chmodSync(currentBinary, 0o755);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Cleanup
|
|
135
|
-
try {
|
|
136
|
-
unlinkSync(archivePath);
|
|
137
|
-
unlinkSync(newBinary);
|
|
138
|
-
} catch { /* best effort */ }
|
|
139
|
-
|
|
140
|
-
console.log(`Updated to v${latestVersion}.`);
|
|
141
|
-
return 0;
|
|
142
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
export const AGENT_SYSTEM_PROMPT = `You are an API testing assistant powered by zond. You help users run, create, 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
|
-
- **query_results**: Query historical test run results and collections from the database.
|
|
7
|
-
- **diagnose_failure**: Analyze a failed test run to identify root causes and suggest fixes.
|
|
8
|
-
- **send_request**: Send an ad-hoc HTTP request for quick testing.
|
|
9
|
-
|
|
10
|
-
Tool usage examples:
|
|
11
|
-
- run_tests: { testPath: "tests/api.yaml" } or { testPath: "tests/", envName: "staging", safe: true }
|
|
12
|
-
- query_results: action must be "list_runs", "get_run" (requires runId), or "list_collections"
|
|
13
|
-
- List runs: { action: "list_runs", limit: 10 }
|
|
14
|
-
- Get run details: { action: "get_run", runId: 1 }
|
|
15
|
-
- List collections: { action: "list_collections" }
|
|
16
|
-
- diagnose_failure: { runId: 1 }
|
|
17
|
-
|
|
18
|
-
Guidelines:
|
|
19
|
-
- When asked to run tests, use the run_tests tool and report results clearly.
|
|
20
|
-
- When a test run has failures, proactively use diagnose_failure to analyze the issues.
|
|
21
|
-
- When asked about past results, use query_results to look up run history.
|
|
22
|
-
- Always provide actionable suggestions when tests fail.
|
|
23
|
-
- Be concise but thorough in your explanations.
|
|
24
|
-
- If a tool call fails with a validation error, re-read the tool schema and retry with corrected arguments.
|
|
25
|
-
- When in safe mode, only GET (read-only) tests will be executed.
|
|
26
|
-
- When using thinking/reasoning, keep your internal reasoning focused and share conclusions with the user.
|
|
27
|
-
`;
|