libretto 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) 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 +98 -8
  6. package/dist/cli/commands/execution.js +152 -56
  7. package/dist/cli/commands/setup.js +390 -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 +202 -36
  13. package/dist/cli/core/{ai-config.js → config.js} +14 -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/providers/browserbase.js +53 -0
  17. package/dist/cli/core/providers/index.js +48 -0
  18. package/dist/cli/core/providers/kernel.js +46 -0
  19. package/dist/cli/core/providers/libretto-cloud.js +58 -0
  20. package/dist/cli/core/readonly-exec.js +231 -0
  21. package/dist/{shared/llm/client.js → cli/core/resolve-model.js} +4 -68
  22. package/dist/cli/core/session.js +53 -0
  23. package/dist/cli/core/skill-version.js +73 -0
  24. package/dist/cli/core/telemetry.js +1 -54
  25. package/dist/cli/index.js +1 -7
  26. package/dist/cli/router.js +4 -4
  27. package/dist/cli/workers/run-integration-runtime.js +19 -13
  28. package/dist/cli/workers/run-integration-worker-protocol.js +5 -2
  29. package/dist/index.d.ts +2 -4
  30. package/dist/index.js +2 -2
  31. package/dist/runtime/extract/extract.d.ts +2 -2
  32. package/dist/runtime/extract/extract.js +4 -2
  33. package/dist/runtime/extract/index.d.ts +1 -1
  34. package/dist/runtime/recovery/agent.d.ts +2 -3
  35. package/dist/runtime/recovery/agent.js +5 -3
  36. package/dist/runtime/recovery/errors.d.ts +2 -3
  37. package/dist/runtime/recovery/errors.js +4 -2
  38. package/dist/runtime/recovery/index.d.ts +1 -2
  39. package/dist/runtime/recovery/recovery.d.ts +2 -3
  40. package/dist/runtime/recovery/recovery.js +3 -3
  41. package/dist/shared/debug/pause.js +4 -21
  42. package/dist/shared/run/api.d.ts +2 -0
  43. package/dist/shared/run/browser.d.ts +9 -1
  44. package/dist/shared/run/browser.js +43 -3
  45. package/dist/shared/state/index.d.ts +1 -1
  46. package/dist/shared/state/index.js +2 -0
  47. package/dist/shared/state/session-state.d.ts +20 -1
  48. package/dist/shared/state/session-state.js +12 -2
  49. package/dist/shared/workflow/workflow.d.ts +2 -1
  50. package/dist/shared/workflow/workflow.js +16 -9
  51. package/package.json +17 -16
  52. package/scripts/postinstall.mjs +13 -11
  53. package/scripts/skills-libretto.mjs +14 -4
  54. package/skills/AGENTS.md +11 -0
  55. package/skills/libretto/SKILL.md +30 -9
  56. package/skills/libretto/references/auth-profiles.md +1 -1
  57. package/skills/libretto/references/code-generation-rules.md +3 -3
  58. package/skills/libretto/references/configuration-file-reference.md +11 -6
  59. package/skills/libretto-readonly/SKILL.md +95 -0
  60. package/src/cli/cli.ts +10 -0
  61. package/src/cli/commands/ai.ts +111 -1
  62. package/src/cli/commands/browser.ts +111 -9
  63. package/src/cli/commands/execution.ts +181 -74
  64. package/src/cli/commands/setup.ts +516 -0
  65. package/src/cli/commands/snapshot.ts +2 -2
  66. package/src/cli/commands/status.ts +79 -0
  67. package/src/cli/core/{snapshot-api-config.ts → ai-model.ts} +154 -14
  68. package/src/cli/core/api-snapshot-analyzer.ts +7 -5
  69. package/src/cli/core/browser.ts +242 -35
  70. package/src/cli/core/{ai-config.ts → config.ts} +14 -108
  71. package/src/cli/core/context.ts +1 -45
  72. package/src/cli/core/deploy-artifact.ts +141 -71
  73. package/src/cli/core/providers/browserbase.ts +57 -0
  74. package/src/cli/core/providers/index.ts +62 -0
  75. package/src/cli/core/providers/kernel.ts +49 -0
  76. package/src/cli/core/providers/libretto-cloud.ts +61 -0
  77. package/src/cli/core/providers/types.ts +9 -0
  78. package/src/cli/core/readonly-exec.ts +284 -0
  79. package/src/{shared/llm/client.ts → cli/core/resolve-model.ts} +3 -85
  80. package/src/cli/core/session.ts +75 -2
  81. package/src/cli/core/skill-version.ts +93 -0
  82. package/src/cli/core/telemetry.ts +0 -52
  83. package/src/cli/index.ts +0 -6
  84. package/src/cli/router.ts +4 -4
  85. package/src/cli/workers/run-integration-runtime.ts +18 -16
  86. package/src/cli/workers/run-integration-worker-protocol.ts +4 -1
  87. package/src/index.ts +1 -7
  88. package/src/runtime/extract/extract.ts +6 -5
  89. package/src/runtime/recovery/agent.ts +5 -4
  90. package/src/runtime/recovery/errors.ts +4 -3
  91. package/src/runtime/recovery/recovery.ts +4 -4
  92. package/src/shared/debug/pause.ts +4 -23
  93. package/src/shared/run/browser.ts +50 -1
  94. package/src/shared/state/index.ts +2 -0
  95. package/src/shared/state/session-state.ts +10 -0
  96. package/src/shared/workflow/workflow.ts +24 -13
  97. package/dist/cli/commands/init.js +0 -286
  98. package/dist/cli/commands/logs.js +0 -117
  99. package/dist/shared/llm/ai-sdk-adapter.d.ts +0 -22
  100. package/dist/shared/llm/ai-sdk-adapter.js +0 -49
  101. package/dist/shared/llm/client.d.ts +0 -13
  102. package/dist/shared/llm/index.d.ts +0 -5
  103. package/dist/shared/llm/index.js +0 -6
  104. package/dist/shared/llm/types.d.ts +0 -67
  105. package/src/cli/commands/init.ts +0 -331
  106. package/src/cli/commands/logs.ts +0 -128
  107. package/src/shared/llm/ai-sdk-adapter.ts +0 -81
  108. package/src/shared/llm/index.ts +0 -3
  109. package/src/shared/llm/types.ts +0 -63
  110. /package/dist/{shared/llm → cli/core/providers}/types.js +0 -0
@@ -1,13 +1,14 @@
1
1
  import {
2
2
  defaultLogger
3
3
  } from "../../shared/logger/logger.js";
4
+ import { generateObject } from "ai";
4
5
  import { z } from "zod";
5
6
  const detectSubmissionErrorSchema = z.object({
6
7
  hasError: z.boolean().describe("Whether an error is visible on the page"),
7
8
  matchedKnownErrorId: z.string().nullable().describe("The ID of the matched known error, or null if no match"),
8
9
  errorMessage: z.string().nullable().describe("The error message visible on screen, or null if no error")
9
10
  });
10
- async function detectSubmissionError(page, error, logContext, llmClient, knownErrors = [], logger) {
11
+ async function detectSubmissionError(page, error, logContext, model, knownErrors = [], logger) {
11
12
  const log = logger ?? defaultLogger;
12
13
  let screenshot;
13
14
  let domSnapshot;
@@ -58,7 +59,8 @@ IMPORTANT:
58
59
  ${domSnapshot ? `<dom_snapshot>
59
60
  ${domSnapshot}
60
61
  </dom_snapshot>` : ""}`;
61
- const result = await llmClient.generateObjectFromMessages({
62
+ const { object: result } = await generateObject({
63
+ model,
62
64
  schema: detectSubmissionErrorSchema,
63
65
  messages: [
64
66
  {
@@ -3,5 +3,4 @@ export { attemptWithRecovery } from './recovery.js';
3
3
  export { DetectedSubmissionError, KnownSubmissionError, detectSubmissionError } from './errors.js';
4
4
  import 'playwright';
5
5
  import '../../shared/logger/logger.js';
6
- import '../../shared/llm/types.js';
7
- import 'zod';
6
+ import 'ai';
@@ -1,12 +1,11 @@
1
1
  import { Page } from 'playwright';
2
2
  import { MinimalLogger } from '../../shared/logger/logger.js';
3
- import { LLMClient } from '../../shared/llm/types.js';
4
- import 'zod';
3
+ import { LanguageModel } from 'ai';
5
4
 
6
5
  /**
7
6
  * Attempts to execute a function, and if it fails, runs popup recovery
8
7
  * (if an LLM client is provided) and retries the function once.
9
8
  */
10
- declare function attemptWithRecovery<T>(page: Page, fn: () => Promise<T>, logger?: MinimalLogger, llmClient?: LLMClient): Promise<T>;
9
+ declare function attemptWithRecovery<T>(page: Page, fn: () => Promise<T>, logger?: MinimalLogger, model?: LanguageModel): Promise<T>;
11
10
 
12
11
  export { attemptWithRecovery };
@@ -2,7 +2,7 @@ import {
2
2
  defaultLogger
3
3
  } from "../../shared/logger/logger.js";
4
4
  import { executeRecoveryAgent } from "./agent.js";
5
- async function attemptWithRecovery(page, fn, logger, llmClient) {
5
+ async function attemptWithRecovery(page, fn, logger, model) {
6
6
  const log = logger ?? defaultLogger;
7
7
  try {
8
8
  return await fn();
@@ -13,7 +13,7 @@ async function attemptWithRecovery(page, fn, logger, llmClient) {
13
13
  });
14
14
  throw error;
15
15
  }
16
- if (!llmClient) {
16
+ if (!model) {
17
17
  throw error;
18
18
  }
19
19
  log.info("Action failed, attempting popup recovery", {
@@ -23,7 +23,7 @@ async function attemptWithRecovery(page, fn, logger, llmClient) {
23
23
  page,
24
24
  "Look at the page to see if there is a popup blocking the screen. If so, close the popup.",
25
25
  log,
26
- llmClient
26
+ model
27
27
  );
28
28
  return await fn();
29
29
  }
@@ -5,31 +5,14 @@ import {
5
5
  getPauseSignalPaths,
6
6
  removeSignalIfExists
7
7
  } from "../../cli/core/pause-signals.js";
8
- import {
9
- listSessionsWithStateFile,
10
- readSessionState
11
- } from "../../cli/core/session.js";
12
- function isPidRunning(pid) {
13
- try {
14
- process.kill(pid, 0);
15
- return true;
16
- } catch {
17
- return false;
18
- }
19
- }
20
- function getRunningSessions() {
21
- return listSessionsWithStateFile().filter((candidate) => {
22
- const state = readSessionState(candidate);
23
- return state !== null && state.pid != null && isPidRunning(state.pid);
24
- });
25
- }
8
+ import { listRunningSessions } from "../../cli/core/session.js";
26
9
  function throwMissingSessionError() {
27
- const runningSessions = getRunningSessions();
10
+ const runningSessions = listRunningSessions();
28
11
  const lines = ["pause(session) requires a non-empty session ID."];
29
12
  if (runningSessions.length > 0) {
30
13
  lines.push("", "Running sessions:");
31
- for (const runningSession of runningSessions) {
32
- lines.push(` ${runningSession}`);
14
+ for (const s of runningSessions) {
15
+ lines.push(` ${s.session}`);
33
16
  }
34
17
  }
35
18
  throw new Error(lines.join("\n"));
@@ -1,2 +1,4 @@
1
1
  export { BrowserSession, LaunchBrowserArgs, launchBrowser } from './browser.js';
2
2
  import 'playwright';
3
+ import '../state/session-state.js';
4
+ import 'zod';
@@ -1,4 +1,6 @@
1
1
  import { Browser, BrowserContext, Page } from 'playwright';
2
+ import { SessionAccessMode } from '../state/session-state.js';
3
+ import 'zod';
2
4
 
3
5
  type LaunchBrowserArgs = {
4
6
  sessionName: string;
@@ -8,6 +10,12 @@ type LaunchBrowserArgs = {
8
10
  height: number;
9
11
  };
10
12
  storageStatePath?: string;
13
+ accessMode?: SessionAccessMode;
14
+ cdpEndpoint?: string;
15
+ provider?: {
16
+ name: string;
17
+ sessionId: string;
18
+ };
11
19
  };
12
20
  type BrowserSession = {
13
21
  browser: Browser;
@@ -17,6 +25,6 @@ type BrowserSession = {
17
25
  metadataPath: string;
18
26
  close: () => Promise<void>;
19
27
  };
20
- declare function launchBrowser({ sessionName, headless, viewport, storageStatePath, }: LaunchBrowserArgs): Promise<BrowserSession>;
28
+ declare function launchBrowser({ sessionName, headless, viewport, storageStatePath, accessMode, cdpEndpoint, provider, }: LaunchBrowserArgs): Promise<BrowserSession>;
21
29
 
22
30
  export { type BrowserSession, type LaunchBrowserArgs, launchBrowser };
@@ -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,8 +63,47 @@ 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",
68
+ cdpEndpoint,
69
+ provider
67
70
  }) {
71
+ if (cdpEndpoint) {
72
+ const browser2 = await chromium.connectOverCDP(cdpEndpoint);
73
+ const context2 = browser2.contexts()[0] ?? await browser2.newContext({ viewport });
74
+ const page2 = context2.pages()[0] ?? await context2.newPage();
75
+ page2.setDefaultTimeout(3e4);
76
+ page2.setDefaultNavigationTimeout(45e3);
77
+ const metadataPath2 = ensureLibrettoSessionStatePath(sessionName);
78
+ writeFileSync(
79
+ metadataPath2,
80
+ JSON.stringify(
81
+ {
82
+ version: SESSION_STATE_VERSION,
83
+ session: sessionName,
84
+ port: 0,
85
+ cdpEndpoint,
86
+ pid: process.pid,
87
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
88
+ status: "active",
89
+ mode: accessMode,
90
+ ...provider ? { provider } : {}
91
+ },
92
+ null,
93
+ 2
94
+ )
95
+ );
96
+ return {
97
+ browser: browser2,
98
+ context: context2,
99
+ page: page2,
100
+ debugPort: 0,
101
+ metadataPath: metadataPath2,
102
+ close: async () => {
103
+ await browser2.close();
104
+ }
105
+ };
106
+ }
68
107
  const debugPort = await pickFreePort();
69
108
  const windowPosition = headless ? void 0 : resolveWindowPosition();
70
109
  const browser = await chromium.launch({
@@ -96,7 +135,8 @@ async function launchBrowser({
96
135
  port: debugPort,
97
136
  pid: process.pid,
98
137
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
99
- status: "active"
138
+ status: "active",
139
+ mode: accessMode
100
140
  },
101
141
  null,
102
142
  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,
@@ -7,11 +7,20 @@ declare const SessionStatusSchema: z.ZodEnum<{
7
7
  completed: "completed";
8
8
  failed: "failed";
9
9
  exited: "exited";
10
+ "cleanup-failed": "cleanup-failed";
11
+ }>;
12
+ declare const SessionAccessModeSchema: z.ZodEnum<{
13
+ "read-only": "read-only";
14
+ "write-access": "write-access";
10
15
  }>;
11
16
  declare const SessionViewportSchema: z.ZodObject<{
12
17
  width: z.ZodNumber;
13
18
  height: z.ZodNumber;
14
19
  }, z.core.$strip>;
20
+ declare const ProviderStateSchema: z.ZodObject<{
21
+ name: z.ZodString;
22
+ sessionId: z.ZodString;
23
+ }, z.core.$strip>;
15
24
  declare const SessionStateFileSchema: z.ZodObject<{
16
25
  version: z.ZodLiteral<1>;
17
26
  port: z.ZodNumber;
@@ -25,17 +34,27 @@ declare const SessionStateFileSchema: z.ZodObject<{
25
34
  completed: "completed";
26
35
  failed: "failed";
27
36
  exited: "exited";
37
+ "cleanup-failed": "cleanup-failed";
38
+ }>>;
39
+ mode: z.ZodDefault<z.ZodEnum<{
40
+ "read-only": "read-only";
41
+ "write-access": "write-access";
28
42
  }>>;
29
43
  viewport: z.ZodOptional<z.ZodObject<{
30
44
  width: z.ZodNumber;
31
45
  height: z.ZodNumber;
32
46
  }, z.core.$strip>>;
47
+ provider: z.ZodOptional<z.ZodObject<{
48
+ name: z.ZodString;
49
+ sessionId: z.ZodString;
50
+ }, z.core.$strip>>;
33
51
  }, z.core.$strip>;
34
52
  type SessionStatus = z.infer<typeof SessionStatusSchema>;
53
+ type SessionAccessMode = z.infer<typeof SessionAccessModeSchema>;
35
54
  type SessionStateFile = z.infer<typeof SessionStateFileSchema>;
36
55
  type SessionState = Omit<SessionStateFile, "version">;
37
56
  declare function parseSessionStateData(rawState: unknown, source: string): SessionState;
38
57
  declare function parseSessionStateContent(content: string, source: string): SessionState;
39
58
  declare function serializeSessionState(state: SessionState): SessionStateFile;
40
59
 
41
- export { SESSION_STATE_VERSION, type SessionState, type SessionStateFile, SessionStateFileSchema, type SessionStatus, SessionStatusSchema, SessionViewportSchema, parseSessionStateContent, parseSessionStateData, serializeSessionState };
60
+ export { ProviderStateSchema, SESSION_STATE_VERSION, type SessionAccessMode, SessionAccessModeSchema, type SessionState, type SessionStateFile, SessionStateFileSchema, type SessionStatus, SessionStatusSchema, SessionViewportSchema, parseSessionStateContent, parseSessionStateData, serializeSessionState };
@@ -5,12 +5,18 @@ const SessionStatusSchema = z.enum([
5
5
  "paused",
6
6
  "completed",
7
7
  "failed",
8
- "exited"
8
+ "exited",
9
+ "cleanup-failed"
9
10
  ]);
11
+ const SessionAccessModeSchema = z.enum(["read-only", "write-access"]);
10
12
  const SessionViewportSchema = z.object({
11
13
  width: z.number().int().min(1),
12
14
  height: z.number().int().min(1)
13
15
  });
16
+ const ProviderStateSchema = z.object({
17
+ name: z.string(),
18
+ sessionId: z.string()
19
+ });
14
20
  const SessionStateFileSchema = z.object({
15
21
  version: z.literal(SESSION_STATE_VERSION),
16
22
  port: z.number().int().min(0).max(65535),
@@ -19,7 +25,9 @@ const SessionStateFileSchema = z.object({
19
25
  session: z.string().min(1),
20
26
  startedAt: z.string().datetime({ offset: true }),
21
27
  status: SessionStatusSchema.optional(),
22
- viewport: SessionViewportSchema.optional()
28
+ mode: SessionAccessModeSchema.default("write-access"),
29
+ viewport: SessionViewportSchema.optional(),
30
+ provider: ProviderStateSchema.optional()
23
31
  });
24
32
  function formatIssues(error) {
25
33
  return error.issues.map((issue) => {
@@ -55,7 +63,9 @@ function serializeSessionState(state) {
55
63
  });
56
64
  }
57
65
  export {
66
+ ProviderStateSchema,
58
67
  SESSION_STATE_VERSION,
68
+ SessionAccessModeSchema,
59
69
  SessionStateFileSchema,
60
70
  SessionStatusSchema,
61
71
  SessionViewportSchema,
@@ -21,7 +21,8 @@ type ExportedLibrettoWorkflow = {
21
21
  type WorkflowModuleExports = Record<string, unknown>;
22
22
  declare function isLibrettoWorkflow(value: unknown): value is ExportedLibrettoWorkflow;
23
23
  declare function getWorkflowsFromModuleExports(moduleExports: WorkflowModuleExports): ExportedLibrettoWorkflow[];
24
+ declare function getDefaultWorkflowFromModuleExports(moduleExports: WorkflowModuleExports): ExportedLibrettoWorkflow | null;
24
25
  declare function getWorkflowFromModuleExports(moduleExports: WorkflowModuleExports, workflowName: string): ExportedLibrettoWorkflow | null;
25
26
  declare function workflow<Input = unknown, Output = unknown>(name: string, handler: LibrettoWorkflowHandler<Input, Output>): LibrettoWorkflow<Input, Output>;
26
27
 
27
- 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.5",
3
+ "version": "0.6.0",
4
4
  "description": "AI-powered browser automation library and CLI built on Playwright",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -8,6 +8,7 @@
8
8
  "url": "https://github.com/saffron-health/libretto"
9
9
  },
10
10
  "type": "module",
11
+ "packageManager": "pnpm@10.33.0",
11
12
  "publishConfig": {
12
13
  "access": "public"
13
14
  },
@@ -15,7 +16,7 @@
15
16
  "dist",
16
17
  "src",
17
18
  "scripts",
18
- "skills/libretto"
19
+ "skills"
19
20
  ],
20
21
  "types": "./dist/index.d.ts",
21
22
  "bin": {
@@ -28,6 +29,19 @@
28
29
  "default": "./dist/index.js"
29
30
  }
30
31
  },
32
+ "scripts": {
33
+ "sync:mirrors": "node ../dev-tools/scripts/sync-mirrors.mjs",
34
+ "check:mirrors": "node ../dev-tools/scripts/check-mirrors-sync.mjs",
35
+ "sync-skills": "pnpm run sync:mirrors",
36
+ "check:skills": "pnpm run check:mirrors",
37
+ "build": "tsup --config tsup.config.ts",
38
+ "type-check": "tsc --noEmit",
39
+ "test": "pnpm run build && vitest run",
40
+ "test:watch": "vitest",
41
+ "cli": "node dist/index.js",
42
+ "generate-changelog": "tsx scripts/generate-changelog.ts",
43
+ "prepack": "pnpm run build"
44
+ },
31
45
  "peerDependencies": {
32
46
  "@ai-sdk/anthropic": "^3.0.58",
33
47
  "@ai-sdk/google": "^3.0.51",
@@ -72,18 +86,5 @@
72
86
  "playwright": "^1.58.2",
73
87
  "tsx": "^4.21.0",
74
88
  "zod": "^4.3.6"
75
- },
76
- "scripts": {
77
- "postinstall": "node scripts/postinstall.mjs",
78
- "sync:mirrors": "node ../dev-tools/scripts/sync-mirrors.mjs",
79
- "check:mirrors": "node ../dev-tools/scripts/check-mirrors-sync.mjs",
80
- "sync-skills": "pnpm run sync:mirrors",
81
- "check:skills": "pnpm run check:mirrors",
82
- "build": "tsup --config tsup.config.ts",
83
- "type-check": "tsc --noEmit",
84
- "test": "pnpm run build && vitest run",
85
- "test:watch": "vitest",
86
- "cli": "node dist/index.js",
87
- "generate-changelog": "tsx scripts/generate-changelog.ts"
88
89
  }
89
- }
90
+ }
@@ -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