@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
@@ -1,5 +1,29 @@
1
1
  const SECRET_KEY_PATTERN = /(api[_-]?key|x[_-]?key|authorization|bearer|cookie|password|secret|token|ciphertext|connection[_-]?string|connection[_-]?uri|database[_-]?url|dsn)/i;
2
2
  const OMITTED_KEY_PATTERN = /^(body|payload|prompt|prompts|raw_prompt|raw_prompts|row|rows|input|inputs|output|outputs|request|response|provider_payload|provider_response|customer_data)$/i;
3
+ // OXY-125: keyed redaction (SECRET_KEY_PATTERN) only catches whole fields named
4
+ // like a secret. Credentials also leak as *substrings* of otherwise-ordinary
5
+ // fields — most commonly an Authorization header dumped into error_message /
6
+ // error_stack. This matches the HTTP bearer scheme followed by its token (JWT /
7
+ // base64 / opaque, including our `oxy_live_`/`oxy_sess_` prefixes) anywhere in a
8
+ // string.
9
+ const BEARER_TOKEN_PATTERN = /\bBearer\s+[\w.~+/=-]+/gi;
10
+ // OXY-139: the prod log-hygiene sweep also flags `sk-…` API keys and DB
11
+ // connection URLs that ride along inside ordinary (non-secret-named) fields, so
12
+ // keyed + Bearer redaction is not enough. We scrub the two shapes that carry real
13
+ // secret *material* as substrings of any value:
14
+ // • DB URLs — the whole `postgres(ql)://…` (incl. scheme) so the marker cannot
15
+ // re-trip the sweep's `postgres://[^ ]+` clause.
16
+ // • `sk-…` keys — body allowed to contain `_`/`-` and no word boundary, matching
17
+ // the sweep regex byte-for-byte. This also neutralises the OXY-139 false
18
+ // positive: a Vercel `x-vercel-id` whose first segment ends in `sk`
19
+ // (e.g. `2g5sk-1781047899561-…`, logged as request_id on every cron summary
20
+ // line) is not a credential but is sweep-shaped, so it must be redacted to
21
+ // clear the signal; trace_id stays intact as the correlation key.
22
+ // The bare `OPENAI_API_KEY` *name* is deliberately NOT scrubbed — it is a variable
23
+ // name, not a secret, and hiding it would suppress legitimate "OPENAI_API_KEY is
24
+ // missing" diagnostics (its actual value is an `sk-…` string, already covered).
25
+ const DB_URL_PATTERN = /(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|rediss?|amqps?):\/\/[^\s"'<>()]+/gi;
26
+ const SK_TOKEN_PATTERN = /sk-[A-Za-z0-9_-]{20,}/g;
3
27
  const MAX_LOG_STRING_LENGTH = 8_000;
4
28
  const MAX_TELEMETRY_STRING_LENGTH = 500;
5
29
  const MAX_ARRAY_LENGTH = 20;
@@ -36,7 +60,7 @@ function sanitizeValueForLog(value, key, depth) {
36
60
  if (value === null || value === undefined)
37
61
  return value;
38
62
  if (typeof value === "string")
39
- return truncateString(value, MAX_LOG_STRING_LENGTH);
63
+ return truncateString(redactSecretsInString(value), MAX_LOG_STRING_LENGTH);
40
64
  if (typeof value === "number")
41
65
  return Number.isFinite(value) ? value : null;
42
66
  if (typeof value === "boolean")
@@ -67,7 +91,7 @@ function normalizeTelemetryValue(value, key) {
67
91
  if (OMITTED_KEY_PATTERN.test(key))
68
92
  return "[omitted]";
69
93
  if (typeof value === "string")
70
- return truncateString(value, MAX_TELEMETRY_STRING_LENGTH);
94
+ return truncateString(redactSecretsInString(value), MAX_TELEMETRY_STRING_LENGTH);
71
95
  if (typeof value === "number")
72
96
  return Number.isFinite(value) ? value : undefined;
73
97
  if (typeof value === "boolean")
@@ -80,8 +104,12 @@ function normalizeTelemetryValue(value, key) {
80
104
  .filter((entry) => typeof entry === "string" || typeof entry === "number" || typeof entry === "boolean");
81
105
  if (primitive.length === 0)
82
106
  return undefined;
107
+ // Array elements need the same substring scrub as scalar strings: a string[]
108
+ // attribute (e.g. dumped header lines) carries the same Bearer/sk-/DB-URL
109
+ // material, and this branch was the one telemetry path that exported it
110
+ // verbatim. Redact before truncating so a cut cannot expose token material.
83
111
  if (primitive.every((entry) => typeof entry === "string")) {
84
- return primitive.map((entry) => truncateString(String(entry), MAX_TELEMETRY_STRING_LENGTH));
112
+ return primitive.map((entry) => truncateString(redactSecretsInString(entry), MAX_TELEMETRY_STRING_LENGTH));
85
113
  }
86
114
  if (primitive.every((entry) => typeof entry === "number" && Number.isFinite(entry))) {
87
115
  return primitive;
@@ -89,7 +117,7 @@ function normalizeTelemetryValue(value, key) {
89
117
  if (primitive.every((entry) => typeof entry === "boolean")) {
90
118
  return primitive;
91
119
  }
92
- return primitive.map((entry) => truncateString(String(entry), MAX_TELEMETRY_STRING_LENGTH));
120
+ return primitive.map((entry) => truncateString(redactSecretsInString(String(entry)), MAX_TELEMETRY_STRING_LENGTH));
93
121
  }
94
122
  if (typeof value === "object" && value) {
95
123
  return truncateString(JSON.stringify(sanitizeValueForLog(value, key, 0)), MAX_TELEMETRY_STRING_LENGTH);
@@ -100,6 +128,19 @@ function sanitizeAttributeKey(key) {
100
128
  const normalized = key.trim().replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 120);
101
129
  return normalized || null;
102
130
  }
131
+ // Replaces credential-shaped substrings with token-free markers. Order matters:
132
+ // Bearer first so an `Authorization: Bearer sk-…` header collapses to a single
133
+ // `Bearer[REDACTED]` instead of leaving a stray `Bearer ` (the sweep matches
134
+ // `Bearer ` with a trailing space). Each marker is chosen so a second pass — and
135
+ // every prod-sweep regex (`Bearer `, `sk-…`, `postgres://…`) — finds nothing, so
136
+ // redaction stays idempotent. Sharing the global regexes is safe because
137
+ // String.replace resets their lastIndex between calls.
138
+ function redactSecretsInString(value) {
139
+ return value
140
+ .replace(BEARER_TOKEN_PATTERN, "Bearer[REDACTED]")
141
+ .replace(DB_URL_PATTERN, "[REDACTED_DB_URL]")
142
+ .replace(SK_TOKEN_PATTERN, "[REDACTED_SK]");
143
+ }
103
144
  function truncateString(value, maxLength) {
104
145
  return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
105
146
  }
@@ -0,0 +1,18 @@
1
+ export declare function normalizeText(value: string): string;
2
+ export declare function escapeRegExp(value: string): string;
3
+ export declare function extractUrls(value: string): string[];
4
+ export declare function extractDomains(value: string): string[];
5
+ export declare function parseMoney(amount: string, unit: string | undefined): number;
6
+ export declare function regionDisplayName(code: string): string | null;
7
+ export declare const COUNTRY_NAME_TO_ISO: Map<string, string>;
8
+ export declare const COUNTRY_ALIAS_TO_ISO: Record<string, string>;
9
+ export declare const CITY_TO_ISO: Record<string, string>;
10
+ export declare const KNOWN_TECHNOLOGIES: readonly ["salesforce", "hubspot", "shopify", "stripe", "aws", "azure", "gcp", "google cloud", "snowflake", "databricks", "postgres", "postgresql", "mysql", "mongodb", "redis", "kafka", "kubernetes", "docker", "react", "nextjs", "next.js", "vue", "angular", "node", "python", "ruby on rails", "django", "segment", "amplitude", "mixpanel", "intercom", "zendesk", "marketo", "pardot", "outreach", "salesloft", "gong", "clari", "netsuite", "workday", "sap", "oracle", "twilio", "klaviyo", "webflow", "wordpress", "magento", "bigcommerce", "woocommerce"];
11
+ export declare const INDUSTRY_TERMS: readonly ["saas", "b2b saas", "fintech", "healthtech", "healthcare", "edtech", "martech", "adtech", "proptech", "insurtech", "legaltech", "hrtech", "regtech", "biotech", "medtech", "cleantech", "climate tech", "cybersecurity", "security", "devtools", "developer tools", "e-commerce", "ecommerce", "logistics", "supply chain", "manufacturing", "construction", "real estate", "hospitality", "retail", "gaming", "media", "telecom", "automotive", "aerospace", "agritech", "foodtech", "ai", "artificial intelligence", "machine learning", "data analytics", "consulting", "staffing", "recruiting"];
12
+ export declare const EMPLOYEE_SEGMENTS: Record<string, {
13
+ min?: number;
14
+ max?: number;
15
+ }>;
16
+ export declare const KEYWORD_STOPWORDS: Set<string>;
17
+ export declare const DEPARTMENT_TERMS: Record<string, string>;
18
+ export declare const SENIORITY_TERMS: Record<string, string>;
@@ -0,0 +1,151 @@
1
+ // Shared search-planner vocabulary + stateless text helpers.
2
+ //
3
+ // Single source of truth for the constraint vocabulary and pure text helpers
4
+ // consumed by BOTH the legacy web search planners
5
+ // (apps/web/src/lib/company-search-plan.ts, people-search-plan.ts) and the
6
+ // @oxygen/routing classifier/compiler (via packages/routing/src/vocab.ts,
7
+ // which layers its named tuning deltas on top of these bases). Keeping one copy
8
+ // here is what stops the two surfaces from silently drifting (OXY-414).
9
+ //
10
+ // Everything here is a pure function or a static table: no Date.now, no
11
+ // randomness, no I/O — safe to import from any layer.
12
+ export function normalizeText(value) {
13
+ return value.toLowerCase().replace(/\s+/g, " ").trim();
14
+ }
15
+ export function escapeRegExp(value) {
16
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17
+ }
18
+ export function extractUrls(value) {
19
+ return [...new Set((value.match(/https?:\/\/[^\s)]+/gi) ?? []).map((url) => url.replace(/[.,;]+$/, "")))];
20
+ }
21
+ export function extractDomains(value) {
22
+ const domains = value.match(/\b(?:[a-z0-9-]+\.)+[a-z]{2,}\b/gi) ?? [];
23
+ return [...new Set(domains.map((domain) => domain.toLowerCase()).filter((domain) => !domain.includes("@")))];
24
+ }
25
+ export function parseMoney(amount, unit) {
26
+ const value = Number(amount);
27
+ const normalized = (unit ?? "").toLowerCase();
28
+ if (normalized.startsWith("b"))
29
+ return Math.round(value * 1_000_000_000);
30
+ if (normalized.startsWith("m"))
31
+ return Math.round(value * 1_000_000);
32
+ if (normalized.startsWith("k"))
33
+ return Math.round(value * 1_000);
34
+ return Math.round(value);
35
+ }
36
+ // ISO 3166-1 alpha-2 handling is backed by the platform's Intl region data instead of
37
+ // a hardcoded country table, so every assigned code validates and renders a display name.
38
+ const REGION_DISPLAY = new Intl.DisplayNames(["en"], { type: "region" });
39
+ export function regionDisplayName(code) {
40
+ if (!/^[A-Z]{2}$/.test(code))
41
+ return null;
42
+ try {
43
+ const name = REGION_DISPLAY.of(code);
44
+ return name && name !== code ? name : null;
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ // Lowercased country display name -> ISO code, generated from Intl region data at module load.
51
+ export const COUNTRY_NAME_TO_ISO = (() => {
52
+ const map = new Map();
53
+ for (let first = 65; first <= 90; first += 1) {
54
+ for (let second = 65; second <= 90; second += 1) {
55
+ const code = String.fromCharCode(first) + String.fromCharCode(second);
56
+ const name = regionDisplayName(code);
57
+ if (name)
58
+ map.set(name.toLowerCase(), code);
59
+ }
60
+ }
61
+ return map;
62
+ })();
63
+ // Common aliases and demonyms that Intl display names do not cover. Keys are lowercase prompt phrases.
64
+ export const COUNTRY_ALIAS_TO_ISO = {
65
+ "usa": "US", "u.s.": "US", "u.s.a.": "US", "us": "US", "america": "US", "american": "US", "the states": "US",
66
+ "uk": "GB", "u.k.": "GB", "britain": "GB", "great britain": "GB", "british": "GB", "england": "GB",
67
+ "german": "DE", "french": "FR", "dutch": "NL", "the netherlands": "NL", "holland": "NL",
68
+ "canadian": "CA", "australian": "AU", "austrian": "AT", "swiss": "CH", "swedish": "SE", "danish": "DK",
69
+ "norwegian": "NO", "finnish": "FI", "spanish": "ES", "italian": "IT", "portuguese": "PT", "polish": "PL",
70
+ "irish": "IE", "belgian": "BE", "brazilian": "BR", "mexican": "MX", "indian": "IN", "japanese": "JP",
71
+ "korean": "KR", "south korean": "KR", "chinese": "CN", "singaporean": "SG", "israeli": "IL",
72
+ "emirati": "AE", "uae": "AE", "saudi": "SA", "south african": "ZA", "nordic": "SE", "scandinavian": "SE",
73
+ };
74
+ // Major business-hub cities -> country code, used as geo hints when no country is named.
75
+ export const CITY_TO_ISO = {
76
+ "san francisco": "US", "new york": "US", "nyc": "US", "boston": "US", "austin": "US", "seattle": "US",
77
+ "los angeles": "US", "chicago": "US", "miami": "US", "denver": "US", "atlanta": "US",
78
+ "london": "GB", "manchester": "GB", "berlin": "DE", "munich": "DE", "hamburg": "DE", "cologne": "DE",
79
+ "paris": "FR", "amsterdam": "NL", "rotterdam": "NL", "stockholm": "SE", "copenhagen": "DK", "oslo": "NO",
80
+ "helsinki": "FI", "zurich": "CH", "geneva": "CH", "vienna": "AT", "madrid": "ES", "barcelona": "ES",
81
+ "milan": "IT", "lisbon": "PT", "dublin": "IE", "brussels": "BE", "warsaw": "PL", "toronto": "CA",
82
+ "vancouver": "CA", "montreal": "CA", "sydney": "AU", "melbourne": "AU", "singapore": "SG",
83
+ "tokyo": "JP", "tel aviv": "IL", "dubai": "AE", "bangalore": "IN", "mumbai": "IN", "sao paulo": "BR",
84
+ };
85
+ // Common B2B technologies recognized at query time. Unrecognized "using <X>" phrases become unparsed hints.
86
+ export const KNOWN_TECHNOLOGIES = [
87
+ "salesforce", "hubspot", "shopify", "stripe", "aws", "azure", "gcp", "google cloud", "snowflake",
88
+ "databricks", "postgres", "postgresql", "mysql", "mongodb", "redis", "kafka", "kubernetes", "docker",
89
+ "react", "nextjs", "next.js", "vue", "angular", "node", "python", "ruby on rails", "django",
90
+ "segment", "amplitude", "mixpanel", "intercom", "zendesk", "marketo", "pardot", "outreach",
91
+ "salesloft", "gong", "clari", "netsuite", "workday", "sap", "oracle", "twilio", "klaviyo", "webflow",
92
+ "wordpress", "magento", "bigcommerce", "woocommerce",
93
+ ];
94
+ // Industry-ish terms that go to both keywords and industries (provider enum resolution is reported separately).
95
+ export const INDUSTRY_TERMS = [
96
+ "saas", "b2b saas", "fintech", "healthtech", "healthcare", "edtech", "martech", "adtech", "proptech",
97
+ "insurtech", "legaltech", "hrtech", "regtech", "biotech", "medtech", "cleantech", "climate tech",
98
+ "cybersecurity", "security", "devtools", "developer tools", "e-commerce", "ecommerce", "logistics",
99
+ "supply chain", "manufacturing", "construction", "real estate", "hospitality", "retail", "gaming",
100
+ "media", "telecom", "automotive", "aerospace", "agritech", "foodtech", "ai", "artificial intelligence",
101
+ "machine learning", "data analytics", "consulting", "staffing", "recruiting",
102
+ ];
103
+ export const EMPLOYEE_SEGMENTS = {
104
+ "smb": { min: 1, max: 200 },
105
+ "small business": { min: 1, max: 200 },
106
+ "small businesses": { min: 1, max: 200 },
107
+ "mid-market": { min: 50, max: 1000 },
108
+ "mid market": { min: 50, max: 1000 },
109
+ "midmarket": { min: 50, max: 1000 },
110
+ "enterprise": { min: 1000 },
111
+ "enterprises": { min: 1000 },
112
+ "startup": { min: 1, max: 50 },
113
+ "startups": { min: 1, max: 50 },
114
+ "scaleup": { min: 50, max: 500 },
115
+ "scaleups": { min: 50, max: 500 },
116
+ };
117
+ // Words to drop when harvesting free-text keyword phrases (geo/size words are captured by dedicated extractors).
118
+ export const KEYWORD_STOPWORDS = new Set([
119
+ "find", "source", "build", "list", "of", "for", "the", "a", "an", "top", "best", "new", "all", "some",
120
+ "more", "with", "and", "or", "in", "at", "us", "uk", "eu", "based", "headquartered", "located",
121
+ "small", "large", "mid-market", "enterprise", "smb", "startup", "early-stage", "growth-stage",
122
+ ]);
123
+ // Department phrase -> canonical department label (people search).
124
+ export const DEPARTMENT_TERMS = {
125
+ sales: "Sales",
126
+ marketing: "Marketing",
127
+ engineering: "Engineering",
128
+ product: "Product",
129
+ finance: "Finance",
130
+ "human resources": "Human Resources",
131
+ "people ops": "Human Resources",
132
+ recruiting: "Human Resources",
133
+ operations: "Operations",
134
+ revops: "Revenue Operations",
135
+ "revenue operations": "Revenue Operations",
136
+ "customer success": "Customer Success",
137
+ support: "Customer Success",
138
+ legal: "Legal",
139
+ it: "Information Technology",
140
+ security: "Information Technology",
141
+ data: "Data",
142
+ design: "Design",
143
+ };
144
+ // Seniority phrase -> canonical seniority band (people search).
145
+ export const SENIORITY_TERMS = {
146
+ "c-suite": "C-Suite", "c-level": "C-Suite", chief: "C-Suite", founder: "C-Suite", ceo: "C-Suite",
147
+ cto: "C-Suite", cfo: "C-Suite", coo: "C-Suite", cmo: "C-Suite", cro: "C-Suite", president: "C-Suite",
148
+ vp: "VP", "vice president": "VP", svp: "VP", evp: "VP",
149
+ director: "Director", "head of": "Director",
150
+ manager: "Manager", lead: "Manager", principal: "Manager",
151
+ };
@@ -0,0 +1,18 @@
1
+ export declare const TAG_COLORS: readonly ["gray", "red", "orange", "amber", "yellow", "green", "teal", "blue", "indigo", "purple", "pink"];
2
+ export type TagColor = (typeof TAG_COLORS)[number];
3
+ export declare const DEFAULT_TAG_COLOR: TagColor;
4
+ export declare function isTagColor(value: unknown): value is TagColor;
5
+ export declare function tagColorForIndex(index: number): TagColor;
6
+ export declare const SELECT_COLUMN_SEMANTICS: readonly ["select", "single_select", "multi_select", "tags", "status"];
7
+ export type SelectColumnSemantic = (typeof SELECT_COLUMN_SEMANTICS)[number];
8
+ export declare function isSelectColumnSemantic(semanticType: string | null | undefined): boolean;
9
+ export declare function isMultiSelectSemantic(semanticType: string | null | undefined): boolean;
10
+ export declare function isStatusSemantic(semanticType: string | null | undefined): boolean;
11
+ export type SelectOption = {
12
+ id: string;
13
+ value: string;
14
+ label: string;
15
+ color: TagColor;
16
+ order: number;
17
+ };
18
+ export declare function normalizeSelectOptions(raw: unknown): SelectOption[];
@@ -0,0 +1,121 @@
1
+ // Canonical colored option model for select / multi-select / status columns.
2
+ //
3
+ // Shared across server validation, the web option editor, colored-pill
4
+ // rendering, filters, and the kanban view so every surface agrees. Colors are
5
+ // NAMED (theme-safe), stored once on the option — never raw hex — mirroring how
6
+ // Attio/Notion model status & select option colors.
7
+ export const TAG_COLORS = [
8
+ "gray",
9
+ "red",
10
+ "orange",
11
+ "amber",
12
+ "yellow",
13
+ "green",
14
+ "teal",
15
+ "blue",
16
+ "indigo",
17
+ "purple",
18
+ "pink",
19
+ ];
20
+ export const DEFAULT_TAG_COLOR = "gray";
21
+ export function isTagColor(value) {
22
+ return typeof value === "string" && TAG_COLORS.includes(value);
23
+ }
24
+ // Auto-assign colors for fresh options, skipping gray so a new column looks
25
+ // colorful out of the box; gray stays a deliberate manual choice.
26
+ export function tagColorForIndex(index) {
27
+ const palette = TAG_COLORS.filter((color) => color !== "gray");
28
+ return palette[Math.abs(index) % palette.length] ?? DEFAULT_TAG_COLOR;
29
+ }
30
+ // Semantic types (the column's `semanticType`) that present a fixed option set.
31
+ // `select`/`status` are single-value; `multi_select`/`tags` are array-valued.
32
+ // `single_select` is accepted as an alias of `select`.
33
+ export const SELECT_COLUMN_SEMANTICS = [
34
+ "select",
35
+ "single_select",
36
+ "multi_select",
37
+ "tags",
38
+ "status",
39
+ ];
40
+ export function isSelectColumnSemantic(semanticType) {
41
+ return Boolean(semanticType) && SELECT_COLUMN_SEMANTICS.includes(semanticType);
42
+ }
43
+ export function isMultiSelectSemantic(semanticType) {
44
+ return semanticType === "multi_select" || semanticType === "tags";
45
+ }
46
+ export function isStatusSemantic(semanticType) {
47
+ return semanticType === "status";
48
+ }
49
+ const MAX_OPTIONS = 200;
50
+ // Normalize a raw options array (from a column `definition.options`, CLI/MCP, or
51
+ // the editor) into canonical SelectOptions: dedupe by value, default colors,
52
+ // canonical 0..n ordering. Accepts strings or {value,label,color,order} objects.
53
+ export function normalizeSelectOptions(raw) {
54
+ if (!Array.isArray(raw))
55
+ return [];
56
+ const seen = new Set();
57
+ const collected = [];
58
+ for (const entry of raw) {
59
+ const option = normalizeSelectOption(entry, collected.length);
60
+ if (!option)
61
+ continue;
62
+ const dedupeKey = option.value.toLowerCase();
63
+ if (seen.has(dedupeKey))
64
+ continue;
65
+ seen.add(dedupeKey);
66
+ collected.push(option);
67
+ if (collected.length >= MAX_OPTIONS)
68
+ break;
69
+ }
70
+ collected.sort((a, b) => a.order - b.order);
71
+ return collected.map((option, index) => ({ ...option, order: index }));
72
+ }
73
+ function normalizeSelectOption(entry, fallbackIndex) {
74
+ if (typeof entry === "string")
75
+ return optionFromString(entry, fallbackIndex);
76
+ if (!entry || typeof entry !== "object")
77
+ return null;
78
+ const record = entry;
79
+ const rawValue = readOptionValue(record);
80
+ if (!rawValue)
81
+ return null;
82
+ const label = readTrimmedString(record.label) ?? rawValue;
83
+ const id = readTrimmedString(record.id) ?? slugifyOption(rawValue);
84
+ const color = isTagColor(record.color) ? record.color : tagColorForIndex(fallbackIndex);
85
+ const order = typeof record.order === "number" && Number.isFinite(record.order) ? record.order : fallbackIndex;
86
+ return { id, value: rawValue, label, color, order };
87
+ }
88
+ // A bare string entry becomes a fully-defaulted option (value == label == the
89
+ // trimmed string); blank strings are dropped.
90
+ function optionFromString(entry, fallbackIndex) {
91
+ const value = entry.trim();
92
+ if (!value)
93
+ return null;
94
+ return {
95
+ id: slugifyOption(value),
96
+ value,
97
+ label: value,
98
+ color: tagColorForIndex(fallbackIndex),
99
+ order: fallbackIndex,
100
+ };
101
+ }
102
+ // The cell value comes from `value`, falling back to `label`; "" when neither is
103
+ // a non-blank string (so the caller drops the option).
104
+ function readOptionValue(record) {
105
+ return readTrimmedString(record.value) ?? readTrimmedString(record.label) ?? "";
106
+ }
107
+ // Returns the trimmed string when the field is a non-blank string, else null.
108
+ function readTrimmedString(value) {
109
+ if (typeof value !== "string")
110
+ return null;
111
+ const trimmed = value.trim();
112
+ return trimmed ? trimmed : null;
113
+ }
114
+ function slugifyOption(value) {
115
+ const slug = value
116
+ .toLowerCase()
117
+ .replace(/[^a-z0-9]+/g, "_")
118
+ .replace(/^_+|_+$/g, "")
119
+ .slice(0, 60);
120
+ return slug || "option";
121
+ }
@@ -1,4 +1,4 @@
1
- import { OxygenError } from "./index.js";
1
+ import { OxygenError } from "./cli-result.js";
2
2
  import { renderLinkedInTemplate } from "./linkedin-sequences.js";
3
3
  /**
4
4
  * Multichannel sequence DSL — the shared contract validated identically by CLI,
@@ -21,12 +21,6 @@ export type SqlErrorAttribution = {
21
21
  /** Neon/HTTP transport status code (numeric) when the failure carries one. */
22
22
  statusCode: number | null;
23
23
  };
24
- /**
25
- * Classifies a pg/drizzle error into structured, non-secret attribution.
26
- * Returns null when the error carries no SQL/connection signal at all (so
27
- * non-DB errors never gain spurious `db.*` attributes). Never throws.
28
- */
29
- export declare function describeSqlError(error: unknown): SqlErrorAttribution | null;
30
24
  /** Log-field shape (snake_case) for merging into `errorFields(error)` payloads. */
31
25
  export declare function sqlErrorFields(error: unknown): Record<string, unknown>;
32
26
  /**
@@ -68,23 +68,14 @@ const TRANSIENT_CAUSES = new Set([
68
68
  * Returns null when the error carries no SQL/connection signal at all (so
69
69
  * non-DB errors never gain spurious `db.*` attributes). Never throws.
70
70
  */
71
- export function describeSqlError(error) {
71
+ function describeSqlError(error) {
72
72
  try {
73
73
  const sqlstate = readSqlstate(error);
74
74
  const errno = readErrno(error);
75
75
  const message = readMessage(error);
76
76
  const drizzle = isDrizzleQueryError(error, message);
77
- let cause = sqlstate ? categoryFromSqlstate(sqlstate) ?? "unknown" : "unknown";
78
- if (cause === "unknown") {
79
- if (errno && CONNECT_TIMEOUT_ERRNOS.has(errno))
80
- cause = "connect_timeout";
81
- else if (errno && CONNECTION_ERRNOS.has(errno))
82
- cause = "connection";
83
- else if (CONNECT_TIMEOUT_MESSAGE.test(message))
84
- cause = "connect_timeout";
85
- else if (CONNECTION_MESSAGE.test(message))
86
- cause = "connection";
87
- }
77
+ const sqlstateCause = sqlstate ? categoryFromSqlstate(sqlstate) ?? "unknown" : "unknown";
78
+ const cause = sqlstateCause !== "unknown" ? sqlstateCause : causeFromErrnoOrMessage(errno, message);
88
79
  const hasSignal = Boolean(sqlstate) || Boolean(errno) || cause !== "unknown" || drizzle;
89
80
  if (!hasSignal)
90
81
  return null;
@@ -110,22 +101,35 @@ export function describeSqlError(error) {
110
101
  return null;
111
102
  }
112
103
  }
104
+ // Connection-level fallback when no SQLSTATE resolved a cause: prefer the
105
+ // errno (set before/around the TCP+TLS handshake), then the message text.
106
+ function causeFromErrnoOrMessage(errno, message) {
107
+ if (errno && CONNECT_TIMEOUT_ERRNOS.has(errno))
108
+ return "connect_timeout";
109
+ if (errno && CONNECTION_ERRNOS.has(errno))
110
+ return "connection";
111
+ if (CONNECT_TIMEOUT_MESSAGE.test(message))
112
+ return "connect_timeout";
113
+ if (CONNECTION_MESSAGE.test(message))
114
+ return "connection";
115
+ return "unknown";
116
+ }
113
117
  /** Log-field shape (snake_case) for merging into `errorFields(error)` payloads. */
114
118
  export function sqlErrorFields(error) {
115
- const a = describeSqlError(error);
116
- if (!a)
119
+ const attribution = describeSqlError(error);
120
+ if (!attribution)
117
121
  return {};
118
122
  return {
119
- ...(a.pgCode ? { sql_error_pg_code: a.pgCode } : {}),
120
- sql_error_cause: a.cause,
121
- ...(a.queryKind ? { sql_error_query_kind: a.queryKind } : {}),
122
- ...(a.table ? { sql_error_table: a.table } : {}),
123
- sql_error_transient: a.transient,
124
- ...(a.severity ? { sql_error_severity: a.severity } : {}),
125
- ...(a.schema ? { sql_error_schema: a.schema } : {}),
126
- ...(a.column ? { sql_error_column: a.column } : {}),
127
- ...(a.constraint ? { sql_error_constraint: a.constraint } : {}),
128
- ...(a.statusCode !== null ? { sql_error_status: a.statusCode } : {}),
123
+ ...(attribution.pgCode ? { sql_error_pg_code: attribution.pgCode } : {}),
124
+ sql_error_cause: attribution.cause,
125
+ ...(attribution.queryKind ? { sql_error_query_kind: attribution.queryKind } : {}),
126
+ ...(attribution.table ? { sql_error_table: attribution.table } : {}),
127
+ sql_error_transient: attribution.transient,
128
+ ...(attribution.severity ? { sql_error_severity: attribution.severity } : {}),
129
+ ...(attribution.schema ? { sql_error_schema: attribution.schema } : {}),
130
+ ...(attribution.column ? { sql_error_column: attribution.column } : {}),
131
+ ...(attribution.constraint ? { sql_error_constraint: attribution.constraint } : {}),
132
+ ...(attribution.statusCode !== null ? { sql_error_status: attribution.statusCode } : {}),
129
133
  };
130
134
  }
131
135
  /**
@@ -144,47 +148,52 @@ export function redactSqlParameters(text) {
144
148
  }
145
149
  /** Telemetry-attribute shape (dotted keys) for span error attribution. */
146
150
  export function sqlErrorTelemetryAttributes(error) {
147
- const a = describeSqlError(error);
148
- if (!a)
151
+ const attribution = describeSqlError(error);
152
+ if (!attribution)
149
153
  return {};
150
154
  return {
151
- ...(a.pgCode ? { "db.pg_code": a.pgCode } : {}),
152
- "db.error.cause": a.cause,
153
- ...(a.queryKind ? { "db.error.query_kind": a.queryKind } : {}),
154
- ...(a.table ? { "db.error.table": a.table } : {}),
155
- "db.error.transient": a.transient,
156
- ...(a.severity ? { "db.error.severity": a.severity } : {}),
157
- ...(a.schema ? { "db.error.schema": a.schema } : {}),
158
- ...(a.column ? { "db.error.column": a.column } : {}),
159
- ...(a.constraint ? { "db.error.constraint": a.constraint } : {}),
160
- ...(a.statusCode !== null ? { "db.error.status": a.statusCode } : {}),
155
+ ...(attribution.pgCode ? { "db.pg_code": attribution.pgCode } : {}),
156
+ "db.error.cause": attribution.cause,
157
+ ...(attribution.queryKind ? { "db.error.query_kind": attribution.queryKind } : {}),
158
+ ...(attribution.table ? { "db.error.table": attribution.table } : {}),
159
+ "db.error.transient": attribution.transient,
160
+ ...(attribution.severity ? { "db.error.severity": attribution.severity } : {}),
161
+ ...(attribution.schema ? { "db.error.schema": attribution.schema } : {}),
162
+ ...(attribution.column ? { "db.error.column": attribution.column } : {}),
163
+ ...(attribution.constraint ? { "db.error.constraint": attribution.constraint } : {}),
164
+ ...(attribution.statusCode !== null ? { "db.error.status": attribution.statusCode } : {}),
161
165
  };
162
166
  }
167
+ // Specific SQLSTATEs that must resolve before the coarser class-prefix lookup
168
+ // (e.g. 40001 serialization / 40P01 deadlock are more precise than class 40).
169
+ const EXACT_SQLSTATE_CAUSES = new Map([
170
+ ["57014", "statement_timeout"],
171
+ ["53300", "too_many_connections"],
172
+ ["40001", "serialization"],
173
+ ["40P01", "deadlock"],
174
+ ]);
175
+ // SQLSTATE class (first two chars) → cause, applied only after the exact and
176
+ // schema-drift lookups above so the precise codes keep their dedicated cause.
177
+ const SQLSTATE_CLASS_CAUSES = new Map([
178
+ ["08", "connection"],
179
+ ["28", "auth"],
180
+ ["22", "data_exception"],
181
+ ["23", "integrity_constraint"],
182
+ ["40", "transaction_rollback"],
183
+ ["42", "syntax_or_access"],
184
+ ["53", "insufficient_resources"],
185
+ ["57", "admin_shutdown"],
186
+ ["58", "internal"],
187
+ ["XX", "internal"],
188
+ ["3F", "schema_drift"],
189
+ ]);
163
190
  function categoryFromSqlstate(code) {
164
- if (code === "57014")
165
- return "statement_timeout";
166
- if (code === "53300")
167
- return "too_many_connections";
168
- if (code === "40001")
169
- return "serialization";
170
- if (code === "40P01")
171
- return "deadlock";
191
+ const exact = EXACT_SQLSTATE_CAUSES.get(code);
192
+ if (exact)
193
+ return exact;
172
194
  if (SCHEMA_DRIFT_SQLSTATES.has(code))
173
195
  return "schema_drift";
174
- switch (code.slice(0, 2)) {
175
- case "08": return "connection";
176
- case "28": return "auth";
177
- case "22": return "data_exception";
178
- case "23": return "integrity_constraint";
179
- case "40": return "transaction_rollback";
180
- case "42": return "syntax_or_access";
181
- case "53": return "insufficient_resources";
182
- case "57": return "admin_shutdown";
183
- case "58": return "internal";
184
- case "XX": return "internal";
185
- case "3F": return "schema_drift";
186
- default: return null;
187
- }
196
+ return SQLSTATE_CLASS_CAUSES.get(code.slice(0, 2)) ?? null;
188
197
  }
189
198
  // Walk the Error.cause chain (bounded) and return the first non-null value the
190
199
  // reader extracts. drizzle wraps the pg error on `.cause`, and custom wrappers
@@ -5,7 +5,6 @@ export type WithTelemetrySpanOptions = {
5
5
  export declare function withTelemetrySpan<T>(tracerName: string, name: string, attributes: TelemetryAttributes | undefined, fn: () => Promise<T>, options?: WithTelemetrySpanOptions): Promise<T>;
6
6
  export declare function setActiveTelemetryAttributes(attributes: TelemetryAttributes): void;
7
7
  export declare function markActiveTelemetryError(message: string, attributes?: TelemetryAttributes): void;
8
- export declare function addTelemetryEvent(name: string, attributes?: TelemetryAttributes): void;
9
8
  export declare function recordTelemetryCounter(name: string, value?: number, attributes?: TelemetryAttributes): void;
10
9
  export declare function recordTelemetryHistogram(name: string, value: number, attributes?: TelemetryAttributes): void;
11
10
  export declare function commonTelemetryAttributes(attributes?: TelemetryAttributes): TelemetryAttributes;