autokap 1.8.6 → 1.8.8
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/action-verifier.d.ts +6 -0
- package/dist/action-verifier.js +30 -17
- package/dist/browser.d.ts +59 -0
- package/dist/browser.js +259 -0
- package/dist/cli-config.js +7 -12
- package/dist/cli-contract.d.ts +5 -9
- package/dist/cli-contract.js +11 -38
- package/dist/cli-runner.d.ts +0 -1
- package/dist/cli-runner.js +74 -59
- package/dist/cli.js +7 -7
- package/dist/clip-capture-loop.d.ts +28 -7
- package/dist/clip-capture-loop.js +102 -19
- package/dist/engine-version.d.ts +24 -0
- package/dist/engine-version.js +25 -0
- package/dist/execution-schema.d.ts +22 -0
- package/dist/execution-schema.js +59 -8
- package/dist/execution-types.d.ts +116 -0
- package/dist/opcode-runner.d.ts +8 -1
- package/dist/opcode-runner.js +120 -29
- package/dist/postcondition.d.ts +18 -3
- package/dist/postcondition.js +75 -27
- package/dist/program-hash.d.ts +11 -0
- package/dist/program-hash.js +28 -0
- package/dist/program-migrations.d.ts +31 -0
- package/dist/program-migrations.js +93 -0
- package/dist/program-signing.d.ts +11 -0
- package/dist/program-signing.js +1 -0
- package/dist/recovery-chain.js +8 -11
- package/dist/scenario-cookie.d.ts +36 -0
- package/dist/scenario-cookie.js +62 -0
- package/dist/security.d.ts +21 -0
- package/dist/security.js +46 -8
- package/dist/server-credit-usage.d.ts +1 -1
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/dist/video-narration-schema.d.ts +3 -0
- package/dist/video-narration-schema.js +3 -0
- package/dist/wait-contract.d.ts +104 -0
- package/dist/wait-contract.js +144 -0
- package/dist/web-playwright-local.d.ts +9 -1
- package/dist/web-playwright-local.js +0 -0
- package/package.json +2 -2
- package/readme.md +9 -15
|
@@ -12,6 +12,12 @@ export interface ActionVerification {
|
|
|
12
12
|
changes: ActionChange[];
|
|
13
13
|
/** Summary for logging */
|
|
14
14
|
summary: string;
|
|
15
|
+
/**
|
|
16
|
+
* AUT-240 (decision 2): the after-state was unreadable even after a settle +
|
|
17
|
+
* retry, so the effect was assumed as a last resort. The capture this taints
|
|
18
|
+
* is flagged low-confidence rather than failed.
|
|
19
|
+
*/
|
|
20
|
+
lowConfidence?: boolean;
|
|
15
21
|
}
|
|
16
22
|
export type ActionChangeKind = 'url_changed' | 'tree_structure_changed' | 'node_appeared' | 'node_disappeared' | 'node_state_changed' | 'scroll_changed' | 'overlay_changed' | 'no_change';
|
|
17
23
|
export interface ActionChange {
|
package/dist/action-verifier.js
CHANGED
|
@@ -32,23 +32,36 @@ export class ActionVerifier {
|
|
|
32
32
|
summary: 'no before state captured, assuming action had effect',
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
// AUT-240 (decision 2): "assume OK, but smart". A first `page.evaluate`
|
|
36
|
+
// hiccup (typically mid-navigation) is no longer assumed-OK immediately —
|
|
37
|
+
// settle the page and retry once. Only if it still throws do we assume the
|
|
38
|
+
// effect, and then we flag the result low-confidence for downstream scrutiny.
|
|
39
|
+
let after = null;
|
|
37
40
|
try {
|
|
38
|
-
[
|
|
39
|
-
|
|
40
|
-
adapter.getAKTree(),
|
|
41
|
-
]);
|
|
41
|
+
const [url, tree] = await Promise.all([adapter.getCurrentUrl(), adapter.getAKTree()]);
|
|
42
|
+
after = { url, tree };
|
|
42
43
|
}
|
|
43
44
|
catch {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
45
|
+
try {
|
|
46
|
+
if (adapter.waitForVisuallyStable) {
|
|
47
|
+
await adapter.waitForVisuallyStable({ maxWaitMs: 2000 });
|
|
48
|
+
}
|
|
49
|
+
const [url, tree] = await Promise.all([adapter.getCurrentUrl(), adapter.getAKTree()]);
|
|
50
|
+
after = { url, tree };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
this.beforeTree = null;
|
|
54
|
+
this.beforeUrl = null;
|
|
55
|
+
return {
|
|
56
|
+
hadEffect: true,
|
|
57
|
+
changes: [],
|
|
58
|
+
summary: 'after state unavailable after settle, assuming effect (low-confidence)',
|
|
59
|
+
lowConfidence: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
51
62
|
}
|
|
63
|
+
const afterUrl = after.url;
|
|
64
|
+
const afterTree = after.tree;
|
|
52
65
|
const changes = [];
|
|
53
66
|
// 1. URL change
|
|
54
67
|
if (afterUrl !== this.beforeUrl) {
|
|
@@ -105,11 +118,11 @@ export class ActionVerifier {
|
|
|
105
118
|
const beforeState = collectVisibleNodeState(this.beforeTree.root);
|
|
106
119
|
const afterState = collectVisibleNodeState(afterTree.root);
|
|
107
120
|
const changedStates = [];
|
|
108
|
-
for (const [key,
|
|
109
|
-
const
|
|
110
|
-
if (!
|
|
121
|
+
for (const [key, beforeNodeState] of beforeState) {
|
|
122
|
+
const afterNodeState = afterState.get(key);
|
|
123
|
+
if (!afterNodeState)
|
|
111
124
|
continue;
|
|
112
|
-
if (
|
|
125
|
+
if (beforeNodeState !== afterNodeState) {
|
|
113
126
|
changedStates.push(key);
|
|
114
127
|
if (changedStates.length >= 5)
|
|
115
128
|
break;
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type BrowserContext, type Page } from 'playwright';
|
|
2
2
|
import type { AKNode, AKNodeRuntimeIndexEntry, AKTree, BrowserOptions, BrowserSessionStorageState, BrowserStorageState, InteractiveElement, ObservedRequest, OutscaleConfig, PageState, PageStateLite, ResolvedMock, SelectorValidationResult, VideoPageSignals } from './types.js';
|
|
3
|
+
import type { ProgressSnapshot, VisualStabilityResult } from './execution-types.js';
|
|
3
4
|
export interface SelectorCaptureError {
|
|
4
5
|
error: 'no_match' | 'ambiguous' | 'invisible' | 'zero_size';
|
|
5
6
|
errorMessage: string;
|
|
@@ -103,6 +104,18 @@ export declare class Browser {
|
|
|
103
104
|
private poolContext;
|
|
104
105
|
private persistentContext;
|
|
105
106
|
private ownedChromiumProfileDir;
|
|
107
|
+
/**
|
|
108
|
+
* Network-progress counters (AUT-240, Layer C). Maintained by request
|
|
109
|
+
* lifecycle listeners attached lazily and idempotently per page via
|
|
110
|
+
* `ensureProgressListeners()`, so every page-creation path is covered without
|
|
111
|
+
* touching each one. Count FIRST-PARTY traffic only (same site as the live
|
|
112
|
+
* main-frame origin) so background third-party telemetry does not read as
|
|
113
|
+
* progress. Read cheaply by `getProgressSnapshot()`.
|
|
114
|
+
*/
|
|
115
|
+
private progressListenersPage;
|
|
116
|
+
private inflightRequests;
|
|
117
|
+
private networkEventCount;
|
|
118
|
+
private lastNetworkActivityAtMs;
|
|
106
119
|
/**
|
|
107
120
|
* Xvfb instance backing the headed Chromium used by clip capture on Cloud
|
|
108
121
|
* Run with NVIDIA L4. Set when forClipCapture spawns Xvfb; null otherwise
|
|
@@ -168,6 +181,52 @@ export declare class Browser {
|
|
|
168
181
|
*/
|
|
169
182
|
private waitForDomStability;
|
|
170
183
|
private waitForFontsBeforeScreenshot;
|
|
184
|
+
/**
|
|
185
|
+
* Attach network-progress listeners to `page` once. Idempotent per page: a
|
|
186
|
+
* no-op if already attached, and resets counters only when the `Page` object
|
|
187
|
+
* itself changes (recreated context, pooled re-acquire). A `page.reload()`
|
|
188
|
+
* keeps the same object, so counters stay monotonic across it — harmless, as
|
|
189
|
+
* the watchdog only ever compares deltas via `hasProgress`. Covers every
|
|
190
|
+
* page-creation path without editing each one.
|
|
191
|
+
*/
|
|
192
|
+
private ensureProgressListeners;
|
|
193
|
+
/**
|
|
194
|
+
* Cheap snapshot of page activity for the runner's progress watchdog
|
|
195
|
+
* (AUT-240, Layer C). Network counters are free (maintained by listeners);
|
|
196
|
+
* `readyState`/`domNodeCount` come from one light `evaluate`. Never rejects:
|
|
197
|
+
* an `evaluate` failure (navigation in flight) is itself progress, surfaced
|
|
198
|
+
* as `navigating: true`.
|
|
199
|
+
*/
|
|
200
|
+
getProgressSnapshot(): Promise<ProgressSnapshot>;
|
|
201
|
+
/**
|
|
202
|
+
* Wait until the page is visually stable enough to screenshot (AUT-240,
|
|
203
|
+
* Layer B). Best-effort and non-blocking: composes light semantic signals —
|
|
204
|
+
* fonts ready, images loaded, no visible `[aria-busy]`/`[role=progressbar]`,
|
|
205
|
+
* DOM quiet — and only falls back to a bounded pixel-convergence check when
|
|
206
|
+
* loaders never clear. Returns `stable: false` with a reason instead of
|
|
207
|
+
* throwing or blocking; the caller captures anyway (a perpetual animation is
|
|
208
|
+
* cosmetic, not a reason to fail the capture — decision 1).
|
|
209
|
+
*/
|
|
210
|
+
waitForVisuallyStable(options?: {
|
|
211
|
+
maxWaitMs?: number;
|
|
212
|
+
}): Promise<VisualStabilityResult>;
|
|
213
|
+
private remainingMs;
|
|
214
|
+
/** Poll until in-DOM images have finished loading, or the budget elapses. */
|
|
215
|
+
private waitForImagesSettled;
|
|
216
|
+
/**
|
|
217
|
+
* Poll until no semantically-marked loader is visible, or the deadline hits.
|
|
218
|
+
* Condition: zero visible elements matching
|
|
219
|
+
* `[aria-busy="true"], [role="progressbar"]:not([hidden])` — covers shadcn,
|
|
220
|
+
* Radix, MUI, etc. without a hardcoded class list. Returns true if cleared.
|
|
221
|
+
*/
|
|
222
|
+
private waitForNoVisibleLoaders;
|
|
223
|
+
/**
|
|
224
|
+
* Bounded pixel-convergence fallback (AUT-240 decision 1): take up to
|
|
225
|
+
* `PIXEL_FALLBACK_MAX_PASSES` raw frames; if two consecutive frames are within
|
|
226
|
+
* `PIXEL_FALLBACK_DIFF_THRESHOLD`, the page is settled. Never blocks past the
|
|
227
|
+
* deadline; returns false (capture anyway) for perpetual animations.
|
|
228
|
+
*/
|
|
229
|
+
private pixelConvergenceFallback;
|
|
171
230
|
takeScreenshot(): Promise<Buffer>;
|
|
172
231
|
takeScreenshotForAI(options?: {
|
|
173
232
|
timeoutMs?: number;
|
package/dist/browser.js
CHANGED
|
@@ -3,7 +3,9 @@ import sharp from 'sharp';
|
|
|
3
3
|
import { createHash } from 'crypto';
|
|
4
4
|
import { cp, mkdir, readFile, rm, writeFile } from 'fs/promises';
|
|
5
5
|
import { join } from 'path';
|
|
6
|
+
import { DOM_QUIET_WINDOW_MS, GLOBAL_WAIT_CAP_MS, PIXEL_FALLBACK_DIFF_THRESHOLD, PIXEL_FALLBACK_MAX_PASSES, } from './wait-contract.js';
|
|
6
7
|
import { buildAKNodeRuntimeIndex, deriveInteractiveElementsFromAKTree, disambiguateFingerprint, focusAKTree, fingerprintAKNode, serializeAKTree, } from './ak-tree.js';
|
|
8
|
+
import { isFirstPartyUrl } from './security.js';
|
|
7
9
|
/**
|
|
8
10
|
* Set-of-Marks (SoM) annotation: overlays colored [N] badges on each visible
|
|
9
11
|
* interactive element so the vision model can reference elements by their badge index.
|
|
@@ -876,6 +878,29 @@ export function describeObservationChange(before, after) {
|
|
|
876
878
|
: 'No visible state change detected after the action.',
|
|
877
879
|
};
|
|
878
880
|
}
|
|
881
|
+
function delayMs(ms) {
|
|
882
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Sampled byte-diff ratio between two screenshot buffers (AUT-240, Layer B
|
|
886
|
+
* pixel fallback). Cheap O(sampleCount) approximation — good enough to tell a
|
|
887
|
+
* settled frame from a moving one without decoding the PNG.
|
|
888
|
+
*/
|
|
889
|
+
function sampledBufferDiffRatio(a, b) {
|
|
890
|
+
const maxLength = Math.max(a.length, b.length);
|
|
891
|
+
if (maxLength === 0)
|
|
892
|
+
return 0;
|
|
893
|
+
const sampleCount = Math.min(10000, maxLength);
|
|
894
|
+
const step = Math.max(1, Math.floor(maxLength / sampleCount));
|
|
895
|
+
let checked = 0;
|
|
896
|
+
let changed = 0;
|
|
897
|
+
for (let i = 0; i < maxLength; i += step) {
|
|
898
|
+
checked++;
|
|
899
|
+
if (a[i] !== b[i])
|
|
900
|
+
changed++;
|
|
901
|
+
}
|
|
902
|
+
return checked === 0 ? 0 : changed / checked;
|
|
903
|
+
}
|
|
879
904
|
export class Browser {
|
|
880
905
|
options;
|
|
881
906
|
browser = null;
|
|
@@ -886,6 +911,18 @@ export class Browser {
|
|
|
886
911
|
poolContext = false;
|
|
887
912
|
persistentContext = false;
|
|
888
913
|
ownedChromiumProfileDir = null;
|
|
914
|
+
/**
|
|
915
|
+
* Network-progress counters (AUT-240, Layer C). Maintained by request
|
|
916
|
+
* lifecycle listeners attached lazily and idempotently per page via
|
|
917
|
+
* `ensureProgressListeners()`, so every page-creation path is covered without
|
|
918
|
+
* touching each one. Count FIRST-PARTY traffic only (same site as the live
|
|
919
|
+
* main-frame origin) so background third-party telemetry does not read as
|
|
920
|
+
* progress. Read cheaply by `getProgressSnapshot()`.
|
|
921
|
+
*/
|
|
922
|
+
progressListenersPage = null;
|
|
923
|
+
inflightRequests = 0;
|
|
924
|
+
networkEventCount = 0;
|
|
925
|
+
lastNetworkActivityAtMs = 0;
|
|
889
926
|
/**
|
|
890
927
|
* Xvfb instance backing the headed Chromium used by clip capture on Cloud
|
|
891
928
|
* Run with NVIDIA L4. Set when forClipCapture spawns Xvfb; null otherwise
|
|
@@ -1554,6 +1591,228 @@ export class Browser {
|
|
|
1554
1591
|
logger.debug(`[capture] font paint gate resolved via=${gate.via} elapsedMs=${gate.elapsedMs} totalMs=${Date.now() - startedAt}`);
|
|
1555
1592
|
}
|
|
1556
1593
|
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Attach network-progress listeners to `page` once. Idempotent per page: a
|
|
1596
|
+
* no-op if already attached, and resets counters only when the `Page` object
|
|
1597
|
+
* itself changes (recreated context, pooled re-acquire). A `page.reload()`
|
|
1598
|
+
* keeps the same object, so counters stay monotonic across it — harmless, as
|
|
1599
|
+
* the watchdog only ever compares deltas via `hasProgress`. Covers every
|
|
1600
|
+
* page-creation path without editing each one.
|
|
1601
|
+
*/
|
|
1602
|
+
ensureProgressListeners(page) {
|
|
1603
|
+
if (this.progressListenersPage === page)
|
|
1604
|
+
return;
|
|
1605
|
+
this.progressListenersPage = page;
|
|
1606
|
+
this.inflightRequests = 0;
|
|
1607
|
+
this.networkEventCount = 0;
|
|
1608
|
+
this.lastNetworkActivityAtMs = Date.now();
|
|
1609
|
+
const bump = () => {
|
|
1610
|
+
this.networkEventCount++;
|
|
1611
|
+
this.lastNetworkActivityAtMs = Date.now();
|
|
1612
|
+
};
|
|
1613
|
+
// Only the app's OWN traffic counts as progress (AUT-240). Third-party
|
|
1614
|
+
// telemetry (PostHog beacons, analytics/ad pixels, Sentry, polling/ws-fallback
|
|
1615
|
+
// XHR to other origins) chatters indefinitely on a real app and would
|
|
1616
|
+
// otherwise keep `hasProgress` true forever, so the stuck watchdog could never
|
|
1617
|
+
// cut a wait whose condition will never be met. First-party-ness is decided
|
|
1618
|
+
// once, at request time, against the live main-frame origin; the same request
|
|
1619
|
+
// object is remembered so finished/failed decrement symmetrically even if the
|
|
1620
|
+
// page navigates mid-flight (a foreign decrement is simply skipped).
|
|
1621
|
+
const firstPartyInflight = new WeakSet();
|
|
1622
|
+
page.on('request', (request) => {
|
|
1623
|
+
if (!isFirstPartyUrl(page.url(), request.url()))
|
|
1624
|
+
return;
|
|
1625
|
+
firstPartyInflight.add(request);
|
|
1626
|
+
this.inflightRequests++;
|
|
1627
|
+
bump();
|
|
1628
|
+
});
|
|
1629
|
+
page.on('requestfinished', (request) => {
|
|
1630
|
+
if (!firstPartyInflight.has(request))
|
|
1631
|
+
return;
|
|
1632
|
+
this.inflightRequests = Math.max(0, this.inflightRequests - 1);
|
|
1633
|
+
bump();
|
|
1634
|
+
});
|
|
1635
|
+
page.on('requestfailed', (request) => {
|
|
1636
|
+
if (!firstPartyInflight.has(request))
|
|
1637
|
+
return;
|
|
1638
|
+
this.inflightRequests = Math.max(0, this.inflightRequests - 1);
|
|
1639
|
+
bump();
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Cheap snapshot of page activity for the runner's progress watchdog
|
|
1644
|
+
* (AUT-240, Layer C). Network counters are free (maintained by listeners);
|
|
1645
|
+
* `readyState`/`domNodeCount` come from one light `evaluate`. Never rejects:
|
|
1646
|
+
* an `evaluate` failure (navigation in flight) is itself progress, surfaced
|
|
1647
|
+
* as `navigating: true`.
|
|
1648
|
+
*/
|
|
1649
|
+
async getProgressSnapshot() {
|
|
1650
|
+
const page = this.ensurePage();
|
|
1651
|
+
this.ensureProgressListeners(page);
|
|
1652
|
+
let readyState = 'unknown';
|
|
1653
|
+
let domNodeCount = -1;
|
|
1654
|
+
let navigating = false;
|
|
1655
|
+
try {
|
|
1656
|
+
const probe = await page.evaluate(() => ({
|
|
1657
|
+
rs: document.readyState,
|
|
1658
|
+
n: document.getElementsByTagName('*').length,
|
|
1659
|
+
}));
|
|
1660
|
+
readyState = probe.rs;
|
|
1661
|
+
domNodeCount = probe.n;
|
|
1662
|
+
}
|
|
1663
|
+
catch {
|
|
1664
|
+
navigating = true;
|
|
1665
|
+
}
|
|
1666
|
+
return {
|
|
1667
|
+
networkEventCount: this.networkEventCount,
|
|
1668
|
+
inflightRequests: this.inflightRequests,
|
|
1669
|
+
lastNetworkActivityAtMs: this.lastNetworkActivityAtMs,
|
|
1670
|
+
readyState,
|
|
1671
|
+
domNodeCount,
|
|
1672
|
+
navigating,
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Wait until the page is visually stable enough to screenshot (AUT-240,
|
|
1677
|
+
* Layer B). Best-effort and non-blocking: composes light semantic signals —
|
|
1678
|
+
* fonts ready, images loaded, no visible `[aria-busy]`/`[role=progressbar]`,
|
|
1679
|
+
* DOM quiet — and only falls back to a bounded pixel-convergence check when
|
|
1680
|
+
* loaders never clear. Returns `stable: false` with a reason instead of
|
|
1681
|
+
* throwing or blocking; the caller captures anyway (a perpetual animation is
|
|
1682
|
+
* cosmetic, not a reason to fail the capture — decision 1).
|
|
1683
|
+
*/
|
|
1684
|
+
async waitForVisuallyStable(options) {
|
|
1685
|
+
const page = this.ensurePage();
|
|
1686
|
+
const startedAt = Date.now();
|
|
1687
|
+
const maxWait = Math.max(1000, options?.maxWaitMs ?? GLOBAL_WAIT_CAP_MS.screenshot);
|
|
1688
|
+
const deadline = startedAt + maxWait;
|
|
1689
|
+
// 1. Fonts + images painted (avoid FOUT / broken images).
|
|
1690
|
+
await this.waitForFontsBeforeScreenshot(page).catch(() => { });
|
|
1691
|
+
await this.waitForImagesSettled(page, Math.min(3000, this.remainingMs(deadline)));
|
|
1692
|
+
// 2. Semantic loaders cleared (replaces the legacy 8 hardcoded CSS classes).
|
|
1693
|
+
const loadersCleared = await this.waitForNoVisibleLoaders(page, deadline);
|
|
1694
|
+
// 3. DOM quiet.
|
|
1695
|
+
const domBudget = Math.min(3000, this.remainingMs(deadline));
|
|
1696
|
+
if (domBudget > 0) {
|
|
1697
|
+
await this.waitForDomStability(page, DOM_QUIET_WINDOW_MS, domBudget);
|
|
1698
|
+
}
|
|
1699
|
+
if (loadersCleared) {
|
|
1700
|
+
return { stable: true, reason: 'page visually stable', waitedMs: Date.now() - startedAt };
|
|
1701
|
+
}
|
|
1702
|
+
// 4. Loaders persisted ⇒ bounded pixel-convergence fallback (decision 1).
|
|
1703
|
+
// Two converging frames ⇒ the page is effectively static (e.g. a non-ARIA
|
|
1704
|
+
// spinner that finished); otherwise it is a perpetual animation — capture
|
|
1705
|
+
// anyway rather than block.
|
|
1706
|
+
const converged = await this.pixelConvergenceFallback(page, deadline);
|
|
1707
|
+
return converged
|
|
1708
|
+
? {
|
|
1709
|
+
stable: true,
|
|
1710
|
+
reason: 'stable via pixel convergence (loaders not ARIA-marked)',
|
|
1711
|
+
waitedMs: Date.now() - startedAt,
|
|
1712
|
+
}
|
|
1713
|
+
: {
|
|
1714
|
+
stable: false,
|
|
1715
|
+
reason: 'loaders or animation still active at deadline; capturing anyway',
|
|
1716
|
+
waitedMs: Date.now() - startedAt,
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
remainingMs(deadline) {
|
|
1720
|
+
return Math.max(0, deadline - Date.now());
|
|
1721
|
+
}
|
|
1722
|
+
/** Poll until in-DOM images have finished loading, or the budget elapses. */
|
|
1723
|
+
async waitForImagesSettled(page, budgetMs) {
|
|
1724
|
+
if (budgetMs <= 0)
|
|
1725
|
+
return;
|
|
1726
|
+
try {
|
|
1727
|
+
await page.evaluate((maxWait) => {
|
|
1728
|
+
return new Promise((resolve) => {
|
|
1729
|
+
const deadline = Date.now() + maxWait;
|
|
1730
|
+
const check = () => {
|
|
1731
|
+
const imgs = document.querySelectorAll('img');
|
|
1732
|
+
for (const img of imgs) {
|
|
1733
|
+
if (!img.src || img.src.startsWith('data:'))
|
|
1734
|
+
continue;
|
|
1735
|
+
if (img.width <= 1 && img.height <= 1)
|
|
1736
|
+
continue;
|
|
1737
|
+
if (!img.complete || img.naturalWidth === 0)
|
|
1738
|
+
return false;
|
|
1739
|
+
}
|
|
1740
|
+
return true;
|
|
1741
|
+
};
|
|
1742
|
+
if (check()) {
|
|
1743
|
+
resolve();
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
const iv = setInterval(() => {
|
|
1747
|
+
if (check() || Date.now() >= deadline) {
|
|
1748
|
+
clearInterval(iv);
|
|
1749
|
+
resolve();
|
|
1750
|
+
}
|
|
1751
|
+
}, 100);
|
|
1752
|
+
});
|
|
1753
|
+
}, budgetMs);
|
|
1754
|
+
}
|
|
1755
|
+
catch {
|
|
1756
|
+
// Page navigated during the wait — not stable yet, but best-effort.
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Poll until no semantically-marked loader is visible, or the deadline hits.
|
|
1761
|
+
* Condition: zero visible elements matching
|
|
1762
|
+
* `[aria-busy="true"], [role="progressbar"]:not([hidden])` — covers shadcn,
|
|
1763
|
+
* Radix, MUI, etc. without a hardcoded class list. Returns true if cleared.
|
|
1764
|
+
*/
|
|
1765
|
+
async waitForNoVisibleLoaders(page, deadline) {
|
|
1766
|
+
const selector = '[aria-busy="true"], [role="progressbar"]:not([hidden])';
|
|
1767
|
+
while (Date.now() < deadline) {
|
|
1768
|
+
let count = -1;
|
|
1769
|
+
try {
|
|
1770
|
+
count = await page.evaluate((sel) => {
|
|
1771
|
+
const isVisible = (el) => {
|
|
1772
|
+
const rect = el.getBoundingClientRect();
|
|
1773
|
+
if (rect.width === 0 || rect.height === 0)
|
|
1774
|
+
return false;
|
|
1775
|
+
const style = getComputedStyle(el);
|
|
1776
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
|
|
1777
|
+
};
|
|
1778
|
+
return Array.from(document.querySelectorAll(sel)).filter(isVisible).length;
|
|
1779
|
+
}, selector);
|
|
1780
|
+
}
|
|
1781
|
+
catch {
|
|
1782
|
+
count = -1; // navigation in flight — not settled, keep waiting
|
|
1783
|
+
}
|
|
1784
|
+
if (count === 0)
|
|
1785
|
+
return true;
|
|
1786
|
+
await delayMs(Math.min(150, this.remainingMs(deadline)));
|
|
1787
|
+
}
|
|
1788
|
+
return false;
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Bounded pixel-convergence fallback (AUT-240 decision 1): take up to
|
|
1792
|
+
* `PIXEL_FALLBACK_MAX_PASSES` raw frames; if two consecutive frames are within
|
|
1793
|
+
* `PIXEL_FALLBACK_DIFF_THRESHOLD`, the page is settled. Never blocks past the
|
|
1794
|
+
* deadline; returns false (capture anyway) for perpetual animations.
|
|
1795
|
+
*/
|
|
1796
|
+
async pixelConvergenceFallback(page, deadline) {
|
|
1797
|
+
let prev = null;
|
|
1798
|
+
for (let pass = 0; pass < PIXEL_FALLBACK_MAX_PASSES; pass++) {
|
|
1799
|
+
if (Date.now() >= deadline)
|
|
1800
|
+
break;
|
|
1801
|
+
let frame;
|
|
1802
|
+
try {
|
|
1803
|
+
frame = await page.screenshot();
|
|
1804
|
+
}
|
|
1805
|
+
catch {
|
|
1806
|
+
return false;
|
|
1807
|
+
}
|
|
1808
|
+
if (prev && sampledBufferDiffRatio(prev, frame) <= PIXEL_FALLBACK_DIFF_THRESHOLD) {
|
|
1809
|
+
return true;
|
|
1810
|
+
}
|
|
1811
|
+
prev = frame;
|
|
1812
|
+
await delayMs(Math.min(250, this.remainingMs(deadline)));
|
|
1813
|
+
}
|
|
1814
|
+
return false;
|
|
1815
|
+
}
|
|
1557
1816
|
async takeScreenshot() {
|
|
1558
1817
|
const page = this.ensurePage();
|
|
1559
1818
|
// Move cursor off-screen to avoid hover effects in screenshots
|
package/dist/cli-config.js
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
// CLI-local config helpers.
|
|
1
|
+
// CLI-local config helpers. Self-contained, canonical implementation for the
|
|
2
|
+
// bundled CLI binary.
|
|
2
3
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// `@autokap/core` is not yet published to npm. Once core@0.1.0 is on the
|
|
7
|
-
// registry and the published CLI bumps to declare it as a runtime dep, this
|
|
8
|
-
// file can become a thin re-export shim (just LOCAL_WS_URL + requireConfig).
|
|
9
|
-
//
|
|
10
|
-
// Hardening here must stay in lockstep with packages/core/src/config.ts:
|
|
11
|
-
// atomic writes, scheme allowlist on URL inputs, env-var precedence, and
|
|
12
|
-
// the "untrusted origin override" guard.
|
|
4
|
+
// Security invariants to preserve when editing: atomic writes, scheme
|
|
5
|
+
// allowlist on URL inputs, env-var precedence, and the "untrusted origin
|
|
6
|
+
// override" guard.
|
|
13
7
|
import path from 'node:path';
|
|
14
8
|
import os from 'node:os';
|
|
15
9
|
import crypto from 'node:crypto';
|
|
@@ -131,7 +125,8 @@ export async function writeConfig(config) {
|
|
|
131
125
|
? { exportDebugLogs: config.exportDebugLogs }
|
|
132
126
|
: {}),
|
|
133
127
|
};
|
|
134
|
-
// Atomic write
|
|
128
|
+
// Atomic write: write to a temp file then rename, so a crash mid-write
|
|
129
|
+
// never leaves a truncated config.
|
|
135
130
|
const tmpPath = `${configPath}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
136
131
|
try {
|
|
137
132
|
await fs.writeFile(tmpPath, JSON.stringify(normalizedConfig, null, 2), 'utf-8');
|
package/dist/cli-contract.d.ts
CHANGED
|
@@ -18,15 +18,6 @@ export declare const CLI_DEFAULT_INSTALL_COMMAND = "npx -y autokap@latest";
|
|
|
18
18
|
export declare const CLI_DEFAULT_INSTALLED_SETUP_COMMAND = "autokap login <your-api-key>";
|
|
19
19
|
export declare const CLI_DEFAULT_SETUP_COMMAND = "npx -y autokap@latest login <your-api-key>";
|
|
20
20
|
export declare const CLI_FALLBACK_PROGRAM_COMMAND = "autokap run <preset-id> --program <file>";
|
|
21
|
-
/** Bare MCP runner. Used by generic clients and as the npx command. */
|
|
22
|
-
export declare const MCP_INSTALL_COMMAND = "npx -y @autokap/mcp";
|
|
23
|
-
/** One-shot install command for Claude Code (CLI flag `claude mcp add`). */
|
|
24
|
-
export declare const MCP_CLAUDE_CODE_COMMAND = "claude mcp add autokap -- npx -y @autokap/mcp";
|
|
25
|
-
/**
|
|
26
|
-
* JSON snippet to merge into the IDE-specific MCP config file (Cursor, Codex,
|
|
27
|
-
* Windsurf, Cline...). Format follows the shared `mcpServers` schema.
|
|
28
|
-
*/
|
|
29
|
-
export declare const MCP_CONFIG_SNIPPET_JSON: string;
|
|
30
21
|
export declare function buildCliRunCommand(presetId: string, options?: {
|
|
31
22
|
local?: boolean;
|
|
32
23
|
env?: string;
|
|
@@ -131,6 +122,11 @@ export interface ArtifactUploadMetadata {
|
|
|
131
122
|
targetLabel?: string | null;
|
|
132
123
|
programVersion?: number | null;
|
|
133
124
|
compileFingerprint?: string | null;
|
|
125
|
+
engineVersion?: number | null;
|
|
126
|
+
programSchemaVersion?: number | null;
|
|
127
|
+
programSchemaVersionOrigin?: number | null;
|
|
128
|
+
cliVersion?: string | null;
|
|
129
|
+
programHash?: string | null;
|
|
134
130
|
mediaMode: "screenshot" | "clip" | "video";
|
|
135
131
|
mimeType: string;
|
|
136
132
|
captureType: "fullpage" | "element";
|
package/dist/cli-contract.js
CHANGED
|
@@ -8,47 +8,20 @@ export const CLI_KEY_TERM = API_KEY_TERM;
|
|
|
8
8
|
export const CLI_VERSION_HEADER = "x-autokap-cli-version";
|
|
9
9
|
export const MIN_CLI_VERSION_FOR_SIGNED_PROGRAMS = "1.0.9";
|
|
10
10
|
export const MIN_CLI_VERSION_FOR_DIRECT_ARTIFACT_UPLOADS = "1.5.0";
|
|
11
|
-
//
|
|
12
|
-
// recommend `npx -y autokap@latest` over a global install to
|
|
13
|
-
// drift contained
|
|
14
|
-
//
|
|
15
|
-
// legacy doc surfaces that still want a single shell snippet.
|
|
11
|
+
// The bundled CLI binary ships for CI / Cloud Run and for running preset
|
|
12
|
+
// captures locally; recommend `npx -y autokap@latest` over a global install to
|
|
13
|
+
// keep version drift contained. The `CLI_DEFAULT_INSTALL_COMMAND` alias is the
|
|
14
|
+
// single shell snippet doc surfaces reference.
|
|
16
15
|
export const CLI_DEFAULT_INSTALL_COMMAND = "npx -y autokap@latest";
|
|
17
16
|
// `autokap init` was removed in v2. The setup command now uses the surviving
|
|
18
17
|
// `autokap login` for the same purpose (store the API key in ~/.autokap/config.json).
|
|
19
|
-
// MCP-first workflows should use `autokap_authenticate` from the IDE instead.
|
|
20
18
|
export const CLI_DEFAULT_INSTALLED_SETUP_COMMAND = "autokap login <your-api-key>";
|
|
21
19
|
export const CLI_DEFAULT_SETUP_COMMAND = "npx -y autokap@latest login <your-api-key>";
|
|
22
|
-
// `autokap skill --agent` was removed in v2. The
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// surfacing it would mislead users into typing a non-existent command.
|
|
20
|
+
// `autokap skill --agent` was removed in v2. The legacy `CLI_ADVANCED_SKILL_COMMAND`
|
|
21
|
+
// constant that pointed at the removed command was dropped entirely — it had no
|
|
22
|
+
// consumers and any future doc page surfacing it would mislead users into typing
|
|
23
|
+
// a non-existent command.
|
|
27
24
|
export const CLI_FALLBACK_PROGRAM_COMMAND = "autokap run <preset-id> --program <file>";
|
|
28
|
-
// ── MCP-first install contract ──────────────────────────────────────
|
|
29
|
-
//
|
|
30
|
-
// AutoKap is primarily exposed as a Model Context Protocol server via the
|
|
31
|
-
// `@autokap/mcp` package. Every supported IDE installs the server by adding a
|
|
32
|
-
// single stdio entry that runs `npx -y @autokap/mcp` — the constants below
|
|
33
|
-
// are the canonical shell forms used across onboarding surfaces (wizard,
|
|
34
|
-
// modal, banner, docs). For per-IDE payloads (config paths, snippets, notes),
|
|
35
|
-
// use `getMcpInstallEntries()` from `web/lib/site-discovery.ts`.
|
|
36
|
-
/** Bare MCP runner. Used by generic clients and as the npx command. */
|
|
37
|
-
export const MCP_INSTALL_COMMAND = "npx -y @autokap/mcp";
|
|
38
|
-
/** One-shot install command for Claude Code (CLI flag `claude mcp add`). */
|
|
39
|
-
export const MCP_CLAUDE_CODE_COMMAND = "claude mcp add autokap -- npx -y @autokap/mcp";
|
|
40
|
-
/**
|
|
41
|
-
* JSON snippet to merge into the IDE-specific MCP config file (Cursor, Codex,
|
|
42
|
-
* Windsurf, Cline...). Format follows the shared `mcpServers` schema.
|
|
43
|
-
*/
|
|
44
|
-
export const MCP_CONFIG_SNIPPET_JSON = JSON.stringify({
|
|
45
|
-
mcpServers: {
|
|
46
|
-
autokap: {
|
|
47
|
-
command: "npx",
|
|
48
|
-
args: ["-y", "@autokap/mcp"],
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
}, null, 2);
|
|
52
25
|
export function buildCliRunCommand(presetId, options = {}) {
|
|
53
26
|
const flags = [
|
|
54
27
|
options.local ? " --local" : "",
|
|
@@ -65,19 +38,19 @@ export const CLI_PUBLIC_COMMANDS = [
|
|
|
65
38
|
{
|
|
66
39
|
id: "login",
|
|
67
40
|
command: "autokap login <key>",
|
|
68
|
-
summary: "Authenticate with your API key (used by Cloud Run + CI
|
|
41
|
+
summary: "Authenticate with your API key (used by Cloud Run + CI and for local preset runs)",
|
|
69
42
|
docsDescriptionKey: "cliCmdLogin",
|
|
70
43
|
},
|
|
71
44
|
{
|
|
72
45
|
id: "run",
|
|
73
46
|
command: "autokap run <preset-id> --env local",
|
|
74
|
-
summary: "Run a preset capture using local Playwright
|
|
47
|
+
summary: "Run a preset capture using local Playwright",
|
|
75
48
|
docsDescriptionKey: "cliCmdRun",
|
|
76
49
|
},
|
|
77
50
|
{
|
|
78
51
|
id: "auto-recapture",
|
|
79
52
|
command: "autokap auto-recapture --project <project-id> --env local",
|
|
80
|
-
summary: "Run every preset enabled for Recapture Cloud in a project (the canonical surface for Cloud Run + CI
|
|
53
|
+
summary: "Run every preset enabled for Recapture Cloud in a project (the canonical surface for Cloud Run + CI)",
|
|
81
54
|
docsDescriptionKey: "cliCmdAutoRecapture",
|
|
82
55
|
},
|
|
83
56
|
{
|
package/dist/cli-runner.d.ts
CHANGED
|
@@ -19,7 +19,6 @@ export interface RecordableBrowserSettings {
|
|
|
19
19
|
runtimeDeviceScaleFactor: number;
|
|
20
20
|
}
|
|
21
21
|
export declare function resolveRecordableBrowserSettings(program: ExecutionProgram, variant: VariantSpec): RecordableBrowserSettings;
|
|
22
|
-
export declare function normalizeVideoCaptureProgram(program: ExecutionProgram): ExecutionProgram;
|
|
23
22
|
export interface CLIRunnerOptions {
|
|
24
23
|
/** Preset ID to run */
|
|
25
24
|
presetId: string;
|