zerocheck 0.1.2 → 0.1.3
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 +3 -3
- package/dist/index.js +61 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Paste a checklist, review one drafted check and browser verification result per
|
|
|
9
9
|
Requires Node.js 20.19 or newer and a Zerocheck project/account. Create a project in the web app’s Settings, then authorize a device or create a project token.
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
npm install --save-dev zerocheck@0.1.
|
|
12
|
+
npm install --save-dev zerocheck@0.1.3
|
|
13
13
|
npx zerocheck login
|
|
14
14
|
npx zerocheck init --project your-project-id --url http://localhost:3000
|
|
15
15
|
npx zerocheck install
|
|
@@ -149,7 +149,7 @@ Use the installed CLI as a stdio MCP server, with an explicit project directory:
|
|
|
149
149
|
"mcpServers": {
|
|
150
150
|
"zerocheck": {
|
|
151
151
|
"command": "npx",
|
|
152
|
-
"args": ["--yes", "zerocheck@0.1.
|
|
152
|
+
"args": ["--yes", "zerocheck@0.1.3", "mcp", "--project-dir", "/absolute/path/to/project"]
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
}
|
|
@@ -167,6 +167,6 @@ Local execution sends selected check YAML, the page context needed for AI (inclu
|
|
|
167
167
|
|
|
168
168
|
## Repository development
|
|
169
169
|
|
|
170
|
-
The CLI bundles the shared engine source into `dist/index.js`; it has no unpublished engine-package dependency. From this repository, run `npm run build`, `npm run check`, and `npm test` in `packages/cli`. Tests include a real MCP client communicating with the bundled CLI over stdio. The generated workflow and documentation use version `0.1.
|
|
170
|
+
The CLI bundles the shared engine source into `dist/index.js`; it has no unpublished engine-package dependency. From this repository, run `npm run build`, `npm run check`, and `npm test` in `packages/cli`. Tests include a real MCP client communicating with the bundled CLI over stdio. The generated workflow and documentation use version `0.1.3`; update that pin together with the package version for a release.
|
|
171
171
|
|
|
172
172
|
Hosted runs and imports return a queued handle when browsers are occupied; CLI and MCP keep polling until they finish. The queue holds 20 waiting jobs for up to five minutes and never automatically resumes interrupted jobs after restart. Recent imports is available in the web app’s Checks page.
|
package/dist/index.js
CHANGED
|
@@ -6979,7 +6979,7 @@ async function loginCommand() {
|
|
|
6979
6979
|
let device;
|
|
6980
6980
|
try {
|
|
6981
6981
|
device = await apiRequest("/v1/cli/auth/device", {
|
|
6982
|
-
body: { device_name: deviceName, client_version: "0.1.
|
|
6982
|
+
body: { device_name: deviceName, client_version: "0.1.3" },
|
|
6983
6983
|
authed: false
|
|
6984
6984
|
});
|
|
6985
6985
|
} catch (err) {
|
|
@@ -7492,16 +7492,47 @@ async function isFrameVisible(frame) {
|
|
|
7492
7492
|
return true;
|
|
7493
7493
|
}
|
|
7494
7494
|
|
|
7495
|
+
// ../../src/agent/observation-recovery.ts
|
|
7496
|
+
function isTransientObservationError(error2) {
|
|
7497
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
7498
|
+
if (/closed|cancel|abort|policy|ambiguous/i.test(message)) return false;
|
|
7499
|
+
return /Execution context was destroyed|Cannot find context with specified id|Frame was detached|frame has been detached|Stale computer-use observation: frame/i.test(message);
|
|
7500
|
+
}
|
|
7501
|
+
async function retryObservation(page, read, options = {}) {
|
|
7502
|
+
const deadline = Date.now() + (options.timeoutMs ?? 5e3);
|
|
7503
|
+
for (let attempt = 0; ; attempt++) {
|
|
7504
|
+
options.signal?.throwIfAborted();
|
|
7505
|
+
if (page.isClosed()) throw new Error("Browser page closed during observation.");
|
|
7506
|
+
const remaining = deadline - Date.now();
|
|
7507
|
+
if (remaining <= 0) throw new Error("Browser observation timed out before a complete snapshot was available.");
|
|
7508
|
+
try {
|
|
7509
|
+
const value = await read(remaining);
|
|
7510
|
+
options.signal?.throwIfAborted();
|
|
7511
|
+
if (Date.now() > deadline) throw new Error("Browser observation timed out before a complete snapshot was available.");
|
|
7512
|
+
return value;
|
|
7513
|
+
} catch (error2) {
|
|
7514
|
+
if (attempt >= 2 || page.isClosed() || options.signal?.aborted || !isTransientObservationError(error2) || deadline - Date.now() <= 50) throw error2;
|
|
7515
|
+
options.onRetry?.(error2);
|
|
7516
|
+
await page.waitForTimeout(50);
|
|
7517
|
+
}
|
|
7518
|
+
}
|
|
7519
|
+
}
|
|
7520
|
+
|
|
7495
7521
|
// ../../src/agent/page-state.ts
|
|
7496
|
-
async function capturePageState(page, logger, secrets = [], targetPolicy) {
|
|
7522
|
+
async function capturePageState(page, logger, secrets = [], targetPolicy, options = {}) {
|
|
7523
|
+
return retryObservation(page, (remaining) => capturePageStateOnce(page, logger, secrets, targetPolicy, remaining), options);
|
|
7524
|
+
}
|
|
7525
|
+
async function capturePageStateOnce(page, logger, secrets, targetPolicy, timeoutMs) {
|
|
7497
7526
|
targetPolicy?.assertCurrentUrl(page.url());
|
|
7498
7527
|
const policy = observationPolicy(page, targetPolicy);
|
|
7499
|
-
const
|
|
7500
|
-
|
|
7528
|
+
const observations = await Promise.allSettled([
|
|
7529
|
+
captureScreenshotOnce(page, timeoutMs, secrets, policy),
|
|
7501
7530
|
getAccessibilitySnapshot(page, logger, secrets),
|
|
7502
7531
|
page.url(),
|
|
7503
7532
|
page.title()
|
|
7504
7533
|
]);
|
|
7534
|
+
for (const observation of observations) if (observation.status === "rejected") throw observation.reason;
|
|
7535
|
+
const [screenshot, snapshot, url2, title] = observations.map((observation) => observation.value);
|
|
7505
7536
|
const frameTrees = [];
|
|
7506
7537
|
const blocked = /* @__PURE__ */ new Set();
|
|
7507
7538
|
for (const frame of page.frames()) {
|
|
@@ -7604,6 +7635,9 @@ function buildTreeFromCDP(nodes) {
|
|
|
7604
7635
|
return nodeMap.get(nodes[0].nodeId) ?? null;
|
|
7605
7636
|
}
|
|
7606
7637
|
async function captureScreenshot(page, timeoutMs = 5e3, secrets = [], targetPolicy) {
|
|
7638
|
+
return retryObservation(page, (remaining) => captureScreenshotOnce(page, remaining, secrets, targetPolicy), { timeoutMs });
|
|
7639
|
+
}
|
|
7640
|
+
async function captureScreenshotOnce(page, timeoutMs, secrets, targetPolicy) {
|
|
7607
7641
|
const policy = observationPolicy(page, targetPolicy);
|
|
7608
7642
|
const mask = [];
|
|
7609
7643
|
const owners = [];
|
|
@@ -8125,8 +8159,8 @@ async function waitForVisibleText(page, assertion, timeoutMs, signal, targetPoli
|
|
|
8125
8159
|
targetPolicy?.assertCurrentUrl(page.url());
|
|
8126
8160
|
let found = false;
|
|
8127
8161
|
for (const frame of targetPolicy ? page.frames() : [page.mainFrame()]) {
|
|
8128
|
-
if (targetPolicy && (await blockedFrameOrigins(frame, targetPolicy)).length) continue;
|
|
8129
8162
|
if (!await isFrameVisible(frame)) continue;
|
|
8163
|
+
if (targetPolicy && (await blockedFrameOrigins(frame, targetPolicy)).length) continue;
|
|
8130
8164
|
const locator = frame.getByText(assertion.text, { exact: true });
|
|
8131
8165
|
const count = await locator.count();
|
|
8132
8166
|
for (let index = 0; index < count; index++) {
|
|
@@ -9746,8 +9780,12 @@ var BrowserAgent = class {
|
|
|
9746
9780
|
const before = /* @__PURE__ */ new Map();
|
|
9747
9781
|
for (const assertion of this.config.assertions ?? []) {
|
|
9748
9782
|
if (step.lineNumber >= 0 ? assertion.lineNumber <= step.lineNumber : assertion.lineNumber < 0 && assertion.lineNumber >= step.lineNumber) continue;
|
|
9749
|
-
|
|
9750
|
-
|
|
9783
|
+
try {
|
|
9784
|
+
const observed = await deterministicPredicate(page, assertion.argument, this.config.targetPolicy);
|
|
9785
|
+
if (observed !== void 0) before.set(assertion.raw, observed);
|
|
9786
|
+
} catch (error2) {
|
|
9787
|
+
if (!isTransientObservationError(error2) || page.isClosed() || this.signal?.aborted) throw error2;
|
|
9788
|
+
}
|
|
9751
9789
|
}
|
|
9752
9790
|
const result = await this.withAI(page, step, start, STEP_EXECUTION_SYSTEM, lookup.reason, establishedIdentity);
|
|
9753
9791
|
this.signal?.throwIfAborted();
|
|
@@ -9788,7 +9826,7 @@ var BrowserAgent = class {
|
|
|
9788
9826
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
9789
9827
|
let executionStarted = false;
|
|
9790
9828
|
try {
|
|
9791
|
-
const state = await
|
|
9829
|
+
const state = await this.observe(page);
|
|
9792
9830
|
const plan = await this.plan(step, state, system, recovery);
|
|
9793
9831
|
this.signal?.throwIfAborted();
|
|
9794
9832
|
const groundedTarget = await resolveVisualTarget(page, plan, state, this.config.secrets ?? [], this.config.targetPolicy);
|
|
@@ -9798,13 +9836,20 @@ var BrowserAgent = class {
|
|
|
9798
9836
|
return pass(step, start, { resolvedVia: "ai", confidence: plan.confidence, _resolvedPlan: plan });
|
|
9799
9837
|
} catch (error2) {
|
|
9800
9838
|
const planningRetry = !executionStarted && /temporar|unavailable|rate limit|429|50[234]|ECONNRESET|fetch failed/i.test(String(error2));
|
|
9801
|
-
if (attempt || this.signal?.aborted || planningRetry && this.config.retries === 0 || !planningRetry && !safeToReground(error2, this.signal) &&
|
|
9839
|
+
if (attempt || this.signal?.aborted || planningRetry && this.config.retries === 0 || !planningRetry && !safeToReground(error2, this.signal) && !(!executionStarted && /Stale computer-use observation/.test(String(error2)))) throw error2;
|
|
9802
9840
|
recovery = redactText(String(error2), this.config.secrets ?? []);
|
|
9803
9841
|
this.recoveries.push({ kind: planningRetry ? "grounding" : "interaction", reason: recovery, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
9804
9842
|
}
|
|
9805
9843
|
}
|
|
9806
9844
|
throw new Error("Computer-use grounding exhausted its recovery budget.");
|
|
9807
9845
|
}
|
|
9846
|
+
observe(page) {
|
|
9847
|
+
return capturePageState(page, this.config.logger, this.config.secrets, this.config.targetPolicy, {
|
|
9848
|
+
timeoutMs: Math.max(1, this.deadline - Date.now()),
|
|
9849
|
+
signal: this.signal,
|
|
9850
|
+
onRetry: (error2) => this.recoveries.push({ kind: "interaction", reason: redactText(String(error2), this.config.secrets ?? []), at: (/* @__PURE__ */ new Date()).toISOString() })
|
|
9851
|
+
});
|
|
9852
|
+
}
|
|
9808
9853
|
async plan(step, state, system, recovery) {
|
|
9809
9854
|
const plan = await groundAction({
|
|
9810
9855
|
ai: this.config.ai,
|
|
@@ -9847,7 +9892,7 @@ var BrowserAgent = class {
|
|
|
9847
9892
|
const { deadlineMs, cleanDescription } = parseWaitDescription(step.argument);
|
|
9848
9893
|
const outcome = await pollForPresence({ description: cleanDescription, deadlineMs, pollIntervalMs: 500, check: async () => {
|
|
9849
9894
|
this.signal?.throwIfAborted();
|
|
9850
|
-
const state = await
|
|
9895
|
+
const state = await this.observe(page);
|
|
9851
9896
|
const text = await this.config.ai.complete({ system: WAIT_FOR_ELEMENT_SYSTEM, screenshot: state.screenshot, text: redactText(buildWaitForElementPrompt(cleanDescription, state.accessibilityTree), this.config.secrets ?? []) });
|
|
9852
9897
|
const parsed = extractJSON(text);
|
|
9853
9898
|
return { present: parsed?.present === true, confidence: typeof parsed?.confidence === "number" ? parsed.confidence : 0, reasoning: typeof parsed?.reasoning === "string" ? parsed.reasoning : void 0 };
|
|
@@ -9867,7 +9912,7 @@ var BrowserAgent = class {
|
|
|
9867
9912
|
if (passed) this.confirmCandidates(step);
|
|
9868
9913
|
return passed ? pass(step, start, { resolvedVia: "deterministic" }) : fail(step, start, `Expected exact text "${visible.text}" to be ${visible.visible ? "visible" : "absent"} within ${timeoutMs}ms.`, "assertion", { resolvedVia: "deterministic" });
|
|
9869
9914
|
}
|
|
9870
|
-
const outcome = await evaluateAssertion(this.config.ai, redactText(step.argument, this.config.secrets ?? []), await
|
|
9915
|
+
const outcome = await evaluateAssertion(this.config.ai, redactText(step.argument, this.config.secrets ?? []), await this.observe(page));
|
|
9871
9916
|
if (outcome.confidence < 70) throw new Error(`AI could not verify the expected outcome: ${outcome.reasoning}`);
|
|
9872
9917
|
return outcome.pass ? pass(step, start, { confidence: outcome.confidence, resolvedVia: "ai" }) : fail(step, start, `Assertion failed: ${outcome.reasoning}`, "assertion", { confidence: outcome.confidence, resolvedVia: "ai" });
|
|
9873
9918
|
}
|
|
@@ -9875,7 +9920,7 @@ var BrowserAgent = class {
|
|
|
9875
9920
|
let last;
|
|
9876
9921
|
const performed = /* @__PURE__ */ new Set();
|
|
9877
9922
|
for (let i = 0; i < 5; i++) {
|
|
9878
|
-
const state = await
|
|
9923
|
+
const state = await this.observe(page);
|
|
9879
9924
|
const plan = await this.plan(step, state, ACT_SYSTEM);
|
|
9880
9925
|
const identity = JSON.stringify([plan.action, plan.selector, plan.value]);
|
|
9881
9926
|
if (performed.has(identity)) return fail(step, start, "The agent requested the same action again without confirmed completion; stopped to avoid duplicate writes.", "interaction");
|
|
@@ -9883,7 +9928,7 @@ var BrowserAgent = class {
|
|
|
9883
9928
|
this.signal?.throwIfAborted();
|
|
9884
9929
|
const actionResult = await executeAction(page, plan, { ...this.policy(step), groundedTarget: await resolveVisualTarget(page, plan, state, this.config.secrets ?? [], this.config.targetPolicy) });
|
|
9885
9930
|
last = pass(step, start, { resolvedVia: "ai", confidence: plan.confidence, _resolvedPlan: plan });
|
|
9886
|
-
const after = await
|
|
9931
|
+
const after = await this.observe(page);
|
|
9887
9932
|
const response = await this.config.ai.complete({ system: ACT_COMPLETION_SYSTEM, screenshot: after.screenshot, text: redactText(buildCompletionCheckPrompt(step.argument, after.accessibilityTree), this.config.secrets ?? []) });
|
|
9888
9933
|
if (extractJSON(response)?.complete === true && actionResult.execution?.outcomeObserved) return last;
|
|
9889
9934
|
if (actionResult.execution?.effect !== "observation" && !actionResult.execution?.outcomeObserved) return fail(step, start, "The action has no independently observed completion. Stopped before another action.", "interaction");
|
|
@@ -11499,7 +11544,7 @@ jobs:
|
|
|
11499
11544
|
node-version: '22'
|
|
11500
11545
|
- name: Install Zerocheck
|
|
11501
11546
|
run: |
|
|
11502
|
-
npm install --prefix "$RUNNER_TEMP/zerocheck-cli" --no-save --package-lock=false zerocheck@0.1.
|
|
11547
|
+
npm install --prefix "$RUNNER_TEMP/zerocheck-cli" --no-save --package-lock=false zerocheck@0.1.3
|
|
11503
11548
|
echo "$RUNNER_TEMP/zerocheck-cli/node_modules/.bin" >> "$GITHUB_PATH"
|
|
11504
11549
|
- name: Restore learned targets
|
|
11505
11550
|
uses: actions/cache@v4
|
|
@@ -17532,7 +17577,7 @@ var runner = z2.enum(["local", "hosted"]).default("local").describe("Local brows
|
|
|
17532
17577
|
var output = (value) => ({ content: [{ type: "text", text: JSON.stringify(value) }], structuredContent: { result: value } });
|
|
17533
17578
|
var error = (value) => ({ isError: true, content: [{ type: "text", text: JSON.stringify({ error: "operation_failed", message: value instanceof Error ? value.message : String(value) }) }] });
|
|
17534
17579
|
function createMcpServer(services) {
|
|
17535
|
-
const server = new McpServer({ name: "zerocheck", version: "0.1.
|
|
17580
|
+
const server = new McpServer({ name: "zerocheck", version: "0.1.3" });
|
|
17536
17581
|
server.registerTool("list_checks", {
|
|
17537
17582
|
description: "List the repository YAML checks, IDs, exact contents and revisions. Does not execute them.",
|
|
17538
17583
|
inputSchema: { paths: z2.array(z2.string()).optional(), environment },
|
|
@@ -17672,7 +17717,7 @@ async function mcpCommand(options) {
|
|
|
17672
17717
|
}
|
|
17673
17718
|
|
|
17674
17719
|
// src/index.ts
|
|
17675
|
-
var program = new Command().name("zerocheck").version("0.1.
|
|
17720
|
+
var program = new Command().name("zerocheck").version("0.1.3").description("Turn your team\u2019s manual release checklist into repeatable browser tests.");
|
|
17676
17721
|
var directory = (command) => command.option("--project-dir <path>", "Repository project directory", process.cwd());
|
|
17677
17722
|
var environment2 = (command) => directory(command).addOption(new Option("--env <environment>", "Named project environment").choices(["dev", "staging", "production"]));
|
|
17678
17723
|
var execution = (command) => environment2(command).addOption(new Option("--runner <runner>", "Browser execution location").choices(["local", "hosted"]).default("local"));
|