libretto 0.3.2 → 0.4.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/dist/cli/cli.js +83 -223
- package/dist/cli/commands/ai.js +32 -18
- package/dist/cli/commands/browser.js +126 -85
- package/dist/cli/commands/execution.js +147 -108
- package/dist/cli/commands/init.js +234 -131
- package/dist/cli/commands/logs.js +90 -65
- package/dist/cli/commands/shared.js +50 -0
- package/dist/cli/commands/snapshot.js +62 -37
- package/dist/cli/core/ai-config.js +29 -44
- package/dist/cli/core/api-snapshot-analyzer.js +74 -0
- package/dist/cli/core/context.js +1 -1
- package/dist/cli/core/snapshot-analyzer.js +200 -87
- package/dist/cli/core/snapshot-api-config.js +137 -0
- package/dist/cli/framework/simple-cli.js +776 -0
- package/dist/cli/router.js +29 -0
- package/dist/shared/condense-dom/condense-dom.cjs +462 -0
- package/dist/shared/condense-dom/condense-dom.d.cts +34 -0
- package/dist/shared/condense-dom/condense-dom.d.ts +34 -0
- package/dist/shared/condense-dom/condense-dom.js +438 -0
- package/dist/shared/llm/ai-sdk-adapter.cjs +5 -1
- package/dist/shared/llm/ai-sdk-adapter.js +5 -1
- package/dist/shared/llm/client.cjs +106 -27
- package/dist/shared/llm/client.d.cts +8 -1
- package/dist/shared/llm/client.d.ts +8 -1
- package/dist/shared/llm/client.js +89 -23
- package/dist/shared/llm/types.d.cts +2 -1
- package/dist/shared/llm/types.d.ts +2 -1
- package/package.json +7 -4
- /package/{.agents/skills → skills}/libretto/SKILL.md +0 -0
- /package/{.agents/skills → skills}/libretto/code-generation-rules.md +0 -0
- /package/{.agents/skills → skills}/libretto/integration-approach-selection.md +0 -0
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import * as moduleBuiltin from "node:module";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { z } from "zod";
|
|
5
6
|
import { installInstrumentation } from "../../shared/instrumentation/index.js";
|
|
6
7
|
import {
|
|
7
8
|
connect,
|
|
@@ -12,7 +13,6 @@ import {
|
|
|
12
13
|
assertSessionAvailableForStart,
|
|
13
14
|
clearSessionState,
|
|
14
15
|
readSessionState,
|
|
15
|
-
readSessionStateOrThrow,
|
|
16
16
|
setSessionStatus
|
|
17
17
|
} from "../core/session.js";
|
|
18
18
|
import {
|
|
@@ -20,6 +20,13 @@ import {
|
|
|
20
20
|
readNetworkLog,
|
|
21
21
|
wrapPageForActionLogging
|
|
22
22
|
} from "../core/telemetry.js";
|
|
23
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
24
|
+
import {
|
|
25
|
+
loadSessionStateMiddleware,
|
|
26
|
+
pageOption,
|
|
27
|
+
resolveSessionMiddleware,
|
|
28
|
+
sessionOption
|
|
29
|
+
} from "./shared.js";
|
|
23
30
|
const stripTypeScriptTypes = moduleBuiltin.stripTypeScriptTypes;
|
|
24
31
|
const require2 = moduleBuiltin.createRequire(import.meta.url);
|
|
25
32
|
const tsxCliPath = require2.resolve("tsx/cli");
|
|
@@ -63,7 +70,6 @@ function compileExecFunction(code, helperNames) {
|
|
|
63
70
|
return new AsyncFunction(...helperNames, code);
|
|
64
71
|
}
|
|
65
72
|
async function runExec(code, session, logger, visualize = false, pageId) {
|
|
66
|
-
readSessionStateOrThrow(session);
|
|
67
73
|
logger.info("exec-start", {
|
|
68
74
|
session,
|
|
69
75
|
codeLength: code.length,
|
|
@@ -211,25 +217,24 @@ function readJsonFileIfExists(path) {
|
|
|
211
217
|
return null;
|
|
212
218
|
}
|
|
213
219
|
}
|
|
214
|
-
function
|
|
220
|
+
function readFailureDetails(path) {
|
|
215
221
|
const raw = readJsonFileIfExists(path);
|
|
216
222
|
if (!raw || typeof raw !== "object") return null;
|
|
217
223
|
const message = raw.message;
|
|
218
|
-
if (typeof message !== "string") return null;
|
|
219
224
|
const phase = raw.phase;
|
|
220
225
|
return {
|
|
221
|
-
message,
|
|
226
|
+
message: typeof message === "string" ? message : void 0,
|
|
222
227
|
phase: phase === "setup" || phase === "workflow" ? phase : void 0
|
|
223
228
|
};
|
|
224
229
|
}
|
|
225
|
-
async function
|
|
230
|
+
async function waitForFailureDetails(path, timeoutMs = 1e3) {
|
|
226
231
|
const deadline = Date.now() + timeoutMs;
|
|
227
232
|
while (Date.now() < deadline) {
|
|
228
|
-
const
|
|
229
|
-
if (
|
|
233
|
+
const details = readFailureDetails(path);
|
|
234
|
+
if (details?.message) return details;
|
|
230
235
|
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
231
236
|
}
|
|
232
|
-
return
|
|
237
|
+
return readFailureDetails(path);
|
|
233
238
|
}
|
|
234
239
|
function streamOutputSince(path, offset) {
|
|
235
240
|
if (!existsSync(path)) return offset;
|
|
@@ -255,11 +260,11 @@ async function waitForWorkflowOutcome(args) {
|
|
|
255
260
|
outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
|
|
256
261
|
if (existsSync(signalPaths.failedSignalPath)) {
|
|
257
262
|
outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
|
|
258
|
-
const
|
|
263
|
+
const failureDetails = await waitForFailureDetails(signalPaths.failedSignalPath);
|
|
259
264
|
return {
|
|
260
265
|
status: "failed",
|
|
261
|
-
message:
|
|
262
|
-
|
|
266
|
+
message: failureDetails?.message,
|
|
267
|
+
phase: failureDetails?.phase
|
|
263
268
|
};
|
|
264
269
|
}
|
|
265
270
|
if (existsSync(signalPaths.completedSignalPath)) {
|
|
@@ -277,8 +282,7 @@ async function waitForWorkflowOutcome(args) {
|
|
|
277
282
|
await new Promise((resolveWait) => setTimeout(resolveWait, 250));
|
|
278
283
|
}
|
|
279
284
|
}
|
|
280
|
-
async function runResume(session, logger) {
|
|
281
|
-
const state = readSessionStateOrThrow(session);
|
|
285
|
+
async function runResume(session, logger, sessionState) {
|
|
282
286
|
const {
|
|
283
287
|
pausedSignalPath,
|
|
284
288
|
resumeSignalPath,
|
|
@@ -291,9 +295,9 @@ async function runResume(session, logger) {
|
|
|
291
295
|
`Session "${session}" is not paused. Run "libretto-cli run ... --session ${session}" and call pause() first.`
|
|
292
296
|
);
|
|
293
297
|
}
|
|
294
|
-
if (!isProcessRunning(
|
|
298
|
+
if (!isProcessRunning(sessionState.pid)) {
|
|
295
299
|
throw new Error(
|
|
296
|
-
`No active paused workflow found for session "${session}" (worker pid ${
|
|
300
|
+
`No active paused workflow found for session "${session}" (worker pid ${sessionState.pid} is not running).`
|
|
297
301
|
);
|
|
298
302
|
}
|
|
299
303
|
clearSignalIfExists(pausedSignalPath);
|
|
@@ -316,7 +320,7 @@ async function runResume(session, logger) {
|
|
|
316
320
|
console.log(`Resume signal sent for session "${session}".`);
|
|
317
321
|
const outcome = await waitForWorkflowOutcome({
|
|
318
322
|
session,
|
|
319
|
-
pid:
|
|
323
|
+
pid: sessionState.pid
|
|
320
324
|
});
|
|
321
325
|
if (outcome.status === "completed") {
|
|
322
326
|
setSessionStatus(session, "completed", logger);
|
|
@@ -340,7 +344,6 @@ async function runResume(session, logger) {
|
|
|
340
344
|
}
|
|
341
345
|
async function runIntegrationFromFile(args, logger) {
|
|
342
346
|
await stopExistingFailedRunSession(args.session, logger);
|
|
343
|
-
assertSessionAvailableForStart(args.session, logger);
|
|
344
347
|
const signalPaths = getPauseSignalPaths(args.session);
|
|
345
348
|
clearSignalIfExists(signalPaths.pausedSignalPath);
|
|
346
349
|
clearSignalIfExists(signalPaths.resumeSignalPath);
|
|
@@ -380,14 +383,13 @@ async function runIntegrationFromFile(args, logger) {
|
|
|
380
383
|
}
|
|
381
384
|
if (outcome.status === "failed") {
|
|
382
385
|
setSessionStatus(args.session, "failed", logger);
|
|
383
|
-
|
|
384
|
-
if (outcome.failurePhase === "workflow") {
|
|
386
|
+
if (outcome.phase === "workflow") {
|
|
385
387
|
throw new Error(
|
|
386
|
-
`${message}
|
|
388
|
+
`${outcome.message ?? "Workflow failed during run."}
|
|
387
389
|
Browser is still open. You can use \`exec\` to inspect it. Call \`run\` to re-run the workflow.`
|
|
388
390
|
);
|
|
389
391
|
}
|
|
390
|
-
throw new Error(message);
|
|
392
|
+
throw new Error(outcome.message ?? "Workflow failed during run.");
|
|
391
393
|
}
|
|
392
394
|
if (outcome.status === "exited") {
|
|
393
395
|
setSessionStatus(args.session, "exited", logger);
|
|
@@ -396,95 +398,132 @@ Browser is still open. You can use \`exec\` to inspect it. Call \`run\` to re-ru
|
|
|
396
398
|
);
|
|
397
399
|
}
|
|
398
400
|
setSessionStatus(args.session, "completed", logger);
|
|
401
|
+
console.log("Integration completed.");
|
|
402
|
+
}
|
|
403
|
+
const execInput = SimpleCLI.input({
|
|
404
|
+
positionals: [
|
|
405
|
+
SimpleCLI.positional("codeParts", z.array(z.string()).default([]), {
|
|
406
|
+
help: "Playwright TypeScript code to execute",
|
|
407
|
+
variadic: true
|
|
408
|
+
})
|
|
409
|
+
],
|
|
410
|
+
named: {
|
|
411
|
+
session: sessionOption(),
|
|
412
|
+
visualize: SimpleCLI.flag({ help: "Enable ghost cursor + highlight visualization" }),
|
|
413
|
+
page: pageOption()
|
|
414
|
+
}
|
|
415
|
+
}).refine(
|
|
416
|
+
(input) => input.codeParts.length > 0,
|
|
417
|
+
"Usage: libretto-cli exec <code> [--session <name>] [--visualize]"
|
|
418
|
+
);
|
|
419
|
+
function createExecCommand(logger) {
|
|
420
|
+
return SimpleCLI.command({
|
|
421
|
+
description: "Execute Playwright TypeScript code"
|
|
422
|
+
}).input(execInput).use(resolveSessionMiddleware).use(loadSessionStateMiddleware).handle(async ({ input, ctx }) => {
|
|
423
|
+
await runExec(
|
|
424
|
+
input.codeParts.join(" "),
|
|
425
|
+
ctx.session,
|
|
426
|
+
logger,
|
|
427
|
+
input.visualize,
|
|
428
|
+
input.page
|
|
429
|
+
);
|
|
430
|
+
});
|
|
399
431
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
"
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
432
|
+
const runUsage = "Usage: libretto-cli run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless]";
|
|
433
|
+
const runInput = SimpleCLI.input({
|
|
434
|
+
positionals: [
|
|
435
|
+
SimpleCLI.positional("integrationFile", z.string().optional(), {
|
|
436
|
+
help: "Path to the integration file"
|
|
437
|
+
}),
|
|
438
|
+
SimpleCLI.positional("integrationExport", z.string().optional(), {
|
|
439
|
+
help: "Named workflow export to run"
|
|
440
|
+
})
|
|
441
|
+
],
|
|
442
|
+
named: {
|
|
443
|
+
session: sessionOption(),
|
|
444
|
+
params: SimpleCLI.option(z.string().optional(), {
|
|
445
|
+
help: "Inline JSON params"
|
|
446
|
+
}),
|
|
447
|
+
paramsFile: SimpleCLI.option(z.string().optional(), {
|
|
448
|
+
name: "params-file",
|
|
449
|
+
help: "Path to a JSON params file"
|
|
450
|
+
}),
|
|
451
|
+
tsconfig: SimpleCLI.option(z.string().optional(), {
|
|
452
|
+
help: "Path to a tsconfig used for workflow module resolution"
|
|
453
|
+
}),
|
|
454
|
+
headed: SimpleCLI.flag({ help: "Run in headed mode" }),
|
|
455
|
+
headless: SimpleCLI.flag({ help: "Run in headless mode" }),
|
|
456
|
+
authProfile: SimpleCLI.option(z.string().optional(), {
|
|
457
|
+
name: "auth-profile",
|
|
458
|
+
help: "Domain for local auth profile (e.g. apps.example.com)"
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
}).refine(
|
|
462
|
+
(input) => Boolean(input.integrationFile && input.integrationExport),
|
|
463
|
+
runUsage
|
|
464
|
+
).refine((input) => !(input.params && input.paramsFile), "Pass either --params or --params-file, not both.").refine((input) => !(input.headed && input.headless), "Cannot pass both --headed and --headless.");
|
|
465
|
+
function resolveRunParams(rawInlineParams, paramsFile) {
|
|
466
|
+
if (paramsFile) {
|
|
467
|
+
let content;
|
|
468
|
+
try {
|
|
469
|
+
content = readFileSync(paramsFile, "utf8");
|
|
470
|
+
} catch {
|
|
471
|
+
throw new Error(
|
|
472
|
+
`Could not read --params-file "${paramsFile}". Ensure the file exists and is readable.`
|
|
419
473
|
);
|
|
420
474
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const headlessMode = hasHeadedFlag ? false : hasHeadlessFlag ? true : void 0;
|
|
467
|
-
const authProfileDomain = argv["auth-profile"];
|
|
468
|
-
const tsconfigPath = argv.tsconfig;
|
|
469
|
-
await runIntegrationFromFile({
|
|
470
|
-
integrationPath,
|
|
471
|
-
exportName,
|
|
472
|
-
session,
|
|
473
|
-
params,
|
|
474
|
-
headless: headlessMode ?? false,
|
|
475
|
-
authProfileDomain,
|
|
476
|
-
tsconfigPath
|
|
477
|
-
}, logger);
|
|
478
|
-
}
|
|
479
|
-
).command(
|
|
480
|
-
"resume",
|
|
481
|
-
"Resume a paused workflow for the current session",
|
|
482
|
-
(cmd) => cmd,
|
|
483
|
-
async (argv) => {
|
|
484
|
-
await runResume(String(argv.session), logger);
|
|
485
|
-
}
|
|
486
|
-
);
|
|
475
|
+
return parseJsonArg("--params-file", content);
|
|
476
|
+
}
|
|
477
|
+
if (rawInlineParams) {
|
|
478
|
+
return parseJsonArg("--params", rawInlineParams);
|
|
479
|
+
}
|
|
480
|
+
return {};
|
|
481
|
+
}
|
|
482
|
+
function createRunCommand(logger) {
|
|
483
|
+
return SimpleCLI.command({
|
|
484
|
+
description: "Run an exported Libretto workflow from a file"
|
|
485
|
+
}).input(runInput).use(resolveSessionMiddleware).handle(async ({ input, ctx }) => {
|
|
486
|
+
await stopExistingFailedRunSession(ctx.session, logger);
|
|
487
|
+
assertSessionAvailableForStart(ctx.session, logger);
|
|
488
|
+
const params = resolveRunParams(input.params, input.paramsFile);
|
|
489
|
+
const headlessMode = input.headed ? false : input.headless ? true : void 0;
|
|
490
|
+
await runIntegrationFromFile({
|
|
491
|
+
integrationPath: input.integrationFile,
|
|
492
|
+
exportName: input.integrationExport,
|
|
493
|
+
session: ctx.session,
|
|
494
|
+
params,
|
|
495
|
+
tsconfigPath: input.tsconfig,
|
|
496
|
+
headless: headlessMode ?? false,
|
|
497
|
+
authProfileDomain: input.authProfile
|
|
498
|
+
}, logger);
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
const resumeInput = SimpleCLI.input({
|
|
502
|
+
positionals: [],
|
|
503
|
+
named: {
|
|
504
|
+
session: sessionOption()
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
function createResumeCommand(logger) {
|
|
508
|
+
return SimpleCLI.command({
|
|
509
|
+
description: "Resume a paused workflow for the current session"
|
|
510
|
+
}).input(resumeInput).use(resolveSessionMiddleware).use(loadSessionStateMiddleware).handle(async ({ ctx }) => {
|
|
511
|
+
await runResume(ctx.session, logger, ctx.sessionState);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
function createExecutionCommands(logger) {
|
|
515
|
+
return {
|
|
516
|
+
exec: createExecCommand(logger),
|
|
517
|
+
run: createRunCommand(logger),
|
|
518
|
+
resume: createResumeCommand(logger)
|
|
519
|
+
};
|
|
487
520
|
}
|
|
488
521
|
export {
|
|
489
|
-
|
|
522
|
+
createExecCommand,
|
|
523
|
+
createExecutionCommands,
|
|
524
|
+
createResumeCommand,
|
|
525
|
+
createRunCommand,
|
|
526
|
+
execInput,
|
|
527
|
+
resumeInput,
|
|
528
|
+
runInput
|
|
490
529
|
};
|