waygraph 0.15.21 → 0.15.23

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.
@@ -0,0 +1,102 @@
1
+ // Moved verbatim from the former CHAIN_RUNNER_SCRIPT template string in cli.ts (see src/ARCHITECTURE.md).
2
+ // Runs inside the target project's own waygraph copy - keep it dependency-light and self-contained.
3
+ import { resetPageState } from "./teardown.js";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ /**
8
+ * "chain WAYGRAPH_JSON=1" - the unattended counterpart to "chain --step": no
9
+ * human gate, no overlay, headless by default. The same execution --step
10
+ * already has (session-reset boundaries, per-segment mem-seeding) but
11
+ * reporting built for an agent reading stdout afterward, not a human
12
+ * watching the browser live - a flat JSON object naming exactly which Block
13
+ * failed, what every EARLIER Block resolved to, and a screenshot at the
14
+ * exact moment of failure, since there's no live browser for a human to
15
+ * glance at instead. (Previously its own "waygraph auto" verb - renamed once
16
+ * "auto" came to mean the state-machine discovery tool instead; this is a
17
+ * chain reporting mode, not a distinct command.)
18
+ */
19
+ export async function runJsonReportMode(engine, start, end, context, page, mem, resolved, baseURL) {
20
+ const steps = [];
21
+ for (let i = 0; i < resolved.length; i++) {
22
+ const r = resolved[i];
23
+ if (r.seedMem) r.seedMem();
24
+ if (r.resetSession && i > 0) {
25
+ await resetPageState(context, page, baseURL);
26
+ }
27
+ const startedAt = Date.now();
28
+ const stepFlow = engine.defineFlow([start, r.block, end]);
29
+ try {
30
+ const outcome = await stepFlow.run(context, mem, { page, closeOnFinish: false });
31
+ steps.push({ name: r.block.name, checkpoint: outcome.result, ms: Date.now() - startedAt });
32
+ } catch (err) {
33
+ if (r.expectedFailureReason) {
34
+ steps.push({
35
+ name: r.block.name,
36
+ expectedFailure: true,
37
+ error: err && err.message ? err.message : String(err),
38
+ ms: Date.now() - startedAt,
39
+ });
40
+ return {
41
+ ok: true,
42
+ expectedFailure: true,
43
+ failedAt: r.block.name,
44
+ reason: r.expectedFailureReason,
45
+ steps,
46
+ };
47
+ }
48
+ let screenshot = null;
49
+ try {
50
+ screenshot = join(tmpdir(), "waygraph-chain-failure-" + Date.now() + ".png");
51
+ await page.screenshot({ path: screenshot, fullPage: true });
52
+ } catch {
53
+ screenshot = null;
54
+ }
55
+ return {
56
+ ok: false,
57
+ failedAt: r.block.name,
58
+ stepIndex: i,
59
+ totalSteps: resolved.length,
60
+ error: err && err.message ? err.message : String(err),
61
+ steps,
62
+ screenshot,
63
+ };
64
+ }
65
+ }
66
+ return { ok: true, result: steps.length > 0 ? steps[steps.length - 1].checkpoint : null, steps };
67
+ }
68
+
69
+
70
+ export function isExpectedChainFailure(chainFlows, err) {
71
+ if (!chainFlows || chainFlows.length === 0) return false;
72
+ const last = chainFlows[chainFlows.length - 1];
73
+ if (!last?.expectedFailureReason) return false;
74
+ const msg = err && err.message ? err.message : String(err);
75
+ return msg.includes("viewer-login") || msg.includes("reached-inventory-or-genuinely-blocked");
76
+ }
77
+
78
+
79
+ export async function runChainedFlows(chainFlow, chainFlows, context, mem, page) {
80
+ try {
81
+ const outcome = await chainFlow(...chainFlows).run(context, mem, { page, closeOnFinish: false });
82
+ return outcome?.result !== undefined ? outcome.result : outcome;
83
+ } catch (err) {
84
+ if (isExpectedChainFailure(chainFlows, err)) {
85
+ return { __expectedFailure: true, message: err && err.message ? err.message : String(err) };
86
+ }
87
+ throw err;
88
+ }
89
+ }
90
+
91
+
92
+ /** Parses WAYGRAPH_VIDEO_VIEWPORT / --video-viewport: "1920x1080" or "1920,1080". */
93
+ export function parseVideoViewport(raw) {
94
+ if (!raw || !raw.trim()) return null;
95
+ const m = raw.trim().match(/^(\d{3,5})[xX,](\d{3,5})$/);
96
+ if (!m) return null;
97
+ const width = Number(m[1]);
98
+ const height = Number(m[2]);
99
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
100
+ if (width < 320 || height < 240 || width > 7680 || height > 4320) return null;
101
+ return { width, height };
102
+ }
@@ -0,0 +1,140 @@
1
+ // Moved verbatim from the former CHAIN_RUNNER_SCRIPT template string in cli.ts (see src/ARCHITECTURE.md).
2
+ // Runs inside the target project's own waygraph copy - keep it dependency-light and self-contained.
3
+
4
+ export async function moveCursorTo(page, box, ms) {
5
+ const x = box.x + box.width / 2;
6
+ const y = box.y + box.height / 2;
7
+ const travel = Math.max(0, Number(ms) || 0);
8
+ await page
9
+ .evaluate(({ x, y, ms }) => {
10
+ if (window.__wgMoveCursorTo) window.__wgMoveCursorTo(x, y, ms);
11
+ }, { x, y, ms: travel })
12
+ .catch(() => {});
13
+ // CSS transition is async - wait the full travel so the cursor is ON the
14
+ // target before ring/pulse/real click (NavBlock click nav was skipping
15
+ // this and looked like a teleport).
16
+ if (travel > 0) {
17
+ await new Promise((res) => setTimeout(res, travel));
18
+ }
19
+ return { x, y };
20
+ }
21
+
22
+
23
+ export async function clickPulseAt(page, x, y, tone) {
24
+ await page
25
+ .evaluate(({ x, y, tone }) => {
26
+ if (window.__wgClickPulse) window.__wgClickPulse(x, y, tone || "planned");
27
+ }, { x, y, tone: tone || "planned" })
28
+ .catch(() => {});
29
+ }
30
+
31
+
32
+ /**
33
+ * Touch swipe trail (mobile/tablet). dir: left|right|up|down.
34
+ * Returns after the trail animation completes.
35
+ * Self-contained when overlay helper is not installed yet (device runs before panel).
36
+ */
37
+ export async function playSwipeTrail(page, opts) {
38
+ const o = opts || {};
39
+ const waitMs = await page
40
+ .evaluate((payload) => {
41
+ if (typeof window.__wgSwipeTrail === "function") {
42
+ return window.__wgSwipeTrail(payload);
43
+ }
44
+ // Fallback: inject a one-shot trail without full overlay install.
45
+ const dir =
46
+ payload.dir === "right" || payload.dir === "up" || payload.dir === "down"
47
+ ? payload.dir
48
+ : "left";
49
+ const ms = Math.max(280, Math.min(1400, Number(payload.ms) || 720));
50
+ const label = payload.label || "swipe";
51
+ let layer = document.getElementById("wg-swipe-layer");
52
+ if (layer) layer.remove();
53
+ layer = document.createElement("div");
54
+ layer.id = "wg-swipe-layer";
55
+ layer.setAttribute("data-wg-ui", "1");
56
+ layer.style.cssText =
57
+ "position:fixed;inset:0;z-index:2147483646;pointer-events:none;overflow:hidden;";
58
+ const finger = document.createElement("div");
59
+ finger.style.cssText =
60
+ "position:absolute;width:28px;height:28px;margin:-14px 0 0 -14px;border-radius:50%;" +
61
+ "background:rgba(15,23,42,.92);border:2px solid #fff;box-shadow:0 4px 16px rgba(0,0,0,.4);";
62
+ const lab = document.createElement("div");
63
+ lab.textContent = label;
64
+ lab.style.cssText =
65
+ "position:absolute;left:50%;top:18%;transform:translateX(-50%);padding:6px 12px;" +
66
+ "border-radius:999px;background:rgba(15,23,42,.88);color:#e0f2fe;" +
67
+ "border:1px solid rgba(56,189,248,.55);font:700 12px/1.2 system-ui,sans-serif;";
68
+ layer.appendChild(lab);
69
+ layer.appendChild(finger);
70
+ document.documentElement.appendChild(layer);
71
+ const vw = window.innerWidth || 390;
72
+ const vh = window.innerHeight || 844;
73
+ const pad = Math.min(vw, vh) * 0.18;
74
+ let x0;
75
+ let y0;
76
+ let x1;
77
+ let y1;
78
+ if (dir === "left") {
79
+ x0 = vw - pad;
80
+ x1 = pad;
81
+ y0 = y1 = vh * 0.52;
82
+ } else if (dir === "right") {
83
+ x0 = pad;
84
+ x1 = vw - pad;
85
+ y0 = y1 = vh * 0.52;
86
+ } else if (dir === "up") {
87
+ x0 = x1 = vw * 0.5;
88
+ y0 = vh - pad;
89
+ y1 = pad;
90
+ } else {
91
+ x0 = x1 = vw * 0.5;
92
+ y0 = pad;
93
+ y1 = vh - pad;
94
+ }
95
+ finger.style.left = x0 + "px";
96
+ finger.style.top = y0 + "px";
97
+ const steps = 12;
98
+ const stepMs = Math.floor(ms / steps);
99
+ for (let i = 1; i <= steps; i++) {
100
+ const t = i / steps;
101
+ const e = t * t * (3 - 2 * t);
102
+ const x = x0 + (x1 - x0) * e;
103
+ const y = y0 + (y1 - y0) * e;
104
+ setTimeout(() => {
105
+ finger.style.left = x + "px";
106
+ finger.style.top = y + "px";
107
+ const dot = document.createElement("div");
108
+ dot.style.cssText =
109
+ "position:absolute;width:14px;height:14px;margin:-7px 0 0 -7px;border-radius:50%;" +
110
+ "background:rgba(147,197,253,.55);border:2px solid rgba(255,255,255,.85);left:" +
111
+ x +
112
+ "px;top:" +
113
+ y +
114
+ "px;opacity:" +
115
+ (0.85 - t * 0.55) +
116
+ ";transition:opacity .45s ease,transform .45s ease;";
117
+ layer.appendChild(dot);
118
+ requestAnimationFrame(() => {
119
+ dot.style.opacity = "0";
120
+ dot.style.transform = "scale(1.8)";
121
+ });
122
+ if (i === steps) {
123
+ setTimeout(() => {
124
+ const el = document.getElementById("wg-swipe-layer");
125
+ if (el) el.remove();
126
+ }, 400);
127
+ }
128
+ }, i * stepMs);
129
+ }
130
+ return ms + 120;
131
+ }, {
132
+ dir: o.dir || "left",
133
+ label: o.label || "swipe",
134
+ hint: o.hint || "",
135
+ ms: o.ms || 720,
136
+ })
137
+ .catch(() => 0);
138
+ const n = Number(waitMs) || 0;
139
+ if (n > 0) await new Promise((r) => setTimeout(r, n));
140
+ }
@@ -0,0 +1,314 @@
1
+ // Moved verbatim from the former CHAIN_RUNNER_SCRIPT template string in cli.ts (see src/ARCHITECTURE.md).
2
+ // Runs inside the target project's own waygraph copy - keep it dependency-light and self-contained.
3
+ import { demoPaceIsFast, demoPaceIsSlow, formatDemoPaceBadge, formatDemoPaceLabel, normalizeDemoPace, normalizeHighlightTone } from "../highlights.js";
4
+ import { join } from "node:path";
5
+
6
+ export function demoPaceKind(pace) {
7
+ const p = normalizeDemoPace(pace);
8
+ if (typeof p === "number" && p > 20) return "ms";
9
+ if (typeof p === "number") return demoPaceIsSlow(p) ? "num-slow" : demoPaceIsFast(p) ? "fast" : "normal";
10
+ if (p === "blitz" || p === "fast" || p === "slow") return p;
11
+ return "normal";
12
+ }
13
+
14
+
15
+ export function paceSpeakFields(pace, baseAutoplayMs) {
16
+ const p = normalizeDemoPace(pace);
17
+ const base = Number.isFinite(baseAutoplayMs) && baseAutoplayMs > 0 ? baseAutoplayMs : 1800;
18
+ return {
19
+ pace: p,
20
+ paceBadge: formatDemoPaceBadge(p, base),
21
+ paceLabel: formatDemoPaceLabel(p, base),
22
+ paceKind: demoPaceKind(p),
23
+ };
24
+ }
25
+
26
+
27
+ /** Demo CLI chatter (agents use this to ballpark timing / todo layout). Off for WAYGRAPH_JSON. */
28
+ function demoQuiet() {
29
+ return process.env.WAYGRAPH_JSON === "1";
30
+ }
31
+
32
+ function demoColorEnabled() {
33
+ // FORCE_COLOR wins (agents/CI often set NO_COLOR=1).
34
+ if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
35
+ if (process.env.NO_COLOR === "1" || process.env.NO_COLOR === "true") return false;
36
+ if (process.env.FORCE_COLOR === "0") return false;
37
+ return !!(
38
+ (process.stderr && process.stderr.isTTY) ||
39
+ (process.stdout && process.stdout.isTTY)
40
+ );
41
+ }
42
+
43
+ const ESC = String.fromCharCode(27);
44
+
45
+ export const ANSI = {
46
+ reset: ESC + "[0m",
47
+ bold: ESC + "[1m",
48
+ dim: ESC + "[2m",
49
+ purple: ESC + "[38;5;141m",
50
+ blue: ESC + "[38;5;75m",
51
+ yellow: ESC + "[38;5;220m",
52
+ red: ESC + "[38;5;203m",
53
+ green: ESC + "[38;5;114m",
54
+ gray: ESC + "[38;5;246m",
55
+ cyan: ESC + "[38;5;87m",
56
+ magenta: ESC + "[38;5;213m",
57
+ white: ESC + "[37m",
58
+ };
59
+
60
+ export function ansiPaint(code, text) {
61
+ if (!demoColorEnabled() || text == null || text === "") return String(text ?? "");
62
+ return code + String(text) + ANSI.reset;
63
+ }
64
+
65
+ function toneAnsi(tone) {
66
+ const t = normalizeHighlightTone(tone);
67
+ if (t === "info") return ANSI.blue;
68
+ if (t === "warning") return ANSI.yellow;
69
+ if (t === "danger") return ANSI.red;
70
+ if (t === "success") return ANSI.green;
71
+ if (t === "auto") return ANSI.gray;
72
+ return ANSI.purple;
73
+ }
74
+
75
+ function toneName(tone) {
76
+ const t = normalizeHighlightTone(tone);
77
+ if (t === "info") return "BLUE";
78
+ if (t === "warning") return "YELLOW";
79
+ if (t === "danger") return "RED";
80
+ if (t === "success") return "GREEN";
81
+ if (t === "auto") return "GRAY";
82
+ return "PURPLE";
83
+ }
84
+
85
+ /** Optional authored CSS color (#rrggbb) -> truecolor ANSI when TTY. */
86
+ function hexAnsi(hex) {
87
+ if (!hex || typeof hex !== "string") return null;
88
+ const m = String(hex).trim().match(/^#?([0-9a-fA-F]{6})$/);
89
+ if (!m) return null;
90
+ const n = parseInt(m[1], 16);
91
+ const r = (n >> 16) & 255;
92
+ const g = (n >> 8) & 255;
93
+ const b = n & 255;
94
+ return ESC + "[38;2;" + r + ";" + g + ";" + b + "m";
95
+ }
96
+
97
+ function demoTs() {
98
+ const d = new Date();
99
+ const p = (n) => String(n).padStart(2, "0");
100
+ const ms = String(d.getMilliseconds()).padStart(3, "0");
101
+ return (
102
+ p(d.getHours()) +
103
+ ":" +
104
+ p(d.getMinutes()) +
105
+ ":" +
106
+ p(d.getSeconds()) +
107
+ "." +
108
+ ms
109
+ );
110
+ }
111
+
112
+ function demoPrefix() {
113
+ return ansiPaint(ANSI.dim, "[" + demoTs() + "]") + " waygraph demo: ";
114
+ }
115
+
116
+ export function demoLog(msg) {
117
+ if (demoQuiet()) return;
118
+ console.log(demoPrefix() + msg);
119
+ }
120
+
121
+ /** Fixture catch-all: how it was called + value. */
122
+ export function demoFixture(kind, call, value) {
123
+ if (demoQuiet()) return;
124
+ const head = ansiPaint(ANSI.cyan, "FIXTURE") + " " + ansiPaint(ANSI.bold, kind);
125
+ const callPart = call ? " " + ansiPaint(ANSI.dim, call) : "";
126
+ let val = "";
127
+ if (value !== undefined) {
128
+ if (typeof value === "string") val = value;
129
+ else {
130
+ try {
131
+ val = JSON.stringify(value);
132
+ } catch {
133
+ val = String(value);
134
+ }
135
+ }
136
+ if (val.length > 220) val = val.slice(0, 217) + "...";
137
+ val = " = " + ansiPaint(ANSI.white, val);
138
+ }
139
+ console.log(demoPrefix() + head + callPart + val);
140
+ }
141
+
142
+ /** Highlight trigger line: PURPLE · Cart items - Product landed in cart */
143
+ export function demoHighlight(h, extra) {
144
+ if (demoQuiet()) return;
145
+ const tone = h && h.tone != null ? h.tone : "planned";
146
+ const paint = hexAnsi(h && h.color) || toneAnsi(tone);
147
+ const name = toneName(tone);
148
+ const label = String((h && h.label) || "").trim() || "(no label)";
149
+ const sel = h && h.selector ? String(h.selector) : "";
150
+ const bits = [];
151
+ if (h && h.focus) bits.push("focus");
152
+ if (h && h.zoom != null && Number(h.zoom) > 1) bits.push("zoom=" + h.zoom);
153
+ if (h && h.zoomOut === false) bits.push("zoomOut=false");
154
+ if (h && h.gesture) bits.push("gesture=" + h.gesture);
155
+ if (h && h.weight && h.weight !== "normal") bits.push("weight=" + h.weight);
156
+ if (h && h.size && h.size !== "md") bits.push("size=" + h.size);
157
+ if (h && h.color) bits.push("color=" + h.color);
158
+ if (extra) bits.push(extra);
159
+ const line =
160
+ ansiPaint(paint, name) +
161
+ " · " +
162
+ ansiPaint(paint, label) +
163
+ (sel ? ansiPaint(ANSI.dim, " sel=" + sel) : "") +
164
+ (bits.length ? ansiPaint(ANSI.dim, " [" + bits.join(" ") + "]") : "");
165
+ console.log(demoPrefix() + ansiPaint(ANSI.magenta, "HIGHLIGHT") + " " + line);
166
+ }
167
+
168
+ export function logStubPhaseFixtures(phaseName, phase) {
169
+ if (demoQuiet() || !phase) return;
170
+ const tag = "stub." + phaseName;
171
+ if (phase.title) demoFixture(tag, "ctx.title(...)", JSON.stringify(phase.title));
172
+ if (phase.banner) demoFixture(tag, "ctx.banner(...)", JSON.stringify(phase.banner));
173
+ if (phase.deviceSync && phase.deviceSync !== "keep") {
174
+ demoFixture(
175
+ tag,
176
+ phase.deviceSync === "clear" ? "ctx.clearDevice()" : "ctx.device(...)",
177
+ phase.device
178
+ ? {
179
+ preset: phase.device.preset,
180
+ w: phase.device.viewport && phase.device.viewport.width,
181
+ h: phase.device.viewport && phase.device.viewport.height,
182
+ orient: phase.device.orientation,
183
+ touch: !!phase.device.touchMode,
184
+ }
185
+ : phase.deviceSync,
186
+ );
187
+ }
188
+ if (phase.zoom != null) demoFixture(tag, "ctx.zoom(" + phase.zoom + ")", phase.zoom + "x");
189
+ if (phase.zoomOut !== undefined) demoFixture(tag, "ctx.zoomOut(" + !!phase.zoomOut + ")", !!phase.zoomOut);
190
+ if (phase.todoSync && phase.todoSync !== "keep") {
191
+ demoFixture(
192
+ tag,
193
+ phase.todoSync === "clear" ? "ctx.hideTodos() / clear" : "ctx.todos(...)",
194
+ phase.todoDock
195
+ ? {
196
+ id: phase.todoDock.id,
197
+ pos: phase.todoDock.pos,
198
+ title: phase.todoDock.title,
199
+ n: (phase.todos && phase.todos.length) || 0,
200
+ }
201
+ : phase.todoSync,
202
+ );
203
+ }
204
+ const rings = phase.highlights || [];
205
+ for (let i = 0; i < rings.length; i++) {
206
+ const h = rings[i];
207
+ demoFixture(
208
+ tag,
209
+ "ctx.ring/highlights[" + i + "]",
210
+ {
211
+ selector: h.selector,
212
+ label: h.label || h.caption,
213
+ detail: h.detail,
214
+ tone: h.tone || "planned",
215
+ color: h.color,
216
+ focus: !!h.focus,
217
+ zoom: h.zoom,
218
+ zoomOut: h.zoomOut,
219
+ gesture: h.gesture,
220
+ duration: h.duration,
221
+ },
222
+ );
223
+ }
224
+ if (phase.slides && phase.slides.length) {
225
+ demoFixture(tag, "ctx.slides(...)", { n: phase.slides.length });
226
+ }
227
+ }
228
+
229
+ function summarizeTodoDock(dock) {
230
+ if (!dock) return "todos=none";
231
+ const id = dock.id || "_default";
232
+ const pos = dock.pos || "left";
233
+ const style = dock.style || "sequential";
234
+ const title = dock.title ? String(dock.title) : "";
235
+ const items = [];
236
+ for (const g of dock.groups || []) {
237
+ for (const t of g.items || []) items.push(t);
238
+ }
239
+ const cur = items.find((t) => t.current);
240
+ const done = items.filter((t) => t.done).length;
241
+ return (
242
+ "todos id=" +
243
+ id +
244
+ " pos=" +
245
+ pos +
246
+ " style=" +
247
+ style +
248
+ (title ? ' title="' + title + '"' : "") +
249
+ " n=" +
250
+ items.length +
251
+ " done=" +
252
+ done +
253
+ (cur && cur.text ? ' current="' + String(cur.text).slice(0, 48) + '"' : "")
254
+ );
255
+ }
256
+
257
+ /** Full todo dump for agents (every group/item id + flags). */
258
+ export function logTodoDockFull(dock, tag) {
259
+ const prefix = tag ? tag + " " : "";
260
+ if (!dock) {
261
+ demoLog(prefix + "todos=none");
262
+ return;
263
+ }
264
+ demoLog(prefix + summarizeTodoDock(dock));
265
+ const groups = dock.groups || [];
266
+ if (!groups.length) {
267
+ demoLog(prefix + " (no groups)");
268
+ return;
269
+ }
270
+ for (let gi = 0; gi < groups.length; gi++) {
271
+ const g = groups[gi];
272
+ const gLabel =
273
+ "group[" +
274
+ gi +
275
+ "]" +
276
+ (g.id ? " id=" + g.id : "") +
277
+ (g.title ? ' title="' + String(g.title).slice(0, 40) + '"' : "") +
278
+ (g.style ? " style=" + g.style : "");
279
+ demoLog(prefix + " " + gLabel);
280
+ const items = g.items || [];
281
+ for (let ti = 0; ti < items.length; ti++) {
282
+ const t = items[ti];
283
+ const mark = t.done ? "done" : t.current ? "CURRENT" : "pending";
284
+ demoLog(
285
+ prefix +
286
+ " [" +
287
+ ti +
288
+ "] " +
289
+ mark +
290
+ (t.id ? " id=" + t.id : "") +
291
+ ' text="' +
292
+ String(t.text || "").slice(0, 80) +
293
+ '"' +
294
+ (t.detail ? ' detail="' + String(t.detail).slice(0, 40) + '"' : ""),
295
+ );
296
+ }
297
+ }
298
+ }
299
+
300
+ export function summarizeDevice(d) {
301
+ if (!d) return "device=none";
302
+ const vp = d.viewport || {};
303
+ return (
304
+ "device=" +
305
+ (d.preset || "?") +
306
+ " " +
307
+ (vp.width || "?") +
308
+ "x" +
309
+ (vp.height || "?") +
310
+ " " +
311
+ (d.orientation || "?") +
312
+ (d.touchMode ? " touch" : "")
313
+ );
314
+ }