libretto 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -35
- package/dist/cli/cli.js +22 -97
- package/dist/cli/commands/browser.js +86 -59
- package/dist/cli/commands/execution.js +199 -86
- package/dist/cli/commands/init.js +34 -29
- package/dist/cli/commands/logs.js +4 -5
- package/dist/cli/commands/shared.js +30 -29
- package/dist/cli/commands/snapshot.js +26 -39
- package/dist/cli/core/ai-config.js +21 -4
- package/dist/cli/core/api-snapshot-analyzer.js +15 -5
- package/dist/cli/core/browser.js +207 -37
- package/dist/cli/core/context.js +4 -1
- package/dist/cli/core/session-telemetry.js +434 -174
- package/dist/cli/core/session.js +21 -8
- package/dist/cli/core/snapshot-analyzer.js +14 -31
- package/dist/cli/core/snapshot-api-config.js +2 -6
- package/dist/cli/core/telemetry.js +20 -4
- package/dist/cli/framework/simple-cli.js +45 -25
- package/dist/cli/router.js +14 -21
- package/dist/cli/workers/run-integration-runtime.js +24 -5
- package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
- package/dist/cli/workers/run-integration-worker.js +1 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.js +7 -10
- package/dist/runtime/download/download.js +5 -1
- package/dist/runtime/extract/extract.js +11 -2
- package/dist/runtime/network/network.js +8 -1
- package/dist/runtime/recovery/agent.js +6 -2
- package/dist/runtime/recovery/errors.js +3 -1
- package/dist/runtime/recovery/recovery.js +3 -1
- package/dist/shared/condense-dom/condense-dom.js +17 -69
- package/dist/shared/config/config.d.ts +1 -9
- package/dist/shared/config/config.js +0 -18
- package/dist/shared/config/index.d.ts +2 -1
- package/dist/shared/config/index.js +0 -10
- package/dist/shared/debug/pause.js +9 -3
- package/dist/shared/dom-semantics.d.ts +8 -0
- package/dist/shared/dom-semantics.js +69 -0
- package/dist/shared/instrumentation/instrument.js +101 -5
- package/dist/shared/llm/ai-sdk-adapter.js +3 -1
- package/dist/shared/llm/client.js +3 -1
- package/dist/shared/logger/index.js +4 -1
- package/dist/shared/run/api.js +3 -1
- package/dist/shared/run/browser.js +47 -3
- package/dist/shared/state/session-state.d.ts +2 -1
- package/dist/shared/state/session-state.js +5 -2
- package/dist/shared/visualization/ghost-cursor.js +36 -14
- package/dist/shared/visualization/highlight.js +9 -6
- package/dist/shared/workflow/workflow.d.ts +4 -5
- package/dist/shared/workflow/workflow.js +3 -5
- package/package.json +6 -2
- package/scripts/check-skills-sync.mjs +25 -0
- package/scripts/compare-eval-summary.mjs +47 -0
- package/scripts/postinstall.mjs +15 -15
- package/scripts/prepare-release.sh +97 -0
- package/scripts/skills-libretto.mjs +103 -0
- package/scripts/summarize-evals.mjs +135 -0
- package/scripts/sync-skills.mjs +12 -0
- package/skills/libretto/SKILL.md +132 -54
- package/skills/libretto/references/action-logs.md +101 -0
- package/skills/libretto/references/auth-profiles.md +1 -2
- package/skills/libretto/references/code-generation-rules.md +210 -0
- package/skills/libretto/references/configuration-file-reference.md +53 -0
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/skills/libretto/references/site-security-review.md +143 -0
- package/src/cli/cli.ts +23 -110
- package/src/cli/commands/browser.ts +94 -70
- package/src/cli/commands/execution.ts +233 -102
- package/src/cli/commands/init.ts +37 -33
- package/src/cli/commands/logs.ts +7 -7
- package/src/cli/commands/shared.ts +36 -37
- package/src/cli/commands/snapshot.ts +44 -59
- package/src/cli/core/ai-config.ts +24 -4
- package/src/cli/core/api-snapshot-analyzer.ts +17 -6
- package/src/cli/core/browser.ts +260 -49
- package/src/cli/core/context.ts +7 -2
- package/src/cli/core/session-telemetry.ts +449 -197
- package/src/cli/core/session.ts +21 -7
- package/src/cli/core/snapshot-analyzer.ts +26 -46
- package/src/cli/core/snapshot-api-config.ts +170 -175
- package/src/cli/core/telemetry.ts +39 -4
- package/src/cli/framework/simple-cli.ts +144 -77
- package/src/cli/router.ts +13 -21
- package/src/cli/workers/run-integration-runtime.ts +36 -9
- package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
- package/src/cli/workers/run-integration-worker.ts +1 -4
- package/src/index.ts +73 -66
- package/src/runtime/download/download.ts +62 -58
- package/src/runtime/download/index.ts +5 -5
- package/src/runtime/extract/extract.ts +71 -61
- package/src/runtime/network/index.ts +3 -3
- package/src/runtime/network/network.ts +99 -93
- package/src/runtime/recovery/agent.ts +217 -212
- package/src/runtime/recovery/errors.ts +107 -104
- package/src/runtime/recovery/index.ts +3 -3
- package/src/runtime/recovery/recovery.ts +38 -35
- package/src/shared/condense-dom/condense-dom.ts +27 -82
- package/src/shared/config/config.ts +0 -19
- package/src/shared/config/index.ts +0 -5
- package/src/shared/debug/pause.ts +57 -51
- package/src/shared/dom-semantics.ts +68 -0
- package/src/shared/instrumentation/errors.ts +64 -62
- package/src/shared/instrumentation/index.ts +5 -5
- package/src/shared/instrumentation/instrument.ts +339 -209
- package/src/shared/llm/ai-sdk-adapter.ts +58 -55
- package/src/shared/llm/client.ts +181 -174
- package/src/shared/llm/types.ts +39 -39
- package/src/shared/logger/index.ts +11 -4
- package/src/shared/logger/logger.ts +312 -306
- package/src/shared/logger/sinks.ts +118 -114
- package/src/shared/paths/paths.ts +50 -49
- package/src/shared/paths/repo-root.ts +17 -17
- package/src/shared/run/api.ts +5 -1
- package/src/shared/run/browser.ts +65 -3
- package/src/shared/state/index.ts +9 -9
- package/src/shared/state/session-state.ts +46 -43
- package/src/shared/visualization/ghost-cursor.ts +180 -149
- package/src/shared/visualization/highlight.ts +89 -86
- package/src/shared/visualization/index.ts +13 -13
- package/src/shared/workflow/workflow.ts +19 -25
- package/skills/libretto/references/reverse-engineering-network-requests.md +0 -39
- package/skills/libretto/references/user-action-log.md +0 -31
|
@@ -3,25 +3,26 @@ import { z } from "zod";
|
|
|
3
3
|
export const SESSION_STATE_VERSION = 1;
|
|
4
4
|
|
|
5
5
|
export const SessionStatusSchema = z.enum([
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
"active",
|
|
7
|
+
"paused",
|
|
8
|
+
"completed",
|
|
9
|
+
"failed",
|
|
10
|
+
"exited",
|
|
11
11
|
]);
|
|
12
12
|
export const SessionViewportSchema = z.object({
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
width: z.number().int().min(1),
|
|
14
|
+
height: z.number().int().min(1),
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
export const SessionStateFileSchema = z.object({
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
version: z.literal(SESSION_STATE_VERSION),
|
|
19
|
+
port: z.number().int().min(0).max(65535),
|
|
20
|
+
pid: z.number().int().optional(),
|
|
21
|
+
cdpEndpoint: z.string().url().optional(),
|
|
22
|
+
session: z.string().min(1),
|
|
23
|
+
startedAt: z.string().datetime({ offset: true }),
|
|
24
|
+
status: SessionStatusSchema.optional(),
|
|
25
|
+
viewport: SessionViewportSchema.optional(),
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
export type SessionStatus = z.infer<typeof SessionStatusSchema>;
|
|
@@ -29,46 +30,48 @@ export type SessionStateFile = z.infer<typeof SessionStateFileSchema>;
|
|
|
29
30
|
export type SessionState = Omit<SessionStateFile, "version">;
|
|
30
31
|
|
|
31
32
|
function formatIssues(error: z.ZodError): string {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
return error.issues
|
|
34
|
+
.map((issue) => {
|
|
35
|
+
const path = issue.path.join(".") || "root";
|
|
36
|
+
return `${path}: ${issue.message}`;
|
|
37
|
+
})
|
|
38
|
+
.join("; ");
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export function parseSessionStateData(
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
rawState: unknown,
|
|
43
|
+
source: string,
|
|
43
44
|
): SessionState {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const parsed = SessionStateFileSchema.safeParse(rawState);
|
|
46
|
+
if (!parsed.success) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Session state at ${source} is invalid: ${formatIssues(parsed.error)}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
const { version: _version, ...state } = parsed.data;
|
|
53
|
+
return state;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
export function parseSessionStateContent(
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
content: string,
|
|
58
|
+
source: string,
|
|
56
59
|
): SessionState {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
let rawState: unknown;
|
|
61
|
+
try {
|
|
62
|
+
rawState = JSON.parse(content);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Session state at ${source} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
return parseSessionStateData(rawState, source);
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
export function serializeSessionState(state: SessionState): SessionStateFile {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
return SessionStateFileSchema.parse({
|
|
74
|
+
version: SESSION_STATE_VERSION,
|
|
75
|
+
...state,
|
|
76
|
+
});
|
|
74
77
|
}
|
|
@@ -3,51 +3,73 @@ import type { Page } from "playwright";
|
|
|
3
3
|
export type GhostCursorStyle = "minimal" | "dot" | "screenstudio";
|
|
4
4
|
|
|
5
5
|
export type GhostCursorOptions = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
style?: GhostCursorStyle;
|
|
7
|
+
color?: string;
|
|
8
|
+
size?: number;
|
|
9
|
+
zIndex?: number;
|
|
10
|
+
easing?: string;
|
|
11
|
+
minDurationMs?: number;
|
|
12
|
+
maxDurationMs?: number;
|
|
13
|
+
speedPxPerMs?: number;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
const DEFAULTS: Required<GhostCursorOptions> = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
style: "minimal",
|
|
18
|
+
color: "rgba(255, 70, 70, 0.9)",
|
|
19
|
+
size: 23,
|
|
20
|
+
zIndex: 2147483646,
|
|
21
|
+
easing: "cubic-bezier(0.16, 1, 0.3, 1)",
|
|
22
|
+
minDurationMs: 100,
|
|
23
|
+
maxDurationMs: 600,
|
|
24
|
+
speedPxPerMs: 1.5,
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
const CURSOR_ID = "__libretto_ghost_cursor__";
|
|
28
28
|
|
|
29
|
-
function buildCursorSvg(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
function buildCursorSvg(
|
|
30
|
+
style: GhostCursorStyle,
|
|
31
|
+
color: string,
|
|
32
|
+
size: number,
|
|
33
|
+
): string {
|
|
34
|
+
if (style === "dot") {
|
|
35
|
+
return `<div style="width:${size}px;height:${size}px;border-radius:50%;background:${color};"></div>`;
|
|
36
|
+
}
|
|
37
|
+
if (style === "screenstudio") {
|
|
38
|
+
return `<div style="width:${size * 1.4}px;height:${size * 1.4}px;border-radius:50%;background:${color};box-shadow:0 0 ${size * 0.6}px ${color};opacity:0.7;"></div>`;
|
|
39
|
+
}
|
|
40
|
+
// minimal: default arrow-like SVG cursor
|
|
41
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="display:block;filter:drop-shadow(0 2px 6px rgba(15,23,42,0.22));">
|
|
38
42
|
<path d="M5 3L19 12L12 13L9 20L5 3Z" fill="${color}" stroke="rgba(0,0,0,0.3)" stroke-width="1"/>
|
|
39
43
|
</svg>`;
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
function buildCursorMarkup(
|
|
47
|
+
style: GhostCursorStyle,
|
|
48
|
+
color: string,
|
|
49
|
+
size: number,
|
|
50
|
+
): string {
|
|
51
|
+
const cursor = buildCursorSvg(style, color, size);
|
|
52
|
+
const badgeHeight = Math.max(12, Math.round(size * 0.54));
|
|
53
|
+
const fontSize = Math.max(8, Math.round(size * 0.28));
|
|
54
|
+
const minWidth = Math.max(28, Math.round(size * 1.28));
|
|
55
|
+
const paddingX = Math.max(5, Math.round(size * 0.2));
|
|
56
|
+
const left = Math.round(size * 0.84);
|
|
57
|
+
const top = Math.round(size * 0.74);
|
|
58
|
+
const width = Math.round(size * 2.4);
|
|
59
|
+
const height = Math.round(size * 1.95);
|
|
60
|
+
const badge = `<div aria-hidden="true" style="position:absolute;left:${left}px;top:${top}px;display:flex;align-items:center;justify-content:center;min-width:${minWidth}px;height:${badgeHeight}px;padding:0 ${paddingX}px;border-radius:${badgeHeight}px;background:${color};color:rgba(255,255,255,0.96);font:700 ${fontSize}px/1 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;letter-spacing:0.02em;white-space:nowrap;border:1px solid rgba(0,0,0,0.16);box-shadow:0 4px 12px rgba(0,0,0,0.14);transform-origin:left center;">Agent</div>`;
|
|
61
|
+
return `<div style="position:relative;width:${width}px;height:${height}px;overflow:visible;">${cursor}${badge}</div>`;
|
|
62
|
+
}
|
|
63
|
+
|
|
42
64
|
function buildInitScript(opts: Required<GhostCursorOptions>): string {
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
const markup = buildCursorMarkup(opts.style, opts.color, opts.size);
|
|
66
|
+
return `
|
|
45
67
|
(function() {
|
|
46
68
|
if (document.getElementById("${CURSOR_ID}")) return;
|
|
47
69
|
var el = document.createElement("div");
|
|
48
70
|
el.id = "${CURSOR_ID}";
|
|
49
71
|
el.style.cssText = "position:fixed;top:0;left:0;z-index:${opts.zIndex};pointer-events:none;transform:translate3d(-100px,-100px,0);transition:none;will-change:transform,opacity;opacity:0;";
|
|
50
|
-
el.innerHTML = ${JSON.stringify(
|
|
72
|
+
el.innerHTML = ${JSON.stringify(markup)};
|
|
51
73
|
document.documentElement.appendChild(el);
|
|
52
74
|
})();
|
|
53
75
|
`;
|
|
@@ -56,145 +78,154 @@ function buildInitScript(opts: Required<GhostCursorOptions>): string {
|
|
|
56
78
|
const installedPages = new WeakSet<Page>();
|
|
57
79
|
|
|
58
80
|
export async function ensureGhostCursor(
|
|
59
|
-
|
|
60
|
-
|
|
81
|
+
page: Page,
|
|
82
|
+
options?: GhostCursorOptions,
|
|
61
83
|
): Promise<void> {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
const existingOpts = (page as any).__librettoGhostCursorOpts as
|
|
85
|
+
| Required<GhostCursorOptions>
|
|
86
|
+
| undefined;
|
|
87
|
+
const opts = { ...DEFAULTS, ...(existingOpts ?? {}), ...options };
|
|
88
|
+
const initScript = buildInitScript(opts);
|
|
89
|
+
|
|
90
|
+
if (!installedPages.has(page)) {
|
|
91
|
+
installedPages.add(page);
|
|
92
|
+
await page.addInitScript({ content: initScript });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Store options on the page for later use by move/click
|
|
96
|
+
(page as any).__librettoGhostCursorOpts = opts;
|
|
97
|
+
|
|
98
|
+
// Re-run in-page installer so cursor recovers after page.setContent() or DOM resets.
|
|
99
|
+
try {
|
|
100
|
+
await page.evaluate(new Function(initScript) as () => void);
|
|
101
|
+
} catch {
|
|
102
|
+
// Page might not be ready yet; addInitScript will handle on next navigation
|
|
103
|
+
}
|
|
82
104
|
}
|
|
83
105
|
|
|
84
106
|
export async function moveGhostCursor(
|
|
85
|
-
|
|
86
|
-
|
|
107
|
+
page: Page,
|
|
108
|
+
target: { x: number; y: number; durationMs?: number },
|
|
87
109
|
): Promise<void> {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
const opts: Required<GhostCursorOptions> =
|
|
111
|
+
(page as any).__librettoGhostCursorOpts ?? DEFAULTS;
|
|
112
|
+
|
|
113
|
+
const durationMs =
|
|
114
|
+
target.durationMs ??
|
|
115
|
+
Math.min(
|
|
116
|
+
opts.maxDurationMs,
|
|
117
|
+
Math.max(opts.minDurationMs, 200), // default ~200ms if no distance info
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await page.evaluate(
|
|
122
|
+
({ id, x, y, duration, easing }) => {
|
|
123
|
+
const el = document.getElementById(id);
|
|
124
|
+
if (!el) return;
|
|
125
|
+
el.style.opacity = "1";
|
|
126
|
+
el.style.transition = `transform ${duration}ms ${easing}`;
|
|
127
|
+
el.style.transform = `translate3d(${x}px, ${y}px, 0)`;
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: CURSOR_ID,
|
|
131
|
+
x: target.x,
|
|
132
|
+
y: target.y,
|
|
133
|
+
duration: durationMs,
|
|
134
|
+
easing: opts.easing,
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
await page.waitForTimeout(durationMs);
|
|
139
|
+
} catch {
|
|
140
|
+
// Best-effort: page may have navigated
|
|
141
|
+
}
|
|
114
142
|
}
|
|
115
143
|
|
|
116
144
|
export async function moveGhostCursorWithDistance(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
145
|
+
page: Page,
|
|
146
|
+
from: { x: number; y: number },
|
|
147
|
+
to: { x: number; y: number },
|
|
120
148
|
): Promise<void> {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
149
|
+
const opts: Required<GhostCursorOptions> =
|
|
150
|
+
(page as any).__librettoGhostCursorOpts ?? DEFAULTS;
|
|
151
|
+
|
|
152
|
+
const dx = to.x - from.x;
|
|
153
|
+
const dy = to.y - from.y;
|
|
154
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
155
|
+
const durationMs = Math.min(
|
|
156
|
+
opts.maxDurationMs,
|
|
157
|
+
Math.max(opts.minDurationMs, distance / opts.speedPxPerMs),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
await moveGhostCursor(page, { x: to.x, y: to.y, durationMs });
|
|
133
161
|
}
|
|
134
162
|
|
|
135
163
|
export async function ghostClick(
|
|
136
|
-
|
|
137
|
-
|
|
164
|
+
page: Page,
|
|
165
|
+
target: { x: number; y: number },
|
|
138
166
|
): Promise<void> {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
167
|
+
try {
|
|
168
|
+
// Click feedback: scale down on "press"
|
|
169
|
+
await page.evaluate(
|
|
170
|
+
({ id, x, y }) => {
|
|
171
|
+
const el = document.getElementById(id);
|
|
172
|
+
if (!el) return;
|
|
173
|
+
el.style.transform = `translate3d(${x}px, ${y}px, 0) scale(0.93)`;
|
|
174
|
+
el.style.transition = "transform 80ms ease-out";
|
|
175
|
+
},
|
|
176
|
+
{ id: CURSOR_ID, x: target.x, y: target.y },
|
|
177
|
+
);
|
|
178
|
+
await page.waitForTimeout(100);
|
|
179
|
+
|
|
180
|
+
// Release: scale back up
|
|
181
|
+
await page.evaluate(
|
|
182
|
+
({ id, x, y }) => {
|
|
183
|
+
const el = document.getElementById(id);
|
|
184
|
+
if (!el) return;
|
|
185
|
+
el.style.transform = `translate3d(${x}px, ${y}px, 0) scale(1)`;
|
|
186
|
+
el.style.transition = "transform 120ms ease-out";
|
|
187
|
+
},
|
|
188
|
+
{ id: CURSOR_ID, x: target.x, y: target.y },
|
|
189
|
+
);
|
|
190
|
+
await page.waitForTimeout(130);
|
|
191
|
+
} catch {
|
|
192
|
+
// Best-effort
|
|
193
|
+
}
|
|
166
194
|
}
|
|
167
195
|
|
|
168
196
|
export async function hideGhostCursor(page: Page): Promise<void> {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
197
|
+
try {
|
|
198
|
+
await page.evaluate(
|
|
199
|
+
({ id }) => {
|
|
200
|
+
const el = document.getElementById(id);
|
|
201
|
+
if (!el) return;
|
|
202
|
+
el.style.transition = "opacity 300ms ease-out";
|
|
203
|
+
el.style.opacity = "0";
|
|
204
|
+
},
|
|
205
|
+
{ id: CURSOR_ID },
|
|
206
|
+
);
|
|
207
|
+
} catch {
|
|
208
|
+
// Best-effort
|
|
209
|
+
}
|
|
182
210
|
}
|
|
183
211
|
|
|
184
212
|
export async function getGhostCursorPosition(
|
|
185
|
-
|
|
213
|
+
page: Page,
|
|
186
214
|
): Promise<{ x: number; y: number } | null> {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
215
|
+
try {
|
|
216
|
+
return await page.evaluate(
|
|
217
|
+
({ id }) => {
|
|
218
|
+
const el = document.getElementById(id);
|
|
219
|
+
if (!el) return null;
|
|
220
|
+
const match = el.style.transform.match(
|
|
221
|
+
/translate3d\(\s*([\d.-]+)px\s*,\s*([\d.-]+)px/,
|
|
222
|
+
);
|
|
223
|
+
if (!match) return null;
|
|
224
|
+
return { x: parseFloat(match[1]!), y: parseFloat(match[2]!) };
|
|
225
|
+
},
|
|
226
|
+
{ id: CURSOR_ID },
|
|
227
|
+
);
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
200
231
|
}
|