@oxygen-agent/cli 1.123.1 → 1.128.3

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.
package/README.md CHANGED
@@ -34,4 +34,4 @@ oxygen update
34
34
 
35
35
  For product documentation and support, visit https://oxygen-agent.com.
36
36
 
37
- Version: 1.123.1
37
+ Version: 1.128.3
package/dist/index.js CHANGED
@@ -212,6 +212,12 @@ export function createProgram() {
212
212
  .option("--json", "Print a JSON envelope.")
213
213
  .action(async (options) => {
214
214
  await handleAuthUseTokenAction(options);
215
+ }))
216
+ .addCommand(new Command("doctor")
217
+ .description("Diagnose the active Oxygen CLI credentials without printing token material.")
218
+ .option("--json", "Print a JSON envelope.")
219
+ .action(async (options) => {
220
+ await handleAuthDoctorAction(options);
215
221
  }));
216
222
  program
217
223
  .command("profiles")
@@ -434,6 +440,41 @@ export function createProgram() {
434
440
  },
435
441
  });
436
442
  });
443
+ }))
444
+ .addCommand(new Command("repair-preview")
445
+ .description("Preview tenant database repair issues without mutating production state (staff only).")
446
+ .option("--limit <n>", "Maximum tenant and provisioning issues to inspect. Defaults to 100; hard cap is 500.")
447
+ .option("--json", "Print a JSON envelope.")
448
+ .action(async (options) => {
449
+ await handleAsyncAction("db repair-preview", options, () => {
450
+ const limit = readPositiveInt(options.limit);
451
+ const query = new URLSearchParams();
452
+ if (limit !== undefined)
453
+ query.set("limit", String(limit));
454
+ const suffix = query.toString() ? `?${query.toString()}` : "";
455
+ return requestOxygen(`/api/cli/db/repair-preview${suffix}`);
456
+ });
457
+ }))
458
+ .addCommand(new Command("reconcile")
459
+ .description("Inspect tenant database registry drift. Staff only; defaults to dry-run.")
460
+ .option("--dry-run", "Report drift without applying repairs. This is the default.")
461
+ .option("--apply", "Apply safe registry repairs. Requires --confirm.")
462
+ .option("--confirm", "Confirm --apply for safe registry repairs.")
463
+ .option("--json", "Print a JSON envelope.")
464
+ .action(async (options) => {
465
+ await handleAsyncAction("db reconcile", options, () => {
466
+ if (options.apply && !options.confirm) {
467
+ throw new OxygenError("confirmation_required", "Refusing to apply tenant database reconciliation without --confirm.", { exitCode: 1 });
468
+ }
469
+ return requestOxygen("/api/cli/db/reconcile", {
470
+ method: "POST",
471
+ body: {
472
+ apply: Boolean(options.apply),
473
+ dry_run: !options.apply,
474
+ confirm: Boolean(options.confirm),
475
+ },
476
+ });
477
+ });
437
478
  }))
438
479
  .addCommand(new Command("cost-policy")
439
480
  .description("Show tenant database cost controls and reconciliation status.")
@@ -2014,7 +2055,7 @@ export function createProgram() {
2014
2055
  .description("Compile a company-search prompt into ordered provider routes without provider calls.")
2015
2056
  .requiredOption("--prompt <text-or-file>", "Company-search prompt, or a path to a prompt file.")
2016
2057
  .option("--target-count <n>", "Desired company count for routing and estimates.")
2017
- .option("--source-intent <intent>", "Override detected intent: structured, technology, hiring, local, known_source, web, or fallback.")
2058
+ .option("--source-intent <intent>", "Override detected intent: sizing, structured, technology, hiring, local, known_source, concept, web, url, or fallback.")
2018
2059
  .option("--materialize-preview", "Create a preview table with route rows.")
2019
2060
  .option("--json", "Print a JSON envelope.")
2020
2061
  .action(async (options) => {
@@ -2035,6 +2076,7 @@ export function createProgram() {
2035
2076
  .option("--max-credits <n>", "Required credit ceiling for live runs.")
2036
2077
  .option("--target-count <n>", "Desired company count when planning from --prompt.")
2037
2078
  .option("--source-intent <intent>", "Override detected intent when planning from --prompt.")
2079
+ .option("--preflight-complete", "Confirm required descriptor, count, enum, or provider-specific preflight checks before live mode.")
2038
2080
  .option("--approved", "Required for live runs after inspecting dry-run output.")
2039
2081
  .option("--json", "Print a JSON envelope.")
2040
2082
  .action(async (options) => {
@@ -2093,7 +2135,7 @@ export function createProgram() {
2093
2135
  }))
2094
2136
  .addCommand(new Command("failures")
2095
2137
  .description("List failed background action and ingestion items.")
2096
- .option("--queue <queue>", "all, actions, ingestions, or bullmq. Defaults to all.")
2138
+ .option("--queue <queue>", "all, actions, ingestions, or postgres_jobs. Defaults to all. Legacy aliases: bullmq, redis, jobs.")
2097
2139
  .option("--limit <n>", "Maximum failed items per queue. Defaults to 25; server cap is 100.")
2098
2140
  .option("--json", "Print a JSON envelope.")
2099
2141
  .action(async (options) => {
@@ -2118,13 +2160,13 @@ export function createProgram() {
2118
2160
  }));
2119
2161
  }))
2120
2162
  .addCommand(new Command("run-once")
2121
- .description("Compatibility alias for worker repair; enqueues BullMQ repair work for the current organization.")
2122
- .option("--claim-limit <n>", "Compatibility option; ignored by the BullMQ worker.")
2123
- .option("--concurrency <n>", "Compatibility option; ignored by the BullMQ worker.")
2124
- .option("--enrichment-concurrency <n>", "Compatibility option; ignored by the BullMQ worker.")
2125
- .option("--lease-seconds <n>", "Compatibility option; ignored by the BullMQ worker.")
2126
- .option("--provider-timeout-ms <n>", "Compatibility option; ignored by the BullMQ worker.")
2127
- .option("--recipe-timeout-ms <n>", "Compatibility option; ignored by the BullMQ worker.")
2163
+ .description("Compatibility alias for worker repair; repairs Postgres-backed worker state for the current organization.")
2164
+ .option("--claim-limit <n>", "Compatibility option; ignored by the Postgres worker.")
2165
+ .option("--concurrency <n>", "Compatibility option; ignored by the Postgres worker.")
2166
+ .option("--enrichment-concurrency <n>", "Compatibility option; ignored by the Postgres worker.")
2167
+ .option("--lease-seconds <n>", "Compatibility option; ignored by the Postgres worker.")
2168
+ .option("--provider-timeout-ms <n>", "Compatibility option; ignored by the Postgres worker.")
2169
+ .option("--recipe-timeout-ms <n>", "Compatibility option; ignored by the Postgres worker.")
2128
2170
  .option("--json", "Print a JSON envelope.")
2129
2171
  .action(async (options) => {
2130
2172
  const claimLimit = readPositiveInt(options.claimLimit);
@@ -2215,6 +2257,46 @@ export function createProgram() {
2215
2257
  return requestOxygen(`/api/cli/admin/costs${suffix}`);
2216
2258
  });
2217
2259
  }));
2260
+ program
2261
+ .command("signup-leads")
2262
+ .description("Signup lead delivery queue commands.")
2263
+ .addCommand(new Command("deliveries")
2264
+ .description("Inspect and retry durable signup lead webhook deliveries.")
2265
+ .addCommand(new Command("list")
2266
+ .description("List signup lead webhook deliveries for the current organization.")
2267
+ .option("--status <status>", "Filter by pending, running, succeeded, retrying, failed, or dead.")
2268
+ .option("--limit <n>", "Maximum deliveries to return. Defaults to 50.")
2269
+ .option("--json", "Print a JSON envelope.")
2270
+ .action(async (options) => {
2271
+ await handleAsyncAction("signup-leads deliveries list", options, () => {
2272
+ const params = new URLSearchParams();
2273
+ const status = readOption(options.status);
2274
+ const limit = readPositiveInt(options.limit);
2275
+ if (status)
2276
+ params.set("status", status);
2277
+ if (limit)
2278
+ params.set("limit", String(limit));
2279
+ const suffix = params.toString() ? `?${params.toString()}` : "";
2280
+ return requestOxygen(`/api/cli/signup-leads/deliveries${suffix}`);
2281
+ });
2282
+ }))
2283
+ .addCommand(new Command("get")
2284
+ .description("Get one signup lead webhook delivery by delivery id or event id.")
2285
+ .argument("<id>", "Delivery id or event id.")
2286
+ .option("--json", "Print a JSON envelope.")
2287
+ .action(async (id, options) => {
2288
+ await handleAsyncAction("signup-leads deliveries get", options, () => requestOxygen(`/api/cli/signup-leads/deliveries?id=${encodeURIComponent(id)}`));
2289
+ }))
2290
+ .addCommand(new Command("retry")
2291
+ .description("Retry one failed signup lead webhook delivery.")
2292
+ .argument("<id>", "Delivery id or event id.")
2293
+ .option("--json", "Print a JSON envelope.")
2294
+ .action(async (id, options) => {
2295
+ await handleAsyncAction("signup-leads deliveries retry", options, () => requestOxygen("/api/cli/signup-leads/deliveries", {
2296
+ method: "POST",
2297
+ body: { id },
2298
+ }));
2299
+ })));
2218
2300
  program
2219
2301
  .command("observability")
2220
2302
  .description("Redacted operation event commands for the current organization.")
@@ -2549,10 +2631,10 @@ export function createProgram() {
2549
2631
  .description("Preflight an enrichment column without provider calls or credit usage.")
2550
2632
  .argument("<table>", "Table id or slug.")
2551
2633
  .option("--source-column <column>", "Source column key or id. For mobile_phone this is the LinkedIn URL column.")
2552
- .option("--full-name-column <column>", "Column key or id containing the person's full name for work_email. Pair with company-domain/name for Prospeo, LeadMagic, Hunter, ContactOut, or BetterContact.")
2634
+ .option("--full-name-column <column>", "Column key or id containing the person's full name for work_email. Pair with company-domain/name for Prospeo, RocketReach, LeadMagic, Hunter, Dropleads, ContactOut, BetterContact, or People Data Labs.")
2553
2635
  .option("--first-name-column <column>", "Column key or id containing the person's first name for work_email.")
2554
2636
  .option("--last-name-column <column>", "Column key or id containing the person's last name for work_email.")
2555
- .option("--linkedin-url-column <column>", "Column key or id containing the person's LinkedIn URL. Required by Blitz API and enough for Prospeo, Hunter, ContactOut, or BetterContact.")
2637
+ .option("--linkedin-url-column <column>", "Column key or id containing the person's LinkedIn URL. Required by BlitzAPI and enough for RocketReach, Prospeo, Hunter, ContactOut, BetterContact, Crustdata, or People Data Labs.")
2556
2638
  .option("--email-column <column>", "Column key or id containing a known work email for providers that accept email as an identity fallback.")
2557
2639
  .option("--company-domain-column <column>", "Column key or id containing the company domain for work_email. Pair with full-name/first+last for name+company providers.")
2558
2640
  .option("--company-name-column <column>", "Column key or id containing the company name for work_email when no domain is available.")
@@ -2560,8 +2642,8 @@ export function createProgram() {
2560
2642
  .option("--capability <capability>", "Capability to enrich: mobile_phone or work_email. Defaults to mobile_phone.")
2561
2643
  .option("--target-column <column>", "Target enrichment column key. Defaults to the capability payload column.")
2562
2644
  .option("--on-existing-manual-column <mode>", "How to handle an existing manual target: error, write_if_empty, or create_enrichment_column.")
2563
- .option("--provider-order <providers>", "Comma-separated provider order. Overrides the default waterfall profile.")
2564
- .option("--email-waterfall-profile <profile>", "Work-email waterfall profile: auto, name_domain, linkedin_url, or first_last_domain.")
2645
+ .option("--provider-order <providers>", "Comma-separated provider order. Overrides the default cost-aware waterfall profile.")
2646
+ .option("--email-waterfall-profile <profile>", "Work-email waterfall profile: auto, name_domain, linkedin_url, or first_last_domain. Defaults prefer BlitzAPI/RocketReach/Prospeo where inputs fit.")
2565
2647
  .option("--email-pattern-validation <mode>", "Work-email pattern pre-step: leadmagic_valid_only or disabled.")
2566
2648
  .option("--verify-phone", "Validate found phone numbers with ClearoutPhone before returning them.")
2567
2649
  .option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
@@ -2581,10 +2663,10 @@ export function createProgram() {
2581
2663
  .description("Create or reuse an enrichment column and queue a background run.")
2582
2664
  .argument("<table>", "Table id or slug.")
2583
2665
  .option("--source-column <column>", "Source column key or id. For mobile_phone this is the LinkedIn URL column.")
2584
- .option("--full-name-column <column>", "Column key or id containing the person's full name for work_email. Pair with company-domain/name for Prospeo, LeadMagic, Hunter, ContactOut, or BetterContact.")
2666
+ .option("--full-name-column <column>", "Column key or id containing the person's full name for work_email. Pair with company-domain/name for Prospeo, RocketReach, LeadMagic, Hunter, Dropleads, ContactOut, BetterContact, or People Data Labs.")
2585
2667
  .option("--first-name-column <column>", "Column key or id containing the person's first name for work_email.")
2586
2668
  .option("--last-name-column <column>", "Column key or id containing the person's last name for work_email.")
2587
- .option("--linkedin-url-column <column>", "Column key or id containing the person's LinkedIn URL. Required by Blitz API and enough for Prospeo, Hunter, ContactOut, or BetterContact.")
2669
+ .option("--linkedin-url-column <column>", "Column key or id containing the person's LinkedIn URL. Required by BlitzAPI and enough for RocketReach, Prospeo, Hunter, ContactOut, BetterContact, Crustdata, or People Data Labs.")
2588
2670
  .option("--email-column <column>", "Column key or id containing a known work email for providers that accept email as an identity fallback.")
2589
2671
  .option("--company-domain-column <column>", "Column key or id containing the company domain for work_email. Pair with full-name/first+last for name+company providers.")
2590
2672
  .option("--company-name-column <column>", "Column key or id containing the company name for work_email when no domain is available.")
@@ -2593,8 +2675,8 @@ export function createProgram() {
2593
2675
  .option("--capability <capability>", "Capability to enrich: mobile_phone or work_email. Defaults to mobile_phone.")
2594
2676
  .option("--target-column <column>", "Target enrichment column key. Defaults to the capability payload column.")
2595
2677
  .option("--on-existing-manual-column <mode>", "How to handle an existing manual target: error, write_if_empty, or create_enrichment_column.")
2596
- .option("--provider-order <providers>", "Comma-separated provider order. Overrides the default waterfall profile.")
2597
- .option("--email-waterfall-profile <profile>", "Work-email waterfall profile: auto, name_domain, linkedin_url, or first_last_domain.")
2678
+ .option("--provider-order <providers>", "Comma-separated provider order. Overrides the default cost-aware waterfall profile.")
2679
+ .option("--email-waterfall-profile <profile>", "Work-email waterfall profile: auto, name_domain, linkedin_url, or first_last_domain. Defaults prefer BlitzAPI/RocketReach/Prospeo where inputs fit.")
2598
2680
  .option("--email-pattern-validation <mode>", "Work-email pattern pre-step: leadmagic_valid_only or disabled.")
2599
2681
  .option("--verify-phone", "Validate found phone numbers with ClearoutPhone before returning them.")
2600
2682
  .option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
@@ -2748,8 +2830,8 @@ export function createProgram() {
2748
2830
  }))
2749
2831
  .addCommand(new Command("connect")
2750
2832
  .description("Connect a Composio integration. OAuth toolkits return a redirect URL; API-key toolkits accept --api-key.")
2751
- .argument("<integration_id>", "Integration id, such as 'slack' or 'exa'.")
2752
- .option("--api-key <value>", "API key for Composio API-key toolkits (e.g. Exa, SerpAPI, Resend).")
2833
+ .argument("<integration_id>", "Integration id, such as 'slack' or 'serpapi'.")
2834
+ .option("--api-key <value>", "API key for Composio API-key toolkits (e.g. SerpAPI, Resend).")
2753
2835
  .option("--json", "Print a JSON envelope.")
2754
2836
  .action(async (integrationId, options) => {
2755
2837
  await handleAsyncAction("integrations connect", options, async () => {
@@ -3454,7 +3536,7 @@ runId, options) {
3454
3536
  ...(queuedWithoutWorker
3455
3537
  ? {
3456
3538
  worker_status: "queued_no_worker",
3457
- guidance: "No worker has claimed this workflow run. Check `oxygen worker queue-stats --json` for BullMQ and workflow queue health.",
3539
+ guidance: "No worker has claimed this workflow run. Check `oxygen worker queue-stats --json` for Postgres-backed worker queue health.",
3458
3540
  }
3459
3541
  : {}),
3460
3542
  },
@@ -3652,6 +3734,7 @@ function readCompaniesSearchRunBody(options) {
3652
3734
  ...(targetCount !== undefined ? { target_count: targetCount } : {}),
3653
3735
  ...(options.sourceIntent ? { source_intent: options.sourceIntent } : {}),
3654
3736
  ...(options.approved ? { approved: true } : {}),
3737
+ ...(options.preflightComplete ? { preflight_complete: true } : {}),
3655
3738
  };
3656
3739
  }
3657
3740
  function readCompanySearchPlanJson(value) {
@@ -4870,6 +4953,80 @@ async function handleAuthUseTokenAction(options) {
4870
4953
  process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
4871
4954
  }
4872
4955
  }
4956
+ async function handleAuthDoctorAction(options) {
4957
+ try {
4958
+ const data = await runAuthDoctor();
4959
+ if (options.json) {
4960
+ writeJson(success("auth doctor", data));
4961
+ return;
4962
+ }
4963
+ process.stdout.write(formatAuthDoctor(data));
4964
+ }
4965
+ catch (error) {
4966
+ const failure = toFailure("auth doctor", error);
4967
+ writeJson(failure);
4968
+ process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
4969
+ }
4970
+ }
4971
+ async function runAuthDoctor() {
4972
+ let context = null;
4973
+ let profileError = null;
4974
+ try {
4975
+ context = await resolveActiveProfileWithSource();
4976
+ }
4977
+ catch (error) {
4978
+ const failure = toFailure("profiles current", error);
4979
+ profileError = {
4980
+ code: failure.error.code,
4981
+ message: failure.error.message,
4982
+ ...(failure.error.details !== undefined ? { details: failure.error.details } : {}),
4983
+ };
4984
+ }
4985
+ const credentials = context?.resolution.credentials ?? null;
4986
+ const apiUrl = credentials?.apiUrl ?? defaultApiUrl();
4987
+ const healthCredentials = credentials ?? { token: "", apiUrl };
4988
+ const health = await runAuthDoctorCheck(() => requestOxygen("/api/health", {
4989
+ credentials: healthCredentials,
4990
+ requireAuth: false,
4991
+ enforceMinimumCliVersion: false,
4992
+ }));
4993
+ const identity = credentials
4994
+ ? await runAuthDoctorCheck(() => requestOxygen("/api/cli/whoami", {
4995
+ credentials,
4996
+ enforceMinimumCliVersion: false,
4997
+ }))
4998
+ : { ok: false, skipped: true, reason: "no_credentials" };
4999
+ const cachedIdentity = credentials?.identity ?? null;
5000
+ return {
5001
+ profile: context?.resolution.exists ? context.resolution.name : null,
5002
+ profile_source: context?.source ?? null,
5003
+ stored: Boolean(context?.resolution.exists),
5004
+ api_url: apiUrl,
5005
+ auth_kind: credentials?.authKind ?? null,
5006
+ token_fingerprint: credentials ? formatFingerprint(createCredentialFingerprint(credentials.token)) : null,
5007
+ cached_organization: credentials?.activeOrganization ?? cachedIdentity?.organization ?? null,
5008
+ cached_user: cachedIdentity?.user ?? null,
5009
+ profile_error: profileError,
5010
+ health,
5011
+ identity,
5012
+ };
5013
+ }
5014
+ async function runAuthDoctorCheck(fn) {
5015
+ try {
5016
+ return { ok: true, data: await fn() };
5017
+ }
5018
+ catch (error) {
5019
+ const failure = toFailure("auth doctor", error);
5020
+ return {
5021
+ ok: false,
5022
+ error: {
5023
+ code: failure.error.code,
5024
+ message: failure.error.message,
5025
+ ...(failure.error.details !== undefined ? { details: failure.error.details } : {}),
5026
+ },
5027
+ };
5028
+ }
5029
+ }
4873
5030
  async function handleOrgUseAction(organization, options, command) {
4874
5031
  try {
4875
5032
  const data = await requestOxygen("/api/cli/orgs/select", {
@@ -5560,6 +5717,40 @@ function formatProfilesList(data) {
5560
5717
  }
5561
5718
  return lines.join("\n");
5562
5719
  }
5720
+ function formatAuthDoctor(data) {
5721
+ const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
5722
+ const lines = [
5723
+ "",
5724
+ styles.bold("Oxygen Auth Doctor"),
5725
+ "",
5726
+ ` ${styles.dim("Profile")} ${data.profile ?? "(none)"}${data.profile_source ? ` (${data.profile_source})` : ""}`,
5727
+ ` ${styles.dim("API")} ${data.api_url}`,
5728
+ ` ${styles.dim("Stored")} ${data.stored ? "yes" : "no"}`,
5729
+ ];
5730
+ if (data.auth_kind)
5731
+ lines.push(` ${styles.dim("Auth kind")} ${data.auth_kind}`);
5732
+ if (data.token_fingerprint)
5733
+ lines.push(` ${styles.dim("Fingerprint")} ${data.token_fingerprint}`);
5734
+ if (data.cached_organization) {
5735
+ const org = data.cached_organization.slug ?? data.cached_organization.id;
5736
+ lines.push(` ${styles.dim("Cached org")} ${data.cached_organization.name} (${org})`);
5737
+ }
5738
+ if (data.profile_error) {
5739
+ lines.push(` ${styles.dim("Profile error")} ${data.profile_error.code}: ${data.profile_error.message}`);
5740
+ }
5741
+ lines.push(` ${styles.dim("Health")} ${data.health.ok ? "ok" : `failed (${data.health.error.code})`}`);
5742
+ if ("skipped" in data.identity) {
5743
+ lines.push(` ${styles.dim("Identity")} skipped (${data.identity.reason})`);
5744
+ }
5745
+ else {
5746
+ lines.push(` ${styles.dim("Identity")} ${data.identity.ok ? "ok" : `failed (${data.identity.error.code})`}`);
5747
+ if (!data.identity.ok) {
5748
+ lines.push(` ${styles.dim("Identity error")} ${data.identity.error.message}`);
5749
+ }
5750
+ }
5751
+ lines.push("");
5752
+ return lines.join("\n");
5753
+ }
5563
5754
  function formatProfileOrgCell(profile) {
5564
5755
  if (!profile.organization)
5565
5756
  return "(unknown org — run `oxygen whoami` to refresh)";
@@ -20,6 +20,10 @@ export declare function iterateRowsFileBufferBatches(buffer: Buffer, format: Row
20
20
  sheet?: string;
21
21
  batchSize?: number;
22
22
  }): AsyncGenerator<Record<string, unknown>[]>;
23
+ export declare function iterateRowsFileStreamBatches(chunks: AsyncIterable<Uint8Array | string>, format: RowsFileFormat, options?: {
24
+ sheet?: string;
25
+ batchSize?: number;
26
+ }): AsyncGenerator<Record<string, unknown>[]>;
23
27
  export declare function parseRowsText(text: string, format: Exclude<RowsFileFormat, "xlsx">): Record<string, unknown>[];
24
28
  export declare function inferImportColumnLabels(rows: Record<string, unknown>[]): string[];
25
29
  export declare function normalizeRowsForNewTable(rows: Record<string, unknown>[]): NewTableImportRows;
@@ -46,6 +46,20 @@ export async function* iterateRowsFileBufferBatches(buffer, format, options = {}
46
46
  const rows = await parseRowsFileBuffer(buffer, format, options.sheet ? { sheet: options.sheet } : {});
47
47
  yield* chunkRows(rows, batchSize);
48
48
  }
49
+ export async function* iterateRowsFileStreamBatches(chunks, format, options = {}) {
50
+ const batchSize = normalizeBatchSize(options.batchSize);
51
+ if (format === "csv") {
52
+ yield* iterateCsvRowStreamBatches(chunks, batchSize);
53
+ return;
54
+ }
55
+ if (format === "jsonl") {
56
+ yield* iterateJsonlRowStreamBatches(chunks, batchSize);
57
+ return;
58
+ }
59
+ const buffer = await streamToLimitedBuffer(chunks, format);
60
+ const rows = await parseRowsFileBuffer(buffer, format, options.sheet ? { sheet: options.sheet } : {});
61
+ yield* chunkRows(rows, batchSize);
62
+ }
49
63
  export function parseRowsText(text, format) {
50
64
  if (format === "json")
51
65
  return normalizeRowObjects(parseJsonArray(text));
@@ -208,6 +222,55 @@ function* iterateCsvRowBatches(text, batchSize) {
208
222
  if (state.batch.length > 0)
209
223
  yield state.batch;
210
224
  }
225
+ async function* iterateCsvRowStreamBatches(chunks, batchSize) {
226
+ const decoder = new TextDecoder();
227
+ const state = {
228
+ header: null,
229
+ batch: [],
230
+ record: [],
231
+ field: "",
232
+ inQuotes: false,
233
+ };
234
+ let carry = "";
235
+ for await (const chunk of chunks) {
236
+ const combined = carry + decoder.decode(toUint8Array(chunk), { stream: true });
237
+ if (!combined)
238
+ continue;
239
+ const processUntil = Math.max(0, combined.length - 1);
240
+ for (let index = 0; index < processUntil; index += 1) {
241
+ const ready = applyCsvTextCharacter(state, combined, index, batchSize);
242
+ if (ready.skipNext)
243
+ index += 1;
244
+ if (ready.batch)
245
+ yield ready.batch;
246
+ }
247
+ carry = combined.charAt(combined.length - 1);
248
+ }
249
+ const tail = carry + decoder.decode();
250
+ for (let index = 0; index < tail.length; index += 1) {
251
+ const ready = applyCsvTextCharacter(state, tail, index, batchSize);
252
+ if (ready.skipNext)
253
+ index += 1;
254
+ if (ready.batch)
255
+ yield ready.batch;
256
+ }
257
+ if (state.field || state.record.length > 0) {
258
+ const ready = appendCsvRecordToBatch(state, finishCsvRecord(state), batchSize);
259
+ if (ready)
260
+ yield ready;
261
+ }
262
+ if (state.batch.length > 0)
263
+ yield state.batch;
264
+ }
265
+ function applyCsvTextCharacter(state, text, index, batchSize) {
266
+ const result = applyCsvCharacter(state, text.charAt(index), text.charAt(index + 1));
267
+ if (!result.recordComplete)
268
+ return { skipNext: result.skipNext, batch: null };
269
+ return {
270
+ skipNext: result.skipNext,
271
+ batch: appendCsvRecordToBatch(state, finishCsvRecord(state), batchSize),
272
+ };
273
+ }
211
274
  function applyCsvCharacter(state, char, next) {
212
275
  return state.inQuotes
213
276
  ? applyQuotedCsvCharacter(state, char, next)
@@ -294,6 +357,53 @@ function* iterateJsonlRowBatches(text, batchSize) {
294
357
  if (batch.length > 0)
295
358
  yield batch;
296
359
  }
360
+ async function* iterateJsonlRowStreamBatches(chunks, batchSize) {
361
+ const decoder = new TextDecoder();
362
+ let pending = "";
363
+ let batch = [];
364
+ for await (const chunk of chunks) {
365
+ pending += decoder.decode(toUint8Array(chunk), { stream: true });
366
+ let newline = pending.indexOf("\n");
367
+ while (newline >= 0) {
368
+ const line = pending.slice(0, newline).replace(/\r$/, "").trim();
369
+ pending = pending.slice(newline + 1);
370
+ if (line)
371
+ batch.push(normalizeRowObject(JSON.parse(line)));
372
+ if (batch.length >= batchSize) {
373
+ yield batch;
374
+ batch = [];
375
+ }
376
+ newline = pending.indexOf("\n");
377
+ }
378
+ }
379
+ const tail = (pending + decoder.decode()).replace(/\r$/, "").trim();
380
+ if (tail)
381
+ batch.push(normalizeRowObject(JSON.parse(tail)));
382
+ if (batch.length > 0)
383
+ yield batch;
384
+ }
385
+ async function streamToLimitedBuffer(chunks, format) {
386
+ const buffers = [];
387
+ let total = 0;
388
+ for await (const chunk of chunks) {
389
+ const buffer = Buffer.from(toUint8Array(chunk));
390
+ total += buffer.byteLength;
391
+ if (total > MAX_BUFFERED_IMPORT_PARSE_BYTES) {
392
+ throw new OxygenError("import_file_too_large_for_format", `${format.toUpperCase()} imports must be uploaded as CSV or JSONL for large files.`, {
393
+ details: {
394
+ format,
395
+ max_buffered_parse_bytes: MAX_BUFFERED_IMPORT_PARSE_BYTES,
396
+ },
397
+ exitCode: 1,
398
+ });
399
+ }
400
+ buffers.push(buffer);
401
+ }
402
+ return Buffer.concat(buffers);
403
+ }
404
+ function toUint8Array(chunk) {
405
+ return typeof chunk === "string" ? Buffer.from(chunk) : chunk;
406
+ }
297
407
  function* chunkRows(rows, batchSize) {
298
408
  for (let index = 0; index < rows.length; index += batchSize) {
299
409
  yield rows.slice(index, index + batchSize);
@@ -5,6 +5,7 @@ export * from "./column-types.js";
5
5
  export * from "./credit-guidance.js";
6
6
  export * from "./log.js";
7
7
  export * from "./provider-request-outcomes.js";
8
+ export * from "./signup-lead-deliveries.js";
8
9
  export * from "./telemetry.js";
9
10
  export type JsonValue = string | number | boolean | null | JsonValue[] | {
10
11
  [key: string]: JsonValue;
@@ -6,6 +6,7 @@ export * from "./column-types.js";
6
6
  export * from "./credit-guidance.js";
7
7
  export * from "./log.js";
8
8
  export * from "./provider-request-outcomes.js";
9
+ export * from "./signup-lead-deliveries.js";
9
10
  export * from "./telemetry.js";
10
11
  export class OxygenError extends Error {
11
12
  code;
@@ -21,6 +21,14 @@ export declare function presignImportUpload(input: {
21
21
  export declare function downloadImportObject(input: {
22
22
  storageKey: string;
23
23
  }): Promise<Buffer>;
24
+ export declare function getImportObjectMetadata(input: {
25
+ storageKey: string;
26
+ }): Promise<{
27
+ contentLength: number | null;
28
+ }>;
29
+ export declare function openImportObjectStream(input: {
30
+ storageKey: string;
31
+ }): Promise<NodeJS.ReadableStream>;
24
32
  export declare function deleteImportObject(input: {
25
33
  storageKey: string;
26
34
  }): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client, } from "@aws-sdk/client-s3";
2
+ import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client, } from "@aws-sdk/client-s3";
3
3
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
4
4
  import { OxygenError } from "./index.js";
5
5
  // S3-compatible object storage for large CSV/file imports. The CLI uploads the
@@ -96,6 +96,25 @@ export async function downloadImportObject(input) {
96
96
  }
97
97
  return streamToBuffer(body);
98
98
  }
99
+ export async function getImportObjectMetadata(input) {
100
+ const { client, config } = resolveClient();
101
+ const result = await client.send(new HeadObjectCommand({ Bucket: config.bucket, Key: input.storageKey }));
102
+ return {
103
+ contentLength: typeof result.ContentLength === "number" ? result.ContentLength : null,
104
+ };
105
+ }
106
+ export async function openImportObjectStream(input) {
107
+ const { client, config } = resolveClient();
108
+ const result = await client.send(new GetObjectCommand({ Bucket: config.bucket, Key: input.storageKey }));
109
+ const body = result.Body;
110
+ if (!body) {
111
+ throw new OxygenError("import_object_missing", "Import object had no body.", {
112
+ details: { storage_key: input.storageKey },
113
+ exitCode: 1,
114
+ });
115
+ }
116
+ return body;
117
+ }
99
118
  export async function deleteImportObject(input) {
100
119
  const { client, config } = resolveClient();
101
120
  await client.send(new DeleteObjectCommand({ Bucket: config.bucket, Key: input.storageKey }));
@@ -0,0 +1 @@
1
+ export declare const RESEND_ONBOARDING_DELIVERY_EVENT_TYPE = "resend.onboarding";
@@ -0,0 +1 @@
1
+ export const RESEND_ONBOARDING_DELIVERY_EVENT_TYPE = "resend.onboarding";
@@ -1,2 +1,2 @@
1
- export declare const OXYGEN_VERSION = "1.123.1";
1
+ export declare const OXYGEN_VERSION = "1.128.3";
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.0.0";
@@ -1,3 +1,3 @@
1
- export const OXYGEN_VERSION = "1.123.1";
1
+ export const OXYGEN_VERSION = "1.128.3";
2
2
  // Bump this only when deployed CLI/API contracts require a newer CLI.
3
3
  export const OXYGEN_MINIMUM_CLI_VERSION = "1.0.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.123.1",
3
+ "version": "1.128.3",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",