libretto 0.5.4 → 0.5.6
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 +23 -10
- package/README.template.md +23 -10
- package/dist/cli/cli.js +10 -0
- package/dist/cli/commands/ai.js +77 -2
- package/dist/cli/commands/browser.js +71 -6
- package/dist/cli/commands/execution.js +101 -44
- package/dist/cli/commands/setup.js +376 -0
- package/dist/cli/commands/snapshot.js +2 -2
- package/dist/cli/commands/status.js +62 -0
- package/dist/cli/core/{snapshot-api-config.js → ai-model.js} +81 -7
- package/dist/cli/core/api-snapshot-analyzer.js +7 -5
- package/dist/cli/core/browser.js +81 -42
- package/dist/cli/core/{ai-config.js → config.js} +13 -79
- package/dist/cli/core/context.js +1 -25
- package/dist/cli/core/deploy-artifact.js +121 -61
- package/dist/cli/core/readonly-exec.js +231 -0
- package/dist/{shared/llm/client.js → cli/core/resolve-model.js} +4 -68
- package/dist/cli/core/session.js +44 -0
- package/dist/cli/core/skill-version.js +73 -0
- package/dist/cli/core/telemetry.js +1 -54
- package/dist/cli/index.js +1 -7
- package/dist/cli/router.js +4 -4
- package/dist/cli/workers/run-integration-runtime.js +29 -25
- package/dist/cli/workers/run-integration-worker-protocol.js +3 -2
- package/dist/index.d.ts +2 -4
- package/dist/index.js +2 -2
- package/dist/runtime/extract/extract.d.ts +2 -2
- package/dist/runtime/extract/extract.js +4 -2
- package/dist/runtime/extract/index.d.ts +1 -1
- package/dist/runtime/recovery/agent.d.ts +2 -3
- package/dist/runtime/recovery/agent.js +5 -3
- package/dist/runtime/recovery/errors.d.ts +2 -3
- package/dist/runtime/recovery/errors.js +4 -2
- package/dist/runtime/recovery/index.d.ts +1 -2
- package/dist/runtime/recovery/recovery.d.ts +2 -3
- package/dist/runtime/recovery/recovery.js +3 -3
- package/dist/shared/debug/pause.js +4 -21
- package/dist/shared/run/api.d.ts +2 -0
- package/dist/shared/run/browser.d.ts +4 -1
- package/dist/shared/run/browser.js +5 -3
- package/dist/shared/state/index.d.ts +1 -1
- package/dist/shared/state/index.js +2 -0
- package/dist/shared/state/session-state.d.ts +10 -1
- package/dist/shared/state/session-state.js +3 -0
- package/dist/shared/workflow/workflow.d.ts +2 -3
- package/dist/shared/workflow/workflow.js +16 -9
- package/package.json +3 -4
- package/scripts/postinstall.mjs +13 -11
- package/scripts/skills-libretto.mjs +14 -4
- package/skills/AGENTS.md +11 -0
- package/skills/libretto/SKILL.md +30 -9
- package/skills/libretto/references/auth-profiles.md +1 -1
- package/skills/libretto/references/code-generation-rules.md +6 -6
- package/skills/libretto/references/configuration-file-reference.md +11 -6
- package/skills/libretto-readonly/SKILL.md +95 -0
- package/src/cli/cli.ts +10 -0
- package/src/cli/commands/ai.ts +111 -1
- package/src/cli/commands/browser.ts +81 -7
- package/src/cli/commands/execution.ts +128 -61
- package/src/cli/commands/setup.ts +499 -0
- package/src/cli/commands/snapshot.ts +2 -2
- package/src/cli/commands/status.ts +77 -0
- package/src/cli/core/{snapshot-api-config.ts → ai-model.ts} +154 -14
- package/src/cli/core/api-snapshot-analyzer.ts +7 -5
- package/src/cli/core/browser.ts +107 -45
- package/src/cli/core/{ai-config.ts → config.ts} +13 -108
- package/src/cli/core/context.ts +1 -45
- package/src/cli/core/deploy-artifact.ts +141 -71
- package/src/cli/core/readonly-exec.ts +284 -0
- package/src/{shared/llm/client.ts → cli/core/resolve-model.ts} +3 -85
- package/src/cli/core/session.ts +62 -2
- package/src/cli/core/skill-version.ts +93 -0
- package/src/cli/core/telemetry.ts +0 -52
- package/src/cli/index.ts +0 -6
- package/src/cli/router.ts +4 -4
- package/src/cli/workers/run-integration-runtime.ts +36 -31
- package/src/cli/workers/run-integration-worker-protocol.ts +2 -1
- package/src/index.ts +1 -7
- package/src/runtime/extract/extract.ts +6 -5
- package/src/runtime/recovery/agent.ts +5 -4
- package/src/runtime/recovery/errors.ts +4 -3
- package/src/runtime/recovery/recovery.ts +4 -4
- package/src/shared/debug/pause.ts +4 -23
- package/src/shared/run/browser.ts +5 -1
- package/src/shared/state/index.ts +2 -0
- package/src/shared/state/session-state.ts +3 -0
- package/src/shared/workflow/workflow.ts +24 -15
- package/dist/cli/commands/init.js +0 -286
- package/dist/cli/commands/logs.js +0 -117
- package/dist/shared/llm/ai-sdk-adapter.d.ts +0 -22
- package/dist/shared/llm/ai-sdk-adapter.js +0 -49
- package/dist/shared/llm/client.d.ts +0 -13
- package/dist/shared/llm/index.d.ts +0 -5
- package/dist/shared/llm/index.js +0 -6
- package/dist/shared/llm/types.d.ts +0 -67
- package/dist/shared/llm/types.js +0 -0
- package/src/cli/commands/init.ts +0 -331
- package/src/cli/commands/logs.ts +0 -128
- package/src/shared/llm/ai-sdk-adapter.ts +0 -81
- package/src/shared/llm/index.ts +0 -3
- package/src/shared/llm/types.ts +0 -63
|
@@ -13,15 +13,19 @@ import { parseViewportArg } from "./browser.js";
|
|
|
13
13
|
import { getPauseSignalPaths } from "../core/pause-signals.js";
|
|
14
14
|
import {
|
|
15
15
|
assertSessionAvailableForStart,
|
|
16
|
+
assertSessionAllowsCommand,
|
|
16
17
|
clearSessionState,
|
|
17
18
|
readSessionState,
|
|
18
19
|
setSessionStatus
|
|
19
20
|
} from "../core/session.js";
|
|
21
|
+
import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
|
|
20
22
|
import {
|
|
21
23
|
readActionLog,
|
|
22
24
|
readNetworkLog,
|
|
23
25
|
wrapPageForActionLogging
|
|
24
26
|
} from "../core/telemetry.js";
|
|
27
|
+
import { readLibrettoConfig } from "../core/config.js";
|
|
28
|
+
import { createReadonlyExecHelpers } from "../core/readonly-exec.js";
|
|
25
29
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
26
30
|
import {
|
|
27
31
|
pageOption,
|
|
@@ -135,12 +139,15 @@ function stripEmptyCatchHandlers(code) {
|
|
|
135
139
|
}
|
|
136
140
|
return { cleaned: result, strippedCount };
|
|
137
141
|
}
|
|
138
|
-
async function runExec(code, session, logger,
|
|
142
|
+
async function runExec(code, session, logger, options = {}) {
|
|
143
|
+
const visualize = options.visualize ?? false;
|
|
144
|
+
const pageId = options.pageId;
|
|
145
|
+
const mode = options.mode ?? "exec";
|
|
139
146
|
const { cleaned: cleanedCode, strippedCount } = stripEmptyCatchHandlers(code);
|
|
140
147
|
if (strippedCount > 0) {
|
|
141
148
|
console.log("(Stripped `.catch(() => {})` \u2014 letting errors bubble up)");
|
|
142
149
|
}
|
|
143
|
-
logger.info(
|
|
150
|
+
logger.info(`${mode}-start`, {
|
|
144
151
|
session,
|
|
145
152
|
codeLength: cleanedCode.length,
|
|
146
153
|
codePreview: cleanedCode.slice(0, 200),
|
|
@@ -164,57 +171,61 @@ async function runExec(code, session, logger, visualize = false, pageId) {
|
|
|
164
171
|
const stallInterval = setInterval(() => {
|
|
165
172
|
const silenceMs = Date.now() - lastActivityTs;
|
|
166
173
|
if (silenceMs >= STALL_THRESHOLD_MS) {
|
|
167
|
-
logger.warn(
|
|
174
|
+
logger.warn(`${mode}-stall-warning`, {
|
|
168
175
|
session,
|
|
169
176
|
silenceMs,
|
|
170
177
|
codePreview: cleanedCode.slice(0, 200)
|
|
171
178
|
});
|
|
172
179
|
console.warn(
|
|
173
|
-
`[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1e3)}s \u2014
|
|
180
|
+
`[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1e3)}s \u2014 ${mode} may be hung (code: ${cleanedCode.slice(0, 100)}...)`
|
|
174
181
|
);
|
|
175
182
|
}
|
|
176
183
|
}, STALL_THRESHOLD_MS);
|
|
177
184
|
const execStartTs = Date.now();
|
|
178
185
|
const sigintHandler = () => {
|
|
179
|
-
logger.info(
|
|
186
|
+
logger.info(`${mode}-interrupted`, {
|
|
180
187
|
session,
|
|
181
188
|
duration: Date.now() - execStartTs,
|
|
182
189
|
codePreview: cleanedCode.slice(0, 200)
|
|
183
190
|
});
|
|
184
191
|
};
|
|
185
192
|
process.on("SIGINT", sigintHandler);
|
|
186
|
-
|
|
187
|
-
|
|
193
|
+
if (mode === "exec") {
|
|
194
|
+
wrapPageForActionLogging(page, session, resolvedPageId, onActivity);
|
|
195
|
+
}
|
|
196
|
+
if (visualize && mode === "exec") {
|
|
188
197
|
await installInstrumentation(page, { visualize: true, logger });
|
|
189
198
|
}
|
|
190
199
|
try {
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
200
|
+
const helpers = mode === "readonly-exec" ? createReadonlyExecHelpers(page, { onActivity }) : (() => {
|
|
201
|
+
const execState = {};
|
|
202
|
+
const networkLog = (opts = {}) => {
|
|
203
|
+
return readNetworkLog(session, opts);
|
|
204
|
+
};
|
|
205
|
+
const actionLog = (opts = {}) => {
|
|
206
|
+
return readActionLog(session, opts);
|
|
207
|
+
};
|
|
208
|
+
return {
|
|
209
|
+
page,
|
|
210
|
+
context,
|
|
211
|
+
state: execState,
|
|
212
|
+
browser,
|
|
213
|
+
networkLog,
|
|
214
|
+
actionLog,
|
|
215
|
+
console,
|
|
216
|
+
setTimeout,
|
|
217
|
+
setInterval,
|
|
218
|
+
clearTimeout,
|
|
219
|
+
clearInterval,
|
|
220
|
+
fetch,
|
|
221
|
+
URL,
|
|
222
|
+
Buffer
|
|
223
|
+
};
|
|
224
|
+
})();
|
|
214
225
|
const helperNames = Object.keys(helpers);
|
|
215
226
|
const fn = compileExecFunction(cleanedCode, helperNames);
|
|
216
227
|
const result = await fn(...Object.values(helpers));
|
|
217
|
-
logger.info(
|
|
228
|
+
logger.info(`${mode}-success`, { session, hasResult: result !== void 0 });
|
|
218
229
|
if (result !== void 0) {
|
|
219
230
|
console.log(
|
|
220
231
|
typeof result === "string" ? result : JSON.stringify(result, null, 2)
|
|
@@ -223,7 +234,7 @@ async function runExec(code, session, logger, visualize = false, pageId) {
|
|
|
223
234
|
console.log("Executed successfully");
|
|
224
235
|
}
|
|
225
236
|
} catch (err) {
|
|
226
|
-
logger.error(
|
|
237
|
+
logger.error(`${mode}-error`, {
|
|
227
238
|
error: err,
|
|
228
239
|
session,
|
|
229
240
|
codePreview: cleanedCode.slice(0, 200)
|
|
@@ -445,13 +456,13 @@ async function runIntegrationFromFile(args, logger) {
|
|
|
445
456
|
);
|
|
446
457
|
const payload = JSON.stringify({
|
|
447
458
|
integrationPath: args.integrationPath,
|
|
448
|
-
workflowName: args.workflowName,
|
|
449
459
|
session: args.session,
|
|
450
460
|
params: args.params,
|
|
451
461
|
headless: args.headless,
|
|
452
462
|
visualize: args.visualize,
|
|
453
463
|
authProfileDomain: args.authProfileDomain,
|
|
454
|
-
viewport: args.viewport
|
|
464
|
+
viewport: args.viewport,
|
|
465
|
+
accessMode: args.accessMode
|
|
455
466
|
});
|
|
456
467
|
const worker = spawn(
|
|
457
468
|
process.execPath,
|
|
@@ -526,6 +537,7 @@ const execInput = SimpleCLI.input({
|
|
|
526
537
|
const execCommand = SimpleCLI.command({
|
|
527
538
|
description: "Execute Playwright TypeScript code"
|
|
528
539
|
}).input(execInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
|
|
540
|
+
assertSessionAllowsCommand(ctx.sessionState, "exec", ["write-access"]);
|
|
529
541
|
const code = input.code;
|
|
530
542
|
const codeFromArgsOrStdin = code === "-" ? readStdinSync() : code;
|
|
531
543
|
if (codeFromArgsOrStdin === null) {
|
|
@@ -537,18 +549,48 @@ const execCommand = SimpleCLI.command({
|
|
|
537
549
|
codeFromArgsOrStdin,
|
|
538
550
|
ctx.session,
|
|
539
551
|
ctx.logger,
|
|
540
|
-
|
|
541
|
-
|
|
552
|
+
{
|
|
553
|
+
visualize: input.visualize,
|
|
554
|
+
pageId: input.page,
|
|
555
|
+
mode: "exec"
|
|
556
|
+
}
|
|
542
557
|
);
|
|
543
558
|
});
|
|
544
|
-
const
|
|
559
|
+
const readonlyExecInput = SimpleCLI.input({
|
|
560
|
+
positionals: [
|
|
561
|
+
SimpleCLI.positional("code", z.string().optional(), {
|
|
562
|
+
help: "Read-only Playwright TypeScript code to execute"
|
|
563
|
+
})
|
|
564
|
+
],
|
|
565
|
+
named: {
|
|
566
|
+
session: sessionOption(),
|
|
567
|
+
page: pageOption()
|
|
568
|
+
}
|
|
569
|
+
}).refine(
|
|
570
|
+
(input) => input.code !== void 0,
|
|
571
|
+
`Usage: libretto readonly-exec <code|-> [--session <name>] [--page <id>]
|
|
572
|
+
echo '<code>' | libretto readonly-exec - [--session <name>] [--page <id>]`
|
|
573
|
+
);
|
|
574
|
+
const readonlyExecCommand = SimpleCLI.command({
|
|
575
|
+
description: "Execute read-only Playwright inspection code"
|
|
576
|
+
}).input(readonlyExecInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
|
|
577
|
+
const code = input.code;
|
|
578
|
+
const codeFromArgsOrStdin = code === "-" ? readStdinSync() : code;
|
|
579
|
+
if (codeFromArgsOrStdin === null) {
|
|
580
|
+
throw new Error(
|
|
581
|
+
"Missing stdin input for `readonly-exec -`. Pipe inspection code into stdin."
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
await runExec(codeFromArgsOrStdin, ctx.session, ctx.logger, {
|
|
585
|
+
pageId: input.page,
|
|
586
|
+
mode: "readonly-exec"
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
const runUsage = `Usage: libretto run <integrationFile> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--read-only|--write-access] [--no-visualize] [--viewport WxH]`;
|
|
545
590
|
const runInput = SimpleCLI.input({
|
|
546
591
|
positionals: [
|
|
547
592
|
SimpleCLI.positional("integrationFile", z.string().optional(), {
|
|
548
593
|
help: "Path to the integration file"
|
|
549
|
-
}),
|
|
550
|
-
SimpleCLI.positional("workflowName", z.string().optional(), {
|
|
551
|
-
help: "Workflow name to run (from workflow(name, handler))"
|
|
552
594
|
})
|
|
553
595
|
],
|
|
554
596
|
named: {
|
|
@@ -565,6 +607,14 @@ const runInput = SimpleCLI.input({
|
|
|
565
607
|
}),
|
|
566
608
|
headed: SimpleCLI.flag({ help: "Run in headed mode" }),
|
|
567
609
|
headless: SimpleCLI.flag({ help: "Run in headless mode" }),
|
|
610
|
+
readOnly: SimpleCLI.flag({
|
|
611
|
+
name: "read-only",
|
|
612
|
+
help: "Create the session in read-only mode"
|
|
613
|
+
}),
|
|
614
|
+
writeAccess: SimpleCLI.flag({
|
|
615
|
+
name: "write-access",
|
|
616
|
+
help: "Create the session in write-access mode (overrides config default)"
|
|
617
|
+
}),
|
|
568
618
|
noVisualize: SimpleCLI.flag({
|
|
569
619
|
name: "no-visualize",
|
|
570
620
|
help: "Disable ghost cursor + highlight visualization in headed mode"
|
|
@@ -578,7 +628,7 @@ const runInput = SimpleCLI.input({
|
|
|
578
628
|
})
|
|
579
629
|
}
|
|
580
630
|
}).refine(
|
|
581
|
-
(input) => Boolean(input.integrationFile
|
|
631
|
+
(input) => Boolean(input.integrationFile),
|
|
582
632
|
runUsage
|
|
583
633
|
).refine(
|
|
584
634
|
(input) => !(input.params && input.paramsFile),
|
|
@@ -586,6 +636,9 @@ const runInput = SimpleCLI.input({
|
|
|
586
636
|
).refine(
|
|
587
637
|
(input) => !(input.headed && input.headless),
|
|
588
638
|
"Cannot pass both --headed and --headless."
|
|
639
|
+
).refine(
|
|
640
|
+
(input) => !(input.readOnly && input.writeAccess),
|
|
641
|
+
"Cannot pass both --read-only and --write-access."
|
|
589
642
|
);
|
|
590
643
|
function resolveRunParams(rawInlineParams, paramsFile) {
|
|
591
644
|
if (paramsFile) {
|
|
@@ -605,8 +658,9 @@ function resolveRunParams(rawInlineParams, paramsFile) {
|
|
|
605
658
|
return {};
|
|
606
659
|
}
|
|
607
660
|
const runCommand = SimpleCLI.command({
|
|
608
|
-
description: "Run
|
|
661
|
+
description: "Run the default-exported Libretto workflow from a file"
|
|
609
662
|
}).input(runInput).use(withAutoSession()).handle(async ({ input, ctx }) => {
|
|
663
|
+
warnIfInstalledSkillOutOfDate();
|
|
610
664
|
await stopExistingFailedRunSession(ctx.session, ctx.logger);
|
|
611
665
|
assertSessionAvailableForStart(ctx.session, ctx.logger);
|
|
612
666
|
const params = resolveRunParams(input.params, input.paramsFile);
|
|
@@ -619,14 +673,14 @@ const runCommand = SimpleCLI.command({
|
|
|
619
673
|
await runIntegrationFromFile(
|
|
620
674
|
{
|
|
621
675
|
integrationPath: input.integrationFile,
|
|
622
|
-
workflowName: input.workflowName,
|
|
623
676
|
session: ctx.session,
|
|
624
677
|
params,
|
|
625
678
|
tsconfigPath: input.tsconfig,
|
|
626
679
|
headless: headlessMode ?? false,
|
|
627
680
|
visualize,
|
|
628
681
|
authProfileDomain: input.authProfile,
|
|
629
|
-
viewport
|
|
682
|
+
viewport,
|
|
683
|
+
accessMode: input.readOnly ? "read-only" : input.writeAccess ? "write-access" : readLibrettoConfig().sessionMode ?? "write-access"
|
|
630
684
|
},
|
|
631
685
|
ctx.logger
|
|
632
686
|
);
|
|
@@ -644,6 +698,7 @@ const resumeCommand = SimpleCLI.command({
|
|
|
644
698
|
});
|
|
645
699
|
const executionCommands = {
|
|
646
700
|
exec: execCommand,
|
|
701
|
+
"readonly-exec": readonlyExecCommand,
|
|
647
702
|
run: runCommand,
|
|
648
703
|
resume: resumeCommand
|
|
649
704
|
};
|
|
@@ -651,6 +706,8 @@ export {
|
|
|
651
706
|
execCommand,
|
|
652
707
|
execInput,
|
|
653
708
|
executionCommands,
|
|
709
|
+
readonlyExecCommand,
|
|
710
|
+
readonlyExecInput,
|
|
654
711
|
resumeCommand,
|
|
655
712
|
resumeInput,
|
|
656
713
|
runCommand,
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import {
|
|
3
|
+
appendFileSync,
|
|
4
|
+
cpSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { basename, dirname, join } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { writeAiConfig } from "../core/config.js";
|
|
15
|
+
import {
|
|
16
|
+
ensureLibrettoSetup,
|
|
17
|
+
LIBRETTO_CONFIG_PATH,
|
|
18
|
+
REPO_ROOT
|
|
19
|
+
} from "../core/context.js";
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_SNAPSHOT_MODELS,
|
|
22
|
+
loadSnapshotEnv,
|
|
23
|
+
resolveAiSetupStatus
|
|
24
|
+
} from "../core/ai-model.js";
|
|
25
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
26
|
+
const PROVIDER_CHOICES = [
|
|
27
|
+
{
|
|
28
|
+
key: "1",
|
|
29
|
+
label: "OpenAI",
|
|
30
|
+
provider: "openai",
|
|
31
|
+
envVar: "OPENAI_API_KEY",
|
|
32
|
+
envHint: "Get your key at https://platform.openai.com/api-keys"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: "2",
|
|
36
|
+
label: "Anthropic",
|
|
37
|
+
provider: "anthropic",
|
|
38
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
39
|
+
envHint: "Get your key at https://console.anthropic.com/settings/keys"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: "3",
|
|
43
|
+
label: "Google Gemini",
|
|
44
|
+
provider: "google",
|
|
45
|
+
envVar: "GEMINI_API_KEY",
|
|
46
|
+
envHint: "Get your key at https://aistudio.google.com/apikey"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: "4",
|
|
50
|
+
label: "Google Vertex AI",
|
|
51
|
+
provider: "vertex",
|
|
52
|
+
envVar: "GOOGLE_CLOUD_PROJECT",
|
|
53
|
+
envHint: "Requires `gcloud auth application-default login` and a GCP project ID"
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
function promptUser(rl, question) {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
rl.question(question, (answer) => {
|
|
59
|
+
resolve(answer.trim());
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function ensurePinnedDefaultModel(status) {
|
|
64
|
+
if (status.source !== "config") {
|
|
65
|
+
writeAiConfig(status.model);
|
|
66
|
+
return { ...status, source: "config" };
|
|
67
|
+
}
|
|
68
|
+
return status;
|
|
69
|
+
}
|
|
70
|
+
function printHealthySummary(status) {
|
|
71
|
+
console.log(` \u2713 Model: ${status.model}`);
|
|
72
|
+
console.log(` Config: ${LIBRETTO_CONFIG_PATH}`);
|
|
73
|
+
console.log(
|
|
74
|
+
" To change: npx libretto ai configure openai | anthropic | gemini | vertex"
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
function printInvalidAiConfigWarning(status) {
|
|
78
|
+
if (status.kind !== "invalid-config") return;
|
|
79
|
+
console.log(" ! Existing AI config is invalid:");
|
|
80
|
+
for (const line of status.message.split("\n")) {
|
|
81
|
+
console.log(` ${line}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function buildRepairPlan(status) {
|
|
85
|
+
if (status.kind === "configured-missing-credentials") {
|
|
86
|
+
const choice = PROVIDER_CHOICES.find((c) => c.provider === status.provider);
|
|
87
|
+
return {
|
|
88
|
+
kind: "repair-missing-credentials",
|
|
89
|
+
provider: status.provider,
|
|
90
|
+
model: status.model,
|
|
91
|
+
envVar: choice?.envVar ?? `${status.provider.toUpperCase()}_API_KEY`,
|
|
92
|
+
choices: ["enter-matching-credential", "switch-provider", "skip"]
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (status.kind === "invalid-config") {
|
|
96
|
+
return { kind: "repair-invalid-config", message: status.message };
|
|
97
|
+
}
|
|
98
|
+
return { kind: "no-repair-needed" };
|
|
99
|
+
}
|
|
100
|
+
function formatMissingCredentialsMessage(plan) {
|
|
101
|
+
return [
|
|
102
|
+
` \u2717 ${plan.provider} is configured (model: ${plan.model}), but ${plan.envVar} is not set.`
|
|
103
|
+
].join("\n");
|
|
104
|
+
}
|
|
105
|
+
function printSnapshotApiStatus() {
|
|
106
|
+
const status = resolveAiSetupStatus();
|
|
107
|
+
const envPath = join(REPO_ROOT, ".env");
|
|
108
|
+
console.log("\nSnapshot analysis:");
|
|
109
|
+
console.log(
|
|
110
|
+
" Libretto uses direct API calls for snapshot analysis when supported credentials are available."
|
|
111
|
+
);
|
|
112
|
+
console.log(` Credentials are loaded from process env and ${envPath}.`);
|
|
113
|
+
if (status.kind === "ready") {
|
|
114
|
+
const pinned = ensurePinnedDefaultModel(status);
|
|
115
|
+
printHealthySummary(pinned);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
const plan = buildRepairPlan(status);
|
|
119
|
+
if (plan.kind === "repair-missing-credentials") {
|
|
120
|
+
console.log(formatMissingCredentialsMessage(plan));
|
|
121
|
+
console.log(
|
|
122
|
+
` To fix: add ${plan.envVar} to .env, or run \`npx libretto setup\` interactively to repair.`
|
|
123
|
+
);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (plan.kind === "repair-invalid-config") {
|
|
127
|
+
printInvalidAiConfigWarning(status);
|
|
128
|
+
console.log(" Run `npx libretto setup` interactively to reconfigure.");
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
console.log(" \u2717 No snapshot API credentials detected.");
|
|
132
|
+
console.log(" Add one provider to .env:");
|
|
133
|
+
console.log(" OPENAI_API_KEY=...");
|
|
134
|
+
console.log(" ANTHROPIC_API_KEY=...");
|
|
135
|
+
console.log(" GEMINI_API_KEY=... # or GOOGLE_GENERATIVE_AI_API_KEY");
|
|
136
|
+
console.log(
|
|
137
|
+
" GOOGLE_CLOUD_PROJECT=... # plus application default credentials for Vertex"
|
|
138
|
+
);
|
|
139
|
+
console.log(
|
|
140
|
+
" Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
|
|
141
|
+
);
|
|
142
|
+
console.log(
|
|
143
|
+
" Run `npx libretto setup` interactively to set up credentials."
|
|
144
|
+
);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
function writeEnvVar(envVar, value, envPath) {
|
|
148
|
+
let envContent = "";
|
|
149
|
+
if (existsSync(envPath)) {
|
|
150
|
+
envContent = readFileSync(envPath, "utf-8");
|
|
151
|
+
}
|
|
152
|
+
const envLine = `${envVar}=${value}`;
|
|
153
|
+
if (envContent.includes(`${envVar}=`)) {
|
|
154
|
+
const updated = envContent.replace(
|
|
155
|
+
new RegExp(`^${envVar}=.*$`, "m"),
|
|
156
|
+
() => envLine
|
|
157
|
+
);
|
|
158
|
+
writeFileSync(envPath, updated);
|
|
159
|
+
console.log(`
|
|
160
|
+
\u2713 Updated ${envVar} in ${envPath}`);
|
|
161
|
+
} else {
|
|
162
|
+
const separator = envContent && !envContent.endsWith("\n") ? "\n" : "";
|
|
163
|
+
appendFileSync(envPath, `${separator}${envLine}
|
|
164
|
+
`);
|
|
165
|
+
console.log(`
|
|
166
|
+
\u2713 Added ${envVar} to ${envPath}`);
|
|
167
|
+
}
|
|
168
|
+
process.env[envVar] = value;
|
|
169
|
+
}
|
|
170
|
+
async function promptForCredential(rl, choice, envPath, modelOverride) {
|
|
171
|
+
console.log(`
|
|
172
|
+
${choice.label} selected.`);
|
|
173
|
+
console.log(` ${choice.envHint}
|
|
174
|
+
`);
|
|
175
|
+
const apiKeyValue = await promptUser(rl, ` Enter your ${choice.envVar}: `);
|
|
176
|
+
if (!apiKeyValue) {
|
|
177
|
+
console.log("\n No value entered. Skipping API key setup.");
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
writeEnvVar(choice.envVar, apiKeyValue, envPath);
|
|
181
|
+
loadSnapshotEnv();
|
|
182
|
+
const model = modelOverride ?? DEFAULT_SNAPSHOT_MODELS[choice.provider];
|
|
183
|
+
writeAiConfig(model);
|
|
184
|
+
console.log(` \u2713 Snapshot API ready: ${model}`);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
async function promptProviderSelection(rl, envPath) {
|
|
188
|
+
console.log(
|
|
189
|
+
" Which API provider would you like to use for snapshot analysis?\n"
|
|
190
|
+
);
|
|
191
|
+
for (const choice of PROVIDER_CHOICES) {
|
|
192
|
+
console.log(` ${choice.key}) ${choice.label}`);
|
|
193
|
+
}
|
|
194
|
+
console.log(" s) Skip for now\n");
|
|
195
|
+
const answer = await promptUser(rl, " Choice: ");
|
|
196
|
+
if (answer.toLowerCase() === "s" || !answer) {
|
|
197
|
+
printSkipMessage();
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
const selected = PROVIDER_CHOICES.find((choice) => choice.key === answer);
|
|
201
|
+
if (!selected) {
|
|
202
|
+
console.log(`
|
|
203
|
+
Unknown choice "${answer}". Skipping API setup.`);
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
return promptForCredential(rl, selected, envPath);
|
|
207
|
+
}
|
|
208
|
+
function printSkipMessage() {
|
|
209
|
+
console.log(
|
|
210
|
+
"\n Skipped. You can set up API credentials later by rerunning `npx libretto setup`."
|
|
211
|
+
);
|
|
212
|
+
console.log(" Or add credentials directly to your .env file:");
|
|
213
|
+
console.log(" OPENAI_API_KEY=...");
|
|
214
|
+
console.log(" ANTHROPIC_API_KEY=...");
|
|
215
|
+
console.log(" GEMINI_API_KEY=...");
|
|
216
|
+
console.log(
|
|
217
|
+
" Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
async function runInteractiveApiSetup() {
|
|
221
|
+
const status = resolveAiSetupStatus();
|
|
222
|
+
const envPath = join(REPO_ROOT, ".env");
|
|
223
|
+
console.log("\nSnapshot analysis setup:");
|
|
224
|
+
console.log(" Libretto uses direct API calls for snapshot analysis.");
|
|
225
|
+
console.log(` Credentials are loaded from process env and ${envPath}.`);
|
|
226
|
+
if (status.kind === "ready") {
|
|
227
|
+
const pinned = ensurePinnedDefaultModel(status);
|
|
228
|
+
printHealthySummary(pinned);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const plan = buildRepairPlan(status);
|
|
232
|
+
const rl = createInterface({
|
|
233
|
+
input: process.stdin,
|
|
234
|
+
output: process.stdout
|
|
235
|
+
});
|
|
236
|
+
try {
|
|
237
|
+
if (plan.kind === "repair-missing-credentials") {
|
|
238
|
+
console.log(formatMissingCredentialsMessage(plan));
|
|
239
|
+
console.log("");
|
|
240
|
+
console.log(" How would you like to fix this?\n");
|
|
241
|
+
console.log(` 1) Enter ${plan.envVar}`);
|
|
242
|
+
console.log(" 2) Switch to a different provider");
|
|
243
|
+
console.log(" s) Skip for now\n");
|
|
244
|
+
const answer = await promptUser(rl, " Choice: ");
|
|
245
|
+
if (answer === "1") {
|
|
246
|
+
const matchingChoice = PROVIDER_CHOICES.find(
|
|
247
|
+
(c) => c.provider === plan.provider
|
|
248
|
+
);
|
|
249
|
+
if (matchingChoice) {
|
|
250
|
+
await promptForCredential(rl, matchingChoice, envPath, plan.model);
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (answer === "2") {
|
|
255
|
+
await promptProviderSelection(rl, envPath);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
printSkipMessage();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (plan.kind === "repair-invalid-config") {
|
|
262
|
+
printInvalidAiConfigWarning(status);
|
|
263
|
+
console.log(
|
|
264
|
+
"\n Would you like to reconfigure with a fresh provider selection?\n"
|
|
265
|
+
);
|
|
266
|
+
await promptProviderSelection(rl, envPath);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
console.log(" \u2717 No snapshot API credentials detected.\n");
|
|
270
|
+
await promptProviderSelection(rl, envPath);
|
|
271
|
+
} finally {
|
|
272
|
+
rl.close();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function installBrowsers() {
|
|
276
|
+
console.log("\nInstalling Playwright Chromium...");
|
|
277
|
+
const result = spawnSync("npx", ["playwright", "install", "chromium"], {
|
|
278
|
+
stdio: "inherit",
|
|
279
|
+
shell: true
|
|
280
|
+
});
|
|
281
|
+
if (result.status === 0) {
|
|
282
|
+
console.log(" \u2713 Playwright Chromium installed");
|
|
283
|
+
} else {
|
|
284
|
+
console.error(
|
|
285
|
+
" \u2717 Failed to install Playwright Chromium. Run manually: npx playwright install chromium"
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function getPackageSkillsRoot() {
|
|
290
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
291
|
+
let dir = dirname(thisFile);
|
|
292
|
+
while (dir !== dirname(dir)) {
|
|
293
|
+
if (existsSync(join(dir, "skills", "libretto"))) {
|
|
294
|
+
return join(dir, "skills");
|
|
295
|
+
}
|
|
296
|
+
dir = dirname(dir);
|
|
297
|
+
}
|
|
298
|
+
throw new Error("Could not locate libretto skill files in package");
|
|
299
|
+
}
|
|
300
|
+
function detectAgentDirs(root) {
|
|
301
|
+
const dirs = [];
|
|
302
|
+
if (existsSync(join(root, ".agents"))) dirs.push(join(root, ".agents"));
|
|
303
|
+
if (existsSync(join(root, ".claude"))) dirs.push(join(root, ".claude"));
|
|
304
|
+
return dirs;
|
|
305
|
+
}
|
|
306
|
+
function copySkills() {
|
|
307
|
+
const agentDirs = detectAgentDirs(REPO_ROOT);
|
|
308
|
+
if (agentDirs.length === 0) {
|
|
309
|
+
console.log(
|
|
310
|
+
"\nSkills: No .agents/ or .claude/ directory found in repo root \u2014 skipping."
|
|
311
|
+
);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
let skillsRoot;
|
|
315
|
+
try {
|
|
316
|
+
skillsRoot = getPackageSkillsRoot();
|
|
317
|
+
} catch (e) {
|
|
318
|
+
console.error(` \u2717 ${e instanceof Error ? e.message : String(e)}`);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const skillNames = readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
322
|
+
for (const agentDir of agentDirs) {
|
|
323
|
+
const agentName = basename(agentDir);
|
|
324
|
+
for (const skillName of skillNames) {
|
|
325
|
+
const sourceDir = join(skillsRoot, skillName);
|
|
326
|
+
const skillDest = join(agentDir, "skills", skillName);
|
|
327
|
+
if (existsSync(skillDest)) {
|
|
328
|
+
rmSync(skillDest, { recursive: true });
|
|
329
|
+
}
|
|
330
|
+
cpSync(sourceDir, skillDest, { recursive: true });
|
|
331
|
+
const fileCount = readdirSync(skillDest).length;
|
|
332
|
+
console.log(
|
|
333
|
+
` \u2713 Copied ${fileCount} skill files to ${agentName}/skills/${skillName}/`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const setupInput = SimpleCLI.input({
|
|
339
|
+
positionals: [],
|
|
340
|
+
named: {
|
|
341
|
+
skipBrowsers: SimpleCLI.flag({
|
|
342
|
+
name: "skip-browsers",
|
|
343
|
+
help: "Skip Playwright Chromium installation"
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
const setupCommand = SimpleCLI.command({
|
|
348
|
+
description: "Set up libretto in the current project"
|
|
349
|
+
}).input(setupInput).handle(async ({ input }) => {
|
|
350
|
+
console.log("Setting up libretto...\n");
|
|
351
|
+
ensureLibrettoSetup();
|
|
352
|
+
if (!input.skipBrowsers) {
|
|
353
|
+
installBrowsers();
|
|
354
|
+
} else {
|
|
355
|
+
console.log("\nSkipping browser installation (--skip-browsers)");
|
|
356
|
+
}
|
|
357
|
+
copySkills();
|
|
358
|
+
if (process.stdin.isTTY) {
|
|
359
|
+
await runInteractiveApiSetup();
|
|
360
|
+
} else {
|
|
361
|
+
const ready = printSnapshotApiStatus();
|
|
362
|
+
if (!ready) {
|
|
363
|
+
console.log(
|
|
364
|
+
"\nIf you're an agent, request the user to run `npx libretto setup`."
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
console.log("\n\u2713 libretto setup complete");
|
|
369
|
+
});
|
|
370
|
+
export {
|
|
371
|
+
PROVIDER_CHOICES,
|
|
372
|
+
buildRepairPlan,
|
|
373
|
+
formatMissingCredentialsMessage,
|
|
374
|
+
setupCommand,
|
|
375
|
+
setupInput
|
|
376
|
+
};
|
|
@@ -7,8 +7,8 @@ import { readSessionState } from "../core/session.js";
|
|
|
7
7
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
8
8
|
import { pageOption, sessionOption, withRequiredSession } from "./shared.js";
|
|
9
9
|
import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
|
|
10
|
-
import { readAiConfig } from "../core/
|
|
11
|
-
import { resolveSnapshotApiModelOrThrow } from "../core/
|
|
10
|
+
import { readAiConfig } from "../core/config.js";
|
|
11
|
+
import { resolveSnapshotApiModelOrThrow } from "../core/ai-model.js";
|
|
12
12
|
const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 };
|
|
13
13
|
function generateSnapshotRunId() {
|
|
14
14
|
return `snapshot-${Date.now()}`;
|