@oxygen-agent/cli 1.936.1 → 1.982.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.js +9 -1
  3. package/dist/cli-values.d.ts +14 -0
  4. package/dist/cli-values.js +26 -0
  5. package/dist/command-manifest.js +30 -2
  6. package/dist/functions-commands.js +13 -5
  7. package/dist/help.js +2 -0
  8. package/dist/index.js +1509 -290
  9. package/dist/knowledge-repository-commands.d.ts +6 -0
  10. package/dist/knowledge-repository-commands.js +198 -0
  11. package/dist/skills.js +20 -0
  12. package/dist/ugc-commands.js +470 -15
  13. package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +2 -0
  14. package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +11 -6
  15. package/node_modules/@oxygen/shared/dist/byok-connect.js +14 -6
  16. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -0
  17. package/node_modules/@oxygen/shared/dist/capability-discovery.js +152 -20
  18. package/node_modules/@oxygen/shared/dist/copilot-errors.js +3 -0
  19. package/node_modules/@oxygen/shared/dist/copilot-journeys.d.ts +19 -1
  20. package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.d.ts +19 -0
  21. package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.js +26 -0
  22. package/node_modules/@oxygen/shared/dist/copilot-journeys.js +8 -41
  23. package/node_modules/@oxygen/shared/dist/email-dsn.d.ts +60 -0
  24. package/node_modules/@oxygen/shared/dist/email-dsn.js +120 -0
  25. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.d.ts +64 -0
  26. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.js +90 -0
  27. package/node_modules/@oxygen/shared/dist/inbox-avatar-url.d.ts +28 -0
  28. package/node_modules/@oxygen/shared/dist/inbox-avatar-url.js +57 -0
  29. package/node_modules/@oxygen/shared/dist/index.d.ts +10 -0
  30. package/node_modules/@oxygen/shared/dist/index.js +10 -0
  31. package/node_modules/@oxygen/shared/dist/knowledge-bases.d.ts +74 -0
  32. package/node_modules/@oxygen/shared/dist/knowledge-bases.js +456 -0
  33. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +56 -48
  34. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +50 -49
  35. package/node_modules/@oxygen/shared/dist/knowledge-repository.d.ts +22 -0
  36. package/node_modules/@oxygen/shared/dist/knowledge-repository.js +121 -0
  37. package/node_modules/@oxygen/shared/dist/knowledge-vault-markdown.d.ts +20 -0
  38. package/node_modules/@oxygen/shared/dist/knowledge-vault-markdown.js +155 -0
  39. package/node_modules/@oxygen/shared/dist/langfuse.d.ts +8 -3
  40. package/node_modules/@oxygen/shared/dist/langfuse.js +177 -130
  41. package/node_modules/@oxygen/shared/dist/llm-payload.d.ts +10 -0
  42. package/node_modules/@oxygen/shared/dist/llm-payload.js +54 -0
  43. package/node_modules/@oxygen/shared/dist/llm-usage.d.ts +11 -0
  44. package/node_modules/@oxygen/shared/dist/llm-usage.js +30 -0
  45. package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +10 -0
  46. package/node_modules/@oxygen/shared/dist/mailbox-import.js +53 -0
  47. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +8 -0
  48. package/node_modules/@oxygen/shared/dist/plan-limits.js +8 -0
  49. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
  50. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
  51. package/node_modules/@oxygen/shared/dist/product-analytics-core.d.ts +98 -0
  52. package/node_modules/@oxygen/shared/dist/product-analytics-core.js +159 -0
  53. package/node_modules/@oxygen/shared/dist/product-analytics-environment.d.ts +18 -0
  54. package/node_modules/@oxygen/shared/dist/product-analytics-environment.js +46 -0
  55. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +116 -0
  56. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +120 -0
  57. package/node_modules/@oxygen/shared/dist/recipes.d.ts +6 -0
  58. package/node_modules/@oxygen/shared/dist/recipes.js +23 -0
  59. package/node_modules/@oxygen/shared/dist/sequences.d.ts +126 -2
  60. package/node_modules/@oxygen/shared/dist/sequences.js +280 -4
  61. package/node_modules/@oxygen/shared/dist/ugc-amplification-identity.d.ts +2 -0
  62. package/node_modules/@oxygen/shared/dist/ugc-amplification-identity.js +24 -0
  63. package/node_modules/@oxygen/shared/dist/ugc.d.ts +29 -1
  64. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +8 -1
  65. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  66. package/node_modules/@oxygen/shared/dist/version.js +3 -1
  67. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +6 -2
  68. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +15 -4
  69. package/node_modules/@oxygen/shared/package.json +15 -0
  70. package/node_modules/@oxygen/workflows/dist/graph/lint.js +22 -0
  71. package/package.json +2 -1
@@ -21,6 +21,14 @@ export const TABLE_IMPORT_ROW_LIMIT = WORKSPACE_TABLE_CAPACITY.tableRowLimit;
21
21
  // ceilings below ("how big can an import be"), but it must stay importable from a
22
22
  // browser bundle, so it is defined in ./import-limits and re-exported here.
23
23
  export { VERCEL_REQUEST_BODY_LIMIT_BYTES } from "./import-limits.js";
24
+ /**
25
+ * OXYGEN's own JSON-body ceiling for every `/api/cli/*` route (enforced by
26
+ * `assertCliJsonBodyWithinLimit` on the content-length header). Shared so the CLI
27
+ * pre-splits a row batch by measured bytes instead of learning the number from a
28
+ * 413. Must stay below VERCEL_REQUEST_BODY_LIMIT_BYTES, and must never be raised
29
+ * in the CLI alone — an older server would still 413 at the old number.
30
+ */
31
+ export const MAX_CLI_JSON_BODY_BYTES = 2_000_000;
24
32
  /**
25
33
  * The per-rung limit matrix. `ai_live` deliberately equals `tool_live` at every
26
34
  * rung — one "live actions" mental model; the per-call cost asymmetry between
@@ -176,7 +176,7 @@ export declare const ENRICHMENT_CREDITS: {
176
176
  readonly company_enrich_typical: 99;
177
177
  readonly web_search: 5;
178
178
  readonly page_scrape: 40;
179
- readonly linkedin_profile_scrape: 25.5;
179
+ readonly linkedin_profile_scrape: 10;
180
180
  };
181
181
  /** Editable calculator default: blended credits per fully-enriched row. */
182
182
  export declare const ENRICHMENT_CREDITS_PER_ROW_TYPICAL = 100;
@@ -163,7 +163,7 @@ export const ENRICHMENT_CREDITS = {
163
163
  company_enrich_typical: 99,
164
164
  web_search: 5, // Serper
165
165
  page_scrape: 40, // Firecrawl (wedge price)
166
- linkedin_profile_scrape: 25.5, // HarvestAPI Basic full-profile job, exactly 5x COGS
166
+ linkedin_profile_scrape: 10, // Managed profile credit at $0.002 COGS, exactly 5x
167
167
  };
168
168
  /** Editable calculator default: blended credits per fully-enriched row. */
169
169
  export const ENRICHMENT_CREDITS_PER_ROW_TYPICAL = SNAPSHOT_ENRICHMENT_CREDITS_PER_ROW_TYPICAL;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Product-analytics core — the TRANSPORT-FREE half of PostHog emission.
3
+ *
4
+ * Two runtimes emit product events: the web/API runtime (`apps/web/src/lib/
5
+ * product-analytics.ts`, `posthog-node` on Vercel) and the Fly worker
6
+ * (`apps/worker/src/product-analytics.ts`, `posthog-node` batching). What they
7
+ * must agree on lives here, once: property sanitization, the distinct-id
8
+ * chain, the person-profile posture, org grouping, and the runtime dimensions
9
+ * every event carries. Two copies of that logic drifted in the past — the
10
+ * worker would have minted a throwaway person per request while the web side
11
+ * did not — and PostHog cannot join what the emitters disagree about.
12
+ *
13
+ * Deliberately NO `posthog-node`, no fetch, no I/O: `packages/shared` is
14
+ * vendored into the published `@oxygen-agent/cli` tarball, so every runtime
15
+ * dependency added here lands on every customer's machine
16
+ * (`scripts/cli-package-dependencies.mjs`, the langfuse-class guard). The
17
+ * capture message is a plain object structurally compatible with
18
+ * `posthog-node`'s `EventMessage`; each runtime owns its own client.
19
+ */
20
+ export declare const PRODUCT_ANALYTICS_MAX_PROPERTY_KEY_LENGTH = 80;
21
+ export declare const PRODUCT_ANALYTICS_MAX_PROPERTY_STRING_LENGTH = 500;
22
+ /**
23
+ * Byte ceiling for a structured (object/array) property value.
24
+ *
25
+ * Structured values pass through UNTOUCHED — see `sanitizeStructuredValue` — so
26
+ * this is the only thing standing between a careless caller and an unbounded
27
+ * ingest payload. Set well above `MCP_PAYLOAD_LIMITS.maxSerializedBytes` (8 KiB,
28
+ * in apps/web `analytics/mcp-analytics.ts`) so it can never re-cut a payload
29
+ * that module already bounded and marked; it exists for the callers that bound
30
+ * nothing.
31
+ */
32
+ export declare const PRODUCT_ANALYTICS_MAX_STRUCTURED_PROPERTY_BYTES = 32768;
33
+ /**
34
+ * A JSON value PostHog can index as a structured property.
35
+ *
36
+ * PostHog's reserved MCP-Analytics properties (`$mcp_parameters`,
37
+ * `$mcp_response`, `$mcp_listed_tool_names`) are objects and arrays by vendor
38
+ * contract, so a scalar-only property type made the entire `$mcp_*` surface
39
+ * untransmittable: a `.slice(...)` on an array is a `TypeError`, and that throw
40
+ * escaped `trackProductEvent` — losing the whole event, not just the property.
41
+ */
42
+ export type ProductAnalyticsJsonValue = string | number | boolean | null | readonly ProductAnalyticsJsonValue[] | {
43
+ readonly [key: string]: ProductAnalyticsJsonValue;
44
+ };
45
+ export type ProductAnalyticsValue = ProductAnalyticsJsonValue | undefined;
46
+ /** What `sanitizeProductAnalyticsProperties` is allowed to hand the SDK. */
47
+ export type ProductAnalyticsSanitizedValue = string | number | boolean | null | ProductAnalyticsJsonValue;
48
+ export type ProductAnalyticsProperties = Record<string, ProductAnalyticsValue>;
49
+ export type ProductAnalyticsEventInput = {
50
+ event: string;
51
+ distinctId?: string | null | undefined;
52
+ organizationId?: string | null | undefined;
53
+ userId?: string | null | undefined;
54
+ properties?: ProductAnalyticsProperties;
55
+ /**
56
+ * Ingestion id. PostHog dedupes on it, so a revenue event replayed from a
57
+ * Stripe webhook redelivery — or a run outcome re-read after a worker
58
+ * restart — lands once instead of double-counting. Supply a value derived
59
+ * from the source fact (the KPI event key, the run id), never a random one,
60
+ * or replay stops being deterministic.
61
+ */
62
+ uuid?: string | null | undefined;
63
+ /** When the fact happened, if that is not "now" (webhook replay, backfill, run settle). */
64
+ timestamp?: Date | null | undefined;
65
+ };
66
+ /**
67
+ * The message shape both `posthog-node` capture paths accept. Declared here so
68
+ * the builder needs no `posthog-node` import; the web/worker clients pass it
69
+ * straight to `capture` / `captureImmediate`.
70
+ */
71
+ export type ProductAnalyticsCaptureMessage = {
72
+ distinctId: string;
73
+ event: string;
74
+ properties: Record<string, ProductAnalyticsSanitizedValue>;
75
+ groups?: Record<string, string>;
76
+ uuid?: string;
77
+ timestamp?: Date;
78
+ };
79
+ /**
80
+ * What the emitting runtime knows about itself. `environment` is the
81
+ * PROD/MAIN/DEV/LOCAL dimension every insight filters on
82
+ * (`resolveProductAnalyticsEnvironmentFromEnv`); `runtimeProperties` are the
83
+ * runtime-specific facts (Vercel env/sha/region on the web, Fly app/region on
84
+ * the worker) and are set BEFORE the caller's properties so a caller can
85
+ * override them but never lose them by accident.
86
+ */
87
+ export type ProductAnalyticsRuntimeContext = {
88
+ environment: string;
89
+ oxygenVersion: string;
90
+ runtimeProperties?: ProductAnalyticsProperties;
91
+ };
92
+ export declare function sanitizeProductAnalyticsProperties(properties: ProductAnalyticsProperties): Record<string, ProductAnalyticsSanitizedValue>;
93
+ export declare function normalizeProductAnalyticsIdentifier(value: ProductAnalyticsValue): string | null;
94
+ /**
95
+ * Build the capture message every runtime sends. Returns null when no distinct
96
+ * id can be derived — an event with no subject is dropped, never invented.
97
+ */
98
+ export declare function buildProductAnalyticsCaptureMessage(input: ProductAnalyticsEventInput, context: ProductAnalyticsRuntimeContext): ProductAnalyticsCaptureMessage | null;
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Product-analytics core — the TRANSPORT-FREE half of PostHog emission.
3
+ *
4
+ * Two runtimes emit product events: the web/API runtime (`apps/web/src/lib/
5
+ * product-analytics.ts`, `posthog-node` on Vercel) and the Fly worker
6
+ * (`apps/worker/src/product-analytics.ts`, `posthog-node` batching). What they
7
+ * must agree on lives here, once: property sanitization, the distinct-id
8
+ * chain, the person-profile posture, org grouping, and the runtime dimensions
9
+ * every event carries. Two copies of that logic drifted in the past — the
10
+ * worker would have minted a throwaway person per request while the web side
11
+ * did not — and PostHog cannot join what the emitters disagree about.
12
+ *
13
+ * Deliberately NO `posthog-node`, no fetch, no I/O: `packages/shared` is
14
+ * vendored into the published `@oxygen-agent/cli` tarball, so every runtime
15
+ * dependency added here lands on every customer's machine
16
+ * (`scripts/cli-package-dependencies.mjs`, the langfuse-class guard). The
17
+ * capture message is a plain object structurally compatible with
18
+ * `posthog-node`'s `EventMessage`; each runtime owns its own client.
19
+ */
20
+ export const PRODUCT_ANALYTICS_MAX_PROPERTY_KEY_LENGTH = 80;
21
+ export const PRODUCT_ANALYTICS_MAX_PROPERTY_STRING_LENGTH = 500;
22
+ /**
23
+ * Byte ceiling for a structured (object/array) property value.
24
+ *
25
+ * Structured values pass through UNTOUCHED — see `sanitizeStructuredValue` — so
26
+ * this is the only thing standing between a careless caller and an unbounded
27
+ * ingest payload. Set well above `MCP_PAYLOAD_LIMITS.maxSerializedBytes` (8 KiB,
28
+ * in apps/web `analytics/mcp-analytics.ts`) so it can never re-cut a payload
29
+ * that module already bounded and marked; it exists for the callers that bound
30
+ * nothing.
31
+ */
32
+ export const PRODUCT_ANALYTICS_MAX_STRUCTURED_PROPERTY_BYTES = 32_768;
33
+ export function sanitizeProductAnalyticsProperties(properties) {
34
+ const sanitized = {};
35
+ for (const [rawKey, rawValue] of Object.entries(properties)) {
36
+ const key = sanitizePropertyKey(rawKey);
37
+ if (!key || rawValue === undefined)
38
+ continue;
39
+ const value = sanitizePropertyValue(rawValue);
40
+ if (value !== undefined)
41
+ sanitized[key] = value;
42
+ }
43
+ return sanitized;
44
+ }
45
+ function sanitizePropertyKey(key) {
46
+ const normalized = key
47
+ .trim()
48
+ .replace(/[^A-Za-z0-9_.$-]/g, "_")
49
+ .slice(0, PRODUCT_ANALYTICS_MAX_PROPERTY_KEY_LENGTH);
50
+ return normalized || null;
51
+ }
52
+ function sanitizePropertyValue(value) {
53
+ if (value === null || value === undefined)
54
+ return value;
55
+ if (typeof value === "boolean")
56
+ return value;
57
+ if (typeof value === "number")
58
+ return Number.isFinite(value) ? value : null;
59
+ if (typeof value === "string")
60
+ return value.slice(0, PRODUCT_ANALYTICS_MAX_PROPERTY_STRING_LENGTH);
61
+ return sanitizeStructuredValue(value);
62
+ }
63
+ /**
64
+ * Objects and arrays pass through UNTOUCHED, or not at all.
65
+ *
66
+ * Deliberately not walked, re-keyed, `String()`d, or cut at the string ceiling:
67
+ * the callers that send structured values (apps/web `analytics/mcp-analytics.ts`)
68
+ * have already redacted and bounded them, and a second pass here would either
69
+ * corrupt a payload into invalid JSON — which is exactly what an old
70
+ * `.slice(0, 500)` did to any array — or silently undo the marked truncation
71
+ * the first pass recorded.
72
+ *
73
+ * The only checks are the two that protect the pipe: it must be JSON-encodable
74
+ * (a cycle or a BigInt would otherwise throw inside the SDK, and `JSON.stringify`
75
+ * itself throws HERE, upstream of the emitter's try), and it must be under the
76
+ * byte ceiling. Failing either drops the one property, never the event.
77
+ */
78
+ function sanitizeStructuredValue(value) {
79
+ try {
80
+ const serialized = JSON.stringify(value);
81
+ if (typeof serialized !== "string")
82
+ return undefined;
83
+ if (Buffer.byteLength(serialized, "utf8") > PRODUCT_ANALYTICS_MAX_STRUCTURED_PROPERTY_BYTES) {
84
+ return undefined;
85
+ }
86
+ return value;
87
+ }
88
+ catch {
89
+ return undefined;
90
+ }
91
+ }
92
+ export function normalizeProductAnalyticsIdentifier(value) {
93
+ if (typeof value === "string" && value.trim()) {
94
+ return value.trim().slice(0, PRODUCT_ANALYTICS_MAX_PROPERTY_STRING_LENGTH);
95
+ }
96
+ if (typeof value === "number" && Number.isFinite(value))
97
+ return String(value);
98
+ return null;
99
+ }
100
+ /**
101
+ * Build the capture message every runtime sends. Returns null when no distinct
102
+ * id can be derived — an event with no subject is dropped, never invented.
103
+ */
104
+ export function buildProductAnalyticsCaptureMessage(input, context) {
105
+ const organizationId = normalizeProductAnalyticsIdentifier(input.organizationId);
106
+ const distinctIdSource = [
107
+ { value: input.distinctId, identifiesPerson: true },
108
+ { value: input.userId, identifiesPerson: true },
109
+ // Namespaced: a bare org UUID is indistinguishable from a human UUID at
110
+ // query time, so an unprefixed org fallback silently pollutes person-level
111
+ // counts (and any "users who…" cohort) with workspace rows. The `groups`
112
+ // attachment below keeps the RAW id — PostHog resolves group keys by exact
113
+ // match against the org id the rest of the product uses.
114
+ { value: organizationId ? `org:${organizationId}` : null, identifiesPerson: false },
115
+ { value: input.properties?.trace_id, identifiesPerson: false },
116
+ { value: input.properties?.request_id, identifiesPerson: false },
117
+ ].find((candidate) => candidate.value !== null && candidate.value !== undefined);
118
+ const distinctId = normalizeProductAnalyticsIdentifier(distinctIdSource?.value);
119
+ if (!distinctId)
120
+ return null;
121
+ const properties = sanitizeProductAnalyticsProperties({
122
+ product_analytics_environment: context.environment,
123
+ oxygen_version: context.oxygenVersion,
124
+ ...context.runtimeProperties,
125
+ organization_id: organizationId,
126
+ user_id: normalizeProductAnalyticsIdentifier(input.userId),
127
+ ...input.properties,
128
+ // Person profiles are a per-event decision on backend SDKs, not an init
129
+ // option. `posthog-node`'s client extends `PostHogCoreStateless`, which has
130
+ // no person-mode state at all — only the browser's stateful `PostHogCore`
131
+ // reads `personProfiles`. A `personProfiles: "identified_only"` passed to the
132
+ // constructor typechecks and is then read by nothing, so every event would
133
+ // get a person profile regardless.
134
+ //
135
+ // Without this, the `trace_id`/`request_id` tail of the distinct-id chain
136
+ // mints one throwaway person per request. Set last so a caller-supplied
137
+ // property can never flip the posture.
138
+ //
139
+ // `|| organizationId` is load-bearing, not defensive: PostHog cannot run
140
+ // group analytics on an anonymous event, and `@posthog/core`'s own
141
+ // `_hasPersonProcessing()` is `isIdentified || hasGroups || enabled` — the
142
+ // SDK forces person processing ON whenever `$groups` is attached. Emitting
143
+ // the inverse pairing would silently zero every org-grouped trend, funnel
144
+ // and retention on exactly the two highest-volume events
145
+ // (`cli_command_run`, `$mcp_tool_call`), because org-API-key traffic has no
146
+ // `userId` and falls through to the org-id candidate. The cost this whole
147
+ // switch targets is the per-REQUEST trace/request-id tail; one profile per
148
+ // ORG is cheap and is where the group data lives.
149
+ $process_person_profile: (distinctIdSource?.identifiesPerson ?? false) || Boolean(organizationId),
150
+ });
151
+ return {
152
+ distinctId,
153
+ event: input.event,
154
+ properties,
155
+ ...(organizationId ? { groups: { organization: organizationId } } : {}),
156
+ ...(input.uuid ? { uuid: input.uuid } : {}),
157
+ ...(input.timestamp ? { timestamp: input.timestamp } : {}),
158
+ };
159
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The PROD / MAIN / DEV / LOCAL dimension stamped on every product event as
3
+ * `product_analytics_environment`. One resolver for both emitting runtimes:
4
+ *
5
+ * - explicit pins win (`POSTHOG_ENVIRONMENT`, its `NEXT_PUBLIC_` twin, and the
6
+ * Sentry environment vars the web app already sets);
7
+ * - the web/API runtime has `VERCEL_ENV` (+ the branch for MAIN-preview);
8
+ * - the Fly worker has NO `VERCEL_ENV` and `NODE_ENV=production` on BOTH apps,
9
+ * so — mirroring `apps/worker/src/worker-env.ts` and the Langfuse resolver —
10
+ * the Fly-injected app name is the ground truth (`-dev` marks the dev app; a
11
+ * cross-env Doppler drift cannot fake it), then `FLY_ENVIRONMENT`;
12
+ * - everything else is LOCAL. `NODE_ENV=production` is deliberately NOT read as
13
+ * PROD: it is the default for any built Node process, a local one included.
14
+ */
15
+ export type ProductAnalyticsEnvironment = "PROD" | "MAIN" | "DEV" | "LOCAL";
16
+ type EnvSource = Record<string, string | undefined>;
17
+ export declare function resolveProductAnalyticsEnvironmentFromEnv(env?: EnvSource): ProductAnalyticsEnvironment;
18
+ export {};
@@ -0,0 +1,46 @@
1
+ function normalizeProductAnalyticsEnvironment(value) {
2
+ if (!value)
3
+ return null;
4
+ switch (value.trim().toUpperCase()) {
5
+ case "PROD":
6
+ case "PRODUCTION":
7
+ return "PROD";
8
+ case "MAIN":
9
+ return "MAIN";
10
+ case "DEV":
11
+ case "DEVELOPMENT":
12
+ case "PREVIEW":
13
+ return "DEV";
14
+ case "LOCAL":
15
+ return "LOCAL";
16
+ default:
17
+ return null;
18
+ }
19
+ }
20
+ export function resolveProductAnalyticsEnvironmentFromEnv(env = process.env) {
21
+ const explicit = normalizeProductAnalyticsEnvironment(env.POSTHOG_ENVIRONMENT
22
+ ?? env.NEXT_PUBLIC_POSTHOG_ENVIRONMENT
23
+ ?? env.SENTRY_ENVIRONMENT
24
+ ?? env.NEXT_PUBLIC_SENTRY_ENVIRONMENT);
25
+ if (explicit)
26
+ return explicit;
27
+ const vercelEnv = env.VERCEL_ENV ?? env.NEXT_PUBLIC_VERCEL_ENV;
28
+ if (vercelEnv === "production")
29
+ return "PROD";
30
+ const branch = env.VERCEL_GIT_COMMIT_REF ?? env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF;
31
+ if (vercelEnv === "preview" && branch === "main")
32
+ return "MAIN";
33
+ if (vercelEnv === "preview")
34
+ return "DEV";
35
+ if (vercelEnv === "development")
36
+ return "LOCAL";
37
+ const flyApp = (env.FLY_APP_NAME ?? env.FLY_APP)?.trim();
38
+ if (flyApp)
39
+ return flyApp.includes("-dev") ? "DEV" : "PROD";
40
+ const flyEnvironment = env.FLY_ENVIRONMENT?.trim().toLowerCase();
41
+ if (flyEnvironment === "production")
42
+ return "PROD";
43
+ if (flyEnvironment)
44
+ return "DEV";
45
+ return "LOCAL";
46
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Every product-analytics event name OXYGEN emits, in one place — for BOTH
3
+ * emitting runtimes. The web/API runtime re-exports this from
4
+ * `apps/web/src/lib/analytics/event-names.ts` (which also owns PostHog's
5
+ * reserved `$mcp_*` vocabulary and the KPI-ledger translation); the Fly worker
6
+ * imports it directly.
7
+ *
8
+ * An event name is a contract shared by three things that cannot see each
9
+ * other: the emitter, its test, and the PostHog insight/funnel built on top. A
10
+ * literal typed at each call site drifts silently — the rename lands in the
11
+ * code, the dashboard keeps querying the old string, and the funnel reads zero
12
+ * without erroring. Import from here instead of typing the string.
13
+ *
14
+ * Names are unprefixed `subject_verb` snake_case; the web test pins the exact
15
+ * set, so adding one is a deliberate edit there too.
16
+ */
17
+ export declare const PRODUCT_EVENTS: {
18
+ /** Acquisition. */
19
+ readonly USER_SIGNED_UP: "user_signed_up";
20
+ readonly WORKSPACE_CREATED: "workspace_created";
21
+ /** Activation. */
22
+ readonly ONBOARDING_COMPLETED: "onboarding_completed";
23
+ readonly ONBOARDING_RESET: "onboarding_reset";
24
+ readonly ACTIVATION_PATH_SELECTED: "activation_path_selected";
25
+ readonly AGENT_CONNECTED: "agent_connected";
26
+ readonly FIRST_RECORD_CREATED: "first_record_created";
27
+ /**
28
+ * The prescribed first play (inbound-led outbound), walked on ANY surface.
29
+ *
30
+ * These describe progress through a play, not a lifecycle moment — a founder
31
+ * who arms capture from the CLI and drafts the sequence in the browser
32
+ * produces one continuous series. Which is exactly why they are emitted from
33
+ * one watermark-guarded helper (apps/web `lib/activation/analytics.ts`)
34
+ * rather than from each surface: the same completed-step count read twice
35
+ * must emit once.
36
+ *
37
+ * Deliberately ABSENT from the web's KPI_TYPE_TO_EVENT. The headline
38
+ * activation KPI is CLI/MCP-sourced by design; wiring a web-observable play
39
+ * step into that ledger would silently redefine what the company reports as
40
+ * activation.
41
+ */
42
+ readonly FIRST_PLAY_STARTED: "first_play_started";
43
+ readonly FIRST_PLAY_STEP_COMPLETED: "first_play_step_completed";
44
+ readonly FIRST_PLAY_COMPLETED: "first_play_completed";
45
+ readonly FIRST_PLAY_SKIPPED: "first_play_skipped";
46
+ /**
47
+ * Usage — what was ASKED for. `mcp_tool_called` used to sit here as the
48
+ * second highest-volume event; it was REPLACED (not joined) by the reserved
49
+ * `$mcp_tool_call` in the web's MCP_ANALYTICS_EVENTS — see that block for why
50
+ * both cannot be emitted.
51
+ */
52
+ readonly CLI_COMMAND_RUN: "cli_command_run";
53
+ /**
54
+ * Outcomes — what actually HAPPENED, emitted by the Fly worker when a durable
55
+ * unit of work reaches a terminal state. `cli_command_run` and `$mcp_tool_call`
56
+ * see a run being requested; nothing on the web runtime ever sees it finish,
57
+ * because workflows, column runs, ingestions, agent runs and sequence steps
58
+ * complete on Fly minutes later. Without these, every "did it work" question
59
+ * (success rate, duration, credits per run, replies per enrollment) was
60
+ * unanswerable in PostHog and biased toward optimism — sweeps, repair passes
61
+ * and lease expiries settle runs invisibly.
62
+ *
63
+ * One event per terminal transition, `uuid` = the run/outbox id so a
64
+ * re-read after a worker restart dedupes, `timestamp` = when it settled.
65
+ * Properties are ids, enums and numbers only — never row values, prompts,
66
+ * or lead names (ADR 0021: PostHog stays sanitized).
67
+ */
68
+ readonly WORKFLOW_RUN_SETTLED: "workflow_run_settled";
69
+ readonly TABLE_ACTION_RUN_SETTLED: "table_action_run_settled";
70
+ readonly TABLE_INGESTION_RUN_SETTLED: "table_ingestion_run_settled";
71
+ readonly AGENT_RUN_SETTLED: "agent_run_settled";
72
+ readonly SEQUENCE_ENROLLMENT_SETTLED: "sequence_enrollment_settled";
73
+ /** Revenue lifecycle. */
74
+ readonly TRIAL_STARTED: "trial_started";
75
+ readonly SUBSCRIPTION_ACTIVATED: "subscription_activated";
76
+ readonly SUBSCRIPTION_CHURNED: "subscription_churned";
77
+ readonly TRIAL_CANCEL_SCHEDULED: "trial_cancel_scheduled";
78
+ readonly TRIAL_CANCEL_RESUMED: "trial_cancel_resumed";
79
+ readonly TRIAL_ENDED_UNCONVERTED: "trial_ended_unconverted";
80
+ /**
81
+ * Marketing site. The public hero lookup on oxygen-agent.com: an ANONYMOUS
82
+ * visitor types their website and OXYGEN spends its own credits profiling it.
83
+ *
84
+ * Deliberately absent from the web's KPI_TYPE_TO_EVENT. These describe a
85
+ * pre-signup funnel step, not a lifecycle moment the company reports, and
86
+ * mapping one onto a `company_kpi_events` type would redefine a headline
87
+ * number without anyone editing that number's definition.
88
+ *
89
+ * Properties are enums, numbers and the domain HASH only — never the raw
90
+ * domain. The visitor has no account and never consented to their employer's
91
+ * name landing in an analytics warehouse (ADR 0021: PostHog stays sanitized).
92
+ * `hero_lookup_cached` is what says whether the lookup cache is paying for
93
+ * itself, which is the whole economics of the surface.
94
+ */
95
+ readonly HERO_LOOKUP_STARTED: "hero_lookup_started";
96
+ readonly HERO_PROFILE_COMPLETED: "hero_profile_completed";
97
+ readonly HERO_LEADS_COMPLETED: "hero_leads_completed";
98
+ readonly HERO_LOOKUP_CACHED: "hero_lookup_cached";
99
+ readonly HERO_LOOKUP_DEGRADED: "hero_lookup_degraded";
100
+ /** Browser-side hero events; props are enums, booleans and counts only. */
101
+ readonly HERO_LOOKUP_SUBMITTED: "hero_lookup_submitted";
102
+ readonly HERO_LOOKUP_RENDERED: "hero_lookup_rendered";
103
+ readonly HERO_SIGNUP_CLICKED: "hero_signup_clicked";
104
+ /** Pipeline heartbeat: proves ingestion is alive when the product is quiet. */
105
+ readonly POSTHOG_CANARY: "posthog_canary";
106
+ };
107
+ export type ProductEventName = (typeof PRODUCT_EVENTS)[keyof typeof PRODUCT_EVENTS];
108
+ /** Every name, for drift checks and dashboard provisioning. */
109
+ export declare const PRODUCT_EVENT_NAMES: readonly ProductEventName[];
110
+ /**
111
+ * The outcome events the worker emits, as a typed subset — so the worker's
112
+ * emitter cannot be handed a lifecycle event that only the control plane has
113
+ * the facts for.
114
+ */
115
+ export declare const WORKER_RUN_OUTCOME_EVENTS: readonly ["workflow_run_settled", "table_action_run_settled", "table_ingestion_run_settled", "agent_run_settled", "sequence_enrollment_settled"];
116
+ export type WorkerRunOutcomeEventName = (typeof WORKER_RUN_OUTCOME_EVENTS)[number];
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Every product-analytics event name OXYGEN emits, in one place — for BOTH
3
+ * emitting runtimes. The web/API runtime re-exports this from
4
+ * `apps/web/src/lib/analytics/event-names.ts` (which also owns PostHog's
5
+ * reserved `$mcp_*` vocabulary and the KPI-ledger translation); the Fly worker
6
+ * imports it directly.
7
+ *
8
+ * An event name is a contract shared by three things that cannot see each
9
+ * other: the emitter, its test, and the PostHog insight/funnel built on top. A
10
+ * literal typed at each call site drifts silently — the rename lands in the
11
+ * code, the dashboard keeps querying the old string, and the funnel reads zero
12
+ * without erroring. Import from here instead of typing the string.
13
+ *
14
+ * Names are unprefixed `subject_verb` snake_case; the web test pins the exact
15
+ * set, so adding one is a deliberate edit there too.
16
+ */
17
+ export const PRODUCT_EVENTS = {
18
+ /** Acquisition. */
19
+ USER_SIGNED_UP: "user_signed_up",
20
+ WORKSPACE_CREATED: "workspace_created",
21
+ /** Activation. */
22
+ ONBOARDING_COMPLETED: "onboarding_completed",
23
+ ONBOARDING_RESET: "onboarding_reset",
24
+ ACTIVATION_PATH_SELECTED: "activation_path_selected",
25
+ AGENT_CONNECTED: "agent_connected",
26
+ FIRST_RECORD_CREATED: "first_record_created",
27
+ /**
28
+ * The prescribed first play (inbound-led outbound), walked on ANY surface.
29
+ *
30
+ * These describe progress through a play, not a lifecycle moment — a founder
31
+ * who arms capture from the CLI and drafts the sequence in the browser
32
+ * produces one continuous series. Which is exactly why they are emitted from
33
+ * one watermark-guarded helper (apps/web `lib/activation/analytics.ts`)
34
+ * rather than from each surface: the same completed-step count read twice
35
+ * must emit once.
36
+ *
37
+ * Deliberately ABSENT from the web's KPI_TYPE_TO_EVENT. The headline
38
+ * activation KPI is CLI/MCP-sourced by design; wiring a web-observable play
39
+ * step into that ledger would silently redefine what the company reports as
40
+ * activation.
41
+ */
42
+ FIRST_PLAY_STARTED: "first_play_started",
43
+ FIRST_PLAY_STEP_COMPLETED: "first_play_step_completed",
44
+ FIRST_PLAY_COMPLETED: "first_play_completed",
45
+ FIRST_PLAY_SKIPPED: "first_play_skipped",
46
+ /**
47
+ * Usage — what was ASKED for. `mcp_tool_called` used to sit here as the
48
+ * second highest-volume event; it was REPLACED (not joined) by the reserved
49
+ * `$mcp_tool_call` in the web's MCP_ANALYTICS_EVENTS — see that block for why
50
+ * both cannot be emitted.
51
+ */
52
+ CLI_COMMAND_RUN: "cli_command_run",
53
+ /**
54
+ * Outcomes — what actually HAPPENED, emitted by the Fly worker when a durable
55
+ * unit of work reaches a terminal state. `cli_command_run` and `$mcp_tool_call`
56
+ * see a run being requested; nothing on the web runtime ever sees it finish,
57
+ * because workflows, column runs, ingestions, agent runs and sequence steps
58
+ * complete on Fly minutes later. Without these, every "did it work" question
59
+ * (success rate, duration, credits per run, replies per enrollment) was
60
+ * unanswerable in PostHog and biased toward optimism — sweeps, repair passes
61
+ * and lease expiries settle runs invisibly.
62
+ *
63
+ * One event per terminal transition, `uuid` = the run/outbox id so a
64
+ * re-read after a worker restart dedupes, `timestamp` = when it settled.
65
+ * Properties are ids, enums and numbers only — never row values, prompts,
66
+ * or lead names (ADR 0021: PostHog stays sanitized).
67
+ */
68
+ WORKFLOW_RUN_SETTLED: "workflow_run_settled",
69
+ TABLE_ACTION_RUN_SETTLED: "table_action_run_settled",
70
+ TABLE_INGESTION_RUN_SETTLED: "table_ingestion_run_settled",
71
+ AGENT_RUN_SETTLED: "agent_run_settled",
72
+ SEQUENCE_ENROLLMENT_SETTLED: "sequence_enrollment_settled",
73
+ /** Revenue lifecycle. */
74
+ TRIAL_STARTED: "trial_started",
75
+ SUBSCRIPTION_ACTIVATED: "subscription_activated",
76
+ SUBSCRIPTION_CHURNED: "subscription_churned",
77
+ TRIAL_CANCEL_SCHEDULED: "trial_cancel_scheduled",
78
+ TRIAL_CANCEL_RESUMED: "trial_cancel_resumed",
79
+ TRIAL_ENDED_UNCONVERTED: "trial_ended_unconverted",
80
+ /**
81
+ * Marketing site. The public hero lookup on oxygen-agent.com: an ANONYMOUS
82
+ * visitor types their website and OXYGEN spends its own credits profiling it.
83
+ *
84
+ * Deliberately absent from the web's KPI_TYPE_TO_EVENT. These describe a
85
+ * pre-signup funnel step, not a lifecycle moment the company reports, and
86
+ * mapping one onto a `company_kpi_events` type would redefine a headline
87
+ * number without anyone editing that number's definition.
88
+ *
89
+ * Properties are enums, numbers and the domain HASH only — never the raw
90
+ * domain. The visitor has no account and never consented to their employer's
91
+ * name landing in an analytics warehouse (ADR 0021: PostHog stays sanitized).
92
+ * `hero_lookup_cached` is what says whether the lookup cache is paying for
93
+ * itself, which is the whole economics of the surface.
94
+ */
95
+ HERO_LOOKUP_STARTED: "hero_lookup_started",
96
+ HERO_PROFILE_COMPLETED: "hero_profile_completed",
97
+ HERO_LEADS_COMPLETED: "hero_leads_completed",
98
+ HERO_LOOKUP_CACHED: "hero_lookup_cached",
99
+ HERO_LOOKUP_DEGRADED: "hero_lookup_degraded",
100
+ /** Browser-side hero events; props are enums, booleans and counts only. */
101
+ HERO_LOOKUP_SUBMITTED: "hero_lookup_submitted",
102
+ HERO_LOOKUP_RENDERED: "hero_lookup_rendered",
103
+ HERO_SIGNUP_CLICKED: "hero_signup_clicked",
104
+ /** Pipeline heartbeat: proves ingestion is alive when the product is quiet. */
105
+ POSTHOG_CANARY: "posthog_canary",
106
+ };
107
+ /** Every name, for drift checks and dashboard provisioning. */
108
+ export const PRODUCT_EVENT_NAMES = Object.freeze(Object.values(PRODUCT_EVENTS));
109
+ /**
110
+ * The outcome events the worker emits, as a typed subset — so the worker's
111
+ * emitter cannot be handed a lifecycle event that only the control plane has
112
+ * the facts for.
113
+ */
114
+ export const WORKER_RUN_OUTCOME_EVENTS = Object.freeze([
115
+ PRODUCT_EVENTS.WORKFLOW_RUN_SETTLED,
116
+ PRODUCT_EVENTS.TABLE_ACTION_RUN_SETTLED,
117
+ PRODUCT_EVENTS.TABLE_INGESTION_RUN_SETTLED,
118
+ PRODUCT_EVENTS.AGENT_RUN_SETTLED,
119
+ PRODUCT_EVENTS.SEQUENCE_ENROLLMENT_SETTLED,
120
+ ]);
@@ -16,4 +16,10 @@ export type RecipePrerequisiteKind = typeof RECIPE_PREREQUISITE_KINDS[number];
16
16
  export declare const RECIPE_BODY_SECTIONS: readonly ["The Play", "What You Get", "When To Run (And When Not To)", "Before You Start", "Steps", "Ready Assets", "Calibrate", "Troubleshooting", "Operator Notes", "Related"];
17
17
  export declare const RECIPE_SLUG_PATTERN: RegExp;
18
18
  export declare function recipeWikiSlug(recipeSlug: string): string;
19
+ export declare const RECIPE_KIT_MAX_STAGES = 6;
20
+ export declare const RECIPE_KIT_TABLE_REF_PATTERN: RegExp;
21
+ export declare const RECIPE_KIT_STAGE_STATUSES: readonly ["planned", "installed", "reused", "skipped_active", "blocked", "failed"];
22
+ export type RecipeKitStageStatus = typeof RECIPE_KIT_STAGE_STATUSES[number];
23
+ export type RecipeKitStatus = "not_applied" | "partial" | "applied";
24
+ export declare function recipeKitSourceRef(recipeSlug: string, version: number): string;
19
25
  export declare const RECIPE_TRIAL_SAFE_PILOT_CREDIT_CAP = 100;
@@ -87,6 +87,29 @@ export const RECIPE_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,119}$/;
87
87
  export function recipeWikiSlug(recipeSlug) {
88
88
  return `playbook-${recipeSlug}`;
89
89
  }
90
+ // ── Kits (ADR 0025) ─────────────────────────────────────────────────────────
91
+ // A recipe carries an ORDERED kit of Blueprint stages. `oxygen recipes apply`
92
+ // preflights every stage into one forecast and installs them all under one
93
+ // approval — 0 credits, no provider call, every installed Workflow disabled.
94
+ // A stage may bind a table created by an earlier stage through a
95
+ // `$stages.<index>.tables.<ref>` reference so later stages graft onto it
96
+ // instead of creating a second table.
97
+ export const RECIPE_KIT_MAX_STAGES = 6;
98
+ export const RECIPE_KIT_TABLE_REF_PATTERN = /^\$stages\.(\d)\.tables\.([a-z][a-z0-9_]*)$/;
99
+ export const RECIPE_KIT_STAGE_STATUSES = [
100
+ "planned",
101
+ "installed",
102
+ "reused",
103
+ "skipped_active",
104
+ "blocked",
105
+ "failed",
106
+ ];
107
+ // page_sources ref an apply writes on the recipe's playbook page — one per
108
+ // recipe version, ON CONFLICT DO NOTHING, so first-apply provenance survives
109
+ // reapplies.
110
+ export function recipeKitSourceRef(recipeSlug, version) {
111
+ return `https://oxygen-agent.com/recipes/${recipeSlug}@v${version}#kit`;
112
+ }
90
113
  // trial_safe recipes must keep their pilot path within 10% of the 7-day
91
114
  // card-required trial's 1,000 managed credits.
92
115
  export const RECIPE_TRIAL_SAFE_PILOT_CREDIT_CAP = 100;