libretto 0.5.5 → 0.6.0
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 +98 -8
- package/dist/cli/commands/execution.js +152 -56
- package/dist/cli/commands/setup.js +390 -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 +202 -36
- package/dist/cli/core/{ai-config.js → config.js} +14 -79
- package/dist/cli/core/context.js +1 -25
- package/dist/cli/core/deploy-artifact.js +121 -61
- package/dist/cli/core/providers/browserbase.js +53 -0
- package/dist/cli/core/providers/index.js +48 -0
- package/dist/cli/core/providers/kernel.js +46 -0
- package/dist/cli/core/providers/libretto-cloud.js +58 -0
- 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 +53 -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 +19 -13
- package/dist/cli/workers/run-integration-worker-protocol.js +5 -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 +9 -1
- package/dist/shared/run/browser.js +43 -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 +20 -1
- package/dist/shared/state/session-state.js +12 -2
- package/dist/shared/workflow/workflow.d.ts +2 -1
- package/dist/shared/workflow/workflow.js +16 -9
- package/package.json +17 -16
- 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 +3 -3
- 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 +111 -9
- package/src/cli/commands/execution.ts +181 -74
- package/src/cli/commands/setup.ts +516 -0
- package/src/cli/commands/snapshot.ts +2 -2
- package/src/cli/commands/status.ts +79 -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 +242 -35
- package/src/cli/core/{ai-config.ts → config.ts} +14 -108
- package/src/cli/core/context.ts +1 -45
- package/src/cli/core/deploy-artifact.ts +141 -71
- package/src/cli/core/providers/browserbase.ts +57 -0
- package/src/cli/core/providers/index.ts +62 -0
- package/src/cli/core/providers/kernel.ts +49 -0
- package/src/cli/core/providers/libretto-cloud.ts +61 -0
- package/src/cli/core/providers/types.ts +9 -0
- 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 +75 -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 +18 -16
- package/src/cli/workers/run-integration-worker-protocol.ts +4 -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 +50 -1
- package/src/shared/state/index.ts +2 -0
- package/src/shared/state/session-state.ts +10 -0
- package/src/shared/workflow/workflow.ts +24 -13
- 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/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
- /package/dist/{shared/llm → cli/core/providers}/types.js +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ProviderApi } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export function createLibrettoCloudProvider(): ProviderApi {
|
|
4
|
+
const apiKey = process.env.LIBRETTO_API_KEY;
|
|
5
|
+
if (!apiKey)
|
|
6
|
+
throw new Error(
|
|
7
|
+
"LIBRETTO_API_KEY is required for the Libretto Cloud provider.",
|
|
8
|
+
);
|
|
9
|
+
const apiUrl = process.env.LIBRETTO_API_URL;
|
|
10
|
+
if (!apiUrl)
|
|
11
|
+
throw new Error(
|
|
12
|
+
"LIBRETTO_API_URL is required for the Libretto Cloud provider.",
|
|
13
|
+
);
|
|
14
|
+
const endpoint = apiUrl.replace(/\/$/, "");
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
async createSession() {
|
|
18
|
+
const timeoutSeconds = Number(
|
|
19
|
+
process.env.LIBRETTO_TIMEOUT_SECONDS ?? 7200,
|
|
20
|
+
);
|
|
21
|
+
const resp = await fetch(`${endpoint}/v1/sessions/create`, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: {
|
|
24
|
+
"x-api-key": apiKey,
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({ timeout_seconds: timeoutSeconds }),
|
|
28
|
+
});
|
|
29
|
+
if (!resp.ok) {
|
|
30
|
+
const body = await resp.text();
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Libretto Cloud API error (${resp.status}): ${body}`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const json = (await resp.json()) as {
|
|
36
|
+
session_id: string;
|
|
37
|
+
cdp_url: string;
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
sessionId: json.session_id,
|
|
41
|
+
cdpEndpoint: json.cdp_url,
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
async closeSession(sessionId) {
|
|
45
|
+
const resp = await fetch(`${endpoint}/v1/sessions/close`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: {
|
|
48
|
+
"x-api-key": apiKey,
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify({ session_id: sessionId }),
|
|
52
|
+
});
|
|
53
|
+
if (!resp.ok) {
|
|
54
|
+
const body = await resp.text();
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Libretto Cloud API error closing session ${sessionId} (${resp.status}): ${body}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type ProviderSession = {
|
|
2
|
+
sessionId: string; // remote session id for cleanup
|
|
3
|
+
cdpEndpoint: string; // CDP WebSocket URL
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type ProviderApi = {
|
|
7
|
+
createSession(): Promise<ProviderSession>;
|
|
8
|
+
closeSession(sessionId: string): Promise<void>;
|
|
9
|
+
};
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import type { Locator, Page } from "playwright";
|
|
2
|
+
|
|
3
|
+
const PAGE_READ_METHODS = new Set([
|
|
4
|
+
"url",
|
|
5
|
+
"title",
|
|
6
|
+
"content",
|
|
7
|
+
"pageErrors",
|
|
8
|
+
"viewportSize",
|
|
9
|
+
"waitForLoadState",
|
|
10
|
+
"waitForRequest",
|
|
11
|
+
"waitForResponse",
|
|
12
|
+
"waitForURL",
|
|
13
|
+
]);
|
|
14
|
+
const PAGE_LOCATOR_FACTORY_METHODS = new Set([
|
|
15
|
+
"locator",
|
|
16
|
+
"getByRole",
|
|
17
|
+
"getByText",
|
|
18
|
+
"getByLabel",
|
|
19
|
+
"getByPlaceholder",
|
|
20
|
+
"getByAltText",
|
|
21
|
+
"getByTitle",
|
|
22
|
+
"getByTestId",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const PAGE_ALLOWED_PROPERTIES = new Set<string>([]);
|
|
26
|
+
|
|
27
|
+
const LOCATOR_READ_METHODS = new Set([
|
|
28
|
+
"textContent",
|
|
29
|
+
"innerText",
|
|
30
|
+
"allTextContents",
|
|
31
|
+
"allInnerTexts",
|
|
32
|
+
"ariaSnapshot",
|
|
33
|
+
"boundingBox",
|
|
34
|
+
"count",
|
|
35
|
+
"getAttribute",
|
|
36
|
+
"inputValue",
|
|
37
|
+
"isChecked",
|
|
38
|
+
"isDisabled",
|
|
39
|
+
"isEditable",
|
|
40
|
+
"isEnabled",
|
|
41
|
+
"isVisible",
|
|
42
|
+
"isHidden",
|
|
43
|
+
"waitFor",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const LOCATOR_FACTORY_METHODS = new Set([
|
|
47
|
+
"locator",
|
|
48
|
+
"getByRole",
|
|
49
|
+
"getByText",
|
|
50
|
+
"getByLabel",
|
|
51
|
+
"getByPlaceholder",
|
|
52
|
+
"getByAltText",
|
|
53
|
+
"getByTitle",
|
|
54
|
+
"getByTestId",
|
|
55
|
+
"filter",
|
|
56
|
+
"and",
|
|
57
|
+
"or",
|
|
58
|
+
"first",
|
|
59
|
+
"last",
|
|
60
|
+
"nth",
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
const LOCATOR_COLLECTION_FACTORY_METHODS = new Set(["all"]);
|
|
64
|
+
|
|
65
|
+
const LOCATOR_SCROLL_METHODS = new Set(["scrollIntoViewIfNeeded"]);
|
|
66
|
+
|
|
67
|
+
const LOCATOR_ALLOWED_PROPERTIES = new Set<string>([]);
|
|
68
|
+
|
|
69
|
+
type ReadonlyExecOptions = {
|
|
70
|
+
onActivity?: () => void;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const readonlyPageCache = new WeakMap<Page, Page>();
|
|
74
|
+
const readonlyLocatorCache = new WeakMap<Locator, Locator>();
|
|
75
|
+
|
|
76
|
+
function markActivity(onActivity?: () => void): void {
|
|
77
|
+
onActivity?.();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class ReadonlyExecDeniedError extends Error {
|
|
81
|
+
constructor(message: string) {
|
|
82
|
+
super(`ReadonlyExecDenied: ${message}`);
|
|
83
|
+
this.name = "ReadonlyExecDenied";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function denyOperation(targetName: "page" | "locator", method: string): never {
|
|
88
|
+
throw new ReadonlyExecDeniedError(
|
|
89
|
+
`${targetName}.${method} is blocked in readonly-exec`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function wrapLocatorForReadonlyExec(
|
|
94
|
+
locator: Locator,
|
|
95
|
+
options: ReadonlyExecOptions = {},
|
|
96
|
+
): Locator {
|
|
97
|
+
const cached = readonlyLocatorCache.get(locator);
|
|
98
|
+
if (cached) return cached;
|
|
99
|
+
|
|
100
|
+
const proxy = new Proxy(locator, {
|
|
101
|
+
get(target, prop, receiver) {
|
|
102
|
+
if (typeof prop !== "string") {
|
|
103
|
+
return Reflect.get(target, prop, receiver);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const value = Reflect.get(target, prop, target);
|
|
107
|
+
if (typeof value !== "function") {
|
|
108
|
+
if (LOCATOR_ALLOWED_PROPERTIES.has(prop)) {
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
return denyOperation("locator", prop);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (LOCATOR_READ_METHODS.has(prop)) {
|
|
115
|
+
return (...args: unknown[]) => {
|
|
116
|
+
const result = value.apply(target, args);
|
|
117
|
+
markActivity(options.onActivity);
|
|
118
|
+
return result;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (LOCATOR_FACTORY_METHODS.has(prop)) {
|
|
123
|
+
return (...args: unknown[]) => {
|
|
124
|
+
const nextLocator = value.apply(target, args) as Locator;
|
|
125
|
+
markActivity(options.onActivity);
|
|
126
|
+
return wrapLocatorForReadonlyExec(nextLocator, options);
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (LOCATOR_COLLECTION_FACTORY_METHODS.has(prop)) {
|
|
131
|
+
return async (...args: unknown[]) => {
|
|
132
|
+
const locators = (await value.apply(target, args)) as Locator[];
|
|
133
|
+
markActivity(options.onActivity);
|
|
134
|
+
return locators.map((locator) =>
|
|
135
|
+
wrapLocatorForReadonlyExec(locator, options),
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (LOCATOR_SCROLL_METHODS.has(prop)) {
|
|
141
|
+
return async (...args: unknown[]) => {
|
|
142
|
+
await value.apply(target, args);
|
|
143
|
+
markActivity(options.onActivity);
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return (..._args: unknown[]) => denyOperation("locator", prop);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
readonlyLocatorCache.set(locator, proxy as Locator);
|
|
152
|
+
return proxy as Locator;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function wrapPageForReadonlyExec(
|
|
156
|
+
page: Page,
|
|
157
|
+
options: ReadonlyExecOptions = {},
|
|
158
|
+
): Page {
|
|
159
|
+
const cached = readonlyPageCache.get(page);
|
|
160
|
+
if (cached) return cached;
|
|
161
|
+
|
|
162
|
+
const proxy = new Proxy(page, {
|
|
163
|
+
get(target, prop, receiver) {
|
|
164
|
+
if (typeof prop !== "string") {
|
|
165
|
+
return Reflect.get(target, prop, receiver);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const value = Reflect.get(target, prop, target);
|
|
169
|
+
if (typeof value !== "function") {
|
|
170
|
+
if (PAGE_ALLOWED_PROPERTIES.has(prop)) {
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
return denyOperation("page", prop);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (PAGE_READ_METHODS.has(prop)) {
|
|
177
|
+
return (...args: unknown[]) => {
|
|
178
|
+
const result = value.apply(target, args);
|
|
179
|
+
markActivity(options.onActivity);
|
|
180
|
+
return result;
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (PAGE_LOCATOR_FACTORY_METHODS.has(prop)) {
|
|
185
|
+
return (...args: unknown[]) => {
|
|
186
|
+
const locator = value.apply(target, args) as Locator;
|
|
187
|
+
markActivity(options.onActivity);
|
|
188
|
+
return wrapLocatorForReadonlyExec(locator, options);
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return (..._args: unknown[]) => denyOperation("page", prop);
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
readonlyPageCache.set(page, proxy as Page);
|
|
197
|
+
return proxy as Page;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveRequestMethod(
|
|
201
|
+
input: RequestInfo | URL,
|
|
202
|
+
init?: RequestInit,
|
|
203
|
+
): string {
|
|
204
|
+
const requestMethod =
|
|
205
|
+
typeof Request !== "undefined" && input instanceof Request
|
|
206
|
+
? input.method
|
|
207
|
+
: undefined;
|
|
208
|
+
return (init?.method ?? requestMethod ?? "GET").toUpperCase();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function assertReadonlyRequestBodyAllowed(
|
|
212
|
+
input: RequestInfo | URL,
|
|
213
|
+
init?: RequestInit,
|
|
214
|
+
): void {
|
|
215
|
+
if (init?.body !== undefined) {
|
|
216
|
+
throw new ReadonlyExecDeniedError(
|
|
217
|
+
"request bodies are blocked in readonly-exec",
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (
|
|
222
|
+
typeof Request !== "undefined" &&
|
|
223
|
+
input instanceof Request &&
|
|
224
|
+
input.body !== null
|
|
225
|
+
) {
|
|
226
|
+
throw new ReadonlyExecDeniedError(
|
|
227
|
+
"request bodies are blocked in readonly-exec",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function createReadonlyExecHelpers(
|
|
233
|
+
page: Page,
|
|
234
|
+
options: ReadonlyExecOptions = {},
|
|
235
|
+
) {
|
|
236
|
+
const readonlyPage = wrapPageForReadonlyExec(page, options);
|
|
237
|
+
const execState: Record<string, unknown> = {};
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
page: readonlyPage,
|
|
241
|
+
state: execState,
|
|
242
|
+
// Playwright has no native viewport scroll method — only locator.scrollIntoViewIfNeeded().
|
|
243
|
+
// Arbitrary scrolling requires page.evaluate(), which is blocked by the readonly proxy
|
|
244
|
+
// since it can run arbitrary code. This helper calls evaluate on the raw (unwrapped) page,
|
|
245
|
+
// scoped to just window.scrollBy.
|
|
246
|
+
scrollBy: async (deltaX: number, deltaY: number) => {
|
|
247
|
+
await page.evaluate(
|
|
248
|
+
([x, y]) => {
|
|
249
|
+
window.scrollBy(x, y);
|
|
250
|
+
},
|
|
251
|
+
[deltaX, deltaY] as const,
|
|
252
|
+
);
|
|
253
|
+
markActivity(options.onActivity);
|
|
254
|
+
},
|
|
255
|
+
get: async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
256
|
+
const method = resolveRequestMethod(input, init);
|
|
257
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
258
|
+
throw new ReadonlyExecDeniedError(
|
|
259
|
+
`${method} requests are blocked in readonly-exec`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
assertReadonlyRequestBodyAllowed(input, init);
|
|
263
|
+
markActivity(options.onActivity);
|
|
264
|
+
return await fetch(input, {
|
|
265
|
+
...init,
|
|
266
|
+
method,
|
|
267
|
+
});
|
|
268
|
+
},
|
|
269
|
+
// Shadows the global Node.js fetch to prevent unrestricted HTTP access.
|
|
270
|
+
// Without this, agent code would fall through to the global fetch (POST, PUT, DELETE, etc.).
|
|
271
|
+
fetch: () => {
|
|
272
|
+
throw new ReadonlyExecDeniedError(
|
|
273
|
+
"fetch is blocked in readonly-exec; use get() instead",
|
|
274
|
+
);
|
|
275
|
+
},
|
|
276
|
+
console,
|
|
277
|
+
setTimeout,
|
|
278
|
+
setInterval,
|
|
279
|
+
clearTimeout,
|
|
280
|
+
clearInterval,
|
|
281
|
+
URL,
|
|
282
|
+
Buffer,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
@@ -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,26 @@ 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.provider) {
|
|
133
|
+
running.push(state);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (state.pid == null || !isPidRunning(state.pid)) continue;
|
|
137
|
+
running.push(state);
|
|
138
|
+
}
|
|
139
|
+
return running;
|
|
140
|
+
}
|
|
141
|
+
|
|
114
142
|
function throwSessionNotFoundError(session: string): never {
|
|
115
143
|
const active = listActiveSessions();
|
|
116
144
|
const lines = [`No session "${session}" found.`];
|
|
@@ -165,11 +193,47 @@ export function writeSessionState(
|
|
|
165
193
|
logger?.info("session-state-write", {
|
|
166
194
|
session: state.session,
|
|
167
195
|
stateFile,
|
|
196
|
+
mode: state.mode,
|
|
168
197
|
port: state.port,
|
|
169
198
|
pid: state.pid,
|
|
170
199
|
});
|
|
171
200
|
}
|
|
172
201
|
|
|
202
|
+
export function setSessionMode(
|
|
203
|
+
session: string,
|
|
204
|
+
mode: SessionAccessMode,
|
|
205
|
+
logger?: LoggerApi,
|
|
206
|
+
): SessionState {
|
|
207
|
+
const state = readSessionStateOrThrow(session);
|
|
208
|
+
const normalizedMode = SessionAccessModeSchema.parse(mode);
|
|
209
|
+
if (state.mode === normalizedMode) {
|
|
210
|
+
return state;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const nextState = {
|
|
214
|
+
...state,
|
|
215
|
+
mode: normalizedMode,
|
|
216
|
+
};
|
|
217
|
+
writeSessionState(nextState, logger);
|
|
218
|
+
return nextState;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function assertSessionAllowsCommand(
|
|
222
|
+
state: SessionState,
|
|
223
|
+
commandName: string,
|
|
224
|
+
allowedModes: readonly SessionAccessMode[],
|
|
225
|
+
): void {
|
|
226
|
+
const mode = resolveSessionAccessMode(state);
|
|
227
|
+
if (allowedModes.includes(mode)) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const supportedModes = [...allowedModes].join(", ");
|
|
232
|
+
throw new Error(
|
|
233
|
+
`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.`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
173
237
|
export function clearSessionState(session: string, logger?: LoggerApi): void {
|
|
174
238
|
const stateFile = getStateFilePath(session);
|
|
175
239
|
if (!existsSync(stateFile)) {
|
|
@@ -180,7 +244,7 @@ export function clearSessionState(session: string, logger?: LoggerApi): void {
|
|
|
180
244
|
logger?.info("session-state-cleared", { session, stateFile });
|
|
181
245
|
}
|
|
182
246
|
|
|
183
|
-
function isPidRunning(pid: number): boolean {
|
|
247
|
+
export function isPidRunning(pid: number): boolean {
|
|
184
248
|
try {
|
|
185
249
|
process.kill(pid, 0);
|
|
186
250
|
return true;
|
|
@@ -212,6 +276,15 @@ export function assertSessionAvailableForStart(
|
|
|
212
276
|
): void {
|
|
213
277
|
const existingState = readSessionState(session, logger);
|
|
214
278
|
if (!existingState) return;
|
|
279
|
+
|
|
280
|
+
// Cloud provider sessions have no local PID — treat them as active
|
|
281
|
+
// if they have a provider field with a cdpEndpoint.
|
|
282
|
+
if (existingState.provider && existingState.cdpEndpoint) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`Session "${session}" is already open via ${existingState.provider.name} provider. Close it first with: libretto close --session ${session}`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
215
288
|
if (existingState.pid == null || !isPidRunning(existingState.pid)) {
|
|
216
289
|
setSessionStatus(session, "exited", logger);
|
|
217
290
|
return;
|
|
@@ -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
|
+
}
|