@oxygen-agent/cli 1.936.1 → 1.982.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.js +9 -1
  3. package/dist/cli-values.d.ts +14 -0
  4. package/dist/cli-values.js +26 -0
  5. package/dist/command-manifest.js +30 -2
  6. package/dist/functions-commands.js +13 -5
  7. package/dist/help.js +2 -0
  8. package/dist/index.js +1509 -290
  9. package/dist/knowledge-repository-commands.d.ts +6 -0
  10. package/dist/knowledge-repository-commands.js +198 -0
  11. package/dist/skills.js +20 -0
  12. package/dist/ugc-commands.js +470 -15
  13. package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +2 -0
  14. package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +11 -6
  15. package/node_modules/@oxygen/shared/dist/byok-connect.js +14 -6
  16. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -0
  17. package/node_modules/@oxygen/shared/dist/capability-discovery.js +152 -20
  18. package/node_modules/@oxygen/shared/dist/copilot-errors.js +3 -0
  19. package/node_modules/@oxygen/shared/dist/copilot-journeys.d.ts +19 -1
  20. package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.d.ts +19 -0
  21. package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.js +26 -0
  22. package/node_modules/@oxygen/shared/dist/copilot-journeys.js +8 -41
  23. package/node_modules/@oxygen/shared/dist/email-dsn.d.ts +60 -0
  24. package/node_modules/@oxygen/shared/dist/email-dsn.js +120 -0
  25. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.d.ts +64 -0
  26. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.js +90 -0
  27. package/node_modules/@oxygen/shared/dist/inbox-avatar-url.d.ts +28 -0
  28. package/node_modules/@oxygen/shared/dist/inbox-avatar-url.js +57 -0
  29. package/node_modules/@oxygen/shared/dist/index.d.ts +10 -0
  30. package/node_modules/@oxygen/shared/dist/index.js +10 -0
  31. package/node_modules/@oxygen/shared/dist/knowledge-bases.d.ts +74 -0
  32. package/node_modules/@oxygen/shared/dist/knowledge-bases.js +456 -0
  33. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +56 -48
  34. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +50 -49
  35. package/node_modules/@oxygen/shared/dist/knowledge-repository.d.ts +22 -0
  36. package/node_modules/@oxygen/shared/dist/knowledge-repository.js +121 -0
  37. package/node_modules/@oxygen/shared/dist/knowledge-vault-markdown.d.ts +20 -0
  38. package/node_modules/@oxygen/shared/dist/knowledge-vault-markdown.js +155 -0
  39. package/node_modules/@oxygen/shared/dist/langfuse.d.ts +8 -3
  40. package/node_modules/@oxygen/shared/dist/langfuse.js +177 -130
  41. package/node_modules/@oxygen/shared/dist/llm-payload.d.ts +10 -0
  42. package/node_modules/@oxygen/shared/dist/llm-payload.js +54 -0
  43. package/node_modules/@oxygen/shared/dist/llm-usage.d.ts +11 -0
  44. package/node_modules/@oxygen/shared/dist/llm-usage.js +30 -0
  45. package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +10 -0
  46. package/node_modules/@oxygen/shared/dist/mailbox-import.js +53 -0
  47. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +8 -0
  48. package/node_modules/@oxygen/shared/dist/plan-limits.js +8 -0
  49. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
  50. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
  51. package/node_modules/@oxygen/shared/dist/product-analytics-core.d.ts +98 -0
  52. package/node_modules/@oxygen/shared/dist/product-analytics-core.js +159 -0
  53. package/node_modules/@oxygen/shared/dist/product-analytics-environment.d.ts +18 -0
  54. package/node_modules/@oxygen/shared/dist/product-analytics-environment.js +46 -0
  55. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +116 -0
  56. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +120 -0
  57. package/node_modules/@oxygen/shared/dist/recipes.d.ts +6 -0
  58. package/node_modules/@oxygen/shared/dist/recipes.js +23 -0
  59. package/node_modules/@oxygen/shared/dist/sequences.d.ts +126 -2
  60. package/node_modules/@oxygen/shared/dist/sequences.js +280 -4
  61. package/node_modules/@oxygen/shared/dist/ugc-amplification-identity.d.ts +2 -0
  62. package/node_modules/@oxygen/shared/dist/ugc-amplification-identity.js +24 -0
  63. package/node_modules/@oxygen/shared/dist/ugc.d.ts +29 -1
  64. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +8 -1
  65. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  66. package/node_modules/@oxygen/shared/dist/version.js +3 -1
  67. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +6 -2
  68. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +15 -4
  69. package/node_modules/@oxygen/shared/package.json +15 -0
  70. package/node_modules/@oxygen/workflows/dist/graph/lint.js +22 -0
  71. package/package.json +2 -1
@@ -7,6 +7,8 @@ export type RecipeToolRunOptions = {
7
7
  optional?: boolean;
8
8
  /** Pin this call to one provider account for every replay/scheduled run. */
9
9
  connectionId?: string;
10
+ /** Pin the credential rail instead of automatically selecting a connected key. */
11
+ credentialMode?: "managed" | "user_api_key" | "user_oauth";
10
12
  };
11
13
  export type RecipeToolApi = {
12
14
  run: <T = unknown>(toolId: string, payload?: Record<string, unknown>, options?: RecipeToolRunOptions) => Promise<T>;
@@ -4,10 +4,10 @@
4
4
  *
5
5
  * WHY THIS EXISTS. `native_provider.execution.supports_byok` says a TOOL can run on
6
6
  * a customer's own key. It does not say the customer has any way to give us one.
7
- * Those are two different facts, and 10 of the 42 byok-capable native providers
7
+ * Those are two different facts, and 9 of the 42 byok-capable native providers
8
8
  * have the first without the second:
9
9
  *
10
- * apify builtwith dropleads lusha openwebninja parallel peopledatalabs
10
+ * apify builtwith dropleads lusha openwebninja peopledatalabs
11
11
  * rocketreach theirstack wiza
12
12
  *
13
13
  * For those, `oxygen integrations connect <id> --api-key` does not reach
@@ -17,11 +17,16 @@
17
17
  * tool is native generic-REST and `shouldRouteStaticToolThroughComposio` is false, so
18
18
  * `resolveOptionalApiKeyCredentials` finds nothing and the call falls back to the
19
19
  * MANAGED key — the very key that was out of credits. The command appears to work,
20
- * changes nothing, and the customer is back where they started with no error.
20
+ * changes nothing, and the customer is back where they started with no error. The
21
+ * connect route now refuses each of those nine with a typed `byok_connect_unavailable`
22
+ * rather than a silent Composio "success"; they stay OFF this list until one gains a
23
+ * real native connect form.
21
24
  *
22
- * That is precisely the failure Plain T-104 would have hit: `parallel.search` was one
23
- * of the refused tools, and a refusal message that told Mentcape to connect their own
24
- * Parallel key would have sent them down a path that silently does nothing.
25
+ * That is precisely the failure Plain T-104 hit: `parallel.search` was one of the
26
+ * refused tools, and a refusal message that told Mentcape to connect their own Parallel
27
+ * key would once have sent them down a path that silently does nothing. Parallel now has
28
+ * a native api-key connect (catalog.ts) and IS on this list, so that message is finally
29
+ * true for it — the nine above are the ones it must still never name.
25
30
  *
26
31
  * So a message may only offer BYOK when BOTH halves hold, and this list is the second
27
32
  * half. It lives in @oxygen/shared because the two sides that need it cannot see each
@@ -4,10 +4,10 @@
4
4
  *
5
5
  * WHY THIS EXISTS. `native_provider.execution.supports_byok` says a TOOL can run on
6
6
  * a customer's own key. It does not say the customer has any way to give us one.
7
- * Those are two different facts, and 10 of the 42 byok-capable native providers
7
+ * Those are two different facts, and 9 of the 42 byok-capable native providers
8
8
  * have the first without the second:
9
9
  *
10
- * apify builtwith dropleads lusha openwebninja parallel peopledatalabs
10
+ * apify builtwith dropleads lusha openwebninja peopledatalabs
11
11
  * rocketreach theirstack wiza
12
12
  *
13
13
  * For those, `oxygen integrations connect <id> --api-key` does not reach
@@ -17,11 +17,16 @@
17
17
  * tool is native generic-REST and `shouldRouteStaticToolThroughComposio` is false, so
18
18
  * `resolveOptionalApiKeyCredentials` finds nothing and the call falls back to the
19
19
  * MANAGED key — the very key that was out of credits. The command appears to work,
20
- * changes nothing, and the customer is back where they started with no error.
20
+ * changes nothing, and the customer is back where they started with no error. The
21
+ * connect route now refuses each of those nine with a typed `byok_connect_unavailable`
22
+ * rather than a silent Composio "success"; they stay OFF this list until one gains a
23
+ * real native connect form.
21
24
  *
22
- * That is precisely the failure Plain T-104 would have hit: `parallel.search` was one
23
- * of the refused tools, and a refusal message that told Mentcape to connect their own
24
- * Parallel key would have sent them down a path that silently does nothing.
25
+ * That is precisely the failure Plain T-104 hit: `parallel.search` was one of the
26
+ * refused tools, and a refusal message that told Mentcape to connect their own Parallel
27
+ * key would once have sent them down a path that silently does nothing. Parallel now has
28
+ * a native api-key connect (catalog.ts) and IS on this list, so that message is finally
29
+ * true for it — the nine above are the ones it must still never name.
25
30
  *
26
31
  * So a message may only offer BYOK when BOTH halves hold, and this list is the second
27
32
  * half. It lives in @oxygen/shared because the two sides that need it cannot see each
@@ -64,6 +69,9 @@ export const NATIVE_API_KEY_CONNECT_INTEGRATION_IDS = new Set([
64
69
  // rest — so the native runner does find the key.
65
70
  "moltsets",
66
71
  "ocean",
72
+ // Native api-key connect added in catalog.ts (2026-09-10): connect writes an
73
+ // integration_secrets api_key row the native parallel.* runner reads. T-104.
74
+ "parallel",
67
75
  "predictleads",
68
76
  "prospeo",
69
77
  "scalelist",
@@ -23,6 +23,12 @@ export type CapabilityRouteMatch = {
23
23
  card: CapabilityRouteCard;
24
24
  recommendedTools: string[];
25
25
  recommendedCommands: string[];
26
+ /**
27
+ * The one exact invocation for asks whose answer is a single command with a
28
+ * specific flag the help text alone did not teach. Present only when the
29
+ * intent is that narrow; `recommendedCommands[0]` is the same command.
30
+ */
31
+ exactCommand?: string;
26
32
  };
27
33
  export type PrimitiveRouteMatch = CapabilityRouteMatch & {
28
34
  card: PrimitiveRouteCard;
@@ -34,3 +40,5 @@ export declare function getCapabilityRouteMatch(id: string): CapabilityRouteMatc
34
40
  export declare function inferCapabilityRoute(query: string): CapabilityRouteMatch | null;
35
41
  export declare function inferPrimitiveRoute(query: string): PrimitiveRouteMatch | null;
36
42
  export declare function serializeCapabilityRoute(route: CapabilityRouteMatch | null): Record<string, unknown> | null;
43
+ /** The exact `companies search plan` invocation for the url_recovery intent. */
44
+ export declare const COMPANY_URL_RECOVERY_COMMAND = "oxygen companies search plan --prompt \"Find the websites for these companies: <company names>\" --source-intent url_recovery";
@@ -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,18 @@ 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.",
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
214
  posture: "mixed",
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
- skills: ["oxygen-gtm", "oxygen-table-tidy", "oxygen-diagnostics", "oxygen-clay-migration"],
218
- endpointSections: ["action-columns", "callables", "functions", "columns", "company-enrichment", "enrich-column", "enrichment", "projects", "table-action-items", "table-action-runs", "table-ingestion-runs", "tables"],
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", "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",
@@ -335,10 +335,13 @@ export const OXYGEN_CAPABILITY_ROUTES = [
335
335
  execution: "Create and schedule on OXYGEN; explicit approval releases the hosted publisher.",
336
336
  posture: "external_write",
337
337
  gatewayTools: ["oxygen_publishing_posts_create", "oxygen_publishing_posts_list", "oxygen_publishing_posts_approve", "oxygen_ugc_get"],
338
- gatewayCommands: ["publishing posts create", "publishing posts list", "publishing posts approve", "ugc programs list"],
338
+ gatewayCommands: ["publishing posts create", "publishing posts list", "publishing posts approve", "ugc programs list", "ugc programs duplicate", "ugc programs archive"],
339
339
  skills: ["oxygen-linkedin-marketing", "oxygen-ugc"],
340
340
  endpointSections: ["publishing", "ugc"],
341
- intentTerms: ["publish", "publishing", "schedule post", "content calendar", "posting calendar", "approve post", "social publishing", "ugc", "creator program", "user generated content", "creator voice", "sponsored creator"],
341
+ // "remove program" / "delete program" landed on route: null in a blind eval
342
+ // (2026-09-10); a program is a UGC noun here, and its lifecycle verbs must
343
+ // resolve to the group that owns `ugc programs archive|delete|duplicate`.
344
+ intentTerms: ["publish", "publishing", "schedule post", "content calendar", "posting calendar", "approve post", "social publishing", "ugc", "creator program", "user generated content", "creator voice", "sponsored creator", "ugc program", "archive program", "delete program", "remove program", "duplicate program", "copy program"],
342
345
  },
343
346
  {
344
347
  id: "workflows",
@@ -523,6 +526,7 @@ export function inferCapabilityRoute(query) {
523
526
  card,
524
527
  recommendedTools: recommendations.tools,
525
528
  recommendedCommands: recommendations.commands,
529
+ ...(recommendations.exactCommand ? { exactCommand: recommendations.exactCommand } : {}),
526
530
  };
527
531
  }
528
532
  export function inferPrimitiveRoute(query) {
@@ -546,6 +550,7 @@ export function serializeCapabilityRoute(route) {
546
550
  endpoint_sections: route.card.endpointSections,
547
551
  recommended_commands: route.recommendedCommands,
548
552
  recommended_tools: route.recommendedTools,
553
+ ...(route.exactCommand ? { exact_command: route.exactCommand } : {}),
549
554
  hydrate: {
550
555
  cli: "oxygen commands get <exact-command> --json",
551
556
  mcp: "oxygen_capabilities_schema",
@@ -555,9 +560,21 @@ export function serializeCapabilityRoute(route) {
555
560
  function normalizeIntent(query) {
556
561
  return query.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
557
562
  }
563
+ function isLinkedInProfileWatcherIntent(query) {
564
+ return /\blinkedin\b/.test(query)
565
+ && /\bprofiles?\b/.test(query)
566
+ && /\b(watcher|watch|watching|monitor|monitoring|daily|every day|recurring)\b/.test(query)
567
+ && (/\b(engagers?|engaging|engagement|reactions?|comments?|posts?)\b/.test(query) || /\bprofile watcher\b/.test(query))
568
+ && !isNetNewLinkedInInitiation(query);
569
+ }
558
570
  function explicitCapabilityIntent(query) {
559
571
  if (/\b(infographic|graphic designer|render html|carousel pages|visual design|gtm flow image)\b/.test(query))
560
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;
561
578
  // A unified sender profile is an owned Sequence identity, even when the ask
562
579
  // names every attached channel (LinkedIn + WhatsApp + email). Resolve this
563
580
  // before public LinkedIn research, whose generic "profile" wording would
@@ -574,6 +591,8 @@ function explicitCapabilityIntent(query) {
574
591
  if (isMailboxOnboardingIntent(query)) {
575
592
  return ROUTE_BY_ID.get("sending-infrastructure") ?? null;
576
593
  }
594
+ if (isLinkedInProfileWatcherIntent(query))
595
+ return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
577
596
  if (isHostedWorkflowIntent(query))
578
597
  return ROUTE_BY_PRIMITIVE.get("workflows") ?? null;
579
598
  if (isOwnedPostCommentIntent(query))
@@ -612,6 +631,17 @@ function explicitCapabilityIntent(query) {
612
631
  if (/\b(my|our|own)\b.{0,40}\blinkedin\b.{0,40}\b(post )?engagers?\b/.test(query)) {
613
632
  return ROUTE_BY_PRIMITIVE.get("signals") ?? null;
614
633
  }
634
+ // Grading addresses a row ALREADY holds — "verify these emails", "which are
635
+ // safe to send", "catch-all or valid" — is Tables work (the Email verification
636
+ // column: `columns add --capability verify_email`, previewed and run through
637
+ // `enrich-column`), with `verify email` as the one-shot twin. Without this
638
+ // rule the bare word "email" scored the Messages card, and a 2026-09-10 blind
639
+ // user asking "verify email deliverability safe to send" was routed to
640
+ // `email send`. Sits after the mailbox rules above so warm-up and sender
641
+ // deliverability keep their owner, and after the scheduling rule so "every
642
+ // Monday verify new signups" still lands on Workflows.
643
+ if (isEmailVerificationIntent(query))
644
+ return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
615
645
  if (/\b(recipe|playbook|proven play|what should i do)\b/.test(query)) {
616
646
  return ROUTE_BY_PRIMITIVE.get("recipes") ?? null;
617
647
  }
@@ -643,9 +673,23 @@ function explicitCapabilityIntent(query) {
643
673
  || /\b(day\s*\d+|days?\s+\d|multi[ -]step|rotate senders?|sender rotation|stop on reply|until (?:they )?reply)\b/.test(query)) {
644
674
  return ROUTE_BY_PRIMITIVE.get("sequences") ?? null;
645
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
+ }
646
682
  if (/\b(reply|respond)\b.{0,30}\b(conversation|thread|email|linkedin|whatsapp|dm)\b/.test(query)) {
647
683
  return ROUTE_BY_PRIMITIVE.get("messages") ?? null;
648
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;
649
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)) {
650
694
  return ROUTE_BY_ID.get("sourcing-and-provider-tools") ?? null;
651
695
  }
@@ -654,16 +698,43 @@ function explicitCapabilityIntent(query) {
654
698
  }
655
699
  return null;
656
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
+ }
657
728
  function highestScoringRoute(query) {
658
729
  let best = null;
659
730
  for (const card of OXYGEN_CAPABILITY_ROUTES) {
660
731
  let score = 0;
661
732
  for (const term of card.intentTerms) {
662
- if (query.includes(term))
733
+ if (matchesIntentTerm(query, term))
663
734
  score += term.includes(" ") ? 6 : 2;
664
735
  }
665
736
  for (const term of card.negativeTerms ?? []) {
666
- if (query.includes(term))
737
+ if (matchesIntentTerm(query, term))
667
738
  score -= term.includes(" ") ? 8 : 3;
668
739
  }
669
740
  if (score > (best?.score ?? 0))
@@ -671,7 +742,16 @@ function highestScoringRoute(query) {
671
742
  }
672
743
  return best?.card ?? null;
673
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';
674
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
+ }
675
755
  if (card.id === "connected-linkedin" && isMutualLinkedInConnectionsIntent(query)) {
676
756
  return {
677
757
  tools: ["oxygen_tools_get", "oxygen_tools_run_live", "oxygen_senders_list"],
@@ -752,12 +832,27 @@ function recommendationsFor(card, query) {
752
832
  };
753
833
  }
754
834
  if (card.primitive === "tables") {
835
+ if (isLinkedInProfileWatcherIntent(query)) {
836
+ return {
837
+ tools: ["oxygen_tables_watcher"],
838
+ commands: ["tables watcher preview", "tables watcher create", "tables watcher get", "tables watcher update", "tables watcher pause", "tables watcher resume"],
839
+ };
840
+ }
755
841
  if (/\b(table )?(action )?runs?\b/.test(query)) {
756
842
  return {
757
843
  tools: ["oxygen_table_runs_get", "oxygen_table_runs_items", "oxygen_table_runs_wait", "oxygen_table_runs_retry_failed"],
758
844
  commands: ["table-runs get", "table-runs items", "table-runs wait", "table-runs retry-failed"],
759
845
  };
760
846
  }
847
+ if (isEmailVerificationIntent(query)) {
848
+ // Define-without-running first (0 credits), then the free preview that
849
+ // prices it, then the approved run, then the one-shot for a handful of
850
+ // addresses that never needed a table.
851
+ return {
852
+ tools: ["oxygen_columns_add", "oxygen_enrich_column_preview", "oxygen_enrich_column_run", "oxygen_verify_email"],
853
+ commands: ["columns add", "enrich-column preview", "enrich-column run", "verify email"],
854
+ };
855
+ }
761
856
  if (/\b(waterfall|enrich|enrichment|work email|mobile phone)\b/.test(query)) {
762
857
  return {
763
858
  tools: ["oxygen_enrich_column_preview", "oxygen_columns_add", "oxygen_enrich_column_run", "oxygen_table_runs_get"],
@@ -889,7 +984,7 @@ function recommendationsFor(card, query) {
889
984
  if (card.primitive === "signals" && /\blinkedin\b.{0,40}\bengagers?\b/.test(query)) {
890
985
  return {
891
986
  tools: ["oxygen_engagement_list_engagers", "oxygen_signals_list", "oxygen_signals_leads_today"],
892
- commands: ["engagement list", "signals list", "signals leads-today"],
987
+ commands: ["engagement engagers", "signals list", "signals leads-today"],
893
988
  };
894
989
  }
895
990
  if (card.id === "connected-whatsapp" && /\b(account|connect|limits?|sync)\b/.test(query)) {
@@ -900,6 +995,35 @@ function recommendationsFor(card, query) {
900
995
  }
901
996
  return { tools: [...card.gatewayTools], commands: [...card.gatewayCommands] };
902
997
  }
998
+ // An email-VERIFICATION ask names a grading verb next to the addresses, or the
999
+ // addresses next to a grade. "check my emails" is deliberately not matched
1000
+ // (that is an inbox), and bare "deliverability" is left to the sending rail —
1001
+ // only "are these emails deliverable" counts. Bare "bounce" describes an
1002
+ // observed delivery failure too; require predictive wording or "bounce risk".
1003
+ function isEmailVerificationIntent(query) {
1004
+ const gradeThenAddress = /\b(verif(?:y|ied|ication)|validat(?:e|ed|ion)|grade|scrub|clean)\b.{0,40}\b(e ?mails?|email addresses|addresses)\b/;
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/;
1006
+ return gradeThenAddress.test(query) || addressThenGrade.test(query);
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
+ }
903
1027
  function isInboxAvatarIntent(query) {
904
1028
  return /\b(avatar|profile (?:picture|photo)|headshot|hosted (?:picture|image)|mailbox (?:picture|photo))\b/.test(query);
905
1029
  }
@@ -926,6 +1050,14 @@ function isMailboxDeleteIntent(query) {
926
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);
927
1051
  return mailboxScope && removal && !scopedDetach;
928
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
+ }
929
1061
  function isNetNewLinkedInInitiation(query) {
930
1062
  const mentionsLinkedIn = /\blinkedin\b|\bdm\b/.test(query);
931
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
  }