libretto 0.4.1 → 0.4.4

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.
@@ -292,7 +292,7 @@ async function runResume(session, logger, sessionState) {
292
292
  } = getPauseSignalPaths(session);
293
293
  if (!existsSync(pausedSignalPath)) {
294
294
  throw new Error(
295
- `Session "${session}" is not paused. Run "libretto-cli run ... --session ${session}" and call pause() first.`
295
+ `Session "${session}" is not paused. Run "libretto-cli run ... --session ${session}" and call pause("${session}") first.`
296
296
  );
297
297
  }
298
298
  if (!isProcessRunning(sessionState.pid)) {
@@ -1,7 +1,7 @@
1
1
  import { createInterface } from "node:readline";
2
- import { appendFileSync, cpSync, existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { appendFileSync, cpSync, existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { spawnSync } from "node:child_process";
4
- import { dirname, join } from "node:path";
4
+ import { basename, dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { readAiConfig } from "../core/ai-config.js";
7
7
  import { REPO_ROOT } from "../core/context.js";
@@ -60,6 +60,17 @@ function safeReadAiConfig() {
60
60
  return null;
61
61
  }
62
62
  }
63
+ function printInvalidAiConfigWarning() {
64
+ try {
65
+ readAiConfig();
66
+ } catch (error) {
67
+ const message = error instanceof Error ? error.message : String(error);
68
+ console.log(" ! Existing AI config is invalid:");
69
+ for (const line of message.split("\n")) {
70
+ console.log(` ${line}`);
71
+ }
72
+ }
73
+ }
63
74
  function printSnapshotApiStatus() {
64
75
  const config = safeReadAiConfig();
65
76
  const selection = resolveSnapshotApiModel(config);
@@ -69,6 +80,7 @@ function printSnapshotApiStatus() {
69
80
  " Libretto uses direct API calls for snapshot analysis when supported credentials are available."
70
81
  );
71
82
  console.log(` Credentials are loaded from process env and ${envPath}.`);
83
+ printInvalidAiConfigWarning();
72
84
  if (selection && hasProviderCredentials(selection.provider)) {
73
85
  console.log(` \u2713 Ready: ${selection.model} (${selection.source})`);
74
86
  console.log(" Snapshot objectives will use the API analyzer by default.");
@@ -84,7 +96,7 @@ function printSnapshotApiStatus() {
84
96
  " GOOGLE_CLOUD_PROJECT=... # plus application default credentials for Vertex"
85
97
  );
86
98
  console.log(
87
- " Or run `npx libretto ai configure <provider>` to set a specific model."
99
+ " Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
88
100
  );
89
101
  console.log(" Run `npx libretto init` interactively to set up credentials.");
90
102
  }
@@ -95,6 +107,7 @@ async function runInteractiveApiSetup() {
95
107
  console.log("\nSnapshot analysis setup:");
96
108
  console.log(" Libretto uses direct API calls for snapshot analysis.");
97
109
  console.log(` Credentials are loaded from process env and ${envPath}.`);
110
+ printInvalidAiConfigWarning();
98
111
  if (selection && hasProviderCredentials(selection.provider)) {
99
112
  console.log(` \u2713 Ready: ${selection.model} (${selection.source})`);
100
113
  console.log(" Snapshot objectives will use the API analyzer by default.");
@@ -119,7 +132,7 @@ async function runInteractiveApiSetup() {
119
132
  console.log(" ANTHROPIC_API_KEY=...");
120
133
  console.log(" GEMINI_API_KEY=...");
121
134
  console.log(
122
- " Or run `npx libretto ai configure <provider>` to set a specific model."
135
+ " Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
123
136
  );
124
137
  return;
125
138
  }
@@ -196,27 +209,21 @@ function getPackageSkillsDir() {
196
209
  }
197
210
  throw new Error("Could not locate libretto skill files in package");
198
211
  }
212
+ function detectAgentDirs(root) {
213
+ const dirs = [];
214
+ if (existsSync(join(root, ".agents"))) dirs.push(join(root, ".agents"));
215
+ if (existsSync(join(root, ".claude"))) dirs.push(join(root, ".claude"));
216
+ return dirs;
217
+ }
199
218
  async function copySkills() {
200
- const cwd = process.cwd();
201
- const agentDirs = [];
202
- if (existsSync(join(cwd, ".agents"))) {
203
- agentDirs.push({
204
- name: ".agents",
205
- skillDest: join(cwd, ".agents", "skills", "libretto")
206
- });
207
- }
208
- if (existsSync(join(cwd, ".claude"))) {
209
- agentDirs.push({
210
- name: ".claude",
211
- skillDest: join(cwd, ".claude", "skills", "libretto")
212
- });
213
- }
219
+ const agentDirs = detectAgentDirs(REPO_ROOT);
214
220
  if (agentDirs.length === 0) {
215
- console.log("\nSkills: No .agents/ or .claude/ directory found \u2014 skipping skill copy.");
221
+ console.log("\nSkills: No .agents/ or .claude/ directory found in repo root \u2014 skipping.");
216
222
  return;
217
223
  }
218
- const dirNames = agentDirs.map((d) => d.name).join(" and ");
219
- const existing = agentDirs.filter((d) => existsSync(d.skillDest));
224
+ const destinations = agentDirs.map((d) => join(d, "skills", "libretto"));
225
+ const dirNames = agentDirs.map((d) => basename(d)).join(" and ");
226
+ const existing = destinations.filter((d) => existsSync(d));
220
227
  const verb = existing.length > 0 ? "Overwrite" : "Install";
221
228
  const proceed = await askYesNo(`
222
229
  ${verb} libretto skills in ${dirNames}?`);
@@ -231,7 +238,12 @@ ${verb} libretto skills in ${dirNames}?`);
231
238
  console.error(` \u2717 ${e instanceof Error ? e.message : String(e)}`);
232
239
  return;
233
240
  }
234
- for (const { name, skillDest } of agentDirs) {
241
+ for (let i = 0; i < agentDirs.length; i++) {
242
+ const skillDest = destinations[i];
243
+ const name = basename(agentDirs[i]);
244
+ if (existsSync(skillDest)) {
245
+ rmSync(skillDest, { recursive: true });
246
+ }
235
247
  cpSync(sourceDir, skillDest, { recursive: true });
236
248
  const fileCount = readdirSync(skillDest).length;
237
249
  console.log(` \u2713 Copied ${fileCount} skill files to ${name}/skills/libretto/`);
@@ -13,6 +13,7 @@ import {
13
13
  } from "./shared.js";
14
14
  import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
15
15
  import { readAiConfig } from "../core/ai-config.js";
16
+ import { resolveSnapshotApiModelOrThrow } from "../core/snapshot-api-config.js";
16
17
  const DEFAULT_SNAPSHOT_CONTEXT = "No additional user context provided.";
17
18
  const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 };
18
19
  function generateSnapshotRunId() {
@@ -191,6 +192,10 @@ async function runSnapshot(session, logger, pageId, objective, context) {
191
192
  "Couldn't run analysis: --objective is required when providing --context."
192
193
  );
193
194
  }
195
+ const configuredAi = normalizedObjective ? readAiConfig() : null;
196
+ if (normalizedObjective) {
197
+ resolveSnapshotApiModelOrThrow(configuredAi);
198
+ }
194
199
  const { pngPath, htmlPath, condensedHtmlPath } = await captureScreenshot(
195
200
  session,
196
201
  logger,
@@ -212,7 +217,7 @@ async function runSnapshot(session, logger, pageId, objective, context) {
212
217
  htmlPath,
213
218
  condensedHtmlPath
214
219
  };
215
- await runApiInterpret(interpretArgs, logger, readAiConfig());
220
+ await runApiInterpret(interpretArgs, logger, configuredAi);
216
221
  }
217
222
  const snapshotInput = SimpleCLI.input({
218
223
  positionals: [],
@@ -6,7 +6,7 @@ const CURRENT_CONFIG_VERSION = 1;
6
6
  const AiConfigSchema = z.object({
7
7
  model: z.string().min(1),
8
8
  updatedAt: z.string()
9
- }).strict();
9
+ });
10
10
  const ViewportConfigSchema = z.object({
11
11
  width: z.number().int().min(1),
12
12
  height: z.number().int().min(1)
@@ -19,22 +19,68 @@ const LibrettoConfigSchema = z.object({
19
19
  const DEFAULT_MODELS = {
20
20
  openai: "openai/gpt-5.4",
21
21
  anthropic: "anthropic/claude-sonnet-4-6",
22
- gemini: "google/gemini-2.5-flash",
23
- google: "google/gemini-2.5-flash",
22
+ gemini: "google/gemini-3-flash-preview",
24
23
  vertex: "vertex/gemini-2.5-pro"
25
24
  };
26
- const CONFIGURE_PROVIDERS = Object.keys(DEFAULT_MODELS);
27
- function invalidConfigError(configPath) {
25
+ const PROVIDER_ALIASES = {
26
+ claude: DEFAULT_MODELS.anthropic,
27
+ google: DEFAULT_MODELS.gemini
28
+ };
29
+ const CONFIGURE_PROVIDERS = ["openai", "anthropic", "gemini", "vertex"];
30
+ function formatConfigureProviders(separator = " | ") {
31
+ return CONFIGURE_PROVIDERS.join(separator);
32
+ }
33
+ function formatConfigIssues(error) {
34
+ return error.issues.map((issue) => ` - ${issue.path.join(".") || "root"}: ${issue.message}`).join("\n");
35
+ }
36
+ function formatExpectedConfigExample() {
37
+ return JSON.stringify(
38
+ {
39
+ version: CURRENT_CONFIG_VERSION,
40
+ ai: {
41
+ model: "openai/gpt-5.4",
42
+ updatedAt: "2026-01-01T00:00:00.000Z"
43
+ },
44
+ viewport: {
45
+ width: 1280,
46
+ height: 800
47
+ }
48
+ },
49
+ null,
50
+ 2
51
+ );
52
+ }
53
+ function invalidConfigError(configPath, detail) {
28
54
  return new Error(
29
- `AI config is invalid at ${configPath}. Fix the file to match the expected schema or delete it.`
55
+ [
56
+ `AI config is invalid at ${configPath}.`,
57
+ detail ? `Problems:
58
+ ${detail}` : null,
59
+ "Expected config example:",
60
+ formatExpectedConfigExample(),
61
+ "Notes:",
62
+ ' - "ai" and "viewport" are optional.',
63
+ ' - "ai.model" must be a provider/model string like "openai/gpt-5.4" or "anthropic/claude-sonnet-4-6".',
64
+ "Fix the file to match this shape, or delete it and rerun:",
65
+ ` npx libretto ai configure ${formatConfigureProviders()}`
66
+ ].filter(Boolean).join("\n")
30
67
  );
31
68
  }
32
69
  function parseConfig(raw, configPath) {
70
+ let parsedJson;
33
71
  try {
34
- return LibrettoConfigSchema.parse(JSON.parse(raw));
35
- } catch {
36
- throw invalidConfigError(configPath);
72
+ parsedJson = JSON.parse(raw);
73
+ } catch (error) {
74
+ throw invalidConfigError(
75
+ configPath,
76
+ ` - root: Invalid JSON: ${error instanceof Error ? error.message : String(error)}`
77
+ );
78
+ }
79
+ const parsed = LibrettoConfigSchema.safeParse(parsedJson);
80
+ if (!parsed.success) {
81
+ throw invalidConfigError(configPath, formatConfigIssues(parsed.error));
37
82
  }
83
+ return parsed.data;
38
84
  }
39
85
  function readLibrettoConfig(configPath = LIBRETTO_CONFIG_PATH) {
40
86
  if (!existsSync(configPath)) {
@@ -88,7 +134,8 @@ function resolveModelFromInput(input) {
88
134
  const trimmed = input.trim();
89
135
  if (!trimmed) return null;
90
136
  if (trimmed.includes("/")) return trimmed;
91
- return DEFAULT_MODELS[trimmed.toLowerCase()] ?? null;
137
+ const normalized = trimmed.toLowerCase();
138
+ return DEFAULT_MODELS[normalized] ?? PROVIDER_ALIASES[normalized] ?? null;
92
139
  }
93
140
  function runAiConfigure(input, options = {}) {
94
141
  const configureCommandName = options.configureCommandName ?? "npx libretto ai configure";
@@ -97,7 +144,10 @@ function runAiConfigure(input, options = {}) {
97
144
  if (!presetArg && !input.clear) {
98
145
  const config2 = readAiConfig(configPath);
99
146
  if (!config2) {
100
- console.log(`No AI config set. Run '${configureCommandName} openai' to set one.`);
147
+ console.log(
148
+ `No AI config set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`
149
+ );
150
+ console.log("Provider credentials still come from your shell or .env file.");
101
151
  return;
102
152
  }
103
153
  printAiConfig(config2, configPath);
@@ -120,7 +170,7 @@ function runAiConfigure(input, options = {}) {
120
170
  ${configureCommandName} --clear`
121
171
  );
122
172
  throw new Error(
123
- `Invalid provider or model. Use one of: ${CONFIGURE_PROVIDERS.join(", ")}, or a full model string like "openai/gpt-4o".`
173
+ `Invalid provider or model. Use one of: ${formatConfigureProviders()}, or a full model string like "openai/gpt-4o".`
124
174
  );
125
175
  }
126
176
  const config = writeAiConfig(model, configPath);
@@ -3,16 +3,15 @@ import { dirname, join, resolve } from "node:path";
3
3
  import {
4
4
  readAiConfig
5
5
  } from "./ai-config.js";
6
- import { REPO_ROOT } from "./context.js";
6
+ import { LIBRETTO_CONFIG_PATH, REPO_ROOT } from "./context.js";
7
7
  import {
8
8
  hasProviderCredentials,
9
- missingProviderCredentialsMessage,
10
9
  parseModel
11
10
  } from "../../shared/llm/client.js";
12
11
  const DEFAULT_SNAPSHOT_MODELS = {
13
12
  openai: "openai/gpt-5.4",
14
13
  anthropic: "anthropic/claude-sonnet-4-6",
15
- google: "google/gemini-2.5-flash",
14
+ google: "google/gemini-3-flash-preview",
16
15
  vertex: "vertex/gemini-2.5-pro"
17
16
  };
18
17
  class SnapshotApiUnavailableError extends Error {
@@ -21,6 +20,48 @@ class SnapshotApiUnavailableError extends Error {
21
20
  this.name = "SnapshotApiUnavailableError";
22
21
  }
23
22
  }
23
+ function providerSetupSentence(provider) {
24
+ switch (provider) {
25
+ case "openai":
26
+ return "Add OPENAI_API_KEY to .env or as a shell environment variable.";
27
+ case "anthropic":
28
+ return "Add ANTHROPIC_API_KEY to .env or as a shell environment variable.";
29
+ case "google":
30
+ return "Add GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY to .env or as a shell environment variable.";
31
+ case "vertex":
32
+ return "Add GOOGLE_CLOUD_PROJECT or GCLOUD_PROJECT to .env or as a shell environment variable, and make sure application default credentials are configured.";
33
+ }
34
+ }
35
+ function defaultModelCommandLine() {
36
+ return "npx libretto ai configure openai | anthropic | gemini | vertex";
37
+ }
38
+ function providerMissingCredentialSummary(provider) {
39
+ switch (provider) {
40
+ case "openai":
41
+ return "OPENAI_API_KEY is missing";
42
+ case "anthropic":
43
+ return "ANTHROPIC_API_KEY is missing";
44
+ case "google":
45
+ return "GEMINI_API_KEY and GOOGLE_GENERATIVE_AI_API_KEY are missing";
46
+ case "vertex":
47
+ return "GOOGLE_CLOUD_PROJECT and GCLOUD_PROJECT are missing";
48
+ }
49
+ }
50
+ function noSnapshotApiConfiguredMessage() {
51
+ return [
52
+ "Failed to analyze snapshot because no snapshot analyzer is configured.",
53
+ `Add OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY, or GOOGLE_CLOUD_PROJECT to .env or as a shell environment variable, or choose a default model with \`${defaultModelCommandLine()}\`.`,
54
+ "For more info, run `npx libretto init`."
55
+ ].join(" ");
56
+ }
57
+ function missingProviderSnapshotMessage(selection) {
58
+ const configuredSource = selection.source === "config" ? ` in ${LIBRETTO_CONFIG_PATH}` : " from process env or .env";
59
+ return [
60
+ `Failed to analyze snapshot because ${selection.provider} is configured${configuredSource}, but ${providerMissingCredentialSummary(selection.provider)}.`,
61
+ providerSetupSentence(selection.provider),
62
+ "For more info, run `npx libretto init`."
63
+ ].join(" ");
64
+ }
24
65
  function readWorktreeEnvPath() {
25
66
  const gitPath = join(REPO_ROOT, ".git");
26
67
  if (!existsSync(gitPath)) return null;
@@ -114,12 +155,12 @@ function resolveSnapshotApiModelOrThrow(config = readAiConfig()) {
114
155
  const selection = resolveSnapshotApiModel(config);
115
156
  if (!selection) {
116
157
  throw new SnapshotApiUnavailableError(
117
- "No API snapshot analyzer is available. Set OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY/GOOGLE_GENERATIVE_AI_API_KEY, or GOOGLE_CLOUD_PROJECT, or run `npx libretto ai configure <provider>` to set a default model."
158
+ noSnapshotApiConfiguredMessage()
118
159
  );
119
160
  }
120
161
  if (!hasProviderCredentials(selection.provider)) {
121
162
  throw new SnapshotApiUnavailableError(
122
- missingProviderCredentialsMessage(selection.provider)
163
+ missingProviderSnapshotMessage(selection)
123
164
  );
124
165
  }
125
166
  return selection;
@@ -6,7 +6,6 @@ import { pathToFileURL } from "node:url";
6
6
  import {
7
7
  launchBrowser
8
8
  } from "../../index.js";
9
- import { setSessionForPause } from "../../shared/debug/pause.js";
10
9
  import { parseSessionStateContent } from "../../shared/state/index.js";
11
10
  import { getProfilePath, normalizeDomain } from "../core/browser.js";
12
11
  import {
@@ -181,7 +180,6 @@ async function runIntegrationInternal(args, options) {
181
180
  appendFileSync(networkLogPath, JSON.stringify(entry) + "\n");
182
181
  }
183
182
  });
184
- setSessionForPause(args.session);
185
183
  const workflowContext = {
186
184
  logger: integrationLogger,
187
185
  page: browserSession.page,
@@ -18,13 +18,11 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var debug_exports = {};
20
20
  __export(debug_exports, {
21
- pause: () => import_pause.pause,
22
- setSessionForPause: () => import_pause.setSessionForPause
21
+ pause: () => import_pause.pause
23
22
  });
24
23
  module.exports = __toCommonJS(debug_exports);
25
24
  var import_pause = require("./pause.js");
26
25
  // Annotate the CommonJS export names for ESM import in node:
27
26
  0 && (module.exports = {
28
- pause,
29
- setSessionForPause
27
+ pause
30
28
  });
@@ -1 +1 @@
1
- export { pause, setSessionForPause } from './pause.cjs';
1
+ export { pause } from './pause.cjs';
@@ -1 +1 @@
1
- export { pause, setSessionForPause } from './pause.js';
1
+ export { pause } from './pause.js';
@@ -1,5 +1,4 @@
1
- import { pause, setSessionForPause } from "./pause.js";
1
+ import { pause } from "./pause.js";
2
2
  export {
3
- pause,
4
- setSessionForPause
3
+ pause
5
4
  };
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,54 +15,53 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
  var pause_exports = {};
30
20
  __export(pause_exports, {
31
- pause: () => pause,
32
- setSessionForPause: () => setSessionForPause
21
+ pause: () => pause
33
22
  });
34
23
  module.exports = __toCommonJS(pause_exports);
35
24
  var import_node_fs = require("node:fs");
36
25
  var import_promises = require("node:fs/promises");
37
- let _sessionName;
38
- function setSessionForPause(session) {
39
- _sessionName = session;
40
- }
41
- function getSessionFromProcessArgs() {
42
- const rawPayload = process.argv[2];
43
- if (!rawPayload) return void 0;
26
+ var import_context = require("../../cli/core/context.js");
27
+ var import_pause_signals = require("../../cli/core/pause-signals.js");
28
+ var import_session = require("../../cli/core/session.js");
29
+ function isPidRunning(pid) {
44
30
  try {
45
- const parsed = JSON.parse(rawPayload);
46
- return typeof parsed.session === "string" ? parsed.session : void 0;
31
+ process.kill(pid, 0);
32
+ return true;
47
33
  } catch {
48
- return void 0;
34
+ return false;
49
35
  }
50
36
  }
51
- function resolveSession() {
52
- return _sessionName ?? getSessionFromProcessArgs();
37
+ function getRunningSessions() {
38
+ return (0, import_session.listSessionsWithStateFile)().filter((candidate) => {
39
+ const state = (0, import_session.readSessionState)(candidate);
40
+ return state !== null && isPidRunning(state.pid);
41
+ });
42
+ }
43
+ function throwMissingSessionError() {
44
+ const runningSessions = getRunningSessions();
45
+ const lines = ["pause(session) requires a non-empty session ID."];
46
+ if (runningSessions.length > 0) {
47
+ lines.push("", "Running sessions:");
48
+ for (const runningSession of runningSessions) {
49
+ lines.push(` ${runningSession}`);
50
+ }
51
+ }
52
+ throw new Error(lines.join("\n"));
53
53
  }
54
- async function pause() {
54
+ async function pause(session) {
55
55
  if (process.env.NODE_ENV === "production") {
56
56
  return;
57
57
  }
58
- const session = resolveSession();
59
- if (!session) {
60
- return;
58
+ if (typeof session !== "string" || session.trim().length === 0) {
59
+ throwMissingSessionError();
61
60
  }
62
- const { getPauseSignalPaths, removeSignalIfExists } = await import("../../cli/core/pause-signals.js");
63
- const { getSessionDir } = await import("../../cli/core/context.js");
64
- const signalPaths = getPauseSignalPaths(session);
61
+ const signalPaths = (0, import_pause_signals.getPauseSignalPaths)(session);
65
62
  const { pausedSignalPath, resumeSignalPath } = signalPaths;
66
- await (0, import_promises.mkdir)(getSessionDir(session), { recursive: true });
67
- await removeSignalIfExists(resumeSignalPath);
63
+ await (0, import_promises.mkdir)((0, import_context.getSessionDir)(session), { recursive: true });
64
+ await (0, import_pause_signals.removeSignalIfExists)(resumeSignalPath);
68
65
  const details = {
69
66
  sessionName: session,
70
67
  pausedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -79,12 +76,11 @@ async function pause() {
79
76
  (resolve) => setTimeout(resolve, RESUME_POLL_INTERVAL_MS)
80
77
  );
81
78
  }
82
- await removeSignalIfExists(resumeSignalPath);
83
- await removeSignalIfExists(pausedSignalPath);
79
+ await (0, import_pause_signals.removeSignalIfExists)(resumeSignalPath);
80
+ await (0, import_pause_signals.removeSignalIfExists)(pausedSignalPath);
84
81
  console.log("[pause] Resume signal received. Continuing workflow...");
85
82
  }
86
83
  // Annotate the CommonJS export names for ESM import in node:
87
84
  0 && (module.exports = {
88
- pause,
89
- setSessionForPause
85
+ pause
90
86
  });
@@ -1,7 +1,3 @@
1
- /**
2
- * Called by the CLI runtime to make the session name available to `pause()`.
3
- */
4
- declare function setSessionForPause(session: string): void;
5
1
  /**
6
2
  * Standalone pause function.
7
3
  *
@@ -11,6 +7,6 @@ declare function setSessionForPause(session: string): void;
11
7
  *
12
8
  * Import directly: `import { pause } from "libretto";`
13
9
  */
14
- declare function pause(): Promise<void>;
10
+ declare function pause(session: string): Promise<void>;
15
11
 
16
- export { pause, setSessionForPause };
12
+ export { pause };
@@ -1,7 +1,3 @@
1
- /**
2
- * Called by the CLI runtime to make the session name available to `pause()`.
3
- */
4
- declare function setSessionForPause(session: string): void;
5
1
  /**
6
2
  * Standalone pause function.
7
3
  *
@@ -11,6 +7,6 @@ declare function setSessionForPause(session: string): void;
11
7
  *
12
8
  * Import directly: `import { pause } from "libretto";`
13
9
  */
14
- declare function pause(): Promise<void>;
10
+ declare function pause(session: string): Promise<void>;
15
11
 
16
- export { pause, setSessionForPause };
12
+ export { pause };
@@ -1,32 +1,40 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { mkdir, writeFile } from "node:fs/promises";
3
- let _sessionName;
4
- function setSessionForPause(session) {
5
- _sessionName = session;
6
- }
7
- function getSessionFromProcessArgs() {
8
- const rawPayload = process.argv[2];
9
- if (!rawPayload) return void 0;
3
+ import { getSessionDir } from "../../cli/core/context.js";
4
+ import { getPauseSignalPaths, removeSignalIfExists } from "../../cli/core/pause-signals.js";
5
+ import { listSessionsWithStateFile, readSessionState } from "../../cli/core/session.js";
6
+ function isPidRunning(pid) {
10
7
  try {
11
- const parsed = JSON.parse(rawPayload);
12
- return typeof parsed.session === "string" ? parsed.session : void 0;
8
+ process.kill(pid, 0);
9
+ return true;
13
10
  } catch {
14
- return void 0;
11
+ return false;
15
12
  }
16
13
  }
17
- function resolveSession() {
18
- return _sessionName ?? getSessionFromProcessArgs();
14
+ function getRunningSessions() {
15
+ return listSessionsWithStateFile().filter((candidate) => {
16
+ const state = readSessionState(candidate);
17
+ return state !== null && isPidRunning(state.pid);
18
+ });
19
+ }
20
+ function throwMissingSessionError() {
21
+ const runningSessions = getRunningSessions();
22
+ const lines = ["pause(session) requires a non-empty session ID."];
23
+ if (runningSessions.length > 0) {
24
+ lines.push("", "Running sessions:");
25
+ for (const runningSession of runningSessions) {
26
+ lines.push(` ${runningSession}`);
27
+ }
28
+ }
29
+ throw new Error(lines.join("\n"));
19
30
  }
20
- async function pause() {
31
+ async function pause(session) {
21
32
  if (process.env.NODE_ENV === "production") {
22
33
  return;
23
34
  }
24
- const session = resolveSession();
25
- if (!session) {
26
- return;
35
+ if (typeof session !== "string" || session.trim().length === 0) {
36
+ throwMissingSessionError();
27
37
  }
28
- const { getPauseSignalPaths, removeSignalIfExists } = await import("../../cli/core/pause-signals.js");
29
- const { getSessionDir } = await import("../../cli/core/context.js");
30
38
  const signalPaths = getPauseSignalPaths(session);
31
39
  const { pausedSignalPath, resumeSignalPath } = signalPaths;
32
40
  await mkdir(getSessionDir(session), { recursive: true });
@@ -50,6 +58,5 @@ async function pause() {
50
58
  console.log("[pause] Resume signal received. Continuing workflow...");
51
59
  }
52
60
  export {
53
- pause,
54
- setSessionForPause
61
+ pause
55
62
  };
@@ -62,7 +62,7 @@ function parseModel(model) {
62
62
  const slashIndex = model.indexOf("/");
63
63
  if (slashIndex === -1) {
64
64
  throw new Error(
65
- `Invalid model string "${model}". Expected format: "provider/model-id" (for example "openai/gpt-5.4", "anthropic/claude-sonnet-4-6", "google/gemini-2.5-pro", or "vertex/gemini-2.5-pro").`
65
+ `Invalid model string "${model}". Expected format: "provider/model-id" (for example "openai/gpt-5.4", "anthropic/claude-sonnet-4-6", "google/gemini-3-flash-preview", or "vertex/gemini-2.5-pro").`
66
66
  );
67
67
  }
68
68
  const providerInput = model.slice(0, slashIndex).toLowerCase();
@@ -90,14 +90,14 @@ function hasProviderCredentials(provider, env = process.env) {
90
90
  function missingProviderCredentialsMessage(provider) {
91
91
  switch (provider) {
92
92
  case "google":
93
- return "Missing Gemini API key. Set GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY.";
93
+ return "Gemini API key is missing. Set GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY.";
94
94
  case "vertex":
95
- return "Missing Vertex AI project. Set GOOGLE_CLOUD_PROJECT (or GCLOUD_PROJECT) and ensure application default credentials are configured.";
95
+ return "Vertex AI project is missing. Set GOOGLE_CLOUD_PROJECT (or GCLOUD_PROJECT) and ensure application default credentials are configured.";
96
96
  case "anthropic": {
97
- return "Missing Anthropic API key. Set ANTHROPIC_API_KEY.";
97
+ return "Anthropic API key is missing. Set ANTHROPIC_API_KEY.";
98
98
  }
99
99
  case "openai": {
100
- return "Missing OpenAI API key. Set OPENAI_API_KEY.";
100
+ return "OpenAI API key is missing. Set OPENAI_API_KEY.";
101
101
  }
102
102
  }
103
103
  }
@@ -26,7 +26,7 @@ function parseModel(model) {
26
26
  const slashIndex = model.indexOf("/");
27
27
  if (slashIndex === -1) {
28
28
  throw new Error(
29
- `Invalid model string "${model}". Expected format: "provider/model-id" (for example "openai/gpt-5.4", "anthropic/claude-sonnet-4-6", "google/gemini-2.5-pro", or "vertex/gemini-2.5-pro").`
29
+ `Invalid model string "${model}". Expected format: "provider/model-id" (for example "openai/gpt-5.4", "anthropic/claude-sonnet-4-6", "google/gemini-3-flash-preview", or "vertex/gemini-2.5-pro").`
30
30
  );
31
31
  }
32
32
  const providerInput = model.slice(0, slashIndex).toLowerCase();
@@ -54,14 +54,14 @@ function hasProviderCredentials(provider, env = process.env) {
54
54
  function missingProviderCredentialsMessage(provider) {
55
55
  switch (provider) {
56
56
  case "google":
57
- return "Missing Gemini API key. Set GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY.";
57
+ return "Gemini API key is missing. Set GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY.";
58
58
  case "vertex":
59
- return "Missing Vertex AI project. Set GOOGLE_CLOUD_PROJECT (or GCLOUD_PROJECT) and ensure application default credentials are configured.";
59
+ return "Vertex AI project is missing. Set GOOGLE_CLOUD_PROJECT (or GCLOUD_PROJECT) and ensure application default credentials are configured.";
60
60
  case "anthropic": {
61
- return "Missing Anthropic API key. Set ANTHROPIC_API_KEY.";
61
+ return "Anthropic API key is missing. Set ANTHROPIC_API_KEY.";
62
62
  }
63
63
  case "openai": {
64
- return "Missing OpenAI API key. Set OPENAI_API_KEY.";
64
+ return "OpenAI API key is missing. Set OPENAI_API_KEY.";
65
65
  }
66
66
  }
67
67
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libretto",
3
- "version": "0.4.1",
3
+ "version": "0.4.4",
4
4
  "description": "AI-powered browser automation library and CLI built on Playwright",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "files": [
16
16
  "dist",
17
+ "scripts",
17
18
  "skills/libretto"
18
19
  ],
19
20
  "bin": {
@@ -28,7 +29,7 @@
28
29
  }
29
30
  },
30
31
  "scripts": {
31
- "postinstall": "playwright install chromium",
32
+ "postinstall": "node scripts/postinstall.mjs",
32
33
  "build": "pnpm run build:runtime && pnpm run build:cli",
33
34
  "build:runtime": "tsup --config tsup.config.ts",
34
35
  "build:cli": "tsup --config tsup.cli.config.ts",
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const packageRoot = join(__dirname, "..");
10
+
11
+ // Install Playwright Chromium
12
+ spawnSync("npx", ["playwright", "install", "chromium"], {
13
+ stdio: "inherit",
14
+ shell: true,
15
+ });
16
+
17
+ // Find git repo root
18
+ const gitResult = spawnSync("git", ["rev-parse", "--show-toplevel"], {
19
+ encoding: "utf-8",
20
+ stdio: ["pipe", "pipe", "pipe"],
21
+ });
22
+ const repoRoot = gitResult.status === 0 && gitResult.stdout
23
+ ? gitResult.stdout.trim()
24
+ : null;
25
+ if (!repoRoot) process.exit(0);
26
+
27
+ // Sync skills to any agent dirs at repo root
28
+ const sourceDir = join(packageRoot, "skills", "libretto");
29
+ if (!existsSync(sourceDir)) process.exit(0);
30
+
31
+ const agentDirNames = [".agents", ".claude"];
32
+ for (const name of agentDirNames) {
33
+ const agentDir = join(repoRoot, name);
34
+ if (!existsSync(agentDir)) continue;
35
+ const dest = join(agentDir, "skills", "libretto");
36
+ if (existsSync(dest)) rmSync(dest, { recursive: true });
37
+ mkdirSync(dirname(dest), { recursive: true });
38
+ cpSync(sourceDir, dest, { recursive: true });
39
+ const count = readdirSync(dest).length;
40
+ console.log(`libretto: synced ${count} skill files to ${dest}`);
41
+ }