@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
@@ -1,3 +1,4 @@
1
+ import { type EnrichmentIntentTaxonomyEntry } from "./enrichment-intents.js";
1
2
  import { type RecipePrimitive } from "./recipes.js";
2
3
  export type CapabilityLayer = "Control" | "Knowledge" | "Data" | "Action" | "External";
3
4
  export type CapabilityPosture = "read_only" | "workspace_write" | "paid_read" | "external_write" | "mixed";
@@ -29,6 +30,22 @@ export type CapabilityRouteMatch = {
29
30
  * intent is that narrow; `recommendedCommands[0]` is the same command.
30
31
  */
31
32
  exactCommand?: string;
33
+ /**
34
+ * The opinionated enrichment catalogue row a data-type ask resolves to
35
+ * ("get the funding round" -> enrich_company:funding). Present only when
36
+ * the ask names a data type; the catalogue command is then the first
37
+ * recommendation, ahead of any raw provider tool.
38
+ */
39
+ catalogIntent?: {
40
+ id: string;
41
+ label: string;
42
+ status: EnrichmentIntentTaxonomyEntry["status"];
43
+ group: EnrichmentIntentTaxonomyEntry["group"];
44
+ /** The catalogue-first path: read the row, then add/preview; `exactCommand` names the row. */
45
+ tools: string[];
46
+ commands: string[];
47
+ exactCommand: string;
48
+ };
32
49
  };
33
50
  export type PrimitiveRouteMatch = CapabilityRouteMatch & {
34
51
  card: PrimitiveRouteCard;
@@ -1,3 +1,4 @@
1
+ import { findEnrichmentIntentByAlias } from "./enrichment-intents.js";
1
2
  import { RECIPE_PRIMITIVES } from "./recipes.js";
2
3
  // One compact semantic index covers every top-level /api/cli endpoint section.
3
4
  // It is navigation, never execution: live schemas, prices, readiness, and
@@ -210,10 +211,10 @@ export const OXYGEN_CAPABILITY_ROUTES = [
210
211
  primitive: "tables",
211
212
  owns: "Typed working datasets, rows, formulas, AI/tool/waterfall columns, reusable Functions with isolated drafts and published versions, cell state, projects, and run provenance.",
212
213
  notFor: "Canonical CRM truth, message cadence, or an off-platform spreadsheet runtime.",
213
- execution: "Create and run work in hosted OXYGEN Tables; validate a small sample before bounded paid runs. For standard person or company enrichment, `columns add <table> --preset person_enrich|company_enrich` (MCP oxygen_columns_add with preset) adds the maintained bundle in one call before any hand-built tool column.",
214
+ execution: "Create and run work in hosted OXYGEN Tables; validate a small sample before bounded paid runs. To see what a table's own columns can be enriched with and what each costs per row before adding anything, run `tools search --for-table <table>` (MCP oxygen_tools_search with for_table): free, it returns priced suggestions with the exact add command. For standard person or company enrichment, `columns add <table> --preset person_enrich|company_enrich` (MCP oxygen_columns_add with preset) adds the maintained bundle in one call before any hand-built tool column. Person questions over that bundle's profile payload (skill set, grad school, location, job fit, current company) and person appearance research (events, keynotes, podcasts, GitHub profile) are `columns catalog --category people` templates, added with `columns add <table> --prompt-key <key> --input <name>=<column>`. For a company fact OXYGEN already knows how to research (founders, parent company, funding, cloud provider, offers demos, industry, NAICS, HQ, LinkedIn/Crunchbase page lookups) or a page extraction, `columns catalog` lists the ready-made template and `columns add <table> --prompt-key <key> --input <name>=<column>` (MCP oxygen_columns_add with prompt_key) adds it with no prompt to write.",
214
215
  posture: "mixed",
215
- gatewayTools: ["oxygen_tables_create", "oxygen_columns_add", "oxygen_enrich_column_preview", "oxygen_tables_link_bulk", "oxygen_callables_manage"],
216
- gatewayCommands: ["tables create", "columns add", "enrich-column preview", "tables link", "functions list", "functions draft"],
216
+ gatewayTools: ["oxygen_tables_create", "oxygen_tools_search", "oxygen_columns_add", "oxygen_columns_catalog", "oxygen_enrich_column_preview", "oxygen_tables_link_bulk", "oxygen_callables_manage"],
217
+ gatewayCommands: ["tables create", "tools search", "columns add", "columns catalog", "enrich-column preview", "tables link", "functions list", "functions draft"],
217
218
  skills: ["oxygen-gtm", "oxygen-table-tidy", "oxygen-diagnostics", "oxygen-clay-migration", "oxygen-linkedin-marketing"],
218
219
  endpointSections: ["action-columns", "callables", "functions", "columns", "company-enrichment", "enrich-column", "enrichment", "projects", "supabase", "table-action-items", "table-action-runs", "table-ingestion-runs", "tables"],
219
220
  // "link"/"join"/"connect"/"relate" route here for `tables link`. Added after a
@@ -221,7 +222,12 @@ export const OXYGEN_CAPABILITY_ROUTES = [
221
222
  // `tables create` / `columns add` / `enrich-column preview` — none of which
222
223
  // do it. The agent only found the right command by grepping the raw 25k-line
223
224
  // command manifest, which is not a discovery path a customer has.
224
- intentTerms: ["table", "spreadsheet", "rows", "column", "columns", "dataset", "csv", "import", "enrich", "enrichment", "waterfall", "score", "formula", "ai column", "lookup", "link", "link tables", "join", "connect", "relate", "relationship", "function", "functions", "reusable function", "function draft", "function version", "callable"],
225
+ // "founders" / "parent company" / "cloud provider" / "company research" route
226
+ // here for `columns catalog`. Added after the 2026-09-16 blind baseline asked
227
+ // `capabilities search "founders of a company"` and got `route: null` with a
228
+ // generic sourcing pointer, then hand-wrote four research prompts for
229
+ // questions the template catalog now answers by key.
230
+ intentTerms: ["table", "spreadsheet", "rows", "column", "columns", "dataset", "csv", "import", "enrich", "enrichment", "waterfall", "score", "formula", "ai column", "lookup", "link", "link tables", "join", "connect", "relate", "relationship", "function", "functions", "reusable function", "function draft", "function version", "callable", "column template", "skill set", "grad school", "podcast appearance", "keynote", "github profile", "job fit", "founders", "parent company", "subsidiaries", "cloud provider", "company research", "research question", "research column", "web research", "template catalog", "ready-made", "offers demos", "naics", "cost per row", "price per row", "what can i enrich", "enrichment cost", "credits per row"],
225
231
  },
226
232
  {
227
233
  id: "messages",
@@ -269,15 +275,26 @@ export const OXYGEN_CAPABILITY_ROUTES = [
269
275
  id: "dashboards",
270
276
  layer: "Data",
271
277
  primitive: "dashboards",
272
- owns: "Curated funnel, channel, spend, and needs-action reporting over native state.",
273
- notFor: "Arbitrary BI modeling or a separate analytics store.",
274
- execution: "Compute hosted curated views over Records, Messages, Signals, Sequences, and runs.",
275
- posture: "read_only",
276
- gatewayTools: ["oxygen_dashboard_summary"],
277
- gatewayCommands: ["dashboards summary"],
278
+ owns: "The curated funnel, channel, spend and needs-action reporting, PLUS the workspace's own saved dashboards: named pages of chart / view / iframe / rich-text / code / live-source widgets over tables and CRM objects.",
279
+ notFor: "Arbitrary BI modeling or a separate analytics store. Outreach volume and replies are not chartable yet — group a table or a CRM object, or read the curated funnel with `dashboards summary`.",
280
+ execution: "Compute hosted curated views over Records, Messages, Signals, Sequences and runs; store and serve user-defined dashboards from ox_dashboards.",
281
+ // Was `read_only` until v1.1002.0, which is when a workspace could first SAVE a
282
+ // dashboard. A blind user eval (2026-09-21) caught the drift the expensive way:
283
+ // the agent asked this router for "dashboard", was told read_only and
284
+ // "not for BI modeling", and only found the eight mutating commands by
285
+ // separately searching the command index. This record is step 4 of the CLI's
286
+ // own quickstart, so a stale posture here routes users away from what shipped.
287
+ // `mixed`, not workspace_write: this capability now spans a read (summary,
288
+ // widget data), a workspace write (dashboards, tabs, widgets) AND one
289
+ // external-reach spending call (a live_source refresh calls its bound
290
+ // provider). "read_write" is not a member of CapabilityPosture -- inventing
291
+ // it is what broke the shared build on the first attempt.
292
+ posture: "mixed",
293
+ gatewayTools: ["oxygen_dashboard_summary", "oxygen_dashboards_list", "oxygen_dashboards_create", "oxygen_dashboards_widget_kinds", "oxygen_dashboards_widget_add", "oxygen_dashboards_widget_data"],
294
+ gatewayCommands: ["dashboards summary", "dashboards list", "dashboards create", "dashboards widgets kinds", "dashboards widgets add", "dashboards widgets data"],
278
295
  skills: ["oxygen-gtm"],
279
296
  endpointSections: ["dashboards"],
280
- intentTerms: ["dashboard", "report", "reporting", "funnel", "metrics", "kpi", "performance", "pipeline summary"],
297
+ intentTerms: ["dashboard", "report", "reporting", "funnel", "metrics", "kpi", "performance", "pipeline summary", "save", "saved dashboard", "custom dashboard", "widget", "chart", "weekly report", "monday", "come back to"],
281
298
  },
282
299
  {
283
300
  id: "ads",
@@ -313,10 +330,15 @@ export const OXYGEN_CAPABILITY_ROUTES = [
313
330
  primitive: "sequences",
314
331
  owns: "Net-new LinkedIn outreach plus versioned multichannel programs, cadence, enrollment, sender limits, suppression, reply-stop, and dispatch.",
315
332
  notFor: "A reply in an existing conversation or a direct one-off email; use Messages. Not for general deterministic automation.",
316
- execution: "Use the one-step gateway for new LinkedIn initiation or create/enroll/start a hosted campaign with bounded send authority.",
333
+ // The external-campaign sentence exists because a 2026-09-16 blind user asked
334
+ // to "push table rows to smartlead campaign", landed here, read the native
335
+ // email track's "Only 'instantly' is supported" and concluded Smartlead was
336
+ // unreachable. The governed path for a campaign the customer already runs
337
+ // elsewhere is `sequences push-external`; it is not a Sequence.
338
+ execution: "Use the one-step gateway for new LinkedIn initiation or create/enroll/start a hosted campaign with bounded send authority. To push table rows into an existing Smartlead, Instantly, lemlist or HeyReach campaign you already run, use `sequences push-external` (dry_run preview, then --approved); the native email track of a Sequence stays Instantly-bound.",
317
339
  posture: "external_write",
318
- gatewayTools: ["oxygen_sequences_send", "oxygen_sequences_create", "oxygen_sequences_enroll", "oxygen_sequences_start", "oxygen_sequences_list"],
319
- gatewayCommands: ["sequences send", "sequences create", "sequences enroll", "sequences start", "sequences list"],
340
+ gatewayTools: ["oxygen_sequences_send", "oxygen_sequences_create", "oxygen_sequences_enroll", "oxygen_sequences_start", "oxygen_sequences_list", "oxygen_sequences_push_external"],
341
+ gatewayCommands: ["sequences send", "sequences create", "sequences enroll", "sequences start", "sequences list", "sequences push-external"],
320
342
  skills: ["oxygen-sequencer"],
321
343
  endpointSections: ["senders", "sequencer", "sequences", "spintax", "suppressions", "voice"],
322
344
  // "sender identity", "sender profile" and "phone number" are here because a
@@ -324,7 +346,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
324
346
  // to workspace-access — "identity" matched the AUTH sense. The nouns a user
325
347
  // reaches for when naming a sending identity or the dialing pool have to land
326
348
  // on the group that actually owns `senders profiles` and `voice numbers`.
327
- intentTerms: ["sequence", "campaign", "cadence", "enroll", "outreach", "linkedin message", "linkedin dm", "nurture", "follow up", "sender rotation", "stop on reply", "sender identity", "sending identity", "sender profile", "phone number", "dialing", "dialer", "call", "cold call"],
349
+ intentTerms: ["sequence", "campaign", "cadence", "enroll", "outreach", "linkedin message", "linkedin dm", "nurture", "follow up", "sender rotation", "stop on reply", "sender identity", "sending identity", "sender profile", "phone number", "dialing", "dialer", "call", "cold call", "smartlead", "instantly", "lemlist", "heyreach", "external sequencer", "push to campaign", "push rows to campaign", "add leads to campaign"],
328
350
  },
329
351
  {
330
352
  id: "publishing",
@@ -493,6 +515,8 @@ export const OXYGEN_CAPABILITY_ROUTES = [
493
515
  export const OXYGEN_PRIMITIVE_ROUTES = OXYGEN_CAPABILITY_ROUTES
494
516
  .filter((card) => card.primitive !== null);
495
517
  const ROUTE_BY_ID = new Map(OXYGEN_CAPABILITY_ROUTES.map((card) => [card.id, card]));
518
+ /** Cards a catalogue row may decorate: the data surfaces, never sequences/posts/billing. */
519
+ const CATALOGUE_CARD_IDS = new Set(["tables", "sourcing-and-provider-tools", "signals"]);
496
520
  const ROUTE_BY_PRIMITIVE = new Map(OXYGEN_PRIMITIVE_ROUTES.map((card) => [card.primitive, card]));
497
521
  const NORMALIZED_PRIMITIVES = [...RECIPE_PRIMITIVES].sort();
498
522
  const NORMALIZED_ROUTE_PRIMITIVES = OXYGEN_PRIMITIVE_ROUTES.map((card) => card.primitive).sort();
@@ -518,15 +542,62 @@ export function inferCapabilityRoute(query) {
518
542
  if (!normalized)
519
543
  return null;
520
544
  const explicit = explicitCapabilityIntent(normalized);
521
- const card = explicit ?? highestScoringRoute(normalized);
545
+ const catalogRow = findEnrichmentIntentByAlias(normalized);
546
+ // Card scoring stays the owner of routing; the catalogue only breaks a
547
+ // no-match tie ("get the funding round for these accounts" names no card
548
+ // term) and decorates the data cards it belongs to.
549
+ const card = explicit ?? highestScoringRoute(normalized) ?? (catalogRow ? cardForCatalogueIntent(catalogRow) : null);
522
550
  if (!card)
523
551
  return null;
524
552
  const recommendations = recommendationsFor(card, normalized);
553
+ // The card's own recommendations are a pinned contract (tool order, no
554
+ // exact command on plain enrichment asks); the catalogue path travels
555
+ // beside them in `catalogIntent`, never inside them.
556
+ const catalogue = catalogRow && CATALOGUE_CARD_IDS.has(card.id) ? catalogueRecommendations(catalogRow, card) : null;
525
557
  return {
526
558
  card,
527
559
  recommendedTools: recommendations.tools,
528
560
  recommendedCommands: recommendations.commands,
529
561
  ...(recommendations.exactCommand ? { exactCommand: recommendations.exactCommand } : {}),
562
+ ...(catalogue && catalogRow
563
+ ? { catalogIntent: { id: catalogRow.id, label: catalogRow.label, status: catalogRow.status, group: catalogRow.group, tools: catalogue.tools, commands: catalogue.commands, exactCommand: catalogue.exactCommand } }
564
+ : {}),
565
+ };
566
+ }
567
+ /**
568
+ * A data-type ask ("tech stack", "decision makers at each account", "funding
569
+ * events") owns its card by taxonomy group: person/contact/company rows are
570
+ * Tables columns, decision-maker rows are the sourcing tools, signal rows are
571
+ * the signals card when one exists.
572
+ */
573
+ function cardForCatalogueIntent(row) {
574
+ if (row.group === "decision_maker")
575
+ return ROUTE_BY_ID.get("sourcing-and-provider-tools") ?? null;
576
+ if (row.group === "signal")
577
+ return ROUTE_BY_ID.get("signals") ?? ROUTE_BY_PRIMITIVE.get("signals") ?? null;
578
+ return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
579
+ }
580
+ function catalogueRecommendations(row, card) {
581
+ const exactCommand = `oxygen enrichment catalog --intent ${row.id}`;
582
+ if (row.group === "decision_maker" || card.id === "sourcing-and-provider-tools") {
583
+ return {
584
+ tools: ["oxygen_enrichment_catalog", "oxygen_people_search_plan", "oxygen_people_search_run"],
585
+ commands: ["enrichment catalog", "people search plan", "people search run"],
586
+ exactCommand,
587
+ };
588
+ }
589
+ if (row.group === "signal") {
590
+ return {
591
+ tools: ["oxygen_enrichment_catalog", "oxygen_signals_search_plan", "oxygen_signals_search_run"],
592
+ commands: ["enrichment catalog", "signals search plan", "signals search run"],
593
+ exactCommand,
594
+ };
595
+ }
596
+ const find = row.surfaces.find ? [`oxygen_${row.surfaces.find.replace(/[\s-]+/g, "_")}`] : [];
597
+ return {
598
+ tools: ["oxygen_enrichment_catalog", "oxygen_columns_add", "oxygen_enrich_column_preview", ...find],
599
+ commands: ["enrichment catalog", "columns add", "enrich-column preview", ...(row.surfaces.find ? [row.surfaces.find] : [])],
600
+ exactCommand,
530
601
  };
531
602
  }
532
603
  export function inferPrimitiveRoute(query) {
@@ -551,6 +622,9 @@ export function serializeCapabilityRoute(route) {
551
622
  recommended_commands: route.recommendedCommands,
552
623
  recommended_tools: route.recommendedTools,
553
624
  ...(route.exactCommand ? { exact_command: route.exactCommand } : {}),
625
+ ...(route.catalogIntent
626
+ ? { catalog_intent: { id: route.catalogIntent.id, label: route.catalogIntent.label, status: route.catalogIntent.status, group: route.catalogIntent.group, tools: route.catalogIntent.tools, commands: route.catalogIntent.commands, exact_command: route.catalogIntent.exactCommand } }
627
+ : {}),
554
628
  hydrate: {
555
629
  cli: "oxygen commands get <exact-command> --json",
556
630
  mcp: "oxygen_capabilities_schema",
@@ -642,6 +716,10 @@ function explicitCapabilityIntent(query) {
642
716
  // Monday verify new signups" still lands on Workflows.
643
717
  if (isEmailVerificationIntent(query))
644
718
  return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
719
+ // "verify these phone numbers" is a verification column on a table, not a
720
+ // dialer action; without this the sequences card's call terms win.
721
+ if (/\b(verify|validate|check)\b[^.]{0,40}\b(phone|mobile)\b/.test(query))
722
+ return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
645
723
  if (/\b(recipe|playbook|proven play|what should i do)\b/.test(query)) {
646
724
  return ROUTE_BY_PRIMITIVE.get("recipes") ?? null;
647
725
  }
@@ -838,6 +916,16 @@ function recommendationsFor(card, query) {
838
916
  commands: ["tables watcher preview", "tables watcher create", "tables watcher get", "tables watcher update", "tables watcher pause", "tables watcher resume"],
839
917
  };
840
918
  }
919
+ if (isCompanyResearchQuestionIntent(query)) {
920
+ // A fact the template catalog already answers by key: list the keys first
921
+ // (free), add the column by key (free), then price and run it. Ahead of
922
+ // the bare "run" noun below, because "which cloud provider does it run
923
+ // on" is a question about the company, not about a run.
924
+ return {
925
+ tools: ["oxygen_columns_catalog", "oxygen_columns_add", "oxygen_columns_run", "oxygen_tables_describe"],
926
+ commands: ["columns catalog", "columns add", "columns run", "tables describe"],
927
+ };
928
+ }
841
929
  if (/\b(table )?(action )?runs?\b/.test(query)) {
842
930
  return {
843
931
  tools: ["oxygen_table_runs_get", "oxygen_table_runs_items", "oxygen_table_runs_wait", "oxygen_table_runs_retry_failed"],
@@ -1012,6 +1100,16 @@ function isEmailVerificationIntent(query) {
1012
1100
  // ask ("find companies with a website in Germany") keeps its owner. LinkedIn
1013
1101
  // page recovery is deliberately left to the LinkedIn rules above it, which
1014
1102
  // already resolve company pages through the public-research catalog.
1103
+ /**
1104
+ * A company fact the template catalog answers by key — founders, parent company,
1105
+ * subsidiaries, cloud provider, NAICS, demos — and the "ready-made / template
1106
+ * catalog / research question" phrasing itself. Kept to nouns no other card
1107
+ * owns: "funding" alone belongs to Signals and "industry" alone to company
1108
+ * search, so both stay off this list.
1109
+ */
1110
+ function isCompanyResearchQuestionIntent(query) {
1111
+ return /\b(founders?|founded by|parent compan(?:y|ies)|subsidiar(?:y|ies)|cloud provider|naics|column templates?|template catalog|ready[- ]made|research question|company research|offers? demos?|gives? demos?)\b/.test(query);
1112
+ }
1015
1113
  function isCompanyUrlRecoveryIntent(query) {
1016
1114
  const urlNoun = /\b(websites?|web ?sites?|website urls?|domains?|urls?|homepages?)\b/;
1017
1115
  const companyNoun = /\b(compan(?:y|ies)|accounts?|organi[sz]ations?|brands?|vendors?)\b/;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * "Did I type this, or did Oxygen?" — the single rule behind the bolt.
3
+ *
4
+ * A CRM record mixes two kinds of field in one list: the handful a founder
5
+ * types (name, stage, owner, a phone they were given) and the majority Oxygen
6
+ * fills from one paid LinkedIn scrape and the free extractors over its payload.
7
+ * Reading a record without that distinction, every cell looks equally
8
+ * authoritative — which is exactly backwards, because a scraped headline and a
9
+ * hand-corrected job title deserve different trust.
10
+ *
11
+ * The grid already marks derived columns in its header (`ColumnDefinitionBadge`)
12
+ * and the record page did not, so the same field read as enriched in one place
13
+ * and hand-entered in the other. This is the shared answer both surfaces call.
14
+ *
15
+ * The rule is TOTAL over column kinds and deliberately coarse: a bolt means
16
+ * "you did not type this", and the label says what did. Splitting hairs between
17
+ * an AI column and a formula over an AI column's payload is an implementation
18
+ * detail a customer has no reason to hold.
19
+ */
20
+ /** What produced the value in this column's cells. */
21
+ export type ColumnAutoFillKind =
22
+ /** A provider call — the paid lanes and everything extracted from their payloads. */
23
+ "enrichment"
24
+ /** A model wrote it (`ai`, `research`). */
25
+ | "ai"
26
+ /** Computed from other cells on the same row. */
27
+ | "derived"
28
+ /** Read across a link from another row or record. */
29
+ | "linked"
30
+ /** Oxygen's own bookkeeping — rollups, sync markers, engagement projections. */
31
+ | "system";
32
+ export type ColumnAutoFill = {
33
+ kind: ColumnAutoFillKind;
34
+ /** One short phrase, shown as the bolt's tooltip. Sentence case, no period. */
35
+ label: string;
36
+ };
37
+ /** The shape this rule needs. Structural so tenant-db, the web and the CLI all fit. */
38
+ export type AutoFillColumnInput = {
39
+ kind?: string | null;
40
+ definition?: Record<string, unknown> | null;
41
+ };
42
+ /**
43
+ * What filled this column, or `null` when the answer is "a person did".
44
+ *
45
+ * `manual` and `source` columns are typed or imported by the workspace.
46
+ * `relation` and `bind` columns hold links a person or an identity match made,
47
+ * and the chip already says what it points at — a bolt there would mark the
48
+ * whole CRM as machine-written.
49
+ */
50
+ export declare function columnAutoFill(column: AutoFillColumnInput): ColumnAutoFill | null;
51
+ /** True when the column's value comes from outside the workspace's own typing. */
52
+ export declare function isAutoFilledColumn(column: AutoFillColumnInput): boolean;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * "Did I type this, or did Oxygen?" — the single rule behind the bolt.
3
+ *
4
+ * A CRM record mixes two kinds of field in one list: the handful a founder
5
+ * types (name, stage, owner, a phone they were given) and the majority Oxygen
6
+ * fills from one paid LinkedIn scrape and the free extractors over its payload.
7
+ * Reading a record without that distinction, every cell looks equally
8
+ * authoritative — which is exactly backwards, because a scraped headline and a
9
+ * hand-corrected job title deserve different trust.
10
+ *
11
+ * The grid already marks derived columns in its header (`ColumnDefinitionBadge`)
12
+ * and the record page did not, so the same field read as enriched in one place
13
+ * and hand-entered in the other. This is the shared answer both surfaces call.
14
+ *
15
+ * The rule is TOTAL over column kinds and deliberately coarse: a bolt means
16
+ * "you did not type this", and the label says what did. Splitting hairs between
17
+ * an AI column and a formula over an AI column's payload is an implementation
18
+ * detail a customer has no reason to hold.
19
+ */
20
+ function readString(definition, key) {
21
+ const value = definition?.[key];
22
+ return typeof value === "string" && value.trim() !== "" ? value : null;
23
+ }
24
+ /**
25
+ * A `fill: "empty"` formula is not an ordinary calculation. It is the codebase's
26
+ * "an extracted fact is a FIELD, not a formula" shape: the expression runs once
27
+ * over an enrichment payload the row already paid for, stores a real typed
28
+ * value, and leaves a human's correction alone on the next run. To the person
29
+ * reading the record that is enrichment, not arithmetic.
30
+ */
31
+ function isExtractedFromPayload(column) {
32
+ return readString(column.definition, "fill") === "empty";
33
+ }
34
+ /** Name the provider lane when the column declares one, so the tooltip is specific. */
35
+ function enrichmentLabel(column) {
36
+ const toolId = readString(column.definition, "toolId") ?? readString(column.definition, "tool_id");
37
+ // Vendor names never reach a customer surface — the managed scraper is a
38
+ // deliberately vendor-neutral rail (`packages/providers/src/descriptors/scraper.ts`),
39
+ // and tests fail if the vendor behind it leaks. Speak in terms of the SOURCE.
40
+ if (toolId?.startsWith("scraper.linkedin"))
41
+ return "Enriched from LinkedIn";
42
+ if (toolId)
43
+ return "Enriched by a data provider";
44
+ return "Enriched automatically";
45
+ }
46
+ /**
47
+ * What filled this column, or `null` when the answer is "a person did".
48
+ *
49
+ * `manual` and `source` columns are typed or imported by the workspace.
50
+ * `relation` and `bind` columns hold links a person or an identity match made,
51
+ * and the chip already says what it points at — a bolt there would mark the
52
+ * whole CRM as machine-written.
53
+ */
54
+ export function columnAutoFill(column) {
55
+ switch (column.kind) {
56
+ case "tool":
57
+ case "enrichment":
58
+ return { kind: "enrichment", label: enrichmentLabel(column) };
59
+ case "ai":
60
+ return { kind: "ai", label: "Written by an AI column" };
61
+ case "research":
62
+ return { kind: "ai", label: "Researched on the web by AI" };
63
+ case "formula":
64
+ return isExtractedFromPayload(column)
65
+ ? { kind: "enrichment", label: "Extracted from enrichment data" }
66
+ : { kind: "derived", label: "Calculated from other fields" };
67
+ case "lookup":
68
+ return { kind: "linked", label: "Read from a linked record" };
69
+ case "workflow":
70
+ return { kind: "system", label: "Written by a workflow" };
71
+ case "system":
72
+ return { kind: "system", label: "Maintained by Oxygen" };
73
+ default:
74
+ return null;
75
+ }
76
+ }
77
+ /** True when the column's value comes from outside the workspace's own typing. */
78
+ export function isAutoFilledColumn(column) {
79
+ return columnAutoFill(column) !== null;
80
+ }
@@ -26,8 +26,10 @@
26
26
  * may contain a dot or a space. Such a field carries `referenceable: false` and
27
27
  * is display-only.
28
28
  */
29
- import { buildResearchCellSchema, deriveResearchOutputContract, usesServerManagedResearchSchema, } from "./research-output-contract.js";
29
+ import { buildResearchCellSchema, isResearchEnvelopeSchema, researchEvidenceRequired, deriveResearchOutputContract, usesServerManagedResearchSchema, } from "./research-output-contract.js";
30
30
  import { isTemplateSafePath, parseJsonPath, readJsonPath } from "./json-path.js";
31
+ import { isRecord } from "./type-guards.js";
32
+ import { readString } from "./value-readers.js";
31
33
  const MAX_FIELDS = 40;
32
34
  const MAX_SCHEMA_DEPTH = 5;
33
35
  const EMPTY_CONTRACT = {
@@ -36,12 +38,6 @@ const EMPTY_CONTRACT = {
36
38
  primaryField: null,
37
39
  serverManaged: false,
38
40
  };
39
- function isRecord(value) {
40
- return typeof value === "object" && value !== null && !Array.isArray(value);
41
- }
42
- function readString(value) {
43
- return typeof value === "string" && value.trim() ? value.trim() : null;
44
- }
45
41
  export function humanizeFieldSegment(segment) {
46
42
  const titled = segment
47
43
  .split(/[_\s-]+/)
@@ -263,13 +259,21 @@ function researchContract(definition) {
263
259
  const prompt = readString(definition.prompt) ?? "";
264
260
  const webSearch = isRecord(definition.webSearch) ? definition.webSearch : null;
265
261
  const contract = deriveResearchOutputContract(prompt, {
266
- evidenceRequired: webSearch?.evidenceMode === "strict",
262
+ evidenceRequired: researchEvidenceRequired(webSearch),
267
263
  });
268
264
  // The STORED shape, not the model-facing one: a research cell carries
269
- // `sources`, never the `citations` the model answered with.
265
+ // `sources`, never the `citations` the model answered with. A column whose
266
+ // own schema is still the research envelope (a catalog template's typed
267
+ // answer) gets the same rewrite, so `answer.plan_count` is offered and
268
+ // `citations` is not; a hand-authored schema that replaced the envelope is
269
+ // used verbatim, because the cell then holds exactly what it declares.
270
270
  const schema = serverManaged
271
271
  ? buildResearchCellSchema(contract)
272
- : (isRecord(definition.outputSchema) ? definition.outputSchema : null);
272
+ : isRecord(definition.outputSchema)
273
+ ? (isResearchEnvelopeSchema(definition.outputSchema)
274
+ ? buildResearchCellSchema(null, definition.outputSchema)
275
+ : definition.outputSchema)
276
+ : null;
273
277
  if (!schema)
274
278
  return EMPTY_CONTRACT;
275
279
  return {
@@ -0,0 +1,108 @@
1
+ export declare const COMPANY_ENRICHMENT_FIELD_CATEGORIES: readonly ["identity", "firmographics", "funding", "relationships", "signals", "technology", "web_presence"];
2
+ export type CompanyEnrichmentFieldCategory = (typeof COMPANY_ENRICHMENT_FIELD_CATEGORIES)[number];
3
+ export declare const COMPANY_ENRICHMENT_FIELD_CATEGORY_LABELS: Record<CompanyEnrichmentFieldCategory, string>;
4
+ export type CompanyEnrichmentFieldDataType = "text" | "numeric" | "jsonb";
5
+ /**
6
+ * Where a DERIVED field reads its value from: another field's already-resolved
7
+ * payload. `autoRun: true` means requesting the derived field on its own fetches
8
+ * that source first (the source's own lanes run as a pre-step, billed and
9
+ * disclosed in the plan). `autoRun: false` means the source is consulted only
10
+ * when the same run resolved it anyway or the row already holds it — never
11
+ * fetched just for this field, because it would cost more than the field is
12
+ * worth (a 490-credit funding lookup for a 5-credit Facebook URL).
13
+ */
14
+ export type CompanyEnrichmentDerivedSource = {
15
+ field: CompanyEnrichmentFieldKey;
16
+ autoRun: boolean;
17
+ };
18
+ export type CompanyEnrichmentFieldDefinition = {
19
+ field: CompanyEnrichmentFieldKey;
20
+ label: string;
21
+ category: CompanyEnrichmentFieldCategory;
22
+ /** ONE founder-voice line, rendered next to the field in every picker. */
23
+ description: string;
24
+ /** Default target column key when a table has no matching column. */
25
+ columnKey: string;
26
+ dataType: CompanyEnrichmentFieldDataType;
27
+ /** Semantic type stamped on the created target column, when one applies. */
28
+ semanticType: string | null;
29
+ /** Ordered sources a derived field reads; empty for a lane-resolved field. */
30
+ derivedFrom: readonly CompanyEnrichmentDerivedSource[];
31
+ /** Extra caller input the field needs before it can answer. */
32
+ requires?: "check_technologies";
33
+ /**
34
+ * `coming_soon` (D51, 2026-09-19): the field keeps its vocabulary entry and
35
+ * column key so existing columns still render, but it has no provider lane,
36
+ * no picker group and no `--fields` acceptance until its lane is re-measured.
37
+ */
38
+ availability?: "coming_soon";
39
+ };
40
+ export type CompanyEnrichmentFieldKey = "domain" | "linkedin_url" | "headcount" | "industry" | "funding" | "revenue" | "technologies" | "hiring_signals" | "company_profile" | "description" | "founded_year" | "hq_address" | "hq_country" | "logo_url" | "specialties" | "company_type" | "funding_stage" | "latest_funding_round" | "total_funding" | "funding_rounds_count" | "investors" | "competitors" | "corporate_structure" | "parent_company" | "subsidiaries" | "acquisitions" | "news" | "job_openings" | "technology_check" | "crunchbase_url" | "github_url" | "facebook_url" | "instagram_url" | "youtube_url";
41
+ export declare const COMPANY_ENRICHMENT_FIELD_DEFINITIONS: readonly CompanyEnrichmentFieldDefinition[];
42
+ export declare const COMPANY_ENRICHMENT_FIELD_KEYS: readonly CompanyEnrichmentFieldKey[];
43
+ export declare function isCompanyEnrichmentFieldKey(value: string): value is CompanyEnrichmentFieldKey;
44
+ export declare function companyEnrichmentFieldDefinition(field: CompanyEnrichmentFieldKey): CompanyEnrichmentFieldDefinition;
45
+ /** D51: a field parked `coming_soon` has no lane, no picker group and is refused by `--fields`. */
46
+ export declare function isComingSoonCompanyEnrichmentField(field: CompanyEnrichmentFieldKey): boolean;
47
+ /** A field whose value is read out of another field's payload, never a provider call of its own. */
48
+ export declare function isDerivedCompanyEnrichmentField(field: CompanyEnrichmentFieldKey): boolean;
49
+ /**
50
+ * What `find company` resolves when the caller names no fields. Identity plus
51
+ * the firmographics that cost nothing beyond the one profile lookup the
52
+ * headcount/industry lanes already make; funding, relationship and web-presence
53
+ * fields stay opt-in because each adds a priced lane.
54
+ */
55
+ export declare const DEFAULT_COMPANY_FIND_FIELDS: readonly CompanyEnrichmentFieldKey[];
56
+ /**
57
+ * What `columns add --preset company_enrich` creates when the caller names no
58
+ * fields: the identity/firmographic set plus the full profile and the funding
59
+ * facts LinkedIn publishes for free off that same profile. Every lane-priced
60
+ * relationship, signal, technology and web-presence field is opt-in through
61
+ * `--fields`, so the default bundle's per-row ceiling stays the profile chain's.
62
+ */
63
+ export declare const DEFAULT_COMPANY_PRESET_FIELDS: readonly CompanyEnrichmentFieldKey[];
64
+ export type CompanyEnrichmentFieldGroupId = "profile_details" | "funding_details" | "job_openings" | "tech_stack" | "technology_check";
65
+ export type CompanyEnrichmentFieldGroup = {
66
+ id: CompanyEnrichmentFieldGroupId;
67
+ /** Picker row and catalog label, in customer words. */
68
+ label: string;
69
+ /** ONE founder-voice line. No provider names. */
70
+ description: string;
71
+ /** The catalog fields this group adds to the company bundle, in column order. */
72
+ fields: readonly CompanyEnrichmentFieldKey[];
73
+ /**
74
+ * What the group does to the per-row bill.
75
+ *
76
+ * `included` — every field is read out of the company profile the default
77
+ * bundle already fetches, so ticking it adds columns and no credits.
78
+ * `extra` — at least one field has its own provider lookup, so the run can
79
+ * bill more per row. The exact number depends on which lane answers, which
80
+ * only a dry run over the real rows can say; no surface quotes one here.
81
+ */
82
+ cost: "included" | "extra";
83
+ /** Set when the group cannot be added without extra caller input. */
84
+ requires?: "check_technologies";
85
+ };
86
+ /**
87
+ * The opt-in company fields, grouped the way a customer asks for them ("add
88
+ * competitors", "add their tech stack"). Every surface that offers a company
89
+ * field beyond the default bundle reads THIS list: the enrichment column catalog
90
+ * (`oxygen enrichment catalog`, `oxygen_enrichment_catalog`), the web add-column
91
+ * picker, and the catalog pricing. Adding a group adds its fields to the SAME
92
+ * company enrichment column — re-applying the bundle widens the existing
93
+ * column's field list instead of creating a second paid column.
94
+ */
95
+ export declare const COMPANY_ENRICHMENT_FIELD_GROUPS: readonly CompanyEnrichmentFieldGroup[];
96
+ export declare function companyEnrichmentFieldGroup(id: string): CompanyEnrichmentFieldGroup | null;
97
+ /**
98
+ * Parse a comma-separated or array field list against the catalog. Returns the
99
+ * unique, lowercased keys in request order, or throws naming every unknown
100
+ * entry with the supported list — never a silent fallback to a default set,
101
+ * which is how `--fields tech,hiring,profile` once ran the four default fields
102
+ * and billed for an answer nobody asked for (OXP-7.2.9).
103
+ */
104
+ export declare function parseCompanyEnrichmentFieldList(value: unknown): {
105
+ fields: CompanyEnrichmentFieldKey[];
106
+ unknown: string[];
107
+ coming_soon: CompanyEnrichmentFieldKey[];
108
+ };