@justram/pie 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +236 -0
  4. package/bin/pie +14 -0
  5. package/dist/cache/index.d.ts +4 -0
  6. package/dist/cache/index.js +3 -0
  7. package/dist/cache/warm.d.ts +3 -0
  8. package/dist/cache/warm.js +23 -0
  9. package/dist/cli/args.d.ts +30 -0
  10. package/dist/cli/args.js +185 -0
  11. package/dist/cli/attachments.d.ts +7 -0
  12. package/dist/cli/attachments.js +29 -0
  13. package/dist/cli/config.d.ts +22 -0
  14. package/dist/cli/config.js +20 -0
  15. package/dist/cli/image.d.ts +17 -0
  16. package/dist/cli/image.js +73 -0
  17. package/dist/cli/index.d.ts +2 -0
  18. package/dist/cli/index.js +1 -0
  19. package/dist/cli/oauth.d.ts +14 -0
  20. package/dist/cli/oauth.js +178 -0
  21. package/dist/cli/stream.d.ts +7 -0
  22. package/dist/cli/stream.js +73 -0
  23. package/dist/cli.d.ts +2 -0
  24. package/dist/cli.js +15 -0
  25. package/dist/core/cache/file.d.ts +4 -0
  26. package/dist/core/cache/file.js +44 -0
  27. package/dist/core/cache/key.d.ts +2 -0
  28. package/dist/core/cache/key.js +12 -0
  29. package/dist/core/cache/memory.d.ts +4 -0
  30. package/dist/core/cache/memory.js +33 -0
  31. package/dist/core/cache/types.d.ts +19 -0
  32. package/dist/core/cache/types.js +1 -0
  33. package/dist/core/errors.d.ts +39 -0
  34. package/dist/core/errors.js +50 -0
  35. package/dist/core/events.d.ts +87 -0
  36. package/dist/core/events.js +1 -0
  37. package/dist/core/extract.d.ts +4 -0
  38. package/dist/core/extract.js +384 -0
  39. package/dist/core/frontmatter.d.ts +5 -0
  40. package/dist/core/frontmatter.js +58 -0
  41. package/dist/core/helpers.d.ts +5 -0
  42. package/dist/core/helpers.js +80 -0
  43. package/dist/core/schema/normalize.d.ts +7 -0
  44. package/dist/core/schema/normalize.js +187 -0
  45. package/dist/core/setup.d.ts +13 -0
  46. package/dist/core/setup.js +174 -0
  47. package/dist/core/types.d.ts +143 -0
  48. package/dist/core/types.js +1 -0
  49. package/dist/core/validators/assert.d.ts +1 -0
  50. package/dist/core/validators/assert.js +18 -0
  51. package/dist/core/validators/command.d.ts +1 -0
  52. package/dist/core/validators/command.js +10 -0
  53. package/dist/core/validators/http.d.ts +1 -0
  54. package/dist/core/validators/http.js +28 -0
  55. package/dist/core/validators/index.d.ts +22 -0
  56. package/dist/core/validators/index.js +55 -0
  57. package/dist/core/validators/shell.d.ts +9 -0
  58. package/dist/core/validators/shell.js +24 -0
  59. package/dist/errors.d.ts +1 -0
  60. package/dist/errors.js +1 -0
  61. package/dist/events.d.ts +1 -0
  62. package/dist/events.js +1 -0
  63. package/dist/extract.d.ts +4 -0
  64. package/dist/extract.js +18 -0
  65. package/dist/index.d.ts +13 -0
  66. package/dist/index.js +8 -0
  67. package/dist/main.d.ts +9 -0
  68. package/dist/main.js +571 -0
  69. package/dist/models.d.ts +21 -0
  70. package/dist/models.js +21 -0
  71. package/dist/recipes/index.d.ts +34 -0
  72. package/dist/recipes/index.js +185 -0
  73. package/dist/runtime/node.d.ts +2 -0
  74. package/dist/runtime/node.js +71 -0
  75. package/dist/runtime/types.d.ts +32 -0
  76. package/dist/runtime/types.js +1 -0
  77. package/dist/setup.d.ts +2 -0
  78. package/dist/setup.js +1 -0
  79. package/dist/types.d.ts +1 -0
  80. package/dist/types.js +1 -0
  81. package/dist/utils/helpers.d.ts +5 -0
  82. package/dist/utils/helpers.js +80 -0
  83. package/package.json +71 -0
@@ -0,0 +1,24 @@
1
+ import { spawn } from "node:child_process";
2
+ export async function runShell(command, options) {
3
+ const child = spawn("sh", ["-c", command], {
4
+ stdio: ["pipe", "pipe", "pipe"],
5
+ signal: options?.signal,
6
+ });
7
+ const stdoutChunks = [];
8
+ const stderrChunks = [];
9
+ child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
10
+ child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
11
+ if (options?.stdin !== undefined) {
12
+ child.stdin.write(options.stdin);
13
+ }
14
+ child.stdin.end();
15
+ const code = await new Promise((resolve, reject) => {
16
+ child.on("error", reject);
17
+ child.on("close", (value) => resolve(value ?? 0));
18
+ });
19
+ return {
20
+ code,
21
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
22
+ stderr: Buffer.concat(stderrChunks).toString("utf8"),
23
+ };
24
+ }
@@ -0,0 +1 @@
1
+ export { AbortError, CommandValidationError, ExtractError, HttpValidationError, MaxTurnsError, SchemaValidationError, } from "./core/errors.js";
package/dist/errors.js ADDED
@@ -0,0 +1 @@
1
+ export { AbortError, CommandValidationError, ExtractError, HttpValidationError, MaxTurnsError, SchemaValidationError, } from "./core/errors.js";
@@ -0,0 +1 @@
1
+ export type * from "./core/events.js";
package/dist/events.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import { type Message } from "@mariozechner/pi-ai";
2
+ import type { ExtractOptions, ExtractStream } from "./types.js";
3
+ export declare function extract<T>(input: string | Message[], options: ExtractOptions<T>): ExtractStream<T>;
4
+ export declare function extractSync<T>(input: string | Message[], options: ExtractOptions<T>): Promise<T>;
@@ -0,0 +1,18 @@
1
+ import { streamSimple } from "@mariozechner/pi-ai";
2
+ import { extract as extractInternal, extractSync as extractSyncInternal } from "./core/extract.js";
3
+ export function extract(input, options) {
4
+ const streamFn = options.streamFn ??
5
+ ((model, context, streamOptions) => {
6
+ streamOptions.onModelSelected?.(model);
7
+ return streamSimple(model, context, streamOptions);
8
+ });
9
+ return extractInternal(input, { ...options, streamFn });
10
+ }
11
+ export async function extractSync(input, options) {
12
+ const streamFn = options.streamFn ??
13
+ ((model, context, streamOptions) => {
14
+ streamOptions.onModelSelected?.(model);
15
+ return streamSimple(model, context, streamOptions);
16
+ });
17
+ return await extractSyncInternal(input, { ...options, streamFn });
18
+ }
@@ -0,0 +1,13 @@
1
+ export { type AssistantMessage, type ImageContent, type Message, type Model, StringEnum, type UserMessage, } from "@mariozechner/pi-ai";
2
+ export { type Static, type TSchema, Type } from "@sinclair/typebox";
3
+ export type { CacheEntry, CacheOptions, CacheStore } from "./cache/index.js";
4
+ export { createFileCache, createMemoryCache, warmCache } from "./cache/index.js";
5
+ export { AbortError, CommandValidationError, ExtractError, HttpValidationError, MaxTurnsError, SchemaValidationError, } from "./errors.js";
6
+ export type { ExtractEvent, ValidatorLayer, } from "./events.js";
7
+ export { extract, extractSync } from "./extract.js";
8
+ export { getModel, getModels, getProviders } from "./models.js";
9
+ export type { LoadRecipeSetupOptions, LoadRecipesOptions, LoadRecipesResult, Recipe, RecipeWarning, } from "./recipes/index.js";
10
+ export { loadRecipeSetup, loadRecipes, resolveRecipe, } from "./recipes/index.js";
11
+ export type { ExtractionSetup, LoadExtractionSetupOptions } from "./setup.js";
12
+ export { loadExtractionSetup } from "./setup.js";
13
+ export type { ExtractOptions, ExtractResult, ExtractStream, ThinkingBudgets, ThinkingLevel, Usage } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { StringEnum, } from "@mariozechner/pi-ai";
2
+ export { Type } from "@sinclair/typebox";
3
+ export { createFileCache, createMemoryCache, warmCache } from "./cache/index.js";
4
+ export { AbortError, CommandValidationError, ExtractError, HttpValidationError, MaxTurnsError, SchemaValidationError, } from "./errors.js";
5
+ export { extract, extractSync } from "./extract.js";
6
+ export { getModel, getModels, getProviders } from "./models.js";
7
+ export { loadRecipeSetup, loadRecipes, resolveRecipe, } from "./recipes/index.js";
8
+ export { loadExtractionSetup } from "./setup.js";
package/dist/main.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ import { extract } from "./extract.js";
3
+ export interface CliDeps {
4
+ extractFn?: typeof extract;
5
+ stdin?: Readable;
6
+ stdout?: Writable;
7
+ stderr?: Writable;
8
+ }
9
+ export declare function main(argv: string[], deps?: CliDeps): Promise<number>;
package/dist/main.js ADDED
@@ -0,0 +1,571 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { parseArgs, renderHelp } from "./cli/args.js";
4
+ import { loadAttachments } from "./cli/attachments.js";
5
+ import { loadCliConfig } from "./cli/config.js";
6
+ import { loginWithOAuthProvider, resolveApiKeyForProvider } from "./cli/oauth.js";
7
+ import { createJsonStreamer } from "./cli/stream.js";
8
+ import { MaxTurnsError } from "./core/errors.js";
9
+ import { normalizeToolSchema } from "./core/schema/normalize.js";
10
+ import { loadExtractionSetupFromContent } from "./core/setup.js";
11
+ import { extract } from "./extract.js";
12
+ import { getModels, getProviders } from "./models.js";
13
+ import { loadRecipeSetup, loadRecipes } from "./recipes/index.js";
14
+ const MODEL_PROVIDER_PREFERENCE = ["anthropic", "openai", "google"];
15
+ class CliExitError extends Error {
16
+ exitCode;
17
+ constructor(message, exitCode) {
18
+ super(message);
19
+ this.exitCode = exitCode;
20
+ this.name = "CliExitError";
21
+ }
22
+ }
23
+ export async function main(argv, deps = {}) {
24
+ const stdout = deps.stdout ?? process.stdout;
25
+ const stderr = deps.stderr ?? process.stderr;
26
+ try {
27
+ return await runCliInternal(argv, deps, stdout, stderr);
28
+ }
29
+ catch (error) {
30
+ const err = error instanceof Error ? error : new Error(String(error));
31
+ if (err instanceof CliExitError) {
32
+ writeLine(stderr, `Error: ${err.message}`);
33
+ return err.exitCode;
34
+ }
35
+ writeLine(stderr, `Error: ${err.message}`);
36
+ return mapExtractionExitCode(err);
37
+ }
38
+ }
39
+ async function runCliInternal(argv, deps, stdout, stderr) {
40
+ const { args, errors } = parseArgs(argv);
41
+ if (args.help) {
42
+ stdout.write(renderHelp());
43
+ return 0;
44
+ }
45
+ if (args.version) {
46
+ stdout.write(`${getVersion()}\n`);
47
+ return 0;
48
+ }
49
+ if (errors.length > 0) {
50
+ throw new CliExitError(errors.join("\n"), 2);
51
+ }
52
+ if (args.login) {
53
+ try {
54
+ await loginWithOAuthProvider(args.login, stderr);
55
+ }
56
+ catch (error) {
57
+ const message = error instanceof Error ? error.message : String(error);
58
+ throw new CliExitError(message, 3);
59
+ }
60
+ writeLine(stderr, `OAuth login completed for ${args.login}.`);
61
+ return 0;
62
+ }
63
+ if (args.prompt && args.promptFile) {
64
+ throw new CliExitError("Specify either --prompt or --prompt-file, not both.", 2);
65
+ }
66
+ const useRecipe = Boolean(args.recipe);
67
+ if (args.config && useRecipe) {
68
+ throw new CliExitError("Specify either --config or --recipe, not both.", 2);
69
+ }
70
+ if (useRecipe && (args.prompt || args.promptFile)) {
71
+ throw new CliExitError("Do not use --prompt or --prompt-file with --recipe.", 2);
72
+ }
73
+ if (!useRecipe && (args.recipeConfig || args.recipeVars)) {
74
+ throw new CliExitError("--recipe-config and --recipe-vars require --recipe.", 2);
75
+ }
76
+ if (args.listRecipes) {
77
+ const result = loadRecipes({ cwd: process.cwd() });
78
+ logRecipeWarnings(result.warnings, stderr);
79
+ const sorted = [...result.recipes].sort((left, right) => left.name.localeCompare(right.name));
80
+ for (const recipe of sorted) {
81
+ stdout.write(`${recipe.name}\t${recipe.description}\t${recipe.source}\n`);
82
+ }
83
+ return 0;
84
+ }
85
+ const promptInput = args.promptFile ? readTextFile(args.promptFile, "prompt") : args.prompt;
86
+ const promptPath = args.promptFile ? resolve(args.promptFile) : undefined;
87
+ const promptIsSetup = !args.config && !useRecipe && typeof promptInput === "string" && promptInput.startsWith("---");
88
+ let setupFromPrompt;
89
+ if (promptIsSetup) {
90
+ const setupPath = promptPath ?? resolve(process.cwd(), "inline-prompt.md");
91
+ try {
92
+ setupFromPrompt = loadExtractionSetupFromContent(promptInput, setupPath);
93
+ }
94
+ catch (error) {
95
+ const message = error instanceof Error ? error.message : String(error);
96
+ throw new CliExitError(message, 2);
97
+ }
98
+ }
99
+ if (!args.config && !useRecipe) {
100
+ if (!args.schema && !setupFromPrompt) {
101
+ throw new CliExitError("Missing required option: --schema", 2);
102
+ }
103
+ if (!args.jsonSchema && !promptInput) {
104
+ throw new CliExitError("Missing required option: --prompt or --prompt-file", 2);
105
+ }
106
+ }
107
+ const baseSchema = args.schema ? loadSchema(args.schema) : undefined;
108
+ const basePrompt = setupFromPrompt ? undefined : promptInput;
109
+ const modelOverride = args.model ? resolveModel(args.model) : undefined;
110
+ const baseModel = setupFromPrompt || useRecipe ? modelOverride : (modelOverride ?? resolveDefaultModel());
111
+ let recipe;
112
+ let recipeVars;
113
+ if (useRecipe) {
114
+ try {
115
+ recipeVars = args.recipeVars ? parseRecipeVars(args.recipeVars) : undefined;
116
+ }
117
+ catch (error) {
118
+ const message = error instanceof Error ? error.message : String(error);
119
+ throw new CliExitError(message, 2);
120
+ }
121
+ const result = loadRecipes({ cwd: process.cwd() });
122
+ logRecipeWarnings(result.warnings, stderr);
123
+ recipe = result.recipes.find((entry) => entry.name === args.recipe);
124
+ if (!recipe) {
125
+ throw new CliExitError(`Recipe not found: ${args.recipe}`, 2);
126
+ }
127
+ }
128
+ const attachments = (() => {
129
+ try {
130
+ return loadAttachments(args.attachments);
131
+ }
132
+ catch (error) {
133
+ const message = error instanceof Error ? error.message : String(error);
134
+ throw new CliExitError(`Failed to load attachments: ${message}`, 4);
135
+ }
136
+ })();
137
+ const input = args.jsonSchema ? "" : await loadInput(args.input, deps.stdin ?? process.stdin);
138
+ const inputText = [attachments.textPrefix, input].filter((part) => part.length > 0).join("\n\n");
139
+ const baseOptions = {
140
+ schema: baseSchema,
141
+ prompt: basePrompt,
142
+ model: baseModel,
143
+ attachments: attachments.images,
144
+ maxTurns: args.maxTurns,
145
+ validateCommand: args.validateCommand,
146
+ validateUrl: args.validateUrl,
147
+ };
148
+ const recipeOverrides = {
149
+ ...(baseSchema ? { schema: baseSchema } : {}),
150
+ ...(modelOverride ? { model: modelOverride } : {}),
151
+ ...(args.maxTurns ? { maxTurns: args.maxTurns } : {}),
152
+ ...(args.validateCommand ? { validateCommand: args.validateCommand } : {}),
153
+ ...(args.validateUrl ? { validateUrl: args.validateUrl } : {}),
154
+ };
155
+ let finalInput = inputText;
156
+ let outputPath = args.output;
157
+ let quiet = Boolean(args.quiet);
158
+ let verbose = Boolean(args.verbose);
159
+ let streamOutput = Boolean(args.stream);
160
+ let options;
161
+ if (useRecipe) {
162
+ if (!recipe) {
163
+ throw new CliExitError("Recipe not resolved.", 2);
164
+ }
165
+ let recipePath = args.recipeConfig ?? "setup.md";
166
+ let resolvedRecipePath = resolve(recipe.baseDir, recipePath);
167
+ let recipeUsesConfig = isConfigFile(resolvedRecipePath);
168
+ if (!args.recipeConfig && !existsSync(resolvedRecipePath)) {
169
+ const fallback = resolve(recipe.baseDir, "config.ts");
170
+ if (existsSync(fallback)) {
171
+ recipePath = "config.ts";
172
+ resolvedRecipePath = fallback;
173
+ recipeUsesConfig = true;
174
+ }
175
+ }
176
+ if (recipeUsesConfig) {
177
+ let configFn;
178
+ try {
179
+ configFn = await loadCliConfig(resolvedRecipePath);
180
+ }
181
+ catch (error) {
182
+ const message = error instanceof Error ? error.message : String(error);
183
+ throw new CliExitError(message, 4);
184
+ }
185
+ let configResult;
186
+ try {
187
+ configResult = await configFn({
188
+ args,
189
+ input,
190
+ inputText,
191
+ attachments,
192
+ resolveModel,
193
+ resolveApiKeyForProvider: async (provider) => await resolveApiKeyForProvider(provider, stderr),
194
+ });
195
+ }
196
+ catch (error) {
197
+ const message = error instanceof Error ? error.message : String(error);
198
+ throw new CliExitError(`Config execution failed: ${message}`, 4);
199
+ }
200
+ if (!configResult || typeof configResult !== "object" || !("options" in configResult)) {
201
+ throw new CliExitError("Config must return an object with an options field.", 2);
202
+ }
203
+ const result = configResult;
204
+ if (!result.options || typeof result.options !== "object") {
205
+ throw new CliExitError("Config must return an options object.", 2);
206
+ }
207
+ options = {
208
+ ...result.options,
209
+ ...recipeOverrides,
210
+ attachments: attachments.images,
211
+ };
212
+ finalInput = result.input ?? finalInput;
213
+ outputPath = result.output ?? outputPath;
214
+ quiet = result.quiet ?? quiet;
215
+ verbose = result.verbose ?? verbose;
216
+ streamOutput = result.stream ?? streamOutput;
217
+ }
218
+ else {
219
+ let setup;
220
+ try {
221
+ setup = loadRecipeSetup(recipe, { setupFile: recipePath, vars: recipeVars });
222
+ }
223
+ catch (error) {
224
+ const message = error instanceof Error ? error.message : String(error);
225
+ throw new CliExitError(`Failed to load recipe setup: ${message}`, 4);
226
+ }
227
+ options = { ...setup.options, ...recipeOverrides, attachments: attachments.images };
228
+ }
229
+ }
230
+ else if (args.config) {
231
+ let configFn;
232
+ try {
233
+ configFn = await loadCliConfig(args.config);
234
+ }
235
+ catch (error) {
236
+ const message = error instanceof Error ? error.message : String(error);
237
+ throw new CliExitError(message, 4);
238
+ }
239
+ let configResult;
240
+ try {
241
+ configResult = await configFn({
242
+ args,
243
+ input,
244
+ inputText,
245
+ attachments,
246
+ resolveModel,
247
+ resolveApiKeyForProvider: async (provider) => await resolveApiKeyForProvider(provider, stderr),
248
+ });
249
+ }
250
+ catch (error) {
251
+ const message = error instanceof Error ? error.message : String(error);
252
+ throw new CliExitError(`Config execution failed: ${message}`, 4);
253
+ }
254
+ if (!configResult || typeof configResult !== "object" || !("options" in configResult)) {
255
+ throw new CliExitError("Config must return an object with an options field.", 2);
256
+ }
257
+ const result = configResult;
258
+ if (!result.options || typeof result.options !== "object") {
259
+ throw new CliExitError("Config must return an options object.", 2);
260
+ }
261
+ options = { ...baseOptions, ...result.options };
262
+ finalInput = result.input ?? finalInput;
263
+ outputPath = result.output ?? outputPath;
264
+ quiet = result.quiet ?? quiet;
265
+ verbose = result.verbose ?? verbose;
266
+ streamOutput = result.stream ?? streamOutput;
267
+ }
268
+ else if (setupFromPrompt) {
269
+ const setupOverrides = {
270
+ ...(baseSchema ? { schema: baseSchema } : {}),
271
+ ...(modelOverride ? { model: modelOverride } : {}),
272
+ ...(args.maxTurns ? { maxTurns: args.maxTurns } : {}),
273
+ ...(args.validateCommand ? { validateCommand: args.validateCommand } : {}),
274
+ ...(args.validateUrl ? { validateUrl: args.validateUrl } : {}),
275
+ };
276
+ options = {
277
+ ...setupFromPrompt.options,
278
+ ...setupOverrides,
279
+ attachments: attachments.images,
280
+ };
281
+ }
282
+ else {
283
+ options = baseOptions;
284
+ }
285
+ if (!options.schema) {
286
+ throw new CliExitError("Missing schema. Provide --schema, include schema in a setup, use --recipe with schema, or return schema from --config.", 2);
287
+ }
288
+ if (!options.model) {
289
+ throw new CliExitError("Missing model. Provide --model, include model in a setup, use --recipe with model, or return model from --config.", 2);
290
+ }
291
+ if (!args.jsonSchema && !options.prompt) {
292
+ throw new CliExitError("Missing prompt. Provide --prompt/--prompt-file, include prompt in a setup, use --recipe with prompt, or return prompt from --config.", 2);
293
+ }
294
+ if (args.jsonSchema) {
295
+ const normalized = normalizeToolSchema(options.model, options.schema).schema;
296
+ stdout.write(`${JSON.stringify(normalized, null, 2)}\n`);
297
+ return 0;
298
+ }
299
+ if (!options.apiKey) {
300
+ try {
301
+ options.apiKey = await resolveApiKeyForProvider(options.model.provider, stderr);
302
+ }
303
+ catch (error) {
304
+ const message = error instanceof Error ? error.message : String(error);
305
+ throw new CliExitError(`Authentication failed: ${message}`, 3);
306
+ }
307
+ }
308
+ const extractFn = deps.extractFn ?? extract;
309
+ const stream = extractFn(finalInput, options);
310
+ const effectiveQuiet = Boolean(quiet);
311
+ const effectiveVerbose = Boolean(verbose) && !effectiveQuiet;
312
+ const effectiveStreamOutput = Boolean(streamOutput) && !effectiveQuiet;
313
+ const jsonStreamer = effectiveStreamOutput
314
+ ? createJsonStreamer((line) => {
315
+ stderr.write(line);
316
+ })
317
+ : null;
318
+ let finalResult;
319
+ let extractionError;
320
+ for await (const event of stream) {
321
+ if (effectiveStreamOutput) {
322
+ if (event.type === "llm_delta") {
323
+ jsonStreamer?.handleDelta(event.delta);
324
+ }
325
+ if (event.type === "tool_call") {
326
+ jsonStreamer?.handleToolCall(event.toolCall.arguments);
327
+ }
328
+ if (event.type === "turn_start") {
329
+ jsonStreamer?.reset();
330
+ }
331
+ }
332
+ if (effectiveVerbose) {
333
+ logVerbose(event, stderr);
334
+ }
335
+ if (event.type === "complete") {
336
+ finalResult = { data: event.result, turns: event.turns, usage: event.usage };
337
+ }
338
+ if (event.type === "error") {
339
+ extractionError = event.error;
340
+ }
341
+ }
342
+ const result = finalResult ?? (await stream.result());
343
+ if (extractionError || !result) {
344
+ const err = extractionError ?? new Error("Extraction failed.");
345
+ writeLine(stderr, `Error: ${err.message}`);
346
+ return mapExtractionExitCode(err);
347
+ }
348
+ const json = `${JSON.stringify(result.data, null, 2)}\n`;
349
+ if (outputPath) {
350
+ try {
351
+ writeFileSync(resolve(outputPath), json, "utf8");
352
+ }
353
+ catch (error) {
354
+ const message = error instanceof Error ? error.message : String(error);
355
+ throw new CliExitError(`Failed to write output file: ${message}`, 4);
356
+ }
357
+ return 0;
358
+ }
359
+ stdout.write(json);
360
+ return 0;
361
+ }
362
+ function loadSchema(schemaArg) {
363
+ const trimmed = schemaArg.trim();
364
+ try {
365
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
366
+ return JSON.parse(trimmed);
367
+ }
368
+ const raw = readTextFile(trimmed, "schema");
369
+ return JSON.parse(raw);
370
+ }
371
+ catch (error) {
372
+ if (error instanceof CliExitError) {
373
+ throw error;
374
+ }
375
+ const message = error instanceof Error ? error.message : String(error);
376
+ throw new CliExitError(`Invalid schema: ${message}`, 2);
377
+ }
378
+ }
379
+ function readTextFile(path, label) {
380
+ try {
381
+ return readFileSync(resolve(path), "utf8");
382
+ }
383
+ catch (error) {
384
+ const message = error instanceof Error ? error.message : String(error);
385
+ throw new CliExitError(`Failed to read ${label} file: ${message}`, 4);
386
+ }
387
+ }
388
+ function parseRecipeVars(value) {
389
+ let parsed;
390
+ try {
391
+ parsed = JSON.parse(value);
392
+ }
393
+ catch (error) {
394
+ const message = error instanceof Error ? error.message : String(error);
395
+ throw new CliExitError(`Invalid recipe vars JSON: ${message}`, 2);
396
+ }
397
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
398
+ throw new CliExitError("Recipe vars must be a JSON object.", 2);
399
+ }
400
+ return parsed;
401
+ }
402
+ function logRecipeWarnings(warnings, stderr) {
403
+ for (const warning of warnings) {
404
+ writeLine(stderr, `[recipe] ${warning.recipePath}: ${warning.message}`);
405
+ }
406
+ }
407
+ function isConfigFile(path) {
408
+ return path.endsWith(".ts") || path.endsWith(".js") || path.endsWith(".mjs") || path.endsWith(".cjs");
409
+ }
410
+ async function loadInput(inputPath, stdin) {
411
+ if (inputPath) {
412
+ return readTextFile(inputPath, "input");
413
+ }
414
+ const isTty = typeof stdin.isTTY === "boolean" && stdin.isTTY;
415
+ if (isTty) {
416
+ return "";
417
+ }
418
+ try {
419
+ return await readStream(stdin);
420
+ }
421
+ catch (error) {
422
+ const message = error instanceof Error ? error.message : String(error);
423
+ throw new CliExitError(`Failed to read stdin: ${message}`, 4);
424
+ }
425
+ }
426
+ function resolveModel(value) {
427
+ if (value.includes("/")) {
428
+ const [provider, pattern] = value.split("/");
429
+ if (!provider || !pattern) {
430
+ throw new CliExitError(`Invalid model spec: ${value}`, 2);
431
+ }
432
+ if (!isKnownProvider(provider)) {
433
+ throw new CliExitError(`Unknown provider: ${provider}`, 2);
434
+ }
435
+ const matches = findModelMatches(getModels(provider), pattern);
436
+ if (matches.length === 0) {
437
+ throw new CliExitError(`Model not found: ${value}`, 2);
438
+ }
439
+ return pickBestModelMatch(matches);
440
+ }
441
+ const allModels = getProviders().flatMap((provider) => getModels(provider));
442
+ const matches = findModelMatches(allModels, value);
443
+ if (matches.length === 0) {
444
+ throw new CliExitError(`Unknown model: ${value}`, 2);
445
+ }
446
+ return pickBestModelMatch(matches);
447
+ }
448
+ function resolveDefaultModel() {
449
+ const providers = getProviders();
450
+ const preferredProviders = MODEL_PROVIDER_PREFERENCE.filter((provider) => providers.includes(provider));
451
+ const orderedProviders = [
452
+ ...preferredProviders,
453
+ ...providers.filter((provider) => !preferredProviders.includes(provider)),
454
+ ];
455
+ for (const provider of orderedProviders) {
456
+ const models = getModels(provider);
457
+ if (models.length === 0) {
458
+ continue;
459
+ }
460
+ return pickBestModelMatch(models);
461
+ }
462
+ throw new CliExitError("No models available. Configure a provider API key or select a model explicitly.", 3);
463
+ }
464
+ function isAliasModelId(id) {
465
+ if (id.endsWith("-latest")) {
466
+ return true;
467
+ }
468
+ return !/-\d{8}$/.test(id);
469
+ }
470
+ function findModelMatches(models, pattern) {
471
+ const normalized = pattern.toLowerCase();
472
+ const exactMatches = models.filter((model) => model.id.toLowerCase() === normalized);
473
+ if (exactMatches.length > 0) {
474
+ return exactMatches;
475
+ }
476
+ const prefixMatches = models.filter((model) => model.id.toLowerCase().startsWith(normalized));
477
+ if (prefixMatches.length > 0) {
478
+ return prefixMatches;
479
+ }
480
+ return models.filter((model) => {
481
+ const id = model.id.toLowerCase();
482
+ const name = model.name?.toLowerCase() ?? "";
483
+ return id.includes(normalized) || name.includes(normalized);
484
+ });
485
+ }
486
+ function pickBestModelMatch(matches) {
487
+ const sorted = [...matches].sort((left, right) => {
488
+ const providerRank = providerPreferenceRank(left.provider) - providerPreferenceRank(right.provider);
489
+ if (providerRank !== 0) {
490
+ return providerRank;
491
+ }
492
+ const aliasRank = (isAliasModelId(left.id) ? 0 : 1) - (isAliasModelId(right.id) ? 0 : 1);
493
+ if (aliasRank !== 0) {
494
+ return aliasRank;
495
+ }
496
+ return right.id.localeCompare(left.id);
497
+ });
498
+ return sorted[0];
499
+ }
500
+ function providerPreferenceRank(provider) {
501
+ const index = MODEL_PROVIDER_PREFERENCE.indexOf(provider);
502
+ return index === -1 ? MODEL_PROVIDER_PREFERENCE.length : index;
503
+ }
504
+ function isKnownProvider(value) {
505
+ return getProviders().includes(value);
506
+ }
507
+ function logVerbose(event, stderr) {
508
+ switch (event.type) {
509
+ case "start":
510
+ writeLine(stderr, `[start] Max turns: ${event.maxTurns}`);
511
+ break;
512
+ case "turn_start":
513
+ writeLine(stderr, `[turn] Starting turn ${event.turn}`);
514
+ break;
515
+ case "llm_start":
516
+ writeLine(stderr, "[llm] Calling model");
517
+ break;
518
+ case "llm_selected":
519
+ writeLine(stderr, `[llm] Selected ${event.model.provider}:${event.model.id}`);
520
+ break;
521
+ case "llm_end":
522
+ writeLine(stderr, `[llm] Response received`);
523
+ break;
524
+ case "validation_start":
525
+ writeLine(stderr, `[validate] Running: ${event.layer}`);
526
+ break;
527
+ case "validation_pass":
528
+ writeLine(stderr, `[validate] Passed: ${event.layer}`);
529
+ break;
530
+ case "validation_error":
531
+ writeLine(stderr, `[validate] Failed: ${event.layer} - ${event.error}`);
532
+ break;
533
+ case "warning":
534
+ writeLine(stderr, `[warning] ${event.message}`);
535
+ break;
536
+ case "complete":
537
+ writeLine(stderr, `[complete] Success after ${event.turns} turn(s)`);
538
+ break;
539
+ case "error":
540
+ writeLine(stderr, `[error] ${event.error.message}`);
541
+ break;
542
+ default:
543
+ break;
544
+ }
545
+ }
546
+ function mapExtractionExitCode(error) {
547
+ if (error instanceof MaxTurnsError) {
548
+ return 1;
549
+ }
550
+ return 3;
551
+ }
552
+ function writeLine(stream, message) {
553
+ stream.write(`${message}\n`);
554
+ }
555
+ function getVersion() {
556
+ const pkgPath = new URL("../package.json", import.meta.url);
557
+ const raw = readFileSync(pkgPath, "utf8");
558
+ const pkg = JSON.parse(raw);
559
+ return pkg.version ?? "0.0.0";
560
+ }
561
+ async function readStream(stream) {
562
+ return await new Promise((resolveStream, reject) => {
563
+ let data = "";
564
+ stream.setEncoding("utf8");
565
+ stream.on("data", (chunk) => {
566
+ data += chunk;
567
+ });
568
+ stream.on("error", (error) => reject(error));
569
+ stream.on("end", () => resolveStream(data));
570
+ });
571
+ }