libretto 0.3.0 → 0.3.2

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 (162) hide show
  1. package/README.md +51 -125
  2. package/dist/cli/cli.js +298 -0
  3. package/dist/cli/commands/ai.js +21 -0
  4. package/dist/cli/commands/browser.js +103 -0
  5. package/dist/cli/commands/execution.js +490 -0
  6. package/dist/cli/commands/init.js +166 -0
  7. package/dist/cli/commands/logs.js +93 -0
  8. package/dist/cli/commands/snapshot.js +217 -0
  9. package/dist/cli/core/ai-config.js +156 -0
  10. package/dist/cli/core/browser.js +669 -0
  11. package/dist/cli/core/context.js +117 -0
  12. package/dist/cli/core/pause-signals.js +29 -0
  13. package/dist/cli/core/session-telemetry.js +491 -0
  14. package/dist/cli/core/session.js +183 -0
  15. package/dist/cli/core/snapshot-analyzer.js +570 -0
  16. package/dist/cli/core/telemetry.js +362 -0
  17. package/dist/cli/index.js +14 -0
  18. package/dist/cli/workers/run-integration-runtime.js +234 -0
  19. package/dist/cli/workers/run-integration-worker-protocol.js +12 -0
  20. package/dist/cli/workers/run-integration-worker.js +67 -0
  21. package/dist/index.cjs +144 -0
  22. package/dist/index.d.cts +21 -0
  23. package/dist/index.d.ts +21 -0
  24. package/dist/index.js +114 -0
  25. package/dist/runtime/download/download.cjs +70 -0
  26. package/dist/runtime/download/download.d.cts +35 -0
  27. package/dist/runtime/download/download.d.ts +35 -0
  28. package/dist/runtime/download/download.js +45 -0
  29. package/dist/runtime/download/index.cjs +30 -0
  30. package/dist/runtime/download/index.d.cts +3 -0
  31. package/dist/runtime/download/index.d.ts +3 -0
  32. package/dist/runtime/download/index.js +8 -0
  33. package/dist/runtime/extract/extract.cjs +88 -0
  34. package/dist/runtime/extract/extract.d.cts +23 -0
  35. package/dist/runtime/extract/extract.d.ts +23 -0
  36. package/dist/runtime/extract/extract.js +64 -0
  37. package/dist/runtime/extract/index.cjs +28 -0
  38. package/dist/runtime/extract/index.d.cts +5 -0
  39. package/dist/runtime/extract/index.d.ts +5 -0
  40. package/dist/runtime/extract/index.js +4 -0
  41. package/dist/runtime/network/index.cjs +28 -0
  42. package/dist/runtime/network/index.d.cts +4 -0
  43. package/dist/runtime/network/index.d.ts +4 -0
  44. package/dist/runtime/network/index.js +6 -0
  45. package/dist/runtime/network/network.cjs +91 -0
  46. package/dist/runtime/network/network.d.cts +28 -0
  47. package/dist/runtime/network/network.d.ts +28 -0
  48. package/dist/runtime/network/network.js +67 -0
  49. package/dist/runtime/recovery/agent.cjs +223 -0
  50. package/dist/runtime/recovery/agent.d.cts +13 -0
  51. package/dist/runtime/recovery/agent.d.ts +13 -0
  52. package/dist/runtime/recovery/agent.js +199 -0
  53. package/dist/runtime/recovery/errors.cjs +124 -0
  54. package/dist/runtime/recovery/errors.d.cts +31 -0
  55. package/dist/runtime/recovery/errors.d.ts +31 -0
  56. package/dist/runtime/recovery/errors.js +100 -0
  57. package/dist/runtime/recovery/index.cjs +34 -0
  58. package/dist/runtime/recovery/index.d.cts +7 -0
  59. package/dist/runtime/recovery/index.d.ts +7 -0
  60. package/dist/runtime/recovery/index.js +10 -0
  61. package/dist/runtime/recovery/recovery.cjs +55 -0
  62. package/dist/runtime/recovery/recovery.d.cts +12 -0
  63. package/dist/runtime/recovery/recovery.d.ts +12 -0
  64. package/dist/runtime/recovery/recovery.js +31 -0
  65. package/dist/shared/config/config.cjs +44 -0
  66. package/dist/shared/config/config.d.cts +10 -0
  67. package/dist/shared/config/config.d.ts +10 -0
  68. package/dist/shared/config/config.js +18 -0
  69. package/dist/shared/config/index.cjs +32 -0
  70. package/dist/shared/config/index.d.cts +1 -0
  71. package/dist/shared/config/index.d.ts +1 -0
  72. package/dist/shared/config/index.js +10 -0
  73. package/dist/shared/debug/index.cjs +30 -0
  74. package/dist/shared/debug/index.d.cts +1 -0
  75. package/dist/shared/debug/index.d.ts +1 -0
  76. package/dist/shared/debug/index.js +5 -0
  77. package/dist/shared/debug/pause.cjs +90 -0
  78. package/dist/shared/debug/pause.d.cts +16 -0
  79. package/dist/shared/debug/pause.d.ts +16 -0
  80. package/dist/shared/debug/pause.js +55 -0
  81. package/dist/shared/instrumentation/errors.cjs +81 -0
  82. package/dist/shared/instrumentation/errors.d.cts +12 -0
  83. package/dist/shared/instrumentation/errors.d.ts +12 -0
  84. package/dist/shared/instrumentation/errors.js +57 -0
  85. package/dist/shared/instrumentation/index.cjs +35 -0
  86. package/dist/shared/instrumentation/index.d.cts +6 -0
  87. package/dist/shared/instrumentation/index.d.ts +6 -0
  88. package/dist/shared/instrumentation/index.js +12 -0
  89. package/dist/shared/instrumentation/instrument.cjs +206 -0
  90. package/dist/shared/instrumentation/instrument.d.cts +32 -0
  91. package/dist/shared/instrumentation/instrument.d.ts +32 -0
  92. package/dist/shared/instrumentation/instrument.js +190 -0
  93. package/dist/shared/llm/ai-sdk-adapter.cjs +67 -0
  94. package/dist/shared/llm/ai-sdk-adapter.d.cts +22 -0
  95. package/dist/shared/llm/ai-sdk-adapter.d.ts +22 -0
  96. package/dist/shared/llm/ai-sdk-adapter.js +43 -0
  97. package/dist/shared/llm/client.cjs +139 -0
  98. package/dist/shared/llm/client.d.cts +6 -0
  99. package/dist/shared/llm/client.d.ts +6 -0
  100. package/dist/shared/llm/client.js +115 -0
  101. package/dist/shared/llm/index.cjs +31 -0
  102. package/dist/shared/llm/index.d.cts +5 -0
  103. package/dist/shared/llm/index.d.ts +5 -0
  104. package/dist/shared/llm/index.js +6 -0
  105. package/dist/shared/llm/types.cjs +16 -0
  106. package/dist/shared/llm/types.d.cts +66 -0
  107. package/dist/shared/llm/types.d.ts +66 -0
  108. package/dist/shared/llm/types.js +0 -0
  109. package/dist/shared/logger/index.cjs +37 -0
  110. package/dist/shared/logger/index.d.cts +2 -0
  111. package/dist/shared/logger/index.d.ts +2 -0
  112. package/dist/shared/logger/index.js +13 -0
  113. package/dist/shared/logger/logger.cjs +232 -0
  114. package/dist/shared/logger/logger.d.cts +86 -0
  115. package/dist/shared/logger/logger.d.ts +86 -0
  116. package/dist/shared/logger/logger.js +207 -0
  117. package/dist/shared/logger/sinks.cjs +160 -0
  118. package/dist/shared/logger/sinks.d.cts +9 -0
  119. package/dist/shared/logger/sinks.d.ts +9 -0
  120. package/dist/shared/logger/sinks.js +124 -0
  121. package/dist/shared/paths/paths.cjs +104 -0
  122. package/dist/shared/paths/paths.d.cts +10 -0
  123. package/dist/shared/paths/paths.d.ts +10 -0
  124. package/dist/shared/paths/paths.js +73 -0
  125. package/dist/shared/run/api.cjs +28 -0
  126. package/dist/shared/run/api.d.cts +2 -0
  127. package/dist/shared/run/api.d.ts +2 -0
  128. package/dist/shared/run/api.js +4 -0
  129. package/dist/shared/run/browser.cjs +98 -0
  130. package/dist/shared/run/browser.d.cts +22 -0
  131. package/dist/shared/run/browser.d.ts +22 -0
  132. package/dist/shared/run/browser.js +74 -0
  133. package/dist/shared/state/index.cjs +38 -0
  134. package/dist/shared/state/index.d.cts +2 -0
  135. package/dist/shared/state/index.d.ts +2 -0
  136. package/dist/shared/state/index.js +16 -0
  137. package/dist/shared/state/session-state.cjs +92 -0
  138. package/dist/shared/state/session-state.d.cts +40 -0
  139. package/dist/shared/state/session-state.d.ts +40 -0
  140. package/dist/shared/state/session-state.js +62 -0
  141. package/dist/shared/visualization/ghost-cursor.cjs +174 -0
  142. package/dist/shared/visualization/ghost-cursor.d.cts +37 -0
  143. package/dist/shared/visualization/ghost-cursor.d.ts +37 -0
  144. package/dist/shared/visualization/ghost-cursor.js +145 -0
  145. package/dist/shared/visualization/highlight.cjs +134 -0
  146. package/dist/shared/visualization/highlight.d.cts +22 -0
  147. package/dist/shared/visualization/highlight.d.ts +22 -0
  148. package/dist/shared/visualization/highlight.js +108 -0
  149. package/dist/shared/visualization/index.cjs +45 -0
  150. package/dist/shared/visualization/index.d.cts +3 -0
  151. package/dist/shared/visualization/index.d.ts +3 -0
  152. package/dist/shared/visualization/index.js +24 -0
  153. package/dist/shared/workflow/workflow.cjs +47 -0
  154. package/dist/shared/workflow/workflow.d.cts +21 -0
  155. package/dist/shared/workflow/workflow.d.ts +21 -0
  156. package/dist/shared/workflow/workflow.js +21 -0
  157. package/package.json +37 -95
  158. package/bin/libretto.mjs +0 -18
  159. package/scripts/postinstall.mjs +0 -48
  160. /package/{skill → .agents/skills/libretto}/SKILL.md +0 -0
  161. /package/{skill → .agents/skills/libretto}/code-generation-rules.md +0 -0
  162. /package/{skill → .agents/skills/libretto}/integration-approach-selection.md +0 -0
@@ -0,0 +1,93 @@
1
+ import { listOpenPages } from "../core/browser.js";
2
+ import { withSessionLogger } from "../core/context.js";
3
+ import {
4
+ clearActionLog,
5
+ clearNetworkLog,
6
+ formatActionEntry,
7
+ formatNetworkEntry,
8
+ readActionLog,
9
+ readNetworkLog
10
+ } from "../core/telemetry.js";
11
+ async function resolvePageId(session, pageId) {
12
+ if (!pageId) return void 0;
13
+ const pages = await withSessionLogger(
14
+ session,
15
+ async (logger) => listOpenPages(session, logger)
16
+ );
17
+ const foundPage = pages.find((page) => page.id === pageId);
18
+ if (!foundPage) {
19
+ throw new Error(
20
+ `Page "${pageId}" was not found in session "${session}". Run "libretto-cli pages --session ${session}" to list ids.`
21
+ );
22
+ }
23
+ return pageId;
24
+ }
25
+ function registerLogCommands(yargs) {
26
+ return yargs.command(
27
+ "network",
28
+ "View captured network requests",
29
+ (cmd) => cmd.option("last", { type: "number" }).option("filter", { type: "string" }).option("method", { type: "string" }).option("page", { type: "string" }).option("clear", { type: "boolean", default: false }),
30
+ async (argv) => {
31
+ const session = String(argv.session);
32
+ if (argv.clear) {
33
+ clearNetworkLog(session);
34
+ console.log("Network log cleared.");
35
+ return;
36
+ }
37
+ const pageId = await resolvePageId(
38
+ session,
39
+ argv.page ? String(argv.page) : void 0
40
+ );
41
+ const entries = readNetworkLog(session, {
42
+ last: typeof argv.last === "number" ? argv.last : void 0,
43
+ filter: argv.filter,
44
+ method: argv.method,
45
+ pageId
46
+ });
47
+ if (entries.length === 0) {
48
+ console.log("No network requests captured.");
49
+ return;
50
+ }
51
+ for (const entry of entries) {
52
+ console.log(formatNetworkEntry(entry));
53
+ }
54
+ console.log(`
55
+ ${entries.length} request(s) shown.`);
56
+ }
57
+ ).command(
58
+ "actions",
59
+ "View captured actions",
60
+ (cmd) => cmd.option("last", { type: "number" }).option("filter", { type: "string" }).option("action", { type: "string" }).option("source", { type: "string" }).option("page", { type: "string" }).option("clear", { type: "boolean", default: false }),
61
+ async (argv) => {
62
+ const session = String(argv.session);
63
+ if (argv.clear) {
64
+ clearActionLog(session);
65
+ console.log("Action log cleared.");
66
+ return;
67
+ }
68
+ const pageId = await resolvePageId(
69
+ session,
70
+ argv.page ? String(argv.page) : void 0
71
+ );
72
+ const entries = readActionLog(session, {
73
+ last: typeof argv.last === "number" ? argv.last : void 0,
74
+ filter: argv.filter,
75
+ action: argv.action,
76
+ source: argv.source,
77
+ pageId
78
+ });
79
+ if (entries.length === 0) {
80
+ console.log("No actions captured.");
81
+ return;
82
+ }
83
+ for (const entry of entries) {
84
+ console.log(formatActionEntry(entry));
85
+ }
86
+ console.log(`
87
+ ${entries.length} action(s) shown.`);
88
+ }
89
+ );
90
+ }
91
+ export {
92
+ registerLogCommands
93
+ };
@@ -0,0 +1,217 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { connect, disconnectBrowser } from "../core/browser.js";
3
+ import { getSessionSnapshotRunDir } from "../core/context.js";
4
+ import { readSessionState } from "../core/session.js";
5
+ import {
6
+ canAnalyzeSnapshots,
7
+ runInterpret
8
+ } from "../core/snapshot-analyzer.js";
9
+ const DEFAULT_SNAPSHOT_CONTEXT = "No additional user context provided.";
10
+ const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 };
11
+ function generateSnapshotRunId() {
12
+ return `snapshot-${Date.now()}`;
13
+ }
14
+ function isZeroViewport(value) {
15
+ return typeof value === "number" && value <= 0;
16
+ }
17
+ function shouldForceSnapshotViewport(metrics) {
18
+ return isZeroViewport(metrics.configuredWidth) || isZeroViewport(metrics.configuredHeight) || isZeroViewport(metrics.innerWidth) || isZeroViewport(metrics.innerHeight);
19
+ }
20
+ function isZeroWidthScreenshotError(error) {
21
+ return error instanceof Error && error.message.includes("Cannot take screenshot with 0 width");
22
+ }
23
+ async function readSnapshotViewportMetrics(page) {
24
+ const configuredViewport = page.viewportSize();
25
+ let innerWidth = null;
26
+ let innerHeight = null;
27
+ try {
28
+ const innerViewport = await page.evaluate(() => ({
29
+ width: window.innerWidth,
30
+ height: window.innerHeight
31
+ }));
32
+ innerWidth = innerViewport.width;
33
+ innerHeight = innerViewport.height;
34
+ } catch {
35
+ }
36
+ return {
37
+ configuredWidth: configuredViewport?.width ?? null,
38
+ configuredHeight: configuredViewport?.height ?? null,
39
+ innerWidth,
40
+ innerHeight
41
+ };
42
+ }
43
+ function resolveSnapshotViewport(session, logger) {
44
+ const state = readSessionState(session, logger);
45
+ if (state?.viewport) {
46
+ logger.info("screenshot-viewport-from-session-state", {
47
+ session,
48
+ viewport: state.viewport
49
+ });
50
+ return state.viewport;
51
+ }
52
+ logger.info("screenshot-viewport-fallback", {
53
+ session,
54
+ reason: "no viewport in session state",
55
+ viewport: FALLBACK_SNAPSHOT_VIEWPORT
56
+ });
57
+ return FALLBACK_SNAPSHOT_VIEWPORT;
58
+ }
59
+ async function forceSnapshotViewport(page, viewport, logger, session, pageId, reason) {
60
+ await page.setViewportSize(viewport);
61
+ logger.warn("screenshot-viewport-forced", {
62
+ session,
63
+ pageId,
64
+ reason,
65
+ viewport
66
+ });
67
+ }
68
+ async function captureScreenshot(session, logger, pageId) {
69
+ logger.info("screenshot-start", { session, pageId });
70
+ const snapshotRunId = generateSnapshotRunId();
71
+ const snapshotRunDir = getSessionSnapshotRunDir(session, snapshotRunId);
72
+ mkdirSync(snapshotRunDir, { recursive: true });
73
+ const { browser, page } = await connect(session, logger, 1e4, {
74
+ pageId,
75
+ requireSinglePage: true
76
+ });
77
+ try {
78
+ let title = null;
79
+ try {
80
+ title = await page.title();
81
+ } catch (error) {
82
+ logger.warn("screenshot-title-read-failed", {
83
+ session,
84
+ pageId,
85
+ error
86
+ });
87
+ }
88
+ let pageUrl = null;
89
+ try {
90
+ pageUrl = page.url();
91
+ } catch (error) {
92
+ logger.warn("screenshot-url-read-failed", {
93
+ session,
94
+ pageId,
95
+ error
96
+ });
97
+ }
98
+ const pngPath = `${snapshotRunDir}/page.png`;
99
+ const htmlPath = `${snapshotRunDir}/page.html`;
100
+ const restoreViewport = resolveSnapshotViewport(session, logger);
101
+ const viewportMetrics = await readSnapshotViewportMetrics(page);
102
+ logger.info("screenshot-viewport-metrics", {
103
+ session,
104
+ pageId,
105
+ restoreViewport,
106
+ ...viewportMetrics
107
+ });
108
+ await forceSnapshotViewport(
109
+ page,
110
+ restoreViewport,
111
+ logger,
112
+ session,
113
+ pageId,
114
+ shouldForceSnapshotViewport(viewportMetrics) ? "preflight-invalid-viewport" : "preflight-normalize-viewport"
115
+ );
116
+ try {
117
+ await page.screenshot({ path: pngPath });
118
+ } catch (error) {
119
+ if (!isZeroWidthScreenshotError(error)) {
120
+ throw error;
121
+ }
122
+ await forceSnapshotViewport(
123
+ page,
124
+ restoreViewport,
125
+ logger,
126
+ session,
127
+ pageId,
128
+ "retry-after-zero-width-screenshot-error"
129
+ );
130
+ await page.screenshot({ path: pngPath });
131
+ }
132
+ const htmlContent = await page.content();
133
+ const fs = await import("node:fs/promises");
134
+ await fs.writeFile(htmlPath, htmlContent);
135
+ logger.info("screenshot-success", {
136
+ session,
137
+ pageUrl,
138
+ title,
139
+ pngPath,
140
+ htmlPath,
141
+ snapshotRunId
142
+ });
143
+ return { pngPath, htmlPath, baseName: snapshotRunId };
144
+ } catch (err) {
145
+ let pageAlive = false;
146
+ let browserConnected = false;
147
+ try {
148
+ browserConnected = browser.isConnected();
149
+ pageAlive = !page.isClosed();
150
+ } catch {
151
+ }
152
+ logger.error("screenshot-error", {
153
+ error: err,
154
+ session,
155
+ pageAlive,
156
+ browserConnected,
157
+ pageUrl: (() => {
158
+ try {
159
+ return page.url();
160
+ } catch {
161
+ return null;
162
+ }
163
+ })()
164
+ });
165
+ throw err;
166
+ } finally {
167
+ disconnectBrowser(browser, logger, session);
168
+ }
169
+ }
170
+ async function runSnapshot(session, logger, pageId, objective, context) {
171
+ const { pngPath, htmlPath } = await captureScreenshot(session, logger, pageId);
172
+ console.log("Screenshot saved:");
173
+ console.log(` PNG: ${pngPath}`);
174
+ console.log(` HTML: ${htmlPath}`);
175
+ const normalizedObjective = objective?.trim();
176
+ const normalizedContext = context?.trim();
177
+ if (!normalizedObjective && !normalizedContext) {
178
+ console.log("Use --objective flag to analyze snapshots.");
179
+ return;
180
+ }
181
+ if (!normalizedObjective) {
182
+ throw new Error(
183
+ "Couldn't run analysis: --objective is required when providing --context."
184
+ );
185
+ }
186
+ if (!canAnalyzeSnapshots()) {
187
+ throw new Error(
188
+ "Couldn't run analysis: no AI config set. Run 'libretto-cli ai configure codex' (or claude/gemini) to enable analysis."
189
+ );
190
+ }
191
+ await runInterpret({
192
+ objective: normalizedObjective,
193
+ session,
194
+ context: normalizedContext ?? DEFAULT_SNAPSHOT_CONTEXT,
195
+ pngPath,
196
+ htmlPath
197
+ }, logger);
198
+ }
199
+ function registerSnapshotCommands(yargs, logger) {
200
+ return yargs.command(
201
+ "snapshot",
202
+ "Capture PNG + HTML; analyze when --objective is provided (--context optional)",
203
+ (cmd) => cmd.option("page", { type: "string" }).option("objective", { type: "string" }).option("context", { type: "string" }),
204
+ async (argv) => {
205
+ await runSnapshot(
206
+ String(argv.session),
207
+ logger,
208
+ argv.page ? String(argv.page) : void 0,
209
+ argv.objective,
210
+ argv.context
211
+ );
212
+ }
213
+ );
214
+ }
215
+ export {
216
+ registerSnapshotCommands
217
+ };
@@ -0,0 +1,156 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { z } from "zod";
5
+ import { LIBRETTO_CONFIG_PATH } from "./context.js";
6
+ const CURRENT_CONFIG_VERSION = 1;
7
+ const AiPresetSchema = z.enum(["codex", "claude", "gemini"]);
8
+ const AiConfigSchema = z.object({
9
+ preset: AiPresetSchema,
10
+ commandPrefix: z.array(z.string()).min(1),
11
+ updatedAt: z.string()
12
+ }).strict();
13
+ const ViewportConfigSchema = z.object({
14
+ width: z.number().int().min(1),
15
+ height: z.number().int().min(1)
16
+ });
17
+ const LibrettoConfigSchema = z.object({
18
+ version: z.literal(CURRENT_CONFIG_VERSION),
19
+ ai: AiConfigSchema.optional(),
20
+ viewport: ViewportConfigSchema.optional()
21
+ }).passthrough();
22
+ const AI_CONFIG_PRESETS = {
23
+ codex: ["codex", "exec", "--skip-git-repo-check", "--sandbox", "read-only"],
24
+ claude: [join(homedir(), ".claude", "local", "claude"), "-p"],
25
+ gemini: ["gemini", "--output-format", "json"]
26
+ };
27
+ function invalidConfigError(configPath) {
28
+ return new Error(
29
+ `AI config is invalid at ${configPath}. Fix the file to match the expected schema or delete it.`
30
+ );
31
+ }
32
+ function parseConfig(raw, configPath) {
33
+ try {
34
+ return LibrettoConfigSchema.parse(JSON.parse(raw));
35
+ } catch {
36
+ throw invalidConfigError(configPath);
37
+ }
38
+ }
39
+ function readLibrettoConfig(configPath = LIBRETTO_CONFIG_PATH) {
40
+ if (!existsSync(configPath)) {
41
+ return { version: CURRENT_CONFIG_VERSION };
42
+ }
43
+ return parseConfig(readFileSync(configPath, "utf-8"), configPath);
44
+ }
45
+ function writeLibrettoConfig(config, configPath = LIBRETTO_CONFIG_PATH) {
46
+ const parsed = LibrettoConfigSchema.parse(config);
47
+ mkdirSync(dirname(configPath), { recursive: true });
48
+ writeFileSync(configPath, JSON.stringify(parsed, null, 2), "utf-8");
49
+ return parsed;
50
+ }
51
+ function readAiConfig(configPath = LIBRETTO_CONFIG_PATH) {
52
+ return readLibrettoConfig(configPath).ai ?? null;
53
+ }
54
+ function quoteShellArg(value) {
55
+ if (/^[a-zA-Z0-9_./:@=-]+$/.test(value)) return value;
56
+ return JSON.stringify(value);
57
+ }
58
+ function formatCommandPrefix(prefix) {
59
+ return prefix.map((arg) => quoteShellArg(arg)).join(" ");
60
+ }
61
+ function writeAiConfig(preset, commandPrefix, configPath = LIBRETTO_CONFIG_PATH) {
62
+ const librettoConfig = readLibrettoConfig(configPath);
63
+ const ai = AiConfigSchema.parse({
64
+ preset,
65
+ commandPrefix,
66
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
67
+ });
68
+ writeLibrettoConfig(
69
+ {
70
+ ...librettoConfig,
71
+ version: CURRENT_CONFIG_VERSION,
72
+ ai
73
+ },
74
+ configPath
75
+ );
76
+ return ai;
77
+ }
78
+ function clearAiConfig(configPath = LIBRETTO_CONFIG_PATH) {
79
+ const librettoConfig = readLibrettoConfig(configPath);
80
+ if (!librettoConfig.ai) return false;
81
+ const { ai: _ai, ...rest } = librettoConfig;
82
+ writeLibrettoConfig(
83
+ {
84
+ ...rest
85
+ },
86
+ configPath
87
+ );
88
+ return true;
89
+ }
90
+ function printAiConfig(config, configPath) {
91
+ console.log(`AI preset: ${config.preset}`);
92
+ console.log(`Command prefix: ${formatCommandPrefix(config.commandPrefix)}`);
93
+ console.log(`Config file: ${configPath}`);
94
+ console.log(`Updated at: ${config.updatedAt}`);
95
+ }
96
+ function printConfigureUsage(commandName) {
97
+ console.log(
98
+ `Usage: ${commandName} <codex|claude|gemini> [-- <command prefix...>]
99
+ ${commandName}
100
+ ${commandName} --clear`
101
+ );
102
+ }
103
+ function runAiConfigure(input, options = {}) {
104
+ const configureCommandName = options.configureCommandName ?? "libretto-cli ai configure";
105
+ const configPath = options.configPath ?? LIBRETTO_CONFIG_PATH;
106
+ const presetArg = input.preset?.trim();
107
+ const customPrefix = (input.customPrefix ?? []).filter(Boolean);
108
+ if (!presetArg && customPrefix.length === 0 && !input.clear) {
109
+ const config2 = readAiConfig(configPath);
110
+ if (!config2) {
111
+ console.log(`No AI config set. Run '${configureCommandName} codex' to set one.`);
112
+ return;
113
+ }
114
+ printAiConfig(config2, configPath);
115
+ return;
116
+ }
117
+ if (input.clear) {
118
+ const removed = clearAiConfig(configPath);
119
+ if (removed) {
120
+ console.log(`Cleared AI config: ${configPath}`);
121
+ } else {
122
+ console.log("No AI config was set.");
123
+ }
124
+ return;
125
+ }
126
+ const parsedPreset = AiPresetSchema.safeParse(presetArg);
127
+ if (!parsedPreset.success) {
128
+ printConfigureUsage(configureCommandName);
129
+ throw new Error(
130
+ "Missing or invalid preset. Use one of: codex, claude, gemini."
131
+ );
132
+ }
133
+ if (input.customPrefix && input.customPrefix.length > 0 && customPrefix.length === 0) {
134
+ throw new Error("Custom command prefix cannot be empty.");
135
+ }
136
+ const preset = parsedPreset.data;
137
+ const commandPrefix = customPrefix.length > 0 ? customPrefix : AI_CONFIG_PRESETS[preset];
138
+ const config = writeAiConfig(preset, commandPrefix, configPath);
139
+ console.log("AI config saved.");
140
+ printAiConfig(config, configPath);
141
+ }
142
+ export {
143
+ AI_CONFIG_PRESETS,
144
+ AiConfigSchema,
145
+ AiPresetSchema,
146
+ CURRENT_CONFIG_VERSION,
147
+ LibrettoConfigSchema,
148
+ ViewportConfigSchema,
149
+ clearAiConfig,
150
+ formatCommandPrefix,
151
+ readAiConfig,
152
+ readLibrettoConfig,
153
+ runAiConfigure,
154
+ writeAiConfig,
155
+ writeLibrettoConfig
156
+ };