libretto 0.4.4 → 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 +39 -113
- package/dist/cli/commands/ai.js +1 -1
- package/dist/cli/commands/browser.js +87 -60
- package/dist/cli/commands/execution.js +201 -88
- package/dist/cli/commands/init.js +30 -8
- package/dist/cli/commands/logs.js +5 -6
- 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 +141 -33
- package/dist/cli/core/context.js +7 -18
- package/dist/cli/core/session-telemetry.js +5 -2
- package/dist/cli/core/session.js +23 -10
- package/dist/cli/core/snapshot-analyzer.js +16 -33
- 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 +26 -7
- 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/paths/paths.js +2 -1
- package/dist/shared/paths/repo-root.d.ts +3 -0
- package/dist/shared/paths/repo-root.js +24 -0
- 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 +11 -8
- package/scripts/check-skills-sync.mjs +25 -0
- package/scripts/compare-eval-summary.mjs +47 -0
- package/scripts/postinstall.mjs +26 -17
- 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 +130 -377
- package/skills/libretto/references/auth-profiles.md +30 -0
- package/skills/libretto/{code-generation-rules.md → references/code-generation-rules.md} +27 -42
- package/skills/libretto/references/configuration-file-reference.md +53 -0
- package/skills/libretto/references/pages-and-page-targeting.md +29 -0
- package/skills/libretto/references/site-security-review.md +143 -0
- package/src/cli/cli.ts +86 -0
- package/src/cli/commands/ai.ts +35 -0
- package/src/cli/commands/browser.ts +189 -0
- package/src/cli/commands/execution.ts +822 -0
- package/src/cli/commands/init.ts +350 -0
- package/src/cli/commands/logs.ts +128 -0
- package/src/cli/commands/shared.ts +69 -0
- package/src/cli/commands/snapshot.ts +312 -0
- package/src/cli/core/ai-config.ts +264 -0
- package/src/cli/core/api-snapshot-analyzer.ts +108 -0
- package/src/cli/core/browser.ts +976 -0
- package/src/cli/core/context.ts +127 -0
- package/src/cli/core/pause-signals.ts +35 -0
- package/src/cli/core/session-telemetry.ts +564 -0
- package/src/cli/core/session.ts +223 -0
- package/src/cli/core/snapshot-analyzer.ts +855 -0
- package/src/cli/core/snapshot-api-config.ts +231 -0
- package/src/cli/core/telemetry.ts +459 -0
- package/src/cli/framework/simple-cli.ts +1340 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/router.ts +20 -0
- package/src/cli/workers/run-integration-runtime.ts +338 -0
- package/src/cli/workers/run-integration-worker-protocol.ts +16 -0
- package/src/cli/workers/run-integration-worker.ts +72 -0
- package/src/index.ts +127 -0
- package/src/runtime/download/download.ts +104 -0
- package/src/runtime/download/index.ts +7 -0
- package/src/runtime/extract/extract.ts +102 -0
- package/src/runtime/extract/index.ts +1 -0
- package/src/runtime/network/index.ts +5 -0
- package/src/runtime/network/network.ts +119 -0
- package/{dist/runtime/recovery/agent.cjs → src/runtime/recovery/agent.ts} +114 -76
- package/src/runtime/recovery/errors.ts +155 -0
- package/src/runtime/recovery/index.ts +7 -0
- package/src/runtime/recovery/recovery.ts +53 -0
- package/{dist/shared/condense-dom/condense-dom.cjs → src/shared/condense-dom/condense-dom.ts} +249 -124
- package/src/shared/config/config.ts +3 -0
- package/src/shared/config/index.ts +0 -0
- package/src/shared/debug/index.ts +1 -0
- package/src/shared/debug/pause.ts +91 -0
- package/src/shared/instrumentation/errors.ts +84 -0
- package/src/shared/instrumentation/index.ts +9 -0
- package/src/shared/instrumentation/instrument.ts +406 -0
- package/src/shared/llm/ai-sdk-adapter.ts +81 -0
- package/{dist/shared/llm/client.cjs → src/shared/llm/client.ts} +86 -80
- package/src/shared/llm/index.ts +3 -0
- package/src/shared/llm/types.ts +63 -0
- package/src/shared/logger/index.ts +13 -0
- package/src/shared/logger/logger.ts +358 -0
- package/src/shared/logger/sinks.ts +148 -0
- package/src/shared/paths/paths.ts +110 -0
- package/src/shared/paths/repo-root.ts +27 -0
- package/src/shared/run/api.ts +6 -0
- package/src/shared/run/browser.ts +107 -0
- package/src/shared/state/index.ts +11 -0
- package/src/shared/state/session-state.ts +77 -0
- package/src/shared/visualization/ghost-cursor.ts +213 -0
- package/src/shared/visualization/highlight.ts +149 -0
- package/src/shared/visualization/index.ts +18 -0
- package/src/shared/workflow/workflow.ts +36 -0
- package/dist/index.cjs +0 -144
- package/dist/index.d.cts +0 -21
- package/dist/runtime/download/download.cjs +0 -70
- package/dist/runtime/download/download.d.cts +0 -35
- package/dist/runtime/download/index.cjs +0 -30
- package/dist/runtime/download/index.d.cts +0 -3
- package/dist/runtime/extract/extract.cjs +0 -88
- package/dist/runtime/extract/extract.d.cts +0 -23
- package/dist/runtime/extract/index.cjs +0 -28
- package/dist/runtime/extract/index.d.cts +0 -5
- package/dist/runtime/network/index.cjs +0 -28
- package/dist/runtime/network/index.d.cts +0 -4
- package/dist/runtime/network/network.cjs +0 -91
- package/dist/runtime/network/network.d.cts +0 -28
- package/dist/runtime/recovery/agent.d.cts +0 -13
- package/dist/runtime/recovery/errors.cjs +0 -124
- package/dist/runtime/recovery/errors.d.cts +0 -31
- package/dist/runtime/recovery/index.cjs +0 -34
- package/dist/runtime/recovery/index.d.cts +0 -7
- package/dist/runtime/recovery/recovery.cjs +0 -55
- package/dist/runtime/recovery/recovery.d.cts +0 -12
- package/dist/shared/condense-dom/condense-dom.d.cts +0 -34
- package/dist/shared/config/config.cjs +0 -44
- package/dist/shared/config/config.d.cts +0 -10
- package/dist/shared/config/index.cjs +0 -32
- package/dist/shared/config/index.d.cts +0 -1
- package/dist/shared/debug/index.cjs +0 -28
- package/dist/shared/debug/index.d.cts +0 -1
- package/dist/shared/debug/pause.cjs +0 -86
- package/dist/shared/debug/pause.d.cts +0 -12
- package/dist/shared/instrumentation/errors.cjs +0 -81
- package/dist/shared/instrumentation/errors.d.cts +0 -12
- package/dist/shared/instrumentation/index.cjs +0 -35
- package/dist/shared/instrumentation/index.d.cts +0 -6
- package/dist/shared/instrumentation/instrument.cjs +0 -206
- package/dist/shared/instrumentation/instrument.d.cts +0 -32
- package/dist/shared/llm/ai-sdk-adapter.cjs +0 -71
- package/dist/shared/llm/ai-sdk-adapter.d.cts +0 -22
- package/dist/shared/llm/client.d.cts +0 -13
- package/dist/shared/llm/index.cjs +0 -31
- package/dist/shared/llm/index.d.cts +0 -5
- package/dist/shared/llm/types.cjs +0 -16
- package/dist/shared/llm/types.d.cts +0 -67
- package/dist/shared/logger/index.cjs +0 -37
- package/dist/shared/logger/index.d.cts +0 -2
- package/dist/shared/logger/logger.cjs +0 -232
- package/dist/shared/logger/logger.d.cts +0 -86
- package/dist/shared/logger/sinks.cjs +0 -160
- package/dist/shared/logger/sinks.d.cts +0 -9
- package/dist/shared/paths/paths.cjs +0 -104
- package/dist/shared/paths/paths.d.cts +0 -10
- package/dist/shared/run/api.cjs +0 -28
- package/dist/shared/run/api.d.cts +0 -2
- package/dist/shared/run/browser.cjs +0 -98
- package/dist/shared/run/browser.d.cts +0 -22
- package/dist/shared/state/index.cjs +0 -38
- package/dist/shared/state/index.d.cts +0 -2
- package/dist/shared/state/session-state.cjs +0 -92
- package/dist/shared/state/session-state.d.cts +0 -40
- package/dist/shared/visualization/ghost-cursor.cjs +0 -174
- package/dist/shared/visualization/ghost-cursor.d.cts +0 -37
- package/dist/shared/visualization/highlight.cjs +0 -134
- package/dist/shared/visualization/highlight.d.cts +0 -22
- package/dist/shared/visualization/index.cjs +0 -45
- package/dist/shared/visualization/index.d.cts +0 -3
- package/dist/shared/workflow/workflow.cjs +0 -47
- package/dist/shared/workflow/workflow.d.cts +0 -21
- package/skills/libretto/integration-approach-selection.md +0 -174
|
@@ -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,16 +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
|
-
|
|
148
|
+
if (state.pid == null || !isPidRunning(state.pid)) {
|
|
149
|
+
clearSessionState(session, logger);
|
|
150
|
+
throw new Error(
|
|
151
|
+
`No browser running for session "${session}". Run 'libretto open <url> --session ${session}' first.`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
146
154
|
throw new Error(
|
|
147
|
-
`
|
|
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.`
|
|
148
156
|
);
|
|
149
157
|
}
|
|
150
158
|
const contexts = browser.contexts();
|
|
@@ -170,14 +178,14 @@ async function connect(session, logger, timeoutMs = 1e4, options) {
|
|
|
170
178
|
}
|
|
171
179
|
if (options?.requireSinglePage && !options.pageId && pages.length > 1) {
|
|
172
180
|
throw new Error(
|
|
173
|
-
`Multiple pages are open in session "${session}". Pass --page <id> to target a page (run "libretto
|
|
181
|
+
`Multiple pages are open in session "${session}". Pass --page <id> to target a page (run "libretto pages --session ${session}" to list ids).`
|
|
174
182
|
);
|
|
175
183
|
}
|
|
176
184
|
const pageRefs = await resolvePageReferences(pages);
|
|
177
185
|
const pageRef = options?.pageId ? pageRefs.find((ref) => ref.id === options.pageId) ?? null : pageRefs[pageRefs.length - 1];
|
|
178
186
|
if (!pageRef) {
|
|
179
187
|
throw new Error(
|
|
180
|
-
`Page "${options?.pageId}" was not found in session "${session}". Run "libretto
|
|
188
|
+
`Page "${options?.pageId}" was not found in session "${session}". Run "libretto pages --session ${session}" to list ids.`
|
|
181
189
|
);
|
|
182
190
|
}
|
|
183
191
|
const page = pageRef.page;
|
|
@@ -225,10 +233,16 @@ function resolveViewport(cliViewport, logger) {
|
|
|
225
233
|
}
|
|
226
234
|
const config = readLibrettoConfig();
|
|
227
235
|
if (config.viewport) {
|
|
228
|
-
logger.info("viewport-source", {
|
|
236
|
+
logger.info("viewport-source", {
|
|
237
|
+
source: "config",
|
|
238
|
+
viewport: config.viewport
|
|
239
|
+
});
|
|
229
240
|
return config.viewport;
|
|
230
241
|
}
|
|
231
|
-
logger.info("viewport-source", {
|
|
242
|
+
logger.info("viewport-source", {
|
|
243
|
+
source: "default",
|
|
244
|
+
viewport: DEFAULT_VIEWPORT
|
|
245
|
+
});
|
|
232
246
|
return DEFAULT_VIEWPORT;
|
|
233
247
|
}
|
|
234
248
|
async function runOpen(rawUrl, headed, session, logger, options) {
|
|
@@ -293,7 +307,7 @@ function childLog(level, event, data = {}) {
|
|
|
293
307
|
timestamp: new Date().toISOString(),
|
|
294
308
|
id: Math.random().toString(36).slice(2, 10),
|
|
295
309
|
level,
|
|
296
|
-
scope: 'libretto
|
|
310
|
+
scope: 'libretto.child',
|
|
297
311
|
event,
|
|
298
312
|
data,
|
|
299
313
|
});
|
|
@@ -409,14 +423,17 @@ await new Promise(() => {});
|
|
|
409
423
|
logger.info("open-waiting-for-cdp", { attempt: i, port, session });
|
|
410
424
|
}
|
|
411
425
|
if (ready) {
|
|
412
|
-
writeSessionState(
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
+
);
|
|
420
437
|
logger.info("open-success", {
|
|
421
438
|
url,
|
|
422
439
|
mode: browserMode,
|
|
@@ -512,8 +529,10 @@ async function runClose(session, logger) {
|
|
|
512
529
|
return;
|
|
513
530
|
}
|
|
514
531
|
logger.info("close-killing", { session, pid: state.pid, port: state.port });
|
|
515
|
-
|
|
516
|
-
|
|
532
|
+
if (state.pid != null) {
|
|
533
|
+
sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
|
|
534
|
+
await waitForCloseSignalWindow(CLOSE_WAIT_MS);
|
|
535
|
+
}
|
|
517
536
|
clearSessionState(session, logger);
|
|
518
537
|
logger.info("close-success", { session });
|
|
519
538
|
console.log(`Browser closed (session: ${session}).`);
|
|
@@ -570,7 +589,7 @@ function resolveClosableSessions(logger) {
|
|
|
570
589
|
function clearStoppedSessionStates(sessions, logger) {
|
|
571
590
|
let cleared = 0;
|
|
572
591
|
for (const session of sessions) {
|
|
573
|
-
if (!isPidRunning(session.pid)) {
|
|
592
|
+
if (session.pid == null || !isPidRunning(session.pid)) {
|
|
574
593
|
clearSessionState(session.session, logger);
|
|
575
594
|
cleared += 1;
|
|
576
595
|
}
|
|
@@ -596,17 +615,26 @@ async function runCloseAll(logger, options) {
|
|
|
596
615
|
pid: target.pid,
|
|
597
616
|
port: target.port
|
|
598
617
|
});
|
|
599
|
-
|
|
618
|
+
if (target.pid != null) {
|
|
619
|
+
sendSignalToProcessGroupOrPid(
|
|
620
|
+
target.pid,
|
|
621
|
+
"SIGTERM",
|
|
622
|
+
logger,
|
|
623
|
+
target.session
|
|
624
|
+
);
|
|
625
|
+
}
|
|
600
626
|
}
|
|
601
627
|
await waitForCloseSignalWindow(CLOSE_WAIT_MS);
|
|
602
|
-
let survivors = closable.filter(
|
|
628
|
+
let survivors = closable.filter(
|
|
629
|
+
(target) => target.pid != null && isPidRunning(target.pid)
|
|
630
|
+
);
|
|
603
631
|
if (survivors.length > 0 && !force) {
|
|
604
632
|
const closed = clearStoppedSessionStates(closable, logger);
|
|
605
633
|
throw new Error(
|
|
606
634
|
[
|
|
607
635
|
`Failed to close ${survivors.length} session(s) gracefully: ${formatSessionList(survivors)}.`,
|
|
608
636
|
`Closed ${closed} session(s).`,
|
|
609
|
-
|
|
637
|
+
`Retry with: libretto close --all --force`
|
|
610
638
|
].join("\n")
|
|
611
639
|
);
|
|
612
640
|
}
|
|
@@ -617,11 +645,20 @@ async function runCloseAll(logger, options) {
|
|
|
617
645
|
session: survivor.session,
|
|
618
646
|
pid: survivor.pid
|
|
619
647
|
});
|
|
620
|
-
|
|
648
|
+
if (survivor.pid != null) {
|
|
649
|
+
sendSignalToProcessGroupOrPid(
|
|
650
|
+
survivor.pid,
|
|
651
|
+
"SIGKILL",
|
|
652
|
+
logger,
|
|
653
|
+
survivor.session
|
|
654
|
+
);
|
|
655
|
+
}
|
|
621
656
|
forceKilled += 1;
|
|
622
657
|
}
|
|
623
658
|
await waitForCloseSignalWindow(FORCE_CLOSE_WAIT_MS);
|
|
624
|
-
survivors = survivors.filter(
|
|
659
|
+
survivors = survivors.filter(
|
|
660
|
+
(target) => target.pid != null && isPidRunning(target.pid)
|
|
661
|
+
);
|
|
625
662
|
if (survivors.length > 0) {
|
|
626
663
|
const closed = clearStoppedSessionStates(closable, logger);
|
|
627
664
|
throw new Error(
|
|
@@ -643,6 +680,75 @@ async function runCloseAll(logger, options) {
|
|
|
643
680
|
console.log(`Force-killed ${forceKilled} session(s).`);
|
|
644
681
|
}
|
|
645
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
|
+
}
|
|
646
752
|
function resolvePath(filePath) {
|
|
647
753
|
return join(process.cwd(), filePath);
|
|
648
754
|
}
|
|
@@ -661,8 +767,10 @@ export {
|
|
|
661
767
|
normalizeDomain,
|
|
662
768
|
normalizeUrl,
|
|
663
769
|
resolvePath,
|
|
770
|
+
resolveViewport,
|
|
664
771
|
runClose,
|
|
665
772
|
runCloseAll,
|
|
773
|
+
runConnect,
|
|
666
774
|
runOpen,
|
|
667
775
|
runPages,
|
|
668
776
|
runSave
|
package/dist/cli/core/context.js
CHANGED
|
@@ -1,23 +1,9 @@
|
|
|
1
1
|
import { Logger, createFileLogSink } from "../../shared/logger/index.js";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { cwd } from "node:process";
|
|
4
2
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { join
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { resolveLibrettoRepoRoot } from "../../shared/paths/repo-root.js";
|
|
6
5
|
import { validateSessionName } from "./session.js";
|
|
7
|
-
|
|
8
|
-
const override = process.env.LIBRETTO_REPO_ROOT?.trim();
|
|
9
|
-
if (override) {
|
|
10
|
-
return resolve(override);
|
|
11
|
-
}
|
|
12
|
-
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
13
|
-
encoding: "utf-8"
|
|
14
|
-
});
|
|
15
|
-
if (result.status === 0 && result.stdout) {
|
|
16
|
-
return result.stdout.trim();
|
|
17
|
-
}
|
|
18
|
-
return cwd();
|
|
19
|
-
}
|
|
20
|
-
const REPO_ROOT = getRepoRoot();
|
|
6
|
+
const REPO_ROOT = resolveLibrettoRepoRoot();
|
|
21
7
|
const LIBRETTO_CONFIG_DIR = join(REPO_ROOT, ".libretto");
|
|
22
8
|
const LIBRETTO_CONFIG_PATH = join(LIBRETTO_CONFIG_DIR, "config.json");
|
|
23
9
|
const PROFILES_DIR = join(LIBRETTO_CONFIG_DIR, "profiles");
|
|
@@ -63,7 +49,10 @@ function createLoggerForSession(session) {
|
|
|
63
49
|
const sessionDir = getSessionDir(session);
|
|
64
50
|
mkdirSync(sessionDir, { recursive: true });
|
|
65
51
|
const logFilePath = getSessionLogsPath(session);
|
|
66
|
-
return new Logger(
|
|
52
|
+
return new Logger(
|
|
53
|
+
["libretto"],
|
|
54
|
+
[createFileLogSink({ filePath: logFilePath })]
|
|
55
|
+
);
|
|
67
56
|
}
|
|
68
57
|
async function closeLogger(logger) {
|
|
69
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);
|
|
@@ -93,7 +100,7 @@ function throwSessionNotFoundError(session) {
|
|
|
93
100
|
}
|
|
94
101
|
lines.push("");
|
|
95
102
|
lines.push("Start one with:");
|
|
96
|
-
lines.push(` libretto
|
|
103
|
+
lines.push(` libretto open <url> --session ${session}`);
|
|
97
104
|
throw new Error(lines.join("\n"));
|
|
98
105
|
}
|
|
99
106
|
function assertSessionStateExistsOrThrow(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,31 +157,34 @@ 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
|
}
|
|
162
175
|
const endpoint = `http://127.0.0.1:${existingState.port}`;
|
|
163
176
|
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
|
|
177
|
+
`Session "${session}" is already open and connected to ${endpoint} (pid ${existingState.pid}). Create a new session or close the current one with: libretto close --session ${session}`
|
|
165
178
|
);
|
|
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,
|