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
|
@@ -8,7 +8,9 @@ import type { LoggerApi } from "../../shared/logger/index.js";
|
|
|
8
8
|
import {
|
|
9
9
|
connect,
|
|
10
10
|
disconnectBrowser,
|
|
11
|
+
resolveViewport,
|
|
11
12
|
} from "../core/browser.js";
|
|
13
|
+
import { parseViewportArg } from "./browser.js";
|
|
12
14
|
import { getPauseSignalPaths } from "../core/pause-signals.js";
|
|
13
15
|
import {
|
|
14
16
|
assertSessionAvailableForStart,
|
|
@@ -22,15 +24,13 @@ import {
|
|
|
22
24
|
readNetworkLog,
|
|
23
25
|
wrapPageForActionLogging,
|
|
24
26
|
} from "../core/telemetry.js";
|
|
25
|
-
import type {
|
|
26
|
-
RunIntegrationWorkerRequest,
|
|
27
|
-
} from "../workers/run-integration-worker-protocol.js";
|
|
27
|
+
import type { RunIntegrationWorkerRequest } from "../workers/run-integration-worker-protocol.js";
|
|
28
28
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
29
29
|
import {
|
|
30
|
-
loadSessionStateMiddleware,
|
|
31
30
|
pageOption,
|
|
32
|
-
resolveSessionMiddleware,
|
|
33
31
|
sessionOption,
|
|
32
|
+
withAutoSession,
|
|
33
|
+
withRequiredSession,
|
|
34
34
|
} from "./shared.js";
|
|
35
35
|
|
|
36
36
|
type ExecFunction = (...args: unknown[]) => Promise<unknown>;
|
|
@@ -117,6 +117,87 @@ function compileExecFunction(
|
|
|
117
117
|
return new AsyncFunction(...helperNames, code);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Strip `.catch(() => {})` / `?.catch(() => {})` from executable code,
|
|
122
|
+
* skipping occurrences inside string literals (single, double, backtick)
|
|
123
|
+
* and single-line / multi-line comments so we never corrupt non-code text.
|
|
124
|
+
*/
|
|
125
|
+
function stripEmptyCatchHandlers(code: string): {
|
|
126
|
+
cleaned: string;
|
|
127
|
+
strippedCount: number;
|
|
128
|
+
} {
|
|
129
|
+
const catchRe = /\??\s*\.catch\(\s*\(\)\s*=>\s*\{\s*\}\s*\)/g;
|
|
130
|
+
let strippedCount = 0;
|
|
131
|
+
let result = "";
|
|
132
|
+
let i = 0;
|
|
133
|
+
|
|
134
|
+
while (i < code.length) {
|
|
135
|
+
// Single-line comment
|
|
136
|
+
if (code[i] === "/" && code[i + 1] === "/") {
|
|
137
|
+
const end = code.indexOf("\n", i);
|
|
138
|
+
const slice = end === -1 ? code.slice(i) : code.slice(i, end + 1);
|
|
139
|
+
result += slice;
|
|
140
|
+
i += slice.length;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
// Multi-line comment
|
|
144
|
+
if (code[i] === "/" && code[i + 1] === "*") {
|
|
145
|
+
const end = code.indexOf("*/", i + 2);
|
|
146
|
+
const slice = end === -1 ? code.slice(i) : code.slice(i, end + 2);
|
|
147
|
+
result += slice;
|
|
148
|
+
i += slice.length;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
// String literals
|
|
152
|
+
if (code[i] === '"' || code[i] === "'" || code[i] === "`") {
|
|
153
|
+
const quote = code[i];
|
|
154
|
+
let j = i + 1;
|
|
155
|
+
while (j < code.length) {
|
|
156
|
+
if (code[j] === "\\" && quote !== "`") {
|
|
157
|
+
j += 2;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (code[j] === "\\" && quote === "`") {
|
|
161
|
+
j += 2;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (code[j] === quote) {
|
|
165
|
+
j++;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
// Template literal interpolation — skip nested braces
|
|
169
|
+
if (quote === "`" && code[j] === "$" && code[j + 1] === "{") {
|
|
170
|
+
let depth = 1;
|
|
171
|
+
j += 2;
|
|
172
|
+
while (j < code.length && depth > 0) {
|
|
173
|
+
if (code[j] === "{") depth++;
|
|
174
|
+
else if (code[j] === "}") depth--;
|
|
175
|
+
j++;
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
j++;
|
|
180
|
+
}
|
|
181
|
+
result += code.slice(i, j);
|
|
182
|
+
i = j;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
// Try to match the catch pattern at the current position
|
|
186
|
+
catchRe.lastIndex = i;
|
|
187
|
+
const match = catchRe.exec(code);
|
|
188
|
+
if (match && match.index === i) {
|
|
189
|
+
strippedCount++;
|
|
190
|
+
i += match[0].length;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// Regular character
|
|
194
|
+
result += code[i];
|
|
195
|
+
i++;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { cleaned: result, strippedCount };
|
|
199
|
+
}
|
|
200
|
+
|
|
120
201
|
async function runExec(
|
|
121
202
|
code: string,
|
|
122
203
|
session: string,
|
|
@@ -124,22 +205,26 @@ async function runExec(
|
|
|
124
205
|
visualize = false,
|
|
125
206
|
pageId?: string,
|
|
126
207
|
): Promise<void> {
|
|
208
|
+
const { cleaned: cleanedCode, strippedCount } = stripEmptyCatchHandlers(code);
|
|
209
|
+
if (strippedCount > 0) {
|
|
210
|
+
console.log("(Stripped `.catch(() => {})` — letting errors bubble up)");
|
|
211
|
+
}
|
|
127
212
|
logger.info("exec-start", {
|
|
128
213
|
session,
|
|
129
|
-
codeLength:
|
|
130
|
-
codePreview:
|
|
214
|
+
codeLength: cleanedCode.length,
|
|
215
|
+
codePreview: cleanedCode.slice(0, 200),
|
|
131
216
|
visualize,
|
|
132
217
|
pageId,
|
|
133
218
|
});
|
|
134
|
-
const {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
);
|
|
219
|
+
const {
|
|
220
|
+
browser,
|
|
221
|
+
context,
|
|
222
|
+
page,
|
|
223
|
+
pageId: resolvedPageId,
|
|
224
|
+
} = await connect(session, logger, 10000, {
|
|
225
|
+
pageId,
|
|
226
|
+
requireSinglePage: true,
|
|
227
|
+
});
|
|
143
228
|
|
|
144
229
|
const STALL_THRESHOLD_MS = 60_000;
|
|
145
230
|
let lastActivityTs = Date.now();
|
|
@@ -153,10 +238,10 @@ async function runExec(
|
|
|
153
238
|
logger.warn("exec-stall-warning", {
|
|
154
239
|
session,
|
|
155
240
|
silenceMs,
|
|
156
|
-
codePreview:
|
|
241
|
+
codePreview: cleanedCode.slice(0, 200),
|
|
157
242
|
});
|
|
158
243
|
console.warn(
|
|
159
|
-
`[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1000)}s — exec may be hung (code: ${
|
|
244
|
+
`[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1000)}s — exec may be hung (code: ${cleanedCode.slice(0, 100)}...)`,
|
|
160
245
|
);
|
|
161
246
|
}
|
|
162
247
|
}, STALL_THRESHOLD_MS);
|
|
@@ -166,7 +251,7 @@ async function runExec(
|
|
|
166
251
|
logger.info("exec-interrupted", {
|
|
167
252
|
session,
|
|
168
253
|
duration: Date.now() - execStartTs,
|
|
169
|
-
codePreview:
|
|
254
|
+
codePreview: cleanedCode.slice(0, 200),
|
|
170
255
|
});
|
|
171
256
|
};
|
|
172
257
|
process.on("SIGINT", sigintHandler);
|
|
@@ -181,7 +266,12 @@ async function runExec(
|
|
|
181
266
|
const execState: Record<string, unknown> = {};
|
|
182
267
|
|
|
183
268
|
const networkLog = (
|
|
184
|
-
opts: {
|
|
269
|
+
opts: {
|
|
270
|
+
last?: number;
|
|
271
|
+
filter?: string;
|
|
272
|
+
method?: string;
|
|
273
|
+
pageId?: string;
|
|
274
|
+
} = {},
|
|
185
275
|
) => {
|
|
186
276
|
return readNetworkLog(session, opts);
|
|
187
277
|
};
|
|
@@ -216,7 +306,7 @@ async function runExec(
|
|
|
216
306
|
};
|
|
217
307
|
|
|
218
308
|
const helperNames = Object.keys(helpers);
|
|
219
|
-
const fn = compileExecFunction(
|
|
309
|
+
const fn = compileExecFunction(cleanedCode, helperNames);
|
|
220
310
|
|
|
221
311
|
const result = await fn(...Object.values(helpers));
|
|
222
312
|
logger.info("exec-success", { session, hasResult: result !== undefined });
|
|
@@ -224,12 +314,14 @@ async function runExec(
|
|
|
224
314
|
console.log(
|
|
225
315
|
typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
|
226
316
|
);
|
|
317
|
+
} else {
|
|
318
|
+
console.log("Executed successfully");
|
|
227
319
|
}
|
|
228
320
|
} catch (err) {
|
|
229
321
|
logger.error("exec-error", {
|
|
230
322
|
error: err,
|
|
231
323
|
session,
|
|
232
|
-
codePreview:
|
|
324
|
+
codePreview: cleanedCode.slice(0, 200),
|
|
233
325
|
});
|
|
234
326
|
throw err;
|
|
235
327
|
} finally {
|
|
@@ -273,6 +365,8 @@ async function stopExistingFailedRunSession(
|
|
|
273
365
|
});
|
|
274
366
|
clearSessionState(session, logger);
|
|
275
367
|
|
|
368
|
+
if (existingState.pid == null) return;
|
|
369
|
+
|
|
276
370
|
const stopDeadline = Date.now() + 3_000;
|
|
277
371
|
while (isProcessRunning(existingState.pid) && Date.now() < stopDeadline) {
|
|
278
372
|
await new Promise((resolveWait) => setTimeout(resolveWait, 100));
|
|
@@ -371,11 +465,19 @@ async function waitForWorkflowOutcome(
|
|
|
371
465
|
let outputOffset = 0;
|
|
372
466
|
|
|
373
467
|
while (true) {
|
|
374
|
-
outputOffset = streamOutputSince(
|
|
468
|
+
outputOffset = streamOutputSince(
|
|
469
|
+
signalPaths.outputSignalPath,
|
|
470
|
+
outputOffset,
|
|
471
|
+
);
|
|
375
472
|
|
|
376
473
|
if (existsSync(signalPaths.failedSignalPath)) {
|
|
377
|
-
outputOffset = streamOutputSince(
|
|
378
|
-
|
|
474
|
+
outputOffset = streamOutputSince(
|
|
475
|
+
signalPaths.outputSignalPath,
|
|
476
|
+
outputOffset,
|
|
477
|
+
);
|
|
478
|
+
const failureDetails = await waitForFailureDetails(
|
|
479
|
+
signalPaths.failedSignalPath,
|
|
480
|
+
);
|
|
379
481
|
return {
|
|
380
482
|
status: "failed",
|
|
381
483
|
message: failureDetails?.message,
|
|
@@ -384,17 +486,26 @@ async function waitForWorkflowOutcome(
|
|
|
384
486
|
}
|
|
385
487
|
|
|
386
488
|
if (existsSync(signalPaths.completedSignalPath)) {
|
|
387
|
-
outputOffset = streamOutputSince(
|
|
489
|
+
outputOffset = streamOutputSince(
|
|
490
|
+
signalPaths.outputSignalPath,
|
|
491
|
+
outputOffset,
|
|
492
|
+
);
|
|
388
493
|
return { status: "completed" };
|
|
389
494
|
}
|
|
390
495
|
|
|
391
496
|
if (existsSync(signalPaths.pausedSignalPath)) {
|
|
392
|
-
outputOffset = streamOutputSince(
|
|
497
|
+
outputOffset = streamOutputSince(
|
|
498
|
+
signalPaths.outputSignalPath,
|
|
499
|
+
outputOffset,
|
|
500
|
+
);
|
|
393
501
|
return { status: "paused" };
|
|
394
502
|
}
|
|
395
503
|
|
|
396
504
|
if (!isProcessRunning(args.pid)) {
|
|
397
|
-
outputOffset = streamOutputSince(
|
|
505
|
+
outputOffset = streamOutputSince(
|
|
506
|
+
signalPaths.outputSignalPath,
|
|
507
|
+
outputOffset,
|
|
508
|
+
);
|
|
398
509
|
return { status: "exited" };
|
|
399
510
|
}
|
|
400
511
|
|
|
@@ -421,9 +532,9 @@ async function runResume(
|
|
|
421
532
|
);
|
|
422
533
|
}
|
|
423
534
|
|
|
424
|
-
if (!isProcessRunning(sessionState.pid)) {
|
|
535
|
+
if (sessionState.pid == null || !isProcessRunning(sessionState.pid)) {
|
|
425
536
|
throw new Error(
|
|
426
|
-
`No active paused workflow found for session "${session}" (worker pid ${sessionState.pid} is not running).`,
|
|
537
|
+
`No active paused workflow found for session "${session}" (worker pid ${sessionState.pid ?? "unknown"} is not running).`,
|
|
427
538
|
);
|
|
428
539
|
}
|
|
429
540
|
|
|
@@ -451,7 +562,7 @@ async function runResume(
|
|
|
451
562
|
|
|
452
563
|
const outcome = await waitForWorkflowOutcome({
|
|
453
564
|
session,
|
|
454
|
-
pid: sessionState.pid
|
|
565
|
+
pid: sessionState.pid!,
|
|
455
566
|
});
|
|
456
567
|
|
|
457
568
|
if (outcome.status === "completed") {
|
|
@@ -498,18 +609,24 @@ async function runIntegrationFromFile(
|
|
|
498
609
|
session: args.session,
|
|
499
610
|
params: args.params,
|
|
500
611
|
headless: args.headless,
|
|
612
|
+
visualize: args.visualize,
|
|
501
613
|
authProfileDomain: args.authProfileDomain,
|
|
614
|
+
viewport: args.viewport,
|
|
502
615
|
} satisfies RunIntegrationWorkerRequest);
|
|
503
|
-
const worker = spawn(
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
616
|
+
const worker = spawn(
|
|
617
|
+
process.execPath,
|
|
618
|
+
[
|
|
619
|
+
tsxCliPath,
|
|
620
|
+
...(args.tsconfigPath ? ["--tsconfig", args.tsconfigPath] : []),
|
|
621
|
+
workerEntryPath,
|
|
622
|
+
payload,
|
|
623
|
+
],
|
|
624
|
+
{
|
|
625
|
+
detached: true,
|
|
626
|
+
stdio: "ignore",
|
|
627
|
+
env: process.env,
|
|
628
|
+
},
|
|
629
|
+
);
|
|
513
630
|
worker.unref();
|
|
514
631
|
const outcome = await waitForWorkflowOutcome({
|
|
515
632
|
session: args.session,
|
|
@@ -548,7 +665,9 @@ export const execInput = SimpleCLI.input({
|
|
|
548
665
|
],
|
|
549
666
|
named: {
|
|
550
667
|
session: sessionOption(),
|
|
551
|
-
visualize: SimpleCLI.flag({
|
|
668
|
+
visualize: SimpleCLI.flag({
|
|
669
|
+
help: "Enable ghost cursor + highlight visualization",
|
|
670
|
+
}),
|
|
552
671
|
page: pageOption(),
|
|
553
672
|
},
|
|
554
673
|
}).refine(
|
|
@@ -556,26 +675,22 @@ export const execInput = SimpleCLI.input({
|
|
|
556
675
|
`Usage: libretto exec <code> [--session <name>] [--visualize]`,
|
|
557
676
|
);
|
|
558
677
|
|
|
559
|
-
export
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
);
|
|
574
|
-
});
|
|
575
|
-
}
|
|
678
|
+
export const execCommand = SimpleCLI.command({
|
|
679
|
+
description: "Execute Playwright TypeScript code",
|
|
680
|
+
})
|
|
681
|
+
.input(execInput)
|
|
682
|
+
.use(withRequiredSession())
|
|
683
|
+
.handle(async ({ input, ctx }) => {
|
|
684
|
+
await runExec(
|
|
685
|
+
input.codeParts.join(" "),
|
|
686
|
+
ctx.session,
|
|
687
|
+
ctx.logger,
|
|
688
|
+
input.visualize,
|
|
689
|
+
input.page,
|
|
690
|
+
);
|
|
691
|
+
});
|
|
576
692
|
|
|
577
|
-
const runUsage =
|
|
578
|
-
`Usage: libretto run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless]`;
|
|
693
|
+
const runUsage = `Usage: libretto run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--no-visualize] [--viewport WxH]`;
|
|
579
694
|
|
|
580
695
|
export const runInput = SimpleCLI.input({
|
|
581
696
|
positionals: [
|
|
@@ -600,18 +715,31 @@ export const runInput = SimpleCLI.input({
|
|
|
600
715
|
}),
|
|
601
716
|
headed: SimpleCLI.flag({ help: "Run in headed mode" }),
|
|
602
717
|
headless: SimpleCLI.flag({ help: "Run in headless mode" }),
|
|
718
|
+
noVisualize: SimpleCLI.flag({
|
|
719
|
+
name: "no-visualize",
|
|
720
|
+
help: "Disable ghost cursor + highlight visualization in headed mode",
|
|
721
|
+
}),
|
|
603
722
|
authProfile: SimpleCLI.option(z.string().optional(), {
|
|
604
723
|
name: "auth-profile",
|
|
605
724
|
help: "Domain for local auth profile (e.g. apps.example.com)",
|
|
606
725
|
}),
|
|
726
|
+
viewport: SimpleCLI.option(z.string().optional(), {
|
|
727
|
+
help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)",
|
|
728
|
+
}),
|
|
607
729
|
},
|
|
608
730
|
})
|
|
609
731
|
.refine(
|
|
610
732
|
(input) => Boolean(input.integrationFile && input.integrationExport),
|
|
611
733
|
runUsage,
|
|
612
734
|
)
|
|
613
|
-
.refine(
|
|
614
|
-
|
|
735
|
+
.refine(
|
|
736
|
+
(input) => !(input.params && input.paramsFile),
|
|
737
|
+
"Pass either --params or --params-file, not both.",
|
|
738
|
+
)
|
|
739
|
+
.refine(
|
|
740
|
+
(input) => !(input.headed && input.headless),
|
|
741
|
+
"Cannot pass both --headed and --headless.",
|
|
742
|
+
);
|
|
615
743
|
|
|
616
744
|
function resolveRunParams(
|
|
617
745
|
rawInlineParams: string | undefined,
|
|
@@ -634,34 +762,42 @@ function resolveRunParams(
|
|
|
634
762
|
return {};
|
|
635
763
|
}
|
|
636
764
|
|
|
637
|
-
export
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
765
|
+
export const runCommand = SimpleCLI.command({
|
|
766
|
+
description: "Run an exported Libretto workflow from a file",
|
|
767
|
+
})
|
|
768
|
+
.input(runInput)
|
|
769
|
+
.use(withAutoSession())
|
|
770
|
+
.handle(async ({ input, ctx }) => {
|
|
771
|
+
await stopExistingFailedRunSession(ctx.session, ctx.logger);
|
|
772
|
+
assertSessionAvailableForStart(ctx.session, ctx.logger);
|
|
773
|
+
|
|
774
|
+
const params = resolveRunParams(input.params, input.paramsFile);
|
|
775
|
+
const headlessMode = input.headed
|
|
776
|
+
? false
|
|
777
|
+
: input.headless
|
|
778
|
+
? true
|
|
779
|
+
: undefined;
|
|
780
|
+
const visualize = !input.noVisualize;
|
|
781
|
+
const viewport = resolveViewport(
|
|
782
|
+
parseViewportArg(input.viewport),
|
|
783
|
+
ctx.logger,
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
await runIntegrationFromFile(
|
|
787
|
+
{
|
|
655
788
|
integrationPath: input.integrationFile!,
|
|
656
789
|
exportName: input.integrationExport!,
|
|
657
790
|
session: ctx.session,
|
|
658
791
|
params,
|
|
659
792
|
tsconfigPath: input.tsconfig,
|
|
660
793
|
headless: headlessMode ?? false,
|
|
794
|
+
visualize,
|
|
661
795
|
authProfileDomain: input.authProfile,
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
796
|
+
viewport,
|
|
797
|
+
},
|
|
798
|
+
ctx.logger,
|
|
799
|
+
);
|
|
800
|
+
});
|
|
665
801
|
|
|
666
802
|
export const resumeInput = SimpleCLI.input({
|
|
667
803
|
positionals: [],
|
|
@@ -670,22 +806,17 @@ export const resumeInput = SimpleCLI.input({
|
|
|
670
806
|
},
|
|
671
807
|
});
|
|
672
808
|
|
|
673
|
-
export
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
.
|
|
680
|
-
|
|
681
|
-
await runResume(ctx.session, logger, ctx.sessionState);
|
|
682
|
-
});
|
|
683
|
-
}
|
|
809
|
+
export const resumeCommand = SimpleCLI.command({
|
|
810
|
+
description: "Resume a paused workflow for the current session",
|
|
811
|
+
})
|
|
812
|
+
.input(resumeInput)
|
|
813
|
+
.use(withRequiredSession())
|
|
814
|
+
.handle(async ({ ctx }) => {
|
|
815
|
+
await runResume(ctx.session, ctx.logger, ctx.sessionState);
|
|
816
|
+
});
|
|
684
817
|
|
|
685
|
-
export
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
};
|
|
691
|
-
}
|
|
818
|
+
export const executionCommands = {
|
|
819
|
+
exec: execCommand,
|
|
820
|
+
run: runCommand,
|
|
821
|
+
resume: resumeCommand,
|
|
822
|
+
};
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
appendFileSync,
|
|
4
|
+
cpSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
3
11
|
import { spawnSync } from "node:child_process";
|
|
4
12
|
import { basename, dirname, join } from "node:path";
|
|
5
13
|
import { fileURLToPath } from "node:url";
|
|
@@ -42,7 +50,8 @@ const PROVIDER_CHOICES: ProviderChoice[] = [
|
|
|
42
50
|
key: "4",
|
|
43
51
|
label: "Google Vertex AI",
|
|
44
52
|
envVar: "GOOGLE_CLOUD_PROJECT",
|
|
45
|
-
envHint:
|
|
53
|
+
envHint:
|
|
54
|
+
"Requires gcloud auth application-default login and a GCP project ID",
|
|
46
55
|
},
|
|
47
56
|
];
|
|
48
57
|
|
|
@@ -101,7 +110,9 @@ function printSnapshotApiStatus(): void {
|
|
|
101
110
|
|
|
102
111
|
if (selection && hasProviderCredentials(selection.provider)) {
|
|
103
112
|
console.log(` ✓ Ready: ${selection.model} (${selection.source})`);
|
|
104
|
-
console.log(
|
|
113
|
+
console.log(
|
|
114
|
+
" Snapshot objectives will use the API analyzer by default.",
|
|
115
|
+
);
|
|
105
116
|
console.log(" No further action required.");
|
|
106
117
|
return;
|
|
107
118
|
}
|
|
@@ -117,7 +128,9 @@ function printSnapshotApiStatus(): void {
|
|
|
117
128
|
console.log(
|
|
118
129
|
" Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model.",
|
|
119
130
|
);
|
|
120
|
-
console.log(
|
|
131
|
+
console.log(
|
|
132
|
+
" Run `npx libretto init` interactively to set up credentials.",
|
|
133
|
+
);
|
|
121
134
|
}
|
|
122
135
|
|
|
123
136
|
async function runInteractiveApiSetup(): Promise<void> {
|
|
@@ -132,7 +145,9 @@ async function runInteractiveApiSetup(): Promise<void> {
|
|
|
132
145
|
|
|
133
146
|
if (selection && hasProviderCredentials(selection.provider)) {
|
|
134
147
|
console.log(` ✓ Ready: ${selection.model} (${selection.source})`);
|
|
135
|
-
console.log(
|
|
148
|
+
console.log(
|
|
149
|
+
" Snapshot objectives will use the API analyzer by default.",
|
|
150
|
+
);
|
|
136
151
|
return;
|
|
137
152
|
}
|
|
138
153
|
|
|
@@ -144,7 +159,9 @@ async function runInteractiveApiSetup(): Promise<void> {
|
|
|
144
159
|
});
|
|
145
160
|
|
|
146
161
|
try {
|
|
147
|
-
console.log(
|
|
162
|
+
console.log(
|
|
163
|
+
" Which API provider would you like to use for snapshot analysis?\n",
|
|
164
|
+
);
|
|
148
165
|
for (const choice of PROVIDER_CHOICES) {
|
|
149
166
|
console.log(` ${choice.key}) ${choice.label}`);
|
|
150
167
|
}
|
|
@@ -153,7 +170,9 @@ async function runInteractiveApiSetup(): Promise<void> {
|
|
|
153
170
|
const answer = await promptUser(rl, " Choice: ");
|
|
154
171
|
|
|
155
172
|
if (answer.toLowerCase() === "s" || !answer) {
|
|
156
|
-
console.log(
|
|
173
|
+
console.log(
|
|
174
|
+
"\n Skipped. You can set up API credentials later by rerunning `npx libretto init`.",
|
|
175
|
+
);
|
|
157
176
|
console.log(" Or add credentials directly to your .env file:");
|
|
158
177
|
console.log(" OPENAI_API_KEY=...");
|
|
159
178
|
console.log(" ANTHROPIC_API_KEY=...");
|
|
@@ -255,7 +274,9 @@ async function copySkills(): Promise<void> {
|
|
|
255
274
|
const agentDirs = detectAgentDirs(REPO_ROOT);
|
|
256
275
|
|
|
257
276
|
if (agentDirs.length === 0) {
|
|
258
|
-
console.log(
|
|
277
|
+
console.log(
|
|
278
|
+
"\nSkills: No .agents/ or .claude/ directory found in repo root — skipping.",
|
|
279
|
+
);
|
|
259
280
|
return;
|
|
260
281
|
}
|
|
261
282
|
|
|
@@ -289,7 +310,9 @@ async function copySkills(): Promise<void> {
|
|
|
289
310
|
}
|
|
290
311
|
cpSync(sourceDir, skillDest, { recursive: true });
|
|
291
312
|
const fileCount = readdirSync(skillDest).length;
|
|
292
|
-
console.log(
|
|
313
|
+
console.log(
|
|
314
|
+
` ✓ Copied ${fileCount} skill files to ${name}/skills/libretto/`,
|
|
315
|
+
);
|
|
293
316
|
}
|
|
294
317
|
}
|
|
295
318
|
|
package/src/cli/commands/logs.ts
CHANGED
|
@@ -12,13 +12,15 @@ import {
|
|
|
12
12
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
13
13
|
import {
|
|
14
14
|
integerOption,
|
|
15
|
-
loadSessionStateMiddleware,
|
|
16
15
|
pageOption,
|
|
17
|
-
resolveSessionMiddleware,
|
|
18
16
|
sessionOption,
|
|
17
|
+
withRequiredSession,
|
|
19
18
|
} from "./shared.js";
|
|
20
19
|
|
|
21
|
-
async function resolvePageId(
|
|
20
|
+
async function resolvePageId(
|
|
21
|
+
session: string,
|
|
22
|
+
pageId?: string,
|
|
23
|
+
): Promise<string | undefined> {
|
|
22
24
|
if (!pageId) return undefined;
|
|
23
25
|
const pages = await withSessionLogger(session, async (logger) =>
|
|
24
26
|
listOpenPages(session, logger),
|
|
@@ -48,8 +50,7 @@ export const networkCommand = SimpleCLI.command({
|
|
|
48
50
|
description: "View captured network requests",
|
|
49
51
|
})
|
|
50
52
|
.input(networkInput)
|
|
51
|
-
.use(
|
|
52
|
-
.use(loadSessionStateMiddleware)
|
|
53
|
+
.use(withRequiredSession())
|
|
53
54
|
.handle(async ({ input, ctx }) => {
|
|
54
55
|
if (input.clear) {
|
|
55
56
|
clearNetworkLog(ctx.session);
|
|
@@ -93,8 +94,7 @@ export const actionsCommand = SimpleCLI.command({
|
|
|
93
94
|
description: "View captured actions",
|
|
94
95
|
})
|
|
95
96
|
.input(actionsInput)
|
|
96
|
-
.use(
|
|
97
|
-
.use(loadSessionStateMiddleware)
|
|
97
|
+
.use(withRequiredSession())
|
|
98
98
|
.handle(async ({ input, ctx }) => {
|
|
99
99
|
if (input.clear) {
|
|
100
100
|
clearActionLog(ctx.session);
|