@oxygen-agent/cli 1.162.10 → 1.177.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 (38) hide show
  1. package/README.md +1 -1
  2. package/dist/http-client.js +2 -78
  3. package/dist/index.js +1312 -527
  4. package/dist/run-wait.d.ts +23 -0
  5. package/dist/run-wait.js +57 -0
  6. package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +0 -5
  7. package/node_modules/@oxygen/shared/dist/cell-format.d.ts +2 -14
  8. package/node_modules/@oxygen/shared/dist/cell-format.js +3 -10
  9. package/node_modules/@oxygen/shared/dist/cli-envelope.d.ts +27 -0
  10. package/node_modules/@oxygen/shared/dist/cli-envelope.js +102 -0
  11. package/node_modules/@oxygen/shared/dist/cli-result.d.ts +39 -0
  12. package/node_modules/@oxygen/shared/dist/cli-result.js +52 -0
  13. package/node_modules/@oxygen/shared/dist/credit-guidance.d.ts +0 -1
  14. package/node_modules/@oxygen/shared/dist/credit-guidance.js +1 -1
  15. package/node_modules/@oxygen/shared/dist/file-import.js +1 -1
  16. package/node_modules/@oxygen/shared/dist/index.d.ts +3 -39
  17. package/node_modules/@oxygen/shared/dist/index.js +3 -44
  18. package/node_modules/@oxygen/shared/dist/log.d.ts +0 -1
  19. package/node_modules/@oxygen/shared/dist/log.js +8 -3
  20. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +0 -3
  21. package/node_modules/@oxygen/shared/dist/object-storage.js +1 -24
  22. package/node_modules/@oxygen/shared/dist/redaction.js +45 -4
  23. package/node_modules/@oxygen/shared/dist/search-vocab.d.ts +18 -0
  24. package/node_modules/@oxygen/shared/dist/search-vocab.js +151 -0
  25. package/node_modules/@oxygen/shared/dist/select-options.d.ts +18 -0
  26. package/node_modules/@oxygen/shared/dist/select-options.js +121 -0
  27. package/node_modules/@oxygen/shared/dist/sequences.js +1 -1
  28. package/node_modules/@oxygen/shared/dist/sql-error.d.ts +0 -6
  29. package/node_modules/@oxygen/shared/dist/sql-error.js +67 -58
  30. package/node_modules/@oxygen/shared/dist/telemetry.d.ts +0 -1
  31. package/node_modules/@oxygen/shared/dist/telemetry.js +23 -18
  32. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  33. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  34. package/node_modules/@oxygen/shared/dist/worker-failures-queue.d.ts +22 -0
  35. package/node_modules/@oxygen/shared/dist/worker-failures-queue.js +56 -0
  36. package/node_modules/@oxygen/workflows/dist/index.d.ts +12 -11
  37. package/node_modules/@oxygen/workflows/dist/index.js +58 -0
  38. package/package.json +1 -1
@@ -0,0 +1,23 @@
1
+ export interface CliRunWaitConfig {
2
+ /** Run id echoed into the timeout error details. */
3
+ runId: string;
4
+ /** Raw --timeout-seconds flag; falls back to defaultTimeoutSeconds when unset. */
5
+ requestedTimeoutSeconds?: string | undefined;
6
+ /** Raw --interval-seconds flag; falls back to defaultIntervalSeconds when unset. */
7
+ requestedIntervalSeconds?: string | undefined;
8
+ defaultTimeoutSeconds: number;
9
+ defaultIntervalSeconds: number;
10
+ /** Fetches the latest run snapshot to inspect this poll. */
11
+ fetchRun: () => Promise<Record<string, unknown>>;
12
+ /** True when the run status is terminal for this resource. */
13
+ isTerminal: (status: string | null) => boolean;
14
+ /** Builds the response once a terminal status is observed. */
15
+ shapeTerminal: (run: Record<string, unknown>, status: string | null, polls: number, elapsedMs: number) => Record<string, unknown>;
16
+ /** OxygenError shape thrown when the deadline passes before a terminal status. */
17
+ timeoutCode: string;
18
+ timeoutMessage: string;
19
+ timeoutDetailIdKey: string;
20
+ /** Optional extra timeout details (e.g. workflow queued-no-worker guidance). */
21
+ timeoutExtraDetails?: (latestRun: Record<string, unknown> | null, status: string | null) => Record<string, unknown>;
22
+ }
23
+ export declare function waitForCliRun(config: CliRunWaitConfig): Promise<Record<string, unknown>>;
@@ -0,0 +1,57 @@
1
+ import { OxygenError } from "@oxygen/shared";
2
+ export async function waitForCliRun(config) {
3
+ const timeoutSeconds = readPositiveInt(config.requestedTimeoutSeconds)
4
+ ?? config.defaultTimeoutSeconds;
5
+ const intervalSeconds = readPositiveInt(config.requestedIntervalSeconds)
6
+ ?? config.defaultIntervalSeconds;
7
+ const startedAt = Date.now();
8
+ const deadline = startedAt + timeoutSeconds * 1000;
9
+ let polls = 0;
10
+ while (true) {
11
+ polls += 1;
12
+ const latestRun = await config.fetchRun();
13
+ const status = readRecordString(latestRun, "status");
14
+ if (config.isTerminal(status)) {
15
+ return config.shapeTerminal(latestRun, status, polls, Date.now() - startedAt);
16
+ }
17
+ const remainingMs = deadline - Date.now();
18
+ if (remainingMs <= 0) {
19
+ throw new OxygenError(config.timeoutCode, config.timeoutMessage, {
20
+ details: {
21
+ [config.timeoutDetailIdKey]: config.runId,
22
+ status: status ?? null,
23
+ timeout_seconds: timeoutSeconds,
24
+ polls,
25
+ ...(config.timeoutExtraDetails?.(latestRun, status) ?? {}),
26
+ },
27
+ exitCode: 1,
28
+ });
29
+ }
30
+ await sleep(Math.min(intervalSeconds * 1000, remainingMs));
31
+ }
32
+ }
33
+ // Local copies of index.ts's tiny readers keep this helper free of an
34
+ // index.ts <-> run-wait.ts import cycle (the MCP run-wait.ts module likewise
35
+ // defines its own sleep rather than importing from a tool file).
36
+ function readPositiveInt(value) {
37
+ const trimmed = value?.trim();
38
+ if (!trimmed)
39
+ return undefined;
40
+ const parsed = Number(trimmed);
41
+ if (!Number.isInteger(parsed) || parsed < 1) {
42
+ throw new OxygenError("invalid_number", "Expected a positive integer.", {
43
+ details: { value },
44
+ exitCode: 1,
45
+ });
46
+ }
47
+ return parsed;
48
+ }
49
+ function readRecordString(value, key) {
50
+ if (!value || typeof value !== "object" || Array.isArray(value))
51
+ return null;
52
+ const entry = value[key];
53
+ return typeof entry === "string" ? entry : null;
54
+ }
55
+ function sleep(ms) {
56
+ return new Promise((resolve) => setTimeout(resolve, ms));
57
+ }
@@ -105,7 +105,6 @@ export type RecipeContext = {
105
105
  };
106
106
  export type DurableRecipeContext = RecipeContext;
107
107
  export type RecipeRunFunction = (ctx: RecipeContext) => unknown | Promise<unknown>;
108
- export type DurableRecipeRunFunction = RecipeRunFunction;
109
108
  export type RecipeVisualBaseStep = {
110
109
  id: string;
111
110
  label: string;
@@ -155,10 +154,6 @@ export type DefineRecipeInput = {
155
154
  visualPlan?: RecipeVisualPlan;
156
155
  run: RecipeRunFunction;
157
156
  };
158
- export type DurableRecipeDefinition = RecipeDefinition;
159
- export type DefineDurableRecipeInput = DefineRecipeInput & {
160
- runtime: "durable";
161
- };
162
157
  export declare function defineRecipe(input: DefineRecipeInput): RecipeDefinition;
163
158
  export declare function isRecipeDefinition(value: unknown): value is RecipeDefinition;
164
159
  export declare function recipeVisualPlan(steps: RecipeVisualStep[]): RecipeVisualPlan;
@@ -11,7 +11,8 @@
11
11
  * and provider adapters, so a `text` column whose values parse cleanly as
12
12
  * numbers still gets thousands grouping at display time.
13
13
  * 3. Refuse to "rescue" things that aren't actually numbers (IP addresses,
14
- * version strings, phone numbers, URLs), via {@link looksLikeNumericText}.
14
+ * version strings, phone numbers, URLs): {@link rescueNumericText} returns
15
+ * null for those.
15
16
  */
16
17
  /** Surfaces have different escaping/length budgets, but the canonical string is shared. */
17
18
  export type CellFormatSurface = "cli" | "mcp" | "web";
@@ -38,19 +39,6 @@ export type CellFormatOptions = {
38
39
  * escaping, ellipsis budgets) belong outside this function.
39
40
  */
40
41
  export declare function formatCellForDisplay(value: unknown, column: CellColumnLike | null | undefined, options: CellFormatOptions): string;
41
- /**
42
- * Best-effort parse of a string that looks numeric (with or without thousands
43
- * grouping, with or without dotted IP-style separators) into a finite number.
44
- * Returns `null` when the string is clearly *not* a single number — IP
45
- * addresses, version strings, phone numbers, anything alphabetic.
46
- */
47
- export declare function rescueNumericText(raw: string): number | null;
48
- /**
49
- * True when a string is unambiguously numeric (integer, decimal, grouped, or
50
- * a mangled dotted form we can recover). Exported for callers that want to
51
- * highlight rescue cases without re-running the formatter.
52
- */
53
- export declare function looksLikeNumericText(value: string): boolean;
54
42
  /**
55
43
  * Decide whether the same value-level + column-level guards used inside
56
44
  * `formatCellForDisplay`'s text path should rescue this string. Exposed so
@@ -11,7 +11,8 @@
11
11
  * and provider adapters, so a `text` column whose values parse cleanly as
12
12
  * numbers still gets thousands grouping at display time.
13
13
  * 3. Refuse to "rescue" things that aren't actually numbers (IP addresses,
14
- * version strings, phone numbers, URLs), via {@link looksLikeNumericText}.
14
+ * version strings, phone numbers, URLs): {@link rescueNumericText} returns
15
+ * null for those.
15
16
  */
16
17
  const DEFAULT_LOCALE = "en-US";
17
18
  const INTEGER_RE = /^-?\d{1,15}$/;
@@ -99,7 +100,7 @@ export function formatCellForDisplay(value, column, options) {
99
100
  * Returns `null` when the string is clearly *not* a single number — IP
100
101
  * addresses, version strings, phone numbers, anything alphabetic.
101
102
  */
102
- export function rescueNumericText(raw) {
103
+ function rescueNumericText(raw) {
103
104
  const trimmed = raw.trim();
104
105
  if (!trimmed)
105
106
  return null;
@@ -168,14 +169,6 @@ function decodeDottedNumeric(raw) {
168
169
  const n = Number(normalized || "0");
169
170
  return Number.isFinite(n) ? sign * n : null;
170
171
  }
171
- /**
172
- * True when a string is unambiguously numeric (integer, decimal, grouped, or
173
- * a mangled dotted form we can recover). Exported for callers that want to
174
- * highlight rescue cases without re-running the formatter.
175
- */
176
- export function looksLikeNumericText(value) {
177
- return rescueNumericText(value) !== null;
178
- }
179
172
  function readDataType(column) {
180
173
  const raw = column?.dataType ?? column?.data_type ?? null;
181
174
  if (!raw)
@@ -0,0 +1,27 @@
1
+ import type { CliResult } from "./cli-result.js";
2
+ /** Server compatibility metadata extracted from a `CliResult.meta` envelope. */
3
+ export type ServerCompatibility = {
4
+ version?: string;
5
+ minimumCliVersion?: string;
6
+ };
7
+ /**
8
+ * Type guard for the shared `CliResult` envelope. Accepts `{ ok: true, data }`
9
+ * and `{ ok: false, error: {...} }`; rejects anything else (non-objects,
10
+ * arrays, missing discriminant, malformed error).
11
+ */
12
+ export declare function isCliResult<T>(value: unknown): value is CliResult<T>;
13
+ /**
14
+ * Read the server version and minimum-CLI floor out of a `CliResult.meta`
15
+ * block. Missing or malformed metadata yields an empty object so callers can
16
+ * treat "no compatibility info" uniformly.
17
+ */
18
+ export declare function readEnvelopeCompatibility(envelope: CliResult<unknown>): ServerCompatibility;
19
+ export declare function withRetryAfterDetails(details: unknown, response: Response): unknown;
20
+ /**
21
+ * Header fragment that lets automated clients reach a password-protected Vercel
22
+ * preview deployment. Returns `{ "x-vercel-protection-bypass": <secret> }` only
23
+ * when `VERCEL_AUTOMATION_BYPASS_SECRET` is set and the API URL targets a
24
+ * `*.vercel.app` host; otherwise an empty object. Callers spread the result
25
+ * into their request headers, so a non-preview URL is a no-op.
26
+ */
27
+ export declare function vercelProtectionBypassHeaders(apiUrl: string): Record<string, string>;
@@ -0,0 +1,102 @@
1
+ // Shared mechanics for the Oxygen CLI-result envelope that both the CLI HTTP
2
+ // client (`packages/cli/src/http-client.ts`) and the MCP API client
3
+ // (`packages/mcp-server/src/api-client.ts`) consume. These are pure functions —
4
+ // no runtime imports from runtime-specific modules — so each client keeps its
5
+ // own error class (`OxygenError` vs `OxygenApiError`) and surface-specific
6
+ // guidance while sharing the parsing/extraction logic that previously drifted
7
+ // after being copy-pasted between the two clients.
8
+ /**
9
+ * Type guard for the shared `CliResult` envelope. Accepts `{ ok: true, data }`
10
+ * and `{ ok: false, error: {...} }`; rejects anything else (non-objects,
11
+ * arrays, missing discriminant, malformed error).
12
+ */
13
+ export function isCliResult(value) {
14
+ if (!value || typeof value !== "object" || Array.isArray(value))
15
+ return false;
16
+ const ok = value.ok;
17
+ if (ok === true)
18
+ return "data" in value;
19
+ if (ok !== false)
20
+ return false;
21
+ const error = value.error;
22
+ return Boolean(error) && typeof error === "object" && !Array.isArray(error);
23
+ }
24
+ /**
25
+ * Read the server version and minimum-CLI floor out of a `CliResult.meta`
26
+ * block. Missing or malformed metadata yields an empty object so callers can
27
+ * treat "no compatibility info" uniformly.
28
+ */
29
+ export function readEnvelopeCompatibility(envelope) {
30
+ const meta = envelope.meta;
31
+ if (!meta || typeof meta !== "object" || Array.isArray(meta))
32
+ return {};
33
+ const record = meta;
34
+ const compatibility = {};
35
+ if (typeof record.version === "string")
36
+ compatibility.version = record.version;
37
+ if (typeof record.minimum_cli_version === "string") {
38
+ compatibility.minimumCliVersion = record.minimum_cli_version;
39
+ }
40
+ return compatibility;
41
+ }
42
+ // Surface the server's 429 backoff as a first-class `retry_after_seconds` detail
43
+ // so loop-driving callers can wait the right amount of time instead of dead-
44
+ // reckoning. Prefers a value the API already put in details; otherwise derives
45
+ // it from the RFC 6585 Retry-After header (or reset_at). Non-429 responses and
46
+ // unparseable values pass through untouched.
47
+ export function withRetryAfterDetails(details, response) {
48
+ if (response.status !== 429)
49
+ return details;
50
+ const record = details && typeof details === "object" && !Array.isArray(details)
51
+ ? details
52
+ : null;
53
+ if (record && typeof record.retry_after_seconds === "number")
54
+ return details;
55
+ const retryAfterSeconds = retryAfterSecondsFromResponse(response, record);
56
+ if (retryAfterSeconds === null)
57
+ return details;
58
+ if (record)
59
+ return { ...record, retry_after_seconds: retryAfterSeconds };
60
+ if (details === undefined)
61
+ return { retry_after_seconds: retryAfterSeconds };
62
+ return { details, retry_after_seconds: retryAfterSeconds };
63
+ }
64
+ function retryAfterSecondsFromResponse(response, details) {
65
+ const header = response.headers.get("retry-after");
66
+ if (header) {
67
+ const seconds = Number(header);
68
+ if (Number.isFinite(seconds) && seconds >= 0)
69
+ return Math.ceil(seconds);
70
+ }
71
+ const resetAt = details?.reset_at;
72
+ if (typeof resetAt === "string") {
73
+ const resetMs = Date.parse(resetAt);
74
+ if (!Number.isNaN(resetMs))
75
+ return Math.max(0, Math.ceil((resetMs - Date.now()) / 1000));
76
+ }
77
+ return null;
78
+ }
79
+ const VERCEL_PROTECTION_BYPASS_HEADER = "x-vercel-protection-bypass";
80
+ /**
81
+ * Header fragment that lets automated clients reach a password-protected Vercel
82
+ * preview deployment. Returns `{ "x-vercel-protection-bypass": <secret> }` only
83
+ * when `VERCEL_AUTOMATION_BYPASS_SECRET` is set and the API URL targets a
84
+ * `*.vercel.app` host; otherwise an empty object. Callers spread the result
85
+ * into their request headers, so a non-preview URL is a no-op.
86
+ */
87
+ export function vercelProtectionBypassHeaders(apiUrl) {
88
+ const secret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET?.trim();
89
+ if (!secret)
90
+ return {};
91
+ let hostname;
92
+ try {
93
+ hostname = new URL(apiUrl).hostname;
94
+ }
95
+ catch {
96
+ return {};
97
+ }
98
+ if (hostname === "vercel.app" || hostname.endsWith(".vercel.app")) {
99
+ return { [VERCEL_PROTECTION_BYPASS_HEADER]: secret };
100
+ }
101
+ return {};
102
+ }
@@ -0,0 +1,39 @@
1
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
2
+ [key: string]: JsonValue;
3
+ };
4
+ export type CliMeta = {
5
+ command: string;
6
+ version: string;
7
+ minimum_cli_version?: string;
8
+ };
9
+ export type CliSuccess<T> = {
10
+ ok: true;
11
+ data: T;
12
+ meta: CliMeta;
13
+ };
14
+ export type CliFailure = {
15
+ ok: false;
16
+ error: {
17
+ code: string;
18
+ message: string;
19
+ details?: unknown;
20
+ };
21
+ meta: CliMeta;
22
+ };
23
+ export type CliResult<T> = CliSuccess<T> | CliFailure;
24
+ export declare class OxygenError extends Error {
25
+ readonly code: string;
26
+ readonly details?: unknown;
27
+ readonly exitCode: number;
28
+ constructor(code: string, message: string, options?: {
29
+ details?: unknown;
30
+ exitCode?: number;
31
+ });
32
+ }
33
+ export declare function success<T>(command: string, data: T, version?: string, minimumCliVersion?: string): CliSuccess<T>;
34
+ export declare function failure(command: string, error: {
35
+ code: string;
36
+ message: string;
37
+ details?: unknown;
38
+ }, version?: string, minimumCliVersion?: string): CliFailure;
39
+ export declare function toFailure(command: string, error: unknown, version?: string): CliFailure;
@@ -0,0 +1,52 @@
1
+ // Core CLI-result envelope + error primitives, owned by a dependency-free leaf
2
+ // module so the shared package's other leaf modules (sequences, file-import,
3
+ // object-storage, cli-envelope, ...) can depend on `OxygenError` / `CliResult`
4
+ // without importing the root barrel (`./index.js`). The barrel re-exports
5
+ // everything here, so public imports from `@oxygen/shared` are unchanged; this
6
+ // only removes an internal runtime cycle (barrel → leaf → barrel) and stops
7
+ // subpath modules pulling the whole barrel for one class. Depends only on
8
+ // `./version.js` (itself a leaf).
9
+ import { OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_VERSION } from "./version.js";
10
+ export class OxygenError extends Error {
11
+ code;
12
+ details;
13
+ exitCode;
14
+ constructor(code, message, options = {}) {
15
+ super(message);
16
+ this.name = "OxygenError";
17
+ this.code = code;
18
+ this.details = options.details;
19
+ this.exitCode = options.exitCode ?? 1;
20
+ }
21
+ }
22
+ export function success(command, data, version = OXYGEN_VERSION, minimumCliVersion = OXYGEN_MINIMUM_CLI_VERSION) {
23
+ return {
24
+ ok: true,
25
+ data,
26
+ meta: {
27
+ command,
28
+ version,
29
+ minimum_cli_version: minimumCliVersion,
30
+ }
31
+ };
32
+ }
33
+ export function failure(command, error, version = OXYGEN_VERSION, minimumCliVersion = OXYGEN_MINIMUM_CLI_VERSION) {
34
+ return {
35
+ ok: false,
36
+ error,
37
+ meta: {
38
+ command,
39
+ version,
40
+ minimum_cli_version: minimumCliVersion,
41
+ }
42
+ };
43
+ }
44
+ export function toFailure(command, error, version = OXYGEN_VERSION) {
45
+ if (error instanceof OxygenError) {
46
+ return failure(command, { code: error.code, message: error.message, details: error.details }, version);
47
+ }
48
+ if (error instanceof Error) {
49
+ return failure(command, { code: "unexpected_error", message: error.message }, version);
50
+ }
51
+ return failure(command, { code: "unexpected_error", message: "An unexpected error occurred." }, version);
52
+ }
@@ -11,4 +11,3 @@ export declare function buildCreditGuidance(input: {
11
11
  availableCredits: number | null | undefined;
12
12
  headroomMultiplier?: number;
13
13
  }): CreditGuidance;
14
- export declare function recommendedCreditCeiling(estimatedCredits: number | null | undefined, headroomMultiplier?: number): number | null;
@@ -38,7 +38,7 @@ export function buildCreditGuidance(input) {
38
38
  credit_guidance: "Credits are tight for the recommended ceiling; ask before running, reduce scope, or top up credits.",
39
39
  };
40
40
  }
41
- export function recommendedCreditCeiling(estimatedCredits, headroomMultiplier = DEFAULT_HEADROOM_MULTIPLIER) {
41
+ function recommendedCreditCeiling(estimatedCredits, headroomMultiplier = DEFAULT_HEADROOM_MULTIPLIER) {
42
42
  const normalized = normalizeCreditValue(estimatedCredits);
43
43
  if (normalized === null)
44
44
  return null;
@@ -1,7 +1,7 @@
1
1
  import { extname } from "node:path";
2
2
  import readXlsxFile from "read-excel-file/node";
3
3
  import { inferImportColumnDataType, parseDateValueToIso, } from "./column-types.js";
4
- import { OxygenError } from "./index.js";
4
+ import { OxygenError } from "./cli-result.js";
5
5
  const MAX_IDENTIFIER_LENGTH = 63;
6
6
  export const MAX_BUFFERED_IMPORT_PARSE_BYTES = 10 * 1024 * 1024;
7
7
  export function inferRowsFileFormat(path) {
@@ -2,6 +2,8 @@ export { OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_VERSION } from "./version.js";
2
2
  export { WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS, clearWorkflowTriggerAutoPauseMetadata, } from "./workflow-trigger-metadata.js";
3
3
  export * from "./billing.js";
4
4
  export * from "./cell-format.js";
5
+ export * from "./cli-envelope.js";
6
+ export * from "./cli-result.js";
5
7
  export * from "./column-types.js";
6
8
  export * from "./credit-guidance.js";
7
9
  export * from "./linkedin-sequences.js";
@@ -11,46 +13,8 @@ export * from "./provider-request-outcomes.js";
11
13
  export * from "./signup-lead-deliveries.js";
12
14
  export * from "./sql-error.js";
13
15
  export * from "./telemetry.js";
16
+ export * from "./worker-failures-queue.js";
14
17
  export declare const MAX_ROW_LOOP_WRITE_ROWS = 500;
15
- export type JsonValue = string | number | boolean | null | JsonValue[] | {
16
- [key: string]: JsonValue;
17
- };
18
- export type CliMeta = {
19
- command: string;
20
- version: string;
21
- minimum_cli_version?: string;
22
- };
23
- export type CliSuccess<T> = {
24
- ok: true;
25
- data: T;
26
- meta: CliMeta;
27
- };
28
- export type CliFailure = {
29
- ok: false;
30
- error: {
31
- code: string;
32
- message: string;
33
- details?: unknown;
34
- };
35
- meta: CliMeta;
36
- };
37
- export type CliResult<T> = CliSuccess<T> | CliFailure;
38
- export declare class OxygenError extends Error {
39
- readonly code: string;
40
- readonly details?: unknown;
41
- readonly exitCode: number;
42
- constructor(code: string, message: string, options?: {
43
- details?: unknown;
44
- exitCode?: number;
45
- });
46
- }
47
- export declare function success<T>(command: string, data: T, version?: string, minimumCliVersion?: string): CliSuccess<T>;
48
- export declare function failure(command: string, error: {
49
- code: string;
50
- message: string;
51
- details?: unknown;
52
- }, version?: string, minimumCliVersion?: string): CliFailure;
53
- export declare function toFailure(command: string, error: unknown, version?: string): CliFailure;
54
18
  export type SemanticVersion = {
55
19
  major: number;
56
20
  minor: number;
@@ -1,8 +1,9 @@
1
- import { OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_VERSION } from "./version.js";
2
1
  export { OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_VERSION } from "./version.js";
3
2
  export { WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS, clearWorkflowTriggerAutoPauseMetadata, } from "./workflow-trigger-metadata.js";
4
3
  export * from "./billing.js";
5
4
  export * from "./cell-format.js";
5
+ export * from "./cli-envelope.js";
6
+ export * from "./cli-result.js";
6
7
  export * from "./column-types.js";
7
8
  export * from "./credit-guidance.js";
8
9
  export * from "./linkedin-sequences.js";
@@ -12,55 +13,13 @@ export * from "./provider-request-outcomes.js";
12
13
  export * from "./signup-lead-deliveries.js";
13
14
  export * from "./sql-error.js";
14
15
  export * from "./telemetry.js";
16
+ export * from "./worker-failures-queue.js";
15
17
  // Maximum rows a single row-loop write (insert/upsert/preview) may process. The
16
18
  // row-loop engine issues one DB round-trip per row, so a 500-row write already
17
19
  // approaches request timeouts (~50s observed in prod); larger batches must use
18
20
  // the COPY-based bulk engine. Tenant-db enforces this and the CLI/API row caps
19
21
  // reference it so they never advertise a batch the row-loop will reject.
20
22
  export const MAX_ROW_LOOP_WRITE_ROWS = 500;
21
- export class OxygenError extends Error {
22
- code;
23
- details;
24
- exitCode;
25
- constructor(code, message, options = {}) {
26
- super(message);
27
- this.name = "OxygenError";
28
- this.code = code;
29
- this.details = options.details;
30
- this.exitCode = options.exitCode ?? 1;
31
- }
32
- }
33
- export function success(command, data, version = OXYGEN_VERSION, minimumCliVersion = OXYGEN_MINIMUM_CLI_VERSION) {
34
- return {
35
- ok: true,
36
- data,
37
- meta: {
38
- command,
39
- version,
40
- minimum_cli_version: minimumCliVersion,
41
- }
42
- };
43
- }
44
- export function failure(command, error, version = OXYGEN_VERSION, minimumCliVersion = OXYGEN_MINIMUM_CLI_VERSION) {
45
- return {
46
- ok: false,
47
- error,
48
- meta: {
49
- command,
50
- version,
51
- minimum_cli_version: minimumCliVersion,
52
- }
53
- };
54
- }
55
- export function toFailure(command, error, version = OXYGEN_VERSION) {
56
- if (error instanceof OxygenError) {
57
- return failure(command, { code: error.code, message: error.message, details: error.details }, version);
58
- }
59
- if (error instanceof Error) {
60
- return failure(command, { code: "unexpected_error", message: error.message }, version);
61
- }
62
- return failure(command, { code: "unexpected_error", message: "An unexpected error occurred." }, version);
63
- }
64
23
  /**
65
24
  * Parse a three-segment semantic version (e.g. `1.142.17`). Pre-release and
66
25
  * build metadata suffixes (`-rc.1`, `+build`) are tolerated but ignored.
@@ -15,7 +15,6 @@ export type LogContext = {
15
15
  surface?: "mcp" | "cli" | "web" | "worker" | undefined;
16
16
  };
17
17
  export declare function withLogContext<T>(ctx: LogContext, fn: () => T): T;
18
- export declare function getLogContext(): LogContext;
19
18
  export declare function log(level: LogLevel, msg: string, fields?: Record<string, unknown>): void;
20
19
  export declare function errorId(err: unknown): string;
21
20
  export declare function errorFields(err: unknown): Record<string, unknown>;
@@ -7,7 +7,7 @@ export function withLogContext(ctx, fn) {
7
7
  const merged = { ...(store.getStore() ?? {}), ...ctx };
8
8
  return store.run(merged, fn);
9
9
  }
10
- export function getLogContext() {
10
+ function getLogContext() {
11
11
  return store.getStore() ?? {};
12
12
  }
13
13
  export function log(level, msg, fields) {
@@ -68,9 +68,14 @@ export function errorFields(err) {
68
68
  error_id: errorId(err),
69
69
  error_name: err.name,
70
70
  // Strip drizzle's `\nparams: <values>` tail so SQL parameter values never
71
- // reach logs (OXY-46); the SQL text and stack frames are preserved.
71
+ // reach logs (OXY-46).
72
72
  error_message: redactSqlParameters(err.message),
73
- error_stack: err.stack ? redactSqlParameters(err.stack) : err.stack,
73
+ // Deliberately NOT emitting `error_stack`: raw stack frames in
74
+ // Axiom-bound log fields leak absolute file paths and any credential
75
+ // dumped into a frame, and they tripped the prod/preview log-hygiene
76
+ // sweep (OXY-166). `error_id` already hashes name + message + first frame,
77
+ // so same-origin errors still group for triage without shipping the
78
+ // frames themselves; reproduce locally or follow trace_id for the stack.
74
79
  };
75
80
  }
76
81
  return { error_id: "non_error", error_message: redactSqlParameters(String(err)) };
@@ -18,9 +18,6 @@ export declare function presignImportUpload(input: {
18
18
  contentType?: string | null;
19
19
  contentLength: number;
20
20
  }): Promise<PresignedImportUpload>;
21
- export declare function downloadImportObject(input: {
22
- storageKey: string;
23
- }): Promise<Buffer>;
24
21
  export declare function getImportObjectMetadata(input: {
25
22
  storageKey: string;
26
23
  }): Promise<{
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client, } from "@aws-sdk/client-s3";
3
3
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
4
- import { OxygenError } from "./index.js";
4
+ import { OxygenError } from "./cli-result.js";
5
5
  // S3-compatible object storage for large CSV/file imports. The CLI uploads the
6
6
  // raw file straight to the bucket via a presigned PUT URL (bypassing Vercel's
7
7
  // ~4.5MB request-body limit), then the Fly worker downloads it and COPY-loads
@@ -80,22 +80,6 @@ export async function presignImportUpload(input) {
80
80
  expiresInSeconds: PRESIGN_EXPIRY_SECONDS,
81
81
  };
82
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
83
  export async function getImportObjectMetadata(input) {
100
84
  const { client, config } = resolveClient();
101
85
  const result = await client.send(new HeadObjectCommand({ Bucket: config.bucket, Key: input.storageKey }));
@@ -125,10 +109,3 @@ function sanitizeFileName(fileName) {
125
109
  const base = fileName.split(/[\\/]/).pop() ?? "";
126
110
  return base.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 120);
127
111
  }
128
- async function streamToBuffer(stream) {
129
- const chunks = [];
130
- for await (const chunk of stream) {
131
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk));
132
- }
133
- return Buffer.concat(chunks);
134
- }