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
package/src/cli/core/browser.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
chromium,
|
|
3
|
+
type Browser,
|
|
4
|
+
type BrowserContext,
|
|
5
|
+
type CDPSession,
|
|
6
|
+
type Page,
|
|
7
|
+
} from "playwright";
|
|
2
8
|
import { openSync, existsSync, writeFileSync } from "node:fs";
|
|
3
9
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
10
|
import { fileURLToPath } from "node:url";
|
|
@@ -66,13 +72,12 @@ export function hasProfile(domain: string): boolean {
|
|
|
66
72
|
return existsSync(getProfilePath(domain));
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
async function
|
|
70
|
-
|
|
75
|
+
async function tryConnectToCDP(
|
|
76
|
+
endpoint: string,
|
|
71
77
|
logger: LoggerApi,
|
|
72
78
|
timeoutMs: number = 5000,
|
|
73
79
|
): Promise<Browser | null> {
|
|
74
|
-
|
|
75
|
-
logger.info("cdp-connect-attempt", { port, endpoint, timeoutMs });
|
|
80
|
+
logger.info("cdp-connect-attempt", { endpoint, timeoutMs });
|
|
76
81
|
try {
|
|
77
82
|
const connectPromise = chromium.connectOverCDP(endpoint);
|
|
78
83
|
const timeoutPromise = new Promise<null>((resolve) =>
|
|
@@ -81,16 +86,15 @@ async function tryConnectToPort(
|
|
|
81
86
|
const browser = await Promise.race([connectPromise, timeoutPromise]);
|
|
82
87
|
if (browser) {
|
|
83
88
|
logger.info("cdp-connect-success", {
|
|
84
|
-
port,
|
|
85
89
|
endpoint,
|
|
86
90
|
contexts: browser.contexts().length,
|
|
87
91
|
});
|
|
88
92
|
} else {
|
|
89
|
-
logger.warn("cdp-connect-timeout", {
|
|
93
|
+
logger.warn("cdp-connect-timeout", { endpoint, timeoutMs });
|
|
90
94
|
}
|
|
91
95
|
return browser;
|
|
92
96
|
} catch (err) {
|
|
93
|
-
logger.error("cdp-connect-error", { error: err,
|
|
97
|
+
logger.error("cdp-connect-error", { error: err, endpoint });
|
|
94
98
|
return null;
|
|
95
99
|
}
|
|
96
100
|
}
|
|
@@ -135,10 +139,12 @@ async function resolvePageId(page: Page): Promise<string> {
|
|
|
135
139
|
const cdpSession: CDPSession = await page.context().newCDPSession(page);
|
|
136
140
|
try {
|
|
137
141
|
const targetInfo = await cdpSession.send("Target.getTargetInfo");
|
|
138
|
-
const targetId = (targetInfo as { targetInfo?: { targetId?: unknown } })
|
|
139
|
-
?.targetId;
|
|
142
|
+
const targetId = (targetInfo as { targetInfo?: { targetId?: unknown } })
|
|
143
|
+
?.targetInfo?.targetId;
|
|
140
144
|
if (typeof targetId !== "string" || targetId.length === 0) {
|
|
141
|
-
throw new Error(
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Could not resolve target id for page at URL "${page.url()}".`,
|
|
147
|
+
);
|
|
142
148
|
}
|
|
143
149
|
return targetId;
|
|
144
150
|
} finally {
|
|
@@ -162,7 +168,10 @@ export async function listOpenPages(
|
|
|
162
168
|
): Promise<OpenPageSummary[]> {
|
|
163
169
|
const { browser, page: activePage } = await connect(session, logger);
|
|
164
170
|
try {
|
|
165
|
-
const pages = browser
|
|
171
|
+
const pages = browser
|
|
172
|
+
.contexts()
|
|
173
|
+
.flatMap((ctx) => ctx.pages())
|
|
174
|
+
.filter(isOperationalPage);
|
|
166
175
|
const pageRefs = await resolvePageReferences(pages);
|
|
167
176
|
return pageRefs.map(({ id, page }) => ({
|
|
168
177
|
id,
|
|
@@ -190,14 +199,15 @@ export async function connect(
|
|
|
190
199
|
}> {
|
|
191
200
|
logger.info("connect", { session, timeoutMs });
|
|
192
201
|
const state = readSessionStateOrThrow(session);
|
|
193
|
-
const
|
|
202
|
+
const endpoint = state.cdpEndpoint ?? `http://localhost:${state.port}`;
|
|
203
|
+
const browser = await tryConnectToCDP(endpoint, logger, timeoutMs);
|
|
194
204
|
if (!browser) {
|
|
195
205
|
logger.error("connect-no-browser", {
|
|
196
206
|
session,
|
|
197
|
-
|
|
207
|
+
endpoint,
|
|
198
208
|
pid: state.pid,
|
|
199
209
|
});
|
|
200
|
-
if (!isPidRunning(state.pid)) {
|
|
210
|
+
if (state.pid == null || !isPidRunning(state.pid)) {
|
|
201
211
|
clearSessionState(session, logger);
|
|
202
212
|
throw new Error(
|
|
203
213
|
`No browser running for session "${session}". Run 'libretto open <url> --session ${session}' first.`,
|
|
@@ -205,7 +215,7 @@ export async function connect(
|
|
|
205
215
|
}
|
|
206
216
|
|
|
207
217
|
throw new Error(
|
|
208
|
-
`Could not connect to the browser for session "${session}" at
|
|
218
|
+
`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.`,
|
|
209
219
|
);
|
|
210
220
|
}
|
|
211
221
|
|
|
@@ -276,7 +286,10 @@ export async function connect(
|
|
|
276
286
|
return { browser, context, page, pageId: pageRef.id };
|
|
277
287
|
}
|
|
278
288
|
|
|
279
|
-
export async function runPages(
|
|
289
|
+
export async function runPages(
|
|
290
|
+
session: string,
|
|
291
|
+
logger: LoggerApi,
|
|
292
|
+
): Promise<void> {
|
|
280
293
|
logger.info("pages-start", { session });
|
|
281
294
|
const pageSummaries = await listOpenPages(session, logger);
|
|
282
295
|
|
|
@@ -294,7 +307,7 @@ export async function runPages(session: string, logger: LoggerApi): Promise<void
|
|
|
294
307
|
|
|
295
308
|
const DEFAULT_VIEWPORT = { width: 1366, height: 768 } as const;
|
|
296
309
|
|
|
297
|
-
function resolveViewport(
|
|
310
|
+
export function resolveViewport(
|
|
298
311
|
cliViewport: { width: number; height: number } | undefined,
|
|
299
312
|
logger: LoggerApi,
|
|
300
313
|
): { width: number; height: number } {
|
|
@@ -304,10 +317,16 @@ function resolveViewport(
|
|
|
304
317
|
}
|
|
305
318
|
const config = readLibrettoConfig();
|
|
306
319
|
if (config.viewport) {
|
|
307
|
-
logger.info("viewport-source", {
|
|
320
|
+
logger.info("viewport-source", {
|
|
321
|
+
source: "config",
|
|
322
|
+
viewport: config.viewport,
|
|
323
|
+
});
|
|
308
324
|
return config.viewport;
|
|
309
325
|
}
|
|
310
|
-
logger.info("viewport-source", {
|
|
326
|
+
logger.info("viewport-source", {
|
|
327
|
+
source: "default",
|
|
328
|
+
viewport: DEFAULT_VIEWPORT,
|
|
329
|
+
});
|
|
311
330
|
return DEFAULT_VIEWPORT;
|
|
312
331
|
}
|
|
313
332
|
|
|
@@ -474,8 +493,10 @@ await new Promise(() => {});
|
|
|
474
493
|
logger.info("open-child-spawned", { pid: child.pid, port, session });
|
|
475
494
|
|
|
476
495
|
let childSpawnError: Error | null = null;
|
|
477
|
-
let childEarlyExit: {
|
|
478
|
-
null;
|
|
496
|
+
let childEarlyExit: {
|
|
497
|
+
code: number | null;
|
|
498
|
+
signal: NodeJS.Signals | null;
|
|
499
|
+
} | null = null;
|
|
479
500
|
|
|
480
501
|
child.on("error", (err) => {
|
|
481
502
|
childSpawnError = err;
|
|
@@ -529,14 +550,17 @@ await new Promise(() => {});
|
|
|
529
550
|
logger.info("open-waiting-for-cdp", { attempt: i, port, session });
|
|
530
551
|
}
|
|
531
552
|
if (ready) {
|
|
532
|
-
writeSessionState(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
553
|
+
writeSessionState(
|
|
554
|
+
{
|
|
555
|
+
port,
|
|
556
|
+
pid: child.pid!,
|
|
557
|
+
session,
|
|
558
|
+
startedAt: new Date().toISOString(),
|
|
559
|
+
status: "active",
|
|
560
|
+
viewport,
|
|
561
|
+
},
|
|
562
|
+
logger,
|
|
563
|
+
);
|
|
540
564
|
logger.info("open-success", {
|
|
541
565
|
url,
|
|
542
566
|
mode: browserMode,
|
|
@@ -644,7 +668,10 @@ export async function runSave(
|
|
|
644
668
|
}
|
|
645
669
|
}
|
|
646
670
|
|
|
647
|
-
export async function runClose(
|
|
671
|
+
export async function runClose(
|
|
672
|
+
session: string,
|
|
673
|
+
logger: LoggerApi,
|
|
674
|
+
): Promise<void> {
|
|
648
675
|
logger.info("close-start", { session });
|
|
649
676
|
const state = readSessionState(session, logger);
|
|
650
677
|
if (!state) {
|
|
@@ -655,9 +682,10 @@ export async function runClose(session: string, logger: LoggerApi): Promise<void
|
|
|
655
682
|
|
|
656
683
|
logger.info("close-killing", { session, pid: state.pid, port: state.port });
|
|
657
684
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
685
|
+
if (state.pid != null) {
|
|
686
|
+
sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
|
|
687
|
+
await waitForCloseSignalWindow(CLOSE_WAIT_MS);
|
|
688
|
+
}
|
|
661
689
|
|
|
662
690
|
clearSessionState(session, logger);
|
|
663
691
|
logger.info("close-success", { session });
|
|
@@ -666,7 +694,7 @@ export async function runClose(session: string, logger: LoggerApi): Promise<void
|
|
|
666
694
|
|
|
667
695
|
type ClosableSession = {
|
|
668
696
|
session: string;
|
|
669
|
-
pid
|
|
697
|
+
pid?: number;
|
|
670
698
|
port: number;
|
|
671
699
|
};
|
|
672
700
|
|
|
@@ -705,7 +733,9 @@ function sendSignalToProcessGroupOrPid(
|
|
|
705
733
|
}
|
|
706
734
|
}
|
|
707
735
|
|
|
708
|
-
function formatSessionList(
|
|
736
|
+
function formatSessionList(
|
|
737
|
+
targets: ReadonlyArray<{ session: string }>,
|
|
738
|
+
): string {
|
|
709
739
|
return targets.map((target) => `"${target.session}"`).join(", ");
|
|
710
740
|
}
|
|
711
741
|
|
|
@@ -739,7 +769,7 @@ function clearStoppedSessionStates(
|
|
|
739
769
|
): number {
|
|
740
770
|
let cleared = 0;
|
|
741
771
|
for (const session of sessions) {
|
|
742
|
-
if (!isPidRunning(session.pid)) {
|
|
772
|
+
if (session.pid == null || !isPidRunning(session.pid)) {
|
|
743
773
|
clearSessionState(session.session, logger);
|
|
744
774
|
cleared += 1;
|
|
745
775
|
}
|
|
@@ -770,12 +800,21 @@ export async function runCloseAll(
|
|
|
770
800
|
pid: target.pid,
|
|
771
801
|
port: target.port,
|
|
772
802
|
});
|
|
773
|
-
|
|
803
|
+
if (target.pid != null) {
|
|
804
|
+
sendSignalToProcessGroupOrPid(
|
|
805
|
+
target.pid,
|
|
806
|
+
"SIGTERM",
|
|
807
|
+
logger,
|
|
808
|
+
target.session,
|
|
809
|
+
);
|
|
810
|
+
}
|
|
774
811
|
}
|
|
775
812
|
|
|
776
813
|
await waitForCloseSignalWindow(CLOSE_WAIT_MS);
|
|
777
814
|
|
|
778
|
-
let survivors = closable.filter(
|
|
815
|
+
let survivors = closable.filter(
|
|
816
|
+
(target) => target.pid != null && isPidRunning(target.pid),
|
|
817
|
+
);
|
|
779
818
|
if (survivors.length > 0 && !force) {
|
|
780
819
|
const closed = clearStoppedSessionStates(closable, logger);
|
|
781
820
|
|
|
@@ -795,11 +834,20 @@ export async function runCloseAll(
|
|
|
795
834
|
session: survivor.session,
|
|
796
835
|
pid: survivor.pid,
|
|
797
836
|
});
|
|
798
|
-
|
|
837
|
+
if (survivor.pid != null) {
|
|
838
|
+
sendSignalToProcessGroupOrPid(
|
|
839
|
+
survivor.pid,
|
|
840
|
+
"SIGKILL",
|
|
841
|
+
logger,
|
|
842
|
+
survivor.session,
|
|
843
|
+
);
|
|
844
|
+
}
|
|
799
845
|
forceKilled += 1;
|
|
800
846
|
}
|
|
801
847
|
await waitForCloseSignalWindow(FORCE_CLOSE_WAIT_MS);
|
|
802
|
-
survivors = survivors.filter(
|
|
848
|
+
survivors = survivors.filter(
|
|
849
|
+
(target) => target.pid != null && isPidRunning(target.pid),
|
|
850
|
+
);
|
|
803
851
|
if (survivors.length > 0) {
|
|
804
852
|
const closed = clearStoppedSessionStates(closable, logger);
|
|
805
853
|
throw new Error(
|
|
@@ -824,6 +872,95 @@ export async function runCloseAll(
|
|
|
824
872
|
}
|
|
825
873
|
}
|
|
826
874
|
|
|
875
|
+
export async function runConnect(
|
|
876
|
+
cdpUrl: string,
|
|
877
|
+
session: string,
|
|
878
|
+
logger: LoggerApi,
|
|
879
|
+
): Promise<void> {
|
|
880
|
+
logger.info("connect-start", { cdpUrl, session });
|
|
881
|
+
assertSessionAvailableForStart(session, logger);
|
|
882
|
+
|
|
883
|
+
let parsedUrl: URL;
|
|
884
|
+
try {
|
|
885
|
+
parsedUrl = new URL(cdpUrl);
|
|
886
|
+
} catch {
|
|
887
|
+
throw new Error(
|
|
888
|
+
[
|
|
889
|
+
`Invalid CDP URL: ${cdpUrl}`,
|
|
890
|
+
``,
|
|
891
|
+
`Expected an HTTP URL pointing to a Chrome DevTools Protocol endpoint, for example:`,
|
|
892
|
+
` libretto connect http://127.0.0.1:9222`,
|
|
893
|
+
` libretto connect http://remote-host:9222`,
|
|
894
|
+
` libretto connect http://remote-host:9222/devtools/browser/<id>`,
|
|
895
|
+
].join("\n"),
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const endpoint = parsedUrl.href;
|
|
900
|
+
const port = parsedUrl.port
|
|
901
|
+
? Number(parsedUrl.port)
|
|
902
|
+
: parsedUrl.protocol === "https:"
|
|
903
|
+
? 443
|
|
904
|
+
: 80;
|
|
905
|
+
|
|
906
|
+
console.log(
|
|
907
|
+
`Connecting to CDP endpoint at ${endpoint} (session: ${session})...`,
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
// Verify the CDP endpoint is reachable
|
|
911
|
+
const versionUrl = `${parsedUrl.protocol}//${parsedUrl.host}/json/version`;
|
|
912
|
+
try {
|
|
913
|
+
const resp = await fetch(versionUrl);
|
|
914
|
+
const versionInfo = await resp.json();
|
|
915
|
+
logger.info("connect-version-ok", { versionUrl, versionInfo });
|
|
916
|
+
} catch (err) {
|
|
917
|
+
logger.error("connect-version-failed", { versionUrl, error: err });
|
|
918
|
+
throw new Error(
|
|
919
|
+
`Cannot reach CDP endpoint at ${versionUrl}. Make sure the target is running and accessible at ${parsedUrl.host}.`,
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Connect via CDP using the full endpoint URL
|
|
924
|
+
const browser = await tryConnectToCDP(endpoint, logger, 10_000);
|
|
925
|
+
if (!browser) {
|
|
926
|
+
throw new Error(
|
|
927
|
+
`CDP endpoint at ${endpoint} is reachable but Playwright could not connect. Check that the URL is a Chrome DevTools Protocol endpoint.`,
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const pages = resolveOperationalPages(browser);
|
|
932
|
+
logger.info("connect-pages", {
|
|
933
|
+
session,
|
|
934
|
+
pageCount: pages.length,
|
|
935
|
+
urls: pages.map((p) => p.url()),
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
disconnectBrowser(browser, logger, session);
|
|
939
|
+
|
|
940
|
+
writeSessionState(
|
|
941
|
+
{
|
|
942
|
+
port,
|
|
943
|
+
cdpEndpoint: endpoint,
|
|
944
|
+
session,
|
|
945
|
+
startedAt: new Date().toISOString(),
|
|
946
|
+
status: "active",
|
|
947
|
+
},
|
|
948
|
+
logger,
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
logger.info("connect-success", { cdpUrl: endpoint, session, port });
|
|
952
|
+
console.log(`Connected to ${endpoint} (session: ${session})`);
|
|
953
|
+
console.log(` Pages found: ${pages.length}`);
|
|
954
|
+
if (pages.length > 0) {
|
|
955
|
+
for (const p of pages.slice(0, 5)) {
|
|
956
|
+
console.log(` ${p.url()}`);
|
|
957
|
+
}
|
|
958
|
+
if (pages.length > 5) {
|
|
959
|
+
console.log(` ... and ${pages.length - 5} more`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
827
964
|
export function resolvePath(filePath: string): string {
|
|
828
965
|
return join(process.cwd(), filePath);
|
|
829
966
|
}
|
package/src/cli/core/context.ts
CHANGED
|
@@ -66,10 +66,15 @@ export function createLoggerForSession(session: string): Logger {
|
|
|
66
66
|
const sessionDir = getSessionDir(session);
|
|
67
67
|
mkdirSync(sessionDir, { recursive: true });
|
|
68
68
|
const logFilePath = getSessionLogsPath(session);
|
|
69
|
-
return new Logger(
|
|
69
|
+
return new Logger(
|
|
70
|
+
["libretto"],
|
|
71
|
+
[createFileLogSink({ filePath: logFilePath })],
|
|
72
|
+
);
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
export async function closeLogger(
|
|
75
|
+
export async function closeLogger(
|
|
76
|
+
logger: Logger | null | undefined,
|
|
77
|
+
): Promise<void> {
|
|
73
78
|
if (!logger) return;
|
|
74
79
|
await logger.close();
|
|
75
80
|
}
|
|
@@ -13,7 +13,8 @@ type InstallSessionTelemetryOptions = {
|
|
|
13
13
|
export async function installSessionTelemetry(
|
|
14
14
|
options: InstallSessionTelemetryOptions,
|
|
15
15
|
): Promise<void> {
|
|
16
|
-
const STATIC_EXT_RE =
|
|
16
|
+
const STATIC_EXT_RE =
|
|
17
|
+
/\.(css|js|png|jpg|jpeg|gif|woff|woff2|ttf|ico|svg)(\?|$)/i;
|
|
17
18
|
const { context, initialPage, logAction, logNetwork } = options;
|
|
18
19
|
const includeUserDomActions = options.includeUserDomActions ?? false;
|
|
19
20
|
const pageIdCache = new WeakMap<Page, string>();
|
|
@@ -25,10 +26,12 @@ export async function installSessionTelemetry(
|
|
|
25
26
|
const cdpSession = await context.newCDPSession(page);
|
|
26
27
|
try {
|
|
27
28
|
const targetInfo = await cdpSession.send("Target.getTargetInfo");
|
|
28
|
-
const targetId = (targetInfo as { targetInfo?: { targetId?: unknown } })
|
|
29
|
-
?.targetId;
|
|
29
|
+
const targetId = (targetInfo as { targetInfo?: { targetId?: unknown } })
|
|
30
|
+
?.targetInfo?.targetId;
|
|
30
31
|
if (typeof targetId !== "string" || targetId.length === 0) {
|
|
31
|
-
throw new Error(
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Could not resolve target id for page at URL "${page.url()}".`,
|
|
34
|
+
);
|
|
32
35
|
}
|
|
33
36
|
pageIdCache.set(page, targetId);
|
|
34
37
|
return targetId;
|
|
@@ -51,7 +54,10 @@ export async function installSessionTelemetry(
|
|
|
51
54
|
});
|
|
52
55
|
};
|
|
53
56
|
|
|
54
|
-
const markApiActionInProgress = async (
|
|
57
|
+
const markApiActionInProgress = async (
|
|
58
|
+
page: Page,
|
|
59
|
+
inProgress: boolean,
|
|
60
|
+
): Promise<void> => {
|
|
55
61
|
await page.evaluate((flag) => {
|
|
56
62
|
(window as any).__btApiActionInProgress = flag;
|
|
57
63
|
}, inProgress);
|
|
@@ -180,7 +186,10 @@ export async function installSessionTelemetry(
|
|
|
180
186
|
return locator;
|
|
181
187
|
};
|
|
182
188
|
|
|
183
|
-
const installUserDomTracking = async (
|
|
189
|
+
const installUserDomTracking = async (
|
|
190
|
+
page: Page,
|
|
191
|
+
pageId: string,
|
|
192
|
+
): Promise<void> => {
|
|
184
193
|
if (exposedPages.has(page)) return;
|
|
185
194
|
exposedPages.add(page);
|
|
186
195
|
|
|
@@ -425,7 +434,8 @@ export async function installSessionTelemetry(
|
|
|
425
434
|
action: method,
|
|
426
435
|
source: "agent",
|
|
427
436
|
selector: typeof args[0] === "string" ? args[0] : undefined,
|
|
428
|
-
value:
|
|
437
|
+
value:
|
|
438
|
+
args[1] !== undefined ? String(args[1]).slice(0, 100) : undefined,
|
|
429
439
|
duration: Date.now() - start,
|
|
430
440
|
success: true,
|
|
431
441
|
});
|
|
@@ -497,7 +507,8 @@ export async function installSessionTelemetry(
|
|
|
497
507
|
page.on("response", async (response) => {
|
|
498
508
|
const request = response.request();
|
|
499
509
|
const url = request.url();
|
|
500
|
-
if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://"))
|
|
510
|
+
if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://"))
|
|
511
|
+
return;
|
|
501
512
|
emitNetwork({
|
|
502
513
|
pageId,
|
|
503
514
|
method: request.method(),
|
package/src/cli/core/session.ts
CHANGED
|
@@ -23,9 +23,17 @@ import {
|
|
|
23
23
|
|
|
24
24
|
const SESSION_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
25
25
|
|
|
26
|
-
export const SESSION_DEFAULT = "default";
|
|
27
26
|
export const SESSION_DEV_SERVER = "dev-server";
|
|
28
27
|
export const SESSION_BROWSER_AGENT = "browser-agent";
|
|
28
|
+
|
|
29
|
+
export function generateSessionName(): string {
|
|
30
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
31
|
+
let id = "";
|
|
32
|
+
for (let i = 0; i < 4; i++) {
|
|
33
|
+
id += chars[Math.floor(Math.random() * chars.length)];
|
|
34
|
+
}
|
|
35
|
+
return `ses-${id}`;
|
|
36
|
+
}
|
|
29
37
|
export { SESSION_STATE_VERSION };
|
|
30
38
|
export type { SessionStatus, SessionState };
|
|
31
39
|
|
|
@@ -136,7 +144,10 @@ export function readSessionStateOrThrow(session: string): SessionState {
|
|
|
136
144
|
}
|
|
137
145
|
|
|
138
146
|
try {
|
|
139
|
-
return parseSessionStateContent(
|
|
147
|
+
return parseSessionStateContent(
|
|
148
|
+
readFileSync(stateFile, "utf-8"),
|
|
149
|
+
stateFile,
|
|
150
|
+
);
|
|
140
151
|
} catch (err) {
|
|
141
152
|
throw new Error(
|
|
142
153
|
`Could not read session state for "${session}": ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -186,10 +197,13 @@ export function setSessionStatus(
|
|
|
186
197
|
const state = readSessionState(session, logger);
|
|
187
198
|
if (!state) return;
|
|
188
199
|
if (state.status === status) return;
|
|
189
|
-
writeSessionState(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
200
|
+
writeSessionState(
|
|
201
|
+
{
|
|
202
|
+
...state,
|
|
203
|
+
status,
|
|
204
|
+
},
|
|
205
|
+
logger,
|
|
206
|
+
);
|
|
193
207
|
}
|
|
194
208
|
|
|
195
209
|
export function assertSessionAvailableForStart(
|
|
@@ -198,7 +212,7 @@ export function assertSessionAvailableForStart(
|
|
|
198
212
|
): void {
|
|
199
213
|
const existingState = readSessionState(session, logger);
|
|
200
214
|
if (!existingState) return;
|
|
201
|
-
if (!isPidRunning(existingState.pid)) {
|
|
215
|
+
if (existingState.pid == null || !isPidRunning(existingState.pid)) {
|
|
202
216
|
setSessionStatus(session, "exited", logger);
|
|
203
217
|
return;
|
|
204
218
|
}
|
|
@@ -8,15 +8,10 @@
|
|
|
8
8
|
* to the CLI-agent approach if needed.
|
|
9
9
|
*
|
|
10
10
|
* Shared types and utilities (InterpretResultSchema, buildInlinePromptSelection,
|
|
11
|
-
*
|
|
11
|
+
* etc.) are still actively used by the API analyzer.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
existsSync,
|
|
16
|
-
mkdtempSync,
|
|
17
|
-
readFileSync,
|
|
18
|
-
rmSync,
|
|
19
|
-
} from "node:fs";
|
|
14
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
20
15
|
import { extname, isAbsolute, join, resolve } from "node:path";
|
|
21
16
|
import { spawn } from "node:child_process";
|
|
22
17
|
import { tmpdir } from "node:os";
|
|
@@ -147,7 +142,12 @@ abstract class UserCodingAgent {
|
|
|
147
142
|
logger: LoggerApi,
|
|
148
143
|
stdinText?: string,
|
|
149
144
|
): Promise<ExternalCommandResult> {
|
|
150
|
-
const result = await runExternalCommand(
|
|
145
|
+
const result = await runExternalCommand(
|
|
146
|
+
this.command,
|
|
147
|
+
args,
|
|
148
|
+
logger,
|
|
149
|
+
stdinText,
|
|
150
|
+
);
|
|
151
151
|
if (result.exitCode !== 0) {
|
|
152
152
|
throw new Error(
|
|
153
153
|
`Analyzer command failed (${[this.command, ...args].join(" ")}).\n${stripAnsi(result.stderr).trim() || stripAnsi(result.stdout).trim() || "No error output."}`,
|
|
@@ -586,17 +586,18 @@ function estimateTokensFromChars(chars: number): number {
|
|
|
586
586
|
return Math.ceil(chars / 4);
|
|
587
587
|
}
|
|
588
588
|
|
|
589
|
-
function inferContextWindowTokens(
|
|
590
|
-
|
|
591
|
-
|
|
589
|
+
function inferContextWindowTokens(model: string): {
|
|
590
|
+
contextWindowTokens: number;
|
|
591
|
+
source: string;
|
|
592
|
+
} {
|
|
592
593
|
const normalized = model.trim().toLowerCase();
|
|
593
594
|
if (normalized.includes("claude")) {
|
|
594
595
|
return { contextWindowTokens: 200_000, source: "model:claude" };
|
|
595
596
|
}
|
|
596
597
|
if (
|
|
597
|
-
normalized.includes("gpt-5")
|
|
598
|
-
|
|
599
|
-
|
|
598
|
+
normalized.includes("gpt-5") ||
|
|
599
|
+
normalized.includes("o3") ||
|
|
600
|
+
normalized.includes("o4")
|
|
600
601
|
) {
|
|
601
602
|
return { contextWindowTokens: 200_000, source: "model:openai" };
|
|
602
603
|
}
|
|
@@ -699,7 +700,9 @@ export function buildInlinePromptSelection(
|
|
|
699
700
|
fullDomChars: fullHtmlContent.length,
|
|
700
701
|
fullDomEstimatedTokens: estimateTokensFromChars(fullHtmlContent.length),
|
|
701
702
|
condensedDomChars: condensedHtmlContent.length,
|
|
702
|
-
condensedDomEstimatedTokens: estimateTokensFromChars(
|
|
703
|
+
condensedDomEstimatedTokens: estimateTokensFromChars(
|
|
704
|
+
condensedHtmlContent.length,
|
|
705
|
+
),
|
|
703
706
|
configuredModel: model,
|
|
704
707
|
};
|
|
705
708
|
|
|
@@ -740,8 +743,7 @@ export function buildInlinePromptSelection(
|
|
|
740
743
|
false,
|
|
741
744
|
);
|
|
742
745
|
if (fullCandidate.promptEstimatedTokens <= budget.promptBudgetTokens) {
|
|
743
|
-
const selectionReason =
|
|
744
|
-
`Full DOM fits within the estimated prompt budget (~${fullCandidate.promptEstimatedTokens.toLocaleString()} <= ${budget.promptBudgetTokens.toLocaleString()} tokens), so the analyzer receives the uncondensed page HTML.`;
|
|
746
|
+
const selectionReason = `Full DOM fits within the estimated prompt budget (~${fullCandidate.promptEstimatedTokens.toLocaleString()} <= ${budget.promptBudgetTokens.toLocaleString()} tokens), so the analyzer receives the uncondensed page HTML.`;
|
|
745
747
|
const prompt = buildInlineHtmlPrompt(args, {
|
|
746
748
|
htmlContent: fullHtmlContent,
|
|
747
749
|
domLabel: "full DOM",
|
|
@@ -784,7 +786,10 @@ export function buildInlinePromptSelection(
|
|
|
784
786
|
2_000,
|
|
785
787
|
budget.promptBudgetTokens - estimateTokensFromChars(basePrompt.length),
|
|
786
788
|
);
|
|
787
|
-
const truncatedHtml = truncateText(
|
|
789
|
+
const truncatedHtml = truncateText(
|
|
790
|
+
condensedHtmlContent,
|
|
791
|
+
availableHtmlTokens * 4,
|
|
792
|
+
);
|
|
788
793
|
|
|
789
794
|
return buildCandidate(
|
|
790
795
|
"condensed",
|
|
@@ -794,31 +799,6 @@ export function buildInlinePromptSelection(
|
|
|
794
799
|
);
|
|
795
800
|
}
|
|
796
801
|
|
|
797
|
-
export function formatInterpretationOutput(
|
|
798
|
-
parsed: InterpretResult,
|
|
799
|
-
header: string = "Interpretation:",
|
|
800
|
-
): string {
|
|
801
|
-
const outputLines: string[] = [];
|
|
802
|
-
outputLines.push(header);
|
|
803
|
-
outputLines.push(`Answer: ${parsed.answer}`);
|
|
804
|
-
outputLines.push("");
|
|
805
|
-
if (parsed.selectors.length === 0) {
|
|
806
|
-
outputLines.push("Selectors: none found.");
|
|
807
|
-
} else {
|
|
808
|
-
outputLines.push("Selectors:");
|
|
809
|
-
parsed.selectors.forEach((selector, index) => {
|
|
810
|
-
outputLines.push(` ${index + 1}. ${selector.label}`);
|
|
811
|
-
outputLines.push(` selector: ${selector.selector}`);
|
|
812
|
-
outputLines.push(` rationale: ${selector.rationale}`);
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
if (parsed.notes && parsed.notes.trim()) {
|
|
816
|
-
outputLines.push("");
|
|
817
|
-
outputLines.push(`Notes: ${parsed.notes.trim()}`);
|
|
818
|
-
}
|
|
819
|
-
return outputLines.join("\n");
|
|
820
|
-
}
|
|
821
|
-
|
|
822
802
|
export async function runInterpret(
|
|
823
803
|
args: InterpretArgs,
|
|
824
804
|
logger: LoggerApi,
|
|
@@ -860,14 +840,14 @@ export async function runInterpret(
|
|
|
860
840
|
// re-enabled, the caller must supply a valid provider/model-id string.
|
|
861
841
|
throw new Error(
|
|
862
842
|
"The CLI-agent snapshot analysis path is not active. " +
|
|
863
|
-
|
|
864
|
-
|
|
843
|
+
"Update your config to the current format with `npx libretto ai configure <provider>`, " +
|
|
844
|
+
"or set API credentials in .env for direct API analysis.",
|
|
865
845
|
);
|
|
866
846
|
|
|
867
847
|
// Preserved for reference — to re-enable, remove the throw above and:
|
|
868
848
|
// const selection = buildInlinePromptSelection(args, fullHtmlContent, condensedHtmlContent, model);
|
|
869
849
|
// const parsed = await configuredAgent.analyzeSnapshot(selection.prompt, pngPath, logger);
|
|
870
|
-
// console.log(
|
|
850
|
+
// console.log(parsed.answer);
|
|
871
851
|
}
|
|
872
852
|
|
|
873
853
|
export function canAnalyzeSnapshots(): boolean {
|