@oxygen-agent/cli 1.948.1 → 1.987.20

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 (84) 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 +6 -0
  6. package/dist/functions-commands.js +33 -9
  7. package/dist/help.d.ts +21 -0
  8. package/dist/help.js +94 -0
  9. package/dist/index.js +1493 -297
  10. package/dist/knowledge-repository-commands.d.ts +6 -0
  11. package/dist/knowledge-repository-commands.js +198 -0
  12. package/dist/skills.js +20 -0
  13. package/dist/ugc-commands.d.ts +3 -6
  14. package/dist/ugc-commands.js +2 -1086
  15. package/node_modules/@oxygen/cli-ugc/dist/commands.d.ts +3 -0
  16. package/node_modules/@oxygen/cli-ugc/dist/commands.js +1178 -0
  17. package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +7 -0
  18. package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +25 -0
  19. package/node_modules/@oxygen/cli-ugc/dist/index.d.ts +14 -0
  20. package/node_modules/@oxygen/cli-ugc/dist/index.js +5 -0
  21. package/node_modules/@oxygen/cli-ugc/package.json +15 -0
  22. package/node_modules/@oxygen/formula/dist/expression.js +14 -1
  23. package/node_modules/@oxygen/formula/dist/formula-functions.js +71 -1
  24. package/node_modules/@oxygen/formula/dist/index.d.ts +1 -0
  25. package/node_modules/@oxygen/formula/dist/index.js +1 -0
  26. package/node_modules/@oxygen/formula/dist/value-cleaners.d.ts +69 -0
  27. package/node_modules/@oxygen/formula/dist/value-cleaners.js +374 -0
  28. package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +2 -0
  29. package/node_modules/@oxygen/shared/dist/billing.d.ts +27 -27
  30. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -0
  31. package/node_modules/@oxygen/shared/dist/capability-discovery.js +127 -18
  32. package/node_modules/@oxygen/shared/dist/column-output-fields.js +12 -4
  33. package/node_modules/@oxygen/shared/dist/copilot-errors.js +3 -0
  34. package/node_modules/@oxygen/shared/dist/copilot-journeys.d.ts +19 -1
  35. package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.d.ts +19 -0
  36. package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.js +26 -0
  37. package/node_modules/@oxygen/shared/dist/copilot-journeys.js +8 -41
  38. package/node_modules/@oxygen/shared/dist/copilot-playbooks.d.ts +18 -0
  39. package/node_modules/@oxygen/shared/dist/copilot-playbooks.js +43 -0
  40. package/node_modules/@oxygen/shared/dist/copilot-skills.d.ts +15 -0
  41. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +31 -0
  42. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +41 -0
  43. package/node_modules/@oxygen/shared/dist/copilot-skills.js +6 -0
  44. package/node_modules/@oxygen/shared/dist/inbox-avatar-url.d.ts +28 -0
  45. package/node_modules/@oxygen/shared/dist/inbox-avatar-url.js +57 -0
  46. package/node_modules/@oxygen/shared/dist/index.d.ts +5 -0
  47. package/node_modules/@oxygen/shared/dist/index.js +5 -0
  48. package/node_modules/@oxygen/shared/dist/knowledge-bases.d.ts +74 -0
  49. package/node_modules/@oxygen/shared/dist/knowledge-bases.js +456 -0
  50. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +32 -38
  51. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +38 -41
  52. package/node_modules/@oxygen/shared/dist/knowledge-repository.d.ts +22 -0
  53. package/node_modules/@oxygen/shared/dist/knowledge-repository.js +121 -0
  54. package/node_modules/@oxygen/shared/dist/knowledge-vault-markdown.d.ts +20 -0
  55. package/node_modules/@oxygen/shared/dist/knowledge-vault-markdown.js +155 -0
  56. package/node_modules/@oxygen/shared/dist/langfuse.d.ts +12 -1
  57. package/node_modules/@oxygen/shared/dist/langfuse.js +48 -8
  58. package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +10 -0
  59. package/node_modules/@oxygen/shared/dist/mailbox-import.js +53 -0
  60. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +8 -0
  61. package/node_modules/@oxygen/shared/dist/plan-limits.js +8 -0
  62. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
  63. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
  64. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +24 -0
  65. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +24 -0
  66. package/node_modules/@oxygen/shared/dist/recipes.d.ts +6 -0
  67. package/node_modules/@oxygen/shared/dist/recipes.js +23 -0
  68. package/node_modules/@oxygen/shared/dist/research-output-contract.d.ts +33 -1
  69. package/node_modules/@oxygen/shared/dist/research-output-contract.js +64 -2
  70. package/node_modules/@oxygen/shared/dist/sequence-crm-events.d.ts +1 -1
  71. package/node_modules/@oxygen/shared/dist/sequence-hubspot-sync.d.ts +1 -1
  72. package/node_modules/@oxygen/shared/dist/sequences.d.ts +152 -2
  73. package/node_modules/@oxygen/shared/dist/sequences.js +304 -4
  74. package/node_modules/@oxygen/shared/dist/ugc-amplification-identity.d.ts +2 -0
  75. package/node_modules/@oxygen/shared/dist/ugc-amplification-identity.js +24 -0
  76. package/node_modules/@oxygen/shared/dist/ugc.d.ts +8 -0
  77. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +8 -1
  78. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  79. package/node_modules/@oxygen/shared/dist/version.js +3 -1
  80. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +6 -2
  81. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +15 -4
  82. package/node_modules/@oxygen/shared/package.json +25 -0
  83. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +15 -15
  84. package/package.json +6 -2
@@ -56,11 +56,11 @@ export const OXYGEN_CAPABILITY_ROUTES = [
56
56
  primitive: null,
57
57
  owns: "First-run setup and interactive Workspace Copilot sessions over the shared primitive contracts.",
58
58
  notFor: "Recurring deterministic automation or standing adaptive jobs; use Workflows or Agents.",
59
- execution: "Resolve factual onboarding state, then let the user choose a Recipe, Blueprint, or their own outcome before starting a hosted Copilot session.",
59
+ execution: "Use existing onboarding context to understand the user goal and GTM bottleneck, recommend a useful next improvement, and guide its native implementation when selected.",
60
60
  posture: "mixed",
61
61
  gatewayTools: ["oxygen_whoami", "oxygen_recipes_list", "oxygen_blueprints_list", "oxygen_context_resolve", "oxygen_copilot_start"],
62
- gatewayCommands: ["whoami", "recipes list", "blueprints list", "context resolve", "copilot start"],
63
- skills: ["oxygen-quickstart", "oxygen-gtm"],
62
+ gatewayCommands: ["whoami", "context resolve", "recipes list", "blueprints list", "copilot start"],
63
+ skills: ["oxygen-onboarding", "oxygen-quickstart", "oxygen-gtm"],
64
64
  // `activation` remains an exact legacy API family for saved clients, but it
65
65
  // has no recommended gateway and is not an intent term or first-touch path.
66
66
  endpointSections: ["activation", "copilot", "home"],
@@ -68,7 +68,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
68
68
  // users say ("next step of this workflow run"), and a 6-point multi-word hit
69
69
  // would drag those queries here. The terms kept are the ones only a founder
70
70
  // asking where they are in setup actually types.
71
- intentTerms: ["onboard", "onboarding", "first run", "first day", "setup oxygen", "copilot", "assistant", "guided", "where am i", "next step in setup", "what do i do next"],
71
+ intentTerms: ["onboard", "onboarding", "first run", "first day", "setup oxygen", "copilot", "assistant", "guided", "where am i", "next step in setup", "what do i do next", "gtm consultant", "gtm planning", "improve our gtm operation"],
72
72
  },
73
73
  {
74
74
  id: "billing-and-budgets",
@@ -82,7 +82,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
82
82
  gatewayCommands: ["billing balance", "billing usage", "budget list"],
83
83
  skills: ["oxygen-gtm", "oxygen-diagnostics"],
84
84
  endpointSections: ["billing", "budget", "limits"],
85
- intentTerms: ["billing", "credits", "credit balance", "usage", "spend", "cost", "budget", "subscription", "invoice", "cancel plan"],
85
+ intentTerms: ["billing", "credits", "credit balance", "usage", "spend", "cost", "budget", "subscription", "invoice", "cancel plan", "change plan", "upgrade", "downgrade"],
86
86
  },
87
87
  {
88
88
  id: "support-and-feedback",
@@ -174,7 +174,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
174
174
  gatewayCommands: ["knowledge search", "knowledge index", "knowledge page get"],
175
175
  skills: ["oxygen-knowledge"],
176
176
  endpointSections: ["context", "knowledge"],
177
- intentTerms: ["knowledge", "wiki", "context", "icp", "positioning", "offer", "rubric", "brand", "voice", "company profile"],
177
+ intentTerms: ["knowledge", "wiki", "context", "icp", "ideal customer", "positioning", "offer", "rubric", "brand", "voice", "company profile", "github", "obsidian", "vault", "repository sync", "markdown import"],
178
178
  },
179
179
  {
180
180
  id: "recipes",
@@ -182,13 +182,13 @@ export const OXYGEN_CAPABILITY_ROUTES = [
182
182
  primitive: "recipes",
183
183
  owns: "Versioned advisory playbooks that teach when and why to run proven GTM motions through native primitives.",
184
184
  notFor: "A hidden runtime, mandatory bundle, or replacement for hosted Workflows.",
185
- execution: "Browse or install the Recipe; optional Blueprints scaffold infrastructure while native primitives execute.",
185
+ execution: "Browse the Recipe, apply its kit (ordered Blueprint stages, one approval, 0 credits, workflows disabled), then run the play through native primitives.",
186
186
  posture: "workspace_write",
187
187
  gatewayTools: ["oxygen_recipes_list", "oxygen_recipes_get"],
188
- gatewayCommands: ["recipes list", "recipes show", "recipes install"],
189
- skills: ["oxygen-recipes"],
188
+ gatewayCommands: ["recipes list", "recipes show", "recipes apply", "recipes install"],
189
+ skills: ["oxygen-recipes", "oxygen-playbooks"],
190
190
  endpointSections: ["blueprints", "recipes", "templates"],
191
- intentTerms: ["recipe", "playbook", "proven play", "activation play", "setup play", "journey", "day 1", "day 7", "day 30", "blueprint", "template", "what should i do"],
191
+ intentTerms: ["recipe", "playbook", "proven play", "activation play", "setup play", "journey", "day 1", "day 7", "day 30", "blueprint", "template", "what should i do", "inbound led outbound", "engagers into a sequence", "warm outbound from my posts", "whole motion", "end to end motion", "kit"],
192
192
  },
193
193
  {
194
194
  id: "records",
@@ -210,18 +210,23 @@ export const OXYGEN_CAPABILITY_ROUTES = [
210
210
  primitive: "tables",
211
211
  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
212
  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.",
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. 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
214
  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"],
215
+ gatewayTools: ["oxygen_tables_create", "oxygen_columns_add", "oxygen_columns_catalog", "oxygen_enrich_column_preview", "oxygen_tables_link_bulk", "oxygen_callables_manage"],
216
+ gatewayCommands: ["tables create", "columns add", "columns catalog", "enrich-column preview", "tables link", "functions list", "functions draft"],
217
217
  skills: ["oxygen-gtm", "oxygen-table-tidy", "oxygen-diagnostics", "oxygen-clay-migration", "oxygen-linkedin-marketing"],
218
- endpointSections: ["action-columns", "callables", "functions", "columns", "company-enrichment", "enrich-column", "enrichment", "projects", "table-action-items", "table-action-runs", "table-ingestion-runs", "tables"],
218
+ endpointSections: ["action-columns", "callables", "functions", "columns", "company-enrichment", "enrich-column", "enrichment", "projects", "supabase", "table-action-items", "table-action-runs", "table-ingestion-runs", "tables"],
219
219
  // "link"/"join"/"connect"/"relate" route here for `tables link`. Added after a
220
220
  // blind user eval asked for exactly "link two tables" and was routed to
221
221
  // `tables create` / `columns add` / `enrich-column preview` — none of which
222
222
  // do it. The agent only found the right command by grepping the raw 25k-line
223
223
  // command manifest, which is not a discovery path a customer has.
224
- intentTerms: ["table", "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"],
224
+ // "founders" / "parent company" / "cloud provider" / "company research" route
225
+ // here for `columns catalog`. Added after the 2026-09-16 blind baseline asked
226
+ // `capabilities search "founders of a company"` and got `route: null` with a
227
+ // generic sourcing pointer, then hand-wrote four research prompts for
228
+ // questions the template catalog now answers by key.
229
+ 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"],
225
230
  },
226
231
  {
227
232
  id: "messages",
@@ -324,7 +329,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
324
329
  // to workspace-access — "identity" matched the AUTH sense. The nouns a user
325
330
  // reaches for when naming a sending identity or the dialing pool have to land
326
331
  // 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"],
332
+ 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"],
328
333
  },
329
334
  {
330
335
  id: "publishing",
@@ -526,6 +531,7 @@ export function inferCapabilityRoute(query) {
526
531
  card,
527
532
  recommendedTools: recommendations.tools,
528
533
  recommendedCommands: recommendations.commands,
534
+ ...(recommendations.exactCommand ? { exactCommand: recommendations.exactCommand } : {}),
529
535
  };
530
536
  }
531
537
  export function inferPrimitiveRoute(query) {
@@ -549,6 +555,7 @@ export function serializeCapabilityRoute(route) {
549
555
  endpoint_sections: route.card.endpointSections,
550
556
  recommended_commands: route.recommendedCommands,
551
557
  recommended_tools: route.recommendedTools,
558
+ ...(route.exactCommand ? { exact_command: route.exactCommand } : {}),
552
559
  hydrate: {
553
560
  cli: "oxygen commands get <exact-command> --json",
554
561
  mcp: "oxygen_capabilities_schema",
@@ -568,6 +575,11 @@ function isLinkedInProfileWatcherIntent(query) {
568
575
  function explicitCapabilityIntent(query) {
569
576
  if (/\b(infographic|graphic designer|render html|carousel pages|visual design|gtm flow image)\b/.test(query))
570
577
  return ROUTE_BY_ID.get("visual-rendering") ?? null;
578
+ // A whole-motion ask ("set up inbound-led outbound for my posts") is a
579
+ // Recipe kit + playbook, not the connected-LinkedIn signals card that the
580
+ // "my LinkedIn … engagers" rule below would otherwise claim. Resolve it first.
581
+ if (isInboundLedOutboundIntent(query))
582
+ return ROUTE_BY_PRIMITIVE.get("recipes") ?? null;
571
583
  // A unified sender profile is an owned Sequence identity, even when the ask
572
584
  // names every attached channel (LinkedIn + WhatsApp + email). Resolve this
573
585
  // before public LinkedIn research, whose generic "profile" wording would
@@ -666,9 +678,23 @@ function explicitCapabilityIntent(query) {
666
678
  || /\b(day\s*\d+|days?\s+\d|multi[ -]step|rotate senders?|sender rotation|stop on reply|until (?:they )?reply)\b/.test(query)) {
667
679
  return ROUTE_BY_PRIMITIVE.get("sequences") ?? null;
668
680
  }
681
+ // Existing reply handling is Messages even without a channel name. Keep this
682
+ // after the explicit cadence/enrollment rules so reply-stop programs retain
683
+ // Sequences. The onboarding baseline phrased this as "follow up on replies".
684
+ if (/\b(follow up|triage|answer|handle)\b.{0,40}\breplies\b/.test(query)) {
685
+ return ROUTE_BY_PRIMITIVE.get("messages") ?? null;
686
+ }
669
687
  if (/\b(reply|respond)\b.{0,30}\b(conversation|thread|email|linkedin|whatsapp|dm)\b/.test(query)) {
670
688
  return ROUTE_BY_PRIMITIVE.get("messages") ?? null;
671
689
  }
690
+ // Resolving websites / domains / LinkedIn pages for company NAMES the user
691
+ // already holds is the companies-search `url_recovery` intent, not a fresh
692
+ // sourcing search and not an enrichment column. A 2026-09-11 blind user
693
+ // asking "get company website URL from company name" got NO route here
694
+ // (null), never discovered `companies search plan`, and priced the whole
695
+ // company_enrich preset instead.
696
+ if (isCompanyUrlRecoveryIntent(query))
697
+ return ROUTE_BY_ID.get("sourcing-and-provider-tools") ?? null;
672
698
  if (/\b(find emails?|find phones?|find companies|find people|source (?:leads?|a list|companies|people)|build (?:me )?a list|tam|prospect)\b/.test(query)) {
673
699
  return ROUTE_BY_ID.get("sourcing-and-provider-tools") ?? null;
674
700
  }
@@ -677,16 +703,43 @@ function explicitCapabilityIntent(query) {
677
703
  }
678
704
  return null;
679
705
  }
706
+ /**
707
+ * Does `term` occur in `query` starting on a word boundary?
708
+ *
709
+ * A plain `query.includes(term)` is what sent "enrich these le**ad**s" and
710
+ * "build me a spre**ad**sheet" to the competitor-ad-intelligence card, and
711
+ * "our i**deal** customer" to CRM Records: a short term matches inside any
712
+ * longer word that happens to contain its letters, and nothing else scores, so
713
+ * the wrong card wins outright rather than narrowly.
714
+ *
715
+ * Only the START of the match is anchored, deliberately. Requiring a boundary at
716
+ * BOTH ends looks stricter and is wrong here, because the cards do not list
717
+ * their own plurals consistently — `sequence`, `workflow`, `table`, `agent` and
718
+ * `campaign` have no plural term beside them — so whole-word matching silently
719
+ * stops routing "sequences", "workflows" and "tables", which are the phrasings
720
+ * people actually type. Anchoring the head keeps every plural and inflection
721
+ * working while refusing the mid-word accidents, and it is the same
722
+ * word-boundary treatment v1.915.0 applied to the Tables guard this scorer sits
723
+ * behind.
724
+ */
725
+ function matchesIntentTerm(query, term) {
726
+ for (let at = query.indexOf(term); at >= 0; at = query.indexOf(term, at + 1)) {
727
+ const preceding = at === 0 ? "" : query[at - 1] ?? "";
728
+ if (!/[a-z0-9]/.test(preceding))
729
+ return true;
730
+ }
731
+ return false;
732
+ }
680
733
  function highestScoringRoute(query) {
681
734
  let best = null;
682
735
  for (const card of OXYGEN_CAPABILITY_ROUTES) {
683
736
  let score = 0;
684
737
  for (const term of card.intentTerms) {
685
- if (query.includes(term))
738
+ if (matchesIntentTerm(query, term))
686
739
  score += term.includes(" ") ? 6 : 2;
687
740
  }
688
741
  for (const term of card.negativeTerms ?? []) {
689
- if (query.includes(term))
742
+ if (matchesIntentTerm(query, term))
690
743
  score -= term.includes(" ") ? 8 : 3;
691
744
  }
692
745
  if (score > (best?.score ?? 0))
@@ -694,7 +747,16 @@ function highestScoringRoute(query) {
694
747
  }
695
748
  return best?.card ?? null;
696
749
  }
750
+ /** The exact `companies search plan` invocation for the url_recovery intent. */
751
+ export const COMPANY_URL_RECOVERY_COMMAND = 'oxygen companies search plan --prompt "Find the websites for these companies: <company names>" --source-intent url_recovery';
697
752
  function recommendationsFor(card, query) {
753
+ if (card.id === "sourcing-and-provider-tools" && isCompanyUrlRecoveryIntent(query)) {
754
+ return {
755
+ tools: ["oxygen_companies_search_plan", "oxygen_companies_search_run"],
756
+ commands: ["companies search plan", "companies search run"],
757
+ exactCommand: COMPANY_URL_RECOVERY_COMMAND,
758
+ };
759
+ }
698
760
  if (card.id === "connected-linkedin" && isMutualLinkedInConnectionsIntent(query)) {
699
761
  return {
700
762
  tools: ["oxygen_tools_get", "oxygen_tools_run_live", "oxygen_senders_list"],
@@ -781,6 +843,16 @@ function recommendationsFor(card, query) {
781
843
  commands: ["tables watcher preview", "tables watcher create", "tables watcher get", "tables watcher update", "tables watcher pause", "tables watcher resume"],
782
844
  };
783
845
  }
846
+ if (isCompanyResearchQuestionIntent(query)) {
847
+ // A fact the template catalog already answers by key: list the keys first
848
+ // (free), add the column by key (free), then price and run it. Ahead of
849
+ // the bare "run" noun below, because "which cloud provider does it run
850
+ // on" is a question about the company, not about a run.
851
+ return {
852
+ tools: ["oxygen_columns_catalog", "oxygen_columns_add", "oxygen_columns_run", "oxygen_tables_describe"],
853
+ commands: ["columns catalog", "columns add", "columns run", "tables describe"],
854
+ };
855
+ }
784
856
  if (/\b(table )?(action )?runs?\b/.test(query)) {
785
857
  return {
786
858
  tools: ["oxygen_table_runs_get", "oxygen_table_runs_items", "oxygen_table_runs_wait", "oxygen_table_runs_retry_failed"],
@@ -948,6 +1020,35 @@ function isEmailVerificationIntent(query) {
948
1020
  const addressThenGrade = /\b(e ?mails?|addresses)\b.{0,40}\b(verif(?:y|ied|ication)|validat(?:e|ed|ion)|valid|invalid|deliverable|(?:will|would|might) bounce|bounce risk|risky|safe to (?:send|email)|catch ?all|accept ?all)\b/;
949
1021
  return gradeThenAddress.test(query) || addressThenGrade.test(query);
950
1022
  }
1023
+ // "get company website URL from company name", "find the websites for this
1024
+ // list of company names", "recover domains for company names" — a URL-ish
1025
+ // noun, a company noun, a recovery verb, and the hint that the input is NAMES
1026
+ // or a LIST (or that URLs are MISSING). Every leg is required so a sourcing
1027
+ // ask ("find companies with a website in Germany") keeps its owner. LinkedIn
1028
+ // page recovery is deliberately left to the LinkedIn rules above it, which
1029
+ // already resolve company pages through the public-research catalog.
1030
+ /**
1031
+ * A company fact the template catalog answers by key — founders, parent company,
1032
+ * subsidiaries, cloud provider, NAICS, demos — and the "ready-made / template
1033
+ * catalog / research question" phrasing itself. Kept to nouns no other card
1034
+ * owns: "funding" alone belongs to Signals and "industry" alone to company
1035
+ * search, so both stay off this list.
1036
+ */
1037
+ function isCompanyResearchQuestionIntent(query) {
1038
+ 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);
1039
+ }
1040
+ function isCompanyUrlRecoveryIntent(query) {
1041
+ const urlNoun = /\b(websites?|web ?sites?|website urls?|domains?|urls?|homepages?)\b/;
1042
+ const companyNoun = /\b(compan(?:y|ies)|accounts?|organi[sz]ations?|brands?|vendors?)\b/;
1043
+ const recover = /\b(recover|resolve|look ?up|lookup|missing|get|find|fill(?: in)?|add)\b/;
1044
+ const fromNames = /\b(names?|list|missing|these|those)\b/;
1045
+ // An email / phone / contact ask that merely MENTIONS domains is enrichment,
1046
+ // and a column / table ask is a Tables ask — both keep their owners.
1047
+ const otherOwner = /\b(e-?mails?|phones?|mobiles?|numbers?|contacts?|people|persons?|columns?|tables?|enrich\w*|verif\w*)\b/;
1048
+ return urlNoun.test(query) && companyNoun.test(query) && recover.test(query) && fromNames.test(query)
1049
+ && !/\b(scrape|engagers?|comments?|reactions?|posts?)\b/.test(query)
1050
+ && !otherOwner.test(query);
1051
+ }
951
1052
  function isInboxAvatarIntent(query) {
952
1053
  return /\b(avatar|profile (?:picture|photo)|headshot|hosted (?:picture|image)|mailbox (?:picture|photo))\b/.test(query);
953
1054
  }
@@ -974,6 +1075,14 @@ function isMailboxDeleteIntent(query) {
974
1075
  const scopedDetach = /\b(remove|disconnect)\b.{0,48}\b(mailbox(?:es)?|inbox(?:es)?|sender accounts?|email accounts?)\b.{0,24}\b(?:from|in|on)\b.{0,24}\b(sequence|campaign|cadence|sender profile)\b/.test(query);
975
1076
  return mailboxScope && removal && !scopedDetach;
976
1077
  }
1078
+ // The composed inbound-led outbound motion: engagers → ICP gate → capped
1079
+ // sequence → CRM. Hyphens are already normalised to spaces by the caller.
1080
+ function isInboundLedOutboundIntent(query) {
1081
+ return /\binbound led outbound\b/.test(query)
1082
+ || /\bengagers?\b.{0,40}\b(into|to) (a |my )?(sequence|pipeline)\b/.test(query)
1083
+ || /\bwarm outbound\b.{0,40}\b(posts?|engagement)\b/.test(query)
1084
+ || /\b(whole|end to end|complete) (gtm )?motion\b/.test(query);
1085
+ }
977
1086
  function isNetNewLinkedInInitiation(query) {
978
1087
  const mentionsLinkedIn = /\blinkedin\b|\bdm\b/.test(query);
979
1088
  const startsConversation = /\b(send|message|dm|contact|reach out|initiate|start)\b/.test(query)
@@ -26,7 +26,7 @@
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
31
  const MAX_FIELDS = 40;
32
32
  const MAX_SCHEMA_DEPTH = 5;
@@ -263,13 +263,21 @@ function researchContract(definition) {
263
263
  const prompt = readString(definition.prompt) ?? "";
264
264
  const webSearch = isRecord(definition.webSearch) ? definition.webSearch : null;
265
265
  const contract = deriveResearchOutputContract(prompt, {
266
- evidenceRequired: webSearch?.evidenceMode === "strict",
266
+ evidenceRequired: researchEvidenceRequired(webSearch),
267
267
  });
268
268
  // The STORED shape, not the model-facing one: a research cell carries
269
- // `sources`, never the `citations` the model answered with.
269
+ // `sources`, never the `citations` the model answered with. A column whose
270
+ // own schema is still the research envelope (a catalog template's typed
271
+ // answer) gets the same rewrite, so `answer.plan_count` is offered and
272
+ // `citations` is not; a hand-authored schema that replaced the envelope is
273
+ // used verbatim, because the cell then holds exactly what it declares.
270
274
  const schema = serverManaged
271
275
  ? buildResearchCellSchema(contract)
272
- : (isRecord(definition.outputSchema) ? definition.outputSchema : null);
276
+ : isRecord(definition.outputSchema)
277
+ ? (isResearchEnvelopeSchema(definition.outputSchema)
278
+ ? buildResearchCellSchema(null, definition.outputSchema)
279
+ : definition.outputSchema)
280
+ : null;
273
281
  if (!schema)
274
282
  return EMPTY_CONTRACT;
275
283
  return {
@@ -21,6 +21,9 @@ const WORKER_STEP_TIMEOUT_MESSAGE = /\bWorker step '[^']+' exceeded \d+ms deadli
21
21
  export function customerFacingCopilotError(input) {
22
22
  const code = input.code ?? null;
23
23
  const message = input.message ?? null;
24
+ if (code === "agent_response_incomplete") {
25
+ return { code, message: "I stopped before completing the requested work. Completed actions are saved; ask me to continue." };
26
+ }
24
27
  const isTimeout = code === "worker_step_timeout" ||
25
28
  code === COPILOT_TURN_TIMEOUT_CODE ||
26
29
  code === COPILOT_TURN_DEADLINE_EXCEEDED_CODE ||
@@ -8,6 +8,24 @@ export type CopilotJourney = {
8
8
  seedMessage: string;
9
9
  };
10
10
  export declare const LINKEDIN_NETWORK_COPILOT_JOURNEY_SLUG = "inbound-led-outbound";
11
- export declare const COPILOT_JOURNEYS: readonly CopilotJourney[];
11
+ export declare const COPILOT_JOURNEYS: readonly [{
12
+ readonly slug: "inbound-led-outbound";
13
+ readonly title: "Inbound-led outbound";
14
+ readonly description: "Turn the people who engage with your LinkedIn posts into a capped, evidence-gated warm sequence with replies routed to your CRM.";
15
+ readonly recipeSlugs: readonly ["inbound-led-outbound", "linkedin-network-first-motion", "founder-posting-system", "reply-to-meeting"];
16
+ readonly seedMessage: "I chose the inbound-led-outbound recipe. Help me set up inbound-led outbound for my LinkedIn posts: watch who engages with them every day, assess each person against my ICP, hand me the strong fits, put the possible fits into a draft LinkedIn sequence on a schedule, move replies into my CRM, and write what we learn back into my knowledge wiki. Inspect the workspace first, follow the recipe, and resume from durable evidence instead of starting over. Do not connect an account, enroll anyone, send anything, or spend credits without asking me first.";
17
+ }, {
18
+ readonly slug: "email-infra-setup";
19
+ readonly title: "Outbound infrastructure";
20
+ readonly description: "Domains, inboxes, warmup, sender posture — reviewed and approved before anything is bought or sent.";
21
+ readonly recipeSlugs: readonly ["email-infra-setup"];
22
+ readonly seedMessage: "Inspect my current sending setup (domains, inboxes, warmup, senders, caps, deliverability) and propose what's missing for a safe cold-email motion. Show quotes and costs — do not buy or change anything without my approval.";
23
+ }, {
24
+ readonly slug: "tam-sourcing";
25
+ readonly title: "Map your market";
26
+ readonly description: "Source and segment your addressable market from your ICP, starting with a bounded sample.";
27
+ readonly recipeSlugs: readonly ["icp-to-account-map", "outbound-pilot-50"];
28
+ readonly seedMessage: "Help me map my total addressable market from my ICP. Ground it in my company profile and website, source a bounded sample first, and show me coverage and credit estimates before expanding.";
29
+ }];
12
30
  export type CopilotJourneySlug = (typeof COPILOT_JOURNEYS)[number]["slug"];
13
31
  export declare function getCopilotJourney(slug: string): CopilotJourney | null;
@@ -0,0 +1,19 @@
1
+ export declare const COPILOT_JOURNEYS_GENERATED: readonly [{
2
+ readonly slug: "inbound-led-outbound";
3
+ readonly title: "Inbound-led outbound";
4
+ readonly description: "Turn the people who engage with your LinkedIn posts into a capped, evidence-gated warm sequence with replies routed to your CRM.";
5
+ readonly recipeSlugs: readonly ["inbound-led-outbound", "linkedin-network-first-motion", "founder-posting-system", "reply-to-meeting"];
6
+ readonly seedMessage: "I chose the inbound-led-outbound recipe. Help me set up inbound-led outbound for my LinkedIn posts: watch who engages with them every day, assess each person against my ICP, hand me the strong fits, put the possible fits into a draft LinkedIn sequence on a schedule, move replies into my CRM, and write what we learn back into my knowledge wiki. Inspect the workspace first, follow the recipe, and resume from durable evidence instead of starting over. Do not connect an account, enroll anyone, send anything, or spend credits without asking me first.";
7
+ }, {
8
+ readonly slug: "email-infra-setup";
9
+ readonly title: "Outbound infrastructure";
10
+ readonly description: "Domains, inboxes, warmup, sender posture — reviewed and approved before anything is bought or sent.";
11
+ readonly recipeSlugs: readonly ["email-infra-setup"];
12
+ readonly seedMessage: "Inspect my current sending setup (domains, inboxes, warmup, senders, caps, deliverability) and propose what's missing for a safe cold-email motion. Show quotes and costs — do not buy or change anything without my approval.";
13
+ }, {
14
+ readonly slug: "tam-sourcing";
15
+ readonly title: "Map your market";
16
+ readonly description: "Source and segment your addressable market from your ICP, starting with a bounded sample.";
17
+ readonly recipeSlugs: readonly ["icp-to-account-map", "outbound-pilot-50"];
18
+ readonly seedMessage: "Help me map my total addressable market from my ICP. Ground it in my company profile and website, source a bounded sample first, and show me coverage and credit estimates before expanding.";
19
+ }];
@@ -0,0 +1,26 @@
1
+ // GENERATED by apps/web/scripts/generate-copilot-journeys.ts from the recipe
2
+ // catalog (`journey:` blocks in apps/web/src/agent-skills/oxygen-recipes/recipes).
3
+ // Do not edit by hand — run `npm run skills:metadata -w @oxygen/web`.
4
+ export const COPILOT_JOURNEYS_GENERATED = [
5
+ {
6
+ slug: "inbound-led-outbound",
7
+ title: "Inbound-led outbound",
8
+ description: "Turn the people who engage with your LinkedIn posts into a capped, evidence-gated warm sequence with replies routed to your CRM.",
9
+ recipeSlugs: ["inbound-led-outbound", "linkedin-network-first-motion", "founder-posting-system", "reply-to-meeting"],
10
+ seedMessage: "I chose the inbound-led-outbound recipe. Help me set up inbound-led outbound for my LinkedIn posts: watch who engages with them every day, assess each person against my ICP, hand me the strong fits, put the possible fits into a draft LinkedIn sequence on a schedule, move replies into my CRM, and write what we learn back into my knowledge wiki. Inspect the workspace first, follow the recipe, and resume from durable evidence instead of starting over. Do not connect an account, enroll anyone, send anything, or spend credits without asking me first.",
11
+ },
12
+ {
13
+ slug: "email-infra-setup",
14
+ title: "Outbound infrastructure",
15
+ description: "Domains, inboxes, warmup, sender posture — reviewed and approved before anything is bought or sent.",
16
+ recipeSlugs: ["email-infra-setup"],
17
+ seedMessage: "Inspect my current sending setup (domains, inboxes, warmup, senders, caps, deliverability) and propose what's missing for a safe cold-email motion. Show quotes and costs — do not buy or change anything without my approval.",
18
+ },
19
+ {
20
+ slug: "tam-sourcing",
21
+ title: "Map your market",
22
+ description: "Source and segment your addressable market from your ICP, starting with a bounded sample.",
23
+ recipeSlugs: ["icp-to-account-map", "outbound-pilot-50"],
24
+ seedMessage: "Help me map my total addressable market from my ICP. Ground it in my company profile and website, source a bounded sample first, and show me coverage and credit estimates before expanding.",
25
+ },
26
+ ];
@@ -1,47 +1,14 @@
1
- // Workspace Copilot guided journeys (ADR 0013 §11). One canonical copy shared by
2
- // the runtime (system-prompt grounding on the named recipes) and the web journey
3
- // cards (seeding a session's first message) — the data is pure and client-safe.
4
- // Each journey is a thin descriptor over EXISTING advisory catalog Recipes:
5
- // journeys add no runtime behavior, and every guardrail (never enroll/send/buy/
6
- // change DNS without approval) rides those recipes' own approval gates.
1
+ import { COPILOT_JOURNEYS_GENERATED } from "./copilot-journeys.generated.js";
7
2
  // Stable slug for sessions deliberately launched from the matching Recipe.
8
3
  // It is never a default: first-touch surfaces stay outcome-neutral.
9
4
  export const LINKEDIN_NETWORK_COPILOT_JOURNEY_SLUG = "inbound-led-outbound";
10
- export const COPILOT_JOURNEYS = [
11
- {
12
- slug: LINKEDIN_NETWORK_COPILOT_JOURNEY_SLUG,
13
- title: "Inbound-led outbound",
14
- description: "Work the LinkedIn network you already have — connections and post engagers — as a capped warm motion.",
15
- // The network-first play leads: connections and post engagers are the two
16
- // sources a founder actually has on day 1, and profile viewers (the old lead)
17
- // are a thin, anonymity-capped sample by comparison. Posting is second because
18
- // it CREATES the engager stream this play captures, and replies close the loop.
19
- recipeSlugs: [
20
- "linkedin-network-first-motion",
21
- "founder-posting-system",
22
- "reply-to-meeting",
23
- ],
24
- // Deliberately free of steps, commands, table names, and caps: encoding
25
- // mechanics here would fork a second, unversioned copy of the play that no
26
- // catalog gate can lint (ADR 0013 §11 — a journey is a thin pointer at
27
- // advisory recipes, never a hardcoded kit).
28
- seedMessage: "I chose the linkedin-network-first-motion recipe. Help me turn the LinkedIn network I already have — my connections and the people who engage with my posts — into a small, safe, approval-gated outreach motion. Inspect the relevant workspace primitives, follow the recipe, and resume from durable evidence instead of starting over. Do not connect an account, enroll anyone, send anything, or spend credits without asking me first.",
29
- },
30
- {
31
- slug: "tam-sourcing",
32
- title: "Map your market",
33
- description: "Source and segment your addressable market from your ICP, starting with a bounded sample.",
34
- recipeSlugs: ["icp-to-account-map", "outbound-pilot-50"],
35
- seedMessage: "Help me map my total addressable market from my ICP. Ground it in my company profile and website, source a bounded sample first, and show me coverage and credit estimates before expanding.",
36
- },
37
- {
38
- slug: "email-infra-setup",
39
- title: "Outbound infrastructure",
40
- description: "Domains, inboxes, warmup, sender posture — reviewed and approved before anything is bought or sent.",
41
- recipeSlugs: ["email-infra-setup"],
42
- seedMessage: "Inspect my current sending setup (domains, inboxes, warmup, senders, caps, deliverability) and propose what's missing for a safe cold-email motion. Show quotes and costs — do not buy or change anything without my approval.",
43
- },
44
- ];
5
+ // Derived from the recipe catalog (ADR 0025): every recipe carrying a
6
+ // `journey:` block leads one journey. The list is generated into
7
+ // ./copilot-journeys.generated.ts by apps/web/scripts/generate-copilot-journeys.ts
8
+ // (run through `npm run skills:metadata -w @oxygen/web`) and gated against the
9
+ // catalog by apps/web/src/lib/recipes/copilot-journeys.test.ts, because this
10
+ // package cannot read the markdown the catalog is served from.
11
+ export const COPILOT_JOURNEYS = COPILOT_JOURNEYS_GENERATED;
45
12
  export function getCopilotJourney(slug) {
46
13
  return COPILOT_JOURNEYS.find((journey) => journey.slug === slug) ?? null;
47
14
  }
@@ -0,0 +1,18 @@
1
+ export type CopilotPlaybookSlug = "tam-sourcing" | "linkedin-content-strategy" | "inbound-led-outbound" | "signal-based-outbound";
2
+ export type CopilotPlaybook = {
3
+ /**
4
+ * The skill slug the Copilot loads through its `skills` tool, and the
5
+ * `name:` in the playbook file's frontmatter. The two are gated equal.
6
+ */
7
+ slug: CopilotPlaybookSlug;
8
+ /** Customer-facing name, exactly as it is spoken in the composer prompt and shown on the Home chip. */
9
+ title: string;
10
+ };
11
+ export declare const COPILOT_PLAYBOOKS: readonly CopilotPlaybook[];
12
+ /**
13
+ * The sentence a Home card writes into the composer. It names the skill the way
14
+ * a person would say it, and the Copilot's prompt maps that title back to the
15
+ * slug — so the user reads and sends exactly what the card promised.
16
+ */
17
+ export declare function copilotPlaybookPrompt(playbook: Pick<CopilotPlaybook, "title">): string;
18
+ export declare function getCopilotPlaybook(slug: string): CopilotPlaybook | null;
@@ -0,0 +1,43 @@
1
+ // The four end-to-end GTM playbooks the Workspace Copilot can run, as pure,
2
+ // client-safe data. One canonical copy is read by three consumers: the Home
3
+ // cards (which pre-write the prompt below into the composer), the Copilot's
4
+ // system prompt (which maps the spoken title back to the skill slug the model
5
+ // loads through its `skills` tool), and the apps/web gate that checks every slug
6
+ // here has a served playbook file with a matching frontmatter `name`.
7
+ //
8
+ // A playbook is the operator contract for one whole motion — stages, owners,
9
+ // exact commands, caps, approval gates, stop conditions, learning loop — served
10
+ // as a product skill (`apps/web/src/agent-skills/oxygen-playbooks/playbooks/`).
11
+ // Loading one is never permission to spend or send: every gate inside it still
12
+ // fires. Founder decision 2026-09-16: Home offers exactly these four, above the
13
+ // composer as compact glass chips (title only), and a chip writes the sentence
14
+ // into the box rather than starting a session, so there stays exactly one door.
15
+ export const COPILOT_PLAYBOOKS = [
16
+ {
17
+ slug: "tam-sourcing",
18
+ title: "TAM sourcing",
19
+ },
20
+ {
21
+ slug: "linkedin-content-strategy",
22
+ title: "LinkedIn content strategy",
23
+ },
24
+ {
25
+ slug: "inbound-led-outbound",
26
+ title: "Inbound-led outbound",
27
+ },
28
+ {
29
+ slug: "signal-based-outbound",
30
+ title: "Signal-based outbound",
31
+ },
32
+ ];
33
+ /**
34
+ * The sentence a Home card writes into the composer. It names the skill the way
35
+ * a person would say it, and the Copilot's prompt maps that title back to the
36
+ * slug — so the user reads and sends exactly what the card promised.
37
+ */
38
+ export function copilotPlaybookPrompt(playbook) {
39
+ return `Use the ${playbook.title} skill to get started.`;
40
+ }
41
+ export function getCopilotPlaybook(slug) {
42
+ return COPILOT_PLAYBOOKS.find((playbook) => playbook.slug === slug) ?? null;
43
+ }
@@ -0,0 +1,15 @@
1
+ export type CopilotSkillSnapshot = {
2
+ /** What the model passes to the `skills` tool; equals the bundle's frontmatter `name`. */
3
+ slug: string;
4
+ /** Catalog title, shown when the frontmatter carries no description. */
5
+ title: string;
6
+ /** Served skill files bundled into `content`, as `skill/path`, for provenance. */
7
+ sources: readonly string[];
8
+ /** The exact bundle the tool serves, frontmatter included. */
9
+ content: string;
10
+ /** Lowercase hex sha256 of `content`; the runtime re-hashes every served body against it. */
11
+ sha256: string;
12
+ };
13
+ export declare const COPILOT_SKILL_SNAPSHOTS: readonly CopilotSkillSnapshot[];
14
+ export declare const COPILOT_SKILL_SLUGS: readonly string[];
15
+ export declare function getCopilotSkillSnapshot(slug: string): CopilotSkillSnapshot | null;