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.
Files changed (101) hide show
  1. package/README.md +23 -10
  2. package/README.template.md +23 -10
  3. package/dist/cli/cli.js +10 -0
  4. package/dist/cli/commands/ai.js +77 -2
  5. package/dist/cli/commands/browser.js +71 -6
  6. package/dist/cli/commands/execution.js +101 -44
  7. package/dist/cli/commands/setup.js +376 -0
  8. package/dist/cli/commands/snapshot.js +2 -2
  9. package/dist/cli/commands/status.js +62 -0
  10. package/dist/cli/core/{snapshot-api-config.js → ai-model.js} +81 -7
  11. package/dist/cli/core/api-snapshot-analyzer.js +7 -5
  12. package/dist/cli/core/browser.js +81 -42
  13. package/dist/cli/core/{ai-config.js → config.js} +13 -79
  14. package/dist/cli/core/context.js +1 -25
  15. package/dist/cli/core/deploy-artifact.js +121 -61
  16. package/dist/cli/core/readonly-exec.js +231 -0
  17. package/dist/{shared/llm/client.js → cli/core/resolve-model.js} +4 -68
  18. package/dist/cli/core/session.js +44 -0
  19. package/dist/cli/core/skill-version.js +73 -0
  20. package/dist/cli/core/telemetry.js +1 -54
  21. package/dist/cli/index.js +1 -7
  22. package/dist/cli/router.js +4 -4
  23. package/dist/cli/workers/run-integration-runtime.js +29 -25
  24. package/dist/cli/workers/run-integration-worker-protocol.js +3 -2
  25. package/dist/index.d.ts +2 -4
  26. package/dist/index.js +2 -2
  27. package/dist/runtime/extract/extract.d.ts +2 -2
  28. package/dist/runtime/extract/extract.js +4 -2
  29. package/dist/runtime/extract/index.d.ts +1 -1
  30. package/dist/runtime/recovery/agent.d.ts +2 -3
  31. package/dist/runtime/recovery/agent.js +5 -3
  32. package/dist/runtime/recovery/errors.d.ts +2 -3
  33. package/dist/runtime/recovery/errors.js +4 -2
  34. package/dist/runtime/recovery/index.d.ts +1 -2
  35. package/dist/runtime/recovery/recovery.d.ts +2 -3
  36. package/dist/runtime/recovery/recovery.js +3 -3
  37. package/dist/shared/debug/pause.js +4 -21
  38. package/dist/shared/run/api.d.ts +2 -0
  39. package/dist/shared/run/browser.d.ts +4 -1
  40. package/dist/shared/run/browser.js +5 -3
  41. package/dist/shared/state/index.d.ts +1 -1
  42. package/dist/shared/state/index.js +2 -0
  43. package/dist/shared/state/session-state.d.ts +10 -1
  44. package/dist/shared/state/session-state.js +3 -0
  45. package/dist/shared/workflow/workflow.d.ts +2 -3
  46. package/dist/shared/workflow/workflow.js +16 -9
  47. package/package.json +3 -4
  48. package/scripts/postinstall.mjs +13 -11
  49. package/scripts/skills-libretto.mjs +14 -4
  50. package/skills/AGENTS.md +11 -0
  51. package/skills/libretto/SKILL.md +30 -9
  52. package/skills/libretto/references/auth-profiles.md +1 -1
  53. package/skills/libretto/references/code-generation-rules.md +6 -6
  54. package/skills/libretto/references/configuration-file-reference.md +11 -6
  55. package/skills/libretto-readonly/SKILL.md +95 -0
  56. package/src/cli/cli.ts +10 -0
  57. package/src/cli/commands/ai.ts +111 -1
  58. package/src/cli/commands/browser.ts +81 -7
  59. package/src/cli/commands/execution.ts +128 -61
  60. package/src/cli/commands/setup.ts +499 -0
  61. package/src/cli/commands/snapshot.ts +2 -2
  62. package/src/cli/commands/status.ts +77 -0
  63. package/src/cli/core/{snapshot-api-config.ts → ai-model.ts} +154 -14
  64. package/src/cli/core/api-snapshot-analyzer.ts +7 -5
  65. package/src/cli/core/browser.ts +107 -45
  66. package/src/cli/core/{ai-config.ts → config.ts} +13 -108
  67. package/src/cli/core/context.ts +1 -45
  68. package/src/cli/core/deploy-artifact.ts +141 -71
  69. package/src/cli/core/readonly-exec.ts +284 -0
  70. package/src/{shared/llm/client.ts → cli/core/resolve-model.ts} +3 -85
  71. package/src/cli/core/session.ts +62 -2
  72. package/src/cli/core/skill-version.ts +93 -0
  73. package/src/cli/core/telemetry.ts +0 -52
  74. package/src/cli/index.ts +0 -6
  75. package/src/cli/router.ts +4 -4
  76. package/src/cli/workers/run-integration-runtime.ts +36 -31
  77. package/src/cli/workers/run-integration-worker-protocol.ts +2 -1
  78. package/src/index.ts +1 -7
  79. package/src/runtime/extract/extract.ts +6 -5
  80. package/src/runtime/recovery/agent.ts +5 -4
  81. package/src/runtime/recovery/errors.ts +4 -3
  82. package/src/runtime/recovery/recovery.ts +4 -4
  83. package/src/shared/debug/pause.ts +4 -23
  84. package/src/shared/run/browser.ts +5 -1
  85. package/src/shared/state/index.ts +2 -0
  86. package/src/shared/state/session-state.ts +3 -0
  87. package/src/shared/workflow/workflow.ts +24 -15
  88. package/dist/cli/commands/init.js +0 -286
  89. package/dist/cli/commands/logs.js +0 -117
  90. package/dist/shared/llm/ai-sdk-adapter.d.ts +0 -22
  91. package/dist/shared/llm/ai-sdk-adapter.js +0 -49
  92. package/dist/shared/llm/client.d.ts +0 -13
  93. package/dist/shared/llm/index.d.ts +0 -5
  94. package/dist/shared/llm/index.js +0 -6
  95. package/dist/shared/llm/types.d.ts +0 -67
  96. package/dist/shared/llm/types.js +0 -0
  97. package/src/cli/commands/init.ts +0 -331
  98. package/src/cli/commands/logs.ts +0 -128
  99. package/src/shared/llm/ai-sdk-adapter.ts +0 -81
  100. package/src/shared/llm/index.ts +0 -3
  101. package/src/shared/llm/types.ts +0 -63
@@ -1,7 +1,117 @@
1
1
  import { z } from "zod";
2
- import { runAiConfigure } from "../core/ai-config.js";
2
+ import {
3
+ CURRENT_CONFIG_VERSION,
4
+ readAiConfig,
5
+ writeAiConfig,
6
+ clearAiConfig,
7
+ type AiConfig,
8
+ } from "../core/config.js";
9
+ import { LIBRETTO_CONFIG_PATH } from "../core/context.js";
10
+ import { DEFAULT_SNAPSHOT_MODELS } from "../core/ai-model.js";
3
11
  import { SimpleCLI } from "../framework/simple-cli.js";
4
12
 
13
+ const PROVIDER_ALIASES: Record<string, string> = {
14
+ claude: DEFAULT_SNAPSHOT_MODELS.anthropic,
15
+ gemini: DEFAULT_SNAPSHOT_MODELS.google,
16
+ google: DEFAULT_SNAPSHOT_MODELS.google,
17
+ };
18
+
19
+ const CONFIGURE_PROVIDERS = [
20
+ "openai",
21
+ "anthropic",
22
+ "gemini",
23
+ "vertex",
24
+ ] as const;
25
+
26
+ function formatConfigureProviders(separator = " | "): string {
27
+ return CONFIGURE_PROVIDERS.join(separator);
28
+ }
29
+
30
+ function printAiConfig(config: AiConfig, configPath: string): void {
31
+ console.log(`Model: ${config.model}`);
32
+ console.log(`Config file: ${configPath}`);
33
+ console.log(`Updated at: ${config.updatedAt}`);
34
+ }
35
+
36
+ /**
37
+ * Resolve the model string from a `ai configure` argument.
38
+ * Accepts a provider shorthand ("openai", "anthropic", "gemini", "vertex")
39
+ * or a full provider/model-id string ("openai/gpt-4o", "anthropic/claude-sonnet-4-6").
40
+ */
41
+ function resolveModelFromInput(input: string): string | null {
42
+ const trimmed = input.trim();
43
+ if (!trimmed) return null;
44
+
45
+ // Full model string (contains a slash)
46
+ if (trimmed.includes("/")) return trimmed;
47
+
48
+ // Provider shorthand
49
+ const normalized = trimmed.toLowerCase();
50
+ return (
51
+ (DEFAULT_SNAPSHOT_MODELS as Record<string, string>)[normalized] ??
52
+ PROVIDER_ALIASES[normalized] ??
53
+ null
54
+ );
55
+ }
56
+
57
+ export function runAiConfigure(
58
+ input: {
59
+ preset?: string;
60
+ clear?: boolean;
61
+ },
62
+ options: {
63
+ configureCommandName?: string;
64
+ configPath?: string;
65
+ } = {},
66
+ ): void {
67
+ const configureCommandName =
68
+ options.configureCommandName ?? "npx libretto ai configure";
69
+ const configPath = options.configPath ?? LIBRETTO_CONFIG_PATH;
70
+
71
+ const presetArg = input.preset?.trim();
72
+
73
+ if (!presetArg && !input.clear) {
74
+ const config = readAiConfig(configPath);
75
+ if (!config) {
76
+ console.log(
77
+ `No AI config set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`,
78
+ );
79
+ console.log(
80
+ "Provider credentials still come from your shell or .env file.",
81
+ );
82
+ return;
83
+ }
84
+ printAiConfig(config, configPath);
85
+ return;
86
+ }
87
+
88
+ if (input.clear) {
89
+ const removed = clearAiConfig(configPath);
90
+ if (removed) {
91
+ console.log(`Cleared AI config: ${configPath}`);
92
+ } else {
93
+ console.log("No AI config was set.");
94
+ }
95
+ return;
96
+ }
97
+
98
+ const model = resolveModelFromInput(presetArg!);
99
+ if (!model) {
100
+ console.log(
101
+ `Usage: ${configureCommandName} <${CONFIGURE_PROVIDERS.join("|")}|provider/model-id>\n` +
102
+ ` ${configureCommandName}\n` +
103
+ ` ${configureCommandName} --clear`,
104
+ );
105
+ throw new Error(
106
+ `Invalid provider or model. Use one of: ${formatConfigureProviders()}, or a full model string like "openai/gpt-4o".`,
107
+ );
108
+ }
109
+
110
+ const config = writeAiConfig(model, configPath);
111
+ console.log("AI config saved.");
112
+ printAiConfig(config, configPath);
113
+ }
114
+
5
115
  export const aiConfigureInput = SimpleCLI.input({
6
116
  positionals: [
7
117
  SimpleCLI.positional("preset", z.string().optional(), {
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { SessionAccessModeSchema } from "../../shared/state/index.js";
2
3
  import {
3
4
  runClose as runCloseWithLogger,
4
5
  runCloseAll as runCloseAllWithLogger,
@@ -7,11 +8,15 @@ import {
7
8
  runPages,
8
9
  runSave,
9
10
  } from "../core/browser.js";
11
+ import { readLibrettoConfig } from "../core/config.js";
10
12
  import { createLoggerForSession, withSessionLogger } from "../core/context.js";
11
13
  import {
14
+ type SessionAccessMode,
12
15
  assertSessionAvailableForStart,
16
+ setSessionMode,
13
17
  validateSessionName,
14
18
  } from "../core/session.js";
19
+ import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
15
20
  import { SimpleCLI } from "../framework/simple-cli.js";
16
21
  import {
17
22
  sessionOption,
@@ -42,6 +47,13 @@ export function parseViewportArg(
42
47
  return { width, height };
43
48
  }
44
49
 
50
+ function resolveRequestedSessionMode(readOnly: boolean | undefined, writeAccess: boolean | undefined): SessionAccessMode {
51
+ if (readOnly) return "read-only";
52
+ if (writeAccess) return "write-access";
53
+ const config = readLibrettoConfig();
54
+ return config.sessionMode ?? "write-access";
55
+ }
56
+
45
57
  export const openInput = SimpleCLI.input({
46
58
  positionals: [
47
59
  SimpleCLI.positional("url", z.string().optional(), {
@@ -52,6 +64,14 @@ export const openInput = SimpleCLI.input({
52
64
  session: sessionOption(),
53
65
  headed: SimpleCLI.flag({ help: "Run browser in headed mode" }),
54
66
  headless: SimpleCLI.flag({ help: "Run browser in headless mode" }),
67
+ readOnly: SimpleCLI.flag({
68
+ name: "read-only",
69
+ help: "Create the session in read-only mode",
70
+ }),
71
+ writeAccess: SimpleCLI.flag({
72
+ name: "write-access",
73
+ help: "Create the session in write-access mode (overrides config default)",
74
+ }),
55
75
  viewport: SimpleCLI.option(z.string().optional(), {
56
76
  help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)",
57
77
  }),
@@ -59,11 +79,15 @@ export const openInput = SimpleCLI.input({
59
79
  })
60
80
  .refine(
61
81
  (input) => Boolean(input.url),
62
- `Usage: libretto open <url> [--headless] [--viewport WxH] [--session <name>]`,
82
+ `Usage: libretto open <url> [--headless] [--read-only|--write-access] [--viewport WxH] [--session <name>]`,
63
83
  )
64
84
  .refine(
65
85
  (input) => !(input.headed && input.headless),
66
86
  "Cannot pass both --headed and --headless.",
87
+ )
88
+ .refine(
89
+ (input) => !(input.readOnly && input.writeAccess),
90
+ "Cannot pass both --read-only and --write-access.",
67
91
  );
68
92
 
69
93
  export const openCommand = SimpleCLI.command({
@@ -72,10 +96,14 @@ export const openCommand = SimpleCLI.command({
72
96
  .input(openInput)
73
97
  .use(withAutoSession())
74
98
  .handle(async ({ input, ctx }) => {
99
+ warnIfInstalledSkillOutOfDate();
75
100
  assertSessionAvailableForStart(ctx.session, ctx.logger);
76
101
  const headed = input.headed || !input.headless;
77
102
  const viewport = parseViewportArg(input.viewport);
78
- await runOpen(input.url!, headed, ctx.session, ctx.logger, { viewport });
103
+ await runOpen(input.url!, headed, ctx.session, ctx.logger, {
104
+ viewport,
105
+ accessMode: resolveRequestedSessionMode(input.readOnly, input.writeAccess),
106
+ });
79
107
  });
80
108
 
81
109
  export const connectInput = SimpleCLI.input({
@@ -86,11 +114,24 @@ export const connectInput = SimpleCLI.input({
86
114
  ],
87
115
  named: {
88
116
  session: sessionOption(),
117
+ readOnly: SimpleCLI.flag({
118
+ name: "read-only",
119
+ help: "Create the session in read-only mode",
120
+ }),
121
+ writeAccess: SimpleCLI.flag({
122
+ name: "write-access",
123
+ help: "Create the session in write-access mode (overrides config default)",
124
+ }),
89
125
  },
90
- }).refine(
91
- (input) => Boolean(input.cdpUrl),
92
- `Usage: libretto connect <cdp-url> --session <name>`,
93
- );
126
+ })
127
+ .refine(
128
+ (input) => Boolean(input.cdpUrl),
129
+ `Usage: libretto connect <cdp-url> [--read-only|--write-access] --session <name>`,
130
+ )
131
+ .refine(
132
+ (input) => !(input.readOnly && input.writeAccess),
133
+ "Cannot pass both --read-only and --write-access.",
134
+ );
94
135
 
95
136
  export const connectCommand = SimpleCLI.command({
96
137
  description: "Connect to an existing Chrome DevTools Protocol (CDP) endpoint",
@@ -98,7 +139,13 @@ export const connectCommand = SimpleCLI.command({
98
139
  .input(connectInput)
99
140
  .use(withAutoSession())
100
141
  .handle(async ({ input, ctx }) => {
101
- await runConnectWithLogger(input.cdpUrl!, ctx.session, ctx.logger);
142
+ warnIfInstalledSkillOutOfDate();
143
+ await runConnectWithLogger(
144
+ input.cdpUrl!,
145
+ ctx.session,
146
+ ctx.logger,
147
+ resolveRequestedSessionMode(input.readOnly, input.writeAccess),
148
+ );
102
149
  });
103
150
 
104
151
  export const saveInput = SimpleCLI.input({
@@ -140,6 +187,32 @@ export const pagesCommand = SimpleCLI.command({
140
187
  await runPages(ctx.session, ctx.logger);
141
188
  });
142
189
 
190
+ export const sessionModeInput = SimpleCLI.input({
191
+ positionals: [
192
+ SimpleCLI.positional("mode", SessionAccessModeSchema.optional(), {
193
+ help: "Session mode to set",
194
+ }),
195
+ ],
196
+ named: {
197
+ session: sessionOption(),
198
+ },
199
+ });
200
+
201
+ export const sessionModeCommand = SimpleCLI.command({
202
+ description: "View or set the session access mode",
203
+ })
204
+ .input(sessionModeInput)
205
+ .use(withRequiredSession())
206
+ .handle(async ({ input, ctx }) => {
207
+ if (!input.mode) {
208
+ console.log(`Session "${ctx.session}" mode: ${ctx.sessionState.mode}`);
209
+ return;
210
+ }
211
+
212
+ const nextState = setSessionMode(ctx.session, input.mode, ctx.logger);
213
+ console.log(`Session "${ctx.session}" mode set to ${nextState.mode}.`);
214
+ });
215
+
143
216
  export const closeInput = SimpleCLI.input({
144
217
  positionals: [],
145
218
  named: {
@@ -179,6 +252,7 @@ export const browserCommands = {
179
252
  connect: connectCommand,
180
253
  save: saveCommand,
181
254
  pages: pagesCommand,
255
+ "session-mode": sessionModeCommand,
182
256
  close: closeCommand,
183
257
  };
184
258
 
@@ -14,16 +14,20 @@ import { parseViewportArg } from "./browser.js";
14
14
  import { getPauseSignalPaths } from "../core/pause-signals.js";
15
15
  import {
16
16
  assertSessionAvailableForStart,
17
+ assertSessionAllowsCommand,
17
18
  clearSessionState,
18
19
  readSessionState,
19
20
  setSessionStatus,
20
21
  type SessionState,
21
22
  } from "../core/session.js";
23
+ import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
22
24
  import {
23
25
  readActionLog,
24
26
  readNetworkLog,
25
27
  wrapPageForActionLogging,
26
28
  } from "../core/telemetry.js";
29
+ import { readLibrettoConfig } from "../core/config.js";
30
+ import { createReadonlyExecHelpers } from "../core/readonly-exec.js";
27
31
  import type { RunIntegrationWorkerRequest } from "../workers/run-integration-worker-protocol.js";
28
32
  import { SimpleCLI } from "../framework/simple-cli.js";
29
33
  import {
@@ -37,6 +41,7 @@ type ExecFunction = (...args: unknown[]) => Promise<unknown>;
37
41
  type RunIntegrationCommandRequest = RunIntegrationWorkerRequest & {
38
42
  tsconfigPath?: string;
39
43
  };
44
+ type ExecMode = "exec" | "readonly-exec";
40
45
 
41
46
  type StripTypeScriptTypesFn = (
42
47
  code: string,
@@ -202,14 +207,20 @@ async function runExec(
202
207
  code: string,
203
208
  session: string,
204
209
  logger: LoggerApi,
205
- visualize = false,
206
- pageId?: string,
210
+ options: {
211
+ visualize?: boolean;
212
+ pageId?: string;
213
+ mode?: ExecMode;
214
+ } = {},
207
215
  ): Promise<void> {
216
+ const visualize = options.visualize ?? false;
217
+ const pageId = options.pageId;
218
+ const mode = options.mode ?? "exec";
208
219
  const { cleaned: cleanedCode, strippedCount } = stripEmptyCatchHandlers(code);
209
220
  if (strippedCount > 0) {
210
221
  console.log("(Stripped `.catch(() => {})` — letting errors bubble up)");
211
222
  }
212
- logger.info("exec-start", {
223
+ logger.info(`${mode}-start`, {
213
224
  session,
214
225
  codeLength: cleanedCode.length,
215
226
  codePreview: cleanedCode.slice(0, 200),
@@ -235,20 +246,20 @@ async function runExec(
235
246
  const stallInterval = setInterval(() => {
236
247
  const silenceMs = Date.now() - lastActivityTs;
237
248
  if (silenceMs >= STALL_THRESHOLD_MS) {
238
- logger.warn("exec-stall-warning", {
249
+ logger.warn(`${mode}-stall-warning`, {
239
250
  session,
240
251
  silenceMs,
241
252
  codePreview: cleanedCode.slice(0, 200),
242
253
  });
243
254
  console.warn(
244
- `[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1000)}s — exec may be hung (code: ${cleanedCode.slice(0, 100)}...)`,
255
+ `[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1000)}s — ${mode} may be hung (code: ${cleanedCode.slice(0, 100)}...)`,
245
256
  );
246
257
  }
247
258
  }, STALL_THRESHOLD_MS);
248
259
 
249
260
  const execStartTs = Date.now();
250
261
  const sigintHandler = () => {
251
- logger.info("exec-interrupted", {
262
+ logger.info(`${mode}-interrupted`, {
252
263
  session,
253
264
  duration: Date.now() - execStartTs,
254
265
  codePreview: cleanedCode.slice(0, 200),
@@ -256,60 +267,67 @@ async function runExec(
256
267
  };
257
268
  process.on("SIGINT", sigintHandler);
258
269
 
259
- wrapPageForActionLogging(page, session, resolvedPageId, onActivity);
270
+ if (mode === "exec") {
271
+ wrapPageForActionLogging(page, session, resolvedPageId, onActivity);
272
+ }
260
273
 
261
- if (visualize) {
274
+ if (visualize && mode === "exec") {
262
275
  await installInstrumentation(page, { visualize: true, logger });
263
276
  }
264
277
 
265
278
  try {
266
- const execState: Record<string, unknown> = {};
267
-
268
- const networkLog = (
269
- opts: {
270
- last?: number;
271
- filter?: string;
272
- method?: string;
273
- pageId?: string;
274
- } = {},
275
- ) => {
276
- return readNetworkLog(session, opts);
277
- };
278
-
279
- const actionLog = (
280
- opts: {
281
- last?: number;
282
- filter?: string;
283
- action?: string;
284
- source?: string;
285
- pageId?: string;
286
- } = {},
287
- ) => {
288
- return readActionLog(session, opts);
289
- };
290
-
291
- const helpers = {
292
- page,
293
- context,
294
- state: execState,
295
- browser,
296
- networkLog,
297
- actionLog,
298
- console,
299
- setTimeout,
300
- setInterval,
301
- clearTimeout,
302
- clearInterval,
303
- fetch,
304
- URL,
305
- Buffer,
306
- };
279
+ const helpers =
280
+ mode === "readonly-exec"
281
+ ? createReadonlyExecHelpers(page, { onActivity })
282
+ : (() => {
283
+ const execState: Record<string, unknown> = {};
284
+
285
+ const networkLog = (
286
+ opts: {
287
+ last?: number;
288
+ filter?: string;
289
+ method?: string;
290
+ pageId?: string;
291
+ } = {},
292
+ ) => {
293
+ return readNetworkLog(session, opts);
294
+ };
295
+
296
+ const actionLog = (
297
+ opts: {
298
+ last?: number;
299
+ filter?: string;
300
+ action?: string;
301
+ source?: string;
302
+ pageId?: string;
303
+ } = {},
304
+ ) => {
305
+ return readActionLog(session, opts);
306
+ };
307
+
308
+ return {
309
+ page,
310
+ context,
311
+ state: execState,
312
+ browser,
313
+ networkLog,
314
+ actionLog,
315
+ console,
316
+ setTimeout,
317
+ setInterval,
318
+ clearTimeout,
319
+ clearInterval,
320
+ fetch,
321
+ URL,
322
+ Buffer,
323
+ };
324
+ })();
307
325
 
308
326
  const helperNames = Object.keys(helpers);
309
327
  const fn = compileExecFunction(cleanedCode, helperNames);
310
328
 
311
329
  const result = await fn(...Object.values(helpers));
312
- logger.info("exec-success", { session, hasResult: result !== undefined });
330
+ logger.info(`${mode}-success`, { session, hasResult: result !== undefined });
313
331
  if (result !== undefined) {
314
332
  console.log(
315
333
  typeof result === "string" ? result : JSON.stringify(result, null, 2),
@@ -318,7 +336,7 @@ async function runExec(
318
336
  console.log("Executed successfully");
319
337
  }
320
338
  } catch (err) {
321
- logger.error("exec-error", {
339
+ logger.error(`${mode}-error`, {
322
340
  error: err,
323
341
  session,
324
342
  codePreview: cleanedCode.slice(0, 200),
@@ -605,13 +623,13 @@ async function runIntegrationFromFile(
605
623
  );
606
624
  const payload = JSON.stringify({
607
625
  integrationPath: args.integrationPath,
608
- workflowName: args.workflowName,
609
626
  session: args.session,
610
627
  params: args.params,
611
628
  headless: args.headless,
612
629
  visualize: args.visualize,
613
630
  authProfileDomain: args.authProfileDomain,
614
631
  viewport: args.viewport,
632
+ accessMode: args.accessMode,
615
633
  } satisfies RunIntegrationWorkerRequest);
616
634
  const worker = spawn(
617
635
  process.execPath,
@@ -690,6 +708,7 @@ export const execCommand = SimpleCLI.command({
690
708
  .input(execInput)
691
709
  .use(withRequiredSession())
692
710
  .handle(async ({ input, ctx }) => {
711
+ assertSessionAllowsCommand(ctx.sessionState, "exec", ["write-access"]);
693
712
  const code = input.code!;
694
713
  const codeFromArgsOrStdin = code === "-" ? readStdinSync() : code;
695
714
  if (codeFromArgsOrStdin === null) {
@@ -701,21 +720,55 @@ export const execCommand = SimpleCLI.command({
701
720
  codeFromArgsOrStdin,
702
721
  ctx.session,
703
722
  ctx.logger,
704
- input.visualize,
705
- input.page,
723
+ {
724
+ visualize: input.visualize,
725
+ pageId: input.page,
726
+ mode: "exec",
727
+ },
706
728
  );
707
729
  });
708
730
 
709
- const runUsage = `Usage: libretto run <integrationFile> <workflowName> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--no-visualize] [--viewport WxH]`;
731
+ export const readonlyExecInput = SimpleCLI.input({
732
+ positionals: [
733
+ SimpleCLI.positional("code", z.string().optional(), {
734
+ help: "Read-only Playwright TypeScript code to execute",
735
+ }),
736
+ ],
737
+ named: {
738
+ session: sessionOption(),
739
+ page: pageOption(),
740
+ },
741
+ }).refine(
742
+ (input) => input.code !== undefined,
743
+ `Usage: libretto readonly-exec <code|-> [--session <name>] [--page <id>]\n echo '<code>' | libretto readonly-exec - [--session <name>] [--page <id>]`,
744
+ );
745
+
746
+ export const readonlyExecCommand = SimpleCLI.command({
747
+ description: "Execute read-only Playwright inspection code",
748
+ })
749
+ .input(readonlyExecInput)
750
+ .use(withRequiredSession())
751
+ .handle(async ({ input, ctx }) => {
752
+ const code = input.code!;
753
+ const codeFromArgsOrStdin = code === "-" ? readStdinSync() : code;
754
+ if (codeFromArgsOrStdin === null) {
755
+ throw new Error(
756
+ "Missing stdin input for `readonly-exec -`. Pipe inspection code into stdin.",
757
+ );
758
+ }
759
+ await runExec(codeFromArgsOrStdin, ctx.session, ctx.logger, {
760
+ pageId: input.page,
761
+ mode: "readonly-exec",
762
+ });
763
+ });
764
+
765
+ const runUsage = `Usage: libretto run <integrationFile> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--read-only|--write-access] [--no-visualize] [--viewport WxH]`;
710
766
 
711
767
  export const runInput = SimpleCLI.input({
712
768
  positionals: [
713
769
  SimpleCLI.positional("integrationFile", z.string().optional(), {
714
770
  help: "Path to the integration file",
715
771
  }),
716
- SimpleCLI.positional("workflowName", z.string().optional(), {
717
- help: "Workflow name to run (from workflow(name, handler))",
718
- }),
719
772
  ],
720
773
  named: {
721
774
  session: sessionOption(),
@@ -731,6 +784,14 @@ export const runInput = SimpleCLI.input({
731
784
  }),
732
785
  headed: SimpleCLI.flag({ help: "Run in headed mode" }),
733
786
  headless: SimpleCLI.flag({ help: "Run in headless mode" }),
787
+ readOnly: SimpleCLI.flag({
788
+ name: "read-only",
789
+ help: "Create the session in read-only mode",
790
+ }),
791
+ writeAccess: SimpleCLI.flag({
792
+ name: "write-access",
793
+ help: "Create the session in write-access mode (overrides config default)",
794
+ }),
734
795
  noVisualize: SimpleCLI.flag({
735
796
  name: "no-visualize",
736
797
  help: "Disable ghost cursor + highlight visualization in headed mode",
@@ -745,7 +806,7 @@ export const runInput = SimpleCLI.input({
745
806
  },
746
807
  })
747
808
  .refine(
748
- (input) => Boolean(input.integrationFile && input.workflowName),
809
+ (input) => Boolean(input.integrationFile),
749
810
  runUsage,
750
811
  )
751
812
  .refine(
@@ -755,6 +816,10 @@ export const runInput = SimpleCLI.input({
755
816
  .refine(
756
817
  (input) => !(input.headed && input.headless),
757
818
  "Cannot pass both --headed and --headless.",
819
+ )
820
+ .refine(
821
+ (input) => !(input.readOnly && input.writeAccess),
822
+ "Cannot pass both --read-only and --write-access.",
758
823
  );
759
824
 
760
825
  function resolveRunParams(
@@ -779,11 +844,12 @@ function resolveRunParams(
779
844
  }
780
845
 
781
846
  export const runCommand = SimpleCLI.command({
782
- description: "Run an exported Libretto workflow from a file",
847
+ description: "Run the default-exported Libretto workflow from a file",
783
848
  })
784
849
  .input(runInput)
785
850
  .use(withAutoSession())
786
851
  .handle(async ({ input, ctx }) => {
852
+ warnIfInstalledSkillOutOfDate();
787
853
  await stopExistingFailedRunSession(ctx.session, ctx.logger);
788
854
  assertSessionAvailableForStart(ctx.session, ctx.logger);
789
855
 
@@ -802,7 +868,6 @@ export const runCommand = SimpleCLI.command({
802
868
  await runIntegrationFromFile(
803
869
  {
804
870
  integrationPath: input.integrationFile!,
805
- workflowName: input.workflowName!,
806
871
  session: ctx.session,
807
872
  params,
808
873
  tsconfigPath: input.tsconfig,
@@ -810,6 +875,7 @@ export const runCommand = SimpleCLI.command({
810
875
  visualize,
811
876
  authProfileDomain: input.authProfile,
812
877
  viewport,
878
+ accessMode: input.readOnly ? "read-only" : input.writeAccess ? "write-access" : (readLibrettoConfig().sessionMode ?? "write-access"),
813
879
  },
814
880
  ctx.logger,
815
881
  );
@@ -833,6 +899,7 @@ export const resumeCommand = SimpleCLI.command({
833
899
 
834
900
  export const executionCommands = {
835
901
  exec: execCommand,
902
+ "readonly-exec": readonlyExecCommand,
836
903
  run: runCommand,
837
904
  resume: resumeCommand,
838
905
  };