@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,187 @@
1
+ const PROVIDERS_WITH_SCHEMA_CONSTRAINTS = new Set([
2
+ "google-antigravity",
3
+ "google-gemini-cli",
4
+ ]);
5
+ const PROVIDERS_REQUIRING_OBJECT_SCHEMA = new Set([
6
+ "google-antigravity",
7
+ "google-gemini-cli",
8
+ "openai-codex",
9
+ ]);
10
+ const WRAPPED_VALUE_KEY = "value";
11
+ export function normalizeToolSchema(model, schema) {
12
+ let normalizedSchema = schema;
13
+ if (PROVIDERS_WITH_SCHEMA_CONSTRAINTS.has(model.provider)) {
14
+ const cloned = deepClone(schema);
15
+ const withoutRefs = resolveRefs(cloned);
16
+ const normalized = normalizeLiterals(withoutRefs);
17
+ normalizedSchema = stripUnsupportedKeywords(normalized);
18
+ }
19
+ if (!PROVIDERS_REQUIRING_OBJECT_SCHEMA.has(model.provider)) {
20
+ return { schema: normalizedSchema, unwrapKey: null };
21
+ }
22
+ return wrapNonObjectSchema(normalizedSchema);
23
+ }
24
+ function deepClone(value) {
25
+ if (typeof structuredClone === "function") {
26
+ return structuredClone(value);
27
+ }
28
+ return JSON.parse(JSON.stringify(value));
29
+ }
30
+ function resolveRefs(schema) {
31
+ if (!schema || typeof schema !== "object") {
32
+ return schema;
33
+ }
34
+ const root = schema;
35
+ const defs = root.$defs ?? root.definitions;
36
+ const seen = new Map();
37
+ const resolving = new Set();
38
+ const resolveNode = (node) => {
39
+ if (Array.isArray(node)) {
40
+ return node.map(resolveNode);
41
+ }
42
+ if (!node || typeof node !== "object") {
43
+ return node;
44
+ }
45
+ const obj = node;
46
+ const ref = typeof obj.$ref === "string" ? obj.$ref : null;
47
+ if (ref) {
48
+ const target = resolveRef(ref, defs);
49
+ if (target) {
50
+ if (seen.has(ref)) {
51
+ return seen.get(ref);
52
+ }
53
+ if (resolving.has(ref)) {
54
+ return obj;
55
+ }
56
+ resolving.add(ref);
57
+ const resolved = resolveNode(target);
58
+ resolving.delete(ref);
59
+ seen.set(ref, resolved);
60
+ return resolved;
61
+ }
62
+ }
63
+ const result = {};
64
+ for (const [key, value] of Object.entries(obj)) {
65
+ if (key === "$defs" || key === "definitions") {
66
+ continue;
67
+ }
68
+ result[key] = resolveNode(value);
69
+ }
70
+ return result;
71
+ };
72
+ return resolveNode(root);
73
+ }
74
+ function resolveRef(ref, defs) {
75
+ if (!defs) {
76
+ return null;
77
+ }
78
+ const prefix = ref.startsWith("#/$defs/") ? "#/$defs/" : ref.startsWith("#/definitions/") ? "#/definitions/" : null;
79
+ if (!prefix) {
80
+ return null;
81
+ }
82
+ const key = ref.slice(prefix.length);
83
+ return defs[key];
84
+ }
85
+ function normalizeLiterals(schema) {
86
+ if (Array.isArray(schema)) {
87
+ return schema.map(normalizeLiterals);
88
+ }
89
+ if (!schema || typeof schema !== "object") {
90
+ return schema;
91
+ }
92
+ const node = schema;
93
+ const anyOf = node.anyOf;
94
+ if (Array.isArray(anyOf) && anyOf.length > 0 && anyOf.every(isConstSchema)) {
95
+ const values = anyOf.map((item) => item.const);
96
+ const types = collectEnumTypes(anyOf);
97
+ const next = {};
98
+ for (const [key, value] of Object.entries(node)) {
99
+ if (key === "anyOf") {
100
+ continue;
101
+ }
102
+ next[key] = normalizeLiterals(value);
103
+ }
104
+ next.enum = values;
105
+ if (types.length === 1) {
106
+ next.type = types[0];
107
+ }
108
+ else if (types.length === 0) {
109
+ delete next.type;
110
+ }
111
+ return next;
112
+ }
113
+ if ("const" in node) {
114
+ const value = node.const;
115
+ const next = {};
116
+ for (const [key, val] of Object.entries(node)) {
117
+ if (key === "const") {
118
+ continue;
119
+ }
120
+ next[key] = normalizeLiterals(val);
121
+ }
122
+ next.enum = [value];
123
+ return next;
124
+ }
125
+ const result = {};
126
+ for (const [key, value] of Object.entries(node)) {
127
+ result[key] = normalizeLiterals(value);
128
+ }
129
+ return result;
130
+ }
131
+ function stripUnsupportedKeywords(schema) {
132
+ if (Array.isArray(schema)) {
133
+ return schema.map(stripUnsupportedKeywords);
134
+ }
135
+ if (!schema || typeof schema !== "object") {
136
+ return schema;
137
+ }
138
+ const node = schema;
139
+ const result = {};
140
+ for (const [key, value] of Object.entries(node)) {
141
+ if (key === "examples") {
142
+ continue;
143
+ }
144
+ result[key] = stripUnsupportedKeywords(value);
145
+ }
146
+ return result;
147
+ }
148
+ function wrapNonObjectSchema(schema) {
149
+ if (isObjectSchema(schema)) {
150
+ return { schema, unwrapKey: null };
151
+ }
152
+ const wrapped = {
153
+ type: "object",
154
+ properties: {
155
+ [WRAPPED_VALUE_KEY]: schema,
156
+ },
157
+ required: [WRAPPED_VALUE_KEY],
158
+ additionalProperties: false,
159
+ };
160
+ return { schema: wrapped, unwrapKey: WRAPPED_VALUE_KEY };
161
+ }
162
+ function isObjectSchema(schema) {
163
+ if (!schema || typeof schema !== "object") {
164
+ return false;
165
+ }
166
+ const node = schema;
167
+ if (node.type === "object") {
168
+ return true;
169
+ }
170
+ return "properties" in node;
171
+ }
172
+ function isConstSchema(node) {
173
+ return !!node && typeof node === "object" && "const" in node;
174
+ }
175
+ function collectEnumTypes(items) {
176
+ const types = new Set();
177
+ for (const item of items) {
178
+ if (!item || typeof item !== "object") {
179
+ return [];
180
+ }
181
+ const type = item.type;
182
+ if (typeof type === "string") {
183
+ types.add(type);
184
+ }
185
+ }
186
+ return Array.from(types);
187
+ }
@@ -0,0 +1,13 @@
1
+ import type { ExtractOptions } from "./types.js";
2
+ export interface LoadExtractionSetupOptions<T> {
3
+ vars?: Record<string, unknown>;
4
+ overrides?: Partial<Omit<ExtractOptions<T>, "prompt" | "schema">>;
5
+ }
6
+ export interface ExtractionSetup<T> {
7
+ path: string;
8
+ prompt: string;
9
+ frontmatter: Record<string, unknown>;
10
+ options: ExtractOptions<T>;
11
+ }
12
+ export declare function loadExtractionSetup<T>(setupPath: string, options?: LoadExtractionSetupOptions<T>): ExtractionSetup<T>;
13
+ export declare function loadExtractionSetupFromContent<T>(content: string, setupPath: string, options?: LoadExtractionSetupOptions<T>): ExtractionSetup<T>;
@@ -0,0 +1,174 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, isAbsolute, resolve } from "node:path";
3
+ import { Environment } from "minijinja-js";
4
+ import { getModels, getProviders } from "../models.js";
5
+ import { ExtractError } from "./errors.js";
6
+ import { parseFrontmatter } from "./frontmatter.js";
7
+ export function loadExtractionSetup(setupPath, options = {}) {
8
+ const resolvedPath = isAbsolute(setupPath) ? setupPath : resolve(process.cwd(), setupPath);
9
+ const raw = readFileSync(resolvedPath, "utf8");
10
+ return loadExtractionSetupFromContent(raw, resolvedPath, options);
11
+ }
12
+ export function loadExtractionSetupFromContent(content, setupPath, options = {}) {
13
+ const resolvedPath = isAbsolute(setupPath) ? setupPath : resolve(process.cwd(), setupPath);
14
+ const { frontmatter, body } = parseFrontmatter(content);
15
+ const promptSource = resolvePrompt(frontmatter, body, resolvedPath);
16
+ const schema = resolveSchema(frontmatter, resolvedPath);
17
+ const frontmatterOptions = mapFrontmatterToOptions(frontmatter, resolvedPath);
18
+ const mergedOptions = { ...frontmatterOptions, ...options.overrides, schema, prompt: promptSource };
19
+ const prompt = renderPromptIfNeeded(promptSource, frontmatter, options.vars, resolvedPath);
20
+ const finalOptions = { ...mergedOptions, prompt };
21
+ const model = finalOptions.model;
22
+ if (!model) {
23
+ throw new ExtractError(`Missing required "model" in setup file or overrides: ${resolvedPath}`);
24
+ }
25
+ const typedOptions = { ...finalOptions, model };
26
+ return {
27
+ path: resolvedPath,
28
+ prompt,
29
+ frontmatter,
30
+ options: typedOptions,
31
+ };
32
+ }
33
+ function resolvePrompt(frontmatter, body, setupPath) {
34
+ const bodyPrompt = body.trim();
35
+ if (bodyPrompt.length > 0) {
36
+ return bodyPrompt;
37
+ }
38
+ const frontmatterPrompt = frontmatter.prompt;
39
+ if (typeof frontmatterPrompt === "string" && frontmatterPrompt.trim().length > 0) {
40
+ return frontmatterPrompt.trim();
41
+ }
42
+ throw new ExtractError(`Missing prompt content in setup file: ${setupPath}`);
43
+ }
44
+ function renderPromptIfNeeded(prompt, frontmatter, vars, setupPath) {
45
+ const templateEnabled = toBoolean(frontmatter.template);
46
+ if (!templateEnabled) {
47
+ return prompt;
48
+ }
49
+ const env = new Environment();
50
+ const mergedVars = { ...resolveFrontmatterVars(frontmatter, setupPath), ...vars };
51
+ try {
52
+ return env.renderStr(prompt, mergedVars);
53
+ }
54
+ catch (error) {
55
+ const message = error instanceof Error ? error.message : String(error);
56
+ throw new ExtractError(`Template rendering failed for ${setupPath}: ${message}`);
57
+ }
58
+ }
59
+ function resolveSchema(frontmatter, setupPath) {
60
+ const schemaValue = frontmatter.schema;
61
+ if (typeof schemaValue !== "string") {
62
+ throw new ExtractError(`Missing or invalid "schema" in setup file: ${setupPath}`);
63
+ }
64
+ const trimmed = schemaValue.trim();
65
+ if (!trimmed) {
66
+ throw new ExtractError(`Missing "schema" value in setup file: ${setupPath}`);
67
+ }
68
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
69
+ try {
70
+ return JSON.parse(trimmed);
71
+ }
72
+ catch (error) {
73
+ const message = error instanceof Error ? error.message : String(error);
74
+ throw new ExtractError(`Invalid inline schema JSON in setup file ${setupPath}: ${message}`);
75
+ }
76
+ }
77
+ const schemaPath = resolve(dirname(setupPath), trimmed);
78
+ try {
79
+ const raw = readFileSync(schemaPath, "utf8");
80
+ return JSON.parse(raw);
81
+ }
82
+ catch (error) {
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ throw new ExtractError(`Failed to load schema from ${schemaPath}: ${message}`);
85
+ }
86
+ }
87
+ function mapFrontmatterToOptions(frontmatter, setupPath) {
88
+ const modelSpec = frontmatter.model;
89
+ const model = typeof modelSpec === "string" ? resolveModel(modelSpec, setupPath) : undefined;
90
+ const apiKey = typeof frontmatter.apiKey === "string" ? frontmatter.apiKey : undefined;
91
+ const maxTurns = toNumber(frontmatter.maxTurns);
92
+ const validateCommand = typeof frontmatter.validateCommand === "string" ? frontmatter.validateCommand : undefined;
93
+ const validateUrl = typeof frontmatter.validateUrl === "string" ? frontmatter.validateUrl : undefined;
94
+ const cache = normalizeCache(frontmatter.cache);
95
+ return {
96
+ model,
97
+ apiKey,
98
+ maxTurns,
99
+ validateCommand,
100
+ validateUrl,
101
+ cache,
102
+ };
103
+ }
104
+ function resolveModel(modelSpec, setupPath) {
105
+ const [provider, modelId] = modelSpec.split("/");
106
+ if (!provider || !modelId) {
107
+ throw new ExtractError(`Invalid model "${modelSpec}" in setup file ${setupPath}. Use "provider/model-id".`);
108
+ }
109
+ const providers = getProviders();
110
+ const providerId = providers.find((item) => item === provider);
111
+ if (!providerId) {
112
+ throw new ExtractError(`Unknown provider "${provider}" in setup file ${setupPath}.`);
113
+ }
114
+ const model = getModels(providerId).find((candidate) => candidate.id === modelId);
115
+ if (!model) {
116
+ throw new ExtractError(`Model "${modelSpec}" not found (from ${setupPath}).`);
117
+ }
118
+ return model;
119
+ }
120
+ function resolveFrontmatterVars(frontmatter, setupPath) {
121
+ const varsValue = frontmatter.vars;
122
+ if (!varsValue) {
123
+ return {};
124
+ }
125
+ if (typeof varsValue === "object" && !Array.isArray(varsValue)) {
126
+ return varsValue;
127
+ }
128
+ if (typeof varsValue === "string") {
129
+ const trimmed = varsValue.trim();
130
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
131
+ try {
132
+ const parsed = JSON.parse(trimmed);
133
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
134
+ return parsed;
135
+ }
136
+ }
137
+ catch (error) {
138
+ const message = error instanceof Error ? error.message : String(error);
139
+ throw new ExtractError(`Invalid vars JSON in setup file ${setupPath}: ${message}`);
140
+ }
141
+ }
142
+ }
143
+ throw new ExtractError(`Frontmatter "vars" must be an object or JSON object string in ${setupPath}.`);
144
+ }
145
+ function normalizeCache(value) {
146
+ if (typeof value === "boolean")
147
+ return value;
148
+ if (typeof value === "string") {
149
+ const bool = toBoolean(value);
150
+ return typeof bool === "boolean" ? bool : undefined;
151
+ }
152
+ return undefined;
153
+ }
154
+ function toBoolean(value) {
155
+ if (typeof value === "boolean")
156
+ return value;
157
+ if (typeof value === "string") {
158
+ const lower = value.trim().toLowerCase();
159
+ if (lower === "true")
160
+ return true;
161
+ if (lower === "false")
162
+ return false;
163
+ }
164
+ return undefined;
165
+ }
166
+ function toNumber(value) {
167
+ if (typeof value === "number" && Number.isFinite(value)) {
168
+ return value;
169
+ }
170
+ if (typeof value === "string" && /^-?\d+(\.\d+)?$/.test(value.trim())) {
171
+ return Number(value);
172
+ }
173
+ return undefined;
174
+ }
@@ -0,0 +1,143 @@
1
+ import type { Api, AssistantMessageEventStream, Context, ImageContent, Model, ThinkingBudgets as PiAiThinkingBudgets, ThinkingLevel as PiAiThinkingLevel } from "@mariozechner/pi-ai";
2
+ import type { TSchema } from "@sinclair/typebox";
3
+ import type { CacheOptions } from "./cache/types.js";
4
+ import type { ExtractEvent } from "./events.js";
5
+ export type StreamFnOptions = {
6
+ apiKey?: string;
7
+ signal?: AbortSignal;
8
+ maxTokens: number;
9
+ reasoning?: PiAiThinkingLevel;
10
+ thinkingBudgets?: ThinkingBudgets;
11
+ onModelSelected?: (model: Model<Api>) => void;
12
+ };
13
+ export type StreamFn = (model: Model<Api>, context: Context, options: StreamFnOptions) => AssistantMessageEventStream;
14
+ /**
15
+ * Thinking/reasoning level for models that support extended thinking.
16
+ * - "off": Disable thinking (default)
17
+ * - "minimal" to "xhigh": Increasing levels of thinking effort
18
+ *
19
+ * Note: "xhigh" is only supported by OpenAI gpt-5.1-codex-max, gpt-5.2, and gpt-5.2-codex models.
20
+ */
21
+ export type ThinkingLevel = "off" | PiAiThinkingLevel;
22
+ /**
23
+ * Custom token budgets for thinking levels (token-based providers only).
24
+ */
25
+ export type ThinkingBudgets = PiAiThinkingBudgets;
26
+ /**
27
+ * Options for extract() and extractSync()
28
+ */
29
+ export interface ExtractOptions<T> {
30
+ /**
31
+ * TypeBox schema defining the expected output structure.
32
+ * Converted to JSON Schema for the LLM and used by AJV for validation.
33
+ */
34
+ schema: TSchema;
35
+ /**
36
+ * System prompt instructing the LLM what to extract.
37
+ * Automatically appended with "You MUST call the 'respond' tool."
38
+ */
39
+ prompt: string;
40
+ /**
41
+ * LLM model to use. Get one via getModel("provider", "model-id").
42
+ */
43
+ model: Model<Api>;
44
+ /**
45
+ * API key for the model provider.
46
+ * @default Reads from environment (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)
47
+ */
48
+ apiKey?: string;
49
+ /**
50
+ * Image attachments for vision-capable models.
51
+ * Images are sent alongside the text input for multimodal extraction.
52
+ */
53
+ attachments?: ImageContent[];
54
+ /**
55
+ * Synchronous validation function.
56
+ * Throw an error to fail validation.
57
+ */
58
+ validate?: (data: T) => void;
59
+ /**
60
+ * Asynchronous validation function.
61
+ * Can call external APIs. Throw/reject to fail validation.
62
+ */
63
+ validateAsync?: (data: T) => Promise<void>;
64
+ /**
65
+ * Shell command for validation.
66
+ * Receives JSON on stdin. Exit 0 = pass, non-zero = fail (stderr = error).
67
+ */
68
+ validateCommand?: string;
69
+ /**
70
+ * HTTP endpoint for validation.
71
+ * POST JSON body. 2xx = pass, 4xx/5xx = fail (body = error).
72
+ */
73
+ validateUrl?: string;
74
+ /**
75
+ * Maximum turns in the extraction loop. Each turn is one LLM call.
76
+ * @default 3
77
+ */
78
+ maxTurns?: number;
79
+ /**
80
+ * Override the LLM stream function (e.g. proxy/routing).
81
+ */
82
+ streamFn?: StreamFn;
83
+ /**
84
+ * Enable response caching.
85
+ * @default false
86
+ */
87
+ cache?: boolean | CacheOptions;
88
+ /**
89
+ * Abort signal for cancellation.
90
+ */
91
+ signal?: AbortSignal;
92
+ /**
93
+ * Thinking/reasoning level for models that support extended thinking.
94
+ * Controls how much "thinking effort" the model uses before responding.
95
+ * @default "off"
96
+ */
97
+ thinking?: ThinkingLevel;
98
+ /**
99
+ * Custom token budgets for thinking levels (token-based providers only).
100
+ * Override the default budgets for specific thinking levels.
101
+ */
102
+ thinkingBudgets?: ThinkingBudgets;
103
+ }
104
+ /**
105
+ * Result of a successful extraction.
106
+ */
107
+ export interface ExtractResult<T> {
108
+ /** The extracted data, validated against the schema */
109
+ data: T;
110
+ /** Number of turns taken (1 = first turn succeeded) */
111
+ turns: number;
112
+ /** Cumulative token usage across all turns */
113
+ usage: Usage;
114
+ }
115
+ /**
116
+ * Token usage and cost information.
117
+ */
118
+ export interface Usage {
119
+ /** Input tokens (prompt + context) */
120
+ inputTokens: number;
121
+ /** Output tokens (response) */
122
+ outputTokens: number;
123
+ /** Total tokens (input + output) */
124
+ totalTokens: number;
125
+ /** Estimated cost in USD */
126
+ cost: number;
127
+ }
128
+ /**
129
+ * Async iterable stream of extraction events.
130
+ */
131
+ export interface ExtractStream<T> extends AsyncIterable<ExtractEvent<T>> {
132
+ /**
133
+ * Get the final result.
134
+ * Resolves when extraction completes.
135
+ * Returns undefined if extraction failed.
136
+ */
137
+ result(): Promise<ExtractResult<T> | undefined>;
138
+ /**
139
+ * Abort the extraction.
140
+ * Causes the stream to emit an error event.
141
+ */
142
+ abort(): void;
143
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function runAsserts(data: unknown, asserts: string[], signal?: AbortSignal): Promise<void>;
@@ -0,0 +1,18 @@
1
+ import { AssertionError } from "../errors.js";
2
+ import { runShell } from "./shell.js";
3
+ function shSingleQuote(text) {
4
+ // Wrap in single quotes, escaping any embedded single quotes:
5
+ // abc'def -> 'abc'\''def'
6
+ return `'${text.replaceAll("'", "'\\''")}'`;
7
+ }
8
+ export async function runAsserts(data, asserts, signal) {
9
+ const json = JSON.stringify(data);
10
+ for (const expr of asserts) {
11
+ const result = await runShell(`jq -e ${shSingleQuote(expr)}`, { stdin: json, signal });
12
+ if (result.code === 0) {
13
+ continue;
14
+ }
15
+ const message = result.stderr.trim() || `Assertion failed: ${expr}`;
16
+ throw new AssertionError(message, expr);
17
+ }
18
+ }
@@ -0,0 +1 @@
1
+ export declare function runCommandValidator(data: unknown, command: string, signal?: AbortSignal): Promise<void>;
@@ -0,0 +1,10 @@
1
+ import { CommandValidationError } from "../errors.js";
2
+ import { runShell } from "./shell.js";
3
+ export async function runCommandValidator(data, command, signal) {
4
+ const result = await runShell(command, { stdin: JSON.stringify(data), signal });
5
+ if (result.code === 0) {
6
+ return;
7
+ }
8
+ const message = result.stderr.trim() || `Command exited with code ${result.code}`;
9
+ throw new CommandValidationError(message, command, result.code);
10
+ }
@@ -0,0 +1 @@
1
+ export declare function runHttpValidator(data: unknown, url: string, signal?: AbortSignal): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import { HttpValidationError } from "../errors.js";
2
+ async function readErrorBody(response) {
3
+ const text = await response.text();
4
+ if (!text) {
5
+ return "";
6
+ }
7
+ try {
8
+ const json = JSON.parse(text);
9
+ const msg = typeof json.error === "string" ? json.error : typeof json.message === "string" ? json.message : null;
10
+ return msg ?? text;
11
+ }
12
+ catch {
13
+ return text;
14
+ }
15
+ }
16
+ export async function runHttpValidator(data, url, signal) {
17
+ const response = await fetch(url, {
18
+ method: "POST",
19
+ headers: { "Content-Type": "application/json" },
20
+ body: JSON.stringify(data),
21
+ signal,
22
+ });
23
+ if (response.ok) {
24
+ return;
25
+ }
26
+ const message = (await readErrorBody(response)) || `Validator returned ${response.status}`;
27
+ throw new HttpValidationError(message, url, response.status);
28
+ }
@@ -0,0 +1,22 @@
1
+ import type { ValidatorLayer } from "../events.js";
2
+ import type { ExtractOptions } from "../types.js";
3
+ export type EmitValidationEvent = (event: {
4
+ type: "validation_start";
5
+ layer: ValidatorLayer;
6
+ }) => void;
7
+ export type EmitValidationPassEvent = (event: {
8
+ type: "validation_pass";
9
+ layer: ValidatorLayer;
10
+ }) => void;
11
+ export type EmitValidationErrorEvent = (event: {
12
+ type: "validation_error";
13
+ layer: ValidatorLayer;
14
+ error: string;
15
+ }) => void;
16
+ export interface ValidationEmitter {
17
+ start(layer: ValidatorLayer): void;
18
+ pass(layer: ValidatorLayer): void;
19
+ fail(layer: ValidatorLayer, error: string): void;
20
+ }
21
+ export declare function createValidationEmitter(emit: (event: any) => void): ValidationEmitter;
22
+ export declare function runValidators<T>(data: T, options: ExtractOptions<T>, emitter: ValidationEmitter, signal?: AbortSignal): Promise<void>;
@@ -0,0 +1,55 @@
1
+ import { runCommandValidator } from "./command.js";
2
+ import { runHttpValidator } from "./http.js";
3
+ export function createValidationEmitter(emit) {
4
+ return {
5
+ start: (layer) => emit({ type: "validation_start", layer }),
6
+ pass: (layer) => emit({ type: "validation_pass", layer }),
7
+ fail: (layer, error) => emit({ type: "validation_error", layer, error }),
8
+ };
9
+ }
10
+ export async function runValidators(data, options, emitter, signal) {
11
+ if (options.validate) {
12
+ emitter.start("sync");
13
+ try {
14
+ options.validate(data);
15
+ emitter.pass("sync");
16
+ }
17
+ catch (e) {
18
+ emitter.fail("sync", e instanceof Error ? e.message : String(e));
19
+ throw e;
20
+ }
21
+ }
22
+ if (options.validateAsync) {
23
+ emitter.start("async");
24
+ try {
25
+ await options.validateAsync(data);
26
+ emitter.pass("async");
27
+ }
28
+ catch (e) {
29
+ emitter.fail("async", e instanceof Error ? e.message : String(e));
30
+ throw e;
31
+ }
32
+ }
33
+ if (options.validateCommand) {
34
+ emitter.start("command");
35
+ try {
36
+ await runCommandValidator(data, options.validateCommand, signal);
37
+ emitter.pass("command");
38
+ }
39
+ catch (e) {
40
+ emitter.fail("command", e instanceof Error ? e.message : String(e));
41
+ throw e;
42
+ }
43
+ }
44
+ if (options.validateUrl) {
45
+ emitter.start("http");
46
+ try {
47
+ await runHttpValidator(data, options.validateUrl, signal);
48
+ emitter.pass("http");
49
+ }
50
+ catch (e) {
51
+ emitter.fail("http", e instanceof Error ? e.message : String(e));
52
+ throw e;
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,9 @@
1
+ export interface ShellResult {
2
+ code: number;
3
+ stdout: string;
4
+ stderr: string;
5
+ }
6
+ export declare function runShell(command: string, options?: {
7
+ stdin?: string;
8
+ signal?: AbortSignal;
9
+ }): Promise<ShellResult>;