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
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { type AiConfig, readAiConfig } from "./ai-config.js";
|
|
4
|
+
import { LIBRETTO_CONFIG_PATH, REPO_ROOT } from "./context.js";
|
|
5
|
+
import {
|
|
6
|
+
hasProviderCredentials,
|
|
7
|
+
parseModel,
|
|
8
|
+
type Provider,
|
|
9
|
+
} from "../../shared/llm/client.js";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_SNAPSHOT_MODELS = {
|
|
12
|
+
openai: "openai/gpt-5.4",
|
|
13
|
+
anthropic: "anthropic/claude-sonnet-4-6",
|
|
14
|
+
google: "google/gemini-3-flash-preview",
|
|
15
|
+
vertex: "vertex/gemini-2.5-pro",
|
|
16
|
+
} as const satisfies Record<Provider, string>;
|
|
17
|
+
|
|
18
|
+
export type SnapshotApiModelSelection = {
|
|
19
|
+
model: string;
|
|
20
|
+
provider: Provider;
|
|
21
|
+
source:
|
|
22
|
+
| "config"
|
|
23
|
+
| "env:auto-openai"
|
|
24
|
+
| "env:auto-anthropic"
|
|
25
|
+
| "env:auto-google"
|
|
26
|
+
| "env:auto-vertex";
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class SnapshotApiUnavailableError extends Error {
|
|
30
|
+
constructor(message: string) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "SnapshotApiUnavailableError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function providerSetupSentence(provider: Provider): string {
|
|
37
|
+
switch (provider) {
|
|
38
|
+
case "openai":
|
|
39
|
+
return "Add OPENAI_API_KEY to .env or as a shell environment variable.";
|
|
40
|
+
case "anthropic":
|
|
41
|
+
return "Add ANTHROPIC_API_KEY to .env or as a shell environment variable.";
|
|
42
|
+
case "google":
|
|
43
|
+
return "Add GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY to .env or as a shell environment variable.";
|
|
44
|
+
case "vertex":
|
|
45
|
+
return "Add GOOGLE_CLOUD_PROJECT or GCLOUD_PROJECT to .env or as a shell environment variable, and make sure application default credentials are configured.";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function defaultModelCommandLine(): string {
|
|
50
|
+
return "npx libretto ai configure openai | anthropic | gemini | vertex";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function providerMissingCredentialSummary(provider: Provider): string {
|
|
54
|
+
switch (provider) {
|
|
55
|
+
case "openai":
|
|
56
|
+
return "OPENAI_API_KEY is missing";
|
|
57
|
+
case "anthropic":
|
|
58
|
+
return "ANTHROPIC_API_KEY is missing";
|
|
59
|
+
case "google":
|
|
60
|
+
return "GEMINI_API_KEY and GOOGLE_GENERATIVE_AI_API_KEY are missing";
|
|
61
|
+
case "vertex":
|
|
62
|
+
return "GOOGLE_CLOUD_PROJECT and GCLOUD_PROJECT are missing";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function noSnapshotApiConfiguredMessage(): string {
|
|
67
|
+
return [
|
|
68
|
+
"Failed to analyze snapshot because no snapshot analyzer is configured.",
|
|
69
|
+
`Add OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY, or GOOGLE_CLOUD_PROJECT to .env or as a shell environment variable, or choose a default model with \`${defaultModelCommandLine()}\`.`,
|
|
70
|
+
"For more info, run `npx libretto init`.",
|
|
71
|
+
].join(" ");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function missingProviderSnapshotMessage(
|
|
75
|
+
selection: SnapshotApiModelSelection,
|
|
76
|
+
): string {
|
|
77
|
+
const configuredSource =
|
|
78
|
+
selection.source === "config"
|
|
79
|
+
? ` in ${LIBRETTO_CONFIG_PATH}`
|
|
80
|
+
: " from process env or .env";
|
|
81
|
+
return [
|
|
82
|
+
`Failed to analyze snapshot because ${selection.provider} is configured${configuredSource}, but ${providerMissingCredentialSummary(selection.provider)}.`,
|
|
83
|
+
providerSetupSentence(selection.provider),
|
|
84
|
+
"For more info, run `npx libretto init`.",
|
|
85
|
+
].join(" ");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readWorktreeEnvPath(): string | null {
|
|
89
|
+
const gitPath = join(REPO_ROOT, ".git");
|
|
90
|
+
if (!existsSync(gitPath)) return null;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const gitPointer = readFileSync(gitPath, "utf-8").trim();
|
|
94
|
+
const match = gitPointer.match(/^gitdir:\s*(.+)$/i);
|
|
95
|
+
if (!match?.[1]) return null;
|
|
96
|
+
const worktreeGitDir = resolve(REPO_ROOT, match[1].trim());
|
|
97
|
+
const commonGitDir = resolve(worktreeGitDir, "..", "..");
|
|
98
|
+
return join(dirname(commonGitDir), ".env");
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function loadSnapshotEnv(): void {
|
|
105
|
+
if (process.env.LIBRETTO_DISABLE_DOTENV?.trim() === "1") return;
|
|
106
|
+
|
|
107
|
+
const envPathCandidates = [
|
|
108
|
+
join(REPO_ROOT, ".env"),
|
|
109
|
+
readWorktreeEnvPath(),
|
|
110
|
+
].filter((value): value is string => Boolean(value));
|
|
111
|
+
|
|
112
|
+
const envPath = envPathCandidates.find((candidate) => existsSync(candidate));
|
|
113
|
+
if (!envPath) return;
|
|
114
|
+
|
|
115
|
+
for (const line of readFileSync(envPath, "utf-8").split("\n")) {
|
|
116
|
+
const parsed = parseDotEnvAssignment(line);
|
|
117
|
+
if (!parsed) continue;
|
|
118
|
+
if (!(parsed.key in process.env)) {
|
|
119
|
+
process.env[parsed.key] = parsed.value;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function parseDotEnvAssignment(
|
|
125
|
+
line: string,
|
|
126
|
+
): { key: string; value: string } | null {
|
|
127
|
+
const trimmed = line.trim();
|
|
128
|
+
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
129
|
+
|
|
130
|
+
const withoutExport = trimmed.startsWith("export ")
|
|
131
|
+
? trimmed.slice("export ".length).trimStart()
|
|
132
|
+
: trimmed;
|
|
133
|
+
const eqIdx = withoutExport.indexOf("=");
|
|
134
|
+
if (eqIdx < 1) return null;
|
|
135
|
+
|
|
136
|
+
const key = withoutExport.slice(0, eqIdx).trim();
|
|
137
|
+
if (!key) return null;
|
|
138
|
+
|
|
139
|
+
const rawValue = withoutExport.slice(eqIdx + 1).trimStart();
|
|
140
|
+
if (!rawValue) {
|
|
141
|
+
return { key, value: "" };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (rawValue.startsWith('"')) {
|
|
145
|
+
const closeIdx = rawValue.indexOf('"', 1);
|
|
146
|
+
if (closeIdx > 0) {
|
|
147
|
+
return { key, value: rawValue.slice(1, closeIdx) };
|
|
148
|
+
}
|
|
149
|
+
return { key, value: rawValue.slice(1) };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (rawValue.startsWith("'")) {
|
|
153
|
+
const closeIdx = rawValue.indexOf("'", 1);
|
|
154
|
+
if (closeIdx > 0) {
|
|
155
|
+
return { key, value: rawValue.slice(1, closeIdx) };
|
|
156
|
+
}
|
|
157
|
+
return { key, value: rawValue.slice(1) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const inlineCommentIndex = rawValue.search(/\s#/);
|
|
161
|
+
const value =
|
|
162
|
+
inlineCommentIndex >= 0
|
|
163
|
+
? rawValue.slice(0, inlineCommentIndex).trimEnd()
|
|
164
|
+
: rawValue.trim();
|
|
165
|
+
return { key, value };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function inferAutoSnapshotModel(): SnapshotApiModelSelection | null {
|
|
169
|
+
const providersInPriorityOrder: Provider[] = [
|
|
170
|
+
"openai",
|
|
171
|
+
"anthropic",
|
|
172
|
+
"google",
|
|
173
|
+
"vertex",
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
for (const provider of providersInPriorityOrder) {
|
|
177
|
+
if (!hasProviderCredentials(provider)) continue;
|
|
178
|
+
return {
|
|
179
|
+
model: DEFAULT_SNAPSHOT_MODELS[provider],
|
|
180
|
+
provider,
|
|
181
|
+
source: `env:auto-${provider}` as SnapshotApiModelSelection["source"],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Resolve which API model to use for snapshot analysis.
|
|
190
|
+
*
|
|
191
|
+
* Priority:
|
|
192
|
+
* 1. Model from .libretto/config.json ai.model field (set via `ai configure`)
|
|
193
|
+
* 2. Auto-detect from available API credentials in env
|
|
194
|
+
*/
|
|
195
|
+
export function resolveSnapshotApiModel(
|
|
196
|
+
config: AiConfig | null = readAiConfig(),
|
|
197
|
+
): SnapshotApiModelSelection | null {
|
|
198
|
+
loadSnapshotEnv();
|
|
199
|
+
|
|
200
|
+
if (config?.model) {
|
|
201
|
+
const { provider } = parseModel(config.model);
|
|
202
|
+
return {
|
|
203
|
+
model: config.model,
|
|
204
|
+
provider,
|
|
205
|
+
source: "config",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return inferAutoSnapshotModel();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function resolveSnapshotApiModelOrThrow(
|
|
213
|
+
config: AiConfig | null = readAiConfig(),
|
|
214
|
+
): SnapshotApiModelSelection {
|
|
215
|
+
const selection = resolveSnapshotApiModel(config);
|
|
216
|
+
if (!selection) {
|
|
217
|
+
throw new SnapshotApiUnavailableError(noSnapshotApiConfiguredMessage());
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!hasProviderCredentials(selection.provider)) {
|
|
221
|
+
throw new SnapshotApiUnavailableError(
|
|
222
|
+
missingProviderSnapshotMessage(selection),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return selection;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function isSnapshotApiUnavailableError(error: unknown): boolean {
|
|
230
|
+
return error instanceof SnapshotApiUnavailableError;
|
|
231
|
+
}
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import type { Page } from "playwright";
|
|
8
|
+
import {
|
|
9
|
+
getSessionActionsLogPath,
|
|
10
|
+
getSessionNetworkLogPath,
|
|
11
|
+
} from "./context.js";
|
|
12
|
+
import { assertSessionStateExistsOrThrow } from "./session.js";
|
|
13
|
+
|
|
14
|
+
export type NetworkLogEntry = {
|
|
15
|
+
ts: string;
|
|
16
|
+
pageId?: string;
|
|
17
|
+
method: string;
|
|
18
|
+
url: string;
|
|
19
|
+
status: number;
|
|
20
|
+
contentType: string | null;
|
|
21
|
+
postData?: string;
|
|
22
|
+
responseBody?: string | null;
|
|
23
|
+
size: number | null;
|
|
24
|
+
durationMs: number | null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function readNetworkLog(
|
|
28
|
+
session: string,
|
|
29
|
+
opts: {
|
|
30
|
+
last?: number;
|
|
31
|
+
filter?: string;
|
|
32
|
+
method?: string;
|
|
33
|
+
pageId?: string;
|
|
34
|
+
} = {},
|
|
35
|
+
): NetworkLogEntry[] {
|
|
36
|
+
assertSessionStateExistsOrThrow(session);
|
|
37
|
+
const logPath = getSessionNetworkLogPath(session);
|
|
38
|
+
if (!existsSync(logPath)) return [];
|
|
39
|
+
|
|
40
|
+
const lines = readFileSync(logPath, "utf-8")
|
|
41
|
+
.trim()
|
|
42
|
+
.split("\n")
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
let entries: NetworkLogEntry[] = lines.map(
|
|
45
|
+
(line) => JSON.parse(line) as NetworkLogEntry,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (opts.method) {
|
|
49
|
+
const m = opts.method.toUpperCase();
|
|
50
|
+
entries = entries.filter((e) => e.method === m);
|
|
51
|
+
}
|
|
52
|
+
if (opts.filter) {
|
|
53
|
+
const re = new RegExp(opts.filter, "i");
|
|
54
|
+
entries = entries.filter((e) => re.test(e.url));
|
|
55
|
+
}
|
|
56
|
+
if (opts.pageId) {
|
|
57
|
+
entries = entries.filter((e) => e.pageId === opts.pageId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const last = opts.last ?? 20;
|
|
61
|
+
if (entries.length > last) {
|
|
62
|
+
entries = entries.slice(-last);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return entries;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function formatNetworkEntry(e: NetworkLogEntry): string {
|
|
69
|
+
const time = e.ts.replace(/.*T/, "").replace(/\.\d+Z$/, "");
|
|
70
|
+
const duration = e.durationMs != null ? `${e.durationMs}ms` : "?ms";
|
|
71
|
+
const size = e.size != null ? `${e.size}B` : "";
|
|
72
|
+
const parts = [
|
|
73
|
+
`[${time}]`,
|
|
74
|
+
`${e.status}`,
|
|
75
|
+
`${e.method.padEnd(6)}`,
|
|
76
|
+
e.url,
|
|
77
|
+
duration,
|
|
78
|
+
size,
|
|
79
|
+
].filter(Boolean);
|
|
80
|
+
let line = parts.join(" ");
|
|
81
|
+
if (e.postData) {
|
|
82
|
+
line += `\n body: ${e.postData.substring(0, 120)}${e.postData.length > 120 ? "..." : ""}`;
|
|
83
|
+
}
|
|
84
|
+
return line;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function clearNetworkLog(session: string): void {
|
|
88
|
+
assertSessionStateExistsOrThrow(session);
|
|
89
|
+
const logPath = getSessionNetworkLogPath(session);
|
|
90
|
+
writeFileSync(logPath, "");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type ActionLogEntry = {
|
|
94
|
+
ts: string;
|
|
95
|
+
pageId?: string;
|
|
96
|
+
action: string;
|
|
97
|
+
source: "user" | "agent";
|
|
98
|
+
selector?: string;
|
|
99
|
+
value?: string;
|
|
100
|
+
url?: string;
|
|
101
|
+
duration?: number;
|
|
102
|
+
success: boolean;
|
|
103
|
+
error?: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export function parentLogAction(
|
|
107
|
+
session: string,
|
|
108
|
+
entry: Record<string, unknown>,
|
|
109
|
+
): void {
|
|
110
|
+
try {
|
|
111
|
+
const record = { ts: new Date().toISOString(), ...entry };
|
|
112
|
+
appendFileSync(
|
|
113
|
+
getSessionActionsLogPath(session),
|
|
114
|
+
JSON.stringify(record) + "\n",
|
|
115
|
+
);
|
|
116
|
+
} catch {}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function readActionLog(
|
|
120
|
+
session: string,
|
|
121
|
+
opts: {
|
|
122
|
+
last?: number;
|
|
123
|
+
filter?: string;
|
|
124
|
+
action?: string;
|
|
125
|
+
source?: string;
|
|
126
|
+
pageId?: string;
|
|
127
|
+
} = {},
|
|
128
|
+
): ActionLogEntry[] {
|
|
129
|
+
assertSessionStateExistsOrThrow(session);
|
|
130
|
+
const logPath = getSessionActionsLogPath(session);
|
|
131
|
+
if (!existsSync(logPath)) return [];
|
|
132
|
+
|
|
133
|
+
const lines = readFileSync(logPath, "utf-8")
|
|
134
|
+
.trim()
|
|
135
|
+
.split("\n")
|
|
136
|
+
.filter(Boolean);
|
|
137
|
+
let entries: ActionLogEntry[] = lines.map(
|
|
138
|
+
(line) => JSON.parse(line) as ActionLogEntry,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (opts.action) {
|
|
142
|
+
const a = opts.action.toLowerCase();
|
|
143
|
+
entries = entries.filter((e) => e.action === a);
|
|
144
|
+
}
|
|
145
|
+
if (opts.source) {
|
|
146
|
+
const s = opts.source.toLowerCase();
|
|
147
|
+
entries = entries.filter((e) => e.source === s);
|
|
148
|
+
}
|
|
149
|
+
if (opts.filter) {
|
|
150
|
+
const re = new RegExp(opts.filter, "i");
|
|
151
|
+
entries = entries.filter(
|
|
152
|
+
(e) =>
|
|
153
|
+
re.test(e.action) ||
|
|
154
|
+
re.test(e.selector || "") ||
|
|
155
|
+
re.test(e.value || "") ||
|
|
156
|
+
re.test(e.url || ""),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
if (opts.pageId) {
|
|
160
|
+
entries = entries.filter((e) => e.pageId === opts.pageId);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const last = opts.last ?? 20;
|
|
164
|
+
if (entries.length > last) {
|
|
165
|
+
entries = entries.slice(-last);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return entries;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function formatActionEntry(e: ActionLogEntry): string {
|
|
172
|
+
const time = e.ts.replace(/.*T/, "").replace(/\.\d+Z$/, "");
|
|
173
|
+
const src = e.source.toUpperCase().padEnd(5);
|
|
174
|
+
const parts = [`[${time}]`, `[${src}]`, e.action];
|
|
175
|
+
if (e.selector) parts.push(e.selector);
|
|
176
|
+
if (e.value) parts.push(`"${e.value}"`);
|
|
177
|
+
if (e.url) parts.push(e.url);
|
|
178
|
+
if (e.duration != null) parts.push(`${e.duration}ms`);
|
|
179
|
+
if (!e.success) parts.push(`ERROR: ${e.error || "unknown"}`);
|
|
180
|
+
return parts.join(" ");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function clearActionLog(session: string): void {
|
|
184
|
+
assertSessionStateExistsOrThrow(session);
|
|
185
|
+
const logPath = getSessionActionsLogPath(session);
|
|
186
|
+
writeFileSync(logPath, "");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const LOCATOR_ACTION_METHODS = [
|
|
190
|
+
"click",
|
|
191
|
+
"dblclick",
|
|
192
|
+
"fill",
|
|
193
|
+
"type",
|
|
194
|
+
"press",
|
|
195
|
+
"check",
|
|
196
|
+
"uncheck",
|
|
197
|
+
"selectOption",
|
|
198
|
+
"hover",
|
|
199
|
+
"focus",
|
|
200
|
+
"scrollIntoViewIfNeeded",
|
|
201
|
+
"waitFor",
|
|
202
|
+
"innerHTML",
|
|
203
|
+
"innerText",
|
|
204
|
+
"textContent",
|
|
205
|
+
"inputValue",
|
|
206
|
+
"isChecked",
|
|
207
|
+
"isDisabled",
|
|
208
|
+
"isEditable",
|
|
209
|
+
"isEnabled",
|
|
210
|
+
"isHidden",
|
|
211
|
+
"isVisible",
|
|
212
|
+
"count",
|
|
213
|
+
"boundingBox",
|
|
214
|
+
"screenshot",
|
|
215
|
+
"evaluate",
|
|
216
|
+
"evaluateAll",
|
|
217
|
+
"evaluateHandle",
|
|
218
|
+
"getAttribute",
|
|
219
|
+
"dispatchEvent",
|
|
220
|
+
"setInputFiles",
|
|
221
|
+
"selectText",
|
|
222
|
+
"dragTo",
|
|
223
|
+
"highlight",
|
|
224
|
+
"tap",
|
|
225
|
+
] as const;
|
|
226
|
+
|
|
227
|
+
const LOCATOR_RETURNING_METHODS = [
|
|
228
|
+
"first",
|
|
229
|
+
"last",
|
|
230
|
+
"locator",
|
|
231
|
+
"getByRole",
|
|
232
|
+
"getByText",
|
|
233
|
+
"getByLabel",
|
|
234
|
+
"getByPlaceholder",
|
|
235
|
+
"getByAltText",
|
|
236
|
+
"getByTitle",
|
|
237
|
+
"getByTestId",
|
|
238
|
+
"filter",
|
|
239
|
+
"and",
|
|
240
|
+
"or",
|
|
241
|
+
] as const;
|
|
242
|
+
|
|
243
|
+
function formatHint(method: string, args: any[]): string {
|
|
244
|
+
const formatted = args.map((a: any) => JSON.stringify(a)).join(", ");
|
|
245
|
+
return `${method}(${formatted})`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function wrapLocator(
|
|
249
|
+
locator: any,
|
|
250
|
+
hint: string,
|
|
251
|
+
session: string,
|
|
252
|
+
page: Page,
|
|
253
|
+
pageId?: string,
|
|
254
|
+
onActivity?: () => void,
|
|
255
|
+
): any {
|
|
256
|
+
if (locator.__librettoActionLogged) return locator;
|
|
257
|
+
locator.__librettoActionLogged = true;
|
|
258
|
+
|
|
259
|
+
for (const actMethod of LOCATOR_ACTION_METHODS) {
|
|
260
|
+
if (typeof locator[actMethod] !== "function") continue;
|
|
261
|
+
const origAct = locator[actMethod].bind(locator);
|
|
262
|
+
locator[actMethod] = async (...actArgs: any[]) => {
|
|
263
|
+
const start = Date.now();
|
|
264
|
+
try {
|
|
265
|
+
await page.evaluate(() => {
|
|
266
|
+
(window as any).__btApiActionInProgress = true;
|
|
267
|
+
});
|
|
268
|
+
} catch {}
|
|
269
|
+
try {
|
|
270
|
+
const result = await origAct(...actArgs);
|
|
271
|
+
parentLogAction(session, {
|
|
272
|
+
pageId,
|
|
273
|
+
action: actMethod,
|
|
274
|
+
source: "agent",
|
|
275
|
+
selector: hint,
|
|
276
|
+
value:
|
|
277
|
+
actArgs[0] !== undefined
|
|
278
|
+
? String(actArgs[0]).slice(0, 100)
|
|
279
|
+
: undefined,
|
|
280
|
+
duration: Date.now() - start,
|
|
281
|
+
success: true,
|
|
282
|
+
});
|
|
283
|
+
onActivity?.();
|
|
284
|
+
return result;
|
|
285
|
+
} catch (err: any) {
|
|
286
|
+
parentLogAction(session, {
|
|
287
|
+
pageId,
|
|
288
|
+
action: actMethod,
|
|
289
|
+
source: "agent",
|
|
290
|
+
selector: hint,
|
|
291
|
+
duration: Date.now() - start,
|
|
292
|
+
success: false,
|
|
293
|
+
error: err.message,
|
|
294
|
+
});
|
|
295
|
+
onActivity?.();
|
|
296
|
+
throw err;
|
|
297
|
+
} finally {
|
|
298
|
+
try {
|
|
299
|
+
await page.evaluate(() => {
|
|
300
|
+
(window as any).__btApiActionInProgress = false;
|
|
301
|
+
});
|
|
302
|
+
} catch {}
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
for (const method of LOCATOR_RETURNING_METHODS) {
|
|
308
|
+
if (typeof locator[method] !== "function") continue;
|
|
309
|
+
const origMethod = locator[method].bind(locator);
|
|
310
|
+
locator[method] = (...args: any[]) => {
|
|
311
|
+
const child = origMethod(...args);
|
|
312
|
+
const childHint =
|
|
313
|
+
args.length > 0
|
|
314
|
+
? `${hint}.${formatHint(method, args)}`
|
|
315
|
+
: `${hint}.${method}()`;
|
|
316
|
+
return wrapLocator(child, childHint, session, page, pageId, onActivity);
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (typeof locator.nth === "function") {
|
|
321
|
+
const origNth = locator.nth.bind(locator);
|
|
322
|
+
locator.nth = (index: number) => {
|
|
323
|
+
const child = origNth(index);
|
|
324
|
+
const childHint = `${hint}.nth(${index})`;
|
|
325
|
+
return wrapLocator(child, childHint, session, page, pageId, onActivity);
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (typeof locator.all === "function") {
|
|
330
|
+
const origAll = locator.all.bind(locator);
|
|
331
|
+
locator.all = async () => {
|
|
332
|
+
const items: any[] = await origAll();
|
|
333
|
+
return items.map((item: any, i: number) => {
|
|
334
|
+
const childHint = `${hint}.all()[${i}]`;
|
|
335
|
+
return wrapLocator(item, childHint, session, page, pageId, onActivity);
|
|
336
|
+
});
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return locator;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function wrapPageForActionLogging(
|
|
344
|
+
page: Page,
|
|
345
|
+
session: string,
|
|
346
|
+
pageId?: string,
|
|
347
|
+
onActivity?: () => void,
|
|
348
|
+
): void {
|
|
349
|
+
const PAGE_ACTIONS = [
|
|
350
|
+
"click",
|
|
351
|
+
"dblclick",
|
|
352
|
+
"fill",
|
|
353
|
+
"type",
|
|
354
|
+
"press",
|
|
355
|
+
"check",
|
|
356
|
+
"uncheck",
|
|
357
|
+
"selectOption",
|
|
358
|
+
"hover",
|
|
359
|
+
"focus",
|
|
360
|
+
] as const;
|
|
361
|
+
const NAV_ACTIONS = ["goto", "reload", "goBack", "goForward"] as const;
|
|
362
|
+
|
|
363
|
+
for (const method of PAGE_ACTIONS) {
|
|
364
|
+
const orig = (page as any)[method].bind(page);
|
|
365
|
+
(page as any)[method] = async (...args: any[]) => {
|
|
366
|
+
const start = Date.now();
|
|
367
|
+
try {
|
|
368
|
+
await page.evaluate(() => {
|
|
369
|
+
(window as any).__btApiActionInProgress = true;
|
|
370
|
+
});
|
|
371
|
+
} catch {}
|
|
372
|
+
try {
|
|
373
|
+
const result = await orig(...args);
|
|
374
|
+
parentLogAction(session, {
|
|
375
|
+
pageId,
|
|
376
|
+
action: method,
|
|
377
|
+
source: "agent",
|
|
378
|
+
selector: typeof args[0] === "string" ? args[0] : undefined,
|
|
379
|
+
value:
|
|
380
|
+
args[1] !== undefined ? String(args[1]).slice(0, 100) : undefined,
|
|
381
|
+
duration: Date.now() - start,
|
|
382
|
+
success: true,
|
|
383
|
+
});
|
|
384
|
+
onActivity?.();
|
|
385
|
+
return result;
|
|
386
|
+
} catch (err: any) {
|
|
387
|
+
parentLogAction(session, {
|
|
388
|
+
pageId,
|
|
389
|
+
action: method,
|
|
390
|
+
source: "agent",
|
|
391
|
+
selector: typeof args[0] === "string" ? args[0] : undefined,
|
|
392
|
+
duration: Date.now() - start,
|
|
393
|
+
success: false,
|
|
394
|
+
error: err.message,
|
|
395
|
+
});
|
|
396
|
+
onActivity?.();
|
|
397
|
+
throw err;
|
|
398
|
+
} finally {
|
|
399
|
+
try {
|
|
400
|
+
await page.evaluate(() => {
|
|
401
|
+
(window as any).__btApiActionInProgress = false;
|
|
402
|
+
});
|
|
403
|
+
} catch {}
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
for (const method of NAV_ACTIONS) {
|
|
409
|
+
const orig = (page as any)[method].bind(page);
|
|
410
|
+
(page as any)[method] = async (...args: any[]) => {
|
|
411
|
+
const start = Date.now();
|
|
412
|
+
try {
|
|
413
|
+
const result = await orig(...args);
|
|
414
|
+
parentLogAction(session, {
|
|
415
|
+
pageId,
|
|
416
|
+
action: method,
|
|
417
|
+
source: "agent",
|
|
418
|
+
url: typeof args[0] === "string" ? args[0] : page.url(),
|
|
419
|
+
duration: Date.now() - start,
|
|
420
|
+
success: true,
|
|
421
|
+
});
|
|
422
|
+
onActivity?.();
|
|
423
|
+
return result;
|
|
424
|
+
} catch (err: any) {
|
|
425
|
+
parentLogAction(session, {
|
|
426
|
+
pageId,
|
|
427
|
+
action: method,
|
|
428
|
+
source: "agent",
|
|
429
|
+
url: typeof args[0] === "string" ? args[0] : undefined,
|
|
430
|
+
duration: Date.now() - start,
|
|
431
|
+
success: false,
|
|
432
|
+
error: err.message,
|
|
433
|
+
});
|
|
434
|
+
onActivity?.();
|
|
435
|
+
throw err;
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const LOCATOR_FACTORIES = [
|
|
441
|
+
"locator",
|
|
442
|
+
"getByRole",
|
|
443
|
+
"getByText",
|
|
444
|
+
"getByLabel",
|
|
445
|
+
"getByPlaceholder",
|
|
446
|
+
"getByAltText",
|
|
447
|
+
"getByTitle",
|
|
448
|
+
"getByTestId",
|
|
449
|
+
] as const;
|
|
450
|
+
|
|
451
|
+
for (const factory of LOCATOR_FACTORIES) {
|
|
452
|
+
const orig = (page as any)[factory].bind(page);
|
|
453
|
+
(page as any)[factory] = (...factoryArgs: any[]) => {
|
|
454
|
+
const locator = orig(...factoryArgs);
|
|
455
|
+
const hint = formatHint(factory, factoryArgs);
|
|
456
|
+
return wrapLocator(locator, hint, session, page, pageId, onActivity);
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
}
|