ccqa 1.42.1 → 1.43.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/dist/bin/ccqa.mjs CHANGED
@@ -872,6 +872,35 @@ async function runPool(items, concurrency, fn, opts = {}) {
872
872
  return results;
873
873
  }
874
874
  //#endregion
875
+ //#region src/claude/env-keys.ts
876
+ /**
877
+ * Variables that carry a credential the Claude Code process can use on its
878
+ * own, with no login on the host: an API key, a gateway bearer token, or a
879
+ * subscription token from `claude setup-token`.
880
+ */
881
+ const CREDENTIAL_ENV_KEYS = [
882
+ "ANTHROPIC_API_KEY",
883
+ "ANTHROPIC_AUTH_TOKEN",
884
+ "CLAUDE_CODE_OAUTH_TOKEN"
885
+ ];
886
+ /**
887
+ * Standard Claude Code environment variables that select the API endpoint and
888
+ * credentials. ccqa forwards whichever of these are set to the underlying
889
+ * Claude Code process; it does not read or interpret their values.
890
+ *
891
+ * - `ANTHROPIC_BASE_URL` — the API endpoint to send requests to.
892
+ * - `ANTHROPIC_AUTH_TOKEN` — sent as `Authorization: Bearer <token>`.
893
+ * - `ANTHROPIC_API_KEY` — API key, when used instead of a token.
894
+ * - `ANTHROPIC_CUSTOM_HEADERS` — extra request headers.
895
+ * - `CLAUDE_CODE_OAUTH_TOKEN` — long-lived subscription token from
896
+ * `claude setup-token`, the headless-CI counterpart of a login.
897
+ */
898
+ const ENDPOINT_ENV_KEYS = [
899
+ "ANTHROPIC_BASE_URL",
900
+ "ANTHROPIC_CUSTOM_HEADERS",
901
+ ...CREDENTIAL_ENV_KEYS
902
+ ];
903
+ //#endregion
875
904
  //#region src/drift/auth.ts
876
905
  /**
877
906
  * Claude Code can also run against AWS Bedrock / Google Vertex AI, selected by
@@ -889,21 +918,20 @@ function cloudProviderEnabled() {
889
918
  }
890
919
  /**
891
920
  * Probe whether the host has any credential the Anthropic SDK can pick up:
892
- * 1. ANTHROPIC_API_KEY env var (CI / scripted use)
893
- * 2. CLAUDE_CODE_OAUTH_TOKEN env var (a long-lived subscription token from
894
- * `claude setup-token`, the headless-CI counterpart of a login)
895
- * 3. CLAUDE_CODE_USE_BEDROCK / CLAUDE_CODE_USE_VERTEX (cloud-provider
896
- * endpoints authenticated by the cloud SDK's credential chain)
897
- * 4. ~/.claude/.credentials.json (Claude Code login, file-based platforms)
898
- * 5. macOS Keychain item "Claude Code-credentials" (Claude Code login on
899
- * darwin stores the OAuth credentials in the Keychain, not on disk)
921
+ * - one of CREDENTIAL_ENV_KEYS (API key, gateway bearer token, or the
922
+ * subscription token from `claude setup-token`)
923
+ * - CLAUDE_CODE_USE_BEDROCK / CLAUDE_CODE_USE_VERTEX (cloud-provider
924
+ * endpoints authenticated by the cloud SDK's credential chain)
925
+ * - ~/.claude/.credentials.json (Claude Code login, file-based platforms)
926
+ * - macOS Keychain item "Claude Code-credentials" (Claude Code login on
927
+ * darwin stores the OAuth credentials in the Keychain, not on disk)
900
928
  *
901
929
  * Claude-driven hooks are opt-in, so the caller only consults this after the
902
930
  * user has asked for analysis. We never throw — auth absence is a normal flow
903
931
  * that surfaces as "analysis skipped".
904
932
  */
905
933
  function driftAuthAvailable() {
906
- for (const key of ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"]) {
934
+ for (const key of CREDENTIAL_ENV_KEYS) {
907
935
  const value = process.env[key];
908
936
  if (typeof value === "string" && value.length > 0) return { ok: true };
909
937
  }
@@ -912,7 +940,7 @@ function driftAuthAvailable() {
912
940
  if (process.platform === "darwin" && keychainHasClaudeCredentials()) return { ok: true };
913
941
  return {
914
942
  ok: false,
915
- reason: "no ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN / Bedrock or Vertex env / claude login"
943
+ reason: `no ${CREDENTIAL_ENV_KEYS.join(" / ")} / Bedrock or Vertex env / claude login`
916
944
  };
917
945
  }
918
946
  /**
@@ -1520,31 +1548,17 @@ function sum(costs) {
1520
1548
  }
1521
1549
  //#endregion
1522
1550
  //#region src/claude/invoke.ts
1551
+ /** The built-in tools an allow-list names: `Bash(*)` is `Bash`; `mcp__*` are not built-ins. */
1552
+ function builtinToolNames(allowedTools) {
1553
+ const names = allowedTools.map((entry) => entry.replace(/\(.*\)$/, "")).filter((name) => !name.startsWith("mcp__"));
1554
+ return [...new Set(names)];
1555
+ }
1523
1556
  function resolveModel(explicit) {
1524
1557
  if (explicit) return explicit;
1525
1558
  const envModel = process.env["CCQA_MODEL"];
1526
1559
  return envModel && envModel.length > 0 ? envModel : void 0;
1527
1560
  }
1528
1561
  /**
1529
- * Standard Claude Code environment variables that select the API endpoint and
1530
- * credentials. ccqa forwards whichever of these are set to the underlying
1531
- * Claude Code process; it does not read or interpret their values.
1532
- *
1533
- * - `ANTHROPIC_BASE_URL` — the API endpoint to send requests to.
1534
- * - `ANTHROPIC_AUTH_TOKEN` — sent as `Authorization: Bearer <token>`.
1535
- * - `ANTHROPIC_API_KEY` — API key, when used instead of a token.
1536
- * - `ANTHROPIC_CUSTOM_HEADERS` — extra request headers.
1537
- * - `CLAUDE_CODE_OAUTH_TOKEN` — long-lived subscription token from
1538
- * `claude setup-token`, the headless-CI counterpart of a login.
1539
- */
1540
- const ENDPOINT_ENV_KEYS = [
1541
- "ANTHROPIC_BASE_URL",
1542
- "ANTHROPIC_AUTH_TOKEN",
1543
- "ANTHROPIC_API_KEY",
1544
- "ANTHROPIC_CUSTOM_HEADERS",
1545
- "CLAUDE_CODE_OAUTH_TOKEN"
1546
- ];
1547
- /**
1548
1562
  * When both credentials are present the OAuth token wins and the API key is
1549
1563
  * dropped. Left to the CLI the API key would win, which makes "switch a CI
1550
1564
  * job to the subscription token" require unwiring the key everywhere; with
@@ -1556,21 +1570,6 @@ function preferOauthToken(env) {
1556
1570
  if (env["CLAUDE_CODE_OAUTH_TOKEN"]) delete env["ANTHROPIC_API_KEY"];
1557
1571
  }
1558
1572
  /**
1559
- * Collects the endpoint/auth variables set in the current process environment
1560
- * so they can be forwarded, verbatim, to every Claude Code invocation. Returns
1561
- * only the keys that are actually set (non-empty), so unset variables never
1562
- * override the SDK's own defaults. Credential precedence per preferOauthToken.
1563
- */
1564
- function resolveEndpointEnv() {
1565
- const endpointEnv = {};
1566
- for (const key of ENDPOINT_ENV_KEYS) {
1567
- const value = process.env[key];
1568
- if (value && value.length > 0) endpointEnv[key] = value;
1569
- }
1570
- preferOauthToken(endpointEnv);
1571
- return endpointEnv;
1572
- }
1573
- /**
1574
1573
  * Drop endpoint variables that are present but empty, so an empty value never
1575
1574
  * reaches the Claude Code process as an override. "Set to nothing" is how a
1576
1575
  * caller that cannot omit the key says "use the default" — a CI job wiring
@@ -1591,18 +1590,14 @@ function withoutEmptyEndpointVars(env) {
1591
1590
  * resolved view: left to the CLI the API key would win, silently moving every
1592
1591
  * call from the subscription to metered billing when a CI job wires both
1593
1592
  * (which is exactly what happened before this function existed).
1594
- *
1595
- * Returns undefined when no endpoint variable is set and the caller passes no
1596
- * env, so the SDK keeps its own default environment.
1597
1593
  */
1598
1594
  function buildInvocationEnv(env) {
1599
- const hasEndpointEnv = Object.keys(resolveEndpointEnv()).length > 0;
1600
- if (!env && !hasEndpointEnv) return void 0;
1601
1595
  const merged = withoutEmptyEndpointVars({
1602
1596
  ...process.env,
1603
1597
  ...env
1604
1598
  });
1605
1599
  preferOauthToken(merged);
1600
+ merged["CLAUDE_CODE_DISABLE_AUTO_MEMORY"] = "1";
1606
1601
  return merged;
1607
1602
  }
1608
1603
  let nativeBinaryWarned = false;
@@ -1624,7 +1619,7 @@ function formatDuration$1(ms) {
1624
1619
  return `${minutes} minute${minutes === 1 ? "" : "s"}`;
1625
1620
  }
1626
1621
  async function invokeClaudeStreaming(options, onEvent) {
1627
- const { prompt, systemPrompt, allowedTools, disableBuiltinTools = false, disableThinking = false, mcpServers, maxTurns, timeoutMs, env, model, cwd, onAbAction, onAbActionFailed, silenceBashLog = false, envScrubMap = [], relaxAbConstraints = false } = options;
1622
+ const { prompt, systemPrompt, allowedTools, disableThinking = false, mcpServers, maxTurns, timeoutMs, env, model, cwd, onAbAction, onAbActionFailed, silenceBashLog = false, envScrubMap = [], relaxAbConstraints = false } = options;
1628
1623
  const resolvedModel = resolveModel(model);
1629
1624
  const mergedEnv = buildInvocationEnv(env);
1630
1625
  const abortController = new AbortController();
@@ -1637,15 +1632,17 @@ async function invokeClaudeStreaming(options, onEvent) {
1637
1632
  const sdkOptions = {
1638
1633
  systemPrompt,
1639
1634
  maxTurns,
1640
- allowedTools: allowedTools ?? ["Bash(*)"],
1635
+ allowedTools,
1636
+ tools: builtinToolNames(allowedTools),
1637
+ strictMcpConfig: true,
1638
+ settingSources: [],
1641
1639
  permissionMode: "bypassPermissions",
1642
1640
  allowDangerouslySkipPermissions: true,
1643
1641
  abortController,
1644
1642
  ...resolvedModel ? { model: resolvedModel } : {},
1645
1643
  ...cwd ? { cwd } : {},
1646
- ...mergedEnv ? { env: mergedEnv } : {},
1644
+ env: mergedEnv,
1647
1645
  ...mcpServers ? { mcpServers } : {},
1648
- ...disableBuiltinTools ? { tools: [] } : {},
1649
1646
  ...disableThinking ? { thinking: { type: "disabled" } } : {},
1650
1647
  hooks: onAbAction || onAbActionFailed ? {
1651
1648
  PreToolUse: [{ hooks: [async (input) => {
@@ -1774,8 +1771,9 @@ function extractInvocationCost(msg) {
1774
1771
  const usage = m["usage"];
1775
1772
  const modelUsage = m["modelUsage"];
1776
1773
  const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
1774
+ const models = modelUsage && typeof modelUsage === "object" ? Object.keys(modelUsage) : [];
1777
1775
  return {
1778
- totalCostUsd: num(m["total_cost_usd"]),
1776
+ totalCostUsd: pricedForClaude(models) ? num(m["total_cost_usd"]) : null,
1779
1777
  durationMs: num(m["duration_ms"]),
1780
1778
  durationApiMs: num(m["duration_api_ms"]),
1781
1779
  numTurns: num(m["num_turns"]),
@@ -1783,9 +1781,16 @@ function extractInvocationCost(msg) {
1783
1781
  cacheCreationInputTokens: num(usage?.["cache_creation_input_tokens"]),
1784
1782
  cacheReadInputTokens: num(usage?.["cache_read_input_tokens"]),
1785
1783
  outputTokens: num(usage?.["output_tokens"]),
1786
- models: modelUsage && typeof modelUsage === "object" ? Object.keys(modelUsage) : []
1784
+ models
1787
1785
  };
1788
1786
  }
1787
+ /**
1788
+ * The SDK prices an unknown model id at a default Claude rate rather than
1789
+ * returning null, so a self-hosted model would report dollars nobody is billed.
1790
+ */
1791
+ function pricedForClaude(models) {
1792
+ return models.every((id) => /claude/i.test(id));
1793
+ }
1789
1794
  const BLOCKED_AB_SUBCOMMANDS = new Set([
1790
1795
  "eval",
1791
1796
  "js",
@@ -5015,12 +5020,11 @@ const DraftNamingSchema = z.object({
5015
5020
  * Returns null only when the invocation reported nothing at all (a mock run,
5016
5021
  * an SDK error, or a command that never called a model).
5017
5022
  *
5018
- * The price is one segment among several, not a precondition. An endpoint the
5019
- * SDK has no pricing table for any Anthropic-compatible gateway in front of
5020
- * a third-party model reports usage but no `total_cost_usd`, and dropping
5021
- * the whole line there would hide real consumption behind silence. Tokens come
5022
- * from the API response rather than a price list, so they survive that case
5023
- * and become the signal to read.
5023
+ * The price is one segment among several, not a precondition. A model that is
5024
+ * not a Claude model has no price (`extractInvocationCost` drops the SDK's
5025
+ * estimate), and dropping the whole line there would hide real consumption
5026
+ * behind silence. Tokens come from the API response rather than a price list,
5027
+ * so they survive that case and become the signal to read.
5024
5028
  *
5025
5029
  * `compact: false` (default for CLI logs) keeps raw numbers and adds a
5026
5030
  * `model=...` segment. `compact: true` (HTML chip) thousand-separates fresh
@@ -6353,6 +6357,14 @@ function resolveCovered(prepared, ranges) {
6353
6357
  const VENDOR = /(^|\/)node_modules\//;
6354
6358
  /** `(rsc)`, `(pages-dir-browser)`, ... — which build layer, not part of the path. */
6355
6359
  const LAYER = /^\([^)]*\)\//;
6360
+ /**
6361
+ * What follows `[project]/` is relative to the build's project root, not to
6362
+ * where the build ran. Only this one root is stripped: Turbopack's others
6363
+ * (`[output]`, `[externals]`) name generated code, not files on disk.
6364
+ */
6365
+ const TURBOPACK_PROJECT = /^\[project\]\//;
6366
+ /** The ` [layer] (ecmascript)` suffix Turbopack appends when it names a module. */
6367
+ const MODULE_IDENTIFIER = / \[[^\]]*\]/;
6356
6368
  const DEPENDENCY = { kind: "dependency" };
6357
6369
  const UNRESOLVED = { kind: "unresolved" };
6358
6370
  /** `absolute` as a posix path under `root`, or undefined when it is not under it. */
@@ -6371,9 +6383,12 @@ function normalizeSourcePath(raw, roots) {
6371
6383
  path = slash < 0 ? afterScheme : afterScheme.slice(from);
6372
6384
  }
6373
6385
  path = posix.normalize(path.replace(LAYER, ""));
6386
+ const rooted = TURBOPACK_PROJECT.test(path);
6387
+ if (rooted) path = path.replace(TURBOPACK_PROJECT, "");
6374
6388
  if (path === "" || path === ".") return UNRESOLVED;
6375
6389
  if (path.startsWith("<") || path.startsWith("[")) return UNRESOLVED;
6376
- const absolute = path.startsWith("/") ? path : resolve(roots.base, path);
6390
+ if (rooted && MODULE_IDENTIFIER.test(path)) return UNRESOLVED;
6391
+ const absolute = path.startsWith("/") ? path : resolve(rooted ? roots.root : roots.base, path);
6377
6392
  const rel = toProjectRelative(roots.root, absolute);
6378
6393
  if (rel === void 0) return UNRESOLVED;
6379
6394
  return VENDOR.test(rel) ? DEPENDENCY : {
@@ -6575,18 +6590,14 @@ var FrontendResolution = class {
6575
6590
  * nearly all of them misses on a deployment that pushes nothing.
6576
6591
  */
6577
6592
  sourceMapLoaders(script, source) {
6578
- const loaders = [];
6579
6593
  const reference = readSourceMappingUrl(source);
6580
- if (reference !== void 0) {
6581
- const inline = decodeInlineSourceMap(reference);
6582
- if (inline !== void 0) loaders.push(() => Promise.resolve(inline));
6583
- else {
6584
- const target = absoluteOrUndefined(reference, script.url);
6585
- if (target !== void 0) loaders.push(() => this.fetchText(target));
6586
- }
6587
- }
6594
+ const inline = reference === void 0 ? void 0 : decodeInlineSourceMap(reference);
6595
+ const mapUrl = reference !== void 0 && inline === void 0 ? absoluteOrUndefined(reference, script.url) : void 0;
6596
+ const loaders = [];
6597
+ if (inline !== void 0) loaders.push(() => Promise.resolve(inline));
6598
+ if (mapUrl !== void 0) loaders.push(() => this.fetchText(mapUrl));
6588
6599
  const stored = this.fetchStoredMap;
6589
- if (stored !== void 0) loaders.push(() => stored(script.url));
6600
+ if (stored !== void 0) loaders.push(() => stored(mapUrl ?? script.url, script.url));
6590
6601
  return loaders;
6591
6602
  }
6592
6603
  };
@@ -7191,7 +7202,7 @@ var Engine = class {
7191
7202
  coverageDir: opts.coverageDir,
7192
7203
  roots: opts.roots,
7193
7204
  fetchText: (url) => this.fetchThroughBrowser(url),
7194
- fetchStoredMap: (scriptUrl) => this.storedMapFor(scriptUrl),
7205
+ fetchStoredMap: (mapUrl, scriptUrl) => this.storedMapFor(mapUrl, scriptUrl),
7195
7206
  warn: opts.warn
7196
7207
  });
7197
7208
  }
@@ -7458,19 +7469,24 @@ var Engine = class {
7458
7469
  }
7459
7470
  }
7460
7471
  /**
7461
- * The map a deploy stored for `scriptUrl`, if this run knows where to ask.
7472
+ * The map a deploy stored for a script, if this run knows where to ask.
7462
7473
  * Addressed by the path under the asset origin rather than the URL, so the
7463
7474
  * push and the read agree without either knowing the other's host.
7475
+ *
7476
+ * The script's pointer names the map, but it may name it somewhere this run
7477
+ * never declared — a map-only host. Guessing from the script is worse than
7478
+ * the pointer and better than not asking, so it stays as the fallback.
7464
7479
  */
7465
- async storedMapFor(scriptUrl) {
7480
+ async storedMapFor(mapUrl, scriptUrl) {
7466
7481
  const stored = this.opts.fetchStoredSourceMap;
7467
7482
  if (stored === void 0) return void 0;
7468
- const assetPath = assetPathOf(scriptUrl, [...this.opts.origins, ...this.opts.assetOrigins ?? []]);
7469
- if (assetPath === void 0) return void 0;
7483
+ const origins = [...this.opts.origins, ...this.opts.assetOrigins ?? []];
7484
+ const key = storeKeyOf(mapUrl, origins) ?? storeKeyOf(scriptUrl, origins);
7485
+ if (key === void 0) return void 0;
7470
7486
  try {
7471
- return await stored(`${assetPath}.map`);
7487
+ return await stored(key);
7472
7488
  } catch (err) {
7473
- this.opts.warn(`could not read the stored source map for ${assetPath}: ${message(err)}`);
7489
+ this.opts.warn(`could not read the stored source map for ${key}: ${message(err)}`);
7474
7490
  return;
7475
7491
  }
7476
7492
  }
@@ -7494,6 +7510,16 @@ function message(error) {
7494
7510
  * is part of what the browser asked for, so the push has to have used it too.
7495
7511
  * A URL from an origin the run never declared is not ours to look up.
7496
7512
  */
7513
+ /**
7514
+ * The key a stored map is read by: the asset path, with `.map` appended only
7515
+ * when it is missing. A push files each map under its own path, so a URL that
7516
+ * already names a map needs no suffix; only a script URL has to guess one.
7517
+ */
7518
+ function storeKeyOf(url, origins) {
7519
+ const assetPath = assetPathOf(url, origins);
7520
+ if (assetPath === void 0) return void 0;
7521
+ return assetPath.endsWith(".map") ? assetPath : `${assetPath}.map`;
7522
+ }
7497
7523
  function assetPathOf(url, origins) {
7498
7524
  let parsed;
7499
7525
  try {
@@ -13507,7 +13533,6 @@ async function runLiveExecutor(input) {
13507
13533
  prompt: buildStepVerdictPrompt(step, transcript),
13508
13534
  model: input.model,
13509
13535
  allowedTools: [],
13510
- disableBuiltinTools: true,
13511
13536
  disableThinking: true,
13512
13537
  maxTurns: 1,
13513
13538
  timeoutMs: VERDICT_TIMEOUT_MS
@@ -14682,7 +14707,7 @@ async function cleanupActions$1(actions, model) {
14682
14707
  try {
14683
14708
  const { result, isError } = await invokeClaudeStreaming({
14684
14709
  prompt: buildCleanupPrompt(actions),
14685
- disableBuiltinTools: true,
14710
+ allowedTools: [],
14686
14711
  maxTurns: 1,
14687
14712
  model
14688
14713
  }, () => {});
@@ -16611,7 +16636,6 @@ async function updateAgentPrompt(args) {
16611
16636
  prompt: userPrompt,
16612
16637
  systemPrompt,
16613
16638
  allowedTools: [],
16614
- disableBuiltinTools: true,
16615
16639
  disableThinking: true,
16616
16640
  ...model ? { model } : {}
16617
16641
  }, () => {});
@@ -30591,7 +30615,6 @@ function createLearningWorker(deps) {
30591
30615
  prompt: buildLearningUserPrompt(cases.slice(0, LEARNING_MAX_CASES)),
30592
30616
  systemPrompt: LEARNING_SYSTEM_PROMPT,
30593
30617
  allowedTools: [],
30594
- disableBuiltinTools: true,
30595
30618
  maxTurns: 1
30596
30619
  }, () => {});
30597
30620
  const guidance = result?.trim();
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.42.1",
3
+ "version": "1.43.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.42.1",
3
+ "version": "1.43.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {