libretto 0.6.9 → 0.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.js +2 -0
- package/dist/cli/commands/auth.js +535 -0
- package/dist/cli/commands/billing.js +74 -0
- package/dist/cli/commands/browser.js +8 -3
- package/dist/cli/commands/deploy.js +2 -7
- package/dist/cli/commands/execution.js +99 -136
- package/dist/cli/commands/snapshot.js +38 -126
- package/dist/cli/core/ai-model.js +0 -3
- package/dist/cli/core/auth-fetch.js +195 -0
- package/dist/cli/core/auth-storage.js +52 -0
- package/dist/cli/core/browser.js +128 -202
- package/dist/cli/core/daemon/config.js +6 -0
- package/dist/cli/core/daemon/daemon.js +298 -0
- package/dist/cli/core/daemon/exec.js +86 -0
- package/dist/cli/core/daemon/index.js +16 -0
- package/dist/cli/core/daemon/ipc.js +171 -0
- package/dist/cli/core/daemon/pages.js +15 -0
- package/dist/cli/core/daemon/snapshot.js +86 -0
- package/dist/cli/core/daemon/spawn.js +90 -0
- package/dist/cli/core/exec-compiler.js +111 -0
- package/dist/cli/core/prompt.js +72 -0
- package/dist/cli/core/providers/libretto-cloud.js +2 -6
- package/dist/cli/core/readonly-exec.js +1 -1
- package/dist/cli/router.js +4 -0
- package/dist/cli/workers/run-integration-runtime.js +0 -5
- package/dist/shared/state/session-state.d.ts +1 -0
- package/dist/shared/state/session-state.js +2 -1
- package/docs/browser-automation-approaches.md +435 -0
- package/docs/releasing.md +117 -0
- package/package.json +4 -3
- package/skills/libretto/SKILL.md +14 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/cli.ts +2 -0
- package/src/cli/commands/auth.ts +787 -0
- package/src/cli/commands/billing.ts +133 -0
- package/src/cli/commands/browser.ts +8 -2
- package/src/cli/commands/deploy.ts +2 -7
- package/src/cli/commands/execution.ts +126 -186
- package/src/cli/commands/snapshot.ts +46 -143
- package/src/cli/core/ai-model.ts +4 -5
- package/src/cli/core/auth-fetch.ts +283 -0
- package/src/cli/core/auth-storage.ts +102 -0
- package/src/cli/core/browser.ts +159 -242
- package/src/cli/core/daemon/config.ts +46 -0
- package/src/cli/core/daemon/daemon.ts +429 -0
- package/src/cli/core/daemon/exec.ts +128 -0
- package/src/cli/core/daemon/index.ts +24 -0
- package/src/cli/core/daemon/ipc.ts +294 -0
- package/src/cli/core/daemon/pages.ts +21 -0
- package/src/cli/core/daemon/snapshot.ts +114 -0
- package/src/cli/core/daemon/spawn.ts +171 -0
- package/src/cli/core/exec-compiler.ts +169 -0
- package/src/cli/core/prompt.ts +94 -0
- package/src/cli/core/providers/libretto-cloud.ts +2 -6
- package/src/cli/core/readonly-exec.ts +2 -1
- package/src/cli/router.ts +4 -0
- package/src/cli/workers/run-integration-runtime.ts +0 -6
- package/src/shared/state/session-state.ts +1 -0
- package/dist/cli/core/browser-daemon.js +0 -122
- package/src/cli/core/browser-daemon.ts +0 -198
|
@@ -16,17 +16,23 @@ import {
|
|
|
16
16
|
assertSessionAllowsCommand,
|
|
17
17
|
clearSessionState,
|
|
18
18
|
readSessionState,
|
|
19
|
+
readSessionStateOrThrow,
|
|
19
20
|
setSessionStatus
|
|
20
21
|
} from "../core/session.js";
|
|
21
22
|
import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
|
|
23
|
+
import { readLibrettoConfig } from "../core/config.js";
|
|
24
|
+
import { resolveProviderName, getCloudProviderApi } from "../core/providers/index.js";
|
|
25
|
+
import {
|
|
26
|
+
compileExecFunction,
|
|
27
|
+
stripEmptyCatchHandlers
|
|
28
|
+
} from "../core/exec-compiler.js";
|
|
29
|
+
import { DaemonClient } from "../core/daemon/index.js";
|
|
30
|
+
import { createReadonlyExecHelpers } from "../core/readonly-exec.js";
|
|
22
31
|
import {
|
|
23
32
|
readActionLog,
|
|
24
33
|
readNetworkLog,
|
|
25
34
|
wrapPageForActionLogging
|
|
26
35
|
} from "../core/telemetry.js";
|
|
27
|
-
import { readLibrettoConfig } from "../core/config.js";
|
|
28
|
-
import { resolveProviderName, getCloudProviderApi } from "../core/providers/index.js";
|
|
29
|
-
import { createReadonlyExecHelpers } from "../core/readonly-exec.js";
|
|
30
36
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
31
37
|
import {
|
|
32
38
|
pageOption,
|
|
@@ -34,113 +40,59 @@ import {
|
|
|
34
40
|
withAutoSession,
|
|
35
41
|
withRequiredSession
|
|
36
42
|
} from "./shared.js";
|
|
37
|
-
const stripTypeScriptTypes = moduleBuiltin.stripTypeScriptTypes;
|
|
38
43
|
const require2 = moduleBuiltin.createRequire(import.meta.url);
|
|
39
44
|
const tsxCliPath = require2.resolve("tsx/cli");
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const warningMessage = typeof warning === "string" ? warning : warning instanceof Error ? warning.message : "";
|
|
47
|
-
const warningType = typeof typeOrOptions === "string" ? typeOrOptions : typeof typeOrOptions === "object" && typeOrOptions !== null && "type" in typeOrOptions && typeof typeOrOptions.type === "string" ? typeOrOptions.type ?? "" : "";
|
|
48
|
-
if (warningType === "ExperimentalWarning" && warningMessage.includes("stripTypeScriptTypes")) {
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
originalEmitWarning(...args);
|
|
52
|
-
};
|
|
53
|
-
try {
|
|
54
|
-
return action();
|
|
55
|
-
} finally {
|
|
56
|
-
mutableProcess.emitWarning = originalEmitWarning;
|
|
45
|
+
function writeDaemonExecOutput(output) {
|
|
46
|
+
if (output?.stdout) {
|
|
47
|
+
process.stdout.write(output.stdout);
|
|
48
|
+
}
|
|
49
|
+
if (output?.stderr) {
|
|
50
|
+
process.stderr.write(output.stderr);
|
|
57
51
|
}
|
|
58
52
|
}
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
result
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (code[i] === '"' || code[i] === "'" || code[i] === "`") {
|
|
100
|
-
const quote = code[i];
|
|
101
|
-
let j = i + 1;
|
|
102
|
-
while (j < code.length) {
|
|
103
|
-
if (code[j] === "\\" && quote !== "`") {
|
|
104
|
-
j += 2;
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
if (code[j] === "\\" && quote === "`") {
|
|
108
|
-
j += 2;
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (code[j] === quote) {
|
|
112
|
-
j++;
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
if (quote === "`" && code[j] === "$" && code[j + 1] === "{") {
|
|
116
|
-
let depth = 1;
|
|
117
|
-
j += 2;
|
|
118
|
-
while (j < code.length && depth > 0) {
|
|
119
|
-
if (code[j] === "{") depth++;
|
|
120
|
-
else if (code[j] === "}") depth--;
|
|
121
|
-
j++;
|
|
122
|
-
}
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
j++;
|
|
126
|
-
}
|
|
127
|
-
result += code.slice(i, j);
|
|
128
|
-
i = j;
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
catchRe.lastIndex = i;
|
|
132
|
-
const match = catchRe.exec(code);
|
|
133
|
-
if (match && match.index === i) {
|
|
134
|
-
strippedCount++;
|
|
135
|
-
i += match[0].length;
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
result += code[i];
|
|
139
|
-
i++;
|
|
53
|
+
async function execViaDaemon(code, session, daemonSocketPath, logger, options) {
|
|
54
|
+
const mode = options.mode ?? "exec";
|
|
55
|
+
const { cleaned: cleanedCode, strippedCount } = stripEmptyCatchHandlers(code);
|
|
56
|
+
if (strippedCount > 0) {
|
|
57
|
+
console.log("(Stripped `.catch(() => {})` \u2014 letting errors bubble up)");
|
|
58
|
+
}
|
|
59
|
+
logger.info(`${mode}-start`, {
|
|
60
|
+
session,
|
|
61
|
+
codeLength: cleanedCode.length,
|
|
62
|
+
codePreview: cleanedCode.slice(0, 200),
|
|
63
|
+
visualize: options.visualize,
|
|
64
|
+
pageId: options.pageId,
|
|
65
|
+
via: "daemon"
|
|
66
|
+
});
|
|
67
|
+
const client = new DaemonClient(daemonSocketPath);
|
|
68
|
+
const response = mode === "exec" ? await client.exec({
|
|
69
|
+
code: cleanedCode,
|
|
70
|
+
pageId: options.pageId,
|
|
71
|
+
visualize: options.visualize
|
|
72
|
+
}) : await client.readonlyExec({
|
|
73
|
+
code: cleanedCode,
|
|
74
|
+
pageId: options.pageId
|
|
75
|
+
});
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
writeDaemonExecOutput(response.output);
|
|
78
|
+
throw new Error(response.message);
|
|
79
|
+
}
|
|
80
|
+
const { result, output } = response.data;
|
|
81
|
+
writeDaemonExecOutput(output);
|
|
82
|
+
logger.info(`${mode}-success`, {
|
|
83
|
+
session,
|
|
84
|
+
hasResult: result !== void 0,
|
|
85
|
+
via: "daemon"
|
|
86
|
+
});
|
|
87
|
+
if (result !== void 0) {
|
|
88
|
+
console.log(
|
|
89
|
+
typeof result === "string" ? result : JSON.stringify(result, null, 2)
|
|
90
|
+
);
|
|
91
|
+
} else {
|
|
92
|
+
console.log("Executed successfully");
|
|
140
93
|
}
|
|
141
|
-
return { cleaned: result, strippedCount };
|
|
142
94
|
}
|
|
143
|
-
async function
|
|
95
|
+
async function execViaCdpFallback(code, session, logger, options) {
|
|
144
96
|
const visualize = options.visualize ?? false;
|
|
145
97
|
const pageId = options.pageId;
|
|
146
98
|
const mode = options.mode ?? "exec";
|
|
@@ -153,7 +105,8 @@ async function runExec(code, session, logger, options = {}) {
|
|
|
153
105
|
codeLength: cleanedCode.length,
|
|
154
106
|
codePreview: cleanedCode.slice(0, 200),
|
|
155
107
|
visualize,
|
|
156
|
-
pageId
|
|
108
|
+
pageId,
|
|
109
|
+
via: "cdp-fallback"
|
|
157
110
|
});
|
|
158
111
|
const {
|
|
159
112
|
browser,
|
|
@@ -161,8 +114,7 @@ async function runExec(code, session, logger, options = {}) {
|
|
|
161
114
|
page,
|
|
162
115
|
pageId: resolvedPageId
|
|
163
116
|
} = await connect(session, logger, 1e4, {
|
|
164
|
-
pageId
|
|
165
|
-
requireSinglePage: true
|
|
117
|
+
pageId
|
|
166
118
|
});
|
|
167
119
|
const STALL_THRESHOLD_MS = 6e4;
|
|
168
120
|
let lastActivityTs = Date.now();
|
|
@@ -175,7 +127,8 @@ async function runExec(code, session, logger, options = {}) {
|
|
|
175
127
|
logger.warn(`${mode}-stall-warning`, {
|
|
176
128
|
session,
|
|
177
129
|
silenceMs,
|
|
178
|
-
codePreview: cleanedCode.slice(0, 200)
|
|
130
|
+
codePreview: cleanedCode.slice(0, 200),
|
|
131
|
+
via: "cdp-fallback"
|
|
179
132
|
});
|
|
180
133
|
console.warn(
|
|
181
134
|
`[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1e3)}s \u2014 ${mode} may be hung (code: ${cleanedCode.slice(0, 100)}...)`
|
|
@@ -187,7 +140,8 @@ async function runExec(code, session, logger, options = {}) {
|
|
|
187
140
|
logger.info(`${mode}-interrupted`, {
|
|
188
141
|
session,
|
|
189
142
|
duration: Date.now() - execStartTs,
|
|
190
|
-
codePreview: cleanedCode.slice(0, 200)
|
|
143
|
+
codePreview: cleanedCode.slice(0, 200),
|
|
144
|
+
via: "cdp-fallback"
|
|
191
145
|
});
|
|
192
146
|
};
|
|
193
147
|
process.on("SIGINT", sigintHandler);
|
|
@@ -198,35 +152,31 @@ async function runExec(code, session, logger, options = {}) {
|
|
|
198
152
|
await installInstrumentation(page, { visualize: true, logger });
|
|
199
153
|
}
|
|
200
154
|
try {
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
setInterval,
|
|
219
|
-
clearTimeout,
|
|
220
|
-
clearInterval,
|
|
221
|
-
fetch,
|
|
222
|
-
URL,
|
|
223
|
-
Buffer
|
|
224
|
-
};
|
|
225
|
-
})();
|
|
155
|
+
const execState = {};
|
|
156
|
+
const helpers = mode === "readonly-exec" ? createReadonlyExecHelpers(page, { onActivity }) : {
|
|
157
|
+
page,
|
|
158
|
+
context,
|
|
159
|
+
state: execState,
|
|
160
|
+
browser,
|
|
161
|
+
networkLog: (opts = {}) => readNetworkLog(session, opts),
|
|
162
|
+
actionLog: (opts = {}) => readActionLog(session, opts),
|
|
163
|
+
console,
|
|
164
|
+
setTimeout,
|
|
165
|
+
setInterval,
|
|
166
|
+
clearTimeout,
|
|
167
|
+
clearInterval,
|
|
168
|
+
fetch,
|
|
169
|
+
URL,
|
|
170
|
+
Buffer
|
|
171
|
+
};
|
|
226
172
|
const helperNames = Object.keys(helpers);
|
|
227
173
|
const fn = compileExecFunction(cleanedCode, helperNames);
|
|
228
174
|
const result = await fn(...Object.values(helpers));
|
|
229
|
-
logger.info(`${mode}-success`, {
|
|
175
|
+
logger.info(`${mode}-success`, {
|
|
176
|
+
session,
|
|
177
|
+
hasResult: result !== void 0,
|
|
178
|
+
via: "cdp-fallback"
|
|
179
|
+
});
|
|
230
180
|
if (result !== void 0) {
|
|
231
181
|
console.log(
|
|
232
182
|
typeof result === "string" ? result : JSON.stringify(result, null, 2)
|
|
@@ -238,7 +188,8 @@ async function runExec(code, session, logger, options = {}) {
|
|
|
238
188
|
logger.error(`${mode}-error`, {
|
|
239
189
|
error: err,
|
|
240
190
|
session,
|
|
241
|
-
codePreview: cleanedCode.slice(0, 200)
|
|
191
|
+
codePreview: cleanedCode.slice(0, 200),
|
|
192
|
+
via: "cdp-fallback"
|
|
242
193
|
});
|
|
243
194
|
throw err;
|
|
244
195
|
} finally {
|
|
@@ -247,6 +198,18 @@ async function runExec(code, session, logger, options = {}) {
|
|
|
247
198
|
disconnectBrowser(browser, logger, session);
|
|
248
199
|
}
|
|
249
200
|
}
|
|
201
|
+
async function runExec(code, session, logger, options = {}) {
|
|
202
|
+
const state = readSessionStateOrThrow(session);
|
|
203
|
+
if (!state.daemonSocketPath) {
|
|
204
|
+
logger.warn(`${options.mode ?? "exec"}-daemon-socket-missing-cdp-fallback`, {
|
|
205
|
+
session,
|
|
206
|
+
hasCdpEndpoint: Boolean(state.cdpEndpoint),
|
|
207
|
+
port: state.port
|
|
208
|
+
});
|
|
209
|
+
return execViaCdpFallback(code, session, logger, options);
|
|
210
|
+
}
|
|
211
|
+
return execViaDaemon(code, session, state.daemonSocketPath, logger, options);
|
|
212
|
+
}
|
|
250
213
|
function parseJsonArg(label, raw) {
|
|
251
214
|
try {
|
|
252
215
|
return JSON.parse(raw);
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { connect, disconnectBrowser } from "../core/browser.js";
|
|
4
|
-
import { getSessionSnapshotRunDir } from "../core/context.js";
|
|
5
3
|
import { condenseDom } from "../../shared/condense-dom/condense-dom.js";
|
|
6
4
|
import { readSessionState } from "../core/session.js";
|
|
7
5
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
@@ -9,10 +7,8 @@ import { pageOption, sessionOption, withRequiredSession } from "./shared.js";
|
|
|
9
7
|
import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
|
|
10
8
|
import { readSnapshotModel } from "../core/config.js";
|
|
11
9
|
import { resolveSnapshotApiModelOrThrow } from "../core/ai-model.js";
|
|
10
|
+
import { DaemonClient } from "../core/daemon/index.js";
|
|
12
11
|
const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 };
|
|
13
|
-
function generateSnapshotRunId() {
|
|
14
|
-
return `snapshot-${Date.now()}`;
|
|
15
|
-
}
|
|
16
12
|
function isZeroViewport(value) {
|
|
17
13
|
return typeof value === "number" && value <= 0;
|
|
18
14
|
}
|
|
@@ -67,133 +63,42 @@ async function forceSnapshotViewport(page, viewport, logger, session, pageId, re
|
|
|
67
63
|
viewport
|
|
68
64
|
});
|
|
69
65
|
}
|
|
70
|
-
async function
|
|
71
|
-
logger.info("
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
let pageUrl = null;
|
|
91
|
-
try {
|
|
92
|
-
pageUrl = page.url();
|
|
93
|
-
} catch (error) {
|
|
94
|
-
logger.warn("screenshot-url-read-failed", {
|
|
95
|
-
session,
|
|
96
|
-
pageId,
|
|
97
|
-
error
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
const pngPath = `${snapshotRunDir}/page.png`;
|
|
101
|
-
const htmlPath = `${snapshotRunDir}/page.html`;
|
|
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
|
-
const restoreViewport = resolveSnapshotViewport(session, logger);
|
|
110
|
-
const viewportMetrics = await readSnapshotViewportMetrics(page);
|
|
111
|
-
logger.info("screenshot-viewport-metrics", {
|
|
112
|
-
session,
|
|
113
|
-
pageId,
|
|
114
|
-
restoreViewport,
|
|
115
|
-
...viewportMetrics
|
|
116
|
-
});
|
|
117
|
-
await forceSnapshotViewport(
|
|
118
|
-
page,
|
|
119
|
-
restoreViewport,
|
|
120
|
-
logger,
|
|
121
|
-
session,
|
|
122
|
-
pageId,
|
|
123
|
-
shouldForceSnapshotViewport(viewportMetrics) ? "preflight-invalid-viewport" : "preflight-normalize-viewport"
|
|
124
|
-
);
|
|
125
|
-
try {
|
|
126
|
-
await page.screenshot({ path: pngPath });
|
|
127
|
-
} catch (error) {
|
|
128
|
-
if (!isZeroWidthScreenshotError(error)) {
|
|
129
|
-
throw error;
|
|
130
|
-
}
|
|
131
|
-
await forceSnapshotViewport(
|
|
132
|
-
page,
|
|
133
|
-
restoreViewport,
|
|
134
|
-
logger,
|
|
135
|
-
session,
|
|
136
|
-
pageId,
|
|
137
|
-
"retry-after-zero-width-screenshot-error"
|
|
138
|
-
);
|
|
139
|
-
await page.screenshot({ path: pngPath });
|
|
140
|
-
}
|
|
141
|
-
const htmlContent = await page.content();
|
|
142
|
-
const fs = await import("node:fs/promises");
|
|
143
|
-
await fs.writeFile(htmlPath, htmlContent);
|
|
144
|
-
const condenseResult = condenseDom(htmlContent);
|
|
145
|
-
await fs.writeFile(condensedHtmlPath, condenseResult.html);
|
|
146
|
-
logger.info("screenshot-success", {
|
|
147
|
-
session,
|
|
148
|
-
pageUrl,
|
|
149
|
-
title,
|
|
150
|
-
pngPath,
|
|
151
|
-
htmlPath,
|
|
152
|
-
condensedHtmlPath,
|
|
153
|
-
snapshotRunId,
|
|
154
|
-
domCondenseStats: {
|
|
155
|
-
originalLength: condenseResult.originalLength,
|
|
156
|
-
condensedLength: condenseResult.condensedLength,
|
|
157
|
-
reductions: condenseResult.reductions
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
return { pngPath, htmlPath, condensedHtmlPath, baseName: snapshotRunId };
|
|
161
|
-
} catch (err) {
|
|
162
|
-
let pageAlive = false;
|
|
163
|
-
let browserConnected = false;
|
|
164
|
-
try {
|
|
165
|
-
browserConnected = browser.isConnected();
|
|
166
|
-
pageAlive = !page.isClosed();
|
|
167
|
-
} catch {
|
|
66
|
+
async function captureSnapshot(session, logger, daemonSocketPath, pageId) {
|
|
67
|
+
logger.info("snapshot-via-daemon", { session, pageId });
|
|
68
|
+
const client = new DaemonClient(daemonSocketPath);
|
|
69
|
+
const { pngPath, htmlPath, snapshotRunId, pageUrl, title } = await client.snapshot({ pageId });
|
|
70
|
+
const htmlContent = readFileSync(htmlPath, "utf8");
|
|
71
|
+
const condenseResult = condenseDom(htmlContent);
|
|
72
|
+
const condensedHtmlPath = htmlPath.replace(/\.html$/, ".condensed.html");
|
|
73
|
+
writeFileSync(condensedHtmlPath, condenseResult.html);
|
|
74
|
+
logger.info("snapshot-daemon-success", {
|
|
75
|
+
session,
|
|
76
|
+
pageUrl,
|
|
77
|
+
title,
|
|
78
|
+
pngPath,
|
|
79
|
+
htmlPath,
|
|
80
|
+
condensedHtmlPath,
|
|
81
|
+
snapshotRunId,
|
|
82
|
+
domCondenseStats: {
|
|
83
|
+
originalLength: condenseResult.originalLength,
|
|
84
|
+
condensedLength: condenseResult.condensedLength,
|
|
85
|
+
reductions: condenseResult.reductions
|
|
168
86
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
session,
|
|
172
|
-
pageAlive,
|
|
173
|
-
browserConnected,
|
|
174
|
-
pageUrl: (() => {
|
|
175
|
-
try {
|
|
176
|
-
return page.url();
|
|
177
|
-
} catch {
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
})()
|
|
181
|
-
});
|
|
182
|
-
throw err;
|
|
183
|
-
} finally {
|
|
184
|
-
disconnectBrowser(browser, logger, session);
|
|
185
|
-
}
|
|
87
|
+
});
|
|
88
|
+
return { pngPath, htmlPath, condensedHtmlPath, baseName: snapshotRunId };
|
|
186
89
|
}
|
|
187
90
|
async function runSnapshot(session, logger, pageId, objective, context) {
|
|
188
91
|
const normalizedObjective = objective.trim();
|
|
189
92
|
const normalizedContext = context.trim();
|
|
190
93
|
const snapshotModel = readSnapshotModel();
|
|
191
94
|
resolveSnapshotApiModelOrThrow(snapshotModel);
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
95
|
+
const state = readSessionState(session, logger);
|
|
96
|
+
if (!state?.daemonSocketPath) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Session "${session}" has no daemon socket. The browser daemon may have crashed. Close and reopen the session: libretto close --session ${session}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const { pngPath, htmlPath, condensedHtmlPath } = await captureSnapshot(session, logger, state.daemonSocketPath, pageId);
|
|
197
102
|
console.log("Screenshot saved:");
|
|
198
103
|
console.log(` PNG: ${pngPath}`);
|
|
199
104
|
console.log(` HTML: ${htmlPath}`);
|
|
@@ -229,6 +134,13 @@ const snapshotCommand = SimpleCLI.command({
|
|
|
229
134
|
);
|
|
230
135
|
});
|
|
231
136
|
export {
|
|
137
|
+
FALLBACK_SNAPSHOT_VIEWPORT,
|
|
138
|
+
forceSnapshotViewport,
|
|
139
|
+
isZeroViewport,
|
|
140
|
+
isZeroWidthScreenshotError,
|
|
141
|
+
readSnapshotViewportMetrics,
|
|
142
|
+
resolveSnapshotViewport,
|
|
143
|
+
shouldForceSnapshotViewport,
|
|
232
144
|
snapshotCommand,
|
|
233
145
|
snapshotInput
|
|
234
146
|
};
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
hasProviderCredentials,
|
|
5
5
|
parseModel
|
|
6
6
|
} from "./resolve-model.js";
|
|
7
|
-
import { loadEnv } from "../../shared/env/load-env.js";
|
|
8
7
|
import { parseDotEnvAssignment } from "../../shared/env/load-env.js";
|
|
9
8
|
const DEFAULT_SNAPSHOT_MODELS = {
|
|
10
9
|
openai: "openai/gpt-5.4",
|
|
@@ -104,7 +103,6 @@ function inferAutoSnapshotModel() {
|
|
|
104
103
|
return null;
|
|
105
104
|
}
|
|
106
105
|
function resolveSnapshotApiModel(snapshotModel = readSnapshotModel()) {
|
|
107
|
-
loadEnv();
|
|
108
106
|
if (snapshotModel) {
|
|
109
107
|
const { provider } = parseModel(snapshotModel);
|
|
110
108
|
return {
|
|
@@ -141,7 +139,6 @@ function readSnapshotModelSafely(configPath) {
|
|
|
141
139
|
}
|
|
142
140
|
}
|
|
143
141
|
function resolveAiSetupStatus(configPath = LIBRETTO_CONFIG_PATH) {
|
|
144
|
-
loadEnv();
|
|
145
142
|
const result = readSnapshotModelSafely(configPath);
|
|
146
143
|
if (!result.ok) {
|
|
147
144
|
return { kind: "invalid-config", message: result.message };
|