libretto 0.4.4 → 0.5.1

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 (194) hide show
  1. package/README.md +106 -36
  2. package/dist/cli/cli.js +39 -113
  3. package/dist/cli/commands/ai.js +1 -1
  4. package/dist/cli/commands/browser.js +87 -60
  5. package/dist/cli/commands/execution.js +201 -88
  6. package/dist/cli/commands/init.js +30 -8
  7. package/dist/cli/commands/logs.js +5 -6
  8. package/dist/cli/commands/shared.js +30 -29
  9. package/dist/cli/commands/snapshot.js +26 -39
  10. package/dist/cli/core/ai-config.js +9 -2
  11. package/dist/cli/core/api-snapshot-analyzer.js +15 -5
  12. package/dist/cli/core/browser.js +141 -33
  13. package/dist/cli/core/context.js +7 -18
  14. package/dist/cli/core/session-telemetry.js +5 -2
  15. package/dist/cli/core/session.js +23 -10
  16. package/dist/cli/core/snapshot-analyzer.js +16 -33
  17. package/dist/cli/core/snapshot-api-config.js +2 -6
  18. package/dist/cli/core/telemetry.js +10 -2
  19. package/dist/cli/framework/simple-cli.js +45 -25
  20. package/dist/cli/router.js +14 -21
  21. package/dist/cli/workers/run-integration-runtime.js +26 -7
  22. package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
  23. package/dist/cli/workers/run-integration-worker.js +1 -4
  24. package/dist/index.d.ts +1 -2
  25. package/dist/index.js +7 -10
  26. package/dist/runtime/download/download.js +5 -1
  27. package/dist/runtime/extract/extract.js +11 -2
  28. package/dist/runtime/network/network.js +8 -1
  29. package/dist/runtime/recovery/agent.js +6 -2
  30. package/dist/runtime/recovery/errors.js +3 -1
  31. package/dist/runtime/recovery/recovery.js +3 -1
  32. package/dist/shared/condense-dom/condense-dom.js +6 -13
  33. package/dist/shared/config/config.d.ts +1 -9
  34. package/dist/shared/config/config.js +0 -18
  35. package/dist/shared/config/index.d.ts +2 -1
  36. package/dist/shared/config/index.js +0 -10
  37. package/dist/shared/debug/pause.js +9 -3
  38. package/dist/shared/instrumentation/instrument.js +101 -5
  39. package/dist/shared/llm/ai-sdk-adapter.js +3 -1
  40. package/dist/shared/llm/client.js +3 -1
  41. package/dist/shared/logger/index.js +4 -1
  42. package/dist/shared/paths/paths.js +2 -1
  43. package/dist/shared/paths/repo-root.d.ts +3 -0
  44. package/dist/shared/paths/repo-root.js +24 -0
  45. package/dist/shared/run/api.js +3 -1
  46. package/dist/shared/run/browser.js +7 -2
  47. package/dist/shared/state/session-state.d.ts +2 -1
  48. package/dist/shared/state/session-state.js +5 -2
  49. package/dist/shared/visualization/ghost-cursor.js +19 -10
  50. package/dist/shared/visualization/highlight.js +9 -6
  51. package/dist/shared/workflow/workflow.d.ts +4 -5
  52. package/dist/shared/workflow/workflow.js +3 -5
  53. package/package.json +11 -8
  54. package/scripts/check-skills-sync.mjs +25 -0
  55. package/scripts/compare-eval-summary.mjs +47 -0
  56. package/scripts/postinstall.mjs +26 -17
  57. package/scripts/prepare-release.sh +97 -0
  58. package/scripts/skills-libretto.mjs +103 -0
  59. package/scripts/summarize-evals.mjs +135 -0
  60. package/scripts/sync-skills.mjs +12 -0
  61. package/skills/libretto/SKILL.md +130 -377
  62. package/skills/libretto/references/auth-profiles.md +30 -0
  63. package/skills/libretto/{code-generation-rules.md → references/code-generation-rules.md} +27 -42
  64. package/skills/libretto/references/configuration-file-reference.md +53 -0
  65. package/skills/libretto/references/pages-and-page-targeting.md +29 -0
  66. package/skills/libretto/references/site-security-review.md +143 -0
  67. package/src/cli/cli.ts +86 -0
  68. package/src/cli/commands/ai.ts +35 -0
  69. package/src/cli/commands/browser.ts +189 -0
  70. package/src/cli/commands/execution.ts +822 -0
  71. package/src/cli/commands/init.ts +350 -0
  72. package/src/cli/commands/logs.ts +128 -0
  73. package/src/cli/commands/shared.ts +69 -0
  74. package/src/cli/commands/snapshot.ts +312 -0
  75. package/src/cli/core/ai-config.ts +264 -0
  76. package/src/cli/core/api-snapshot-analyzer.ts +108 -0
  77. package/src/cli/core/browser.ts +976 -0
  78. package/src/cli/core/context.ts +127 -0
  79. package/src/cli/core/pause-signals.ts +35 -0
  80. package/src/cli/core/session-telemetry.ts +564 -0
  81. package/src/cli/core/session.ts +223 -0
  82. package/src/cli/core/snapshot-analyzer.ts +855 -0
  83. package/src/cli/core/snapshot-api-config.ts +231 -0
  84. package/src/cli/core/telemetry.ts +459 -0
  85. package/src/cli/framework/simple-cli.ts +1340 -0
  86. package/src/cli/index.ts +13 -0
  87. package/src/cli/router.ts +20 -0
  88. package/src/cli/workers/run-integration-runtime.ts +338 -0
  89. package/src/cli/workers/run-integration-worker-protocol.ts +16 -0
  90. package/src/cli/workers/run-integration-worker.ts +72 -0
  91. package/src/index.ts +127 -0
  92. package/src/runtime/download/download.ts +104 -0
  93. package/src/runtime/download/index.ts +7 -0
  94. package/src/runtime/extract/extract.ts +102 -0
  95. package/src/runtime/extract/index.ts +1 -0
  96. package/src/runtime/network/index.ts +5 -0
  97. package/src/runtime/network/network.ts +119 -0
  98. package/{dist/runtime/recovery/agent.cjs → src/runtime/recovery/agent.ts} +114 -76
  99. package/src/runtime/recovery/errors.ts +155 -0
  100. package/src/runtime/recovery/index.ts +7 -0
  101. package/src/runtime/recovery/recovery.ts +53 -0
  102. package/{dist/shared/condense-dom/condense-dom.cjs → src/shared/condense-dom/condense-dom.ts} +249 -124
  103. package/src/shared/config/config.ts +3 -0
  104. package/src/shared/config/index.ts +0 -0
  105. package/src/shared/debug/index.ts +1 -0
  106. package/src/shared/debug/pause.ts +91 -0
  107. package/src/shared/instrumentation/errors.ts +84 -0
  108. package/src/shared/instrumentation/index.ts +9 -0
  109. package/src/shared/instrumentation/instrument.ts +406 -0
  110. package/src/shared/llm/ai-sdk-adapter.ts +81 -0
  111. package/{dist/shared/llm/client.cjs → src/shared/llm/client.ts} +86 -80
  112. package/src/shared/llm/index.ts +3 -0
  113. package/src/shared/llm/types.ts +63 -0
  114. package/src/shared/logger/index.ts +13 -0
  115. package/src/shared/logger/logger.ts +358 -0
  116. package/src/shared/logger/sinks.ts +148 -0
  117. package/src/shared/paths/paths.ts +110 -0
  118. package/src/shared/paths/repo-root.ts +27 -0
  119. package/src/shared/run/api.ts +6 -0
  120. package/src/shared/run/browser.ts +107 -0
  121. package/src/shared/state/index.ts +11 -0
  122. package/src/shared/state/session-state.ts +77 -0
  123. package/src/shared/visualization/ghost-cursor.ts +213 -0
  124. package/src/shared/visualization/highlight.ts +149 -0
  125. package/src/shared/visualization/index.ts +18 -0
  126. package/src/shared/workflow/workflow.ts +36 -0
  127. package/dist/index.cjs +0 -144
  128. package/dist/index.d.cts +0 -21
  129. package/dist/runtime/download/download.cjs +0 -70
  130. package/dist/runtime/download/download.d.cts +0 -35
  131. package/dist/runtime/download/index.cjs +0 -30
  132. package/dist/runtime/download/index.d.cts +0 -3
  133. package/dist/runtime/extract/extract.cjs +0 -88
  134. package/dist/runtime/extract/extract.d.cts +0 -23
  135. package/dist/runtime/extract/index.cjs +0 -28
  136. package/dist/runtime/extract/index.d.cts +0 -5
  137. package/dist/runtime/network/index.cjs +0 -28
  138. package/dist/runtime/network/index.d.cts +0 -4
  139. package/dist/runtime/network/network.cjs +0 -91
  140. package/dist/runtime/network/network.d.cts +0 -28
  141. package/dist/runtime/recovery/agent.d.cts +0 -13
  142. package/dist/runtime/recovery/errors.cjs +0 -124
  143. package/dist/runtime/recovery/errors.d.cts +0 -31
  144. package/dist/runtime/recovery/index.cjs +0 -34
  145. package/dist/runtime/recovery/index.d.cts +0 -7
  146. package/dist/runtime/recovery/recovery.cjs +0 -55
  147. package/dist/runtime/recovery/recovery.d.cts +0 -12
  148. package/dist/shared/condense-dom/condense-dom.d.cts +0 -34
  149. package/dist/shared/config/config.cjs +0 -44
  150. package/dist/shared/config/config.d.cts +0 -10
  151. package/dist/shared/config/index.cjs +0 -32
  152. package/dist/shared/config/index.d.cts +0 -1
  153. package/dist/shared/debug/index.cjs +0 -28
  154. package/dist/shared/debug/index.d.cts +0 -1
  155. package/dist/shared/debug/pause.cjs +0 -86
  156. package/dist/shared/debug/pause.d.cts +0 -12
  157. package/dist/shared/instrumentation/errors.cjs +0 -81
  158. package/dist/shared/instrumentation/errors.d.cts +0 -12
  159. package/dist/shared/instrumentation/index.cjs +0 -35
  160. package/dist/shared/instrumentation/index.d.cts +0 -6
  161. package/dist/shared/instrumentation/instrument.cjs +0 -206
  162. package/dist/shared/instrumentation/instrument.d.cts +0 -32
  163. package/dist/shared/llm/ai-sdk-adapter.cjs +0 -71
  164. package/dist/shared/llm/ai-sdk-adapter.d.cts +0 -22
  165. package/dist/shared/llm/client.d.cts +0 -13
  166. package/dist/shared/llm/index.cjs +0 -31
  167. package/dist/shared/llm/index.d.cts +0 -5
  168. package/dist/shared/llm/types.cjs +0 -16
  169. package/dist/shared/llm/types.d.cts +0 -67
  170. package/dist/shared/logger/index.cjs +0 -37
  171. package/dist/shared/logger/index.d.cts +0 -2
  172. package/dist/shared/logger/logger.cjs +0 -232
  173. package/dist/shared/logger/logger.d.cts +0 -86
  174. package/dist/shared/logger/sinks.cjs +0 -160
  175. package/dist/shared/logger/sinks.d.cts +0 -9
  176. package/dist/shared/paths/paths.cjs +0 -104
  177. package/dist/shared/paths/paths.d.cts +0 -10
  178. package/dist/shared/run/api.cjs +0 -28
  179. package/dist/shared/run/api.d.cts +0 -2
  180. package/dist/shared/run/browser.cjs +0 -98
  181. package/dist/shared/run/browser.d.cts +0 -22
  182. package/dist/shared/state/index.cjs +0 -38
  183. package/dist/shared/state/index.d.cts +0 -2
  184. package/dist/shared/state/session-state.cjs +0 -92
  185. package/dist/shared/state/session-state.d.cts +0 -40
  186. package/dist/shared/visualization/ghost-cursor.cjs +0 -174
  187. package/dist/shared/visualization/ghost-cursor.d.cts +0 -37
  188. package/dist/shared/visualization/highlight.cjs +0 -134
  189. package/dist/shared/visualization/highlight.d.cts +0 -22
  190. package/dist/shared/visualization/index.cjs +0 -45
  191. package/dist/shared/visualization/index.d.cts +0 -3
  192. package/dist/shared/workflow/workflow.cjs +0 -47
  193. package/dist/shared/workflow/workflow.d.cts +0 -21
  194. package/skills/libretto/integration-approach-selection.md +0 -174
@@ -1,81 +1,69 @@
1
- "use strict";
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
19
- };
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
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
- var client_exports = {};
30
- __export(client_exports, {
31
- createLLMClient: () => createLLMClient,
32
- hasProviderCredentials: () => hasProviderCredentials,
33
- missingProviderCredentialsMessage: () => missingProviderCredentialsMessage,
34
- parseModel: () => parseModel
35
- });
36
- module.exports = __toCommonJS(client_exports);
37
- var import_ai = require("ai");
1
+ import { generateObject, type LanguageModel, type ModelMessage } from "ai";
2
+ import type { ZodType, output as ZodOutput } from "zod";
3
+ import type { LLMClient, Message, MessageContentPart } from "./types.js";
4
+
5
+ export type Provider = "google" | "vertex" | "anthropic" | "openai";
6
+
38
7
  const GEMINI_API_KEY_ENV_VARS = [
39
8
  "GEMINI_API_KEY",
40
- "GOOGLE_GENERATIVE_AI_API_KEY"
41
- ];
9
+ "GOOGLE_GENERATIVE_AI_API_KEY",
10
+ ] as const;
11
+
42
12
  const VERTEX_PROJECT_ENV_VARS = [
43
13
  "GOOGLE_CLOUD_PROJECT",
44
- "GCLOUD_PROJECT"
45
- ];
14
+ "GCLOUD_PROJECT",
15
+ ] as const;
16
+
46
17
  const SUPPORTED_PROVIDER_ALIASES = {
47
18
  google: "google",
48
19
  gemini: "google",
49
20
  vertex: "vertex",
50
21
  anthropic: "anthropic",
51
22
  codex: "openai",
52
- openai: "openai"
53
- };
54
- function readFirstEnvValue(env, names) {
23
+ openai: "openai",
24
+ } as const satisfies Record<string, Provider>;
25
+
26
+ function readFirstEnvValue(
27
+ env: NodeJS.ProcessEnv,
28
+ names: readonly string[],
29
+ ): string | null {
55
30
  for (const name of names) {
56
31
  const value = env[name]?.trim();
57
32
  if (value) return value;
58
33
  }
59
34
  return null;
60
35
  }
61
- function parseModel(model) {
36
+
37
+ export function parseModel(model: string): {
38
+ provider: Provider;
39
+ modelId: string;
40
+ } {
62
41
  const slashIndex = model.indexOf("/");
63
42
  if (slashIndex === -1) {
64
43
  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-3-flash-preview", or "vertex/gemini-2.5-pro").`
44
+ `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
45
  );
67
46
  }
68
47
  const providerInput = model.slice(0, slashIndex).toLowerCase();
69
- const provider = SUPPORTED_PROVIDER_ALIASES[providerInput];
48
+ const provider =
49
+ SUPPORTED_PROVIDER_ALIASES[
50
+ providerInput as keyof typeof SUPPORTED_PROVIDER_ALIASES
51
+ ];
70
52
  const modelId = model.slice(slashIndex + 1);
53
+
71
54
  if (!provider) {
72
55
  throw new Error(
73
- `Unsupported provider "${providerInput}". Supported providers: openai/codex, anthropic, google (Gemini API), and vertex.`
56
+ `Unsupported provider "${providerInput}". Supported providers: openai/codex, anthropic, google (Gemini API), and vertex.`,
74
57
  );
75
58
  }
59
+
76
60
  return { provider, modelId };
77
61
  }
78
- function hasProviderCredentials(provider, env = process.env) {
62
+
63
+ export function hasProviderCredentials(
64
+ provider: Provider,
65
+ env: NodeJS.ProcessEnv = process.env,
66
+ ): boolean {
79
67
  switch (provider) {
80
68
  case "google":
81
69
  return readFirstEnvValue(env, GEMINI_API_KEY_ENV_VARS) !== null;
@@ -87,7 +75,8 @@ function hasProviderCredentials(provider, env = process.env) {
87
75
  return Boolean(env.OPENAI_API_KEY?.trim());
88
76
  }
89
77
  }
90
- function missingProviderCredentialsMessage(provider) {
78
+
79
+ export function missingProviderCredentialsMessage(provider: Provider): string {
91
80
  switch (provider) {
92
81
  case "google":
93
82
  return "Gemini API key is missing. Set GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY.";
@@ -101,7 +90,11 @@ function missingProviderCredentialsMessage(provider) {
101
90
  }
102
91
  }
103
92
  }
104
- async function getProviderModel(provider, modelId) {
93
+
94
+ async function getProviderModel(
95
+ provider: Provider,
96
+ modelId: string,
97
+ ): Promise<LanguageModel> {
105
98
  switch (provider) {
106
99
  case "google": {
107
100
  const apiKey = readFirstEnvValue(process.env, GEMINI_API_KEY_ENV_VARS);
@@ -120,7 +113,7 @@ async function getProviderModel(provider, modelId) {
120
113
  const { createVertex } = await import("@ai-sdk/google-vertex");
121
114
  const vertex = createVertex({
122
115
  project,
123
- location: process.env.GOOGLE_CLOUD_LOCATION || "global"
116
+ location: process.env.GOOGLE_CLOUD_LOCATION || "global",
124
117
  });
125
118
  return vertex(modelId);
126
119
  }
@@ -144,30 +137,38 @@ async function getProviderModel(provider, modelId) {
144
137
  }
145
138
  }
146
139
  }
147
- function convertUserContentParts(parts) {
140
+
141
+ function convertUserContentParts(parts: MessageContentPart[]) {
148
142
  return parts.map((part) => {
149
143
  if (part.type === "text") {
150
- return { type: "text", text: part.text };
144
+ return { type: "text" as const, text: part.text };
151
145
  }
152
146
  return {
153
- type: "image",
147
+ type: "image" as const,
154
148
  image: part.image,
155
- ...part.mediaType ? { mediaType: part.mediaType } : {}
149
+ ...(part.mediaType ? { mediaType: part.mediaType } : {}),
156
150
  };
157
151
  });
158
152
  }
159
- function convertAssistantContentParts(parts) {
160
- return parts.filter((part) => part.type === "text").map((part) => ({ type: "text", text: part.text }));
153
+
154
+ function convertAssistantContentParts(parts: MessageContentPart[]) {
155
+ return parts
156
+ .filter(
157
+ (part): part is MessageContentPart & { type: "text" } =>
158
+ part.type === "text",
159
+ )
160
+ .map((part) => ({ type: "text" as const, text: part.text }));
161
161
  }
162
- function convertMessages(messages) {
163
- return messages.map((msg) => {
162
+
163
+ function convertMessages(messages: Message[]): ModelMessage[] {
164
+ return messages.map((msg): ModelMessage => {
164
165
  if (msg.role === "user") {
165
166
  if (typeof msg.content === "string") {
166
167
  return { role: "user", content: msg.content };
167
168
  }
168
169
  return {
169
170
  role: "user",
170
- content: convertUserContentParts(msg.content)
171
+ content: convertUserContentParts(msg.content),
171
172
  };
172
173
  }
173
174
  if (typeof msg.content === "string") {
@@ -175,44 +176,49 @@ function convertMessages(messages) {
175
176
  }
176
177
  return {
177
178
  role: "assistant",
178
- content: convertAssistantContentParts(msg.content)
179
+ content: convertAssistantContentParts(msg.content),
179
180
  };
180
181
  });
181
182
  }
182
- function createLLMClient(model) {
183
+
184
+ export function createLLMClient(model: string): LLMClient {
183
185
  const { provider, modelId } = parseModel(model);
184
- let modelPromise = null;
186
+ let modelPromise: Promise<LanguageModel> | null = null;
187
+
185
188
  const getModel = () => {
186
189
  modelPromise ??= getProviderModel(provider, modelId);
187
190
  return modelPromise;
188
191
  };
192
+
189
193
  return {
190
- async generateObject(opts) {
194
+ async generateObject<T extends ZodType>(opts: {
195
+ prompt: string;
196
+ schema: T;
197
+ temperature?: number;
198
+ }): Promise<ZodOutput<T>> {
191
199
  const aiModel = await getModel();
192
- const result = await (0, import_ai.generateObject)({
200
+ const result = await generateObject({
193
201
  model: aiModel,
194
202
  prompt: opts.prompt,
195
203
  schema: opts.schema,
196
- temperature: opts.temperature ?? 0
204
+ temperature: opts.temperature ?? 0,
197
205
  });
198
- return result.object;
206
+ return result.object as ZodOutput<T>;
199
207
  },
200
- async generateObjectFromMessages(opts) {
208
+
209
+ async generateObjectFromMessages<T extends ZodType>(opts: {
210
+ messages: Message[];
211
+ schema: T;
212
+ temperature?: number;
213
+ }): Promise<ZodOutput<T>> {
201
214
  const aiModel = await getModel();
202
- const result = await (0, import_ai.generateObject)({
215
+ const result = await generateObject({
203
216
  model: aiModel,
204
217
  messages: convertMessages(opts.messages),
205
218
  schema: opts.schema,
206
- temperature: opts.temperature ?? 0
219
+ temperature: opts.temperature ?? 0,
207
220
  });
208
- return result.object;
209
- }
221
+ return result.object as ZodOutput<T>;
222
+ },
210
223
  };
211
224
  }
212
- // Annotate the CommonJS export names for ESM import in node:
213
- 0 && (module.exports = {
214
- createLLMClient,
215
- hasProviderCredentials,
216
- missingProviderCredentialsMessage,
217
- parseModel
218
- });
@@ -0,0 +1,3 @@
1
+ export type { LLMClient, Message, MessageContentPart } from "./types.js";
2
+ export { createLLMClient } from "./client.js";
3
+ export { createLLMClientFromModel } from "./ai-sdk-adapter.js";
@@ -0,0 +1,63 @@
1
+ import type z from "zod";
2
+
3
+ export type MessageContentPart =
4
+ | { type: "text"; text: string }
5
+ | { type: "image"; image: string | Uint8Array; mediaType?: string };
6
+
7
+ export type Message = {
8
+ role: "user" | "assistant";
9
+ content: string | MessageContentPart[];
10
+ };
11
+
12
+ /**
13
+ * Pluggable LLM client interface.
14
+ *
15
+ * Users provide their own implementation backed by any LLM provider
16
+ * (OpenAI, Anthropic, etc.). Libretto uses this interface for AI extraction,
17
+ * recovery agents, and error detection.
18
+ *
19
+ * **Error handling:** implementations should throw on failure rather than
20
+ * returning sentinel values (e.g. `null` or `undefined`). Libretto relies
21
+ * on exceptions to trigger retry/recovery logic.
22
+ *
23
+ * A ready-made adapter for the Vercel AI SDK is available via
24
+ * {@link createLLMClientFromModel} in `libretto/llm`.
25
+ */
26
+ export interface LLMClient {
27
+ /**
28
+ * Generate a structured object from a single text prompt.
29
+ *
30
+ * The underlying model **must** support structured / JSON output so that
31
+ * the response can be parsed and validated against the provided Zod schema.
32
+ *
33
+ * @param opts.prompt - The text prompt sent to the model.
34
+ * @param opts.schema - A Zod schema describing the expected response shape.
35
+ * @param opts.temperature - Sampling temperature (default chosen by implementation, typically 0).
36
+ * @returns The parsed object matching the schema.
37
+ * @throws On LLM or parsing failure.
38
+ */
39
+ generateObject<T extends z.ZodType>(opts: {
40
+ prompt: string;
41
+ schema: T;
42
+ temperature?: number;
43
+ }): Promise<z.output<T>>;
44
+
45
+ /**
46
+ * Generate a structured object from a conversation-style message array.
47
+ *
48
+ * Messages may contain **image content** (base64 data URIs via
49
+ * {@link MessageContentPart}), so the backing model must support
50
+ * vision / multimodal input when images are present.
51
+ *
52
+ * @param opts.messages - Ordered list of user/assistant messages, potentially multimodal.
53
+ * @param opts.schema - A Zod schema describing the expected response shape.
54
+ * @param opts.temperature - Sampling temperature (default chosen by implementation, typically 0).
55
+ * @returns The parsed object matching the schema.
56
+ * @throws On LLM or parsing failure.
57
+ */
58
+ generateObjectFromMessages<T extends z.ZodType>(opts: {
59
+ messages: Message[];
60
+ schema: T;
61
+ temperature?: number;
62
+ }): Promise<z.output<T>>;
63
+ }
@@ -0,0 +1,13 @@
1
+ export {
2
+ Logger,
3
+ defaultLogger,
4
+ type LoggerApi,
5
+ type MinimalLogger,
6
+ type LoggerSink,
7
+ type LogOptions,
8
+ } from "./logger.js";
9
+ export {
10
+ createFileLogSink,
11
+ prettyConsoleSink,
12
+ jsonlConsoleSink,
13
+ } from "./sinks.js";
@@ -0,0 +1,358 @@
1
+ function generateId(): string {
2
+ return Math.random().toString(36).substring(2, 15);
3
+ }
4
+
5
+ export type LogOptions = {
6
+ timestamp?: Date;
7
+ };
8
+
9
+ /**
10
+ * Minimal logger interface accepted by public-facing runtime functions.
11
+ * Any logger with info/warn/error methods satisfies this — no need to
12
+ * implement withScope, withContext, flush, etc.
13
+ */
14
+ export type MinimalLogger = {
15
+ info: (event: string, data?: any) => void;
16
+ warn: (event: string, data?: any) => void;
17
+ error: (event: string, data?: any) => any;
18
+ };
19
+
20
+ /** Default console logger used when callers omit the logger option. */
21
+ export const defaultLogger: MinimalLogger = {
22
+ info(event, data) {
23
+ console.log(`[INFO] ${event}`, data ?? "");
24
+ },
25
+ warn(event, data) {
26
+ console.warn(`[WARN] ${event}`, data ?? "");
27
+ },
28
+ error(event, data) {
29
+ console.error(`[ERROR] ${event}`, data ?? "");
30
+ },
31
+ };
32
+
33
+ export type LoggerApi = {
34
+ log: (
35
+ event: string,
36
+ data?: Record<string, any>,
37
+ options?: LogOptions,
38
+ ) => void;
39
+ /**
40
+ * Logs an error and returns an Error object that can be thrown
41
+ *
42
+ * either pass in an Error directly as data or as { error: Error, ...other_data }
43
+ */
44
+ error: (
45
+ event: string,
46
+ data?: Error | ({ error: Error } & Record<string, any>) | unknown,
47
+ options?: LogOptions,
48
+ ) => Error;
49
+ warn: (
50
+ event: string,
51
+ data?: Error | ({ error: Error } & Record<string, any>) | unknown,
52
+ options?: LogOptions,
53
+ ) => void;
54
+ info: (
55
+ event: string,
56
+ data?: Error | ({ error: Error } & Record<string, any>) | unknown,
57
+ options?: LogOptions,
58
+ ) => void;
59
+
60
+ /**
61
+ * Context passed in will be attached to all entries in this scope.
62
+ */
63
+ withScope: (scope: string, context?: Record<string, any>) => LoggerApi;
64
+
65
+ /**
66
+ * Context passed in will be attached to all entries.
67
+ */
68
+ withContext: (context: Record<string, any>) => LoggerApi;
69
+
70
+ /**
71
+ * Flushes all sinks in reverse order (most recently added first).
72
+ */
73
+ flush: () => Promise<void>;
74
+ };
75
+
76
+ export type LoggerSink = {
77
+ write: (args: {
78
+ id: string;
79
+ scope: string;
80
+ level: "log" | "error" | "warn" | "info";
81
+ event: string;
82
+ data: Record<string, any>;
83
+ options?: LogOptions;
84
+ }) => void;
85
+ flush?: () => Promise<void>;
86
+ close?: () => Promise<void>;
87
+ };
88
+
89
+ type SinkLifecycleState = {
90
+ closed: boolean;
91
+ closing?: Promise<void>;
92
+ };
93
+
94
+ const sinkLifecycleState = new WeakMap<LoggerSink, SinkLifecycleState>();
95
+
96
+ function getSinkLifecycleState(sink: LoggerSink): SinkLifecycleState {
97
+ const existingState = sinkLifecycleState.get(sink);
98
+ if (existingState) {
99
+ return existingState;
100
+ }
101
+
102
+ const initialState: SinkLifecycleState = { closed: false };
103
+ sinkLifecycleState.set(sink, initialState);
104
+ return initialState;
105
+ }
106
+
107
+ function isSinkClosedOrClosing(sink: LoggerSink): boolean {
108
+ const state = sinkLifecycleState.get(sink);
109
+ return Boolean(state?.closed || state?.closing);
110
+ }
111
+
112
+ async function closeSinkOnce(sink: LoggerSink): Promise<void> {
113
+ if (!sink.close) {
114
+ return;
115
+ }
116
+
117
+ const state = getSinkLifecycleState(sink);
118
+ if (state.closed) {
119
+ return;
120
+ }
121
+
122
+ if (state.closing) {
123
+ return state.closing;
124
+ }
125
+
126
+ state.closing = (async () => {
127
+ try {
128
+ await sink.close?.();
129
+ } catch {
130
+ // Ignore close errors - we're likely shutting down
131
+ } finally {
132
+ state.closed = true;
133
+ state.closing = undefined;
134
+ }
135
+ })();
136
+
137
+ return state.closing;
138
+ }
139
+
140
+ function isObject(value: unknown): value is Record<string, unknown> {
141
+ return typeof value === "object" && value !== null;
142
+ }
143
+
144
+ function removeUndefined(data: any): any {
145
+ if (typeof data === "object" && data !== null) {
146
+ return Object.fromEntries(
147
+ Object.entries(data).filter(([_, value]) => value !== undefined),
148
+ );
149
+ }
150
+ return data;
151
+ }
152
+
153
+ export class Logger implements LoggerApi {
154
+ private readonly prefix: string;
155
+
156
+ constructor(
157
+ private readonly scopes: string[] = [],
158
+ private readonly sinks: LoggerSink[] = [],
159
+ private readonly scopeData: Record<string, any> = {},
160
+ ) {
161
+ this.prefix = scopes.join(".");
162
+ }
163
+
164
+ entry(entry: {
165
+ level: "log" | "error" | "warn" | "info";
166
+ event: string;
167
+ data?: Record<string, any>;
168
+ options?: LogOptions;
169
+ }) {
170
+ this.sinks.forEach((sink) => {
171
+ if (isSinkClosedOrClosing(sink)) {
172
+ return;
173
+ }
174
+
175
+ sink.write({
176
+ id: generateId(),
177
+ scope: this.prefix,
178
+ level: entry.level,
179
+ event: entry.event,
180
+ data: removeUndefined({ ...this.scopeData, ...entry.data }),
181
+ options: entry.options,
182
+ });
183
+ });
184
+ }
185
+
186
+ log(event: string, data?: Record<string, any>, options?: LogOptions) {
187
+ this.entry({ level: "log", event, data, options });
188
+ }
189
+
190
+ error(
191
+ event: string,
192
+ dataOrError?: Error | ({ error: Error } & Record<string, any>) | unknown,
193
+ options?: LogOptions,
194
+ ) {
195
+ const data =
196
+ dataOrError instanceof Error
197
+ ? {
198
+ error: {
199
+ type: dataOrError.constructor.name,
200
+ message: dataOrError.message,
201
+ stack: dataOrError.stack || null,
202
+ },
203
+ }
204
+ : isObject(dataOrError) && dataOrError.error instanceof Error
205
+ ? {
206
+ ...dataOrError,
207
+ error: {
208
+ type: dataOrError.error.constructor.name,
209
+ message: dataOrError.error.message,
210
+ stack: dataOrError.error.stack || null,
211
+ },
212
+ }
213
+ : isObject(dataOrError)
214
+ ? dataOrError
215
+ : dataOrError !== undefined
216
+ ? { error: dataOrError }
217
+ : undefined;
218
+
219
+ this.entry({
220
+ level: "error",
221
+ event,
222
+ data: data as Error | Record<string, any>,
223
+ options,
224
+ });
225
+
226
+ if (dataOrError instanceof Error) {
227
+ return dataOrError;
228
+ }
229
+
230
+ if (isObject(dataOrError) && dataOrError.error instanceof Error) {
231
+ return dataOrError.error;
232
+ }
233
+
234
+ let message = event;
235
+ if (data !== undefined) {
236
+ try {
237
+ message += "\n" + JSON.stringify(data, undefined, 2);
238
+ } catch {
239
+ message += "\n[Unserializable error data]";
240
+ }
241
+ }
242
+ return new Error(message);
243
+ }
244
+
245
+ warn(
246
+ event: string,
247
+ dataOrError?: Error | ({ error: Error } & Record<string, any>) | unknown,
248
+ options?: LogOptions,
249
+ ) {
250
+ const data =
251
+ dataOrError instanceof Error
252
+ ? {
253
+ error: {
254
+ type: dataOrError.constructor.name,
255
+ message: dataOrError.message,
256
+ stack: dataOrError.stack || null,
257
+ },
258
+ }
259
+ : isObject(dataOrError) && dataOrError.error instanceof Error
260
+ ? {
261
+ ...dataOrError,
262
+ error: {
263
+ type: dataOrError.error.constructor.name,
264
+ message: dataOrError.error.message,
265
+ stack: dataOrError.error.stack || null,
266
+ },
267
+ }
268
+ : isObject(dataOrError)
269
+ ? dataOrError
270
+ : dataOrError !== undefined
271
+ ? { error: dataOrError }
272
+ : undefined;
273
+
274
+ this.entry({
275
+ level: "warn",
276
+ event,
277
+ data: data as Record<string, any>,
278
+ options,
279
+ });
280
+ }
281
+
282
+ info(
283
+ event: string,
284
+ dataOrError?: Error | ({ error: Error } & Record<string, any>) | unknown,
285
+ options?: LogOptions,
286
+ ) {
287
+ const data =
288
+ dataOrError instanceof Error
289
+ ? {
290
+ error: {
291
+ type: dataOrError.constructor.name,
292
+ message: dataOrError.message,
293
+ stack: dataOrError.stack || null,
294
+ },
295
+ }
296
+ : isObject(dataOrError) && dataOrError.error instanceof Error
297
+ ? {
298
+ ...dataOrError,
299
+ error: {
300
+ type: dataOrError.error.constructor.name,
301
+ message: dataOrError.error.message,
302
+ stack: dataOrError.error.stack || null,
303
+ },
304
+ }
305
+ : isObject(dataOrError)
306
+ ? dataOrError
307
+ : dataOrError !== undefined
308
+ ? { error: dataOrError }
309
+ : undefined;
310
+
311
+ this.entry({
312
+ level: "info",
313
+ event,
314
+ data: data as Record<string, any>,
315
+ options,
316
+ });
317
+ }
318
+
319
+ withScope(scope: string, context: Record<string, any> = {}): LoggerApi {
320
+ return new Logger([...this.scopes, scope], this.sinks, {
321
+ ...this.scopeData,
322
+ ...context,
323
+ });
324
+ }
325
+
326
+ withContext(context: Record<string, any>): LoggerApi {
327
+ return new Logger(this.scopes, this.sinks, {
328
+ ...this.scopeData,
329
+ ...context,
330
+ });
331
+ }
332
+
333
+ withSink(sink: LoggerSink): Logger {
334
+ return new Logger(this.scopes, [...this.sinks, sink]);
335
+ }
336
+
337
+ async flush(): Promise<void> {
338
+ for (let i = this.sinks.length - 1; i >= 0; i--) {
339
+ const sink = this.sinks[i];
340
+ if (!sink) continue;
341
+ if (isSinkClosedOrClosing(sink)) continue;
342
+ try {
343
+ await sink.flush?.();
344
+ } catch {
345
+ // Ignore flush errors - we're likely shutting down
346
+ }
347
+ }
348
+ }
349
+
350
+ async close(): Promise<void> {
351
+ await this.flush();
352
+ for (let i = this.sinks.length - 1; i >= 0; i--) {
353
+ const sink = this.sinks[i];
354
+ if (!sink) continue;
355
+ await closeSinkOnce(sink);
356
+ }
357
+ }
358
+ }