libretto 0.2.6 → 0.3.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 (160) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +12 -12
  3. package/skill/SKILL.md +20 -18
  4. package/skill/code-generation-rules.md +3 -3
  5. package/skill/integration-approach-selection.md +3 -3
  6. package/dist/cli/cli.js +0 -209
  7. package/dist/cli/commands/ai.js +0 -21
  8. package/dist/cli/commands/browser.js +0 -82
  9. package/dist/cli/commands/execution.js +0 -461
  10. package/dist/cli/commands/init.js +0 -95
  11. package/dist/cli/commands/logs.js +0 -93
  12. package/dist/cli/commands/snapshot.js +0 -106
  13. package/dist/cli/core/ai-config.js +0 -149
  14. package/dist/cli/core/browser.js +0 -648
  15. package/dist/cli/core/context.js +0 -118
  16. package/dist/cli/core/pause-signals.js +0 -29
  17. package/dist/cli/core/session-telemetry.js +0 -491
  18. package/dist/cli/core/session.js +0 -183
  19. package/dist/cli/core/snapshot-analyzer.js +0 -492
  20. package/dist/cli/core/telemetry.js +0 -362
  21. package/dist/cli/index.js +0 -13
  22. package/dist/cli/workers/run-integration-runtime.js +0 -227
  23. package/dist/cli/workers/run-integration-worker-protocol.js +0 -12
  24. package/dist/cli/workers/run-integration-worker.js +0 -66
  25. package/dist/index.cjs +0 -116
  26. package/dist/index.d.cts +0 -21
  27. package/dist/index.d.ts +0 -21
  28. package/dist/index.js +0 -97
  29. package/dist/runtime/download/download.cjs +0 -70
  30. package/dist/runtime/download/download.d.cts +0 -35
  31. package/dist/runtime/download/download.d.ts +0 -35
  32. package/dist/runtime/download/download.js +0 -45
  33. package/dist/runtime/download/index.cjs +0 -30
  34. package/dist/runtime/download/index.d.cts +0 -3
  35. package/dist/runtime/download/index.d.ts +0 -3
  36. package/dist/runtime/download/index.js +0 -8
  37. package/dist/runtime/extract/extract.cjs +0 -88
  38. package/dist/runtime/extract/extract.d.cts +0 -23
  39. package/dist/runtime/extract/extract.d.ts +0 -23
  40. package/dist/runtime/extract/extract.js +0 -64
  41. package/dist/runtime/extract/index.cjs +0 -28
  42. package/dist/runtime/extract/index.d.cts +0 -5
  43. package/dist/runtime/extract/index.d.ts +0 -5
  44. package/dist/runtime/extract/index.js +0 -4
  45. package/dist/runtime/network/index.cjs +0 -28
  46. package/dist/runtime/network/index.d.cts +0 -4
  47. package/dist/runtime/network/index.d.ts +0 -4
  48. package/dist/runtime/network/index.js +0 -6
  49. package/dist/runtime/network/network.cjs +0 -91
  50. package/dist/runtime/network/network.d.cts +0 -28
  51. package/dist/runtime/network/network.d.ts +0 -28
  52. package/dist/runtime/network/network.js +0 -67
  53. package/dist/runtime/recovery/agent.cjs +0 -223
  54. package/dist/runtime/recovery/agent.d.cts +0 -13
  55. package/dist/runtime/recovery/agent.d.ts +0 -13
  56. package/dist/runtime/recovery/agent.js +0 -199
  57. package/dist/runtime/recovery/errors.cjs +0 -124
  58. package/dist/runtime/recovery/errors.d.cts +0 -31
  59. package/dist/runtime/recovery/errors.d.ts +0 -31
  60. package/dist/runtime/recovery/errors.js +0 -100
  61. package/dist/runtime/recovery/index.cjs +0 -34
  62. package/dist/runtime/recovery/index.d.cts +0 -7
  63. package/dist/runtime/recovery/index.d.ts +0 -7
  64. package/dist/runtime/recovery/index.js +0 -10
  65. package/dist/runtime/recovery/recovery.cjs +0 -55
  66. package/dist/runtime/recovery/recovery.d.cts +0 -12
  67. package/dist/runtime/recovery/recovery.d.ts +0 -12
  68. package/dist/runtime/recovery/recovery.js +0 -31
  69. package/dist/shared/config/config.cjs +0 -44
  70. package/dist/shared/config/config.d.cts +0 -10
  71. package/dist/shared/config/config.d.ts +0 -10
  72. package/dist/shared/config/config.js +0 -18
  73. package/dist/shared/config/index.cjs +0 -32
  74. package/dist/shared/config/index.d.cts +0 -1
  75. package/dist/shared/config/index.d.ts +0 -1
  76. package/dist/shared/config/index.js +0 -10
  77. package/dist/shared/debug/index.cjs +0 -30
  78. package/dist/shared/debug/index.d.cts +0 -1
  79. package/dist/shared/debug/index.d.ts +0 -1
  80. package/dist/shared/debug/index.js +0 -5
  81. package/dist/shared/debug/pause.cjs +0 -90
  82. package/dist/shared/debug/pause.d.cts +0 -16
  83. package/dist/shared/debug/pause.d.ts +0 -16
  84. package/dist/shared/debug/pause.js +0 -55
  85. package/dist/shared/instrumentation/errors.cjs +0 -81
  86. package/dist/shared/instrumentation/errors.d.cts +0 -12
  87. package/dist/shared/instrumentation/errors.d.ts +0 -12
  88. package/dist/shared/instrumentation/errors.js +0 -57
  89. package/dist/shared/instrumentation/index.cjs +0 -35
  90. package/dist/shared/instrumentation/index.d.cts +0 -6
  91. package/dist/shared/instrumentation/index.d.ts +0 -6
  92. package/dist/shared/instrumentation/index.js +0 -12
  93. package/dist/shared/instrumentation/instrument.cjs +0 -206
  94. package/dist/shared/instrumentation/instrument.d.cts +0 -32
  95. package/dist/shared/instrumentation/instrument.d.ts +0 -32
  96. package/dist/shared/instrumentation/instrument.js +0 -190
  97. package/dist/shared/llm/ai-sdk-adapter.cjs +0 -67
  98. package/dist/shared/llm/ai-sdk-adapter.d.cts +0 -22
  99. package/dist/shared/llm/ai-sdk-adapter.d.ts +0 -22
  100. package/dist/shared/llm/ai-sdk-adapter.js +0 -43
  101. package/dist/shared/llm/client.cjs +0 -139
  102. package/dist/shared/llm/client.d.cts +0 -6
  103. package/dist/shared/llm/client.d.ts +0 -6
  104. package/dist/shared/llm/client.js +0 -115
  105. package/dist/shared/llm/index.cjs +0 -31
  106. package/dist/shared/llm/index.d.cts +0 -5
  107. package/dist/shared/llm/index.d.ts +0 -5
  108. package/dist/shared/llm/index.js +0 -6
  109. package/dist/shared/llm/types.cjs +0 -16
  110. package/dist/shared/llm/types.d.cts +0 -66
  111. package/dist/shared/llm/types.d.ts +0 -66
  112. package/dist/shared/llm/types.js +0 -0
  113. package/dist/shared/logger/index.cjs +0 -37
  114. package/dist/shared/logger/index.d.cts +0 -2
  115. package/dist/shared/logger/index.d.ts +0 -2
  116. package/dist/shared/logger/index.js +0 -13
  117. package/dist/shared/logger/logger.cjs +0 -213
  118. package/dist/shared/logger/logger.d.cts +0 -82
  119. package/dist/shared/logger/logger.d.ts +0 -82
  120. package/dist/shared/logger/logger.js +0 -188
  121. package/dist/shared/logger/sinks.cjs +0 -160
  122. package/dist/shared/logger/sinks.d.cts +0 -9
  123. package/dist/shared/logger/sinks.d.ts +0 -9
  124. package/dist/shared/logger/sinks.js +0 -124
  125. package/dist/shared/paths/paths.cjs +0 -104
  126. package/dist/shared/paths/paths.d.cts +0 -10
  127. package/dist/shared/paths/paths.d.ts +0 -10
  128. package/dist/shared/paths/paths.js +0 -73
  129. package/dist/shared/run/api.cjs +0 -28
  130. package/dist/shared/run/api.d.cts +0 -2
  131. package/dist/shared/run/api.d.ts +0 -2
  132. package/dist/shared/run/api.js +0 -4
  133. package/dist/shared/run/browser.cjs +0 -98
  134. package/dist/shared/run/browser.d.cts +0 -22
  135. package/dist/shared/run/browser.d.ts +0 -22
  136. package/dist/shared/run/browser.js +0 -74
  137. package/dist/shared/state/index.cjs +0 -38
  138. package/dist/shared/state/index.d.cts +0 -2
  139. package/dist/shared/state/index.d.ts +0 -2
  140. package/dist/shared/state/index.js +0 -16
  141. package/dist/shared/state/session-state.cjs +0 -85
  142. package/dist/shared/state/session-state.d.cts +0 -34
  143. package/dist/shared/state/session-state.d.ts +0 -34
  144. package/dist/shared/state/session-state.js +0 -56
  145. package/dist/shared/visualization/ghost-cursor.cjs +0 -174
  146. package/dist/shared/visualization/ghost-cursor.d.cts +0 -37
  147. package/dist/shared/visualization/ghost-cursor.d.ts +0 -37
  148. package/dist/shared/visualization/ghost-cursor.js +0 -145
  149. package/dist/shared/visualization/highlight.cjs +0 -134
  150. package/dist/shared/visualization/highlight.d.cts +0 -22
  151. package/dist/shared/visualization/highlight.d.ts +0 -22
  152. package/dist/shared/visualization/highlight.js +0 -108
  153. package/dist/shared/visualization/index.cjs +0 -45
  154. package/dist/shared/visualization/index.d.cts +0 -3
  155. package/dist/shared/visualization/index.d.ts +0 -3
  156. package/dist/shared/visualization/index.js +0 -24
  157. package/dist/shared/workflow/workflow.cjs +0 -47
  158. package/dist/shared/workflow/workflow.d.cts +0 -21
  159. package/dist/shared/workflow/workflow.d.ts +0 -21
  160. package/dist/shared/workflow/workflow.js +0 -21
@@ -1,183 +0,0 @@
1
- import {
2
- existsSync,
3
- mkdirSync,
4
- readFileSync,
5
- readdirSync,
6
- unlinkSync,
7
- writeFileSync
8
- } from "node:fs";
9
- import {
10
- getSessionDir,
11
- getSessionLogsPath,
12
- getSessionStatePath,
13
- LIBRETTO_SESSIONS_DIR
14
- } from "./context.js";
15
- import {
16
- SESSION_STATE_VERSION,
17
- parseSessionStateContent,
18
- serializeSessionState
19
- } from "../../shared/state/index.js";
20
- const SESSION_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
21
- const SESSION_DEFAULT = "default";
22
- const SESSION_DEV_SERVER = "dev-server";
23
- const SESSION_BROWSER_AGENT = "browser-agent";
24
- function logFileForSession(session) {
25
- validateSessionName(session);
26
- const dir = getSessionDir(session);
27
- mkdirSync(dir, { recursive: true });
28
- return getSessionLogsPath(session);
29
- }
30
- function validateSessionName(session) {
31
- if (!SESSION_NAME_PATTERN.test(session) || session.includes("..") || session.includes("/") || session.includes("\\")) {
32
- throw new Error(
33
- "Invalid session name. Use only letters, numbers, dots, underscores, and dashes."
34
- );
35
- }
36
- }
37
- function getStateFilePath(session) {
38
- validateSessionName(session);
39
- const sessionDir = getSessionDir(session);
40
- mkdirSync(sessionDir, { recursive: true });
41
- return getSessionStatePath(session);
42
- }
43
- function readSessionState(session, logger) {
44
- const stateFile = getStateFilePath(session);
45
- if (!existsSync(stateFile)) {
46
- logger?.info("session-state-not-found", { session, stateFile });
47
- return null;
48
- }
49
- try {
50
- const content = readFileSync(stateFile, "utf-8");
51
- const state = parseSessionStateContent(content, stateFile);
52
- logger?.info("session-state-read", {
53
- session,
54
- port: state.port,
55
- pid: state.pid
56
- });
57
- return state;
58
- } catch (err) {
59
- logger?.warn("session-state-parse-error", {
60
- error: err instanceof Error ? err.message : String(err),
61
- session,
62
- stateFile
63
- });
64
- return null;
65
- }
66
- }
67
- function listSessionsWithStateFile() {
68
- if (!existsSync(LIBRETTO_SESSIONS_DIR)) return [];
69
- return readdirSync(LIBRETTO_SESSIONS_DIR).filter((session) => {
70
- try {
71
- validateSessionName(session);
72
- } catch {
73
- return false;
74
- }
75
- return existsSync(getSessionStatePath(session));
76
- }).sort();
77
- }
78
- function listActiveSessions() {
79
- return listSessionsWithStateFile();
80
- }
81
- function throwSessionNotFoundError(session) {
82
- const active = listActiveSessions();
83
- const lines = [`No session "${session}" found.`];
84
- if (active.length > 0) {
85
- lines.push("");
86
- lines.push("Active sessions:");
87
- for (const name of active) {
88
- lines.push(` ${name}`);
89
- }
90
- } else {
91
- lines.push("");
92
- lines.push("No active sessions.");
93
- }
94
- lines.push("");
95
- lines.push("Start one with:");
96
- lines.push(` libretto-cli open <url> --session ${session}`);
97
- throw new Error(lines.join("\n"));
98
- }
99
- function assertSessionStateExistsOrThrow(session) {
100
- const stateFile = getStateFilePath(session);
101
- if (!existsSync(stateFile)) {
102
- throwSessionNotFoundError(session);
103
- }
104
- }
105
- function readSessionStateOrThrow(session) {
106
- const stateFile = getStateFilePath(session);
107
- if (!existsSync(stateFile)) {
108
- throwSessionNotFoundError(session);
109
- }
110
- try {
111
- return parseSessionStateContent(readFileSync(stateFile, "utf-8"), stateFile);
112
- } catch (err) {
113
- throw new Error(
114
- `Could not read session state for "${session}": ${err instanceof Error ? err.message : String(err)}`
115
- );
116
- }
117
- }
118
- function writeSessionState(state, logger) {
119
- const stateFile = getStateFilePath(state.session);
120
- const fileState = serializeSessionState(state);
121
- writeFileSync(stateFile, JSON.stringify(fileState, null, 2), "utf-8");
122
- logger?.info("session-state-write", {
123
- session: state.session,
124
- stateFile,
125
- port: state.port,
126
- pid: state.pid
127
- });
128
- }
129
- function clearSessionState(session, logger) {
130
- const stateFile = getStateFilePath(session);
131
- if (!existsSync(stateFile)) {
132
- logger?.info("session-state-clear-missing", { session, stateFile });
133
- return;
134
- }
135
- unlinkSync(stateFile);
136
- logger?.info("session-state-cleared", { session, stateFile });
137
- }
138
- function isPidRunning(pid) {
139
- try {
140
- process.kill(pid, 0);
141
- return true;
142
- } catch {
143
- return false;
144
- }
145
- }
146
- function setSessionStatus(session, status, logger) {
147
- const state = readSessionState(session, logger);
148
- if (!state) return;
149
- if (state.status === status) return;
150
- writeSessionState({
151
- ...state,
152
- status
153
- }, logger);
154
- }
155
- function assertSessionAvailableForStart(session, logger) {
156
- const existingState = readSessionState(session, logger);
157
- if (!existingState) return;
158
- if (!isPidRunning(existingState.pid)) {
159
- setSessionStatus(session, "exited", logger);
160
- return;
161
- }
162
- const endpoint = `http://127.0.0.1:${existingState.port}`;
163
- throw new Error(
164
- `Session "${session}" is already open and connected to ${endpoint} (pid ${existingState.pid}). Create a new session or close the current one with: libretto-cli close --session ${session}`
165
- );
166
- }
167
- export {
168
- SESSION_BROWSER_AGENT,
169
- SESSION_DEFAULT,
170
- SESSION_DEV_SERVER,
171
- SESSION_STATE_VERSION,
172
- assertSessionAvailableForStart,
173
- assertSessionStateExistsOrThrow,
174
- clearSessionState,
175
- getStateFilePath,
176
- listSessionsWithStateFile,
177
- logFileForSession,
178
- readSessionState,
179
- readSessionStateOrThrow,
180
- setSessionStatus,
181
- validateSessionName,
182
- writeSessionState
183
- };
@@ -1,492 +0,0 @@
1
- import {
2
- existsSync,
3
- mkdtempSync,
4
- readFileSync,
5
- rmSync
6
- } from "node:fs";
7
- import { extname, isAbsolute, join, resolve } from "node:path";
8
- import { spawn } from "node:child_process";
9
- import { tmpdir } from "node:os";
10
- import { z } from "zod";
11
- import {
12
- formatCommandPrefix,
13
- readAiConfig
14
- } from "./ai-config.js";
15
- import {
16
- getLLMClientFactory
17
- } from "./context.js";
18
- const InterpretResultSchema = z.object({
19
- answer: z.string(),
20
- selectors: z.array(
21
- z.object({
22
- label: z.string(),
23
- selector: z.string(),
24
- rationale: z.string()
25
- })
26
- ).default([]),
27
- notes: z.string().optional().default("")
28
- });
29
- class UserCodingAgent {
30
- constructor(config) {
31
- this.config = config;
32
- }
33
- static resolveFromConfig(config) {
34
- switch (config.preset) {
35
- case "codex":
36
- return new CodexUserCodingAgent(config);
37
- case "claude":
38
- return new ClaudeUserCodingAgent(config);
39
- case "gemini":
40
- return new GeminiUserCodingAgent(config);
41
- }
42
- }
43
- static readConfiguredConfig() {
44
- return readAiConfig();
45
- }
46
- static getConfigured() {
47
- const config = this.readConfiguredConfig();
48
- return config ? this.resolveFromConfig(config) : null;
49
- }
50
- get snapshotAnalyzerConfig() {
51
- return this.config;
52
- }
53
- get command() {
54
- const command = this.config.commandPrefix[0];
55
- if (!command) {
56
- throw new Error("AI config is invalid: command prefix is empty.");
57
- }
58
- return command;
59
- }
60
- get baseArgs() {
61
- return this.config.commandPrefix.slice(1);
62
- }
63
- screenshotHint(pngPath) {
64
- return `
65
-
66
- Screenshot file path: ${pngPath}
67
- Use the screenshot alongside the HTML snapshot context above.`;
68
- }
69
- async runAnalyzer(args, stdinText) {
70
- const result = await runExternalCommand(this.command, args, stdinText);
71
- if (result.exitCode !== 0) {
72
- throw new Error(
73
- `Analyzer command failed (${formatCommandPrefix([this.command, ...args])}).
74
- ${stripAnsi(result.stderr).trim() || stripAnsi(result.stdout).trim() || "No error output."}`
75
- );
76
- }
77
- return result;
78
- }
79
- async runAndParse(args, stdinText) {
80
- const result = await this.runAnalyzer(args, stdinText);
81
- return parseInterpretResultFromText(result.stdout);
82
- }
83
- }
84
- class CodexUserCodingAgent extends UserCodingAgent {
85
- async analyzeSnapshot(prompt, pngPath) {
86
- const tempDir = mkdtempSync(join(tmpdir(), "libretto-cli-analyzer-"));
87
- const outputPath = join(
88
- tempDir,
89
- `snapshot-analyzer-${Date.now()}-${Math.random().toString(36).slice(2)}.json`
90
- );
91
- const args = [
92
- ...this.baseArgs,
93
- "--output-last-message",
94
- outputPath,
95
- "-i",
96
- pngPath,
97
- "-"
98
- ];
99
- const result = await this.runAnalyzer(args, prompt);
100
- let outputText = result.stdout;
101
- try {
102
- if (existsSync(outputPath)) {
103
- outputText = readFileSync(outputPath, "utf-8");
104
- }
105
- return parseInterpretResultFromText(outputText);
106
- } finally {
107
- rmSync(tempDir, { recursive: true, force: true });
108
- }
109
- }
110
- }
111
- class ClaudeUserCodingAgent extends UserCodingAgent {
112
- async analyzeSnapshot(prompt, pngPath) {
113
- const args = [...this.baseArgs, `${prompt}${this.screenshotHint(pngPath)}`];
114
- return await this.runAndParse(args);
115
- }
116
- }
117
- class GeminiUserCodingAgent extends UserCodingAgent {
118
- async analyzeSnapshot(prompt, pngPath) {
119
- const args = [...this.baseArgs, `${prompt}${this.screenshotHint(pngPath)}`];
120
- return await this.runAndParse(args);
121
- }
122
- }
123
- async function runExternalCommand(command, args, stdinText) {
124
- return await new Promise((resolve2, reject) => {
125
- const child = spawn(command, args, {
126
- stdio: ["pipe", "pipe", "pipe"]
127
- });
128
- let stdout = "";
129
- let stderr = "";
130
- child.stdout.on("data", (chunk) => {
131
- stdout += chunk.toString();
132
- });
133
- child.stderr.on("data", (chunk) => {
134
- stderr += chunk.toString();
135
- });
136
- child.on("error", (err) => {
137
- const error = err;
138
- if (error.code === "ENOENT") {
139
- reject(
140
- new Error(
141
- `Command not found: ${command}. Configure AI with 'libretto-cli ai configure'.`
142
- )
143
- );
144
- return;
145
- }
146
- reject(err);
147
- });
148
- child.on("close", (code) => {
149
- resolve2({
150
- exitCode: code ?? 1,
151
- stdout,
152
- stderr
153
- });
154
- });
155
- if (stdinText !== void 0) {
156
- child.stdin.write(stdinText);
157
- }
158
- child.stdin.end();
159
- });
160
- }
161
- function stripAnsi(value) {
162
- return value.replace(
163
- /\u001b\[[0-9;]*[A-Za-z]|\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g,
164
- ""
165
- );
166
- }
167
- function extractJsonObjectCandidates(text) {
168
- const candidates = [];
169
- const seen = /* @__PURE__ */ new Set();
170
- const add = (value) => {
171
- const trimmed = value.trim();
172
- if (!trimmed || seen.has(trimmed)) return;
173
- seen.add(trimmed);
174
- candidates.push(trimmed);
175
- };
176
- try {
177
- const direct = text.trim();
178
- if (direct.startsWith("{") && direct.endsWith("}")) {
179
- add(direct);
180
- }
181
- } catch {
182
- }
183
- const codeBlockRegex = /```(?:json)?\s*([\s\S]*?)```/gi;
184
- let codeBlockMatch;
185
- while ((codeBlockMatch = codeBlockRegex.exec(text)) !== null) {
186
- const body = codeBlockMatch[1]?.trim();
187
- if (body && body.startsWith("{") && body.endsWith("}")) {
188
- add(body);
189
- }
190
- }
191
- const lines = text.split("\n");
192
- for (const line of lines) {
193
- const trimmed = line.trim();
194
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
195
- add(trimmed);
196
- }
197
- }
198
- let depth = 0;
199
- let start = -1;
200
- let inString = false;
201
- let escaped = false;
202
- for (let i = 0; i < text.length; i++) {
203
- const char = text[i];
204
- if (inString) {
205
- if (escaped) {
206
- escaped = false;
207
- continue;
208
- }
209
- if (char === "\\") {
210
- escaped = true;
211
- continue;
212
- }
213
- if (char === '"') {
214
- inString = false;
215
- }
216
- continue;
217
- }
218
- if (char === '"') {
219
- inString = true;
220
- continue;
221
- }
222
- if (char === "{") {
223
- if (depth === 0) start = i;
224
- depth += 1;
225
- continue;
226
- }
227
- if (char === "}") {
228
- if (depth > 0) depth -= 1;
229
- if (depth === 0 && start >= 0) {
230
- add(text.slice(start, i + 1));
231
- start = -1;
232
- }
233
- }
234
- }
235
- return candidates;
236
- }
237
- function collectStringLeaves(value, out, depth = 0) {
238
- if (depth > 6 || value == null) return;
239
- if (typeof value === "string") {
240
- out.push(value);
241
- return;
242
- }
243
- if (Array.isArray(value)) {
244
- for (const item of value) {
245
- collectStringLeaves(item, out, depth + 1);
246
- }
247
- return;
248
- }
249
- if (typeof value === "object") {
250
- for (const nested of Object.values(value)) {
251
- collectStringLeaves(nested, out, depth + 1);
252
- }
253
- }
254
- }
255
- function parseInterpretResultFromText(text) {
256
- const cleaned = stripAnsi(text).trim();
257
- const candidates = extractJsonObjectCandidates(cleaned);
258
- if (candidates.length === 0) {
259
- throw new Error(
260
- "Analyzer output did not include a JSON object matching the interpret schema."
261
- );
262
- }
263
- for (const candidate of candidates) {
264
- try {
265
- const parsed = JSON.parse(candidate);
266
- const valid = InterpretResultSchema.safeParse(parsed);
267
- if (valid.success) {
268
- return valid.data;
269
- }
270
- const nestedStrings = [];
271
- collectStringLeaves(parsed, nestedStrings);
272
- for (const nestedText of nestedStrings) {
273
- const nestedCandidates = extractJsonObjectCandidates(nestedText);
274
- for (const nestedCandidate of nestedCandidates) {
275
- try {
276
- const nestedParsed = JSON.parse(nestedCandidate);
277
- const nestedValid = InterpretResultSchema.safeParse(nestedParsed);
278
- if (nestedValid.success) {
279
- return nestedValid.data;
280
- }
281
- } catch {
282
- }
283
- }
284
- }
285
- } catch {
286
- }
287
- }
288
- throw new Error(
289
- "Analyzer output could not be parsed as valid interpret JSON. Ensure the configured command returns only the requested JSON object."
290
- );
291
- }
292
- function resolvePath(filePath) {
293
- return isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
294
- }
295
- function getMimeType(filePath) {
296
- const ext = extname(filePath).toLowerCase();
297
- if (ext === ".png") return "image/png";
298
- if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
299
- if (ext === ".webp") return "image/webp";
300
- if (ext === ".gif") return "image/gif";
301
- return "application/octet-stream";
302
- }
303
- function readFileAsBase64(filePath) {
304
- return readFileSync(filePath).toString("base64");
305
- }
306
- function truncateText(text, maxChars) {
307
- if (text.length <= maxChars) {
308
- return { text, truncated: false };
309
- }
310
- const head = text.slice(0, Math.floor(maxChars * 0.6));
311
- const tail = text.slice(-Math.floor(maxChars * 0.4));
312
- return {
313
- text: `${head}
314
-
315
- ... [truncated] ...
316
-
317
- ${tail}`,
318
- truncated: true
319
- };
320
- }
321
- function collectSelectorHints(html, limit = 120) {
322
- const candidates = [];
323
- const seen = /* @__PURE__ */ new Set();
324
- const add = (value) => {
325
- if (candidates.length >= limit || seen.has(value)) return;
326
- seen.add(value);
327
- candidates.push(value);
328
- };
329
- const selectors = [
330
- { attr: "data-testid", format: (value) => `[data-testid="${value}"]` },
331
- { attr: "data-test", format: (value) => `[data-test="${value}"]` },
332
- { attr: "data-qa", format: (value) => `[data-qa="${value}"]` },
333
- { attr: "aria-label", format: (value) => `[aria-label="${value}"]` },
334
- { attr: "role", format: (value) => `[role="${value}"]` },
335
- { attr: "name", format: (value) => `[name="${value}"]` },
336
- { attr: "placeholder", format: (value) => `[placeholder="${value}"]` },
337
- { attr: "id", format: (value) => `#${value}` }
338
- ];
339
- for (const selector of selectors) {
340
- const regex = new RegExp(`${selector.attr}\\s*=\\s*["']([^"']+)["']`, "gi");
341
- let match;
342
- while ((match = regex.exec(html)) !== null) {
343
- const value = match[1]?.trim();
344
- if (!value) continue;
345
- add(selector.format(value));
346
- if (candidates.length >= limit) break;
347
- }
348
- if (candidates.length >= limit) break;
349
- }
350
- return candidates;
351
- }
352
- async function runInterpret(args, logger) {
353
- logger.info("interpret-start", {
354
- objective: args.objective,
355
- pngPath: args.pngPath,
356
- htmlPath: args.htmlPath
357
- });
358
- process.env.NODE_ENV = "development";
359
- const pngPath = resolvePath(args.pngPath);
360
- const htmlPath = resolvePath(args.htmlPath);
361
- if (!existsSync(pngPath)) {
362
- throw new Error(`PNG file not found: ${pngPath}`);
363
- }
364
- if (!existsSync(htmlPath)) {
365
- throw new Error(`HTML file not found: ${htmlPath}`);
366
- }
367
- const htmlContent = readFileSync(htmlPath, "utf-8");
368
- const htmlCharLimit = 5e5;
369
- const { text: trimmedHtml, truncated } = truncateText(
370
- htmlContent,
371
- htmlCharLimit
372
- );
373
- const selectorHints = collectSelectorHints(htmlContent, 120);
374
- let prompt = `# Objective
375
- ${args.objective}
376
-
377
- `;
378
- prompt += `# Context
379
- ${args.context}
380
-
381
- `;
382
- prompt += `# Instructions
383
- `;
384
- prompt += `You are analyzing a screenshot and HTML snapshot of the same web page on behalf of an automation agent.
385
- `;
386
- prompt += `The agent needs to interact with this page programmatically using Playwright.
387
-
388
- `;
389
- prompt += `Based on the objective and context above:
390
- `;
391
- prompt += `1. Answer the objective concisely
392
- `;
393
- prompt += `2. Identify ALL interactive elements relevant to the objective and provide Playwright-ready CSS selectors
394
- `;
395
- prompt += `3. Note any relevant page state (loading indicators, error messages, disabled elements, modals/overlays)
396
- `;
397
- prompt += `4. If elements are inside iframes, identify the iframe selector and the element selector within it
398
-
399
- `;
400
- prompt += `Output JSON with this shape:
401
- `;
402
- prompt += `{"answer": string, "selectors": [{"label": string, "selector": string, "rationale": string}], "notes": string}
403
-
404
- `;
405
- prompt += `Selectors should prefer robust attributes: data-testid, data-test, aria-label, name, id, role. Avoid fragile class-based or positional selectors.
406
- `;
407
- prompt += `Only include selectors that exist in the HTML snapshot.
408
-
409
- `;
410
- if (selectorHints.length > 0) {
411
- prompt += `Selector hints from HTML attributes (use if relevant):
412
- `;
413
- prompt += selectorHints.map((hint) => `- ${hint}`).join("\n");
414
- prompt += "\n\n";
415
- }
416
- if (truncated) {
417
- prompt += `HTML content is truncated to fit token limits.
418
-
419
- `;
420
- }
421
- prompt += `HTML snapshot:
422
-
423
- ${trimmedHtml}`;
424
- prompt += "\n\nReturn only a JSON object. Do not include markdown code fences or extra commentary.";
425
- let parsed;
426
- const configuredAgent = UserCodingAgent.getConfigured();
427
- if (configuredAgent) {
428
- const configuredAnalyzer = configuredAgent.snapshotAnalyzerConfig;
429
- logger.info("interpret-analyzer-config", {
430
- preset: configuredAnalyzer.preset,
431
- commandPrefix: configuredAnalyzer.commandPrefix
432
- });
433
- parsed = await configuredAgent.analyzeSnapshot(prompt, pngPath);
434
- } else {
435
- const llmClientFactory = getLLMClientFactory();
436
- if (!llmClientFactory) {
437
- throw new Error(
438
- "No AI config set. Run 'libretto-cli ai configure codex' (or claude/gemini). Library integrations can still set a factory via setLLMClientFactory()."
439
- );
440
- }
441
- logger.info("interpret-analyzer-factory-fallback", {});
442
- const imageBase64 = readFileAsBase64(pngPath);
443
- const client = await llmClientFactory(logger, "google/gemini-3-flash-preview");
444
- const result = await client.generateObjectFromMessages({
445
- schema: InterpretResultSchema,
446
- messages: [
447
- {
448
- role: "user",
449
- content: [
450
- { type: "text", text: prompt },
451
- {
452
- type: "image",
453
- image: `data:${getMimeType(pngPath)};base64,${imageBase64}`
454
- }
455
- ]
456
- }
457
- ],
458
- temperature: 0.1
459
- });
460
- parsed = InterpretResultSchema.parse(result);
461
- }
462
- logger.info("interpret-success", {
463
- selectorCount: parsed.selectors.length,
464
- answer: parsed.answer.slice(0, 200)
465
- });
466
- const outputLines = [];
467
- outputLines.push("Interpretation:");
468
- outputLines.push(`Answer: ${parsed.answer}`);
469
- outputLines.push("");
470
- if (parsed.selectors.length === 0) {
471
- outputLines.push("Selectors: none found.");
472
- } else {
473
- outputLines.push("Selectors:");
474
- parsed.selectors.forEach((selector, index) => {
475
- outputLines.push(` ${index + 1}. ${selector.label}`);
476
- outputLines.push(` selector: ${selector.selector}`);
477
- outputLines.push(` rationale: ${selector.rationale}`);
478
- });
479
- }
480
- if (parsed.notes.trim()) {
481
- outputLines.push("");
482
- outputLines.push(`Notes: ${parsed.notes.trim()}`);
483
- }
484
- console.log(outputLines.join("\n"));
485
- }
486
- function canAnalyzeSnapshots() {
487
- return UserCodingAgent.getConfigured() !== null || getLLMClientFactory() !== null;
488
- }
489
- export {
490
- canAnalyzeSnapshots,
491
- runInterpret
492
- };