@docsxai/engine 0.2.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.
Files changed (129) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +130 -0
  3. package/dist/auth/api-login.d.ts +69 -0
  4. package/dist/auth/api-login.js +95 -0
  5. package/dist/auth/browser-session.d.ts +28 -0
  6. package/dist/auth/browser-session.js +43 -0
  7. package/dist/auth/cookie-jar.d.ts +58 -0
  8. package/dist/auth/cookie-jar.js +212 -0
  9. package/dist/auth/email-otp.d.ts +210 -0
  10. package/dist/auth/email-otp.js +166 -0
  11. package/dist/auth/http-basic.d.ts +5 -0
  12. package/dist/auth/http-basic.js +17 -0
  13. package/dist/auth/index.d.ts +47 -0
  14. package/dist/auth/index.js +137 -0
  15. package/dist/auth/jwt-injection.d.ts +153 -0
  16. package/dist/auth/jwt-injection.js +136 -0
  17. package/dist/auth/manual-capture.d.ts +35 -0
  18. package/dist/auth/manual-capture.js +30 -0
  19. package/dist/auth/mtls.d.ts +15 -0
  20. package/dist/auth/mtls.js +53 -0
  21. package/dist/auth/pat-header.d.ts +19 -0
  22. package/dist/auth/pat-header.js +34 -0
  23. package/dist/auth/storage-state-cache.d.ts +38 -0
  24. package/dist/auth/storage-state-cache.js +143 -0
  25. package/dist/auth/test-backdoor.d.ts +25 -0
  26. package/dist/auth/test-backdoor.js +51 -0
  27. package/dist/auth/totp.d.ts +39 -0
  28. package/dist/auth/totp.js +108 -0
  29. package/dist/auth/types.d.ts +86 -0
  30. package/dist/auth/types.js +57 -0
  31. package/dist/auth/ui-form.d.ts +204 -0
  32. package/dist/auth/ui-form.js +153 -0
  33. package/dist/auth/webauthn.d.ts +88 -0
  34. package/dist/auth/webauthn.js +67 -0
  35. package/dist/auth.d.ts +1 -0
  36. package/dist/auth.js +3 -0
  37. package/dist/backend-client-contracts.d.ts +88 -0
  38. package/dist/backend-client-contracts.js +19 -0
  39. package/dist/backend-client-oauth-login.d.ts +7 -0
  40. package/dist/backend-client-oauth-login.js +90 -0
  41. package/dist/backend-client-state-cache.d.ts +73 -0
  42. package/dist/backend-client-state-cache.js +185 -0
  43. package/dist/backend-client-token.d.ts +18 -0
  44. package/dist/backend-client-token.js +94 -0
  45. package/dist/backend-client-transport.d.ts +66 -0
  46. package/dist/backend-client-transport.js +181 -0
  47. package/dist/backend-client.d.ts +5 -0
  48. package/dist/backend-client.js +18 -0
  49. package/dist/calibrate.d.ts +31 -0
  50. package/dist/calibrate.js +68 -0
  51. package/dist/cli-commands-authoring.d.ts +5 -0
  52. package/dist/cli-commands-authoring.js +403 -0
  53. package/dist/cli-commands-backend.d.ts +5 -0
  54. package/dist/cli-commands-backend.js +211 -0
  55. package/dist/cli-commands-docpack.d.ts +5 -0
  56. package/dist/cli-commands-docpack.js +280 -0
  57. package/dist/cli-commands-session.d.ts +4 -0
  58. package/dist/cli-commands-session.js +398 -0
  59. package/dist/cli-shared.d.ts +5 -0
  60. package/dist/cli-shared.js +45 -0
  61. package/dist/cli-usage.d.ts +1 -0
  62. package/dist/cli-usage.js +137 -0
  63. package/dist/cli.d.ts +2 -0
  64. package/dist/cli.js +77 -0
  65. package/dist/diagnose.d.ts +50 -0
  66. package/dist/diagnose.js +168 -0
  67. package/dist/diff-compute.d.ts +13 -0
  68. package/dist/diff-compute.js +378 -0
  69. package/dist/diff-report.d.ts +7 -0
  70. package/dist/diff-report.js +125 -0
  71. package/dist/diff-types.d.ts +125 -0
  72. package/dist/diff-types.js +15 -0
  73. package/dist/diff.d.ts +3 -0
  74. package/dist/diff.js +16 -0
  75. package/dist/doc-pack-io.d.ts +30 -0
  76. package/dist/doc-pack-io.js +182 -0
  77. package/dist/doc-pack.d.ts +1814 -0
  78. package/dist/doc-pack.js +328 -0
  79. package/dist/doctor-checks-plugins.d.ts +2 -0
  80. package/dist/doctor-checks-plugins.js +136 -0
  81. package/dist/doctor-checks.d.ts +56 -0
  82. package/dist/doctor-checks.js +367 -0
  83. package/dist/doctor.d.ts +7 -0
  84. package/dist/doctor.js +62 -0
  85. package/dist/export/adf.d.ts +57 -0
  86. package/dist/export/adf.js +323 -0
  87. package/dist/export/playwright-test.d.ts +26 -0
  88. package/dist/export/playwright-test.js +221 -0
  89. package/dist/flow-file.d.ts +21 -0
  90. package/dist/flow-file.js +180 -0
  91. package/dist/flow-lint.d.ts +24 -0
  92. package/dist/flow-lint.js +203 -0
  93. package/dist/flow-runtime.d.ts +113 -0
  94. package/dist/flow-runtime.js +273 -0
  95. package/dist/flow-tree.d.ts +19 -0
  96. package/dist/flow-tree.js +104 -0
  97. package/dist/index.d.ts +27 -0
  98. package/dist/index.js +31 -0
  99. package/dist/playwright-driver.d.ts +105 -0
  100. package/dist/playwright-driver.js +363 -0
  101. package/dist/playwright-instrumented-browser.d.ts +51 -0
  102. package/dist/playwright-instrumented-browser.js +189 -0
  103. package/dist/plugins/load.d.ts +22 -0
  104. package/dist/plugins/load.js +99 -0
  105. package/dist/plugins/lock.d.ts +40 -0
  106. package/dist/plugins/lock.js +122 -0
  107. package/dist/plugins/manifest.d.ts +70 -0
  108. package/dist/plugins/manifest.js +115 -0
  109. package/dist/plugins/plan.d.ts +51 -0
  110. package/dist/plugins/plan.js +279 -0
  111. package/dist/plugins/registry.d.ts +59 -0
  112. package/dist/plugins/registry.js +71 -0
  113. package/dist/plugins/runtime.d.ts +7 -0
  114. package/dist/plugins/runtime.js +27 -0
  115. package/dist/plugins/types.d.ts +58 -0
  116. package/dist/plugins/types.js +4 -0
  117. package/dist/plugins-cli.d.ts +1 -0
  118. package/dist/plugins-cli.js +191 -0
  119. package/dist/redact.d.ts +16 -0
  120. package/dist/redact.js +72 -0
  121. package/dist/style.d.ts +46 -0
  122. package/dist/style.js +151 -0
  123. package/dist/viewer-bin.d.ts +20 -0
  124. package/dist/viewer-bin.js +97 -0
  125. package/dist/workspace.d.ts +60 -0
  126. package/dist/workspace.js +172 -0
  127. package/dist/zip.d.ts +17 -0
  128. package/dist/zip.js +113 -0
  129. package/package.json +64 -0
@@ -0,0 +1,273 @@
1
+ // Flow-file runtime — the deterministic execution side.
2
+ //
3
+ // `docsxai run` translates a parsed flow-file into a sequence of browser actions, executes
4
+ // them headlessly with zero LLM involvement, re-captures screenshots, and re-emits the doc-pack
5
+ // artifacts (annotations, etc.). One canonical locator per step; execution halts on locator or
6
+ // success-criterion failure — drift is a signal to recalibrate, not to absorb (no fallbacks).
7
+ //
8
+ // The runtime is written against a thin {@link BrowserDriver} abstraction so it's testable
9
+ // without a real browser; the Playwright-backed driver lives in a separate module.
10
+ import { locatorRefName } from "./flow-file.js";
11
+ // ---------------------------------------------------------------------------
12
+ // Errors
13
+ // ---------------------------------------------------------------------------
14
+ export class FlowExecutionError extends Error {
15
+ stepId;
16
+ cause;
17
+ constructor(message, stepId, cause) {
18
+ super(message);
19
+ this.stepId = stepId;
20
+ this.cause = cause;
21
+ this.name = "FlowExecutionError";
22
+ }
23
+ }
24
+ /**
25
+ * Best-effort 1-line cause extracted from a Playwright actionability log (or similar driver
26
+ * error). Returns undefined when nothing matches — keeps the halt message short rather than
27
+ * guessing. Surfaced as a `[cause]` prefix on the halt message so the agent doesn't have to
28
+ * scan the multi-line actionability log.
29
+ */
30
+ export function inferHaltCause(rawError) {
31
+ const hints = [
32
+ [/element is disabled\b/i, "target is disabled"],
33
+ [/element is not enabled\b/i, "target is not enabled"],
34
+ [
35
+ /element is not visible\b/i,
36
+ "target is not visible (display:none / visibility:hidden / zero-sized)",
37
+ ],
38
+ [
39
+ /element is not attached\b/i,
40
+ "target was detached from the DOM (likely unmounted by an earlier action)",
41
+ ],
42
+ [/element is outside of the viewport\b/i, "target is outside the visible viewport"],
43
+ [/element is not stable\b/i, "target is animating / not yet stable"],
44
+ [/intercepts? pointer events\b/i, "target is covered by another element"],
45
+ [
46
+ /strict mode violation\b/i,
47
+ "selector matched multiple elements (strict-mode violation) — scope with :visible / :nth-match",
48
+ ],
49
+ [
50
+ /timeout .* exceeded.*waiting for/is,
51
+ "timeout waiting for selector — element didn't appear in time (consider raising timeout_ms or revisiting the locator)",
52
+ ],
53
+ ];
54
+ for (const [re, msg] of hints) {
55
+ if (re.test(rawError))
56
+ return msg;
57
+ }
58
+ const resolved = rawError.match(/locator resolved to (<[^>\n]*>)/);
59
+ if (resolved)
60
+ return `target resolved to ${resolved[1]}`;
61
+ return undefined;
62
+ }
63
+ const defaultScreenshotPath = (flow, stepId) => `docs/${flow}/screenshots/${stepId}.png`;
64
+ /** Resolve a `target` value (`$name` ref or inline selector) using the flow-file's locators (or a custom resolver). */
65
+ export function resolveTarget(value, flow, resolver) {
66
+ const name = locatorRefName(value);
67
+ if (!name)
68
+ return value; // inline selector
69
+ const resolved = (resolver ?? ((n) => flow.locators[n]))(name);
70
+ if (resolved === undefined) {
71
+ throw new Error(`unresolved locator $${name}`);
72
+ }
73
+ return resolved;
74
+ }
75
+ async function applyWait(driver, wait, resolve) {
76
+ if (typeof wait === "string") {
77
+ if (wait === "network_idle")
78
+ return driver.waitForNetworkIdle();
79
+ if (wait === "load")
80
+ return driver.waitForLoad();
81
+ // element_stable without a selector is a no-op signal in this prototype; a real driver may track layout.
82
+ return;
83
+ }
84
+ if ("selector" in wait)
85
+ return driver.waitForSelector(resolve(wait.selector), wait.timeout_ms);
86
+ if ("timeout_ms" in wait)
87
+ return driver.waitForTimeout(wait.timeout_ms);
88
+ }
89
+ async function checkSuccess(driver, success, resolve, stepId) {
90
+ const at = async () => `at ${await driver.currentUrl().catch(() => "?")}`;
91
+ if ("visible" in success) {
92
+ const sel = resolve(success.visible);
93
+ if (!(await driver.isVisible(sel))) {
94
+ throw new FlowExecutionError(`expected ${success.visible} to be visible — ${await at()}; ${await driver.count(sel).catch(() => "?")} element(s) match the selector`, stepId);
95
+ }
96
+ return;
97
+ }
98
+ if ("hidden" in success) {
99
+ const sel = resolve(success.hidden);
100
+ if (await driver.isVisible(sel)) {
101
+ throw new FlowExecutionError(`expected ${success.hidden} to be hidden but a match is visible — ${await at()}; ${await driver.count(sel).catch(() => "?")} element(s) match`, stepId);
102
+ }
103
+ return;
104
+ }
105
+ if ("url_matches" in success) {
106
+ if (!(await driver.urlMatches(success.url_matches))) {
107
+ throw new FlowExecutionError(`expected URL to match /${success.url_matches}/ — actual: ${await driver.currentUrl().catch(() => "?")}`, stepId);
108
+ }
109
+ return;
110
+ }
111
+ if ("text_contains" in success) {
112
+ const { selector, text } = success.text_contains;
113
+ const sel = resolve(selector);
114
+ if (!(await driver.textContains(sel, text))) {
115
+ const actual = ((await driver.textOf(sel).catch(() => null)) ?? "")
116
+ .replace(/\s+/g, " ")
117
+ .trim()
118
+ .slice(0, 200);
119
+ throw new FlowExecutionError(`expected ${selector} to contain ${JSON.stringify(text)} — ${await at()}; actual text: ${JSON.stringify(actual)}`, stepId);
120
+ }
121
+ }
122
+ }
123
+ async function executeAction(driver, step, selector) {
124
+ switch (step.action) {
125
+ case "navigate":
126
+ if (!step.value)
127
+ throw new FlowExecutionError("navigate requires `value` (path/URL)", step.id);
128
+ return driver.goto(step.value);
129
+ case "click":
130
+ return driver.click(needSelector(selector, step));
131
+ case "fill":
132
+ if (step.value === undefined)
133
+ throw new FlowExecutionError("fill requires `value`", step.id);
134
+ return driver.fill(needSelector(selector, step), step.value);
135
+ case "upload":
136
+ if (step.value === undefined)
137
+ throw new FlowExecutionError("upload requires `value` (file path)", step.id);
138
+ return driver.upload(needSelector(selector, step), step.value);
139
+ case "press":
140
+ if (!step.value)
141
+ throw new FlowExecutionError("press requires `value` (key)", step.id);
142
+ return driver.press(selector, step.value);
143
+ case "hover":
144
+ return driver.hover(needSelector(selector, step));
145
+ case "select":
146
+ if (step.value === undefined)
147
+ throw new FlowExecutionError("select requires `value` (option)", step.id);
148
+ return driver.selectOption(needSelector(selector, step), step.value);
149
+ case "check":
150
+ return driver.setChecked(needSelector(selector, step), true);
151
+ case "uncheck":
152
+ return driver.setChecked(needSelector(selector, step), false);
153
+ case "wait":
154
+ return; // a bare `wait` step just runs its `wait_for`
155
+ }
156
+ }
157
+ function needSelector(selector, step) {
158
+ if (selector === null)
159
+ throw new FlowExecutionError(`action "${step.action}" requires a \`target\``, step.id);
160
+ return selector;
161
+ }
162
+ /**
163
+ * Execute a flow-file against a {@link BrowserDriver}, re-capturing screenshots and emitting annotation records.
164
+ * Deterministic: given the same site state and driver behaviour, produces the same result. Halts on the first
165
+ * locator / success-criterion failure.
166
+ */
167
+ export async function runFlow(flow, driver, opts = {}) {
168
+ const captureDocs = opts.captureDocs ?? true;
169
+ const screenshotPathOf = opts.screenshotPath ?? defaultScreenshotPath;
170
+ const resolve = (v) => resolveTarget(v, flow, opts.resolveLocator);
171
+ const executed = [];
172
+ const annotations = [];
173
+ // startFrom validation: if set, must name an actual step id in this (already-merged) flow.
174
+ // Catching the typo here is cheaper than running, halting, and reading the wall of Playwright noise.
175
+ if (opts.startFrom && !flow.steps.some((s) => s.id === opts.startFrom)) {
176
+ throw new Error(`startFrom: no step with id "${opts.startFrom}" in flow "${flow.name}"`);
177
+ }
178
+ let skipping = !!opts.startFrom;
179
+ // Flow-level redactions apply to every screenshot; per-step ones are additive. Resolved up
180
+ // front (locator refs → selectors, default style applied) so halt shots get them too.
181
+ const redactionsFor = (step) => [...(flow.redactions ?? []), ...(step.redactions ?? [])].map((r) => "selector" in r
182
+ ? { selector: resolve(r.selector), style: r.style ?? "box" }
183
+ : { region: r.region, style: r.style ?? "box" });
184
+ for (const step of flow.steps) {
185
+ if (skipping) {
186
+ if (step.id === opts.startFrom)
187
+ skipping = false;
188
+ else
189
+ continue;
190
+ }
191
+ const selector = step.target ? resolve(step.target) : null;
192
+ const redactions = redactionsFor(step);
193
+ try {
194
+ await executeAction(driver, step, selector);
195
+ if (step.wait_for) {
196
+ if (step.wait_for === "element_stable" && selector)
197
+ await driver.waitForElementStable(selector);
198
+ else
199
+ await applyWait(driver, step.wait_for, resolve);
200
+ }
201
+ if (step.success)
202
+ await checkSuccess(driver, step.success, resolve, step.id);
203
+ }
204
+ catch (e) {
205
+ // Optional step (conditionally-present UI): swallow the failure, log it, move on.
206
+ // No screenshot / annotation for a skipped step — same as a `--start-from`-skipped one.
207
+ if (step.optional) {
208
+ process.stderr.write(`runFlow: optional step "${step.id}" (${step.action}) skipped — ${e.message}\n`);
209
+ continue;
210
+ }
211
+ // Halt: dump a screenshot for triage (best-effort), prepend a 1-line inferred cause
212
+ // (parsed from Playwright's actionability log so the agent doesn't have to scan ~20 lines
213
+ // to know why), then surface step id + url + halt-shot path uniformly.
214
+ const haltShot = `docs/${flow.name}/halts/${step.id}.png`;
215
+ // Halt shots can capture the same sensitive UI as step shots — same redactions apply.
216
+ if (captureDocs)
217
+ await driver.screenshot(haltShot, redactions).catch(() => undefined);
218
+ const suffix = captureDocs ? ` (halt screenshot: ${haltShot})` : "";
219
+ const cause = inferHaltCause(e.message ?? "");
220
+ const causePrefix = cause ? `[${cause}] ` : "";
221
+ if (e instanceof FlowExecutionError) {
222
+ throw new FlowExecutionError(`${causePrefix}${e.message}${suffix}`, e.stepId, e.cause);
223
+ }
224
+ const where = await driver.currentUrl().catch(() => "?");
225
+ throw new FlowExecutionError(`${causePrefix}step "${step.id}" (${step.action}) failed at ${where}: ${e.message}${suffix}`, step.id, e);
226
+ }
227
+ const ex = {
228
+ id: step.id,
229
+ action: step.action,
230
+ ...(selector ? { selector } : {}),
231
+ };
232
+ // Doc capture is best-effort. When a step's action *transitions the UI* the action target is often
233
+ // unmounted by the time we capture — `boundingBox` would hang for the driver's default 30s. Short
234
+ // timeout + try/catch → continue with no annotation for this step. `annotation.target` (if set)
235
+ // overrides the anchor — point the halo at a different element that *does* exist in the new state.
236
+ // A step can also have `annotations: [...]` to put multiple numbered call-outs on the same screenshot —
237
+ // each becomes one record with a 1-based `index`; an `annotation` (singular) emits one record without
238
+ // `index` (un-numbered, back-compat).
239
+ const anns = step.annotations ?? (step.annotation ? [step.annotation] : []);
240
+ if (captureDocs && anns.length > 0) {
241
+ try {
242
+ const shot = screenshotPathOf(flow.name, step.id);
243
+ await driver.screenshot(shot, redactions);
244
+ ex.screenshot = shot;
245
+ for (let i = 0; i < anns.length; i++) {
246
+ const ann = anns[i];
247
+ const annSelector = ann.target ? resolve(ann.target) : selector;
248
+ const bbox = annSelector ? await driver.boundingBox(annSelector, 2000) : null;
249
+ annotations.push({
250
+ step: step.id,
251
+ selector: annSelector ?? "",
252
+ ...(bbox ? { bounding_box: bbox } : {}),
253
+ copy: ann.copy,
254
+ ...(ann.arrow ? { arrow_style: ann.arrow } : {}),
255
+ ...(ann.nudge ? { nudge: ann.nudge } : {}),
256
+ ...(anns.length > 1 ? { index: i + 1 } : {}),
257
+ });
258
+ }
259
+ }
260
+ catch (e) {
261
+ process.stderr.write(`runFlow: step "${step.id}" — annotation capture skipped (${e.message})\n`);
262
+ }
263
+ }
264
+ executed.push(ex);
265
+ if (opts.stopAfter && step.id === opts.stopAfter)
266
+ break;
267
+ }
268
+ return {
269
+ flow: flow.name,
270
+ steps: executed,
271
+ annotations: { schema: "docsxai/annotations@1", flow: flow.name, annotations },
272
+ };
273
+ }
@@ -0,0 +1,19 @@
1
+ import type { FlowFile } from "./doc-pack.js";
2
+ export type FlowTreeNode = {
3
+ name: string;
4
+ steps: number;
5
+ children: FlowTreeNode[];
6
+ };
7
+ export type FlowTreeIssue = {
8
+ flow: string;
9
+ message: string;
10
+ };
11
+ export type FlowTreeResult = {
12
+ roots: FlowTreeNode[];
13
+ /** Flows whose `extends` target isn't in the workspace. Listed separately so they're visible. */
14
+ orphans: FlowTreeNode[];
15
+ /** Resolution-time errors (cycles, step-id collisions across the merge, missing extends targets). */
16
+ issues: FlowTreeIssue[];
17
+ };
18
+ export declare function buildFlowTree(flowsByName: Map<string, FlowFile>): Promise<FlowTreeResult>;
19
+ export declare function formatTreeText(result: FlowTreeResult): string;
@@ -0,0 +1,104 @@
1
+ // Visualise the `extends` graph across a workspace's flow-files; report step-id collisions
2
+ // across the merged step list. Pure-static — no Playwright, no live page. Run via `docsxai flow-tree`.
3
+ import { resolveFlowExtends } from "./flow-file.js";
4
+ export async function buildFlowTree(flowsByName) {
5
+ const childrenOf = new Map();
6
+ const orphanNames = [];
7
+ const rootNames = [];
8
+ for (const [name, flow] of flowsByName) {
9
+ if (!flow.extends) {
10
+ rootNames.push(name);
11
+ }
12
+ else if (!flowsByName.has(flow.extends)) {
13
+ orphanNames.push(name);
14
+ }
15
+ else {
16
+ const list = childrenOf.get(flow.extends) ?? [];
17
+ list.push(name);
18
+ childrenOf.set(flow.extends, list);
19
+ }
20
+ }
21
+ const build = (name) => {
22
+ const flow = flowsByName.get(name);
23
+ const kids = (childrenOf.get(name) ?? []).slice().sort().map(build);
24
+ return { name, steps: flow.steps.length, children: kids };
25
+ };
26
+ const issues = [];
27
+ const loadFlow = (n) => {
28
+ const f = flowsByName.get(n);
29
+ if (!f)
30
+ throw new Error(`extends target not found: ${n}`);
31
+ return f;
32
+ };
33
+ for (const [name, flow] of flowsByName) {
34
+ if (!flow.extends || orphanNames.includes(name))
35
+ continue;
36
+ try {
37
+ await resolveFlowExtends(flow, loadFlow);
38
+ }
39
+ catch (e) {
40
+ issues.push({ flow: name, message: e.message });
41
+ }
42
+ }
43
+ return {
44
+ roots: rootNames.slice().sort().map(build),
45
+ orphans: orphanNames.slice().sort().map(build),
46
+ issues,
47
+ };
48
+ }
49
+ export function formatTreeText(result) {
50
+ let out = "";
51
+ for (const root of result.roots) {
52
+ out += renderRoot(root);
53
+ out += "\n";
54
+ }
55
+ if (result.orphans.length) {
56
+ out += "(extends parent not in workspace — orphan flows)\n";
57
+ for (const o of result.orphans)
58
+ out += renderRoot(o);
59
+ out += "\n";
60
+ }
61
+ const total = countTotal(result);
62
+ const maxDepth = Math.max(0, ...result.roots.map(depth), ...result.orphans.map(depth));
63
+ out += `${total} flow${total !== 1 ? "s" : ""}, max chain depth ${maxDepth}\n`;
64
+ if (result.issues.length) {
65
+ out += "\nissues:\n";
66
+ for (const i of result.issues)
67
+ out += ` ${i.flow}: ${i.message}\n`;
68
+ }
69
+ return out;
70
+ }
71
+ function renderRoot(node) {
72
+ let out = `${node.name} [${node.steps} step${node.steps !== 1 ? "s" : ""}]\n`;
73
+ for (let i = 0; i < node.children.length; i++) {
74
+ out += renderChild(node.children[i], "", i === node.children.length - 1);
75
+ }
76
+ return out;
77
+ }
78
+ function renderChild(node, parentPrefix, isLast) {
79
+ const connector = isLast ? "└── " : "├── ";
80
+ let out = `${parentPrefix}${connector}${node.name} [+${node.steps} step${node.steps !== 1 ? "s" : ""}]\n`;
81
+ const childPrefix = parentPrefix + (isLast ? " " : "│ ");
82
+ for (let i = 0; i < node.children.length; i++) {
83
+ out += renderChild(node.children[i], childPrefix, i === node.children.length - 1);
84
+ }
85
+ return out;
86
+ }
87
+ function countTotal(result) {
88
+ let n = 0;
89
+ const walk = (node) => {
90
+ n++;
91
+ for (const c of node.children)
92
+ walk(c);
93
+ };
94
+ for (const r of result.roots)
95
+ walk(r);
96
+ for (const o of result.orphans)
97
+ walk(o);
98
+ return n;
99
+ }
100
+ function depth(node) {
101
+ if (node.children.length === 0)
102
+ return 1;
103
+ return 1 + Math.max(...node.children.map(depth));
104
+ }
@@ -0,0 +1,27 @@
1
+ export declare const name = "@docsxai/engine";
2
+ export * from "./doc-pack.js";
3
+ export * from "./doc-pack-io.js";
4
+ export * from "./flow-file.js";
5
+ export * from "./flow-runtime.js";
6
+ export * from "./flow-lint.js";
7
+ export * from "./flow-tree.js";
8
+ export * from "./auth.js";
9
+ export * from "./backend-client.js";
10
+ export * from "./calibrate.js";
11
+ export * from "./diagnose.js";
12
+ export * from "./doctor.js";
13
+ export * from "./playwright-driver.js";
14
+ export * from "./playwright-instrumented-browser.js";
15
+ export * from "./style.js";
16
+ export * from "./workspace.js";
17
+ export * from "./zip.js";
18
+ export * from "./plugins/types.js";
19
+ export * from "./plugins/manifest.js";
20
+ export * from "./plugins/registry.js";
21
+ export * from "./plugins/runtime.js";
22
+ export * from "./plugins/lock.js";
23
+ export * from "./redact.js";
24
+ export * from "./viewer-bin.js";
25
+ export * from "./export/adf.js";
26
+ export * from "./export/playwright-test.js";
27
+ export * from "./diff.js";
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ // @docsxai/engine
2
+ // LLM-agnostic engine: flow-file parser + deterministic runtime + the target-site auth-strategy
3
+ // layer + calibration-aid helpers (lint, diagnose, flow-tree, style) + doc-pack IO, the backend
4
+ // client, and the zip hand-off packager.
5
+ export const name = "@docsxai/engine";
6
+ export * from "./doc-pack.js";
7
+ export * from "./doc-pack-io.js";
8
+ export * from "./flow-file.js";
9
+ export * from "./flow-runtime.js";
10
+ export * from "./flow-lint.js";
11
+ export * from "./flow-tree.js";
12
+ export * from "./auth.js";
13
+ export * from "./backend-client.js";
14
+ export * from "./calibrate.js";
15
+ export * from "./diagnose.js";
16
+ export * from "./doctor.js";
17
+ export * from "./playwright-driver.js";
18
+ export * from "./playwright-instrumented-browser.js";
19
+ export * from "./style.js";
20
+ export * from "./workspace.js";
21
+ export * from "./zip.js";
22
+ export * from "./plugins/types.js";
23
+ export * from "./plugins/manifest.js";
24
+ export * from "./plugins/registry.js";
25
+ export * from "./plugins/runtime.js";
26
+ export * from "./plugins/lock.js";
27
+ export * from "./redact.js";
28
+ export * from "./viewer-bin.js";
29
+ export * from "./export/adf.js";
30
+ export * from "./export/playwright-test.js";
31
+ export * from "./diff.js";
@@ -0,0 +1,105 @@
1
+ import { type Browser, type BrowserContext, type Page } from "playwright-core";
2
+ import { type BoundingBox, type EnvironmentSpec, type ViewportSize } from "./doc-pack.js";
3
+ import { type ActionableState, type BrowserDriver, type ResolvedRedaction } from "./flow-runtime.js";
4
+ import { type StorageState } from "./auth.js";
5
+ /**
6
+ * The cached Chromium binary path, or `undefined` when `playwright-core` has none installed.
7
+ *
8
+ * This is the single sanctioned entry point to `chromium.executablePath()`: keeping the
9
+ * availability probe here (the one module allowed to static-import playwright-core) means callers
10
+ * like `docsxai doctor` can ask "is a browser present?" without opening a second playwright-core
11
+ * import site. `executablePath()` only reports where the binary *would* live, so we existsSync it.
12
+ */
13
+ export declare function chromiumExecutablePath(): string | undefined;
14
+ /**
15
+ * Transparent pass-through to `browser.newContext` for auth mechanisms the engine doesn't model
16
+ * itself (HTTP basic, mTLS client certs, static header tokens). The shapes mirror Playwright's
17
+ * context options; the engine never inspects them.
18
+ */
19
+ export interface SessionContextOptions {
20
+ httpCredentials?: {
21
+ username: string;
22
+ password: string;
23
+ };
24
+ clientCertificates?: unknown[];
25
+ extraHTTPHeaders?: Record<string, string>;
26
+ }
27
+ export interface PlaywrightSessionOptions {
28
+ /** Base URL for relative `goto` paths. */
29
+ baseURL?: string;
30
+ /** Captured session to seed the context with (from a calibration capture / the auth strategy). */
31
+ storageState?: StorageState;
32
+ /** Run headed (useful for `manual-capture` and debugging). Default: headless. */
33
+ headed?: boolean;
34
+ /** Accept self-signed / invalid TLS certs (e.g. an app's local HTTPS dev cert). Default: false. */
35
+ ignoreHTTPSErrors?: boolean;
36
+ /** Extra Chromium args (e.g. the security-lowered flags `manual-capture` uses). */
37
+ chromiumArgs?: string[];
38
+ /** Doc-pack root that screenshot paths are resolved against. */
39
+ docPackRoot?: string;
40
+ /** If set, attach to a running Chrome at this CDP endpoint (e.g. `http://localhost:9222`) instead of launching one. `close()` won't close it. */
41
+ connectOverCdp?: string;
42
+ /**
43
+ * Deterministic execution environment (a flow-file's resolved `environment` block). Locale /
44
+ * timezone / viewport / color-scheme / reduced-motion are context options; the clock is frozen
45
+ * via Playwright's clock API on the session's page. With `connectOverCdp` the context is
46
+ * externally owned — only the clock applies; the rest is skipped with one stderr warning.
47
+ */
48
+ environment?: EnvironmentSpec;
49
+ /** Extra `browser.newContext` options — see {@link SessionContextOptions}. Ignored with `connectOverCdp`. */
50
+ contextOptions?: SessionContextOptions;
51
+ }
52
+ /** Map an {@link EnvironmentSpec} to the Playwright context options it pins (clock excluded — that's a page-level install). */
53
+ export declare function environmentContextOptions(env: EnvironmentSpec): {
54
+ locale?: string;
55
+ timezoneId?: string;
56
+ viewport?: ViewportSize;
57
+ colorScheme?: "light" | "dark";
58
+ reducedMotion?: "reduce" | "no-preference";
59
+ };
60
+ /** A launched Playwright browser + context + page, plus the driver bound to it. Call `close()` when done. */
61
+ export interface PlaywrightSession {
62
+ browser: Browser;
63
+ context: BrowserContext;
64
+ page: Page;
65
+ driver: BrowserDriver;
66
+ /** Snapshot the context's `storageState` (cookies + localStorage + sessionStorage). */
67
+ storageState(): Promise<StorageState>;
68
+ close(): Promise<void>;
69
+ }
70
+ /** Launch Chromium (or, with `connectOverCdp`, attach to a running one) and return a {@link PlaywrightSession}. */
71
+ export declare function launchPlaywrightSession(opts?: PlaywrightSessionOptions): Promise<PlaywrightSession>;
72
+ export declare class PlaywrightDriver implements BrowserDriver {
73
+ private readonly page;
74
+ private readonly docPackRoot;
75
+ constructor(page: Page, docPackRoot?: string);
76
+ goto(url: string): Promise<void>;
77
+ click(selector: string): Promise<void>;
78
+ fill(selector: string, value: string): Promise<void>;
79
+ upload(selector: string, filePath: string): Promise<void>;
80
+ press(selector: string | null, key: string): Promise<void>;
81
+ hover(selector: string): Promise<void>;
82
+ selectOption(selector: string, value: string): Promise<void>;
83
+ setChecked(selector: string, checked: boolean): Promise<void>;
84
+ waitForNetworkIdle(): Promise<void>;
85
+ waitForLoad(): Promise<void>;
86
+ waitForElementStable(selector: string): Promise<void>;
87
+ waitForSelector(selector: string, timeoutMs?: number): Promise<void>;
88
+ waitForTimeout(ms: number): Promise<void>;
89
+ isVisible(selector: string): Promise<boolean>;
90
+ urlMatches(pattern: string): Promise<boolean>;
91
+ textContains(selector: string, text: string): Promise<boolean>;
92
+ currentUrl(): Promise<string>;
93
+ count(selector: string): Promise<number>;
94
+ textOf(selector: string): Promise<string | null>;
95
+ boundingBox(selector: string, timeoutMs?: number): Promise<BoundingBox | null>;
96
+ screenshot(relPath: string, redactions?: ResolvedRedaction[]): Promise<void>;
97
+ /**
98
+ * Resolve redactions to pixel rects in the screenshot's device-pixel space: selector entries via
99
+ * {@link boundingBox} (already dpr-scaled), fixed regions scaled by the page's devicePixelRatio.
100
+ * A selector matching nothing on-page is skipped with a warning — an absent element is vacuously
101
+ * redacted; halting would punish flows for UI that legitimately isn't there.
102
+ */
103
+ private resolveRedactionBoxes;
104
+ actionable(selector: string, timeoutMs?: number): Promise<ActionableState>;
105
+ }