@oxygen-agent/cli 1.982.3 → 1.1003.12

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 (127) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.d.ts +0 -2
  3. package/dist/admin-primary-providers-render.js +1 -1
  4. package/dist/browser-login.js +1 -4
  5. package/dist/command-manifest.d.ts +3 -2
  6. package/dist/command-manifest.js +10 -0
  7. package/dist/credentials.d.ts +1 -1
  8. package/dist/functions-commands.js +27 -7
  9. package/dist/help.d.ts +29 -0
  10. package/dist/help.js +139 -0
  11. package/dist/index.js +1875 -164
  12. package/dist/knowledge-mirror.d.ts +2 -2
  13. package/dist/runtime.d.ts +0 -15
  14. package/dist/runtime.js +1 -1
  15. package/dist/session.d.ts +4 -3
  16. package/dist/skills.d.ts +8 -7
  17. package/dist/skills.js +24 -10
  18. package/dist/transcript.d.ts +2 -1
  19. package/dist/ugc-commands.d.ts +3 -6
  20. package/dist/ugc-commands.js +2 -1200
  21. package/dist/util.d.ts +1 -1
  22. package/dist/util.js +1 -3
  23. package/node_modules/@oxygen/cli-ugc/dist/commands.d.ts +3 -0
  24. package/node_modules/@oxygen/cli-ugc/dist/commands.js +1178 -0
  25. package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +7 -0
  26. package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +25 -0
  27. package/node_modules/@oxygen/cli-ugc/dist/index.d.ts +14 -0
  28. package/node_modules/@oxygen/cli-ugc/dist/index.js +5 -0
  29. package/node_modules/@oxygen/cli-ugc/package.json +15 -0
  30. package/node_modules/@oxygen/formula/dist/coerce.d.ts +10 -0
  31. package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
  32. package/node_modules/@oxygen/formula/dist/expression.js +14 -1
  33. package/node_modules/@oxygen/formula/dist/formula-functions.js +136 -1
  34. package/node_modules/@oxygen/formula/dist/hash.d.ts +19 -0
  35. package/node_modules/@oxygen/formula/dist/hash.js +199 -0
  36. package/node_modules/@oxygen/formula/dist/index.d.ts +1 -0
  37. package/node_modules/@oxygen/formula/dist/index.js +1 -0
  38. package/node_modules/@oxygen/formula/dist/value-cleaners.d.ts +74 -0
  39. package/node_modules/@oxygen/formula/dist/value-cleaners.js +358 -0
  40. package/node_modules/@oxygen/shared/dist/array-utils.d.ts +5 -0
  41. package/node_modules/@oxygen/shared/dist/array-utils.js +11 -0
  42. package/node_modules/@oxygen/shared/dist/billing.d.ts +103 -47
  43. package/node_modules/@oxygen/shared/dist/billing.js +150 -40
  44. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +17 -0
  45. package/node_modules/@oxygen/shared/dist/capability-discovery.js +114 -16
  46. package/node_modules/@oxygen/shared/dist/column-autofill.d.ts +52 -0
  47. package/node_modules/@oxygen/shared/dist/column-autofill.js +80 -0
  48. package/node_modules/@oxygen/shared/dist/column-output-fields.js +14 -10
  49. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +108 -0
  50. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +545 -0
  51. package/node_modules/@oxygen/shared/dist/copilot-playbooks.d.ts +18 -0
  52. package/node_modules/@oxygen/shared/dist/copilot-playbooks.js +43 -0
  53. package/node_modules/@oxygen/shared/dist/copilot-skills.d.ts +15 -0
  54. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +31 -0
  55. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +41 -0
  56. package/node_modules/@oxygen/shared/dist/copilot-skills.js +6 -0
  57. package/node_modules/@oxygen/shared/dist/deploy-env.d.ts +74 -0
  58. package/node_modules/@oxygen/shared/dist/deploy-env.js +82 -0
  59. package/node_modules/@oxygen/shared/dist/dnc-rules.d.ts +130 -0
  60. package/node_modules/@oxygen/shared/dist/dnc-rules.js +221 -0
  61. package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +103 -0
  62. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +819 -0
  63. package/node_modules/@oxygen/shared/dist/error-message.d.ts +1 -0
  64. package/node_modules/@oxygen/shared/dist/error-message.js +3 -0
  65. package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -3
  66. package/node_modules/@oxygen/shared/dist/external-write-policy.d.ts +33 -0
  67. package/node_modules/@oxygen/shared/dist/external-write-policy.js +68 -0
  68. package/node_modules/@oxygen/shared/dist/format-percent.d.ts +8 -0
  69. package/node_modules/@oxygen/shared/dist/format-percent.js +13 -0
  70. package/node_modules/@oxygen/shared/dist/freemail-domains.d.ts +81 -0
  71. package/node_modules/@oxygen/shared/dist/freemail-domains.js +157 -0
  72. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.d.ts +1 -0
  73. package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.js +1 -1
  74. package/node_modules/@oxygen/shared/dist/index.d.ts +14 -0
  75. package/node_modules/@oxygen/shared/dist/index.js +14 -0
  76. package/node_modules/@oxygen/shared/dist/json-path.js +1 -3
  77. package/node_modules/@oxygen/shared/dist/knowledge-bases.js +1 -3
  78. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +17 -2
  79. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +28 -6
  80. package/node_modules/@oxygen/shared/dist/langfuse.d.ts +12 -1
  81. package/node_modules/@oxygen/shared/dist/langfuse.js +57 -8
  82. package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +32 -0
  83. package/node_modules/@oxygen/shared/dist/linkedin-countries.js +359 -0
  84. package/node_modules/@oxygen/shared/dist/log-sink-selector.d.ts +39 -0
  85. package/node_modules/@oxygen/shared/dist/log-sink-selector.js +56 -0
  86. package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
  87. package/node_modules/@oxygen/shared/dist/log.js +6 -1
  88. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +17 -0
  89. package/node_modules/@oxygen/shared/dist/object-storage.js +21 -0
  90. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +54 -0
  91. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +213 -0
  92. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +1 -0
  93. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +23 -22
  94. package/node_modules/@oxygen/shared/dist/plan-limits.js +45 -18
  95. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +48 -41
  96. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +36 -25
  97. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +22 -22
  98. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +40 -34
  99. package/node_modules/@oxygen/shared/dist/product-analytics-environment.js +9 -0
  100. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +15 -0
  101. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +15 -0
  102. package/node_modules/@oxygen/shared/dist/rate-window.d.ts +5 -0
  103. package/node_modules/@oxygen/shared/dist/rate-window.js +8 -0
  104. package/node_modules/@oxygen/shared/dist/research-output-contract.d.ts +33 -1
  105. package/node_modules/@oxygen/shared/dist/research-output-contract.js +65 -5
  106. package/node_modules/@oxygen/shared/dist/search-vocab.js +4 -5
  107. package/node_modules/@oxygen/shared/dist/select-options.js +6 -1
  108. package/node_modules/@oxygen/shared/dist/sequence-crm-events.d.ts +1 -1
  109. package/node_modules/@oxygen/shared/dist/sequence-failures.js +1 -5
  110. package/node_modules/@oxygen/shared/dist/sequence-hubspot-sync.d.ts +1 -1
  111. package/node_modules/@oxygen/shared/dist/sequences.d.ts +49 -0
  112. package/node_modules/@oxygen/shared/dist/sequences.js +134 -4
  113. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +22 -10
  114. package/node_modules/@oxygen/shared/dist/spend-safety.js +15 -21
  115. package/node_modules/@oxygen/shared/dist/sql-rows.d.ts +1 -0
  116. package/node_modules/@oxygen/shared/dist/sql-rows.js +3 -0
  117. package/node_modules/@oxygen/shared/dist/telemetry.js +9 -1
  118. package/node_modules/@oxygen/shared/dist/type-guards.d.ts +22 -0
  119. package/node_modules/@oxygen/shared/dist/type-guards.js +35 -0
  120. package/node_modules/@oxygen/shared/dist/value-readers.d.ts +21 -0
  121. package/node_modules/@oxygen/shared/dist/value-readers.js +59 -0
  122. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  123. package/node_modules/@oxygen/shared/package.json +60 -0
  124. package/node_modules/@oxygen/workflows/dist/graph/expression.js +2 -5
  125. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +15 -15
  126. package/node_modules/@oxygen/workflows/dist/graph/params.js +1 -1
  127. package/package.json +6 -3
@@ -38,6 +38,8 @@ export type LlmSpanBody = {
38
38
  userId?: string | null;
39
39
  };
40
40
  export type LlmGenerationBody = LlmSpanBody & {
41
+ /** Persist this physical id when a later annotation must target this attempt. */
42
+ observationId?: string;
41
43
  model?: string | null;
42
44
  modelParameters?: Record<string, unknown>;
43
45
  completionStartTime?: Date | null;
@@ -63,9 +65,11 @@ export type LlmEventBody = {
63
65
  * because that variant requires a *string* value. Widening later stays additive.
64
66
  */
65
67
  export type LlmScoreBody = {
66
- /** Deterministic id upserts; omit for a new score each call. */
68
+ /** Retry-safe scores need the same id, name AND timestamp on every attempt. */
67
69
  id?: string;
68
70
  traceId: string;
71
+ observationId?: string;
72
+ timestamp?: Date;
69
73
  name: string;
70
74
  value: number;
71
75
  dataType?: "BOOLEAN" | "NUMERIC";
@@ -103,6 +107,13 @@ export type LlmTracingClient = {
103
107
  */
104
108
  export declare function isLlmTracingEnabled(env?: EnvMap): boolean;
105
109
  export declare function resolveLlmTracingEnvironment(env?: EnvMap): string;
110
+ /**
111
+ * The private Langfuse provider must identify its own runtime rather than
112
+ * inherit the process-wide OTel resource. Fly identity wins because it is
113
+ * injected per machine and cannot drift; a local CLI/test process is neither
114
+ * the web app nor the worker.
115
+ */
116
+ export declare function resolveLlmTracingServiceName(env?: EnvMap): string;
106
117
  /**
107
118
  * Deterministic Langfuse trace id for an external seed (copilot turn id, agent
108
119
  * run id, AI-column run id).
@@ -32,7 +32,7 @@
32
32
  // * Trace-level input/output is deprecated in v5. Overall IO goes on the ROOT
33
33
  // observation instead; setTraceIO()/setActiveTraceIO() are deliberately not
34
34
  // used here.
35
- import { createHash, randomBytes } from "node:crypto";
35
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
36
36
  import { OXYGEN_VERSION } from "./version.js";
37
37
  import { prepareLlmPayload } from "./llm-payload.js";
38
38
  import { log } from "./log.js";
@@ -73,6 +73,15 @@ export function resolveLlmTracingEnvironment(env = process.env) {
73
73
  const explicit = env.LANGFUSE_TRACING_ENVIRONMENT?.trim().toLowerCase();
74
74
  if (explicit)
75
75
  return explicit.replace(/[^a-z0-9_-]/g, "-");
76
+ // Self-hosted first: those processes carry neither VERCEL_ENV nor a Fly app
77
+ // name, so without this every self-hosted tier would report "development".
78
+ // selfhost-shadow stays "development" deliberately — production-shaped data,
79
+ // not production traffic.
80
+ const deployEnv = env.OXYGEN_DEPLOY_ENV?.trim().toLowerCase();
81
+ if (deployEnv === "selfhost-prod")
82
+ return "production";
83
+ if (deployEnv === "selfhost-shadow" || deployEnv === "selfhost-dev")
84
+ return "development";
76
85
  if (env.VERCEL_ENV)
77
86
  return env.VERCEL_ENV === "production" ? "production" : "development";
78
87
  const flyApp = env.FLY_APP_NAME ?? env.FLY_APP;
@@ -83,6 +92,23 @@ export function resolveLlmTracingEnvironment(env = process.env) {
83
92
  return flyEnv === "production" ? "production" : "development";
84
93
  return "development";
85
94
  }
95
+ /**
96
+ * The private Langfuse provider must identify its own runtime rather than
97
+ * inherit the process-wide OTel resource. Fly identity wins because it is
98
+ * injected per machine and cannot drift; a local CLI/test process is neither
99
+ * the web app nor the worker.
100
+ */
101
+ export function resolveLlmTracingServiceName(env = process.env) {
102
+ const flyApp = env.FLY_APP_NAME?.trim() || env.FLY_APP?.trim();
103
+ if (flyApp)
104
+ return flyApp.includes("-dev") ? "oxygen-worker-dev" : "oxygen-worker";
105
+ const flyEnvironment = env.FLY_ENVIRONMENT?.trim().toLowerCase();
106
+ if (flyEnvironment)
107
+ return flyEnvironment === "production" ? "oxygen-worker" : "oxygen-worker-dev";
108
+ if (env.VERCEL?.trim() || env.VERCEL_ENV?.trim())
109
+ return "oxygen-web";
110
+ return "oxygen-local";
111
+ }
86
112
  /**
87
113
  * Deterministic Langfuse trace id for an external seed (copilot turn id, agent
88
114
  * run id, AI-column run id).
@@ -140,8 +166,9 @@ function createOtelEmitter(env, warn) {
140
166
  const init = () => {
141
167
  handle ??= (async () => {
142
168
  try {
143
- const [{ LangfuseSpanProcessor }, { BasicTracerProvider, AlwaysOnSampler }, tracing, otel, core] = await Promise.all([
169
+ const [{ LangfuseSpanProcessor }, { BasicTracerProvider, AlwaysOnSampler }, { resourceFromAttributes }, tracing, otel, core] = await Promise.all([
144
170
  import("@langfuse/otel"), import("@opentelemetry/sdk-trace-base"),
171
+ import("@opentelemetry/resources"),
145
172
  import("@langfuse/tracing"), import("@opentelemetry/api"), import("@langfuse/core"),
146
173
  ]);
147
174
  const processor = new LangfuseSpanProcessor({
@@ -160,6 +187,11 @@ function createOtelEmitter(env, warn) {
160
187
  });
161
188
  let identity;
162
189
  const provider = new BasicTracerProvider({
190
+ // This provider is deliberately isolated from the application's
191
+ // global OTel pipeline, so it also needs its own explicit resource.
192
+ // Never trust OTEL_SERVICE_NAME here: a cross-environment Doppler
193
+ // value must not relabel prompt-bearing Langfuse observations.
194
+ resource: resourceFromAttributes({ "service.name": resolveLlmTracingServiceName(env) }),
163
195
  spanProcessors: [{
164
196
  onStart: (span) => {
165
197
  // Directly pass the private parent claim to the processor. A
@@ -266,7 +298,7 @@ function createApiScorer(env, warn) {
266
298
  username: env.LANGFUSE_PUBLIC_KEY,
267
299
  password: env.LANGFUSE_SECRET_KEY,
268
300
  });
269
- return client.scores;
301
+ return client;
270
302
  }
271
303
  catch (error) {
272
304
  warn(error, { stage: "score_init" });
@@ -281,15 +313,31 @@ function createApiScorer(env, warn) {
281
313
  const api = await init();
282
314
  if (!api)
283
315
  return false;
284
- await api.create(compact({
285
- id: body.id,
316
+ const payload = {
317
+ ...(body.id ? { id: body.id } : {}),
286
318
  traceId: body.traceId,
319
+ ...(body.observationId ? { observationId: body.observationId } : {}),
287
320
  name: body.name,
288
321
  value: body.value,
289
- dataType: body.dataType,
290
- comment: typeof body.comment === "string" ? boundComment(body.comment) : undefined,
322
+ ...(body.dataType ? { dataType: body.dataType } : {}),
323
+ ...(typeof body.comment === "string" ? { comment: boundComment(body.comment) } : {}),
291
324
  environment: resolveLlmTracingEnvironment(env),
292
- }));
325
+ };
326
+ const requestOptions = { timeoutInSeconds: 5, maxRetries: 0 };
327
+ if (body.timestamp) {
328
+ // POST /scores cannot carry a timestamp. Score ingestion remains
329
+ // supported in v4 (unlike trace/observation ingestion) and preserves
330
+ // the date component of the score's deduplication identity.
331
+ if (!body.id || !Number.isFinite(body.timestamp.getTime()))
332
+ return false;
333
+ const eventId = randomUUID();
334
+ const result = await api.ingestion.batch({ batch: [{
335
+ type: "score-create", id: eventId, timestamp: body.timestamp.toISOString(), body: payload,
336
+ }] }, requestOptions);
337
+ // A 207 is transport success even if the event was rejected.
338
+ return result.errors.length === 0 && result.successes.some((event) => event.id === eventId && event.status >= 200 && event.status < 300);
339
+ }
340
+ await api.scores.create(payload, requestOptions);
293
341
  return true;
294
342
  }
295
343
  catch (error) {
@@ -398,6 +446,7 @@ export function createLlmTracingClient(env = process.env, options) {
398
446
  emitter.emit({
399
447
  kind,
400
448
  traceSeed: body.traceId,
449
+ ...(body.observationId ? { observationId: body.observationId } : {}),
401
450
  name: body.name,
402
451
  attributes: compact({
403
452
  input: body.input,
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The country vocabulary behind the CRM's "Primary Location — Country" column.
3
+ *
4
+ * WHY A CHECKED-IN TABLE AND NOT `Intl.DisplayNames` AT RUNTIME: these strings
5
+ * are persisted. They become `definition.options[].value` on the column and the
6
+ * literal text stored in every cell, so deriving them from ICU would let a Node
7
+ * upgrade silently rename an option and orphan every row that already carried
8
+ * the old spelling. The table was GENERATED from CLDR English region names once
9
+ * (the 249 officially assigned ISO 3166-1 alpha-2 codes, plus XK/Kosovo, which
10
+ * LinkedIn lists and ISO does not), then frozen here with the overrides below.
11
+ *
12
+ * NAMING: LinkedIn spells a handful differently from CLDR's English short name
13
+ * — those are the entries carrying a trailing comment. Everything else is the
14
+ * CLDR name verbatim, which is what LinkedIn uses. This is a faithful
15
+ * reproduction of LinkedIn's vocabulary, not a scrape of its dropdown: the
16
+ * RESOLVER below is what makes that safe, because it accepts the alpha-2 code,
17
+ * any spelling in this table, and the aliases in COUNTRY_ALIASES. A scrape that
18
+ * returns "US", "United States", "USA" or "Congo - Kinshasa" all land on the
19
+ * one canonical option rather than on an empty cell.
20
+ */
21
+ /** Canonical LinkedIn country name for each ISO 3166-1 alpha-2 code, A→Z by name. */
22
+ export declare const LINKEDIN_COUNTRIES: ReadonlyArray<readonly [code: string, name: string]>;
23
+ /**
24
+ * Resolve a provider value — an alpha-2 code, a canonical name, or a known
25
+ * alias — to its canonical LinkedIn country name.
26
+ *
27
+ * Returns null for anything unrecognized, which is deliberate: the column is a
28
+ * select, so writing a value that is not one of its options would produce a
29
+ * cell no filter can match and no dropdown can show. An empty cell is the
30
+ * honest outcome, and the raw payload is still on the record.
31
+ */
32
+ export declare function resolveLinkedInCountry(value: unknown): string | null;
@@ -0,0 +1,359 @@
1
+ /**
2
+ * The country vocabulary behind the CRM's "Primary Location — Country" column.
3
+ *
4
+ * WHY A CHECKED-IN TABLE AND NOT `Intl.DisplayNames` AT RUNTIME: these strings
5
+ * are persisted. They become `definition.options[].value` on the column and the
6
+ * literal text stored in every cell, so deriving them from ICU would let a Node
7
+ * upgrade silently rename an option and orphan every row that already carried
8
+ * the old spelling. The table was GENERATED from CLDR English region names once
9
+ * (the 249 officially assigned ISO 3166-1 alpha-2 codes, plus XK/Kosovo, which
10
+ * LinkedIn lists and ISO does not), then frozen here with the overrides below.
11
+ *
12
+ * NAMING: LinkedIn spells a handful differently from CLDR's English short name
13
+ * — those are the entries carrying a trailing comment. Everything else is the
14
+ * CLDR name verbatim, which is what LinkedIn uses. This is a faithful
15
+ * reproduction of LinkedIn's vocabulary, not a scrape of its dropdown: the
16
+ * RESOLVER below is what makes that safe, because it accepts the alpha-2 code,
17
+ * any spelling in this table, and the aliases in COUNTRY_ALIASES. A scrape that
18
+ * returns "US", "United States", "USA" or "Congo - Kinshasa" all land on the
19
+ * one canonical option rather than on an empty cell.
20
+ */
21
+ /** Canonical LinkedIn country name for each ISO 3166-1 alpha-2 code, A→Z by name. */
22
+ export const LINKEDIN_COUNTRIES = [
23
+ ["AF", "Afghanistan"],
24
+ ["AX", "Åland Islands"],
25
+ ["AL", "Albania"],
26
+ ["DZ", "Algeria"],
27
+ ["AS", "American Samoa"],
28
+ ["AD", "Andorra"],
29
+ ["AO", "Angola"],
30
+ ["AI", "Anguilla"],
31
+ ["AQ", "Antarctica"],
32
+ ["AG", "Antigua & Barbuda"],
33
+ ["AR", "Argentina"],
34
+ ["AM", "Armenia"],
35
+ ["AW", "Aruba"],
36
+ ["AU", "Australia"],
37
+ ["AT", "Austria"],
38
+ ["AZ", "Azerbaijan"],
39
+ ["BS", "Bahamas"],
40
+ ["BH", "Bahrain"],
41
+ ["BD", "Bangladesh"],
42
+ ["BB", "Barbados"],
43
+ ["BY", "Belarus"],
44
+ ["BE", "Belgium"],
45
+ ["BZ", "Belize"],
46
+ ["BJ", "Benin"],
47
+ ["BM", "Bermuda"],
48
+ ["BT", "Bhutan"],
49
+ ["BO", "Bolivia"],
50
+ ["BA", "Bosnia & Herzegovina"],
51
+ ["BW", "Botswana"],
52
+ ["BV", "Bouvet Island"],
53
+ ["BR", "Brazil"],
54
+ ["IO", "British Indian Ocean Territory"],
55
+ ["VG", "British Virgin Islands"],
56
+ ["BN", "Brunei"],
57
+ ["BG", "Bulgaria"],
58
+ ["BF", "Burkina Faso"],
59
+ ["BI", "Burundi"],
60
+ ["KH", "Cambodia"],
61
+ ["CM", "Cameroon"],
62
+ ["CA", "Canada"],
63
+ ["CV", "Cape Verde"],
64
+ ["BQ", "Caribbean Netherlands"],
65
+ ["KY", "Cayman Islands"],
66
+ ["CF", "Central African Republic"],
67
+ ["TD", "Chad"],
68
+ ["CL", "Chile"],
69
+ ["CN", "China"],
70
+ ["CX", "Christmas Island"],
71
+ ["CC", "Cocos (Keeling) Islands"],
72
+ ["CO", "Colombia"],
73
+ ["KM", "Comoros"],
74
+ ["CD", "Congo (DRC)"],
75
+ ["CG", "Congo (Republic)"],
76
+ ["CK", "Cook Islands"],
77
+ ["CR", "Costa Rica"],
78
+ ["CI", "Côte d'Ivoire"],
79
+ ["HR", "Croatia"],
80
+ ["CU", "Cuba"],
81
+ ["CW", "Curaçao"],
82
+ ["CY", "Cyprus"],
83
+ ["CZ", "Czechia"],
84
+ ["DK", "Denmark"],
85
+ ["DJ", "Djibouti"],
86
+ ["DM", "Dominica"],
87
+ ["DO", "Dominican Republic"],
88
+ ["EC", "Ecuador"],
89
+ ["EG", "Egypt"],
90
+ ["SV", "El Salvador"],
91
+ ["GQ", "Equatorial Guinea"],
92
+ ["ER", "Eritrea"],
93
+ ["EE", "Estonia"],
94
+ ["SZ", "Eswatini"],
95
+ ["ET", "Ethiopia"],
96
+ ["FK", "Falkland Islands"],
97
+ ["FO", "Faroe Islands"],
98
+ ["FJ", "Fiji"],
99
+ ["FI", "Finland"],
100
+ ["FR", "France"],
101
+ ["GF", "French Guiana"],
102
+ ["PF", "French Polynesia"],
103
+ ["TF", "French Southern Territories"],
104
+ ["GA", "Gabon"],
105
+ ["GM", "Gambia"],
106
+ ["GE", "Georgia"],
107
+ ["DE", "Germany"],
108
+ ["GH", "Ghana"],
109
+ ["GI", "Gibraltar"],
110
+ ["GR", "Greece"],
111
+ ["GL", "Greenland"],
112
+ ["GD", "Grenada"],
113
+ ["GP", "Guadeloupe"],
114
+ ["GU", "Guam"],
115
+ ["GT", "Guatemala"],
116
+ ["GG", "Guernsey"],
117
+ ["GN", "Guinea"],
118
+ ["GW", "Guinea-Bissau"],
119
+ ["GY", "Guyana"],
120
+ ["HT", "Haiti"],
121
+ ["HM", "Heard & McDonald Islands"],
122
+ ["HN", "Honduras"],
123
+ ["HK", "Hong Kong SAR"],
124
+ ["HU", "Hungary"],
125
+ ["IS", "Iceland"],
126
+ ["IN", "India"],
127
+ ["ID", "Indonesia"],
128
+ ["IR", "Iran"],
129
+ ["IQ", "Iraq"],
130
+ ["IE", "Ireland"],
131
+ ["IM", "Isle of Man"],
132
+ ["IL", "Israel"],
133
+ ["IT", "Italy"],
134
+ ["JM", "Jamaica"],
135
+ ["JP", "Japan"],
136
+ ["JE", "Jersey"],
137
+ ["JO", "Jordan"],
138
+ ["KZ", "Kazakhstan"],
139
+ ["KE", "Kenya"],
140
+ ["KI", "Kiribati"],
141
+ ["XK", "Kosovo"],
142
+ ["KW", "Kuwait"],
143
+ ["KG", "Kyrgyzstan"],
144
+ ["LA", "Laos"],
145
+ ["LV", "Latvia"],
146
+ ["LB", "Lebanon"],
147
+ ["LS", "Lesotho"],
148
+ ["LR", "Liberia"],
149
+ ["LY", "Libya"],
150
+ ["LI", "Liechtenstein"],
151
+ ["LT", "Lithuania"],
152
+ ["LU", "Luxembourg"],
153
+ ["MO", "Macao SAR"],
154
+ ["MG", "Madagascar"],
155
+ ["MW", "Malawi"],
156
+ ["MY", "Malaysia"],
157
+ ["MV", "Maldives"],
158
+ ["ML", "Mali"],
159
+ ["MT", "Malta"],
160
+ ["MH", "Marshall Islands"],
161
+ ["MQ", "Martinique"],
162
+ ["MR", "Mauritania"],
163
+ ["MU", "Mauritius"],
164
+ ["YT", "Mayotte"],
165
+ ["MX", "Mexico"],
166
+ ["FM", "Micronesia"],
167
+ ["MD", "Moldova"],
168
+ ["MC", "Monaco"],
169
+ ["MN", "Mongolia"],
170
+ ["ME", "Montenegro"],
171
+ ["MS", "Montserrat"],
172
+ ["MA", "Morocco"],
173
+ ["MZ", "Mozambique"],
174
+ ["MM", "Myanmar"],
175
+ ["NA", "Namibia"],
176
+ ["NR", "Nauru"],
177
+ ["NP", "Nepal"],
178
+ ["NL", "Netherlands"],
179
+ ["NC", "New Caledonia"],
180
+ ["NZ", "New Zealand"],
181
+ ["NI", "Nicaragua"],
182
+ ["NE", "Niger"],
183
+ ["NG", "Nigeria"],
184
+ ["NU", "Niue"],
185
+ ["NF", "Norfolk Island"],
186
+ ["KP", "North Korea"],
187
+ ["MK", "North Macedonia"],
188
+ ["MP", "Northern Mariana Islands"],
189
+ ["NO", "Norway"],
190
+ ["OM", "Oman"],
191
+ ["PK", "Pakistan"],
192
+ ["PW", "Palau"],
193
+ ["PS", "Palestinian Territories"],
194
+ ["PA", "Panama"],
195
+ ["PG", "Papua New Guinea"],
196
+ ["PY", "Paraguay"],
197
+ ["PE", "Peru"],
198
+ ["PH", "Philippines"],
199
+ ["PN", "Pitcairn Islands"],
200
+ ["PL", "Poland"],
201
+ ["PT", "Portugal"],
202
+ ["PR", "Puerto Rico"],
203
+ ["QA", "Qatar"],
204
+ ["RE", "Réunion"],
205
+ ["RO", "Romania"],
206
+ ["RU", "Russia"],
207
+ ["RW", "Rwanda"],
208
+ ["WS", "Samoa"],
209
+ ["SM", "San Marino"],
210
+ ["ST", "São Tomé & Príncipe"],
211
+ ["SA", "Saudi Arabia"],
212
+ ["SN", "Senegal"],
213
+ ["RS", "Serbia"],
214
+ ["SC", "Seychelles"],
215
+ ["SL", "Sierra Leone"],
216
+ ["SG", "Singapore"],
217
+ ["SX", "Sint Maarten"],
218
+ ["SK", "Slovakia"],
219
+ ["SI", "Slovenia"],
220
+ ["SB", "Solomon Islands"],
221
+ ["SO", "Somalia"],
222
+ ["ZA", "South Africa"],
223
+ ["GS", "South Georgia & South Sandwich Islands"],
224
+ ["KR", "South Korea"],
225
+ ["SS", "South Sudan"],
226
+ ["ES", "Spain"],
227
+ ["LK", "Sri Lanka"],
228
+ ["BL", "St. Barthélemy"],
229
+ ["SH", "St. Helena"],
230
+ ["KN", "St. Kitts & Nevis"],
231
+ ["LC", "St. Lucia"],
232
+ ["MF", "St. Martin"],
233
+ ["PM", "St. Pierre & Miquelon"],
234
+ ["VC", "St. Vincent & Grenadines"],
235
+ ["SD", "Sudan"],
236
+ ["SR", "Suriname"],
237
+ ["SJ", "Svalbard & Jan Mayen"],
238
+ ["SE", "Sweden"],
239
+ ["CH", "Switzerland"],
240
+ ["SY", "Syria"],
241
+ ["TW", "Taiwan"],
242
+ ["TJ", "Tajikistan"],
243
+ ["TZ", "Tanzania"],
244
+ ["TH", "Thailand"],
245
+ ["TL", "Timor-Leste"],
246
+ ["TG", "Togo"],
247
+ ["TK", "Tokelau"],
248
+ ["TO", "Tonga"],
249
+ ["TT", "Trinidad & Tobago"],
250
+ ["TN", "Tunisia"],
251
+ ["TR", "Türkiye"],
252
+ ["TM", "Turkmenistan"],
253
+ ["TC", "Turks & Caicos Islands"],
254
+ ["TV", "Tuvalu"],
255
+ ["UM", "U.S. Outlying Islands"],
256
+ ["VI", "U.S. Virgin Islands"],
257
+ ["UG", "Uganda"],
258
+ ["UA", "Ukraine"],
259
+ ["AE", "United Arab Emirates"],
260
+ ["GB", "United Kingdom"],
261
+ ["US", "United States"],
262
+ ["UY", "Uruguay"],
263
+ ["UZ", "Uzbekistan"],
264
+ ["VU", "Vanuatu"],
265
+ ["VA", "Vatican City"],
266
+ ["VE", "Venezuela"],
267
+ ["VN", "Vietnam"],
268
+ ["WF", "Wallis & Futuna"],
269
+ ["EH", "Western Sahara"],
270
+ ["YE", "Yemen"],
271
+ ["ZM", "Zambia"],
272
+ ["ZW", "Zimbabwe"],
273
+ ];
274
+ /**
275
+ * Spellings a provider may return that are not the canonical name. Lowercased
276
+ * keys; the resolver lowercases its input before the lookup. Kept deliberately
277
+ * short — it covers the CLDR spellings this table overrides (so a payload
278
+ * carrying the un-overridden name still resolves), the common colloquial forms,
279
+ * and the alpha-3 codes, because Up2Data's `hq.country` is not contractually
280
+ * alpha-2 and has been observed carrying a display name.
281
+ */
282
+ const COUNTRY_ALIASES = {
283
+ // CLDR spellings this table overrides.
284
+ "hong kong sar china": "HK",
285
+ "macao sar china": "MO",
286
+ "macau sar china": "MO",
287
+ "myanmar (burma)": "MM",
288
+ "congo - kinshasa": "CD",
289
+ "congo - brazzaville": "CG",
290
+ "côte d’ivoire": "CI",
291
+ "st. helena": "SH",
292
+ // Colloquial and legacy forms.
293
+ usa: "US",
294
+ "u.s.": "US",
295
+ "u.s.a.": "US",
296
+ "united states of america": "US",
297
+ uk: "GB",
298
+ "u.k.": "GB",
299
+ "great britain": "GB",
300
+ england: "GB",
301
+ scotland: "GB",
302
+ wales: "GB",
303
+ "northern ireland": "GB",
304
+ "czech republic": "CZ",
305
+ turkey: "TR",
306
+ "ivory coast": "CI",
307
+ burma: "MM",
308
+ swaziland: "SZ",
309
+ macedonia: "MK",
310
+ holland: "NL",
311
+ "the netherlands": "NL",
312
+ "south korea": "KR",
313
+ "korea, republic of": "KR",
314
+ "republic of korea": "KR",
315
+ "north korea": "KP",
316
+ "russian federation": "RU",
317
+ vatican: "VA",
318
+ "holy see": "VA",
319
+ palestine: "PS",
320
+ laos: "LA",
321
+ "cape verde": "CV",
322
+ "east timor": "TL",
323
+ uae: "AE",
324
+ "democratic republic of the congo": "CD",
325
+ "republic of the congo": "CG",
326
+ "hong kong": "HK",
327
+ macao: "MO",
328
+ macau: "MO",
329
+ };
330
+ const BY_CODE = new Map(LINKEDIN_COUNTRIES.map(([code, name]) => [code, name]));
331
+ const BY_LOWER_NAME = new Map(LINKEDIN_COUNTRIES.map(([code, name]) => [name.toLowerCase(), code]));
332
+ /**
333
+ * Resolve a provider value — an alpha-2 code, a canonical name, or a known
334
+ * alias — to its canonical LinkedIn country name.
335
+ *
336
+ * Returns null for anything unrecognized, which is deliberate: the column is a
337
+ * select, so writing a value that is not one of its options would produce a
338
+ * cell no filter can match and no dropdown can show. An empty cell is the
339
+ * honest outcome, and the raw payload is still on the record.
340
+ */
341
+ export function resolveLinkedInCountry(value) {
342
+ if (typeof value !== "string")
343
+ return null;
344
+ const trimmed = value.trim();
345
+ if (!trimmed)
346
+ return null;
347
+ const upper = trimmed.toUpperCase();
348
+ if (upper.length === 2) {
349
+ const direct = BY_CODE.get(upper);
350
+ if (direct)
351
+ return direct;
352
+ }
353
+ const lower = trimmed.toLowerCase();
354
+ const byName = BY_LOWER_NAME.get(lower);
355
+ if (byName)
356
+ return BY_CODE.get(byName) ?? null;
357
+ const aliased = COUNTRY_ALIASES[lower];
358
+ return aliased ? BY_CODE.get(aliased) ?? null : null;
359
+ }
@@ -0,0 +1,39 @@
1
+ import type { LogSink } from "./log.js";
2
+ /**
3
+ * Which transport(s) `log()` records leave the process on.
4
+ *
5
+ * Portability selector (`.agents/skills/oxygen-platform-portability/SKILL.md`):
6
+ * one concern, one variable, and **unset means managed**. An environment that
7
+ * sets nothing keeps today's behaviour exactly — the Axiom shipper alone, with
8
+ * the same handle and the same drain — so this can land on `dev` and flow to
9
+ * managed production with no flag day. Self-hosted opts in with `otlp` (an
10
+ * OTLP/HTTP collector instead of Axiom) or `both` (during a cutover window,
11
+ * when the two stores must be comparable before Axiom is switched off).
12
+ *
13
+ * An unrecognised value resolves to `axiom` rather than throwing: a typo in a
14
+ * telemetry variable must not decide whether a process boots.
15
+ */
16
+ export type LogSinkSelector = "axiom" | "otlp" | "both";
17
+ export declare function resolveLogSinkSelector(env?: NodeJS.ProcessEnv): LogSinkSelector;
18
+ export declare function logSinkSelectorIncludesAxiom(selector: LogSinkSelector): boolean;
19
+ export declare function logSinkSelectorIncludesOtlp(selector: LogSinkSelector): boolean;
20
+ /**
21
+ * Failed sink invocations swallowed by a composed sink since process start.
22
+ *
23
+ * A composed sink cannot report its own failure through `log()` — it IS what
24
+ * `log()` calls, so logging from it would recurse. The count is the honest
25
+ * alternative to silence: readable from a diagnostic surface without the
26
+ * failing transport ever getting a second chance to throw.
27
+ */
28
+ export declare function composedLogSinkFailureCount(): number;
29
+ /**
30
+ * Fans one record out to several transports.
31
+ *
32
+ * Every sink is called, in order, and each is wrapped: `both` exists precisely
33
+ * for the window where one of the two stores is not trusted yet, so a throwing
34
+ * Axiom sink must not stop the OTLP one from receiving the record (or the
35
+ * reverse). Nulls are dropped, so a caller can pass an adapter that declined to
36
+ * start without branching; all-null composes to `null`, which `setLogSink`
37
+ * reads as "no sink" and which keeps `log()` on its plain stdout path.
38
+ */
39
+ export declare function composeLogSinks(...sinks: Array<LogSink | null | undefined>): LogSink | null;
@@ -0,0 +1,56 @@
1
+ export function resolveLogSinkSelector(env = process.env) {
2
+ const configured = env.OXYGEN_LOG_SINK?.trim().toLowerCase();
3
+ if (configured === "otlp")
4
+ return "otlp";
5
+ if (configured === "both")
6
+ return "both";
7
+ return "axiom";
8
+ }
9
+ export function logSinkSelectorIncludesAxiom(selector) {
10
+ return selector === "axiom" || selector === "both";
11
+ }
12
+ export function logSinkSelectorIncludesOtlp(selector) {
13
+ return selector === "otlp" || selector === "both";
14
+ }
15
+ let composedSinkFailures = 0;
16
+ /**
17
+ * Failed sink invocations swallowed by a composed sink since process start.
18
+ *
19
+ * A composed sink cannot report its own failure through `log()` — it IS what
20
+ * `log()` calls, so logging from it would recurse. The count is the honest
21
+ * alternative to silence: readable from a diagnostic surface without the
22
+ * failing transport ever getting a second chance to throw.
23
+ */
24
+ export function composedLogSinkFailureCount() {
25
+ return composedSinkFailures;
26
+ }
27
+ /**
28
+ * Fans one record out to several transports.
29
+ *
30
+ * Every sink is called, in order, and each is wrapped: `both` exists precisely
31
+ * for the window where one of the two stores is not trusted yet, so a throwing
32
+ * Axiom sink must not stop the OTLP one from receiving the record (or the
33
+ * reverse). Nulls are dropped, so a caller can pass an adapter that declined to
34
+ * start without branching; all-null composes to `null`, which `setLogSink`
35
+ * reads as "no sink" and which keeps `log()` on its plain stdout path.
36
+ */
37
+ export function composeLogSinks(...sinks) {
38
+ const active = sinks.filter((sink) => typeof sink === "function");
39
+ if (active.length === 0)
40
+ return null;
41
+ const only = active[0];
42
+ if (active.length === 1 && only)
43
+ return only;
44
+ return (record) => {
45
+ for (const sink of active) {
46
+ try {
47
+ sink(record);
48
+ }
49
+ catch {
50
+ // Telemetry must never break the code it observes, and this is the one
51
+ // place that cannot say so out loud (see composedLogSinkFailureCount).
52
+ composedSinkFailures += 1;
53
+ }
54
+ }
55
+ };
56
+ }
@@ -19,6 +19,7 @@ export declare function enterLogContext(ctx: LogContext): void;
19
19
  export declare function logLevelForHttpStatus(status: number): LogLevel;
20
20
  export type LogSink = (record: Record<string, unknown>) => void;
21
21
  export declare function setLogSink(sink: LogSink | null): void;
22
+ export declare function getLogSink(): LogSink | null;
22
23
  export declare function log(level: LogLevel, msg: string, fields?: Record<string, unknown>): void;
23
24
  export declare function errorId(err: unknown): string;
24
25
  export declare function errorFields(err: unknown): Record<string, unknown>;
@@ -118,7 +118,12 @@ const LOG_SINK_SLOT = Symbol.for("oxygen.log.sink");
118
118
  export function setLogSink(sink) {
119
119
  globalThis[LOG_SINK_SLOT] = sink;
120
120
  }
121
- function getLogSink() {
121
+ // Exported so a second transport can COMPOSE with the sink already registered
122
+ // rather than replacing it: under `OXYGEN_LOG_SINK=both` the OTLP adapter starts
123
+ // after the Axiom shipper has claimed the slot, and reading the slot back is what
124
+ // lets it fan out to both (`log-sink-selector.ts`). Reading through this function,
125
+ // not the raw Symbol, keeps the slot's location owned by exactly one module.
126
+ export function getLogSink() {
122
127
  return globalThis[LOG_SINK_SLOT] ?? null;
123
128
  }
124
129
  const LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40 };