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
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
type MinimalLogger,
|
|
4
4
|
defaultLogger,
|
|
5
5
|
} from "../../shared/logger/logger.js";
|
|
6
|
-
import type
|
|
6
|
+
import { generateObject, type LanguageModel } from "ai";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -47,7 +47,7 @@ export async function detectSubmissionError(
|
|
|
47
47
|
page: Page,
|
|
48
48
|
error: unknown,
|
|
49
49
|
logContext: string,
|
|
50
|
-
|
|
50
|
+
model: LanguageModel,
|
|
51
51
|
knownErrors: KnownSubmissionError[] = [],
|
|
52
52
|
logger?: MinimalLogger,
|
|
53
53
|
): Promise<DetectedSubmissionError> {
|
|
@@ -109,7 +109,8 @@ IMPORTANT:
|
|
|
109
109
|
|
|
110
110
|
${domSnapshot ? `<dom_snapshot>\n${domSnapshot}\n</dom_snapshot>` : ""}`;
|
|
111
111
|
|
|
112
|
-
const result = await
|
|
112
|
+
const { object: result } = await generateObject({
|
|
113
|
+
model,
|
|
113
114
|
schema: detectSubmissionErrorSchema,
|
|
114
115
|
messages: [
|
|
115
116
|
{
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
type MinimalLogger,
|
|
4
4
|
defaultLogger,
|
|
5
5
|
} from "../../shared/logger/logger.js";
|
|
6
|
-
import type {
|
|
6
|
+
import type { LanguageModel } from "ai";
|
|
7
7
|
import { executeRecoveryAgent } from "./agent.js";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -14,7 +14,7 @@ export async function attemptWithRecovery<T>(
|
|
|
14
14
|
page: Page,
|
|
15
15
|
fn: () => Promise<T>,
|
|
16
16
|
logger?: MinimalLogger,
|
|
17
|
-
|
|
17
|
+
model?: LanguageModel,
|
|
18
18
|
): Promise<T> {
|
|
19
19
|
const log = logger ?? defaultLogger;
|
|
20
20
|
try {
|
|
@@ -33,7 +33,7 @@ export async function attemptWithRecovery<T>(
|
|
|
33
33
|
throw error;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
if (!
|
|
36
|
+
if (!model) {
|
|
37
37
|
throw error;
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -45,7 +45,7 @@ export async function attemptWithRecovery<T>(
|
|
|
45
45
|
page,
|
|
46
46
|
"Look at the page to see if there is a popup blocking the screen. If so, close the popup.",
|
|
47
47
|
log,
|
|
48
|
-
|
|
48
|
+
model,
|
|
49
49
|
);
|
|
50
50
|
|
|
51
51
|
return await fn();
|
|
@@ -5,35 +5,16 @@ import {
|
|
|
5
5
|
getPauseSignalPaths,
|
|
6
6
|
removeSignalIfExists,
|
|
7
7
|
} from "../../cli/core/pause-signals.js";
|
|
8
|
-
import {
|
|
9
|
-
listSessionsWithStateFile,
|
|
10
|
-
readSessionState,
|
|
11
|
-
} from "../../cli/core/session.js";
|
|
12
|
-
|
|
13
|
-
function isPidRunning(pid: number): boolean {
|
|
14
|
-
try {
|
|
15
|
-
process.kill(pid, 0);
|
|
16
|
-
return true;
|
|
17
|
-
} catch {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function getRunningSessions(): string[] {
|
|
23
|
-
return listSessionsWithStateFile().filter((candidate) => {
|
|
24
|
-
const state = readSessionState(candidate);
|
|
25
|
-
return state !== null && state.pid != null && isPidRunning(state.pid);
|
|
26
|
-
});
|
|
27
|
-
}
|
|
8
|
+
import { listRunningSessions } from "../../cli/core/session.js";
|
|
28
9
|
|
|
29
10
|
function throwMissingSessionError(): never {
|
|
30
|
-
const runningSessions =
|
|
11
|
+
const runningSessions = listRunningSessions();
|
|
31
12
|
const lines = ["pause(session) requires a non-empty session ID."];
|
|
32
13
|
|
|
33
14
|
if (runningSessions.length > 0) {
|
|
34
15
|
lines.push("", "Running sessions:");
|
|
35
|
-
for (const
|
|
36
|
-
lines.push(` ${
|
|
16
|
+
for (const s of runningSessions) {
|
|
17
|
+
lines.push(` ${s.session}`);
|
|
37
18
|
}
|
|
38
19
|
}
|
|
39
20
|
|
|
@@ -9,9 +9,10 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
9
9
|
import { ensureLibrettoSessionStatePath } from "../paths/paths.js";
|
|
10
10
|
import {
|
|
11
11
|
SESSION_STATE_VERSION,
|
|
12
|
+
type SessionAccessMode,
|
|
12
13
|
SessionStateFileSchema,
|
|
13
14
|
} from "../state/session-state.js";
|
|
14
|
-
import { readLibrettoConfig } from "../../cli/core/
|
|
15
|
+
import { readLibrettoConfig } from "../../cli/core/config.js";
|
|
15
16
|
|
|
16
17
|
async function pickFreePort(): Promise<number> {
|
|
17
18
|
return await new Promise((resolve, reject) => {
|
|
@@ -34,6 +35,7 @@ export type LaunchBrowserArgs = {
|
|
|
34
35
|
headless?: boolean;
|
|
35
36
|
viewport?: { width: number; height: number };
|
|
36
37
|
storageStatePath?: string;
|
|
38
|
+
accessMode?: SessionAccessMode;
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
export type BrowserSession = {
|
|
@@ -97,6 +99,7 @@ export async function launchBrowser({
|
|
|
97
99
|
headless = false,
|
|
98
100
|
viewport = { width: 1366, height: 768 },
|
|
99
101
|
storageStatePath,
|
|
102
|
+
accessMode = "write-access",
|
|
100
103
|
}: LaunchBrowserArgs): Promise<BrowserSession> {
|
|
101
104
|
const debugPort = await pickFreePort();
|
|
102
105
|
const windowPosition = headless ? undefined : resolveWindowPosition();
|
|
@@ -141,6 +144,7 @@ export async function launchBrowser({
|
|
|
141
144
|
pid: process.pid,
|
|
142
145
|
startedAt: new Date().toISOString(),
|
|
143
146
|
status: "active",
|
|
147
|
+
mode: accessMode,
|
|
144
148
|
},
|
|
145
149
|
null,
|
|
146
150
|
2,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export {
|
|
2
|
+
SessionAccessModeSchema,
|
|
2
3
|
SESSION_STATE_VERSION,
|
|
3
4
|
SessionStatusSchema,
|
|
4
5
|
SessionStateFileSchema,
|
|
5
6
|
parseSessionStateData,
|
|
6
7
|
parseSessionStateContent,
|
|
7
8
|
serializeSessionState,
|
|
9
|
+
type SessionAccessMode,
|
|
8
10
|
type SessionStatus,
|
|
9
11
|
type SessionState,
|
|
10
12
|
type SessionStateFile,
|
|
@@ -9,6 +9,7 @@ export const SessionStatusSchema = z.enum([
|
|
|
9
9
|
"failed",
|
|
10
10
|
"exited",
|
|
11
11
|
]);
|
|
12
|
+
export const SessionAccessModeSchema = z.enum(["read-only", "write-access"]);
|
|
12
13
|
export const SessionViewportSchema = z.object({
|
|
13
14
|
width: z.number().int().min(1),
|
|
14
15
|
height: z.number().int().min(1),
|
|
@@ -22,10 +23,12 @@ export const SessionStateFileSchema = z.object({
|
|
|
22
23
|
session: z.string().min(1),
|
|
23
24
|
startedAt: z.string().datetime({ offset: true }),
|
|
24
25
|
status: SessionStatusSchema.optional(),
|
|
26
|
+
mode: SessionAccessModeSchema.default("write-access"),
|
|
25
27
|
viewport: SessionViewportSchema.optional(),
|
|
26
28
|
});
|
|
27
29
|
|
|
28
30
|
export type SessionStatus = z.infer<typeof SessionStatusSchema>;
|
|
31
|
+
export type SessionAccessMode = z.infer<typeof SessionAccessModeSchema>;
|
|
29
32
|
export type SessionStateFile = z.infer<typeof SessionStateFileSchema>;
|
|
30
33
|
export type SessionState = Omit<SessionStateFile, "version">;
|
|
31
34
|
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import type { Page } from "playwright";
|
|
2
|
-
import type { MinimalLogger } from "../logger/logger.js";
|
|
3
2
|
|
|
4
3
|
export const LIBRETTO_WORKFLOW_BRAND = Symbol.for("libretto.workflow");
|
|
5
4
|
|
|
6
5
|
export type LibrettoWorkflowContext = {
|
|
7
6
|
session: string;
|
|
8
7
|
page: Page;
|
|
9
|
-
logger: MinimalLogger;
|
|
10
8
|
};
|
|
11
9
|
|
|
12
10
|
export type LibrettoWorkflowHandler<Input = unknown, Output = unknown> = (
|
|
@@ -19,10 +17,7 @@ export class LibrettoWorkflow<Input = unknown, Output = unknown> {
|
|
|
19
17
|
public readonly name: string;
|
|
20
18
|
private readonly handler: LibrettoWorkflowHandler<Input, Output>;
|
|
21
19
|
|
|
22
|
-
constructor(
|
|
23
|
-
name: string,
|
|
24
|
-
handler: LibrettoWorkflowHandler<Input, Output>,
|
|
25
|
-
) {
|
|
20
|
+
constructor(name: string, handler: LibrettoWorkflowHandler<Input, Output>) {
|
|
26
21
|
this.name = name;
|
|
27
22
|
this.handler = handler;
|
|
28
23
|
}
|
|
@@ -72,31 +67,45 @@ function addWorkflowOrThrow(
|
|
|
72
67
|
workflowsByName.set(value.name, value);
|
|
73
68
|
}
|
|
74
69
|
|
|
70
|
+
function collectWorkflowsOrThrow(
|
|
71
|
+
values: Iterable<unknown>,
|
|
72
|
+
): ExportedLibrettoWorkflow[] {
|
|
73
|
+
const workflowsByName = new Map<string, ExportedLibrettoWorkflow>();
|
|
74
|
+
|
|
75
|
+
for (const value of values) {
|
|
76
|
+
addWorkflowOrThrow(workflowsByName, value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return [...workflowsByName.values()];
|
|
80
|
+
}
|
|
81
|
+
|
|
75
82
|
export function getWorkflowsFromModuleExports(
|
|
76
83
|
moduleExports: WorkflowModuleExports,
|
|
77
84
|
): ExportedLibrettoWorkflow[] {
|
|
78
|
-
const
|
|
85
|
+
const discoveredValues: unknown[] = [];
|
|
79
86
|
|
|
80
87
|
for (const [exportName, value] of Object.entries(moduleExports)) {
|
|
81
88
|
if (exportName === "workflows" && value && typeof value === "object") {
|
|
82
89
|
// Support both `export const workflows = workflow(...)` and
|
|
83
90
|
// `export const workflows = { myWorkflow }`.
|
|
84
91
|
if (isLibrettoWorkflow(value)) {
|
|
85
|
-
|
|
92
|
+
discoveredValues.push(value);
|
|
86
93
|
} else {
|
|
87
|
-
|
|
88
|
-
value as Record<string, unknown>,
|
|
89
|
-
)) {
|
|
90
|
-
addWorkflowOrThrow(workflowsByName, nestedValue);
|
|
91
|
-
}
|
|
94
|
+
discoveredValues.push(...Object.values(value as Record<string, unknown>));
|
|
92
95
|
}
|
|
93
96
|
continue;
|
|
94
97
|
}
|
|
95
98
|
|
|
96
|
-
|
|
99
|
+
discoveredValues.push(value);
|
|
97
100
|
}
|
|
98
101
|
|
|
99
|
-
return
|
|
102
|
+
return collectWorkflowsOrThrow(discoveredValues);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getDefaultWorkflowFromModuleExports(
|
|
106
|
+
moduleExports: WorkflowModuleExports,
|
|
107
|
+
): ExportedLibrettoWorkflow | null {
|
|
108
|
+
return isLibrettoWorkflow(moduleExports.default) ? moduleExports.default : null;
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
export function getWorkflowFromModuleExports(
|
|
@@ -1,286 +0,0 @@
|
|
|
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 { readAiConfig } from "../core/ai-config.js";
|
|
15
|
-
import { REPO_ROOT } from "../core/context.js";
|
|
16
|
-
import {
|
|
17
|
-
loadSnapshotEnv,
|
|
18
|
-
resolveSnapshotApiModel
|
|
19
|
-
} from "../core/snapshot-api-config.js";
|
|
20
|
-
import { hasProviderCredentials } from "../../shared/llm/client.js";
|
|
21
|
-
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
22
|
-
const PROVIDER_CHOICES = [
|
|
23
|
-
{
|
|
24
|
-
key: "1",
|
|
25
|
-
label: "OpenAI",
|
|
26
|
-
envVar: "OPENAI_API_KEY",
|
|
27
|
-
envHint: "Get your key at https://platform.openai.com/api-keys"
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
key: "2",
|
|
31
|
-
label: "Anthropic",
|
|
32
|
-
envVar: "ANTHROPIC_API_KEY",
|
|
33
|
-
envHint: "Get your key at https://console.anthropic.com/settings/keys"
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
key: "3",
|
|
37
|
-
label: "Google Gemini",
|
|
38
|
-
envVar: "GEMINI_API_KEY",
|
|
39
|
-
envHint: "Get your key at https://aistudio.google.com/apikey"
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
key: "4",
|
|
43
|
-
label: "Google Vertex AI",
|
|
44
|
-
envVar: "GOOGLE_CLOUD_PROJECT",
|
|
45
|
-
envHint: "Requires gcloud auth application-default login and a GCP project ID"
|
|
46
|
-
}
|
|
47
|
-
];
|
|
48
|
-
function promptUser(rl, question) {
|
|
49
|
-
return new Promise((resolve) => {
|
|
50
|
-
rl.question(question, (answer) => {
|
|
51
|
-
resolve(answer.trim());
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
function safeReadAiConfig() {
|
|
56
|
-
try {
|
|
57
|
-
return readAiConfig();
|
|
58
|
-
} catch {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
function printInvalidAiConfigWarning() {
|
|
63
|
-
try {
|
|
64
|
-
readAiConfig();
|
|
65
|
-
} catch (error) {
|
|
66
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
-
console.log(" ! Existing AI config is invalid:");
|
|
68
|
-
for (const line of message.split("\n")) {
|
|
69
|
-
console.log(` ${line}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
function printSnapshotApiStatus() {
|
|
74
|
-
const config = safeReadAiConfig();
|
|
75
|
-
const selection = resolveSnapshotApiModel(config);
|
|
76
|
-
const envPath = join(REPO_ROOT, ".env");
|
|
77
|
-
console.log("\nSnapshot analysis:");
|
|
78
|
-
console.log(
|
|
79
|
-
" Libretto uses direct API calls for snapshot analysis when supported credentials are available."
|
|
80
|
-
);
|
|
81
|
-
console.log(` Credentials are loaded from process env and ${envPath}.`);
|
|
82
|
-
printInvalidAiConfigWarning();
|
|
83
|
-
if (selection && hasProviderCredentials(selection.provider)) {
|
|
84
|
-
console.log(` \u2713 Ready: ${selection.model} (${selection.source})`);
|
|
85
|
-
console.log(
|
|
86
|
-
" Snapshot objectives will use the API analyzer by default."
|
|
87
|
-
);
|
|
88
|
-
console.log(" No further action required.");
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
console.log(" \u2717 No snapshot API credentials detected.");
|
|
92
|
-
console.log(" Add one provider to .env:");
|
|
93
|
-
console.log(" OPENAI_API_KEY=...");
|
|
94
|
-
console.log(" ANTHROPIC_API_KEY=...");
|
|
95
|
-
console.log(" GEMINI_API_KEY=... # or GOOGLE_GENERATIVE_AI_API_KEY");
|
|
96
|
-
console.log(
|
|
97
|
-
" GOOGLE_CLOUD_PROJECT=... # plus application default credentials for Vertex"
|
|
98
|
-
);
|
|
99
|
-
console.log(
|
|
100
|
-
" Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
|
|
101
|
-
);
|
|
102
|
-
console.log(
|
|
103
|
-
" Run `npx libretto init` interactively to set up credentials."
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
async function runInteractiveApiSetup() {
|
|
107
|
-
const config = safeReadAiConfig();
|
|
108
|
-
const selection = resolveSnapshotApiModel(config);
|
|
109
|
-
const envPath = join(REPO_ROOT, ".env");
|
|
110
|
-
console.log("\nSnapshot analysis setup:");
|
|
111
|
-
console.log(" Libretto uses direct API calls for snapshot analysis.");
|
|
112
|
-
console.log(` Credentials are loaded from process env and ${envPath}.`);
|
|
113
|
-
printInvalidAiConfigWarning();
|
|
114
|
-
if (selection && hasProviderCredentials(selection.provider)) {
|
|
115
|
-
console.log(` \u2713 Ready: ${selection.model} (${selection.source})`);
|
|
116
|
-
console.log(
|
|
117
|
-
" Snapshot objectives will use the API analyzer by default."
|
|
118
|
-
);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
console.log(" \u2717 No snapshot API credentials detected.\n");
|
|
122
|
-
const rl = createInterface({
|
|
123
|
-
input: process.stdin,
|
|
124
|
-
output: process.stdout
|
|
125
|
-
});
|
|
126
|
-
try {
|
|
127
|
-
console.log(
|
|
128
|
-
" Which API provider would you like to use for snapshot analysis?\n"
|
|
129
|
-
);
|
|
130
|
-
for (const choice of PROVIDER_CHOICES) {
|
|
131
|
-
console.log(` ${choice.key}) ${choice.label}`);
|
|
132
|
-
}
|
|
133
|
-
console.log(" s) Skip for now\n");
|
|
134
|
-
const answer = await promptUser(rl, " Choice: ");
|
|
135
|
-
if (answer.toLowerCase() === "s" || !answer) {
|
|
136
|
-
console.log(
|
|
137
|
-
"\n Skipped. You can set up API credentials later by rerunning `npx libretto init`."
|
|
138
|
-
);
|
|
139
|
-
console.log(" Or add credentials directly to your .env file:");
|
|
140
|
-
console.log(" OPENAI_API_KEY=...");
|
|
141
|
-
console.log(" ANTHROPIC_API_KEY=...");
|
|
142
|
-
console.log(" GEMINI_API_KEY=...");
|
|
143
|
-
console.log(
|
|
144
|
-
" Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
|
|
145
|
-
);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
const selected = PROVIDER_CHOICES.find((choice) => choice.key === answer);
|
|
149
|
-
if (!selected) {
|
|
150
|
-
console.log(`
|
|
151
|
-
Unknown choice "${answer}". Skipping API setup.`);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
console.log(`
|
|
155
|
-
${selected.label} selected.`);
|
|
156
|
-
console.log(` ${selected.envHint}
|
|
157
|
-
`);
|
|
158
|
-
const apiKeyValue = await promptUser(
|
|
159
|
-
rl,
|
|
160
|
-
` Enter your ${selected.envVar}: `
|
|
161
|
-
);
|
|
162
|
-
if (!apiKeyValue) {
|
|
163
|
-
console.log("\n No value entered. Skipping API key setup.");
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
let envContent = "";
|
|
167
|
-
if (existsSync(envPath)) {
|
|
168
|
-
envContent = readFileSync(envPath, "utf-8");
|
|
169
|
-
}
|
|
170
|
-
const envLine = `${selected.envVar}=${apiKeyValue}`;
|
|
171
|
-
if (envContent.includes(`${selected.envVar}=`)) {
|
|
172
|
-
const updated = envContent.replace(
|
|
173
|
-
new RegExp(`^${selected.envVar}=.*$`, "m"),
|
|
174
|
-
() => envLine
|
|
175
|
-
);
|
|
176
|
-
writeFileSync(envPath, updated);
|
|
177
|
-
console.log(`
|
|
178
|
-
\u2713 Updated ${selected.envVar} in ${envPath}`);
|
|
179
|
-
} else {
|
|
180
|
-
const separator = envContent && !envContent.endsWith("\n") ? "\n" : "";
|
|
181
|
-
appendFileSync(envPath, `${separator}${envLine}
|
|
182
|
-
`);
|
|
183
|
-
console.log(`
|
|
184
|
-
\u2713 Added ${selected.envVar} to ${envPath}`);
|
|
185
|
-
}
|
|
186
|
-
loadSnapshotEnv();
|
|
187
|
-
process.env[selected.envVar] = apiKeyValue;
|
|
188
|
-
const newSelection = resolveSnapshotApiModel(safeReadAiConfig());
|
|
189
|
-
if (newSelection && hasProviderCredentials(newSelection.provider)) {
|
|
190
|
-
console.log(` \u2713 Snapshot API ready: ${newSelection.model}`);
|
|
191
|
-
}
|
|
192
|
-
} finally {
|
|
193
|
-
rl.close();
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
function installBrowsers() {
|
|
197
|
-
console.log("\nInstalling Playwright Chromium...");
|
|
198
|
-
const result = spawnSync("npx", ["playwright", "install", "chromium"], {
|
|
199
|
-
stdio: "inherit",
|
|
200
|
-
shell: true
|
|
201
|
-
});
|
|
202
|
-
if (result.status === 0) {
|
|
203
|
-
console.log(" \u2713 Playwright Chromium installed");
|
|
204
|
-
} else {
|
|
205
|
-
console.error(
|
|
206
|
-
" \u2717 Failed to install Playwright Chromium. Run manually: npx playwright install chromium"
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
function getPackageSkillsDir() {
|
|
211
|
-
const thisFile = fileURLToPath(import.meta.url);
|
|
212
|
-
let dir = dirname(thisFile);
|
|
213
|
-
while (dir !== dirname(dir)) {
|
|
214
|
-
if (existsSync(join(dir, "skills", "libretto"))) {
|
|
215
|
-
return join(dir, "skills", "libretto");
|
|
216
|
-
}
|
|
217
|
-
dir = dirname(dir);
|
|
218
|
-
}
|
|
219
|
-
throw new Error("Could not locate libretto skill files in package");
|
|
220
|
-
}
|
|
221
|
-
function detectAgentDirs(root) {
|
|
222
|
-
const dirs = [];
|
|
223
|
-
if (existsSync(join(root, ".agents"))) dirs.push(join(root, ".agents"));
|
|
224
|
-
if (existsSync(join(root, ".claude"))) dirs.push(join(root, ".claude"));
|
|
225
|
-
return dirs;
|
|
226
|
-
}
|
|
227
|
-
function copySkills() {
|
|
228
|
-
const agentDirs = detectAgentDirs(REPO_ROOT);
|
|
229
|
-
if (agentDirs.length === 0) {
|
|
230
|
-
console.log(
|
|
231
|
-
"\nSkills: No .agents/ or .claude/ directory found in repo root \u2014 skipping."
|
|
232
|
-
);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
const destinations = agentDirs.map((d) => join(d, "skills", "libretto"));
|
|
236
|
-
let sourceDir;
|
|
237
|
-
try {
|
|
238
|
-
sourceDir = getPackageSkillsDir();
|
|
239
|
-
} catch (e) {
|
|
240
|
-
console.error(` \u2717 ${e instanceof Error ? e.message : String(e)}`);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
for (let i = 0; i < agentDirs.length; i++) {
|
|
244
|
-
const skillDest = destinations[i];
|
|
245
|
-
const name = basename(agentDirs[i]);
|
|
246
|
-
if (existsSync(skillDest)) {
|
|
247
|
-
rmSync(skillDest, { recursive: true });
|
|
248
|
-
}
|
|
249
|
-
cpSync(sourceDir, skillDest, { recursive: true });
|
|
250
|
-
const fileCount = readdirSync(skillDest).length;
|
|
251
|
-
console.log(
|
|
252
|
-
` \u2713 Copied ${fileCount} skill files to ${name}/skills/libretto/`
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
const initInput = SimpleCLI.input({
|
|
257
|
-
positionals: [],
|
|
258
|
-
named: {
|
|
259
|
-
skipBrowsers: SimpleCLI.flag({
|
|
260
|
-
name: "skip-browsers",
|
|
261
|
-
help: "Skip Playwright Chromium installation"
|
|
262
|
-
})
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
const initCommand = SimpleCLI.command({
|
|
266
|
-
description: "Initialize libretto in the current project"
|
|
267
|
-
}).input(initInput).handle(async ({ input }) => {
|
|
268
|
-
console.log("Initializing libretto...\n");
|
|
269
|
-
if (!input.skipBrowsers) {
|
|
270
|
-
installBrowsers();
|
|
271
|
-
} else {
|
|
272
|
-
console.log("\nSkipping browser installation (--skip-browsers)");
|
|
273
|
-
}
|
|
274
|
-
copySkills();
|
|
275
|
-
if (process.stdin.isTTY) {
|
|
276
|
-
await runInteractiveApiSetup();
|
|
277
|
-
} else {
|
|
278
|
-
loadSnapshotEnv();
|
|
279
|
-
printSnapshotApiStatus();
|
|
280
|
-
}
|
|
281
|
-
console.log("\n\u2713 libretto init complete");
|
|
282
|
-
});
|
|
283
|
-
export {
|
|
284
|
-
initCommand,
|
|
285
|
-
initInput
|
|
286
|
-
};
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { listOpenPages } from "../core/browser.js";
|
|
3
|
-
import { withSessionLogger } from "../core/context.js";
|
|
4
|
-
import {
|
|
5
|
-
clearActionLog,
|
|
6
|
-
clearNetworkLog,
|
|
7
|
-
formatActionEntry,
|
|
8
|
-
formatNetworkEntry,
|
|
9
|
-
readActionLog,
|
|
10
|
-
readNetworkLog
|
|
11
|
-
} from "../core/telemetry.js";
|
|
12
|
-
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
13
|
-
import {
|
|
14
|
-
integerOption,
|
|
15
|
-
pageOption,
|
|
16
|
-
sessionOption,
|
|
17
|
-
withRequiredSession
|
|
18
|
-
} from "./shared.js";
|
|
19
|
-
async function resolvePageId(session, pageId) {
|
|
20
|
-
if (!pageId) return void 0;
|
|
21
|
-
const pages = await withSessionLogger(
|
|
22
|
-
session,
|
|
23
|
-
async (logger) => listOpenPages(session, logger)
|
|
24
|
-
);
|
|
25
|
-
const foundPage = pages.find((page) => page.id === pageId);
|
|
26
|
-
if (!foundPage) {
|
|
27
|
-
throw new Error(
|
|
28
|
-
`Page "${pageId}" was not found in session "${session}". Run "libretto pages --session ${session}" to list ids.`
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
return pageId;
|
|
32
|
-
}
|
|
33
|
-
const networkInput = SimpleCLI.input({
|
|
34
|
-
positionals: [],
|
|
35
|
-
named: {
|
|
36
|
-
session: sessionOption(),
|
|
37
|
-
last: integerOption(),
|
|
38
|
-
filter: SimpleCLI.option(z.string().optional()),
|
|
39
|
-
method: SimpleCLI.option(z.string().optional()),
|
|
40
|
-
page: pageOption(),
|
|
41
|
-
clear: SimpleCLI.flag()
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
const networkCommand = SimpleCLI.command({
|
|
45
|
-
description: "View captured network requests"
|
|
46
|
-
}).input(networkInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
|
|
47
|
-
if (input.clear) {
|
|
48
|
-
clearNetworkLog(ctx.session);
|
|
49
|
-
console.log("Network log cleared.");
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const pageId = await resolvePageId(ctx.session, input.page);
|
|
53
|
-
const entries = readNetworkLog(ctx.session, {
|
|
54
|
-
last: input.last,
|
|
55
|
-
filter: input.filter,
|
|
56
|
-
method: input.method,
|
|
57
|
-
pageId
|
|
58
|
-
});
|
|
59
|
-
if (entries.length === 0) {
|
|
60
|
-
console.log("No network requests captured.");
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
for (const entry of entries) {
|
|
64
|
-
console.log(formatNetworkEntry(entry));
|
|
65
|
-
}
|
|
66
|
-
console.log(`
|
|
67
|
-
${entries.length} request(s) shown.`);
|
|
68
|
-
});
|
|
69
|
-
const actionsInput = SimpleCLI.input({
|
|
70
|
-
positionals: [],
|
|
71
|
-
named: {
|
|
72
|
-
session: sessionOption(),
|
|
73
|
-
last: integerOption(),
|
|
74
|
-
filter: SimpleCLI.option(z.string().optional()),
|
|
75
|
-
action: SimpleCLI.option(z.string().optional()),
|
|
76
|
-
source: SimpleCLI.option(z.string().optional()),
|
|
77
|
-
page: pageOption(),
|
|
78
|
-
clear: SimpleCLI.flag()
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
const actionsCommand = SimpleCLI.command({
|
|
82
|
-
description: "View captured actions"
|
|
83
|
-
}).input(actionsInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
|
|
84
|
-
if (input.clear) {
|
|
85
|
-
clearActionLog(ctx.session);
|
|
86
|
-
console.log("Action log cleared.");
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
const pageId = await resolvePageId(ctx.session, input.page);
|
|
90
|
-
const entries = readActionLog(ctx.session, {
|
|
91
|
-
last: input.last,
|
|
92
|
-
filter: input.filter,
|
|
93
|
-
action: input.action,
|
|
94
|
-
source: input.source,
|
|
95
|
-
pageId
|
|
96
|
-
});
|
|
97
|
-
if (entries.length === 0) {
|
|
98
|
-
console.log("No actions captured.");
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
for (const entry of entries) {
|
|
102
|
-
console.log(formatActionEntry(entry));
|
|
103
|
-
}
|
|
104
|
-
console.log(`
|
|
105
|
-
${entries.length} action(s) shown.`);
|
|
106
|
-
});
|
|
107
|
-
const logCommands = {
|
|
108
|
-
network: networkCommand,
|
|
109
|
-
actions: actionsCommand
|
|
110
|
-
};
|
|
111
|
-
export {
|
|
112
|
-
actionsCommand,
|
|
113
|
-
actionsInput,
|
|
114
|
-
logCommands,
|
|
115
|
-
networkCommand,
|
|
116
|
-
networkInput
|
|
117
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { LanguageModel } from 'ai';
|
|
2
|
-
import { LLMClient } from './types.js';
|
|
3
|
-
import 'zod';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Creates a libretto LLMClient from a Vercel AI SDK LanguageModel.
|
|
7
|
-
*
|
|
8
|
-
* This eliminates the need for consumers to write their own adapter
|
|
9
|
-
* when using @ai-sdk/openai, @ai-sdk/anthropic, @ai-sdk/google-vertex,
|
|
10
|
-
* or any other Vercel AI SDK-compatible provider.
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```typescript
|
|
14
|
-
* import { createLLMClientFromModel } from "libretto/llm";
|
|
15
|
-
* import { openai } from "@ai-sdk/openai";
|
|
16
|
-
*
|
|
17
|
-
* const llmClient = createLLMClientFromModel(openai("gpt-4o"));
|
|
18
|
-
* ```
|
|
19
|
-
*/
|
|
20
|
-
declare function createLLMClientFromModel(model: LanguageModel): LLMClient;
|
|
21
|
-
|
|
22
|
-
export { createLLMClientFromModel };
|