libretto 0.6.11 → 0.6.13
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 +7 -8
- package/README.template.md +7 -8
- package/dist/cli/cli.js +0 -22
- package/dist/cli/commands/browser.js +18 -24
- package/dist/cli/commands/execution.js +254 -234
- package/dist/cli/commands/experiments.js +100 -0
- package/dist/cli/commands/setup.js +3 -310
- package/dist/cli/commands/shared.js +10 -0
- package/dist/cli/commands/snapshot.js +46 -64
- package/dist/cli/commands/status.js +1 -40
- package/dist/cli/core/browser.js +303 -124
- package/dist/cli/core/config.js +5 -6
- package/dist/cli/core/context.js +4 -0
- package/dist/cli/core/daemon/config.js +0 -6
- package/dist/cli/core/daemon/daemon.js +497 -90
- package/dist/cli/core/daemon/ipc.js +170 -129
- package/dist/cli/core/daemon/snapshot.js +48 -9
- package/dist/cli/core/experiments.js +39 -0
- package/dist/cli/core/session.js +5 -4
- package/dist/cli/core/skill-version.js +2 -1
- package/dist/cli/core/workflow-runner/runner.js +147 -0
- package/dist/cli/core/workflow-runtime.js +60 -0
- package/dist/cli/index.js +0 -2
- package/dist/cli/router.js +4 -3
- package/dist/shared/debug/pause-handler.d.ts +9 -0
- package/dist/shared/debug/pause-handler.js +15 -0
- package/dist/shared/debug/pause.d.ts +1 -2
- package/dist/shared/debug/pause.js +13 -36
- package/dist/shared/instrumentation/instrument.js +4 -4
- package/dist/shared/ipc/child-process-transport.d.ts +7 -0
- package/dist/shared/ipc/child-process-transport.js +60 -0
- package/dist/shared/ipc/child-process-transport.spec.d.ts +2 -0
- package/dist/shared/ipc/child-process-transport.spec.js +68 -0
- package/dist/shared/ipc/ipc.d.ts +46 -0
- package/dist/shared/ipc/ipc.js +165 -0
- package/dist/shared/ipc/ipc.spec.d.ts +2 -0
- package/dist/shared/ipc/ipc.spec.js +114 -0
- package/dist/shared/ipc/socket-transport.d.ts +9 -0
- package/dist/shared/ipc/socket-transport.js +143 -0
- package/dist/shared/ipc/socket-transport.spec.d.ts +2 -0
- package/dist/shared/ipc/socket-transport.spec.js +117 -0
- package/dist/shared/package-manager.d.ts +7 -0
- package/dist/shared/package-manager.js +60 -0
- package/dist/shared/paths/paths.d.ts +1 -8
- package/dist/shared/paths/paths.js +1 -49
- package/dist/shared/snapshot/capture-snapshot.d.ts +9 -0
- package/dist/shared/snapshot/capture-snapshot.js +463 -0
- package/dist/shared/snapshot/diff-snapshots.d.ts +72 -0
- package/dist/shared/snapshot/diff-snapshots.js +358 -0
- package/dist/shared/snapshot/render-snapshot.d.ts +39 -0
- package/dist/shared/snapshot/render-snapshot.js +651 -0
- package/dist/shared/snapshot/snapshot.spec.d.ts +2 -0
- package/dist/shared/snapshot/snapshot.spec.js +333 -0
- package/dist/shared/snapshot/types.d.ts +40 -0
- package/dist/shared/snapshot/types.js +0 -0
- package/dist/shared/snapshot/wait-for-page-stable.d.ts +17 -0
- package/dist/shared/snapshot/wait-for-page-stable.js +281 -0
- package/dist/shared/state/session-state.d.ts +1 -0
- package/dist/shared/state/session-state.js +1 -0
- package/docs/experiments.md +67 -0
- package/docs/releasing.md +8 -6
- package/package.json +5 -2
- package/skills/libretto/SKILL.md +19 -19
- package/skills/libretto/references/configuration-file-reference.md +6 -12
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/skills/libretto-readonly/SKILL.md +2 -9
- package/src/cli/AGENTS.md +7 -0
- package/src/cli/cli.ts +0 -23
- package/src/cli/commands/browser.ts +14 -18
- package/src/cli/commands/execution.ts +303 -271
- package/src/cli/commands/experiments.ts +120 -0
- package/src/cli/commands/setup.ts +3 -400
- package/src/cli/commands/shared.ts +20 -0
- package/src/cli/commands/snapshot.ts +54 -94
- package/src/cli/commands/status.ts +1 -48
- package/src/cli/core/browser.ts +372 -150
- package/src/cli/core/config.ts +4 -5
- package/src/cli/core/context.ts +4 -0
- package/src/cli/core/daemon/config.ts +35 -19
- package/src/cli/core/daemon/daemon.ts +645 -107
- package/src/cli/core/daemon/ipc.ts +319 -214
- package/src/cli/core/daemon/snapshot.ts +71 -15
- package/src/cli/core/experiments.ts +56 -0
- package/src/cli/core/resolve-model.ts +5 -0
- package/src/cli/core/session.ts +5 -4
- package/src/cli/core/skill-version.ts +2 -1
- package/src/cli/core/workflow-runner/runner.ts +237 -0
- package/src/cli/core/workflow-runtime.ts +86 -0
- package/src/cli/index.ts +0 -1
- package/src/cli/router.ts +4 -3
- package/src/shared/debug/pause-handler.ts +20 -0
- package/src/shared/debug/pause.ts +14 -48
- package/src/shared/instrumentation/instrument.ts +4 -4
- package/src/shared/ipc/AGENTS.md +24 -0
- package/src/shared/ipc/child-process-transport.spec.ts +86 -0
- package/src/shared/ipc/child-process-transport.ts +96 -0
- package/src/shared/ipc/ipc.spec.ts +161 -0
- package/src/shared/ipc/ipc.ts +288 -0
- package/src/shared/ipc/socket-transport.spec.ts +141 -0
- package/src/shared/ipc/socket-transport.ts +189 -0
- package/src/shared/package-manager.ts +76 -0
- package/src/shared/paths/paths.ts +0 -72
- package/src/shared/snapshot/capture-snapshot.ts +615 -0
- package/src/shared/snapshot/diff-snapshots.ts +579 -0
- package/src/shared/snapshot/render-snapshot.ts +962 -0
- package/src/shared/snapshot/snapshot.spec.ts +388 -0
- package/src/shared/snapshot/types.ts +43 -0
- package/src/shared/snapshot/wait-for-page-stable.ts +425 -0
- package/src/shared/state/session-state.ts +1 -0
- package/dist/cli/commands/ai.js +0 -109
- package/dist/cli/core/ai-model.js +0 -192
- package/dist/cli/core/api-snapshot-analyzer.js +0 -86
- package/dist/cli/core/daemon/index.js +0 -16
- package/dist/cli/core/daemon/spawn.js +0 -90
- package/dist/cli/core/pause-signals.js +0 -29
- package/dist/cli/core/snapshot-analyzer.js +0 -666
- package/dist/cli/workers/run-integration-runtime.js +0 -235
- package/dist/cli/workers/run-integration-worker-protocol.js +0 -17
- package/dist/cli/workers/run-integration-worker.js +0 -64
- package/scripts/summarize-evals.mjs +0 -135
- package/src/cli/commands/ai.ts +0 -143
- package/src/cli/core/ai-model.ts +0 -298
- package/src/cli/core/api-snapshot-analyzer.ts +0 -110
- package/src/cli/core/daemon/index.ts +0 -24
- package/src/cli/core/daemon/spawn.ts +0 -171
- package/src/cli/core/pause-signals.ts +0 -35
- package/src/cli/core/snapshot-analyzer.ts +0 -855
- package/src/cli/workers/run-integration-runtime.ts +0 -326
- package/src/cli/workers/run-integration-worker-protocol.ts +0 -19
- package/src/cli/workers/run-integration-worker.ts +0 -72
package/src/cli/core/ai-model.ts
DELETED
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
import { readSnapshotModel } from "./config.js";
|
|
2
|
-
import { LIBRETTO_CONFIG_PATH } from "./context.js";
|
|
3
|
-
import {
|
|
4
|
-
hasProviderCredentials,
|
|
5
|
-
parseModel,
|
|
6
|
-
type Provider,
|
|
7
|
-
} from "./resolve-model.js";
|
|
8
|
-
|
|
9
|
-
// Re-export so existing consumers (e.g. tests) don't break.
|
|
10
|
-
export { parseDotEnvAssignment } from "../../shared/env/load-env.js";
|
|
11
|
-
|
|
12
|
-
// ── Default models ──────────────────────────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
export const DEFAULT_SNAPSHOT_MODELS = {
|
|
15
|
-
openai: "openai/gpt-5.4",
|
|
16
|
-
anthropic: "anthropic/claude-sonnet-4-6",
|
|
17
|
-
google: "google/gemini-3-flash-preview",
|
|
18
|
-
vertex: "vertex/gemini-2.5-flash",
|
|
19
|
-
openrouter: "openrouter/free",
|
|
20
|
-
} as const satisfies Record<Provider, string>;
|
|
21
|
-
|
|
22
|
-
// ── Source detection ────────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Detect which specific env var provides credentials for a provider.
|
|
26
|
-
* Returns the env var name (e.g. "OPENAI_API_KEY", "GEMINI_API_KEY"),
|
|
27
|
-
* or null if no credential is found.
|
|
28
|
-
*/
|
|
29
|
-
function detectProviderEnvVar(
|
|
30
|
-
provider: Provider,
|
|
31
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
32
|
-
): string | null {
|
|
33
|
-
switch (provider) {
|
|
34
|
-
case "openai":
|
|
35
|
-
return env.OPENAI_API_KEY?.trim() ? "OPENAI_API_KEY" : null;
|
|
36
|
-
case "anthropic":
|
|
37
|
-
return env.ANTHROPIC_API_KEY?.trim() ? "ANTHROPIC_API_KEY" : null;
|
|
38
|
-
case "google":
|
|
39
|
-
if (env.GEMINI_API_KEY?.trim()) return "GEMINI_API_KEY";
|
|
40
|
-
if (env.GOOGLE_GENERATIVE_AI_API_KEY?.trim())
|
|
41
|
-
return "GOOGLE_GENERATIVE_AI_API_KEY";
|
|
42
|
-
return null;
|
|
43
|
-
case "vertex":
|
|
44
|
-
if (env.GOOGLE_CLOUD_PROJECT?.trim()) return "GOOGLE_CLOUD_PROJECT";
|
|
45
|
-
if (env.GCLOUD_PROJECT?.trim()) return "GCLOUD_PROJECT";
|
|
46
|
-
return null;
|
|
47
|
-
case "openrouter":
|
|
48
|
-
return env.OPENROUTER_API_KEY?.trim() ? "OPENROUTER_API_KEY" : null;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ── Snapshot model resolution ───────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
export type SnapshotApiModelSelection = {
|
|
55
|
-
model: string;
|
|
56
|
-
provider: Provider;
|
|
57
|
-
source: "config" | `env:${string}`;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
export class SnapshotApiUnavailableError extends Error {
|
|
61
|
-
constructor(message: string) {
|
|
62
|
-
super(message);
|
|
63
|
-
this.name = "SnapshotApiUnavailableError";
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function providerSetupSentence(provider: Provider): string {
|
|
68
|
-
switch (provider) {
|
|
69
|
-
case "openai":
|
|
70
|
-
return "Add OPENAI_API_KEY to .env or as a shell environment variable.";
|
|
71
|
-
case "anthropic":
|
|
72
|
-
return "Add ANTHROPIC_API_KEY to .env or as a shell environment variable.";
|
|
73
|
-
case "google":
|
|
74
|
-
return "Add GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY to .env or as a shell environment variable.";
|
|
75
|
-
case "vertex":
|
|
76
|
-
return "Add GOOGLE_CLOUD_PROJECT or GCLOUD_PROJECT to .env or as a shell environment variable, and make sure application default credentials are configured.";
|
|
77
|
-
case "openrouter":
|
|
78
|
-
return "Add OPENROUTER_API_KEY to .env or as a shell environment variable.";
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function defaultModelCommandLine(): string {
|
|
83
|
-
return "npx libretto ai configure openai | anthropic | gemini | vertex | openrouter";
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function providerMissingCredentialSummary(provider: Provider): string {
|
|
87
|
-
switch (provider) {
|
|
88
|
-
case "openai":
|
|
89
|
-
return "OPENAI_API_KEY is missing";
|
|
90
|
-
case "anthropic":
|
|
91
|
-
return "ANTHROPIC_API_KEY is missing";
|
|
92
|
-
case "google":
|
|
93
|
-
return "GEMINI_API_KEY and GOOGLE_GENERATIVE_AI_API_KEY are missing";
|
|
94
|
-
case "vertex":
|
|
95
|
-
return "GOOGLE_CLOUD_PROJECT and GCLOUD_PROJECT are missing";
|
|
96
|
-
case "openrouter":
|
|
97
|
-
return "OPENROUTER_API_KEY is missing";
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function noSnapshotApiConfiguredMessage(): string {
|
|
102
|
-
return [
|
|
103
|
-
"Failed to analyze snapshot because no snapshot analyzer is configured.",
|
|
104
|
-
`Add OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY, GOOGLE_CLOUD_PROJECT, or OPENROUTER_API_KEY to .env or as a shell environment variable, or choose a default model with \`${defaultModelCommandLine()}\`.`,
|
|
105
|
-
"For more info, run `npx libretto setup`.",
|
|
106
|
-
].join(" ");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function missingProviderSnapshotMessage(
|
|
110
|
-
selection: SnapshotApiModelSelection,
|
|
111
|
-
): string {
|
|
112
|
-
const configuredSource =
|
|
113
|
-
selection.source === "config"
|
|
114
|
-
? ` in ${LIBRETTO_CONFIG_PATH}`
|
|
115
|
-
: " from process env or .env";
|
|
116
|
-
return [
|
|
117
|
-
`Failed to analyze snapshot because ${selection.provider} is configured${configuredSource}, but ${providerMissingCredentialSummary(selection.provider)}.`,
|
|
118
|
-
providerSetupSentence(selection.provider),
|
|
119
|
-
"For more info, run `npx libretto setup`.",
|
|
120
|
-
].join(" ");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ── Model resolution ────────────────────────────────────────────────────────
|
|
124
|
-
|
|
125
|
-
function inferAutoSnapshotModel(): SnapshotApiModelSelection | null {
|
|
126
|
-
const providersInPriorityOrder: Provider[] = [
|
|
127
|
-
"openai",
|
|
128
|
-
"anthropic",
|
|
129
|
-
"google",
|
|
130
|
-
"vertex",
|
|
131
|
-
"openrouter",
|
|
132
|
-
];
|
|
133
|
-
|
|
134
|
-
for (const provider of providersInPriorityOrder) {
|
|
135
|
-
const envVar = detectProviderEnvVar(provider);
|
|
136
|
-
if (!envVar) continue;
|
|
137
|
-
return {
|
|
138
|
-
model: DEFAULT_SNAPSHOT_MODELS[provider],
|
|
139
|
-
provider,
|
|
140
|
-
source: `env:${envVar}`,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Resolve which API model to use for snapshot analysis.
|
|
149
|
-
*
|
|
150
|
-
* Environment variables are loaded by the CLI entrypoint (`runLibrettoCLI` in
|
|
151
|
-
* `cli.ts`) before this resolver runs. Keep dotenv loading centralized there so
|
|
152
|
-
* model resolution and browser provider resolution share the same env setup.
|
|
153
|
-
*
|
|
154
|
-
* Priority:
|
|
155
|
-
* 1. snapshotModel from .libretto/config.json (set via `ai configure`)
|
|
156
|
-
* 2. Auto-detect from available API credentials in env
|
|
157
|
-
*/
|
|
158
|
-
export function resolveSnapshotApiModel(
|
|
159
|
-
snapshotModel: string | null = readSnapshotModel(),
|
|
160
|
-
): SnapshotApiModelSelection | null {
|
|
161
|
-
if (snapshotModel) {
|
|
162
|
-
const { provider } = parseModel(snapshotModel);
|
|
163
|
-
return {
|
|
164
|
-
model: snapshotModel,
|
|
165
|
-
provider,
|
|
166
|
-
source: "config",
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return inferAutoSnapshotModel();
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function resolveSnapshotApiModelOrThrow(
|
|
174
|
-
snapshotModel: string | null = readSnapshotModel(),
|
|
175
|
-
): SnapshotApiModelSelection {
|
|
176
|
-
const selection = resolveSnapshotApiModel(snapshotModel);
|
|
177
|
-
if (!selection) {
|
|
178
|
-
throw new SnapshotApiUnavailableError(noSnapshotApiConfiguredMessage());
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (!hasProviderCredentials(selection.provider)) {
|
|
182
|
-
throw new SnapshotApiUnavailableError(
|
|
183
|
-
missingProviderSnapshotMessage(selection),
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return selection;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export function isSnapshotApiUnavailableError(error: unknown): boolean {
|
|
191
|
-
return error instanceof SnapshotApiUnavailableError;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// ── AI setup status ─────────────────────────────────────────────────────────
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Workspace AI setup health states.
|
|
198
|
-
*
|
|
199
|
-
* - `ready`: a usable model was resolved and the matching provider has credentials.
|
|
200
|
-
* - `configured-missing-credentials`: config pins a provider whose credentials are absent.
|
|
201
|
-
* - `invalid-config`: `.libretto/config.json` exists but fails schema validation.
|
|
202
|
-
* - `unconfigured`: no config and no env credentials detected.
|
|
203
|
-
*/
|
|
204
|
-
export type AiSetupStatus =
|
|
205
|
-
| {
|
|
206
|
-
kind: "ready";
|
|
207
|
-
model: string;
|
|
208
|
-
provider: Provider;
|
|
209
|
-
source: "config" | `env:${string}`;
|
|
210
|
-
}
|
|
211
|
-
| {
|
|
212
|
-
kind: "configured-missing-credentials";
|
|
213
|
-
model: string;
|
|
214
|
-
provider: Provider;
|
|
215
|
-
}
|
|
216
|
-
| { kind: "invalid-config"; message: string }
|
|
217
|
-
| { kind: "unconfigured" };
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Read snapshot model without throwing on invalid files.
|
|
221
|
-
* Returns the model string or an error message.
|
|
222
|
-
*/
|
|
223
|
-
function readSnapshotModelSafely(
|
|
224
|
-
configPath: string,
|
|
225
|
-
): { ok: true; model: string | null } | { ok: false; message: string } {
|
|
226
|
-
try {
|
|
227
|
-
return { ok: true, model: readSnapshotModel(configPath) };
|
|
228
|
-
} catch (err) {
|
|
229
|
-
return {
|
|
230
|
-
ok: false,
|
|
231
|
-
message: err instanceof Error ? err.message : String(err),
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Resolve the workspace's current AI setup health.
|
|
238
|
-
*
|
|
239
|
-
* Uses the existing config reader and snapshot model resolver, but wraps
|
|
240
|
-
* them to distinguish broken states (invalid config, missing credentials)
|
|
241
|
-
* that the throwing APIs collapse into errors.
|
|
242
|
-
*
|
|
243
|
-
* 1. If config read throws → `invalid-config`.
|
|
244
|
-
* 2. If config has a `snapshotModel` → check credentials for that provider.
|
|
245
|
-
* 3. If no `snapshotModel` → auto-detect from env via existing resolver.
|
|
246
|
-
*/
|
|
247
|
-
export function resolveAiSetupStatus(
|
|
248
|
-
configPath: string = LIBRETTO_CONFIG_PATH,
|
|
249
|
-
): AiSetupStatus {
|
|
250
|
-
const result = readSnapshotModelSafely(configPath);
|
|
251
|
-
|
|
252
|
-
if (!result.ok) {
|
|
253
|
-
return { kind: "invalid-config", message: result.message };
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Config has a snapshotModel — use it directly to check credentials
|
|
257
|
-
if (result.model) {
|
|
258
|
-
let selection: SnapshotApiModelSelection | null;
|
|
259
|
-
try {
|
|
260
|
-
selection = resolveSnapshotApiModel(result.model);
|
|
261
|
-
} catch (err) {
|
|
262
|
-
return {
|
|
263
|
-
kind: "invalid-config",
|
|
264
|
-
message: err instanceof Error ? err.message : String(err),
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
if (!selection) {
|
|
268
|
-
// Should not happen when config has a model, but handle gracefully
|
|
269
|
-
return { kind: "unconfigured" };
|
|
270
|
-
}
|
|
271
|
-
if (hasProviderCredentials(selection.provider)) {
|
|
272
|
-
return {
|
|
273
|
-
kind: "ready",
|
|
274
|
-
model: selection.model,
|
|
275
|
-
provider: selection.provider,
|
|
276
|
-
source: selection.source,
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
return {
|
|
280
|
-
kind: "configured-missing-credentials",
|
|
281
|
-
model: selection.model,
|
|
282
|
-
provider: selection.provider,
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// No snapshotModel — fall back to env auto-detect via existing resolver
|
|
287
|
-
const envSelection = resolveSnapshotApiModel(null);
|
|
288
|
-
if (envSelection && hasProviderCredentials(envSelection.provider)) {
|
|
289
|
-
return {
|
|
290
|
-
kind: "ready",
|
|
291
|
-
model: envSelection.model,
|
|
292
|
-
provider: envSelection.provider,
|
|
293
|
-
source: envSelection.source,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
return { kind: "unconfigured" };
|
|
298
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* API-based snapshot analyzer.
|
|
3
|
-
*
|
|
4
|
-
* Sends the DOM snapshot (condensed or full depending on sizing) and screenshot
|
|
5
|
-
* directly to a supported API provider via the Vercel AI SDK, without spawning
|
|
6
|
-
* a CLI process.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { readFileSync } from "node:fs";
|
|
10
|
-
import type { LoggerApi } from "../../shared/logger/index.js";
|
|
11
|
-
import { generateObject } from "ai";
|
|
12
|
-
import { resolveModel } from "./resolve-model.js";
|
|
13
|
-
import {
|
|
14
|
-
InterpretResultSchema,
|
|
15
|
-
buildInlinePromptSelection,
|
|
16
|
-
getMimeType,
|
|
17
|
-
readFileAsBase64,
|
|
18
|
-
type InterpretResult,
|
|
19
|
-
type InterpretArgs,
|
|
20
|
-
} from "./snapshot-analyzer.js";
|
|
21
|
-
import { readSnapshotModel } from "./config.js";
|
|
22
|
-
import { resolveSnapshotApiModelOrThrow } from "./ai-model.js";
|
|
23
|
-
|
|
24
|
-
export async function runApiInterpret(
|
|
25
|
-
args: InterpretArgs,
|
|
26
|
-
logger: LoggerApi,
|
|
27
|
-
snapshotModel: string | null = readSnapshotModel(),
|
|
28
|
-
): Promise<void> {
|
|
29
|
-
const selection = resolveSnapshotApiModelOrThrow(snapshotModel);
|
|
30
|
-
|
|
31
|
-
logger.info("api-interpret-start", {
|
|
32
|
-
objective: args.objective,
|
|
33
|
-
pngPath: args.pngPath,
|
|
34
|
-
htmlPath: args.htmlPath,
|
|
35
|
-
condensedHtmlPath: args.condensedHtmlPath,
|
|
36
|
-
model: selection.model,
|
|
37
|
-
modelSource: selection.source,
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const fullHtmlContent = readFileSync(args.htmlPath, "utf-8");
|
|
41
|
-
const condensedHtmlContent = readFileSync(args.condensedHtmlPath, "utf-8");
|
|
42
|
-
|
|
43
|
-
const promptSelection = buildInlinePromptSelection(
|
|
44
|
-
args,
|
|
45
|
-
fullHtmlContent,
|
|
46
|
-
condensedHtmlContent,
|
|
47
|
-
selection.model,
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
logger.info("api-interpret-dom-selection", {
|
|
51
|
-
configuredModel: promptSelection.stats.configuredModel,
|
|
52
|
-
fullDomEstimatedTokens: promptSelection.stats.fullDomEstimatedTokens,
|
|
53
|
-
condensedDomEstimatedTokens:
|
|
54
|
-
promptSelection.stats.condensedDomEstimatedTokens,
|
|
55
|
-
contextWindowTokens: promptSelection.budget.contextWindowTokens,
|
|
56
|
-
promptBudgetTokens: promptSelection.budget.promptBudgetTokens,
|
|
57
|
-
selectedDom: promptSelection.domSource,
|
|
58
|
-
selectedHtmlEstimatedTokens: promptSelection.htmlEstimatedTokens,
|
|
59
|
-
selectedPromptEstimatedTokens: promptSelection.promptEstimatedTokens,
|
|
60
|
-
selectionReason: promptSelection.selectionReason,
|
|
61
|
-
truncated: promptSelection.truncated,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
const imageBase64 = readFileAsBase64(args.pngPath);
|
|
65
|
-
const imageMimeType = getMimeType(args.pngPath);
|
|
66
|
-
const imageBytes = Buffer.from(imageBase64, "base64");
|
|
67
|
-
|
|
68
|
-
const model = await resolveModel(selection.model);
|
|
69
|
-
|
|
70
|
-
const { object: result } = await generateObject({
|
|
71
|
-
model,
|
|
72
|
-
schema: InterpretResultSchema,
|
|
73
|
-
messages: [
|
|
74
|
-
{
|
|
75
|
-
role: "user",
|
|
76
|
-
content: [
|
|
77
|
-
{ type: "text", text: promptSelection.prompt },
|
|
78
|
-
{
|
|
79
|
-
type: "image",
|
|
80
|
-
image: imageBytes,
|
|
81
|
-
mediaType: imageMimeType,
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
},
|
|
85
|
-
],
|
|
86
|
-
temperature: 0.1,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const parsed: InterpretResult = InterpretResultSchema.parse(result);
|
|
90
|
-
|
|
91
|
-
logger.info("api-interpret-success", {
|
|
92
|
-
selectorCount: parsed.selectors.length,
|
|
93
|
-
answer: parsed.answer.slice(0, 200),
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
console.log("");
|
|
97
|
-
console.log("Analysis:");
|
|
98
|
-
console.log(parsed.answer);
|
|
99
|
-
if (parsed.selectors.length > 0) {
|
|
100
|
-
console.log("");
|
|
101
|
-
console.log("Selectors:");
|
|
102
|
-
parsed.selectors.forEach((selector, index) => {
|
|
103
|
-
console.log(` ${index + 1}. ${selector.label}: ${selector.selector}`);
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
if (parsed.notes?.trim()) {
|
|
107
|
-
console.log("");
|
|
108
|
-
console.log(`Notes: ${parsed.notes.trim()}`);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
DaemonServer,
|
|
3
|
-
DaemonClient,
|
|
4
|
-
DaemonClientError,
|
|
5
|
-
getDaemonSocketPath,
|
|
6
|
-
type DaemonCommandResult,
|
|
7
|
-
type DaemonExecOutput,
|
|
8
|
-
type DaemonRequest,
|
|
9
|
-
type DaemonResponse,
|
|
10
|
-
type DaemonResultMap,
|
|
11
|
-
type RequestHandler,
|
|
12
|
-
} from "./ipc.js";
|
|
13
|
-
|
|
14
|
-
export {
|
|
15
|
-
type DaemonLaunchConfig,
|
|
16
|
-
type DaemonConnectConfig,
|
|
17
|
-
type DaemonConfig,
|
|
18
|
-
} from "./config.js";
|
|
19
|
-
|
|
20
|
-
export {
|
|
21
|
-
spawnSessionDaemon,
|
|
22
|
-
type SpawnSessionDaemonOptions,
|
|
23
|
-
type SpawnSessionDaemonResult,
|
|
24
|
-
} from "./spawn.js";
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Spawn and wait for a browser daemon process.
|
|
3
|
-
*
|
|
4
|
-
* Shared by `runOpen`, `runConnect`, and `runOpenWithProvider` in
|
|
5
|
-
* `browser.ts`. Encapsulates the child-process lifecycle and IPC
|
|
6
|
-
* readiness polling so callers only need to provide config and
|
|
7
|
-
* handle session-state persistence.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { openSync, closeSync } from "node:fs";
|
|
11
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
|
-
import { createRequire } from "node:module";
|
|
13
|
-
import { spawn } from "node:child_process";
|
|
14
|
-
import type { LoggerApi } from "../../../shared/logger/index.js";
|
|
15
|
-
import { getDaemonSocketPath } from "./ipc.js";
|
|
16
|
-
import { DaemonClient } from "./ipc.js";
|
|
17
|
-
import type { DaemonConfig } from "./config.js";
|
|
18
|
-
|
|
19
|
-
// ── Public types ─────────────────────────────────────────────────────
|
|
20
|
-
|
|
21
|
-
export type SpawnSessionDaemonOptions = {
|
|
22
|
-
/** Daemon config — serialized as JSON and passed to the child process. */
|
|
23
|
-
config: DaemonConfig;
|
|
24
|
-
session: string;
|
|
25
|
-
logger: LoggerApi;
|
|
26
|
-
/** Path for the child's stderr log file. */
|
|
27
|
-
logPath: string;
|
|
28
|
-
/** How long to wait for the daemon's IPC server (default: 10 000 ms). */
|
|
29
|
-
ipcTimeoutMs?: number;
|
|
30
|
-
/**
|
|
31
|
-
* Called before throwing when the daemon fails to start (spawn error,
|
|
32
|
-
* early exit, or IPC timeout). Use for cleanup — e.g. closing a cloud
|
|
33
|
-
* provider session. Return value is ignored.
|
|
34
|
-
*/
|
|
35
|
-
onFailure?: () => Promise<unknown>;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export type SpawnSessionDaemonResult = {
|
|
39
|
-
/** PID of the detached daemon child process. */
|
|
40
|
-
pid: number;
|
|
41
|
-
/** Unix domain socket path for daemon IPC. */
|
|
42
|
-
socketPath: string;
|
|
43
|
-
/** Ready-to-use IPC client (already confirmed reachable via ping). */
|
|
44
|
-
client: DaemonClient;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// ── Implementation ───────────────────────────────────────────────────
|
|
48
|
-
|
|
49
|
-
const DEFAULT_IPC_TIMEOUT_MS = 10_000;
|
|
50
|
-
const IPC_POLL_INTERVAL_MS = 250;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Spawn a daemon child process with the given config and wait for its
|
|
54
|
-
* IPC server to become reachable.
|
|
55
|
-
*
|
|
56
|
-
* The daemon entry point is resolved relative to this module so the
|
|
57
|
-
* caller doesn't need to know where the daemon script lives.
|
|
58
|
-
*/
|
|
59
|
-
export async function spawnSessionDaemon(
|
|
60
|
-
options: SpawnSessionDaemonOptions,
|
|
61
|
-
): Promise<SpawnSessionDaemonResult> {
|
|
62
|
-
const {
|
|
63
|
-
config,
|
|
64
|
-
session,
|
|
65
|
-
logger,
|
|
66
|
-
logPath,
|
|
67
|
-
ipcTimeoutMs = DEFAULT_IPC_TIMEOUT_MS,
|
|
68
|
-
onFailure,
|
|
69
|
-
} = options;
|
|
70
|
-
|
|
71
|
-
// Resolve paths for the daemon entry point and tsx loader.
|
|
72
|
-
const daemonEntryPath = fileURLToPath(
|
|
73
|
-
new URL("./daemon.js", import.meta.url),
|
|
74
|
-
);
|
|
75
|
-
const require = createRequire(import.meta.url);
|
|
76
|
-
const tsxImportPath = pathToFileURL(require.resolve("tsx/esm")).href;
|
|
77
|
-
|
|
78
|
-
// Spawn detached child process with stderr going to the log file.
|
|
79
|
-
const childStderrFd = openSync(logPath, "a");
|
|
80
|
-
const child = spawn(
|
|
81
|
-
process.execPath,
|
|
82
|
-
["--import", tsxImportPath, daemonEntryPath, JSON.stringify(config)],
|
|
83
|
-
{
|
|
84
|
-
detached: true,
|
|
85
|
-
stdio: ["ignore", "ignore", childStderrFd],
|
|
86
|
-
},
|
|
87
|
-
);
|
|
88
|
-
child.unref();
|
|
89
|
-
closeSync(childStderrFd);
|
|
90
|
-
|
|
91
|
-
const pid = child.pid!;
|
|
92
|
-
logger.info("daemon-spawned", { pid, session });
|
|
93
|
-
|
|
94
|
-
// Track spawn errors and early exits so the polling loop can fail fast.
|
|
95
|
-
let childSpawnError: Error | null = null;
|
|
96
|
-
let childEarlyExit: {
|
|
97
|
-
code: number | null;
|
|
98
|
-
signal: NodeJS.Signals | null;
|
|
99
|
-
} | null = null;
|
|
100
|
-
|
|
101
|
-
child.on("error", (err) => {
|
|
102
|
-
childSpawnError = err;
|
|
103
|
-
logger.error("daemon-spawn-error", { error: err, session });
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
child.on("exit", (code, signal) => {
|
|
107
|
-
childEarlyExit = { code, signal };
|
|
108
|
-
logger.warn("daemon-early-exit", { code, signal, session, pid });
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// Poll the daemon's IPC server until it responds to a ping.
|
|
112
|
-
const socketPath = getDaemonSocketPath(session);
|
|
113
|
-
const client = new DaemonClient(socketPath);
|
|
114
|
-
const maxAttempts = Math.ceil(ipcTimeoutMs / IPC_POLL_INTERVAL_MS);
|
|
115
|
-
let ipcReady = false;
|
|
116
|
-
|
|
117
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
118
|
-
// Fail fast on spawn errors. The cast is needed because TypeScript
|
|
119
|
-
// doesn't track that the variable is mutated asynchronously by the
|
|
120
|
-
// child's "error" event handler.
|
|
121
|
-
const spawnError = childSpawnError as Error | null;
|
|
122
|
-
if (spawnError !== null) {
|
|
123
|
-
await onFailure?.();
|
|
124
|
-
const errWithCode = spawnError as Error & { code?: string };
|
|
125
|
-
const hint =
|
|
126
|
-
errWithCode.code === "ENOENT"
|
|
127
|
-
? " Ensure Node.js is available in PATH for child processes."
|
|
128
|
-
: "";
|
|
129
|
-
throw new Error(
|
|
130
|
-
`Failed to spawn daemon: ${spawnError.message}.${hint} Check logs: ${logPath}`,
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Fail fast on early exit.
|
|
135
|
-
const earlyExit = childEarlyExit as {
|
|
136
|
-
code: number | null;
|
|
137
|
-
signal: NodeJS.Signals | null;
|
|
138
|
-
} | null;
|
|
139
|
-
if (earlyExit !== null) {
|
|
140
|
-
await onFailure?.();
|
|
141
|
-
const status = earlyExit.code ?? earlyExit.signal ?? "unknown";
|
|
142
|
-
throw new Error(
|
|
143
|
-
`Daemon exited before startup (status: ${status}). Check logs: ${logPath}`,
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
await new Promise((r) => setTimeout(r, IPC_POLL_INTERVAL_MS));
|
|
148
|
-
ipcReady = await client.ping();
|
|
149
|
-
if (ipcReady) break;
|
|
150
|
-
|
|
151
|
-
if (i > 0 && i % 10 === 0) {
|
|
152
|
-
logger.info("daemon-waiting-for-ipc", { attempt: i, session });
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (!ipcReady) {
|
|
157
|
-
// Kill the orphaned daemon process before reporting failure.
|
|
158
|
-
try {
|
|
159
|
-
process.kill(pid, "SIGTERM");
|
|
160
|
-
} catch {
|
|
161
|
-
// Process may have already exited.
|
|
162
|
-
}
|
|
163
|
-
await onFailure?.();
|
|
164
|
-
throw new Error(
|
|
165
|
-
`Daemon failed to start within ${Math.ceil(ipcTimeoutMs / 1000)}s. Check logs: ${logPath}`,
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
logger.info("daemon-ipc-ready", { session, socketPath });
|
|
170
|
-
return { pid, socketPath, client };
|
|
171
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { unlink } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { getSessionDir } from "./context.js";
|
|
5
|
-
|
|
6
|
-
export type PauseSignalPaths = {
|
|
7
|
-
pausedSignalPath: string;
|
|
8
|
-
resumeSignalPath: string;
|
|
9
|
-
completedSignalPath: string;
|
|
10
|
-
failedSignalPath: string;
|
|
11
|
-
outputSignalPath: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export function getPauseSignalPaths(session: string): PauseSignalPaths {
|
|
15
|
-
const sessionDir = getSessionDir(session);
|
|
16
|
-
return {
|
|
17
|
-
pausedSignalPath: join(sessionDir, `${session}.paused`),
|
|
18
|
-
resumeSignalPath: join(sessionDir, `${session}.resume`),
|
|
19
|
-
completedSignalPath: join(sessionDir, `${session}.completed`),
|
|
20
|
-
failedSignalPath: join(sessionDir, `${session}.failed`),
|
|
21
|
-
outputSignalPath: join(sessionDir, `${session}.output`),
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function removeSignalIfExists(path: string): Promise<void> {
|
|
26
|
-
if (!existsSync(path)) return;
|
|
27
|
-
try {
|
|
28
|
-
await unlink(path);
|
|
29
|
-
} catch (error) {
|
|
30
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
31
|
-
if (code !== "ENOENT") {
|
|
32
|
-
throw error;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|