firegoat 0.16.0 → 0.17.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 +33 -29
- package/dist/cli.js +234 -172
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,19 +7,21 @@ Status: early. See `plan.md`.
|
|
|
7
7
|
## Quickstart
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npx firegoat init #
|
|
11
|
-
#
|
|
12
|
-
npx firegoat eval #
|
|
10
|
+
npx firegoat init # Creates firegoat.yaml and .firegoat/, installs the firegoat-onboarding skill, copies a prompt for your agent
|
|
11
|
+
# Edit firegoat.yaml (or paste the prompt into your coding agent and let it)
|
|
12
|
+
npx firegoat eval # Runs your claude on each test, in .firegoat/runs/<runId>/<test>/<n>/work
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
That's it: two commands. `claude` must be installed and logged in (run `claude` once).
|
|
16
16
|
|
|
17
|
+
`init` installs the firegoat-onboarding skill globally, into `~/.claude/skills/` (in a terminal it asks: global, this repo, or none). `--local-skills` puts it in this repo's `.claude/skills/` instead (commit it so teammates and CI get it); `--no-skills` skips it.
|
|
18
|
+
|
|
17
19
|
## Upload to firegoat.ai
|
|
18
20
|
|
|
19
21
|
```bash
|
|
20
|
-
npx firegoat login #
|
|
21
|
-
npx firegoat eval #
|
|
22
|
-
npx firegoat logout #
|
|
22
|
+
npx firegoat login # Device flow: open the URL, enter the code; key saved to ~/.firegoat/credentials.json (0600)
|
|
23
|
+
npx firegoat eval # Now also uploads each run as a private trace and prints its URL
|
|
24
|
+
npx firegoat logout # Forgets the login (revoke the key itself on the firegoat.ai traces page)
|
|
23
25
|
```
|
|
24
26
|
|
|
25
27
|
Logging in means agreeing to the firegoat.ai [Terms of Service](https://firegoat.ai/terms) and [Privacy Policy](https://firegoat.ai/privacy); login says so before showing the code. `init` offers the login in a terminal (`--no-login` skips it). In CI, set `FIREGOAT_API_KEY=fire_...` instead. `eval --no-upload` keeps runs local; without a key, runs stay local and eval says so in one line.
|
|
@@ -27,7 +29,7 @@ Logging in means agreeing to the firegoat.ai [Terms of Service](https://firegoat
|
|
|
27
29
|
- Per run, eval uploads claude's session (`session.jsonl`: prompts, tool calls and outputs, file contents, so possibly secrets from your env or repo), `checks.txt` and `stderr.txt` (check output sees `vars:`), plus the run's test, status, checks, the model asked for and the one claude reported, and (in a git repo) the branch, commit and whether the tree was dirty. firegoat.ai masks secrets when it shows a trace, but stores what was sent. Traces start private; you share one from its page.
|
|
28
30
|
- Before any run, eval checks the key (`GET /api/traces/me`) and prints `Uploading sessions to <origin> as <email>`: a key answered with 401/403 exits 3; any other failure (unreachable, 429, 5xx, a wrong `FIREGOAT_BASE_URL` path) exits 2.
|
|
29
31
|
- Each run prints `✓ <test>#<n> <url>` or `✗ <test>#<n> Upload failed: <why>`, and `run.json` / `summary.json` get `upload` (`{status: "uploaded", url}`, `{status: "failed", error}`, `{status: "skipped", reason}`, or null when not uploading). A failed upload never changes a run's status or eval's exit code (0/1/2 stay the eval's verdict); eval ends with `N uploads failed` on stderr. Each file may be at most 20 MiB: a bigger one fails that run's upload. Runs without a session file are skipped.
|
|
30
|
-
- The key is never passed to the agent,
|
|
32
|
+
- The key is never passed to the agent, build, install or checks (every `FIREGOAT_*` var is removed from their env), is never written to run files, and goes only to the base URL's origin, never through a redirect; presigned upload URLs get no auth header. But there is no sandbox: the agent runs as you, so it can read `~/.firegoat/credentials.json`, and with `FIREGOAT_API_KEY` it can read the key from firegoat's own process environment (`ps eww $PPID`, `/proc/<pid>/environ`), which no program can scrub. In CI, use a dedicated key and revoke it if a run looks off. The key can only upload and read your own traces.
|
|
31
33
|
- `FIREGOAT_BASE_URL` (default `https://firegoat.ai/evals`) points firegoat elsewhere: https only, http only for `localhost` / `127.0.0.1`. A saved login only works for the base URL it was made for.
|
|
32
34
|
|
|
33
35
|
## Examples
|
|
@@ -35,8 +37,8 @@ Logging in means agreeing to the firegoat.ai [Terms of Service](https://firegoat
|
|
|
35
37
|
### Can an agent get started with my npm package?
|
|
36
38
|
|
|
37
39
|
```yaml
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
build: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
41
|
+
install: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
|
|
40
42
|
tests:
|
|
41
43
|
- name: quickstart
|
|
42
44
|
prompt: >-
|
|
@@ -47,12 +49,12 @@ tests:
|
|
|
47
49
|
- run: grep -qE 'evt_[A-Za-z0-9]+' "$FIREGOAT_TRANSCRIPT"
|
|
48
50
|
```
|
|
49
51
|
|
|
50
|
-
`
|
|
52
|
+
`build` builds your package once; `install` installs that build into each fresh folder, so the agent uses your local code, not the registry.
|
|
51
53
|
|
|
52
54
|
### Does my CLI make sense to an agent?
|
|
53
55
|
|
|
54
56
|
```yaml
|
|
55
|
-
|
|
57
|
+
build: go build -o "$FIREGOAT_OUT/acme" ./cmd/acme
|
|
56
58
|
tests:
|
|
57
59
|
- name: deploy
|
|
58
60
|
prompt: Deploy the hello-world template to a preview environment with the acme CLI and print the URL.
|
|
@@ -61,7 +63,7 @@ tests:
|
|
|
61
63
|
- run: grep -q '\$ acme deploy' "$FIREGOAT_TRANSCRIPT"
|
|
62
64
|
```
|
|
63
65
|
|
|
64
|
-
Whatever `
|
|
66
|
+
Whatever `build` writes to `$FIREGOAT_OUT` is first on the agent's `PATH`. That only helps for binaries called by bare name: if users run your CLI via `npx`/`uvx`, pack and install it like the npm example, or `npx` fetches the registry release.
|
|
65
67
|
|
|
66
68
|
### Can an agent find the right package from my docs alone?
|
|
67
69
|
|
|
@@ -80,8 +82,8 @@ tests:
|
|
|
80
82
|
|
|
81
83
|
```yaml
|
|
82
84
|
runs: 3
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
build: uv build --wheel --out-dir "$FIREGOAT_OUT"
|
|
86
|
+
install: uv venv && uv pip install "$FIREGOAT_OUT"/*.whl
|
|
85
87
|
tests:
|
|
86
88
|
- name: sandbox-hello
|
|
87
89
|
prompt: >-
|
|
@@ -115,41 +117,43 @@ tests:
|
|
|
115
117
|
|
|
116
118
|
```yaml
|
|
117
119
|
env:
|
|
118
|
-
ACME_HOME: "/tmp/acme-{{run.id}}-{{run.n}}" #
|
|
120
|
+
ACME_HOME: "/tmp/acme-{{run.id}}-{{run.n}}" # Sessions/config never collide across runs
|
|
119
121
|
ACME_TELEMETRY: "off"
|
|
120
|
-
UV_FIND_LINKS: "$FIREGOAT_OUT" #
|
|
122
|
+
UV_FIND_LINKS: "$FIREGOAT_OUT" # Resolve the wheel build: made
|
|
121
123
|
tests:
|
|
122
124
|
- name: login
|
|
123
125
|
prompt: Log in to acme with the CLI and print the current user.
|
|
124
126
|
end_goal: The agent completes acme's login flow and prints the user.
|
|
125
127
|
env:
|
|
126
|
-
ACME_BETA_LOGIN: "1" #
|
|
128
|
+
ACME_BETA_LOGIN: "1" # This test only, on top of env:
|
|
127
129
|
checks:
|
|
128
130
|
- run: ls "$ACME_HOME"/sessions | grep -q .
|
|
129
131
|
```
|
|
130
132
|
|
|
131
|
-
`env:` goes to the agent, and to `
|
|
133
|
+
`env:` goes to the agent, and to `install` 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 `build`, `install` and checks see. There is no other `$` expansion: `$HOME` stays the literal text `$HOME`. `build` 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:`.
|
|
134
|
+
|
|
135
|
+
Setting `HOME`: on macOS claude reads its login from `$HOME/Library`, so with a per-run `HOME` every run says "Not logged in" unless `install` runs `ln -s <real home>/Library "$HOME/Library"`. Get the real home in `build`, which doesn't get entries using `{{test.name}}` or `{{run.n}}` (e.g. `build: echo "$HOME" > "$FIREGOAT_OUT/.real-home"`).
|
|
132
136
|
|
|
133
137
|
## What eval does
|
|
134
138
|
|
|
135
139
|
- 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.
|
|
136
|
-
- No sandbox: the agent's folder is inside `.firegoat/runs/`, and it can reach the rest of your machine.
|
|
137
|
-
- 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 `
|
|
138
|
-
- 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,
|
|
140
|
+
- No sandbox: the agent's folder is inside `.firegoat/runs/`, and it can reach the rest of your machine. Commit `.firegoat/` with `firegoat.yaml`: its `.gitignore` ignores only `runs/` and `.claude/`, test fixtures (scripts, sample data) go in `.firegoat/fixtures/`, and its `README.md` says what each folder is.
|
|
141
|
+
- 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 `build:` 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, uploaded, uploadFailed`) to tell runs from different branches apart.
|
|
142
|
+
- 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, build, install, 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. Every `FIREGOAT_*` var firegoat was started with (`FIREGOAT_API_KEY` above all) is removed too.
|
|
139
143
|
- 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.
|
|
140
144
|
- 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.
|
|
141
|
-
- 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.
|
|
145
|
+
- 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`; greppable line by line: every line of a shell call starts with `[n] $ `, every line of the agent's text and final answer with `agent: `, output is indented, so `grep -E '^\[[0-9]+\] \$ .*npm install'` matches only commands, also inside heredocs and multi-line scripts), `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.
|
|
142
146
|
|
|
143
|
-
`firegoat.yaml` keys: `runs`, `model`, `
|
|
147
|
+
`firegoat.yaml` keys: `runs`, `model`, `build`, `install`, `vars`, `env`, and `tests` (`name`, `prompt`, `end_goal`, `checks`, `timeout`, `env`). Unknown keys are errors. Full reference: `npx firegoat init --help`.
|
|
144
148
|
|
|
145
149
|
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).
|
|
146
150
|
|
|
147
151
|
| exit code | meaning |
|
|
148
152
|
|---|---|
|
|
149
|
-
| 0 |
|
|
150
|
-
| 1 |
|
|
151
|
-
| 2 |
|
|
152
|
-
| 3 |
|
|
153
|
+
| 0 | Every run's checks passed |
|
|
154
|
+
| 1 | A check failed |
|
|
155
|
+
| 2 | At least one run was not graded (claude missing or logged out, crash, timeout, build failed); wins over 1 |
|
|
156
|
+
| 3 | Usage or config error (including firegoat.ai rejecting the upload key) |
|
|
153
157
|
|
|
154
158
|
Upload failures don't change the exit code.
|
|
155
159
|
|
|
@@ -168,6 +172,6 @@ It goes to the firegoat team with the firegoat version, your OS and node version
|
|
|
168
172
|
```bash
|
|
169
173
|
bun install
|
|
170
174
|
bun run format
|
|
171
|
-
bun test #
|
|
172
|
-
FIREGOAT_E2E_REAL=1 bun test #
|
|
175
|
+
bun test # End-to-end: packs the package and drives the installed bin
|
|
176
|
+
FIREGOAT_E2E_REAL=1 bun test # Also runs one eval against your real claude
|
|
173
177
|
```
|
package/dist/cli.js
CHANGED
|
@@ -6947,7 +6947,7 @@ 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.17.1",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -7076,16 +7076,16 @@ function parseConfig(text) {
|
|
|
7076
7076
|
onlyKeys(raw, "(root)", [
|
|
7077
7077
|
"runs",
|
|
7078
7078
|
"model",
|
|
7079
|
-
"
|
|
7080
|
-
"
|
|
7079
|
+
"build",
|
|
7080
|
+
"install",
|
|
7081
7081
|
"vars",
|
|
7082
7082
|
"env",
|
|
7083
7083
|
"tests"
|
|
7084
7084
|
]);
|
|
7085
7085
|
const runs = posInt(raw.runs, "runs", 1);
|
|
7086
7086
|
const models = parseModels(raw.model);
|
|
7087
|
-
const
|
|
7088
|
-
const
|
|
7087
|
+
const build = commands(raw.build, "build");
|
|
7088
|
+
const install = commands(raw.install, "install");
|
|
7089
7089
|
const vars = parseVars(raw.vars);
|
|
7090
7090
|
const env = parseEnv(raw.env, "env");
|
|
7091
7091
|
if (!Array.isArray(raw.tests) || raw.tests.length === 0)
|
|
@@ -7125,8 +7125,8 @@ function parseConfig(text) {
|
|
|
7125
7125
|
return {
|
|
7126
7126
|
runs,
|
|
7127
7127
|
models,
|
|
7128
|
-
|
|
7129
|
-
|
|
7128
|
+
build,
|
|
7129
|
+
install,
|
|
7130
7130
|
vars,
|
|
7131
7131
|
env,
|
|
7132
7132
|
tests
|
|
@@ -7236,7 +7236,7 @@ function render(text, vars) {
|
|
|
7236
7236
|
return text.replace(PLACEHOLDER, (_, key) => {
|
|
7237
7237
|
const v = vars[key];
|
|
7238
7238
|
if (v === undefined)
|
|
7239
|
-
throw new ConfigError(`
|
|
7239
|
+
throw new ConfigError(`No value for {{vars.${key}}}`);
|
|
7240
7240
|
return v;
|
|
7241
7241
|
});
|
|
7242
7242
|
}
|
|
@@ -7252,7 +7252,7 @@ import { randomBytes as randomBytes2 } from "node:crypto";
|
|
|
7252
7252
|
import {
|
|
7253
7253
|
appendFileSync,
|
|
7254
7254
|
createWriteStream as createWriteStream2,
|
|
7255
|
-
existsSync as
|
|
7255
|
+
existsSync as existsSync4,
|
|
7256
7256
|
mkdirSync as mkdirSync3,
|
|
7257
7257
|
readdirSync as readdirSync2,
|
|
7258
7258
|
writeFileSync as writeFileSync3
|
|
@@ -7732,16 +7732,28 @@ function resolveKey(base) {
|
|
|
7732
7732
|
var EXIT = { pass: 0, fail: 1, notGraded: 2, usage: 3 };
|
|
7733
7733
|
|
|
7734
7734
|
// src/firegoat-dir.ts
|
|
7735
|
-
import { mkdirSync as mkdirSync2, writeFileSync } from "node:fs";
|
|
7735
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync } from "node:fs";
|
|
7736
7736
|
import { join as join2 } from "node:path";
|
|
7737
|
-
var GITIGNORE =
|
|
7738
|
-
|
|
7739
|
-
|
|
7737
|
+
var GITIGNORE = `runs/
|
|
7738
|
+
.claude/
|
|
7739
|
+
`;
|
|
7740
|
+
var README = `# .firegoat
|
|
7741
|
+
|
|
7742
|
+
Files for the evals in firegoat.yaml (run them with \`npx firegoat eval\`).
|
|
7743
|
+
|
|
7744
|
+
- \`fixtures/\`: test fixtures (helper scripts, sample data) for build:, install: and checks. Commit them.
|
|
7745
|
+
- \`runs/\`: one folder per eval (transcripts, sessions, check output). Ignored by git.
|
|
7746
|
+
- \`.claude/\`: the empty claude config eval runs the agent with. Ignored by git.
|
|
7747
|
+
|
|
7748
|
+
Reference: \`npx firegoat init --help\`.
|
|
7740
7749
|
`;
|
|
7741
7750
|
function ensureFiregoatDir(root) {
|
|
7742
7751
|
const dir = join2(root, ".firegoat");
|
|
7743
|
-
mkdirSync2(dir, { recursive: true });
|
|
7752
|
+
mkdirSync2(join2(dir, "fixtures"), { recursive: true });
|
|
7744
7753
|
writeFileSync(join2(dir, ".gitignore"), GITIGNORE);
|
|
7754
|
+
const readme = join2(dir, "README.md");
|
|
7755
|
+
if (!existsSync2(readme))
|
|
7756
|
+
writeFileSync(readme, README);
|
|
7745
7757
|
return dir;
|
|
7746
7758
|
}
|
|
7747
7759
|
|
|
@@ -7803,11 +7815,11 @@ function gitRepo(dir) {
|
|
|
7803
7815
|
|
|
7804
7816
|
// src/help.ts
|
|
7805
7817
|
var EXIT_CODES = `Exit codes:
|
|
7806
|
-
0
|
|
7807
|
-
1
|
|
7808
|
-
2
|
|
7809
|
-
|
|
7810
|
-
3
|
|
7818
|
+
0 Every run's script checks passed
|
|
7819
|
+
1 A script check failed
|
|
7820
|
+
2 At least one run was not graded (claude missing or logged out, crash, timeout,
|
|
7821
|
+
build failed); wins over 1
|
|
7822
|
+
3 Usage or config error (bad flag, unknown test, invalid or missing firegoat.yaml,
|
|
7811
7823
|
firegoat.ai answered 401/403 for the upload key); any other failure of
|
|
7812
7824
|
that key check exits 2
|
|
7813
7825
|
Upload failures don't change the exit code.`;
|
|
@@ -7816,74 +7828,85 @@ function topHelp(version) {
|
|
|
7816
7828
|
folder, with none of your claude config; firegoat saves the session and runs your
|
|
7817
7829
|
script checks.
|
|
7818
7830
|
|
|
7819
|
-
npx firegoat init
|
|
7820
|
-
npx firegoat eval
|
|
7831
|
+
npx firegoat init Create firegoat.yaml and .firegoat/, install the skill
|
|
7832
|
+
npx firegoat eval Run the tests (claude must be logged in: run \`claude\` once)
|
|
7821
7833
|
npx firegoat login Log in to firegoat.ai: eval then uploads each session there
|
|
7822
7834
|
npx firegoat logout Forget that login
|
|
7823
|
-
npx firegoat feedback "<what got in the way>"
|
|
7835
|
+
npx firegoat feedback "<what got in the way>" Tell the firegoat team
|
|
7824
7836
|
|
|
7825
7837
|
Typical flow for a coding agent driving firegoat:
|
|
7826
|
-
1.
|
|
7838
|
+
1. Read npx firegoat init --help, then edit firegoat.yaml
|
|
7827
7839
|
2. npx firegoat eval --json
|
|
7828
|
-
3.
|
|
7840
|
+
3. Judge each run's end_goal from its run.json, session and workdir: firegoat only
|
|
7829
7841
|
runs the script checks
|
|
7830
7842
|
|
|
7831
7843
|
Run firegoat <command> --help for details and examples.
|
|
7832
7844
|
|
|
7833
7845
|
${EXIT_CODES}`;
|
|
7834
7846
|
}
|
|
7835
|
-
var INIT_HELP = `Usage: firegoat init [--force] [--no-skills] [--no-login]
|
|
7847
|
+
var INIT_HELP = `Usage: firegoat init [--force] [--local-skills | --no-skills] [--no-login]
|
|
7836
7848
|
|
|
7837
7849
|
In the current folder: creates .firegoat/ and a starter firegoat.yaml, installs the
|
|
7838
|
-
firegoat-onboarding skill into
|
|
7839
|
-
|
|
7840
|
-
terminal only; otherwise prints how), then prints a prompt to
|
|
7841
|
-
your coding agent.
|
|
7842
|
-
(helper scripts, sample data)
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
--
|
|
7846
|
-
--
|
|
7850
|
+
firegoat-onboarding skill globally, into ~/.claude/skills/ (npx skills add
|
|
7851
|
+
firegoat-ai/skills -g; in a terminal it asks: global, this repo, or none), offers
|
|
7852
|
+
firegoat login (in a terminal only; otherwise prints how), then prints a prompt to
|
|
7853
|
+
paste into your coding agent. Commit .firegoat/ with firegoat.yaml: its .gitignore
|
|
7854
|
+
ignores only runs/ and .claude/, and test fixtures (helper scripts, sample data)
|
|
7855
|
+
go in .firegoat/fixtures/. Its README.md says what each folder is.
|
|
7856
|
+
|
|
7857
|
+
--force Replace an existing firegoat.yaml
|
|
7858
|
+
--local-skills Install the skill into this repo's .claude/skills/ instead
|
|
7859
|
+
(commit it so teammates and CI get it), without asking
|
|
7860
|
+
--no-skills Don't install the skill
|
|
7861
|
+
--no-login Don't offer firegoat login
|
|
7847
7862
|
|
|
7848
7863
|
firegoat.yaml reference:
|
|
7849
|
-
runs: 1
|
|
7864
|
+
runs: 1 Runs per test
|
|
7850
7865
|
model: <name> | [<name>, ...]
|
|
7851
7866
|
claude --model; a list runs every test runs times
|
|
7852
7867
|
per model (n counts 1..runs x models). Unset: no
|
|
7853
7868
|
--model, claude's default. eval --model overrides
|
|
7854
|
-
|
|
7869
|
+
build: <sh> | [<sh>, ...] Once per eval, in this folder; what it writes to
|
|
7855
7870
|
$FIREGOAT_OUT is first on the agent's PATH
|
|
7856
|
-
|
|
7857
|
-
vars: {KEY: value}
|
|
7871
|
+
install: <sh> | [<sh>, ...] In each fresh folder, before the agent starts
|
|
7872
|
+
vars: {KEY: value} Defaults for {{vars.KEY}} in prompts and checks;
|
|
7858
7873
|
--var KEY=value overrides
|
|
7859
|
-
env: {NAME: value}
|
|
7874
|
+
env: {NAME: value} Env vars for the agent, and for install and
|
|
7860
7875
|
checks of the same run; values are strings and can
|
|
7861
7876
|
use {{vars.KEY}}, {{run.id}}, {{run.n}},
|
|
7862
7877
|
{{test.name}}, $FIREGOAT_OUT, $FIREGOAT_WORKDIR
|
|
7863
7878
|
(no other $ expansion: $HOME stays literal).
|
|
7864
|
-
|
|
7879
|
+
The build step gets only the entries that use only
|
|
7865
7880
|
{{vars.*}}, {{run.id}} and $FIREGOAT_OUT.
|
|
7866
7881
|
PATH, CLAUDE_CONFIG_DIR, FIREGOAT_* and
|
|
7867
7882
|
ANTHROPIC_MODEL (use model:) are reserved.
|
|
7868
7883
|
Runs go 4 at a time: give a tool with a global home
|
|
7869
7884
|
a per-run one (or use eval --parallel 1), e.g.
|
|
7870
7885
|
MYTOOL_HOME: "/tmp/mytool-{{run.id}}-{{test.name}}-{{run.n}}"
|
|
7886
|
+
Setting HOME: on macOS claude reads its login from
|
|
7887
|
+
$HOME/Library, so a per-run HOME needs
|
|
7888
|
+
ln -s <real home>/Library "$HOME/Library" in
|
|
7889
|
+
install (get the real home in build, which
|
|
7890
|
+
doesn't get entries using {{test.name}}/{{run.n}}).
|
|
7871
7891
|
tests:
|
|
7872
|
-
- name: quickstart
|
|
7873
|
-
prompt: ...
|
|
7874
|
-
end_goal: ...
|
|
7892
|
+
- name: quickstart Letters, digits, '.', '_', '-'; unique
|
|
7893
|
+
prompt: ... What a user would ask the agent; one concrete outcome
|
|
7894
|
+
end_goal: ... What is true when the problem is solved; not graded by
|
|
7875
7895
|
firegoat, handed to you to judge
|
|
7876
|
-
checks:
|
|
7877
|
-
- run: ...
|
|
7878
|
-
timeout: 600
|
|
7879
|
-
env: {NAME: value}
|
|
7896
|
+
checks: Optional shell commands, run in the agent's folder
|
|
7897
|
+
- run: ... Afterwards; exit 0 = pass; they set eval's exit code
|
|
7898
|
+
timeout: 600 Optional, seconds
|
|
7899
|
+
env: {NAME: value} Optional, on top of the top-level env:
|
|
7880
7900
|
|
|
7881
7901
|
Unknown keys are errors. Keep helper files out of firegoat.yaml: put them in
|
|
7882
|
-
.firegoat/fixtures/ and copy them in
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7902
|
+
.firegoat/fixtures/ and copy them in build or install. The build and install
|
|
7903
|
+
commands and the checks get the vars as env vars, plus $FIREGOAT_OUT and
|
|
7904
|
+
$FIREGOAT_WORKDIR. Checks also get $FIREGOAT_TRANSCRIPT, a short plain-text
|
|
7905
|
+
transcript, greppable line by line: "[1] $ <command>" on every line of a shell
|
|
7906
|
+
call (one line per line of the command, same n), "[2] Write <path>" for other
|
|
7907
|
+
tools, each output indented, and "agent: " on every line of the agent's text and
|
|
7908
|
+
final answer. So '^\\[[0-9]+\\] \\$ .*foo' matches only commands. Checks also
|
|
7909
|
+
get $FIREGOAT_SESSION (claude's full session file, JSON lines).
|
|
7887
7910
|
|
|
7888
7911
|
The agent never gets vars, only env:. The vars npm/npx set for firegoat itself
|
|
7889
7912
|
(npm_config_*, npm_lifecycle_*, npm_package_*, npm_execpath, npm_node_execpath,
|
|
@@ -7892,8 +7915,8 @@ firegoat.yaml reference:
|
|
|
7892
7915
|
(FIREGOAT_API_KEY above all) firegoat itself was started with.
|
|
7893
7916
|
|
|
7894
7917
|
Example:
|
|
7895
|
-
|
|
7896
|
-
|
|
7918
|
+
build: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
7919
|
+
install: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
|
|
7897
7920
|
tests:
|
|
7898
7921
|
- name: first-call
|
|
7899
7922
|
prompt: >-
|
|
@@ -7916,7 +7939,7 @@ Logs in to firegoat.ai so eval uploads each run's claude session there as a priv
|
|
|
7916
7939
|
trace (you share it from its page). OAuth device flow: prints a URL and a code (and
|
|
7917
7940
|
opens the browser in a terminal); approve it there, and the trace key is saved to
|
|
7918
7941
|
~/.firegoat/credentials.json (mode 0600). The key is never passed to the agent,
|
|
7919
|
-
|
|
7942
|
+
build, install or checks, but the agent runs as you, without a sandbox: it can
|
|
7920
7943
|
read ~/.firegoat, and a FIREGOAT_API_KEY in firegoat's own process env (ps eww,
|
|
7921
7944
|
/proc/<pid>/environ). Use a dedicated key in CI. The key can only upload and read
|
|
7922
7945
|
your own traces; revoke it on the firegoat.ai traces page.
|
|
@@ -7947,7 +7970,7 @@ var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--model NAME].. [--pa
|
|
|
7947
7970
|
[--var KEY=value].. [--json] [--no-upload] [--claude-bin PATH]
|
|
7948
7971
|
|
|
7949
7972
|
Runs each test in firegoat.yaml (in the current folder): your claude gets the prompt
|
|
7950
|
-
in a fresh, empty folder, .firegoat/runs/<runId>/<test>/<n>/work, with
|
|
7973
|
+
in a fresh, empty folder, .firegoat/runs/<runId>/<test>/<n>/work, with build:'s
|
|
7951
7974
|
outputs on PATH. Then the test's checks run in that folder.
|
|
7952
7975
|
|
|
7953
7976
|
claude runs on its own login (or ANTHROPIC_API_KEY if set, as claude always does)
|
|
@@ -7957,20 +7980,21 @@ sandbox: the agent can reach the rest of your machine, including the repo.
|
|
|
7957
7980
|
|
|
7958
7981
|
In a linked git worktree (git worktree add), .firegoat/runs/ and .firegoat/.claude
|
|
7959
7982
|
live in the main checkout instead, so runs outlive the worktree; firegoat.yaml,
|
|
7960
|
-
fixtures and
|
|
7983
|
+
fixtures and build: stay in the current folder.
|
|
7961
7984
|
|
|
7962
|
-
test..
|
|
7963
|
-
--runs N
|
|
7985
|
+
test.. Only these tests (by name)
|
|
7986
|
+
--runs N Runs per test (overrides runs: in firegoat.yaml)
|
|
7964
7987
|
--model NAME claude --model, replacing model: in firegoat.yaml; repeat it
|
|
7965
7988
|
(--model a --model b) to run each test runs times per model
|
|
7966
|
-
--parallel N
|
|
7989
|
+
--parallel N Runs at once (default 4; 1 runs them one after another).
|
|
7967
7990
|
Tools with global on-disk state collide: give each run its
|
|
7968
7991
|
own home via env: ({{run.id}}-{{test.name}}-{{run.n}}) or use 1
|
|
7969
|
-
--var KEY=value
|
|
7970
|
-
--json
|
|
7992
|
+
--var KEY=value Fills {{vars.KEY}}; env for build, install and checks
|
|
7993
|
+
--json The summary as JSON on stdout (or {error, exitCode} if eval
|
|
7971
7994
|
stops early); progress goes to stderr
|
|
7972
7995
|
--no-upload Keep runs local even when logged in to firegoat.ai
|
|
7973
|
-
--claude-bin PATH claude
|
|
7996
|
+
--claude-bin PATH Path to claude (env FIREGOAT_CLAUDE_BIN; default: claude on
|
|
7997
|
+
PATH)
|
|
7974
7998
|
|
|
7975
7999
|
Upload: after firegoat login (or with FIREGOAT_API_KEY), each run's claude session,
|
|
7976
8000
|
checks.txt and stderr.txt go to firegoat.ai as a private trace, and its URL is
|
|
@@ -7983,13 +8007,13 @@ Runs without a session file are not uploaded. FIREGOAT_BASE_URL points elsewhere
|
|
|
7983
8007
|
(default https://firegoat.ai/evals). See firegoat login --help.
|
|
7984
8008
|
|
|
7985
8009
|
Output: .firegoat/runs/<runId>/
|
|
7986
|
-
summary.json
|
|
8010
|
+
summary.json Every run, plus totals, dir (where eval ran),
|
|
7987
8011
|
git {branch, commit, dirty} (no git outside a repo)
|
|
7988
8012
|
and upload {origin, email} (null when not uploading)
|
|
7989
|
-
../index.jsonl
|
|
8013
|
+
../index.jsonl One line per eval: runId, startedAt, dir, branch,
|
|
7990
8014
|
commit, passed, failed, notGraded, cost, uploaded,
|
|
7991
8015
|
uploadFailed
|
|
7992
|
-
<test>/<n>/run.json
|
|
8016
|
+
<test>/<n>/run.json One run: status and reason (from the checks), model
|
|
7993
8017
|
(asked for, or null) and claudeModel (what claude's
|
|
7994
8018
|
init line reported), prompt, end_goal,
|
|
7995
8019
|
checks [{run, code}], and paths: workdir,
|
|
@@ -7997,19 +8021,21 @@ Output: .firegoat/runs/<runId>/
|
|
|
7997
8021
|
checksLog; startedAt; upload ({status: "uploaded",
|
|
7998
8022
|
url} | {status: "failed", error} | {status:
|
|
7999
8023
|
"skipped", reason}, null when not uploading)
|
|
8000
|
-
<test>/<n>/transcript.txt
|
|
8001
|
-
|
|
8002
|
-
|
|
8024
|
+
<test>/<n>/transcript.txt What the agent did: each tool call with its command
|
|
8025
|
+
("[n] $ " on every line of a shell call), short output
|
|
8026
|
+
(indented), its text and final answer ("agent: " on
|
|
8027
|
+
every line), files it made
|
|
8028
|
+
build.txt, <test>/<n>/install.txt, checks.txt Logs
|
|
8003
8029
|
|
|
8004
8030
|
Grading: firegoat runs only the script checks; they set the exit code. end_goal is
|
|
8005
8031
|
yours to judge: read transcript.txt first (a few KB); the session file has everything
|
|
8006
8032
|
(often 50x larger).
|
|
8007
8033
|
|
|
8008
8034
|
Examples:
|
|
8009
|
-
npx firegoat eval --json
|
|
8010
|
-
npx firegoat eval quickstart --runs 3
|
|
8035
|
+
npx firegoat eval --json All tests, JSON summary on stdout
|
|
8036
|
+
npx firegoat eval quickstart --runs 3 One test, three runs
|
|
8011
8037
|
npx firegoat eval --model claude-haiku-5-5 --model claude-sonnet-5-5
|
|
8012
|
-
|
|
8038
|
+
Each test on both models
|
|
8013
8039
|
npx firegoat eval --var SHA=$(git rev-parse HEAD)
|
|
8014
8040
|
|
|
8015
8041
|
A run can take minutes (default timeout 600s per test).
|
|
@@ -8022,8 +8048,8 @@ firegoat confused you, failed you or cost you tokens, say so here: one concrete
|
|
|
8022
8048
|
title, then what you ran and what happened in --body (- reads stdin).
|
|
8023
8049
|
|
|
8024
8050
|
"<title>" 8-200 characters, e.g. "eval hid claude's spend-limit error"
|
|
8025
|
-
--body TEXT
|
|
8026
|
-
--type bug, feature_request or feedback (default)
|
|
8051
|
+
--body TEXT Details: commands, output, what you expected
|
|
8052
|
+
--type One of bug, feature_request or feedback (default)
|
|
8027
8053
|
|
|
8028
8054
|
Sent along: the firegoat version, your OS and node version, and which agent you are
|
|
8029
8055
|
(claude-code/codex/cursor, from the environment). Don't include secrets: a report that
|
|
@@ -8037,7 +8063,7 @@ Example:
|
|
|
8037
8063
|
|
|
8038
8064
|
Exit codes: 0 sent, 2 the service failed (or rate limit), 3 usage error.`;
|
|
8039
8065
|
|
|
8040
|
-
// src/
|
|
8066
|
+
// src/steps.ts
|
|
8041
8067
|
import { createWriteStream } from "node:fs";
|
|
8042
8068
|
var STEP_TIMEOUT_MS = 600000;
|
|
8043
8069
|
async function runSteps(opts) {
|
|
@@ -8077,18 +8103,20 @@ function truncate(text, maxLines, maxChars) {
|
|
|
8077
8103
|
return out.length < text.length ? `${out}
|
|
8078
8104
|
… (${text.length - out.length} more chars)` : out;
|
|
8079
8105
|
}
|
|
8080
|
-
function
|
|
8106
|
+
function prefixLines(prefix, text) {
|
|
8081
8107
|
return text.split(`
|
|
8082
|
-
`).map((l) =>
|
|
8108
|
+
`).map((l) => `${prefix}${l}`).join(`
|
|
8083
8109
|
`);
|
|
8084
8110
|
}
|
|
8085
|
-
|
|
8111
|
+
var indent = (text) => prefixLines(" ", text);
|
|
8112
|
+
var prose = (text) => prefixLines("agent: ", text);
|
|
8113
|
+
function describeCall(step, name, input) {
|
|
8086
8114
|
if (typeof input.command === "string")
|
|
8087
|
-
return
|
|
8115
|
+
return prefixLines(`[${step}] $ `, input.command);
|
|
8088
8116
|
for (const key of ["file_path", "path", "url", "pattern", "query"])
|
|
8089
8117
|
if (typeof input[key] === "string")
|
|
8090
|
-
return
|
|
8091
|
-
return
|
|
8118
|
+
return `[${step}] ${name} ${input[key]}`;
|
|
8119
|
+
return `[${step}] ${name} ${truncate(JSON.stringify(input), 1, 300)}`;
|
|
8092
8120
|
}
|
|
8093
8121
|
function resultText(content) {
|
|
8094
8122
|
if (typeof content === "string")
|
|
@@ -8125,10 +8153,10 @@ function writeTranscript(opts) {
|
|
|
8125
8153
|
if (e?.type === "assistant")
|
|
8126
8154
|
for (const b of content) {
|
|
8127
8155
|
if (b.type === "tool_use")
|
|
8128
|
-
out.push(
|
|
8156
|
+
out.push(describeCall(++step, String(b.name), b.input ?? {}));
|
|
8129
8157
|
else if (b.type === "text" && typeof b.text === "string") {
|
|
8130
8158
|
lastText = out.length;
|
|
8131
|
-
out.push(
|
|
8159
|
+
out.push(prose(truncate(b.text, 30, TEXT_CHARS)));
|
|
8132
8160
|
}
|
|
8133
8161
|
}
|
|
8134
8162
|
else if (e?.type === "user")
|
|
@@ -8143,9 +8171,9 @@ ${indent(truncate(text, OUTPUT_LINES, OUTPUT_CHARS))}` : ` → ${status}`);
|
|
|
8143
8171
|
else if (e?.type === "result" && typeof e.result === "string")
|
|
8144
8172
|
final = e.result;
|
|
8145
8173
|
}
|
|
8146
|
-
if (lastText === out.length - 1 && final && out[lastText] ===
|
|
8174
|
+
if (lastText === out.length - 1 && final && out[lastText] === prose(truncate(final, 30, TEXT_CHARS)))
|
|
8147
8175
|
out.pop();
|
|
8148
|
-
out.push("", "## final answer", final
|
|
8176
|
+
out.push("", "## final answer", final ? prose(final) : "(none)");
|
|
8149
8177
|
out.push("", "## files in the workdir", ...workdirFiles(opts.workdir));
|
|
8150
8178
|
writeFileSync2(opts.path, `${out.join(`
|
|
8151
8179
|
`)}
|
|
@@ -8243,13 +8271,13 @@ function plainUI(write) {
|
|
|
8243
8271
|
const printed = new Set;
|
|
8244
8272
|
const uploadPrinted = new Set;
|
|
8245
8273
|
return {
|
|
8246
|
-
auth: (text) => write(`
|
|
8274
|
+
auth: (text) => write(`Auth: ${text}`),
|
|
8247
8275
|
note: write,
|
|
8248
|
-
|
|
8276
|
+
build: (state, seconds) => {
|
|
8249
8277
|
if (state === "running")
|
|
8250
|
-
write("
|
|
8278
|
+
write("Build...");
|
|
8251
8279
|
else if (state === "fail")
|
|
8252
|
-
write(`
|
|
8280
|
+
write(`Build failed (${seconds}s)`);
|
|
8253
8281
|
},
|
|
8254
8282
|
runs: (r) => {
|
|
8255
8283
|
rows = r;
|
|
@@ -8277,14 +8305,14 @@ function plainUI(write) {
|
|
|
8277
8305
|
const tr = tier(all);
|
|
8278
8306
|
write("");
|
|
8279
8307
|
write(`${c.pass} passed, ${c.fail} failed, ${c.error} not graded (${all.length} runs, $${t.cost.toFixed(2)})${tr ? `, tier ${tr}` : ""}`);
|
|
8280
|
-
write(`
|
|
8308
|
+
write(`Runs: ${runsDir} (end_goal is for you to judge: read <test>/<n>/transcript.txt)`);
|
|
8281
8309
|
}
|
|
8282
8310
|
};
|
|
8283
8311
|
}
|
|
8284
8312
|
function ttyUI(out, opts) {
|
|
8285
8313
|
const p = painter(opts.color);
|
|
8286
8314
|
let rows = [];
|
|
8287
|
-
let
|
|
8315
|
+
let build = null;
|
|
8288
8316
|
let authText = "";
|
|
8289
8317
|
const notes = [];
|
|
8290
8318
|
let frame = 0;
|
|
@@ -8301,9 +8329,9 @@ function ttyUI(out, opts) {
|
|
|
8301
8329
|
case "error":
|
|
8302
8330
|
return p(C.amber, "NOT GRADED");
|
|
8303
8331
|
case "running":
|
|
8304
|
-
return p(C.ember, "
|
|
8332
|
+
return p(C.ember, "Running ");
|
|
8305
8333
|
default:
|
|
8306
|
-
return p(C.gray, "
|
|
8334
|
+
return p(C.gray, "Queued ");
|
|
8307
8335
|
}
|
|
8308
8336
|
};
|
|
8309
8337
|
const glyph = (r) => {
|
|
@@ -8331,12 +8359,12 @@ function ttyUI(out, opts) {
|
|
|
8331
8359
|
rule
|
|
8332
8360
|
];
|
|
8333
8361
|
if (authText)
|
|
8334
|
-
out.push(p(C.gray, `
|
|
8362
|
+
out.push(p(C.gray, ` Auth ${authText}`));
|
|
8335
8363
|
for (const n of notes)
|
|
8336
8364
|
out.push(p(C.gray, ` ${cut(n, w - 2)}`));
|
|
8337
|
-
if (
|
|
8338
|
-
const mark =
|
|
8339
|
-
out.push(` ${
|
|
8365
|
+
if (build) {
|
|
8366
|
+
const mark = build.state === "ok" ? p(C.green, "✓") : build.state === "fail" ? p(C.red, "✗") : p(C.ember, SPIN[frame % SPIN.length]);
|
|
8367
|
+
out.push(` ${build.state === "running" ? p(C.ember, "◐") : p(C.green, "●")} Build${" ".repeat(Math.max(1, w - 20))}${mark}${build.seconds !== undefined ? ` ${build.seconds}s` : ""}`);
|
|
8340
8368
|
}
|
|
8341
8369
|
const modelCols = Math.min(24, Math.max(0, ...rows.map((r) => r.model?.length ?? 0)));
|
|
8342
8370
|
rows.forEach((r, i) => {
|
|
@@ -8346,7 +8374,7 @@ function ttyUI(out, opts) {
|
|
|
8346
8374
|
const model = modelCols > 0 ? ` ${p(C.cyan, cut(r.model ?? "", modelCols).padEnd(modelCols))}` : "";
|
|
8347
8375
|
let detail = "";
|
|
8348
8376
|
if (r.state === "running")
|
|
8349
|
-
detail = p(C.gray, `
|
|
8377
|
+
detail = p(C.gray, `Turn ${r.turns}`);
|
|
8350
8378
|
else if (r.state === "pass" || r.state === "fail")
|
|
8351
8379
|
detail = `${checksPassed(r).padEnd(11)} ${p(C.gray, usage(r))}`;
|
|
8352
8380
|
else if (r.state === "error")
|
|
@@ -8381,11 +8409,11 @@ function ttyUI(out, opts) {
|
|
|
8381
8409
|
const goated = c.pass === rows.length && rows.length > 0;
|
|
8382
8410
|
const tally = `${c.pass} PASS · ${c.fail} FAIL · ${c.error} NOT GRADED`;
|
|
8383
8411
|
const left = goated ? `${rows.length}/${rows.length} runs passed their checks` : tally;
|
|
8384
|
-
const right = goated ? `GOATED · tier ${tr}` : tr ? `
|
|
8412
|
+
const right = goated ? `GOATED · tier ${tr}` : tr ? `Tier ${tr}` : "";
|
|
8385
8413
|
out.push(` ${goated ? p(C.green, left) : left}${" ".repeat(Math.max(2, w - left.length - right.length - 3))}${goated ? p(C.ember, right) : right}`);
|
|
8386
|
-
out.push(p(C.gray, `
|
|
8387
|
-
out.push(p(C.gray, `
|
|
8388
|
-
out.push(`
|
|
8414
|
+
out.push(p(C.gray, ` Total $${t.cost.toFixed(2)} · ${t.turns} turns · ${duration(finished.seconds)}`));
|
|
8415
|
+
out.push(p(C.gray, ` Runs ${shortPath(finished.runsDir)}/`));
|
|
8416
|
+
out.push(` Checks are a score, not the verdict: judge each end_goal from ${p(C.cyan, "<test>/<n>/transcript.txt")}`);
|
|
8389
8417
|
return out;
|
|
8390
8418
|
};
|
|
8391
8419
|
const rowsOf = (line) => {
|
|
@@ -8413,8 +8441,8 @@ function ttyUI(out, opts) {
|
|
|
8413
8441
|
note: (text) => {
|
|
8414
8442
|
notes.push(text);
|
|
8415
8443
|
},
|
|
8416
|
-
|
|
8417
|
-
|
|
8444
|
+
build: (state, seconds) => {
|
|
8445
|
+
build = { state, seconds };
|
|
8418
8446
|
render();
|
|
8419
8447
|
},
|
|
8420
8448
|
runs: (r) => {
|
|
@@ -8431,7 +8459,7 @@ function ttyUI(out, opts) {
|
|
|
8431
8459
|
}
|
|
8432
8460
|
|
|
8433
8461
|
// src/upload.ts
|
|
8434
|
-
import { existsSync as
|
|
8462
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
8435
8463
|
import { basename as basename2 } from "node:path";
|
|
8436
8464
|
var MAX_FILE = 20 * 1024 * 1024;
|
|
8437
8465
|
var MAX_CHECKS = 200;
|
|
@@ -8507,7 +8535,7 @@ async function uploadRun(t, r) {
|
|
|
8507
8535
|
};
|
|
8508
8536
|
const bytes = {};
|
|
8509
8537
|
for (const name of FILES) {
|
|
8510
|
-
const b =
|
|
8538
|
+
const b = existsSync3(paths[name]) ? readFileSync3(paths[name]) : Buffer.alloc(0);
|
|
8511
8539
|
if (b.byteLength > MAX_FILE)
|
|
8512
8540
|
return {
|
|
8513
8541
|
status: "failed",
|
|
@@ -8668,7 +8696,7 @@ function parseArgs(args) {
|
|
|
8668
8696
|
} else if (flag === "--claude-bin")
|
|
8669
8697
|
opts.claudeBin = value();
|
|
8670
8698
|
else if (a.startsWith("-"))
|
|
8671
|
-
throw new UsageError(`
|
|
8699
|
+
throw new UsageError(`Unknown flag: ${a}`);
|
|
8672
8700
|
else
|
|
8673
8701
|
opts.names.push(a);
|
|
8674
8702
|
}
|
|
@@ -8676,11 +8704,11 @@ function parseArgs(args) {
|
|
|
8676
8704
|
}
|
|
8677
8705
|
function findSession(configDir, sessionId) {
|
|
8678
8706
|
const projects = join5(configDir, "projects");
|
|
8679
|
-
if (!
|
|
8707
|
+
if (!existsSync4(projects))
|
|
8680
8708
|
return null;
|
|
8681
8709
|
for (const d of readdirSync2(projects)) {
|
|
8682
8710
|
const f = join5(projects, d, `${sessionId}.jsonl`);
|
|
8683
|
-
if (
|
|
8711
|
+
if (existsSync4(f))
|
|
8684
8712
|
return f;
|
|
8685
8713
|
}
|
|
8686
8714
|
return null;
|
|
@@ -8772,13 +8800,13 @@ async function runOne(opts) {
|
|
|
8772
8800
|
FIREGOAT_WORKDIR: workdir
|
|
8773
8801
|
});
|
|
8774
8802
|
const stepEnv = { ...opts.stepEnv, ...env, FIREGOAT_WORKDIR: workdir };
|
|
8775
|
-
if (opts.
|
|
8803
|
+
if (opts.install.length > 0) {
|
|
8776
8804
|
const error = await runSteps({
|
|
8777
|
-
what: "
|
|
8778
|
-
commands: opts.
|
|
8805
|
+
what: "install",
|
|
8806
|
+
commands: opts.install,
|
|
8779
8807
|
cwd: workdir,
|
|
8780
8808
|
env: stepEnv,
|
|
8781
|
-
logPath: join5(dir, "
|
|
8809
|
+
logPath: join5(dir, "install.txt")
|
|
8782
8810
|
});
|
|
8783
8811
|
if (error)
|
|
8784
8812
|
return done("error", `not graded: ${error}`);
|
|
@@ -8869,7 +8897,7 @@ async function evalCommand(args, root) {
|
|
|
8869
8897
|
const known = new Set(tests.map((t) => t.name));
|
|
8870
8898
|
const unknown = opts.names.filter((n) => !known.has(n));
|
|
8871
8899
|
if (unknown.length > 0)
|
|
8872
|
-
return fatal(EXIT.usage, `
|
|
8900
|
+
return fatal(EXIT.usage, `Unknown test ${unknown.map((n) => JSON.stringify(n)).join(", ")} (known: ${[...known].join(", ")})`);
|
|
8873
8901
|
tests = tests.filter((t) => opts.names.includes(t.name));
|
|
8874
8902
|
}
|
|
8875
8903
|
const runs = opts.runs ?? config.runs;
|
|
@@ -8883,10 +8911,10 @@ async function evalCommand(args, root) {
|
|
|
8883
8911
|
]).flatMap(varRefs).filter((k) => !(k in vars)))
|
|
8884
8912
|
];
|
|
8885
8913
|
if (missing.length > 0)
|
|
8886
|
-
return fatal(EXIT.usage, `
|
|
8914
|
+
return fatal(EXIT.usage, `No value for ${missing.map((k) => `{{vars.${k}}}`).join(", ")}: pass --var ${missing[0]}=... or set it under vars: in firegoat.yaml`);
|
|
8887
8915
|
const claudePath = resolveBin(opts.claudeBin);
|
|
8888
8916
|
if (!claudePath)
|
|
8889
|
-
return fatal(EXIT.notGraded, `
|
|
8917
|
+
return fatal(EXIT.notGraded, `Not graded: claude binary ${JSON.stringify(opts.claudeBin)} not found. Install Claude Code (https://claude.com/claude-code) or pass --claude-bin PATH / FIREGOAT_CLAUDE_BIN.`);
|
|
8890
8918
|
let repo;
|
|
8891
8919
|
try {
|
|
8892
8920
|
repo = gitRepo(root);
|
|
@@ -8903,7 +8931,7 @@ async function evalCommand(args, root) {
|
|
|
8903
8931
|
cwd: root
|
|
8904
8932
|
});
|
|
8905
8933
|
if (loginProblem)
|
|
8906
|
-
return fatal(EXIT.notGraded, `
|
|
8934
|
+
return fatal(EXIT.notGraded, `Not graded: ${loginProblem}`);
|
|
8907
8935
|
const evalStarted = Date.now();
|
|
8908
8936
|
const runId = newRunId();
|
|
8909
8937
|
let upload;
|
|
@@ -8935,23 +8963,23 @@ async function evalCommand(args, root) {
|
|
|
8935
8963
|
state: "queued",
|
|
8936
8964
|
turns: 0
|
|
8937
8965
|
}))));
|
|
8938
|
-
if (config.
|
|
8939
|
-
const
|
|
8940
|
-
ui.
|
|
8966
|
+
if (config.build.length > 0) {
|
|
8967
|
+
const buildStarted = Date.now();
|
|
8968
|
+
ui.build("running");
|
|
8941
8969
|
const error = await runSteps({
|
|
8942
|
-
what: "
|
|
8943
|
-
commands: config.
|
|
8970
|
+
what: "build",
|
|
8971
|
+
commands: config.build,
|
|
8944
8972
|
cwd: root,
|
|
8945
8973
|
env: {
|
|
8946
8974
|
...stepEnv,
|
|
8947
8975
|
...renderEnv(Object.fromEntries(Object.entries(config.env).filter(([, v]) => isEvalLevel(v))), vars, { "run.id": runId, FIREGOAT_OUT: out })
|
|
8948
8976
|
},
|
|
8949
|
-
logPath: join5(runDir, "
|
|
8977
|
+
logPath: join5(runDir, "build.txt")
|
|
8950
8978
|
});
|
|
8951
|
-
const secs = Math.round((Date.now() -
|
|
8952
|
-
ui.
|
|
8979
|
+
const secs = Math.round((Date.now() - buildStarted) / 1000);
|
|
8980
|
+
ui.build(error ? "fail" : "ok", secs);
|
|
8953
8981
|
if (error)
|
|
8954
|
-
return fatal(EXIT.notGraded, `
|
|
8982
|
+
return fatal(EXIT.notGraded, `Not graded: ${error}`);
|
|
8955
8983
|
}
|
|
8956
8984
|
ui.runs(rows);
|
|
8957
8985
|
const results = new Array(rows.length);
|
|
@@ -8982,7 +9010,7 @@ async function evalCommand(args, root) {
|
|
|
8982
9010
|
runDir,
|
|
8983
9011
|
claudeBin: claudePath,
|
|
8984
9012
|
configDir,
|
|
8985
|
-
|
|
9013
|
+
install: config.install,
|
|
8986
9014
|
out,
|
|
8987
9015
|
stepEnv,
|
|
8988
9016
|
vars
|
|
@@ -9282,7 +9310,7 @@ async function feedbackCommand(args) {
|
|
|
9282
9310
|
throw new Error(`--type must be one of ${TYPES.join(", ")}, got ${v}`);
|
|
9283
9311
|
type = v;
|
|
9284
9312
|
} else if (a.startsWith("-"))
|
|
9285
|
-
throw new Error(`
|
|
9313
|
+
throw new Error(`Unknown flag: ${a}`);
|
|
9286
9314
|
else
|
|
9287
9315
|
titleParts.push(a);
|
|
9288
9316
|
} catch (e) {
|
|
@@ -9292,7 +9320,7 @@ async function feedbackCommand(args) {
|
|
|
9292
9320
|
}
|
|
9293
9321
|
const title = titleParts.join(" ").trim();
|
|
9294
9322
|
if (title.length < 8 || title.length > 200) {
|
|
9295
|
-
console.error(`firegoat feedback:
|
|
9323
|
+
console.error(`firegoat feedback: The title must be 8-200 characters (got ${title.length})`);
|
|
9296
9324
|
return EXIT.usage;
|
|
9297
9325
|
}
|
|
9298
9326
|
const client = new FeedbackClient({ domain: "firegoat.ai" });
|
|
@@ -9310,7 +9338,7 @@ firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
|
|
|
9310
9338
|
nodeVersion: process.version
|
|
9311
9339
|
}
|
|
9312
9340
|
});
|
|
9313
|
-
console.log(`
|
|
9341
|
+
console.log(`Sent ${type} ${id} to the firegoat team${similarCount > 0 ? ` (${similarCount} similar report${similarCount === 1 ? "" : "s"} already)` : ""}. Thanks.`);
|
|
9314
9342
|
return EXIT.pass;
|
|
9315
9343
|
} catch (e) {
|
|
9316
9344
|
if (e instanceof FeedbackError) {
|
|
@@ -9324,7 +9352,7 @@ firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
|
|
|
9324
9352
|
|
|
9325
9353
|
// src/init.ts
|
|
9326
9354
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
9327
|
-
import { existsSync as
|
|
9355
|
+
import { existsSync as existsSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
9328
9356
|
import { basename as basename4, join as join6 } from "node:path";
|
|
9329
9357
|
import { createInterface } from "node:readline/promises";
|
|
9330
9358
|
|
|
@@ -9516,6 +9544,12 @@ var SKILLS_ARGV = [
|
|
|
9516
9544
|
"--copy",
|
|
9517
9545
|
"-y"
|
|
9518
9546
|
];
|
|
9547
|
+
function skillsDir(scope) {
|
|
9548
|
+
if (scope === "local")
|
|
9549
|
+
return ".claude/skills/";
|
|
9550
|
+
const config = process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
9551
|
+
return config ? `${join6(config, "skills")}/` : "~/.claude/skills/";
|
|
9552
|
+
}
|
|
9519
9553
|
function template(product) {
|
|
9520
9554
|
return `# firegoat.yaml: evals for how well coding agents use ${product}.
|
|
9521
9555
|
# Each test runs your claude in an empty folder (.firegoat/runs/...) with \`prompt\`
|
|
@@ -9529,27 +9563,27 @@ runs: 1
|
|
|
9529
9563
|
# model: [claude-haiku-5-5, claude-sonnet-5-5]
|
|
9530
9564
|
|
|
9531
9565
|
# Optional: test your local build instead of the published package.
|
|
9532
|
-
#
|
|
9533
|
-
# agent's PATH.
|
|
9566
|
+
# build: runs once per eval, here; what it writes to $FIREGOAT_OUT is put on the
|
|
9567
|
+
# agent's PATH. install: runs in each fresh folder before the agent starts.
|
|
9534
9568
|
#
|
|
9535
|
-
# npm:
|
|
9536
|
-
#
|
|
9537
|
-
#
|
|
9569
|
+
# For npm:
|
|
9570
|
+
# build: npm pack --pack-destination "$FIREGOAT_OUT"
|
|
9571
|
+
# install: npm init -y >/dev/null && npm install "$FIREGOAT_OUT"/*.tgz
|
|
9538
9572
|
#
|
|
9539
|
-
# python:
|
|
9540
|
-
#
|
|
9541
|
-
#
|
|
9573
|
+
# For python:
|
|
9574
|
+
# build: uv build --wheel --out-dir "$FIREGOAT_OUT"
|
|
9575
|
+
# install: uv venv && uv pip install "$FIREGOAT_OUT"/*.whl
|
|
9542
9576
|
#
|
|
9543
|
-
# a CLI binary users call by bare name (on PATH as is):
|
|
9544
|
-
#
|
|
9545
|
-
# (
|
|
9577
|
+
# For a CLI binary users call by bare name (on PATH as is):
|
|
9578
|
+
# build: go build -o "$FIREGOAT_OUT/mycli" ./cmd/mycli
|
|
9579
|
+
# (An npm/pypi CLI that users run via npx/uvx: pack + install it as above instead;
|
|
9546
9580
|
# a binary on PATH does not stop "npx mycli" from fetching the registry release)
|
|
9547
9581
|
#
|
|
9548
9582
|
# Helper files (scripts, sample data) go in .firegoat/fixtures/ (committed), not inline here.
|
|
9549
|
-
#
|
|
9550
|
-
#
|
|
9583
|
+
# The build step runs in this folder, so: build: cp -R .firegoat/fixtures/. "$FIREGOAT_OUT"
|
|
9584
|
+
# Then e.g. install: cp "$FIREGOAT_OUT/wizard.mjs" .
|
|
9551
9585
|
|
|
9552
|
-
# Optional: env vars for the agent (and for
|
|
9586
|
+
# Optional: env vars for the agent (and for install and checks of the same
|
|
9553
9587
|
# run). The agent gets none of vars:, only these. Placeholders: {{vars.KEY}},
|
|
9554
9588
|
# {{run.id}}, {{run.n}}, {{test.name}}, and $FIREGOAT_OUT / $FIREGOAT_WORKDIR (no
|
|
9555
9589
|
# other $ expansion: $HOME stays literal). A test can add its own env:.
|
|
@@ -9590,15 +9624,30 @@ function copyToClipboard(text) {
|
|
|
9590
9624
|
return !r.error && r.status === 0;
|
|
9591
9625
|
});
|
|
9592
9626
|
}
|
|
9593
|
-
async function
|
|
9627
|
+
async function ask(question) {
|
|
9594
9628
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
9595
9629
|
try {
|
|
9596
|
-
|
|
9597
|
-
return a === "" || /^y(es)?$/i.test(a);
|
|
9630
|
+
return (await rl.question(question)).trim();
|
|
9598
9631
|
} finally {
|
|
9599
9632
|
rl.close();
|
|
9600
9633
|
}
|
|
9601
9634
|
}
|
|
9635
|
+
async function askYes(question) {
|
|
9636
|
+
const a = await ask(`${question} [Y/n] `);
|
|
9637
|
+
return a === "" || /^y(es)?$/i.test(a);
|
|
9638
|
+
}
|
|
9639
|
+
async function askScope(question) {
|
|
9640
|
+
for (;; ) {
|
|
9641
|
+
const a = await ask(`${question} [G/l/n] `);
|
|
9642
|
+
if (a === "" || /^g(lobal)?$/i.test(a))
|
|
9643
|
+
return "global";
|
|
9644
|
+
if (/^l(ocal)?$/i.test(a))
|
|
9645
|
+
return "local";
|
|
9646
|
+
if (/^no?$/i.test(a))
|
|
9647
|
+
return "none";
|
|
9648
|
+
process.stdout.write("\x1B[1A\x1B[2K");
|
|
9649
|
+
}
|
|
9650
|
+
}
|
|
9602
9651
|
var LOGIN_HINT = "Runs stay local: npx firegoat login uploads them to firegoat.ai";
|
|
9603
9652
|
async function loginStep(interactive, done, paint) {
|
|
9604
9653
|
const label = "firegoat.ai login";
|
|
@@ -9634,7 +9683,8 @@ async function loginStep(interactive, done, paint) {
|
|
|
9634
9683
|
}
|
|
9635
9684
|
async function initCommand(args, cwd) {
|
|
9636
9685
|
let force = false;
|
|
9637
|
-
let
|
|
9686
|
+
let noSkills = false;
|
|
9687
|
+
let localSkills = false;
|
|
9638
9688
|
let login = true;
|
|
9639
9689
|
for (const a of args) {
|
|
9640
9690
|
if (a === "--help" || a === "-h") {
|
|
@@ -9644,16 +9694,25 @@ async function initCommand(args, cwd) {
|
|
|
9644
9694
|
if (a === "--force" || a === "-f")
|
|
9645
9695
|
force = true;
|
|
9646
9696
|
else if (a === "--no-skills")
|
|
9647
|
-
|
|
9697
|
+
noSkills = true;
|
|
9698
|
+
else if (a === "--local-skills")
|
|
9699
|
+
localSkills = true;
|
|
9648
9700
|
else if (a === "--no-login")
|
|
9649
9701
|
login = false;
|
|
9650
9702
|
else {
|
|
9651
|
-
console.error(`firegoat init:
|
|
9703
|
+
console.error(`firegoat init: Unknown argument: ${a}
|
|
9652
9704
|
|
|
9653
9705
|
${INIT_HELP}`);
|
|
9654
9706
|
return EXIT.usage;
|
|
9655
9707
|
}
|
|
9656
9708
|
}
|
|
9709
|
+
if (noSkills && localSkills) {
|
|
9710
|
+
console.error(`firegoat init: --no-skills and --local-skills contradict each other: pass one
|
|
9711
|
+
|
|
9712
|
+
${INIT_HELP}`);
|
|
9713
|
+
return EXIT.usage;
|
|
9714
|
+
}
|
|
9715
|
+
let scope = noSkills ? "none" : localSkills ? "local" : "global";
|
|
9657
9716
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
9658
9717
|
const color = interactive && !process.env.NO_COLOR;
|
|
9659
9718
|
const paint = (code, text) => color ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
@@ -9667,25 +9726,28 @@ ${INIT_HELP}`);
|
|
|
9667
9726
|
}
|
|
9668
9727
|
ensureFiregoatDir(cwd);
|
|
9669
9728
|
const configPath = join6(cwd, CONFIG_FILE);
|
|
9670
|
-
const keep =
|
|
9729
|
+
const keep = existsSync5(configPath) && !force;
|
|
9671
9730
|
if (!keep)
|
|
9672
9731
|
writeFileSync4(configPath, template(basename4(cwd)));
|
|
9673
9732
|
if (interactive) {
|
|
9674
|
-
step(1, CONFIG_FILE, keep ? "
|
|
9733
|
+
step(1, CONFIG_FILE, keep ? "Kept (--force replaces it)" : "Created");
|
|
9675
9734
|
console.log(paint("90", " │"));
|
|
9676
|
-
step(2, ".firegoat/", "
|
|
9735
|
+
step(2, ".firegoat/", "Ready");
|
|
9677
9736
|
} else
|
|
9678
|
-
console.log(keep ? `
|
|
9679
|
-
if (
|
|
9737
|
+
console.log(keep ? `Kept ${CONFIG_FILE} (--force replaces it)` : `Wrote ${CONFIG_FILE}: edit the quickstart test to fit your users`);
|
|
9738
|
+
if (interactive && !noSkills)
|
|
9680
9739
|
console.log(paint("90", " │"));
|
|
9681
|
-
|
|
9740
|
+
if (interactive && !noSkills && !localSkills) {
|
|
9741
|
+
scope = await askScope(` ${paint("38;5;208", "◉")} ${paint("90", "03")} Install the firegoat-onboarding skill globally (~/.claude/skills) or in this repo (.claude/skills)?`);
|
|
9682
9742
|
process.stdout.write("\x1B[1A\x1B[2K");
|
|
9683
9743
|
}
|
|
9684
|
-
if (
|
|
9744
|
+
if (scope !== "none") {
|
|
9745
|
+
const argv = [...SKILLS_ARGV, ...scope === "global" ? ["-g"] : []];
|
|
9746
|
+
const where = skillsDir(scope);
|
|
9685
9747
|
if (!interactive)
|
|
9686
|
-
console.log(`
|
|
9748
|
+
console.log(`Installing the skill: npx ${argv.join(" ")}`);
|
|
9687
9749
|
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => k !== "npm_config_package" && !k.startsWith("FIREGOAT_")));
|
|
9688
|
-
const r = spawnSync3("npx", ["-y", ...
|
|
9750
|
+
const r = spawnSync3("npx", ["-y", ...argv], {
|
|
9689
9751
|
cwd,
|
|
9690
9752
|
env,
|
|
9691
9753
|
stdio: "pipe",
|
|
@@ -9693,21 +9755,21 @@ ${INIT_HELP}`);
|
|
|
9693
9755
|
});
|
|
9694
9756
|
if (r.error || r.status !== 0) {
|
|
9695
9757
|
process.stderr.write(`${r.stdout ?? ""}${r.stderr ?? ""}`);
|
|
9696
|
-
console.error(`firegoat init:
|
|
9758
|
+
console.error(`firegoat init: Installing the skill failed (${r.error?.message ?? `exit ${r.status}`}). Run it yourself: npx ${argv.join(" ")}`);
|
|
9697
9759
|
return EXIT.notGraded;
|
|
9698
9760
|
}
|
|
9699
9761
|
if (interactive)
|
|
9700
|
-
step(3, "firegoat-onboarding skill",
|
|
9762
|
+
step(3, "firegoat-onboarding skill", `Installed → ${where}`);
|
|
9701
9763
|
else
|
|
9702
|
-
console.log(
|
|
9703
|
-
const ignored = spawnSync3("git", ["check-ignore", "-q", ".claude/skills"], {
|
|
9764
|
+
console.log(`Installed skill firegoat-onboarding in ${where}`);
|
|
9765
|
+
const ignored = scope === "local" && spawnSync3("git", ["check-ignore", "-q", ".claude/skills"], {
|
|
9704
9766
|
cwd,
|
|
9705
9767
|
env: hostEnv()
|
|
9706
|
-
});
|
|
9707
|
-
if (ignored
|
|
9708
|
-
console.error("
|
|
9768
|
+
}).status === 0;
|
|
9769
|
+
if (ignored)
|
|
9770
|
+
console.error("Warning: git ignores .claude/skills, so teammates and CI won't get the skill. Commit them with: git add -f .claude/skills");
|
|
9709
9771
|
} else if (interactive)
|
|
9710
|
-
step(3, "firegoat-onboarding skill", "
|
|
9772
|
+
step(3, "firegoat-onboarding skill", "Skipped");
|
|
9711
9773
|
if (login) {
|
|
9712
9774
|
if (interactive)
|
|
9713
9775
|
console.log(paint("90", " │"));
|
|
@@ -9721,12 +9783,12 @@ Prompt for your coding agent: ${AGENT_PROMPT}`);
|
|
|
9721
9783
|
return EXIT.pass;
|
|
9722
9784
|
}
|
|
9723
9785
|
console.log(rule);
|
|
9724
|
-
console.log(`
|
|
9786
|
+
console.log(` Next: hand this to your coding agent
|
|
9725
9787
|
`);
|
|
9726
9788
|
console.log(AGENT_PROMPT);
|
|
9727
9789
|
console.log("");
|
|
9728
9790
|
const copied = copyToClipboard(AGENT_PROMPT);
|
|
9729
|
-
console.log(` ${copied ? `${paint("32", "✓ Prompt copied!")} ` : ""}
|
|
9791
|
+
console.log(` ${copied ? `${paint("32", "✓ Prompt copied!")} ` : ""}Then: ${paint("36", "npx firegoat eval")}`);
|
|
9730
9792
|
return EXIT.pass;
|
|
9731
9793
|
}
|
|
9732
9794
|
|
|
@@ -9754,7 +9816,7 @@ async function main(argv) {
|
|
|
9754
9816
|
case "logout":
|
|
9755
9817
|
return logoutCommand(rest);
|
|
9756
9818
|
default:
|
|
9757
|
-
console.error(`
|
|
9819
|
+
console.error(`Unknown command: ${cmd}
|
|
9758
9820
|
|
|
9759
9821
|
${topHelp(package_default.version)}`);
|
|
9760
9822
|
return EXIT.usage;
|
|
@@ -9767,7 +9829,7 @@ main(process.argv.slice(2)).then((code) => process.exit(code), (err) => {
|
|
|
9767
9829
|
}
|
|
9768
9830
|
if (err?.code === "ABORT_ERR") {
|
|
9769
9831
|
console.error(`
|
|
9770
|
-
|
|
9832
|
+
Aborted.`);
|
|
9771
9833
|
process.exit(130);
|
|
9772
9834
|
}
|
|
9773
9835
|
console.error(err);
|