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
@@ -10,11 +10,16 @@ import {
10
10
  rmSync,
11
11
  writeFileSync
12
12
  } from "node:fs";
13
+ import { createRequire, Module } from "node:module";
13
14
  import { tmpdir } from "node:os";
14
15
  import { dirname, isAbsolute, join, resolve } from "node:path";
15
16
  import { fileURLToPath } from "node:url";
16
17
  import { gzipSync } from "node:zlib";
17
18
  import { build } from "esbuild";
19
+ import {
20
+ getWorkflowsFromModuleExports,
21
+ LIBRETTO_WORKFLOW_BRAND
22
+ } from "../../shared/workflow/workflow.js";
18
23
  const DEFAULT_RUNTIME_EXTERNALS = [
19
24
  "libretto",
20
25
  "playwright",
@@ -43,6 +48,7 @@ const CURRENT_LIBRETTO_VERSION = readCurrentLibrettoVersion();
43
48
  const CURRENT_LIBRETTO_PACKAGE_DIR = fileURLToPath(
44
49
  new URL("../../..", import.meta.url)
45
50
  );
51
+ const require2 = createRequire(import.meta.url);
46
52
  function readCurrentLibrettoVersion() {
47
53
  const packageJsonPath = fileURLToPath(
48
54
  new URL("../../../package.json", import.meta.url)
@@ -464,36 +470,114 @@ function formatBuildError(error) {
464
470
  return `${location} ${entry.text ?? error.message}`;
465
471
  }).join("\n");
466
472
  }
467
- function extractExportNamesFromEsmBundle(bundleSource) {
468
- const exportNames = /* @__PURE__ */ new Set();
469
- for (const entry of bundleSource.matchAll(
470
- /export\s+(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/g
471
- )) {
472
- exportNames.add(entry[1]);
473
- }
474
- for (const entry of bundleSource.matchAll(/export\s+\{([^}]+)\};/g)) {
475
- const specifiers = entry[1]?.split(",") ?? [];
476
- for (const specifier of specifiers) {
477
- const trimmed = specifier.trim();
478
- if (!trimmed) {
479
- continue;
473
+ function getGeneratedWorkflowExportName(index) {
474
+ return `workflow_${index}`;
475
+ }
476
+ function getPackageNameFromImportPath(importPath) {
477
+ if (importPath.startsWith("@")) {
478
+ return importPath.split("/").slice(0, 2).join("/");
479
+ }
480
+ return importPath.split("/")[0] ?? importPath;
481
+ }
482
+ function createExternalDiscoveryStub() {
483
+ const stub = (() => createExternalDiscoveryStub());
484
+ return new Proxy(stub, {
485
+ apply: () => createExternalDiscoveryStub(),
486
+ construct: () => createExternalDiscoveryStub(),
487
+ get: (_target, property) => {
488
+ if (property === "__esModule") {
489
+ return true;
480
490
  }
481
- const aliasMatch = trimmed.match(
482
- /^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*|default)$/
483
- );
484
- if (aliasMatch?.[2]) {
485
- exportNames.add(aliasMatch[2]);
486
- continue;
491
+ if (property === "default") {
492
+ return createExternalDiscoveryStub();
487
493
  }
488
- if (/^[A-Za-z_$][\w$]*$/.test(trimmed)) {
489
- exportNames.add(trimmed);
494
+ if (property === Symbol.toPrimitive) {
495
+ return () => "";
490
496
  }
497
+ return createExternalDiscoveryStub();
491
498
  }
499
+ });
500
+ }
501
+ function createDiscoveryLibrettoModule(workflowNames) {
502
+ const moduleShape = {
503
+ LIBRETTO_WORKFLOW_BRAND,
504
+ workflow: (name) => {
505
+ workflowNames.add(name);
506
+ return {
507
+ [LIBRETTO_WORKFLOW_BRAND]: true,
508
+ name,
509
+ async run() {
510
+ return void 0;
511
+ }
512
+ };
513
+ }
514
+ };
515
+ return new Proxy(moduleShape, {
516
+ get(target, property) {
517
+ if (property in target) {
518
+ return target[property];
519
+ }
520
+ if (property === "__esModule") {
521
+ return true;
522
+ }
523
+ return createExternalDiscoveryStub();
524
+ }
525
+ });
526
+ }
527
+ function discoverBundledWorkflowNames(args) {
528
+ const discoveryPath = join(
529
+ args.absSourceDir,
530
+ `.libretto-deploy-discovery-${process.pid}-${Date.now()}.cjs`
531
+ );
532
+ const originalRequire = Module.prototype.require;
533
+ const workflowNames = /* @__PURE__ */ new Set();
534
+ const discoveryLibrettoModule = createDiscoveryLibrettoModule(workflowNames);
535
+ let loadedModuleExports = null;
536
+ try {
537
+ writeFileSync(discoveryPath, args.bundleBuffer);
538
+ Module.prototype.require = function patchedRequire(id) {
539
+ const packageName = getPackageNameFromImportPath(id);
540
+ if (packageName === "libretto") {
541
+ return discoveryLibrettoModule;
542
+ }
543
+ if (packageName !== "libretto" && args.externalPackages.has(packageName)) {
544
+ return createExternalDiscoveryStub();
545
+ }
546
+ return originalRequire.call(this, id);
547
+ };
548
+ loadedModuleExports = require2(discoveryPath);
549
+ } catch (error) {
550
+ throw new Error(
551
+ `Failed to evaluate deploy entry point ${args.absEntryPoint} while discovering workflows.
552
+ ${formatBuildError(error)}`
553
+ );
554
+ } finally {
555
+ Module.prototype.require = originalRequire;
556
+ delete require2.cache?.[discoveryPath];
557
+ rmSync(discoveryPath, { force: true });
492
558
  }
493
- if (/\bexport\s+default\b/m.test(bundleSource)) {
494
- exportNames.add("default");
559
+ const discoveredWorkflowNames = [...workflowNames].sort(
560
+ (left, right) => left.localeCompare(right)
561
+ );
562
+ if (discoveredWorkflowNames.length === 0) {
563
+ throw new Error(
564
+ `No workflows were found in ${args.absEntryPoint}. Import the workflow files you want to deploy from the entry point, or export a workflow directly from it.`
565
+ );
566
+ }
567
+ const exportedWorkflowNames = new Set(
568
+ getWorkflowsFromModuleExports(loadedModuleExports ?? {}).map(
569
+ (workflow) => workflow.name
570
+ )
571
+ );
572
+ const nonExportedWorkflowNames = discoveredWorkflowNames.filter(
573
+ (name) => !exportedWorkflowNames.has(name)
574
+ );
575
+ if (nonExportedWorkflowNames.length > 0) {
576
+ throw new Error(
577
+ `Workflows discovered in ${args.absEntryPoint} must be exported from the deploy entry point. Re-export them from the entry point or export them through a \`workflows\` object. Non-exported workflows: ${nonExportedWorkflowNames.join(", ")}`
578
+ );
495
579
  }
496
- return [...exportNames];
580
+ return discoveredWorkflowNames;
497
581
  }
498
582
  function createBootstrapSource(args) {
499
583
  const bundleHash = createHash("sha256").update(args.bundleBuffer).digest("hex").slice(0, 16);
@@ -501,17 +585,15 @@ function createBootstrapSource(args) {
501
585
  "base64"
502
586
  );
503
587
  const outputPrefix = `${normalizePackageName(args.deploymentName)}-`;
504
- const hasDefaultExport = args.exportNames.includes("default");
505
- const exportLines = args.exportNames.filter((name) => name !== "default").map(
506
- (name) => `export const ${name} = createWorkflowProxy(${JSON.stringify(name)});`
588
+ const exportLines = args.workflowNames.map(
589
+ (name, index) => `export const ${getGeneratedWorkflowExportName(index)} = createWorkflowProxy(${JSON.stringify(name)});`
507
590
  ).join("\n");
508
- const defaultExportLine = hasDefaultExport ? 'export default createWorkflowProxy("default");' : "";
509
591
  return `import { createRequire } from "node:module";
510
592
  import { existsSync, writeFileSync } from "node:fs";
511
593
  import { tmpdir } from "node:os";
512
594
  import { join } from "node:path";
513
595
  import { gunzipSync } from "node:zlib";
514
- import { workflow } from "libretto";
596
+ import { getWorkflowFromModuleExports, workflow } from "libretto";
515
597
 
516
598
  const BUNDLE_HASH = ${JSON.stringify(bundleHash)};
517
599
  const BUNDLE_GZIP_BASE64 = ${JSON.stringify(bundleBase64)};
@@ -534,13 +616,13 @@ function ensureBundleFile() {
534
616
  return BUNDLE_FILENAME;
535
617
  }
536
618
 
537
- function createWorkflowProxy(exportName) {
538
- return workflow(exportName, async (ctx, input) => {
619
+ function createWorkflowProxy(workflowName) {
620
+ return workflow(workflowName, async (ctx, input) => {
539
621
  const impl = nativeRequire(ensureBundleFile());
540
- const target = impl[exportName];
622
+ const target = getWorkflowFromModuleExports(impl, workflowName);
541
623
  if (!target || typeof target.run !== "function") {
542
624
  throw new Error(
543
- \`Expected workflow export "\${exportName}" to be available in the bundled deployment implementation.\`,
625
+ \`Expected exported workflow "\${workflowName}" to be available in the bundled deployment implementation.\`,
544
626
  );
545
627
  }
546
628
  return await target.run(ctx, input);
@@ -548,7 +630,6 @@ function createWorkflowProxy(exportName) {
548
630
  }
549
631
 
550
632
  ${exportLines}
551
- ${defaultExportLine}
552
633
  `;
553
634
  }
554
635
  async function writeBundledDeployEntrypoint(args) {
@@ -576,39 +657,18 @@ async function writeBundledDeployEntrypoint(args) {
576
657
  "Bundler did not produce a deployment implementation file."
577
658
  );
578
659
  }
579
- const exportBuild = await build({
580
- absWorkingDir: args.absSourceDir,
581
- bundle: true,
582
- entryPoints: [args.absEntryPoint],
583
- external: [...args.externalPackages],
584
- format: "esm",
585
- outfile: "entry-exports.js",
586
- platform: "node",
587
- plugins: [
588
- workspaceSourcePlugin(args.workspacePackages, args.externalPackages)
589
- ],
590
- splitting: false,
591
- target: "node20",
592
- write: false
660
+ const workflowNames = discoverBundledWorkflowNames({
661
+ absEntryPoint: args.absEntryPoint,
662
+ absSourceDir: args.absSourceDir,
663
+ bundleBuffer: Buffer.from(bundledImplementation.contents),
664
+ externalPackages: args.externalPackages
593
665
  });
594
- const bundledExports = exportBuild.outputFiles?.find(
595
- (file) => file.path.endsWith("entry-exports.js")
596
- );
597
- if (!bundledExports) {
598
- throw new Error("Bundler did not produce an export analysis file.");
599
- }
600
- const exportNames = extractExportNamesFromEsmBundle(bundledExports.text);
601
- if (exportNames.length === 0) {
602
- throw new Error(
603
- `No named exports were found in ${args.absEntryPoint}. Hosted deploy expects the entry point to export one or more workflows.`
604
- );
605
- }
606
666
  writeFileSync(
607
667
  join(args.outputDir, "index.js"),
608
668
  createBootstrapSource({
609
669
  bundleBuffer: Buffer.from(bundledImplementation.contents),
610
670
  deploymentName: args.deploymentName,
611
- exportNames
671
+ workflowNames
612
672
  })
613
673
  );
614
674
  } catch (error) {
@@ -0,0 +1,231 @@
1
+ const PAGE_READ_METHODS = /* @__PURE__ */ new Set([
2
+ "url",
3
+ "title",
4
+ "content",
5
+ "pageErrors",
6
+ "viewportSize",
7
+ "waitForLoadState",
8
+ "waitForRequest",
9
+ "waitForResponse",
10
+ "waitForURL"
11
+ ]);
12
+ const PAGE_LOCATOR_FACTORY_METHODS = /* @__PURE__ */ new Set([
13
+ "locator",
14
+ "getByRole",
15
+ "getByText",
16
+ "getByLabel",
17
+ "getByPlaceholder",
18
+ "getByAltText",
19
+ "getByTitle",
20
+ "getByTestId"
21
+ ]);
22
+ const PAGE_ALLOWED_PROPERTIES = /* @__PURE__ */ new Set([]);
23
+ const LOCATOR_READ_METHODS = /* @__PURE__ */ new Set([
24
+ "textContent",
25
+ "innerText",
26
+ "allTextContents",
27
+ "allInnerTexts",
28
+ "ariaSnapshot",
29
+ "boundingBox",
30
+ "count",
31
+ "getAttribute",
32
+ "inputValue",
33
+ "isChecked",
34
+ "isDisabled",
35
+ "isEditable",
36
+ "isEnabled",
37
+ "isVisible",
38
+ "isHidden",
39
+ "waitFor"
40
+ ]);
41
+ const LOCATOR_FACTORY_METHODS = /* @__PURE__ */ new Set([
42
+ "locator",
43
+ "getByRole",
44
+ "getByText",
45
+ "getByLabel",
46
+ "getByPlaceholder",
47
+ "getByAltText",
48
+ "getByTitle",
49
+ "getByTestId",
50
+ "filter",
51
+ "and",
52
+ "or",
53
+ "first",
54
+ "last",
55
+ "nth"
56
+ ]);
57
+ const LOCATOR_COLLECTION_FACTORY_METHODS = /* @__PURE__ */ new Set(["all"]);
58
+ const LOCATOR_SCROLL_METHODS = /* @__PURE__ */ new Set(["scrollIntoViewIfNeeded"]);
59
+ const LOCATOR_ALLOWED_PROPERTIES = /* @__PURE__ */ new Set([]);
60
+ const readonlyPageCache = /* @__PURE__ */ new WeakMap();
61
+ const readonlyLocatorCache = /* @__PURE__ */ new WeakMap();
62
+ function markActivity(onActivity) {
63
+ onActivity?.();
64
+ }
65
+ class ReadonlyExecDeniedError extends Error {
66
+ constructor(message) {
67
+ super(`ReadonlyExecDenied: ${message}`);
68
+ this.name = "ReadonlyExecDenied";
69
+ }
70
+ }
71
+ function denyOperation(targetName, method) {
72
+ throw new ReadonlyExecDeniedError(
73
+ `${targetName}.${method} is blocked in readonly-exec`
74
+ );
75
+ }
76
+ function wrapLocatorForReadonlyExec(locator, options = {}) {
77
+ const cached = readonlyLocatorCache.get(locator);
78
+ if (cached) return cached;
79
+ const proxy = new Proxy(locator, {
80
+ get(target, prop, receiver) {
81
+ if (typeof prop !== "string") {
82
+ return Reflect.get(target, prop, receiver);
83
+ }
84
+ const value = Reflect.get(target, prop, target);
85
+ if (typeof value !== "function") {
86
+ if (LOCATOR_ALLOWED_PROPERTIES.has(prop)) {
87
+ return value;
88
+ }
89
+ return denyOperation("locator", prop);
90
+ }
91
+ if (LOCATOR_READ_METHODS.has(prop)) {
92
+ return (...args) => {
93
+ const result = value.apply(target, args);
94
+ markActivity(options.onActivity);
95
+ return result;
96
+ };
97
+ }
98
+ if (LOCATOR_FACTORY_METHODS.has(prop)) {
99
+ return (...args) => {
100
+ const nextLocator = value.apply(target, args);
101
+ markActivity(options.onActivity);
102
+ return wrapLocatorForReadonlyExec(nextLocator, options);
103
+ };
104
+ }
105
+ if (LOCATOR_COLLECTION_FACTORY_METHODS.has(prop)) {
106
+ return async (...args) => {
107
+ const locators = await value.apply(target, args);
108
+ markActivity(options.onActivity);
109
+ return locators.map(
110
+ (locator2) => wrapLocatorForReadonlyExec(locator2, options)
111
+ );
112
+ };
113
+ }
114
+ if (LOCATOR_SCROLL_METHODS.has(prop)) {
115
+ return async (...args) => {
116
+ await value.apply(target, args);
117
+ markActivity(options.onActivity);
118
+ };
119
+ }
120
+ return (..._args) => denyOperation("locator", prop);
121
+ }
122
+ });
123
+ readonlyLocatorCache.set(locator, proxy);
124
+ return proxy;
125
+ }
126
+ function wrapPageForReadonlyExec(page, options = {}) {
127
+ const cached = readonlyPageCache.get(page);
128
+ if (cached) return cached;
129
+ const proxy = new Proxy(page, {
130
+ get(target, prop, receiver) {
131
+ if (typeof prop !== "string") {
132
+ return Reflect.get(target, prop, receiver);
133
+ }
134
+ const value = Reflect.get(target, prop, target);
135
+ if (typeof value !== "function") {
136
+ if (PAGE_ALLOWED_PROPERTIES.has(prop)) {
137
+ return value;
138
+ }
139
+ return denyOperation("page", prop);
140
+ }
141
+ if (PAGE_READ_METHODS.has(prop)) {
142
+ return (...args) => {
143
+ const result = value.apply(target, args);
144
+ markActivity(options.onActivity);
145
+ return result;
146
+ };
147
+ }
148
+ if (PAGE_LOCATOR_FACTORY_METHODS.has(prop)) {
149
+ return (...args) => {
150
+ const locator = value.apply(target, args);
151
+ markActivity(options.onActivity);
152
+ return wrapLocatorForReadonlyExec(locator, options);
153
+ };
154
+ }
155
+ return (..._args) => denyOperation("page", prop);
156
+ }
157
+ });
158
+ readonlyPageCache.set(page, proxy);
159
+ return proxy;
160
+ }
161
+ function resolveRequestMethod(input, init) {
162
+ const requestMethod = typeof Request !== "undefined" && input instanceof Request ? input.method : void 0;
163
+ return (init?.method ?? requestMethod ?? "GET").toUpperCase();
164
+ }
165
+ function assertReadonlyRequestBodyAllowed(input, init) {
166
+ if (init?.body !== void 0) {
167
+ throw new ReadonlyExecDeniedError(
168
+ "request bodies are blocked in readonly-exec"
169
+ );
170
+ }
171
+ if (typeof Request !== "undefined" && input instanceof Request && input.body !== null) {
172
+ throw new ReadonlyExecDeniedError(
173
+ "request bodies are blocked in readonly-exec"
174
+ );
175
+ }
176
+ }
177
+ function createReadonlyExecHelpers(page, options = {}) {
178
+ const readonlyPage = wrapPageForReadonlyExec(page, options);
179
+ const execState = {};
180
+ return {
181
+ page: readonlyPage,
182
+ state: execState,
183
+ // Playwright has no native viewport scroll method — only locator.scrollIntoViewIfNeeded().
184
+ // Arbitrary scrolling requires page.evaluate(), which is blocked by the readonly proxy
185
+ // since it can run arbitrary code. This helper calls evaluate on the raw (unwrapped) page,
186
+ // scoped to just window.scrollBy.
187
+ scrollBy: async (deltaX, deltaY) => {
188
+ await page.evaluate(
189
+ ([x, y]) => {
190
+ window.scrollBy(x, y);
191
+ },
192
+ [deltaX, deltaY]
193
+ );
194
+ markActivity(options.onActivity);
195
+ },
196
+ get: async (input, init) => {
197
+ const method = resolveRequestMethod(input, init);
198
+ if (method !== "GET" && method !== "HEAD") {
199
+ throw new ReadonlyExecDeniedError(
200
+ `${method} requests are blocked in readonly-exec`
201
+ );
202
+ }
203
+ assertReadonlyRequestBodyAllowed(input, init);
204
+ markActivity(options.onActivity);
205
+ return await fetch(input, {
206
+ ...init,
207
+ method
208
+ });
209
+ },
210
+ // Shadows the global Node.js fetch to prevent unrestricted HTTP access.
211
+ // Without this, agent code would fall through to the global fetch (POST, PUT, DELETE, etc.).
212
+ fetch: () => {
213
+ throw new ReadonlyExecDeniedError(
214
+ "fetch is blocked in readonly-exec; use get() instead"
215
+ );
216
+ },
217
+ console,
218
+ setTimeout,
219
+ setInterval,
220
+ clearTimeout,
221
+ clearInterval,
222
+ URL,
223
+ Buffer
224
+ };
225
+ }
226
+ export {
227
+ ReadonlyExecDeniedError,
228
+ createReadonlyExecHelpers,
229
+ wrapLocatorForReadonlyExec,
230
+ wrapPageForReadonlyExec
231
+ };
@@ -1,4 +1,3 @@
1
- import { generateObject } from "ai";
2
1
  const GEMINI_API_KEY_ENV_VARS = [
3
2
  "GEMINI_API_KEY",
4
3
  "GOOGLE_GENERATIVE_AI_API_KEY"
@@ -108,76 +107,13 @@ async function getProviderModel(provider, modelId) {
108
107
  }
109
108
  }
110
109
  }
111
- function convertUserContentParts(parts) {
112
- return parts.map((part) => {
113
- if (part.type === "text") {
114
- return { type: "text", text: part.text };
115
- }
116
- return {
117
- type: "image",
118
- image: part.image,
119
- ...part.mediaType ? { mediaType: part.mediaType } : {}
120
- };
121
- });
122
- }
123
- function convertAssistantContentParts(parts) {
124
- return parts.filter(
125
- (part) => part.type === "text"
126
- ).map((part) => ({ type: "text", text: part.text }));
127
- }
128
- function convertMessages(messages) {
129
- return messages.map((msg) => {
130
- if (msg.role === "user") {
131
- if (typeof msg.content === "string") {
132
- return { role: "user", content: msg.content };
133
- }
134
- return {
135
- role: "user",
136
- content: convertUserContentParts(msg.content)
137
- };
138
- }
139
- if (typeof msg.content === "string") {
140
- return { role: "assistant", content: msg.content };
141
- }
142
- return {
143
- role: "assistant",
144
- content: convertAssistantContentParts(msg.content)
145
- };
146
- });
147
- }
148
- function createLLMClient(model) {
110
+ async function resolveModel(model) {
149
111
  const { provider, modelId } = parseModel(model);
150
- let modelPromise = null;
151
- const getModel = () => {
152
- modelPromise ??= getProviderModel(provider, modelId);
153
- return modelPromise;
154
- };
155
- return {
156
- async generateObject(opts) {
157
- const aiModel = await getModel();
158
- const result = await generateObject({
159
- model: aiModel,
160
- prompt: opts.prompt,
161
- schema: opts.schema,
162
- temperature: opts.temperature ?? 0
163
- });
164
- return result.object;
165
- },
166
- async generateObjectFromMessages(opts) {
167
- const aiModel = await getModel();
168
- const result = await generateObject({
169
- model: aiModel,
170
- messages: convertMessages(opts.messages),
171
- schema: opts.schema,
172
- temperature: opts.temperature ?? 0
173
- });
174
- return result.object;
175
- }
176
- };
112
+ return getProviderModel(provider, modelId);
177
113
  }
178
114
  export {
179
- createLLMClient,
180
115
  hasProviderCredentials,
181
116
  missingProviderCredentialsMessage,
182
- parseModel
117
+ parseModel,
118
+ resolveModel
183
119
  };
@@ -13,6 +13,7 @@ import {
13
13
  LIBRETTO_SESSIONS_DIR
14
14
  } from "./context.js";
15
15
  import {
16
+ SessionAccessModeSchema,
16
17
  SESSION_STATE_VERSION,
17
18
  parseSessionStateContent,
18
19
  serializeSessionState
@@ -28,6 +29,9 @@ function generateSessionName() {
28
29
  }
29
30
  return `ses-${id}`;
30
31
  }
32
+ function resolveSessionAccessMode(state) {
33
+ return SessionAccessModeSchema.parse(state?.mode);
34
+ }
31
35
  function logFileForSession(session) {
32
36
  validateSessionName(session);
33
37
  const dir = getSessionDir(session);
@@ -85,6 +89,17 @@ function listSessionsWithStateFile() {
85
89
  function listActiveSessions() {
86
90
  return listSessionsWithStateFile();
87
91
  }
92
+ function listRunningSessions() {
93
+ const sessions = listSessionsWithStateFile();
94
+ const running = [];
95
+ for (const name of sessions) {
96
+ const state = readSessionState(name);
97
+ if (!state) continue;
98
+ if (state.pid == null || !isPidRunning(state.pid)) continue;
99
+ running.push(state);
100
+ }
101
+ return running;
102
+ }
88
103
  function throwSessionNotFoundError(session) {
89
104
  const active = listActiveSessions();
90
105
  const lines = [`No session "${session}" found.`];
@@ -132,10 +147,34 @@ function writeSessionState(state, logger) {
132
147
  logger?.info("session-state-write", {
133
148
  session: state.session,
134
149
  stateFile,
150
+ mode: state.mode,
135
151
  port: state.port,
136
152
  pid: state.pid
137
153
  });
138
154
  }
155
+ function setSessionMode(session, mode, logger) {
156
+ const state = readSessionStateOrThrow(session);
157
+ const normalizedMode = SessionAccessModeSchema.parse(mode);
158
+ if (state.mode === normalizedMode) {
159
+ return state;
160
+ }
161
+ const nextState = {
162
+ ...state,
163
+ mode: normalizedMode
164
+ };
165
+ writeSessionState(nextState, logger);
166
+ return nextState;
167
+ }
168
+ function assertSessionAllowsCommand(state, commandName, allowedModes) {
169
+ const mode = resolveSessionAccessMode(state);
170
+ if (allowedModes.includes(mode)) {
171
+ return;
172
+ }
173
+ const supportedModes = [...allowedModes].join(", ");
174
+ throw new Error(
175
+ `Command "${commandName}" is blocked for session "${state.session}" because it is in ${mode} mode. Allowed modes for this command: ${supportedModes}. Run \`libretto session-mode write-access --session ${state.session}\` to unlock the session.`
176
+ );
177
+ }
139
178
  function clearSessionState(session, logger) {
140
179
  const stateFile = getStateFilePath(session);
141
180
  if (!existsSync(stateFile)) {
@@ -181,15 +220,20 @@ export {
181
220
  SESSION_BROWSER_AGENT,
182
221
  SESSION_DEV_SERVER,
183
222
  SESSION_STATE_VERSION,
223
+ assertSessionAllowsCommand,
184
224
  assertSessionAvailableForStart,
185
225
  assertSessionStateExistsOrThrow,
186
226
  clearSessionState,
187
227
  generateSessionName,
188
228
  getStateFilePath,
229
+ isPidRunning,
230
+ listRunningSessions,
189
231
  listSessionsWithStateFile,
190
232
  logFileForSession,
191
233
  readSessionState,
192
234
  readSessionStateOrThrow,
235
+ resolveSessionAccessMode,
236
+ setSessionMode,
193
237
  setSessionStatus,
194
238
  validateSessionName,
195
239
  writeSessionState