@oxygen-agent/cli 1.1011.97 → 1.1011.288

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 (58) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.js +25 -5
  3. package/dist/column-run-notices.d.ts +16 -0
  4. package/dist/column-run-notices.js +57 -0
  5. package/dist/command-manifest.js +10 -1
  6. package/dist/help.d.ts +27 -1
  7. package/dist/help.js +112 -29
  8. package/dist/http-client.js +8 -0
  9. package/dist/index.js +348 -127
  10. package/node_modules/@oxygen/cli-ugc/dist/commands.js +20 -69
  11. package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +6 -4
  12. package/node_modules/@oxygen/shared/dist/byok-connect.js +11 -4
  13. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +127 -1
  14. package/node_modules/@oxygen/shared/dist/capability-discovery.js +286 -24
  15. package/node_modules/@oxygen/shared/dist/collab.d.ts +1 -1
  16. package/node_modules/@oxygen/shared/dist/collab.js +12 -0
  17. package/node_modules/@oxygen/shared/dist/column-decision.js +7 -3
  18. package/node_modules/@oxygen/shared/dist/column-output-fields.js +45 -3
  19. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +20 -13
  20. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +29 -13
  21. package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +1 -1
  22. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
  23. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
  24. package/node_modules/@oxygen/shared/dist/credit-guidance.js +12 -1
  25. package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +37 -0
  26. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +79 -44
  27. package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +7 -0
  28. package/node_modules/@oxygen/shared/dist/linkedin-url.js +9 -0
  29. package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
  30. package/node_modules/@oxygen/shared/dist/log.js +15 -0
  31. package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +1 -0
  32. package/node_modules/@oxygen/shared/dist/operational-telemetry.js +6 -2
  33. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +7 -3
  34. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +22 -1
  35. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +3 -7
  36. package/node_modules/@oxygen/shared/dist/plan-limits.js +2 -14
  37. package/node_modules/@oxygen/shared/dist/sending-schedule.d.ts +39 -0
  38. package/node_modules/@oxygen/shared/dist/sending-schedule.js +41 -0
  39. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.d.ts +19 -0
  40. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.js +20 -0
  41. package/node_modules/@oxygen/shared/dist/sequence-sender-variables.d.ts +35 -0
  42. package/node_modules/@oxygen/shared/dist/sequence-sender-variables.js +63 -0
  43. package/node_modules/@oxygen/shared/dist/sequences.d.ts +13 -7
  44. package/node_modules/@oxygen/shared/dist/sequences.js +26 -0
  45. package/node_modules/@oxygen/shared/dist/signup-lead-payload.d.ts +2 -0
  46. package/node_modules/@oxygen/shared/dist/signup-lead-payload.js +5 -0
  47. package/node_modules/@oxygen/shared/dist/social-post-metrics-series.js +1 -15
  48. package/node_modules/@oxygen/shared/dist/standard-webhooks.d.ts +51 -0
  49. package/node_modules/@oxygen/shared/dist/standard-webhooks.js +124 -0
  50. package/node_modules/@oxygen/shared/dist/tags.d.ts +1 -1
  51. package/node_modules/@oxygen/shared/dist/tags.js +13 -1
  52. package/node_modules/@oxygen/shared/dist/ugc.d.ts +11 -7
  53. package/node_modules/@oxygen/shared/dist/ugc.js +14 -6
  54. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  55. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  56. package/node_modules/@oxygen/shared/dist/zoned-time.d.ts +39 -0
  57. package/node_modules/@oxygen/shared/dist/zoned-time.js +81 -0
  58. package/package.json +1 -1
@@ -214,7 +214,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
214
214
  primitive: "tables",
215
215
  owns: "Typed working datasets, rows, formulas, AI/tool/waterfall columns, reusable Functions with isolated drafts and published versions, cell state, projects, and run provenance.",
216
216
  notFor: "Canonical CRM truth, message cadence, or an off-platform spreadsheet runtime.",
217
- execution: "Create and run work in hosted OXYGEN Tables; validate a small sample before bounded paid runs. To see what a table's own columns can be enriched with and what each costs per row before adding anything, run `tools search --for-table <table>` (MCP oxygen_tools_search with for_table): free, it returns priced suggestions with the exact add command. For standard person or company enrichment, `columns add <table> --preset person_enrich|company_enrich` (MCP oxygen_columns_add with preset) adds the maintained bundle in one call before any hand-built tool column. Person questions over that bundle's profile payload (skill set, grad school, location, job fit, current company) and person appearance research (events, keynotes, podcasts, GitHub profile) are `columns catalog --category people` templates, added with `columns add <table> --prompt-key <key> --input <name>=<column>`. For a company fact OXYGEN already knows how to research (founders, parent company, funding, cloud provider, offers demos, industry, NAICS, HQ, LinkedIn/Crunchbase page lookups) or a page extraction, `columns catalog` lists the ready-made template and `columns add <table> --prompt-key <key> --input <name>=<column>` (MCP oxygen_columns_add with prompt_key) adds it with no prompt to write.",
217
+ execution: "Create and run work in hosted OXYGEN Tables; validate a small sample before bounded paid runs. To see what a table's own columns can be enriched with and what each costs per row before adding anything, run `tools search --for-table <table>` (MCP oxygen_tools_search with for_table): free, it returns priced suggestions with the exact add command. For standard person or company enrichment, `columns add <table> --preset person_enrich|company_enrich` (MCP oxygen_columns_add with preset) adds the maintained bundle in one call before any hand-built tool column. Person questions over that bundle's profile payload (skill set, grad school, location, job fit, current company) and person appearance research (events, keynotes, podcasts, GitHub profile) are `columns catalog --category people` templates, added with `columns add <table> --prompt-key <key> --input <name>=<column>`. For a company fact OXYGEN already knows how to research (founders, parent company, funding, cloud provider, offers demos, industry, NAICS, HQ, LinkedIn/Crunchbase page lookups) or a page extraction, `columns catalog` lists the ready-made template and `columns add <table> --prompt-key <key> --input <name>=<column>` (MCP oxygen_columns_add with prompt_key) adds it with no prompt to write. For ICP tiers, read the saved profile with `knowledge page get icp`, then add native Classify Score: `columns add <table> --label 'ICP fit' --score 'low,medium,high' --prompt '<ICP criteria using {{evidence_column}} tokens>' --grounding workspace`. It costs 0.075 credits/row; preview with `columns run --dry-run`. `columns catalog --family classification --json` puts Classify in `data.native`; `data.entries` holds separate prompt templates that can cost more.",
218
218
  posture: "mixed",
219
219
  gatewayTools: ["oxygen_tables_create", "oxygen_tools_search", "oxygen_columns_add", "oxygen_columns_catalog", "oxygen_enrich_column_preview", "oxygen_tables_link_bulk", "oxygen_callables_manage"],
220
220
  gatewayCommands: ["tables create", "tools search", "columns add", "columns catalog", "enrich-column preview", "tables link", "functions list", "functions draft"],
@@ -273,13 +273,13 @@ export const OXYGEN_CAPABILITY_ROUTES = [
273
273
  primitive: "signals",
274
274
  owns: "Durable typed pulse, engagement, and product events resolved onto records.",
275
275
  notFor: "Message bodies, post artifacts, or ad-hoc provider webhooks as private automation state.",
276
- execution: "Read the hosted signal stream and feed subscriptions, Sequences, or Workflows.",
276
+ execution: "Read the hosted signal stream and type registry; tag a registered type with tags attach. Feed subscriptions, Sequences, or Workflows.",
277
277
  posture: "workspace_write",
278
- gatewayTools: ["oxygen_signals_list", "oxygen_signals_leads_today"],
279
- gatewayCommands: ["signals list", "signals leads-today", "signals registry"],
278
+ gatewayTools: ["oxygen_signals_list", "oxygen_signals_leads_today", "oxygen_signals_registry", "oxygen_tags_attach"],
279
+ gatewayCommands: ["signals list", "signals leads-today", "signals registry", "tags attach"],
280
280
  skills: ["oxygen-gtm", "oxygen-linkedin-marketing"],
281
281
  endpointSections: ["signals", "signup-leads"],
282
- intentTerms: ["signal", "signals", "intent", "website visit", "job change", "fundraising", "warm leads", "engager", "follower", "viewer"],
282
+ intentTerms: ["signal", "signals", "signal type", "tag signal", "intent", "website visit", "job change", "fundraising", "warm leads", "engager", "follower", "viewer"],
283
283
  },
284
284
  {
285
285
  id: "dashboards",
@@ -310,15 +310,15 @@ export const OXYGEN_CAPABILITY_ROUTES = [
310
310
  id: "ads",
311
311
  layer: "Data",
312
312
  primitive: "ads",
313
- owns: "Competitor ad intelligence today; owned-audience activation is a roadmap boundary.",
313
+ owns: "Competitor ad intelligence and saved search definitions; owned-audience activation is a roadmap boundary.",
314
314
  notFor: "Organic Posts, Publishing, or claiming unshipped audience activation.",
315
- execution: "Discover and run the hosted ad-intelligence provider operation with the normal paid-read gate.",
315
+ execution: "Save a reusable Ads search definition for free; running the hosted ad-intelligence provider operation uses the normal paid-read gate. Guide: /docs/data/ads.",
316
316
  posture: "paid_read",
317
- gatewayTools: ["oxygen_adyntel_ads_search", "oxygen_tools_search"],
318
- gatewayCommands: ["tools search"],
317
+ gatewayTools: ["oxygen_ads_searches", "oxygen_adyntel_ads_search", "oxygen_tools_search"],
318
+ gatewayCommands: ["ads searches list", "ads searches save", "tools search"],
319
319
  skills: ["oxygen-gtm"],
320
- endpointSections: [],
321
- intentTerms: ["ad", "ads", "advertising", "ad creative", "competitor ads", "ad intelligence", "meta ads", "linkedin ads"],
320
+ endpointSections: ["ads"],
321
+ intentTerms: ["ad", "ads", "advertising", "ad creative", "competitor ads", "ad intelligence", "meta ads", "linkedin ads", "saved ads search", "reusable ads search"],
322
322
  },
323
323
  {
324
324
  id: "tags",
@@ -326,10 +326,10 @@ export const OXYGEN_CAPABILITY_ROUTES = [
326
326
  primitive: "tags",
327
327
  owns: "The normalized cross-primitive label vocabulary and campaign handle.",
328
328
  notFor: "A campaign entity, private per-surface labels, or arbitrary execution state.",
329
- execution: "Read or mutate tags through the hosted union surface; rename is preview-first.",
329
+ execution: "Read or mutate tags through the hosted union surface; rename is preview-first. For saved Ads searches, save a definition with oxygen_ads_searches (CLI: ads searches save), then attach or read its tags here. Guide: /docs/workspace/tags.",
330
330
  posture: "workspace_write",
331
- gatewayTools: ["oxygen_tags_list", "oxygen_tags_get", "oxygen_tags_rename"],
332
- gatewayCommands: ["tags list", "tags get", "tags rename"],
331
+ gatewayTools: ["oxygen_tags_list", "oxygen_tags_get", "oxygen_tags_attach", "oxygen_tags_rename"],
332
+ gatewayCommands: ["tags list", "tags get", "tags attach", "tags rename"],
333
333
  skills: ["oxygen-gtm"],
334
334
  endpointSections: ["tags"],
335
335
  intentTerms: ["tag", "tags", "label", "labels", "campaign handle", "grouping", "organize campaign"],
@@ -345,7 +345,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
345
345
  // email track's "Only 'instantly' is supported" and concluded Smartlead was
346
346
  // unreachable. The governed path for a campaign the customer already runs
347
347
  // elsewhere is `sequences push-external`; it is not a Sequence.
348
- execution: "Use the one-step gateway for new LinkedIn initiation or create/enroll/start a hosted campaign with bounded send authority. Native email sends through connected Google/Microsoft mailboxes, including Microsoft SMTP transport. To push table rows into an existing Smartlead, Instantly, lemlist or HeyReach campaign you already run, use `sequences push-external` (dry_run preview, then --approved). To stop a company's other leads when one of them replies, set `--company-reply-pause` on the campaign (choose the reply categories); to pause or resume a company on demand, e.g. from a workflow when a meeting is booked, use `sequences pause-company` / `resume-company`. A pause is resumable; a company do-not-contact entry is a permanent block on every campaign.",
348
+ execution: "Use the one-step gateway for new LinkedIn initiation or create/enroll/start a hosted campaign with bounded send authority. Native email sends through connected Google/Microsoft mailboxes, including Microsoft SMTP transport. To push table rows into an existing Smartlead, Instantly, lemlist or HeyReach campaign you already run, use `sequences push-external` (dry_run preview, then --approved). To stop a company's other leads when one of them replies, set `--company-reply-pause` on the campaign (choose the reply categories); to pause or resume a company on demand, e.g. from a workflow when a meeting is booked, use `sequences pause-company` / `resume-company`. A pause is resumable; a company do-not-contact entry is a permanent block on every campaign. Step copy takes lead columns as {{column}} and the sending person's name as {{sender_first_name}}, {{sender_last_name}} or {{sender_full_name}} (email adds {{sender_email}}), with no table column.",
349
349
  posture: "external_write",
350
350
  gatewayTools: ["oxygen_sequences_send", "oxygen_sequences_create", "oxygen_sequences_enroll", "oxygen_sequences_start", "oxygen_sequences_list", "oxygen_sequences_push_external", "oxygen_sequences_company_pause"],
351
351
  gatewayCommands: ["sequences send", "sequences create", "sequences enroll", "sequences start", "sequences list", "sequences push-external", "sequences pause-company"],
@@ -356,7 +356,10 @@ export const OXYGEN_CAPABILITY_ROUTES = [
356
356
  // to workspace-access — "identity" matched the AUTH sense. The nouns a user
357
357
  // reaches for when naming a sending identity or the dialing pool have to land
358
358
  // on the group that actually owns `senders profiles` and `voice numbers`.
359
- intentTerms: ["sequence", "campaign", "cadence", "enroll", "outreach", "linkedin message", "linkedin dm", "nurture", "follow up", "sender rotation", "stop on reply", "company replied", "pause company", "pause outreach to a company", "stop outreach to a company", "stop emailing a company", "colleagues", "do not contact a company", "sender identity", "sending identity", "sender profile", "phone number", "dialing", "dialer", "call", "cold call", "smartlead", "instantly", "lemlist", "heyreach", "external sequencer", "push to campaign", "push rows to campaign", "add leads to campaign"],
359
+ // The sign-off terms are here because a 2026-09-29 blind user searched
360
+ // "preview rendered message per sender sign-off variable" and landed on
361
+ // collaboration; the {{sender_*}} copy variables live on sequence steps.
362
+ intentTerms: ["sequence", "campaign", "cadence", "enroll", "outreach", "linkedin message", "linkedin dm", "nurture", "follow up", "sender rotation", "stop on reply", "company replied", "pause company", "pause outreach to a company", "stop outreach to a company", "stop emailing a company", "colleagues", "do not contact a company", "sender identity", "sending identity", "sender profile", "phone number", "dialing", "dialer", "call", "cold call", "smartlead", "instantly", "lemlist", "heyreach", "external sequencer", "push to campaign", "push rows to campaign", "add leads to campaign", "sign-off", "sign off", "signature", "sender name", "sender first name", "sender variable", "sender_first_name"],
360
363
  },
361
364
  {
362
365
  id: "publishing",
@@ -565,12 +568,13 @@ export function inferCapabilityRoute(query) {
565
568
  // beside them in `catalogIntent`, never inside them.
566
569
  const catalogue = catalogRow && CATALOGUE_CARD_IDS.has(card.id) ? catalogueRecommendations(catalogRow, card) : null;
567
570
  return {
568
- card,
571
+ card: routeCardFor(card, normalized),
569
572
  recommendedTools: recommendations.tools,
570
573
  recommendedCommands: recommendations.commands,
571
574
  ...(recommendations.exactCommand ? { exactCommand: recommendations.exactCommand } : {}),
575
+ ...(recommendations.exactMcpCall ? { exactMcpCall: recommendations.exactMcpCall } : {}),
572
576
  ...(catalogue && catalogRow
573
- ? { catalogIntent: { id: catalogRow.id, label: catalogRow.label, status: catalogRow.status, group: catalogRow.group, tools: catalogue.tools, commands: catalogue.commands, exactCommand: catalogue.exactCommand } }
577
+ ? { catalogIntent: { id: catalogRow.id, label: catalogRow.label, status: catalogRow.status, group: catalogRow.group, tools: catalogue.tools, commands: catalogue.commands, exactCommand: catalogue.exactCommand, exactMcpCall: catalogue.exactMcpCall } }
574
578
  : {}),
575
579
  };
576
580
  }
@@ -587,13 +591,25 @@ function cardForCatalogueIntent(row) {
587
591
  return ROUTE_BY_ID.get("signals") ?? ROUTE_BY_PRIMITIVE.get("signals") ?? null;
588
592
  return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
589
593
  }
594
+ /**
595
+ * The MCP tool a catalogue row's `find` command maps to, named by its command
596
+ * words alone: "find company --domain <domain> --fields funding,..." once became
597
+ * the tool "oxygen_find_company_domain_<domain>_fields_...", which does not
598
+ * exist (funding, hiring-signal and revenue rows, 2026-09-29).
599
+ */
600
+ export function catalogueFindTool(row) {
601
+ const command = row.surfaces.find?.split(/\s+--/)[0]?.trim();
602
+ return command ? `oxygen_${command.replace(/[\s-]+/g, "_")}` : null;
603
+ }
590
604
  function catalogueRecommendations(row, card) {
591
605
  const exactCommand = `oxygen enrichment catalog --intent ${row.id}`;
606
+ const exactMcpCall = catalogueIntentMcpCall(row.id);
592
607
  if (row.group === "decision_maker" || card.id === "sourcing-and-provider-tools") {
593
608
  return {
594
609
  tools: ["oxygen_enrichment_catalog", "oxygen_people_search_plan", "oxygen_people_search_run"],
595
610
  commands: ["enrichment catalog", "people search plan", "people search run"],
596
611
  exactCommand,
612
+ exactMcpCall,
597
613
  };
598
614
  }
599
615
  if (row.group === "signal") {
@@ -601,13 +617,16 @@ function catalogueRecommendations(row, card) {
601
617
  tools: ["oxygen_enrichment_catalog", "oxygen_signals_search_plan", "oxygen_signals_search_run"],
602
618
  commands: ["enrichment catalog", "signals search plan", "signals search run"],
603
619
  exactCommand,
620
+ exactMcpCall,
604
621
  };
605
622
  }
606
- const find = row.surfaces.find ? [`oxygen_${row.surfaces.find.replace(/[\s-]+/g, "_")}`] : [];
623
+ const findTool = catalogueFindTool(row);
624
+ const find = findTool ? [findTool] : [];
607
625
  return {
608
626
  tools: ["oxygen_enrichment_catalog", "oxygen_columns_add", "oxygen_enrich_column_preview", ...find],
609
627
  commands: ["enrichment catalog", "columns add", "enrich-column preview", ...(row.surfaces.find ? [row.surfaces.find] : [])],
610
628
  exactCommand,
629
+ exactMcpCall,
611
630
  };
612
631
  }
613
632
  export function inferPrimitiveRoute(query) {
@@ -616,7 +635,7 @@ export function inferPrimitiveRoute(query) {
616
635
  return null;
617
636
  return route;
618
637
  }
619
- export function serializeCapabilityRoute(route) {
638
+ export function serializeCapabilityRoute(route, surface = "cli") {
620
639
  if (!route)
621
640
  return null;
622
641
  return {
@@ -631,9 +650,23 @@ export function serializeCapabilityRoute(route) {
631
650
  endpoint_sections: route.card.endpointSections,
632
651
  recommended_commands: route.recommendedCommands,
633
652
  recommended_tools: route.recommendedTools,
634
- ...(route.exactCommand ? { exact_command: route.exactCommand } : {}),
653
+ ...(surface === "mcp" && route.exactMcpCall
654
+ ? { exact_mcp_call: route.exactMcpCall }
655
+ : route.exactCommand ? { exact_command: route.exactCommand } : {}),
635
656
  ...(route.catalogIntent
636
- ? { catalog_intent: { id: route.catalogIntent.id, label: route.catalogIntent.label, status: route.catalogIntent.status, group: route.catalogIntent.group, tools: route.catalogIntent.tools, commands: route.catalogIntent.commands, exact_command: route.catalogIntent.exactCommand } }
657
+ ? {
658
+ catalog_intent: {
659
+ id: route.catalogIntent.id,
660
+ label: route.catalogIntent.label,
661
+ status: route.catalogIntent.status,
662
+ group: route.catalogIntent.group,
663
+ tools: route.catalogIntent.tools,
664
+ commands: route.catalogIntent.commands,
665
+ ...(surface === "mcp"
666
+ ? { exact_mcp_call: route.catalogIntent.exactMcpCall }
667
+ : { exact_command: route.catalogIntent.exactCommand }),
668
+ },
669
+ }
637
670
  : {}),
638
671
  hydrate: {
639
672
  cli: "oxygen commands get <exact-command> --json",
@@ -669,6 +702,70 @@ function isUgcProgramIntent(query) {
669
702
  function isUgcCreatorSideIntent(query) {
670
703
  return /\b(join|apply|applying|find|discover|browse|accept)\w*\b.{0,30}\b(creator|ugc|ambassador) programs?\b/.test(query);
671
704
  }
705
+ /** The lifecycle verb of an operator's program ask ("archive program", "delete my creator program"), or null. */
706
+ function ugcProgramLifecycleVerb(query) {
707
+ return /\b(archive|restore|delete|remove|duplicate|copy|clone) (?:(?:a|an|the|this|that|my|our|old|archived|creator|ugc|brand|ambassador) ){0,3}programs?\b/.exec(query)?.[1] ?? null;
708
+ }
709
+ /** The `ugc programs` leaf and MCP tool per lifecycle verb; delete previews first and names archive when refused. */
710
+ const UGC_PROGRAM_LIFECYCLE = {
711
+ archive: ["ugc programs archive", "oxygen_ugc_program_update"],
712
+ restore: ["ugc programs restore", "oxygen_ugc_program_update"],
713
+ delete: ["ugc programs delete", "oxygen_ugc_program_delete"],
714
+ remove: ["ugc programs delete", "oxygen_ugc_program_delete"],
715
+ duplicate: ["ugc programs duplicate", "oxygen_ugc_program_duplicate"],
716
+ copy: ["ugc programs duplicate", "oxygen_ugc_program_duplicate"],
717
+ clone: ["ugc programs duplicate", "oxygen_ugc_program_duplicate"],
718
+ };
719
+ /**
720
+ * A creator's own writing voice: `ugc voice import` learns it from their LinkedIn
721
+ * history (the web's "Learn your writing voice") and `ugc voice save` keeps what
722
+ * they write themselves. Only an ask that names the creator context routes here:
723
+ * "learn my writing voice from my LinkedIn messages" is just as likely the
724
+ * workspace voice `knowledge synthesize --kind voice` learns from sent LinkedIn and
725
+ * WhatsApp messages, and brand voice stays with the Knowledge Graph.
726
+ */
727
+ function isUgcVoiceIntent(query) {
728
+ return /\bcreators?'?s? voice\b/.test(query)
729
+ || (/\bvoice\b/.test(query) && /\b(creator|ugc|ambassador)s?\b/.test(query) && !/\bbrand voice\b/.test(query));
730
+ }
731
+ /**
732
+ * Who approves a creator's post. It is the creator, or the program organizer when
733
+ * the creator delegated approval; a brand can never require its own approval of
734
+ * creator posts (Philipp, 2026-09-29).
735
+ * The MCP acceptance run's "brand approval of creator post before publish" routed
736
+ * to the Knowledge Graph, so the rule it needed never reached it. Only an
737
+ * approval word next to a creator's post counts: "find LinkedIn creator posts
738
+ * about AI" stays a keyword post search.
739
+ */
740
+ function isUgcPostApprovalIntent(query) {
741
+ return /\b(?:approv\w*|review\w*|sign[- ]?off)\b.{0,40}\b(?:creator|ugc|ambassador)s?'?\s+posts?\b/.test(query)
742
+ || /\b(?:creator|ugc|ambassador)s?'?\s+posts?\b.{0,40}\b(?:approv\w*|review\w*|sign[- ]?off)\b/.test(query)
743
+ || /\bbrand (?:approval|review)\b.{0,40}\b(?:creators?|ugc|ambassadors?)\b/.test(query);
744
+ }
745
+ function isUgcIntent(query) {
746
+ return isUgcProgramIntent(query) || isUgcCreatorSideIntent(query) || isUgcVoiceIntent(query)
747
+ || isUgcPostApprovalIntent(query) || ugcProgramLifecycleVerb(query) !== null;
748
+ }
749
+ /**
750
+ * UGC is Publishing's creator-program subpage (OXYGEN_PRODUCT.md § UGC programs): a
751
+ * creator-program ask keeps the publishing id, primitive, endpoint sections and
752
+ * gateways, but must not read Publishing's post-scheduler ownership back (UGC E2E pass, 2026-09-29).
753
+ */
754
+ const UGC_ROUTE_VIEW = {
755
+ owns: "UGC creator programs: brand programs, creator invitations and applications, participations, creator voice, program post drafts and approvals, sponsorship, results, and bounded amplification.",
756
+ notFor: "Scheduling the workspace's own posts, one-to-one messaging, outreach campaigns, or keyword post search.",
757
+ execution: "Brands create a program from its product URL and invite creators; creators apply or accept in their own workspace. A post publishes only after the creator approves its exact content, or the program organizer does when the creator delegated approval; brands cannot require approval. Paid drafts, voice imports and amplification preview first and need an approved credit ceiling.",
758
+ posture: "mixed",
759
+ skills: ["oxygen-ugc", "oxygen-linkedin-marketing"],
760
+ };
761
+ /** The card a route reports: Publishing's own, or its UGC view for a creator-program ask. */
762
+ function routeCardFor(card, query) {
763
+ if (card.primitive === "publishing" && isUgcIntent(query))
764
+ return { ...card, ...UGC_ROUTE_VIEW };
765
+ if (card.id === "knowledge-graph" && isWorkspaceVoiceIntent(query))
766
+ return { ...card, execution: WORKSPACE_VOICE_EXECUTION };
767
+ return card;
768
+ }
672
769
  function isLinkedInKeywordPostIntent(query) {
673
770
  return /\blinkedin\b/.test(query)
674
771
  && /\b(posts?|posting)\b/.test(query)
@@ -700,6 +797,103 @@ function isOwnLinkedInNetworkIntent(query) {
700
797
  const bothHalves = /\bconnections?\b/.test(query) && /\bfollowers?\b/.test(query);
701
798
  return (ownNetwork || bothHalves) && !isNetNewLinkedInInitiation(query);
702
799
  }
800
+ /**
801
+ * The workspace's own voice. `knowledge synthesize --kind voice` learns a writing
802
+ * voice from the workspace's SENT LinkedIn and WhatsApp messages; brand voice is a
803
+ * wiki page. "learn my writing voice from my LinkedIn history" scored higher on
804
+ * the connected-LinkedIn card ("my linkedin") and was offered `linkedin network
805
+ * setup` (UGC E2E pass, 2026-09-29, OXP-1.15.12). A creator's voice is UGC and is
806
+ * claimed first (isUgcVoiceIntent); phone voice (numbers, calls) never matches.
807
+ * The blind baseline also searched "writing style" and "voice learn".
808
+ */
809
+ function isWorkspaceVoiceIntent(query) {
810
+ if (/\b(?:calls?|calling|dial\w*|phone|softphone|voicemail|numbers?)\b/.test(query))
811
+ return false;
812
+ return /\b(?:writing (?:voice|style)|brand voice|tone of voice|voice guide|(?:my|our) (?:own )?voice|(?:write|sound)s? like (?:me|us))\b/.test(query)
813
+ || /\b(?:learn|capture|mimic|copy|match)\w*\b.{0,30}\bvoice\b/.test(query)
814
+ || /\bvoice\b.{0,30}\b(?:learn|capture|mimic|copy|match)\w*\b/.test(query);
815
+ }
816
+ const WORKSPACE_VOICE_EXECUTION = "A writing voice is learned from the workspace's own sent LinkedIn and WhatsApp messages: `oxygen knowledge synthesize --kind voice --json` (MCP: oxygen_context_synthesize with kind voice) runs one low-cost AI call, priced as one low-reasoning AI column cell, the preview included, and reports credits_used; to pin the guide a preview showed, pass its body back with --approved --body (no new AI call), because --approved alone synthesizes and charges again. The pinned guide is the canonical voice at once and grounds message, reply, sequence and post drafts and AI columns. It reads sent messages only, not posts, emails or web pages. Brand voice and tone live in wiki pages (knowledge search). A creator's voice in a UGC program is `ugc voice import`.";
817
+ /** One schedule for every channel of a Sequence, the same as Launch's Sending schedule. */
818
+ export const SEQUENCE_SENDING_SCHEDULE_COMMAND = "oxygen sequences update <sequence> --schedule-timezone <IANA zone> --schedule-days mon-fri --schedule-hours-start 09:00 --schedule-hours-end 17:00 --json";
819
+ /** Launch's unsubscribe switch: the visible link and the one-click headers together. */
820
+ export const SEQUENCE_UNSUBSCRIBE_COMMAND = "oxygen sequences update <sequence> --include-unsubscribe --json";
821
+ /**
822
+ * Every exact CLI command a route hands out, as the MCP tool call that sends the
823
+ * same request. The MCP surface serves these in place of the command: an MCP
824
+ * agent handed a CLI command had to translate it (blind user eval, 2026-09-29).
825
+ * Each was traced to the same `/api/cli/*` request as its command; the
826
+ * mcp-server test proves every tool exists and declares every argument.
827
+ * Factories, so a caller can edit the object it gets.
828
+ */
829
+ export const CAPABILITY_ROUTE_MCP_CALLS = {
830
+ sequenceSendingSchedule: () => ({
831
+ tool: "oxygen_sequences_update",
832
+ arguments: { sequence: "<sequence>", sending_schedule: { timezone: "<IANA zone>", days: [1, 2, 3, 4, 5], start: "09:00", end: "17:00" } },
833
+ }),
834
+ sequenceUnsubscribe: () => ({ tool: "oxygen_sequences_update", arguments: { sequence: "<sequence>", include_unsubscribe: true } }),
835
+ ugcProgramCreate: () => ({ tool: "oxygen_ugc_program_create", arguments: { product_url: "<your product site>" } }),
836
+ // The first comment rides in `content`, as the CLI's --first-comment does.
837
+ publishingFirstComment: () => ({
838
+ tool: "oxygen_publishing_posts_create",
839
+ arguments: { content_text: "<post>", publish_at: "<iso>", sender_account_id: "<sender_id>", content: { first_comment: "<comment, e.g. the link>" } },
840
+ }),
841
+ companyUrlRecovery: () => ({
842
+ tool: "oxygen_companies_search_plan",
843
+ arguments: { prompt: "Find the websites for these companies: <company names>", source_intent: "url_recovery" },
844
+ }),
845
+ // `records` is a number the tool drops when it is not one; 1000 is its default.
846
+ companyPageFollowers: () => ({
847
+ tool: "oxygen_tables_followers",
848
+ arguments: { action: "preview", company_url: "<linkedin company page URL>", records: 1000 },
849
+ }),
850
+ localBusinessScrape: () => ({
851
+ tool: "oxygen_companies_search_preview",
852
+ arguments: { action: "preview", source: "google_maps", filters: { q: "<category> in <city, region>" } },
853
+ }),
854
+ // Like the CLI command, it leaves the LinkedIn risk acknowledgement to the user.
855
+ senderConnect: () => ({ tool: "oxygen_senders_connect", arguments: { country: "<cc>" } }),
856
+ rowClassification: () => ({ tool: "oxygen_columns_catalog", arguments: { family: "classification" } }),
857
+ // The tool has no `family`: it always searches linkedin_posts, which the API reads as the same source.
858
+ linkedinKeywordPosts: () => ({
859
+ tool: "oxygen_signals_search_preview",
860
+ arguments: {
861
+ action: "preview",
862
+ filters: { keywords: ["<any of>"], all_keywords: ["<all of>"], exclude_keywords: ["<none of>"], ai_filter: "<posts to keep>" },
863
+ },
864
+ }),
865
+ };
866
+ /** The catalogue row's `enrichment catalog --intent <id>` as an MCP call. */
867
+ export function catalogueIntentMcpCall(intentId) {
868
+ return { tool: "oxygen_enrichment_catalog", arguments: { intent: intentId } };
869
+ }
870
+ // A Sequence's sending schedule or send window. Workflow language still wins
871
+ // ("a workflow that changes the send window nightly" is automation), and
872
+ // "sending hours" stays with the connected account's working hours.
873
+ function isSequenceScheduleIntent(query) {
874
+ return /\bsend(?:ing)? (?:schedule|window)s?\b/.test(query)
875
+ && !/\b(workflow|automation|automate|webhook|cron)\b/.test(query);
876
+ }
877
+ // The unsubscribe option on a Sequence's emails, not the list of people who
878
+ // opted out: a list, import or do-not-contact ask stays with suppressions.
879
+ function isSequenceUnsubscribeIntent(query) {
880
+ return /\bunsubscribe\b/.test(query)
881
+ && !/\b(workflow|automation|automate|webhook|cron)\b/.test(query)
882
+ && !/\b(unsubscribed|lists?|import\w*|suppress\w*|block ?list|blocklist|do not contact|dnc)\b/.test(query);
883
+ }
884
+ /** A post's first comment is set with the post; `publishing posts update` takes the same flag. */
885
+ export const PUBLISHING_FIRST_COMMENT_COMMAND = 'oxygen publishing posts create --publish-at <iso> --sender <sender_id> --text "<post>" --first-comment "<comment, e.g. the link>" --json';
886
+ /**
887
+ * The comment OXYGEN posts right after a post goes out ("put the link in the first
888
+ * comment") is part of the post, not a reply to one. The 2026-09-29 baselines found
889
+ * no flag, guessed `content.first_comment`, and searched in vain.
890
+ */
891
+ function isFirstCommentIntent(query) {
892
+ return /\bfirst comments?\b/.test(query)
893
+ && !/\b(repl(?:y|ies|ied)|respond\w*|answer\w*)\b/.test(query)
894
+ // Reading comments people left is owned-post comment work, not setting one.
895
+ && !/\b(show|list|read|see|view|who|whose)\b/.test(query);
896
+ }
703
897
  export const COMPANY_PAGE_FOLLOWERS_COMMAND = "oxygen tables followers check --company <linkedin company page URL> --records <n> --json";
704
898
  /**
705
899
  * "The people who follow HubSpot on LinkedIn" / "a competitor's followers" is a
@@ -735,8 +929,10 @@ function isOpenLinkedInRequestsIntent(query) {
735
929
  function explicitCapabilityIntent(query) {
736
930
  if (/\b(infographic|graphic designer|render html|carousel pages|visual design|gtm flow image)\b/.test(query))
737
931
  return ROUTE_BY_ID.get("visual-rendering") ?? null;
738
- if (isUgcProgramIntent(query) || isUgcCreatorSideIntent(query))
932
+ if (isUgcIntent(query))
739
933
  return ROUTE_BY_PRIMITIVE.get("publishing") ?? null;
934
+ if (isWorkspaceVoiceIntent(query))
935
+ return ROUTE_BY_ID.get("knowledge-graph") ?? null;
740
936
  if (isOwnLinkedInNetworkIntent(query) || isOpenLinkedInRequestsIntent(query))
741
937
  return ROUTE_BY_ID.get("connected-linkedin") ?? null;
742
938
  if (isCompanyPageFollowersIntent(query))
@@ -750,7 +946,7 @@ function explicitCapabilityIntent(query) {
750
946
  // names every attached channel (LinkedIn + WhatsApp + email). Resolve this
751
947
  // before public LinkedIn research, whose generic "profile" wording would
752
948
  // otherwise route an account-readiness question to scraper tools.
753
- if (isSenderProfileIntent(query)) {
949
+ if (isSenderProfileIntent(query) || isSenderCopyVariableIntent(query)) {
754
950
  return ROUTE_BY_PRIMITIVE.get("sequences") ?? null;
755
951
  }
756
952
  if (isInboxAvatarIntent(query)) {
@@ -773,6 +969,10 @@ function explicitCapabilityIntent(query) {
773
969
  // enrichment baseline).
774
970
  if (/\bprofile viewers?\b/.test(query))
775
971
  return ROUTE_BY_ID.get("connected-linkedin") ?? null;
972
+ // Ahead of the keyword-post, workflow and owned-post-comment rules below, which
973
+ // claimed "a first comment on my LinkedIn post" and "... automatically".
974
+ if (isFirstCommentIntent(query))
975
+ return ROUTE_BY_PRIMITIVE.get("publishing") ?? null;
776
976
  if (isLinkedInKeywordPostIntent(query))
777
977
  return ROUTE_BY_ID.get("signals") ?? null;
778
978
  // Ahead of the workflow rule, whose "retry" would claim it. Why a post failed and how to get it out is Publishing's attempt ledger and
@@ -781,6 +981,12 @@ function explicitCapabilityIntent(query) {
781
981
  // publish" to the connected-account catalog.
782
982
  if (isFailedPostIntent(query))
783
983
  return ROUTE_BY_PRIMITIVE.get("publishing") ?? null;
984
+ // When and how a Sequence sends: its sending schedule and its unsubscribe
985
+ // option. Ahead of the workflow rule, whose bare "schedule" sent the 2026-09-29
986
+ // unified-launch acceptance agent's "sending schedule" search to Workflows and
987
+ // hid every Sequences command.
988
+ if (isSequenceScheduleIntent(query) || isSequenceUnsubscribeIntent(query))
989
+ return ROUTE_BY_PRIMITIVE.get("sequences") ?? null;
784
990
  if (isHostedWorkflowIntent(query))
785
991
  return ROUTE_BY_PRIMITIVE.get("workflows") ?? null;
786
992
  if (isOwnedPostCommentIntent(query))
@@ -972,17 +1178,38 @@ function isLocalBusinessIntent(query) {
972
1178
  return /\b(local business(?:es)?|google maps|maps listings?|storefronts?|near me|(?:dental|medical|law|veterinary) (?:practices?|offices?|clinics?)|dentists?|restaurants?|cafes?|coffee shops?|gyms?|salons?|plumbers?|electricians?|contractors?|hvac|auto repair|real estate agents?|businesses (?:in|near|around) [a-z])\b/.test(query);
973
1179
  }
974
1180
  function recommendationsFor(card, query) {
1181
+ // A program's lifecycle and a creator's voice are UGC work the Publishing card
1182
+ // also claims ("archive program", "creator voice"); before these they got the
1183
+ // post tools (UGC E2E pass, 2026-09-29).
1184
+ const lifecycle = card.primitive === "publishing" ? UGC_PROGRAM_LIFECYCLE[ugcProgramLifecycleVerb(query) ?? ""] : undefined;
1185
+ if (lifecycle)
1186
+ return { tools: [lifecycle[1], "oxygen_ugc_get"], commands: [lifecycle[0], "ugc programs list"] };
975
1187
  if (card.primitive === "publishing" && isUgcCreatorSideIntent(query)) {
976
1188
  return {
977
1189
  tools: ["oxygen_ugc_get", "oxygen_ugc_application_create", "oxygen_ugc_invite_accept"],
978
1190
  commands: ["ugc programs discover", "ugc programs apply", "ugc creators accept"],
979
1191
  };
980
1192
  }
1193
+ if (card.primitive === "publishing" && isUgcVoiceIntent(query)) {
1194
+ return {
1195
+ tools: ["oxygen_ugc_voice_import", "oxygen_ugc_voice_save", "oxygen_ugc_get"],
1196
+ commands: ["ugc voice import", "ugc voice save", "ugc voice get"],
1197
+ };
1198
+ }
981
1199
  if (card.primitive === "publishing" && isUgcProgramIntent(query)) {
982
1200
  return {
983
1201
  tools: ["oxygen_ugc_program_create", "oxygen_ugc_invite_create", "oxygen_ugc_get"],
984
1202
  commands: ["ugc programs create", "ugc creators invite", "ugc programs list"],
985
1203
  exactCommand: "oxygen ugc programs create --product-url <your product site> --json",
1204
+ exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.ugcProgramCreate(),
1205
+ };
1206
+ }
1207
+ if (card.primitive === "publishing" && isFirstCommentIntent(query)) {
1208
+ return {
1209
+ tools: ["oxygen_publishing_posts_create", "oxygen_publishing_posts_update", "oxygen_publishing_posts_get"],
1210
+ commands: ["publishing posts create", "publishing posts update", "publishing posts get"],
1211
+ exactCommand: PUBLISHING_FIRST_COMMENT_COMMAND,
1212
+ exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.publishingFirstComment(),
986
1213
  };
987
1214
  }
988
1215
  if (card.id === "sourcing-and-provider-tools" && isCompanyUrlRecoveryIntent(query)) {
@@ -990,6 +1217,7 @@ function recommendationsFor(card, query) {
990
1217
  tools: ["oxygen_companies_search_plan", "oxygen_companies_search_run"],
991
1218
  commands: ["companies search plan", "companies search run"],
992
1219
  exactCommand: COMPANY_URL_RECOVERY_COMMAND,
1220
+ exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.companyUrlRecovery(),
993
1221
  };
994
1222
  }
995
1223
  if (card.primitive === "tables" && isCompanyPageFollowersIntent(query)) {
@@ -997,6 +1225,7 @@ function recommendationsFor(card, query) {
997
1225
  tools: ["oxygen_tables_followers", "oxygen_tables_sources"],
998
1226
  commands: ["tables followers check", "tables followers order", "tables sources"],
999
1227
  exactCommand: COMPANY_PAGE_FOLLOWERS_COMMAND,
1228
+ exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.companyPageFollowers(),
1000
1229
  };
1001
1230
  }
1002
1231
  if (card.id === "sourcing-and-provider-tools" && isLocalBusinessIntent(query)) {
@@ -1004,6 +1233,7 @@ function recommendationsFor(card, query) {
1004
1233
  tools: ["oxygen_companies_search_preview", "oxygen_companies_search_run"],
1005
1234
  commands: ["companies search filters", "companies search preview", "companies search run"],
1006
1235
  exactCommand: LOCAL_BUSINESS_SCRAPE_COMMAND,
1236
+ exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.localBusinessScrape(),
1007
1237
  };
1008
1238
  }
1009
1239
  if (card.id === "connected-linkedin" && isLinkedInAccountConnectIntent(query)) {
@@ -1011,6 +1241,7 @@ function recommendationsFor(card, query) {
1011
1241
  tools: ["oxygen_senders_connect", "oxygen_senders_list", "oxygen_billing_commitments"],
1012
1242
  commands: ["senders connect", "senders list", "billing commitments"],
1013
1243
  exactCommand: "oxygen senders connect --country <cc> --json",
1244
+ exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.senderConnect(),
1014
1245
  };
1015
1246
  }
1016
1247
  if (card.id === "connected-linkedin" && isLinkedInAccountLimitsIntent(query)) {
@@ -1122,6 +1353,7 @@ function recommendationsFor(card, query) {
1122
1353
  tools: ["oxygen_columns_catalog", "oxygen_columns_add", "oxygen_columns_run", "oxygen_tables_query"],
1123
1354
  commands: ["columns catalog", "columns add", "columns run", "tables query"],
1124
1355
  exactCommand: ROW_CLASSIFICATION_COMMAND,
1356
+ exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.rowClassification(),
1125
1357
  };
1126
1358
  }
1127
1359
  if (isCompanyResearchQuestionIntent(query)) {
@@ -1184,6 +1416,16 @@ function recommendationsFor(card, query) {
1184
1416
  }
1185
1417
  }
1186
1418
  if (card.primitive === "sequences") {
1419
+ if (isSequenceScheduleIntent(query) || isSequenceUnsubscribeIntent(query)) {
1420
+ return {
1421
+ tools: ["oxygen_sequences_update", "oxygen_sequences_get"],
1422
+ commands: ["sequences update", "sequences get"],
1423
+ exactCommand: isSequenceScheduleIntent(query) ? SEQUENCE_SENDING_SCHEDULE_COMMAND : SEQUENCE_UNSUBSCRIBE_COMMAND,
1424
+ exactMcpCall: isSequenceScheduleIntent(query)
1425
+ ? CAPABILITY_ROUTE_MCP_CALLS.sequenceSendingSchedule()
1426
+ : CAPABILITY_ROUTE_MCP_CALLS.sequenceUnsubscribe(),
1427
+ };
1428
+ }
1187
1429
  if (isSenderProfileIntent(query)) {
1188
1430
  return {
1189
1431
  tools: ["oxygen_senders_profiles_list", "oxygen_senders_profiles_get"],
@@ -1282,6 +1524,7 @@ function recommendationsFor(card, query) {
1282
1524
  tools: ["oxygen_signals_search_preview", "oxygen_signals_search_run"],
1283
1525
  commands: ["signals search preview", "signals search run"],
1284
1526
  exactCommand: LINKEDIN_KEYWORD_POST_COMMAND,
1527
+ exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.linkedinKeywordPosts(),
1285
1528
  };
1286
1529
  }
1287
1530
  if (card.primitive === "signals" && /\blinkedin\b.{0,40}\bengagers?\b/.test(query)) {
@@ -1296,6 +1539,13 @@ function recommendationsFor(card, query) {
1296
1539
  commands: ["whatsapp accounts", "whatsapp get", "whatsapp limits get", "whatsapp connect"],
1297
1540
  };
1298
1541
  }
1542
+ // Brand voice is read from its wiki page; only a writing voice is synthesized.
1543
+ if (card.id === "knowledge-graph" && isWorkspaceVoiceIntent(query) && !/\bbrand voice\b/.test(query)) {
1544
+ return {
1545
+ tools: ["oxygen_context_synthesize", "oxygen_knowledge_search", "oxygen_knowledge_page_get"],
1546
+ commands: ["knowledge synthesize", "knowledge search", "knowledge page get"],
1547
+ };
1548
+ }
1299
1549
  return { tools: [...card.gatewayTools], commands: [...card.gatewayCommands] };
1300
1550
  }
1301
1551
  // An email-VERIFICATION ask names a grading verb next to the addresses, or the
@@ -1375,6 +1625,18 @@ function isSenderProfileIntent(query) {
1375
1625
  || (/\bprofiles?\b.{0,64}\b(?:linkedin|whatsapp|mailboxes?|inboxes?|email)\b/.test(query)
1376
1626
  && /\b(?:sender|sending|unif(?:y|ied))\b/.test(query));
1377
1627
  }
1628
+ /**
1629
+ * Naming whoever sends a step inside its copy ({{sender_first_name}}, a sign-off
1630
+ * with the sender's name) is Sequence step copy. A 2026-09-29 blind user asked
1631
+ * "preview rendered message per sender sign-off variable" and was routed to
1632
+ * comments and approvals, and "sign every email with …" reads as a one-off
1633
+ * Message. A bare "sender name" stays out: renaming an inbox's From is not copy.
1634
+ */
1635
+ function isSenderCopyVariableIntent(query) {
1636
+ return /\bsender (?:first|last|full) name\b/.test(query)
1637
+ || /\bsign off (?:variable|with|per sender)\b|\bper sender sign off\b/.test(query)
1638
+ || /\bsign (?:every|each|all) (?:emails?|messages?|dms?|notes?) with\b/.test(query);
1639
+ }
1378
1640
  /** The account's own working hours or daily caps (`senders limits`), not its network. */
1379
1641
  /**
1380
1642
  * Connecting, or asking what it takes to connect, the workspace's own LinkedIn
@@ -12,7 +12,7 @@ import { type TagKind } from "./tags.js";
12
12
  * both ways: {@link EveryTagKindIsCommentable} fails the build if a taggable
13
13
  * kind is missing, and the paired test pins today's list exactly.
14
14
  */
15
- export declare const COLLAB_SUBJECT_KINDS: readonly ["knowledge_page", "publishing_post", "sequence", "table", "workflow", "recipe", "conversation", "mailbox", "sender", "record", "domain", "project", "sender_profile", "voice_number"];
15
+ export declare const COLLAB_SUBJECT_KINDS: readonly ["knowledge_page", "publishing_post", "sequence", "table", "workflow", "recipe", "conversation", "mailbox", "sender", "record", "domain", "project", "sender_profile", "voice_number", "signal_type", "dashboard", "agent", "ad_search_collection"];
16
16
  export type CollabSubjectKind = (typeof COLLAB_SUBJECT_KINDS)[number];
17
17
  /** Compile-time assertion helper: instantiating with `false` is a type error. */
18
18
  type AssertTrue<T extends true> = T;
@@ -45,6 +45,10 @@ export const COLLAB_SUBJECT_KINDS = [
45
45
  "project",
46
46
  "sender_profile",
47
47
  "voice_number",
48
+ "signal_type",
49
+ "dashboard",
50
+ "agent",
51
+ "ad_search_collection",
48
52
  ];
49
53
  export function isCollabSubjectKind(value) {
50
54
  return typeof value === "string" && COLLAB_SUBJECT_KINDS.includes(value);
@@ -74,6 +78,10 @@ export const COLLAB_SUBJECT_LABELS = {
74
78
  project: { one: "project", many: "projects" },
75
79
  sender_profile: { one: "sender profile", many: "sender profiles" },
76
80
  voice_number: { one: "phone number", many: "phone numbers" },
81
+ signal_type: { one: "signal type", many: "signal types" },
82
+ dashboard: { one: "dashboard", many: "dashboards" },
83
+ agent: { one: "agent", many: "agents" },
84
+ ad_search_collection: { one: "saved Ads search", many: "saved Ads searches" },
77
85
  };
78
86
  /**
79
87
  * What a thread on each kind is usually about, for `--help` and MCP tool
@@ -100,6 +108,10 @@ export const COLLAB_SUBJECT_PROSE = {
100
108
  project: "a project's scope",
101
109
  sender_profile: "a sending identity's accounts and inboxes",
102
110
  voice_number: "a dialing number's caps and warmup",
111
+ signal_type: "a signal type's classification and use",
112
+ dashboard: "a dashboard's layout and measures",
113
+ agent: "an Agent's configuration and runs",
114
+ ad_search_collection: "a saved Ads search's targeting query",
103
115
  };
104
116
  /** Oxford-comma list: ["a"] -> "a"; ["a","b"] -> "a and b"; ["a","b","c"] -> "a, b, and c". */
105
117
  function formatProseList(items) {
@@ -213,11 +213,15 @@ export function columnDecisionOutputSchema(decision) {
213
213
  const properties = {
214
214
  [decision.field]: answer,
215
215
  ...(decision.mode === "score"
216
- ? { level: { type: ["string", "null"], enum: [...(decision.levels ?? []), null], description: "The nearest level." } }
216
+ ? { level: { type: ["string", "null"], enum: [...(decision.levels ?? []), null], description: "Level nearest the numeric score; it can differ from the most probable level." } }
217
217
  : {}),
218
- confidence: { type: "number", description: "How sure the classifier is, 0 to 1." },
218
+ confidence: { type: "number", description: decision.mode === "score"
219
+ ? "System One confidence, 0 to 1. It is separate from level probabilities, not their top share or the gap between the top two."
220
+ : "How sure the classifier is, 0 to 1." },
219
221
  uncertain: { type: "boolean", description: "True when confidence is below the column's floor." },
220
- reason: { type: "string", description: "The answer and its confidence in words." },
222
+ reason: { type: "string", description: decision.mode === "score"
223
+ ? "Readable level, confidence, and top probabilities; it does not explain the row evidence."
224
+ : "The answer and its confidence in words." },
221
225
  };
222
226
  return {
223
227
  type: "object",