firegoat 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/cli.js +291 -62
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -105,6 +105,7 @@ tests:
|
|
|
105
105
|
- 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.
|
|
106
106
|
- No sandbox: the agent's folder is inside `.firegoat/runs/`, and it can reach the rest of your machine. In `.firegoat/`, git ignores `runs/` and `.claude/`; keep test fixtures (scripts, sample data) in `.firegoat/fixtures/` and commit them with `firegoat.yaml`.
|
|
107
107
|
- 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.
|
|
108
|
+
- 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.
|
|
108
109
|
- 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.
|
|
109
110
|
- 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.
|
|
110
111
|
|
|
@@ -119,6 +120,16 @@ Want a plain `npm i acme-sdk` to get your build, with no hint in the prompt? See
|
|
|
119
120
|
| 2 | at least one run was not graded (claude missing or logged out, crash, timeout, setup failed); wins over 1 |
|
|
120
121
|
| 3 | usage or config error |
|
|
121
122
|
|
|
123
|
+
## Feedback
|
|
124
|
+
|
|
125
|
+
Something in firegoat got in your way (or your agent's)? Tell us from the terminal:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
npx firegoat feedback "eval hid claude's spend-limit error" --type bug --body "ran npx firegoat eval, got 'not graded' x3"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
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.
|
|
132
|
+
|
|
122
133
|
## Develop
|
|
123
134
|
|
|
124
135
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -6947,13 +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.13.0",
|
|
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.0.0",
|
|
6957
6958
|
"@types/bun": "latest",
|
|
6958
6959
|
typescript: "^7",
|
|
6959
6960
|
yaml: "^2.9.1"
|
|
@@ -7438,6 +7439,7 @@ script checks.
|
|
|
7438
7439
|
|
|
7439
7440
|
npx firegoat init create firegoat.yaml and .firegoat/, install the skills
|
|
7440
7441
|
npx firegoat eval run the tests (claude must be logged in: run \`claude\` once)
|
|
7442
|
+
npx firegoat feedback "<what got in the way>" tell the firegoat team
|
|
7441
7443
|
|
|
7442
7444
|
Typical flow for a coding agent driving firegoat:
|
|
7443
7445
|
1. read npx firegoat init --help, then edit firegoat.yaml
|
|
@@ -7512,8 +7514,8 @@ Example:
|
|
|
7512
7514
|
! grep -Eq '$ .*npm (install|i|add) .*acme' "$FIREGOAT_TRANSCRIPT"
|
|
7513
7515
|
|
|
7514
7516
|
Exit codes: 0 done, 2 the skills install failed, 3 usage error.`;
|
|
7515
|
-
var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--var KEY=value]..
|
|
7516
|
-
[--claude-bin PATH]
|
|
7517
|
+
var EVAL_HELP = `Usage: firegoat eval [test..] [--runs N] [--parallel N] [--var KEY=value]..
|
|
7518
|
+
[--json] [--claude-bin PATH]
|
|
7517
7519
|
|
|
7518
7520
|
Runs each test in firegoat.yaml (in the current folder): your claude gets the prompt
|
|
7519
7521
|
in a fresh, empty folder, .firegoat/runs/<runId>/<test>/<n>/work, with setup:'s
|
|
@@ -7526,6 +7528,7 @@ sandbox: the agent can reach the rest of your machine, including the repo.
|
|
|
7526
7528
|
|
|
7527
7529
|
test.. only these tests (by name)
|
|
7528
7530
|
--runs N runs per test (overrides runs: in firegoat.yaml)
|
|
7531
|
+
--parallel N runs at once (default 4; 1 runs them one after another)
|
|
7529
7532
|
--var KEY=value fills {{vars.KEY}}; env for setup, workdir_setup and checks
|
|
7530
7533
|
--json the summary as JSON on stdout (or {error, exitCode} if eval
|
|
7531
7534
|
stops early); progress goes to stderr
|
|
@@ -7553,6 +7556,25 @@ Examples:
|
|
|
7553
7556
|
A run can take minutes (default timeout 600s per test).
|
|
7554
7557
|
|
|
7555
7558
|
${EXIT_CODES}`;
|
|
7559
|
+
var FEEDBACK_HELP = `Usage: firegoat feedback "<title>" [--body TEXT|-] [--type bug|feature_request|feedback]
|
|
7560
|
+
|
|
7561
|
+
Sends feedback about firegoat to the firegoat team (firegoat.ai). Coding agents: if
|
|
7562
|
+
firegoat confused you, failed you or cost you tokens, say so here: one concrete
|
|
7563
|
+
title, then what you ran and what happened in --body (- reads stdin).
|
|
7564
|
+
|
|
7565
|
+
"<title>" 8-200 characters, e.g. "eval hid claude's spend-limit error"
|
|
7566
|
+
--body TEXT details: commands, output, what you expected
|
|
7567
|
+
--type bug, feature_request or feedback (default)
|
|
7568
|
+
|
|
7569
|
+
Sent along: the firegoat version, your OS and node version, and which agent you are
|
|
7570
|
+
(claude-code/codex/cursor, from the environment). Don't include secrets: a report that
|
|
7571
|
+
looks like it contains one is refused.
|
|
7572
|
+
|
|
7573
|
+
Example:
|
|
7574
|
+
npx firegoat feedback "checks can't grep commands with quotes" --type bug \\
|
|
7575
|
+
--body "grep '"command":"...' stops at the first escaped quote"
|
|
7576
|
+
|
|
7577
|
+
Exit codes: 0 sent, 2 the service failed (or rate limit), 3 usage error.`;
|
|
7556
7578
|
|
|
7557
7579
|
// src/setup.ts
|
|
7558
7580
|
import { createWriteStream } from "node:fs";
|
|
@@ -7686,6 +7708,17 @@ var C = {
|
|
|
7686
7708
|
bold: "1",
|
|
7687
7709
|
cyan: "36"
|
|
7688
7710
|
};
|
|
7711
|
+
var BUTTERFLY = [" \\ /", " (_\\/_)", " (_/\\_)"];
|
|
7712
|
+
var LOGO_COLS = 10;
|
|
7713
|
+
function logoHeader(paint, title, right, width) {
|
|
7714
|
+
return BUTTERFLY.map((wing, i) => {
|
|
7715
|
+
const art = paint(C.ember, wing);
|
|
7716
|
+
if (i !== 1)
|
|
7717
|
+
return art;
|
|
7718
|
+
const used = LOGO_COLS + title.length + right.replace(ANSI, "").length;
|
|
7719
|
+
return `${art}${" ".repeat(LOGO_COLS - wing.length)}${paint(C.bold, title)}${" ".repeat(Math.max(2, width - used - 1))}${right}`;
|
|
7720
|
+
});
|
|
7721
|
+
}
|
|
7689
7722
|
function tier(rows) {
|
|
7690
7723
|
const graded = rows.filter((r) => r.state === "pass" || r.state === "fail");
|
|
7691
7724
|
if (graded.length === 0)
|
|
@@ -7720,9 +7753,10 @@ function checksPassed(r) {
|
|
|
7720
7753
|
function total(rows) {
|
|
7721
7754
|
const cost = rows.reduce((s, r) => s + (r.cost ?? 0), 0);
|
|
7722
7755
|
const turns = rows.reduce((s, r) => s + r.turns, 0);
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7756
|
+
return { cost, turns };
|
|
7757
|
+
}
|
|
7758
|
+
function duration(seconds) {
|
|
7759
|
+
return seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`;
|
|
7726
7760
|
}
|
|
7727
7761
|
function counts(rows) {
|
|
7728
7762
|
return {
|
|
@@ -7809,9 +7843,8 @@ function ttyUI(out, opts) {
|
|
|
7809
7843
|
const c = counts(rows);
|
|
7810
7844
|
const doneCount = c.pass + c.fail + c.error;
|
|
7811
7845
|
const badge = finished ? `DONE · RUN ${finished.runId.slice(-4)}` : `RUNNING · ${doneCount}/${rows.length}`;
|
|
7812
|
-
const title = ` \uD83E\uDD8B firegoat eval · ${opts.project}`;
|
|
7813
7846
|
const out = [
|
|
7814
|
-
|
|
7847
|
+
...logoHeader(p, `firegoat eval · ${opts.project}`, p(finished ? C.green : C.ember, badge), w),
|
|
7815
7848
|
rule
|
|
7816
7849
|
];
|
|
7817
7850
|
if (authText)
|
|
@@ -7847,22 +7880,23 @@ function ttyUI(out, opts) {
|
|
|
7847
7880
|
if (!finished) {
|
|
7848
7881
|
const track = Math.max(10, w - 34);
|
|
7849
7882
|
const at = rows.length ? Math.round(doneCount / rows.length * (track - 1)) : 0;
|
|
7850
|
-
const
|
|
7851
|
-
out.push(` ${p(C.ember,
|
|
7883
|
+
const progress = `${"·".repeat(at)}▶${"·".repeat(Math.max(0, track - at - 1))}`;
|
|
7884
|
+
out.push(` ${p(C.ember, progress)} ${p(C.gray, `${doneCount}/${rows.length} · $${t.cost.toFixed(2)} so far`)}`);
|
|
7852
7885
|
return out;
|
|
7853
7886
|
}
|
|
7854
7887
|
const tr = tier(rows);
|
|
7855
7888
|
const goated = c.pass === rows.length && rows.length > 0;
|
|
7856
7889
|
const tally = `${c.pass} PASS · ${c.fail} FAIL · ${c.error} NOT GRADED`;
|
|
7857
|
-
const
|
|
7858
|
-
|
|
7859
|
-
out.push(p(C.
|
|
7890
|
+
const left = goated ? `${rows.length}/${rows.length} runs passed their checks` : tally;
|
|
7891
|
+
const right = goated ? `GOATED · tier ${tr}` : tr ? `tier ${tr}` : "";
|
|
7892
|
+
out.push(` ${goated ? p(C.green, left) : left}${" ".repeat(Math.max(2, w - left.length - right.length - 3))}${goated ? p(C.ember, right) : right}`);
|
|
7893
|
+
out.push(p(C.gray, ` total $${t.cost.toFixed(2)} · ${t.turns} turns · ${duration(finished.seconds)}`));
|
|
7860
7894
|
out.push(p(C.gray, ` runs ${relative2(process.cwd(), finished.runsDir)}/`));
|
|
7861
7895
|
out.push(` checks are a score, not the verdict: judge each end_goal from ${p(C.cyan, "<test>/<n>/transcript.txt")}`);
|
|
7862
7896
|
return out;
|
|
7863
7897
|
};
|
|
7864
7898
|
const rowsOf = (line) => {
|
|
7865
|
-
const visible = line.replace(ANSI, "")
|
|
7899
|
+
const visible = line.replace(ANSI, "");
|
|
7866
7900
|
return Math.max(1, Math.ceil(visible.length / (out.columns || 80)));
|
|
7867
7901
|
};
|
|
7868
7902
|
const render = () => {
|
|
@@ -7892,9 +7926,9 @@ function ttyUI(out, opts) {
|
|
|
7892
7926
|
render();
|
|
7893
7927
|
},
|
|
7894
7928
|
update: render,
|
|
7895
|
-
done: ({ runId, runsDir }) => {
|
|
7929
|
+
done: ({ runId, runsDir, seconds }) => {
|
|
7896
7930
|
clearInterval(timer);
|
|
7897
|
-
finished = { runId, runsDir };
|
|
7931
|
+
finished = { runId, runsDir, seconds };
|
|
7898
7932
|
render();
|
|
7899
7933
|
}
|
|
7900
7934
|
};
|
|
@@ -7929,6 +7963,7 @@ class UsageError extends Error {
|
|
|
7929
7963
|
function parseArgs(args) {
|
|
7930
7964
|
const opts = {
|
|
7931
7965
|
names: [],
|
|
7966
|
+
parallel: 4,
|
|
7932
7967
|
json: false,
|
|
7933
7968
|
vars: {},
|
|
7934
7969
|
claudeBin: process.env.FIREGOAT_CLAUDE_BIN || "claude"
|
|
@@ -7961,6 +7996,12 @@ function parseArgs(args) {
|
|
|
7961
7996
|
if (!/^\d+$/.test(v) || n < 1)
|
|
7962
7997
|
throw new UsageError(`--runs must be a positive integer, got ${v}`);
|
|
7963
7998
|
opts.runs = n;
|
|
7999
|
+
} else if (flag === "--parallel") {
|
|
8000
|
+
const v = value();
|
|
8001
|
+
const n = Number(v);
|
|
8002
|
+
if (!/^\d+$/.test(v) || n < 1)
|
|
8003
|
+
throw new UsageError(`--parallel must be a positive integer, got ${v}`);
|
|
8004
|
+
opts.parallel = n;
|
|
7964
8005
|
} else if (flag === "--claude-bin")
|
|
7965
8006
|
opts.claudeBin = value();
|
|
7966
8007
|
else if (a.startsWith("-"))
|
|
@@ -8147,6 +8188,7 @@ async function evalCommand(args, root) {
|
|
|
8147
8188
|
});
|
|
8148
8189
|
if (loginProblem)
|
|
8149
8190
|
return fatal(EXIT.notGraded, `not graded: ${loginProblem}`);
|
|
8191
|
+
const evalStarted = Date.now();
|
|
8150
8192
|
const runId = newRunId();
|
|
8151
8193
|
const runDir = join4(firegoatDir, "runs", runId);
|
|
8152
8194
|
const out = join4(runDir, "out");
|
|
@@ -8182,61 +8224,247 @@ async function evalCommand(args, root) {
|
|
|
8182
8224
|
return fatal(EXIT.notGraded, `not graded: ${error}`);
|
|
8183
8225
|
}
|
|
8184
8226
|
ui.runs(rows);
|
|
8185
|
-
const results =
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8227
|
+
const results = new Array(rows.length);
|
|
8228
|
+
const runAt = async (i) => {
|
|
8229
|
+
const row = rows[i];
|
|
8230
|
+
const test = tests.find((t) => t.name === row.test);
|
|
8231
|
+
row.state = "running";
|
|
8232
|
+
ui.update();
|
|
8233
|
+
const r = await runOne({
|
|
8234
|
+
onEvent: (e) => {
|
|
8235
|
+
if (e.type !== "assistant")
|
|
8236
|
+
return;
|
|
8237
|
+
row.turns++;
|
|
8238
|
+
const content = e.message?.content;
|
|
8239
|
+
if (!Array.isArray(content))
|
|
8240
|
+
return;
|
|
8241
|
+
for (const b of content) {
|
|
8242
|
+
const cmd = b.input?.command;
|
|
8243
|
+
if (b.type === "tool_use" && typeof cmd === "string")
|
|
8244
|
+
row.command = cmd.split(`
|
|
8203
8245
|
`)[0];
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8246
|
+
}
|
|
8247
|
+
},
|
|
8248
|
+
test,
|
|
8249
|
+
n: row.n,
|
|
8250
|
+
runId,
|
|
8251
|
+
runDir,
|
|
8252
|
+
claudeBin: claudePath,
|
|
8253
|
+
configDir,
|
|
8254
|
+
workdirSetup: config.workdirSetup,
|
|
8255
|
+
stepEnv,
|
|
8256
|
+
vars
|
|
8257
|
+
});
|
|
8258
|
+
results[i] = r;
|
|
8259
|
+
Object.assign(row, {
|
|
8260
|
+
state: r.status,
|
|
8261
|
+
turns: r.num_turns ?? row.turns,
|
|
8262
|
+
reason: r.reason,
|
|
8263
|
+
checks: r.checks,
|
|
8264
|
+
cost: r.cost,
|
|
8265
|
+
seconds: Math.round(r.duration_ms / 1000),
|
|
8266
|
+
command: undefined
|
|
8267
|
+
});
|
|
8268
|
+
ui.update();
|
|
8269
|
+
};
|
|
8270
|
+
let next = 0;
|
|
8271
|
+
const worker = async () => {
|
|
8272
|
+
while (next < rows.length)
|
|
8273
|
+
await runAt(next++);
|
|
8274
|
+
};
|
|
8275
|
+
await Promise.all(Array.from({ length: Math.min(opts.parallel, rows.length) }, worker));
|
|
8228
8276
|
const totals = { pass: 0, fail: 0, error: 0 };
|
|
8229
8277
|
for (const r of results)
|
|
8230
8278
|
totals[r.status]++;
|
|
8231
8279
|
const doc = { runId, runsDir: runDir, totals, runs: results };
|
|
8232
8280
|
writeFileSync3(join4(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
|
|
8233
8281
|
`);
|
|
8234
|
-
ui.done({
|
|
8282
|
+
ui.done({
|
|
8283
|
+
runId,
|
|
8284
|
+
runsDir: runDir,
|
|
8285
|
+
rows,
|
|
8286
|
+
seconds: Math.round((Date.now() - evalStarted) / 1000)
|
|
8287
|
+
});
|
|
8235
8288
|
if (opts.json)
|
|
8236
8289
|
console.log(JSON.stringify(doc, null, 2));
|
|
8237
8290
|
return totals.error > 0 ? EXIT.notGraded : totals.fail > 0 ? EXIT.fail : EXIT.pass;
|
|
8238
8291
|
}
|
|
8239
8292
|
|
|
8293
|
+
// src/feedback.ts
|
|
8294
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
8295
|
+
|
|
8296
|
+
// node_modules/@firegoat/feedback/dist/index.js
|
|
8297
|
+
var VERSION = "0.0.0";
|
|
8298
|
+
var DEFAULT_BASE_URL = "https://firegoat.ai/evals";
|
|
8299
|
+
var TIMEOUT_MS = 15000;
|
|
8300
|
+
function envBaseUrl() {
|
|
8301
|
+
return globalThis.process?.env?.FEEDBACK_API_URL;
|
|
8302
|
+
}
|
|
8303
|
+
|
|
8304
|
+
class FeedbackError extends Error {
|
|
8305
|
+
status;
|
|
8306
|
+
code;
|
|
8307
|
+
retryAfterSeconds;
|
|
8308
|
+
detected;
|
|
8309
|
+
constructor(status, code, message, retryAfterSeconds, detected) {
|
|
8310
|
+
super(message);
|
|
8311
|
+
this.status = status;
|
|
8312
|
+
this.code = code;
|
|
8313
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
8314
|
+
this.detected = detected;
|
|
8315
|
+
this.name = "FeedbackError";
|
|
8316
|
+
}
|
|
8317
|
+
}
|
|
8318
|
+
var TITLE_MIN = 8;
|
|
8319
|
+
var TITLE_MAX = 200;
|
|
8320
|
+
|
|
8321
|
+
class FeedbackClient {
|
|
8322
|
+
domain;
|
|
8323
|
+
baseUrl;
|
|
8324
|
+
token;
|
|
8325
|
+
fetchImpl;
|
|
8326
|
+
constructor(options) {
|
|
8327
|
+
if (!options.domain?.trim()) {
|
|
8328
|
+
throw new Error("FeedbackClient: `domain` is required (your product's domain, e.g. example.com)");
|
|
8329
|
+
}
|
|
8330
|
+
this.domain = options.domain.trim();
|
|
8331
|
+
this.baseUrl = (options.baseUrl ?? envBaseUrl() ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
8332
|
+
this.token = options.token;
|
|
8333
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
8334
|
+
}
|
|
8335
|
+
async registerDevice() {
|
|
8336
|
+
const data = await this.post("/api/feedback/device", undefined);
|
|
8337
|
+
if (typeof data.token !== "string" || !data.token) {
|
|
8338
|
+
throw new FeedbackError(201, "invalid_response", "server response missing token");
|
|
8339
|
+
}
|
|
8340
|
+
this.token = data.token;
|
|
8341
|
+
return data.token;
|
|
8342
|
+
}
|
|
8343
|
+
async send(input) {
|
|
8344
|
+
const title = input.title.trim();
|
|
8345
|
+
if (title.length < TITLE_MIN || title.length > TITLE_MAX) {
|
|
8346
|
+
throw new FeedbackError(400, "invalid_request", `title must be ${TITLE_MIN}-${TITLE_MAX} characters, got ${title.length}`);
|
|
8347
|
+
}
|
|
8348
|
+
const meta = {
|
|
8349
|
+
...input.meta,
|
|
8350
|
+
cliVersion: `@firegoat/feedback@${VERSION}`,
|
|
8351
|
+
feedbackType: input.type
|
|
8352
|
+
};
|
|
8353
|
+
const data = await this.post("/api/feedback", {
|
|
8354
|
+
domain: this.domain,
|
|
8355
|
+
title,
|
|
8356
|
+
body: input.body ?? "",
|
|
8357
|
+
meta,
|
|
8358
|
+
maskSecrets: input.maskSecrets ?? false
|
|
8359
|
+
});
|
|
8360
|
+
if (typeof data.id !== "string" || !data.id) {
|
|
8361
|
+
throw new FeedbackError(201, "invalid_response", "server response missing id");
|
|
8362
|
+
}
|
|
8363
|
+
return { id: data.id, similarCount: data.similarCount ?? 0 };
|
|
8364
|
+
}
|
|
8365
|
+
async post(path, body) {
|
|
8366
|
+
const headers = {
|
|
8367
|
+
"content-type": "application/json"
|
|
8368
|
+
};
|
|
8369
|
+
if (this.token)
|
|
8370
|
+
headers["x-feedback-token"] = this.token;
|
|
8371
|
+
const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
8372
|
+
method: "POST",
|
|
8373
|
+
headers,
|
|
8374
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
8375
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
8376
|
+
});
|
|
8377
|
+
const text = await res.text();
|
|
8378
|
+
let data;
|
|
8379
|
+
try {
|
|
8380
|
+
data = JSON.parse(text);
|
|
8381
|
+
} catch {
|
|
8382
|
+
throw new FeedbackError(res.status, "invalid_response", `non-JSON response (${res.status}): ${text.slice(0, 200)}`);
|
|
8383
|
+
}
|
|
8384
|
+
if (!res.ok) {
|
|
8385
|
+
throw new FeedbackError(res.status, String(data.error ?? "unknown_error"), String(data.message ?? `request failed with status ${res.status}`), typeof data.retryAfterSeconds === "number" ? data.retryAfterSeconds : undefined, Array.isArray(data.detected) ? data.detected : undefined);
|
|
8386
|
+
}
|
|
8387
|
+
return data;
|
|
8388
|
+
}
|
|
8389
|
+
}
|
|
8390
|
+
|
|
8391
|
+
// src/feedback.ts
|
|
8392
|
+
var TYPES = ["bug", "feature_request", "feedback"];
|
|
8393
|
+
function agentName() {
|
|
8394
|
+
if (process.env.CLAUDECODE)
|
|
8395
|
+
return "claude-code";
|
|
8396
|
+
if (process.env.CODEX_HOME || process.env.CODEX_SANDBOX)
|
|
8397
|
+
return "codex";
|
|
8398
|
+
if (process.env.CURSOR_TRACE_ID)
|
|
8399
|
+
return "cursor";
|
|
8400
|
+
return;
|
|
8401
|
+
}
|
|
8402
|
+
async function feedbackCommand(args) {
|
|
8403
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
8404
|
+
console.log(FEEDBACK_HELP);
|
|
8405
|
+
return args.length === 0 ? EXIT.usage : EXIT.pass;
|
|
8406
|
+
}
|
|
8407
|
+
const titleParts = [];
|
|
8408
|
+
let body;
|
|
8409
|
+
let type = "feedback";
|
|
8410
|
+
for (let i = 0;i < args.length; i++) {
|
|
8411
|
+
const a = args[i];
|
|
8412
|
+
const value = () => {
|
|
8413
|
+
const v = args[++i];
|
|
8414
|
+
if (v === undefined)
|
|
8415
|
+
throw new Error(`${a} needs a value`);
|
|
8416
|
+
return v;
|
|
8417
|
+
};
|
|
8418
|
+
try {
|
|
8419
|
+
if (a === "--body") {
|
|
8420
|
+
const v = value();
|
|
8421
|
+
body = v === "-" ? readFileSync2(0, "utf8") : v;
|
|
8422
|
+
} else if (a === "--type") {
|
|
8423
|
+
const v = value();
|
|
8424
|
+
if (!TYPES.includes(v))
|
|
8425
|
+
throw new Error(`--type must be one of ${TYPES.join(", ")}, got ${v}`);
|
|
8426
|
+
type = v;
|
|
8427
|
+
} else if (a.startsWith("-"))
|
|
8428
|
+
throw new Error(`unknown flag: ${a}`);
|
|
8429
|
+
else
|
|
8430
|
+
titleParts.push(a);
|
|
8431
|
+
} catch (e) {
|
|
8432
|
+
console.error(`firegoat feedback: ${e.message}`);
|
|
8433
|
+
return EXIT.usage;
|
|
8434
|
+
}
|
|
8435
|
+
}
|
|
8436
|
+
const title = titleParts.join(" ").trim();
|
|
8437
|
+
if (title.length < 8 || title.length > 200) {
|
|
8438
|
+
console.error(`firegoat feedback: the title must be 8-200 characters (got ${title.length})`);
|
|
8439
|
+
return EXIT.usage;
|
|
8440
|
+
}
|
|
8441
|
+
const client = new FeedbackClient({ domain: "firegoat.ai" });
|
|
8442
|
+
try {
|
|
8443
|
+
const agent = agentName();
|
|
8444
|
+
const { id, similarCount } = await client.send({
|
|
8445
|
+
title,
|
|
8446
|
+
body: body?.trim() ? `${body.trim()}
|
|
8447
|
+
|
|
8448
|
+
firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
|
|
8449
|
+
type,
|
|
8450
|
+
meta: {
|
|
8451
|
+
...agent ? { agent } : {},
|
|
8452
|
+
os: process.platform,
|
|
8453
|
+
nodeVersion: process.version
|
|
8454
|
+
}
|
|
8455
|
+
});
|
|
8456
|
+
console.log(`sent ${type} ${id} to the firegoat team${similarCount > 0 ? ` (${similarCount} similar report${similarCount === 1 ? "" : "s"} already)` : ""}. thanks.`);
|
|
8457
|
+
return EXIT.pass;
|
|
8458
|
+
} catch (e) {
|
|
8459
|
+
if (e instanceof FeedbackError) {
|
|
8460
|
+
const hint = e.code === "possible_secret" ? " Remove the secret from the title/body and send again." : e.code === "rate_limited" && e.retryAfterSeconds ? ` Try again in ${e.retryAfterSeconds}s.` : "";
|
|
8461
|
+
console.error(`firegoat feedback: ${e.code}: ${e.message}.${hint}`);
|
|
8462
|
+
return e.code === "invalid_request" || e.code === "possible_secret" ? EXIT.usage : EXIT.notGraded;
|
|
8463
|
+
}
|
|
8464
|
+
throw e;
|
|
8465
|
+
}
|
|
8466
|
+
}
|
|
8467
|
+
|
|
8240
8468
|
// src/init.ts
|
|
8241
8469
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
8242
8470
|
import { existsSync as existsSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
@@ -8356,9 +8584,8 @@ ${INIT_HELP}`);
|
|
|
8356
8584
|
const rule = paint("90", ` ${"─".repeat(width - 2)}`);
|
|
8357
8585
|
const step = (n, label, status) => console.log(` ${paint("32", "●")} ${paint("90", String(n).padStart(2, "0"))} ${label.padEnd(28)}${paint("90", status)}`);
|
|
8358
8586
|
if (interactive) {
|
|
8359
|
-
const
|
|
8360
|
-
|
|
8361
|
-
console.log(`${paint("38;5;208", " \uD83E\uDD8B")} firegoat init${" ".repeat(Math.max(2, width - title.length - tag.length - 2))}${paint("90", tag)}`);
|
|
8587
|
+
for (const line of logoHeader(paint, "firegoat init", paint("90", "Get agent-ready."), width))
|
|
8588
|
+
console.log(line);
|
|
8362
8589
|
console.log(rule);
|
|
8363
8590
|
}
|
|
8364
8591
|
ensureFiregoatDir(cwd);
|
|
@@ -8436,6 +8663,8 @@ async function main(argv) {
|
|
|
8436
8663
|
return initCommand(rest, process.cwd());
|
|
8437
8664
|
case "eval":
|
|
8438
8665
|
return evalCommand(rest, process.cwd());
|
|
8666
|
+
case "feedback":
|
|
8667
|
+
return feedbackCommand(rest);
|
|
8439
8668
|
default:
|
|
8440
8669
|
console.error(`unknown command: ${cmd}
|
|
8441
8670
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "firegoat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
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.0.0",
|
|
10
11
|
"@types/bun": "latest",
|
|
11
12
|
"typescript": "^7",
|
|
12
13
|
"yaml": "^2.9.1"
|