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
@@ -8,7 +8,7 @@ import {
8
8
  SESSION_STATE_VERSION,
9
9
  SessionStateFileSchema
10
10
  } from "../state/session-state.js";
11
- import { readLibrettoConfig } from "../../cli/core/ai-config.js";
11
+ import { readLibrettoConfig } from "../../cli/core/config.js";
12
12
  async function pickFreePort() {
13
13
  return await new Promise((resolve, reject) => {
14
14
  const server = createServer();
@@ -63,7 +63,8 @@ async function launchBrowser({
63
63
  sessionName,
64
64
  headless = false,
65
65
  viewport = { width: 1366, height: 768 },
66
- storageStatePath
66
+ storageStatePath,
67
+ accessMode = "write-access"
67
68
  }) {
68
69
  const debugPort = await pickFreePort();
69
70
  const windowPosition = headless ? void 0 : resolveWindowPosition();
@@ -96,7 +97,8 @@ async function launchBrowser({
96
97
  port: debugPort,
97
98
  pid: process.pid,
98
99
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
99
- status: "active"
100
+ status: "active",
101
+ mode: accessMode
100
102
  },
101
103
  null,
102
104
  2
@@ -1,2 +1,2 @@
1
- export { SESSION_STATE_VERSION, SessionState, SessionStateFile, SessionStateFileSchema, SessionStatus, SessionStatusSchema, parseSessionStateContent, parseSessionStateData, serializeSessionState } from './session-state.js';
1
+ export { SESSION_STATE_VERSION, SessionAccessMode, SessionAccessModeSchema, SessionState, SessionStateFile, SessionStateFileSchema, SessionStatus, SessionStatusSchema, parseSessionStateContent, parseSessionStateData, serializeSessionState } from './session-state.js';
2
2
  import 'zod';
@@ -1,4 +1,5 @@
1
1
  import {
2
+ SessionAccessModeSchema,
2
3
  SESSION_STATE_VERSION,
3
4
  SessionStatusSchema,
4
5
  SessionStateFileSchema,
@@ -8,6 +9,7 @@ import {
8
9
  } from "./session-state.js";
9
10
  export {
10
11
  SESSION_STATE_VERSION,
12
+ SessionAccessModeSchema,
11
13
  SessionStateFileSchema,
12
14
  SessionStatusSchema,
13
15
  parseSessionStateContent,
@@ -8,6 +8,10 @@ declare const SessionStatusSchema: z.ZodEnum<{
8
8
  failed: "failed";
9
9
  exited: "exited";
10
10
  }>;
11
+ declare const SessionAccessModeSchema: z.ZodEnum<{
12
+ "read-only": "read-only";
13
+ "write-access": "write-access";
14
+ }>;
11
15
  declare const SessionViewportSchema: z.ZodObject<{
12
16
  width: z.ZodNumber;
13
17
  height: z.ZodNumber;
@@ -26,16 +30,21 @@ declare const SessionStateFileSchema: z.ZodObject<{
26
30
  failed: "failed";
27
31
  exited: "exited";
28
32
  }>>;
33
+ mode: z.ZodDefault<z.ZodEnum<{
34
+ "read-only": "read-only";
35
+ "write-access": "write-access";
36
+ }>>;
29
37
  viewport: z.ZodOptional<z.ZodObject<{
30
38
  width: z.ZodNumber;
31
39
  height: z.ZodNumber;
32
40
  }, z.core.$strip>>;
33
41
  }, z.core.$strip>;
34
42
  type SessionStatus = z.infer<typeof SessionStatusSchema>;
43
+ type SessionAccessMode = z.infer<typeof SessionAccessModeSchema>;
35
44
  type SessionStateFile = z.infer<typeof SessionStateFileSchema>;
36
45
  type SessionState = Omit<SessionStateFile, "version">;
37
46
  declare function parseSessionStateData(rawState: unknown, source: string): SessionState;
38
47
  declare function parseSessionStateContent(content: string, source: string): SessionState;
39
48
  declare function serializeSessionState(state: SessionState): SessionStateFile;
40
49
 
41
- export { SESSION_STATE_VERSION, type SessionState, type SessionStateFile, SessionStateFileSchema, type SessionStatus, SessionStatusSchema, SessionViewportSchema, parseSessionStateContent, parseSessionStateData, serializeSessionState };
50
+ export { SESSION_STATE_VERSION, type SessionAccessMode, SessionAccessModeSchema, type SessionState, type SessionStateFile, SessionStateFileSchema, type SessionStatus, SessionStatusSchema, SessionViewportSchema, parseSessionStateContent, parseSessionStateData, serializeSessionState };
@@ -7,6 +7,7 @@ const SessionStatusSchema = z.enum([
7
7
  "failed",
8
8
  "exited"
9
9
  ]);
10
+ const SessionAccessModeSchema = z.enum(["read-only", "write-access"]);
10
11
  const SessionViewportSchema = z.object({
11
12
  width: z.number().int().min(1),
12
13
  height: z.number().int().min(1)
@@ -19,6 +20,7 @@ const SessionStateFileSchema = z.object({
19
20
  session: z.string().min(1),
20
21
  startedAt: z.string().datetime({ offset: true }),
21
22
  status: SessionStatusSchema.optional(),
23
+ mode: SessionAccessModeSchema.default("write-access"),
22
24
  viewport: SessionViewportSchema.optional()
23
25
  });
24
26
  function formatIssues(error) {
@@ -56,6 +58,7 @@ function serializeSessionState(state) {
56
58
  }
57
59
  export {
58
60
  SESSION_STATE_VERSION,
61
+ SessionAccessModeSchema,
59
62
  SessionStateFileSchema,
60
63
  SessionStatusSchema,
61
64
  SessionViewportSchema,
@@ -1,11 +1,9 @@
1
1
  import { Page } from 'playwright';
2
- import { MinimalLogger } from '../logger/logger.js';
3
2
 
4
3
  declare const LIBRETTO_WORKFLOW_BRAND: unique symbol;
5
4
  type LibrettoWorkflowContext = {
6
5
  session: string;
7
6
  page: Page;
8
- logger: MinimalLogger;
9
7
  };
10
8
  type LibrettoWorkflowHandler<Input = unknown, Output = unknown> = (ctx: LibrettoWorkflowContext, input: Input) => Promise<Output>;
11
9
  declare class LibrettoWorkflow<Input = unknown, Output = unknown> {
@@ -23,7 +21,8 @@ type ExportedLibrettoWorkflow = {
23
21
  type WorkflowModuleExports = Record<string, unknown>;
24
22
  declare function isLibrettoWorkflow(value: unknown): value is ExportedLibrettoWorkflow;
25
23
  declare function getWorkflowsFromModuleExports(moduleExports: WorkflowModuleExports): ExportedLibrettoWorkflow[];
24
+ declare function getDefaultWorkflowFromModuleExports(moduleExports: WorkflowModuleExports): ExportedLibrettoWorkflow | null;
26
25
  declare function getWorkflowFromModuleExports(moduleExports: WorkflowModuleExports, workflowName: string): ExportedLibrettoWorkflow | null;
27
26
  declare function workflow<Input = unknown, Output = unknown>(name: string, handler: LibrettoWorkflowHandler<Input, Output>): LibrettoWorkflow<Input, Output>;
28
27
 
29
- export { type ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowHandler, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, workflow };
28
+ export { type ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowHandler, getDefaultWorkflowFromModuleExports, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, workflow };
@@ -26,24 +26,30 @@ function addWorkflowOrThrow(workflowsByName, value) {
26
26
  }
27
27
  workflowsByName.set(value.name, value);
28
28
  }
29
- function getWorkflowsFromModuleExports(moduleExports) {
29
+ function collectWorkflowsOrThrow(values) {
30
30
  const workflowsByName = /* @__PURE__ */ new Map();
31
+ for (const value of values) {
32
+ addWorkflowOrThrow(workflowsByName, value);
33
+ }
34
+ return [...workflowsByName.values()];
35
+ }
36
+ function getWorkflowsFromModuleExports(moduleExports) {
37
+ const discoveredValues = [];
31
38
  for (const [exportName, value] of Object.entries(moduleExports)) {
32
39
  if (exportName === "workflows" && value && typeof value === "object") {
33
40
  if (isLibrettoWorkflow(value)) {
34
- addWorkflowOrThrow(workflowsByName, value);
41
+ discoveredValues.push(value);
35
42
  } else {
36
- for (const nestedValue of Object.values(
37
- value
38
- )) {
39
- addWorkflowOrThrow(workflowsByName, nestedValue);
40
- }
43
+ discoveredValues.push(...Object.values(value));
41
44
  }
42
45
  continue;
43
46
  }
44
- addWorkflowOrThrow(workflowsByName, value);
47
+ discoveredValues.push(value);
45
48
  }
46
- return [...workflowsByName.values()];
49
+ return collectWorkflowsOrThrow(discoveredValues);
50
+ }
51
+ function getDefaultWorkflowFromModuleExports(moduleExports) {
52
+ return isLibrettoWorkflow(moduleExports.default) ? moduleExports.default : null;
47
53
  }
48
54
  function getWorkflowFromModuleExports(moduleExports, workflowName) {
49
55
  for (const workflow2 of getWorkflowsFromModuleExports(moduleExports)) {
@@ -59,6 +65,7 @@ function workflow(name, handler) {
59
65
  export {
60
66
  LIBRETTO_WORKFLOW_BRAND,
61
67
  LibrettoWorkflow,
68
+ getDefaultWorkflowFromModuleExports,
62
69
  getWorkflowFromModuleExports,
63
70
  getWorkflowsFromModuleExports,
64
71
  isLibrettoWorkflow,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libretto",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "AI-powered browser automation library and CLI built on Playwright",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -8,7 +8,7 @@
8
8
  "url": "https://github.com/saffron-health/libretto"
9
9
  },
10
10
  "type": "module",
11
- "packageManager": "pnpm@9.15.4",
11
+ "packageManager": "pnpm@10.33.0",
12
12
  "publishConfig": {
13
13
  "access": "public"
14
14
  },
@@ -16,7 +16,7 @@
16
16
  "dist",
17
17
  "src",
18
18
  "scripts",
19
- "skills/libretto"
19
+ "skills"
20
20
  ],
21
21
  "types": "./dist/index.d.ts",
22
22
  "bin": {
@@ -30,7 +30,6 @@
30
30
  }
31
31
  },
32
32
  "scripts": {
33
- "postinstall": "node scripts/postinstall.mjs",
34
33
  "sync:mirrors": "node ../dev-tools/scripts/sync-mirrors.mjs",
35
34
  "check:mirrors": "node ../dev-tools/scripts/check-mirrors-sync.mjs",
36
35
  "sync-skills": "pnpm run sync:mirrors",
@@ -5,7 +5,7 @@ import { dirname, join } from "node:path";
5
5
  import { spawnSync } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
- import { SKILL_DIRS, syncSkillDir } from "./skills-libretto.mjs";
8
+ import { SKILL_MIRRORS, syncSkillDir } from "./skills-libretto.mjs";
9
9
 
10
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
11
  const packageRoot = join(__dirname, "..");
@@ -36,15 +36,17 @@ const repoRoot =
36
36
  ? gitResult.stdout.trim()
37
37
  : installCwd;
38
38
 
39
- const sourceDir = join(packageRoot, "skills", "libretto");
40
- if (!existsSync(sourceDir)) process.exit(0);
41
-
42
39
  const syncMissingDirs = repoRoot === packageRoot;
43
- for (const dir of SKILL_DIRS.slice(1)) {
44
- const rootName = dir.split("/")[0];
45
- const rootDir = join(repoRoot, rootName);
46
- if (!syncMissingDirs && !existsSync(rootDir)) continue;
47
- const dest = join(repoRoot, dir);
48
- syncSkillDir(sourceDir, dest);
49
- console.log(`libretto: synced skills/libretto -> ${dest}`);
40
+ for (const skillMirror of SKILL_MIRRORS) {
41
+ const sourceDir = join(packageRoot, skillMirror.source.replace(/^packages\/libretto\//, ""));
42
+ if (!existsSync(sourceDir)) continue;
43
+
44
+ for (const dir of skillMirror.targets) {
45
+ const rootName = dir.split("/")[0];
46
+ const rootDir = join(repoRoot, rootName);
47
+ if (!syncMissingDirs && !existsSync(rootDir)) continue;
48
+ const dest = join(repoRoot, dir);
49
+ syncSkillDir(sourceDir, dest);
50
+ console.log(`libretto: synced ${skillMirror.source} -> ${dest}`);
51
+ }
50
52
  }
@@ -2,10 +2,20 @@
2
2
 
3
3
  import { cpSync, mkdirSync, rmSync } from "node:fs";
4
4
 
5
- export const SKILL_DIRS = [
6
- "packages/libretto/skills/libretto",
7
- ".agents/skills/libretto",
8
- ".claude/skills/libretto",
5
+ export const SKILL_MIRRORS = [
6
+ {
7
+ name: "libretto",
8
+ source: "packages/libretto/skills/libretto",
9
+ targets: [".agents/skills/libretto", ".claude/skills/libretto"],
10
+ },
11
+ {
12
+ name: "libretto-readonly",
13
+ source: "packages/libretto/skills/libretto-readonly",
14
+ targets: [
15
+ ".agents/skills/libretto-readonly",
16
+ ".claude/skills/libretto-readonly",
17
+ ],
18
+ },
9
19
  ];
10
20
 
11
21
  export function syncSkillDir(sourceDir, destDir) {
@@ -0,0 +1,11 @@
1
+ # Skills Directory
2
+
3
+ - `skills/libretto` is the source of truth for the interactive Libretto skill.
4
+ - `skills/libretto-readonly` is the source of truth for the read-only diagnosis skill.
5
+ - The mirrored copies in `.agents/skills/*` and `.claude/skills/*` are generated from the matching source directories under `skills/`.
6
+ - Edit files under `skills/` directly. Do not hand-edit the mirrored copies.
7
+
8
+ ## Syncing
9
+
10
+ - Run `pnpm sync:mirrors` after changing anything under `skills/`.
11
+ - Run `pnpm check:mirrors` to verify that generated READMEs, skill mirrors, and skill version metadata are in sync.
@@ -4,7 +4,7 @@ description: "Browser automation CLI for building, maintaining, and running brow
4
4
  license: MIT
5
5
  metadata:
6
6
  author: saffron-health
7
- version: "0.5.4"
7
+ version: "0.5.6"
8
8
  ---
9
9
 
10
10
  ## How Libretto Works
@@ -21,8 +21,10 @@ metadata:
21
21
 
22
22
  ## Setup
23
23
 
24
- - Use `npx libretto init` for first-time workspace setup (sets up config file and snapshot command).
25
- - If credentials are already available, `npx libretto ai configure openai|anthropic|gemini|vertex` is usually enough.
24
+ - Use `npx libretto setup` for first-time workspace onboarding. It installs Chromium, syncs skills, and pins the default snapshot model to `.libretto/config.json` when provider credentials are available.
25
+ - Re-running `setup` on a healthy workspace shows the current configuration. If credentials are missing for a configured provider, it offers an interactive repair flow.
26
+ - Use `npx libretto status` to inspect AI configuration health and open sessions without triggering setup.
27
+ - Use `npx libretto ai configure openai|anthropic|gemini|vertex` to explicitly change the snapshot model or provider (advanced override).
26
28
 
27
29
  ## Working Rules
28
30
 
@@ -35,6 +37,7 @@ metadata:
35
37
  - Treat exploration sessions as disposable unless the user explicitly wants one kept open.
36
38
  - Get explicit user confirmation before mutating actions or replaying network requests that may have side effects.
37
39
  - Never run multiple `exec` commands at the same time.
40
+ - If the browser must remain read-only, switch to the `libretto-readonly` skill and use `readonly-exec` instead of `exec`.
38
41
 
39
42
  ## Commands
40
43
 
@@ -43,23 +46,38 @@ metadata:
43
46
  - Open a page before using `exec` or `snapshot`.
44
47
  - Use `open` at the start of script authoring when you need live page state to decide how the workflow should work.
45
48
  - Use headed mode when the user needs to log in or watch the workflow.
49
+ - Pass `--read-only` when you want the session locked for inspection from the moment it is created.
46
50
 
47
51
  ```bash
48
52
  npx libretto open https://example.com --headed
53
+ npx libretto open https://example.com --headless --read-only --session readonly-example
49
54
  npx libretto open https://example.com --headless --session debug-example
50
55
  ```
51
56
 
52
57
  ### `connect`
53
58
 
54
59
  - Use `connect` to attach to any existing Chrome DevTools Protocol (CDP) endpoint — a browser started with `--remote-debugging-port`, an Electron app, or any other CDP-compatible target.
55
- - After connecting, `exec`, `snapshot`, `pages`, and all other session commands work normally.
60
+ - After connecting, `exec`, `snapshot`, `pages`, and the rest of the session commands follow that session's stored mode.
56
61
  - Libretto does not manage the connected process's lifecycle. `close` clears the session but does not terminate the remote process.
62
+ - Pass `--read-only` if the connected session must stay inspection-only from the start.
57
63
 
58
64
  ```bash
59
65
  npx libretto connect http://127.0.0.1:9222 --session my-session
66
+ npx libretto connect http://127.0.0.1:9222 --read-only --session readonly-session
60
67
  npx libretto connect http://127.0.0.1:9223 --session another-session
61
68
  ```
62
69
 
70
+ ### `session-mode`
71
+
72
+ - Use `session-mode` to inspect whether an existing session is `write-access` or `read-only`.
73
+ - Only a user can change the session mode for an existing session. Never change a session's mode on your own — the user must change it themselves manually.
74
+ - `open`, `run`, and `connect` default new sessions to `write-access` unless the config sets `sessionMode` to `read-only`.
75
+ - Pass `--read-only` or `--write-access` to override the config default for a single command.
76
+
77
+ ```bash
78
+ npx libretto session-mode --session my-session
79
+ ```
80
+
63
81
  ### `snapshot`
64
82
 
65
83
  - Use `snapshot` as the primary page observation tool.
@@ -87,6 +105,7 @@ npx libretto snapshot \
87
105
  - Available globals: `page`, `context`, `browser`, `state`, `fetch`, `Buffer`.
88
106
  - Let failures throw. Do not hide `exec` failures with `try/catch` or `.catch()`.
89
107
  - Do not run multiple `exec` commands in parallel.
108
+ - Do not use `exec` in read-only diagnosis flows. Use `readonly-exec` from the `libretto-readonly` skill for those sessions.
90
109
 
91
110
  ```bash
92
111
  npx libretto exec "return await page.url()"
@@ -98,7 +117,7 @@ echo "return await page.url()" | npx libretto exec - --session debug-example
98
117
  ### `pages`
99
118
 
100
119
  - Use `pages` when a popup, new tab, or second page appears.
101
- - If `exec`, `snapshot`, `network`, or `actions` complains about multiple pages, list page ids first and then pass `--page`.
120
+ - If `exec` or `snapshot` complains about multiple pages, list page ids first and then pass `--page`.
102
121
 
103
122
  ```bash
104
123
  npx libretto pages --session debug-example
@@ -109,14 +128,16 @@ npx libretto exec --session debug-example --page <page-id> "return await page.ur
109
128
 
110
129
  - Use `run` to verify a workflow file after creating it or editing it, preferring `run --headless` for the normal fix/verify loop.
111
130
  - Plain `run` defaults to headed mode.
131
+ - Pass `--read-only` if the preserved session should come back locked for follow-up terminal inspection after the workflow run.
112
132
  - If the workflow fails, Libretto keeps the browser open. Inspect the failed state with `snapshot` and `exec` before editing code.
113
133
  - Insert `await pause(session)` statements in the workflow file when you need to stop at specific states for interactive debugging, like breakpoints in the browser flow.
114
134
  - If the workflow pauses, resume it with `npx libretto resume --session <name>`.
115
135
  - Re-run the same workflow after each fix to verify the browser behavior end to end.
116
136
 
117
137
  ```bash
118
- npx libretto run ./integration.ts workflowName --headless --params '{"status":"open"}'
119
- npx libretto run ./integration.ts workflowName --auth-profile app.example.com
138
+ npx libretto run ./integration.ts --headless --params '{"status":"open"}'
139
+ npx libretto run ./integration.ts --headless --read-only
140
+ npx libretto run ./integration.ts --auth-profile app.example.com
120
141
  ```
121
142
 
122
143
  ### `resume`
@@ -191,7 +212,7 @@ Assistant: I'll inspect the real site first if needed, but before I finish I'll
191
212
  Assistant: [Runs `npx libretto open https://target.example.com --headed`]
192
213
  Assistant: [Reads `references/site-security-review.md` before choosing between passive network inspection, direct browser fetch calls, and Playwright-first automation]
193
214
  Assistant: [Runs `npx libretto snapshot --objective "Find the next required action" --context "We are starting the workflow from the landing page and need the first meaningful step."`]
194
- Assistant: [Uses `network`, `snapshot`, and `exec` as needed to understand the site and decide the implementation path]
215
+ Assistant: [Uses `snapshot` and `exec` as needed to understand the site and decide the implementation path]
195
216
  Assistant: [Reads `references/code-generation-rules.md` before writing production workflow code]
196
217
  Assistant: I found the working path. I'll now update the workflow file outside Libretto and verify it with `npx libretto run ...`.
197
218
  </example>
@@ -203,7 +224,7 @@ Assistant: I found the working path. I'll now update the workflow file outside L
203
224
  <example>
204
225
  [Context: The user has an existing Libretto workflow that is failing]
205
226
  Assistant: I'll reproduce the failure first so we can inspect the exact browser state it leaves behind.
206
- Assistant: [Runs `npx libretto run ./integration.ts main --session debug-flow --headed`]
227
+ Assistant: [Runs `npx libretto run ./integration.ts --session debug-flow --headed`]
207
228
  Assistant: The workflow failed and Libretto kept the browser open. I'll inspect the page state before changing code.
208
229
  Assistant: [Runs `npx libretto snapshot --session debug-flow --objective "Find the blocking error or broken selector target" --context "The workflow just failed after trying to continue from the review step, and I need to identify the visible blocker on the current page."`]
209
230
  Assistant: [Runs `npx libretto exec --session debug-flow "...focused inspection or prototype..."`]
@@ -19,7 +19,7 @@ Use this reference only when the user explicitly asks to save or reuse local aut
19
19
  ```bash
20
20
  npx libretto open https://app.example.com --headed
21
21
  npx libretto save app.example.com
22
- npx libretto run ./integration.ts main --auth-profile app.example.com
22
+ npx libretto run ./integration.ts --auth-profile app.example.com
23
23
  ```
24
24
 
25
25
  ## Notes
@@ -6,7 +6,7 @@ Follow the user's existing codebase conventions, abstractions, and patterns when
6
6
 
7
7
  ## Workflow File Structure
8
8
 
9
- Generated files must export a `workflow()` instance so they can be run via `npx libretto run <file> <workflowName>`. Import `workflow` and its types from `"libretto"`:
9
+ Generated files must default-export a `workflow()` instance so they can be run via `npx libretto run <file>`. Import `workflow` and its types from `"libretto"`:
10
10
 
11
11
  ```typescript
12
12
  import { workflow, pause, type LibrettoWorkflowContext } from "libretto";
@@ -22,12 +22,12 @@ type Output = {
22
22
  results: Array<{ name: string; value: string }>;
23
23
  };
24
24
 
25
- export const myWorkflow = workflow<Input, Output>(
25
+ export default workflow<Input, Output>(
26
26
  "myWorkflow",
27
27
  async (ctx: LibrettoWorkflowContext, input): Promise<Output> => {
28
- const { session, page, logger } = ctx;
28
+ const { session, page } = ctx;
29
29
 
30
- logger.info("workflow-start", { session, query: input.query });
30
+ console.log("workflow-start", { session, query: input.query });
31
31
  await page.goto("https://example.com");
32
32
  await pause(session);
33
33
 
@@ -39,8 +39,8 @@ export const myWorkflow = workflow<Input, Output>(
39
39
  Key points:
40
40
 
41
41
  - `workflow(name, handler)` takes a unique workflow name and returns the workflow object that Libretto can run.
42
- - `npx libretto run ./file.ts myWorkflow` resolves `myWorkflow` from the workflows exported by `./file.ts`, so export or re-export the workflow from that file directly or through a `workflows` object, and make sure the run argument matches the name passed to `workflow("myWorkflow", ...)`.
43
- - `ctx` provides `session`, `page`, and `logger`
42
+ - `npx libretto run ./file.ts` executes the file's default-exported workflow, so always use `export default workflow(...)`.
43
+ - `ctx` provides `session` and `page`. Use `console.log`/`console.warn`/`console.error` for logging — the runtime wraps these with structured metadata automatically.
44
44
  - `input` comes from `--params '{"query":"foo"}'` or `--params-file params.json` on the CLI
45
45
  - Use `await pause(ctx.session)` (or `await pause(session)`) to pause the workflow for debugging. It is a no-op in production.
46
46
  - After validation is complete and the workflow is confirmed working end to end, remove all `pause()` calls and pause-only workflow params unless the user explicitly says to keep them.
@@ -12,8 +12,9 @@ Use this reference when you need to inspect or change the workspace configuratio
12
12
 
13
13
  Libretto reads workspace config from `.libretto/config.json`.
14
14
 
15
- - The file is usually created or updated by `npx libretto ai configure ...`.
15
+ - The file is created by `npx libretto setup` during first-time onboarding (auto-pins the default model for the detected provider) or by `npx libretto ai configure ...` for explicit overrides.
16
16
  - API credentials still come from your shell environment or `.env`. The config file stores the selected model, not the secret itself.
17
+ - Use `npx libretto status` to inspect the current AI configuration and open sessions without changing anything.
17
18
  - For first-time setup instructions, follow the main `SKILL.md` flow instead of expanding this reference.
18
19
 
19
20
  ## Supported Settings
@@ -21,6 +22,7 @@ Libretto reads workspace config from `.libretto/config.json`.
21
22
  - `ai.model` selects the configured analysis model for `snapshot`.
22
23
  - `viewport` is an optional top-level setting used by `open` and `run` when you do not pass `--viewport`.
23
24
  - Viewport precedence is: CLI `--viewport`, then `.libretto/config.json`, then the default `1366x768`.
25
+ - `sessionMode` sets the default session access mode for new sessions created by `open`, `connect`, and `run`. Must be `"read-only"` or `"write-access"`. When omitted, defaults to `"write-access"`. Pass `--read-only` or `--write-access` to `open`, `connect`, or `run` to override when creating a session.
24
26
 
25
27
  Example:
26
28
 
@@ -34,20 +36,23 @@ Example:
34
36
  "viewport": {
35
37
  "width": 1280,
36
38
  "height": 800
37
- }
39
+ },
40
+ "sessionMode": "write-access"
38
41
  }
39
42
  ```
40
43
 
41
44
  ## Common Commands
42
45
 
43
46
  ```bash
44
- npx libretto init
45
- npx libretto ai configure openai
47
+ npx libretto setup # first-time onboarding, auto-pins default model
48
+ npx libretto status # inspect AI config and open sessions
49
+ npx libretto ai configure openai # explicitly change provider/model
46
50
  npx libretto open https://app.example.com --viewport 1440x900
47
- npx libretto run ./integration.ts main --viewport 1440x900
51
+ npx libretto run ./integration.ts --viewport 1440x900
48
52
  ```
49
53
 
50
54
  ## Notes
51
55
 
52
56
  - If you want a persistent default viewport for the workspace, add `viewport` to `.libretto/config.json` instead of repeating `--viewport` on every command.
53
- - If `snapshot` analysis is not configured yet, return to the setup steps in the main `SKILL.md` flow.
57
+ - If `snapshot` analysis is not configured yet, run `npx libretto setup` to auto-configure, or see the main `SKILL.md` flow.
58
+ - Run `npx libretto status` at any time to check which model is active and whether credentials are present.
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: libretto-readonly
3
+ description: "Read-only Libretto workflow for diagnosing live browser state without clicks, typing, navigation, or mutation requests."
4
+ license: MIT
5
+ metadata:
6
+ author: saffron-health
7
+ version: "0.5.6"
8
+ ---
9
+
10
+ ## How Libretto Read-Only Works
11
+
12
+ - Use this skill when the browser session must stay strictly read-only.
13
+ - Libretto stores read-only vs write-access on the session itself.
14
+ - The primary inspection tools are `snapshot` and `readonly-exec`.
15
+ - `readonly-exec` reuses Libretto's normal execution pipeline, but it only exposes read-only helpers and denies mutating Playwright methods.
16
+ - Only a user can change the session mode for an existing session. Never change a session's mode on your own — the user must change it themselves manually.
17
+
18
+ ## Working Rules
19
+
20
+ - Announce which session you are using and what page you are inspecting.
21
+ - Do not use `exec`, `run`, or any direct Playwright action that could change browser or application state.
22
+ - Do not click, type, submit forms, navigate, upload files, dispatch DOM events, or send non-GET requests.
23
+ - Prefer `snapshot` first when the visible page state is unclear.
24
+ - Use `readonly-exec` for focused inspection: titles, HTML, locator text, counts, visibility checks, and GET requests.
25
+ - Keep snippets small and purpose-built. Do not run multiple `readonly-exec` commands at the same time.
26
+ - End with diagnosis and handoff guidance, not an attempted in-browser repair.
27
+
28
+ ## Commands
29
+
30
+ ### `connect`
31
+
32
+ - Use `connect` to attach to an existing CDP endpoint for a preserved browser session.
33
+ - Use `--read-only` when creating the Libretto session handle for a preserved browser session.
34
+ - Libretto read-only mode is enforced through Libretto commands; direct CDP clients that skip Libretto are outside this boundary.
35
+
36
+ ```bash
37
+ npx libretto connect http://127.0.0.1:9222 --read-only --session failed-job-debug
38
+ ```
39
+
40
+ ### `pages`
41
+
42
+ - Use `pages` when a popup, new tab, or second page exists.
43
+ - If `readonly-exec` or `snapshot` complains about multiple pages, list ids first and then pass `--page`.
44
+
45
+ ```bash
46
+ npx libretto pages --session failed-job-debug
47
+ ```
48
+
49
+ ### `snapshot`
50
+
51
+ - Use `snapshot` as the first high-level observation tool.
52
+ - Always provide both `--objective` and `--context`.
53
+
54
+ ```bash
55
+ npx libretto snapshot \
56
+ --session failed-job-debug \
57
+ --objective "Identify the visible failure state and likely blocking UI condition" \
58
+ --context "The workflow already failed and the preserved browser must remain read-only."
59
+ ```
60
+
61
+ ### `readonly-exec`
62
+
63
+ - Use `readonly-exec` for narrow inspection code only.
64
+ - Denied operations fail with `ReadonlyExecDenied: ...`.
65
+
66
+ #### Helpers
67
+
68
+ - `page` — a read-only Playwright `Page` proxy. Standard Playwright read methods work normally (`url()`, `title()`, `content()`, `getByRole()`, `locator()`, `textContent()`, `isVisible()`, `count()`, `scrollIntoViewIfNeeded()`, etc.). Anything that mutates the page (`click`, `fill`, `goto`, `evaluate`, `keyboard`, `mouse`) is blocked.
69
+ - `state` — the current Libretto session state object.
70
+ - `get(url, options?)` — HTTP client restricted to **GET and HEAD** requests. Replaces `fetch`, which is blocked in readonly mode. Any request with a body or a non-GET/HEAD method throws `ReadonlyExecDenied`.
71
+ - `scrollBy(deltaX, deltaY)` — scroll the viewport by pixel offset. Use this to inspect content below the fold without targeting a specific element.
72
+
73
+ Standard JS globals `console`, `URL`, `Buffer`, `setTimeout`, and `setInterval` are also available.
74
+
75
+ #### Examples
76
+
77
+ ```bash
78
+ npx libretto readonly-exec "return page.url()" --session failed-job-debug
79
+ npx libretto readonly-exec "return await page.getByRole('heading').first().textContent()" --session failed-job-debug
80
+
81
+ # HTTP GET inspection
82
+ echo "const r = await get('https://api.example.com/status'); return await r.json()" \
83
+ | npx libretto readonly-exec - --session failed-job-debug
84
+
85
+ # Scroll down to inspect below-the-fold content
86
+ npx libretto readonly-exec "await scrollBy(0, 500)" --session failed-job-debug
87
+ ```
88
+
89
+ ### `close`
90
+
91
+ - Use `close` when the inspection session is no longer needed.
92
+
93
+ ```bash
94
+ npx libretto close --session failed-job-debug
95
+ ```
package/src/cli/cli.ts CHANGED
@@ -16,6 +16,10 @@ Examples:
16
16
 
17
17
  libretto exec "await page.locator('button:has-text(\\"Sign in\\")').click()"
18
18
  libretto exec "await page.fill('input[name=\\"email\\"]', 'test@example.com')"
19
+ libretto readonly-exec "return await page.title()" --session test1
20
+ libretto connect http://127.0.0.1:9222 --read-only --session test1
21
+ libretto run ./integration.ts --read-only --session test1
22
+ libretto status
19
23
  libretto ai configure openai
20
24
  libretto ai configure anthropic
21
25
  libretto ai configure gemini
@@ -36,6 +40,9 @@ Examples:
36
40
  Available in exec:
37
41
  page, context, state, browser, networkLog, actionLog
38
42
 
43
+ Available in readonly-exec:
44
+ page, state, snapshot, scrollBy, get
45
+
39
46
  Profiles:
40
47
  Profiles are saved to .libretto/profiles/<domain>.json (git-ignored)
41
48
  They persist cookies, localStorage, and session data across browser launches.
@@ -46,6 +53,9 @@ Sessions:
46
53
  Session state is stored in .libretto/sessions/<session>/state.json
47
54
  CLI logs are stored in .libretto/sessions/<session>/logs.jsonl
48
55
  Each session runs an isolated browser instance on a dynamic port.
56
+ Session mode is stored per session as read-only or write-access.
57
+ Use --read-only on open, connect, or run to create a read-only session.
58
+ Session mode is enforced by Libretto commands, not by raw CDP clients outside Libretto.
49
59
  `;
50
60
  }
51
61