@oxygen-agent/cli 1.50.37 → 1.98.7

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.
@@ -3,6 +3,7 @@ import readXlsxFile from "read-excel-file/node";
3
3
  import { inferImportColumnDataType, parseDateValueToIso, } from "./column-types.js";
4
4
  import { OxygenError } from "./index.js";
5
5
  const MAX_IDENTIFIER_LENGTH = 63;
6
+ export const MAX_BUFFERED_IMPORT_PARSE_BYTES = 10 * 1024 * 1024;
6
7
  export function inferRowsFileFormat(path) {
7
8
  const extension = extname(path).toLowerCase();
8
9
  if (extension === ".jsonl" || extension === ".ndjson")
@@ -31,6 +32,20 @@ export async function parseRowsFileBuffer(buffer, format, options = {}) {
31
32
  return await parseXlsxRows(buffer, options);
32
33
  return parseRowsText(buffer.toString("utf8"), format);
33
34
  }
35
+ export async function* iterateRowsFileBufferBatches(buffer, format, options = {}) {
36
+ const batchSize = normalizeBatchSize(options.batchSize);
37
+ if (format === "csv") {
38
+ yield* iterateCsvRowBatches(buffer.toString("utf8"), batchSize);
39
+ return;
40
+ }
41
+ if (format === "jsonl") {
42
+ yield* iterateJsonlRowBatches(buffer.toString("utf8"), batchSize);
43
+ return;
44
+ }
45
+ assertBufferedParseWithinLimit(buffer, format);
46
+ const rows = await parseRowsFileBuffer(buffer, format, options.sheet ? { sheet: options.sheet } : {});
47
+ yield* chunkRows(rows, batchSize);
48
+ }
34
49
  export function parseRowsText(text, format) {
35
50
  if (format === "json")
36
51
  return normalizeRowObjects(parseJsonArray(text));
@@ -145,12 +160,13 @@ function parseJsonArray(text) {
145
160
  return parsed;
146
161
  }
147
162
  function normalizeRowObjects(rows) {
148
- return rows.map((row) => {
149
- if (!row || typeof row !== "object" || Array.isArray(row)) {
150
- throw new OxygenError("invalid_rows", "Rows must be JSON objects.", { exitCode: 1 });
151
- }
152
- return row;
153
- });
163
+ return rows.map((row) => normalizeRowObject(row));
164
+ }
165
+ function normalizeRowObject(row) {
166
+ if (!row || typeof row !== "object" || Array.isArray(row)) {
167
+ throw new OxygenError("invalid_rows", "Rows must be JSON objects.", { exitCode: 1 });
168
+ }
169
+ return row;
154
170
  }
155
171
  function parseCsvRows(text) {
156
172
  const records = parseCsvRecords(text);
@@ -166,6 +182,140 @@ function normalizeCsvImportCell(value) {
166
182
  return null;
167
183
  return value;
168
184
  }
185
+ function* iterateCsvRowBatches(text, batchSize) {
186
+ const state = {
187
+ header: null,
188
+ batch: [],
189
+ record: [],
190
+ field: "",
191
+ inQuotes: false,
192
+ };
193
+ for (let index = 0; index < text.length; index += 1) {
194
+ const result = applyCsvCharacter(state, text.charAt(index), text.charAt(index + 1));
195
+ if (result.skipNext)
196
+ index += 1;
197
+ if (!result.recordComplete)
198
+ continue;
199
+ const ready = appendCsvRecordToBatch(state, finishCsvRecord(state), batchSize);
200
+ if (ready)
201
+ yield ready;
202
+ }
203
+ if (state.field || state.record.length > 0) {
204
+ const ready = appendCsvRecordToBatch(state, finishCsvRecord(state), batchSize);
205
+ if (ready)
206
+ yield ready;
207
+ }
208
+ if (state.batch.length > 0)
209
+ yield state.batch;
210
+ }
211
+ function applyCsvCharacter(state, char, next) {
212
+ return state.inQuotes
213
+ ? applyQuotedCsvCharacter(state, char, next)
214
+ : applyUnquotedCsvCharacter(state, char);
215
+ }
216
+ function applyQuotedCsvCharacter(state, char, next) {
217
+ if (char === "\"" && next === "\"") {
218
+ state.field += "\"";
219
+ return { recordComplete: false, skipNext: true };
220
+ }
221
+ if (char === "\"") {
222
+ state.inQuotes = false;
223
+ }
224
+ else {
225
+ state.field += char;
226
+ }
227
+ return { recordComplete: false, skipNext: false };
228
+ }
229
+ function applyUnquotedCsvCharacter(state, char) {
230
+ if (char === "\"") {
231
+ state.inQuotes = true;
232
+ return { recordComplete: false, skipNext: false };
233
+ }
234
+ if (char === ",") {
235
+ pushCsvField(state);
236
+ return { recordComplete: false, skipNext: false };
237
+ }
238
+ if (char === "\n") {
239
+ return { recordComplete: true, skipNext: false };
240
+ }
241
+ if (char !== "\r")
242
+ state.field += char;
243
+ return { recordComplete: false, skipNext: false };
244
+ }
245
+ function pushCsvField(state) {
246
+ state.record.push(state.field);
247
+ state.field = "";
248
+ }
249
+ function finishCsvRecord(state) {
250
+ pushCsvField(state);
251
+ const record = state.record;
252
+ state.record = [];
253
+ state.field = "";
254
+ return record;
255
+ }
256
+ function appendCsvRecordToBatch(state, record, batchSize) {
257
+ const row = csvRecordToRow(record, state.header);
258
+ if (state.header === null)
259
+ state.header = record;
260
+ if (row)
261
+ state.batch.push(row);
262
+ if (state.batch.length < batchSize)
263
+ return null;
264
+ const ready = state.batch;
265
+ state.batch = [];
266
+ return ready;
267
+ }
268
+ function csvRecordToRow(record, header) {
269
+ if (header === null)
270
+ return null;
271
+ if (!record.some((cell) => cell.trim()))
272
+ return null;
273
+ return Object.fromEntries(header.map((key, index) => [key, normalizeCsvImportCell(record[index])]));
274
+ }
275
+ function* iterateJsonlRowBatches(text, batchSize) {
276
+ let batch = [];
277
+ let lineStart = 0;
278
+ for (let index = 0; index < text.length; index += 1) {
279
+ if (text.charAt(index) !== "\n")
280
+ continue;
281
+ const line = text.slice(lineStart, index).replace(/\r$/, "").trim();
282
+ lineStart = index + 1;
283
+ if (!line)
284
+ continue;
285
+ batch.push(normalizeRowObject(JSON.parse(line)));
286
+ if (batch.length >= batchSize) {
287
+ yield batch;
288
+ batch = [];
289
+ }
290
+ }
291
+ const trailingLine = text.slice(lineStart).replace(/\r$/, "").trim();
292
+ if (trailingLine)
293
+ batch.push(normalizeRowObject(JSON.parse(trailingLine)));
294
+ if (batch.length > 0)
295
+ yield batch;
296
+ }
297
+ function* chunkRows(rows, batchSize) {
298
+ for (let index = 0; index < rows.length; index += batchSize) {
299
+ yield rows.slice(index, index + batchSize);
300
+ }
301
+ }
302
+ function normalizeBatchSize(value) {
303
+ if (!Number.isFinite(value) || value === undefined)
304
+ return 1000;
305
+ return Math.max(1, Math.trunc(value));
306
+ }
307
+ function assertBufferedParseWithinLimit(buffer, format) {
308
+ if (buffer.byteLength <= MAX_BUFFERED_IMPORT_PARSE_BYTES)
309
+ return;
310
+ throw new OxygenError("buffered_import_too_large", "Large JSON array and XLSX imports require buffered parsing; use CSV or JSONL for large staged imports.", {
311
+ details: {
312
+ format,
313
+ file_bytes: buffer.byteLength,
314
+ max_buffered_parse_bytes: MAX_BUFFERED_IMPORT_PARSE_BYTES,
315
+ },
316
+ exitCode: 1,
317
+ });
318
+ }
169
319
  function parseCsvRecords(text) {
170
320
  const records = [];
171
321
  let record = [];
@@ -4,6 +4,7 @@ export * from "./cell-format.js";
4
4
  export * from "./column-types.js";
5
5
  export * from "./credit-guidance.js";
6
6
  export * from "./log.js";
7
+ export * from "./provider-request-outcomes.js";
7
8
  export * from "./telemetry.js";
8
9
  export type JsonValue = string | number | boolean | null | JsonValue[] | {
9
10
  [key: string]: JsonValue;
@@ -5,6 +5,7 @@ export * from "./cell-format.js";
5
5
  export * from "./column-types.js";
6
6
  export * from "./credit-guidance.js";
7
7
  export * from "./log.js";
8
+ export * from "./provider-request-outcomes.js";
8
9
  export * from "./telemetry.js";
9
10
  export class OxygenError extends Error {
10
11
  code;
@@ -0,0 +1,26 @@
1
+ export declare function isObjectStorageConfigured(): boolean;
2
+ export declare function buildImportObjectKey(input: {
3
+ organizationId: string;
4
+ fileName?: string | null;
5
+ }): string;
6
+ export declare function isImportObjectKeyForOrganization(key: string, organizationId: string): boolean;
7
+ export type PresignedImportUpload = {
8
+ uploadUrl: string;
9
+ bucket: string;
10
+ storageKey: string;
11
+ contentLength: number;
12
+ provider: "s3";
13
+ expiresInSeconds: number;
14
+ };
15
+ export declare function presignImportUpload(input: {
16
+ organizationId: string;
17
+ fileName?: string | null;
18
+ contentType?: string | null;
19
+ contentLength: number;
20
+ }): Promise<PresignedImportUpload>;
21
+ export declare function downloadImportObject(input: {
22
+ storageKey: string;
23
+ }): Promise<Buffer>;
24
+ export declare function deleteImportObject(input: {
25
+ storageKey: string;
26
+ }): Promise<void>;
@@ -0,0 +1,115 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client, } from "@aws-sdk/client-s3";
3
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
4
+ import { OxygenError } from "./index.js";
5
+ // S3-compatible object storage for large CSV/file imports. The CLI uploads the
6
+ // raw file straight to the bucket via a presigned PUT URL (bypassing Vercel's
7
+ // ~4.5MB request-body limit), then the Fly worker downloads it and COPY-loads
8
+ // it. Configured against Hetzner Object Storage (S3-compatible) via the
9
+ // OXYGEN_IMPORT_S3_* env vars; works with any S3-compatible endpoint.
10
+ const PRESIGN_EXPIRY_SECONDS = 900;
11
+ function readImportStorageConfig() {
12
+ const endpoint = process.env.OXYGEN_IMPORT_S3_ENDPOINT?.trim();
13
+ const bucket = process.env.OXYGEN_IMPORT_S3_BUCKET?.trim();
14
+ const accessKeyId = process.env.OXYGEN_IMPORT_S3_ACCESS_KEY_ID?.trim();
15
+ const secretAccessKey = process.env.OXYGEN_IMPORT_S3_SECRET_ACCESS_KEY?.trim();
16
+ if (!endpoint || !bucket || !accessKeyId || !secretAccessKey)
17
+ return null;
18
+ // Hetzner uses a location code (fsn1/nbg1/hel1) as its region; the S3 SDK
19
+ // only needs a non-empty string, so default to "auto" when unset.
20
+ const region = process.env.OXYGEN_IMPORT_S3_REGION?.trim() || "auto";
21
+ // Path-style addressing avoids bucket-in-hostname DNS/TLS edge cases on
22
+ // S3-compatible providers; default on, opt out with "false".
23
+ const forcePathStyle = process.env.OXYGEN_IMPORT_S3_FORCE_PATH_STYLE?.trim() !== "false";
24
+ return { endpoint, region, bucket, accessKeyId, secretAccessKey, forcePathStyle };
25
+ }
26
+ export function isObjectStorageConfigured() {
27
+ return readImportStorageConfig() !== null;
28
+ }
29
+ let cachedClient = null;
30
+ function resolveClient() {
31
+ const config = readImportStorageConfig();
32
+ if (!config) {
33
+ throw new OxygenError("object_storage_not_configured", "Import object storage (OXYGEN_IMPORT_S3_*) is not configured.", { exitCode: 1 });
34
+ }
35
+ const cacheKey = `${config.endpoint}|${config.region}|${config.accessKeyId}|${config.forcePathStyle}`;
36
+ if (!cachedClient || cachedClient.key !== cacheKey) {
37
+ cachedClient = {
38
+ key: cacheKey,
39
+ client: new S3Client({
40
+ endpoint: config.endpoint,
41
+ region: config.region,
42
+ forcePathStyle: config.forcePathStyle,
43
+ credentials: {
44
+ accessKeyId: config.accessKeyId,
45
+ secretAccessKey: config.secretAccessKey,
46
+ },
47
+ }),
48
+ };
49
+ }
50
+ return { client: cachedClient.client, config };
51
+ }
52
+ // Keys are namespaced by org so a tenant can only ever be handed (and the
53
+ // enqueue route only accepts) keys under its own prefix.
54
+ export function buildImportObjectKey(input) {
55
+ const safeName = sanitizeFileName(input.fileName) || "import";
56
+ return `imports/${input.organizationId}/${randomUUID()}/${safeName}`;
57
+ }
58
+ export function isImportObjectKeyForOrganization(key, organizationId) {
59
+ return key.startsWith(`imports/${organizationId}/`);
60
+ }
61
+ export async function presignImportUpload(input) {
62
+ const { client, config } = resolveClient();
63
+ const storageKey = buildImportObjectKey({
64
+ organizationId: input.organizationId,
65
+ fileName: input.fileName ?? null,
66
+ });
67
+ const command = new PutObjectCommand({
68
+ Bucket: config.bucket,
69
+ Key: storageKey,
70
+ ContentLength: input.contentLength,
71
+ ...(input.contentType ? { ContentType: input.contentType } : {}),
72
+ });
73
+ const uploadUrl = await getSignedUrl(client, command, { expiresIn: PRESIGN_EXPIRY_SECONDS });
74
+ return {
75
+ uploadUrl,
76
+ bucket: config.bucket,
77
+ storageKey,
78
+ contentLength: input.contentLength,
79
+ provider: "s3",
80
+ expiresInSeconds: PRESIGN_EXPIRY_SECONDS,
81
+ };
82
+ }
83
+ export async function downloadImportObject(input) {
84
+ const { client, config } = resolveClient();
85
+ const result = await client.send(new GetObjectCommand({ Bucket: config.bucket, Key: input.storageKey }));
86
+ const body = result.Body;
87
+ if (!body) {
88
+ throw new OxygenError("import_object_missing", "Import object had no body.", {
89
+ details: { storage_key: input.storageKey },
90
+ exitCode: 1,
91
+ });
92
+ }
93
+ const transform = body.transformToByteArray;
94
+ if (typeof transform === "function") {
95
+ return Buffer.from(await transform.call(body));
96
+ }
97
+ return streamToBuffer(body);
98
+ }
99
+ export async function deleteImportObject(input) {
100
+ const { client, config } = resolveClient();
101
+ await client.send(new DeleteObjectCommand({ Bucket: config.bucket, Key: input.storageKey }));
102
+ }
103
+ function sanitizeFileName(fileName) {
104
+ if (!fileName)
105
+ return "";
106
+ const base = fileName.split(/[\\/]/).pop() ?? "";
107
+ return base.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 120);
108
+ }
109
+ async function streamToBuffer(stream) {
110
+ const chunks = [];
111
+ for await (const chunk of stream) {
112
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk));
113
+ }
114
+ return Buffer.concat(chunks);
115
+ }
@@ -0,0 +1,3 @@
1
+ export declare const PROVIDER_REQUEST_OUTCOMES: readonly ["success", "error", "blocked"];
2
+ export type ProviderRequestOutcome = (typeof PROVIDER_REQUEST_OUTCOMES)[number];
3
+ export declare function isProviderRequestOutcome(value: unknown): value is ProviderRequestOutcome;
@@ -0,0 +1,5 @@
1
+ export const PROVIDER_REQUEST_OUTCOMES = ["success", "error", "blocked"];
2
+ const PROVIDER_REQUEST_OUTCOME_SET = new Set(PROVIDER_REQUEST_OUTCOMES);
3
+ export function isProviderRequestOutcome(value) {
4
+ return typeof value === "string" && PROVIDER_REQUEST_OUTCOME_SET.has(value);
5
+ }
@@ -1 +1 @@
1
- export declare const OXYGEN_VERSION = "1.50.37";
1
+ export declare const OXYGEN_VERSION = "1.98.7";
@@ -1 +1 @@
1
- export const OXYGEN_VERSION = "1.50.37";
1
+ export const OXYGEN_VERSION = "1.98.7";
@@ -1,7 +1,10 @@
1
1
  export declare const WORKFLOW_MANIFEST_VERSION = 1;
2
2
  export declare const WORKFLOW_COMPILER_VERSION = "oxygen-workflows-v1";
3
3
  export declare const DURABLE_RECIPE_COMPILER_VERSION = "oxygen-recipes-v2";
4
+ export declare const BLUEPRINT_VERSION = 1;
5
+ export declare const BLUEPRINT_COMPILER_VERSION = "oxygen-blueprints-v1";
4
6
  export declare const MAX_RECIPE_BUNDLE_BYTES = 2000000;
7
+ export declare const MAX_BLUEPRINT_BYTES = 4000000;
5
8
  export declare const DEFAULT_WORKFLOW_CRON_TIMEZONE = "UTC";
6
9
  export type WorkflowMode = "dry_run" | "live" | "smoke_test";
7
10
  export type WorkflowTriggerType = "api" | "webhook" | "cron" | "event";
@@ -100,6 +103,72 @@ export type RecipeManifest = {
100
103
  created_at: string;
101
104
  };
102
105
  export type AnyWorkflowManifest = WorkflowManifest | RecipeManifest;
106
+ export type BlueprintColumnInput = {
107
+ key?: string;
108
+ label: string;
109
+ dataType?: string;
110
+ kind?: string;
111
+ semanticType?: string | null;
112
+ definition?: Record<string, unknown>;
113
+ };
114
+ export type BlueprintTable = {
115
+ ref: string;
116
+ name: string;
117
+ description?: string;
118
+ columns: BlueprintColumnInput[];
119
+ metadata?: Record<string, unknown>;
120
+ };
121
+ export type BlueprintColumnGraft = {
122
+ table_ref: string;
123
+ column: BlueprintColumnInput;
124
+ };
125
+ export type BlueprintPromptKind = "ai_column_system" | "scoring_rubric" | "other";
126
+ export type BlueprintPromptTemplate = {
127
+ slug: string;
128
+ name: string;
129
+ description?: string | null;
130
+ kind: BlueprintPromptKind;
131
+ body: string;
132
+ };
133
+ export type BlueprintIntegrationRequirement = {
134
+ kind: string;
135
+ purpose?: string;
136
+ required?: boolean;
137
+ };
138
+ export type BlueprintByokRequirement = {
139
+ kind: "openai" | "anthropic" | "google" | "openrouter" | string;
140
+ required?: boolean;
141
+ };
142
+ export type BlueprintRequires = {
143
+ integrations?: BlueprintIntegrationRequirement[];
144
+ context_keys?: string[];
145
+ byok?: BlueprintByokRequirement[];
146
+ };
147
+ export type BlueprintWorkflow = {
148
+ manifest: AnyWorkflowManifest;
149
+ table_refs?: Record<string, string>;
150
+ prompt_template_slugs?: string[];
151
+ };
152
+ export type Blueprint = {
153
+ blueprint_version: typeof BLUEPRINT_VERSION;
154
+ id: string;
155
+ name: string;
156
+ summary: string;
157
+ tags: string[];
158
+ audience?: string[];
159
+ requires: BlueprintRequires;
160
+ input_schema?: JsonSchema;
161
+ tables: BlueprintTable[];
162
+ column_grafts?: BlueprintColumnGraft[];
163
+ prompt_templates: BlueprintPromptTemplate[];
164
+ workflows: BlueprintWorkflow[];
165
+ exported_at: string;
166
+ exported_from?: {
167
+ oxygen_version?: string;
168
+ };
169
+ source_hash: string;
170
+ compiler_version: typeof BLUEPRINT_COMPILER_VERSION;
171
+ };
103
172
  export type WorkflowApplyInput = {
104
173
  manifest: WorkflowManifest;
105
174
  };
@@ -229,6 +298,28 @@ export declare function lintRecipeManifest(// skipcq: JS-R1005
229
298
  value: unknown, options?: WorkflowManifestValidationOptions): WorkflowLintResult;
230
299
  export declare function assertRecipeManifest(value: unknown, options?: WorkflowManifestValidationOptions): asserts value is RecipeManifest;
231
300
  export declare function assertWorkflowManifest(value: unknown, options?: WorkflowManifestValidationOptions): asserts value is WorkflowManifest;
301
+ export declare function isBlueprint(value: unknown): value is Blueprint;
302
+ export declare function lintBlueprint(// skipcq: JS-R1005
303
+ value: unknown, options?: WorkflowManifestValidationOptions): WorkflowLintResult;
304
+ export declare function assertBlueprint(value: unknown, options?: WorkflowManifestValidationOptions): asserts value is Blueprint;
305
+ export declare function buildBlueprint(input: {
306
+ id: string;
307
+ name: string;
308
+ summary?: string;
309
+ tags?: string[];
310
+ audience?: string[];
311
+ requires?: BlueprintRequires;
312
+ inputSchema?: JsonSchema;
313
+ tables: BlueprintTable[];
314
+ columnGrafts?: BlueprintColumnGraft[];
315
+ promptTemplates?: BlueprintPromptTemplate[];
316
+ workflows: BlueprintWorkflow[];
317
+ exportedFrom?: {
318
+ oxygen_version?: string;
319
+ };
320
+ exportedAt?: Date;
321
+ sourceHash?: string;
322
+ }): Blueprint;
232
323
  export declare function validateJsonSchemaValue(value: unknown, schema: JsonSchema | undefined, path?: string): JsonSchemaValidationIssue[];
233
324
  export declare function assertRecipeBundleSafe(bundle: string): void;
234
325
  export declare function lintRecipeBundleSafety(bundle: string, path?: string): WorkflowLintIssue[];