@oxygen-agent/cli 1.1011.855 → 1.1011.1091
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.
- package/README.md +1 -1
- package/dist/admin-lead-magnet-commands.d.ts +7 -0
- package/dist/admin-lead-magnet-commands.js +128 -0
- package/dist/command-manifest.js +29 -0
- package/dist/crm-support-flags.d.ts +1 -0
- package/dist/crm-support-flags.js +5 -1
- package/dist/customer-operation-commands.js +30 -1
- package/dist/functions-commands.js +1 -1
- package/dist/help.js +10 -2
- package/dist/http-client.d.ts +4 -0
- package/dist/http-client.js +21 -10
- package/dist/index.js +444 -208
- package/dist/sender-limits-flags.d.ts +7 -0
- package/dist/sender-limits-flags.js +13 -0
- package/dist/workspace-utility-flags.d.ts +7 -0
- package/dist/workspace-utility-flags.js +45 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.js +83 -0
- package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/billing-anchors.js +7 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +279 -13
- package/node_modules/@oxygen/shared/dist/cli-result.js +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +21 -9
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.d.ts +7 -1
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.js +5 -1
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +14 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +13 -11
- package/node_modules/@oxygen/shared/dist/contracts/limits-read.js +11 -1
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/schema.js +10 -6
- package/node_modules/@oxygen/shared/dist/contracts/spintax.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/contracts/spintax.js +11 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.js +21 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-utilities.d.ts +162 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-utilities.js +46 -0
- package/node_modules/@oxygen/shared/dist/credit-topup-review.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/credit-topup-review.js +3 -0
- package/node_modules/@oxygen/shared/dist/emailguard-placement-probe.d.ts +70 -0
- package/node_modules/@oxygen/shared/dist/emailguard-placement-probe.js +161 -0
- package/node_modules/@oxygen/shared/dist/future-signup-events.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/future-signup-events.js +4 -0
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +2 -2
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +48 -0
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +94 -0
- package/node_modules/@oxygen/shared/dist/partner-coupon-bonus.d.ts +45 -0
- package/node_modules/@oxygen/shared/dist/partner-coupon-bonus.js +57 -0
- package/node_modules/@oxygen/shared/dist/provider-balance-signal.js +3 -2
- package/node_modules/@oxygen/shared/dist/table-filter-operators.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/table-filter-operators.js +50 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +4 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.d.ts +42 -0
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.js +68 -0
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +33 -0
- package/node_modules/@oxygen/shared/package.json +5 -5
- package/node_modules/@oxygen/workflows/dist/index.d.ts +1 -0
- package/node_modules/@oxygen/workflows/dist/index.js +1 -0
- package/node_modules/@oxygen/workflows/dist/recipe-timestamp.d.ts +22 -0
- package/node_modules/@oxygen/workflows/dist/recipe-timestamp.js +72 -0
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +9 -5
- package/package.json +1 -1
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.d.ts +0 -9
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.js +0 -33
|
@@ -88,8 +88,8 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
88
88
|
notFor: "Authorizing a specific provider call or external write; that action still needs its own preview and approval.",
|
|
89
89
|
execution: "Read billing state or set a hosted budget policy; exact action ceilings remain primitive-owned.",
|
|
90
90
|
posture: "mixed",
|
|
91
|
-
gatewayTools: ["oxygen_billing_balance", "oxygen_billing_usage", "oxygen_billing_estimate", "oxygen_budget_list", "oxygen_customer_credit_prices"],
|
|
92
|
-
gatewayCommands: ["billing balance", "billing usage", "billing estimate", "budget list", "billing invoices", "pricing customer-credit-prices"],
|
|
91
|
+
gatewayTools: ["oxygen_billing_balance", "oxygen_billing_usage", "oxygen_billing_audit", "oxygen_billing_estimate", "oxygen_budget_list", "oxygen_customer_credit_prices"],
|
|
92
|
+
gatewayCommands: ["billing balance", "billing usage", "billing audit", "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
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"],
|
|
@@ -206,7 +206,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
206
206
|
primitive: "records",
|
|
207
207
|
owns: "Canonical companies, people, deals, identities, relationships, activities, notes, tasks (reminders), files, merges, and CRM sync state.",
|
|
208
208
|
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 contacts|companies
|
|
209
|
+
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
210
|
posture: "workspace_write",
|
|
211
211
|
gatewayTools: ["oxygen_crm_record_search", "oxygen_crm_record_get"],
|
|
212
212
|
gatewayCommands: ["crm search", "crm get", "crm describe", "crm tasks list", "crm notes list"],
|
|
@@ -225,8 +225,8 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
225
225
|
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
226
|
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
227
|
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"],
|
|
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", "oxygen_tables_records_import"],
|
|
229
|
+
gatewayCommands: ["tables create", "tools search", "columns add", "columns catalog", "enrich-column preview", "tables link", "functions list", "functions draft", "tables records import"],
|
|
230
230
|
skills: ["oxygen-tables", "oxygen-enrichment", "oxygen-table-tidy", "oxygen-diagnostics", "oxygen-clay-migration", "oxygen-signals"],
|
|
231
231
|
endpointSections: ["action-columns", "callables", "functions", "columns", "company-enrichment", "enrich-column", "enrichment", "projects", "supabase", "table-action-items", "table-action-runs", "table-ingestion-runs", "tables"],
|
|
232
232
|
// "link"/"join"/"connect"/"relate" route here for `tables link`. Added after a
|
|
@@ -260,7 +260,11 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
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
|
-
|
|
263
|
+
// "inbox view" / "default view" / "conversations by sender" route here for
|
|
264
|
+
// `inbox views`. Added after the 2026-10-05 blind acceptance: asked to save a
|
|
265
|
+
// filtered inbox as the team's view, `capabilities search` recommended
|
|
266
|
+
// `inbox send` first and the agent found the views commands only by name.
|
|
267
|
+
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", "inbox view", "unibox view", "saved inbox view", "default inbox view", "conversations by sender", "sender filter"],
|
|
264
268
|
},
|
|
265
269
|
{
|
|
266
270
|
id: "posts",
|
|
@@ -1014,13 +1018,32 @@ function routeCardFor(card, query) {
|
|
|
1014
1018
|
const contactFind = card.id === "sourcing-and-provider-tools" ? oneShotContactFindingRecommendation(query) : null;
|
|
1015
1019
|
if (contactFind)
|
|
1016
1020
|
card = { ...card, execution: contactFind.execution };
|
|
1021
|
+
const crmSample = card.id === "records" ? crmSampleRecordRecommendation(query) : null;
|
|
1022
|
+
if (crmSample)
|
|
1023
|
+
return { ...card, execution: crmSample.execution };
|
|
1017
1024
|
const crmContactRead = card.id === "records" ? storedCrmContactReadRecommendation(query) : null;
|
|
1018
1025
|
if (crmContactRead)
|
|
1019
1026
|
card = { ...card, posture: "read_only", execution: crmContactRead.execution };
|
|
1020
1027
|
const replyDraftRead = card.primitive === "messages" ? storedReplyDraftReadRecommendation(query) : null;
|
|
1021
1028
|
if (replyDraftRead)
|
|
1022
1029
|
card = { ...card, posture: "read_only", execution: replyDraftRead.execution };
|
|
1023
|
-
|
|
1030
|
+
const providerRead = card.id === "billing-and-budgets" ? providerPolicyReadRecommendation(query) : null;
|
|
1031
|
+
if (providerRead)
|
|
1032
|
+
card = { ...card, posture: "read_only", execution: providerRead.execution };
|
|
1033
|
+
const spendRead = card.id === "billing-and-budgets" && !providerRead ? spendBreakdownRecommendation(query) : null;
|
|
1034
|
+
if (spendRead)
|
|
1035
|
+
card = { ...card, posture: "read_only", execution: spendRead.execution };
|
|
1036
|
+
const blockRead = card.primitive === "sequences" ? sendingBlockReadRecommendation(query) : null;
|
|
1037
|
+
if (blockRead)
|
|
1038
|
+
card = { ...card, posture: "read_only", execution: blockRead.execution };
|
|
1039
|
+
if (card.primitive === "sequences" && !blockRead && isSpintaxIntent(query))
|
|
1040
|
+
card = {
|
|
1041
|
+
...card, posture: "paid_read",
|
|
1042
|
+
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
|
+
};
|
|
1044
|
+
if (card.id === "knowledge-graph" && isCanonicalCopyReadIntent(query))
|
|
1045
|
+
card = { ...card, posture: "read_only", execution: CANONICAL_COPY_READ_EXECUTION };
|
|
1046
|
+
else if (card.id === "knowledge-graph" && isWorkspaceVoiceIntent(query))
|
|
1024
1047
|
card = { ...card, execution: WORKSPACE_VOICE_EXECUTION };
|
|
1025
1048
|
if (card.primitive === "workflows" && isRecurringCompanyMonitoringIntent(query))
|
|
1026
1049
|
card = {
|
|
@@ -1042,12 +1065,18 @@ function routeCardFor(card, query) {
|
|
|
1042
1065
|
...card,
|
|
1043
1066
|
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.",
|
|
1044
1067
|
};
|
|
1068
|
+
const recordsCopy = card.primitive === "tables" ? recordsTableImportRecommendation(query) : null;
|
|
1069
|
+
if (recordsCopy)
|
|
1070
|
+
card = { ...card, posture: "workspace_write", execution: recordsCopy.execution };
|
|
1045
1071
|
const maintenance = card.primitive === "tables" ? tableMaintenanceRecommendation(query) : null;
|
|
1046
1072
|
if (maintenance)
|
|
1047
1073
|
card = { ...card, execution: maintenance.execution };
|
|
1048
1074
|
const read = card.primitive === "tables" ? tableReadRecommendation(query) : null;
|
|
1049
1075
|
if (read)
|
|
1050
1076
|
card = { ...card, execution: read.execution };
|
|
1077
|
+
const removal = card.primitive === "tables" ? tableRowRemovalRecommendation(query) : null;
|
|
1078
|
+
if (removal)
|
|
1079
|
+
card = { ...card, posture: "workspace_write", execution: removal.execution };
|
|
1051
1080
|
const sent = card.id === "connected-linkedin" ? sentLinkedInInvitationReadRecommendation(query) : null;
|
|
1052
1081
|
if (sent)
|
|
1053
1082
|
card = { ...card, execution: sent.execution };
|
|
@@ -1141,6 +1170,23 @@ function isOwnLinkedInNetworkIntent(query) {
|
|
|
1141
1170
|
* claimed first (isUgcVoiceIntent); phone voice (numbers, calls) never matches.
|
|
1142
1171
|
* The blind baseline also searched "writing style" and "voice learn".
|
|
1143
1172
|
*/
|
|
1173
|
+
/**
|
|
1174
|
+
* Which voice, brand or positioning AI copy uses right now: a free read of the pinned
|
|
1175
|
+
* canonical doc. A blind operator searched "current canonical voice", got the wiki
|
|
1176
|
+
* search tools, and concluded no such read existed; "which voice does AI copy use"
|
|
1177
|
+
* read as "copy my voice" and was sent to paid synthesis (OXP-1.4.18).
|
|
1178
|
+
*/
|
|
1179
|
+
function isCanonicalCopyReadIntent(query) {
|
|
1180
|
+
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))
|
|
1181
|
+
return false;
|
|
1182
|
+
const copyType = /\b(?:voice|brand|positioning)\b/;
|
|
1183
|
+
if (!copyType.test(query))
|
|
1184
|
+
return false;
|
|
1185
|
+
return /\b(?:canonical|pinned|current|active|live)\b/.test(query)
|
|
1186
|
+
|| /\b(?:ai|generated) copy\b/.test(query)
|
|
1187
|
+
|| /\bwhich\b.{0,20}\b(?:voice|brand|positioning)\b/.test(query);
|
|
1188
|
+
}
|
|
1189
|
+
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.";
|
|
1144
1190
|
function isWorkspaceVoiceIntent(query) {
|
|
1145
1191
|
if (/\b(?:calls?|calling|dial\w*|phone|softphone|voicemail|numbers?)\b/.test(query))
|
|
1146
1192
|
return false;
|
|
@@ -1148,7 +1194,7 @@ function isWorkspaceVoiceIntent(query) {
|
|
|
1148
1194
|
|| /\b(?:learn|capture|mimic|copy|match)\w*\b.{0,30}\bvoice\b/.test(query)
|
|
1149
1195
|
|| /\bvoice\b.{0,30}\b(?:learn|capture|mimic|copy|match)\w*\b/.test(query);
|
|
1150
1196
|
}
|
|
1151
|
-
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
|
|
1197
|
+
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.";
|
|
1152
1198
|
/** One schedule for every channel of a Sequence, the same as Launch's Sending schedule. */
|
|
1153
1199
|
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";
|
|
1154
1200
|
/** Launch's unsubscribe switch: the visible link and the one-click headers together. */
|
|
@@ -1162,6 +1208,7 @@ export const SEQUENCE_UNSUBSCRIBE_COMMAND = "oxygen sequences update <sequence>
|
|
|
1162
1208
|
* Factories, so a caller can edit the object it gets.
|
|
1163
1209
|
*/
|
|
1164
1210
|
export const CAPABILITY_ROUTE_MCP_CALLS = {
|
|
1211
|
+
connectionPrices: () => ({ tool: "oxygen_billing_commitments", arguments: { view: "connection-prices" } }),
|
|
1165
1212
|
connectedAppsList: () => ({ tool: "oxygen_connector_list", arguments: {} }),
|
|
1166
1213
|
connectedAppEvents: () => ({ tool: "oxygen_connector_events_get", arguments: { connection: "<app>" } }),
|
|
1167
1214
|
workspacePagesList: () => ({ tool: "oxygen_settings_pages_get", arguments: {} }),
|
|
@@ -1221,6 +1268,13 @@ export function catalogueIntentMcpCall(intentId) {
|
|
|
1221
1268
|
// A Sequence's sending schedule or send window. Workflow language still wins
|
|
1222
1269
|
// ("a workflow that changes the send window nightly" is automation), and
|
|
1223
1270
|
// "sending hours" stays with the connected account's working hours.
|
|
1271
|
+
// Where each enrolled lead stands: its next message time, what holds it, why it
|
|
1272
|
+
// stopped. Both 2026-10-06 collateral acceptance operators searched for this
|
|
1273
|
+
// ("why is a lead stuck in a LinkedIn campaign, next message time") and got only
|
|
1274
|
+
// create/enroll/start, never the read that answers it.
|
|
1275
|
+
function isSequenceLeadProgressIntent(query) {
|
|
1276
|
+
return /\b(?:next (?:message|step|send)|stuck|blocked|held|per[- ]lead (?:status|progress)|why (?:\w+ )?(?:stopped|failed))\b/.test(query);
|
|
1277
|
+
}
|
|
1224
1278
|
function isSequenceScheduleIntent(query) {
|
|
1225
1279
|
return /\bsend(?:ing)? (?:schedule|window)s?\b/.test(query)
|
|
1226
1280
|
&& !/\b(workflow|automation|automate|webhook|cron)\b/.test(query);
|
|
@@ -1433,6 +1487,18 @@ function isCompanySourcingIntent(query) {
|
|
|
1433
1487
|
return /\b(companies|businesses|firms|company search|company sourcing)\b/.test(query)
|
|
1434
1488
|
&& /\b(search|plan|find|source|sourcing|discover|build|tam|prospect)\b/.test(query);
|
|
1435
1489
|
}
|
|
1490
|
+
/** Scoped row deletion keeps creation and enrichment out of its discovery path. */
|
|
1491
|
+
function tableRowRemovalRecommendation(query) {
|
|
1492
|
+
if (!/\brows?\b/.test(query) || !/\b(delete|remove)\b/.test(query)
|
|
1493
|
+
|| /\b(workflows?|agents?|campaigns?|sequences?|columns?|functions?|callables?|crm|records?|views?|dedup\w*|duplicates?)\b/.test(query)
|
|
1494
|
+
|| isSecondPartyDecisionIntent(query))
|
|
1495
|
+
return null;
|
|
1496
|
+
return {
|
|
1497
|
+
tools: ["oxygen_table_rows_remove_selection", "oxygen_tables_query", "oxygen_table_rows_page"],
|
|
1498
|
+
commands: ["tables rows remove-selection", "tables query", "tables rows page"],
|
|
1499
|
+
execution: "Delete Table rows with tables rows remove-selection (oxygen_table_rows_remove_selection): exact row_ids or all_matching with the browser's serialized filter, search, sort, range and exclusions. Hydrate its schema first. Unsupported filters arrays are refused; unqualified all_matching deletes every row. This is permanent and costs 0 credits. Read the matching rows before deletion and the survivors afterward; deleting rows does not stop campaign enrollment.",
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1436
1502
|
/** Reads of grid values/state are separate from executing or resolving actions. */
|
|
1437
1503
|
function tableReadRecommendation(query) {
|
|
1438
1504
|
if (!/\btables?\b/.test(query) || isFunctionIntent(query) || isSecondPartyDecisionIntent(query)
|
|
@@ -1564,6 +1630,41 @@ function storedReplyDraftReadRecommendation(query) {
|
|
|
1564
1630
|
const commands = tools.map(tool => tool === "oxygen_whatsapp_drafts_list" ? "whatsapp drafts list" : "inbox drafts list");
|
|
1565
1631
|
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." };
|
|
1566
1632
|
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Seeded sample records, and keeping one as real (OXP-6.3.14). In the
|
|
1635
|
+
* 2026-10-06 blind acceptance the records route answered "mark a sample deal as
|
|
1636
|
+
* a real deal" with search/get alone, and `commands search "sample"` ranked
|
|
1637
|
+
* `crm keep` third, behind `formulas validate` and `sequences start`. A sample
|
|
1638
|
+
* OF something (rows, a run, a batch, copy) is a test slice that other owners
|
|
1639
|
+
* keep; seed/demo/example name samples only beside data or records, so "seed
|
|
1640
|
+
* companies" and "demo deals" keep their sourcing and pipeline meanings.
|
|
1641
|
+
*/
|
|
1642
|
+
function crmSampleRecordRecommendation(query) {
|
|
1643
|
+
const record = /\b(records?|deals?|compan(?:y|ies)|contacts?|people|persons?|accounts?|ceos?|opportunit(?:y|ies))\b/;
|
|
1644
|
+
const named = /\b(?:crm|record) keep\b/.test(query);
|
|
1645
|
+
const slice = /\b(tables?|rows?|columns?|cells?|spreadsheets?|enrich\w*|runs?|batch(?:es)?|previews?|formulas?|sequences?|campaigns?|messages?|posts?|copy|templates?|prompts?|recipes?|workflows?|agents?)\b/.test(query)
|
|
1646
|
+
|| /\bsamples? (?:of|size|run|batch)\b|\b(?:small|random|test|representative) samples?\b/.test(query);
|
|
1647
|
+
const asReal = /\bas (?:an? )?real\b/.test(query) && record.test(query);
|
|
1648
|
+
const sample = (/\bsamples?\b/.test(query) && (/^samples?$/.test(query) || record.test(query) || /\b(data|pipeline|crm)\b/.test(query)))
|
|
1649
|
+
|| /\b(?:seed(?:ed)?|demo|example) (?:data|records?)\b/.test(query)
|
|
1650
|
+
|| /\bseeded (?:deals?|compan(?:y|ies)|contacts?|people|persons?|accounts?|pipeline)\b/.test(query)
|
|
1651
|
+
|| /\b(?:dummy|placeholder|fake) (?:data|records?|deals?|compan(?:y|ies)|contacts?|people|persons?|accounts?)\b/.test(query);
|
|
1652
|
+
if (!named && (slice || (!asReal && !sample)))
|
|
1653
|
+
return null;
|
|
1654
|
+
return {
|
|
1655
|
+
tools: ["oxygen_crm_record_keep", "oxygen_crm_pipeline", "oxygen_crm_record_search", "oxygen_crm_record_get"],
|
|
1656
|
+
commands: ["crm keep", "crm pipeline", "crm search", "crm get"],
|
|
1657
|
+
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.",
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
/** Inventory reads select existing Table fields and retain honest sample totals. */
|
|
1661
|
+
function crmInventoryRecommendation(query) {
|
|
1662
|
+
if (!/\b(crm|records?)\b/.test(query)
|
|
1663
|
+
|| !/\b(count|how many|inventory|list|enumerate|browse)\b/.test(query)
|
|
1664
|
+
|| /\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))
|
|
1665
|
+
return null;
|
|
1666
|
+
return { tools: ["oxygen_crm_objects_list", "oxygen_tables_preview", "oxygen_crm_record_get"], commands: ["crm objects", "tables preview", "crm get"] };
|
|
1667
|
+
}
|
|
1567
1668
|
/** Saved contact facts belong to Records, regardless of email/phone channel words. */
|
|
1568
1669
|
function storedCrmContactReadRecommendation(query) {
|
|
1569
1670
|
if (!/\b(crm|records?)\b/.test(query)
|
|
@@ -1610,13 +1711,84 @@ function tableFeedRecommendation(rawQuery) {
|
|
|
1610
1711
|
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." };
|
|
1611
1712
|
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." };
|
|
1612
1713
|
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Records into a working Table (OXP-1.1.10): "turn my CRM view into a table
|
|
1716
|
+
* that stays current". External CRM imports keep their own lanes, and a Table
|
|
1717
|
+
* going INTO Records is promote, the other direction.
|
|
1718
|
+
*/
|
|
1719
|
+
function recordsTableImportRecommendation(query) {
|
|
1720
|
+
if (!/\b(crm|records?)\b/.test(query)
|
|
1721
|
+
|| !/\b(tables?|working copy|spreadsheet)\b/.test(query)
|
|
1722
|
+
|| !/\b(import\w*|cop(?:y|ies)|turn\w*|pull\w*|bring|sync\w*|mirror\w*|into)\b/.test(query)
|
|
1723
|
+
|| /\b(hubspot|attio|pipedrive|salesforce|csv|promot\w*|bind)\b/.test(query)
|
|
1724
|
+
|| /\b(tables?|rows?)\b.{0,40}\b(?:into|to)\s+(?:the\s+|my\s+|our\s+)?(?:crm|records)\b/.test(query))
|
|
1725
|
+
return null;
|
|
1726
|
+
return {
|
|
1727
|
+
tools: ["oxygen_tables_records_import", "oxygen_tables_records_sync", "oxygen_tables_records_status"],
|
|
1728
|
+
commands: ["tables records import", "tables records sync", "tables records status"],
|
|
1729
|
+
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.",
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
function providerPolicyReadRecommendation(query) {
|
|
1733
|
+
if (!/\bproviders?\b/.test(query)
|
|
1734
|
+
|| !/\b(limits?|request caps?|rate caps?|overrides?)\b/.test(query)
|
|
1735
|
+
|| /\b(set|setting|change|changing|update|updating|delete|deleting|remove|removing|clear|clearing|raise|raising|lower|lowering|reset|resetting|enrich\w*|find|search companies|send\w*|scrape\w*)\b/.test(query))
|
|
1736
|
+
return null;
|
|
1737
|
+
return {
|
|
1738
|
+
tools: ["oxygen_provider_limits_list"], commands: ["limits provider list"],
|
|
1739
|
+
execution: "Read saved provider request-limit overrides and the plan's BYOK daily default with its enforced or observe-only mode. Hydrate oxygen_provider_limits_list; external MCP uses oxygen_capabilities_read, built-in agents invoke the native capability. MCP pages are bounded; continue at paging.next_offset with the same provider filter. Reads cost 0 credits. Setting a policy or removing its exact selector is a separate approved admin/owner write; removing one does not clear other operation overrides.",
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* "Where did our credits go": the audit ranks spend areas with shares over the
|
|
1744
|
+
* whole window. Blind agents read the raw usage ledger or left for the docs to
|
|
1745
|
+
* find it (OXP-43.63, 2026-10-06). Balance, buying, pricing and budget asks
|
|
1746
|
+
* keep their own readers.
|
|
1747
|
+
*/
|
|
1748
|
+
function spendBreakdownRecommendation(query) {
|
|
1749
|
+
if (!/\b(credits?|spend|spent|spending)\b/.test(query)
|
|
1750
|
+
|| !/\b(where|went|breakdown|break down|broken down|by (?:category|provider|tool|model|source|run|workspace)|categor(?:y|ies)|areas?|biggest|largest|audit)\b/.test(query)
|
|
1751
|
+
|| /\b(left|remaining|balance|buy|purchase|top ?up|refill|invoices?|budgets?|limits?|caps?|estimate|prices?|pricing|plans?|subscription|cancel)\b/.test(query))
|
|
1752
|
+
return null;
|
|
1753
|
+
return {
|
|
1754
|
+
tools: ["oxygen_billing_audit", "oxygen_billing_usage"], commands: ["billing audit", "billing usage"],
|
|
1755
|
+
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_read; built-in agents invoke the native capability. Reads cost 0 credits.",
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
function sendingBlockReadRecommendation(query) {
|
|
1759
|
+
if (!/\b(emails?|address(?:es)?|domains?)\b/.test(query)
|
|
1760
|
+
|| !/\b(suppressions?|sending blocks?|blocked)\b/.test(query)
|
|
1761
|
+
|| /\b(add|adding|import|importing|remove|removing|delete|deleting|clear|clearing|unblock|unblocking|send|sending now|reply|replies|inbox|unibox|dns|websites?)\b/.test(query))
|
|
1762
|
+
return null;
|
|
1763
|
+
const domain = /\bdomains?\b/.test(query);
|
|
1764
|
+
const address = /\baddress(?:es)?\b/.test(query) || !domain;
|
|
1765
|
+
return {
|
|
1766
|
+
tools: [...(address ? ["oxygen_suppressions_addresses_list"] : []), ...(domain ? ["oxygen_suppressed_domains_list"] : [])],
|
|
1767
|
+
commands: [...(address ? ["suppressions addresses"] : []), ...(domain ? ["suppressions domains"] : [])],
|
|
1768
|
+
execution: "Read the separate email-address and domain sending-block ledgers. Hydrate the selected native schemas; their reason enums differ. External MCP uses oxygen_capabilities_read; built-in agents invoke the native reads. MCP defaults to bounded summary pages with explicit omitted fields; continue at paging.next_offset with the same filters. Reads cost 0 credits. Removing exact approved keys is a separate write that can restore future sending eligibility; other suppression ledgers still apply and enrollments do not resume automatically.",
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
function isSpintaxIntent(query) {
|
|
1772
|
+
// Copy variation belongs to the native spintax owner; campaign A/B stats and
|
|
1773
|
+
// unrelated physical variants keep their existing owners.
|
|
1774
|
+
if (/\bspintax\b/.test(query))
|
|
1775
|
+
return true;
|
|
1776
|
+
if (/\b(posts?|publishing|ugc|visuals?|images?|videos?|scoreboard|stats|statistics|performance|winners?)\b/.test(query))
|
|
1777
|
+
return false;
|
|
1778
|
+
return /\b(wording|copy|message|draft|subject(?: line)?)\b.{0,45}\b(variations?|variants?|paraphrase|rewrite)\b/.test(query)
|
|
1779
|
+
|| /\b(paraphrase|rewrite|variations?)\b.{0,45}\b(wording|copy|message|draft|subject(?: line)?)\b/.test(query);
|
|
1780
|
+
}
|
|
1613
1781
|
function explicitCapabilityIntent(query) {
|
|
1782
|
+
if (isConnectionPriceIntent(query))
|
|
1783
|
+
return ROUTE_BY_ID.get("billing-and-budgets") ?? null;
|
|
1614
1784
|
if (sentLinkedInInvitationReadRecommendation(query))
|
|
1615
1785
|
return ROUTE_BY_ID.get("connected-linkedin") ?? null;
|
|
1616
1786
|
if (isCrmEnrichmentConfigurationRead(query))
|
|
1617
1787
|
return ROUTE_BY_ID.get("records") ?? null;
|
|
1618
1788
|
if (crmMaintenanceRecommendation(query))
|
|
1619
1789
|
return ROUTE_BY_ID.get("records") ?? null;
|
|
1790
|
+
if (crmSampleRecordRecommendation(query))
|
|
1791
|
+
return ROUTE_BY_ID.get("records") ?? null;
|
|
1620
1792
|
if (databaseRecommendation(query))
|
|
1621
1793
|
return ROUTE_BY_ID.get("platform-operations") ?? null;
|
|
1622
1794
|
if (oneShotContactFindingRecommendation(query))
|
|
@@ -1627,9 +1799,19 @@ function explicitCapabilityIntent(query) {
|
|
|
1627
1799
|
return ROUTE_BY_PRIMITIVE.get("messages") ?? null;
|
|
1628
1800
|
if (tableFeedRecommendation(query))
|
|
1629
1801
|
return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
|
|
1802
|
+
if (recordsTableImportRecommendation(query))
|
|
1803
|
+
return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
|
|
1804
|
+
if (providerPolicyReadRecommendation(query))
|
|
1805
|
+
return ROUTE_BY_ID.get("billing-and-budgets") ?? null;
|
|
1806
|
+
if (sendingBlockReadRecommendation(query))
|
|
1807
|
+
return ROUTE_BY_PRIMITIVE.get("sequences") ?? null;
|
|
1808
|
+
if (isSpintaxIntent(query))
|
|
1809
|
+
return ROUTE_BY_PRIMITIVE.get("sequences") ?? null;
|
|
1630
1810
|
if (isConnectedAppsIntent(query) || isConnectedAppEventsIntent(query) || isConnectedAppSpendIntent(query)) {
|
|
1631
1811
|
return ROUTE_BY_ID.get("workspace-access") ?? null;
|
|
1632
1812
|
}
|
|
1813
|
+
if (spendBreakdownRecommendation(query))
|
|
1814
|
+
return ROUTE_BY_ID.get("billing-and-budgets") ?? null;
|
|
1633
1815
|
// Ahead of every primitive rule: the pages a workspace hides are NAMED after
|
|
1634
1816
|
// primitives ("hide Publishing and Workflows"), which those rules would claim.
|
|
1635
1817
|
if (isWorkspacePagesIntent(query) || isWorkspaceSettingsIntent(query)) {
|
|
@@ -1807,9 +1989,12 @@ function explicitCapabilityIntent(query) {
|
|
|
1807
1989
|
if (/\bwhatsapp\b.{0,30}\b(account|connect|limits?|sync)\b/.test(query)) {
|
|
1808
1990
|
return ROUTE_BY_ID.get("connected-whatsapp") ?? null;
|
|
1809
1991
|
}
|
|
1992
|
+
const hasSequenceProgramIntent = /\b(sequence|campaign|cadence|enroll|nurture)\b/.test(query)
|
|
1993
|
+
|| /\b(day\s*\d+|days?\s+\d|multi[ -]step|rotate senders?|sender rotation|stop on reply|until (?:they )?reply)\b/.test(query);
|
|
1810
1994
|
if (!isNetNewLinkedInInitiation(query)
|
|
1811
1995
|
&& (/\b(one[ -]off|ad[ -]hoc|just once|reply once|single message|direct message|direct email|no cadence)\b/.test(query)
|
|
1812
|
-
||
|
|
1996
|
+
|| (!hasSequenceProgramIntent
|
|
1997
|
+
&& /\b(reply|respond|follow up)\b.{0,30}\b(conversation|thread|email|linkedin|whatsapp|dm)\b/.test(query)))) {
|
|
1813
1998
|
return ROUTE_BY_PRIMITIVE.get("messages") ?? null;
|
|
1814
1999
|
}
|
|
1815
2000
|
if (/\b(my|our|own|connected)\b.{0,40}\blinkedin\b|\b(sales navigator|salesnav|recruiter|profile viewers?|linkedin inbox)\b/.test(query)) {
|
|
@@ -1818,8 +2003,7 @@ function explicitCapabilityIntent(query) {
|
|
|
1818
2003
|
if (/\b(publish|schedule|approve)\b.{0,24}\b(post|content)\b|\bcontent calendar\b/.test(query)) {
|
|
1819
2004
|
return ROUTE_BY_PRIMITIVE.get("publishing") ?? null;
|
|
1820
2005
|
}
|
|
1821
|
-
if (
|
|
1822
|
-
|| /\b(day\s*\d+|days?\s+\d|multi[ -]step|rotate senders?|sender rotation|stop on reply|until (?:they )?reply)\b/.test(query)) {
|
|
2006
|
+
if (hasSequenceProgramIntent) {
|
|
1823
2007
|
return ROUTE_BY_PRIMITIVE.get("sequences") ?? null;
|
|
1824
2008
|
}
|
|
1825
2009
|
// Existing reply handling is Messages even without a channel name. Keep this
|
|
@@ -1920,12 +2104,41 @@ function recommendationsFor(card, query) {
|
|
|
1920
2104
|
const contactFind = card.id === "sourcing-and-provider-tools" ? oneShotContactFindingRecommendation(query) : null;
|
|
1921
2105
|
if (contactFind)
|
|
1922
2106
|
return contactFind;
|
|
2107
|
+
if (card.id === "knowledge-graph" && isCanonicalCopyReadIntent(query)) {
|
|
2108
|
+
return {
|
|
2109
|
+
tools: ["oxygen_context_canonical_get", "oxygen_knowledge_page_get", "oxygen_knowledge_search"],
|
|
2110
|
+
commands: ["context canonical get", "knowledge page get", "knowledge search"],
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
const crmSample = card.id === "records" ? crmSampleRecordRecommendation(query) : null;
|
|
2114
|
+
if (crmSample)
|
|
2115
|
+
return crmSample;
|
|
1923
2116
|
const crmContactRead = card.id === "records" ? storedCrmContactReadRecommendation(query) : null;
|
|
1924
2117
|
if (crmContactRead)
|
|
1925
2118
|
return crmContactRead;
|
|
2119
|
+
const crmInventory = card.id === "records" ? crmInventoryRecommendation(query) : null;
|
|
2120
|
+
if (crmInventory)
|
|
2121
|
+
return crmInventory;
|
|
1926
2122
|
const replyDraftRead = card.primitive === "messages" ? storedReplyDraftReadRecommendation(query) : null;
|
|
1927
2123
|
if (replyDraftRead)
|
|
1928
2124
|
return replyDraftRead;
|
|
2125
|
+
const providerRead = card.id === "billing-and-budgets" ? providerPolicyReadRecommendation(query) : null;
|
|
2126
|
+
if (providerRead)
|
|
2127
|
+
return providerRead;
|
|
2128
|
+
const spendRead = card.id === "billing-and-budgets" ? spendBreakdownRecommendation(query) : null;
|
|
2129
|
+
if (spendRead)
|
|
2130
|
+
return spendRead;
|
|
2131
|
+
if (card.id === "billing-and-budgets" && isConnectionPriceIntent(query)) {
|
|
2132
|
+
return {
|
|
2133
|
+
tools: ["oxygen_billing_commitments"],
|
|
2134
|
+
commands: ["billing commitments"],
|
|
2135
|
+
exactCommand: "oxygen billing commitments --view connection-prices --json",
|
|
2136
|
+
exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.connectionPrices(),
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
const blockRead = card.primitive === "sequences" ? sendingBlockReadRecommendation(query) : null;
|
|
2140
|
+
if (blockRead)
|
|
2141
|
+
return blockRead;
|
|
1929
2142
|
const sent = card.id === "connected-linkedin" ? sentLinkedInInvitationReadRecommendation(query) : null;
|
|
1930
2143
|
if (sent)
|
|
1931
2144
|
return sent;
|
|
@@ -1940,6 +2153,9 @@ function recommendationsFor(card, query) {
|
|
|
1940
2153
|
const feed = card.primitive === "tables" ? tableFeedRecommendation(query) : null;
|
|
1941
2154
|
if (feed)
|
|
1942
2155
|
return { tools: feed.tools, commands: feed.commands };
|
|
2156
|
+
const recordsCopy = card.primitive === "tables" ? recordsTableImportRecommendation(query) : null;
|
|
2157
|
+
if (recordsCopy)
|
|
2158
|
+
return { tools: recordsCopy.tools, commands: recordsCopy.commands };
|
|
1943
2159
|
if (card.id === "billing-and-budgets" && /\b(credit prices|credit price catalog|customer credit)\b/.test(query)) {
|
|
1944
2160
|
return { tools: ["oxygen_customer_credit_prices"], commands: ["pricing customer-credit-prices"] };
|
|
1945
2161
|
}
|
|
@@ -1994,7 +2210,7 @@ function recommendationsFor(card, query) {
|
|
|
1994
2210
|
if (card.primitive === "tables" && isCompanyPageFollowersIntent(query)) {
|
|
1995
2211
|
return {
|
|
1996
2212
|
tools: ["oxygen_tables_followers", "oxygen_tables_sources"],
|
|
1997
|
-
commands: ["tables followers check", "tables followers order", "tables sources"],
|
|
2213
|
+
commands: ["tables followers check", "tables followers order", "tables followers status", "tables sources"],
|
|
1998
2214
|
exactCommand: COMPANY_PAGE_FOLLOWERS_COMMAND,
|
|
1999
2215
|
exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.companyPageFollowers(),
|
|
2000
2216
|
};
|
|
@@ -2179,6 +2395,9 @@ function recommendationsFor(card, query) {
|
|
|
2179
2395
|
};
|
|
2180
2396
|
}
|
|
2181
2397
|
if (card.primitive === "tables") {
|
|
2398
|
+
const removal = tableRowRemovalRecommendation(query);
|
|
2399
|
+
if (removal)
|
|
2400
|
+
return removal;
|
|
2182
2401
|
const read = tableReadRecommendation(query);
|
|
2183
2402
|
if (read)
|
|
2184
2403
|
return read;
|
|
@@ -2334,6 +2553,15 @@ function recommendationsFor(card, query) {
|
|
|
2334
2553
|
}
|
|
2335
2554
|
}
|
|
2336
2555
|
if (card.primitive === "messages") {
|
|
2556
|
+
// A saved, shared or default inbox view is the views commands, never a send.
|
|
2557
|
+
// "view my inbox" stays a read; the noun needs "views" or a save/default/share word.
|
|
2558
|
+
if ((/\bviews\b/.test(query) && /\b(inbox|unibox)\b/.test(query))
|
|
2559
|
+
|| (/\bview\b/.test(query) && /\b(saved?|default|shared|team)\b/.test(query))) {
|
|
2560
|
+
return {
|
|
2561
|
+
tools: ["oxygen_inbox_views_list", "oxygen_inbox_view_create", "oxygen_inbox_view_update"],
|
|
2562
|
+
commands: ["inbox views list", "inbox views create", "inbox views update"],
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2337
2565
|
if (/\b(message )?reviews?\b/.test(query)) {
|
|
2338
2566
|
return {
|
|
2339
2567
|
tools: ["oxygen_reviews_list", "oxygen_reviews_next", "oxygen_reviews_decide"],
|
|
@@ -2349,6 +2577,9 @@ function recommendationsFor(card, query) {
|
|
|
2349
2577
|
}
|
|
2350
2578
|
}
|
|
2351
2579
|
if (card.primitive === "sequences") {
|
|
2580
|
+
if (isSpintaxIntent(query)) {
|
|
2581
|
+
return { tools: ["oxygen_spintax_generate"], commands: ["spintax generate"] };
|
|
2582
|
+
}
|
|
2352
2583
|
if (isSequenceScheduleIntent(query) || isSequenceUnsubscribeIntent(query)) {
|
|
2353
2584
|
return {
|
|
2354
2585
|
tools: ["oxygen_sequences_update", "oxygen_sequences_get"],
|
|
@@ -2365,6 +2596,12 @@ function recommendationsFor(card, query) {
|
|
|
2365
2596
|
commands: ["senders profiles list", "senders profiles get"],
|
|
2366
2597
|
};
|
|
2367
2598
|
}
|
|
2599
|
+
if (isSequenceLeadProgressIntent(query)) {
|
|
2600
|
+
return {
|
|
2601
|
+
tools: ["oxygen_sequences_enrollments", "oxygen_sequences_stats", "oxygen_sequences_list"],
|
|
2602
|
+
commands: ["sequences enrollments", "sequences stats", "sequences list"],
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2368
2605
|
if (isNetNewLinkedInInitiation(query)) {
|
|
2369
2606
|
const publicResearch = isPublicLinkedInRead(query);
|
|
2370
2607
|
const harvestEngagers = publicResearch && isPublicLinkedInEngagerHarvest(query);
|
|
@@ -2588,7 +2825,33 @@ function isSenderCopyVariableIntent(query) {
|
|
|
2588
2825
|
|| /\bsign off (?:variable|with|per sender)\b|\bper sender sign off\b/.test(query)
|
|
2589
2826
|
|| /\bsign (?:every|each|all) (?:emails?|messages?|dms?|notes?) with\b/.test(query);
|
|
2590
2827
|
}
|
|
2591
|
-
/**
|
|
2828
|
+
/** OXP-43.36: inspect recurring account prices before creating authorization. */
|
|
2829
|
+
function isConnectionPriceIntent(query) {
|
|
2830
|
+
if (!/\b(costs?|prices?|credits?|reser(?:ve[sd]?|vations?)|charges?|charged|fees?|pay|quote)\b/.test(query))
|
|
2831
|
+
return false;
|
|
2832
|
+
if (/\b(per (?:message|send|action|call)|scrap\w*|research|enrich\w*|public (?:profiles?|posts?))\b/.test(query))
|
|
2833
|
+
return false;
|
|
2834
|
+
// A whole mailbox onboarding motion can mention price alongside importing
|
|
2835
|
+
// history and syncing CRM. Keep that owner unless the ask isolates a quote.
|
|
2836
|
+
if (/\b(import|sync)\b/.test(query) && /\b(history|crm|unibox)\b/.test(query)
|
|
2837
|
+
&& !/\b(monthly|recurring|quote|before (?:consent|connect|authorization))\b/.test(query))
|
|
2838
|
+
return false;
|
|
2839
|
+
// Lookup, purchase and sending prices keep their own previews. Naming a
|
|
2840
|
+
// mailbox or a phone number alone does not make it an account connection.
|
|
2841
|
+
if (/\b(?:find|look ?up|lookup)\b/.test(query) && /\bphones?\b/.test(query))
|
|
2842
|
+
return false;
|
|
2843
|
+
if (/\b(?:warm ?up|deliverability|placement)\b/.test(query))
|
|
2844
|
+
return false;
|
|
2845
|
+
if (/\bmailbox(?:es)?\b/.test(query) && /\b(?:buy|purchase|order)\b/.test(query))
|
|
2846
|
+
return false;
|
|
2847
|
+
if (/\blinkedin\b/.test(query) && /\b(?:messages?|invites?|invitations?|connection requests?)\b/.test(query))
|
|
2848
|
+
return false;
|
|
2849
|
+
const accountContext = /\b(accounts?|connect(?:ing|ed|ion)?|reconnect|oauth|consent)\b/.test(query);
|
|
2850
|
+
return /\b(connected accounts?|account connections?|connection prices?)\b/.test(query)
|
|
2851
|
+
|| (/\b(linkedin|whatsapp|twitter|x)\b/.test(query)
|
|
2852
|
+
&& (accountContext || /\b(numbers?|seats?)\b/.test(query)))
|
|
2853
|
+
|| (/\bmailbox(?:es)?\b/.test(query) && accountContext);
|
|
2854
|
+
}
|
|
2592
2855
|
/**
|
|
2593
2856
|
* Connecting, or asking what it takes to connect, the workspace's own LinkedIn
|
|
2594
2857
|
* sending account. The 2026-09-26 repricing baseline asked "connect a LinkedIn
|
|
@@ -2693,6 +2956,9 @@ function isFailedPostIntent(query) {
|
|
|
2693
2956
|
function isPostCoverOrPreviewIntent(query) {
|
|
2694
2957
|
if (/\b(ads?|competitors?)\b/.test(query))
|
|
2695
2958
|
return false;
|
|
2959
|
+
// A draft in an existing inbox conversation is a reply, not a post artifact.
|
|
2960
|
+
if (/\b(inbox|unibox|conversation)\b/.test(query) && !/\bposts?\b/.test(query))
|
|
2961
|
+
return false;
|
|
2696
2962
|
return (/\b(cover|thumbnail)\b/.test(query) && /\b(post|video|draft)\b/.test(query))
|
|
2697
2963
|
|| /\bpreview\b.{0,40}\b(post|draft)\b/.test(query);
|
|
2698
2964
|
}
|
|
@@ -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
|
}
|