@oxygen-agent/cli 1.1011.974 → 1.1012.2

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.d.ts +3 -1
  3. package/dist/command-manifest.js +29 -0
  4. package/dist/crm-support-flags.d.ts +1 -0
  5. package/dist/crm-support-flags.js +5 -1
  6. package/dist/customer-operation-commands.js +30 -1
  7. package/dist/help.js +10 -2
  8. package/dist/http-client.d.ts +4 -0
  9. package/dist/http-client.js +21 -10
  10. package/dist/index.js +326 -111
  11. package/dist/sender-limits-flags.d.ts +7 -0
  12. package/dist/sender-limits-flags.js +13 -0
  13. package/dist/transcript.js +1 -1
  14. package/node_modules/@oxygen/formula/dist/expression.js +16 -0
  15. package/node_modules/@oxygen/shared/dist/attempt-provenance.d.ts +30 -0
  16. package/node_modules/@oxygen/shared/dist/attempt-provenance.js +98 -0
  17. package/node_modules/@oxygen/shared/dist/byok-connect.js +2 -0
  18. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -1
  19. package/node_modules/@oxygen/shared/dist/capability-discovery.js +172 -13
  20. package/node_modules/@oxygen/shared/dist/cli-result.js +6 -0
  21. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +19 -7
  22. package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.d.ts +7 -1
  23. package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.js +5 -1
  24. package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +14 -0
  25. package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +4 -0
  26. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +5 -5
  27. package/node_modules/@oxygen/shared/dist/contracts/geo.d.ts +2 -0
  28. package/node_modules/@oxygen/shared/dist/contracts/geo.js +138 -0
  29. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
  30. package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
  31. package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -0
  32. package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.d.ts +16 -0
  33. package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.js +27 -0
  34. package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +6 -0
  35. package/node_modules/@oxygen/shared/dist/cost-estimate.js +23 -8
  36. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +2 -1
  37. package/node_modules/@oxygen/shared/dist/external-effect-arming.d.ts +54 -0
  38. package/node_modules/@oxygen/shared/dist/external-effect-arming.js +90 -0
  39. package/node_modules/@oxygen/shared/dist/geo.d.ts +247 -0
  40. package/node_modules/@oxygen/shared/dist/geo.js +314 -0
  41. package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +2 -2
  42. package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +9 -0
  43. package/node_modules/@oxygen/shared/dist/linkedin-countries.js +1 -1
  44. package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +48 -0
  45. package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +94 -0
  46. package/node_modules/@oxygen/shared/dist/managed-email-branding.d.ts +15 -0
  47. package/node_modules/@oxygen/shared/dist/managed-email-branding.js +81 -0
  48. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +20 -24
  49. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +15 -16
  50. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +3 -3
  51. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +3 -3
  52. package/node_modules/@oxygen/shared/dist/product-doctrine.js +6 -5
  53. package/node_modules/@oxygen/shared/dist/provider-balance-signal.js +3 -2
  54. package/node_modules/@oxygen/shared/dist/recipes.d.ts +1 -1
  55. package/node_modules/@oxygen/shared/dist/recipes.js +1 -0
  56. package/node_modules/@oxygen/shared/dist/repricing.d.ts +4 -14
  57. package/node_modules/@oxygen/shared/dist/repricing.js +25 -64
  58. package/node_modules/@oxygen/shared/dist/research-depth.d.ts +11 -0
  59. package/node_modules/@oxygen/shared/dist/research-depth.js +7 -0
  60. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  61. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  62. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  63. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  64. package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.d.ts +88 -0
  65. package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.js +118 -0
  66. package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.d.ts +15 -0
  67. package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +33 -0
  68. package/node_modules/@oxygen/shared/package.json +15 -5
  69. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +9 -5
  70. package/package.json +1 -1
  71. package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.d.ts +0 -9
  72. package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.js +0 -33
@@ -4,6 +4,16 @@ import { RECIPE_PRIMITIVES } from "./recipes.js";
4
4
  // It is navigation, never execution: live schemas, prices, readiness, and
5
5
  // approval enforcement remain on the exact command/tool selected from here.
6
6
  export const OXYGEN_CAPABILITY_ROUTES = [
7
+ {
8
+ id: "geo", layer: "Data", primitive: "geo", posture: "mixed",
9
+ owns: "GEO / AEO AI-search tracking: buyer prompts, consumer answers, citations, competitive visibility, customer chat evidence and optimization experiments. Experimental staff/dev access.",
10
+ notFor: "Direct model API benchmarks, automatic public edits, autonomous publishing or personalized chat attribution.",
11
+ execution: "Staff in dev workspaces only. Preview consumer collection and cost, start within the $10 shared pilot cap, inspect answers and citations, then record reviewed changes and compare later samples. Reads never collect or spend.",
12
+ gatewayTools: ["oxygen_geo_projects_list", "oxygen_geo_engines_list", "oxygen_geo_runs_preview"],
13
+ gatewayCommands: ["geo projects list", "geo engines list", "geo runs preview"],
14
+ endpointSections: ["geo"], skills: ["oxygen-geo"],
15
+ intentTerms: ["GEO", "AEO", "answer engine optimization", "generative engine optimization", "AI search visibility", "AI visibility", "consumer AI answers", "answer engines", "brand visibility", "brand citations", "chatgpt mentions", "citation gaps"],
16
+ },
7
17
  {
8
18
  id: "visual-rendering",
9
19
  layer: "Action",
@@ -206,7 +216,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
206
216
  primitive: "records",
207
217
  owns: "Canonical companies, people, deals, identities, relationships, activities, notes, tasks (reminders), files, merges, and CRM sync state.",
208
218
  notFor: "Disposable sourcing, enrichment, or scoring work; use Tables before promotion.",
209
- execution: "Operate the hosted native CRM and preserve field/relationship provenance. Importing FROM a connected CRM lands here via `crm sync import <provider> --object <object>` (HubSpot contacts|companies, Attio people|companies, Pipedrive persons|organizations|deals), which pulls the whole object and has no list/segment filter. To import ONE HubSpot saved list or ONE Pipedrive saved filter into a new table the user can filter and enrich — optionally suppressing a do-not-contact list/filter first — use `tables hubspot import` or `tables pipedrive import` instead. Open work across the workspace is one read: `crm tasks list`.",
219
+ execution: "Operate the hosted native CRM and preserve field/relationship provenance. For an object inventory, `crm objects` supplies each backing Table; `tables preview <table> --fields <field-key> --summary-only` returns its exact total row count at 0 credits. Choose an existing column key from the object description for both counts and row lists (Companies: name; People: full_name). That total includes sample rows; native CRM reads mark seeded samples `_sample: true`; an absent marker means the record is not a seeded sample. Importing FROM a connected CRM lands here via `crm sync import <provider> --object <object>` (HubSpot contacts|companies, Attio people|companies, Pipedrive persons|organizations|deals), which pulls the whole object and has no list/segment filter. To import ONE HubSpot saved list or ONE Pipedrive saved filter into a new table the user can filter and enrich — optionally suppressing a do-not-contact list/filter first — use `tables hubspot import` or `tables pipedrive import` instead. Open work across the workspace is one read: `crm tasks list`.",
210
220
  posture: "workspace_write",
211
221
  gatewayTools: ["oxygen_crm_record_search", "oxygen_crm_record_get"],
212
222
  gatewayCommands: ["crm search", "crm get", "crm describe", "crm tasks list", "crm notes list"],
@@ -225,8 +235,8 @@ export const OXYGEN_CAPABILITY_ROUTES = [
225
235
  notFor: "Canonical CRM truth: use Records via oxygen_crm_objects_list, oxygen_crm_record_get or oxygen_crm_record_search. Not message cadence or an off-platform spreadsheet runtime.",
226
236
  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.",
227
237
  posture: "mixed",
228
- gatewayTools: ["oxygen_tables_create", "oxygen_tools_search", "oxygen_columns_add", "oxygen_columns_catalog", "oxygen_enrich_column_preview", "oxygen_tables_link_bulk", "oxygen_callables_manage"],
229
- gatewayCommands: ["tables create", "tools search", "columns add", "columns catalog", "enrich-column preview", "tables link", "functions list", "functions draft"],
238
+ gatewayTools: ["oxygen_tables_create", "oxygen_tools_search", "oxygen_columns_add", "oxygen_columns_catalog", "oxygen_enrich_column_preview", "oxygen_tables_link_bulk", "oxygen_callables_manage", "oxygen_tables_records_import"],
239
+ gatewayCommands: ["tables create", "tools search", "columns add", "columns catalog", "enrich-column preview", "tables link", "functions list", "functions draft", "tables records import"],
230
240
  skills: ["oxygen-tables", "oxygen-enrichment", "oxygen-table-tidy", "oxygen-diagnostics", "oxygen-clay-migration", "oxygen-signals"],
231
241
  endpointSections: ["action-columns", "callables", "functions", "columns", "company-enrichment", "enrich-column", "enrichment", "projects", "supabase", "table-action-items", "table-action-runs", "table-ingestion-runs", "tables"],
232
242
  // "link"/"join"/"connect"/"relate" route here for `tables link`. Added after a
@@ -686,6 +696,7 @@ export function inferPrimitiveRoute(query) {
686
696
  export const RETIRED_PRODUCT_SKILLS = Object.freeze(["oxygen-playbooks", "oxygen-visual-design", "oxygen-linkedin-marketing"]);
687
697
  /** Current hosted instruction audiences, shared by discovery and Agent snapshots. */
688
698
  export const OXYGEN_PRODUCT_SKILL_AUDIENCES = Object.freeze({
699
+ "oxygen-geo": "internal_dev",
689
700
  "oxygen-onboarding": "workspace",
690
701
  "oxygen-gtm-consultant": "workspace",
691
702
  "oxygen-gtm": "workspace",
@@ -721,7 +732,7 @@ export function canReadProductSkillInstructions(name, entitlements) {
721
732
  const audience = Object.hasOwn(OXYGEN_PRODUCT_SKILL_AUDIENCES, name)
722
733
  ? OXYGEN_PRODUCT_SKILL_AUDIENCES[name]
723
734
  : undefined;
724
- return audience === "workspace" || (audience === "agency_partner" && entitlements.agencyPartner);
735
+ return audience === "workspace" || (audience === "internal_dev" && entitlements.internalDev === true) || (audience === "agency_partner" && entitlements.agencyPartner);
725
736
  }
726
737
  /** Identify hosted instruction bodies by native tool lineage, never by user prose. */
727
738
  export function readProductSkillInstructionResult(capabilityName, result) {
@@ -1041,7 +1052,9 @@ function routeCardFor(card, query) {
1041
1052
  ...card, posture: "paid_read",
1042
1053
  execution: "Generate wording variations with the native Spintax owner; sending is separate. Hydrate oxygen_spintax_generate. External MCP uses oxygen_capabilities_call, including its explicit free mode=dry_run preview; built-in agents invoke the native capability under their current policy. Omitted mode is live and costs 3 credits. After preview, live requires approval of the exact text with approved=true and max_credits>=3. Failed generation refunds the reservation.",
1043
1054
  };
1044
- if (card.id === "knowledge-graph" && isWorkspaceVoiceIntent(query))
1055
+ if (card.id === "knowledge-graph" && isCanonicalCopyReadIntent(query))
1056
+ card = { ...card, posture: "read_only", execution: CANONICAL_COPY_READ_EXECUTION };
1057
+ else if (card.id === "knowledge-graph" && isWorkspaceVoiceIntent(query))
1045
1058
  card = { ...card, execution: WORKSPACE_VOICE_EXECUTION };
1046
1059
  if (card.primitive === "workflows" && isRecurringCompanyMonitoringIntent(query))
1047
1060
  card = {
@@ -1063,12 +1076,18 @@ function routeCardFor(card, query) {
1063
1076
  ...card,
1064
1077
  execution: "Create a workspace-private Table or inspect its schema and rows. Manual rows, column labels, visibility and member favorites cost 0 credits. Change only the display label with columns update (oxygen_columns_update); columns rename (oxygen_columns_rename) requires a new stable key. Favorite one table or folder with tables favorite (oxygen_tables_favorite), then read tables favorites (oxygen_tables_favorites). For tidy guidance, read oxygen-table-tidy with skills get (oxygen_skills_get); tables tidy-suggest (oxygen_tables_tidy_suggest) analyzes without changes. Read the owning schema, apply the requested scope, then describe or query the Table to verify it.",
1065
1078
  };
1079
+ const recordsCopy = card.primitive === "tables" ? recordsTableImportRecommendation(query) : null;
1080
+ if (recordsCopy)
1081
+ card = { ...card, posture: "workspace_write", execution: recordsCopy.execution };
1066
1082
  const maintenance = card.primitive === "tables" ? tableMaintenanceRecommendation(query) : null;
1067
1083
  if (maintenance)
1068
1084
  card = { ...card, execution: maintenance.execution };
1069
1085
  const read = card.primitive === "tables" ? tableReadRecommendation(query) : null;
1070
1086
  if (read)
1071
1087
  card = { ...card, execution: read.execution };
1088
+ const removal = card.primitive === "tables" ? tableRowRemovalRecommendation(query) : null;
1089
+ if (removal)
1090
+ card = { ...card, posture: "workspace_write", execution: removal.execution };
1072
1091
  const sent = card.id === "connected-linkedin" ? sentLinkedInInvitationReadRecommendation(query) : null;
1073
1092
  if (sent)
1074
1093
  card = { ...card, execution: sent.execution };
@@ -1162,6 +1181,23 @@ function isOwnLinkedInNetworkIntent(query) {
1162
1181
  * claimed first (isUgcVoiceIntent); phone voice (numbers, calls) never matches.
1163
1182
  * The blind baseline also searched "writing style" and "voice learn".
1164
1183
  */
1184
+ /**
1185
+ * Which voice, brand or positioning AI copy uses right now: a free read of the pinned
1186
+ * canonical doc. A blind operator searched "current canonical voice", got the wiki
1187
+ * search tools, and concluded no such read existed; "which voice does AI copy use"
1188
+ * read as "copy my voice" and was sent to paid synthesis (OXP-1.4.18).
1189
+ */
1190
+ function isCanonicalCopyReadIntent(query) {
1191
+ if (/\b(?:learn\w*|synthesi[sz]\w*|generat\w*|creat\w*|writ\w*|set|pin|unpin|chang\w*|edit\w*|updat\w*|approv\w*|propos\w*|import\w*|calls?|phone|numbers?|creators?|ugc)\b/.test(query))
1192
+ return false;
1193
+ const copyType = /\b(?:voice|brand|positioning)\b/;
1194
+ if (!copyType.test(query))
1195
+ return false;
1196
+ return /\b(?:canonical|pinned|current|active|live)\b/.test(query)
1197
+ || /\b(?:ai|generated) copy\b/.test(query)
1198
+ || /\bwhich\b.{0,20}\b(?:voice|brand|positioning)\b/.test(query);
1199
+ }
1200
+ const CANONICAL_COPY_READ_EXECUTION = "Read the canonical copy AI uses with `oxygen context canonical get --type <voice|brand|positioning> --json` (MCP: oxygen_context_canonical_get), a free read. present=false means none is pinned, and grounding_when_absent says what AI copy uses instead. Pinning one (knowledge page pin) files a proposal a human approves.";
1165
1201
  function isWorkspaceVoiceIntent(query) {
1166
1202
  if (/\b(?:calls?|calling|dial\w*|phone|softphone|voicemail|numbers?)\b/.test(query))
1167
1203
  return false;
@@ -1169,7 +1205,7 @@ function isWorkspaceVoiceIntent(query) {
1169
1205
  || /\b(?:learn|capture|mimic|copy|match)\w*\b.{0,30}\bvoice\b/.test(query)
1170
1206
  || /\bvoice\b.{0,30}\b(?:learn|capture|mimic|copy|match)\w*\b/.test(query);
1171
1207
  }
1172
- 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`.";
1208
+ 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 save the guide a preview showed, pass its body back with --approved --body (no new AI call) at 0 credits, because --approved alone synthesizes and charges again. Saving files a pin proposal: the guide becomes the canonical voice, which grounds message, reply, sequence and post drafts and AI columns, only once a human approves that proposal (oxygen knowledge proposals approve <id>; MCP oxygen_knowledge_proposal_decide). 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`. When the user already has the guide's text, no synthesis is needed: save it with `knowledge page upsert --type voice` and request the pin with `knowledge page pin` (MCP oxygen_knowledge_page_upsert with canonical: true), both 0 credits. Which voice AI copy uses now: `context canonical get --type voice` (MCP oxygen_context_canonical_get). To hard-cap a synthesis run, pass `--max-credits <n>` (MCP max_credits): a higher estimate is refused before the AI call, with nothing charged.";
1173
1209
  /** One schedule for every channel of a Sequence, the same as Launch's Sending schedule. */
1174
1210
  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";
1175
1211
  /** Launch's unsubscribe switch: the visible link and the one-click headers together. */
@@ -1183,6 +1219,7 @@ export const SEQUENCE_UNSUBSCRIBE_COMMAND = "oxygen sequences update <sequence>
1183
1219
  * Factories, so a caller can edit the object it gets.
1184
1220
  */
1185
1221
  export const CAPABILITY_ROUTE_MCP_CALLS = {
1222
+ connectionPrices: () => ({ tool: "oxygen_billing_commitments", arguments: { view: "connection-prices" } }),
1186
1223
  connectedAppsList: () => ({ tool: "oxygen_connector_list", arguments: {} }),
1187
1224
  connectedAppEvents: () => ({ tool: "oxygen_connector_events_get", arguments: { connection: "<app>" } }),
1188
1225
  workspacePagesList: () => ({ tool: "oxygen_settings_pages_get", arguments: {} }),
@@ -1242,6 +1279,13 @@ export function catalogueIntentMcpCall(intentId) {
1242
1279
  // A Sequence's sending schedule or send window. Workflow language still wins
1243
1280
  // ("a workflow that changes the send window nightly" is automation), and
1244
1281
  // "sending hours" stays with the connected account's working hours.
1282
+ // Where each enrolled lead stands: its next message time, what holds it, why it
1283
+ // stopped. Both 2026-10-06 collateral acceptance operators searched for this
1284
+ // ("why is a lead stuck in a LinkedIn campaign, next message time") and got only
1285
+ // create/enroll/start, never the read that answers it.
1286
+ function isSequenceLeadProgressIntent(query) {
1287
+ return /\b(?:next (?:message|step|send)|stuck|blocked|held|per[- ]lead (?:status|progress)|why (?:\w+ )?(?:stopped|failed))\b/.test(query);
1288
+ }
1245
1289
  function isSequenceScheduleIntent(query) {
1246
1290
  return /\bsend(?:ing)? (?:schedule|window)s?\b/.test(query)
1247
1291
  && !/\b(workflow|automation|automate|webhook|cron)\b/.test(query);
@@ -1454,6 +1498,18 @@ function isCompanySourcingIntent(query) {
1454
1498
  return /\b(companies|businesses|firms|company search|company sourcing)\b/.test(query)
1455
1499
  && /\b(search|plan|find|source|sourcing|discover|build|tam|prospect)\b/.test(query);
1456
1500
  }
1501
+ /** Scoped row deletion keeps creation and enrichment out of its discovery path. */
1502
+ function tableRowRemovalRecommendation(query) {
1503
+ if (!/\brows?\b/.test(query) || !/\b(delete|remove)\b/.test(query)
1504
+ || /\b(workflows?|agents?|campaigns?|sequences?|columns?|functions?|callables?|crm|records?|views?|dedup\w*|duplicates?)\b/.test(query)
1505
+ || isSecondPartyDecisionIntent(query))
1506
+ return null;
1507
+ return {
1508
+ tools: ["oxygen_table_rows_remove_selection", "oxygen_tables_query", "oxygen_table_rows_page"],
1509
+ commands: ["tables rows remove-selection", "tables query", "tables rows page"],
1510
+ execution: "Delete Table rows with tables rows remove-selection (oxygen_table_rows_remove_selection): the rows a readable filter tree matches (--filter-tree-json / filter_tree, the same tree tables query takes), exact row_ids, or an all_matching selection with search, sort, range and exclusions. Hydrate its schema first. Unreadable filter nodes and filters arrays are refused; unqualified all_matching deletes every row. This is permanent and costs 0 credits; to keep a restorable copy first, tables duplicate (oxygen_tables_duplicate, 0 credits). Read the matching rows before deletion and the survivors afterward; each removed row's values stay readable with rows history (oxygen_rows_history). Deleting rows does not stop campaign enrollment.",
1511
+ };
1512
+ }
1457
1513
  /** Reads of grid values/state are separate from executing or resolving actions. */
1458
1514
  function tableReadRecommendation(query) {
1459
1515
  if (!/\btables?\b/.test(query) || isFunctionIntent(query) || isSecondPartyDecisionIntent(query)
@@ -1612,6 +1668,14 @@ function crmSampleRecordRecommendation(query) {
1612
1668
  execution: "A new workspace's seeded companies, CEOs and deals read _sample: true (sample: true on suggest hits and crm pipeline deals); pipeline, dashboard and chart totals leave them out and say how many. Keep one as real only when the user says it is: crm keep <object> <record> takes a row id or an identity such as stripe.com. It is free and idempotent, has no undo, and keeps that record alone: a kept deal's company and contact stay samples until each is kept. MCP runs oxygen_crm_record_keep through oxygen_capabilities_call.",
1613
1669
  };
1614
1670
  }
1671
+ /** Inventory reads select existing Table fields and retain honest sample totals. */
1672
+ function crmInventoryRecommendation(query) {
1673
+ if (!/\b(crm|records?)\b/.test(query)
1674
+ || !/\b(count|how many|inventory|list|enumerate|browse)\b/.test(query)
1675
+ || /\b(enrich\w*|configur\w*|settings?|presets?|fields?|attributes?|identities|relationships?|tasks?|notes?|files?|sync|hubspot|attio|pipedrive|import|create|add|edit|update|delete|remove|merge|workflows?|agents?)\b/.test(query))
1676
+ return null;
1677
+ return { tools: ["oxygen_crm_objects_list", "oxygen_tables_preview", "oxygen_crm_record_get"], commands: ["crm objects", "tables preview", "crm get"] };
1678
+ }
1615
1679
  /** Saved contact facts belong to Records, regardless of email/phone channel words. */
1616
1680
  function storedCrmContactReadRecommendation(query) {
1617
1681
  if (!/\b(crm|records?)\b/.test(query)
@@ -1658,6 +1722,24 @@ function tableFeedRecommendation(rawQuery) {
1658
1722
  return { tools: ["oxygen_feeds_bind", "oxygen_feeds_list"], commands: ["feeds bind", "feeds list"], execution: "Bind the native Table row-supply feed. Inspect source, cadence and standing credit scope first; pull feeds are distinct from inbound webhook endpoints. Feed tools are executable through the core gateways or Signals pack." };
1659
1723
  return { tools: ["oxygen_feeds_list", "oxygen_feeds_get", "oxygen_feeds_update"], commands: ["feeds list", "feeds get", "feeds update"], execution: "Inspect the existing Table feed with feeds_get(include_route=true), then update it in place with oxygen_feeds_update (Signals pack or core write gateway). Preserve the complete frozen request; source replacement resets the cursor. Scheduled replacements require one executable lane; fallbacks require no cadence. Changed cadence, increased paid cap or replaced paid source can require renewed standing authorization. Configuration is free; future refreshes retain their approved cycle ceiling." };
1660
1724
  }
1725
+ /**
1726
+ * Records into a working Table (OXP-1.1.10): "turn my CRM view into a table
1727
+ * that stays current". External CRM imports keep their own lanes, and a Table
1728
+ * going INTO Records is promote, the other direction.
1729
+ */
1730
+ function recordsTableImportRecommendation(query) {
1731
+ if (!/\b(crm|records?)\b/.test(query)
1732
+ || !/\b(tables?|working copy|spreadsheet)\b/.test(query)
1733
+ || !/\b(import\w*|cop(?:y|ies)|turn\w*|pull\w*|bring|sync\w*|mirror\w*|into)\b/.test(query)
1734
+ || /\b(hubspot|attio|pipedrive|salesforce|csv|promot\w*|bind)\b/.test(query)
1735
+ || /\b(tables?|rows?)\b.{0,40}\b(?:into|to)\s+(?:the\s+|my\s+|our\s+)?(?:crm|records)\b/.test(query))
1736
+ return null;
1737
+ return {
1738
+ tools: ["oxygen_tables_records_import", "oxygen_tables_records_sync", "oxygen_tables_records_status"],
1739
+ commands: ["tables records import", "tables records sync", "tables records status"],
1740
+ execution: "Copy a Records object or saved view into a new Table with oxygen_tables_records_import (dry_run first counts records and lists columns); refresh=sync with approved=true keeps it current on a schedule. One-way: the copy is free and Records never change, rows key on record_id, cells edited in the Table are kept, and records that leave the view stay as rows with in_records=false. Sync now or change the schedule with oxygen_tables_records_sync; read the last sync with oxygen_tables_records_status. Each scheduled sync bills one workflow step.",
1741
+ };
1742
+ }
1661
1743
  function providerPolicyReadRecommendation(query) {
1662
1744
  if (!/\bproviders?\b/.test(query)
1663
1745
  || !/\b(limits?|request caps?|rate caps?|overrides?)\b/.test(query)
@@ -1681,7 +1763,7 @@ function spendBreakdownRecommendation(query) {
1681
1763
  return null;
1682
1764
  return {
1683
1765
  tools: ["oxygen_billing_audit", "oxygen_billing_usage"], commands: ["billing audit", "billing usage"],
1684
- execution: "Read where managed credits went with oxygen_billing_audit: spend_by_category ranks each area's credits and share of managed_credits_spent, largest first, over the whole window; group_by provider, tool, model, run or workspace narrows it. Spent is reserved minus released, so it includes open holds; oxygen_billing_usage lists the ledger and its managed_credits_used counts captured credits only. External MCP uses oxygen_capabilities_read; built-in agents invoke the native capability. Reads cost 0 credits.",
1766
+ execution: "Read where managed credits went with oxygen_billing_audit: spend_by_category ranks each area's credits and share of managed_credits_spent, largest first, over the whole window; group_by provider, tool, model, run or workspace narrows it. Spent is reserved minus released in the window; it differs from captured (oxygen_billing_usage's managed_credits_used) by holds still open and holds settling across the window edge, so do not attribute the whole gap to open holds. External MCP uses oxygen_capabilities_call; built-in agents invoke the native capability. Reads cost 0 credits.",
1685
1767
  };
1686
1768
  }
1687
1769
  function sendingBlockReadRecommendation(query) {
@@ -1708,6 +1790,11 @@ function isSpintaxIntent(query) {
1708
1790
  || /\b(paraphrase|rewrite|variations?)\b.{0,45}\b(wording|copy|message|draft|subject(?: line)?)\b/.test(query);
1709
1791
  }
1710
1792
  function explicitCapabilityIntent(query) {
1793
+ // Importing an AI answer is GEO evidence, rather than a Table file import.
1794
+ if (/\b(?:geo|aeo|ai (?:search )?visibility|consumer ai answers?|answer engine optimization|generative engine optimization)\b/.test(query))
1795
+ return ROUTE_BY_ID.get("geo") ?? null;
1796
+ if (isConnectionPriceIntent(query))
1797
+ return ROUTE_BY_ID.get("billing-and-budgets") ?? null;
1711
1798
  if (sentLinkedInInvitationReadRecommendation(query))
1712
1799
  return ROUTE_BY_ID.get("connected-linkedin") ?? null;
1713
1800
  if (isCrmEnrichmentConfigurationRead(query))
@@ -1726,6 +1813,8 @@ function explicitCapabilityIntent(query) {
1726
1813
  return ROUTE_BY_PRIMITIVE.get("messages") ?? null;
1727
1814
  if (tableFeedRecommendation(query))
1728
1815
  return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
1816
+ if (recordsTableImportRecommendation(query))
1817
+ return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
1729
1818
  if (providerPolicyReadRecommendation(query))
1730
1819
  return ROUTE_BY_ID.get("billing-and-budgets") ?? null;
1731
1820
  if (sendingBlockReadRecommendation(query))
@@ -1782,7 +1871,7 @@ function explicitCapabilityIntent(query) {
1782
1871
  if (isMailboxDeleteIntent(query) || isSendingDomainDeleteIntent(query)) {
1783
1872
  return ROUTE_BY_ID.get("sending-infrastructure") ?? null;
1784
1873
  }
1785
- if (isMailboxOnboardingIntent(query)) {
1874
+ if (isMailboxOnboardingIntent(query) || isWarmupEnrollmentIntent(query)) {
1786
1875
  return ROUTE_BY_ID.get("sending-infrastructure") ?? null;
1787
1876
  }
1788
1877
  if (isLinkedInAccountConnectIntent(query))
@@ -1914,9 +2003,12 @@ function explicitCapabilityIntent(query) {
1914
2003
  if (/\bwhatsapp\b.{0,30}\b(account|connect|limits?|sync)\b/.test(query)) {
1915
2004
  return ROUTE_BY_ID.get("connected-whatsapp") ?? null;
1916
2005
  }
2006
+ const hasSequenceProgramIntent = /\b(sequence|campaign|cadence|enroll|nurture)\b/.test(query)
2007
+ || /\b(day\s*\d+|days?\s+\d|multi[ -]step|rotate senders?|sender rotation|stop on reply|until (?:they )?reply)\b/.test(query);
1917
2008
  if (!isNetNewLinkedInInitiation(query)
1918
2009
  && (/\b(one[ -]off|ad[ -]hoc|just once|reply once|single message|direct message|direct email|no cadence)\b/.test(query)
1919
- || /\b(reply|respond|follow up)\b.{0,30}\b(conversation|thread|email|linkedin|whatsapp|dm)\b/.test(query))) {
2010
+ || (!hasSequenceProgramIntent
2011
+ && /\b(reply|respond|follow up)\b.{0,30}\b(conversation|thread|email|linkedin|whatsapp|dm)\b/.test(query)))) {
1920
2012
  return ROUTE_BY_PRIMITIVE.get("messages") ?? null;
1921
2013
  }
1922
2014
  if (/\b(my|our|own|connected)\b.{0,40}\blinkedin\b|\b(sales navigator|salesnav|recruiter|profile viewers?|linkedin inbox)\b/.test(query)) {
@@ -1925,8 +2017,7 @@ function explicitCapabilityIntent(query) {
1925
2017
  if (/\b(publish|schedule|approve)\b.{0,24}\b(post|content)\b|\bcontent calendar\b/.test(query)) {
1926
2018
  return ROUTE_BY_PRIMITIVE.get("publishing") ?? null;
1927
2019
  }
1928
- if (/\b(sequence|campaign|cadence|enroll|nurture)\b/.test(query)
1929
- || /\b(day\s*\d+|days?\s+\d|multi[ -]step|rotate senders?|sender rotation|stop on reply|until (?:they )?reply)\b/.test(query)) {
2020
+ if (hasSequenceProgramIntent) {
1930
2021
  return ROUTE_BY_PRIMITIVE.get("sequences") ?? null;
1931
2022
  }
1932
2023
  // Existing reply handling is Messages even without a channel name. Keep this
@@ -2027,12 +2118,21 @@ function recommendationsFor(card, query) {
2027
2118
  const contactFind = card.id === "sourcing-and-provider-tools" ? oneShotContactFindingRecommendation(query) : null;
2028
2119
  if (contactFind)
2029
2120
  return contactFind;
2121
+ if (card.id === "knowledge-graph" && isCanonicalCopyReadIntent(query)) {
2122
+ return {
2123
+ tools: ["oxygen_context_canonical_get", "oxygen_knowledge_page_get", "oxygen_knowledge_search"],
2124
+ commands: ["context canonical get", "knowledge page get", "knowledge search"],
2125
+ };
2126
+ }
2030
2127
  const crmSample = card.id === "records" ? crmSampleRecordRecommendation(query) : null;
2031
2128
  if (crmSample)
2032
2129
  return crmSample;
2033
2130
  const crmContactRead = card.id === "records" ? storedCrmContactReadRecommendation(query) : null;
2034
2131
  if (crmContactRead)
2035
2132
  return crmContactRead;
2133
+ const crmInventory = card.id === "records" ? crmInventoryRecommendation(query) : null;
2134
+ if (crmInventory)
2135
+ return crmInventory;
2036
2136
  const replyDraftRead = card.primitive === "messages" ? storedReplyDraftReadRecommendation(query) : null;
2037
2137
  if (replyDraftRead)
2038
2138
  return replyDraftRead;
@@ -2042,6 +2142,14 @@ function recommendationsFor(card, query) {
2042
2142
  const spendRead = card.id === "billing-and-budgets" ? spendBreakdownRecommendation(query) : null;
2043
2143
  if (spendRead)
2044
2144
  return spendRead;
2145
+ if (card.id === "billing-and-budgets" && isConnectionPriceIntent(query)) {
2146
+ return {
2147
+ tools: ["oxygen_billing_commitments"],
2148
+ commands: ["billing commitments"],
2149
+ exactCommand: "oxygen billing commitments --view connection-prices --json",
2150
+ exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.connectionPrices(),
2151
+ };
2152
+ }
2045
2153
  const blockRead = card.primitive === "sequences" ? sendingBlockReadRecommendation(query) : null;
2046
2154
  if (blockRead)
2047
2155
  return blockRead;
@@ -2059,6 +2167,9 @@ function recommendationsFor(card, query) {
2059
2167
  const feed = card.primitive === "tables" ? tableFeedRecommendation(query) : null;
2060
2168
  if (feed)
2061
2169
  return { tools: feed.tools, commands: feed.commands };
2170
+ const recordsCopy = card.primitive === "tables" ? recordsTableImportRecommendation(query) : null;
2171
+ if (recordsCopy)
2172
+ return { tools: recordsCopy.tools, commands: recordsCopy.commands };
2062
2173
  if (card.id === "billing-and-budgets" && /\b(credit prices|credit price catalog|customer credit)\b/.test(query)) {
2063
2174
  return { tools: ["oxygen_customer_credit_prices"], commands: ["pricing customer-credit-prices"] };
2064
2175
  }
@@ -2113,7 +2224,7 @@ function recommendationsFor(card, query) {
2113
2224
  if (card.primitive === "tables" && isCompanyPageFollowersIntent(query)) {
2114
2225
  return {
2115
2226
  tools: ["oxygen_tables_followers", "oxygen_tables_sources"],
2116
- commands: ["tables followers check", "tables followers order", "tables sources"],
2227
+ commands: ["tables followers check", "tables followers order", "tables followers status", "tables sources"],
2117
2228
  exactCommand: COMPANY_PAGE_FOLLOWERS_COMMAND,
2118
2229
  exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.companyPageFollowers(),
2119
2230
  };
@@ -2235,6 +2346,12 @@ function recommendationsFor(card, query) {
2235
2346
  commands: ["mailboxes ramp set", "mailboxes cap set", "mailboxes get"],
2236
2347
  };
2237
2348
  }
2349
+ if (card.id === "sending-infrastructure" && isWarmupEnrollmentIntent(query)) {
2350
+ return {
2351
+ tools: ["oxygen_mailboxes_warmup_enable", "oxygen_mailboxes_compatibility", "oxygen_mailboxes_list"],
2352
+ commands: ["mailboxes warmup enable", "mailboxes compatibility", "mailboxes list"],
2353
+ };
2354
+ }
2238
2355
  if (card.id === "sending-infrastructure" && isMailboxOnboardingIntent(query)) {
2239
2356
  return {
2240
2357
  tools: [
@@ -2298,6 +2415,9 @@ function recommendationsFor(card, query) {
2298
2415
  };
2299
2416
  }
2300
2417
  if (card.primitive === "tables") {
2418
+ const removal = tableRowRemovalRecommendation(query);
2419
+ if (removal)
2420
+ return removal;
2301
2421
  const read = tableReadRecommendation(query);
2302
2422
  if (read)
2303
2423
  return read;
@@ -2496,6 +2616,12 @@ function recommendationsFor(card, query) {
2496
2616
  commands: ["senders profiles list", "senders profiles get"],
2497
2617
  };
2498
2618
  }
2619
+ if (isSequenceLeadProgressIntent(query)) {
2620
+ return {
2621
+ tools: ["oxygen_sequences_enrollments", "oxygen_sequences_stats", "oxygen_sequences_list"],
2622
+ commands: ["sequences enrollments", "sequences stats", "sequences list"],
2623
+ };
2624
+ }
2499
2625
  if (isNetNewLinkedInInitiation(query)) {
2500
2626
  const publicResearch = isPublicLinkedInRead(query);
2501
2627
  const harvestEngagers = publicResearch && isPublicLinkedInEngagerHarvest(query);
@@ -2719,7 +2845,33 @@ function isSenderCopyVariableIntent(query) {
2719
2845
  || /\bsign off (?:variable|with|per sender)\b|\bper sender sign off\b/.test(query)
2720
2846
  || /\bsign (?:every|each|all) (?:emails?|messages?|dms?|notes?) with\b/.test(query);
2721
2847
  }
2722
- /** The account's own working hours or daily caps (`senders limits`), not its network. */
2848
+ /** OXP-43.36: inspect recurring account prices before creating authorization. */
2849
+ function isConnectionPriceIntent(query) {
2850
+ if (!/\b(costs?|prices?|credits?|reser(?:ve[sd]?|vations?)|charges?|charged|fees?|pay|quote)\b/.test(query))
2851
+ return false;
2852
+ if (/\b(per (?:message|send|action|call)|scrap\w*|research|enrich\w*|public (?:profiles?|posts?))\b/.test(query))
2853
+ return false;
2854
+ // A whole mailbox onboarding motion can mention price alongside importing
2855
+ // history and syncing CRM. Keep that owner unless the ask isolates a quote.
2856
+ if (/\b(import|sync)\b/.test(query) && /\b(history|crm|unibox)\b/.test(query)
2857
+ && !/\b(monthly|recurring|quote|before (?:consent|connect|authorization))\b/.test(query))
2858
+ return false;
2859
+ // Lookup, purchase and sending prices keep their own previews. Naming a
2860
+ // mailbox or a phone number alone does not make it an account connection.
2861
+ if (/\b(?:find|look ?up|lookup)\b/.test(query) && /\bphones?\b/.test(query))
2862
+ return false;
2863
+ if (/\b(?:warm ?up|deliverability|placement)\b/.test(query))
2864
+ return false;
2865
+ if (/\bmailbox(?:es)?\b/.test(query) && /\b(?:buy|purchase|order)\b/.test(query))
2866
+ return false;
2867
+ if (/\blinkedin\b/.test(query) && /\b(?:messages?|invites?|invitations?|connection requests?)\b/.test(query))
2868
+ return false;
2869
+ const accountContext = /\b(accounts?|connect(?:ing|ed|ion)?|reconnect|oauth|consent)\b/.test(query);
2870
+ return /\b(connected accounts?|account connections?|connection prices?)\b/.test(query)
2871
+ || (/\b(linkedin|whatsapp|twitter|x)\b/.test(query)
2872
+ && (accountContext || /\b(numbers?|seats?)\b/.test(query)))
2873
+ || (/\bmailbox(?:es)?\b/.test(query) && accountContext);
2874
+ }
2723
2875
  /**
2724
2876
  * Connecting, or asking what it takes to connect, the workspace's own LinkedIn
2725
2877
  * sending account. The 2026-09-26 repricing baseline asked "connect a LinkedIn
@@ -2750,6 +2902,10 @@ function isMutualLinkedInConnectionsIntent(query) {
2750
2902
  || /\b(connections?|relations?)\b.{0,48}\b(mutual|shared|in common)\b/.test(query);
2751
2903
  return linkedInContext && mutualContext;
2752
2904
  }
2905
+ function isWarmupEnrollmentIntent(query) {
2906
+ return /\b(warmup|warm-up|warm up)\b/.test(query)
2907
+ && /\b(enroll\w*|enable|quote|price|cost)\b/.test(query);
2908
+ }
2753
2909
  function isMailboxOnboardingIntent(query) {
2754
2910
  const operation = /\b(import|migrat\w*|onboard|register|upload|bring|connect existing|add existing)\b/.test(query);
2755
2911
  const mailboxScope = /\b(mailbox(?:es)?|inbox(?:es)?|sender accounts?|email accounts?|sending pool)\b/.test(query);
@@ -2824,6 +2980,9 @@ function isFailedPostIntent(query) {
2824
2980
  function isPostCoverOrPreviewIntent(query) {
2825
2981
  if (/\b(ads?|competitors?)\b/.test(query))
2826
2982
  return false;
2983
+ // A draft in an existing inbox conversation is a reply, not a post artifact.
2984
+ if (/\b(inbox|unibox|conversation)\b/.test(query) && !/\bposts?\b/.test(query))
2985
+ return false;
2827
2986
  return (/\b(cover|thumbnail)\b/.test(query) && /\b(post|video|draft)\b/.test(query))
2828
2987
  || /\bpreview\b.{0,40}\b(post|draft)\b/.test(query);
2829
2988
  }
@@ -79,6 +79,12 @@ export function exitCodeForErrorCode(code) {
79
79
  if (code.startsWith("invalid_")
80
80
  || code.startsWith("missing_")
81
81
  || code === "unknown_fields"
82
+ || code === "unknown_filter_column"
83
+ || code === "unknown_sort_column"
84
+ || code === "formula_filter_not_supported"
85
+ || code === "formula_sort_not_supported"
86
+ || code === "lookup_filter_not_supported"
87
+ || code === "lookup_sort_not_supported"
82
88
  || code === "validation_failed") {
83
89
  return 2;
84
90
  }
@@ -1,7 +1,19 @@
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
3
  import { TABLE_ROW_READ_WINDOW_FIELDS } from "./table-row-read.js";
4
+ import { TABLE_ROW_REMOVAL_FIELDS } from "./table-row-removal.js";
4
5
  const text = (description) => str(description ? { description } : {});
6
+ // The grid's own URL state (Sequence lead/contact grids reuse the Table grid): copying f / s from a
7
+ // filtered, sorted browser view is the supported way to build them.
8
+ const FILTER_PARAM = "Filter tree as the grid URL's f parameter (base64url JSON); copy it from a filtered browser view.";
9
+ const SORT_PARAM = "Sort list as the grid URL's s parameter (base64url JSON); copy it from a sorted browser view.";
10
+ // Sequence lead/contact rows: one row can carry ~100 KB of research and
11
+ // enrichment values, so these reads select columns and compact values (OXP-1.11.67).
12
+ const sequenceRowsProjection = {
13
+ fields: text("Comma-separated column keys to return, e.g. full_name,company,__seq_status; omit for every column."),
14
+ projection: enumOf(["grid", "values"], { description: "values: compact {id, values} rows, long values capped; grid (default): the full grid payload." }),
15
+ };
16
+ const SEQUENCE_ROWS_TRUNCATION_HINT = "Result trimmed for context size. Pass projection=values (values over 2,000 characters are capped) and fields=<column keys>; a smaller limit cannot shrink one row.";
5
17
  const json = { type: "object", additionalProperties: true };
6
18
  const strings = () => arr(str());
7
19
  const objects = () => arr(json);
@@ -15,12 +27,12 @@ const selection = obj({
15
27
  const actions = arr(obj({ type: enumOf(["tool_column"]), column: text(), definition: json }, { required: ["column"] }));
16
28
  const viewFields = { name: text(), view_type: text(), config: json, group_by: text(), is_default: bool(), position: int({ minimum: 0 }) };
17
29
  const crmLinks = arr(obj({ object: text(), row_id: text(), role: text() }, { required: ["object", "row_id"] }));
18
- function operation(id, route, method, cli, mcp, pack, summary, fields = {}, required = [], options = {}) {
30
+ function operation(id, route, method, cli, mcp, pack, summary, fields = {}, required = [], options = {}, annotations) {
19
31
  return {
20
32
  id, route, method, parameters: [...route.matchAll(/\{(\w+)\}/g)].map((match) => match[1]),
21
33
  contract: obj(fields, { required }),
22
34
  cli: { path: cli.split(" "), summary },
23
- mcp: { name: mcp, title: `Oxygen: ${cli}`, description: summary, pack },
35
+ mcp: { name: mcp, title: `Oxygen: ${cli}`, description: summary, pack, ...(annotations ? { annotations } : {}) },
24
36
  mutates: method !== "GET", spends: false, ...options,
25
37
  };
26
38
  }
@@ -33,17 +45,17 @@ const multipart = { contentField: "content_base64", nameField: "file_name", type
33
45
  const nativeCli = { generated: { cli: false, mcp: false } };
34
46
  export const BROWSER_OPERATIONS = [
35
47
  operation("tables.resource.export_csv", `${table}/export/csv`, "GET", "tables export-csv", "oxygen_table_export_csv", "tables", "Download every row in the exact filtered Table view as streamed CSV. CLI writes --output; MCP returns an authenticated download resource link. Unsupported live lookup filters fail before download; rows and bytes are not capped or truncated.", { f: text("Serialized filter tree."), s: text("Serialized sort state."), q: text(), fields: text("Comma-separated exact column keys.") }, [], { binary: { stream: true, resourceParameter: "table" } }),
36
- operation("billing.allowance.view", "/api/cli/billing/allowance/view", "GET", "billing allowance view", "oxygen_billing_allowance_view", "billing", "Read the complete current-cycle allowance view, including billing-owner display and partner-bonus eligibility."),
48
+ operation("billing.allowance.view", "/api/cli/billing/allowance/view", "GET", "billing allowance view", "oxygen_billing_allowance_view", "billing", "Inspect the current-cycle allowance, billing owner and partner-bonus eligibility; due plan grants or credit expiry may apply. 0 credits; reserves nothing.", {}, [], { mutates: true }),
37
49
  operation("billing.commitments.view", "/api/cli/billing/commitments/view", "GET", "billing commitments view", "oxygen_billing_commitments_view", "billing", "Read the complete fixed-credit commitment view and linked billing-owner display, without enumerating a child's sibling resources."),
38
50
  operation("billing.invoices.view", "/api/cli/billing/invoices/view", "GET", "billing invoices view", "oxygen_billing_invoices_view", "billing", "Read the browser invoice-history projection, including exact native timestamps, amounts and signed Stripe invoice links."),
39
51
  operation("billing.summary", "/api/cli/billing/summary", "GET", "billing summary", "oxygen_billing_summary", "billing", "Read the complete non-writing wallet, plan, seat-capacity and billing-owner summary."),
40
- operation("billing.usage.view", "/api/cli/billing/usage/view", "GET", "billing usage view", "oxygen_billing_usage_view", "billing", "Credit spend over 7, 30 or 90 days by category and provider, each with its share, plus permitted linked-workspace rollups. totalSpent includes open holds; billing usage managed_credits_used counts captured credits only. Other windows or groupings: billing audit.", { days: enumOf(["7", "30", "90"]), workspace: text() }),
52
+ operation("billing.usage.view", "/api/cli/billing/usage/view", "GET", "billing usage view", "oxygen_billing_usage_view", "billing", "Credit spend over 7, 30 or 90 days by category and provider, each with its share, plus permitted linked-workspace rollups. totalSpent is reserved minus released; billing usage managed_credits_used counts captured credits only. Other windows or groupings: billing audit.", { days: enumOf(["7", "30", "90"]), workspace: text() }),
41
53
  operation("integrations.managed_endpoints", "/api/cli/integrations/composio/endpoints", "GET", "integrations managed-endpoints", "oxygen_integration_managed_endpoints", "integrations", "Read a managed integration's full action catalog and parameter schemas.", { integration_id: text() }, ["integration_id"]),
42
54
  operation("integrations.openrouter_models", "/api/cli/integrations/openrouter/models", "GET", "integrations openrouter-models", "oxygen_openrouter_models", "integrations", "Read model and pricing metadata using this workspace's connected OpenRouter account; credentials are never returned."),
43
55
  operation("integrations.status", "/api/cli/integrations/status", "GET", "integrations status", "oxygen_integration_status", "integrations", "Read active connection counts, sender connection state and the workspace's usable BYOK plan entitlement."),
44
56
  operation("pricing.customer_credit_prices", "/api/cli/pricing/customer-credit-prices", "GET", "pricing customer-credit-prices", "oxygen_customer_credit_prices", "workspace", "Read the customer credit-price catalog, quantity decisions and price provenance used by the Table picker."),
45
- operation("sequences.contacts.rows", "/api/cli/sequences/{sequence}/contacts/rows", "GET", "sequences contacts rows", "oxygen_sequence_contacts_rows", "sequences", "Read the full contact grid projection with exact contact-state filter and native synthetic status columns.", { contact_state: enumOf(["all", "not_contacted", "contacted", "connected", "replied", "positive_reply"]), limit: int({ minimum: 1, maximum: 2000 }), cursor: text(), offset: int({ minimum: 0 }), f: text(), s: text() }),
46
- operation("sequences.leads.rows", "/api/cli/sequences/{sequence}/leads/rows", "GET", "sequences leads rows", "oxygen_sequence_leads_rows", "sequences", "Read the complete enrolled-lead grid projection, including sequence status, step, action and failure columns.", { limit: int({ minimum: 1, maximum: 2000 }), cursor: text(), offset: int({ minimum: 0 }), f: text(), s: text() }),
57
+ operation("sequences.contacts.rows", "/api/cli/sequences/{sequence}/contacts/rows", "GET", "sequences contacts rows", "oxygen_sequence_contacts_rows", "sequences", "Read the full contact grid projection with exact contact-state filter and native synthetic status columns.", { contact_state: enumOf(["all", "not_contacted", "contacted", "connected", "replied", "positive_reply"]), limit: int({ minimum: 1, maximum: 2000 }), cursor: text(), offset: int({ minimum: 0 }), f: text(FILTER_PARAM), s: text(SORT_PARAM), ...sequenceRowsProjection }, [], { truncationHint: SEQUENCE_ROWS_TRUNCATION_HINT }),
58
+ operation("sequences.leads.rows", "/api/cli/sequences/{sequence}/leads/rows", "GET", "sequences leads rows", "oxygen_sequence_leads_rows", "sequences", "Read the complete enrolled-lead grid projection, including sequence status, step, action and failure columns.", { limit: int({ minimum: 1, maximum: 2000 }), cursor: text(), offset: int({ minimum: 0 }), f: text(FILTER_PARAM), s: text(SORT_PARAM), ...sequenceRowsProjection }, [], { truncationHint: SEQUENCE_ROWS_TRUNCATION_HINT }),
47
59
  operation("unibox.metadata", "/api/cli/unibox/metadata", "GET", "unibox metadata", "oxygen_unibox_metadata", "inbox", "Read the complete authenticated inbox account, filter and tag metadata, including each sender profile (person) and its id."),
48
60
  operation("workflows.observed_output", "/api/cli/workflows/{workflow}/observed-output", "GET", "workflows observed-output", "oxygen_workflow_observed_output", "workflows", "Inspect observed output paths and bounded samples from an exact run or recent matching workflow revision.", { run_id: text(), source_hash: text() }),
49
61
  operation("workflows.run.events", "/api/cli/workflows/{workflow}/runs/{run}/events", "GET", "workflows runs events", "oxygen_workflow_run_events", "workflows", "Read the exact Workflow run's bounded event history and whether it is still active.", { limit: int({ minimum: 1, maximum: 1000 }) }),
@@ -70,7 +82,7 @@ export const BROWSER_OPERATIONS = [
70
82
  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"] }),
71
83
  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"]),
72
84
  operation("tables.resource.rows.delete", `${table}/rows/{row}`, "DELETE", "tables rows remove", "oxygen_table_row_remove", "tables", "Remove one row from a workspace Table."),
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 }),
85
+ operation("tables.resource.rows.bulk_delete", `${table}/rows/delete`, "POST", "tables rows remove-selection", "oxygen_table_rows_remove_selection", "tables", "Permanently delete Table rows by a readable filter tree, exact ids or a matching selection. Unsupported scope fields are refused.", TABLE_ROW_REMOVAL_FIELDS, [], {}, { destructiveHint: true }),
74
86
  operation("tables.resource.rows.ids", `${table}/rows/ids`, "GET", "tables rows range-ids", "oxygen_table_rows_range_ids", "tables", "Resolve a bounded row range into exact ids, preserving the current filter, search, sort and CRM scope.", { offset: int({ minimum: 0 }), limit: int({ minimum: 1 }), filter: text(), search: text(), sort: text(), crm_object: text() }, ["offset", "limit"]),
75
87
  operation("tables.resource.relationships.get", `${table}/records/{row}/relationships`, "GET", "tables rows relationships", "oxygen_table_row_relationships", "crm", "Read a CRM-backed Table row's relationship links and available targets."),
76
88
  operation("tables.resource.relationships.set", `${table}/records/{row}/relationships`, "POST", "tables rows relationship-set", "oxygen_table_row_relationship_set", "crm", "Set one CRM-backed Table row's relationship and return the saved links.", { relationship: text(), target_row_id: text() }, ["relationship", "target_row_id"]),
@@ -1,4 +1,10 @@
1
1
  import type { ObjectProperty } from "./schema.js";
2
2
  /** Custom CRM column writes accept both canonical snake_case and legacy aliases. */
3
- export declare const CRM_CUSTOM_COLUMN_SCHEMA: ObjectProperty;
3
+ export declare const CRM_CUSTOM_COLUMN_SCHEMA: ObjectProperty & {
4
+ allOf: Array<{
5
+ anyOf: Array<{
6
+ required: string[];
7
+ }>;
8
+ }>;
9
+ };
4
10
  export declare const CRM_CUSTOM_IDENTITY_SCHEMA: ObjectProperty;
@@ -5,7 +5,7 @@ export const CRM_CUSTOM_COLUMN_SCHEMA = {
5
5
  key: { type: "string" }, label: { type: "string" },
6
6
  data_type: { type: "string", enum: ["text", "numeric", "boolean", "jsonb", "timestamptz"] },
7
7
  dataType: { type: "string", enum: ["text", "numeric", "boolean", "jsonb", "timestamptz"] },
8
- semantic_type: { type: "string" }, semanticType: { type: "string" },
8
+ semantic_type: { type: "string", minLength: 1, description: "Required with data_type; text for a generic field, or a semantic such as company.name, company.domain or email." }, semanticType: { type: "string", minLength: 1, description: "Alias of semantic_type." },
9
9
  kind: { type: "string" }, attribute_kind: { type: "string" }, attributeKind: { type: "string" },
10
10
  option_model_type: { type: "string" }, optionModelType: { type: "string" },
11
11
  is_required: { type: "boolean" }, isRequired: { type: "boolean" },
@@ -21,6 +21,10 @@ export const CRM_CUSTOM_COLUMN_SCHEMA = {
21
21
  attributeMetadata: { type: "object", additionalProperties: true },
22
22
  },
23
23
  required: ["key", "label"],
24
+ allOf: [
25
+ { anyOf: [{ required: ["data_type"] }, { required: ["dataType"] }] },
26
+ { anyOf: [{ required: ["semantic_type"] }, { required: ["semanticType"] }] },
27
+ ],
24
28
  additionalProperties: false,
25
29
  };
26
30
  export const CRM_CUSTOM_IDENTITY_SCHEMA = {
@@ -72,6 +72,20 @@ export declare const CRM_OBJECTS_QUERY_CONTRACT: import("./schema.js").ContractO
72
72
  "x-clamp"?: true;
73
73
  };
74
74
  }, never>;
75
+ export declare const CRM_RECORD_QUERY_CONTRACT: import("./schema.js").ContractObject<{
76
+ readonly projection: {
77
+ type: "string";
78
+ enum: readonly ("compact" | "full")[];
79
+ description?: string;
80
+ };
81
+ readonly max_chars: {
82
+ type: "integer";
83
+ description?: string;
84
+ minimum?: number;
85
+ maximum?: number;
86
+ "x-clamp"?: true;
87
+ };
88
+ }, never>;
75
89
  export declare const CRM_CONNECTIONS_QUERY_CONTRACT: import("./schema.js").ContractObject<{
76
90
  readonly projection: {
77
91
  type: "string";
@@ -30,6 +30,10 @@ export const CRM_OBJECTS_QUERY_CONTRACT = obj({
30
30
  limit: int({ minimum: 1, maximum: 100, description: "Objects in one batch; default 20 for bounded reads." }),
31
31
  max_chars: int({ minimum: 2000, maximum: 40_000, description: "Complete response window; oversized objects refuse without advancing." }),
32
32
  });
33
+ export const CRM_RECORD_QUERY_CONTRACT = obj({
34
+ projection: enumOf(["compact", "full"], { description: "compact omits object configuration and oversized row fields, naming omissions. HTTP/CLI default full; MCP default compact." }),
35
+ max_chars: int({ minimum: 2000, maximum: 40_000, description: "Complete receipt window. Omitted cells remain available through the Table row inspector." }),
36
+ });
33
37
  export const CRM_CONNECTIONS_QUERY_CONTRACT = obj({
34
38
  projection: enumOf(["compact", "full"], { description: "Compact returns identifiers and short communication previews. HTTP default full." }),
35
39
  max_chars: int({ minimum: 2000, maximum: 40_000, description: "Complete recent-window response; hasMore includes omitted links." }),