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