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.
Files changed (3) hide show
  1. package/README.md +11 -0
  2. package/dist/cli.js +291 -62
  3. 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.12.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].. [--json]
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
- const seconds = rows.reduce((s, r) => s + (r.seconds ?? 0), 0);
7724
- const time = seconds >= 60 ? `${Math.floor(seconds / 60)}m ${seconds % 60}s` : `${seconds}s`;
7725
- return { cost, turns, time };
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
- `${p(C.ember, " \uD83E\uDD8B")}${title.slice(3)}${" ".repeat(Math.max(1, w - title.length - badge.length - 1))}${p(finished ? C.green : C.ember, badge)}`,
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 goat = `${"·".repeat(at)}\uD83D\uDC10${"·".repeat(Math.max(0, track - at - 1))}`;
7851
- out.push(` ${p(C.ember, goat)} ${p(C.gray, `${doneCount}/${rows.length} · $${t.cost.toFixed(2)} so far`)}`);
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 right = goated ? p(C.ember, `\uD83D\uDC10 GOATED · tier ${tr}`) : tr ? `tier ${tr}` : "";
7858
- out.push(` ${goated ? p(C.green, `${rows.length}/${rows.length} runs passed their checks`) : tally}${" ".repeat(Math.max(2, w - tally.length - 22))}${right}`);
7859
- out.push(p(C.gray, ` total $${t.cost.toFixed(2)} · ${t.turns} turns · ${t.time}`));
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, "").replace(/🐐|🦋/g, "gg");
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
- for (const test of tests)
8187
- for (let n = 1;n <= runs; n++) {
8188
- const row = rows.find((x) => x.test === test.name && x.n === n);
8189
- row.state = "running";
8190
- ui.update();
8191
- const r = await runOne({
8192
- onEvent: (e) => {
8193
- if (e.type !== "assistant")
8194
- return;
8195
- row.turns++;
8196
- const content = e.message?.content;
8197
- if (!Array.isArray(content))
8198
- return;
8199
- for (const b of content) {
8200
- const cmd = b.input?.command;
8201
- if (b.type === "tool_use" && typeof cmd === "string")
8202
- row.command = cmd.split(`
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
- test,
8207
- n,
8208
- runId,
8209
- runDir,
8210
- claudeBin: claudePath,
8211
- configDir,
8212
- workdirSetup: config.workdirSetup,
8213
- stepEnv,
8214
- vars
8215
- });
8216
- results.push(r);
8217
- Object.assign(row, {
8218
- state: r.status,
8219
- turns: r.num_turns ?? row.turns,
8220
- reason: r.reason,
8221
- checks: r.checks,
8222
- cost: r.cost,
8223
- seconds: Math.round(r.duration_ms / 1000),
8224
- command: undefined
8225
- });
8226
- ui.update();
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({ runId, runsDir: runDir, rows });
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 title = " \uD83E\uDD8B firegoat init";
8360
- const tag = "Get agent-ready.";
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.12.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"