@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.
- package/README.md +1 -1
- package/dist/command-manifest.d.ts +3 -1
- 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/help.js +10 -2
- package/dist/http-client.d.ts +4 -0
- package/dist/http-client.js +21 -10
- package/dist/index.js +326 -111
- package/dist/sender-limits-flags.d.ts +7 -0
- package/dist/sender-limits-flags.js +13 -0
- package/dist/transcript.js +1 -1
- package/node_modules/@oxygen/formula/dist/expression.js +16 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.js +98 -0
- package/node_modules/@oxygen/shared/dist/byok-connect.js +2 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +172 -13
- package/node_modules/@oxygen/shared/dist/cli-result.js +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +19 -7
- 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 +5 -5
- package/node_modules/@oxygen/shared/dist/contracts/geo.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/geo.js +138 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
- package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.d.ts +16 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.js +27 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.js +23 -8
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +2 -1
- package/node_modules/@oxygen/shared/dist/external-effect-arming.d.ts +54 -0
- package/node_modules/@oxygen/shared/dist/external-effect-arming.js +90 -0
- package/node_modules/@oxygen/shared/dist/geo.d.ts +247 -0
- package/node_modules/@oxygen/shared/dist/geo.js +314 -0
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +2 -2
- package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/linkedin-countries.js +1 -1
- 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/managed-email-branding.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/managed-email-branding.js +81 -0
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +20 -24
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +15 -16
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +3 -3
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +3 -3
- package/node_modules/@oxygen/shared/dist/product-doctrine.js +6 -5
- package/node_modules/@oxygen/shared/dist/provider-balance-signal.js +3 -2
- package/node_modules/@oxygen/shared/dist/recipes.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/recipes.js +1 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +4 -14
- package/node_modules/@oxygen/shared/dist/repricing.js +25 -64
- package/node_modules/@oxygen/shared/dist/research-depth.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/research-depth.js +7 -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 +88 -0
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.js +118 -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 +15 -5
- 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
|
@@ -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" &&
|
|
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
|
|
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
|
|
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
|
-
||
|
|
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 (
|
|
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
|
-
/**
|
|
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", "
|
|
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
|
|
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", "
|
|
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." }),
|