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
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { ZodType, output as ZodOutput } from "zod";
|
|
3
|
-
import type { LLMClient, Message, MessageContentPart } from "./types.js";
|
|
1
|
+
import type { LanguageModel } from "ai";
|
|
4
2
|
|
|
5
3
|
export type Provider = "google" | "vertex" | "anthropic" | "openai";
|
|
6
4
|
|
|
@@ -138,87 +136,7 @@ async function getProviderModel(
|
|
|
138
136
|
}
|
|
139
137
|
}
|
|
140
138
|
|
|
141
|
-
function
|
|
142
|
-
return parts.map((part) => {
|
|
143
|
-
if (part.type === "text") {
|
|
144
|
-
return { type: "text" as const, text: part.text };
|
|
145
|
-
}
|
|
146
|
-
return {
|
|
147
|
-
type: "image" as const,
|
|
148
|
-
image: part.image,
|
|
149
|
-
...(part.mediaType ? { mediaType: part.mediaType } : {}),
|
|
150
|
-
};
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function convertAssistantContentParts(parts: MessageContentPart[]) {
|
|
155
|
-
return parts
|
|
156
|
-
.filter(
|
|
157
|
-
(part): part is MessageContentPart & { type: "text" } =>
|
|
158
|
-
part.type === "text",
|
|
159
|
-
)
|
|
160
|
-
.map((part) => ({ type: "text" as const, text: part.text }));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function convertMessages(messages: Message[]): ModelMessage[] {
|
|
164
|
-
return messages.map((msg): ModelMessage => {
|
|
165
|
-
if (msg.role === "user") {
|
|
166
|
-
if (typeof msg.content === "string") {
|
|
167
|
-
return { role: "user", content: msg.content };
|
|
168
|
-
}
|
|
169
|
-
return {
|
|
170
|
-
role: "user",
|
|
171
|
-
content: convertUserContentParts(msg.content),
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
if (typeof msg.content === "string") {
|
|
175
|
-
return { role: "assistant", content: msg.content };
|
|
176
|
-
}
|
|
177
|
-
return {
|
|
178
|
-
role: "assistant",
|
|
179
|
-
content: convertAssistantContentParts(msg.content),
|
|
180
|
-
};
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export function createLLMClient(model: string): LLMClient {
|
|
139
|
+
export async function resolveModel(model: string): Promise<LanguageModel> {
|
|
185
140
|
const { provider, modelId } = parseModel(model);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const getModel = () => {
|
|
189
|
-
modelPromise ??= getProviderModel(provider, modelId);
|
|
190
|
-
return modelPromise;
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
async generateObject<T extends ZodType>(opts: {
|
|
195
|
-
prompt: string;
|
|
196
|
-
schema: T;
|
|
197
|
-
temperature?: number;
|
|
198
|
-
}): Promise<ZodOutput<T>> {
|
|
199
|
-
const aiModel = await getModel();
|
|
200
|
-
const result = await generateObject({
|
|
201
|
-
model: aiModel,
|
|
202
|
-
prompt: opts.prompt,
|
|
203
|
-
schema: opts.schema,
|
|
204
|
-
temperature: opts.temperature ?? 0,
|
|
205
|
-
});
|
|
206
|
-
return result.object as ZodOutput<T>;
|
|
207
|
-
},
|
|
208
|
-
|
|
209
|
-
async generateObjectFromMessages<T extends ZodType>(opts: {
|
|
210
|
-
messages: Message[];
|
|
211
|
-
schema: T;
|
|
212
|
-
temperature?: number;
|
|
213
|
-
}): Promise<ZodOutput<T>> {
|
|
214
|
-
const aiModel = await getModel();
|
|
215
|
-
const result = await generateObject({
|
|
216
|
-
model: aiModel,
|
|
217
|
-
messages: convertMessages(opts.messages),
|
|
218
|
-
schema: opts.schema,
|
|
219
|
-
temperature: opts.temperature ?? 0,
|
|
220
|
-
});
|
|
221
|
-
return result.object as ZodOutput<T>;
|
|
222
|
-
},
|
|
223
|
-
};
|
|
141
|
+
return getProviderModel(provider, modelId);
|
|
224
142
|
}
|
package/src/cli/core/session.ts
CHANGED
|
@@ -14,9 +14,11 @@ import {
|
|
|
14
14
|
LIBRETTO_SESSIONS_DIR,
|
|
15
15
|
} from "./context.js";
|
|
16
16
|
import {
|
|
17
|
+
SessionAccessModeSchema,
|
|
17
18
|
SESSION_STATE_VERSION,
|
|
18
19
|
parseSessionStateContent,
|
|
19
20
|
serializeSessionState,
|
|
21
|
+
type SessionAccessMode,
|
|
20
22
|
type SessionStatus,
|
|
21
23
|
type SessionState,
|
|
22
24
|
} from "../../shared/state/index.js";
|
|
@@ -35,7 +37,13 @@ export function generateSessionName(): string {
|
|
|
35
37
|
return `ses-${id}`;
|
|
36
38
|
}
|
|
37
39
|
export { SESSION_STATE_VERSION };
|
|
38
|
-
export type { SessionStatus, SessionState };
|
|
40
|
+
export type { SessionAccessMode, SessionStatus, SessionState };
|
|
41
|
+
|
|
42
|
+
export function resolveSessionAccessMode(
|
|
43
|
+
state: Pick<SessionState, "mode"> | null | undefined,
|
|
44
|
+
): SessionAccessMode {
|
|
45
|
+
return SessionAccessModeSchema.parse(state?.mode);
|
|
46
|
+
}
|
|
39
47
|
|
|
40
48
|
export function logFileForSession(session: string): string {
|
|
41
49
|
validateSessionName(session);
|
|
@@ -111,6 +119,22 @@ function listActiveSessions(): string[] {
|
|
|
111
119
|
return listSessionsWithStateFile();
|
|
112
120
|
}
|
|
113
121
|
|
|
122
|
+
/**
|
|
123
|
+
* List sessions whose state file exists and whose pid is still running.
|
|
124
|
+
* Returns session states (not just names) so callers can access port, status, etc.
|
|
125
|
+
*/
|
|
126
|
+
export function listRunningSessions(): SessionState[] {
|
|
127
|
+
const sessions = listSessionsWithStateFile();
|
|
128
|
+
const running: SessionState[] = [];
|
|
129
|
+
for (const name of sessions) {
|
|
130
|
+
const state = readSessionState(name);
|
|
131
|
+
if (!state) continue;
|
|
132
|
+
if (state.pid == null || !isPidRunning(state.pid)) continue;
|
|
133
|
+
running.push(state);
|
|
134
|
+
}
|
|
135
|
+
return running;
|
|
136
|
+
}
|
|
137
|
+
|
|
114
138
|
function throwSessionNotFoundError(session: string): never {
|
|
115
139
|
const active = listActiveSessions();
|
|
116
140
|
const lines = [`No session "${session}" found.`];
|
|
@@ -165,11 +189,47 @@ export function writeSessionState(
|
|
|
165
189
|
logger?.info("session-state-write", {
|
|
166
190
|
session: state.session,
|
|
167
191
|
stateFile,
|
|
192
|
+
mode: state.mode,
|
|
168
193
|
port: state.port,
|
|
169
194
|
pid: state.pid,
|
|
170
195
|
});
|
|
171
196
|
}
|
|
172
197
|
|
|
198
|
+
export function setSessionMode(
|
|
199
|
+
session: string,
|
|
200
|
+
mode: SessionAccessMode,
|
|
201
|
+
logger?: LoggerApi,
|
|
202
|
+
): SessionState {
|
|
203
|
+
const state = readSessionStateOrThrow(session);
|
|
204
|
+
const normalizedMode = SessionAccessModeSchema.parse(mode);
|
|
205
|
+
if (state.mode === normalizedMode) {
|
|
206
|
+
return state;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const nextState = {
|
|
210
|
+
...state,
|
|
211
|
+
mode: normalizedMode,
|
|
212
|
+
};
|
|
213
|
+
writeSessionState(nextState, logger);
|
|
214
|
+
return nextState;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function assertSessionAllowsCommand(
|
|
218
|
+
state: SessionState,
|
|
219
|
+
commandName: string,
|
|
220
|
+
allowedModes: readonly SessionAccessMode[],
|
|
221
|
+
): void {
|
|
222
|
+
const mode = resolveSessionAccessMode(state);
|
|
223
|
+
if (allowedModes.includes(mode)) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const supportedModes = [...allowedModes].join(", ");
|
|
228
|
+
throw new Error(
|
|
229
|
+
`Command "${commandName}" is blocked for session "${state.session}" because it is in ${mode} mode. Allowed modes for this command: ${supportedModes}. Run \`libretto session-mode write-access --session ${state.session}\` to unlock the session.`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
173
233
|
export function clearSessionState(session: string, logger?: LoggerApi): void {
|
|
174
234
|
const stateFile = getStateFilePath(session);
|
|
175
235
|
if (!existsSync(stateFile)) {
|
|
@@ -180,7 +240,7 @@ export function clearSessionState(session: string, logger?: LoggerApi): void {
|
|
|
180
240
|
logger?.info("session-state-cleared", { session, stateFile });
|
|
181
241
|
}
|
|
182
242
|
|
|
183
|
-
function isPidRunning(pid: number): boolean {
|
|
243
|
+
export function isPidRunning(pid: number): boolean {
|
|
184
244
|
try {
|
|
185
245
|
process.kill(pid, 0);
|
|
186
246
|
return true;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { REPO_ROOT } from "./context.js";
|
|
5
|
+
|
|
6
|
+
type PackageManifest = {
|
|
7
|
+
version?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const INSTALLED_SKILL_PATHS = [
|
|
11
|
+
[".agents", "skills", "libretto", "SKILL.md"],
|
|
12
|
+
[".claude", "skills", "libretto", "SKILL.md"],
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
let cachedCliVersion: string | null = null;
|
|
16
|
+
|
|
17
|
+
function readCurrentCliVersion(): string {
|
|
18
|
+
if (cachedCliVersion) {
|
|
19
|
+
return cachedCliVersion;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const packageJsonPath = fileURLToPath(
|
|
23
|
+
new URL("../../../package.json", import.meta.url),
|
|
24
|
+
);
|
|
25
|
+
const manifest = JSON.parse(
|
|
26
|
+
readFileSync(packageJsonPath, "utf8"),
|
|
27
|
+
) as PackageManifest;
|
|
28
|
+
|
|
29
|
+
if (!manifest.version) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Unable to determine current libretto version from ${packageJsonPath}.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
cachedCliVersion = manifest.version;
|
|
36
|
+
return cachedCliVersion;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readInstalledSkillVersion(skillPath: string): string | null {
|
|
40
|
+
if (!existsSync(skillPath)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const contents = readFileSync(skillPath, "utf8");
|
|
45
|
+
const frontmatterMatch = contents.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
46
|
+
if (!frontmatterMatch) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const metadataBlock = frontmatterMatch[1].match(
|
|
51
|
+
/^metadata:\s*\r?\n((?:[ \t]+.*(?:\r?\n|$))*)/m,
|
|
52
|
+
)?.[1];
|
|
53
|
+
if (!metadataBlock) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const versionMatch = metadataBlock.match(
|
|
58
|
+
/^[ \t]+version:\s*["']?([^"'\r\n]+)["']?\s*$/m,
|
|
59
|
+
);
|
|
60
|
+
return versionMatch?.[1]?.trim() ?? null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function findInstalledSkillVersionMismatch(): {
|
|
64
|
+
installedVersion: string;
|
|
65
|
+
cliVersion: string;
|
|
66
|
+
} | null {
|
|
67
|
+
const cliVersion = readCurrentCliVersion();
|
|
68
|
+
|
|
69
|
+
for (const relativePathParts of INSTALLED_SKILL_PATHS) {
|
|
70
|
+
const skillPath = join(REPO_ROOT, ...relativePathParts);
|
|
71
|
+
const installedVersion = readInstalledSkillVersion(skillPath);
|
|
72
|
+
if (installedVersion && installedVersion !== cliVersion) {
|
|
73
|
+
return { installedVersion, cliVersion };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function warnIfInstalledSkillOutOfDate(): void {
|
|
81
|
+
try {
|
|
82
|
+
const mismatch = findInstalledSkillVersionMismatch();
|
|
83
|
+
if (!mismatch) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.error(
|
|
88
|
+
`Warning: Your agent skill (${mismatch.installedVersion}) is out of date with your Libretto CLI (${mismatch.cliVersion}). Please run \`npx libretto setup\` to update your skills to the correct version.`,
|
|
89
|
+
);
|
|
90
|
+
} catch {
|
|
91
|
+
// Never block command execution on a best-effort skill version check.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
appendFileSync,
|
|
3
3
|
existsSync,
|
|
4
4
|
readFileSync,
|
|
5
|
-
writeFileSync,
|
|
6
5
|
} from "node:fs";
|
|
7
6
|
import type { Page } from "playwright";
|
|
8
7
|
import {
|
|
@@ -65,31 +64,6 @@ export function readNetworkLog(
|
|
|
65
64
|
return entries;
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
export function formatNetworkEntry(e: NetworkLogEntry): string {
|
|
69
|
-
const time = e.ts.replace(/.*T/, "").replace(/\.\d+Z$/, "");
|
|
70
|
-
const duration = e.durationMs != null ? `${e.durationMs}ms` : "?ms";
|
|
71
|
-
const size = e.size != null ? `${e.size}B` : "";
|
|
72
|
-
const parts = [
|
|
73
|
-
`[${time}]`,
|
|
74
|
-
`${e.status}`,
|
|
75
|
-
`${e.method.padEnd(6)}`,
|
|
76
|
-
e.url,
|
|
77
|
-
duration,
|
|
78
|
-
size,
|
|
79
|
-
].filter(Boolean);
|
|
80
|
-
let line = parts.join(" ");
|
|
81
|
-
if (e.postData) {
|
|
82
|
-
line += `\n body: ${e.postData.substring(0, 120)}${e.postData.length > 120 ? "..." : ""}`;
|
|
83
|
-
}
|
|
84
|
-
return line;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function clearNetworkLog(session: string): void {
|
|
88
|
-
assertSessionStateExistsOrThrow(session);
|
|
89
|
-
const logPath = getSessionNetworkLogPath(session);
|
|
90
|
-
writeFileSync(logPath, "");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
67
|
export type ActionLogEntry = {
|
|
94
68
|
ts: string;
|
|
95
69
|
pageId?: string;
|
|
@@ -182,32 +156,6 @@ export function readActionLog(
|
|
|
182
156
|
return entries;
|
|
183
157
|
}
|
|
184
158
|
|
|
185
|
-
export function formatActionEntry(e: ActionLogEntry): string {
|
|
186
|
-
const time = e.ts.replace(/.*T/, "").replace(/\.\d+Z$/, "");
|
|
187
|
-
const src = e.source.toUpperCase().padEnd(5);
|
|
188
|
-
const displaySelector = e.bestSemanticSelector || e.selector;
|
|
189
|
-
const parts = [`[${time}]`, `[${src}]`, e.action];
|
|
190
|
-
if (displaySelector) parts.push(displaySelector);
|
|
191
|
-
if (e.targetSelector && e.targetSelector !== displaySelector) {
|
|
192
|
-
parts.push(`target=${e.targetSelector}`);
|
|
193
|
-
}
|
|
194
|
-
if (e.nearbyText) parts.push(`text="${e.nearbyText}"`);
|
|
195
|
-
if (e.coordinates) {
|
|
196
|
-
parts.push(`@(${e.coordinates.x},${e.coordinates.y})`);
|
|
197
|
-
}
|
|
198
|
-
if (e.value) parts.push(`"${e.value}"`);
|
|
199
|
-
if (e.url) parts.push(e.url);
|
|
200
|
-
if (e.duration != null) parts.push(`${e.duration}ms`);
|
|
201
|
-
if (!e.success) parts.push(`ERROR: ${e.error || "unknown"}`);
|
|
202
|
-
return parts.join(" ");
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function clearActionLog(session: string): void {
|
|
206
|
-
assertSessionStateExistsOrThrow(session);
|
|
207
|
-
const logPath = getSessionActionsLogPath(session);
|
|
208
|
-
writeFileSync(logPath, "");
|
|
209
|
-
}
|
|
210
|
-
|
|
211
159
|
const LOCATOR_ACTION_METHODS = [
|
|
212
160
|
"click",
|
|
213
161
|
"dblclick",
|
package/src/cli/index.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runLibrettoCLI } from "./cli.js";
|
|
3
|
-
import {
|
|
4
|
-
maybeConfigureLLMClientFactoryFromEnv,
|
|
5
|
-
setLLMClientFactory,
|
|
6
|
-
} from "./core/context.js";
|
|
7
3
|
|
|
8
|
-
export { setLLMClientFactory };
|
|
9
4
|
export { runClose } from "./commands/browser.js";
|
|
10
5
|
export { runLibrettoCLI };
|
|
11
6
|
|
|
12
|
-
maybeConfigureLLMClientFactoryFromEnv();
|
|
13
7
|
void runLibrettoCLI();
|
package/src/cli/router.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { aiCommands } from "./commands/ai.js";
|
|
|
2
2
|
import { browserCommands } from "./commands/browser.js";
|
|
3
3
|
import { deployCommand } from "./commands/deploy.js";
|
|
4
4
|
import { executionCommands } from "./commands/execution.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { setupCommand } from "./commands/setup.js";
|
|
6
|
+
import { statusCommand } from "./commands/status.js";
|
|
7
7
|
import { snapshotCommand } from "./commands/snapshot.js";
|
|
8
8
|
import { SimpleCLI } from "./framework/simple-cli.js";
|
|
9
9
|
|
|
@@ -11,9 +11,9 @@ export const cliRoutes = {
|
|
|
11
11
|
...browserCommands,
|
|
12
12
|
deploy: deployCommand,
|
|
13
13
|
...executionCommands,
|
|
14
|
-
...logCommands,
|
|
15
14
|
ai: aiCommands,
|
|
16
|
-
|
|
15
|
+
setup: setupCommand,
|
|
16
|
+
status: statusCommand,
|
|
17
17
|
snapshot: snapshotCommand,
|
|
18
18
|
};
|
|
19
19
|
|
|
@@ -5,7 +5,7 @@ import { cwd } from "node:process";
|
|
|
5
5
|
import { isAbsolute, resolve } from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
getDefaultWorkflowFromModuleExports,
|
|
9
9
|
getWorkflowsFromModuleExports,
|
|
10
10
|
instrumentContext,
|
|
11
11
|
launchBrowser,
|
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
} from "../../index.js";
|
|
15
15
|
import type { LoggerApi } from "../../shared/logger/index.js";
|
|
16
16
|
import { parseSessionStateContent } from "../../shared/state/index.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
getProfilePath,
|
|
19
|
+
normalizeDomain,
|
|
20
|
+
normalizeUrl,
|
|
21
|
+
} from "../core/browser.js";
|
|
18
22
|
import {
|
|
19
23
|
getSessionActionsLogPath,
|
|
20
24
|
getSessionDir,
|
|
@@ -109,23 +113,18 @@ async function waitForFailureSessionRelease(args: {
|
|
|
109
113
|
}
|
|
110
114
|
}
|
|
111
115
|
|
|
112
|
-
function resolveLocalAuthProfilePath(domain: string): string {
|
|
113
|
-
return getProfilePath(normalizeDomain(domain));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
116
|
function getMissingLocalAuthProfileError(args: {
|
|
117
|
-
|
|
117
|
+
normalizedDomain: string;
|
|
118
118
|
profilePath: string;
|
|
119
119
|
session: string;
|
|
120
120
|
}): string {
|
|
121
|
-
const normalizedDomain = normalizeDomain(args.domain);
|
|
122
121
|
return [
|
|
123
|
-
`Local auth profile not found for domain "${normalizedDomain}".`,
|
|
122
|
+
`Local auth profile not found for domain "${args.normalizedDomain}".`,
|
|
124
123
|
`Expected profile file: ${args.profilePath}`,
|
|
125
124
|
"To create it:",
|
|
126
|
-
` 1. libretto open https://${normalizedDomain} --headed --session ${args.session}`,
|
|
125
|
+
` 1. libretto open https://${args.normalizedDomain} --headed --session ${args.session}`,
|
|
127
126
|
" 2. Log in manually in the browser window.",
|
|
128
|
-
` 3. libretto save ${normalizedDomain} --session ${args.session}`,
|
|
127
|
+
` 3. libretto save ${args.normalizedDomain} --session ${args.session}`,
|
|
129
128
|
].join("\n");
|
|
130
129
|
}
|
|
131
130
|
|
|
@@ -139,9 +138,8 @@ function getAbsoluteIntegrationPath(integrationPath: string): string {
|
|
|
139
138
|
return absolutePath;
|
|
140
139
|
}
|
|
141
140
|
|
|
142
|
-
async function
|
|
141
|
+
async function loadDefaultWorkflow(
|
|
143
142
|
absolutePath: string,
|
|
144
|
-
workflowName: string,
|
|
145
143
|
): Promise<LoadedLibrettoWorkflow> {
|
|
146
144
|
let loadedModule: Record<string, unknown>;
|
|
147
145
|
try {
|
|
@@ -157,22 +155,23 @@ async function loadWorkflowByName(
|
|
|
157
155
|
);
|
|
158
156
|
}
|
|
159
157
|
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
162
|
-
return
|
|
158
|
+
const defaultWorkflow = getDefaultWorkflowFromModuleExports(loadedModule);
|
|
159
|
+
if (defaultWorkflow) {
|
|
160
|
+
return defaultWorkflow as LoadedLibrettoWorkflow;
|
|
163
161
|
}
|
|
164
162
|
|
|
165
|
-
const
|
|
163
|
+
const availableWorkflowNames = getWorkflowsFromModuleExports(loadedModule).map(
|
|
166
164
|
(candidate) => candidate.name,
|
|
167
165
|
);
|
|
168
166
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
167
|
+
if (availableWorkflowNames.length === 0) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`No default-exported workflow found in ${absolutePath}. Export the workflow with \`export default workflow("name", handler)\`.`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
173
172
|
|
|
174
173
|
throw new Error(
|
|
175
|
-
`
|
|
174
|
+
`No default-exported workflow found in ${absolutePath}. libretto run only uses the file's default export. Available named workflows: ${availableWorkflowNames.join(", ")}`,
|
|
176
175
|
);
|
|
177
176
|
}
|
|
178
177
|
|
|
@@ -195,7 +194,7 @@ async function runIntegrationInternal(
|
|
|
195
194
|
): Promise<RunIntegrationOutcome> {
|
|
196
195
|
const { logger } = options;
|
|
197
196
|
const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
|
|
198
|
-
const workflow = await
|
|
197
|
+
const workflow = await loadDefaultWorkflow(absolutePath);
|
|
199
198
|
const signalPaths = getPauseSignalPaths(args.session);
|
|
200
199
|
await removeSignalIfExists(signalPaths.pausedSignalPath);
|
|
201
200
|
await removeSignalIfExists(signalPaths.resumeSignalPath);
|
|
@@ -204,24 +203,31 @@ async function runIntegrationInternal(
|
|
|
204
203
|
const restoreStdout = mirrorStdoutToFile(signalPaths.outputSignalPath);
|
|
205
204
|
|
|
206
205
|
console.log(
|
|
207
|
-
`Running workflow "${
|
|
206
|
+
`Running workflow "${workflow.name}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`,
|
|
208
207
|
);
|
|
209
208
|
|
|
210
209
|
const integrationLogger = logger.withScope("integration-run", {
|
|
211
210
|
integrationPath: absolutePath,
|
|
212
|
-
workflowName:
|
|
211
|
+
workflowName: workflow.name,
|
|
213
212
|
session: args.session,
|
|
214
213
|
});
|
|
215
214
|
|
|
216
215
|
// Resolve auth profile from CLI flag (--auth-profile <domain>)
|
|
217
216
|
const authProfileDomain = args.authProfileDomain;
|
|
218
|
-
const
|
|
219
|
-
?
|
|
217
|
+
const normalizedAuthProfileDomain = authProfileDomain
|
|
218
|
+
? normalizeDomain(normalizeUrl(authProfileDomain))
|
|
219
|
+
: undefined;
|
|
220
|
+
const storageStatePath = normalizedAuthProfileDomain
|
|
221
|
+
? getProfilePath(normalizedAuthProfileDomain)
|
|
220
222
|
: undefined;
|
|
221
|
-
if (
|
|
223
|
+
if (
|
|
224
|
+
normalizedAuthProfileDomain &&
|
|
225
|
+
storageStatePath &&
|
|
226
|
+
!existsSync(storageStatePath)
|
|
227
|
+
) {
|
|
222
228
|
throw new Error(
|
|
223
229
|
getMissingLocalAuthProfileError({
|
|
224
|
-
|
|
230
|
+
normalizedDomain: normalizedAuthProfileDomain,
|
|
225
231
|
profilePath: storageStatePath,
|
|
226
232
|
session: args.session,
|
|
227
233
|
}),
|
|
@@ -232,6 +238,7 @@ async function runIntegrationInternal(
|
|
|
232
238
|
headless: args.headless,
|
|
233
239
|
storageStatePath,
|
|
234
240
|
viewport: args.viewport,
|
|
241
|
+
accessMode: args.accessMode,
|
|
235
242
|
});
|
|
236
243
|
if (!args.headless && args.visualize !== false) {
|
|
237
244
|
await installHeadedWorkflowVisualization({
|
|
@@ -255,7 +262,6 @@ async function runIntegrationInternal(
|
|
|
255
262
|
|
|
256
263
|
const workflowContext: LibrettoWorkflowContext = {
|
|
257
264
|
session: args.session,
|
|
258
|
-
logger: integrationLogger,
|
|
259
265
|
page: browserSession.page,
|
|
260
266
|
};
|
|
261
267
|
|
|
@@ -290,7 +296,6 @@ async function runIntegrationInternal(
|
|
|
290
296
|
JSON.stringify({ completedAt: new Date().toISOString() }, null, 2),
|
|
291
297
|
"utf8",
|
|
292
298
|
);
|
|
293
|
-
console.log("Integration completed.");
|
|
294
299
|
return { status: "completed" };
|
|
295
300
|
} finally {
|
|
296
301
|
restoreStdout();
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { SessionAccessModeSchema } from "../../shared/state/index.js";
|
|
2
3
|
|
|
3
4
|
export const RunIntegrationWorkerRequestSchema = z.object({
|
|
4
5
|
integrationPath: z.string().min(1),
|
|
5
|
-
workflowName: z.string().min(1),
|
|
6
6
|
session: z.string().min(1),
|
|
7
7
|
params: z.unknown(),
|
|
8
8
|
headless: z.boolean(),
|
|
9
9
|
visualize: z.boolean().default(true),
|
|
10
10
|
authProfileDomain: z.string().optional(),
|
|
11
11
|
viewport: z.object({ width: z.number(), height: z.number() }).optional(),
|
|
12
|
+
accessMode: SessionAccessModeSchema.default("write-access"),
|
|
12
13
|
});
|
|
13
14
|
|
|
14
15
|
export type RunIntegrationWorkerRequest = z.infer<
|
package/src/index.ts
CHANGED
|
@@ -16,13 +16,6 @@ export {
|
|
|
16
16
|
jsonlConsoleSink,
|
|
17
17
|
} from "./shared/logger/sinks.js";
|
|
18
18
|
|
|
19
|
-
// LLM client interface
|
|
20
|
-
export type {
|
|
21
|
-
LLMClient,
|
|
22
|
-
Message,
|
|
23
|
-
MessageContentPart,
|
|
24
|
-
} from "./shared/llm/types.js";
|
|
25
|
-
export { createLLMClientFromModel } from "./shared/llm/ai-sdk-adapter.js";
|
|
26
19
|
export {
|
|
27
20
|
SESSION_STATE_VERSION,
|
|
28
21
|
SessionStatusSchema,
|
|
@@ -102,6 +95,7 @@ export {
|
|
|
102
95
|
|
|
103
96
|
// Workflow helpers
|
|
104
97
|
export {
|
|
98
|
+
getDefaultWorkflowFromModuleExports,
|
|
105
99
|
getWorkflowFromModuleExports,
|
|
106
100
|
getWorkflowsFromModuleExports,
|
|
107
101
|
isLibrettoWorkflow,
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
type MinimalLogger,
|
|
5
5
|
defaultLogger,
|
|
6
6
|
} from "../../shared/logger/logger.js";
|
|
7
|
-
import type
|
|
7
|
+
import { generateObject, type LanguageModel } from "ai";
|
|
8
8
|
|
|
9
9
|
export type ExtractOptions<T extends z.ZodType> = {
|
|
10
10
|
page: Page;
|
|
11
11
|
instruction: string;
|
|
12
12
|
schema: T;
|
|
13
|
-
|
|
13
|
+
model: LanguageModel;
|
|
14
14
|
logger?: MinimalLogger;
|
|
15
15
|
/** Optional CSS selector to scope extraction to a specific element. */
|
|
16
16
|
selector?: string;
|
|
@@ -31,7 +31,7 @@ export async function extractFromPage<T extends z.ZodType>(
|
|
|
31
31
|
schema,
|
|
32
32
|
selector,
|
|
33
33
|
logger = defaultLogger,
|
|
34
|
-
|
|
34
|
+
model,
|
|
35
35
|
} = options;
|
|
36
36
|
|
|
37
37
|
let screenshot: string;
|
|
@@ -79,7 +79,8 @@ ${domContent ? `Here is the HTML content for additional context:\n<html>\n${domC
|
|
|
79
79
|
|
|
80
80
|
Extract the requested information from the screenshot and return it in the specified format. Be precise and only extract what is visible.`;
|
|
81
81
|
|
|
82
|
-
const result = await
|
|
82
|
+
const { object: result } = await generateObject({
|
|
83
|
+
model,
|
|
83
84
|
schema,
|
|
84
85
|
messages: [
|
|
85
86
|
{
|
|
@@ -98,5 +99,5 @@ Extract the requested information from the screenshot and return it in the speci
|
|
|
98
99
|
instruction: instruction.slice(0, 100),
|
|
99
100
|
});
|
|
100
101
|
|
|
101
|
-
return result
|
|
102
|
+
return result as z.infer<T>;
|
|
102
103
|
}
|
|
@@ -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
|
|
|
8
8
|
type BrowserAction =
|
|
9
9
|
| { type: "click"; x: number; y: number; button?: string }
|
|
@@ -183,9 +183,9 @@ export async function executeRecoveryAgent(
|
|
|
183
183
|
page: Page,
|
|
184
184
|
instruction: string,
|
|
185
185
|
logger?: MinimalLogger,
|
|
186
|
-
|
|
186
|
+
model?: LanguageModel,
|
|
187
187
|
): Promise<void> {
|
|
188
|
-
if (!
|
|
188
|
+
if (!model) {
|
|
189
189
|
return;
|
|
190
190
|
}
|
|
191
191
|
const log = logger ?? defaultLogger;
|
|
@@ -213,7 +213,8 @@ export async function executeRecoveryAgent(
|
|
|
213
213
|
|
|
214
214
|
const maxSteps = 3;
|
|
215
215
|
for (let step = 1; step <= maxSteps; step++) {
|
|
216
|
-
const result = await
|
|
216
|
+
const { object: result } = await generateObject({
|
|
217
|
+
model,
|
|
217
218
|
schema: recoveryActionSchema,
|
|
218
219
|
messages: [
|
|
219
220
|
{
|