aztrx-cli 0.2.3 → 0.3.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/README.md CHANGED
@@ -47,7 +47,7 @@ aztrx-cli run http://localhost:3000
47
47
  ## Fix it, not just find it
48
48
 
49
49
  The only feature that needs a key. `--fix` hands the crash to an LLM and applies a
50
- verified patch to your working tree. Set `ANTHROPIC_API_KEY` and run:
50
+ verified patch to your working tree. Anthropic by default — set `ANTHROPIC_API_KEY`:
51
51
 
52
52
  ```bash
53
53
  export ANTHROPIC_API_KEY="your-api-key"
@@ -55,6 +55,16 @@ npx aztrx-cli run http://localhost:3000 --fix # find → explain → he
55
55
  npx aztrx-cli run http://localhost:3000 --fix --yes # non-interactive (CI)
56
56
  ```
57
57
 
58
+ Or use **any model** — Grok, DeepSeek, Gemini, GPT, Kimi, Mistral, OpenRouter, or a
59
+ local Ollama/vLLM. Point it at any OpenAI-compatible `/v1` endpoint:
60
+
61
+ ```bash
62
+ export AZTRX_API_BASE="https://openrouter.ai/api/v1" # or api.openai.com/v1, api.x.ai/v1, ...
63
+ export AZTRX_API_KEY="your-key"
64
+ export AZTRX_MODEL="anthropic/claude-sonnet-5" # any model the provider serves
65
+ npx aztrx-cli run http://localhost:3000 --fix
66
+ ```
67
+
58
68
  `--fix` chains find → explain → heal, then asks *"Apply the fix?"* (`y/N`). On yes, the
59
69
  verified patch lands in your working tree — `git diff` shows the result. Aztrx AI never
60
70
  commits.
@@ -138,8 +148,8 @@ and niche tuning knobs).
138
148
  | `--workers <n>` | Number of parallel detection workers | `1` |
139
149
  | `--swarm` | Hidden alias for `--workers auto` | — |
140
150
  | `--repro-runs <n>` | Flake-rate replay iterations | `3` |
141
- | `--heal-model <model>` | Fallback LLM tier | `claude-sonnet-5` |
142
- | `--heal-fast-model <model>` | Fast/cheap first tier | `claude-haiku-4-5` |
151
+ | `--heal-model <model>` | Fallback LLM tier | `claude-sonnet-5` / `$AZTRX_MODEL` |
152
+ | `--heal-fast-model <model>` | Fast/cheap first tier | `claude-haiku-4-5` / `$AZTRX_FAST_MODEL` |
143
153
  | `--test-command <cmd>` | Test command run against a healed patch | `npm test` (auto-detected) |
144
154
  | `--test-timeout <ms>` | Timeout for the heal test gate | `300000` |
145
155
  | `--no-test` | Skip the test gate during healing | — |
@@ -196,7 +206,7 @@ jobs:
196
206
  permissions: { contents: read, pull-requests: write }
197
207
  steps:
198
208
  - uses: actions/checkout@v4
199
- - uses: DanisChaparov/aztrx@14a187d686710cf59392505075a54b00838fceae
209
+ - uses: DanisChaparov/aztrx@ee586a4410240efb7ed211ab0e1bf9d188361efa
200
210
  with:
201
211
  url: http://localhost:3000
202
212
  start-command: npm run dev # optional — boot the app in the background
@@ -210,7 +220,7 @@ Or as a reusable workflow:
210
220
  on: pull_request
211
221
  jobs:
212
222
  aztrx:
213
- uses: DanisChaparov/aztrx/.github/workflows/aztrx-pr.yml@14a187d686710cf59392505075a54b00838fceae
223
+ uses: DanisChaparov/aztrx/.github/workflows/aztrx-pr.yml@ee586a4410240efb7ed211ab0e1bf9d188361efa
214
224
  with:
215
225
  url: http://localhost:3000
216
226
  start-command: npm run dev
@@ -261,7 +271,7 @@ jobs:
261
271
  sleep 2
262
272
  done
263
273
  - name: Generate badge
264
- run: npx --yes aztrx-cli@0.2.3 run http://localhost:3000 --badge badge.svg
274
+ run: npx --yes aztrx-cli@0.3.0 run http://localhost:3000 --badge badge.svg
265
275
  - name: Commit badge
266
276
  run: |
267
277
  git config user.name "github-actions[bot]"
@@ -279,12 +289,13 @@ are, and the run still exits 0 so the commit step always runs. Swap `push` for a
279
289
 
280
290
  ## Smart Cloud Router
281
291
 
282
- `--heal` is backed by a two-tier router. The fast/cheap model
283
- (`claude-haiku-4-5`) generates first; its patch is gated, compiled, and replayed
284
- against the repro. If the bug still reproduces or the patch fails a gate
285
- aztrx falls back to `claude-sonnet-5` and tries again. Most one-line fixes never
286
- pay for the big model. Tiers are configurable via `AZTRX_FAST_MODEL` /
287
- `AZTRX_MODEL` or `--heal-fast-model` / `--heal-model`.
292
+ `--heal` is backed by a two-tier router: a fast/cheap model generates first, its patch
293
+ is gated, compiled, and replayed against the repro, and if the bug still reproduces (or
294
+ the patch fails a gate) aztrx falls back to the stronger model and tries again. Most
295
+ one-line fixes never pay for the big model. With Anthropic the defaults are
296
+ `claude-haiku-4-5` `claude-sonnet-5`; with any other provider you pick both via
297
+ `AZTRX_FAST_MODEL` / `AZTRX_MODEL` (or `--heal-fast-model` / `--heal-model`). When only
298
+ one model is set, the router collapses to a single tier.
288
299
 
289
300
  ## Security & data flow
290
301
 
@@ -320,7 +331,7 @@ LLM call.
320
331
  CORS.
321
332
  - **`.aztrx/` is gitignored** on `init` — repro specs, reports, and patches stay
322
333
  out of history.
323
- - **Pinned supply chain.** The GitHub Action pins `aztrx-cli@0.2.3` (never
334
+ - **Pinned supply chain.** The GitHub Action pins `aztrx-cli@0.3.0` (never
324
335
  `@latest`).
325
336
 
326
337
  ## Telemetry & privacy
package/dist/cli.js CHANGED
@@ -122,8 +122,8 @@ program
122
122
  .addOption(opt("--fix", "find → explain → heal → apply: one-command fix", "fix"))
123
123
  .addOption(opt("--heal", "closed-loop healing for crash/error findings (implies --repro)", "fix"))
124
124
  .addOption(opt("--magic-fix", "alias for --fix (deprecated)").hideHelp())
125
- .addOption(opt("--heal-model <model>", "LLM model for healing — the fallback tier (default claude-sonnet-5)", "advanced"))
126
- .addOption(opt("--heal-fast-model <model>", "fast/cheap first tier for the smart router (default claude-haiku-4-5)", "advanced"))
125
+ .addOption(opt("--heal-model <model>", "LLM model for healing (default: claude-sonnet-5 or $AZTRX_MODEL)", "advanced"))
126
+ .addOption(opt("--heal-fast-model <model>", "fast/cheap first tier (default: claude-haiku-4-5 or $AZTRX_FAST_MODEL)", "advanced"))
127
127
  .addOption(opt("--test-command <cmd>", "test command run against a healed patch (default: npm test, auto-detected)", "advanced"))
128
128
  .addOption(opt("--test-timeout <ms>", "timeout for the heal test gate, ms", "advanced").default("300000"))
129
129
  .addOption(opt("--no-test", "skip the test gate during healing", "advanced"))
@@ -258,9 +258,9 @@ program
258
258
  }
259
259
  }
260
260
  }
261
- if (!useUi) {
262
- suggestNext(findings, opts);
263
- }
261
+ // Suggest the next flag (e.g. --fix) after the run, in both the live TUI
262
+ // and plain paths. In the TUI this prints after the panel has finished.
263
+ suggestNext(findings, opts);
264
264
  // Drain any in-flight telemetry uploads (each bounded) before exit, so a
265
265
  // pending `--share-data` dispatch isn't killed mid-flight. Never affects
266
266
  // the exit code.
@@ -20,6 +20,7 @@ import * as path from "path";
20
20
  import { redact, unredact } from "./redact.js";
21
21
  import { auditPatch } from "./gates.js";
22
22
  import { generatePatch, modelTiers } from "./llm.js";
23
+ import { hasLlmKey } from "../llm.js";
23
24
  import { applyHunks, createWorktree, diffWorktree, runTests, typecheckWorktree, writeWorktreeFile } from "./sandbox.js";
24
25
  import { bootServer, detectStartCommand } from "./boot.js";
25
26
  import { verifyFix } from "./verify.js";
@@ -121,18 +122,18 @@ export async function heal(finding, opts) {
121
122
  const ctx = { finding, filePath, fileContent: original, redactedContent: red.text };
122
123
  // No transport configured (and no injected generator) → nothing to try. A
123
124
  // higher model tier can't fix a missing key, so bail before paying anything.
124
- if (!opts.patchFn && !process.env.ANTHROPIC_API_KEY) {
125
+ if (!opts.patchFn && !hasLlmKey()) {
125
126
  return {
126
127
  ...base,
127
128
  status: "no-llm",
128
- error: "heal: ANTHROPIC_API_KEY is not set (and no patchFn was injected)",
129
+ error: "heal: no LLM API key is set (set ANTHROPIC_API_KEY, AZTRX_API_KEY, or AZTRX_API_BASE)",
129
130
  };
130
131
  }
131
132
  // The Smart Cloud Router tier plan: fast/cheap first, Sonnet as the fallback.
132
133
  // An injected patchFn collapses to a single tier (there is no model to route).
133
134
  const tiers = opts.patchFn
134
- ? [{ model: opts.model ?? "claude-sonnet-5", label: "sonnet" }]
135
- : modelTiers(opts.model);
135
+ ? [{ model: opts.model ?? "default", label: "sonnet" }]
136
+ : modelTiers(opts.model, opts.fastModel);
136
137
  const wt = await createWorktree(opts.repoRoot, finding.id);
137
138
  // The winning (or last) patch + verification, held back for the final save.
138
139
  let savedPatch = null;
@@ -7,25 +7,23 @@
7
7
  * module's callers.
8
8
  */
9
9
  import { redact } from "./redact.js";
10
- const DEFAULT_MODEL = process.env.AZTRX_MODEL || "claude-sonnet-5";
11
- /** Cheap/fast first tier for the Smart Cloud Router. The idea: most one-line
12
- * fixes are trivial, so try the small model before paying for the big one. */
13
- const FAST_MODEL = process.env.AZTRX_FAST_MODEL || "claude-haiku-4-5-20251001";
14
- const API_URL = "https://api.anthropic.com/v1/messages";
10
+ import { complete, primaryModel, fastModel } from "../llm.js";
15
11
  /**
16
12
  * The Smart Cloud Router's tier plan: fast/cheap first, then the capable model
17
13
  * as the fallback. Collapses to a single tier when the two resolve to the same
18
14
  * model (e.g. `AZTRX_FAST_MODEL=claude-sonnet-5`). Consumers loop over this in
19
15
  * order and stop at the first `healed` result.
20
16
  */
21
- export function modelTiers(fallbackModel) {
22
- const fast = process.env.AZTRX_FAST_MODEL || FAST_MODEL;
23
- const sonnet = fallbackModel || DEFAULT_MODEL;
24
- if (fast === sonnet)
25
- return [{ model: sonnet, label: "sonnet" }];
17
+ export function modelTiers(fallbackModel, fastFallback) {
18
+ const fast = fastFallback || fastModel();
19
+ const primary = fallbackModel || primaryModel();
20
+ if (!primary)
21
+ return []; // no model configured the caller reports no-llm
22
+ if (!fast || fast === primary)
23
+ return [{ model: primary, label: "sonnet" }];
26
24
  return [
27
25
  { model: fast, label: "fast" },
28
- { model: sonnet, label: "sonnet" },
26
+ { model: primary, label: "sonnet" },
29
27
  ];
30
28
  }
31
29
  const SYSTEM = `You are a meticulous bug-fixing engineer. You are given a single source file and a runtime error that occurs in it. Produce a MINIMAL fix as a Search & Replace diff.
@@ -76,33 +74,12 @@ export function parsePatch(raw) {
76
74
  export async function generatePatch(ctx, opts = {}) {
77
75
  if (opts.patchFn)
78
76
  return opts.patchFn(ctx);
79
- const key = process.env.ANTHROPIC_API_KEY;
80
- if (!key) {
81
- throw new Error("heal: ANTHROPIC_API_KEY is not set (and no patchFn was injected)");
82
- }
83
- const res = await fetch(API_URL, {
84
- method: "POST",
85
- headers: {
86
- "content-type": "application/json",
87
- "x-api-key": key,
88
- "anthropic-version": "2023-06-01",
89
- },
90
- body: JSON.stringify({
91
- model: opts.model ?? DEFAULT_MODEL,
92
- max_tokens: 2048,
93
- temperature: 0,
94
- system: SYSTEM,
95
- messages: [{ role: "user", content: buildPrompt(ctx) }],
96
- }),
77
+ const text = await complete({
78
+ system: SYSTEM,
79
+ prompt: buildPrompt(ctx),
80
+ model: opts.model,
81
+ maxTokens: 2048,
82
+ temperature: 0,
97
83
  });
98
- if (!res.ok) {
99
- const body = await res.text().catch(() => "");
100
- throw new Error(`heal: LLM request failed (${res.status}): ${body.slice(0, 300)}`);
101
- }
102
- const data = (await res.json());
103
- const text = (data.content ?? [])
104
- .filter((c) => c.type === "text")
105
- .map((c) => c.text ?? "")
106
- .join("\n");
107
84
  return parsePatch(text);
108
85
  }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Provider-agnostic LLM client. Two transports behind one `complete()`:
3
+ *
4
+ * - Anthropic Messages API (default) — key from `ANTHROPIC_API_KEY` (or
5
+ * `AZTRX_API_KEY`), primary model defaults to `claude-sonnet-5`.
6
+ * - Any OpenAI-compatible `/chat/completions` endpoint — selected by setting
7
+ * `AZTRX_API_BASE`. That single flag unlocks OpenAI, Grok, DeepSeek, Gemini,
8
+ * Kimi, Mistral, OpenRouter, and local models (Ollama / vLLM / LM Studio)
9
+ * with their existing keys.
10
+ *
11
+ * `AZTRX_MODEL` / `AZTRX_FAST_MODEL` pick the model(s); they default sensibly
12
+ * for Anthropic and must be set explicitly when a custom base URL is used.
13
+ */
14
+ /** Resolve the active provider from the environment. */
15
+ export function resolveSettings() {
16
+ const base = process.env.AZTRX_API_BASE?.trim();
17
+ if (base) {
18
+ return {
19
+ provider: "openai",
20
+ apiKey: process.env.AZTRX_API_KEY || process.env.OPENAI_API_KEY,
21
+ baseUrl: base.replace(/\/+$/, ""),
22
+ };
23
+ }
24
+ return {
25
+ provider: "anthropic",
26
+ apiKey: process.env.ANTHROPIC_API_KEY || process.env.AZTRX_API_KEY,
27
+ };
28
+ }
29
+ /** Is any provider configured? Used to decide whether to attempt an LLM call. */
30
+ export function hasLlmKey() {
31
+ return Boolean(resolveSettings().apiKey);
32
+ }
33
+ /** The primary model for the active provider. */
34
+ export function primaryModel() {
35
+ const s = resolveSettings();
36
+ if (s.provider === "anthropic")
37
+ return process.env.AZTRX_MODEL || "claude-sonnet-5";
38
+ return process.env.AZTRX_MODEL || "";
39
+ }
40
+ /** The cheap/fast first tier, or undefined when the provider has none. */
41
+ export function fastModel() {
42
+ const s = resolveSettings();
43
+ if (s.provider === "anthropic")
44
+ return process.env.AZTRX_FAST_MODEL || "claude-haiku-4-5-20251001";
45
+ return process.env.AZTRX_FAST_MODEL || undefined;
46
+ }
47
+ /** Run one completion against the active provider and return the text. */
48
+ export async function complete(opts) {
49
+ const s = resolveSettings();
50
+ const model = opts.model || primaryModel();
51
+ if (!model) {
52
+ throw new Error("no model configured — set AZTRX_MODEL (e.g. AZTRX_MODEL=gpt-4o)");
53
+ }
54
+ if (!s.apiKey) {
55
+ throw new Error(s.provider === "anthropic"
56
+ ? "ANTHROPIC_API_KEY is not set"
57
+ : "AZTRX_API_KEY (or OPENAI_API_KEY) is not set");
58
+ }
59
+ return s.provider === "anthropic"
60
+ ? anthropicComplete(s, model, opts)
61
+ : openaiComplete(s, model, opts);
62
+ }
63
+ async function anthropicComplete(s, model, opts) {
64
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
65
+ method: "POST",
66
+ headers: {
67
+ "content-type": "application/json",
68
+ "x-api-key": s.apiKey,
69
+ "anthropic-version": "2023-06-01",
70
+ },
71
+ body: JSON.stringify({
72
+ model,
73
+ max_tokens: opts.maxTokens ?? 2048,
74
+ temperature: opts.temperature ?? 0,
75
+ system: opts.system,
76
+ messages: [{ role: "user", content: opts.prompt }],
77
+ }),
78
+ });
79
+ if (!res.ok) {
80
+ const body = await res.text().catch(() => "");
81
+ throw new Error(`LLM request failed (${res.status}): ${body.slice(0, 300)}`);
82
+ }
83
+ const data = (await res.json());
84
+ return (data.content ?? [])
85
+ .filter((c) => c.type === "text")
86
+ .map((c) => c.text ?? "")
87
+ .join("\n");
88
+ }
89
+ async function openaiComplete(s, model, opts) {
90
+ const res = await fetch(`${s.baseUrl}/chat/completions`, {
91
+ method: "POST",
92
+ headers: {
93
+ "content-type": "application/json",
94
+ authorization: `Bearer ${s.apiKey}`,
95
+ },
96
+ body: JSON.stringify({
97
+ model,
98
+ max_tokens: opts.maxTokens ?? 2048,
99
+ temperature: opts.temperature ?? 0,
100
+ messages: [
101
+ { role: "system", content: opts.system },
102
+ { role: "user", content: opts.prompt },
103
+ ],
104
+ }),
105
+ });
106
+ if (!res.ok) {
107
+ const body = await res.text().catch(() => "");
108
+ throw new Error(`LLM request failed (${res.status}): ${body.slice(0, 300)}`);
109
+ }
110
+ const data = (await res.json());
111
+ const content = data.choices?.[0]?.message?.content;
112
+ if (typeof content === "string")
113
+ return content;
114
+ if (Array.isArray(content)) {
115
+ return content
116
+ .filter((c) => typeof c === "object" && c !== null && c.type === "text")
117
+ .map((c) => c.text ?? "")
118
+ .join("\n");
119
+ }
120
+ return "";
121
+ }
@@ -12,8 +12,7 @@
12
12
  import * as fs from "fs";
13
13
  import * as path from "path";
14
14
  import * as ts from "typescript";
15
- const MODEL = process.env.AZTRX_MODEL || "claude-sonnet-5";
16
- const API_URL = "https://api.anthropic.com/v1/messages";
15
+ import { complete, hasLlmKey } from "./llm.js";
17
16
  export function detectLang(filePath) {
18
17
  const ext = path.extname(filePath).toLowerCase();
19
18
  if ([".ts", ".tsx", ".mts", ".cts"].includes(ext))
@@ -86,39 +85,20 @@ export async function modernizeFile(repoRoot, filePath) {
86
85
  catch (e) {
87
86
  return { ok: false, original: "", changes: [], error: `cannot read ${filePath}: ${e.message}` };
88
87
  }
89
- const key = process.env.ANTHROPIC_API_KEY;
90
- if (!key) {
91
- return { ok: false, original, changes: [], lang, error: "ANTHROPIC_API_KEY is not set" };
88
+ if (!hasLlmKey()) {
89
+ return { ok: false, original, changes: [], lang, error: "no LLM API key is set (set ANTHROPIC_API_KEY, AZTRX_API_KEY, or AZTRX_API_BASE)" };
92
90
  }
93
91
  let reply;
94
92
  try {
95
- const res = await fetch(API_URL, {
96
- method: "POST",
97
- headers: {
98
- "content-type": "application/json",
99
- "x-api-key": key,
100
- "anthropic-version": "2023-06-01",
101
- },
102
- body: JSON.stringify({
103
- model: MODEL,
104
- max_tokens: 8192,
105
- temperature: 0,
106
- system: SYSTEM,
107
- messages: [{ role: "user", content: buildPrompt(original, lang) }],
108
- }),
93
+ reply = await complete({
94
+ system: SYSTEM,
95
+ prompt: buildPrompt(original, lang),
96
+ maxTokens: 8192,
97
+ temperature: 0,
109
98
  });
110
- if (!res.ok) {
111
- const body = await res.text().catch(() => "");
112
- return { ok: false, original, changes: [], lang, error: `LLM request failed (${res.status}): ${body.slice(0, 300)}` };
113
- }
114
- const data = (await res.json());
115
- reply = (data.content ?? [])
116
- .filter((c) => c.type === "text")
117
- .map((c) => c.text ?? "")
118
- .join("\n");
119
99
  }
120
100
  catch (e) {
121
- return { ok: false, original, changes: [], lang, error: `LLM request failed: ${e.message}` };
101
+ return { ok: false, original, changes: [], lang, error: e.message };
122
102
  }
123
103
  let parsed;
124
104
  try {
@@ -11,7 +11,7 @@ export async function attachNetworkGuard(page, opts) {
11
11
  host = new URL(url).hostname.toLowerCase();
12
12
  }
13
13
  catch {
14
- await route.abort();
14
+ await route.abort("blockedbyclient");
15
15
  return;
16
16
  }
17
17
  if (host === "localhost" || host === "127.0.0.1" || host === "::1") {
@@ -25,7 +25,7 @@ export async function attachNetworkGuard(page, opts) {
25
25
  }
26
26
  }
27
27
  opts.onBlock?.(url);
28
- await route.abort();
28
+ await route.abort("blockedbyclient");
29
29
  });
30
30
  }
31
31
  /** Builds the allow-list: target origin + each `--allow-host`. */
@@ -11,8 +11,7 @@
11
11
  * The offer-to-apply line is emitted only when verified fixes are actually ready,
12
12
  * so `--explain` (no healing) never promises a fix it doesn't have.
13
13
  */
14
- const MODEL = process.env.AZTRX_MODEL || "claude-sonnet-5";
15
- const API_URL = "https://api.anthropic.com/v1/messages";
14
+ import { complete, hasLlmKey } from "./llm.js";
16
15
  function normalizeLang(lang) {
17
16
  return lang === "ru" ? "ru" : "en";
18
17
  }
@@ -126,31 +125,12 @@ function buildLlmPrompt(findings, lang, hasHealed) {
126
125
  const SYSTEM = "You are the plain-spoken explainer for a QA tool called Aztrx AI. You turn raw runtime-finding data into a concise, human-language summary for a developer. Never invent details absent from the data. Respond in the requested language only.";
127
126
  async function summarizeFindingsLlm(findings, lang) {
128
127
  const hasHealed = findings.some((f) => f.heal?.status === "healed");
129
- const res = await fetch(API_URL, {
130
- method: "POST",
131
- headers: {
132
- "content-type": "application/json",
133
- "x-api-key": process.env.ANTHROPIC_API_KEY,
134
- "anthropic-version": "2023-06-01",
135
- },
136
- body: JSON.stringify({
137
- model: MODEL,
138
- max_tokens: 1024,
139
- temperature: 0.2,
140
- system: SYSTEM,
141
- messages: [{ role: "user", content: buildLlmPrompt(findings, lang, hasHealed) }],
142
- }),
143
- });
144
- if (!res.ok) {
145
- const body = await res.text().catch(() => "");
146
- throw new Error(`summarize: LLM request failed (${res.status}): ${body.slice(0, 300)}`);
147
- }
148
- const data = (await res.json());
149
- const text = (data.content ?? [])
150
- .filter((c) => c.type === "text")
151
- .map((c) => c.text ?? "")
152
- .join("\n")
153
- .trim();
128
+ const text = (await complete({
129
+ system: SYSTEM,
130
+ prompt: buildLlmPrompt(findings, lang, hasHealed),
131
+ maxTokens: 1024,
132
+ temperature: 0.2,
133
+ })).trim();
154
134
  return text || summarizeFindingsTemplate(findings, lang);
155
135
  }
156
136
  /**
@@ -162,7 +142,7 @@ export async function summarizeFindings(findings, opts = {}) {
162
142
  const lang = normalizeLang(opts.lang);
163
143
  if (findings.length === 0)
164
144
  return summarizeFindingsTemplate(findings, lang);
165
- if (!process.env.ANTHROPIC_API_KEY)
145
+ if (!hasLlmKey())
166
146
  return summarizeFindingsTemplate(findings, lang);
167
147
  try {
168
148
  return await summarizeFindingsLlm(findings, lang);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aztrx-cli",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "Aztrx AI — runtime stress-tester for web apps. Detect bugs, then prove them with an executable repro.",
6
6
  "license": "Apache-2.0",