ccqa 0.12.0 → 0.13.1

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
@@ -1168,9 +1168,44 @@ function resolveModel(explicit) {
1168
1168
  const envModel = process.env["CCQA_MODEL"];
1169
1169
  return envModel && envModel.length > 0 ? envModel : void 0;
1170
1170
  }
1171
+ /**
1172
+ * Standard Claude Code environment variables that select the API endpoint and
1173
+ * credentials. ccqa forwards whichever of these are set to the underlying
1174
+ * Claude Code process; it does not read or interpret their values.
1175
+ *
1176
+ * - `ANTHROPIC_BASE_URL` — the API endpoint to send requests to.
1177
+ * - `ANTHROPIC_AUTH_TOKEN` — sent as `Authorization: Bearer <token>`.
1178
+ * - `ANTHROPIC_API_KEY` — API key, when used instead of a token.
1179
+ * - `ANTHROPIC_CUSTOM_HEADERS` — extra request headers.
1180
+ */
1181
+ const ENDPOINT_ENV_KEYS = [
1182
+ "ANTHROPIC_BASE_URL",
1183
+ "ANTHROPIC_AUTH_TOKEN",
1184
+ "ANTHROPIC_API_KEY",
1185
+ "ANTHROPIC_CUSTOM_HEADERS"
1186
+ ];
1187
+ /**
1188
+ * Collects the endpoint/auth variables set in the current process environment
1189
+ * so they can be forwarded, verbatim, to every Claude Code invocation. Returns
1190
+ * only the keys that are actually set (non-empty), so unset variables never
1191
+ * override the SDK's own defaults.
1192
+ */
1193
+ function resolveEndpointEnv() {
1194
+ const endpointEnv = {};
1195
+ for (const key of ENDPOINT_ENV_KEYS) {
1196
+ const value = process.env[key];
1197
+ if (value && value.length > 0) endpointEnv[key] = value;
1198
+ }
1199
+ return endpointEnv;
1200
+ }
1171
1201
  async function invokeClaudeStreaming(options, onEvent) {
1172
1202
  const { prompt, systemPrompt, allowedTools, disableBuiltinTools = false, maxTurns, env, model, cwd, onAbAction, onAbActionFailed, silenceBashLog = false, relaxAbConstraints = false } = options;
1173
1203
  const resolvedModel = resolveModel(model);
1204
+ const hasEndpointEnv = Object.keys(resolveEndpointEnv()).length > 0;
1205
+ const mergedEnv = env || hasEndpointEnv ? {
1206
+ ...process.env,
1207
+ ...env
1208
+ } : void 0;
1174
1209
  let lastAbToolUseId = null;
1175
1210
  const claimAbToolUse = (toolUseId) => {
1176
1211
  if (toolUseId !== lastAbToolUseId) return false;
@@ -1185,10 +1220,7 @@ async function invokeClaudeStreaming(options, onEvent) {
1185
1220
  allowDangerouslySkipPermissions: true,
1186
1221
  ...resolvedModel ? { model: resolvedModel } : {},
1187
1222
  ...cwd ? { cwd } : {},
1188
- ...env ? { env: {
1189
- ...process.env,
1190
- ...env
1191
- } } : {},
1223
+ ...mergedEnv ? { env: mergedEnv } : {},
1192
1224
  ...disableBuiltinTools ? { tools: [] } : {},
1193
1225
  hooks: onAbAction || onAbActionFailed ? {
1194
1226
  PreToolUse: [{ hooks: [async (input) => {
@@ -3199,6 +3231,44 @@ function resolveProjectOrThrow(project, cwd) {
3199
3231
  */
3200
3232
  const hubUrlOption = ["--hub-url <url>", "ccqa hub base URL (or CCQA_HUB_URL)."];
3201
3233
  const hubTokenOption = ["--hub-token <token>", "ccqa hub bearer token (or CCQA_HUB_TOKEN)."];
3234
+ const hubHeaderOption = [
3235
+ "--hub-header <header>",
3236
+ "Extra header sent with every hub request, as \"key:value\" (or CCQA_HUB_HEADER). Repeatable. For infra that gates the hub behind a header check (e.g. a load balancer bypass rule).",
3237
+ collectHubHeader,
3238
+ []
3239
+ ];
3240
+ /** commander `--hub-header` accumulator: collects repeated flags into an array. */
3241
+ function collectHubHeader(value, previous) {
3242
+ return [...previous, value];
3243
+ }
3244
+ /**
3245
+ * Parse `"key:value"` entries (from `--hub-header`, repeatable) into a
3246
+ * headers map. The value may itself contain `:` (e.g. a URL), so only the
3247
+ * first colon is treated as the separator. Throws on an entry with no colon.
3248
+ */
3249
+ function parseHubHeaders(entries) {
3250
+ const headers = {};
3251
+ for (const entry of entries) {
3252
+ const i = entry.indexOf(":");
3253
+ if (i < 0) throw new Error(`invalid --hub-header (expected "key:value"): ${entry}`);
3254
+ const key = entry.slice(0, i).trim();
3255
+ const value = entry.slice(i + 1).trim();
3256
+ if (!key) throw new Error(`invalid --hub-header (expected "key:value"): ${entry}`);
3257
+ headers[key] = value;
3258
+ }
3259
+ return headers;
3260
+ }
3261
+ /**
3262
+ * Resolve custom hub headers from flags, falling back to `CCQA_HUB_HEADER`
3263
+ * (a single `"key:value"` entry) when no `--hub-header` flag was given.
3264
+ * CI setups typically pass exactly one gateway header via env; `--hub-header`
3265
+ * is repeatable for the (rarer) local/multi-header case.
3266
+ */
3267
+ function resolveHubHeaders(hubHeader) {
3268
+ if (hubHeader && hubHeader.length > 0) return parseHubHeaders(hubHeader);
3269
+ const envHeader = process.env.CCQA_HUB_HEADER;
3270
+ if (envHeader) return parseHubHeaders([envHeader]);
3271
+ }
3202
3272
  /** Thrown by `requireHubClient` when the URL and/or token can't be resolved from flags/env. */
3203
3273
  var HubConnectionError = class extends Error {
3204
3274
  constructor(message) {
@@ -3215,9 +3285,11 @@ function resolveHubClient(opts) {
3215
3285
  const baseUrl = opts.hubUrl ?? process.env.CCQA_HUB_URL;
3216
3286
  const token = opts.hubToken ?? process.env.CCQA_HUB_TOKEN;
3217
3287
  if (!baseUrl || !token) return null;
3288
+ const headers = resolveHubHeaders(opts.hubHeader);
3218
3289
  return createHubClient({
3219
3290
  baseUrl: baseUrl.replace(/\/+$/, ""),
3220
- token
3291
+ token,
3292
+ ...headers ? { headers } : {}
3221
3293
  });
3222
3294
  }
3223
3295
  /** Same as `resolveHubClient`, but throws `HubConnectionError` instead of returning `null`. */
@@ -3286,7 +3358,7 @@ function addProfileOption(command) {
3286
3358
  * options so help text and behaviour don't drift.
3287
3359
  */
3288
3360
  function addHubOptions(command) {
3289
- return command.option(...hubUrlOption).option(...hubTokenOption);
3361
+ return command.option(...hubUrlOption).option(...hubTokenOption).option(...hubHeaderOption);
3290
3362
  }
3291
3363
  /**
3292
3364
  * CLI wrapper around `resolveProfileEnv`: on failure, print the error and
@@ -5218,7 +5290,8 @@ async function executeRun(targets, opts) {
5218
5290
  project: projectForProfile,
5219
5291
  cwd,
5220
5292
  hubUrl: opts.hubUrl,
5221
- hubToken: opts.hubToken
5293
+ hubToken: opts.hubToken,
5294
+ hubHeader: opts.hubHeader
5222
5295
  });
5223
5296
  } else await resolveProfileEnv({
5224
5297
  profile: void 0,
@@ -5236,6 +5309,7 @@ async function executeRun(targets, opts) {
5236
5309
  hubCtx = resolveHubContext({
5237
5310
  hubUrl: opts.hubUrl,
5238
5311
  hubToken: opts.hubToken,
5312
+ hubHeader: opts.hubHeader,
5239
5313
  project: opts.project,
5240
5314
  cwd
5241
5315
  });
@@ -8597,7 +8671,8 @@ const recordCommand = addHubOptions(addProfileOption(addLanguageOption(new Comma
8597
8671
  project,
8598
8672
  cwd: cwdForProfile,
8599
8673
  hubUrl: opts.hubUrl,
8600
- hubToken: opts.hubToken
8674
+ hubToken: opts.hubToken,
8675
+ hubHeader: opts.hubHeader
8601
8676
  });
8602
8677
  else await applyProfileFromOption({
8603
8678
  profile: void 0,
@@ -8606,7 +8681,8 @@ const recordCommand = addHubOptions(addProfileOption(addLanguageOption(new Comma
8606
8681
  });
8607
8682
  const hubClientForTrace = resolveHubClient({
8608
8683
  hubUrl: opts.hubUrl,
8609
- hubToken: opts.hubToken
8684
+ hubToken: opts.hubToken,
8685
+ hubHeader: opts.hubHeader
8610
8686
  });
8611
8687
  const hubContext = hubClientForTrace && project ? {
8612
8688
  hub: hubClientForTrace,
@@ -9325,7 +9401,7 @@ Return the spec keys that might be affected by any of the new files. Conservativ
9325
9401
  //#endregion
9326
9402
  //#region src/cli/drift.ts
9327
9403
  const DEFAULT_CONCURRENCY = 3;
9328
- const driftCommand = addLanguageOption(new Command("drift").argument("[feature/spec]", "Optional spec id. If omitted, every spec under .ccqa/features/ is checked.").description("Standalone spec ↔ codebase static audit. Use for PR checks where the browser isn't run. For run-time audit with a structured report, see `ccqa run --report`.").option("--format <fmt>", "Output format: text | json | github", "text").option("--severity <level>", "Exit non-zero on this severity or higher: warn | error", "error").option("--concurrency <n>", `Parallel spec checks (default: ${DEFAULT_CONCURRENCY})`).option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--cwd <path>", "Working directory used as both the .ccqa root and the codebase Claude reads. Useful for monorepos. Defaults to process.cwd().").option("--changed", "Restrict drift checks to specs whose relatedPaths intersect the git diff against --base (or, in CI, $GITHUB_BASE_REF, else origin/main). New files are routed to specs via a single lightweight Claude call.").option("--base <ref>", "Base ref to diff against when --changed is set. Defaults to $GITHUB_BASE_REF (CI) or origin/main.").option("--push", "Push the drift result to a ccqa hub as a run (kind: drift).").option("--project <name>", "Logical project name for the pushed run. Defaults to the current directory's name.").option(...hubUrlOption).option(...hubTokenOption)).action(async (specPath, opts) => {
9404
+ const driftCommand = addLanguageOption(new Command("drift").argument("[feature/spec]", "Optional spec id. If omitted, every spec under .ccqa/features/ is checked.").description("Standalone spec ↔ codebase static audit. Use for PR checks where the browser isn't run. For run-time audit with a structured report, see `ccqa run --report`.").option("--format <fmt>", "Output format: text | json | github", "text").option("--severity <level>", "Exit non-zero on this severity or higher: warn | error", "error").option("--concurrency <n>", `Parallel spec checks (default: ${DEFAULT_CONCURRENCY})`).option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--cwd <path>", "Working directory used as both the .ccqa root and the codebase Claude reads. Useful for monorepos. Defaults to process.cwd().").option("--changed", "Restrict drift checks to specs whose relatedPaths intersect the git diff against --base (or, in CI, $GITHUB_BASE_REF, else origin/main). New files are routed to specs via a single lightweight Claude call.").option("--base <ref>", "Base ref to diff against when --changed is set. Defaults to $GITHUB_BASE_REF (CI) or origin/main.").option("--push", "Push the drift result to a ccqa hub as a run (kind: drift).").option("--project <name>", "Logical project name for the pushed run. Defaults to the current directory's name.").option(...hubUrlOption).option(...hubTokenOption).option(...hubHeaderOption)).action(async (specPath, opts) => {
9329
9405
  const format = parseFormat(opts.format);
9330
9406
  const threshold = parseSeverity(opts.severity);
9331
9407
  const concurrency = parseConcurrency(opts.concurrency);
@@ -11597,7 +11673,7 @@ const CSS = `
11597
11673
  order decides). */
11598
11674
  .chip.kind-chip { color: var(--violet); background: var(--violet-bg); border-color: var(--violet-border); font-family: var(--font); margin-left: 6px; }
11599
11675
  .chip.drift-count-chip { color: var(--amber); background: var(--amber-bg); border-color: var(--amber-border); margin-left: 6px; }
11600
- .chip.drift-errors-chip { color: var(--fail); background: var(--fail-bg); border-color: var(--fail-border); }
11676
+ .chip.drift-errors-chip { color: var(--fail); background: var(--fail-bg); border-color: var(--fail-border); margin-left: 6px; }
11601
11677
  .drift-meta-box { display: flex; flex-direction: column; gap: 4px; }
11602
11678
  .drift-meta-chips { display: flex; gap: 6px; }
11603
11679
  .specs { display: inline-flex; align-items: center; gap: 9px; }
@@ -181,6 +181,12 @@ type LabelsExport = z.infer<typeof LabelsExportSchema>;
181
181
  interface HubClientOptions {
182
182
  baseUrl: string;
183
183
  token: string;
184
+ /**
185
+ * Extra headers sent on every request, ahead of any per-call headers and
186
+ * the `Authorization` header (e.g. an infra gateway/ALB bypass header in
187
+ * CI — never overrides `Authorization`).
188
+ */
189
+ headers?: Record<string, string>;
184
190
  /** Override for testing; defaults to the global `fetch`. */
185
191
  fetchImpl?: typeof fetch;
186
192
  }
@@ -51,6 +51,7 @@ function createHubClient(opts) {
51
51
  ...init,
52
52
  signal,
53
53
  headers: {
54
+ ...opts.headers,
54
55
  ...init.headers,
55
56
  Authorization: `Bearer ${opts.token}`
56
57
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
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": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {