@canaryai/cli 0.2.9 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-C2PGZRYK.js → chunk-CEW4BDXD.js} +26 -7
- package/dist/chunk-CEW4BDXD.js.map +1 -0
- package/dist/chunk-ERSNYLMZ.js +229 -0
- package/dist/chunk-ERSNYLMZ.js.map +1 -0
- package/dist/{chunk-XGO62PO2.js → chunk-MSMC6UXW.js} +198 -11
- package/dist/{chunk-XGO62PO2.js.map → chunk-MSMC6UXW.js.map} +1 -1
- package/dist/{chunk-LC7ZVXPH.js → chunk-Q7WFBG5C.js} +2 -2
- package/dist/{debug-workflow-I3F36JBL.js → debug-workflow-53ULOFJC.js} +4 -4
- package/dist/{docs-REHST3YB.js → docs-BEE3LOCO.js} +2 -2
- package/dist/{feature-flag-3HB5NTMY.js → feature-flag-CYTDV4ZB.js} +2 -2
- package/dist/index.js +61 -139
- package/dist/index.js.map +1 -1
- package/dist/init-M6I3MG3D.js +146 -0
- package/dist/init-M6I3MG3D.js.map +1 -0
- package/dist/{issues-YU57CHXS.js → issues-NLM72HLU.js} +2 -2
- package/dist/{knobs-QJ4IBLCT.js → knobs-O35GAU5M.js} +2 -2
- package/dist/list-4K4EIGAT.js +57 -0
- package/dist/list-4K4EIGAT.js.map +1 -0
- package/dist/local-NHXXPHZ3.js +63 -0
- package/dist/local-NHXXPHZ3.js.map +1 -0
- package/dist/{local-browser-MKTJ36KY.js → local-browser-VAZORCO3.js} +3 -3
- package/dist/login-ZLP64YQP.js +130 -0
- package/dist/login-ZLP64YQP.js.map +1 -0
- package/dist/{mcp-ZOKM2AUE.js → mcp-ZF5G5DCB.js} +4 -126
- package/dist/mcp-ZF5G5DCB.js.map +1 -0
- package/dist/{record-TNDBT3NY.js → record-V6QKFFH3.js} +6 -47
- package/dist/record-V6QKFFH3.js.map +1 -0
- package/dist/{release-L4IXOHDF.js → release-7TI7EIGD.js} +8 -4
- package/dist/release-7TI7EIGD.js.map +1 -0
- package/dist/{session-RNLKFS2Z.js → session-UGNJXRUW.js} +138 -70
- package/dist/session-UGNJXRUW.js.map +1 -0
- package/dist/skill-ORWAPBDW.js +424 -0
- package/dist/skill-ORWAPBDW.js.map +1 -0
- package/dist/{src-2WSMYBMJ.js → src-4VIDSK4A.js} +2 -2
- package/dist/start-E532F3BU.js +112 -0
- package/dist/start-E532F3BU.js.map +1 -0
- package/dist/workflow-HXIUXRFI.js +613 -0
- package/dist/workflow-HXIUXRFI.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-C2PGZRYK.js.map +0 -1
- package/dist/chunk-DXIAHB72.js +0 -340
- package/dist/chunk-DXIAHB72.js.map +0 -1
- package/dist/chunk-QLFSJG5O.js +0 -93
- package/dist/chunk-QLFSJG5O.js.map +0 -1
- package/dist/mcp-ZOKM2AUE.js.map +0 -1
- package/dist/record-TNDBT3NY.js.map +0 -1
- package/dist/release-L4IXOHDF.js.map +0 -1
- package/dist/session-RNLKFS2Z.js.map +0 -1
- package/dist/skill-CZ7SHI3P.js +0 -156
- package/dist/skill-CZ7SHI3P.js.map +0 -1
- /package/dist/{chunk-LC7ZVXPH.js.map → chunk-Q7WFBG5C.js.map} +0 -0
- /package/dist/{debug-workflow-I3F36JBL.js.map → debug-workflow-53ULOFJC.js.map} +0 -0
- /package/dist/{docs-REHST3YB.js.map → docs-BEE3LOCO.js.map} +0 -0
- /package/dist/{feature-flag-3HB5NTMY.js.map → feature-flag-CYTDV4ZB.js.map} +0 -0
- /package/dist/{issues-YU57CHXS.js.map → issues-NLM72HLU.js.map} +0 -0
- /package/dist/{knobs-QJ4IBLCT.js.map → knobs-O35GAU5M.js.map} +0 -0
- /package/dist/{local-browser-MKTJ36KY.js.map → local-browser-VAZORCO3.js.map} +0 -0
- /package/dist/{src-2WSMYBMJ.js.map → src-4VIDSK4A.js.map} +0 -0
|
@@ -3,6 +3,7 @@ import { createRequire as __cr } from "module"; const require = __cr(import.meta
|
|
|
3
3
|
// src/session/daemon-client.ts
|
|
4
4
|
import { spawn } from "child_process";
|
|
5
5
|
import fs from "fs/promises";
|
|
6
|
+
import { existsSync } from "fs";
|
|
6
7
|
import path from "path";
|
|
7
8
|
import os from "os";
|
|
8
9
|
var PIDFILE_DIR = path.join(os.homedir(), ".config", "canary-cli");
|
|
@@ -45,12 +46,20 @@ async function healthCheck(port) {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
async function spawnDaemon() {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
"..",
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
const dir = path.dirname(new URL(import.meta.url).pathname);
|
|
50
|
+
const candidates = [
|
|
51
|
+
path.resolve(dir, "..", "index.ts"),
|
|
52
|
+
// source layout
|
|
53
|
+
path.resolve(dir, "index.js"),
|
|
54
|
+
// dist layout (flat)
|
|
55
|
+
path.resolve(dir, "..", "index.js")
|
|
56
|
+
// fallback
|
|
57
|
+
];
|
|
58
|
+
const cliEntry = candidates.find((p) => existsSync(p));
|
|
59
|
+
if (!cliEntry) {
|
|
60
|
+
throw new Error(`Cannot find CLI entry point. Searched: ${candidates.join(", ")}`);
|
|
61
|
+
}
|
|
62
|
+
const child = spawn(process.execPath, [cliEntry, "session", "daemon"], {
|
|
54
63
|
detached: true,
|
|
55
64
|
stdio: ["ignore", "pipe", "ignore"],
|
|
56
65
|
env: { ...process.env }
|
|
@@ -122,6 +131,14 @@ async function deleteAllSessions() {
|
|
|
122
131
|
const port = await ensureDaemon();
|
|
123
132
|
return daemonFetch(port, "DELETE", "/sessions");
|
|
124
133
|
}
|
|
134
|
+
async function swapSessionContext(sessionId, params) {
|
|
135
|
+
const port = await ensureDaemon();
|
|
136
|
+
return daemonFetch(port, "POST", `/sessions/${sessionId}/swap-context`, params);
|
|
137
|
+
}
|
|
138
|
+
async function getSessionStorageState(sessionId) {
|
|
139
|
+
const port = await ensureDaemon();
|
|
140
|
+
return daemonFetch(port, "GET", `/sessions/${sessionId}/storage-state`);
|
|
141
|
+
}
|
|
125
142
|
async function callTool(sessionId, toolName, args) {
|
|
126
143
|
const port = await ensureDaemon();
|
|
127
144
|
return daemonFetch(port, "POST", `/sessions/${sessionId}/tools/${toolName}`, args);
|
|
@@ -161,7 +178,9 @@ export {
|
|
|
161
178
|
getSession,
|
|
162
179
|
deleteSession,
|
|
163
180
|
deleteAllSessions,
|
|
181
|
+
swapSessionContext,
|
|
182
|
+
getSessionStorageState,
|
|
164
183
|
callTool,
|
|
165
184
|
resolveTargetSession
|
|
166
185
|
};
|
|
167
|
-
//# sourceMappingURL=chunk-
|
|
186
|
+
//# sourceMappingURL=chunk-CEW4BDXD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/session/daemon-client.ts"],"sourcesContent":["/**\n * Daemon client — HTTP client for the session daemon.\n *\n * Handles pidfile read/write, stale PID detection, auto-start,\n * and provides typed HTTP helpers for daemon communication.\n *\n * @module\n */\n\nimport { spawn } from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport path from 'node:path';\nimport os from 'node:os';\nimport type {\n DaemonState,\n DaemonResponse,\n SessionInfo,\n CreateSessionRequest,\n SwapContextRequest,\n ToolResponse,\n StorageStateResponse,\n} from './types.js';\n\nconst PIDFILE_DIR = path.join(os.homedir(), '.config', 'canary-cli');\nconst PIDFILE_PATH = path.join(PIDFILE_DIR, 'daemon.json');\nconst HEALTH_POLL_INTERVAL_MS = 100;\nconst HEALTH_POLL_TIMEOUT_MS = 15_000;\n\n/* ── Pidfile helpers ─────────────────────────────────────────────────── */\n\nasync function readPidfile(): Promise<DaemonState | null> {\n try {\n const content = await fs.readFile(PIDFILE_PATH, 'utf-8');\n return JSON.parse(content) as DaemonState;\n } catch {\n return null;\n }\n}\n\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/* ── HTTP helpers ────────────────────────────────────────────────────── */\n\nasync function daemonFetch(\n port: number,\n method: string,\n path: string,\n body?: unknown\n): Promise<unknown> {\n const url = `http://127.0.0.1:${port}${path}`;\n const res = await fetch(url, {\n method,\n headers: body ? { 'Content-Type': 'application/json' } : undefined,\n body: body ? JSON.stringify(body) : undefined,\n });\n return res.json();\n}\n\nasync function healthCheck(port: number): Promise<boolean> {\n try {\n const res = await fetch(`http://127.0.0.1:${port}/health`, {\n signal: AbortSignal.timeout(2000),\n });\n return res.ok;\n } catch {\n return false;\n }\n}\n\n/* ── Auto-start ──────────────────────────────────────────────────────── */\n\nasync function spawnDaemon(): Promise<number> {\n // Resolve the canary CLI entry point\n // In source: this file is src/session/daemon-client.ts → ../index.ts\n // In dist: this file is dist/chunk-*.js → ./index.js (flat)\n const dir = path.dirname(new URL(import.meta.url).pathname);\n const candidates = [\n path.resolve(dir, '..', 'index.ts'), // source layout\n path.resolve(dir, 'index.js'), // dist layout (flat)\n path.resolve(dir, '..', 'index.js'), // fallback\n ];\n const cliEntry = candidates.find((p) => existsSync(p));\n if (!cliEntry) {\n throw new Error(`Cannot find CLI entry point. Searched: ${candidates.join(', ')}`);\n }\n\n const child = spawn(process.execPath, [cliEntry, 'session', 'daemon'], {\n detached: true,\n stdio: ['ignore', 'pipe', 'ignore'],\n env: { ...process.env },\n });\n\n child.unref();\n\n // Wait for \"DAEMON_READY:<port>\" on stdout\n return new Promise<number>((resolve, reject) => {\n let output = '';\n const timeout = setTimeout(() => {\n reject(new Error('Daemon startup timed out'));\n }, HEALTH_POLL_TIMEOUT_MS);\n\n child.stdout!.on('data', (data: Buffer) => {\n output += data.toString();\n const match = output.match(/DAEMON_READY:(\\d+)/);\n if (match) {\n clearTimeout(timeout);\n resolve(parseInt(match[1], 10));\n }\n });\n\n child.on('error', (err) => {\n clearTimeout(timeout);\n reject(err);\n });\n\n child.on('exit', (code) => {\n clearTimeout(timeout);\n if (!output.includes('DAEMON_READY')) {\n reject(new Error(`Daemon exited with code ${code} before becoming ready`));\n }\n });\n });\n}\n\n/* ── Public API ──────────────────────────────────────────────────────── */\n\n/**\n * Ensure the daemon is running and return its port.\n * Starts the daemon if needed, cleans up stale pidfiles.\n */\nexport async function ensureDaemon(): Promise<number> {\n const state = await readPidfile();\n\n if (state) {\n if (isProcessAlive(state.pid)) {\n // Verify it actually responds\n if (await healthCheck(state.port)) {\n return state.port;\n }\n }\n // Stale pidfile — clean up\n try {\n await fs.unlink(PIDFILE_PATH);\n } catch {\n // ignore\n }\n }\n\n // Spawn new daemon\n const port = await spawnDaemon();\n\n // Poll until healthy\n const deadline = Date.now() + HEALTH_POLL_TIMEOUT_MS;\n while (Date.now() < deadline) {\n if (await healthCheck(port)) return port;\n await new Promise((r) => setTimeout(r, HEALTH_POLL_INTERVAL_MS));\n }\n\n throw new Error('Daemon failed to become healthy after spawn');\n}\n\n/**\n * Get daemon port if running, null otherwise.\n */\nexport async function getDaemonPort(): Promise<number | null> {\n const state = await readPidfile();\n if (!state) return null;\n if (!isProcessAlive(state.pid)) return null;\n if (!(await healthCheck(state.port))) return null;\n return state.port;\n}\n\n/* ── Session operations ──────────────────────────────────────────────── */\n\nexport async function createSession(params: CreateSessionRequest): Promise<DaemonResponse<SessionInfo>> {\n const port = await ensureDaemon();\n return daemonFetch(port, 'POST', '/sessions', params) as Promise<DaemonResponse<SessionInfo>>;\n}\n\nexport async function listSessions(): Promise<DaemonResponse<SessionInfo[]>> {\n const port = await ensureDaemon();\n return daemonFetch(port, 'GET', '/sessions') as Promise<DaemonResponse<SessionInfo[]>>;\n}\n\nexport async function getSession(sessionId: string): Promise<DaemonResponse<SessionInfo>> {\n const port = await ensureDaemon();\n return daemonFetch(port, 'GET', `/sessions/${sessionId}`) as Promise<DaemonResponse<SessionInfo>>;\n}\n\nexport async function deleteSession(sessionId: string): Promise<DaemonResponse> {\n const port = await ensureDaemon();\n return daemonFetch(port, 'DELETE', `/sessions/${sessionId}`) as Promise<DaemonResponse>;\n}\n\nexport async function deleteAllSessions(): Promise<DaemonResponse> {\n const port = await ensureDaemon();\n return daemonFetch(port, 'DELETE', '/sessions') as Promise<DaemonResponse>;\n}\n\nexport async function swapSessionContext(\n sessionId: string,\n params: SwapContextRequest\n): Promise<DaemonResponse<SessionInfo>> {\n const port = await ensureDaemon();\n return daemonFetch(port, 'POST', `/sessions/${sessionId}/swap-context`, params) as Promise<\n DaemonResponse<SessionInfo>\n >;\n}\n\nexport async function getSessionStorageState(sessionId: string): Promise<StorageStateResponse> {\n const port = await ensureDaemon();\n return daemonFetch(port, 'GET', `/sessions/${sessionId}/storage-state`) as Promise<StorageStateResponse>;\n}\n\nexport async function callTool(\n sessionId: string,\n toolName: string,\n args: Record<string, unknown>\n): Promise<ToolResponse> {\n const port = await ensureDaemon();\n return daemonFetch(port, 'POST', `/sessions/${sessionId}/tools/${toolName}`, args) as Promise<ToolResponse>;\n}\n\n/**\n * Resolve the target session for a command.\n * If there's exactly one session, auto-targets it.\n * If a sessionId or name is provided, looks it up.\n */\nexport async function resolveTargetSession(sessionIdOrName?: string): Promise<SessionInfo> {\n const result = await listSessions();\n if (!result.ok || !result.data) {\n throw new Error('Failed to list sessions');\n }\n const sessions = result.data;\n\n if (sessions.length === 0) {\n throw new Error('No active sessions. Start one with: canary session start');\n }\n\n if (sessionIdOrName) {\n const match = sessions.find(\n (s) => s.id === sessionIdOrName || s.name === sessionIdOrName\n );\n if (!match) {\n throw new Error(\n `Session \"${sessionIdOrName}\" not found. Active sessions: ${sessions.map((s) => s.id).join(', ')}`\n );\n }\n return match;\n }\n\n if (sessions.length === 1) {\n return sessions[0];\n }\n\n throw new Error(\n `Multiple sessions active. Specify one with --session:\\n${sessions.map((s) => ` ${s.id} (${s.name})`).join('\\n')}`\n );\n}\n"],"mappings":"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","names":["path"]}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { createRequire as __cr } from "module"; const require = __cr(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
getArgValue
|
|
4
|
+
} from "./chunk-PWWQGYFG.js";
|
|
5
|
+
import {
|
|
6
|
+
getCanaryTmpDir
|
|
7
|
+
} from "./chunk-XAA5VQ5N.js";
|
|
8
|
+
|
|
9
|
+
// src/cli-helpers.ts
|
|
10
|
+
import fs from "fs/promises";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import process from "process";
|
|
13
|
+
function toLifecycleLabel(stage) {
|
|
14
|
+
switch (stage) {
|
|
15
|
+
case "deprecated":
|
|
16
|
+
return "deprecated";
|
|
17
|
+
case "ready_for_cleanup":
|
|
18
|
+
return "ready_for_cleanup";
|
|
19
|
+
default:
|
|
20
|
+
return "active";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function parseLifecycleStage(argv) {
|
|
24
|
+
const stage = getArgValue(argv, "--stage");
|
|
25
|
+
if (!stage || !["active", "deprecated", "ready_for_cleanup"].includes(stage)) {
|
|
26
|
+
console.error("Error: --stage is required and must be one of: active, deprecated, ready_for_cleanup");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
return stage;
|
|
30
|
+
}
|
|
31
|
+
async function apiRequest(apiUrl, token, method, path2, body) {
|
|
32
|
+
const res = await fetch(`${apiUrl}${path2}`, {
|
|
33
|
+
method,
|
|
34
|
+
headers: {
|
|
35
|
+
Authorization: `Bearer ${token}`,
|
|
36
|
+
"Content-Type": "application/json"
|
|
37
|
+
},
|
|
38
|
+
...body ? { body: JSON.stringify(body) } : {}
|
|
39
|
+
});
|
|
40
|
+
if (res.status === 401) {
|
|
41
|
+
console.error("Error: Unauthorized. Your session may have expired.");
|
|
42
|
+
console.error("Run: canary login");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
return await res.json();
|
|
46
|
+
}
|
|
47
|
+
async function downloadStorageState(opts) {
|
|
48
|
+
const tmpFile = path.join(
|
|
49
|
+
getCanaryTmpDir(),
|
|
50
|
+
`${opts.prefix ?? "canary-ss"}-${Date.now()}.json`
|
|
51
|
+
);
|
|
52
|
+
try {
|
|
53
|
+
const res = await fetch(
|
|
54
|
+
`${opts.apiUrl}/org/properties/${opts.propertyId}/credentials/${opts.credentialId}/storage-state/download`,
|
|
55
|
+
{
|
|
56
|
+
headers: { Authorization: `Bearer ${opts.token}` },
|
|
57
|
+
redirect: "follow"
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
if (res.ok) {
|
|
61
|
+
const body = await res.text();
|
|
62
|
+
await fs.writeFile(tmpFile, body, "utf-8");
|
|
63
|
+
return tmpFile;
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
async function selectCredential(apiUrl, token, credentialArg) {
|
|
70
|
+
const credentials = await fetchList(apiUrl, token, "/org/credentials", "items");
|
|
71
|
+
if (credentials.length === 0) {
|
|
72
|
+
console.error("No credentials found. Create one first in the web app.");
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (credentialArg) {
|
|
76
|
+
const match = credentials.find(
|
|
77
|
+
(c) => c.id === credentialArg || c.name.toLowerCase() === credentialArg.toLowerCase()
|
|
78
|
+
);
|
|
79
|
+
if (!match) {
|
|
80
|
+
console.error(`Credential "${credentialArg}" not found.`);
|
|
81
|
+
console.error("Available credentials:");
|
|
82
|
+
for (const c of credentials) {
|
|
83
|
+
const baseUrl = c.propertyBaseUrl ? ` \u2014 ${c.propertyBaseUrl}` : "";
|
|
84
|
+
console.error(` - ${c.name} [${c.propertyName ?? c.propertyId}${baseUrl}]`);
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return match;
|
|
89
|
+
}
|
|
90
|
+
console.log("\nSelect a credential:\n");
|
|
91
|
+
const labels = credentials.map((c) => {
|
|
92
|
+
const property = c.propertyName ?? c.propertyId;
|
|
93
|
+
const baseUrl = c.propertyBaseUrl ? ` \u2014 ${c.propertyBaseUrl}` : "";
|
|
94
|
+
return `${c.name} [${property}${baseUrl}]`;
|
|
95
|
+
});
|
|
96
|
+
const readline = await import("readline");
|
|
97
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
98
|
+
const answer = await new Promise((resolve) => {
|
|
99
|
+
for (let i = 0; i < labels.length; i++) {
|
|
100
|
+
console.log(` ${i + 1}. ${labels[i]}`);
|
|
101
|
+
}
|
|
102
|
+
rl.question("\nEnter number: ", (ans) => {
|
|
103
|
+
rl.close();
|
|
104
|
+
resolve(ans.trim());
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
const idx = parseInt(answer, 10) - 1;
|
|
108
|
+
if (isNaN(idx) || idx < 0 || idx >= credentials.length) {
|
|
109
|
+
console.error("Invalid selection.");
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return credentials[idx];
|
|
113
|
+
}
|
|
114
|
+
async function fetchProperties(apiUrl, token) {
|
|
115
|
+
return fetchList(apiUrl, token, "/org/properties", "data");
|
|
116
|
+
}
|
|
117
|
+
async function selectProperty(apiUrl, token, hint) {
|
|
118
|
+
const properties = await fetchProperties(apiUrl, token);
|
|
119
|
+
if (properties.length === 0) {
|
|
120
|
+
console.error("No properties found. Create one first in the web app.");
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
if (hint) {
|
|
124
|
+
const match = properties.find(
|
|
125
|
+
(p) => p.id === hint || p.name.toLowerCase() === hint.toLowerCase()
|
|
126
|
+
);
|
|
127
|
+
if (!match) {
|
|
128
|
+
console.error(`Property "${hint}" not found.`);
|
|
129
|
+
console.error("Available properties:");
|
|
130
|
+
for (const p of properties) {
|
|
131
|
+
console.error(` - ${p.name} (${p.baseUrl})`);
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return match;
|
|
136
|
+
}
|
|
137
|
+
if (properties.length === 1) {
|
|
138
|
+
console.log(`Auto-selected property: ${properties[0].name}`);
|
|
139
|
+
return properties[0];
|
|
140
|
+
}
|
|
141
|
+
return promptChoice(
|
|
142
|
+
"Select a property:",
|
|
143
|
+
properties.map((p) => ({ label: `${p.name} \u2014 ${p.baseUrl}`, value: p }))
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
async function promptInput(question, defaultValue) {
|
|
147
|
+
const readline = await import("readline");
|
|
148
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
149
|
+
const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
|
|
150
|
+
const answer = await new Promise((resolve) => {
|
|
151
|
+
rl.question(prompt, (ans) => {
|
|
152
|
+
rl.close();
|
|
153
|
+
resolve(ans.trim());
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
return answer || defaultValue || "";
|
|
157
|
+
}
|
|
158
|
+
async function promptChoice(question, options) {
|
|
159
|
+
console.log(`
|
|
160
|
+
${question}
|
|
161
|
+
`);
|
|
162
|
+
for (let i = 0; i < options.length; i++) {
|
|
163
|
+
console.log(` ${i + 1}. ${options[i].label}`);
|
|
164
|
+
}
|
|
165
|
+
const readline = await import("readline");
|
|
166
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
167
|
+
const answer = await new Promise((resolve) => {
|
|
168
|
+
rl.question("\nEnter number: ", (ans) => {
|
|
169
|
+
rl.close();
|
|
170
|
+
resolve(ans.trim());
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
const idx = parseInt(answer, 10) - 1;
|
|
174
|
+
if (isNaN(idx) || idx < 0 || idx >= options.length) {
|
|
175
|
+
console.error("Invalid selection.");
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
return options[idx].value;
|
|
179
|
+
}
|
|
180
|
+
async function uploadStorageState(opts) {
|
|
181
|
+
const res = await fetch(
|
|
182
|
+
`${opts.apiUrl}/org/properties/${opts.propertyId}/credentials/${opts.credentialId}/storage-state/upload`,
|
|
183
|
+
{
|
|
184
|
+
method: "PUT",
|
|
185
|
+
headers: {
|
|
186
|
+
Authorization: `Bearer ${opts.token}`,
|
|
187
|
+
"Content-Type": "application/json"
|
|
188
|
+
},
|
|
189
|
+
body: JSON.stringify({ storageState: opts.storageState })
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
if (res.status === 401) {
|
|
193
|
+
console.error("Error: Unauthorized. Your session may have expired.");
|
|
194
|
+
console.error("Run: canary login");
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
return await res.json();
|
|
198
|
+
}
|
|
199
|
+
async function fetchList(apiUrl, token, path2, listKey) {
|
|
200
|
+
const res = await fetch(`${apiUrl}${path2}`, {
|
|
201
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
202
|
+
});
|
|
203
|
+
if (res.status === 401) {
|
|
204
|
+
console.error("Error: Unauthorized. Your session may have expired.");
|
|
205
|
+
console.error("Run: canary login");
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
const json = await res.json();
|
|
209
|
+
if (!json.ok) {
|
|
210
|
+
console.error(`Error: ${json.error}`);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
return json[listKey] ?? [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export {
|
|
217
|
+
toLifecycleLabel,
|
|
218
|
+
parseLifecycleStage,
|
|
219
|
+
apiRequest,
|
|
220
|
+
downloadStorageState,
|
|
221
|
+
selectCredential,
|
|
222
|
+
fetchProperties,
|
|
223
|
+
selectProperty,
|
|
224
|
+
promptInput,
|
|
225
|
+
promptChoice,
|
|
226
|
+
uploadStorageState,
|
|
227
|
+
fetchList
|
|
228
|
+
};
|
|
229
|
+
//# sourceMappingURL=chunk-ERSNYLMZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli-helpers.ts"],"sourcesContent":["/**\n * Shared CLI helpers for superadmin management commands (knobs, feature-flags).\n */\n\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport { getCanaryTmpDir } from \"@chatsdet/tmp\";\nimport { getArgValue } from \"./auth.js\";\n\nexport type LifecycleStage = \"active\" | \"deprecated\" | \"ready_for_cleanup\";\n\nexport function toLifecycleLabel(stage: LifecycleStage): string {\n switch (stage) {\n case \"deprecated\":\n return \"deprecated\";\n case \"ready_for_cleanup\":\n return \"ready_for_cleanup\";\n default:\n return \"active\";\n }\n}\n\nexport function parseLifecycleStage(argv: string[]): LifecycleStage {\n const stage = getArgValue(argv, \"--stage\");\n if (!stage || ![\"active\", \"deprecated\", \"ready_for_cleanup\"].includes(stage)) {\n console.error(\"Error: --stage is required and must be one of: active, deprecated, ready_for_cleanup\");\n process.exit(1);\n }\n return stage as LifecycleStage;\n}\n\nexport async function apiRequest<T extends { ok: boolean; error?: string }>(\n apiUrl: string,\n token: string,\n method: string,\n path: string,\n body?: Record<string, unknown>\n): Promise<T> {\n const res = await fetch(`${apiUrl}${path}`, {\n method,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n ...(body ? { body: JSON.stringify(body) } : {}),\n });\n\n if (res.status === 401) {\n console.error(\"Error: Unauthorized. Your session may have expired.\");\n console.error(\"Run: canary login\");\n process.exit(1);\n }\n\n return (await res.json()) as T;\n}\n\nexport async function downloadStorageState(opts: {\n apiUrl: string;\n token: string;\n propertyId: string;\n credentialId: string;\n prefix?: string;\n}): Promise<string | undefined> {\n const tmpFile = path.join(\n getCanaryTmpDir(),\n `${opts.prefix ?? \"canary-ss\"}-${Date.now()}.json`\n );\n try {\n const res = await fetch(\n `${opts.apiUrl}/org/properties/${opts.propertyId}/credentials/${opts.credentialId}/storage-state/download`,\n {\n headers: { Authorization: `Bearer ${opts.token}` },\n redirect: \"follow\",\n }\n );\n if (res.ok) {\n const body = await res.text();\n await fs.writeFile(tmpFile, body, \"utf-8\");\n return tmpFile;\n }\n } catch {\n // Caller handles missing storage state\n }\n return undefined;\n}\n\n/* ── Credential selection ─────────────────────────────────────────────── */\n\nexport interface CredentialListItem {\n id: string;\n name: string;\n propertyId: string;\n propertyName?: string;\n propertyBaseUrl?: string;\n loginUrl?: string;\n storageStateS3Key?: string | null;\n}\n\n/**\n * Fetch org credentials and let the user pick one interactively.\n *\n * @param credentialArg - Credential name or ID to auto-select, or undefined for interactive\n * @returns The selected credential, or null if none found / invalid selection\n */\nexport async function selectCredential(\n apiUrl: string,\n token: string,\n credentialArg?: string\n): Promise<CredentialListItem | null> {\n const credentials = await fetchList<CredentialListItem>(apiUrl, token, \"/org/credentials\", \"items\");\n\n if (credentials.length === 0) {\n console.error(\"No credentials found. Create one first in the web app.\");\n return null;\n }\n\n if (credentialArg) {\n const match = credentials.find(\n (c) =>\n c.id === credentialArg ||\n c.name.toLowerCase() === credentialArg.toLowerCase()\n );\n if (!match) {\n console.error(`Credential \"${credentialArg}\" not found.`);\n console.error(\"Available credentials:\");\n for (const c of credentials) {\n const baseUrl = c.propertyBaseUrl ? ` — ${c.propertyBaseUrl}` : '';\n console.error(` - ${c.name} [${c.propertyName ?? c.propertyId}${baseUrl}]`);\n }\n return null;\n }\n return match;\n }\n\n // Interactive selection\n console.log(\"\\nSelect a credential:\\n\");\n const labels = credentials.map((c) => {\n const property = c.propertyName ?? c.propertyId;\n const baseUrl = c.propertyBaseUrl ? ` — ${c.propertyBaseUrl}` : '';\n return `${c.name} [${property}${baseUrl}]`;\n });\n\n const readline = await import(\"node:readline\");\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n const answer = await new Promise<string>((resolve) => {\n for (let i = 0; i < labels.length; i++) {\n console.log(` ${i + 1}. ${labels[i]}`);\n }\n rl.question(\"\\nEnter number: \", (ans) => {\n rl.close();\n resolve(ans.trim());\n });\n });\n\n const idx = parseInt(answer, 10) - 1;\n if (isNaN(idx) || idx < 0 || idx >= credentials.length) {\n console.error(\"Invalid selection.\");\n return null;\n }\n\n return credentials[idx];\n}\n\n/* ── Property selection ──────────────────────────────────────────────── */\n\nexport interface PropertyListItem {\n id: string;\n name: string;\n baseUrl: string;\n}\n\nexport async function fetchProperties(\n apiUrl: string,\n token: string\n): Promise<PropertyListItem[]> {\n return fetchList<PropertyListItem>(apiUrl, token, \"/org/properties\", \"data\");\n}\n\nexport async function selectProperty(\n apiUrl: string,\n token: string,\n hint?: string\n): Promise<PropertyListItem | null> {\n const properties = await fetchProperties(apiUrl, token);\n\n if (properties.length === 0) {\n console.error(\"No properties found. Create one first in the web app.\");\n return null;\n }\n\n if (hint) {\n const match = properties.find(\n (p) =>\n p.id === hint ||\n p.name.toLowerCase() === hint.toLowerCase()\n );\n if (!match) {\n console.error(`Property \"${hint}\" not found.`);\n console.error(\"Available properties:\");\n for (const p of properties) {\n console.error(` - ${p.name} (${p.baseUrl})`);\n }\n return null;\n }\n return match;\n }\n\n if (properties.length === 1) {\n console.log(`Auto-selected property: ${properties[0].name}`);\n return properties[0];\n }\n\n return promptChoice(\n \"Select a property:\",\n properties.map((p) => ({ label: `${p.name} — ${p.baseUrl}`, value: p }))\n );\n}\n\n/* ── Interactive prompt helpers ──────────────────────────────────────── */\n\nexport async function promptInput(question: string, defaultValue?: string): Promise<string> {\n const readline = await import(\"node:readline\");\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;\n const answer = await new Promise<string>((resolve) => {\n rl.question(prompt, (ans) => {\n rl.close();\n resolve(ans.trim());\n });\n });\n return answer || defaultValue || \"\";\n}\n\nexport async function promptChoice<T>(\n question: string,\n options: Array<{ label: string; value: T }>\n): Promise<T | null> {\n console.log(`\\n${question}\\n`);\n for (let i = 0; i < options.length; i++) {\n console.log(` ${i + 1}. ${options[i].label}`);\n }\n const readline = await import(\"node:readline\");\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n const answer = await new Promise<string>((resolve) => {\n rl.question(\"\\nEnter number: \", (ans) => {\n rl.close();\n resolve(ans.trim());\n });\n });\n\n const idx = parseInt(answer, 10) - 1;\n if (isNaN(idx) || idx < 0 || idx >= options.length) {\n console.error(\"Invalid selection.\");\n return null;\n }\n return options[idx].value;\n}\n\nexport async function promptConfirm(question: string): Promise<boolean> {\n const answer = await promptInput(`${question} (y/n)`, \"n\");\n return answer.toLowerCase() === \"y\" || answer.toLowerCase() === \"yes\";\n}\n\nexport async function uploadStorageState(opts: {\n apiUrl: string;\n token: string;\n propertyId: string;\n credentialId: string;\n storageState: unknown;\n}): Promise<{ ok: boolean; s3Key?: string; error?: string }> {\n const res = await fetch(\n `${opts.apiUrl}/org/properties/${opts.propertyId}/credentials/${opts.credentialId}/storage-state/upload`,\n {\n method: \"PUT\",\n headers: {\n Authorization: `Bearer ${opts.token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ storageState: opts.storageState }),\n }\n );\n\n if (res.status === 401) {\n console.error(\"Error: Unauthorized. Your session may have expired.\");\n console.error(\"Run: canary login\");\n process.exit(1);\n }\n\n return (await res.json()) as { ok: boolean; s3Key?: string; error?: string };\n}\n\nexport async function fetchList<T>(\n apiUrl: string,\n token: string,\n path: string,\n listKey: string\n): Promise<T[]> {\n const res = await fetch(`${apiUrl}${path}`, {\n headers: { Authorization: `Bearer ${token}` },\n });\n\n if (res.status === 401) {\n console.error(\"Error: Unauthorized. Your session may have expired.\");\n console.error(\"Run: canary login\");\n process.exit(1);\n }\n\n const json = (await res.json()) as Record<string, unknown>;\n\n if (!json.ok) {\n console.error(`Error: ${json.error}`);\n process.exit(1);\n }\n\n return (json[listKey] as T[]) ?? [];\n}\n"],"mappings":"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","names":["path","path"]}
|
|
@@ -38537,6 +38537,18 @@ function normalizeIconLabelText(role, text) {
|
|
|
38537
38537
|
}
|
|
38538
38538
|
return ICON_ONLY_ROLES.has(role) ? "(icon)" : "";
|
|
38539
38539
|
}
|
|
38540
|
+
var STYLED_WRAPPER_RE = /^Styled\(\w+\)$/;
|
|
38541
|
+
var MUI_CLASS_RE = /^Mui\w+-[\w-]+$/;
|
|
38542
|
+
var HAS_ASCII_ALNUM_RE = /[A-Za-z0-9]/;
|
|
38543
|
+
function isNoisyLabel(text) {
|
|
38544
|
+
const trimmed = text.trim();
|
|
38545
|
+
if (!trimmed) return false;
|
|
38546
|
+
if (STYLED_WRAPPER_RE.test(trimmed)) return true;
|
|
38547
|
+
if (MUI_CLASS_RE.test(trimmed)) return true;
|
|
38548
|
+
if (HAS_PRIVATE_USE_GLYPH_RE.test(trimmed)) return false;
|
|
38549
|
+
if (!HAS_ASCII_ALNUM_RE.test(trimmed)) return true;
|
|
38550
|
+
return false;
|
|
38551
|
+
}
|
|
38540
38552
|
|
|
38541
38553
|
// ../browser-core/src/snapshot-formatter-shared.ts
|
|
38542
38554
|
var MAX_SEARCH_MATCHES = 10;
|
|
@@ -38651,9 +38663,34 @@ function composeLabel(element) {
|
|
|
38651
38663
|
}
|
|
38652
38664
|
return textParts.join(" | ") || element.role;
|
|
38653
38665
|
}
|
|
38666
|
+
function composeChildLabel(element) {
|
|
38667
|
+
const textParts = [];
|
|
38668
|
+
function collectText(el, depth) {
|
|
38669
|
+
if (depth > 3) return;
|
|
38670
|
+
const text = getElementText(el);
|
|
38671
|
+
if (text && TEXT_CARRYING_ROLES.has(el.role)) {
|
|
38672
|
+
textParts.push(text);
|
|
38673
|
+
}
|
|
38674
|
+
for (const child of el.children) {
|
|
38675
|
+
if (!INTERACTIVE_LEAF_ROLES.has(child.role)) {
|
|
38676
|
+
collectText(child, depth + 1);
|
|
38677
|
+
}
|
|
38678
|
+
}
|
|
38679
|
+
}
|
|
38680
|
+
for (const child of element.children) {
|
|
38681
|
+
if (!INTERACTIVE_LEAF_ROLES.has(child.role)) {
|
|
38682
|
+
collectText(child, 0);
|
|
38683
|
+
}
|
|
38684
|
+
}
|
|
38685
|
+
return textParts.length > 0 ? textParts.join(" | ") : null;
|
|
38686
|
+
}
|
|
38654
38687
|
function getDisplayText(element, options) {
|
|
38655
38688
|
const isClickable = isActionableClickableGeneric(element);
|
|
38656
|
-
|
|
38689
|
+
let rawTextSource = options?.overrideLabel ?? (isClickable ? composeLabel(element) : element.text ?? "");
|
|
38690
|
+
if (rawTextSource && isNoisyLabel(rawTextSource) && INTERACTIVE_LEAF_ROLES.has(element.role)) {
|
|
38691
|
+
const childText = composeChildLabel(element);
|
|
38692
|
+
rawTextSource = childText ?? "";
|
|
38693
|
+
}
|
|
38657
38694
|
return normalizeIconLabelText(element.role, rawTextSource);
|
|
38658
38695
|
}
|
|
38659
38696
|
function hasInteractiveDescendants(element) {
|
|
@@ -38852,7 +38889,7 @@ function extractGridCell(cellEl) {
|
|
|
38852
38889
|
findInteractive(cellEl);
|
|
38853
38890
|
if (interactiveElements.length === 0) {
|
|
38854
38891
|
const textValue = extractCellText(cellEl);
|
|
38855
|
-
if (textValue) {
|
|
38892
|
+
if (textValue && !isNoisyLabel(textValue)) {
|
|
38856
38893
|
return {
|
|
38857
38894
|
ref: cellEl.ref,
|
|
38858
38895
|
column: cellEl.text || "",
|
|
@@ -38866,7 +38903,8 @@ function extractGridCell(cellEl) {
|
|
|
38866
38903
|
(best, current) => gridCellPriority(current) > gridCellPriority(best) ? current : best
|
|
38867
38904
|
);
|
|
38868
38905
|
const column = stripBlankPatterns(found.column);
|
|
38869
|
-
const
|
|
38906
|
+
const rawValue = isBlankValue(found.value) ? "" : stripBlankPatterns(found.value).trim();
|
|
38907
|
+
const value = isNoisyLabel(rawValue) ? "" : rawValue;
|
|
38870
38908
|
const gridcellLabel = cellEl.text ? stripBlankPatterns(cellEl.text).trim() : void 0;
|
|
38871
38909
|
return {
|
|
38872
38910
|
ref: found.ref,
|
|
@@ -39821,6 +39859,24 @@ function markBackgroundSections(sections) {
|
|
|
39821
39859
|
markBackgroundSections(section.subsections);
|
|
39822
39860
|
}
|
|
39823
39861
|
}
|
|
39862
|
+
function mergeFilterDropdownPairs(elements) {
|
|
39863
|
+
for (let i = elements.length - 2; i >= 0; i--) {
|
|
39864
|
+
const current = elements[i];
|
|
39865
|
+
const next = elements[i + 1];
|
|
39866
|
+
if (current.role === "generic" && current.isClickable && next.role === "img" && next.isClickable && (!next.label || next.label === "img")) {
|
|
39867
|
+
if (!current.fieldLabel && next.fieldLabel) {
|
|
39868
|
+
current.fieldLabel = next.fieldLabel;
|
|
39869
|
+
}
|
|
39870
|
+
elements.splice(i + 1, 1);
|
|
39871
|
+
}
|
|
39872
|
+
}
|
|
39873
|
+
}
|
|
39874
|
+
function applyFilterMerge(sections) {
|
|
39875
|
+
for (const section of sections) {
|
|
39876
|
+
mergeFilterDropdownPairs(section.elements);
|
|
39877
|
+
applyFilterMerge(section.subsections);
|
|
39878
|
+
}
|
|
39879
|
+
}
|
|
39824
39880
|
function buildSectionTree(elements) {
|
|
39825
39881
|
const sections = [];
|
|
39826
39882
|
for (const element of elements) {
|
|
@@ -39830,6 +39886,7 @@ function buildSectionTree(elements) {
|
|
|
39830
39886
|
}
|
|
39831
39887
|
}
|
|
39832
39888
|
markBackgroundSections(sections);
|
|
39889
|
+
applyFilterMerge(sections);
|
|
39833
39890
|
return sections;
|
|
39834
39891
|
}
|
|
39835
39892
|
function markMatchingSections(sections, matchedRefs) {
|
|
@@ -40146,6 +40203,60 @@ ACTIONS:
|
|
|
40146
40203
|
- Use bash or python_execute on the FULL TREE FILE for detailed element analysis
|
|
40147
40204
|
---`;
|
|
40148
40205
|
}
|
|
40206
|
+
function disambiguateDuplicateLabels(sections) {
|
|
40207
|
+
const elementEntries = [];
|
|
40208
|
+
function collectElements(section) {
|
|
40209
|
+
for (const el of section.elements) {
|
|
40210
|
+
elementEntries.push({ el, sectionHeading: section.heading });
|
|
40211
|
+
}
|
|
40212
|
+
for (const sub of section.subsections) {
|
|
40213
|
+
collectElements(sub);
|
|
40214
|
+
}
|
|
40215
|
+
}
|
|
40216
|
+
for (const section of sections) {
|
|
40217
|
+
collectElements(section);
|
|
40218
|
+
}
|
|
40219
|
+
const SKIP_LABELS = /* @__PURE__ */ new Set(["(icon)", "button", "link", "generic"]);
|
|
40220
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
40221
|
+
for (const entry of elementEntries) {
|
|
40222
|
+
const label = entry.el.label || entry.el.name;
|
|
40223
|
+
if (!label || label.length === 0) continue;
|
|
40224
|
+
if (SKIP_LABELS.has(label)) continue;
|
|
40225
|
+
if (!entry.el.ref) continue;
|
|
40226
|
+
const existing = byLabel.get(label);
|
|
40227
|
+
if (existing) {
|
|
40228
|
+
existing.push(entry);
|
|
40229
|
+
} else {
|
|
40230
|
+
byLabel.set(label, [entry]);
|
|
40231
|
+
}
|
|
40232
|
+
}
|
|
40233
|
+
for (const [, entries] of byLabel) {
|
|
40234
|
+
if (entries.length < 2) continue;
|
|
40235
|
+
const distinctSections = new Set(entries.map((e) => e.sectionHeading));
|
|
40236
|
+
if (distinctSections.size > 1) {
|
|
40237
|
+
for (const entry of entries) {
|
|
40238
|
+
const currentLabel = entry.el.label || entry.el.name || "";
|
|
40239
|
+
if (entry.el.label) {
|
|
40240
|
+
entry.el.label = `${currentLabel} (in ${entry.sectionHeading})`;
|
|
40241
|
+
} else if (entry.el.name) {
|
|
40242
|
+
entry.el.name = `${currentLabel} (in ${entry.sectionHeading})`;
|
|
40243
|
+
}
|
|
40244
|
+
}
|
|
40245
|
+
} else {
|
|
40246
|
+
const ordinals = ["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th"];
|
|
40247
|
+
for (let i = 0; i < entries.length; i++) {
|
|
40248
|
+
const entry = entries[i];
|
|
40249
|
+
const ordinal = i < ordinals.length ? ordinals[i] : `${i + 1}th`;
|
|
40250
|
+
const currentLabel = entry.el.label || entry.el.name || "";
|
|
40251
|
+
if (entry.el.label) {
|
|
40252
|
+
entry.el.label = `${currentLabel} (${ordinal})`;
|
|
40253
|
+
} else if (entry.el.name) {
|
|
40254
|
+
entry.el.name = `${currentLabel} (${ordinal})`;
|
|
40255
|
+
}
|
|
40256
|
+
}
|
|
40257
|
+
}
|
|
40258
|
+
}
|
|
40259
|
+
}
|
|
40149
40260
|
function formatSemanticSnapshot(yaml, options) {
|
|
40150
40261
|
if (!yaml || yaml.trim() === "") {
|
|
40151
40262
|
return "---\nSEMANTIC SNAPSHOT - 0 elements\nPage appears empty or not loaded.\n---";
|
|
@@ -40179,6 +40290,7 @@ function formatSemanticSnapshot(yaml, options) {
|
|
|
40179
40290
|
if (options?.probeResult?.ref) {
|
|
40180
40291
|
markMatchingSections(sortedSections, /* @__PURE__ */ new Set([options.probeResult.ref]));
|
|
40181
40292
|
}
|
|
40293
|
+
disambiguateDuplicateLabels(sortedSections);
|
|
40182
40294
|
const elementCount = countRenderedElements(sortedSections);
|
|
40183
40295
|
const sectionCount = countTotalSections(sortedSections);
|
|
40184
40296
|
const lines = [];
|
|
@@ -40204,7 +40316,11 @@ function formatSemanticSnapshot(yaml, options) {
|
|
|
40204
40316
|
lines.push("");
|
|
40205
40317
|
}
|
|
40206
40318
|
}
|
|
40207
|
-
|
|
40319
|
+
let result = lines.join("\n").trim();
|
|
40320
|
+
if (options?.offScreenRefs && options.offScreenRefs.size > 0) {
|
|
40321
|
+
result = markOffScreenElements(result, options.offScreenRefs);
|
|
40322
|
+
}
|
|
40323
|
+
return result;
|
|
40208
40324
|
}
|
|
40209
40325
|
function expandSection(yaml, sectionRef) {
|
|
40210
40326
|
if (!yaml || yaml.trim() === "") {
|
|
@@ -40278,6 +40394,15 @@ Section with ref="${sectionRef}" not found in snapshot.
|
|
|
40278
40394
|
targetSection.collapsed = false;
|
|
40279
40395
|
return formatExpandedSectionOutput(targetSection);
|
|
40280
40396
|
}
|
|
40397
|
+
function markOffScreenElements(text, offScreenRefs) {
|
|
40398
|
+
if (offScreenRefs.size === 0) return text;
|
|
40399
|
+
return text.replace(/\[(e\d+)\](?! \[off-screen\])/g, (match, ref) => {
|
|
40400
|
+
if (offScreenRefs.has(ref)) {
|
|
40401
|
+
return `[${ref}] [off-screen]`;
|
|
40402
|
+
}
|
|
40403
|
+
return match;
|
|
40404
|
+
});
|
|
40405
|
+
}
|
|
40281
40406
|
|
|
40282
40407
|
// ../browser-core/src/table-extractor.ts
|
|
40283
40408
|
function extractTablesFromSnapshot(yamlOrResponse, options) {
|
|
@@ -41458,6 +41583,42 @@ ${effectiveShowCoordinateGrid ? "Coordinate grid enabled - read coordinates from
|
|
|
41458
41583
|
await fs2.writeFile(snapshotFilePath, yaml, "utf-8");
|
|
41459
41584
|
} catch {
|
|
41460
41585
|
}
|
|
41586
|
+
let offScreenRefs;
|
|
41587
|
+
try {
|
|
41588
|
+
const viewportSize = this.client.getViewportSize();
|
|
41589
|
+
const offScreen = await this.client.evaluate(
|
|
41590
|
+
`() => {
|
|
41591
|
+
const vw = ${viewportSize.width};
|
|
41592
|
+
const vh = ${viewportSize.height};
|
|
41593
|
+
const offscreen = [];
|
|
41594
|
+
const els = document.querySelectorAll('[aria-ref]');
|
|
41595
|
+
for (const el of els) {
|
|
41596
|
+
const ref = el.getAttribute('aria-ref');
|
|
41597
|
+
if (!ref) continue;
|
|
41598
|
+
const r = el.getBoundingClientRect();
|
|
41599
|
+
if (r.width === 0 && r.height === 0) continue;
|
|
41600
|
+
if (r.right < 0 || r.left > vw || r.bottom < 0 || r.top > vh) {
|
|
41601
|
+
offscreen.push(ref);
|
|
41602
|
+
}
|
|
41603
|
+
}
|
|
41604
|
+
return offscreen;
|
|
41605
|
+
}`,
|
|
41606
|
+
{ signal: this.signal }
|
|
41607
|
+
);
|
|
41608
|
+
if (typeof offScreen === "string") {
|
|
41609
|
+
try {
|
|
41610
|
+
const parsed = JSON.parse(offScreen);
|
|
41611
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
41612
|
+
offScreenRefs = new Set(parsed);
|
|
41613
|
+
}
|
|
41614
|
+
} catch {
|
|
41615
|
+
}
|
|
41616
|
+
}
|
|
41617
|
+
} catch (e) {
|
|
41618
|
+
this.logger?.debug?.("[browser_snapshot] Off-screen detection failed", {
|
|
41619
|
+
error: e instanceof Error ? e.message : String(e)
|
|
41620
|
+
});
|
|
41621
|
+
}
|
|
41461
41622
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
41462
41623
|
const semanticText = formatSemanticSnapshot(yaml, {
|
|
41463
41624
|
searchMatches: searchMatches.length > 0 ? searchMatches : void 0,
|
|
@@ -41465,7 +41626,8 @@ ${effectiveShowCoordinateGrid ? "Coordinate grid enabled - read coordinates from
|
|
|
41465
41626
|
snapshotFilePath,
|
|
41466
41627
|
probeResult,
|
|
41467
41628
|
currentTime: now,
|
|
41468
|
-
capturedToasts: this.client.getCapturedToasts?.() ?? []
|
|
41629
|
+
capturedToasts: this.client.getCapturedToasts?.() ?? [],
|
|
41630
|
+
offScreenRefs
|
|
41469
41631
|
});
|
|
41470
41632
|
if (hasImages) {
|
|
41471
41633
|
const resolutionNote = buildResolutionNote(
|
|
@@ -42179,7 +42341,7 @@ function getBrowserToolDefinitions() {
|
|
|
42179
42341
|
},
|
|
42180
42342
|
toRef: {
|
|
42181
42343
|
type: "string",
|
|
42182
|
-
description: "Scroll the page so this element is centered
|
|
42344
|
+
description: "Scroll the page so this element is centered in the viewport. Handles both vertical and horizontal scrolling automatically \u2014 works for grid columns that are horizontally off-screen."
|
|
42183
42345
|
},
|
|
42184
42346
|
x: {
|
|
42185
42347
|
type: "number",
|
|
@@ -46149,17 +46311,42 @@ Use coordinate-based clicking (x, y) to interact with elements visible in the sc
|
|
|
46149
46311
|
const locator = await this.resolveRef(toRef);
|
|
46150
46312
|
const box = await locator.boundingBox();
|
|
46151
46313
|
if (box) {
|
|
46314
|
+
const viewportSize = this.getViewportSize();
|
|
46152
46315
|
const elementCenterY = box.y + box.height / 2;
|
|
46153
|
-
const
|
|
46154
|
-
const viewportCenterY = viewportHeight / 2;
|
|
46316
|
+
const viewportCenterY = viewportSize.height / 2;
|
|
46155
46317
|
const scrollY = elementCenterY - viewportCenterY;
|
|
46318
|
+
const elementCenterX = box.x + box.width / 2;
|
|
46319
|
+
const viewportCenterX = viewportSize.width / 2;
|
|
46320
|
+
const boxRight = box.x + box.width;
|
|
46321
|
+
const needsHorizontalScroll = boxRight < 0 || box.x > viewportSize.width || elementCenterX < 0 || elementCenterX > viewportSize.width;
|
|
46322
|
+
const scrollX = needsHorizontalScroll ? elementCenterX - viewportCenterX : 0;
|
|
46156
46323
|
this.logger.debug("[DirectPlaywright] Scrolling to center element", {
|
|
46157
46324
|
toRef,
|
|
46158
46325
|
elementCenterY,
|
|
46159
46326
|
viewportCenterY,
|
|
46160
|
-
scrollY
|
|
46327
|
+
scrollY,
|
|
46328
|
+
elementCenterX,
|
|
46329
|
+
scrollX,
|
|
46330
|
+
needsHorizontalScroll
|
|
46161
46331
|
});
|
|
46162
|
-
await page.evaluate(`window.scrollBy(
|
|
46332
|
+
await page.evaluate(`window.scrollBy(${scrollX}, ${scrollY})`);
|
|
46333
|
+
if (needsHorizontalScroll) {
|
|
46334
|
+
await page.evaluate((ref) => {
|
|
46335
|
+
const el = document.querySelector(`[aria-ref="${ref}"]`);
|
|
46336
|
+
if (!el) return;
|
|
46337
|
+
let parent = el.parentElement;
|
|
46338
|
+
for (let i = 0; i < 10 && parent; i++) {
|
|
46339
|
+
if (parent.scrollWidth > parent.clientWidth) {
|
|
46340
|
+
const elRect = el.getBoundingClientRect();
|
|
46341
|
+
const parentRect = parent.getBoundingClientRect();
|
|
46342
|
+
const targetScrollLeft = parent.scrollLeft + (elRect.left - parentRect.left) - parentRect.width / 2 + elRect.width / 2;
|
|
46343
|
+
parent.scrollLeft = Math.max(0, targetScrollLeft);
|
|
46344
|
+
break;
|
|
46345
|
+
}
|
|
46346
|
+
parent = parent.parentElement;
|
|
46347
|
+
}
|
|
46348
|
+
}, toRef);
|
|
46349
|
+
}
|
|
46163
46350
|
} else {
|
|
46164
46351
|
this.logger.warn("[DirectPlaywright] No bounding box, using scrollIntoViewIfNeeded", {
|
|
46165
46352
|
toRef
|
|
@@ -48285,4 +48472,4 @@ playwright-extra/dist/index.esm.js:
|
|
|
48285
48472
|
* @license MIT
|
|
48286
48473
|
*)
|
|
48287
48474
|
*/
|
|
48288
|
-
//# sourceMappingURL=chunk-
|
|
48475
|
+
//# sourceMappingURL=chunk-MSMC6UXW.js.map
|