@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,181 @@
1
+ // REST transport for `@docsxai/backend`: the {@link BackendClient} HTTP surface, plus the
2
+ // token-resolving {@link createBackendClient} factory and the `docsxai run` history relay. Used by
3
+ // `docsxai push` / `pull` / `login` / `run`. Re-exported from `./backend-client.js`.
4
+ import { API_VERSION, API_VERSION_HEADER, BackendClientError, } from "./backend-client-contracts.js";
5
+ import { resolveBackendToken } from "./backend-client-token.js";
6
+ export class BackendClient {
7
+ baseUrl;
8
+ token;
9
+ doFetch;
10
+ constructor(opts) {
11
+ if (!opts.baseUrl)
12
+ throw new BackendClientError("baseUrl is required");
13
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
14
+ this.token = opts.token ?? process.env.DOCSX_TOKEN ?? "";
15
+ this.doFetch = opts.fetch ?? globalThis.fetch;
16
+ if (!this.token) {
17
+ throw new BackendClientError("no bearer token — set DOCSX_TOKEN env var or pass `token`. Run `docsxai login` to validate.");
18
+ }
19
+ }
20
+ headers(extra) {
21
+ return {
22
+ authorization: `Bearer ${this.token}`,
23
+ [API_VERSION_HEADER]: API_VERSION,
24
+ "content-type": "application/json",
25
+ ...(extra ?? {}),
26
+ };
27
+ }
28
+ async req(method, path, body) {
29
+ const url = `${this.baseUrl}${path}`;
30
+ const res = await this.doFetch(url, {
31
+ method,
32
+ headers: this.headers(),
33
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
34
+ });
35
+ if (!res.ok) {
36
+ const text = await res.text().catch(() => "");
37
+ let parsed = text;
38
+ try {
39
+ parsed = JSON.parse(text);
40
+ }
41
+ catch {
42
+ /* leave as text */
43
+ }
44
+ throw new BackendClientError(`${method} ${path} → ${res.status}: ${text.slice(0, 200)}`, res.status, parsed);
45
+ }
46
+ if (res.status === 204)
47
+ return undefined;
48
+ return (await res.json());
49
+ }
50
+ health() {
51
+ // /v1/health is the only no-auth endpoint; bypass the bearer here in case caller's token is bad.
52
+ return this.doFetch(`${this.baseUrl}/v1/health`).then((r) => {
53
+ if (!r.ok)
54
+ throw new BackendClientError(`health → ${r.status}`);
55
+ return r.json();
56
+ });
57
+ }
58
+ // --- workspaces ---
59
+ listWorkspaces() {
60
+ return this.req("GET", "/v1/workspaces");
61
+ }
62
+ createWorkspace(name) {
63
+ return this.req("POST", "/v1/workspaces", { name });
64
+ }
65
+ getWorkspace(id) {
66
+ return this.req("GET", `/v1/workspaces/${encodeURIComponent(id)}`);
67
+ }
68
+ // --- projects ---
69
+ listProjects(wsId) {
70
+ return this.req("GET", `/v1/workspaces/${encodeURIComponent(wsId)}/projects`);
71
+ }
72
+ createProject(wsId, name) {
73
+ return this.req("POST", `/v1/workspaces/${encodeURIComponent(wsId)}/projects`, { name });
74
+ }
75
+ getProject(wsId, projectId) {
76
+ return this.req("GET", `/v1/workspaces/${encodeURIComponent(wsId)}/projects/${encodeURIComponent(projectId)}`);
77
+ }
78
+ // --- revisions ---
79
+ listRevisions(wsId, projectId) {
80
+ return this.req("GET", `/v1/workspaces/${encodeURIComponent(wsId)}/projects/${encodeURIComponent(projectId)}/revisions`);
81
+ }
82
+ createRevision(wsId, projectId, body) {
83
+ return this.req("POST", `/v1/workspaces/${encodeURIComponent(wsId)}/projects/${encodeURIComponent(projectId)}/revisions`, body);
84
+ }
85
+ getRevision(wsId, projectId, rev) {
86
+ return this.req("GET", `/v1/workspaces/${encodeURIComponent(wsId)}/projects/${encodeURIComponent(projectId)}/revisions/${encodeURIComponent(rev)}`);
87
+ }
88
+ /** Finalize a revision (idempotent). Artifact PUTs afterwards are rejected with 409. */
89
+ finalizeRevision(wsId, projectId, rev) {
90
+ return this.req("POST", `/v1/workspaces/${encodeURIComponent(wsId)}/projects/${encodeURIComponent(projectId)}/revisions/${encodeURIComponent(rev)}/finalize`);
91
+ }
92
+ /** PUT an artifact's payload on a revision. The backend treats the payload as opaque JSON. */
93
+ putArtifact(wsId, projectId, rev, artifact, payload) {
94
+ return this.req("PUT", `/v1/workspaces/${encodeURIComponent(wsId)}/projects/${encodeURIComponent(projectId)}/revisions/${encodeURIComponent(rev)}/${artifact}`, payload);
95
+ }
96
+ getArtifact(wsId, projectId, rev, artifact) {
97
+ return this.req("GET", `/v1/workspaces/${encodeURIComponent(wsId)}/projects/${encodeURIComponent(projectId)}/revisions/${encodeURIComponent(rev)}/${artifact}`);
98
+ }
99
+ // --- run history ---
100
+ appendRun(wsId, projectId, rec) {
101
+ return this.req("POST", `/v1/workspaces/${encodeURIComponent(wsId)}/projects/${encodeURIComponent(projectId)}/run-history`, rec);
102
+ }
103
+ listRuns(wsId, projectId) {
104
+ return this.req("GET", `/v1/workspaces/${encodeURIComponent(wsId)}/projects/${encodeURIComponent(projectId)}/run-history`);
105
+ }
106
+ // --- content-addressed blobs ---
107
+ /** Upload raw bytes; the backend stores them under their sha256. Idempotent. */
108
+ async putBlob(data) {
109
+ const res = await this.doFetch(`${this.baseUrl}/v1/blobs`, {
110
+ method: "POST",
111
+ headers: this.headers({ "content-type": "application/octet-stream" }),
112
+ body: data,
113
+ });
114
+ if (!res.ok) {
115
+ throw new BackendClientError(`POST /v1/blobs → ${res.status}`, res.status);
116
+ }
117
+ return (await res.json());
118
+ }
119
+ /** HEAD-probe a blob — true when the backend already has these bytes. */
120
+ async hasBlob(sha256) {
121
+ const res = await this.doFetch(`${this.baseUrl}/v1/blobs/${encodeURIComponent(sha256)}`, {
122
+ method: "HEAD",
123
+ headers: this.headers(),
124
+ });
125
+ if (res.status === 404)
126
+ return false;
127
+ if (!res.ok)
128
+ throw new BackendClientError(`HEAD /v1/blobs/${sha256} → ${res.status}`, res.status);
129
+ return true;
130
+ }
131
+ async getBlob(sha256) {
132
+ const res = await this.doFetch(`${this.baseUrl}/v1/blobs/${encodeURIComponent(sha256)}`, {
133
+ headers: this.headers(),
134
+ });
135
+ if (!res.ok)
136
+ throw new BackendClientError(`GET /v1/blobs/${sha256} → ${res.status}`, res.status);
137
+ return new Uint8Array(await res.arrayBuffer());
138
+ }
139
+ }
140
+ /** Build a {@link BackendClient} with the token resolved via {@link resolveBackendToken}. */
141
+ export async function createBackendClient(opts) {
142
+ const token = await resolveBackendToken({
143
+ baseUrl: opts.baseUrl,
144
+ ...(opts.token !== undefined ? { token: opts.token } : {}),
145
+ ...(opts.workspaceDir !== undefined ? { workspaceDir: opts.workspaceDir } : {}),
146
+ ...(opts.fetch !== undefined ? { fetch: opts.fetch } : {}),
147
+ });
148
+ return new BackendClient({
149
+ baseUrl: opts.baseUrl,
150
+ token,
151
+ ...(opts.fetch !== undefined ? { fetch: opts.fetch } : {}),
152
+ });
153
+ }
154
+ // --- run-history wiring (`docsxai run`) -------------------------------------
155
+ /**
156
+ * Append an execution-run record for a backend-bound workspace. A no-op when the workspace config
157
+ * lacks the backend binding; never throws — `docsxai run` must stay offline-tolerant, so failures
158
+ * come back as a warning string for the caller to surface.
159
+ */
160
+ export async function recordRunHistory(opts) {
161
+ const { backend_url, backend_workspace_id, backend_project_id } = opts.config;
162
+ if (!backend_url || !backend_workspace_id || !backend_project_id)
163
+ return { recorded: false };
164
+ try {
165
+ const client = await createBackendClient({
166
+ baseUrl: backend_url,
167
+ workspaceDir: opts.workspaceDir,
168
+ ...(opts.fetch !== undefined ? { fetch: opts.fetch } : {}),
169
+ });
170
+ await client.appendRun(backend_workspace_id, backend_project_id, {
171
+ rev: "head",
172
+ ok: opts.ok,
173
+ duration_ms: opts.durationMs,
174
+ summary: opts.summary,
175
+ });
176
+ return { recorded: true };
177
+ }
178
+ catch (e) {
179
+ return { recorded: false, warning: `failed to record run history: ${e.message}` };
180
+ }
181
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./backend-client-contracts.js";
2
+ export * from "./backend-client-transport.js";
3
+ export * from "./backend-client-token.js";
4
+ export * from "./backend-client-oauth-login.js";
5
+ export * from "./backend-client-state-cache.js";
@@ -0,0 +1,18 @@
1
+ // HTTP client for `@docsxai/backend`. Used by `docsxai push` / `pull` / `login` / `run`.
2
+ //
3
+ // Barrel: the implementation lives in flat siblings, split by reason-to-change. This file preserves
4
+ // the original public surface so `./backend-client.js` importers (and colocated tests) don't move.
5
+ // • backend-client-contracts.ts — wire DTOs / payloads / error class / option shapes (the leaf)
6
+ // • backend-client-transport.ts — the REST `BackendClient` + `createBackendClient` + run history
7
+ // • backend-client-token.ts — stored-token file + bearer-token resolution / refresh
8
+ // • backend-client-oauth-login.ts — the OAuth 2.1 + PKCE login flow
9
+ // • backend-client-state-cache.ts — the AES-256-GCM `BackendStateCache` relay
10
+ //
11
+ // The contract types are *redeclared* in the contracts leaf (not imported from the backend package)
12
+ // so the engine stays decoupled at the package level — there's no runtime nor build-time dep on the
13
+ // backend. Drift is caught by the round-trip integration test that spins up a real stub.
14
+ export * from "./backend-client-contracts.js";
15
+ export * from "./backend-client-transport.js";
16
+ export * from "./backend-client-token.js";
17
+ export * from "./backend-client-oauth-login.js";
18
+ export * from "./backend-client-state-cache.js";
@@ -0,0 +1,31 @@
1
+ import { type FlowFile } from "./doc-pack.js";
2
+ export declare class CalibrateError extends Error {
3
+ readonly cause?: unknown | undefined;
4
+ constructor(message: string, cause?: unknown | undefined);
5
+ }
6
+ /**
7
+ * Extract a flow-file from a structured flow-guide. Accepts: the YAML of a flow-file directly, or a Markdown
8
+ * doc whose first ```yaml fenced block parses as one. Throws {@link CalibrateError} if neither works (which is
9
+ * the signal that the input is loose prose → use the calibrate skill / agent path instead).
10
+ */
11
+ export declare function extractFlowFile(text: string, source?: string): FlowFile;
12
+ export interface CalibrateOptions {
13
+ workspaceDir: string;
14
+ /** The flow-guide text. */
15
+ fromText: string;
16
+ /** Where it came from (for error messages). */
17
+ fromSource?: string;
18
+ /** Override the flow name (default: the flow-file's `name`). */
19
+ flowName?: string;
20
+ }
21
+ export interface CalibrateResult {
22
+ flow: FlowFile;
23
+ /** Path the flow-file was written to. */
24
+ flowFilePath: string;
25
+ /** Path of the style artifact (whether newly written or pre-existing). */
26
+ stylePath: string;
27
+ /** True if the style artifact was newly written (false if it already existed and was left alone). */
28
+ wroteStyle: boolean;
29
+ }
30
+ /** Run the deterministic calibration step: extract the flow-file from a structured guide, write it + a default style. */
31
+ export declare function calibrate(opts: CalibrateOptions): Promise<CalibrateResult>;
@@ -0,0 +1,68 @@
1
+ // Calibration — the deterministic, structured-input path.
2
+ //
3
+ // `docsxai calibrate <workspace> --from <flow.md|.yaml>` takes a *structured flow-guide* — a flow-file in
4
+ // YAML, or a Markdown doc containing a ```yaml fenced block that parses as one (the the first-consumer testing guide
5
+ // shape: prerequisites + locators reference + per-step actions + success criteria) — and writes it as
6
+ // `<workspace>/flows/<name>.flow.yaml`, plus a default `docs/style.yaml` if absent. Then `docsxai run`
7
+ // exercises it against the live app to fill in screenshots + bounding boxes + the real `annotations.json`.
8
+ //
9
+ // Loose-prose flow descriptions (and live element-picking / ambiguity resolution) need the host agent —
10
+ // that's the `/docsxai:calibrate` *skill* (see packages/plugin/skills/calibrate/SKILL.md); ambiguity
11
+ // signalling lives at the MCP/skill layer. This module covers only the part that's deterministic.
12
+ import { promises as fs } from "node:fs";
13
+ import { FlowFileError, parseFlowFile, serializeFlowFile } from "./flow-file.js";
14
+ import { initStyleIfAbsent } from "./style.js";
15
+ import { resolveWorkspacePath, resolveWorkspacePathReal } from "./workspace.js";
16
+ export class CalibrateError extends Error {
17
+ cause;
18
+ constructor(message, cause) {
19
+ super(message);
20
+ this.cause = cause;
21
+ this.name = "CalibrateError";
22
+ }
23
+ }
24
+ const YAML_FENCE = /```ya?ml\s*\n([\s\S]*?)\n```/i;
25
+ /**
26
+ * Extract a flow-file from a structured flow-guide. Accepts: the YAML of a flow-file directly, or a Markdown
27
+ * doc whose first ```yaml fenced block parses as one. Throws {@link CalibrateError} if neither works (which is
28
+ * the signal that the input is loose prose → use the calibrate skill / agent path instead).
29
+ */
30
+ export function extractFlowFile(text, source = "<flow-guide>") {
31
+ // 1. The whole text as flow-file YAML.
32
+ try {
33
+ return parseFlowFile(text, source);
34
+ }
35
+ catch (e) {
36
+ if (!(e instanceof FlowFileError))
37
+ throw e;
38
+ }
39
+ // 2. A ```yaml fenced block.
40
+ const m = YAML_FENCE.exec(text);
41
+ if (m) {
42
+ try {
43
+ return parseFlowFile(m[1], `${source} (yaml block)`);
44
+ }
45
+ catch (e) {
46
+ if (e instanceof FlowFileError) {
47
+ throw new CalibrateError(`${source}: found a yaml block but it isn't a valid flow-file:\n${e.message}`, e);
48
+ }
49
+ throw e;
50
+ }
51
+ }
52
+ throw new CalibrateError(`${source}: not a structured flow-guide (no parseable flow-file YAML found).\n` +
53
+ `\`calibrate --from\` only takes a flow-file in YAML, or a Markdown doc with a \`\`\`yaml fenced block that *is* one.\n` +
54
+ `Loose prose — e.g. a hand-written test guide whose fenced blocks are numbered prose pseudo-steps for an agent\n` +
55
+ `to *test* rather than flow-file YAML — must be turned into a flow-file by hand: follow the /docsxai:calibrate skill\n` +
56
+ `(walk the live app via Claude in Chrome, pin one canonical locator per step), then \`calibrate --from\` it or just \`run\`.`);
57
+ }
58
+ /** Run the deterministic calibration step: extract the flow-file from a structured guide, write it + a default style. */
59
+ export async function calibrate(opts) {
60
+ const flow = extractFlowFile(opts.fromText, opts.fromSource ?? "<flow-guide>");
61
+ const name = opts.flowName ?? flow.name;
62
+ await fs.mkdir(resolveWorkspacePath(opts.workspaceDir, "flows"), { recursive: true });
63
+ // The flow name is guide-supplied — resolve with the symlink-aware variant before writing.
64
+ const flowFilePath = await resolveWorkspacePathReal(opts.workspaceDir, "flows", `${name}.flow.yaml`);
65
+ await fs.writeFile(flowFilePath, serializeFlowFile(flow), "utf8");
66
+ const { paths, created } = await initStyleIfAbsent(opts.workspaceDir);
67
+ return { flow, flowFilePath, stylePath: paths.yamlPath, wroteStyle: created };
68
+ }
@@ -0,0 +1,5 @@
1
+ export declare function cmdInspect(args: string[]): Promise<number>;
2
+ export declare function cmdLint(args: string[]): Promise<number>;
3
+ export declare function cmdFlowTree(args: string[]): Promise<number>;
4
+ export declare function cmdDiagnose(args: string[]): Promise<number>;
5
+ export declare function cmdStyle(args: string[]): Promise<number>;