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,29 +1,56 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { type AiConfig, readAiConfig } from "./
|
|
3
|
+
import { type AiConfig, readAiConfig } from "./config.js";
|
|
4
4
|
import { LIBRETTO_CONFIG_PATH, REPO_ROOT } from "./context.js";
|
|
5
5
|
import {
|
|
6
6
|
hasProviderCredentials,
|
|
7
7
|
parseModel,
|
|
8
8
|
type Provider,
|
|
9
|
-
} from "
|
|
9
|
+
} from "./resolve-model.js";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// ── Default models ──────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_SNAPSHOT_MODELS = {
|
|
12
14
|
openai: "openai/gpt-5.4",
|
|
13
15
|
anthropic: "anthropic/claude-sonnet-4-6",
|
|
14
16
|
google: "google/gemini-3-flash-preview",
|
|
15
|
-
vertex: "vertex/gemini-2.5-
|
|
17
|
+
vertex: "vertex/gemini-2.5-flash",
|
|
16
18
|
} as const satisfies Record<Provider, string>;
|
|
17
19
|
|
|
20
|
+
// ── Source detection ────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Detect which specific env var provides credentials for a provider.
|
|
24
|
+
* Returns the env var name (e.g. "OPENAI_API_KEY", "GEMINI_API_KEY"),
|
|
25
|
+
* or null if no credential is found.
|
|
26
|
+
*/
|
|
27
|
+
function detectProviderEnvVar(
|
|
28
|
+
provider: Provider,
|
|
29
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
30
|
+
): string | null {
|
|
31
|
+
switch (provider) {
|
|
32
|
+
case "openai":
|
|
33
|
+
return env.OPENAI_API_KEY?.trim() ? "OPENAI_API_KEY" : null;
|
|
34
|
+
case "anthropic":
|
|
35
|
+
return env.ANTHROPIC_API_KEY?.trim() ? "ANTHROPIC_API_KEY" : null;
|
|
36
|
+
case "google":
|
|
37
|
+
if (env.GEMINI_API_KEY?.trim()) return "GEMINI_API_KEY";
|
|
38
|
+
if (env.GOOGLE_GENERATIVE_AI_API_KEY?.trim())
|
|
39
|
+
return "GOOGLE_GENERATIVE_AI_API_KEY";
|
|
40
|
+
return null;
|
|
41
|
+
case "vertex":
|
|
42
|
+
if (env.GOOGLE_CLOUD_PROJECT?.trim()) return "GOOGLE_CLOUD_PROJECT";
|
|
43
|
+
if (env.GCLOUD_PROJECT?.trim()) return "GCLOUD_PROJECT";
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Snapshot model resolution ───────────────────────────────────────────────
|
|
49
|
+
|
|
18
50
|
export type SnapshotApiModelSelection = {
|
|
19
51
|
model: string;
|
|
20
52
|
provider: Provider;
|
|
21
|
-
source:
|
|
22
|
-
| "config"
|
|
23
|
-
| "env:auto-openai"
|
|
24
|
-
| "env:auto-anthropic"
|
|
25
|
-
| "env:auto-google"
|
|
26
|
-
| "env:auto-vertex";
|
|
53
|
+
source: "config" | `env:${string}`;
|
|
27
54
|
};
|
|
28
55
|
|
|
29
56
|
export class SnapshotApiUnavailableError extends Error {
|
|
@@ -67,7 +94,7 @@ function noSnapshotApiConfiguredMessage(): string {
|
|
|
67
94
|
return [
|
|
68
95
|
"Failed to analyze snapshot because no snapshot analyzer is configured.",
|
|
69
96
|
`Add OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY, or GOOGLE_CLOUD_PROJECT to .env or as a shell environment variable, or choose a default model with \`${defaultModelCommandLine()}\`.`,
|
|
70
|
-
"For more info, run `npx libretto
|
|
97
|
+
"For more info, run `npx libretto setup`.",
|
|
71
98
|
].join(" ");
|
|
72
99
|
}
|
|
73
100
|
|
|
@@ -81,10 +108,12 @@ function missingProviderSnapshotMessage(
|
|
|
81
108
|
return [
|
|
82
109
|
`Failed to analyze snapshot because ${selection.provider} is configured${configuredSource}, but ${providerMissingCredentialSummary(selection.provider)}.`,
|
|
83
110
|
providerSetupSentence(selection.provider),
|
|
84
|
-
"For more info, run `npx libretto
|
|
111
|
+
"For more info, run `npx libretto setup`.",
|
|
85
112
|
].join(" ");
|
|
86
113
|
}
|
|
87
114
|
|
|
115
|
+
// ── Dotenv loading ──────────────────────────────────────────────────────────
|
|
116
|
+
|
|
88
117
|
function readWorktreeEnvPath(): string | null {
|
|
89
118
|
const gitPath = join(REPO_ROOT, ".git");
|
|
90
119
|
if (!existsSync(gitPath)) return null;
|
|
@@ -165,6 +194,8 @@ export function parseDotEnvAssignment(
|
|
|
165
194
|
return { key, value };
|
|
166
195
|
}
|
|
167
196
|
|
|
197
|
+
// ── Model resolution ────────────────────────────────────────────────────────
|
|
198
|
+
|
|
168
199
|
function inferAutoSnapshotModel(): SnapshotApiModelSelection | null {
|
|
169
200
|
const providersInPriorityOrder: Provider[] = [
|
|
170
201
|
"openai",
|
|
@@ -174,11 +205,12 @@ function inferAutoSnapshotModel(): SnapshotApiModelSelection | null {
|
|
|
174
205
|
];
|
|
175
206
|
|
|
176
207
|
for (const provider of providersInPriorityOrder) {
|
|
177
|
-
|
|
208
|
+
const envVar = detectProviderEnvVar(provider);
|
|
209
|
+
if (!envVar) continue;
|
|
178
210
|
return {
|
|
179
211
|
model: DEFAULT_SNAPSHOT_MODELS[provider],
|
|
180
212
|
provider,
|
|
181
|
-
source: `env
|
|
213
|
+
source: `env:${envVar}`,
|
|
182
214
|
};
|
|
183
215
|
}
|
|
184
216
|
|
|
@@ -229,3 +261,111 @@ export function resolveSnapshotApiModelOrThrow(
|
|
|
229
261
|
export function isSnapshotApiUnavailableError(error: unknown): boolean {
|
|
230
262
|
return error instanceof SnapshotApiUnavailableError;
|
|
231
263
|
}
|
|
264
|
+
|
|
265
|
+
// ── AI setup status ─────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Workspace AI setup health states.
|
|
269
|
+
*
|
|
270
|
+
* - `ready`: a usable model was resolved and the matching provider has credentials.
|
|
271
|
+
* - `configured-missing-credentials`: config pins a provider whose credentials are absent.
|
|
272
|
+
* - `invalid-config`: `.libretto/config.json` exists but fails schema validation.
|
|
273
|
+
* - `unconfigured`: no config and no env credentials detected.
|
|
274
|
+
*/
|
|
275
|
+
export type AiSetupStatus =
|
|
276
|
+
| {
|
|
277
|
+
kind: "ready";
|
|
278
|
+
model: string;
|
|
279
|
+
provider: Provider;
|
|
280
|
+
source: "config" | `env:${string}`;
|
|
281
|
+
}
|
|
282
|
+
| {
|
|
283
|
+
kind: "configured-missing-credentials";
|
|
284
|
+
model: string;
|
|
285
|
+
provider: Provider;
|
|
286
|
+
}
|
|
287
|
+
| { kind: "invalid-config"; message: string }
|
|
288
|
+
| { kind: "unconfigured" };
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Read AI config without throwing on invalid files.
|
|
292
|
+
* Returns the config or an error message.
|
|
293
|
+
*/
|
|
294
|
+
function readAiConfigSafely(
|
|
295
|
+
configPath: string,
|
|
296
|
+
): { ok: true; config: AiConfig | null } | { ok: false; message: string } {
|
|
297
|
+
try {
|
|
298
|
+
return { ok: true, config: readAiConfig(configPath) };
|
|
299
|
+
} catch (err) {
|
|
300
|
+
return {
|
|
301
|
+
ok: false,
|
|
302
|
+
message: err instanceof Error ? err.message : String(err),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Resolve the workspace's current AI setup health.
|
|
309
|
+
*
|
|
310
|
+
* Uses the existing config reader and snapshot model resolver, but wraps
|
|
311
|
+
* them to distinguish broken states (invalid config, missing credentials)
|
|
312
|
+
* that the throwing APIs collapse into errors.
|
|
313
|
+
*
|
|
314
|
+
* 1. If config read throws → `invalid-config`.
|
|
315
|
+
* 2. If config has an `ai` block → check credentials for that provider.
|
|
316
|
+
* 3. If no config or no `ai` block → auto-detect from env via existing resolver.
|
|
317
|
+
*/
|
|
318
|
+
export function resolveAiSetupStatus(
|
|
319
|
+
configPath: string = LIBRETTO_CONFIG_PATH,
|
|
320
|
+
): AiSetupStatus {
|
|
321
|
+
loadSnapshotEnv();
|
|
322
|
+
|
|
323
|
+
const configResult = readAiConfigSafely(configPath);
|
|
324
|
+
|
|
325
|
+
if (!configResult.ok) {
|
|
326
|
+
return { kind: "invalid-config", message: configResult.message };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Config exists with an ai block — use it directly to check credentials
|
|
330
|
+
if (configResult.config) {
|
|
331
|
+
let selection: SnapshotApiModelSelection | null;
|
|
332
|
+
try {
|
|
333
|
+
selection = resolveSnapshotApiModel(configResult.config);
|
|
334
|
+
} catch (err) {
|
|
335
|
+
return {
|
|
336
|
+
kind: "invalid-config",
|
|
337
|
+
message: err instanceof Error ? err.message : String(err),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
if (!selection) {
|
|
341
|
+
// Should not happen when config has a model, but handle gracefully
|
|
342
|
+
return { kind: "unconfigured" };
|
|
343
|
+
}
|
|
344
|
+
if (hasProviderCredentials(selection.provider)) {
|
|
345
|
+
return {
|
|
346
|
+
kind: "ready",
|
|
347
|
+
model: selection.model,
|
|
348
|
+
provider: selection.provider,
|
|
349
|
+
source: selection.source,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
kind: "configured-missing-credentials",
|
|
354
|
+
model: selection.model,
|
|
355
|
+
provider: selection.provider,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// No ai config — fall back to env auto-detect via existing resolver
|
|
360
|
+
const envSelection = resolveSnapshotApiModel(null);
|
|
361
|
+
if (envSelection && hasProviderCredentials(envSelection.provider)) {
|
|
362
|
+
return {
|
|
363
|
+
kind: "ready",
|
|
364
|
+
model: envSelection.model,
|
|
365
|
+
provider: envSelection.provider,
|
|
366
|
+
source: envSelection.source,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return { kind: "unconfigured" };
|
|
371
|
+
}
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import { readFileSync } from "node:fs";
|
|
10
10
|
import type { LoggerApi } from "../../shared/logger/index.js";
|
|
11
|
-
import {
|
|
11
|
+
import { generateObject } from "ai";
|
|
12
|
+
import { resolveModel } from "./resolve-model.js";
|
|
12
13
|
import {
|
|
13
14
|
InterpretResultSchema,
|
|
14
15
|
buildInlinePromptSelection,
|
|
@@ -17,8 +18,8 @@ import {
|
|
|
17
18
|
type InterpretResult,
|
|
18
19
|
type InterpretArgs,
|
|
19
20
|
} from "./snapshot-analyzer.js";
|
|
20
|
-
import { readAiConfig, type AiConfig } from "./
|
|
21
|
-
import { resolveSnapshotApiModelOrThrow } from "./
|
|
21
|
+
import { readAiConfig, type AiConfig } from "./config.js";
|
|
22
|
+
import { resolveSnapshotApiModelOrThrow } from "./ai-model.js";
|
|
22
23
|
|
|
23
24
|
export async function runApiInterpret(
|
|
24
25
|
args: InterpretArgs,
|
|
@@ -64,9 +65,10 @@ export async function runApiInterpret(
|
|
|
64
65
|
const imageMimeType = getMimeType(args.pngPath);
|
|
65
66
|
const imageBytes = Buffer.from(imageBase64, "base64");
|
|
66
67
|
|
|
67
|
-
const
|
|
68
|
+
const model = await resolveModel(selection.model);
|
|
68
69
|
|
|
69
|
-
const result = await
|
|
70
|
+
const { object: result } = await generateObject({
|
|
71
|
+
model,
|
|
70
72
|
schema: InterpretResultSchema,
|
|
71
73
|
messages: [
|
|
72
74
|
{
|
package/src/cli/core/browser.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { createRequire } from "node:module";
|
|
|
12
12
|
import { createServer } from "node:net";
|
|
13
13
|
import { spawn } from "node:child_process";
|
|
14
14
|
import type { LoggerApi } from "../../shared/logger/index.js";
|
|
15
|
+
import type { SessionAccessMode } from "../../shared/state/index.js";
|
|
15
16
|
import {
|
|
16
17
|
filterSemanticClasses,
|
|
17
18
|
INTERACTIVE_ROLE_NAMES,
|
|
@@ -25,10 +26,11 @@ import {
|
|
|
25
26
|
getSessionNetworkLogPath,
|
|
26
27
|
PROFILES_DIR,
|
|
27
28
|
} from "./context.js";
|
|
28
|
-
import { readLibrettoConfig } from "./
|
|
29
|
+
import { readLibrettoConfig } from "./config.js";
|
|
29
30
|
import {
|
|
30
31
|
assertSessionAvailableForStart,
|
|
31
32
|
clearSessionState,
|
|
33
|
+
isPidRunning,
|
|
32
34
|
listSessionsWithStateFile,
|
|
33
35
|
readSessionStateOrThrow,
|
|
34
36
|
logFileForSession,
|
|
@@ -56,20 +58,61 @@ async function pickFreePort(): Promise<number> {
|
|
|
56
58
|
});
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return
|
|
61
|
+
function tryParseAbsoluteUrl(url: string): URL | null {
|
|
62
|
+
try {
|
|
63
|
+
return new URL(url);
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
62
66
|
}
|
|
63
|
-
return url;
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
function isLikelyHostWithPort(parsedUrl: URL, rawUrl: string): boolean {
|
|
70
|
+
// `new URL("localhost:3000")` parses successfully, but treats `localhost:`
|
|
71
|
+
// as a custom scheme instead of a bare host with port. Detect that shape so
|
|
72
|
+
// CLI shorthand like `libretto open localhost:3000` still normalizes to
|
|
73
|
+
// `https://localhost:3000/`.
|
|
74
|
+
const remainder = rawUrl.slice(parsedUrl.protocol.length);
|
|
75
|
+
if (remainder.length === 0) return false;
|
|
76
|
+
|
|
77
|
+
let index = 0;
|
|
78
|
+
while (index < remainder.length) {
|
|
79
|
+
const charCode = remainder.charCodeAt(index);
|
|
80
|
+
if (charCode < 48 || charCode > 57) break;
|
|
81
|
+
index += 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (index === 0) return false;
|
|
85
|
+
if (index === remainder.length) return true;
|
|
86
|
+
|
|
87
|
+
const nextChar = remainder[index];
|
|
88
|
+
return nextChar === "/" || nextChar === "?" || nextChar === "#";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function normalizeUrl(url: string): URL {
|
|
92
|
+
const parsedUrl = tryParseAbsoluteUrl(url);
|
|
93
|
+
if (!parsedUrl) {
|
|
94
|
+
return new URL(`https://${url}`);
|
|
72
95
|
}
|
|
96
|
+
|
|
97
|
+
if (
|
|
98
|
+
parsedUrl.protocol === "http:" ||
|
|
99
|
+
parsedUrl.protocol === "https:" ||
|
|
100
|
+
parsedUrl.protocol === "file:"
|
|
101
|
+
) {
|
|
102
|
+
return parsedUrl;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (isLikelyHostWithPort(parsedUrl, url)) {
|
|
106
|
+
return new URL(`https://${url}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Unsupported URL protocol: ${parsedUrl.protocol}. Use http://, https://, or file://.`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function normalizeDomain(url: URL): string {
|
|
115
|
+
return url.hostname.replace(/^www\./, "");
|
|
73
116
|
}
|
|
74
117
|
|
|
75
118
|
export function getProfilePath(domain: string): string {
|
|
@@ -357,12 +400,24 @@ export async function runOpen(
|
|
|
357
400
|
headed: boolean,
|
|
358
401
|
session: string,
|
|
359
402
|
logger: LoggerApi,
|
|
360
|
-
options?: {
|
|
403
|
+
options?: {
|
|
404
|
+
viewport?: { width: number; height: number };
|
|
405
|
+
accessMode?: SessionAccessMode;
|
|
406
|
+
},
|
|
361
407
|
): Promise<void> {
|
|
362
|
-
const
|
|
408
|
+
const parsedUrl = normalizeUrl(rawUrl);
|
|
409
|
+
const url = parsedUrl.href;
|
|
363
410
|
const viewport = resolveViewport(options?.viewport, logger);
|
|
411
|
+
const accessMode = options?.accessMode ?? "write-access";
|
|
364
412
|
const windowPosition = headed ? resolveWindowPosition(logger) : undefined;
|
|
365
|
-
logger.info("open-start", {
|
|
413
|
+
logger.info("open-start", {
|
|
414
|
+
url,
|
|
415
|
+
headed,
|
|
416
|
+
session,
|
|
417
|
+
viewport,
|
|
418
|
+
windowPosition,
|
|
419
|
+
accessMode,
|
|
420
|
+
});
|
|
366
421
|
assertSessionAvailableForStart(session, logger);
|
|
367
422
|
|
|
368
423
|
const port = await pickFreePort();
|
|
@@ -371,9 +426,11 @@ export async function runOpen(
|
|
|
371
426
|
const actionsLogPath = getSessionActionsLogPath(session);
|
|
372
427
|
|
|
373
428
|
const browserMode = headed ? "headed" : "headless";
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
const
|
|
429
|
+
const supportsSavedProfile =
|
|
430
|
+
parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:";
|
|
431
|
+
const domain = supportsSavedProfile ? normalizeDomain(parsedUrl) : undefined;
|
|
432
|
+
const profilePath = domain ? getProfilePath(domain) : undefined;
|
|
433
|
+
const useProfile = domain ? hasProfile(domain) : false;
|
|
377
434
|
|
|
378
435
|
logger.info("open-launching", {
|
|
379
436
|
url,
|
|
@@ -390,12 +447,11 @@ export async function runOpen(
|
|
|
390
447
|
}
|
|
391
448
|
console.log(`Launching ${browserMode} browser (session: ${session})...`);
|
|
392
449
|
|
|
393
|
-
const escapedProfilePath = profilePath
|
|
394
|
-
.replace(/\\/g, "\\\\")
|
|
395
|
-
.replace(/'/g, "\\'");
|
|
396
450
|
const escapedUrl = url.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
397
451
|
const storageStateCode = useProfile
|
|
398
|
-
? `storageState: '${
|
|
452
|
+
? `storageState: '${profilePath!
|
|
453
|
+
.replace(/\\/g, "\\\\")
|
|
454
|
+
.replace(/'/g, "\\'")}',`
|
|
399
455
|
: "";
|
|
400
456
|
|
|
401
457
|
const escapedLogPath = runLogPath.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
@@ -631,6 +687,7 @@ await new Promise(() => {});
|
|
|
631
687
|
session,
|
|
632
688
|
startedAt: new Date().toISOString(),
|
|
633
689
|
status: "active",
|
|
690
|
+
mode: accessMode,
|
|
634
691
|
viewport,
|
|
635
692
|
},
|
|
636
693
|
logger,
|
|
@@ -671,7 +728,7 @@ export async function runSave(
|
|
|
671
728
|
try {
|
|
672
729
|
await new Promise((r) => setTimeout(r, 500));
|
|
673
730
|
|
|
674
|
-
const domain = normalizeDomain(urlOrDomain);
|
|
731
|
+
const domain = normalizeDomain(normalizeUrl(urlOrDomain));
|
|
675
732
|
const profilePath = getProfilePath(domain);
|
|
676
733
|
|
|
677
734
|
const cdpSession = await context.newCDPSession(page);
|
|
@@ -776,15 +833,6 @@ function waitForCloseSignalWindow(ms: number): Promise<void> {
|
|
|
776
833
|
return new Promise((r) => setTimeout(r, ms));
|
|
777
834
|
}
|
|
778
835
|
|
|
779
|
-
function isPidRunning(pid: number): boolean {
|
|
780
|
-
try {
|
|
781
|
-
process.kill(pid, 0);
|
|
782
|
-
return true;
|
|
783
|
-
} catch {
|
|
784
|
-
return false;
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
836
|
function sendSignalToProcessGroupOrPid(
|
|
789
837
|
pid: number,
|
|
790
838
|
signal: NodeJS.Signals,
|
|
@@ -950,8 +998,9 @@ export async function runConnect(
|
|
|
950
998
|
cdpUrl: string,
|
|
951
999
|
session: string,
|
|
952
1000
|
logger: LoggerApi,
|
|
1001
|
+
accessMode: SessionAccessMode = "write-access",
|
|
953
1002
|
): Promise<void> {
|
|
954
|
-
logger.info("connect-start", { cdpUrl, session });
|
|
1003
|
+
logger.info("connect-start", { cdpUrl, session, accessMode });
|
|
955
1004
|
assertSessionAvailableForStart(session, logger);
|
|
956
1005
|
|
|
957
1006
|
let parsedUrl: URL;
|
|
@@ -962,18 +1011,22 @@ export async function runConnect(
|
|
|
962
1011
|
[
|
|
963
1012
|
`Invalid CDP URL: ${cdpUrl}`,
|
|
964
1013
|
``,
|
|
965
|
-
`Expected an HTTP URL pointing to a Chrome DevTools Protocol endpoint, for example:`,
|
|
1014
|
+
`Expected an HTTP or WebSocket URL pointing to a Chrome DevTools Protocol endpoint, for example:`,
|
|
966
1015
|
` libretto connect http://127.0.0.1:9222`,
|
|
967
1016
|
` libretto connect http://remote-host:9222`,
|
|
968
1017
|
` libretto connect http://remote-host:9222/devtools/browser/<id>`,
|
|
1018
|
+
` libretto connect ws://remote-host:9222/devtools/browser/<id>`,
|
|
1019
|
+
` libretto connect wss://remote-host/cdp-endpoint`,
|
|
969
1020
|
].join("\n"),
|
|
970
1021
|
);
|
|
971
1022
|
}
|
|
972
1023
|
|
|
973
1024
|
const endpoint = parsedUrl.href;
|
|
1025
|
+
const isWebSocket =
|
|
1026
|
+
parsedUrl.protocol === "ws:" || parsedUrl.protocol === "wss:";
|
|
974
1027
|
const port = parsedUrl.port
|
|
975
1028
|
? Number(parsedUrl.port)
|
|
976
|
-
: parsedUrl.protocol === "https:"
|
|
1029
|
+
: parsedUrl.protocol === "https:" || parsedUrl.protocol === "wss:"
|
|
977
1030
|
? 443
|
|
978
1031
|
: 80;
|
|
979
1032
|
|
|
@@ -981,17 +1034,25 @@ export async function runConnect(
|
|
|
981
1034
|
`Connecting to CDP endpoint at ${endpoint} (session: ${session})...`,
|
|
982
1035
|
);
|
|
983
1036
|
|
|
984
|
-
// Verify the CDP endpoint is reachable
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
const
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1037
|
+
// Verify the CDP endpoint is reachable (HTTP only — WebSocket
|
|
1038
|
+
// endpoints are validated by the Playwright connect call below).
|
|
1039
|
+
if (!isWebSocket) {
|
|
1040
|
+
const versionUrl = `${parsedUrl.protocol}//${parsedUrl.host}/json/version`;
|
|
1041
|
+
try {
|
|
1042
|
+
const resp = await fetch(versionUrl);
|
|
1043
|
+
const versionInfo = await resp.json();
|
|
1044
|
+
logger.info("connect-version-ok", { versionUrl, versionInfo });
|
|
1045
|
+
} catch (err) {
|
|
1046
|
+
logger.error("connect-version-failed", { versionUrl, error: err });
|
|
1047
|
+
throw new Error(
|
|
1048
|
+
`Cannot reach CDP endpoint at ${versionUrl}. Make sure the target is running and accessible at ${parsedUrl.host}.`,
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
} else {
|
|
1052
|
+
logger.info("connect-skip-version-check", {
|
|
1053
|
+
reason: "WebSocket-only endpoint, skipping HTTP version check",
|
|
1054
|
+
endpoint,
|
|
1055
|
+
});
|
|
995
1056
|
}
|
|
996
1057
|
|
|
997
1058
|
// Connect via CDP using the full endpoint URL
|
|
@@ -1018,6 +1079,7 @@ export async function runConnect(
|
|
|
1018
1079
|
session,
|
|
1019
1080
|
startedAt: new Date().toISOString(),
|
|
1020
1081
|
status: "active",
|
|
1082
|
+
mode: accessMode,
|
|
1021
1083
|
},
|
|
1022
1084
|
logger,
|
|
1023
1085
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { SessionAccessModeSchema } from "../../shared/state/index.js";
|
|
4
5
|
import { LIBRETTO_CONFIG_PATH } from "./context.js";
|
|
5
6
|
|
|
6
7
|
export const CURRENT_CONFIG_VERSION = 1;
|
|
@@ -41,34 +42,11 @@ export const LibrettoConfigSchema = z
|
|
|
41
42
|
ai: AiConfigSchema.optional(),
|
|
42
43
|
viewport: ViewportConfigSchema.optional(),
|
|
43
44
|
windowPosition: WindowPositionConfigSchema.optional(),
|
|
45
|
+
sessionMode: SessionAccessModeSchema.optional(),
|
|
44
46
|
})
|
|
45
47
|
.passthrough();
|
|
46
48
|
export type LibrettoConfig = z.infer<typeof LibrettoConfigSchema>;
|
|
47
49
|
|
|
48
|
-
/** Default models for each provider shorthand accepted by `ai configure`. */
|
|
49
|
-
const DEFAULT_MODELS: Record<string, string> = {
|
|
50
|
-
openai: "openai/gpt-5.4",
|
|
51
|
-
anthropic: "anthropic/claude-sonnet-4-6",
|
|
52
|
-
gemini: "google/gemini-3-flash-preview",
|
|
53
|
-
vertex: "vertex/gemini-2.5-pro",
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const PROVIDER_ALIASES: Record<string, string> = {
|
|
57
|
-
claude: DEFAULT_MODELS.anthropic,
|
|
58
|
-
google: DEFAULT_MODELS.gemini,
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const CONFIGURE_PROVIDERS = [
|
|
62
|
-
"openai",
|
|
63
|
-
"anthropic",
|
|
64
|
-
"gemini",
|
|
65
|
-
"vertex",
|
|
66
|
-
] as const;
|
|
67
|
-
|
|
68
|
-
function formatConfigureProviders(separator = " | "): string {
|
|
69
|
-
return CONFIGURE_PROVIDERS.join(separator);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
50
|
function formatConfigIssues(error: z.ZodError): string {
|
|
73
51
|
return error.issues
|
|
74
52
|
.map((issue) => ` - ${issue.path.join(".") || "root"}: ${issue.message}`)
|
|
@@ -91,6 +69,7 @@ function formatExpectedConfigExample(): string {
|
|
|
91
69
|
x: 1600,
|
|
92
70
|
y: 120,
|
|
93
71
|
},
|
|
72
|
+
sessionMode: "write-access",
|
|
94
73
|
},
|
|
95
74
|
null,
|
|
96
75
|
2,
|
|
@@ -105,10 +84,10 @@ function invalidConfigError(configPath: string, detail?: string): Error {
|
|
|
105
84
|
"Expected config example:",
|
|
106
85
|
formatExpectedConfigExample(),
|
|
107
86
|
"Notes:",
|
|
108
|
-
' - "ai", "viewport", and "
|
|
87
|
+
' - "ai", "viewport", "windowPosition", and "sessionMode" are optional.',
|
|
109
88
|
' - "ai.model" must be a provider/model string like "openai/gpt-5.4" or "anthropic/claude-sonnet-4-6".',
|
|
110
89
|
"Fix the file to match this shape, or delete it and rerun:",
|
|
111
|
-
` npx libretto ai configure
|
|
90
|
+
` npx libretto ai configure openai | anthropic | gemini | vertex`,
|
|
112
91
|
]
|
|
113
92
|
.filter(Boolean)
|
|
114
93
|
.join("\n"),
|
|
@@ -162,7 +141,14 @@ export function writeAiConfig(
|
|
|
162
141
|
model: string,
|
|
163
142
|
configPath: string = LIBRETTO_CONFIG_PATH,
|
|
164
143
|
): AiConfig {
|
|
165
|
-
|
|
144
|
+
let librettoConfig: LibrettoConfig;
|
|
145
|
+
try {
|
|
146
|
+
librettoConfig = readLibrettoConfig(configPath);
|
|
147
|
+
} catch {
|
|
148
|
+
// Existing config is malformed — start fresh so repair flows can
|
|
149
|
+
// overwrite a broken file instead of throwing.
|
|
150
|
+
librettoConfig = { version: CURRENT_CONFIG_VERSION };
|
|
151
|
+
}
|
|
166
152
|
const ai = AiConfigSchema.parse({
|
|
167
153
|
model,
|
|
168
154
|
updatedAt: new Date().toISOString(),
|
|
@@ -192,84 +178,3 @@ export function clearAiConfig(
|
|
|
192
178
|
);
|
|
193
179
|
return true;
|
|
194
180
|
}
|
|
195
|
-
|
|
196
|
-
function printAiConfig(config: AiConfig, configPath: string): void {
|
|
197
|
-
console.log(`Model: ${config.model}`);
|
|
198
|
-
console.log(`Config file: ${configPath}`);
|
|
199
|
-
console.log(`Updated at: ${config.updatedAt}`);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Resolve the model string from a `ai configure` argument.
|
|
204
|
-
* Accepts a provider shorthand ("openai", "anthropic", "gemini", "vertex")
|
|
205
|
-
* or a full provider/model-id string ("openai/gpt-4o", "anthropic/claude-sonnet-4-6").
|
|
206
|
-
*/
|
|
207
|
-
function resolveModelFromInput(input: string): string | null {
|
|
208
|
-
const trimmed = input.trim();
|
|
209
|
-
if (!trimmed) return null;
|
|
210
|
-
|
|
211
|
-
// Full model string (contains a slash)
|
|
212
|
-
if (trimmed.includes("/")) return trimmed;
|
|
213
|
-
|
|
214
|
-
// Provider shorthand
|
|
215
|
-
const normalized = trimmed.toLowerCase();
|
|
216
|
-
return DEFAULT_MODELS[normalized] ?? PROVIDER_ALIASES[normalized] ?? null;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export function runAiConfigure(
|
|
220
|
-
input: {
|
|
221
|
-
preset?: string;
|
|
222
|
-
clear?: boolean;
|
|
223
|
-
},
|
|
224
|
-
options: {
|
|
225
|
-
configureCommandName?: string;
|
|
226
|
-
configPath?: string;
|
|
227
|
-
} = {},
|
|
228
|
-
): void {
|
|
229
|
-
const configureCommandName =
|
|
230
|
-
options.configureCommandName ?? "npx libretto ai configure";
|
|
231
|
-
const configPath = options.configPath ?? LIBRETTO_CONFIG_PATH;
|
|
232
|
-
|
|
233
|
-
const presetArg = input.preset?.trim();
|
|
234
|
-
|
|
235
|
-
if (!presetArg && !input.clear) {
|
|
236
|
-
const config = readAiConfig(configPath);
|
|
237
|
-
if (!config) {
|
|
238
|
-
console.log(
|
|
239
|
-
`No AI config set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`,
|
|
240
|
-
);
|
|
241
|
-
console.log(
|
|
242
|
-
"Provider credentials still come from your shell or .env file.",
|
|
243
|
-
);
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
printAiConfig(config, configPath);
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (input.clear) {
|
|
251
|
-
const removed = clearAiConfig(configPath);
|
|
252
|
-
if (removed) {
|
|
253
|
-
console.log(`Cleared AI config: ${configPath}`);
|
|
254
|
-
} else {
|
|
255
|
-
console.log("No AI config was set.");
|
|
256
|
-
}
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const model = resolveModelFromInput(presetArg!);
|
|
261
|
-
if (!model) {
|
|
262
|
-
console.log(
|
|
263
|
-
`Usage: ${configureCommandName} <${CONFIGURE_PROVIDERS.join("|")}|provider/model-id>\n` +
|
|
264
|
-
` ${configureCommandName}\n` +
|
|
265
|
-
` ${configureCommandName} --clear`,
|
|
266
|
-
);
|
|
267
|
-
throw new Error(
|
|
268
|
-
`Invalid provider or model. Use one of: ${formatConfigureProviders()}, or a full model string like "openai/gpt-4o".`,
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const config = writeAiConfig(model, configPath);
|
|
273
|
-
console.log("AI config saved.");
|
|
274
|
-
printAiConfig(config, configPath);
|
|
275
|
-
}
|