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.
- package/README.md +22 -251
- package/dist/cli.js +587 -348
- 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.
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
8372
|
+
import { join as join4 } from "node:path";
|
|
8250
8373
|
function homeEnv(home) {
|
|
8251
8374
|
return {
|
|
8252
8375
|
HOME: home,
|
|
8253
|
-
XDG_CONFIG_HOME:
|
|
8254
|
-
XDG_CACHE_HOME:
|
|
8255
|
-
XDG_DATA_HOME:
|
|
8256
|
-
XDG_STATE_HOME:
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
8345
|
-
if (
|
|
8560
|
+
function describeToolCall(step, c) {
|
|
8561
|
+
if (c.name === "")
|
|
8346
8562
|
return `[${step}] (tool call without a name)`;
|
|
8347
|
-
if (
|
|
8348
|
-
return `[${step}] ${
|
|
8349
|
-
return describeCall(step,
|
|
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
|
|
8570
|
+
for (const name of readdirSync2(d)) {
|
|
8363
8571
|
if (SKIP_DIRS.has(name))
|
|
8364
8572
|
continue;
|
|
8365
|
-
const p =
|
|
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
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
if (
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
8885
|
-
return
|
|
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(
|
|
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:
|
|
9022
|
-
FIREGOAT_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 =
|
|
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,
|
|
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 =
|
|
9064
|
-
const workdir =
|
|
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:
|
|
9084
|
-
stderr:
|
|
9085
|
-
checksLog:
|
|
9270
|
+
transcript: join7(dir, "transcript.txt"),
|
|
9271
|
+
stderr: join7(dir, "stderr.txt"),
|
|
9272
|
+
checksLog: join7(dir, "checks.txt"),
|
|
9086
9273
|
cleanup: [],
|
|
9087
|
-
cleanupLog:
|
|
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:
|
|
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
|
|
9134
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
9338
|
+
onProgress: opts.onProgress
|
|
9149
9339
|
});
|
|
9150
|
-
r.claudeModel = outcome.
|
|
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
|
-
|
|
9350
|
+
steps: outcome.steps,
|
|
9351
|
+
final: outcome.final,
|
|
9159
9352
|
workdir
|
|
9160
9353
|
});
|
|
9161
|
-
r.num_turns = outcome.
|
|
9162
|
-
r.cost = outcome.
|
|
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:
|
|
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 ${
|
|
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:
|
|
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
|
|
9295
|
-
|
|
9296
|
-
|
|
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 =
|
|
9506
|
+
const configDir = join7(firegoatDir, harness.configDirName);
|
|
9305
9507
|
mkdirSync4(configDir, { recursive: true });
|
|
9306
|
-
ui.auth(
|
|
9307
|
-
const loginProblem = checkLogin({
|
|
9308
|
-
bin:
|
|
9309
|
-
env:
|
|
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 =
|
|
9328
|
-
const runDir =
|
|
9329
|
-
const 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:
|
|
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
|
-
|
|
9369
|
-
if (e.type !== "assistant")
|
|
9370
|
-
return;
|
|
9570
|
+
onProgress: (p) => {
|
|
9371
9571
|
row.turns++;
|
|
9372
|
-
|
|
9373
|
-
|
|
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
|
-
|
|
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(
|
|
9637
|
+
writeFileSync4(join7(runDir, "summary.json"), `${JSON.stringify(doc, null, 2)}
|
|
9436
9638
|
`);
|
|
9437
|
-
appendFileSync(
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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 ? `${
|
|
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 =
|
|
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
|
-
|
|
10450
|
+
function crashed(err) {
|
|
10229
10451
|
if (err instanceof ConfigError) {
|
|
10452
|
+
failWith("config");
|
|
10230
10453
|
console.error(err.message);
|
|
10231
|
-
|
|
10454
|
+
return EXIT.usage;
|
|
10232
10455
|
}
|
|
10233
10456
|
if (err?.code === "ABORT_ERR") {
|
|
10457
|
+
failWith("aborted");
|
|
10234
10458
|
console.error(`
|
|
10235
10459
|
Aborted.`);
|
|
10236
|
-
|
|
10460
|
+
return 130;
|
|
10237
10461
|
}
|
|
10462
|
+
failWith("unknown");
|
|
10238
10463
|
console.error(err);
|
|
10239
|
-
|
|
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
|
});
|