firegoat 0.14.0 → 0.15.1
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 +21 -6
- package/dist/cli.js +212 -34
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Status: early (v0.11). See `plan.md`.
|
|
|
7
7
|
## Quickstart
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npx firegoat init # creates firegoat.yaml and .firegoat/, installs the
|
|
10
|
+
npx firegoat init # creates firegoat.yaml and .firegoat/, installs the firegoat-onboarding skill, copies a prompt for your agent
|
|
11
11
|
# edit firegoat.yaml (or paste the prompt into your coding agent and let it)
|
|
12
12
|
npx firegoat eval # runs your claude on each test, in .firegoat/runs/<runId>/<test>/<n>/work
|
|
13
13
|
```
|
|
@@ -82,6 +82,21 @@ tests:
|
|
|
82
82
|
npx firegoat eval --var SHA=$(git rev-parse HEAD)
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
### Same tests on several models
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
model: [claude-haiku-5-5, claude-sonnet-5-5]
|
|
89
|
+
runs: 2
|
|
90
|
+
tests:
|
|
91
|
+
- name: quickstart
|
|
92
|
+
prompt: Get the acme SDK's hello-world example running and save its output to output.txt.
|
|
93
|
+
end_goal: output.txt holds the example's output.
|
|
94
|
+
checks:
|
|
95
|
+
- run: test -s output.txt
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`model:` is passed to claude as `--model`. A list is a matrix: each test runs `runs` times per model, numbered `1..runs×models` (here `quickstart/1`, `2` on haiku, `3`, `4` on sonnet). `npx firegoat eval --model claude-opus-5-5` replaces it for one eval; repeat the flag for a matrix. Unset, firegoat passes no `--model` and claude uses its default. Each run's `run.json` records `model` (what firegoat asked for, or null) and `claudeModel` (what claude reported starting with), and the output labels each run with its model.
|
|
99
|
+
|
|
85
100
|
### Env vars for the agent: a home dir per run, a feature flag
|
|
86
101
|
|
|
87
102
|
```yaml
|
|
@@ -99,19 +114,19 @@ tests:
|
|
|
99
114
|
- run: ls "$ACME_HOME"/sessions | grep -q .
|
|
100
115
|
```
|
|
101
116
|
|
|
102
|
-
`env:` goes to the agent, and to `workdir_setup` and checks of the same run, so checks see the same values. Placeholders: `{{vars.KEY}}`, `{{run.id}}` (the eval's run id), `{{run.n}}` (1..runs), `{{test.name}}`, and `$FIREGOAT_OUT` / `$FIREGOAT_WORKDIR` (also `${...}`), the same dirs `setup`, `workdir_setup` and checks see. There is no other `$` expansion: `$HOME` stays the literal text `$HOME`. `setup` runs once, before any run: it gets only the top-level entries that use nothing but `{{vars.*}}`, `{{run.id}}` and `$FIREGOAT_OUT`. Values are strings (quote numbers). `PATH`, `CLAUDE_CONFIG_DIR` and `FIREGOAT_*` are firegoat's and can't be set
|
|
117
|
+
`env:` goes to the agent, and to `workdir_setup` and checks of the same run, so checks see the same values. Placeholders: `{{vars.KEY}}`, `{{run.id}}` (the eval's run id), `{{run.n}}` (1..runs, or 1..runs×models), `{{test.name}}`, and `$FIREGOAT_OUT` / `$FIREGOAT_WORKDIR` (also `${...}`), the same dirs `setup`, `workdir_setup` and checks see. There is no other `$` expansion: `$HOME` stays the literal text `$HOME`. `setup` runs once, before any run: it gets only the top-level entries that use nothing but `{{vars.*}}`, `{{run.id}}` and `$FIREGOAT_OUT`. Values are strings (quote numbers). `PATH`, `CLAUDE_CONFIG_DIR` and `FIREGOAT_*` are firegoat's and can't be set; `ANTHROPIC_MODEL` neither: choose the model with `model:`.
|
|
103
118
|
|
|
104
119
|
## What eval does
|
|
105
120
|
|
|
106
121
|
- claude runs on its own login (or `ANTHROPIC_API_KEY`, if set) with `CLAUDE_CONFIG_DIR=.firegoat/.claude` and `--setting-sources "" --strict-mcp-config --disable-slash-commands`: none of your settings, CLAUDE.md, plugins, skills or MCP servers. claude saves its session there, in its own format.
|
|
107
|
-
- No sandbox: the agent's folder is inside `.firegoat/runs/`, and it can reach the rest of your machine. In `.firegoat/`, git ignores
|
|
122
|
+
- No sandbox: the agent's folder is inside `.firegoat/runs/`, and it can reach the rest of your machine. In `.firegoat/`, git ignores everything except `fixtures/`: keep test fixtures (scripts, sample data) in `.firegoat/fixtures/` and commit them with `firegoat.yaml`.
|
|
108
123
|
- In a linked git worktree (`git worktree add`), `.firegoat/runs/` and `.firegoat/.claude/` are in the main checkout, so runs (and their session files) survive `git worktree remove`; `firegoat.yaml`, fixtures and `setup:` stay in the worktree. Each `summary.json` records `dir` (where eval ran) and `git: {branch, commit, dirty}`, and `.firegoat/runs/index.jsonl` gets one line per eval (`runId, startedAt, dir, branch, commit, passed, failed, notGraded, cost`) to tell runs from different branches apart.
|
|
109
124
|
- The agent gets your environment plus `env:`, never `vars:`. Vars that npm/npx set for firegoat itself (`npm_config_*`, `npm_lifecycle_*`, `npm_package_*`, `npm_execpath`, `npm_node_execpath`, `npm_command`, `INIT_CWD`) are removed from every env firegoat starts (agent, setup, workdir_setup, checks): `npx -y firegoat` sets `npm_config_yes=true`, which would make the agent's `npm init` / `npx create-*` skip their prompts. Set one under `env:` to keep it.
|
|
110
|
-
- Runs (tests × `runs`) go 4 at a time, each in its own folder with its own session; `--parallel N` changes that, `--parallel 1` runs them one after another. `summary.json` lists them in `firegoat.yaml` order.
|
|
125
|
+
- Runs (tests × models × `runs`) go 4 at a time, each in its own folder with its own session; `--parallel N` changes that, `--parallel 1` runs them one after another. `summary.json` lists them in `firegoat.yaml` order.
|
|
111
126
|
- Script checks run in the agent's folder afterwards and set the exit code. `end_goal` is not graded by firegoat: it's for you, or your coding agent, to judge from `run.json`, the session and the folder.
|
|
112
127
|
- Output per run, in `.firegoat/runs/<runId>/<test>/<n>/`: `transcript.txt` (each tool call with its command and short output, the final answer, files made; checks get it as `$FIREGOAT_TRANSCRIPT`), `run.json` (status, prompt, end_goal, check results, paths), `work/`, logs. Read the transcript to judge `end_goal`; claude's full session file is ~50x larger. `summary.json` covers the whole eval; `--json` prints it.
|
|
113
128
|
|
|
114
|
-
`firegoat.yaml` keys: `runs`, `setup`, `workdir_setup`, `vars`, `env`, and `tests` (`name`, `prompt`, `end_goal`, `checks`, `timeout`, `env`). Unknown keys are errors. Full reference: `npx firegoat init --help`.
|
|
129
|
+
`firegoat.yaml` keys: `runs`, `model`, `setup`, `workdir_setup`, `vars`, `env`, and `tests` (`name`, `prompt`, `end_goal`, `checks`, `timeout`, `env`). Unknown keys are errors. Full reference: `npx firegoat init --help`.
|
|
115
130
|
|
|
116
131
|
Want a plain `npm i acme-sdk` to get your build, with no hint in the prompt? See [`examples/local-npm-registry`](examples/local-npm-registry/README.md).
|
|
117
132
|
|
|
@@ -130,7 +145,7 @@ Something in firegoat got in your way (or your agent's)? Tell us from the termin
|
|
|
130
145
|
npx firegoat feedback "eval hid claude's spend-limit error" --type bug --body "ran npx firegoat eval, got 'not graded' x3"
|
|
131
146
|
```
|
|
132
147
|
|
|
133
|
-
It goes to the firegoat team with the firegoat version, your OS and node version, and which coding agent sent it. Don't include secrets: a report that looks like it has one is refused.
|
|
148
|
+
It goes to the firegoat team with the firegoat version, your OS and node version, and which coding agent sent it. Don't include secrets: a report that looks like it has one is refused. The first send registers your device and saves its token to `~/.config/firegoat/feedback-device.json` (`$XDG_CONFIG_HOME` respected), which raises the daily report limit.
|
|
134
149
|
|
|
135
150
|
## Develop
|
|
136
151
|
|
package/dist/cli.js
CHANGED
|
@@ -6947,14 +6947,14 @@ var require_public_api = __commonJS(function(exports) {
|
|
|
6947
6947
|
// package.json
|
|
6948
6948
|
var package_default = {
|
|
6949
6949
|
name: "firegoat",
|
|
6950
|
-
version: "0.
|
|
6950
|
+
version: "0.15.1",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
6954
6954
|
},
|
|
6955
6955
|
devDependencies: {
|
|
6956
6956
|
"@biomejs/biome": "^2.5.15",
|
|
6957
|
-
"@firegoat/feedback": "0.
|
|
6957
|
+
"@firegoat/feedback": "0.2.0",
|
|
6958
6958
|
"@types/bun": "latest",
|
|
6959
6959
|
typescript: "^7",
|
|
6960
6960
|
yaml: "^2.9.1"
|
|
@@ -7075,6 +7075,7 @@ function parseConfig(text) {
|
|
|
7075
7075
|
fail("(root)", "must be a mapping");
|
|
7076
7076
|
onlyKeys(raw, "(root)", [
|
|
7077
7077
|
"runs",
|
|
7078
|
+
"model",
|
|
7078
7079
|
"setup",
|
|
7079
7080
|
"workdir_setup",
|
|
7080
7081
|
"vars",
|
|
@@ -7082,6 +7083,7 @@ function parseConfig(text) {
|
|
|
7082
7083
|
"tests"
|
|
7083
7084
|
]);
|
|
7084
7085
|
const runs = posInt(raw.runs, "runs", 1);
|
|
7086
|
+
const models = parseModels(raw.model);
|
|
7085
7087
|
const setup = commands(raw.setup, "setup");
|
|
7086
7088
|
const workdirSetup = commands(raw.workdir_setup, "workdir_setup");
|
|
7087
7089
|
const vars = parseVars(raw.vars);
|
|
@@ -7122,6 +7124,7 @@ function parseConfig(text) {
|
|
|
7122
7124
|
});
|
|
7123
7125
|
return {
|
|
7124
7126
|
runs,
|
|
7127
|
+
models,
|
|
7125
7128
|
setup,
|
|
7126
7129
|
workdirSetup,
|
|
7127
7130
|
vars,
|
|
@@ -7139,6 +7142,22 @@ function commands(raw, path) {
|
|
|
7139
7142
|
fail(path, "must be a shell command or a list of them");
|
|
7140
7143
|
return raw.map((c, i) => str(c, `${path}[${i}]`));
|
|
7141
7144
|
}
|
|
7145
|
+
function parseModels(raw) {
|
|
7146
|
+
if (raw === undefined)
|
|
7147
|
+
return [];
|
|
7148
|
+
if (typeof raw === "string")
|
|
7149
|
+
return [str(raw, "model")];
|
|
7150
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
7151
|
+
fail("model", "must be a model name or a non-empty list of them");
|
|
7152
|
+
const seen = new Set;
|
|
7153
|
+
return raw.map((m, i) => {
|
|
7154
|
+
const name = str(m, `model[${i}]`);
|
|
7155
|
+
if (seen.has(name))
|
|
7156
|
+
fail(`model[${i}]`, `duplicate model ${JSON.stringify(name)}`);
|
|
7157
|
+
seen.add(name);
|
|
7158
|
+
return name;
|
|
7159
|
+
});
|
|
7160
|
+
}
|
|
7142
7161
|
var VAR_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
7143
7162
|
function parseVars(raw) {
|
|
7144
7163
|
if (raw === undefined)
|
|
@@ -7175,6 +7194,8 @@ function parseEnv(raw, path) {
|
|
|
7175
7194
|
const p = `${path}.${k}`;
|
|
7176
7195
|
if (!VAR_KEY.test(k))
|
|
7177
7196
|
fail(p, "name must be letters, digits and _, not starting with a digit");
|
|
7197
|
+
if (k === "ANTHROPIC_MODEL")
|
|
7198
|
+
fail(p, "is reserved: choose the model with top-level model: (or eval --model)");
|
|
7178
7199
|
if (RESERVED_ENV.has(k) || k.startsWith("FIREGOAT_"))
|
|
7179
7200
|
fail(p, "is set by firegoat and cannot be overridden (reserved: PATH, CLAUDE_CONFIG_DIR, CLAUDE_SECURESTORAGE_CONFIG_DIR, FIREGOAT_*)");
|
|
7180
7201
|
if (typeof v !== "string")
|
|
@@ -7298,7 +7319,12 @@ async function runClaude(opts) {
|
|
|
7298
7319
|
let pending = "";
|
|
7299
7320
|
const exited = await run({
|
|
7300
7321
|
cmd: opts.bin,
|
|
7301
|
-
args: [
|
|
7322
|
+
args: [
|
|
7323
|
+
"-p",
|
|
7324
|
+
opts.prompt,
|
|
7325
|
+
...CLAUDE_FLAGS,
|
|
7326
|
+
...opts.model === undefined ? [] : ["--model", opts.model]
|
|
7327
|
+
],
|
|
7302
7328
|
cwd: opts.cwd,
|
|
7303
7329
|
env: opts.env,
|
|
7304
7330
|
timeoutMs: opts.timeoutMs,
|
|
@@ -7322,6 +7348,7 @@ async function runClaude(opts) {
|
|
|
7322
7348
|
if (exited.error)
|
|
7323
7349
|
return {
|
|
7324
7350
|
events: [],
|
|
7351
|
+
claudeModel: null,
|
|
7325
7352
|
ok: false,
|
|
7326
7353
|
reason: `could not start ${opts.bin}: ${exited.error.message}`
|
|
7327
7354
|
};
|
|
@@ -7339,15 +7366,18 @@ async function runClaude(opts) {
|
|
|
7339
7366
|
`)[0]?.slice(0, 300);
|
|
7340
7367
|
return {
|
|
7341
7368
|
events,
|
|
7369
|
+
claudeModel: null,
|
|
7342
7370
|
ok: false,
|
|
7343
7371
|
reason: `claude did not start a session (exit ${exited.code ?? "by signal"})${said ? `: ${said}` : ", see stderr.txt"}`
|
|
7344
7372
|
};
|
|
7345
7373
|
}
|
|
7346
7374
|
const sessionId = String(init.session_id);
|
|
7375
|
+
const claudeModel = typeof init.model === "string" ? init.model : null;
|
|
7347
7376
|
const result = events.findLast((e) => e?.type === "result");
|
|
7348
7377
|
if (exited.timedOut)
|
|
7349
7378
|
return {
|
|
7350
7379
|
events,
|
|
7380
|
+
claudeModel,
|
|
7351
7381
|
ok: false,
|
|
7352
7382
|
reason: `timed out after ${opts.timeoutMs / 1000}s`,
|
|
7353
7383
|
result,
|
|
@@ -7356,6 +7386,7 @@ async function runClaude(opts) {
|
|
|
7356
7386
|
if (!result)
|
|
7357
7387
|
return {
|
|
7358
7388
|
events,
|
|
7389
|
+
claudeModel,
|
|
7359
7390
|
ok: false,
|
|
7360
7391
|
reason: `claude exited ${exited.code ?? "by signal"} without a result, see stderr.txt`,
|
|
7361
7392
|
sessionId
|
|
@@ -7363,12 +7394,13 @@ async function runClaude(opts) {
|
|
|
7363
7394
|
if (result.is_error === true || result.subtype !== "success")
|
|
7364
7395
|
return {
|
|
7365
7396
|
events,
|
|
7397
|
+
claudeModel,
|
|
7366
7398
|
ok: false,
|
|
7367
7399
|
reason: `claude result is an error (subtype=${result.subtype})${result.result ? `: ${result.result.slice(0, 300)}` : ""}`,
|
|
7368
7400
|
result,
|
|
7369
7401
|
sessionId
|
|
7370
7402
|
};
|
|
7371
|
-
return { events, ok: true, result, sessionId };
|
|
7403
|
+
return { events, claudeModel, ok: true, result, sessionId };
|
|
7372
7404
|
}
|
|
7373
7405
|
async function runShell(opts) {
|
|
7374
7406
|
opts.log.write(`$ ${opts.run}
|
|
@@ -7420,8 +7452,9 @@ var EXIT = { pass: 0, fail: 1, notGraded: 2, usage: 3 };
|
|
|
7420
7452
|
// src/firegoat-dir.ts
|
|
7421
7453
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7422
7454
|
import { join } from "node:path";
|
|
7423
|
-
var GITIGNORE =
|
|
7424
|
-
|
|
7455
|
+
var GITIGNORE = `*
|
|
7456
|
+
!fixtures/
|
|
7457
|
+
!fixtures/**
|
|
7425
7458
|
`;
|
|
7426
7459
|
function ensureFiregoatDir(root) {
|
|
7427
7460
|
const dir = join(root, ".firegoat");
|
|
@@ -7487,7 +7520,7 @@ function topHelp(version) {
|
|
|
7487
7520
|
folder, with none of your claude config; firegoat saves the session and runs your
|
|
7488
7521
|
script checks.
|
|
7489
7522
|
|
|
7490
|
-
npx firegoat init create firegoat.yaml and .firegoat/, install the
|
|
7523
|
+
npx firegoat init create firegoat.yaml and .firegoat/, install the skill
|
|
7491
7524
|
npx firegoat eval run the tests (claude must be logged in: run \`claude\` once)
|
|
7492
7525
|
npx firegoat feedback "<what got in the way>" tell the firegoat team
|
|
7493
7526
|
|
|
@@ -7504,16 +7537,20 @@ ${EXIT_CODES}`;
|
|
|
7504
7537
|
var INIT_HELP = `Usage: firegoat init [--force] [--no-skills]
|
|
7505
7538
|
|
|
7506
7539
|
In the current folder: creates .firegoat/ and a starter firegoat.yaml, installs the
|
|
7507
|
-
firegoat-onboarding
|
|
7540
|
+
firegoat-onboarding skill into .claude/skills/ (npx skills
|
|
7508
7541
|
add firegoat-ai/skills; asks first in a terminal), then prints a prompt to paste into
|
|
7509
|
-
your coding agent. In .firegoat/, git ignores
|
|
7542
|
+
your coding agent. In .firegoat/, git ignores everything but fixtures/: keep test fixtures
|
|
7510
7543
|
(helper scripts, sample data) in .firegoat/fixtures/ and commit them.
|
|
7511
7544
|
|
|
7512
7545
|
--force replace an existing firegoat.yaml
|
|
7513
|
-
--no-skills don't install the
|
|
7546
|
+
--no-skills don't install the skill
|
|
7514
7547
|
|
|
7515
7548
|
firegoat.yaml reference:
|
|
7516
7549
|
runs: 1 runs per test
|
|
7550
|
+
model: <name> | [<name>, ...]
|
|
7551
|
+
claude --model; a list runs every test runs times
|
|
7552
|
+
per model (n counts 1..runs x models). Unset: no
|
|
7553
|
+
--model, claude's default. eval --model overrides
|
|
7517
7554
|
setup: <sh> | [<sh>, ...] once per eval, in this folder; what it writes to
|
|
7518
7555
|
$FIREGOAT_OUT is first on the agent's PATH
|
|
7519
7556
|
workdir_setup: <sh> | [...] in each fresh folder before the agent starts
|
|
@@ -7526,7 +7563,11 @@ firegoat.yaml reference:
|
|
|
7526
7563
|
(no other $ expansion: $HOME stays literal).
|
|
7527
7564
|
setup gets only the entries that use nothing but
|
|
7528
7565
|
{{vars.*}}, {{run.id}} and $FIREGOAT_OUT.
|
|
7529
|
-
PATH, CLAUDE_CONFIG_DIR
|
|
7566
|
+
PATH, CLAUDE_CONFIG_DIR, FIREGOAT_* and
|
|
7567
|
+
ANTHROPIC_MODEL (use model:) are reserved.
|
|
7568
|
+
Runs go 4 at a time: give a tool with a global home
|
|
7569
|
+
a per-run one (or use eval --parallel 1), e.g.
|
|
7570
|
+
MYTOOL_HOME: "/tmp/mytool-{{run.id}}-{{test.name}}-{{run.n}}"
|
|
7530
7571
|
tests:
|
|
7531
7572
|
- name: quickstart letters, digits, '.', '_', '-'; unique
|
|
7532
7573
|
prompt: ... what a user would ask the agent; one concrete outcome
|
|
@@ -7563,11 +7604,12 @@ Example:
|
|
|
7563
7604
|
checks:
|
|
7564
7605
|
- run: grep -qE 'evt_[A-Za-z0-9]+' "$FIREGOAT_TRANSCRIPT"
|
|
7565
7606
|
- run: |
|
|
7566
|
-
|
|
7607
|
+
test -s "$FIREGOAT_TRANSCRIPT" &&
|
|
7608
|
+
! grep -Eq '^\\[[0-9]+\\] \\$ .*npm (install|i|add) .*acme' "$FIREGOAT_TRANSCRIPT"
|
|
7567
7609
|
|
|
7568
|
-
Exit codes: 0 done, 2 the
|
|
7569
|
-
var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--
|
|
7570
|
-
[--json] [--claude-bin PATH]
|
|
7610
|
+
Exit codes: 0 done, 2 the skill install failed, 3 usage error.`;
|
|
7611
|
+
var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--model NAME].. [--parallel N]
|
|
7612
|
+
[--var KEY=value].. [--json] [--claude-bin PATH]
|
|
7571
7613
|
|
|
7572
7614
|
Runs each test in firegoat.yaml (in the current folder): your claude gets the prompt
|
|
7573
7615
|
in a fresh, empty folder, .firegoat/runs/<runId>/<test>/<n>/work, with setup:'s
|
|
@@ -7584,7 +7626,11 @@ fixtures and setup: stay in the current folder.
|
|
|
7584
7626
|
|
|
7585
7627
|
test.. only these tests (by name)
|
|
7586
7628
|
--runs N runs per test (overrides runs: in firegoat.yaml)
|
|
7587
|
-
--
|
|
7629
|
+
--model NAME claude --model, replacing model: in firegoat.yaml; repeat it
|
|
7630
|
+
(--model a --model b) to run each test runs times per model
|
|
7631
|
+
--parallel N runs at once (default 4; 1 runs them one after another).
|
|
7632
|
+
Tools with global on-disk state collide: give each run its
|
|
7633
|
+
own home via env: ({{run.id}}-{{test.name}}-{{run.n}}) or use 1
|
|
7588
7634
|
--var KEY=value fills {{vars.KEY}}; env for setup, workdir_setup and checks
|
|
7589
7635
|
--json the summary as JSON on stdout (or {error, exitCode} if eval
|
|
7590
7636
|
stops early); progress goes to stderr
|
|
@@ -7595,8 +7641,10 @@ Output: .firegoat/runs/<runId>/
|
|
|
7595
7641
|
git {branch, commit, dirty} (no git outside a repo)
|
|
7596
7642
|
../index.jsonl one line per eval: runId, startedAt, dir, branch,
|
|
7597
7643
|
commit, passed, failed, notGraded, cost
|
|
7598
|
-
<test>/<n>/run.json one run: status and reason (from the checks),
|
|
7599
|
-
|
|
7644
|
+
<test>/<n>/run.json one run: status and reason (from the checks), model
|
|
7645
|
+
(asked for, or null) and claudeModel (what claude's
|
|
7646
|
+
init line reported), prompt, end_goal,
|
|
7647
|
+
checks [{run, code}], and paths: workdir,
|
|
7600
7648
|
transcript, session (claude's session file), stderr,
|
|
7601
7649
|
checksLog
|
|
7602
7650
|
<test>/<n>/transcript.txt what the agent did: each tool call with its command,
|
|
@@ -7610,6 +7658,8 @@ yours to judge: read transcript.txt first (a few KB); the session file has every
|
|
|
7610
7658
|
Examples:
|
|
7611
7659
|
npx firegoat eval --json all tests, JSON summary on stdout
|
|
7612
7660
|
npx firegoat eval quickstart --runs 3 one test, three runs
|
|
7661
|
+
npx firegoat eval --model claude-haiku-5-5 --model claude-sonnet-5-5
|
|
7662
|
+
each test on both models
|
|
7613
7663
|
npx firegoat eval --var SHA=$(git rev-parse HEAD)
|
|
7614
7664
|
|
|
7615
7665
|
A run can take minutes (default timeout 600s per test).
|
|
@@ -7627,7 +7677,9 @@ title, then what you ran and what happened in --body (- reads stdin).
|
|
|
7627
7677
|
|
|
7628
7678
|
Sent along: the firegoat version, your OS and node version, and which agent you are
|
|
7629
7679
|
(claude-code/codex/cursor, from the environment). Don't include secrets: a report that
|
|
7630
|
-
looks like it contains one is refused.
|
|
7680
|
+
looks like it contains one is refused. The first send registers this device and saves
|
|
7681
|
+
its token to ~/.config/firegoat/feedback-device.json ($XDG_CONFIG_HOME respected),
|
|
7682
|
+
which raises the daily report limit.
|
|
7631
7683
|
|
|
7632
7684
|
Example:
|
|
7633
7685
|
npx firegoat feedback "checks can't grep commands with quotes" --type bug \\
|
|
@@ -7849,7 +7901,8 @@ function plainUI(write) {
|
|
|
7849
7901
|
printed.add(r);
|
|
7850
7902
|
const label = r.state === "error" ? "ERROR" : r.state.toUpperCase();
|
|
7851
7903
|
const u = `$${(r.cost ?? 0).toFixed(2)}, ${r.turns} turns, ${r.seconds ?? 0}s`;
|
|
7852
|
-
|
|
7904
|
+
const model = r.model === null ? "" : ` [${r.model}]`;
|
|
7905
|
+
write(`${label} ${r.test}#${r.n}${model} ${r.reason} (${u})`);
|
|
7853
7906
|
}
|
|
7854
7907
|
},
|
|
7855
7908
|
done: ({ runsDir, rows: all }) => {
|
|
@@ -7916,10 +7969,12 @@ function ttyUI(out, opts) {
|
|
|
7916
7969
|
const mark = setup.state === "ok" ? p(C.green, "✓") : setup.state === "fail" ? p(C.red, "✗") : p(C.ember, SPIN[frame % SPIN.length]);
|
|
7917
7970
|
out.push(` ${setup.state === "running" ? p(C.ember, "◐") : p(C.green, "●")} setup${" ".repeat(Math.max(1, w - 20))}${mark}${setup.seconds !== undefined ? ` ${setup.seconds}s` : ""}`);
|
|
7918
7971
|
}
|
|
7972
|
+
const modelCols = Math.min(24, Math.max(0, ...rows.map((r) => r.model?.length ?? 0)));
|
|
7919
7973
|
rows.forEach((r, i) => {
|
|
7920
7974
|
out.push(p(C.gray, " │"));
|
|
7921
7975
|
const num = String(i + 1).padStart(2, "0");
|
|
7922
7976
|
const name = cut(`${r.test}${r.n > 1 ? `#${r.n}` : ""}`, 16).padEnd(16);
|
|
7977
|
+
const model = modelCols > 0 ? ` ${p(C.cyan, cut(r.model ?? "", modelCols).padEnd(modelCols))}` : "";
|
|
7923
7978
|
let detail = "";
|
|
7924
7979
|
if (r.state === "running")
|
|
7925
7980
|
detail = p(C.gray, `turn ${r.turns}`);
|
|
@@ -7927,7 +7982,7 @@ function ttyUI(out, opts) {
|
|
|
7927
7982
|
detail = `${checksPassed(r).padEnd(11)} ${p(C.gray, usage(r))}`;
|
|
7928
7983
|
else if (r.state === "error")
|
|
7929
7984
|
detail = p(C.gray, usage(r));
|
|
7930
|
-
out.push(` ${glyph(r)} ${p(C.gray, num)} ${p(C.bold, name)} ${p(C.ember, ramp(r))} ${statusWord(r)} ${detail}`);
|
|
7985
|
+
out.push(` ${glyph(r)} ${p(C.gray, num)} ${p(C.bold, name)}${model} ${p(C.ember, ramp(r))} ${statusWord(r)} ${detail}`);
|
|
7931
7986
|
if (r.state === "running" && r.command)
|
|
7932
7987
|
out.push(p(C.gray, ` ┊ └ $ ${cut(r.command, w - 16)}`));
|
|
7933
7988
|
if (r.state === "fail")
|
|
@@ -8026,6 +8081,7 @@ class UsageError extends Error {
|
|
|
8026
8081
|
function parseArgs(args) {
|
|
8027
8082
|
const opts = {
|
|
8028
8083
|
names: [],
|
|
8084
|
+
models: [],
|
|
8029
8085
|
parallel: 4,
|
|
8030
8086
|
json: false,
|
|
8031
8087
|
vars: {},
|
|
@@ -8059,6 +8115,13 @@ function parseArgs(args) {
|
|
|
8059
8115
|
if (!/^\d+$/.test(v) || n < 1)
|
|
8060
8116
|
throw new UsageError(`--runs must be a positive integer, got ${v}`);
|
|
8061
8117
|
opts.runs = n;
|
|
8118
|
+
} else if (flag === "--model") {
|
|
8119
|
+
const v = value();
|
|
8120
|
+
if (v.trim() === "")
|
|
8121
|
+
throw new UsageError("--model needs a model name");
|
|
8122
|
+
if (opts.models.includes(v))
|
|
8123
|
+
throw new UsageError(`--model ${v} given twice`);
|
|
8124
|
+
opts.models.push(v);
|
|
8062
8125
|
} else if (flag === "--parallel") {
|
|
8063
8126
|
const v = value();
|
|
8064
8127
|
const n = Number(v);
|
|
@@ -8085,6 +8148,10 @@ function findSession(configDir, sessionId) {
|
|
|
8085
8148
|
}
|
|
8086
8149
|
return null;
|
|
8087
8150
|
}
|
|
8151
|
+
function grepsSessionJson(run) {
|
|
8152
|
+
const unescaped = run.replace(/\\/g, "");
|
|
8153
|
+
return unescaped.includes("FIREGOAT_SESSION") && /"command":"|"name":"Write"/.test(unescaped);
|
|
8154
|
+
}
|
|
8088
8155
|
function newRunId() {
|
|
8089
8156
|
const ts = new Date().toISOString().replace(/\.\d+Z$/, "Z").replace(/[-:]/g, "");
|
|
8090
8157
|
return `${ts}-${randomBytes(2).toString("hex")}`;
|
|
@@ -8098,6 +8165,8 @@ async function runOne(opts) {
|
|
|
8098
8165
|
const r = {
|
|
8099
8166
|
test: test.name,
|
|
8100
8167
|
n,
|
|
8168
|
+
model: opts.model,
|
|
8169
|
+
claudeModel: null,
|
|
8101
8170
|
status: "error",
|
|
8102
8171
|
reason: "",
|
|
8103
8172
|
prompt: render(test.prompt, opts.vars),
|
|
@@ -8148,14 +8217,16 @@ async function runOne(opts) {
|
|
|
8148
8217
|
env: { ...claudeEnv(opts.configDir, env), PATH: opts.stepEnv.PATH },
|
|
8149
8218
|
stderr,
|
|
8150
8219
|
timeoutMs: test.timeout * 1000,
|
|
8220
|
+
model: opts.model ?? undefined,
|
|
8151
8221
|
onEvent: opts.onEvent
|
|
8152
8222
|
});
|
|
8223
|
+
r.claudeModel = outcome.claudeModel;
|
|
8153
8224
|
await new Promise((res) => stderr.end(res));
|
|
8154
8225
|
if (outcome.sessionId)
|
|
8155
8226
|
r.session = findSession(opts.configDir, outcome.sessionId);
|
|
8156
8227
|
writeTranscript({
|
|
8157
8228
|
path: r.transcript,
|
|
8158
|
-
title: `${test.name}#${n}`,
|
|
8229
|
+
title: `${test.name}#${n}${opts.model ? ` (model ${opts.model})` : ""}`,
|
|
8159
8230
|
prompt: r.prompt,
|
|
8160
8231
|
events: outcome.events,
|
|
8161
8232
|
workdir
|
|
@@ -8227,7 +8298,10 @@ async function evalCommand(args, root) {
|
|
|
8227
8298
|
return fatal(EXIT.usage, `unknown test ${unknown.map((n) => JSON.stringify(n)).join(", ")} (known: ${[...known].join(", ")})`);
|
|
8228
8299
|
tests = tests.filter((t) => opts.names.includes(t.name));
|
|
8229
8300
|
}
|
|
8301
|
+
if (tests.some((t) => t.checks.some((c) => grepsSessionJson(c.run))))
|
|
8302
|
+
console.error('firegoat eval: warning: a check greps $FIREGOAT_SESSION for session JSON ("command":" / "name":"Write"), which breaks on escaping. Grep $FIREGOAT_TRANSCRIPT instead: one "[n] $ <command>" line per shell call, "[n] Write <path>" per file write.');
|
|
8230
8303
|
const runs = opts.runs ?? config.runs;
|
|
8304
|
+
const models = opts.models.length > 0 ? opts.models : config.models;
|
|
8231
8305
|
const vars = { ...config.vars, ...opts.vars };
|
|
8232
8306
|
const missing = [
|
|
8233
8307
|
...new Set(tests.flatMap((t) => [
|
|
@@ -8270,12 +8344,14 @@ async function evalCommand(args, root) {
|
|
|
8270
8344
|
FIREGOAT_OUT: out,
|
|
8271
8345
|
PATH: [out, process.env.PATH ?? ""].join(delimiter2)
|
|
8272
8346
|
};
|
|
8273
|
-
const
|
|
8347
|
+
const matrix = models.length > 0 ? models : [null];
|
|
8348
|
+
const rows = tests.flatMap((t) => matrix.flatMap((model, m) => Array.from({ length: runs }, (_, i) => ({
|
|
8274
8349
|
test: t.name,
|
|
8275
|
-
n: i + 1,
|
|
8350
|
+
n: m * runs + i + 1,
|
|
8351
|
+
model,
|
|
8276
8352
|
state: "queued",
|
|
8277
8353
|
turns: 0
|
|
8278
|
-
})));
|
|
8354
|
+
}))));
|
|
8279
8355
|
if (config.setup.length > 0) {
|
|
8280
8356
|
const setupStarted = Date.now();
|
|
8281
8357
|
ui.setup("running");
|
|
@@ -8318,6 +8394,7 @@ async function evalCommand(args, root) {
|
|
|
8318
8394
|
},
|
|
8319
8395
|
test,
|
|
8320
8396
|
n: row.n,
|
|
8397
|
+
model: row.model,
|
|
8321
8398
|
runId,
|
|
8322
8399
|
runDir,
|
|
8323
8400
|
claudeBin: claudePath,
|
|
@@ -8385,7 +8462,68 @@ async function evalCommand(args, root) {
|
|
|
8385
8462
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
8386
8463
|
|
|
8387
8464
|
// node_modules/@firegoat/feedback/dist/index.js
|
|
8388
|
-
|
|
8465
|
+
function memoryDeviceStore() {
|
|
8466
|
+
const tokens = {};
|
|
8467
|
+
return {
|
|
8468
|
+
load: async (baseUrl) => tokens[baseUrl],
|
|
8469
|
+
save: async (baseUrl, token) => {
|
|
8470
|
+
tokens[baseUrl] = token;
|
|
8471
|
+
},
|
|
8472
|
+
clear: async (baseUrl) => {
|
|
8473
|
+
delete tokens[baseUrl];
|
|
8474
|
+
}
|
|
8475
|
+
};
|
|
8476
|
+
}
|
|
8477
|
+
async function defaultDevicePath() {
|
|
8478
|
+
const { homedir } = await import("node:os");
|
|
8479
|
+
const { join } = await import("node:path");
|
|
8480
|
+
const configHome = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
8481
|
+
return join(configHome, "firegoat", "feedback-device.json");
|
|
8482
|
+
}
|
|
8483
|
+
function fileDeviceStore(path) {
|
|
8484
|
+
const resolvePath = async () => path ?? await defaultDevicePath();
|
|
8485
|
+
async function read() {
|
|
8486
|
+
const { readFile } = await import("node:fs/promises");
|
|
8487
|
+
const file = await resolvePath();
|
|
8488
|
+
let text;
|
|
8489
|
+
try {
|
|
8490
|
+
text = await readFile(file, "utf8");
|
|
8491
|
+
} catch (err) {
|
|
8492
|
+
if (err.code === "ENOENT")
|
|
8493
|
+
return {};
|
|
8494
|
+
throw err;
|
|
8495
|
+
}
|
|
8496
|
+
try {
|
|
8497
|
+
return JSON.parse(text);
|
|
8498
|
+
} catch {
|
|
8499
|
+
throw new Error(`@firegoat/feedback: ${file} is not valid JSON; delete it to register a new device`);
|
|
8500
|
+
}
|
|
8501
|
+
}
|
|
8502
|
+
async function write(tokens) {
|
|
8503
|
+
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
8504
|
+
const { dirname } = await import("node:path");
|
|
8505
|
+
const file = await resolvePath();
|
|
8506
|
+
await mkdir(dirname(file), { recursive: true });
|
|
8507
|
+
await writeFile(file, `${JSON.stringify(tokens, null, 2)}
|
|
8508
|
+
`, {
|
|
8509
|
+
mode: 384
|
|
8510
|
+
});
|
|
8511
|
+
}
|
|
8512
|
+
return {
|
|
8513
|
+
load: async (baseUrl) => (await read())[baseUrl],
|
|
8514
|
+
save: async (baseUrl, token) => write({ ...await read(), [baseUrl]: token }),
|
|
8515
|
+
clear: async (baseUrl) => {
|
|
8516
|
+
const tokens = await read();
|
|
8517
|
+
delete tokens[baseUrl];
|
|
8518
|
+
await write(tokens);
|
|
8519
|
+
}
|
|
8520
|
+
};
|
|
8521
|
+
}
|
|
8522
|
+
function defaultDeviceStore() {
|
|
8523
|
+
const isNode = Boolean(globalThis.process?.versions?.node);
|
|
8524
|
+
return isNode ? fileDeviceStore() : memoryDeviceStore();
|
|
8525
|
+
}
|
|
8526
|
+
var VERSION = "0.2.0";
|
|
8389
8527
|
var DEFAULT_BASE_URL = "https://firegoat.ai/evals";
|
|
8390
8528
|
var TIMEOUT_MS = 15000;
|
|
8391
8529
|
function envBaseUrl() {
|
|
@@ -8414,6 +8552,7 @@ class FeedbackClient {
|
|
|
8414
8552
|
baseUrl;
|
|
8415
8553
|
token;
|
|
8416
8554
|
fetchImpl;
|
|
8555
|
+
device;
|
|
8417
8556
|
constructor(options) {
|
|
8418
8557
|
if (!options.domain?.trim()) {
|
|
8419
8558
|
throw new Error("FeedbackClient: `domain` is required (your product's domain, e.g. example.com)");
|
|
@@ -8422,6 +8561,7 @@ class FeedbackClient {
|
|
|
8422
8561
|
this.baseUrl = (options.baseUrl ?? envBaseUrl() ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
8423
8562
|
this.token = options.token;
|
|
8424
8563
|
this.fetchImpl = options.fetch ?? fetch;
|
|
8564
|
+
this.device = options.token || options.device === false ? undefined : options.device ?? defaultDeviceStore();
|
|
8425
8565
|
}
|
|
8426
8566
|
async registerDevice() {
|
|
8427
8567
|
const data = await this.post("/api/feedback/device", undefined);
|
|
@@ -8429,6 +8569,7 @@ class FeedbackClient {
|
|
|
8429
8569
|
throw new FeedbackError(201, "invalid_response", "server response missing token");
|
|
8430
8570
|
}
|
|
8431
8571
|
this.token = data.token;
|
|
8572
|
+
await this.device?.save(this.baseUrl, data.token);
|
|
8432
8573
|
return data.token;
|
|
8433
8574
|
}
|
|
8434
8575
|
async send(input) {
|
|
@@ -8441,18 +8582,43 @@ class FeedbackClient {
|
|
|
8441
8582
|
cliVersion: `@firegoat/feedback@${VERSION}`,
|
|
8442
8583
|
feedbackType: input.type
|
|
8443
8584
|
};
|
|
8444
|
-
const
|
|
8585
|
+
const payload = {
|
|
8445
8586
|
domain: this.domain,
|
|
8446
8587
|
title,
|
|
8447
8588
|
body: input.body ?? "",
|
|
8448
8589
|
meta,
|
|
8449
8590
|
maskSecrets: input.maskSecrets ?? false
|
|
8450
|
-
}
|
|
8591
|
+
};
|
|
8592
|
+
await this.resolveToken();
|
|
8593
|
+
let data;
|
|
8594
|
+
try {
|
|
8595
|
+
data = await this.post("/api/feedback", payload);
|
|
8596
|
+
} catch (err) {
|
|
8597
|
+
if (!(this.device && isInvalidToken(err)))
|
|
8598
|
+
throw err;
|
|
8599
|
+
await this.device.clear(this.baseUrl);
|
|
8600
|
+
this.token = undefined;
|
|
8601
|
+
await this.registerDevice();
|
|
8602
|
+
data = await this.post("/api/feedback", payload);
|
|
8603
|
+
}
|
|
8451
8604
|
if (typeof data.id !== "string" || !data.id) {
|
|
8452
8605
|
throw new FeedbackError(201, "invalid_response", "server response missing id");
|
|
8453
8606
|
}
|
|
8454
8607
|
return { id: data.id, similarCount: data.similarCount ?? 0 };
|
|
8455
8608
|
}
|
|
8609
|
+
async resolveToken() {
|
|
8610
|
+
if (this.token || !this.device)
|
|
8611
|
+
return;
|
|
8612
|
+
this.token = await this.device.load(this.baseUrl);
|
|
8613
|
+
if (this.token)
|
|
8614
|
+
return;
|
|
8615
|
+
try {
|
|
8616
|
+
await this.registerDevice();
|
|
8617
|
+
} catch (err) {
|
|
8618
|
+
if (!(err instanceof FeedbackError && err.code === "rate_limited"))
|
|
8619
|
+
throw err;
|
|
8620
|
+
}
|
|
8621
|
+
}
|
|
8456
8622
|
async post(path, body) {
|
|
8457
8623
|
const headers = {
|
|
8458
8624
|
"content-type": "application/json"
|
|
@@ -8478,6 +8644,9 @@ class FeedbackClient {
|
|
|
8478
8644
|
return data;
|
|
8479
8645
|
}
|
|
8480
8646
|
}
|
|
8647
|
+
function isInvalidToken(err) {
|
|
8648
|
+
return err instanceof FeedbackError && err.code === "invalid_token";
|
|
8649
|
+
}
|
|
8481
8650
|
|
|
8482
8651
|
// src/feedback.ts
|
|
8483
8652
|
var TYPES = ["bug", "feature_request", "feedback"];
|
|
@@ -8567,8 +8736,6 @@ var SKILLS_ARGV = [
|
|
|
8567
8736
|
"firegoat-ai/skills",
|
|
8568
8737
|
"--skill",
|
|
8569
8738
|
"firegoat-onboarding",
|
|
8570
|
-
"--skill",
|
|
8571
|
-
"suggesting-eval-prompts",
|
|
8572
8739
|
"--agent",
|
|
8573
8740
|
"claude-code",
|
|
8574
8741
|
"--copy",
|
|
@@ -8582,6 +8749,10 @@ function template(product) {
|
|
|
8582
8749
|
# How many times to run each test.
|
|
8583
8750
|
runs: 1
|
|
8584
8751
|
|
|
8752
|
+
# Optional: claude's --model. A list runs every test \`runs\` times per model.
|
|
8753
|
+
# Unset: claude's default. eval --model NAME overrides it.
|
|
8754
|
+
# model: [claude-haiku-5-5, claude-sonnet-5-5]
|
|
8755
|
+
|
|
8585
8756
|
# Optional: test your local build instead of the published package.
|
|
8586
8757
|
# setup: runs once per eval, here; what it writes to $FIREGOAT_OUT is put on the
|
|
8587
8758
|
# agent's PATH. workdir_setup: runs in each fresh folder before the agent starts.
|
|
@@ -8607,8 +8778,10 @@ runs: 1
|
|
|
8607
8778
|
# run). The agent gets none of vars:, only these. Placeholders: {{vars.KEY}},
|
|
8608
8779
|
# {{run.id}}, {{run.n}}, {{test.name}}, and $FIREGOAT_OUT / $FIREGOAT_WORKDIR (no
|
|
8609
8780
|
# other $ expansion: $HOME stays literal). A test can add its own env:.
|
|
8781
|
+
# Runs go 4 at a time (--parallel), so give a tool with a global home (~/.mytool)
|
|
8782
|
+
# a per-run one, as below, or use --parallel 1.
|
|
8610
8783
|
# env:
|
|
8611
|
-
# MYTOOL_HOME: "/tmp/mytool-{{run.id}}-{{run.n}}"
|
|
8784
|
+
# MYTOOL_HOME: "/tmp/mytool-{{run.id}}-{{test.name}}-{{run.n}}"
|
|
8612
8785
|
# UV_FIND_LINKS: "$FIREGOAT_OUT"
|
|
8613
8786
|
|
|
8614
8787
|
tests:
|
|
@@ -8710,18 +8883,18 @@ ${INIT_HELP}`);
|
|
|
8710
8883
|
});
|
|
8711
8884
|
if (r.error || r.status !== 0) {
|
|
8712
8885
|
process.stderr.write(`${r.stdout ?? ""}${r.stderr ?? ""}`);
|
|
8713
|
-
console.error(`firegoat init: installing the
|
|
8886
|
+
console.error(`firegoat init: installing the skill failed (${r.error?.message ?? `exit ${r.status}`}). Run it yourself: npx ${SKILLS_ARGV.join(" ")}`);
|
|
8714
8887
|
return EXIT.notGraded;
|
|
8715
8888
|
}
|
|
8716
8889
|
if (interactive)
|
|
8717
8890
|
step(3, "firegoat-onboarding skill", "installed → .claude/skills/");
|
|
8718
8891
|
else
|
|
8719
|
-
console.log("installed
|
|
8892
|
+
console.log("installed skill firegoat-onboarding in .claude/skills/");
|
|
8720
8893
|
const ignored = spawnSync3("git", ["check-ignore", "-q", ".claude/skills"], {
|
|
8721
8894
|
cwd
|
|
8722
8895
|
});
|
|
8723
8896
|
if (ignored.status === 0)
|
|
8724
|
-
console.error("warning: git ignores .claude/skills, so teammates and CI won't get the
|
|
8897
|
+
console.error("warning: git ignores .claude/skills, so teammates and CI won't get the skill. Commit them with: git add -f .claude/skills");
|
|
8725
8898
|
} else if (interactive)
|
|
8726
8899
|
step(3, "firegoat-onboarding skill", "skipped");
|
|
8727
8900
|
if (!interactive) {
|
|
@@ -8770,6 +8943,11 @@ main(process.argv.slice(2)).then((code) => process.exit(code), (err) => {
|
|
|
8770
8943
|
console.error(err.message);
|
|
8771
8944
|
process.exit(EXIT.usage);
|
|
8772
8945
|
}
|
|
8946
|
+
if (err?.code === "ABORT_ERR") {
|
|
8947
|
+
console.error(`
|
|
8948
|
+
aborted`);
|
|
8949
|
+
process.exit(130);
|
|
8950
|
+
}
|
|
8773
8951
|
console.error(err);
|
|
8774
8952
|
process.exit(EXIT.notGraded);
|
|
8775
8953
|
});
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "firegoat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
7
7
|
},
|
|
8
8
|
"devDependencies": {
|
|
9
9
|
"@biomejs/biome": "^2.5.15",
|
|
10
|
-
"@firegoat/feedback": "0.
|
|
10
|
+
"@firegoat/feedback": "0.2.0",
|
|
11
11
|
"@types/bun": "latest",
|
|
12
12
|
"typescript": "^7",
|
|
13
13
|
"yaml": "^2.9.1"
|