ccqa 1.42.2 → 1.44.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 +321 -210
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { t as EVIDENCE_DIR_ENV } from "../evidence-constants-C425F7ZG.mjs";
|
|
|
4
4
|
import { a as formatAgentBrowserUnavailableMessage, i as assertAgentBrowserAvailable, n as spawnAB, o as pathWithAgentBrowserShim, r as AgentBrowserUnavailableError, s as resolveAgentBrowserBin$1, t as sleepSync } from "../spawn-ab-Bm34WBui.mjs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { Command } from "commander";
|
|
7
|
-
import { accessSync, appendFileSync, createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { accessSync, appendFileSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
9
|
import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
10
10
|
import { access, appendFile, chmod, cp, mkdir, mkdtemp, open, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
@@ -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
|
-
*
|
|
893
|
-
*
|
|
894
|
-
*
|
|
895
|
-
*
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
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
|
|
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:
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
5019
|
-
*
|
|
5020
|
-
*
|
|
5021
|
-
*
|
|
5022
|
-
*
|
|
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
|
|
@@ -11982,6 +11986,14 @@ async function resolveCoverageRoots(changed, cwd) {
|
|
|
11982
11986
|
*/
|
|
11983
11987
|
const MAX_REPORT_RUNS = 20;
|
|
11984
11988
|
/**
|
|
11989
|
+
* How many reads one source keeps in flight. The two sources run together, so
|
|
11990
|
+
* the hub sees twice this. All at once is what it cannot take: it serves one
|
|
11991
|
+
* process, a resolve walks the whole event stream, and a report carries its
|
|
11992
|
+
* screenshots inline — forty of those together is what took it down, rather
|
|
11993
|
+
* than any single one of them.
|
|
11994
|
+
*/
|
|
11995
|
+
const HUB_READ_CONCURRENCY = 4;
|
|
11996
|
+
/**
|
|
11985
11997
|
* Read every spec's most recent measured reach from the hub. Never throws: a
|
|
11986
11998
|
* source that cannot be read warns, and `degraded` flips when the failure
|
|
11987
11999
|
* leaves absence ambiguous (the ledger itself, or the legacy sources while
|
|
@@ -12049,8 +12061,14 @@ async function collectStreamEdges(input, merge) {
|
|
|
12049
12061
|
const { hub, project } = input;
|
|
12050
12062
|
const latest = await hub.getCoverage(project);
|
|
12051
12063
|
ingestResolved(latest.resolved, merge);
|
|
12052
|
-
|
|
12053
|
-
|
|
12064
|
+
return (await runPool(latest.runIds.filter((runId) => runId !== latest.resolved?.runId), HUB_READ_CONCURRENCY, async (runId) => {
|
|
12065
|
+
try {
|
|
12066
|
+
ingestResolved((await hub.getCoverage(project, { runId })).resolved, merge);
|
|
12067
|
+
return true;
|
|
12068
|
+
} catch {
|
|
12069
|
+
return false;
|
|
12070
|
+
}
|
|
12071
|
+
})).filter((ok) => !ok).length;
|
|
12054
12072
|
}
|
|
12055
12073
|
function ingestResolved(resolved, merge) {
|
|
12056
12074
|
if (!resolved) return;
|
|
@@ -12082,7 +12100,7 @@ const ReportCoverageRowsSchema = z.object({ results: z.array(z.object({
|
|
|
12082
12100
|
*/
|
|
12083
12101
|
async function collectReportEdges(input, merge) {
|
|
12084
12102
|
const { hub, project } = input;
|
|
12085
|
-
|
|
12103
|
+
return (await runPool((await hub.listRuns({
|
|
12086
12104
|
project,
|
|
12087
12105
|
kind: "run",
|
|
12088
12106
|
limit: MAX_REPORT_RUNS
|
|
@@ -12094,18 +12112,22 @@ async function collectReportEdges(input, merge) {
|
|
|
12094
12112
|
id: run.id,
|
|
12095
12113
|
measuredAt
|
|
12096
12114
|
}];
|
|
12097
|
-
})
|
|
12098
|
-
|
|
12099
|
-
|
|
12100
|
-
|
|
12101
|
-
|
|
12102
|
-
|
|
12103
|
-
|
|
12104
|
-
|
|
12105
|
-
|
|
12106
|
-
|
|
12115
|
+
}), HUB_READ_CONCURRENCY, async ({ id, measuredAt }) => {
|
|
12116
|
+
try {
|
|
12117
|
+
const parsed = ReportCoverageRowsSchema.safeParse(await hub.getReport(id));
|
|
12118
|
+
if (!parsed.success) return true;
|
|
12119
|
+
for (const row of parsed.data.results) {
|
|
12120
|
+
if (!row.coverage || row.coverage.files.length === 0) continue;
|
|
12121
|
+
merge(`${row.feature}/${row.spec}`, {
|
|
12122
|
+
files: row.coverage.files,
|
|
12123
|
+
measuredAt
|
|
12124
|
+
});
|
|
12125
|
+
}
|
|
12126
|
+
return true;
|
|
12127
|
+
} catch {
|
|
12128
|
+
return false;
|
|
12107
12129
|
}
|
|
12108
|
-
}))
|
|
12130
|
+
})).filter((ok) => !ok).length;
|
|
12109
12131
|
}
|
|
12110
12132
|
//#endregion
|
|
12111
12133
|
//#region src/select/inventory.ts
|
|
@@ -12220,6 +12242,27 @@ function foldTouchIndex(current, entry, selection) {
|
|
|
12220
12242
|
}
|
|
12221
12243
|
return out;
|
|
12222
12244
|
}
|
|
12245
|
+
//#endregion
|
|
12246
|
+
//#region src/coverage/resolve-stream.ts
|
|
12247
|
+
/**
|
|
12248
|
+
* One run's answer out of a project's stored event stream (ADR-0022).
|
|
12249
|
+
*
|
|
12250
|
+
* The stream interleaves many runs and two producers; this is the single
|
|
12251
|
+
* shared interpretation over it — the same gate, join and loss accounting the
|
|
12252
|
+
* run-local sink applies, executed at read time by whichever host asks (the
|
|
12253
|
+
* hub's API, or the CLI). Everything here is a pure fold over the stamps the
|
|
12254
|
+
* events carry: no clock is ever read, so the same stream resolves to the
|
|
12255
|
+
* same answer on any host, at any time.
|
|
12256
|
+
*/
|
|
12257
|
+
/**
|
|
12258
|
+
* How far past a run's last marker an application push still counts as its
|
|
12259
|
+
* audience (ms). The run-local sink keeps listening while a spec settles, so
|
|
12260
|
+
* the work a spec's last action triggered still lands; a stored stream has no
|
|
12261
|
+
* listener to wait, so the resolve re-creates that patience as a fixed bound.
|
|
12262
|
+
* Pushes past it were heard by nobody bound to this run — most likely they
|
|
12263
|
+
* belong to whatever run came next.
|
|
12264
|
+
*/
|
|
12265
|
+
const GRACE_MS = 3e4;
|
|
12223
12266
|
z.object({
|
|
12224
12267
|
runId: z.string(),
|
|
12225
12268
|
hubRunId: z.string().optional(),
|
|
@@ -12257,88 +12300,109 @@ function formatResolvedSpec(spec) {
|
|
|
12257
12300
|
return `${spec.specId}: ${spec.files.length} file(s)${actors ? ` (${actors})` : ""}`;
|
|
12258
12301
|
}
|
|
12259
12302
|
/**
|
|
12260
|
-
*
|
|
12303
|
+
* `runId`'s view of the stream, built one event at a time.
|
|
12261
12304
|
*
|
|
12262
|
-
* Two passes, because the resolver needs its context up front
|
|
12305
|
+
* Two passes, because the resolver needs its context up front. The first
|
|
12263
12306
|
* collects what the run's own markers establish — which ids it issued, which
|
|
12264
12307
|
* identity tags were its to hand out, its universe, and when its first and
|
|
12265
|
-
* last marker arrived
|
|
12266
|
-
*
|
|
12267
|
-
*
|
|
12268
|
-
*
|
|
12269
|
-
*
|
|
12270
|
-
*
|
|
12271
|
-
* this run's to claim — but the
|
|
12272
|
-
* run acked, so on an always-on hub
|
|
12273
|
-
* figures arrived long before this run
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
|
|
12283
|
-
|
|
12284
|
-
|
|
12285
|
-
|
|
12286
|
-
|
|
12287
|
-
|
|
12288
|
-
|
|
12289
|
-
|
|
12290
|
-
|
|
12291
|
-
|
|
12292
|
-
|
|
12293
|
-
|
|
12308
|
+
* last marker arrived; markers are a small part of a stream, so a caller can
|
|
12309
|
+
* hold them. The second replays the stream through the shared resolver: this
|
|
12310
|
+
* run's window markers as they came, and every application push — as-is when
|
|
12311
|
+
* its stamp falls inside the span the run's sink would have been listening
|
|
12312
|
+
* (first marker to last marker plus `GRACE_MS`), stripped of its spec and
|
|
12313
|
+
* actor attribution when it does not. A push outside the span was another
|
|
12314
|
+
* run's audience, so its attribution is not this run's to claim — but the
|
|
12315
|
+
* collector never re-sends what an earlier run acked, so on an always-on hub
|
|
12316
|
+
* the boot set and each process's health figures arrived long before this run
|
|
12317
|
+
* began, and only survive here.
|
|
12318
|
+
*
|
|
12319
|
+
* The constructor takes the markers and `accept` takes one event at a time, so
|
|
12320
|
+
* the second pass can be fed from a stream: it keeps nothing per push, which
|
|
12321
|
+
* is what lets a resolve outlive the point where the stream stops fitting in
|
|
12322
|
+
* memory.
|
|
12323
|
+
*/
|
|
12324
|
+
var StreamResolution = class {
|
|
12325
|
+
runId;
|
|
12326
|
+
resolver;
|
|
12327
|
+
/** The ids this run opened, in that order — a `Set` iterates by insertion. */
|
|
12328
|
+
specIds;
|
|
12329
|
+
browserFiles = /* @__PURE__ */ new Map();
|
|
12330
|
+
/**
|
|
12331
|
+
* The stamps in which the run's sink would have been listening. Undefined
|
|
12332
|
+
* when the markers held no event of this run — then no push is ever inside.
|
|
12333
|
+
*/
|
|
12334
|
+
span;
|
|
12335
|
+
universe;
|
|
12336
|
+
hubRunId;
|
|
12337
|
+
asOf = 0;
|
|
12338
|
+
lastSeq = 0;
|
|
12339
|
+
pushesDuringRun = 0;
|
|
12340
|
+
/** `markers` needs to hold every marker event of the stream; pushes are ignored here. */
|
|
12341
|
+
constructor(markers, runId) {
|
|
12342
|
+
this.runId = runId;
|
|
12343
|
+
const specIds = /* @__PURE__ */ new Set();
|
|
12344
|
+
const tagToKey = /* @__PURE__ */ new Map();
|
|
12345
|
+
let firstAt;
|
|
12346
|
+
let lastAt = 0;
|
|
12347
|
+
for (const event of markers) {
|
|
12348
|
+
const body = event.body;
|
|
12349
|
+
if (!("kind" in body) || body.runId !== runId) continue;
|
|
12350
|
+
if (firstAt === void 0) firstAt = event.at;
|
|
12351
|
+
lastAt = event.at;
|
|
12352
|
+
switch (body.kind) {
|
|
12353
|
+
case "spec-open":
|
|
12354
|
+
specIds.add(body.specId);
|
|
12355
|
+
break;
|
|
12356
|
+
case "window-open":
|
|
12357
|
+
tagToKey.set(body.tag, body.key);
|
|
12358
|
+
break;
|
|
12359
|
+
case "universe":
|
|
12360
|
+
this.universe = {
|
|
12361
|
+
include: body.include,
|
|
12362
|
+
files: body.files
|
|
12363
|
+
};
|
|
12364
|
+
break;
|
|
12365
|
+
case "run-link":
|
|
12366
|
+
this.hubRunId = body.hubRunId;
|
|
12367
|
+
break;
|
|
12368
|
+
case "browser": {
|
|
12369
|
+
const files = this.browserFiles.get(body.specId) ?? /* @__PURE__ */ new Set();
|
|
12370
|
+
for (const file of body.files) files.add(file);
|
|
12371
|
+
this.browserFiles.set(body.specId, files);
|
|
12372
|
+
break;
|
|
12294
12373
|
}
|
|
12295
|
-
break;
|
|
12296
|
-
case "window-open":
|
|
12297
|
-
tagToKey.set(body.tag, body.key);
|
|
12298
|
-
break;
|
|
12299
|
-
case "universe":
|
|
12300
|
-
universe = {
|
|
12301
|
-
include: body.include,
|
|
12302
|
-
files: body.files
|
|
12303
|
-
};
|
|
12304
|
-
break;
|
|
12305
|
-
case "run-link":
|
|
12306
|
-
hubRunId = body.hubRunId;
|
|
12307
|
-
break;
|
|
12308
|
-
case "browser": {
|
|
12309
|
-
const files = browserFiles.get(body.specId) ?? /* @__PURE__ */ new Set();
|
|
12310
|
-
for (const file of body.files) files.add(file);
|
|
12311
|
-
browserFiles.set(body.specId, files);
|
|
12312
|
-
break;
|
|
12313
12374
|
}
|
|
12314
12375
|
}
|
|
12376
|
+
this.specIds = specIds;
|
|
12377
|
+
this.span = firstAt === void 0 ? void 0 : {
|
|
12378
|
+
from: firstAt,
|
|
12379
|
+
until: lastAt + GRACE_MS
|
|
12380
|
+
};
|
|
12381
|
+
this.resolver = new CoverageResolver(specIds, tagToKey);
|
|
12315
12382
|
}
|
|
12316
|
-
|
|
12317
|
-
|
|
12318
|
-
|
|
12319
|
-
let pushesDuringRun = 0;
|
|
12320
|
-
for (const event of events) {
|
|
12321
|
-
if (event.seq > lastSeq) lastSeq = event.seq;
|
|
12383
|
+
/** One event of the second pass. Every event of the stream, in stamp order. */
|
|
12384
|
+
accept(event) {
|
|
12385
|
+
if (event.seq > this.lastSeq) this.lastSeq = event.seq;
|
|
12322
12386
|
const body = event.body;
|
|
12323
12387
|
if ("kind" in body) {
|
|
12324
|
-
if (body.runId !== runId)
|
|
12325
|
-
asOf = event.at;
|
|
12326
|
-
if (body.kind === "window-open") resolver.apply({
|
|
12388
|
+
if (body.runId !== this.runId) return;
|
|
12389
|
+
this.asOf = event.at;
|
|
12390
|
+
if (body.kind === "window-open") this.resolver.apply({
|
|
12327
12391
|
kind: "window-open",
|
|
12328
12392
|
at: event.at,
|
|
12329
12393
|
tag: body.tag,
|
|
12330
12394
|
key: body.key,
|
|
12331
12395
|
specId: body.specId
|
|
12332
12396
|
});
|
|
12333
|
-
else if (body.kind === "window-close") resolver.apply({
|
|
12397
|
+
else if (body.kind === "window-close") this.resolver.apply({
|
|
12334
12398
|
kind: "window-close",
|
|
12335
12399
|
at: event.at,
|
|
12336
12400
|
tag: body.tag
|
|
12337
12401
|
});
|
|
12338
|
-
|
|
12402
|
+
return;
|
|
12339
12403
|
}
|
|
12340
|
-
if (
|
|
12341
|
-
resolver.apply({
|
|
12404
|
+
if (this.span === void 0 || event.at < this.span.from || event.at > this.span.until) {
|
|
12405
|
+
this.resolver.apply({
|
|
12342
12406
|
kind: "push",
|
|
12343
12407
|
at: event.at,
|
|
12344
12408
|
push: {
|
|
@@ -12347,49 +12411,52 @@ function resolveStream(events, runId) {
|
|
|
12347
12411
|
actors: []
|
|
12348
12412
|
}
|
|
12349
12413
|
});
|
|
12350
|
-
|
|
12414
|
+
return;
|
|
12351
12415
|
}
|
|
12352
|
-
asOf = event.at;
|
|
12353
|
-
pushesDuringRun++;
|
|
12354
|
-
resolver.apply({
|
|
12416
|
+
this.asOf = event.at;
|
|
12417
|
+
this.pushesDuringRun++;
|
|
12418
|
+
this.resolver.apply({
|
|
12355
12419
|
kind: "push",
|
|
12356
12420
|
at: event.at,
|
|
12357
12421
|
push: body
|
|
12358
12422
|
});
|
|
12359
12423
|
}
|
|
12360
|
-
|
|
12361
|
-
|
|
12362
|
-
|
|
12424
|
+
/** The answer as of every event accepted so far. */
|
|
12425
|
+
finish() {
|
|
12426
|
+
const specs = [...this.specIds].map((specId) => {
|
|
12427
|
+
const actorEvents = {};
|
|
12428
|
+
for (const [key, count] of this.resolver.actorEventsFor(specId)) actorEvents[key] = count;
|
|
12429
|
+
return {
|
|
12430
|
+
specId,
|
|
12431
|
+
files: [...new Set([...this.resolver.filesFor(specId) ?? [], ...this.browserFiles.get(specId) ?? []])].sort(),
|
|
12432
|
+
actorEvents
|
|
12433
|
+
};
|
|
12434
|
+
});
|
|
12435
|
+
const outsideWindowEvents = {};
|
|
12436
|
+
for (const [key, count] of this.resolver.outsideWindowEvents()) outsideWindowEvents[key] = count;
|
|
12363
12437
|
return {
|
|
12364
|
-
|
|
12365
|
-
|
|
12366
|
-
|
|
12438
|
+
runId: this.runId,
|
|
12439
|
+
...this.hubRunId !== void 0 ? { hubRunId: this.hubRunId } : {},
|
|
12440
|
+
asOf: this.asOf,
|
|
12441
|
+
lastSeq: this.lastSeq,
|
|
12442
|
+
...this.universe !== void 0 ? { universe: this.universe } : {},
|
|
12443
|
+
specs,
|
|
12444
|
+
boot: [...this.resolver.boot()].sort(),
|
|
12445
|
+
health: {
|
|
12446
|
+
heardFromApplication: this.resolver.heardFromApplication(),
|
|
12447
|
+
pushesDuringRun: this.pushesDuringRun,
|
|
12448
|
+
attributedSpecs: this.resolver.attributedSpecs(),
|
|
12449
|
+
rejectedPushes: this.resolver.rejectedPushes(),
|
|
12450
|
+
uninstrumentedFiles: this.resolver.uninstrumentedFiles(),
|
|
12451
|
+
uninstrumentedProcesses: this.resolver.uninstrumentedProcesses(),
|
|
12452
|
+
droppedPushes: this.resolver.droppedPushes(),
|
|
12453
|
+
unmappedActorEvents: this.resolver.unmappedActorEvents(),
|
|
12454
|
+
outsideWindowEvents,
|
|
12455
|
+
specsMeasured: specs.length
|
|
12456
|
+
}
|
|
12367
12457
|
};
|
|
12368
|
-
}
|
|
12369
|
-
|
|
12370
|
-
for (const [key, count] of resolver.outsideWindowEvents()) outsideWindowEvents[key] = count;
|
|
12371
|
-
return {
|
|
12372
|
-
runId,
|
|
12373
|
-
...hubRunId !== void 0 ? { hubRunId } : {},
|
|
12374
|
-
asOf,
|
|
12375
|
-
lastSeq,
|
|
12376
|
-
...universe !== void 0 ? { universe } : {},
|
|
12377
|
-
specs,
|
|
12378
|
-
boot: [...resolver.boot()].sort(),
|
|
12379
|
-
health: {
|
|
12380
|
-
heardFromApplication: resolver.heardFromApplication(),
|
|
12381
|
-
pushesDuringRun,
|
|
12382
|
-
attributedSpecs: resolver.attributedSpecs(),
|
|
12383
|
-
rejectedPushes: resolver.rejectedPushes(),
|
|
12384
|
-
uninstrumentedFiles: resolver.uninstrumentedFiles(),
|
|
12385
|
-
uninstrumentedProcesses: resolver.uninstrumentedProcesses(),
|
|
12386
|
-
droppedPushes: resolver.droppedPushes(),
|
|
12387
|
-
unmappedActorEvents: resolver.unmappedActorEvents(),
|
|
12388
|
-
outsideWindowEvents,
|
|
12389
|
-
specsMeasured: specs.length
|
|
12390
|
-
}
|
|
12391
|
-
};
|
|
12392
|
-
}
|
|
12458
|
+
}
|
|
12459
|
+
};
|
|
12393
12460
|
/**
|
|
12394
12461
|
* Every run that opened a spec in this stream, most recently heard-from
|
|
12395
12462
|
* first — recency by the arrival position of each run's latest spec-open,
|
|
@@ -13529,7 +13596,6 @@ async function runLiveExecutor(input) {
|
|
|
13529
13596
|
prompt: buildStepVerdictPrompt(step, transcript),
|
|
13530
13597
|
model: input.model,
|
|
13531
13598
|
allowedTools: [],
|
|
13532
|
-
disableBuiltinTools: true,
|
|
13533
13599
|
disableThinking: true,
|
|
13534
13600
|
maxTurns: 1,
|
|
13535
13601
|
timeoutMs: VERDICT_TIMEOUT_MS
|
|
@@ -14704,7 +14770,7 @@ async function cleanupActions$1(actions, model) {
|
|
|
14704
14770
|
try {
|
|
14705
14771
|
const { result, isError } = await invokeClaudeStreaming({
|
|
14706
14772
|
prompt: buildCleanupPrompt(actions),
|
|
14707
|
-
|
|
14773
|
+
allowedTools: [],
|
|
14708
14774
|
maxTurns: 1,
|
|
14709
14775
|
model
|
|
14710
14776
|
}, () => {});
|
|
@@ -16633,7 +16699,6 @@ async function updateAgentPrompt(args) {
|
|
|
16633
16699
|
prompt: userPrompt,
|
|
16634
16700
|
systemPrompt,
|
|
16635
16701
|
allowedTools: [],
|
|
16636
|
-
disableBuiltinTools: true,
|
|
16637
16702
|
disableThinking: true,
|
|
16638
16703
|
...model ? { model } : {}
|
|
16639
16704
|
}, () => {});
|
|
@@ -23430,7 +23495,9 @@ function createAppendCoverageEventHandler(config) {
|
|
|
23430
23495
|
}
|
|
23431
23496
|
/**
|
|
23432
23497
|
* GET /api/v1/coverage/events?project=&sinceSeq= — the stream after `sinceSeq`
|
|
23433
|
-
* (exclusive,
|
|
23498
|
+
* (exclusive), decrypted, at most `MAX_EVENTS_PER_READ` of them. A consumer
|
|
23499
|
+
* passes back the `seq` of the last event it received; `lastSeq` is the
|
|
23500
|
+
* stream's head, which `truncated` says the body stopped short of.
|
|
23434
23501
|
* Hub bearer token only (the server's central check): what the append-only
|
|
23435
23502
|
* credential wrote, it must not be able to read back.
|
|
23436
23503
|
*/
|
|
@@ -23439,33 +23506,51 @@ function createGetCoverageEventsHandler(config) {
|
|
|
23439
23506
|
const key = requireKey(config);
|
|
23440
23507
|
const project = requireProjectParam(ctx);
|
|
23441
23508
|
const sinceSeq = requireSinceSeqParam(ctx.url);
|
|
23442
|
-
|
|
23509
|
+
const events = [];
|
|
23510
|
+
let truncated = false;
|
|
23511
|
+
const { lastSeq, skipped } = await scanStream(config, key, project, sinceSeq, (event) => {
|
|
23512
|
+
if (events.length < MAX_EVENTS_PER_READ) events.push(event);
|
|
23513
|
+
else truncated = true;
|
|
23514
|
+
});
|
|
23515
|
+
sendJson(ctx.res, 200, {
|
|
23516
|
+
events,
|
|
23517
|
+
lastSeq,
|
|
23518
|
+
skipped,
|
|
23519
|
+
truncated
|
|
23520
|
+
});
|
|
23443
23521
|
};
|
|
23444
23522
|
}
|
|
23445
|
-
/**
|
|
23446
|
-
|
|
23447
|
-
|
|
23448
|
-
|
|
23523
|
+
/**
|
|
23524
|
+
* Hand each event after `sinceSeq` (exclusive) to `visit`, decrypted and
|
|
23525
|
+
* parsed. Nothing is retained here: what a caller keeps is its own choice,
|
|
23526
|
+
* which is what lets a whole-stream read stay bounded.
|
|
23527
|
+
*/
|
|
23528
|
+
async function scanStream(config, key, project, sinceSeq, visit) {
|
|
23449
23529
|
let unreadable = 0;
|
|
23450
|
-
|
|
23451
|
-
|
|
23452
|
-
|
|
23453
|
-
|
|
23454
|
-
|
|
23455
|
-
|
|
23456
|
-
|
|
23457
|
-
|
|
23458
|
-
|
|
23459
|
-
|
|
23460
|
-
|
|
23530
|
+
const { lastSeq, skipped } = await config.store.scan(project, sinceSeq, (entry) => {
|
|
23531
|
+
let event;
|
|
23532
|
+
try {
|
|
23533
|
+
const plain = decrypt(decodeEncryptedBlob(entry.payload), key);
|
|
23534
|
+
event = {
|
|
23535
|
+
seq: entry.seq,
|
|
23536
|
+
at: entry.at,
|
|
23537
|
+
body: InboxBodySchema.parse(JSON.parse(new TextDecoder().decode(plain)))
|
|
23538
|
+
};
|
|
23539
|
+
} catch {
|
|
23540
|
+
unreadable += 1;
|
|
23541
|
+
return;
|
|
23542
|
+
}
|
|
23543
|
+
visit(event);
|
|
23544
|
+
});
|
|
23461
23545
|
return {
|
|
23462
|
-
events,
|
|
23463
23546
|
lastSeq,
|
|
23464
23547
|
skipped: skipped + unreadable
|
|
23465
23548
|
};
|
|
23466
23549
|
}
|
|
23467
23550
|
/** Newest runs the resolve read offers; past twenty the answer is history nobody pages through. */
|
|
23468
23551
|
const RUN_IDS_LIMIT = 20;
|
|
23552
|
+
/** How many events one `GET /events` body carries; past it the answer is `truncated`. */
|
|
23553
|
+
const MAX_EVENTS_PER_READ = 5e3;
|
|
23469
23554
|
/** Resolved answers kept per handler; one page polls one key, so a handful covers the readers. */
|
|
23470
23555
|
const RESOLVE_CACHE_LIMIT = 8;
|
|
23471
23556
|
/**
|
|
@@ -23522,14 +23607,27 @@ function createResolveCoverageHandler(config) {
|
|
|
23522
23607
|
sendJson(ctx.res, 200, hit);
|
|
23523
23608
|
return;
|
|
23524
23609
|
}
|
|
23525
|
-
const
|
|
23526
|
-
|
|
23610
|
+
const markers = [];
|
|
23611
|
+
let seen = 0;
|
|
23612
|
+
const first = await scanStream(config, key, project, 0, (event) => {
|
|
23613
|
+
seen += 1;
|
|
23614
|
+
if ("kind" in event.body) markers.push(event);
|
|
23615
|
+
});
|
|
23616
|
+
const runIds = listRunIds(markers).slice(0, RUN_IDS_LIMIT);
|
|
23527
23617
|
const runId = runKey !== "" ? runKey : runIds[0];
|
|
23618
|
+
let resolved = null;
|
|
23619
|
+
if (runId !== void 0 && seen > 0) {
|
|
23620
|
+
const resolution = new StreamResolution(markers, runId);
|
|
23621
|
+
await scanStream(config, key, project, 0, (event) => {
|
|
23622
|
+
if (event.seq <= first.lastSeq) resolution.accept(event);
|
|
23623
|
+
});
|
|
23624
|
+
resolved = resolution.finish();
|
|
23625
|
+
}
|
|
23528
23626
|
const answer = {
|
|
23529
|
-
resolved
|
|
23627
|
+
resolved,
|
|
23530
23628
|
runIds
|
|
23531
23629
|
};
|
|
23532
|
-
memo.put(project, runKey, lastSeq, answer);
|
|
23630
|
+
memo.put(project, runKey, first.lastSeq, answer);
|
|
23533
23631
|
sendJson(ctx.res, 200, answer);
|
|
23534
23632
|
};
|
|
23535
23633
|
}
|
|
@@ -30613,7 +30711,6 @@ function createLearningWorker(deps) {
|
|
|
30613
30711
|
prompt: buildLearningUserPrompt(cases.slice(0, LEARNING_MAX_CASES)),
|
|
30614
30712
|
systemPrompt: LEARNING_SYSTEM_PROMPT,
|
|
30615
30713
|
allowedTools: [],
|
|
30616
|
-
disableBuiltinTools: true,
|
|
30617
30714
|
maxTurns: 1
|
|
30618
30715
|
}, () => {});
|
|
30619
30716
|
const guidance = result?.trim();
|
|
@@ -31167,7 +31264,7 @@ function createFileCoverageEventStore(root, caps) {
|
|
|
31167
31264
|
oldestAt: null,
|
|
31168
31265
|
endsWithNewline: raw === "" || raw.endsWith("\n")
|
|
31169
31266
|
};
|
|
31170
|
-
for (const rawLine of
|
|
31267
|
+
for (const rawLine of raw.split("\n")) {
|
|
31171
31268
|
const line = parseLine(rawLine);
|
|
31172
31269
|
if (line === null) continue;
|
|
31173
31270
|
if (line.seq >= state.nextSeq) state.nextSeq = line.seq + 1;
|
|
@@ -31223,19 +31320,17 @@ function createFileCoverageEventStore(root, caps) {
|
|
|
31223
31320
|
return stamp;
|
|
31224
31321
|
});
|
|
31225
31322
|
},
|
|
31226
|
-
async
|
|
31323
|
+
async scan(project, sinceSeq, visit) {
|
|
31227
31324
|
assertSafeName(project, "project");
|
|
31228
31325
|
const path = coverageEventsPath(root, project);
|
|
31229
31326
|
const now = Date.now();
|
|
31230
31327
|
await serialize(path, async () => {
|
|
31231
31328
|
await pruneIfDue(project, path, await loadState(project, path), now);
|
|
31232
31329
|
});
|
|
31233
|
-
const raw = await readRaw(path);
|
|
31234
31330
|
const cutoff = now - retentionMs;
|
|
31235
|
-
const entries = [];
|
|
31236
31331
|
let lastSeq = 0;
|
|
31237
31332
|
let skipped = 0;
|
|
31238
|
-
for (const rawLine of
|
|
31333
|
+
for await (const rawLine of streamLines(path)) {
|
|
31239
31334
|
const line = parseLine(rawLine);
|
|
31240
31335
|
if (line === null) {
|
|
31241
31336
|
skipped += 1;
|
|
@@ -31244,15 +31339,13 @@ function createFileCoverageEventStore(root, caps) {
|
|
|
31244
31339
|
if (line.seq > lastSeq) lastSeq = line.seq;
|
|
31245
31340
|
if (line.seq <= sinceSeq) continue;
|
|
31246
31341
|
if (line.at < cutoff) continue;
|
|
31247
|
-
|
|
31342
|
+
visit({
|
|
31248
31343
|
seq: line.seq,
|
|
31249
31344
|
at: line.at,
|
|
31250
31345
|
payload: new Uint8Array(Buffer.from(line.payload, "base64"))
|
|
31251
31346
|
});
|
|
31252
31347
|
}
|
|
31253
|
-
entries.sort((a, b) => a.seq - b.seq);
|
|
31254
31348
|
return {
|
|
31255
|
-
entries,
|
|
31256
31349
|
lastSeq,
|
|
31257
31350
|
skipped
|
|
31258
31351
|
};
|
|
@@ -31283,13 +31376,31 @@ async function readRaw(path) {
|
|
|
31283
31376
|
throw err;
|
|
31284
31377
|
}
|
|
31285
31378
|
}
|
|
31286
|
-
|
|
31287
|
-
|
|
31379
|
+
/**
|
|
31380
|
+
* The stream's non-empty lines, one at a time. Streamed rather than read as one
|
|
31381
|
+
* string because a project's stream is capped in the hundreds of megabytes, far
|
|
31382
|
+
* past what a reader can hold. Order is the file's, which is append order,
|
|
31383
|
+
* which is seq order — the prune rewrites a suffix and never reorders.
|
|
31384
|
+
*/
|
|
31385
|
+
async function* streamLines(path) {
|
|
31386
|
+
const input = createReadStream(path, { encoding: "utf8" });
|
|
31387
|
+
const lines = createInterface$1({
|
|
31388
|
+
input,
|
|
31389
|
+
crlfDelay: Infinity
|
|
31390
|
+
});
|
|
31391
|
+
try {
|
|
31392
|
+
for await (const line of lines) if (line !== "") yield line;
|
|
31393
|
+
} catch (err) {
|
|
31394
|
+
if (!(err instanceof Error && "code" in err && err.code === "ENOENT")) throw err;
|
|
31395
|
+
} finally {
|
|
31396
|
+
lines.close();
|
|
31397
|
+
input.destroy();
|
|
31398
|
+
}
|
|
31288
31399
|
}
|
|
31289
31400
|
/** Every parseable line of the log; partial or corrupt lines are silently omitted (the read side counts them). */
|
|
31290
31401
|
async function readLines(path) {
|
|
31291
31402
|
const lines = [];
|
|
31292
|
-
for (const rawLine of
|
|
31403
|
+
for await (const rawLine of streamLines(path)) {
|
|
31293
31404
|
const line = parseLine(rawLine);
|
|
31294
31405
|
if (line !== null) lines.push(line);
|
|
31295
31406
|
}
|
package/dist/package.json
CHANGED