ccqa 1.42.2 → 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
@@ -13529,7 +13533,6 @@ async function runLiveExecutor(input) {
13529
13533
  prompt: buildStepVerdictPrompt(step, transcript),
13530
13534
  model: input.model,
13531
13535
  allowedTools: [],
13532
- disableBuiltinTools: true,
13533
13536
  disableThinking: true,
13534
13537
  maxTurns: 1,
13535
13538
  timeoutMs: VERDICT_TIMEOUT_MS
@@ -14704,7 +14707,7 @@ async function cleanupActions$1(actions, model) {
14704
14707
  try {
14705
14708
  const { result, isError } = await invokeClaudeStreaming({
14706
14709
  prompt: buildCleanupPrompt(actions),
14707
- disableBuiltinTools: true,
14710
+ allowedTools: [],
14708
14711
  maxTurns: 1,
14709
14712
  model
14710
14713
  }, () => {});
@@ -16633,7 +16636,6 @@ async function updateAgentPrompt(args) {
16633
16636
  prompt: userPrompt,
16634
16637
  systemPrompt,
16635
16638
  allowedTools: [],
16636
- disableBuiltinTools: true,
16637
16639
  disableThinking: true,
16638
16640
  ...model ? { model } : {}
16639
16641
  }, () => {});
@@ -30613,7 +30615,6 @@ function createLearningWorker(deps) {
30613
30615
  prompt: buildLearningUserPrompt(cases.slice(0, LEARNING_MAX_CASES)),
30614
30616
  systemPrompt: LEARNING_SYSTEM_PROMPT,
30615
30617
  allowedTools: [],
30616
- disableBuiltinTools: true,
30617
30618
  maxTurns: 1
30618
30619
  }, () => {});
30619
30620
  const guidance = result?.trim();
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.42.2",
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.2",
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": {