@oxygen-agent/cli 1.948.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 (54) 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 +13 -5
  7. package/dist/help.js +1 -0
  8. package/dist/index.js +1171 -240
  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 +122 -8
  13. package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +2 -0
  14. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -0
  15. package/node_modules/@oxygen/shared/dist/capability-discovery.js +99 -15
  16. package/node_modules/@oxygen/shared/dist/copilot-errors.js +3 -0
  17. package/node_modules/@oxygen/shared/dist/copilot-journeys.d.ts +19 -1
  18. package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.d.ts +19 -0
  19. package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.js +26 -0
  20. package/node_modules/@oxygen/shared/dist/copilot-journeys.js +8 -41
  21. package/node_modules/@oxygen/shared/dist/inbox-avatar-url.d.ts +28 -0
  22. package/node_modules/@oxygen/shared/dist/inbox-avatar-url.js +57 -0
  23. package/node_modules/@oxygen/shared/dist/index.d.ts +4 -0
  24. package/node_modules/@oxygen/shared/dist/index.js +4 -0
  25. package/node_modules/@oxygen/shared/dist/knowledge-bases.d.ts +74 -0
  26. package/node_modules/@oxygen/shared/dist/knowledge-bases.js +456 -0
  27. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +17 -38
  28. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +14 -39
  29. package/node_modules/@oxygen/shared/dist/knowledge-repository.d.ts +22 -0
  30. package/node_modules/@oxygen/shared/dist/knowledge-repository.js +121 -0
  31. package/node_modules/@oxygen/shared/dist/knowledge-vault-markdown.d.ts +20 -0
  32. package/node_modules/@oxygen/shared/dist/knowledge-vault-markdown.js +155 -0
  33. package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +10 -0
  34. package/node_modules/@oxygen/shared/dist/mailbox-import.js +53 -0
  35. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +8 -0
  36. package/node_modules/@oxygen/shared/dist/plan-limits.js +8 -0
  37. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
  38. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
  39. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +24 -0
  40. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +24 -0
  41. package/node_modules/@oxygen/shared/dist/recipes.d.ts +6 -0
  42. package/node_modules/@oxygen/shared/dist/recipes.js +23 -0
  43. package/node_modules/@oxygen/shared/dist/sequences.d.ts +126 -2
  44. package/node_modules/@oxygen/shared/dist/sequences.js +280 -4
  45. package/node_modules/@oxygen/shared/dist/ugc-amplification-identity.d.ts +2 -0
  46. package/node_modules/@oxygen/shared/dist/ugc-amplification-identity.js +24 -0
  47. package/node_modules/@oxygen/shared/dist/ugc.d.ts +8 -0
  48. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +8 -1
  49. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  50. package/node_modules/@oxygen/shared/dist/version.js +3 -1
  51. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +6 -2
  52. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +15 -4
  53. package/node_modules/@oxygen/shared/package.json +15 -0
  54. package/package.json +2 -1
@@ -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",
@@ -215,13 +215,13 @@ export const OXYGEN_CAPABILITY_ROUTES = [
215
215
  gatewayTools: ["oxygen_tables_create", "oxygen_columns_add", "oxygen_enrich_column_preview", "oxygen_tables_link_bulk", "oxygen_callables_manage"],
216
216
  gatewayCommands: ["tables create", "columns add", "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
+ 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
225
  },
226
226
  {
227
227
  id: "messages",
@@ -324,7 +324,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
324
324
  // to workspace-access — "identity" matched the AUTH sense. The nouns a user
325
325
  // reaches for when naming a sending identity or the dialing pool have to land
326
326
  // 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"],
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"],
328
328
  },
329
329
  {
330
330
  id: "publishing",
@@ -526,6 +526,7 @@ export function inferCapabilityRoute(query) {
526
526
  card,
527
527
  recommendedTools: recommendations.tools,
528
528
  recommendedCommands: recommendations.commands,
529
+ ...(recommendations.exactCommand ? { exactCommand: recommendations.exactCommand } : {}),
529
530
  };
530
531
  }
531
532
  export function inferPrimitiveRoute(query) {
@@ -549,6 +550,7 @@ export function serializeCapabilityRoute(route) {
549
550
  endpoint_sections: route.card.endpointSections,
550
551
  recommended_commands: route.recommendedCommands,
551
552
  recommended_tools: route.recommendedTools,
553
+ ...(route.exactCommand ? { exact_command: route.exactCommand } : {}),
552
554
  hydrate: {
553
555
  cli: "oxygen commands get <exact-command> --json",
554
556
  mcp: "oxygen_capabilities_schema",
@@ -568,6 +570,11 @@ function isLinkedInProfileWatcherIntent(query) {
568
570
  function explicitCapabilityIntent(query) {
569
571
  if (/\b(infographic|graphic designer|render html|carousel pages|visual design|gtm flow image)\b/.test(query))
570
572
  return ROUTE_BY_ID.get("visual-rendering") ?? null;
573
+ // A whole-motion ask ("set up inbound-led outbound for my posts") is a
574
+ // Recipe kit + playbook, not the connected-LinkedIn signals card that the
575
+ // "my LinkedIn … engagers" rule below would otherwise claim. Resolve it first.
576
+ if (isInboundLedOutboundIntent(query))
577
+ return ROUTE_BY_PRIMITIVE.get("recipes") ?? null;
571
578
  // A unified sender profile is an owned Sequence identity, even when the ask
572
579
  // names every attached channel (LinkedIn + WhatsApp + email). Resolve this
573
580
  // before public LinkedIn research, whose generic "profile" wording would
@@ -666,9 +673,23 @@ function explicitCapabilityIntent(query) {
666
673
  || /\b(day\s*\d+|days?\s+\d|multi[ -]step|rotate senders?|sender rotation|stop on reply|until (?:they )?reply)\b/.test(query)) {
667
674
  return ROUTE_BY_PRIMITIVE.get("sequences") ?? null;
668
675
  }
676
+ // Existing reply handling is Messages even without a channel name. Keep this
677
+ // after the explicit cadence/enrollment rules so reply-stop programs retain
678
+ // Sequences. The onboarding baseline phrased this as "follow up on replies".
679
+ if (/\b(follow up|triage|answer|handle)\b.{0,40}\breplies\b/.test(query)) {
680
+ return ROUTE_BY_PRIMITIVE.get("messages") ?? null;
681
+ }
669
682
  if (/\b(reply|respond)\b.{0,30}\b(conversation|thread|email|linkedin|whatsapp|dm)\b/.test(query)) {
670
683
  return ROUTE_BY_PRIMITIVE.get("messages") ?? null;
671
684
  }
685
+ // Resolving websites / domains / LinkedIn pages for company NAMES the user
686
+ // already holds is the companies-search `url_recovery` intent, not a fresh
687
+ // sourcing search and not an enrichment column. A 2026-09-11 blind user
688
+ // asking "get company website URL from company name" got NO route here
689
+ // (null), never discovered `companies search plan`, and priced the whole
690
+ // company_enrich preset instead.
691
+ if (isCompanyUrlRecoveryIntent(query))
692
+ return ROUTE_BY_ID.get("sourcing-and-provider-tools") ?? null;
672
693
  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
694
  return ROUTE_BY_ID.get("sourcing-and-provider-tools") ?? null;
674
695
  }
@@ -677,16 +698,43 @@ function explicitCapabilityIntent(query) {
677
698
  }
678
699
  return null;
679
700
  }
701
+ /**
702
+ * Does `term` occur in `query` starting on a word boundary?
703
+ *
704
+ * A plain `query.includes(term)` is what sent "enrich these le**ad**s" and
705
+ * "build me a spre**ad**sheet" to the competitor-ad-intelligence card, and
706
+ * "our i**deal** customer" to CRM Records: a short term matches inside any
707
+ * longer word that happens to contain its letters, and nothing else scores, so
708
+ * the wrong card wins outright rather than narrowly.
709
+ *
710
+ * Only the START of the match is anchored, deliberately. Requiring a boundary at
711
+ * BOTH ends looks stricter and is wrong here, because the cards do not list
712
+ * their own plurals consistently — `sequence`, `workflow`, `table`, `agent` and
713
+ * `campaign` have no plural term beside them — so whole-word matching silently
714
+ * stops routing "sequences", "workflows" and "tables", which are the phrasings
715
+ * people actually type. Anchoring the head keeps every plural and inflection
716
+ * working while refusing the mid-word accidents, and it is the same
717
+ * word-boundary treatment v1.915.0 applied to the Tables guard this scorer sits
718
+ * behind.
719
+ */
720
+ function matchesIntentTerm(query, term) {
721
+ for (let at = query.indexOf(term); at >= 0; at = query.indexOf(term, at + 1)) {
722
+ const preceding = at === 0 ? "" : query[at - 1] ?? "";
723
+ if (!/[a-z0-9]/.test(preceding))
724
+ return true;
725
+ }
726
+ return false;
727
+ }
680
728
  function highestScoringRoute(query) {
681
729
  let best = null;
682
730
  for (const card of OXYGEN_CAPABILITY_ROUTES) {
683
731
  let score = 0;
684
732
  for (const term of card.intentTerms) {
685
- if (query.includes(term))
733
+ if (matchesIntentTerm(query, term))
686
734
  score += term.includes(" ") ? 6 : 2;
687
735
  }
688
736
  for (const term of card.negativeTerms ?? []) {
689
- if (query.includes(term))
737
+ if (matchesIntentTerm(query, term))
690
738
  score -= term.includes(" ") ? 8 : 3;
691
739
  }
692
740
  if (score > (best?.score ?? 0))
@@ -694,7 +742,16 @@ function highestScoringRoute(query) {
694
742
  }
695
743
  return best?.card ?? null;
696
744
  }
745
+ /** The exact `companies search plan` invocation for the url_recovery intent. */
746
+ export const COMPANY_URL_RECOVERY_COMMAND = 'oxygen companies search plan --prompt "Find the websites for these companies: <company names>" --source-intent url_recovery';
697
747
  function recommendationsFor(card, query) {
748
+ if (card.id === "sourcing-and-provider-tools" && isCompanyUrlRecoveryIntent(query)) {
749
+ return {
750
+ tools: ["oxygen_companies_search_plan", "oxygen_companies_search_run"],
751
+ commands: ["companies search plan", "companies search run"],
752
+ exactCommand: COMPANY_URL_RECOVERY_COMMAND,
753
+ };
754
+ }
698
755
  if (card.id === "connected-linkedin" && isMutualLinkedInConnectionsIntent(query)) {
699
756
  return {
700
757
  tools: ["oxygen_tools_get", "oxygen_tools_run_live", "oxygen_senders_list"],
@@ -948,6 +1005,25 @@ function isEmailVerificationIntent(query) {
948
1005
  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
1006
  return gradeThenAddress.test(query) || addressThenGrade.test(query);
950
1007
  }
1008
+ // "get company website URL from company name", "find the websites for this
1009
+ // list of company names", "recover domains for company names" — a URL-ish
1010
+ // noun, a company noun, a recovery verb, and the hint that the input is NAMES
1011
+ // or a LIST (or that URLs are MISSING). Every leg is required so a sourcing
1012
+ // ask ("find companies with a website in Germany") keeps its owner. LinkedIn
1013
+ // page recovery is deliberately left to the LinkedIn rules above it, which
1014
+ // already resolve company pages through the public-research catalog.
1015
+ function isCompanyUrlRecoveryIntent(query) {
1016
+ const urlNoun = /\b(websites?|web ?sites?|website urls?|domains?|urls?|homepages?)\b/;
1017
+ const companyNoun = /\b(compan(?:y|ies)|accounts?|organi[sz]ations?|brands?|vendors?)\b/;
1018
+ const recover = /\b(recover|resolve|look ?up|lookup|missing|get|find|fill(?: in)?|add)\b/;
1019
+ const fromNames = /\b(names?|list|missing|these|those)\b/;
1020
+ // An email / phone / contact ask that merely MENTIONS domains is enrichment,
1021
+ // and a column / table ask is a Tables ask — both keep their owners.
1022
+ const otherOwner = /\b(e-?mails?|phones?|mobiles?|numbers?|contacts?|people|persons?|columns?|tables?|enrich\w*|verif\w*)\b/;
1023
+ return urlNoun.test(query) && companyNoun.test(query) && recover.test(query) && fromNames.test(query)
1024
+ && !/\b(scrape|engagers?|comments?|reactions?|posts?)\b/.test(query)
1025
+ && !otherOwner.test(query);
1026
+ }
951
1027
  function isInboxAvatarIntent(query) {
952
1028
  return /\b(avatar|profile (?:picture|photo)|headshot|hosted (?:picture|image)|mailbox (?:picture|photo))\b/.test(query);
953
1029
  }
@@ -974,6 +1050,14 @@ function isMailboxDeleteIntent(query) {
974
1050
  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
1051
  return mailboxScope && removal && !scopedDetach;
976
1052
  }
1053
+ // The composed inbound-led outbound motion: engagers → ICP gate → capped
1054
+ // sequence → CRM. Hyphens are already normalised to spaces by the caller.
1055
+ function isInboundLedOutboundIntent(query) {
1056
+ return /\binbound led outbound\b/.test(query)
1057
+ || /\bengagers?\b.{0,40}\b(into|to) (a |my )?(sequence|pipeline)\b/.test(query)
1058
+ || /\bwarm outbound\b.{0,40}\b(posts?|engagement)\b/.test(query)
1059
+ || /\b(whole|end to end|complete) (gtm )?motion\b/.test(query);
1060
+ }
977
1061
  function isNetNewLinkedInInitiation(query) {
978
1062
  const mentionsLinkedIn = /\blinkedin\b|\bdm\b/.test(query);
979
1063
  const startsConversation = /\b(send|message|dm|contact|reach out|initiate|start)\b/.test(query)
@@ -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,28 @@
1
+ /**
2
+ * Where a hosted inbox-avatar URL may be minted, and how.
3
+ *
4
+ * An avatar URL is handed to an external inbox vendor that fetches it server-side,
5
+ * later, possibly more than once — and Oxygen stores nothing about that hand-off
6
+ * afterwards, so a bad URL cannot be found again or reissued. A URL minted on a
7
+ * preview deployment would be baked into a vendor's mailbox record and die when
8
+ * that deployment does; one minted on localhost never resolves at all. So the
9
+ * origin is an allowlist, not "whatever host served this request", and the SAME
10
+ * allowlist binds every minter: the web request path (which prefers the inbound
11
+ * host) and the worker's LinkedIn-photo backfill (which only has the configured
12
+ * app URL). Two allowlists would drift, and the drift would ship as a mailbox
13
+ * photo that resolves from one deployment and 404s from the other.
14
+ */
15
+ export declare const INBOX_AVATAR_ORIGIN_HOSTS: ReadonlySet<string>;
16
+ /**
17
+ * The first candidate that is an https URL on an allowed host, normalized to
18
+ * `protocol//host`, or null when none qualifies. Returning null rather than a
19
+ * best-effort URL is the point: on localhost the only honest answer is "not from
20
+ * here", and an `http://localhost` URL would sail through this function only to
21
+ * be rejected by an https-only check one step later and much harder to read.
22
+ */
23
+ export declare function resolveInboxAvatarOrigin(candidates: readonly (string | null | undefined)[]): string | null;
24
+ /**
25
+ * The public, permanent URL for a stored inbox avatar on an already-resolved
26
+ * origin — or null when there is no origin, or the key is not an inbox avatar.
27
+ */
28
+ export declare function inboxAvatarPublicUrlForOrigin(origin: string | null, storageKey: string): string | null;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Where a hosted inbox-avatar URL may be minted, and how.
3
+ *
4
+ * An avatar URL is handed to an external inbox vendor that fetches it server-side,
5
+ * later, possibly more than once — and Oxygen stores nothing about that hand-off
6
+ * afterwards, so a bad URL cannot be found again or reissued. A URL minted on a
7
+ * preview deployment would be baked into a vendor's mailbox record and die when
8
+ * that deployment does; one minted on localhost never resolves at all. So the
9
+ * origin is an allowlist, not "whatever host served this request", and the SAME
10
+ * allowlist binds every minter: the web request path (which prefers the inbound
11
+ * host) and the worker's LinkedIn-photo backfill (which only has the configured
12
+ * app URL). Two allowlists would drift, and the drift would ship as a mailbox
13
+ * photo that resolves from one deployment and 404s from the other.
14
+ */
15
+ export const INBOX_AVATAR_ORIGIN_HOSTS = new Set([
16
+ "oxygen-agent.com",
17
+ "www.oxygen-agent.com",
18
+ "dev.oxygen-agent.com",
19
+ ]);
20
+ const INBOX_AVATAR_KEY_PREFIX = "inbox-avatars/";
21
+ /**
22
+ * The first candidate that is an https URL on an allowed host, normalized to
23
+ * `protocol//host`, or null when none qualifies. Returning null rather than a
24
+ * best-effort URL is the point: on localhost the only honest answer is "not from
25
+ * here", and an `http://localhost` URL would sail through this function only to
26
+ * be rejected by an https-only check one step later and much harder to read.
27
+ */
28
+ export function resolveInboxAvatarOrigin(candidates) {
29
+ for (const candidate of candidates) {
30
+ if (!candidate)
31
+ continue;
32
+ try {
33
+ const url = new URL(candidate);
34
+ if (url.protocol === "https:" && INBOX_AVATAR_ORIGIN_HOSTS.has(url.host)) {
35
+ return `${url.protocol}//${url.host}`;
36
+ }
37
+ }
38
+ catch {
39
+ // Not a URL at all — try the next candidate.
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+ /**
45
+ * The public, permanent URL for a stored inbox avatar on an already-resolved
46
+ * origin — or null when there is no origin, or the key is not an inbox avatar.
47
+ */
48
+ export function inboxAvatarPublicUrlForOrigin(origin, storageKey) {
49
+ if (!origin || !storageKey.startsWith(INBOX_AVATAR_KEY_PREFIX))
50
+ return null;
51
+ const path = storageKey
52
+ .slice(INBOX_AVATAR_KEY_PREFIX.length)
53
+ .split("/")
54
+ .map((segment) => encodeURIComponent(segment))
55
+ .join("/");
56
+ return `${origin}/api/inbox-avatars/${path}`;
57
+ }
@@ -47,6 +47,7 @@ export * from "./knowledge-constants.js";
47
47
  export * from "./knowledge-bootstrap.js";
48
48
  export * from "./knowledge-links.js";
49
49
  export * from "./knowledge-markdown.js";
50
+ export * from "./knowledge-vault-markdown.js";
50
51
  export * from "./knowledge-seed-content.js";
51
52
  export * from "./langfuse.js";
52
53
  export * from "./llm-usage.js";
@@ -119,3 +120,6 @@ export declare function isVersionGreater(a: string, b: string): boolean;
119
120
  /** True when `a` is a strictly lesser semantic version than `b`. */
120
121
  export declare function isVersionLess(a: string, b: string): boolean;
121
122
  export * from "./ugc.js";
123
+ export * from "./ugc-amplification-identity.js";
124
+ export * from "./knowledge-repository.js";
125
+ export * from "./knowledge-bases.js";
@@ -47,6 +47,7 @@ export * from "./knowledge-constants.js";
47
47
  export * from "./knowledge-bootstrap.js";
48
48
  export * from "./knowledge-links.js";
49
49
  export * from "./knowledge-markdown.js";
50
+ export * from "./knowledge-vault-markdown.js";
50
51
  export * from "./knowledge-seed-content.js";
51
52
  export * from "./langfuse.js";
52
53
  export * from "./llm-usage.js";
@@ -155,3 +156,6 @@ export function isVersionLess(a, b) {
155
156
  return compareSemver(a, b) < 0;
156
157
  }
157
158
  export * from "./ugc.js";
159
+ export * from "./ugc-amplification-identity.js";
160
+ export * from "./knowledge-repository.js";
161
+ export * from "./knowledge-bases.js";
@@ -0,0 +1,74 @@
1
+ export type KnowledgeBaseViewType = "table" | "cards" | "kanban";
2
+ export type KnowledgeBaseFilter = string | {
3
+ and?: KnowledgeBaseFilter[];
4
+ or?: KnowledgeBaseFilter[];
5
+ not?: KnowledgeBaseFilter[];
6
+ };
7
+ export type KnowledgeBaseIssue = {
8
+ code: "invalid_base" | "unsupported_filter" | "unsupported_view" | "invalid_view";
9
+ message: string;
10
+ expression?: string;
11
+ view?: string;
12
+ };
13
+ export type KnowledgeBaseProperty = {
14
+ displayName?: string;
15
+ raw: Record<string, unknown>;
16
+ };
17
+ export type KnowledgeBaseView = {
18
+ type: string;
19
+ name: string;
20
+ groupBy?: {
21
+ property: string;
22
+ direction: "ASC" | "DESC";
23
+ };
24
+ order: string[];
25
+ boardColumns: string[];
26
+ cardTitleProperty?: string;
27
+ sort: Array<{
28
+ property: string;
29
+ direction: "ASC" | "DESC";
30
+ }>;
31
+ filters?: KnowledgeBaseFilter;
32
+ columnColors: Record<string, string>;
33
+ wipLimits: Record<string, number>;
34
+ raw: Record<string, unknown>;
35
+ };
36
+ export type KnowledgeBaseDefinition = {
37
+ path: string;
38
+ name: string;
39
+ filters?: KnowledgeBaseFilter;
40
+ properties: Record<string, KnowledgeBaseProperty>;
41
+ views: KnowledgeBaseView[];
42
+ raw: Record<string, unknown>;
43
+ };
44
+ export type KnowledgeBaseFile = {
45
+ id: string;
46
+ path: string;
47
+ body?: string;
48
+ properties: Record<string, unknown>;
49
+ revision: number;
50
+ };
51
+ export type KnowledgeBaseParseResult = {
52
+ definition: KnowledgeBaseDefinition | null;
53
+ issues: KnowledgeBaseIssue[];
54
+ };
55
+ export type KnowledgeBaseEvaluation = {
56
+ files: KnowledgeBaseFile[];
57
+ issues: KnowledgeBaseIssue[];
58
+ };
59
+ export type KnowledgeBaseGroup = {
60
+ key: string;
61
+ title: string;
62
+ files: KnowledgeBaseFile[];
63
+ };
64
+ export declare function parseKnowledgeBase(path: string, source: string): KnowledgeBaseParseResult;
65
+ export declare function knowledgeBasePropertyKey(property: string): string;
66
+ export declare function knowledgeBasePropertyValue(file: KnowledgeBaseFile, property: string): unknown;
67
+ export declare function knowledgeBasePropertyText(file: KnowledgeBaseFile, property: string): string;
68
+ export declare function evaluateKnowledgeBaseView(definition: KnowledgeBaseDefinition, view: KnowledgeBaseView, files: readonly KnowledgeBaseFile[]): KnowledgeBaseEvaluation;
69
+ export declare function knowledgeBaseGroups(view: KnowledgeBaseView, files: readonly KnowledgeBaseFile[], configuredColumns?: readonly string[]): KnowledgeBaseGroup[];
70
+ export declare function knowledgeBaseDisplayName(definition: KnowledgeBaseDefinition, property: string): string;
71
+ export declare function knowledgeBaseCardTitle(file: KnowledgeBaseFile, view: KnowledgeBaseView): string;
72
+ export declare function parseBaseBoardColumnOrder(value: unknown): Record<string, string[]>;
73
+ /** Resolve Base Board's `<folder>::<view>::<property>` lane order, with view-name shorthand for API callers. */
74
+ export declare function resolveBaseBoardColumnOrder(definition: KnowledgeBaseDefinition, view: KnowledgeBaseView, configured: Readonly<Record<string, readonly string[]>>): string[];