libretto 0.5.0 → 0.5.1
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 +106 -36
- 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 +30 -8
- 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 +9 -2
- package/dist/cli/core/api-snapshot-analyzer.js +15 -5
- package/dist/cli/core/browser.js +132 -29
- package/dist/cli/core/context.js +4 -1
- package/dist/cli/core/session-telemetry.js +5 -2
- 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 +10 -2
- 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 +6 -13
- 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/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 +7 -2
- 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 +19 -10
- 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 +113 -49
- package/skills/libretto/references/code-generation-rules.md +208 -0
- package/skills/libretto/references/configuration-file-reference.md +53 -0
- 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 +32 -9
- 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 +12 -3
- package/src/cli/core/api-snapshot-analyzer.ts +17 -6
- package/src/cli/core/browser.ts +178 -41
- package/src/cli/core/context.ts +7 -2
- package/src/cli/core/session-telemetry.ts +19 -8
- 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 +16 -3
- 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 +15 -18
- 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/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 +12 -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 +161 -148
- 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
|
@@ -5,16 +5,10 @@ import { getSessionSnapshotRunDir } from "../core/context.js";
|
|
|
5
5
|
import { condenseDom } from "../../shared/condense-dom/condense-dom.js";
|
|
6
6
|
import { readSessionState } from "../core/session.js";
|
|
7
7
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
8
|
-
import {
|
|
9
|
-
loadSessionStateMiddleware,
|
|
10
|
-
pageOption,
|
|
11
|
-
resolveSessionMiddleware,
|
|
12
|
-
sessionOption
|
|
13
|
-
} from "./shared.js";
|
|
8
|
+
import { pageOption, sessionOption, withRequiredSession } from "./shared.js";
|
|
14
9
|
import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
|
|
15
10
|
import { readAiConfig } from "../core/ai-config.js";
|
|
16
11
|
import { resolveSnapshotApiModelOrThrow } from "../core/snapshot-api-config.js";
|
|
17
|
-
const DEFAULT_SNAPSHOT_CONTEXT = "No additional user context provided.";
|
|
18
12
|
const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 };
|
|
19
13
|
function generateSnapshotRunId() {
|
|
20
14
|
return `snapshot-${Date.now()}`;
|
|
@@ -106,6 +100,12 @@ async function captureScreenshot(session, logger, pageId) {
|
|
|
106
100
|
const pngPath = `${snapshotRunDir}/page.png`;
|
|
107
101
|
const htmlPath = `${snapshotRunDir}/page.html`;
|
|
108
102
|
const condensedHtmlPath = `${snapshotRunDir}/page.condensed.html`;
|
|
103
|
+
const RENDER_SETTLE_TIMEOUT_MS = 1e4;
|
|
104
|
+
await Promise.race([
|
|
105
|
+
page.waitForLoadState("networkidle").catch(() => {
|
|
106
|
+
}),
|
|
107
|
+
new Promise((resolve) => setTimeout(resolve, RENDER_SETTLE_TIMEOUT_MS))
|
|
108
|
+
]);
|
|
109
109
|
const restoreViewport = resolveSnapshotViewport(session, logger);
|
|
110
110
|
const viewportMetrics = await readSnapshotViewportMetrics(page);
|
|
111
111
|
logger.info("screenshot-viewport-metrics", {
|
|
@@ -185,17 +185,10 @@ async function captureScreenshot(session, logger, pageId) {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
async function runSnapshot(session, logger, pageId, objective, context) {
|
|
188
|
-
const normalizedObjective = objective
|
|
189
|
-
const normalizedContext = context
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
"Couldn't run analysis: --objective is required when providing --context."
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
const configuredAi = normalizedObjective ? readAiConfig() : null;
|
|
196
|
-
if (normalizedObjective) {
|
|
197
|
-
resolveSnapshotApiModelOrThrow(configuredAi);
|
|
198
|
-
}
|
|
188
|
+
const normalizedObjective = objective.trim();
|
|
189
|
+
const normalizedContext = context.trim();
|
|
190
|
+
const configuredAi = readAiConfig();
|
|
191
|
+
resolveSnapshotApiModelOrThrow(configuredAi);
|
|
199
192
|
const { pngPath, htmlPath, condensedHtmlPath } = await captureScreenshot(
|
|
200
193
|
session,
|
|
201
194
|
logger,
|
|
@@ -205,14 +198,10 @@ async function runSnapshot(session, logger, pageId, objective, context) {
|
|
|
205
198
|
console.log(` PNG: ${pngPath}`);
|
|
206
199
|
console.log(` HTML: ${htmlPath}`);
|
|
207
200
|
console.log(` Condensed HTML: ${condensedHtmlPath}`);
|
|
208
|
-
if (!normalizedObjective) {
|
|
209
|
-
console.log("Use --objective flag to analyze snapshots.");
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
201
|
const interpretArgs = {
|
|
213
202
|
objective: normalizedObjective,
|
|
214
203
|
session,
|
|
215
|
-
context: normalizedContext
|
|
204
|
+
context: normalizedContext,
|
|
216
205
|
pngPath,
|
|
217
206
|
htmlPath,
|
|
218
207
|
condensedHtmlPath
|
|
@@ -224,24 +213,22 @@ const snapshotInput = SimpleCLI.input({
|
|
|
224
213
|
named: {
|
|
225
214
|
session: sessionOption(),
|
|
226
215
|
page: pageOption(),
|
|
227
|
-
objective: SimpleCLI.option(z.string()
|
|
228
|
-
context: SimpleCLI.option(z.string()
|
|
216
|
+
objective: SimpleCLI.option(z.string()),
|
|
217
|
+
context: SimpleCLI.option(z.string())
|
|
229
218
|
}
|
|
230
219
|
});
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
});
|
|
243
|
-
}
|
|
220
|
+
const snapshotCommand = SimpleCLI.command({
|
|
221
|
+
description: "Capture PNG + HTML and analyze with --objective and --context"
|
|
222
|
+
}).input(snapshotInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
|
|
223
|
+
await runSnapshot(
|
|
224
|
+
ctx.session,
|
|
225
|
+
ctx.logger,
|
|
226
|
+
input.page,
|
|
227
|
+
input.objective,
|
|
228
|
+
input.context
|
|
229
|
+
);
|
|
230
|
+
});
|
|
244
231
|
export {
|
|
245
|
-
|
|
232
|
+
snapshotCommand,
|
|
246
233
|
snapshotInput
|
|
247
234
|
};
|
|
@@ -26,7 +26,12 @@ const PROVIDER_ALIASES = {
|
|
|
26
26
|
claude: DEFAULT_MODELS.anthropic,
|
|
27
27
|
google: DEFAULT_MODELS.gemini
|
|
28
28
|
};
|
|
29
|
-
const CONFIGURE_PROVIDERS = [
|
|
29
|
+
const CONFIGURE_PROVIDERS = [
|
|
30
|
+
"openai",
|
|
31
|
+
"anthropic",
|
|
32
|
+
"gemini",
|
|
33
|
+
"vertex"
|
|
34
|
+
];
|
|
30
35
|
function formatConfigureProviders(separator = " | ") {
|
|
31
36
|
return CONFIGURE_PROVIDERS.join(separator);
|
|
32
37
|
}
|
|
@@ -147,7 +152,9 @@ function runAiConfigure(input, options = {}) {
|
|
|
147
152
|
console.log(
|
|
148
153
|
`No AI config set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`
|
|
149
154
|
);
|
|
150
|
-
console.log(
|
|
155
|
+
console.log(
|
|
156
|
+
"Provider credentials still come from your shell or .env file."
|
|
157
|
+
);
|
|
151
158
|
return;
|
|
152
159
|
}
|
|
153
160
|
printAiConfig(config2, configPath);
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { createLLMClient } from "../../shared/llm/client.js";
|
|
3
3
|
import {
|
|
4
|
-
formatInterpretationOutput,
|
|
5
4
|
InterpretResultSchema,
|
|
6
5
|
buildInlinePromptSelection,
|
|
7
6
|
getMimeType,
|
|
8
7
|
readFileAsBase64
|
|
9
8
|
} from "./snapshot-analyzer.js";
|
|
10
9
|
import { readAiConfig } from "./ai-config.js";
|
|
11
|
-
import {
|
|
12
|
-
resolveSnapshotApiModelOrThrow
|
|
13
|
-
} from "./snapshot-api-config.js";
|
|
10
|
+
import { resolveSnapshotApiModelOrThrow } from "./snapshot-api-config.js";
|
|
14
11
|
async function runApiInterpret(args, logger, configuredAi = readAiConfig()) {
|
|
15
12
|
const selection = resolveSnapshotApiModelOrThrow(configuredAi);
|
|
16
13
|
logger.info("api-interpret-start", {
|
|
@@ -67,7 +64,20 @@ async function runApiInterpret(args, logger, configuredAi = readAiConfig()) {
|
|
|
67
64
|
selectorCount: parsed.selectors.length,
|
|
68
65
|
answer: parsed.answer.slice(0, 200)
|
|
69
66
|
});
|
|
70
|
-
console.log(
|
|
67
|
+
console.log("");
|
|
68
|
+
console.log("Analysis:");
|
|
69
|
+
console.log(parsed.answer);
|
|
70
|
+
if (parsed.selectors.length > 0) {
|
|
71
|
+
console.log("");
|
|
72
|
+
console.log("Selectors:");
|
|
73
|
+
parsed.selectors.forEach((selector, index) => {
|
|
74
|
+
console.log(` ${index + 1}. ${selector.label}: ${selector.selector}`);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (parsed.notes?.trim()) {
|
|
78
|
+
console.log("");
|
|
79
|
+
console.log(`Notes: ${parsed.notes.trim()}`);
|
|
80
|
+
}
|
|
71
81
|
}
|
|
72
82
|
export {
|
|
73
83
|
runApiInterpret
|
package/dist/cli/core/browser.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
chromium
|
|
3
|
+
} from "playwright";
|
|
2
4
|
import { openSync, existsSync } from "node:fs";
|
|
3
5
|
import { dirname, join, resolve } from "node:path";
|
|
4
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -57,9 +59,8 @@ function getProfilePath(domain) {
|
|
|
57
59
|
function hasProfile(domain) {
|
|
58
60
|
return existsSync(getProfilePath(domain));
|
|
59
61
|
}
|
|
60
|
-
async function
|
|
61
|
-
|
|
62
|
-
logger.info("cdp-connect-attempt", { port, endpoint, timeoutMs });
|
|
62
|
+
async function tryConnectToCDP(endpoint, logger, timeoutMs = 5e3) {
|
|
63
|
+
logger.info("cdp-connect-attempt", { endpoint, timeoutMs });
|
|
63
64
|
try {
|
|
64
65
|
const connectPromise = chromium.connectOverCDP(endpoint);
|
|
65
66
|
const timeoutPromise = new Promise(
|
|
@@ -68,16 +69,15 @@ async function tryConnectToPort(port, logger, timeoutMs = 5e3) {
|
|
|
68
69
|
const browser = await Promise.race([connectPromise, timeoutPromise]);
|
|
69
70
|
if (browser) {
|
|
70
71
|
logger.info("cdp-connect-success", {
|
|
71
|
-
port,
|
|
72
72
|
endpoint,
|
|
73
73
|
contexts: browser.contexts().length
|
|
74
74
|
});
|
|
75
75
|
} else {
|
|
76
|
-
logger.warn("cdp-connect-timeout", {
|
|
76
|
+
logger.warn("cdp-connect-timeout", { endpoint, timeoutMs });
|
|
77
77
|
}
|
|
78
78
|
return browser;
|
|
79
79
|
} catch (err) {
|
|
80
|
-
logger.error("cdp-connect-error", { error: err,
|
|
80
|
+
logger.error("cdp-connect-error", { error: err, endpoint });
|
|
81
81
|
return null;
|
|
82
82
|
}
|
|
83
83
|
}
|
|
@@ -102,7 +102,9 @@ async function resolvePageId(page) {
|
|
|
102
102
|
const targetInfo = await cdpSession.send("Target.getTargetInfo");
|
|
103
103
|
const targetId = targetInfo?.targetInfo?.targetId;
|
|
104
104
|
if (typeof targetId !== "string" || targetId.length === 0) {
|
|
105
|
-
throw new Error(
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Could not resolve target id for page at URL "${page.url()}".`
|
|
107
|
+
);
|
|
106
108
|
}
|
|
107
109
|
return targetId;
|
|
108
110
|
} finally {
|
|
@@ -135,21 +137,22 @@ async function listOpenPages(session, logger) {
|
|
|
135
137
|
async function connect(session, logger, timeoutMs = 1e4, options) {
|
|
136
138
|
logger.info("connect", { session, timeoutMs });
|
|
137
139
|
const state = readSessionStateOrThrow(session);
|
|
138
|
-
const
|
|
140
|
+
const endpoint = state.cdpEndpoint ?? `http://localhost:${state.port}`;
|
|
141
|
+
const browser = await tryConnectToCDP(endpoint, logger, timeoutMs);
|
|
139
142
|
if (!browser) {
|
|
140
143
|
logger.error("connect-no-browser", {
|
|
141
144
|
session,
|
|
142
|
-
|
|
145
|
+
endpoint,
|
|
143
146
|
pid: state.pid
|
|
144
147
|
});
|
|
145
|
-
if (!isPidRunning(state.pid)) {
|
|
148
|
+
if (state.pid == null || !isPidRunning(state.pid)) {
|
|
146
149
|
clearSessionState(session, logger);
|
|
147
150
|
throw new Error(
|
|
148
151
|
`No browser running for session "${session}". Run 'libretto open <url> --session ${session}' first.`
|
|
149
152
|
);
|
|
150
153
|
}
|
|
151
154
|
throw new Error(
|
|
152
|
-
`Could not connect to the browser for session "${session}" at
|
|
155
|
+
`Could not connect to the browser for session "${session}" at ${endpoint}, but the session process (pid ${state.pid}) is still running. Try the command again, or close and reopen the session if it stays stuck.`
|
|
153
156
|
);
|
|
154
157
|
}
|
|
155
158
|
const contexts = browser.contexts();
|
|
@@ -230,10 +233,16 @@ function resolveViewport(cliViewport, logger) {
|
|
|
230
233
|
}
|
|
231
234
|
const config = readLibrettoConfig();
|
|
232
235
|
if (config.viewport) {
|
|
233
|
-
logger.info("viewport-source", {
|
|
236
|
+
logger.info("viewport-source", {
|
|
237
|
+
source: "config",
|
|
238
|
+
viewport: config.viewport
|
|
239
|
+
});
|
|
234
240
|
return config.viewport;
|
|
235
241
|
}
|
|
236
|
-
logger.info("viewport-source", {
|
|
242
|
+
logger.info("viewport-source", {
|
|
243
|
+
source: "default",
|
|
244
|
+
viewport: DEFAULT_VIEWPORT
|
|
245
|
+
});
|
|
237
246
|
return DEFAULT_VIEWPORT;
|
|
238
247
|
}
|
|
239
248
|
async function runOpen(rawUrl, headed, session, logger, options) {
|
|
@@ -414,14 +423,17 @@ await new Promise(() => {});
|
|
|
414
423
|
logger.info("open-waiting-for-cdp", { attempt: i, port, session });
|
|
415
424
|
}
|
|
416
425
|
if (ready) {
|
|
417
|
-
writeSessionState(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
426
|
+
writeSessionState(
|
|
427
|
+
{
|
|
428
|
+
port,
|
|
429
|
+
pid: child.pid,
|
|
430
|
+
session,
|
|
431
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
432
|
+
status: "active",
|
|
433
|
+
viewport
|
|
434
|
+
},
|
|
435
|
+
logger
|
|
436
|
+
);
|
|
425
437
|
logger.info("open-success", {
|
|
426
438
|
url,
|
|
427
439
|
mode: browserMode,
|
|
@@ -517,8 +529,10 @@ async function runClose(session, logger) {
|
|
|
517
529
|
return;
|
|
518
530
|
}
|
|
519
531
|
logger.info("close-killing", { session, pid: state.pid, port: state.port });
|
|
520
|
-
|
|
521
|
-
|
|
532
|
+
if (state.pid != null) {
|
|
533
|
+
sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
|
|
534
|
+
await waitForCloseSignalWindow(CLOSE_WAIT_MS);
|
|
535
|
+
}
|
|
522
536
|
clearSessionState(session, logger);
|
|
523
537
|
logger.info("close-success", { session });
|
|
524
538
|
console.log(`Browser closed (session: ${session}).`);
|
|
@@ -575,7 +589,7 @@ function resolveClosableSessions(logger) {
|
|
|
575
589
|
function clearStoppedSessionStates(sessions, logger) {
|
|
576
590
|
let cleared = 0;
|
|
577
591
|
for (const session of sessions) {
|
|
578
|
-
if (!isPidRunning(session.pid)) {
|
|
592
|
+
if (session.pid == null || !isPidRunning(session.pid)) {
|
|
579
593
|
clearSessionState(session.session, logger);
|
|
580
594
|
cleared += 1;
|
|
581
595
|
}
|
|
@@ -601,10 +615,19 @@ async function runCloseAll(logger, options) {
|
|
|
601
615
|
pid: target.pid,
|
|
602
616
|
port: target.port
|
|
603
617
|
});
|
|
604
|
-
|
|
618
|
+
if (target.pid != null) {
|
|
619
|
+
sendSignalToProcessGroupOrPid(
|
|
620
|
+
target.pid,
|
|
621
|
+
"SIGTERM",
|
|
622
|
+
logger,
|
|
623
|
+
target.session
|
|
624
|
+
);
|
|
625
|
+
}
|
|
605
626
|
}
|
|
606
627
|
await waitForCloseSignalWindow(CLOSE_WAIT_MS);
|
|
607
|
-
let survivors = closable.filter(
|
|
628
|
+
let survivors = closable.filter(
|
|
629
|
+
(target) => target.pid != null && isPidRunning(target.pid)
|
|
630
|
+
);
|
|
608
631
|
if (survivors.length > 0 && !force) {
|
|
609
632
|
const closed = clearStoppedSessionStates(closable, logger);
|
|
610
633
|
throw new Error(
|
|
@@ -622,11 +645,20 @@ async function runCloseAll(logger, options) {
|
|
|
622
645
|
session: survivor.session,
|
|
623
646
|
pid: survivor.pid
|
|
624
647
|
});
|
|
625
|
-
|
|
648
|
+
if (survivor.pid != null) {
|
|
649
|
+
sendSignalToProcessGroupOrPid(
|
|
650
|
+
survivor.pid,
|
|
651
|
+
"SIGKILL",
|
|
652
|
+
logger,
|
|
653
|
+
survivor.session
|
|
654
|
+
);
|
|
655
|
+
}
|
|
626
656
|
forceKilled += 1;
|
|
627
657
|
}
|
|
628
658
|
await waitForCloseSignalWindow(FORCE_CLOSE_WAIT_MS);
|
|
629
|
-
survivors = survivors.filter(
|
|
659
|
+
survivors = survivors.filter(
|
|
660
|
+
(target) => target.pid != null && isPidRunning(target.pid)
|
|
661
|
+
);
|
|
630
662
|
if (survivors.length > 0) {
|
|
631
663
|
const closed = clearStoppedSessionStates(closable, logger);
|
|
632
664
|
throw new Error(
|
|
@@ -648,6 +680,75 @@ async function runCloseAll(logger, options) {
|
|
|
648
680
|
console.log(`Force-killed ${forceKilled} session(s).`);
|
|
649
681
|
}
|
|
650
682
|
}
|
|
683
|
+
async function runConnect(cdpUrl, session, logger) {
|
|
684
|
+
logger.info("connect-start", { cdpUrl, session });
|
|
685
|
+
assertSessionAvailableForStart(session, logger);
|
|
686
|
+
let parsedUrl;
|
|
687
|
+
try {
|
|
688
|
+
parsedUrl = new URL(cdpUrl);
|
|
689
|
+
} catch {
|
|
690
|
+
throw new Error(
|
|
691
|
+
[
|
|
692
|
+
`Invalid CDP URL: ${cdpUrl}`,
|
|
693
|
+
``,
|
|
694
|
+
`Expected an HTTP URL pointing to a Chrome DevTools Protocol endpoint, for example:`,
|
|
695
|
+
` libretto connect http://127.0.0.1:9222`,
|
|
696
|
+
` libretto connect http://remote-host:9222`,
|
|
697
|
+
` libretto connect http://remote-host:9222/devtools/browser/<id>`
|
|
698
|
+
].join("\n")
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
const endpoint = parsedUrl.href;
|
|
702
|
+
const port = parsedUrl.port ? Number(parsedUrl.port) : parsedUrl.protocol === "https:" ? 443 : 80;
|
|
703
|
+
console.log(
|
|
704
|
+
`Connecting to CDP endpoint at ${endpoint} (session: ${session})...`
|
|
705
|
+
);
|
|
706
|
+
const versionUrl = `${parsedUrl.protocol}//${parsedUrl.host}/json/version`;
|
|
707
|
+
try {
|
|
708
|
+
const resp = await fetch(versionUrl);
|
|
709
|
+
const versionInfo = await resp.json();
|
|
710
|
+
logger.info("connect-version-ok", { versionUrl, versionInfo });
|
|
711
|
+
} catch (err) {
|
|
712
|
+
logger.error("connect-version-failed", { versionUrl, error: err });
|
|
713
|
+
throw new Error(
|
|
714
|
+
`Cannot reach CDP endpoint at ${versionUrl}. Make sure the target is running and accessible at ${parsedUrl.host}.`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
const browser = await tryConnectToCDP(endpoint, logger, 1e4);
|
|
718
|
+
if (!browser) {
|
|
719
|
+
throw new Error(
|
|
720
|
+
`CDP endpoint at ${endpoint} is reachable but Playwright could not connect. Check that the URL is a Chrome DevTools Protocol endpoint.`
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
const pages = resolveOperationalPages(browser);
|
|
724
|
+
logger.info("connect-pages", {
|
|
725
|
+
session,
|
|
726
|
+
pageCount: pages.length,
|
|
727
|
+
urls: pages.map((p) => p.url())
|
|
728
|
+
});
|
|
729
|
+
disconnectBrowser(browser, logger, session);
|
|
730
|
+
writeSessionState(
|
|
731
|
+
{
|
|
732
|
+
port,
|
|
733
|
+
cdpEndpoint: endpoint,
|
|
734
|
+
session,
|
|
735
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
736
|
+
status: "active"
|
|
737
|
+
},
|
|
738
|
+
logger
|
|
739
|
+
);
|
|
740
|
+
logger.info("connect-success", { cdpUrl: endpoint, session, port });
|
|
741
|
+
console.log(`Connected to ${endpoint} (session: ${session})`);
|
|
742
|
+
console.log(` Pages found: ${pages.length}`);
|
|
743
|
+
if (pages.length > 0) {
|
|
744
|
+
for (const p of pages.slice(0, 5)) {
|
|
745
|
+
console.log(` ${p.url()}`);
|
|
746
|
+
}
|
|
747
|
+
if (pages.length > 5) {
|
|
748
|
+
console.log(` ... and ${pages.length - 5} more`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
651
752
|
function resolvePath(filePath) {
|
|
652
753
|
return join(process.cwd(), filePath);
|
|
653
754
|
}
|
|
@@ -666,8 +767,10 @@ export {
|
|
|
666
767
|
normalizeDomain,
|
|
667
768
|
normalizeUrl,
|
|
668
769
|
resolvePath,
|
|
770
|
+
resolveViewport,
|
|
669
771
|
runClose,
|
|
670
772
|
runCloseAll,
|
|
773
|
+
runConnect,
|
|
671
774
|
runOpen,
|
|
672
775
|
runPages,
|
|
673
776
|
runSave
|
package/dist/cli/core/context.js
CHANGED
|
@@ -49,7 +49,10 @@ function createLoggerForSession(session) {
|
|
|
49
49
|
const sessionDir = getSessionDir(session);
|
|
50
50
|
mkdirSync(sessionDir, { recursive: true });
|
|
51
51
|
const logFilePath = getSessionLogsPath(session);
|
|
52
|
-
return new Logger(
|
|
52
|
+
return new Logger(
|
|
53
|
+
["libretto"],
|
|
54
|
+
[createFileLogSink({ filePath: logFilePath })]
|
|
55
|
+
);
|
|
53
56
|
}
|
|
54
57
|
async function closeLogger(logger) {
|
|
55
58
|
if (!logger) return;
|
|
@@ -12,7 +12,9 @@ async function installSessionTelemetry(options) {
|
|
|
12
12
|
const targetInfo = await cdpSession.send("Target.getTargetInfo");
|
|
13
13
|
const targetId = targetInfo?.targetInfo?.targetId;
|
|
14
14
|
if (typeof targetId !== "string" || targetId.length === 0) {
|
|
15
|
-
throw new Error(
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Could not resolve target id for page at URL "${page.url()}".`
|
|
17
|
+
);
|
|
16
18
|
}
|
|
17
19
|
pageIdCache.set(page, targetId);
|
|
18
20
|
return targetId;
|
|
@@ -439,7 +441,8 @@ async function installSessionTelemetry(options) {
|
|
|
439
441
|
page.on("response", async (response) => {
|
|
440
442
|
const request = response.request();
|
|
441
443
|
const url = request.url();
|
|
442
|
-
if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://"))
|
|
444
|
+
if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://"))
|
|
445
|
+
return;
|
|
443
446
|
emitNetwork({
|
|
444
447
|
pageId,
|
|
445
448
|
method: request.method(),
|
package/dist/cli/core/session.js
CHANGED
|
@@ -18,9 +18,16 @@ import {
|
|
|
18
18
|
serializeSessionState
|
|
19
19
|
} from "../../shared/state/index.js";
|
|
20
20
|
const SESSION_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
21
|
-
const SESSION_DEFAULT = "default";
|
|
22
21
|
const SESSION_DEV_SERVER = "dev-server";
|
|
23
22
|
const SESSION_BROWSER_AGENT = "browser-agent";
|
|
23
|
+
function generateSessionName() {
|
|
24
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
25
|
+
let id = "";
|
|
26
|
+
for (let i = 0; i < 4; i++) {
|
|
27
|
+
id += chars[Math.floor(Math.random() * chars.length)];
|
|
28
|
+
}
|
|
29
|
+
return `ses-${id}`;
|
|
30
|
+
}
|
|
24
31
|
function logFileForSession(session) {
|
|
25
32
|
validateSessionName(session);
|
|
26
33
|
const dir = getSessionDir(session);
|
|
@@ -108,7 +115,10 @@ function readSessionStateOrThrow(session) {
|
|
|
108
115
|
throwSessionNotFoundError(session);
|
|
109
116
|
}
|
|
110
117
|
try {
|
|
111
|
-
return parseSessionStateContent(
|
|
118
|
+
return parseSessionStateContent(
|
|
119
|
+
readFileSync(stateFile, "utf-8"),
|
|
120
|
+
stateFile
|
|
121
|
+
);
|
|
112
122
|
} catch (err) {
|
|
113
123
|
throw new Error(
|
|
114
124
|
`Could not read session state for "${session}": ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -147,15 +157,18 @@ function setSessionStatus(session, status, logger) {
|
|
|
147
157
|
const state = readSessionState(session, logger);
|
|
148
158
|
if (!state) return;
|
|
149
159
|
if (state.status === status) return;
|
|
150
|
-
writeSessionState(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
160
|
+
writeSessionState(
|
|
161
|
+
{
|
|
162
|
+
...state,
|
|
163
|
+
status
|
|
164
|
+
},
|
|
165
|
+
logger
|
|
166
|
+
);
|
|
154
167
|
}
|
|
155
168
|
function assertSessionAvailableForStart(session, logger) {
|
|
156
169
|
const existingState = readSessionState(session, logger);
|
|
157
170
|
if (!existingState) return;
|
|
158
|
-
if (!isPidRunning(existingState.pid)) {
|
|
171
|
+
if (existingState.pid == null || !isPidRunning(existingState.pid)) {
|
|
159
172
|
setSessionStatus(session, "exited", logger);
|
|
160
173
|
return;
|
|
161
174
|
}
|
|
@@ -166,12 +179,12 @@ function assertSessionAvailableForStart(session, logger) {
|
|
|
166
179
|
}
|
|
167
180
|
export {
|
|
168
181
|
SESSION_BROWSER_AGENT,
|
|
169
|
-
SESSION_DEFAULT,
|
|
170
182
|
SESSION_DEV_SERVER,
|
|
171
183
|
SESSION_STATE_VERSION,
|
|
172
184
|
assertSessionAvailableForStart,
|
|
173
185
|
assertSessionStateExistsOrThrow,
|
|
174
186
|
clearSessionState,
|
|
187
|
+
generateSessionName,
|
|
175
188
|
getStateFilePath,
|
|
176
189
|
listSessionsWithStateFile,
|
|
177
190
|
logFileForSession,
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
existsSync,
|
|
3
|
-
mkdtempSync,
|
|
4
|
-
readFileSync,
|
|
5
|
-
rmSync
|
|
6
|
-
} from "node:fs";
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
7
2
|
import { extname, isAbsolute, join, resolve } from "node:path";
|
|
8
3
|
import { spawn } from "node:child_process";
|
|
9
4
|
import { tmpdir } from "node:os";
|
|
@@ -60,7 +55,12 @@ Screenshot file path: ${pngPath}
|
|
|
60
55
|
Use the screenshot alongside the HTML snapshot context above.`;
|
|
61
56
|
}
|
|
62
57
|
async runAnalyzer(args, logger, stdinText) {
|
|
63
|
-
const result = await runExternalCommand(
|
|
58
|
+
const result = await runExternalCommand(
|
|
59
|
+
this.command,
|
|
60
|
+
args,
|
|
61
|
+
logger,
|
|
62
|
+
stdinText
|
|
63
|
+
);
|
|
64
64
|
if (result.exitCode !== 0) {
|
|
65
65
|
throw new Error(
|
|
66
66
|
`Analyzer command failed (${[this.command, ...args].join(" ")}).
|
|
@@ -535,7 +535,9 @@ function buildInlinePromptSelection(args, fullHtmlContent, condensedHtmlContent,
|
|
|
535
535
|
fullDomChars: fullHtmlContent.length,
|
|
536
536
|
fullDomEstimatedTokens: estimateTokensFromChars(fullHtmlContent.length),
|
|
537
537
|
condensedDomChars: condensedHtmlContent.length,
|
|
538
|
-
condensedDomEstimatedTokens: estimateTokensFromChars(
|
|
538
|
+
condensedDomEstimatedTokens: estimateTokensFromChars(
|
|
539
|
+
condensedHtmlContent.length
|
|
540
|
+
),
|
|
539
541
|
configuredModel: model
|
|
540
542
|
};
|
|
541
543
|
const buildCandidate = (domSource, htmlContent, selectionReason, truncated) => {
|
|
@@ -607,7 +609,10 @@ function buildInlinePromptSelection(args, fullHtmlContent, condensedHtmlContent,
|
|
|
607
609
|
2e3,
|
|
608
610
|
budget.promptBudgetTokens - estimateTokensFromChars(basePrompt.length)
|
|
609
611
|
);
|
|
610
|
-
const truncatedHtml = truncateText(
|
|
612
|
+
const truncatedHtml = truncateText(
|
|
613
|
+
condensedHtmlContent,
|
|
614
|
+
availableHtmlTokens * 4
|
|
615
|
+
);
|
|
611
616
|
return buildCandidate(
|
|
612
617
|
"condensed",
|
|
613
618
|
truncatedHtml.text,
|
|
@@ -615,27 +620,6 @@ function buildInlinePromptSelection(args, fullHtmlContent, condensedHtmlContent,
|
|
|
615
620
|
truncatedHtml.truncated
|
|
616
621
|
);
|
|
617
622
|
}
|
|
618
|
-
function formatInterpretationOutput(parsed, header = "Interpretation:") {
|
|
619
|
-
const outputLines = [];
|
|
620
|
-
outputLines.push(header);
|
|
621
|
-
outputLines.push(`Answer: ${parsed.answer}`);
|
|
622
|
-
outputLines.push("");
|
|
623
|
-
if (parsed.selectors.length === 0) {
|
|
624
|
-
outputLines.push("Selectors: none found.");
|
|
625
|
-
} else {
|
|
626
|
-
outputLines.push("Selectors:");
|
|
627
|
-
parsed.selectors.forEach((selector, index) => {
|
|
628
|
-
outputLines.push(` ${index + 1}. ${selector.label}`);
|
|
629
|
-
outputLines.push(` selector: ${selector.selector}`);
|
|
630
|
-
outputLines.push(` rationale: ${selector.rationale}`);
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
if (parsed.notes && parsed.notes.trim()) {
|
|
634
|
-
outputLines.push("");
|
|
635
|
-
outputLines.push(`Notes: ${parsed.notes.trim()}`);
|
|
636
|
-
}
|
|
637
|
-
return outputLines.join("\n");
|
|
638
|
-
}
|
|
639
623
|
async function runInterpret(args, logger) {
|
|
640
624
|
logger.info("interpret-start", {
|
|
641
625
|
objective: args.objective,
|
|
@@ -676,7 +660,6 @@ export {
|
|
|
676
660
|
InterpretResultSchema,
|
|
677
661
|
buildInlinePromptSelection,
|
|
678
662
|
canAnalyzeSnapshots,
|
|
679
|
-
formatInterpretationOutput,
|
|
680
663
|
getMimeType,
|
|
681
664
|
readFileAsBase64,
|
|
682
665
|
runInterpret
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
readAiConfig
|
|
5
|
-
} from "./ai-config.js";
|
|
3
|
+
import { readAiConfig } from "./ai-config.js";
|
|
6
4
|
import { LIBRETTO_CONFIG_PATH, REPO_ROOT } from "./context.js";
|
|
7
5
|
import {
|
|
8
6
|
hasProviderCredentials,
|
|
@@ -154,9 +152,7 @@ function resolveSnapshotApiModel(config = readAiConfig()) {
|
|
|
154
152
|
function resolveSnapshotApiModelOrThrow(config = readAiConfig()) {
|
|
155
153
|
const selection = resolveSnapshotApiModel(config);
|
|
156
154
|
if (!selection) {
|
|
157
|
-
throw new SnapshotApiUnavailableError(
|
|
158
|
-
noSnapshotApiConfiguredMessage()
|
|
159
|
-
);
|
|
155
|
+
throw new SnapshotApiUnavailableError(noSnapshotApiConfiguredMessage());
|
|
160
156
|
}
|
|
161
157
|
if (!hasProviderCredentials(selection.provider)) {
|
|
162
158
|
throw new SnapshotApiUnavailableError(
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
appendFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
writeFileSync
|
|
6
|
+
} from "node:fs";
|
|
2
7
|
import {
|
|
3
8
|
getSessionActionsLogPath,
|
|
4
9
|
getSessionNetworkLogPath
|
|
@@ -56,7 +61,10 @@ function clearNetworkLog(session) {
|
|
|
56
61
|
function parentLogAction(session, entry) {
|
|
57
62
|
try {
|
|
58
63
|
const record = { ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry };
|
|
59
|
-
appendFileSync(
|
|
64
|
+
appendFileSync(
|
|
65
|
+
getSessionActionsLogPath(session),
|
|
66
|
+
JSON.stringify(record) + "\n"
|
|
67
|
+
);
|
|
60
68
|
} catch {
|
|
61
69
|
}
|
|
62
70
|
}
|