@oxygen-agent/cli 1.1011.493 → 1.1011.607

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 (72) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +7 -0
  3. package/dist/crm-file-read-commands.d.ts +6 -0
  4. package/dist/crm-file-read-commands.js +16 -0
  5. package/dist/feed-read-flags.d.ts +3 -0
  6. package/dist/feed-read-flags.js +9 -0
  7. package/dist/functions-commands.js +2 -1
  8. package/dist/help.js +18 -5
  9. package/dist/index.js +168 -107
  10. package/dist/sender-limits-flags.js +6 -4
  11. package/dist/settings-commands.js +11 -11
  12. package/dist/whatsapp-account-flags.d.ts +4 -0
  13. package/dist/whatsapp-account-flags.js +18 -0
  14. package/node_modules/@oxygen/cli-ugc/dist/commands.js +2 -2
  15. package/node_modules/@oxygen/shared/dist/billing.d.ts +41 -0
  16. package/node_modules/@oxygen/shared/dist/billing.js +56 -0
  17. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +21 -0
  18. package/node_modules/@oxygen/shared/dist/capability-discovery.js +254 -21
  19. package/node_modules/@oxygen/shared/dist/contracts/billing-owner-read.d.ts +6 -0
  20. package/node_modules/@oxygen/shared/dist/contracts/billing-owner-read.js +7 -0
  21. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +4 -2
  22. package/node_modules/@oxygen/shared/dist/contracts/copilot-sessions.d.ts +11 -0
  23. package/node_modules/@oxygen/shared/dist/contracts/copilot-sessions.js +7 -0
  24. package/node_modules/@oxygen/shared/dist/contracts/crm-file-bytes.d.ts +33 -0
  25. package/node_modules/@oxygen/shared/dist/contracts/crm-file-bytes.js +12 -0
  26. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +6 -3
  27. package/node_modules/@oxygen/shared/dist/contracts/function-library-read.d.ts +14 -0
  28. package/node_modules/@oxygen/shared/dist/contracts/function-library-read.js +16 -0
  29. package/node_modules/@oxygen/shared/dist/contracts/inbox-drafts-read.d.ts +45 -0
  30. package/node_modules/@oxygen/shared/dist/contracts/inbox-drafts-read.js +21 -0
  31. package/node_modules/@oxygen/shared/dist/contracts/limits-read.d.ts +29 -0
  32. package/node_modules/@oxygen/shared/dist/contracts/limits-read.js +121 -0
  33. package/node_modules/@oxygen/shared/dist/contracts/linkedin-invites-list.js +1 -1
  34. package/node_modules/@oxygen/shared/dist/contracts/pipedrive-catalog.d.ts +18 -0
  35. package/node_modules/@oxygen/shared/dist/contracts/pipedrive-catalog.js +29 -0
  36. package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.d.ts +181 -0
  37. package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +62 -0
  38. package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +7 -5
  39. package/node_modules/@oxygen/shared/dist/contracts/sequence-list.d.ts +3 -0
  40. package/node_modules/@oxygen/shared/dist/contracts/sequence-list.js +8 -0
  41. package/node_modules/@oxygen/shared/dist/contracts/stored-read-window.d.ts +17 -0
  42. package/node_modules/@oxygen/shared/dist/contracts/stored-read-window.js +8 -0
  43. package/node_modules/@oxygen/shared/dist/contracts/table-feed-read.d.ts +67 -0
  44. package/node_modules/@oxygen/shared/dist/contracts/table-feed-read.js +19 -0
  45. package/node_modules/@oxygen/shared/dist/contracts/table-row-read.d.ts +30 -0
  46. package/node_modules/@oxygen/shared/dist/contracts/table-row-read.js +9 -0
  47. package/node_modules/@oxygen/shared/dist/contracts/whatsapp-reads.d.ts +111 -0
  48. package/node_modules/@oxygen/shared/dist/contracts/whatsapp-reads.js +33 -0
  49. package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.d.ts +6 -5
  50. package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.js +5 -5
  51. package/node_modules/@oxygen/shared/dist/copilot-playbooks.d.ts +4 -1
  52. package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.d.ts +17 -0
  53. package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.js +26 -0
  54. package/node_modules/@oxygen/shared/dist/copilot-playbooks.js +2 -32
  55. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +5 -0
  56. package/node_modules/@oxygen/shared/dist/feature-gates.js +6 -0
  57. package/node_modules/@oxygen/shared/dist/first-month-offer-window.d.ts +34 -0
  58. package/node_modules/@oxygen/shared/dist/first-month-offer-window.js +41 -0
  59. package/node_modules/@oxygen/shared/dist/first-month-offer.d.ts +42 -0
  60. package/node_modules/@oxygen/shared/dist/first-month-offer.js +56 -0
  61. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +32 -1
  62. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +68 -4
  63. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +6 -2
  64. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +6 -1
  65. package/node_modules/@oxygen/shared/dist/sql-error.js +13 -2
  66. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +3 -0
  67. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +17 -1
  68. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  69. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  70. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +11 -1
  71. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +29 -3
  72. package/package.json +1 -1
@@ -22,9 +22,9 @@ export const OXYGEN_CAPABILITY_ROUTES = [
22
22
  id: "workspace-access",
23
23
  layer: "Control",
24
24
  primitive: null,
25
- owns: "Authentication, CLI identities, API keys, sessions, organizations, active-workspace selection, connected apps (ChatGPT, Claude, Codex…) with the default workspace each one works in and the OXYGEN events each one is subscribed to, and which web-app sidebar pages the workspace hides for every member (Settings → Customise).",
25
+ owns: "Authentication, CLI identities, API keys, sessions, organizations, active-workspace selection, connected apps (ChatGPT, Claude, Codex…) with the default workspace each one works in and the OXYGEN events each one is subscribed to, and which web-app sidebar pages each person hides from their own sidebar in a workspace (Settings → Customise).",
26
26
  notFor: "Workspace GTM data, provider credentials, execution inside a primitive, or access control: a hidden sidebar page stays reachable by URL.",
27
- execution: "Resolve identity and select the hosted workspace before operating its primitives. Each app connected with OAuth keeps its own default workspace: `connectors list` shows them and `connectors set` changes one app only; orgs select changes only the app or CLI that calls it. What OXYGEN sends an app (its event subscriptions, such as ChatGPT watching for classified replies, and every delivery with its outcome, never content): `connectors events get <app>`; `connectors events revoke <app> --subscription <id|all>` stops them for good. While an app holds event subscriptions, its paid calls are capped by its rolling 24-hour credit ceiling: the plan default, or `connectors set <app> --credit-ceiling <credits>` (above the default also needs --approve-above-default); `connectors get <app>` shows the ceiling and what it spent. Sidebar pages (Settings → Customise): `settings pages list` (oxygen_settings_pages_get) shows each page and whether it is hidden; an admin or owner hides or shows pages for everyone with `settings pages set --hide <pages>`, `--show <pages>` or `--show-all` (oxygen_settings_pages_update). Nothing is deleted.",
27
+ execution: "Resolve identity and select the hosted workspace before operating its primitives. Each app connected with OAuth keeps its own default workspace: `connectors list` shows them and `connectors set` changes one app only; orgs select changes only the app or CLI that calls it. What OXYGEN sends an app (its event subscriptions, such as ChatGPT watching for classified replies, and every delivery with its outcome, never content): `connectors events get <app>`; `connectors events revoke <app> --subscription <id|all>` stops them for good. While an app holds event subscriptions, its paid calls are capped by its rolling 24-hour credit ceiling: the plan default, or `connectors set <app> --credit-ceiling <credits>` (above the default also needs --approve-above-default); `connectors get <app>` shows the ceiling and what it spent. Sidebar pages (Settings → Customise) are per person and per workspace: `settings pages list` (oxygen_settings_pages_get) shows each page and whether you hid it; `settings pages set --hide <pages>`, `--show <pages>` or `--show-all` (oxygen_settings_pages_update) changes only your own sidebar, never a teammate's. Nothing is deleted.",
28
28
  posture: "mixed",
29
29
  gatewayTools: [
30
30
  "oxygen_whoami",
@@ -92,7 +92,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
92
92
  gatewayCommands: ["billing balance", "billing usage", "billing estimate", "budget list", "billing invoices", "pricing customer-credit-prices"],
93
93
  skills: ["oxygen-gtm", "oxygen-diagnostics"],
94
94
  endpointSections: ["billing", "budget", "limits", "pricing"],
95
- intentTerms: ["billing", "credits", "credit balance", "customer credit prices", "credit price catalog", "usage", "spend", "cost", "cost estimate", "monthly cost", "budget", "subscription", "invoice", "vat", "vat number", "tax id", "cancel plan", "change plan", "upgrade", "downgrade"],
95
+ intentTerms: ["billing", "credits", "credit balance", "customer credit prices", "credit price catalog", "usage", "spend", "cost", "cost estimate", "monthly cost", "budget", "subscription", "invoice", "vat", "vat number", "tax id", "cancel plan", "change plan", "upgrade", "downgrade", "subscription plans", "annual plan", "yearly commitment", "yearly saving", "annual discount"],
96
96
  },
97
97
  {
98
98
  id: "support-and-feedback",
@@ -246,21 +246,21 @@ export const OXYGEN_CAPABILITY_ROUTES = [
246
246
  // `capabilities search "founders of a company"` and got `route: null` with a
247
247
  // generic sourcing pointer, then hand-wrote four research prompts for
248
248
  // questions the template catalog now answers by key.
249
- intentTerms: ["table", "spreadsheet", "rows", "column", "columns", "dataset", "csv", "import", "hubspot list", "hubspot segment", "pipedrive filter", "pipedrive", "saved list", "saved segment", "saved filter", "crm segment", "list import", "segment import", "filter import", "import a segment", "import a list", "import a filter", "enrich", "enrichment", "waterfall", "score", "formula", "ai column", "lookup", "link", "link tables", "join", "connect", "relate", "relationship", "function", "functions", "reusable function", "function draft", "function version", "callable", "column template", "skill set", "grad school", "podcast appearance", "keynote", "github profile", "job fit", "founders", "parent company", "subsidiaries", "cloud provider", "company research", "research question", "research column", "web research", "template catalog", "ready-made", "offers demos", "naics", "cost per row", "price per row", "what can i enrich", "enrichment cost", "credits per row"],
249
+ intentTerms: ["table", "spreadsheet", "rows", "column", "columns", "dataset", "csv", "import", "hubspot list", "hubspot segment", "pipedrive filter", "pipedrive", "saved list", "saved segment", "saved filter", "crm segment", "list import", "segment import", "filter import", "import a segment", "import a list", "import a filter", "enrich", "enrichment", "waterfall", "score", "formula", "ai column", "lookup", "link", "link tables", "join", "connect", "relate", "relationship", "function", "functions", "reusable function", "function draft", "function version", "callable", "column template", "skill set", "grad school", "podcast appearance", "keynote", "github profile", "job fit", "founders", "parent company", "subsidiaries", "cloud provider", "company research", "research question", "research column", "web research", "template catalog", "ready-made", "offers demos", "naics", "cost per row", "price per row", "what can i enrich", "enrichment cost", "credits per row", "flatten", "explode", "json array", "array cell"],
250
250
  },
251
251
  {
252
252
  id: "messages",
253
253
  layer: "Data",
254
254
  primitive: "messages",
255
- owns: "One-to-one message facts, conversations, direct email, inbox drafts, and replies in existing channel threads.",
255
+ owns: "One-to-one message facts, imported mailbox history, conversations, direct email, inbox drafts, and replies in existing channel threads.",
256
256
  notFor: "Net-new LinkedIn initiation, cadence, enrollment, sender rotation, or reply-stop programs; those belong to Sequences.",
257
- execution: "Preview and approve a direct email or existing-thread reply; every sent fact stays in the hosted Messages ledger.",
257
+ execution: "Read conversations and imported email summaries in Unibox. Inspect mailboxes sync for Gmail/Microsoft 365 history and included CRM analysis progress. Preview and approve direct email or existing-thread replies; every sent fact stays in Messages.",
258
258
  posture: "external_write",
259
259
  gatewayTools: ["oxygen_inbox_send", "oxygen_email_send", "oxygen_messages_query", "oxygen_unibox_metadata"],
260
260
  gatewayCommands: ["inbox send", "email send", "messages query", "unibox metadata"],
261
261
  skills: ["oxygen-unibox"],
262
262
  endpointSections: ["email", "inbox", "message-reviews", "messages", "unibox"],
263
- intentTerms: ["message", "messages", "reply", "email", "inbox", "unibox", "inbox metadata", "conversation", "thread", "direct email", "draft reply", "one off"],
263
+ intentTerms: ["message", "messages", "reply", "email", "inbox", "unibox", "inbox metadata", "conversation", "thread", "direct email", "draft reply", "one off", "email history", "mail history", "analyze emails", "email crm"],
264
264
  },
265
265
  {
266
266
  id: "posts",
@@ -476,17 +476,23 @@ export const OXYGEN_CAPABILITY_ROUTES = [
476
476
  id: "sending-infrastructure",
477
477
  layer: "External",
478
478
  primitive: null,
479
- owns: "Sending domains, managed/BYOK mailboxes, warmup, deliverability, egress IPs, and infrastructure health beneath Sequences.",
479
+ owns: "Sending domains, managed/BYOK mailboxes, customer Gmail/Microsoft 365 connections, mail history controls, warmup, deliverability, egress IPs, and infrastructure health.",
480
480
  notFor: "Writing campaign copy or sending outside a Sequence/Message owner.",
481
- execution: "Inspect or provision hosted sending infrastructure with explicit recurring-cost and external-write approval.",
481
+ execution: "Inspect or provision hosted sending infrastructure with explicit recurring-cost and external-write approval. Connect existing Gmail or Microsoft 365/Exchange Online mailboxes using mailboxes connect-oauth --vendor composio where enabled; 100 credits/month per mailbox includes mail history and CRM analysis. Read progress with oxygen_mailboxes_sync_status (CLI: mailboxes sync without --action); control it with oxygen_mailboxes_sync. Read oxygen-email-infra for history coverage and CRM rules.",
482
482
  posture: "mixed",
483
- gatewayTools: ["oxygen_mailboxes_list", "oxygen_domains_list", "oxygen_mailboxes_health"],
484
- gatewayCommands: ["mailboxes list", "domains list", "mailboxes health"],
483
+ gatewayTools: ["oxygen_mailboxes_list", "oxygen_mailboxes_connect_oauth", "oxygen_mailboxes_sync_status", "oxygen_mailboxes_sync", "oxygen_domains_list", "oxygen_mailboxes_health"],
484
+ gatewayCommands: ["mailboxes list", "mailboxes connect-oauth", "mailboxes sync", "domains list", "mailboxes health"],
485
485
  skills: ["oxygen-email-infra", "oxygen-deliverability", "oxygen-sequencer"],
486
486
  endpointSections: ["deliverability", "domains", "egress", "mailboxes", "managed-inboxes"],
487
487
  intentTerms: [
488
488
  "mailbox",
489
489
  "mailboxes",
490
+ "gmail",
491
+ "microsoft 365",
492
+ "exchange online",
493
+ "outlook",
494
+ "connect inbox",
495
+ "mailbox history",
490
496
  "sending domain",
491
497
  "dns",
492
498
  "warmup",
@@ -563,6 +569,33 @@ export function inferCapabilityRoute(query) {
563
569
  const normalized = normalizeIntent(query);
564
570
  if (!normalized)
565
571
  return null;
572
+ // A complete audience play starts from its Recipe and the shared audience
573
+ // guide; onboarding consults on open-ended goals and never routes this ask.
574
+ // Its source nouns otherwise select a one-shot post search or follower order.
575
+ if (isLinkedInAudienceCampaignIntent(query)) {
576
+ const recipes = ROUTE_BY_ID.get("recipes");
577
+ const slug = query.trim().toLowerCase();
578
+ const namedPlay = AUDIENCE_PLAY_SLUGS.includes(slug) ? slug : null;
579
+ if (recipes)
580
+ return {
581
+ card: {
582
+ ...recipes,
583
+ execution: "Pick the matching audience Recipe (own, competitor, creator or keyword post engagers, company-page followers or own followers), resolve current company/ICP context, then read that Recipe and oxygen-recipes file guides/audience-campaign.md. Preserve the exact source, attribution, qualification rule/version and object IDs. Prepare a bounded native audience campaign for email, LinkedIn DM/connection requests or WhatsApp. Keep capture, standing qualification, router and Sequence disabled until their own approval and credit scope; the shared guide owns execution.",
584
+ skills: ["oxygen-recipes", "oxygen-linkedin-marketing"],
585
+ },
586
+ recommendedTools: ["oxygen_recipes_list", "oxygen_recipes_get", "oxygen_context_resolve"],
587
+ recommendedCommands: ["recipes list", "recipes show", "context resolve"],
588
+ ...(namedPlay
589
+ ? {
590
+ exactCommand: `oxygen recipes show ${namedPlay} --json`,
591
+ exactMcpCall: { tool: "oxygen_recipes_get", arguments: { slug: namedPlay } },
592
+ }
593
+ : {
594
+ exactCommand: 'oxygen recipes list "linkedin audience" --view summary --json',
595
+ exactMcpCall: { tool: "oxygen_recipes_list", arguments: { query: "linkedin audience", view: "summary" } },
596
+ }),
597
+ };
598
+ }
566
599
  const explicit = explicitCapabilityIntent(normalized);
567
600
  const catalogRow = findEnrichmentIntentByAlias(normalized);
568
601
  // Card scoring stays the owner of routing; the catalogue only breaks a
@@ -764,6 +797,60 @@ export function serializeCapabilityRoute(route, surfaceOrOptions = "cli", option
764
797
  function normalizeIntent(query) {
765
798
  return query.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
766
799
  }
800
+ /** The six LinkedIn audience-campaign Recipes, by catalog slug. */
801
+ const AUDIENCE_PLAY_SLUGS = [
802
+ "own-post-engagers",
803
+ "competitor-post-engagers",
804
+ "creator-post-engagers",
805
+ "keyword-post-engagers",
806
+ "company-page-followers",
807
+ "own-profile-followers",
808
+ ];
809
+ /** Whole audience acquisition/outreach; individual reads and runtime controls keep their owners. */
810
+ function isLinkedInAudienceCampaignIntent(rawQuery) {
811
+ const query = normalizeIntent(rawQuery);
812
+ // Match exact Recipe slugs before punctuation normalization makes a slug
813
+ // indistinguishable from a plain company-page follower research request.
814
+ const canonicalPlay = new RegExp(`\\b(?:${AUDIENCE_PLAY_SLUGS.join("|")})\\b`, "i").exec(rawQuery);
815
+ if (canonicalPlay?.[0].toLowerCase() === rawQuery.trim().toLowerCase())
816
+ return true;
817
+ if (!/\blinkedin\b/.test(query) && !canonicalPlay)
818
+ return false;
819
+ // Audience nouns can be the subject of a local read/control operation. Only
820
+ // explicit acquisition/setup language may turn such a request into a play.
821
+ const acquisition = /\b(build|prepare|create|start|launch|scrape|capture|collect)\b|\bset up\b/.test(query);
822
+ if (!acquisition && /\b(show|list|read|inspect|verify|validate|label|classify|tag|rank|grade|score|rename|update|change|edit|delete|remove|clear|export|import)\b/.test(query))
823
+ return false;
824
+ if (/\boxygen_\w+\b/i.test(rawQuery)
825
+ || isUgcIntent(query)
826
+ || isWorkspacePagesIntent(query) || isWorkspaceSettingsIntent(query)
827
+ || isPublishingPostIntent(query) || isFirstCommentIntent(query)
828
+ || isPostCoverOrPreviewIntent(query) || isFailedPostIntent(query)
829
+ || isOwnedPostCommentIntent(query) || isSecondPartyDecisionIntent(query)
830
+ || tableReadRecommendation(query) || tableMaintenanceRecommendation(query)
831
+ || isSequenceScheduleIntent(query) || isSequenceUnsubscribeIntent(query)
832
+ || isSenderProfileIntent(query) || isSenderCopyVariableIntent(query)
833
+ || isLinkedInAccountLimitsIntent(query)
834
+ || /\b(workflows?|sequences?|cadences?|webhook|cron|event trigger|branching|retries)\b/.test(query)
835
+ || /\b(status|pause|resume|disable|enable|logs?|tail|price|pricing|order)\b/.test(query)
836
+ || /\b(known|this|single|one) posts?\b|linkedin\.com\/(?:posts\/|feed\/update\/)/.test(query)
837
+ || /\b(one|single) (?:of them|person|recipient)\b/.test(query))
838
+ return false;
839
+ if (canonicalPlay)
840
+ return true;
841
+ const audienceFromSources = /\baudience\b/.test(query) && /\b(from|using|sources?|based on)\b/.test(query);
842
+ if (audienceFromSources && /\b(posts?|creators?|keywords?|profiles?|followers?)\b/.test(query))
843
+ return true;
844
+ const engagement = /\bposts?\b/.test(query) && /\b(engagers?|engaged|engagement|reactors?|reacting|reactions?|commenters?|commenting)\b/.test(query);
845
+ const followers = /\bfollowers?\b/.test(query);
846
+ if (!engagement && !followers)
847
+ return false;
848
+ const outreach = /\b(campaigns?|outreach|warm outbound|inbound led outbound)\b/.test(query)
849
+ || /\b(?:cold )?(email|dm|whatsapp|invite) (?:them|the (?:engagers|followers|audience))\b/.test(query)
850
+ || /\b(send|start)\b.{0,35}\b(emails?|dms?|connection requests?|invitations?|whatsapp messages?)\b/.test(query);
851
+ const namedPlay = /\b(recipes?|playbooks?|plays?|whole motion|complete motion|end to end motion)\b/.test(query);
852
+ return outreach || namedPlay;
853
+ }
767
854
  /**
768
855
  * "LinkedIn posts about X" / "posts mentioning Y" / "monitor LinkedIn posts by
769
856
  * keyword" is keyword post search, a Signals source with all-of / any-of /
@@ -849,6 +936,15 @@ const UGC_ROUTE_VIEW = {
849
936
  function routeCardFor(card, query) {
850
937
  if (card.primitive === "publishing" && isUgcIntent(query))
851
938
  return { ...card, ...UGC_ROUTE_VIEW };
939
+ const contactFind = card.id === "sourcing-and-provider-tools" ? oneShotContactFindingRecommendation(query) : null;
940
+ if (contactFind)
941
+ card = { ...card, execution: contactFind.execution };
942
+ const crmContactRead = card.id === "records" ? storedCrmContactReadRecommendation(query) : null;
943
+ if (crmContactRead)
944
+ card = { ...card, posture: "read_only", execution: crmContactRead.execution };
945
+ const replyDraftRead = card.primitive === "messages" ? storedReplyDraftReadRecommendation(query) : null;
946
+ if (replyDraftRead)
947
+ card = { ...card, posture: "read_only", execution: replyDraftRead.execution };
852
948
  if (card.id === "knowledge-graph" && isWorkspaceVoiceIntent(query))
853
949
  card = { ...card, execution: WORKSPACE_VOICE_EXECUTION };
854
950
  if (card.primitive === "workflows" && isRecurringCompanyMonitoringIntent(query))
@@ -859,7 +955,7 @@ function routeCardFor(card, query) {
859
955
  if (card.primitive === "tables" && isTableCapacityReadIntent(query))
860
956
  card = {
861
957
  ...card,
862
- execution: "Read oxygen_limits_show for Tables capacity, largest_table and largest_table_rows. Use that returned table reference with oxygen_tables_query and a small limit to inspect sample rows; no enrichment run is needed.",
958
+ execution: "Read oxygen_limits_show with section=capacity for largest_table and largest_table_rows. Counts cover all registered workspace Tables, including CRM and archived Tables, and are available only when row_usage_reconciled is true; pending counts are unknown. Use the returned table reference with oxygen_tables_query and a small limit for sample rows. Planned capacity is not current headroom.",
863
959
  };
864
960
  if (card.primitive === "tables" && isFunctionIntent(query))
865
961
  card = {
@@ -877,6 +973,9 @@ function routeCardFor(card, query) {
877
973
  const read = card.primitive === "tables" ? tableReadRecommendation(query) : null;
878
974
  if (read)
879
975
  card = { ...card, execution: read.execution };
976
+ const sent = card.id === "connected-linkedin" ? sentLinkedInInvitationReadRecommendation(query) : null;
977
+ if (sent)
978
+ card = { ...card, execution: sent.execution };
880
979
  const database = card.id === "platform-operations" ? databaseRecommendation(query) : null;
881
980
  if (database)
882
981
  card = { ...card,
@@ -1017,6 +1116,16 @@ export const CAPABILITY_ROUTE_MCP_CALLS = {
1017
1116
  // Like the CLI command, it leaves the LinkedIn risk acknowledgement to the user.
1018
1117
  senderConnect: () => ({ tool: "oxygen_senders_connect", arguments: { country: "<cc>" } }),
1019
1118
  rowClassification: () => ({ tool: "oxygen_columns_catalog", arguments: { family: "classification" } }),
1119
+ rowFanOut: () => ({
1120
+ tool: "oxygen_tables_send",
1121
+ arguments: {
1122
+ source: "<source table>",
1123
+ target: "<target table>",
1124
+ flatten: { column: "<column holding the array>", path: "<path to the array>" },
1125
+ mapping: [{ target: "<target column>", source: { type: "column", key: "$element" }, path: "<field in each element>" }],
1126
+ dry_run: true,
1127
+ },
1128
+ }),
1020
1129
  // The tool has no `family`: it always searches linkedin_posts, which the API reads as the same source.
1021
1130
  linkedinKeywordPosts: () => ({
1022
1131
  tool: "oxygen_signals_search_preview",
@@ -1102,6 +1211,21 @@ function isOpenLinkedInRequestsIntent(query) {
1102
1211
  return /\b(connection|invitation|invite|connect) requests?\b/.test(query)
1103
1212
  || /\b(pending|open|unanswered|outstanding|received|sent) (linkedin )?(invitations?|invites?)\b/.test(query);
1104
1213
  }
1214
+ /** One live sent-invitation page, separate from recurring capture and writes. */
1215
+ function sentLinkedInInvitationReadRecommendation(query) {
1216
+ if (!/\blinkedin\b/.test(query) || !/\b(invitations?|invites?|connection requests?)\b/.test(query)
1217
+ || !/\b(sent|outgoing)\b/.test(query)
1218
+ || /\b(incoming|received|mixed|tables?|workflows?|agents?|functions?|crm|mailbox\w*|setup|capture|monitor\w*|sync|recurring|schedule\w*|cron|send|sending|enroll|withdraw\w*|cancel|accept|decline|sequences?|outreach|cadence|research|third party|public)\b/.test(query))
1219
+ return null;
1220
+ if (!/\b(read|list|show|inspect|review|resume|next page|cursor|pagination)\b/.test(query)
1221
+ && !/\b(cli|command line|native command)\b/.test(query))
1222
+ return null;
1223
+ return {
1224
+ tools: ["oxygen_linkedin_invites_sent", "oxygen_senders_list"],
1225
+ commands: ["linkedin invitations list", "senders list"],
1226
+ execution: "Read one live, free pending-sent invitation page with oxygen_linkedin_invites_sent via oxygen_capabilities_read; CLI twin: oxygen linkedin invitations list --account <ref> --limit <n> --cursor <token> --json. Resolve the sender with oxygen_senders_list. Resume invitations.cursor unchanged with the same account; a full v2 page can need one empty final page. No provider writes or credits. Recurring received/sent capture is linkedin invitations setup; withdrawal is a separate external action.",
1227
+ };
1228
+ }
1105
1229
  /**
1106
1230
  * The apps a person connected with OAuth and where each one works: "make
1107
1231
  * ChatGPT always use workspace X", "default workspace for my Claude
@@ -1163,7 +1287,7 @@ function isConnectedAppSpendIntent(query) {
1163
1287
  return !/\b(linkedin|senders?|mailbox(?:es)?|sequences?|email|emails|whatsapp|warm\s?up|invites?|rate)\b/.test(query);
1164
1288
  }
1165
1289
  /**
1166
- * The web app's sidebar pages a workspace hides for every member (Settings →
1290
+ * The web app's sidebar pages a person hides from their own sidebar (Settings →
1167
1291
  * Customise, `settings pages`). Both blind baselines of 2026-09-30 searched
1168
1292
  * "hide navigation pages sidebar", "customise pages", "pages visibility", "nav
1169
1293
  * items" and "workspace navigation hidden pages unibox publishing workflows",
@@ -1185,6 +1309,14 @@ function isWorkspacePagesIntent(query) {
1185
1309
  return true;
1186
1310
  if (/\bmenu items?\b/.test(query) && /\b(hid(?:e|es|den|ing)|unhide|show(?:s|n|ing)?|visib\w*)\b/.test(query))
1187
1311
  return true;
1312
+ // Sidebar page names with a hide verb and a personal scope ("hide Unibox and
1313
+ // Workflows only for me") name the sidebar, not those pages' data; both
1314
+ // personal-sidebar acceptance runs (2026-10-02) found this routed to Messages.
1315
+ if (/\b(unibox|workflows|publishing|sequencer|tables|crm|analytics|knowledge)\b/.test(query)
1316
+ && /\b(hid(?:e|es|den|ing)|unhide)\b/.test(query)
1317
+ && /\b(for me|only me|just me|myself|my own|personally)\b/.test(query)
1318
+ && !/\b(messages?|emails?|threads?|conversations?|repl(?:y|ies)|contacts?|leads?|runs?)\b/.test(query))
1319
+ return true;
1188
1320
  // Content and navigation within a primitive keep that primitive's owner.
1189
1321
  // Check this before generic "customize pages" or a navigation noun.
1190
1322
  if (/\b(wiki|knowledge|linkedin|compan(?:y|ies)|tables?|landing|web ?pages?|websites?|carousels?|pdfs?|paginat\w*|next page|per page|page size|results?|dashboards?)\b/.test(query))
@@ -1246,16 +1378,17 @@ function tableReadRecommendation(query) {
1246
1378
  if (/\b(filtered?|filters?|sorts?|search|crm)\b/.test(query) && /\b(grid|counts?|registry)\b/.test(query))
1247
1379
  return {
1248
1380
  tools: ["oxygen_table_rows_page", "oxygen_tables_query", "oxygen_tables_preview"], commands: ["tables rows page", "tables query", "tables preview"],
1249
- execution: "Read the native rows page for grid filters/sorts/search and count modes. exact counts the matching rows; estimate uses reconciled registry metadata only for an unfiltered scope, otherwise counts exactly. registry never counts and returns no total for a narrowed or unavailable registry scope; skip returns no total. Follow nextCursor with the same table, filters, search, sort and CRM scope; do not substitute an offset. Canonical tables query reads raw values; format=grid refuses those filters/projections.",
1381
+ execution: "Use tables rows page for filtered/sorted/searchable grid cells and summarized run state, with CRM scope and count modes. exact counts matches; estimate uses reconciled registry only when unnarrowed, else exact. registry never counts and returns null for narrowed/unreconciled scope; skip returns no total (null). Cursor walks retain the same read scope; offset supports random access. Raw tables query supports its declared selectors; query format=grid refuses them.",
1250
1382
  };
1251
- if (/\b(preview|fill rates?|without row|summary only|summary_only)\b/.test(query))
1383
+ if (/\b(fill rates?|without row|summary only|summary_only)\b/.test(query)
1384
+ || (/\bpreview\b/.test(query) && !/\bgrid\b/.test(query)))
1252
1385
  return {
1253
1386
  tools: ["oxygen_tables_preview", "oxygen_tables_query", "oxygen_table_fill_progress"], commands: ["tables preview", "tables query", "tables fill-progress"],
1254
- execution: "Use tables preview for compact rows, optional include_cell_states and whole-table fill stats; summary_only omits row JSON. rowCount is exact, rates may be sampled and summary.statsSampled labels that scope. Follow its nextCursor with the same options. tables query reads raw values or format=grid previews/state. tables fill-progress separately reports ingestion progress/failure; none of these runs columns.",
1387
+ execution: "Use tables preview for projected rows and whole-table fill stats; include_cell_states adds compact state and summary_only omits row JSON. rowCount is exact; summary.statsSampled labels sampled rates. Its cursor pages the MCP widget with a minimal plain receipt, not preview row/stat JSON. For data paging use tables query format=grid or tables rows page; fill-progress reads ingestion state. These reads do not run columns.",
1255
1388
  };
1256
1389
  return {
1257
1390
  tools: ["oxygen_tables_query", "oxygen_tables_preview", "oxygen_table_rows_page", "oxygen_table_fill_progress"], commands: ["tables query", "tables preview", "tables rows page", "tables fill-progress"],
1258
- execution: 'Read tables query raw values, or format="grid" for summarized cell previews and per-cell state. Grid defaults to 50 rows and caps at 200; pass grid.nextCursor as cursor with the same table/limit. Grid refuses row_id, fields, filters/filterTree, search, sorts, formula_values and nonzero offset; it is not a snapshot. Its first page uses the reconciled registry count when available, otherwise exact; continuation skips counting (null total), labeled rowCountMode. include_stats adds optional whole-table column stats/count; unavailable stats are omitted. include_fill only adds optional fill state when the first page is empty; later/nonempty pages omit it. tables preview supplies fill-rate summaries; tables fill-progress reads ingestion status. These reads do not execute columns.',
1391
+ execution: "Use tables query for raw values, format=\"grid\" for cell previews and summarized run state, or tables preview for fill rates. Grid HTTP/CLI defaults 50 and caps 200; MCP defaults 50 and clamps supplied limits to 100. Follow grid.nextCursor with the same read scope; hasMore=false and nextCursor=null end the walk, without a snapshot. Grid refuses row_id, fields, filters/filterTree, search, sorts, formula_values and nonzero offset; system/widget flags do not change it. Without stats, first-page rowCountMode is registry if reconciled, else exact; cursor pages are skipped (rowCount=null). include_stats can replace rowCount with an exact whole-table count even on continuation; column rates may be sampled and unavailable stats are omitted. include_fill adds ingestion state only to an empty first page, distinct from fill rates. The native max_chars window can shorten a whole-row prefix or refuse an oversized first row/metadata. These reads do not run columns.",
1259
1392
  };
1260
1393
  }
1261
1394
  /** Native database work outranks generic workspace access and credit billing. */
@@ -1332,9 +1465,65 @@ function tableMaintenanceRecommendation(query) {
1332
1465
  }
1333
1466
  return null;
1334
1467
  }
1468
+ /** Stored conversation drafts are Messages data, rather than worker queue health. */
1469
+ function storedReplyDraftReadRecommendation(query) {
1470
+ if (!/\bdrafts?\b/.test(query)
1471
+ || !/\b(read|inspect|list|show|stored|retained|queue|history|historical)\b/.test(query)
1472
+ || !/\b(conversations?|threads?|inbox|unibox|repl(?:y|ies)|whatsapp|email|linkedin)\b/.test(query)
1473
+ || /\b(workers?|jobs?|workflows?|cron|repair|publishing|posts?|creators?|ugc|sequences?|campaigns?)\b/.test(query)
1474
+ || /\b(approve|approving|send|sending|edit|editing|reject|rejecting|generate|generating|create|creating)\b/.test(query))
1475
+ return null;
1476
+ const whatsapp = /\bwhatsapp\b/.test(query);
1477
+ const otherChannel = /\b(email|linkedin)\b/.test(query);
1478
+ const tools = whatsapp && !otherChannel ? ["oxygen_whatsapp_drafts_list"]
1479
+ : otherChannel && !whatsapp ? ["oxygen_inbox_drafts_list"]
1480
+ : ["oxygen_inbox_drafts_list", "oxygen_whatsapp_drafts_list"];
1481
+ const commands = tools.map(tool => tool === "oxygen_whatsapp_drafts_list" ? "whatsapp drafts list" : "inbox drafts list");
1482
+ return { tools, commands, execution: "Hydrate the selected native draft-read schema. External MCP clients use oxygen_capabilities_read for an unlisted target; built-in agents invoke the native capability. oxygen_inbox_drafts_list covers email/LinkedIn; oxygen_whatsapp_drafts_list covers WhatsApp. When the channel is unknown, inspect both schemas before selecting. Filter by the native conversation and CSV status controls; personal composer drafts stay private. Historical states are current draft statuses, not a transition audit. Generation, edits, approval and sending are separate actions." };
1483
+ }
1484
+ /** Saved contact facts belong to Records, regardless of email/phone channel words. */
1485
+ function storedCrmContactReadRecommendation(query) {
1486
+ if (!/\b(crm|records?)\b/.test(query)
1487
+ || !/\b(read|show|get|find|lookup|look up|retrieve|inspect|view|describe)\b/.test(query)
1488
+ || !/\b(emails?|phones?|mobile|contact details|contact information)\b/.test(query)
1489
+ || /\b(send\w*|repl(?:y|ies)|messages?|inbox|threads?|drafts?|posts?|comments?|outreach|sequences?|campaigns?|dial\w*|call|create|add|edit|update|change|delete|remove|merge|import|sync|missing|new|enrich\w*|providers?|waterfall|verify|verification|validate|tables?|rows?|columns?|spreadsheet|dataset|workflows?|schedule\w*|recurring|cron)\b/.test(query))
1490
+ return null;
1491
+ return {
1492
+ tools: ["oxygen_crm_record_search", "oxygen_crm_record_get", "oxygen_crm_object_describe"],
1493
+ commands: ["crm search", "crm get", "crm describe"],
1494
+ execution: "Read saved Records; no lookup or send. Hydrate search/get schemas; mode=suggest finds partial names, records resolves identities. Get takes object + row_id; describe the object or one attribute for unknown field keys. External MCP uses oxygen_capabilities_read for these unlisted reads; built-in agents invoke the native tools. Missing contact enrichment and CRM writes are separate actions.",
1495
+ };
1496
+ }
1497
+ /** Finding contact details is a native lookup; channel words alone do not mean sending. */
1498
+ function oneShotContactFindingRecommendation(query) {
1499
+ if (/\b(send\w*|repl(?:y|ies)|messages?|inbox|threads?|drafts?|posts?|comments?|engagement|audiences?|followers?|outreach|sequences?|campaigns?|dial\w*|call|crm|records?|stored|saved|verify|verification|validate|deliverability|mailboxes?|warmup|accounts?|limits?|tables?|rows?|columns?|spreadsheet|dataset|enrich\w*|bulk|batch|workflows?|schedule\w*|recurring|weekly|daily|every|cron|workers?|jobs?|providers?|catalog|schemas?|metadata|pricing|personal|private|home|scrape\w*|public)\b/.test(query)
1500
+ || /\bfind companies\b/.test(query))
1501
+ return null;
1502
+ const verb = /\b(find|lookup|look up|get|locate|obtain)\b/.exec(query);
1503
+ if (!verb)
1504
+ return null;
1505
+ const target = query.slice(verb.index + verb[0].length, verb.index + verb[0].length + 120);
1506
+ const email = /\bemails?\b/.test(target);
1507
+ const phone = /\bphones?\b|\bmobile numbers?\b/.test(target);
1508
+ if (!email && !phone)
1509
+ return null;
1510
+ return {
1511
+ tools: [...(email ? ["oxygen_find_email"] : []), ...(phone ? ["oxygen_find_phone"] : []), "oxygen_tools_search"],
1512
+ commands: [...(email ? ["find email"] : []), ...(phone ? ["find phone"] : []), "tools search"],
1513
+ execution: "Hydrate the selected native one-shot lookup schema. mode=dry_run previews its waterfall; live requires the user's approved exact scope and positive max_credits. readOnlyHint=false and openWorldHint=true still apply to previews. External MCP uses oxygen_capabilities_call for an unlisted lookup; built-in agents call the native capability under their existing policy. Free catalog metadata uses the read gateway. Sending and bulk Table enrichment have separate owners.",
1514
+ };
1515
+ }
1335
1516
  function explicitCapabilityIntent(query) {
1517
+ if (sentLinkedInInvitationReadRecommendation(query))
1518
+ return ROUTE_BY_ID.get("connected-linkedin") ?? null;
1336
1519
  if (databaseRecommendation(query))
1337
1520
  return ROUTE_BY_ID.get("platform-operations") ?? null;
1521
+ if (oneShotContactFindingRecommendation(query))
1522
+ return ROUTE_BY_ID.get("sourcing-and-provider-tools") ?? null;
1523
+ if (storedCrmContactReadRecommendation(query))
1524
+ return ROUTE_BY_ID.get("records") ?? null;
1525
+ if (storedReplyDraftReadRecommendation(query))
1526
+ return ROUTE_BY_PRIMITIVE.get("messages") ?? null;
1338
1527
  if (isConnectedAppsIntent(query) || isConnectedAppEventsIntent(query) || isConnectedAppSpendIntent(query)) {
1339
1528
  return ROUTE_BY_ID.get("workspace-access") ?? null;
1340
1529
  }
@@ -1503,6 +1692,8 @@ function explicitCapabilityIntent(query) {
1503
1692
  // and the "label" wording fell to the Tags card below.
1504
1693
  if (isRowClassificationIntent(query))
1505
1694
  return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
1695
+ if (isRowFanOutIntent(query))
1696
+ return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
1506
1697
  if (isTableMaintenanceIntent(query))
1507
1698
  return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
1508
1699
  if (/\b(tag|tags|label|labels)\b/.test(query))
@@ -1623,12 +1814,27 @@ function isPublishingPostIntent(query) {
1623
1814
  && !(/\b(find|search|scrape|research)\b/.test(query) && /\b(about|mention|containing)\b/.test(query));
1624
1815
  }
1625
1816
  function recommendationsFor(card, query) {
1817
+ const contactFind = card.id === "sourcing-and-provider-tools" ? oneShotContactFindingRecommendation(query) : null;
1818
+ if (contactFind)
1819
+ return contactFind;
1820
+ const crmContactRead = card.id === "records" ? storedCrmContactReadRecommendation(query) : null;
1821
+ if (crmContactRead)
1822
+ return crmContactRead;
1823
+ const replyDraftRead = card.primitive === "messages" ? storedReplyDraftReadRecommendation(query) : null;
1824
+ if (replyDraftRead)
1825
+ return replyDraftRead;
1826
+ const sent = card.id === "connected-linkedin" ? sentLinkedInInvitationReadRecommendation(query) : null;
1827
+ if (sent)
1828
+ return sent;
1626
1829
  const database = card.id === "platform-operations" ? databaseRecommendation(query) : null;
1627
1830
  if (database)
1628
1831
  return database;
1629
1832
  if (card.id === "billing-and-budgets" && /\b(credit prices|credit price catalog|customer credit)\b/.test(query)) {
1630
1833
  return { tools: ["oxygen_customer_credit_prices"], commands: ["pricing customer-credit-prices"] };
1631
1834
  }
1835
+ if (card.id === "billing-and-budgets" && /\b(plan|plans|annual|yearly|subscription)\b/.test(query)) {
1836
+ return { tools: ["oxygen_billing_balance", "oxygen_limits_show", "oxygen_billing_change_plan"], commands: ["billing balance", "limits show", "billing change"] };
1837
+ }
1632
1838
  if (card.id === "messages" && /\b(unibox|inbox)\b/.test(query) && /\b(metadata|taxonomy|filters|accounts|tags)\b/.test(query)) {
1633
1839
  return { tools: ["oxygen_unibox_metadata"], commands: ["unibox metadata"] };
1634
1840
  }
@@ -1805,12 +2011,15 @@ function recommendationsFor(card, query) {
1805
2011
  "oxygen_mailboxes_compatibility",
1806
2012
  "oxygen_mailboxes_import",
1807
2013
  "oxygen_mailboxes_connect_oauth",
2014
+ "oxygen_mailboxes_sync_status",
2015
+ "oxygen_mailboxes_sync",
1808
2016
  "oxygen_mailboxes_oauth_health",
1809
2017
  ],
1810
2018
  commands: [
1811
2019
  "mailboxes compatibility",
1812
2020
  "mailboxes import",
1813
2021
  "mailboxes connect-oauth",
2022
+ "mailboxes sync",
1814
2023
  "mailboxes oauth-health",
1815
2024
  ],
1816
2025
  };
@@ -1868,8 +2077,8 @@ function recommendationsFor(card, query) {
1868
2077
  if (isTableCapacityReadIntent(query))
1869
2078
  return {
1870
2079
  tools: ["oxygen_limits_show", "oxygen_tables_query", "oxygen_tables_describe"],
1871
- commands: ["limits show", "tables query", "tables describe"],
1872
- exactMcpCall: { tool: "oxygen_limits_show", arguments: {} },
2080
+ commands: ["limits show --section capacity", "tables query", "tables describe"],
2081
+ exactMcpCall: { tool: "oxygen_limits_show", arguments: { section: "capacity" } },
1873
2082
  };
1874
2083
  if (isFunctionIntent(query)) {
1875
2084
  if (/\b(?:list|show|read|see|my)\b.{0,30}\bfavou?rites?\b/.test(query))
@@ -1948,6 +2157,15 @@ function recommendationsFor(card, query) {
1948
2157
  commands: ["tables watcher preview", "tables watcher create", "tables watcher get", "tables watcher update", "tables watcher pause", "tables watcher resume"],
1949
2158
  };
1950
2159
  }
2160
+ if (isRowFanOutIntent(query)) {
2161
+ // `tables send --flatten` writes one row per array element (free, durable).
2162
+ return {
2163
+ tools: ["oxygen_tables_send"],
2164
+ commands: ["tables send"],
2165
+ exactCommand: ROW_FAN_OUT_COMMAND,
2166
+ exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.rowFanOut(),
2167
+ };
2168
+ }
1951
2169
  if (isRowClassificationIntent(query)) {
1952
2170
  // The classification list (free) names the template production has and,
1953
2171
  // where the deployment runs it, the native Classify column; any other
@@ -2177,6 +2395,16 @@ function isEmailVerificationIntent(query) {
2177
2395
  * column through `columns add`, which the route recommends next.
2178
2396
  */
2179
2397
  export const ROW_CLASSIFICATION_COMMAND = "oxygen columns catalog --family classification";
2398
+ export const ROW_FAN_OUT_COMMAND = "oxygen tables send <source> <target> --flatten <column> --flatten-path <path> --mapping '[{\"target\":\"<column>\",\"source\":{\"type\":\"column\",\"key\":\"$element\"},\"path\":\"<field>\"}]' --dry-run";
2399
+ // "flatten the search results into one row per company", "explode this JSON array
2400
+ // into rows". Both MCP acceptance agents for OXP-1.39.2 (S26) reported no native way
2401
+ // to turn a search tool column's result array into rows, and two capability searches
2402
+ // never surfaced `tables send`, which does exactly that with --flatten.
2403
+ function isRowFanOutIntent(query) {
2404
+ return /\b(flatten|explode|unnest|fan[ -]?out)\b.{0,60}\b(rows?|arrays?|lists?|json|results?|cells?)\b/.test(query)
2405
+ || /\b(arrays?|lists?|results?)\b.{0,40}\binto\b.{0,20}\bone row (?:per|for each)\b/.test(query)
2406
+ || /\b(json array|array cell|list cell)\b.{0,40}\brows?\b/.test(query);
2407
+ }
2180
2408
  /** Nouns another primitive owns: a classify ask that names one keeps that owner. */
2181
2409
  const ROW_CLASSIFICATION_OTHER_OWNER = /\b(inbox|repl(?:y|ies)|conversations?|threads?|messages?|dms?|e-?mails?|bounces?|signals?|visitors?|workflows?|deals?|crm|records?|icp|knowledge|wiki|pages?|ads|dashboards?|metrics?|agents?|runs)\b/;
2182
2410
  /** What one table row is: the noun a per-row classification is about. */
@@ -2262,12 +2490,17 @@ function isLinkedInAccountConnectIntent(query) {
2262
2490
  return false;
2263
2491
  if (/^(linkedin|unipile)( accounts?)?$/.test(query))
2264
2492
  return true;
2265
- if (/\b(send|sending|enroll|sequence|campaign|cadence|messages?|dms?|invites?|invitations?|posts?|profiles?|network|connections|followers|viewers|limits|hours|ads?|scrape|research|company|companies|url)\b/.test(query))
2493
+ if (/\b(send|sending|enroll|sequence|campaign|cadence|messages?|dms?|invites?|invitations?|posts?|profiles?|network|connections|followers|viewers|limits|hours|ads?|scrape|research|company|companies|url|pac(?:e|ing)|spacing|spaced|spread|apart|throttl\w*)\b/.test(query))
2266
2494
  return false;
2267
2495
  return /\b(connect|reconnect|link|add|accounts?|senders?|seats?|free|plan)\b/.test(query);
2268
2496
  }
2497
+ /**
2498
+ * The connected account's hours, caps or pacing (`senders limits`). The
2499
+ * 2026-10-02 pacing acceptance asked "linkedin sender pacing connection requests
2500
+ * spread over day" and was sent to `senders connect` by the word "sender".
2501
+ */
2269
2502
  function isLinkedInAccountLimitsIntent(query) {
2270
- return /\b(working hours|sending hours|business hours|office hours|weekdays?|time ?zone|linkedin limits|daily limits?)\b/.test(query);
2503
+ return /\b(working hours|sending hours|business hours|office hours|weekdays?|time ?zone|linkedin limits|daily limits?|pac(?:e|ing)|spacing|spaced|spread|apart|throttl\w*)\b/.test(query);
2271
2504
  }
2272
2505
  function isMutualLinkedInConnectionsIntent(query) {
2273
2506
  const linkedInContext = /\blinkedin\b|\bprofiles?\b/.test(query);
@@ -0,0 +1,6 @@
1
+ import { type Infer } from "./schema.js";
2
+ /** The credential selects the workspace; this optional ref only checks it. */
3
+ export declare const BILLING_OWNER_READ_CONTRACT: import("./schema.js").ContractObject<{
4
+ readonly organization: import("./schema.js").StringProperty;
5
+ }, never>;
6
+ export type BillingOwnerReadInput = Infer<typeof BILLING_OWNER_READ_CONTRACT>;
@@ -0,0 +1,7 @@
1
+ import { obj, str } from "./schema.js";
2
+ /** The credential selects the workspace; this optional ref only checks it. */
3
+ export const BILLING_OWNER_READ_CONTRACT = obj({
4
+ organization: str({
5
+ description: "Optional assertion of the current workspace's id, Clerk org id or slug. A mismatch fails before reading billing; this never selects another workspace.",
6
+ }),
7
+ });
@@ -1,5 +1,6 @@
1
1
  /** Browser resource operations, using the same scoped handlers on every surface. */
2
2
  import { arr, bool, enumOf, int, nullable, num, obj, str } from "./schema.js";
3
+ import { TABLE_ROW_READ_WINDOW_FIELDS } from "./table-row-read.js";
3
4
  const text = (description) => str(description ? { description } : {});
4
5
  const json = { type: "object", additionalProperties: true };
5
6
  const strings = () => arr(str());
@@ -66,7 +67,7 @@ export const BROWSER_OPERATIONS = [
66
67
  operation("tables.resource.action_columns", `${table}/action-columns`, "POST", "columns provider-action", "oxygen_table_provider_action_column", "tables", "Preview or create a provider action column; creating it does not execute provider actions.", { provider: text(), label: text(), key: text(), operations: strings(), dry_run: bool() }, ["provider"]),
67
68
  operation("tables.resource.rows.page", `${table}/rows`, "GET", "tables rows page", "oxygen_table_rows_page", "tables", "Read a Table row page with the same filter, sort, counts and CRM relationship projection as the grid.", { limit: int({ minimum: 1 }), cursor: text(), offset: int({ minimum: 0 }), f: text("Serialized filter tree."), s: text("Serialized sort state."), q: text(), count: enumOf(["exact", "estimate", "registry", "skip"]), crm_object: text() }),
68
69
  operation("tables.resource.rows.insert", `${table}/rows`, "POST", "tables rows insert", "oxygen_table_rows_insert", "tables", "Insert Table row values and return the committed rows plus any existing automatic run or dedupe grant.", { rows: objects(), values: json }),
69
- operation("tables.resource.rows.get", `${table}/rows/{row}`, "GET", "tables rows inspect", "oxygen_table_row_inspect", "tables", "Read one Table row; hydrate_relations=1 includes CRM relationship display values. The optional field list narrows cell hydration to exact column keys.", { hydrate_relations: enumOf(["1"]), field: strings() }, [], { repeatedQueryFields: ["field"] }),
70
+ operation("tables.resource.rows.get", `${table}/rows/{row}`, "GET", "tables rows inspect", "oxygen_table_row_inspect", "tables", "Read one row; field selects column keys and hydrate_relations=1 includes relations. projection=value returns lossless JSON fragments; resume value_cursor with identical table/row/field/path.", { hydrate_relations: enumOf(["1"]), field: strings(), ...TABLE_ROW_READ_WINDOW_FIELDS }, [], { repeatedQueryFields: ["field"] }),
70
71
  operation("tables.resource.rows.edit", `${table}/rows/{row}`, "PATCH", "tables rows edit", "oxygen_table_row_edit", "tables", "Patch one row's values and return the committed row.", { values: json }, ["values"]),
71
72
  operation("tables.resource.rows.delete", `${table}/rows/{row}`, "DELETE", "tables rows remove", "oxygen_table_row_remove", "tables", "Remove one row from a workspace Table."),
72
73
  operation("tables.resource.rows.bulk_delete", `${table}/rows/delete`, "POST", "tables rows remove-selection", "oxygen_table_rows_remove_selection", "tables", "Remove exact row ids or the same all-matching filtered selection used by the browser.", { row_ids: strings(), selection }),
@@ -89,7 +90,8 @@ export const BROWSER_OPERATIONS = [
89
90
  operation("tables.resource.ingestions.retry", `${table}/ingestions/{ingestion_run}/retry-failed`, "POST", "tables ingestion retry", "oxygen_table_resource_ingestion_retry", "runs", "Retry failed items in this Table's hosted ingestion run."),
90
91
  operation("tables.resource.import", "/api/cli/tables/import", "POST", "tables upload", "oxygen_table_resource_upload", "tables", "Preview or import small uploaded CSV, JSON or XLSX bytes through the exact browser import contract. Preview writes no rows.", upload, ["content_base64", "file_name"], { multipart }),
91
92
  operation("billing.activate", "/api/cli/billing/activate", "POST", "billing activate", "oxygen_billing_activate", "billing", "Activate a trial subscription now and charge its saved payment method; administrator approval is required. An optional tier reprices the active trial in the same call.", { approved: bool(), tier: text() }, ["approved"], { spends: true }),
92
- operation("billing.checkout", "/api/cli/billing/checkout", "POST", "billing checkout", "oxygen_billing_checkout", "billing", "Open the same hosted plan checkout as the browser; the returned URL carries the payment approval step.", { plan: text(), interval: enumOf(["month", "year"]), currency: text(), trial: bool(), return_to: text(), source: text(), blocked: text() }, ["plan"]),
93
+ operation("billing.checkout", "/api/cli/billing/checkout", "POST", "billing checkout", "oxygen_billing_checkout", "billing", "Open the same hosted plan checkout as the browser; the returned URL carries the payment approval step.", { plan: text(), interval: enumOf(["month", "year"]), term: enumOf(["month", "year"]), terms_version: text(), currency: text(), trial: bool(), return_to: text(), source: text(), blocked: text() }, ["plan"]),
94
+ operation("billing.confirm", "/api/cli/billing/confirm", "POST", "billing confirm", "oxygen_billing_confirm", "billing", "Confirm an exact unexpired plan-change preview. approved accepts its payment, term and effective date; workspace administrator required.", { token: text(), approved: bool() }, ["token", "approved"], { spends: true }),
93
95
  operation("billing.portal", "/api/cli/billing/portal", "POST", "billing portal", "oxygen_billing_portal", "billing", "Create the workspace's authenticated Stripe billing-portal URL; requires workspace administrator authorization.", { returnUrl: text() }),
94
96
  operation("billing.ownership.get", "/api/cli/billing/ownership", "GET", "billing ownership get", "oxygen_billing_ownership_get", "billing", "Read the active workspace's shared billing owner, candidate owners and credit cap."),
95
97
  operation("billing.ownership.set", "/api/cli/billing/ownership", "POST", "billing ownership set", "oxygen_billing_ownership_set", "billing", "Set the billing owner and optional monthly credit cap using the browser's administrator and workspace eligibility checks.", { billing_owner_organization_id: text(), monthly_credit_cap: nullable(num({ minimum: 0 })) }, ["billing_owner_organization_id"]),
@@ -0,0 +1,11 @@
1
+ export declare const COPILOT_SESSION_STATUSES: readonly ["active", "completed", "cancelled", "failed"];
2
+ export declare const COPILOT_SESSIONS_LIST_CONTRACT: import("./schema.js").ContractObject<{
3
+ readonly status: import("./schema.js").StringProperty;
4
+ readonly limit: {
5
+ type: "integer";
6
+ description?: string;
7
+ minimum?: number;
8
+ maximum?: number;
9
+ "x-clamp"?: true;
10
+ };
11
+ }, never>;
@@ -0,0 +1,7 @@
1
+ /** Stored attended session-list selectors. Native HTTP trimming/clamping remains authoritative. */
2
+ import { int, obj, str } from "./schema.js";
3
+ export const COPILOT_SESSION_STATUSES = ["active", "completed", "cancelled", "failed"];
4
+ export const COPILOT_SESSIONS_LIST_CONTRACT = obj({
5
+ status: str({ description: `Session status: ${COPILOT_SESSION_STATUSES.join(", ")}. Separate from latest turn status; omit for all.` }),
6
+ limit: int({ minimum: 1, description: "Newest sessions first. Default 50; native maximum 200 (larger values clamp)." }),
7
+ });
@@ -0,0 +1,33 @@
1
+ export declare const CRM_FILE_BYTES_MAX = 2048;
2
+ export declare const CRM_FILE_BYTES_QUERY_CONTRACT: import("./schema.js").ContractObject<{
3
+ readonly offset: {
4
+ type: "integer";
5
+ description?: string;
6
+ minimum?: number;
7
+ maximum?: number;
8
+ "x-clamp"?: true;
9
+ };
10
+ readonly limit: {
11
+ type: "integer";
12
+ description?: string;
13
+ minimum?: number;
14
+ maximum?: number;
15
+ "x-clamp"?: true;
16
+ };
17
+ readonly sha256: import("./schema.js").StringProperty;
18
+ readonly max_chars: {
19
+ type: "integer";
20
+ description?: string;
21
+ minimum?: number;
22
+ maximum?: number;
23
+ "x-clamp"?: true;
24
+ };
25
+ }, never>;
26
+ /** A same-original resume address. Credentials and organizations never appear in it. */
27
+ export declare function crmFileBytesUri(input: {
28
+ attachmentId: string;
29
+ sha256: string;
30
+ offset: number;
31
+ limit: number;
32
+ maxChars: number;
33
+ }): string;
@@ -0,0 +1,12 @@
1
+ import { int, obj, str } from "./schema.js";
2
+ export const CRM_FILE_BYTES_MAX = 2048;
3
+ export const CRM_FILE_BYTES_QUERY_CONTRACT = obj({
4
+ offset: int({ minimum: 0, maximum: 100 * 1024 * 1024, description: "Byte offset; default 0. Resume at next_offset." }),
5
+ limit: int({ minimum: 1, maximum: CRM_FILE_BYTES_MAX, description: "Original bytes requested; default 2048. Context bounds may return fewer." }),
6
+ sha256: str({ minLength: 64, maxLength: 64, description: "Original SHA-256 from the previous chunk; required when resuming." }),
7
+ max_chars: int({ minimum: 1024, maximum: 40_000, description: "Complete serialized chunk window. Smaller native windows preserve exact continuation." }),
8
+ });
9
+ /** A same-original resume address. Credentials and organizations never appear in it. */
10
+ export function crmFileBytesUri(input) {
11
+ return `oxygen://crm/files/${encodeURIComponent(input.attachmentId)}/original/${input.sha256}/${input.offset}/${input.limit}?max_chars=${input.maxChars}`;
12
+ }