firegoat 0.22.0 → 0.23.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 +22 -251
  2. package/dist/cli.js +587 -348
  3. package/package.json +1 -1
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.22.0",
6950
+ version: "0.23.0",
6951
6951
  repository: {
6952
6952
  type: "git",
6953
6953
  url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
@@ -7325,210 +7325,10 @@ import { randomBytes as randomBytes2 } from "node:crypto";
7325
7325
  import {
7326
7326
  appendFileSync,
7327
7327
  createWriteStream as createWriteStream2,
7328
- existsSync as existsSync3,
7329
7328
  mkdirSync as mkdirSync4,
7330
- readdirSync as readdirSync2,
7331
7329
  writeFileSync as writeFileSync4
7332
7330
  } from "node:fs";
7333
- import { basename as basename3, delimiter as delimiter2, join as join6 } from "node:path";
7334
-
7335
- // src/claude.ts
7336
- import { spawn, spawnSync } from "node:child_process";
7337
- var CLAUDE_FLAGS = [
7338
- "--output-format",
7339
- "stream-json",
7340
- "--verbose",
7341
- "--dangerously-skip-permissions",
7342
- "--strict-mcp-config",
7343
- "--setting-sources",
7344
- "",
7345
- "--disable-slash-commands"
7346
- ];
7347
- var NPM_INJECTED = /^(npm_config_|npm_lifecycle_|npm_package_)|^(npm_execpath|npm_node_execpath|npm_command|INIT_CWD)$/;
7348
- var CLAUDE_SESSION = /^(CLAUDECODE|CLAUDE_CODE_.+|CLAUDE_PID|CLAUDE_ENV_FILE|CLAUDE_SESSION_ID|CLAUDE_PROJECT_DIR|CLAUDE_EFFORT|CLAUDE_AGENT_SDK_.+|CLAUDE_PLUGIN_ROOT|CLAUDE_PLUGIN_DATA|CLAUDE_SKILL_DIR)$/;
7349
- var CLAUDE_AUTH = /^CLAUDE_CODE_(OAUTH_TOKEN|OAUTH_REFRESH_TOKEN|USE_[A-Z_]+|SKIP_[A-Z]+_AUTH|CLIENT_CERT|CLIENT_KEY|CLIENT_KEY_PASSPHRASE)$/;
7350
- function hostEnv() {
7351
- return Object.fromEntries(Object.entries(process.env).filter(([k]) => !NPM_INJECTED.test(k) && !k.startsWith("FIREGOAT_") && (!CLAUDE_SESSION.test(k) || CLAUDE_AUTH.test(k))));
7352
- }
7353
- function claudeEnv(configDir, env = {}, envFile) {
7354
- return {
7355
- ...hostEnv(),
7356
- ...env,
7357
- CLAUDE_CONFIG_DIR: configDir,
7358
- CLAUDE_SECURESTORAGE_CONFIG_DIR: "",
7359
- ...envFile === undefined ? {} : { CLAUDE_ENV_FILE: envFile }
7360
- };
7361
- }
7362
- function run(opts) {
7363
- return new Promise((resolve) => {
7364
- const child = spawn(opts.cmd, opts.args, {
7365
- cwd: opts.cwd,
7366
- env: opts.env,
7367
- stdio: ["ignore", "pipe", "pipe"]
7368
- });
7369
- let timedOut = false;
7370
- let settled = false;
7371
- const timer = setTimeout(() => {
7372
- timedOut = true;
7373
- child.kill("SIGKILL");
7374
- }, opts.timeoutMs);
7375
- const done = (r) => {
7376
- if (settled)
7377
- return;
7378
- settled = true;
7379
- clearTimeout(timer);
7380
- resolve(r);
7381
- };
7382
- child.stdout.on("data", opts.onStdout);
7383
- child.stderr.on("data", opts.onStderr);
7384
- child.on("error", (error) => done({ code: null, timedOut, error }));
7385
- child.on("close", (code) => done({ code, timedOut }));
7386
- child.on("exit", (code) => setTimeout(() => {
7387
- child.stdout.destroy();
7388
- child.stderr.destroy();
7389
- done({ code, timedOut });
7390
- }, 2000).unref());
7391
- });
7392
- }
7393
- async function runClaude(opts) {
7394
- let stdout = "";
7395
- let pending = "";
7396
- const exited = await run({
7397
- cmd: opts.bin,
7398
- args: [
7399
- "-p",
7400
- opts.prompt,
7401
- ...CLAUDE_FLAGS,
7402
- ...opts.model === undefined ? [] : ["--model", opts.model],
7403
- ...opts.extraArgs
7404
- ],
7405
- cwd: opts.cwd,
7406
- env: opts.env,
7407
- timeoutMs: opts.timeoutMs,
7408
- onStdout: (c) => {
7409
- const text = c.toString();
7410
- stdout += text;
7411
- if (!opts.onEvent)
7412
- return;
7413
- pending += text;
7414
- const lines = pending.split(`
7415
- `);
7416
- pending = lines.pop() ?? "";
7417
- for (const line of lines) {
7418
- try {
7419
- opts.onEvent(JSON.parse(line));
7420
- } catch {}
7421
- }
7422
- },
7423
- onStderr: (c) => opts.stderr.write(c)
7424
- });
7425
- if (exited.error)
7426
- return {
7427
- events: [],
7428
- claudeModel: null,
7429
- ok: false,
7430
- reason: `could not start ${opts.bin}: ${exited.error.message}`
7431
- };
7432
- const events = stdout.split(`
7433
- `).filter((l) => l.trim() !== "").map((l) => {
7434
- try {
7435
- return JSON.parse(l);
7436
- } catch {
7437
- return;
7438
- }
7439
- });
7440
- const init = events.find((e) => e?.type === "system" && e.subtype === "init");
7441
- if (!init) {
7442
- const said = stdout.trim().split(`
7443
- `)[0]?.slice(0, 300);
7444
- return {
7445
- events,
7446
- claudeModel: null,
7447
- ok: false,
7448
- reason: `claude did not start a session (exit ${exited.code ?? "by signal"})${said ? `: ${said}` : ", see stderr.txt"}`
7449
- };
7450
- }
7451
- const claudeModel = typeof init.model === "string" ? init.model : null;
7452
- if (typeof init.session_id !== "string" || init.session_id === "")
7453
- return {
7454
- events,
7455
- claudeModel,
7456
- ok: false,
7457
- reason: "claude's init line has no session_id"
7458
- };
7459
- const sessionId = init.session_id;
7460
- const result = events.findLast((e) => e?.type === "result");
7461
- if (exited.timedOut)
7462
- return {
7463
- events,
7464
- claudeModel,
7465
- ok: false,
7466
- reason: `timed out after ${opts.timeoutMs / 1000}s`,
7467
- result,
7468
- sessionId
7469
- };
7470
- if (!result)
7471
- return {
7472
- events,
7473
- claudeModel,
7474
- ok: false,
7475
- reason: `claude exited ${exited.code ?? "by signal"} without a result, see stderr.txt`,
7476
- sessionId
7477
- };
7478
- if (result.is_error === true || result.subtype !== "success")
7479
- return {
7480
- events,
7481
- claudeModel,
7482
- ok: false,
7483
- reason: `claude result is an error (subtype=${result.subtype})${result.result ? `: ${result.result.slice(0, 300)}` : ""}`,
7484
- result,
7485
- sessionId
7486
- };
7487
- return { events, claudeModel, ok: true, result, sessionId };
7488
- }
7489
- async function runShell(opts) {
7490
- opts.log.write(`$ ${opts.run}
7491
- `);
7492
- const r = await run({
7493
- cmd: "/bin/sh",
7494
- args: ["-c", opts.run],
7495
- cwd: opts.cwd,
7496
- env: opts.env,
7497
- timeoutMs: opts.timeoutMs,
7498
- onStdout: (c) => opts.log.write(c),
7499
- onStderr: (c) => opts.log.write(c)
7500
- });
7501
- let error;
7502
- if (r.error)
7503
- error = `could not start /bin/sh: ${r.error.message}`;
7504
- else if (r.timedOut)
7505
- error = `timed out after ${opts.timeoutMs / 1000}s`;
7506
- else if (r.code === null)
7507
- error = "killed by a signal";
7508
- opts.log.write(`[exit ${r.code ?? "none"}${error ? `: ${error}` : ""}]
7509
-
7510
- `);
7511
- return error ? { run: opts.run, code: r.code, error } : { run: opts.run, code: r.code };
7512
- }
7513
- function checkLogin(opts) {
7514
- const r = spawnSync(opts.bin, ["auth", "status"], {
7515
- cwd: opts.cwd,
7516
- env: opts.env,
7517
- encoding: "utf8",
7518
- timeout: 30000
7519
- });
7520
- if (r.error)
7521
- return `could not run ${opts.bin}: ${r.error.message}`;
7522
- let status;
7523
- try {
7524
- status = JSON.parse(r.stdout);
7525
- } catch {}
7526
- if (status?.loggedIn === true)
7527
- return;
7528
- if (status?.loggedIn === false)
7529
- return "claude is not logged in. Run `claude` once and log in (or set ANTHROPIC_API_KEY), then run eval again";
7530
- return `claude auth status gave no login state (exit ${r.status}): ${(r.stdout + r.stderr).trim().slice(0, 300)}`;
7531
- }
7331
+ import { basename as basename3, delimiter as delimiter2, join as join7 } from "node:path";
7532
7332
 
7533
7333
  // src/credentials.ts
7534
7334
  import { randomBytes } from "node:crypto";
@@ -7559,6 +7359,9 @@ class ApiError extends Error {
7559
7359
  this.status = status;
7560
7360
  }
7561
7361
  }
7362
+
7363
+ class NetworkError extends ApiError {
7364
+ }
7562
7365
  function printable(s, max = 300) {
7563
7366
  const t = s.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
7564
7367
  return t.length > max ? `${t.slice(0, max - 1)}…` : t;
@@ -7639,7 +7442,7 @@ async function send(opts) {
7639
7442
  status = res.status;
7640
7443
  text = await readCapped(res);
7641
7444
  } catch (e) {
7642
- throw new ApiError(`${opts.step}: ${networkReason(e, opts.timeoutMs)}`, null);
7445
+ throw new NetworkError(`${opts.step}: ${networkReason(e, opts.timeoutMs)}`);
7643
7446
  }
7644
7447
  let json = null;
7645
7448
  try {
@@ -7833,10 +7636,76 @@ function ensureFiregoatDir(root) {
7833
7636
  }
7834
7637
 
7835
7638
  // src/git.ts
7836
- import { spawnSync as spawnSync2 } from "node:child_process";
7639
+ import { spawnSync } from "node:child_process";
7837
7640
  import { basename, dirname } from "node:path";
7641
+
7642
+ // src/process.ts
7643
+ import { spawn } from "node:child_process";
7644
+ var NPM_INJECTED = /^(npm_config_|npm_lifecycle_|npm_package_)|^(npm_execpath|npm_node_execpath|npm_command|INIT_CWD)$/;
7645
+ var CLAUDE_SESSION = /^(CLAUDECODE|CLAUDE_CODE_.+|CLAUDE_PID|CLAUDE_ENV_FILE|CLAUDE_SESSION_ID|CLAUDE_PROJECT_DIR|CLAUDE_EFFORT|CLAUDE_AGENT_SDK_.+|CLAUDE_PLUGIN_ROOT|CLAUDE_PLUGIN_DATA|CLAUDE_SKILL_DIR)$/;
7646
+ var CLAUDE_AUTH = /^CLAUDE_CODE_(OAUTH_TOKEN|OAUTH_REFRESH_TOKEN|USE_[A-Z_]+|SKIP_[A-Z]+_AUTH|CLIENT_CERT|CLIENT_KEY|CLIENT_KEY_PASSPHRASE)$/;
7647
+ function hostEnv() {
7648
+ return Object.fromEntries(Object.entries(process.env).filter(([k]) => !NPM_INJECTED.test(k) && !k.startsWith("FIREGOAT_") && (!CLAUDE_SESSION.test(k) || CLAUDE_AUTH.test(k))));
7649
+ }
7650
+ function run(opts) {
7651
+ return new Promise((resolve) => {
7652
+ const child = spawn(opts.cmd, opts.args, {
7653
+ cwd: opts.cwd,
7654
+ env: opts.env,
7655
+ stdio: ["ignore", "pipe", "pipe"]
7656
+ });
7657
+ let timedOut = false;
7658
+ let settled = false;
7659
+ const timer = setTimeout(() => {
7660
+ timedOut = true;
7661
+ child.kill("SIGKILL");
7662
+ }, opts.timeoutMs);
7663
+ const done = (r) => {
7664
+ if (settled)
7665
+ return;
7666
+ settled = true;
7667
+ clearTimeout(timer);
7668
+ resolve(r);
7669
+ };
7670
+ child.stdout.on("data", opts.onStdout);
7671
+ child.stderr.on("data", opts.onStderr);
7672
+ child.on("error", (error) => done({ code: null, timedOut, error }));
7673
+ child.on("close", (code) => done({ code, timedOut }));
7674
+ child.on("exit", (code) => setTimeout(() => {
7675
+ child.stdout.destroy();
7676
+ child.stderr.destroy();
7677
+ done({ code, timedOut });
7678
+ }, 2000).unref());
7679
+ });
7680
+ }
7681
+ async function runShell(opts) {
7682
+ opts.log.write(`$ ${opts.run}
7683
+ `);
7684
+ const r = await run({
7685
+ cmd: "/bin/sh",
7686
+ args: ["-c", opts.run],
7687
+ cwd: opts.cwd,
7688
+ env: opts.env,
7689
+ timeoutMs: opts.timeoutMs,
7690
+ onStdout: (c) => opts.log.write(c),
7691
+ onStderr: (c) => opts.log.write(c)
7692
+ });
7693
+ let error;
7694
+ if (r.error)
7695
+ error = `could not start /bin/sh: ${r.error.message}`;
7696
+ else if (r.timedOut)
7697
+ error = `timed out after ${opts.timeoutMs / 1000}s`;
7698
+ else if (r.code === null)
7699
+ error = "killed by a signal";
7700
+ opts.log.write(`[exit ${r.code ?? "none"}${error ? `: ${error}` : ""}]
7701
+
7702
+ `);
7703
+ return error ? { run: opts.run, code: r.code, error } : { run: opts.run, code: r.code };
7704
+ }
7705
+
7706
+ // src/git.ts
7838
7707
  function git(dir, args) {
7839
- const r = spawnSync2("git", args, {
7708
+ const r = spawnSync("git", args, {
7840
7709
  cwd: dir,
7841
7710
  env: hostEnv(),
7842
7711
  encoding: "utf8"
@@ -7846,7 +7715,7 @@ function git(dir, args) {
7846
7715
  return { code: r.status ?? -1, stdout: r.stdout.trim() };
7847
7716
  }
7848
7717
  function gitRepo(dir) {
7849
- const r = spawnSync2("git", [
7718
+ const r = spawnSync("git", [
7850
7719
  "rev-parse",
7851
7720
  "--path-format=absolute",
7852
7721
  "--git-dir",
@@ -7888,6 +7757,249 @@ function gitRepo(dir) {
7888
7757
  };
7889
7758
  }
7890
7759
 
7760
+ // src/harness.ts
7761
+ async function runAgent(harness, opts) {
7762
+ let stdout = "";
7763
+ let pending = "";
7764
+ const exited = await run({
7765
+ cmd: opts.bin,
7766
+ args: harness.args(opts),
7767
+ cwd: opts.cwd,
7768
+ env: opts.env,
7769
+ timeoutMs: opts.timeoutMs,
7770
+ onStdout: (c) => {
7771
+ const text = c.toString();
7772
+ stdout += text;
7773
+ if (!opts.onProgress)
7774
+ return;
7775
+ pending += text;
7776
+ const lines = pending.split(`
7777
+ `);
7778
+ pending = lines.pop() ?? "";
7779
+ for (const line of lines) {
7780
+ const p = harness.progress(line);
7781
+ if (p)
7782
+ opts.onProgress(p);
7783
+ }
7784
+ },
7785
+ onStderr: (c) => opts.stderr.write(c)
7786
+ });
7787
+ if (exited.error)
7788
+ return {
7789
+ ok: false,
7790
+ reason: `could not start ${opts.bin}: ${exited.error.message}`,
7791
+ steps: [],
7792
+ final: "",
7793
+ reportedModel: null,
7794
+ version: null,
7795
+ turns: null,
7796
+ cost: null
7797
+ };
7798
+ return harness.read(stdout, exited, opts.timeoutMs);
7799
+ }
7800
+
7801
+ // src/harnesses/claude.ts
7802
+ import { spawnSync as spawnSync2 } from "node:child_process";
7803
+ import { existsSync as existsSync2, readdirSync } from "node:fs";
7804
+ import { join as join3 } from "node:path";
7805
+ var FLAGS = [
7806
+ "--output-format",
7807
+ "stream-json",
7808
+ "--verbose",
7809
+ "--dangerously-skip-permissions",
7810
+ "--strict-mcp-config",
7811
+ "--setting-sources",
7812
+ "",
7813
+ "--disable-slash-commands"
7814
+ ];
7815
+ function parse(line) {
7816
+ try {
7817
+ const e = JSON.parse(line);
7818
+ return typeof e === "object" && e !== null ? e : undefined;
7819
+ } catch {
7820
+ return;
7821
+ }
7822
+ }
7823
+ function content(e) {
7824
+ const message = e?.message;
7825
+ return Array.isArray(message?.content) ? message.content : [];
7826
+ }
7827
+ function resultText(content) {
7828
+ if (typeof content === "string")
7829
+ return content;
7830
+ if (Array.isArray(content))
7831
+ return content.map((c) => typeof c.text === "string" ? c.text : `[${c.type}]`).join(`
7832
+ `);
7833
+ return "";
7834
+ }
7835
+ function steps(events) {
7836
+ const out = [];
7837
+ for (const e of events) {
7838
+ if (e?.type === "assistant")
7839
+ for (const b of content(e)) {
7840
+ if (b.type === "tool_use")
7841
+ out.push({
7842
+ kind: "tool_call",
7843
+ name: typeof b.name === "string" ? b.name : "",
7844
+ input: typeof b.input === "object" && b.input !== null ? b.input : null
7845
+ });
7846
+ else if (b.type === "text" && typeof b.text === "string")
7847
+ out.push({ kind: "text", text: b.text });
7848
+ }
7849
+ else if (e?.type === "user") {
7850
+ for (const b of content(e))
7851
+ if (b.type === "tool_result")
7852
+ out.push({
7853
+ kind: "tool_result",
7854
+ isError: b.is_error === true,
7855
+ text: resultText(b.content).trimEnd()
7856
+ });
7857
+ }
7858
+ }
7859
+ return out;
7860
+ }
7861
+ function read(stdout, exited, timeoutMs) {
7862
+ const events = stdout.split(`
7863
+ `).filter((l) => l.trim() !== "").map(parse);
7864
+ let final = "";
7865
+ for (const e of events)
7866
+ if (e?.type === "result" && typeof e.result === "string")
7867
+ final = e.result;
7868
+ const result = events.findLast((e) => e?.type === "result");
7869
+ const trace = {
7870
+ steps: steps(events),
7871
+ final,
7872
+ turns: result?.num_turns ?? null,
7873
+ cost: result?.total_cost_usd ?? null
7874
+ };
7875
+ const init = events.find((e) => e?.type === "system" && e.subtype === "init");
7876
+ if (!init) {
7877
+ const said = stdout.trim().split(`
7878
+ `)[0]?.slice(0, 300);
7879
+ return {
7880
+ ...trace,
7881
+ reportedModel: null,
7882
+ version: null,
7883
+ ok: false,
7884
+ reason: `claude did not start a session (exit ${exited.code ?? "by signal"})${said ? `: ${said}` : ", see stderr.txt"}`
7885
+ };
7886
+ }
7887
+ const reportedModel = typeof init.model === "string" ? init.model : null;
7888
+ const version = typeof init.claude_code_version === "string" ? init.claude_code_version : null;
7889
+ if (typeof init.session_id !== "string" || init.session_id === "")
7890
+ return {
7891
+ ...trace,
7892
+ reportedModel,
7893
+ version,
7894
+ ok: false,
7895
+ reason: "claude's init line has no session_id"
7896
+ };
7897
+ const sessionId = init.session_id;
7898
+ if (exited.timedOut)
7899
+ return {
7900
+ ...trace,
7901
+ reportedModel,
7902
+ version,
7903
+ ok: false,
7904
+ reason: `timed out after ${timeoutMs / 1000}s`,
7905
+ sessionId
7906
+ };
7907
+ if (!result)
7908
+ return {
7909
+ ...trace,
7910
+ reportedModel,
7911
+ version,
7912
+ ok: false,
7913
+ reason: `claude exited ${exited.code ?? "by signal"} without a result, see stderr.txt`,
7914
+ sessionId
7915
+ };
7916
+ if (result.is_error === true || result.subtype !== "success")
7917
+ return {
7918
+ ...trace,
7919
+ reportedModel,
7920
+ version,
7921
+ ok: false,
7922
+ reason: `claude result is an error (subtype=${result.subtype})${result.result ? `: ${result.result.slice(0, 300)}` : ""}`,
7923
+ sessionId
7924
+ };
7925
+ return { ...trace, reportedModel, version, ok: true, sessionId };
7926
+ }
7927
+ function progress(line) {
7928
+ const e = parse(line);
7929
+ if (e?.type !== "assistant")
7930
+ return;
7931
+ const p = {};
7932
+ for (const b of content(e)) {
7933
+ const cmd = b.input?.command;
7934
+ if (b.type === "tool_use" && typeof cmd === "string")
7935
+ p.command = cmd.split(`
7936
+ `)[0];
7937
+ }
7938
+ return p;
7939
+ }
7940
+ function checkLogin(opts) {
7941
+ const r = spawnSync2(opts.bin, ["auth", "status"], {
7942
+ cwd: opts.cwd,
7943
+ env: opts.env,
7944
+ encoding: "utf8",
7945
+ timeout: 30000
7946
+ });
7947
+ if (r.error)
7948
+ return {
7949
+ loggedOut: false,
7950
+ reason: `could not run ${opts.bin}: ${r.error.message}`
7951
+ };
7952
+ let status;
7953
+ try {
7954
+ status = JSON.parse(r.stdout);
7955
+ } catch {}
7956
+ if (status?.loggedIn === true)
7957
+ return;
7958
+ if (status?.loggedIn === false)
7959
+ return {
7960
+ loggedOut: true,
7961
+ reason: "claude is not logged in. Run `claude` once and log in (or set ANTHROPIC_API_KEY), then run eval again"
7962
+ };
7963
+ return {
7964
+ loggedOut: false,
7965
+ reason: `claude auth status gave no login state (exit ${r.status}): ${(r.stdout + r.stderr).trim().slice(0, 300)}`
7966
+ };
7967
+ }
7968
+ var claude = {
7969
+ name: "claude",
7970
+ bin: "claude",
7971
+ installHint: "Install Claude Code (https://claude.com/claude-code) or pass --claude-bin PATH.",
7972
+ configDirName: ".claude",
7973
+ args: ({ prompt, model, extraArgs }) => [
7974
+ "-p",
7975
+ prompt,
7976
+ ...FLAGS,
7977
+ ...model === undefined ? [] : ["--model", model],
7978
+ ...extraArgs
7979
+ ],
7980
+ env: ({ configDir, homeEnvFile }) => ({
7981
+ CLAUDE_CONFIG_DIR: configDir,
7982
+ CLAUDE_SECURESTORAGE_CONFIG_DIR: "",
7983
+ ...homeEnvFile === undefined ? {} : { CLAUDE_ENV_FILE: homeEnvFile }
7984
+ }),
7985
+ authLabel: () => process.env.ANTHROPIC_API_KEY ? "ANTHROPIC_API_KEY (claude bills the API key; unset it to use your claude login)" : "your claude login",
7986
+ checkLogin,
7987
+ progress,
7988
+ read,
7989
+ sessionsDir: (configDir) => join3(configDir, "projects"),
7990
+ findSession(configDir, sessionId) {
7991
+ const projects = join3(configDir, "projects");
7992
+ if (!existsSync2(projects))
7993
+ return null;
7994
+ for (const d of readdirSync(projects)) {
7995
+ const f = join3(projects, d, `${sessionId}.jsonl`);
7996
+ if (existsSync2(f))
7997
+ return f;
7998
+ }
7999
+ return null;
8000
+ }
8001
+ };
8002
+
7891
8003
  // src/help.ts
7892
8004
  var EXIT_CODES = `Exit codes:
7893
8005
  0 Every run's script checks passed
@@ -7921,6 +8033,11 @@ Typical flow for a coding agent driving firegoat:
7921
8033
 
7922
8034
  Run firegoat <command> --help for details and examples.
7923
8035
 
8036
+ Telemetry: init, eval, login, logout and feedback each send firegoat.ai one
8037
+ anonymous event (command, exit code, error code, duration, versions, OS, run
8038
+ counts; never prompts, paths, names or env). FIREGOAT_TELEMETRY=0 or DO_NOT_TRACK=1
8039
+ turns it off; FIREGOAT_TELEMETRY accepts only 0 or 1.
8040
+
7924
8041
  ${EXIT_CODES}`;
7925
8042
  }
7926
8043
  var INIT_HELP = `Usage: firegoat init [--force] [--local-skills | --no-skills] [--no-login]
@@ -8060,6 +8177,9 @@ Example:
8060
8177
  end_goal: .env holds a key copied from the dashboard, not a made-up one.
8061
8178
  claude_args: [--chrome]
8062
8179
 
8180
+ init also says that firegoat sends anonymous usage stats; FIREGOAT_TELEMETRY=0
8181
+ (or DO_NOT_TRACK=1) turns them off. Fields: firegoat --help, Telemetry.
8182
+
8063
8183
  Exit codes: 0 done, 1 the firegoat login failed (the files stay), 2 the skill
8064
8184
  install failed, 3 usage error.`;
8065
8185
  var LOGIN_HELP = `Usage: firegoat login
@@ -8083,6 +8203,9 @@ Env:
8083
8203
  saved login
8084
8204
  FIREGOAT_BASE_URL Default https://firegoat.ai/evals; https only (http only
8085
8205
  for localhost / 127.0.0.1)
8206
+ FIREGOAT_TELEMETRY 0 turns off the anonymous usage event each command sends
8207
+ (1 or unset: on; anything else is an error). DO_NOT_TRACK=1
8208
+ turns it off too. With a key, the event carries it
8086
8209
 
8087
8210
  Exit codes: 0 logged in, 1 denied, expired, or firegoat.ai failed or unreachable,
8088
8211
  3 usage error (unknown flag, bad FIREGOAT_BASE_URL, ~/.firegoat a symlink, not a
@@ -8246,23 +8369,23 @@ Exit codes: 0 sent, 2 the service failed (or rate limit), 3 usage error.`;
8246
8369
 
8247
8370
  // src/home.ts
8248
8371
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "node:fs";
8249
- import { join as join3 } from "node:path";
8372
+ import { join as join4 } from "node:path";
8250
8373
  function homeEnv(home) {
8251
8374
  return {
8252
8375
  HOME: home,
8253
- XDG_CONFIG_HOME: join3(home, ".config"),
8254
- XDG_CACHE_HOME: join3(home, ".cache"),
8255
- XDG_DATA_HOME: join3(home, ".local", "share"),
8256
- XDG_STATE_HOME: join3(home, ".local", "state")
8376
+ XDG_CONFIG_HOME: join4(home, ".config"),
8377
+ XDG_CACHE_HOME: join4(home, ".cache"),
8378
+ XDG_DATA_HOME: join4(home, ".local", "share"),
8379
+ XDG_STATE_HOME: join4(home, ".local", "state")
8257
8380
  };
8258
8381
  }
8259
8382
  var shellQuote = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
8260
8383
  function makeRunHome(dir) {
8261
- const home = join3(dir, "home");
8384
+ const home = join4(dir, "home");
8262
8385
  const env = homeEnv(home);
8263
8386
  for (const d of Object.values(env))
8264
8387
  mkdirSync3(d, { recursive: true });
8265
- const envFile = join3(dir, "home.env");
8388
+ const envFile = join4(dir, "home.env");
8266
8389
  writeFileSync2(envFile, Object.entries(env).map(([k, v]) => `export ${k}=${shellQuote(v)}
8267
8390
  `).join(""));
8268
8391
  return { home, envFile };
@@ -8308,9 +8431,102 @@ async function runAllSteps(opts) {
8308
8431
  }
8309
8432
  }
8310
8433
 
8434
+ // src/telemetry.ts
8435
+ var TIMEOUT_MS = 2000;
8436
+ var TELEMETRY_NOTICE = "firegoat sends anonymous usage stats (no prompts, paths or code); FIREGOAT_TELEMETRY=0 turns it off.";
8437
+ var state = {
8438
+ errorCode: null,
8439
+ eval: {
8440
+ harness: null,
8441
+ claudeVersion: null,
8442
+ testCount: null,
8443
+ runCount: null,
8444
+ passCount: null,
8445
+ failCount: null,
8446
+ notGradedCount: null,
8447
+ modelCount: null,
8448
+ parallel: null,
8449
+ uploaded: null,
8450
+ uploadFailed: null
8451
+ },
8452
+ skills: null
8453
+ };
8454
+ function failWith(code) {
8455
+ state.errorCode = code;
8456
+ }
8457
+ function claudeVersion(v) {
8458
+ return typeof v === "string" && /^[\x20-\x7e]{1,64}$/.test(v) ? v : null;
8459
+ }
8460
+ function evalStats(fields) {
8461
+ Object.assign(state.eval, fields);
8462
+ }
8463
+ function initSkills(scope) {
8464
+ state.skills = scope;
8465
+ }
8466
+ function flagSet(v) {
8467
+ return v !== undefined && v !== "" && v !== "0" && v !== "false";
8468
+ }
8469
+ function telemetryEnabled() {
8470
+ const v = process.env.FIREGOAT_TELEMETRY;
8471
+ if (v !== undefined && v !== "0" && v !== "1")
8472
+ throw new ConfigError(`FIREGOAT_TELEMETRY must be 0 (off) or 1 (on), got ${JSON.stringify(v)}`);
8473
+ return v !== "0" && !flagSet(process.env.DO_NOT_TRACK);
8474
+ }
8475
+ var COMMANDS = new Set(["init", "eval", "login", "logout", "feedback"]);
8476
+ function telemetryEvent(argv) {
8477
+ const [cmd, ...rest] = argv;
8478
+ if (cmd === undefined || !COMMANDS.has(cmd))
8479
+ return null;
8480
+ if (rest.includes("--help") || rest.includes("-h"))
8481
+ return null;
8482
+ if (cmd === "eval" && rest.includes("--dry-run"))
8483
+ return "dry-run";
8484
+ return cmd;
8485
+ }
8486
+ function errorCode(exitCode) {
8487
+ if (exitCode === EXIT.pass)
8488
+ return null;
8489
+ return state.errorCode ?? (exitCode === EXIT.usage ? "usage" : "unknown");
8490
+ }
8491
+ async function sendTelemetry(event, exitCode, startedAt) {
8492
+ try {
8493
+ const base = baseUrl();
8494
+ let key = null;
8495
+ try {
8496
+ key = resolveKey(base)?.key ?? null;
8497
+ } catch {}
8498
+ const [major, minor] = process.versions.node.split(".");
8499
+ const body = {
8500
+ event,
8501
+ ok: exitCode === EXIT.pass,
8502
+ exitCode,
8503
+ errorCode: errorCode(exitCode),
8504
+ durationMs: Math.round(Date.now() - startedAt),
8505
+ cliVersion: package_default.version,
8506
+ os: process.platform,
8507
+ arch: process.arch,
8508
+ node: `${major}.${minor}`,
8509
+ ci: flagSet(process.env.CI),
8510
+ ...event === "eval" ? state.eval : {},
8511
+ ...event === "init" ? { skills: state.skills, loggedIn: key !== null } : {}
8512
+ };
8513
+ await send({
8514
+ step: "POST /api/cli/events",
8515
+ url: new URL(`${base}/api/cli/events`),
8516
+ method: "POST",
8517
+ headers: {
8518
+ "Content-Type": "application/json",
8519
+ ...key === null ? {} : { Authorization: `Bearer ${key}` }
8520
+ },
8521
+ body: JSON.stringify(body),
8522
+ timeoutMs: TIMEOUT_MS
8523
+ });
8524
+ } catch {}
8525
+ }
8526
+
8311
8527
  // src/transcript.ts
8312
- import { readdirSync, statSync, writeFileSync as writeFileSync3 } from "node:fs";
8313
- import { join as join4, relative } from "node:path";
8528
+ import { readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync3 } from "node:fs";
8529
+ import { join as join5, relative } from "node:path";
8314
8530
  var OUTPUT_LINES = 15;
8315
8531
  var OUTPUT_CHARS = 1500;
8316
8532
  var TEXT_CHARS = 1500;
@@ -8341,28 +8557,20 @@ function describeCall(step, name, input) {
8341
8557
  return `[${step}] ${name} ${input[key]}`;
8342
8558
  return `[${step}] ${name} ${truncate(JSON.stringify(input), 1, INPUT_CHARS)}`;
8343
8559
  }
8344
- function describeToolUse(step, b) {
8345
- if (typeof b.name !== "string" || b.name === "")
8560
+ function describeToolCall(step, c) {
8561
+ if (c.name === "")
8346
8562
  return `[${step}] (tool call without a name)`;
8347
- if (typeof b.input !== "object" || b.input === null)
8348
- return `[${step}] ${b.name} (no input)`;
8349
- return describeCall(step, b.name, b.input);
8350
- }
8351
- function resultText(content) {
8352
- if (typeof content === "string")
8353
- return content;
8354
- if (Array.isArray(content))
8355
- return content.map((c) => typeof c.text === "string" ? c.text : `[${c.type}]`).join(`
8356
- `);
8357
- return "";
8563
+ if (c.input === null)
8564
+ return `[${step}] ${c.name} (no input)`;
8565
+ return describeCall(step, c.name, c.input);
8358
8566
  }
8359
8567
  function workdirFiles(dir) {
8360
8568
  const out = [];
8361
8569
  const walk = (d) => {
8362
- for (const name of readdirSync(d)) {
8570
+ for (const name of readdirSync2(d)) {
8363
8571
  if (SKIP_DIRS.has(name))
8364
8572
  continue;
8365
- const p = join4(d, name);
8573
+ const p = join5(d, name);
8366
8574
  if (statSync(p).isDirectory())
8367
8575
  walk(p);
8368
8576
  else
@@ -8374,32 +8582,20 @@ function workdirFiles(dir) {
8374
8582
  }
8375
8583
  function writeTranscript(opts) {
8376
8584
  const out = [`# ${opts.title}`, "", "## prompt", opts.prompt, "", "## steps"];
8585
+ const { final } = opts;
8377
8586
  let step = 0;
8378
- let final = "";
8379
8587
  let lastText = -1;
8380
- for (const e of opts.events) {
8381
- const message = e?.message;
8382
- const content = Array.isArray(message?.content) ? message.content : [];
8383
- if (e?.type === "assistant")
8384
- for (const b of content) {
8385
- if (b.type === "tool_use")
8386
- out.push(describeToolUse(++step, b));
8387
- else if (b.type === "text" && typeof b.text === "string") {
8388
- lastText = out.length;
8389
- out.push(prose(truncate(b.text, 30, TEXT_CHARS)));
8390
- }
8391
- }
8392
- else if (e?.type === "user")
8393
- for (const b of content) {
8394
- if (b.type !== "tool_result")
8395
- continue;
8396
- const text = resultText(b.content).trimEnd();
8397
- const status = b.is_error === true ? "error" : "ok";
8398
- out.push(text ? ` → ${status}
8399
- ${indent(truncate(text, OUTPUT_LINES, OUTPUT_CHARS))}` : ` → ${status}`);
8400
- }
8401
- else if (e?.type === "result" && typeof e.result === "string")
8402
- final = e.result;
8588
+ for (const s of opts.steps) {
8589
+ if (s.kind === "tool_call")
8590
+ out.push(describeToolCall(++step, s));
8591
+ else if (s.kind === "text") {
8592
+ lastText = out.length;
8593
+ out.push(prose(truncate(s.text, 30, TEXT_CHARS)));
8594
+ } else {
8595
+ const status = s.isError ? "error" : "ok";
8596
+ out.push(s.text ? ` → ${status}
8597
+ ${indent(truncate(s.text, OUTPUT_LINES, OUTPUT_CHARS))}` : ` → ${status}`);
8598
+ }
8403
8599
  }
8404
8600
  if (lastText === out.length - 1 && final && out[lastText] === prose(truncate(final, 30, TEXT_CHARS)))
8405
8601
  out.pop();
@@ -8703,7 +8899,7 @@ function ttyUI(out, opts) {
8703
8899
  }
8704
8900
 
8705
8901
  // src/upload.ts
8706
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
8902
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
8707
8903
  import { basename as basename2 } from "node:path";
8708
8904
  var MAX_FILE = 20 * 1024 * 1024;
8709
8905
  var MAX_CHECKS = 200;
@@ -8785,7 +8981,7 @@ async function uploadRun(t, r) {
8785
8981
  };
8786
8982
  const bytes = {};
8787
8983
  for (const name of FILES) {
8788
- const b = existsSync2(paths[name]) ? readFileSync3(paths[name]) : Buffer.alloc(0);
8984
+ const b = existsSync3(paths[name]) ? readFileSync3(paths[name]) : Buffer.alloc(0);
8789
8985
  if (b.byteLength > MAX_FILE)
8790
8986
  return {
8791
8987
  status: "failed",
@@ -8868,7 +9064,7 @@ async function uploadRun(t, r) {
8868
9064
 
8869
9065
  // src/which.ts
8870
9066
  import { accessSync, constants, statSync as statSync2 } from "node:fs";
8871
- import { delimiter, join as join5, resolve } from "node:path";
9067
+ import { delimiter, join as join6, resolve } from "node:path";
8872
9068
  function resolveBin(bin) {
8873
9069
  const executable = (p) => {
8874
9070
  try {
@@ -8881,8 +9077,8 @@ function resolveBin(bin) {
8881
9077
  if (bin.includes("/"))
8882
9078
  return executable(bin) ? resolve(bin) : undefined;
8883
9079
  for (const dir of (process.env.PATH ?? "").split(delimiter)) {
8884
- if (dir && executable(join5(dir, bin)))
8885
- return join5(dir, bin);
9080
+ if (dir && executable(join6(dir, bin)))
9081
+ return join6(dir, bin);
8886
9082
  }
8887
9083
  return;
8888
9084
  }
@@ -8957,23 +9153,12 @@ function parseArgs(args) {
8957
9153
  }
8958
9154
  return opts;
8959
9155
  }
8960
- function findSession(configDir, sessionId) {
8961
- const projects = join6(configDir, "projects");
8962
- if (!existsSync3(projects))
8963
- return null;
8964
- for (const d of readdirSync2(projects)) {
8965
- const f = join6(projects, d, `${sessionId}.jsonl`);
8966
- if (existsSync3(f))
8967
- return f;
8968
- }
8969
- return null;
8970
- }
8971
9156
  function newRunId() {
8972
9157
  const ts = new Date().toISOString().replace(/\.\d+Z$/, "Z").replace(/[-:]/g, "");
8973
9158
  return `${ts}-${randomBytes2(2).toString("hex")}`;
8974
9159
  }
8975
9160
  function writeRunJson(r) {
8976
- writeFileSync4(join6(r.dir, "run.json"), `${JSON.stringify(r, null, 2)}
9161
+ writeFileSync4(join7(r.dir, "run.json"), `${JSON.stringify(r, null, 2)}
8977
9162
  `);
8978
9163
  }
8979
9164
  async function uploadPreflight(opts, ui, project, runGroup, git) {
@@ -9004,10 +9189,12 @@ async function uploadPreflight(opts, ui, project, runGroup, git) {
9004
9189
  if (e.status === 401 || e.status === 403)
9005
9190
  return {
9006
9191
  exitCode: EXIT.usage,
9192
+ code: "auth",
9007
9193
  error: `${origin} rejected the key (${e.message}): run firegoat login or pass --no-upload`
9008
9194
  };
9009
9195
  return {
9010
9196
  exitCode: EXIT.notGraded,
9197
+ code: e instanceof NetworkError ? "network" : "server",
9011
9198
  error: `${e.status === null ? `Could not reach ${origin}` : `${origin} answered`} (${e.message}): retry, check FIREGOAT_BASE_URL, or pass --no-upload`
9012
9199
  };
9013
9200
  }
@@ -9018,8 +9205,8 @@ function runFill(runId, n, testName, out, dir) {
9018
9205
  "run.n": String(n),
9019
9206
  "test.name": testName,
9020
9207
  FIREGOAT_OUT: out,
9021
- FIREGOAT_WORKDIR: join6(dir, "work"),
9022
- FIREGOAT_HOME: join6(dir, "home")
9208
+ FIREGOAT_WORKDIR: join7(dir, "work"),
9209
+ FIREGOAT_HOME: join7(dir, "home")
9023
9210
  };
9024
9211
  }
9025
9212
  function renderClaudeArgs(test, vars, fill) {
@@ -9035,10 +9222,10 @@ function planRuns(tests, models, runs) {
9035
9222
  }
9036
9223
  var DRY_RUN_ID = "<runId>";
9037
9224
  function dryRun(opts) {
9038
- const out = join6(opts.runsRoot, DRY_RUN_ID, "out");
9225
+ const out = join7(opts.runsRoot, DRY_RUN_ID, "out");
9039
9226
  const runs = opts.plan.map((p) => {
9040
9227
  const test = opts.tests.find((t) => t.name === p.test);
9041
- const fill = runFill(DRY_RUN_ID, p.n, test.name, out, join6(opts.runsRoot, DRY_RUN_ID, test.name, String(p.n)));
9228
+ const fill = runFill(DRY_RUN_ID, p.n, test.name, out, join7(opts.runsRoot, DRY_RUN_ID, test.name, String(p.n)));
9042
9229
  renderEnv(test.env, opts.vars, fill);
9043
9230
  return {
9044
9231
  ...p,
@@ -9060,8 +9247,8 @@ function dryRun(opts) {
9060
9247
  async function runOne(opts) {
9061
9248
  const { test, n } = opts;
9062
9249
  const started = Date.now();
9063
- const dir = join6(opts.runDir, test.name, String(n));
9064
- const workdir = join6(dir, "work");
9250
+ const dir = join7(opts.runDir, test.name, String(n));
9251
+ const workdir = join7(dir, "work");
9065
9252
  mkdirSync4(workdir, { recursive: true });
9066
9253
  const { home, envFile } = makeRunHome(dir);
9067
9254
  const fill = runFill(opts.runId, n, test.name, opts.out, dir);
@@ -9080,11 +9267,11 @@ async function runOne(opts) {
9080
9267
  workdir,
9081
9268
  home,
9082
9269
  session: null,
9083
- transcript: join6(dir, "transcript.txt"),
9084
- stderr: join6(dir, "stderr.txt"),
9085
- checksLog: join6(dir, "checks.txt"),
9270
+ transcript: join7(dir, "transcript.txt"),
9271
+ stderr: join7(dir, "stderr.txt"),
9272
+ checksLog: join7(dir, "checks.txt"),
9086
9273
  cleanup: [],
9087
- cleanupLog: join6(dir, "cleanup.txt"),
9274
+ cleanupLog: join7(dir, "cleanup.txt"),
9088
9275
  num_turns: null,
9089
9276
  cost: null,
9090
9277
  duration_ms: 0,
@@ -9124,18 +9311,21 @@ async function attempt(opts, r, env, stepEnv, homeEnvFile) {
9124
9311
  commands: install,
9125
9312
  cwd: workdir,
9126
9313
  env: stepEnv,
9127
- logPath: join6(dir, "install.txt")
9314
+ logPath: join7(dir, "install.txt")
9128
9315
  });
9129
9316
  if (error)
9130
9317
  return ["error", `not graded: ${error}`];
9131
9318
  }
9132
9319
  const stderr = createWriteStream2(r.stderr);
9133
- const outcome = await runClaude({
9134
- bin: opts.claudeBin,
9320
+ const { harness } = opts;
9321
+ const outcome = await runAgent(harness, {
9322
+ bin: opts.bin,
9135
9323
  prompt: r.prompt,
9136
9324
  cwd: workdir,
9137
9325
  env: {
9138
- ...claudeEnv(opts.configDir, env, homeEnvFile),
9326
+ ...hostEnv(),
9327
+ ...env,
9328
+ ...harness.env({ configDir: opts.configDir, homeEnvFile }),
9139
9329
  PATH: opts.stepEnv.PATH,
9140
9330
  FIREGOAT_OUT: opts.out,
9141
9331
  FIREGOAT_WORKDIR: workdir,
@@ -9145,27 +9335,30 @@ async function attempt(opts, r, env, stepEnv, homeEnvFile) {
9145
9335
  timeoutMs: test.timeout * 1000,
9146
9336
  model: opts.model ?? undefined,
9147
9337
  extraArgs: r.claudeArgs,
9148
- onEvent: opts.onEvent
9338
+ onProgress: opts.onProgress
9149
9339
  });
9150
- r.claudeModel = outcome.claudeModel;
9340
+ r.claudeModel = outcome.reportedModel;
9341
+ if (outcome.version !== null)
9342
+ evalStats({ claudeVersion: claudeVersion(outcome.version) });
9151
9343
  await new Promise((res) => stderr.end(res));
9152
9344
  if (outcome.sessionId)
9153
- r.session = findSession(opts.configDir, outcome.sessionId);
9345
+ r.session = harness.findSession(opts.configDir, outcome.sessionId);
9154
9346
  writeTranscript({
9155
9347
  path: r.transcript,
9156
9348
  title: `${test.name}#${n}${opts.model ? ` (model ${opts.model})` : ""}`,
9157
9349
  prompt: r.prompt,
9158
- events: outcome.events,
9350
+ steps: outcome.steps,
9351
+ final: outcome.final,
9159
9352
  workdir
9160
9353
  });
9161
- r.num_turns = outcome.result?.num_turns ?? null;
9162
- r.cost = outcome.result?.total_cost_usd ?? null;
9354
+ r.num_turns = outcome.turns;
9355
+ r.cost = outcome.cost;
9163
9356
  if (!outcome.ok)
9164
9357
  return ["error", `not graded: ${outcome.reason}`];
9165
9358
  if (!r.session)
9166
9359
  return [
9167
9360
  "error",
9168
- `not graded: claude saved no session file for ${outcome.sessionId} under ${opts.configDir}/projects`
9361
+ `not graded: ${harness.name} saved no session file for ${outcome.sessionId} under ${harness.sessionsDir(opts.configDir)}`
9169
9362
  ];
9170
9363
  const log = createWriteStream2(r.checksLog);
9171
9364
  try {
@@ -9202,7 +9395,7 @@ function uploadedSummary(origin, results, runDir) {
9202
9395
  return;
9203
9396
  const head = `Uploaded ${urls.length} session${urls.length === 1 ? "" : "s"} to ${new URL(origin).host} (private)`;
9204
9397
  if (urls.length > MAX_LISTED_URLS) {
9205
- console.log(`${head}: each run's upload.url in ${join6(runDir, "summary.json")}`);
9398
+ console.log(`${head}: each run's upload.url in ${join7(runDir, "summary.json")}`);
9206
9399
  return;
9207
9400
  }
9208
9401
  console.log(`${head}:`);
@@ -9215,7 +9408,8 @@ async function evalCommand(args, root) {
9215
9408
  return EXIT.pass;
9216
9409
  }
9217
9410
  const json = args.includes("--json");
9218
- const fatal = (exitCode, error) => {
9411
+ const fatal = (exitCode, error, code) => {
9412
+ failWith(code);
9219
9413
  console.error(`firegoat eval: ${error}`);
9220
9414
  if (json)
9221
9415
  console.log(JSON.stringify({ error, exitCode }));
@@ -9226,7 +9420,7 @@ async function evalCommand(args, root) {
9226
9420
  opts = parseArgs(args);
9227
9421
  } catch (e) {
9228
9422
  if (e instanceof UsageError)
9229
- return fatal(EXIT.usage, e.message);
9423
+ return fatal(EXIT.usage, e.message, "usage");
9230
9424
  throw e;
9231
9425
  }
9232
9426
  const say = opts.json ? console.error : console.log;
@@ -9243,7 +9437,7 @@ async function evalCommand(args, root) {
9243
9437
  config = loadConfig(root);
9244
9438
  } catch (e) {
9245
9439
  if (e instanceof ConfigError)
9246
- return fatal(EXIT.usage, e.message);
9440
+ return fatal(EXIT.usage, e.message, "config");
9247
9441
  throw e;
9248
9442
  }
9249
9443
  let tests = config.tests;
@@ -9251,11 +9445,17 @@ async function evalCommand(args, root) {
9251
9445
  const known = new Set(tests.map((t) => t.name));
9252
9446
  const unknown = opts.names.filter((n) => !known.has(n));
9253
9447
  if (unknown.length > 0)
9254
- return fatal(EXIT.usage, `Unknown test ${unknown.map((n) => JSON.stringify(n)).join(", ")} (known: ${[...known].join(", ")})`);
9448
+ return fatal(EXIT.usage, `Unknown test ${unknown.map((n) => JSON.stringify(n)).join(", ")} (known: ${[...known].join(", ")})`, "usage");
9255
9449
  tests = tests.filter((t) => opts.names.includes(t.name));
9256
9450
  }
9257
9451
  const runs = opts.runs ?? config.runs;
9258
9452
  const models = opts.models.length > 0 ? opts.models : config.models;
9453
+ evalStats({
9454
+ testCount: tests.length,
9455
+ runCount: tests.length * runs * Math.max(models.length, 1),
9456
+ modelCount: models.length,
9457
+ parallel: opts.parallel
9458
+ });
9259
9459
  const vars = { ...config.vars, ...opts.vars };
9260
9460
  const missing = [
9261
9461
  ...new Set(tests.flatMap((t) => [
@@ -9266,13 +9466,13 @@ async function evalCommand(args, root) {
9266
9466
  ]).flatMap(varRefs).filter((k) => !(k in vars)))
9267
9467
  ];
9268
9468
  if (missing.length > 0)
9269
- 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`);
9469
+ 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`, "config");
9270
9470
  if (opts.dryRun) {
9271
9471
  let repo;
9272
9472
  try {
9273
9473
  repo = gitRepo(root);
9274
9474
  } catch (e) {
9275
- return fatal(EXIT.usage, e.message);
9475
+ return fatal(EXIT.usage, e.message, "config");
9276
9476
  }
9277
9477
  try {
9278
9478
  dryRun({
@@ -9280,53 +9480,55 @@ async function evalCommand(args, root) {
9280
9480
  tests,
9281
9481
  plan: planRuns(tests, models, runs),
9282
9482
  vars,
9283
- runsRoot: join6(repo?.mainCheckout ?? root, ".firegoat", "runs"),
9483
+ runsRoot: join7(repo?.mainCheckout ?? root, ".firegoat", "runs"),
9284
9484
  json: opts.json,
9285
9485
  say
9286
9486
  });
9287
9487
  } catch (e) {
9288
9488
  if (e instanceof ConfigError)
9289
- return fatal(EXIT.usage, e.message);
9489
+ return fatal(EXIT.usage, e.message, "config");
9290
9490
  throw e;
9291
9491
  }
9292
9492
  return EXIT.pass;
9293
9493
  }
9294
- const claudePath = resolveBin(opts.claudeBin);
9295
- if (!claudePath)
9296
- 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.`);
9494
+ const harness = claude;
9495
+ evalStats({ harness: harness.name });
9496
+ const binPath = resolveBin(opts.claudeBin);
9497
+ if (!binPath)
9498
+ return fatal(EXIT.notGraded, `Not graded: ${harness.name} binary ${JSON.stringify(opts.claudeBin)} not found. ${harness.installHint}`, "claude_missing");
9297
9499
  let repo;
9298
9500
  try {
9299
9501
  repo = gitRepo(root);
9300
9502
  } catch (e) {
9301
- return fatal(EXIT.usage, e.message);
9503
+ return fatal(EXIT.usage, e.message, "config");
9302
9504
  }
9303
9505
  const firegoatDir = ensureFiregoatDir(repo?.mainCheckout ?? root);
9304
- const configDir = join6(firegoatDir, ".claude");
9506
+ const configDir = join7(firegoatDir, harness.configDirName);
9305
9507
  mkdirSync4(configDir, { recursive: true });
9306
- ui.auth(process.env.ANTHROPIC_API_KEY ? "ANTHROPIC_API_KEY (claude bills the API key; unset it to use your claude login)" : "your claude login");
9307
- const loginProblem = checkLogin({
9308
- bin: claudePath,
9309
- env: claudeEnv(configDir),
9508
+ ui.auth(harness.authLabel());
9509
+ const loginProblem = harness.checkLogin({
9510
+ bin: binPath,
9511
+ env: { ...hostEnv(), ...harness.env({ configDir }) },
9310
9512
  cwd: root
9311
9513
  });
9312
9514
  if (loginProblem)
9313
- return fatal(EXIT.notGraded, `Not graded: ${loginProblem}`);
9515
+ return fatal(EXIT.notGraded, `Not graded: ${loginProblem.reason}`, loginProblem.loggedOut ? "claude_logged_out" : "claude_auth_status");
9314
9516
  const evalStarted = Date.now();
9315
9517
  const runId = newRunId();
9316
9518
  let upload;
9317
9519
  try {
9318
9520
  const pre = await uploadPreflight(opts, ui, basename3(repo?.mainCheckout ?? repo?.toplevel ?? root), runId, repo?.info ?? null);
9319
9521
  if (pre && "exitCode" in pre)
9320
- return fatal(pre.exitCode, pre.error);
9522
+ return fatal(pre.exitCode, pre.error, pre.code);
9321
9523
  upload = pre;
9322
9524
  } catch (e) {
9323
9525
  if (e instanceof ConfigError)
9324
- return fatal(EXIT.usage, e.message);
9526
+ return fatal(EXIT.usage, e.message, "config");
9325
9527
  throw e;
9326
9528
  }
9327
- const runsRoot = join6(firegoatDir, "runs");
9328
- const runDir = join6(runsRoot, runId);
9329
- const out = join6(runDir, "out");
9529
+ const runsRoot = join7(firegoatDir, "runs");
9530
+ const runDir = join7(runsRoot, runId);
9531
+ const out = join7(runDir, "out");
9330
9532
  mkdirSync4(out, { recursive: true });
9331
9533
  const stepEnv = {
9332
9534
  ...hostEnv(),
@@ -9350,12 +9552,12 @@ async function evalCommand(args, root) {
9350
9552
  ...stepEnv,
9351
9553
  ...renderEnv(Object.fromEntries(Object.entries(config.env).filter(([, v]) => isEvalLevel(v))), vars, { "run.id": runId, FIREGOAT_OUT: out })
9352
9554
  },
9353
- logPath: join6(runDir, "build.txt")
9555
+ logPath: join7(runDir, "build.txt")
9354
9556
  });
9355
9557
  const secs = Math.round((Date.now() - buildStarted) / 1000);
9356
9558
  ui.build(error ? "fail" : "ok", secs);
9357
9559
  if (error)
9358
- return fatal(EXIT.notGraded, `Not graded: ${error}`);
9560
+ return fatal(EXIT.notGraded, `Not graded: ${error}`, "build_failed");
9359
9561
  }
9360
9562
  ui.runs(rows);
9361
9563
  const results = new Array(rows.length);
@@ -9365,26 +9567,18 @@ async function evalCommand(args, root) {
9365
9567
  row.state = "running";
9366
9568
  ui.update();
9367
9569
  const r = await runOne({
9368
- onEvent: (e) => {
9369
- if (e.type !== "assistant")
9370
- return;
9570
+ onProgress: (p) => {
9371
9571
  row.turns++;
9372
- const content = e.message?.content;
9373
- if (!Array.isArray(content))
9374
- return;
9375
- for (const b of content) {
9376
- const cmd = b.input?.command;
9377
- if (b.type === "tool_use" && typeof cmd === "string")
9378
- row.command = cmd.split(`
9379
- `)[0];
9380
- }
9572
+ if (p.command !== undefined)
9573
+ row.command = p.command;
9381
9574
  },
9382
9575
  test,
9383
9576
  n: row.n,
9384
9577
  model: row.model,
9385
9578
  runId,
9386
9579
  runDir,
9387
- claudeBin: claudePath,
9580
+ harness,
9581
+ bin: binPath,
9388
9582
  configDir,
9389
9583
  install: config.install,
9390
9584
  cleanup: config.cleanup,
@@ -9422,6 +9616,14 @@ async function evalCommand(args, root) {
9422
9616
  for (const r of results)
9423
9617
  totals[r.status]++;
9424
9618
  const uploadsFailed = results.filter((r) => r.upload?.status === "failed").length;
9619
+ const uploaded = results.filter((r) => r.upload?.status === "uploaded").length;
9620
+ evalStats({
9621
+ passCount: totals.pass,
9622
+ failCount: totals.fail,
9623
+ notGradedCount: totals.error,
9624
+ uploaded,
9625
+ uploadFailed: uploadsFailed
9626
+ });
9425
9627
  const doc = {
9426
9628
  firegoatVersion: package_default.version,
9427
9629
  runId,
@@ -9432,9 +9634,9 @@ async function evalCommand(args, root) {
9432
9634
  totals,
9433
9635
  runs: results
9434
9636
  };
9435
- writeFileSync4(join6(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
9637
+ writeFileSync4(join7(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
9436
9638
  `);
9437
- appendFileSync(join6(runsRoot, "index.jsonl"), `${JSON.stringify({
9639
+ appendFileSync(join7(runsRoot, "index.jsonl"), `${JSON.stringify({
9438
9640
  runId,
9439
9641
  startedAt: new Date(evalStarted).toISOString(),
9440
9642
  dir: root,
@@ -9444,7 +9646,7 @@ async function evalCommand(args, root) {
9444
9646
  failed: totals.fail,
9445
9647
  notGraded: totals.error,
9446
9648
  cost: results.every((r) => r.cost !== null) ? results.reduce((sum, r) => sum + r.cost, 0) : null,
9447
- uploaded: results.filter((r) => r.upload?.status === "uploaded").length,
9649
+ uploaded,
9448
9650
  uploadFailed: uploadsFailed
9449
9651
  })}
9450
9652
  `);
@@ -9464,6 +9666,10 @@ async function evalCommand(args, root) {
9464
9666
  console.error(`firegoat eval: ${uploadsFailed} upload${uploadsFailed === 1 ? "" : "s"} failed (each run's upload.error in summary.json)`);
9465
9667
  if (opts.json)
9466
9668
  console.log(JSON.stringify(doc, null, 2));
9669
+ if (totals.error > 0)
9670
+ failWith("not_graded");
9671
+ else if (totals.fail > 0)
9672
+ failWith("checks_failed");
9467
9673
  return totals.error > 0 ? EXIT.notGraded : totals.fail > 0 ? EXIT.fail : EXIT.pass;
9468
9674
  }
9469
9675
 
@@ -9534,7 +9740,7 @@ function defaultDeviceStore() {
9534
9740
  }
9535
9741
  var VERSION = "0.2.0";
9536
9742
  var DEFAULT_BASE_URL2 = "https://firegoat.ai/evals";
9537
- var TIMEOUT_MS = 15000;
9743
+ var TIMEOUT_MS2 = 15000;
9538
9744
  function envBaseUrl() {
9539
9745
  return globalThis.process?.env?.FEEDBACK_API_URL;
9540
9746
  }
@@ -9638,7 +9844,7 @@ class FeedbackClient {
9638
9844
  method: "POST",
9639
9845
  headers,
9640
9846
  body: body === undefined ? undefined : JSON.stringify(body),
9641
- signal: AbortSignal.timeout(TIMEOUT_MS)
9847
+ signal: AbortSignal.timeout(TIMEOUT_MS2)
9642
9848
  });
9643
9849
  const text = await res.text();
9644
9850
  let data;
@@ -9728,7 +9934,12 @@ firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
9728
9934
  if (e instanceof FeedbackError) {
9729
9935
  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.` : "";
9730
9936
  console.error(`firegoat feedback: ${e.code}: ${e.message}.${hint}`);
9731
- return e.code === "invalid_request" || e.code === "possible_secret" ? EXIT.usage : EXIT.notGraded;
9937
+ if (e.code === "invalid_request" || e.code === "possible_secret") {
9938
+ failWith("usage");
9939
+ return EXIT.usage;
9940
+ }
9941
+ failWith(e.code === "rate_limited" ? "rate_limited" : "server");
9942
+ return EXIT.notGraded;
9732
9943
  }
9733
9944
  throw e;
9734
9945
  }
@@ -9737,12 +9948,12 @@ firegoat ${package_default.version}` : `firegoat ${package_default.version}`,
9737
9948
  // src/init.ts
9738
9949
  import { spawnSync as spawnSync3 } from "node:child_process";
9739
9950
  import { existsSync as existsSync4, writeFileSync as writeFileSync5 } from "node:fs";
9740
- import { basename as basename4, join as join7 } from "node:path";
9951
+ import { basename as basename4, join as join8 } from "node:path";
9741
9952
  import { createInterface } from "node:readline/promises";
9742
9953
 
9743
9954
  // src/login.ts
9744
9955
  import { spawn as spawn2 } from "node:child_process";
9745
- var TIMEOUT_MS2 = 30000;
9956
+ var TIMEOUT_MS3 = 30000;
9746
9957
  var MAX_EXPIRES_S = 900;
9747
9958
  var MAX_INTERVAL_S = 60;
9748
9959
  var FORM = { "Content-Type": "application/x-www-form-urlencoded" };
@@ -9776,7 +9987,7 @@ async function loginFlow(say, interactive) {
9776
9987
  step: cfgStep,
9777
9988
  url: new URL(`${base}/api/traces/oauth/config`),
9778
9989
  method: "GET",
9779
- timeoutMs: TIMEOUT_MS2
9990
+ timeoutMs: TIMEOUT_MS3
9780
9991
  });
9781
9992
  if (cfg.status !== 200)
9782
9993
  throw replyError(cfgStep, cfg);
@@ -9792,7 +10003,7 @@ async function loginFlow(say, interactive) {
9792
10003
  method: "POST",
9793
10004
  headers: FORM,
9794
10005
  body: new URLSearchParams({ client_id: clientId }).toString(),
9795
- timeoutMs: TIMEOUT_MS2
10006
+ timeoutMs: TIMEOUT_MS3
9796
10007
  });
9797
10008
  if (dev.status !== 200)
9798
10009
  throw replyError(devStep, dev);
@@ -9833,7 +10044,7 @@ async function loginFlow(say, interactive) {
9833
10044
  client_id: clientId,
9834
10045
  device_code: deviceCode
9835
10046
  }).toString(),
9836
- timeoutMs: TIMEOUT_MS2
10047
+ timeoutMs: TIMEOUT_MS3
9837
10048
  });
9838
10049
  if (r.status === 200) {
9839
10050
  const t = r.json?.access_token;
@@ -9887,10 +10098,12 @@ ${LOGIN_HELP}`);
9887
10098
  return EXIT.pass;
9888
10099
  } catch (e) {
9889
10100
  if (e instanceof ConfigError) {
10101
+ failWith("config");
9890
10102
  console.error(`firegoat login: ${e.message}`);
9891
10103
  return EXIT.usage;
9892
10104
  }
9893
10105
  if (e instanceof ApiError) {
10106
+ failWith(e instanceof NetworkError ? "network" : "login_failed");
9894
10107
  console.error(`firegoat login: ${e.message}`);
9895
10108
  return EXIT.fail;
9896
10109
  }
@@ -9933,7 +10146,7 @@ function skillsDir(scope) {
9933
10146
  if (scope === "local")
9934
10147
  return ".claude/skills/";
9935
10148
  const config = process.env.CLAUDE_CONFIG_DIR?.trim();
9936
- return config ? `${join7(config, "skills")}/` : "~/.claude/skills/";
10149
+ return config ? `${join8(config, "skills")}/` : "~/.claude/skills/";
9937
10150
  }
9938
10151
  function template(product) {
9939
10152
  return `# firegoat.yaml: evals for how well coding agents use ${product}.
@@ -10080,6 +10293,7 @@ ${UPLOADS_ON}`);
10080
10293
  } catch (e) {
10081
10294
  if (!(e instanceof ApiError))
10082
10295
  throw e;
10296
+ failWith(e instanceof NetworkError ? "network" : "login_failed");
10083
10297
  console.error(`firegoat init: Login failed: ${e.message}`);
10084
10298
  return EXIT.fail;
10085
10299
  }
@@ -10116,6 +10330,7 @@ ${INIT_HELP}`);
10116
10330
  return EXIT.usage;
10117
10331
  }
10118
10332
  let scope = noSkills ? "none" : localSkills ? "local" : "global";
10333
+ initSkills(scope);
10119
10334
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
10120
10335
  const color = interactive && !process.env.NO_COLOR;
10121
10336
  const paint = (code, text) => color ? `\x1B[${code}m${text}\x1B[0m` : text;
@@ -10128,7 +10343,7 @@ ${INIT_HELP}`);
10128
10343
  console.log(rule);
10129
10344
  }
10130
10345
  ensureFiregoatDir(cwd);
10131
- const configPath = join7(cwd, CONFIG_FILE);
10346
+ const configPath = join8(cwd, CONFIG_FILE);
10132
10347
  const keep = existsSync4(configPath) && !force;
10133
10348
  if (!keep)
10134
10349
  writeFileSync5(configPath, template(basename4(cwd)));
@@ -10143,6 +10358,7 @@ ${INIT_HELP}`);
10143
10358
  if (interactive && !noSkills && !localSkills) {
10144
10359
  scope = await askScope(` ${paint("38;5;208", "◉")} ${paint("90", "03")} Install the firegoat-onboarding skill globally (~/.claude/skills) or in this repo (.claude/skills)?`);
10145
10360
  process.stdout.write("\x1B[1A\x1B[2K");
10361
+ initSkills(scope);
10146
10362
  }
10147
10363
  if (scope !== "none") {
10148
10364
  const argv = [...SKILLS_ARGV, ...scope === "global" ? ["-g"] : []];
@@ -10157,6 +10373,7 @@ ${INIT_HELP}`);
10157
10373
  encoding: "utf8"
10158
10374
  });
10159
10375
  if (r.error || r.status !== 0) {
10376
+ failWith("skills_failed");
10160
10377
  process.stderr.write(`${r.stdout ?? ""}${r.stderr ?? ""}`);
10161
10378
  console.error(`firegoat init: Installing the skill failed (${r.error?.message ?? `exit ${r.status}`}). Run it yourself: npx ${argv.join(" ")}`);
10162
10379
  return EXIT.notGraded;
@@ -10180,11 +10397,16 @@ ${INIT_HELP}`);
10180
10397
  if (loginExit !== EXIT.pass)
10181
10398
  return loginExit;
10182
10399
  }
10400
+ const telemetryOn = telemetryEnabled();
10183
10401
  if (!interactive) {
10402
+ if (telemetryOn)
10403
+ console.log(TELEMETRY_NOTICE);
10184
10404
  console.log(`
10185
10405
  Prompt for your coding agent: ${AGENT_PROMPT}`);
10186
10406
  return EXIT.pass;
10187
10407
  }
10408
+ if (telemetryOn)
10409
+ console.log(` ${paint("90", TELEMETRY_NOTICE)}`);
10188
10410
  console.log(rule);
10189
10411
  console.log(` Next: hand this to your coding agent
10190
10412
  `);
@@ -10225,16 +10447,33 @@ ${topHelp(package_default.version)}`);
10225
10447
  return EXIT.usage;
10226
10448
  }
10227
10449
  }
10228
- main(process.argv.slice(2)).then((code) => process.exit(code), (err) => {
10450
+ function crashed(err) {
10229
10451
  if (err instanceof ConfigError) {
10452
+ failWith("config");
10230
10453
  console.error(err.message);
10231
- process.exit(EXIT.usage);
10454
+ return EXIT.usage;
10232
10455
  }
10233
10456
  if (err?.code === "ABORT_ERR") {
10457
+ failWith("aborted");
10234
10458
  console.error(`
10235
10459
  Aborted.`);
10236
- process.exit(130);
10460
+ return 130;
10237
10461
  }
10462
+ failWith("unknown");
10238
10463
  console.error(err);
10239
- process.exit(EXIT.notGraded);
10464
+ return EXIT.notGraded;
10465
+ }
10466
+ var startedAt = Date.now();
10467
+ var argv = process.argv.slice(2);
10468
+ var event;
10469
+ try {
10470
+ event = telemetryEnabled() ? telemetryEvent(argv) : null;
10471
+ } catch (e) {
10472
+ console.error(`firegoat: ${e.message}`);
10473
+ process.exit(EXIT.usage);
10474
+ }
10475
+ main(argv).catch(crashed).then(async (code) => {
10476
+ if (event)
10477
+ await sendTelemetry(event, code, startedAt);
10478
+ process.exit(code);
10240
10479
  });