@oxygen-agent/cli 1.1011.97 → 1.1011.288
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/help.d.ts +27 -1
- package/dist/help.js +112 -29
- package/dist/http-client.js +8 -0
- package/dist/index.js +348 -127
- 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 +127 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +286 -24
- 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/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/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/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/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
|
@@ -214,7 +214,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
214
214
|
primitive: "tables",
|
|
215
215
|
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
216
|
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.",
|
|
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. 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
218
|
posture: "mixed",
|
|
219
219
|
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
220
|
gatewayCommands: ["tables create", "tools search", "columns add", "columns catalog", "enrich-column preview", "tables link", "functions list", "functions draft"],
|
|
@@ -273,13 +273,13 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
273
273
|
primitive: "signals",
|
|
274
274
|
owns: "Durable typed pulse, engagement, and product events resolved onto records.",
|
|
275
275
|
notFor: "Message bodies, post artifacts, or ad-hoc provider webhooks as private automation state.",
|
|
276
|
-
execution: "Read the hosted signal stream and
|
|
276
|
+
execution: "Read the hosted signal stream and type registry; tag a registered type with tags attach. Feed subscriptions, Sequences, or Workflows.",
|
|
277
277
|
posture: "workspace_write",
|
|
278
|
-
gatewayTools: ["oxygen_signals_list", "oxygen_signals_leads_today"],
|
|
279
|
-
gatewayCommands: ["signals list", "signals leads-today", "signals registry"],
|
|
278
|
+
gatewayTools: ["oxygen_signals_list", "oxygen_signals_leads_today", "oxygen_signals_registry", "oxygen_tags_attach"],
|
|
279
|
+
gatewayCommands: ["signals list", "signals leads-today", "signals registry", "tags attach"],
|
|
280
280
|
skills: ["oxygen-gtm", "oxygen-linkedin-marketing"],
|
|
281
281
|
endpointSections: ["signals", "signup-leads"],
|
|
282
|
-
intentTerms: ["signal", "signals", "intent", "website visit", "job change", "fundraising", "warm leads", "engager", "follower", "viewer"],
|
|
282
|
+
intentTerms: ["signal", "signals", "signal type", "tag signal", "intent", "website visit", "job change", "fundraising", "warm leads", "engager", "follower", "viewer"],
|
|
283
283
|
},
|
|
284
284
|
{
|
|
285
285
|
id: "dashboards",
|
|
@@ -310,15 +310,15 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
310
310
|
id: "ads",
|
|
311
311
|
layer: "Data",
|
|
312
312
|
primitive: "ads",
|
|
313
|
-
owns: "Competitor ad intelligence
|
|
313
|
+
owns: "Competitor ad intelligence and saved search definitions; owned-audience activation is a roadmap boundary.",
|
|
314
314
|
notFor: "Organic Posts, Publishing, or claiming unshipped audience activation.",
|
|
315
|
-
execution: "
|
|
315
|
+
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
316
|
posture: "paid_read",
|
|
317
|
-
gatewayTools: ["oxygen_adyntel_ads_search", "oxygen_tools_search"],
|
|
318
|
-
gatewayCommands: ["tools search"],
|
|
317
|
+
gatewayTools: ["oxygen_ads_searches", "oxygen_adyntel_ads_search", "oxygen_tools_search"],
|
|
318
|
+
gatewayCommands: ["ads searches list", "ads searches save", "tools search"],
|
|
319
319
|
skills: ["oxygen-gtm"],
|
|
320
|
-
endpointSections: [],
|
|
321
|
-
intentTerms: ["ad", "ads", "advertising", "ad creative", "competitor ads", "ad intelligence", "meta ads", "linkedin ads"],
|
|
320
|
+
endpointSections: ["ads"],
|
|
321
|
+
intentTerms: ["ad", "ads", "advertising", "ad creative", "competitor ads", "ad intelligence", "meta ads", "linkedin ads", "saved ads search", "reusable ads search"],
|
|
322
322
|
},
|
|
323
323
|
{
|
|
324
324
|
id: "tags",
|
|
@@ -326,10 +326,10 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
326
326
|
primitive: "tags",
|
|
327
327
|
owns: "The normalized cross-primitive label vocabulary and campaign handle.",
|
|
328
328
|
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.",
|
|
329
|
+
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
330
|
posture: "workspace_write",
|
|
331
|
-
gatewayTools: ["oxygen_tags_list", "oxygen_tags_get", "oxygen_tags_rename"],
|
|
332
|
-
gatewayCommands: ["tags list", "tags get", "tags rename"],
|
|
331
|
+
gatewayTools: ["oxygen_tags_list", "oxygen_tags_get", "oxygen_tags_attach", "oxygen_tags_rename"],
|
|
332
|
+
gatewayCommands: ["tags list", "tags get", "tags attach", "tags rename"],
|
|
333
333
|
skills: ["oxygen-gtm"],
|
|
334
334
|
endpointSections: ["tags"],
|
|
335
335
|
intentTerms: ["tag", "tags", "label", "labels", "campaign handle", "grouping", "organize campaign"],
|
|
@@ -345,7 +345,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
345
345
|
// email track's "Only 'instantly' is supported" and concluded Smartlead was
|
|
346
346
|
// unreachable. The governed path for a campaign the customer already runs
|
|
347
347
|
// 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.",
|
|
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. 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
349
|
posture: "external_write",
|
|
350
350
|
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
351
|
gatewayCommands: ["sequences send", "sequences create", "sequences enroll", "sequences start", "sequences list", "sequences push-external", "sequences pause-company"],
|
|
@@ -356,7 +356,10 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
356
356
|
// to workspace-access — "identity" matched the AUTH sense. The nouns a user
|
|
357
357
|
// reaches for when naming a sending identity or the dialing pool have to land
|
|
358
358
|
// on the group that actually owns `senders profiles` and `voice numbers`.
|
|
359
|
-
|
|
359
|
+
// The sign-off terms are here because a 2026-09-29 blind user searched
|
|
360
|
+
// "preview rendered message per sender sign-off variable" and landed on
|
|
361
|
+
// collaboration; the {{sender_*}} copy variables live on sequence steps.
|
|
362
|
+
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
363
|
},
|
|
361
364
|
{
|
|
362
365
|
id: "publishing",
|
|
@@ -565,12 +568,13 @@ export function inferCapabilityRoute(query) {
|
|
|
565
568
|
// beside them in `catalogIntent`, never inside them.
|
|
566
569
|
const catalogue = catalogRow && CATALOGUE_CARD_IDS.has(card.id) ? catalogueRecommendations(catalogRow, card) : null;
|
|
567
570
|
return {
|
|
568
|
-
card,
|
|
571
|
+
card: routeCardFor(card, normalized),
|
|
569
572
|
recommendedTools: recommendations.tools,
|
|
570
573
|
recommendedCommands: recommendations.commands,
|
|
571
574
|
...(recommendations.exactCommand ? { exactCommand: recommendations.exactCommand } : {}),
|
|
575
|
+
...(recommendations.exactMcpCall ? { exactMcpCall: recommendations.exactMcpCall } : {}),
|
|
572
576
|
...(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 } }
|
|
577
|
+
? { 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
578
|
: {}),
|
|
575
579
|
};
|
|
576
580
|
}
|
|
@@ -587,13 +591,25 @@ function cardForCatalogueIntent(row) {
|
|
|
587
591
|
return ROUTE_BY_ID.get("signals") ?? ROUTE_BY_PRIMITIVE.get("signals") ?? null;
|
|
588
592
|
return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
|
|
589
593
|
}
|
|
594
|
+
/**
|
|
595
|
+
* The MCP tool a catalogue row's `find` command maps to, named by its command
|
|
596
|
+
* words alone: "find company --domain <domain> --fields funding,..." once became
|
|
597
|
+
* the tool "oxygen_find_company_domain_<domain>_fields_...", which does not
|
|
598
|
+
* exist (funding, hiring-signal and revenue rows, 2026-09-29).
|
|
599
|
+
*/
|
|
600
|
+
export function catalogueFindTool(row) {
|
|
601
|
+
const command = row.surfaces.find?.split(/\s+--/)[0]?.trim();
|
|
602
|
+
return command ? `oxygen_${command.replace(/[\s-]+/g, "_")}` : null;
|
|
603
|
+
}
|
|
590
604
|
function catalogueRecommendations(row, card) {
|
|
591
605
|
const exactCommand = `oxygen enrichment catalog --intent ${row.id}`;
|
|
606
|
+
const exactMcpCall = catalogueIntentMcpCall(row.id);
|
|
592
607
|
if (row.group === "decision_maker" || card.id === "sourcing-and-provider-tools") {
|
|
593
608
|
return {
|
|
594
609
|
tools: ["oxygen_enrichment_catalog", "oxygen_people_search_plan", "oxygen_people_search_run"],
|
|
595
610
|
commands: ["enrichment catalog", "people search plan", "people search run"],
|
|
596
611
|
exactCommand,
|
|
612
|
+
exactMcpCall,
|
|
597
613
|
};
|
|
598
614
|
}
|
|
599
615
|
if (row.group === "signal") {
|
|
@@ -601,13 +617,16 @@ function catalogueRecommendations(row, card) {
|
|
|
601
617
|
tools: ["oxygen_enrichment_catalog", "oxygen_signals_search_plan", "oxygen_signals_search_run"],
|
|
602
618
|
commands: ["enrichment catalog", "signals search plan", "signals search run"],
|
|
603
619
|
exactCommand,
|
|
620
|
+
exactMcpCall,
|
|
604
621
|
};
|
|
605
622
|
}
|
|
606
|
-
const
|
|
623
|
+
const findTool = catalogueFindTool(row);
|
|
624
|
+
const find = findTool ? [findTool] : [];
|
|
607
625
|
return {
|
|
608
626
|
tools: ["oxygen_enrichment_catalog", "oxygen_columns_add", "oxygen_enrich_column_preview", ...find],
|
|
609
627
|
commands: ["enrichment catalog", "columns add", "enrich-column preview", ...(row.surfaces.find ? [row.surfaces.find] : [])],
|
|
610
628
|
exactCommand,
|
|
629
|
+
exactMcpCall,
|
|
611
630
|
};
|
|
612
631
|
}
|
|
613
632
|
export function inferPrimitiveRoute(query) {
|
|
@@ -616,7 +635,7 @@ export function inferPrimitiveRoute(query) {
|
|
|
616
635
|
return null;
|
|
617
636
|
return route;
|
|
618
637
|
}
|
|
619
|
-
export function serializeCapabilityRoute(route) {
|
|
638
|
+
export function serializeCapabilityRoute(route, surface = "cli") {
|
|
620
639
|
if (!route)
|
|
621
640
|
return null;
|
|
622
641
|
return {
|
|
@@ -631,9 +650,23 @@ export function serializeCapabilityRoute(route) {
|
|
|
631
650
|
endpoint_sections: route.card.endpointSections,
|
|
632
651
|
recommended_commands: route.recommendedCommands,
|
|
633
652
|
recommended_tools: route.recommendedTools,
|
|
634
|
-
...(
|
|
653
|
+
...(surface === "mcp" && route.exactMcpCall
|
|
654
|
+
? { exact_mcp_call: route.exactMcpCall }
|
|
655
|
+
: route.exactCommand ? { exact_command: route.exactCommand } : {}),
|
|
635
656
|
...(route.catalogIntent
|
|
636
|
-
? {
|
|
657
|
+
? {
|
|
658
|
+
catalog_intent: {
|
|
659
|
+
id: route.catalogIntent.id,
|
|
660
|
+
label: route.catalogIntent.label,
|
|
661
|
+
status: route.catalogIntent.status,
|
|
662
|
+
group: route.catalogIntent.group,
|
|
663
|
+
tools: route.catalogIntent.tools,
|
|
664
|
+
commands: route.catalogIntent.commands,
|
|
665
|
+
...(surface === "mcp"
|
|
666
|
+
? { exact_mcp_call: route.catalogIntent.exactMcpCall }
|
|
667
|
+
: { exact_command: route.catalogIntent.exactCommand }),
|
|
668
|
+
},
|
|
669
|
+
}
|
|
637
670
|
: {}),
|
|
638
671
|
hydrate: {
|
|
639
672
|
cli: "oxygen commands get <exact-command> --json",
|
|
@@ -669,6 +702,70 @@ function isUgcProgramIntent(query) {
|
|
|
669
702
|
function isUgcCreatorSideIntent(query) {
|
|
670
703
|
return /\b(join|apply|applying|find|discover|browse|accept)\w*\b.{0,30}\b(creator|ugc|ambassador) programs?\b/.test(query);
|
|
671
704
|
}
|
|
705
|
+
/** The lifecycle verb of an operator's program ask ("archive program", "delete my creator program"), or null. */
|
|
706
|
+
function ugcProgramLifecycleVerb(query) {
|
|
707
|
+
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;
|
|
708
|
+
}
|
|
709
|
+
/** The `ugc programs` leaf and MCP tool per lifecycle verb; delete previews first and names archive when refused. */
|
|
710
|
+
const UGC_PROGRAM_LIFECYCLE = {
|
|
711
|
+
archive: ["ugc programs archive", "oxygen_ugc_program_update"],
|
|
712
|
+
restore: ["ugc programs restore", "oxygen_ugc_program_update"],
|
|
713
|
+
delete: ["ugc programs delete", "oxygen_ugc_program_delete"],
|
|
714
|
+
remove: ["ugc programs delete", "oxygen_ugc_program_delete"],
|
|
715
|
+
duplicate: ["ugc programs duplicate", "oxygen_ugc_program_duplicate"],
|
|
716
|
+
copy: ["ugc programs duplicate", "oxygen_ugc_program_duplicate"],
|
|
717
|
+
clone: ["ugc programs duplicate", "oxygen_ugc_program_duplicate"],
|
|
718
|
+
};
|
|
719
|
+
/**
|
|
720
|
+
* A creator's own writing voice: `ugc voice import` learns it from their LinkedIn
|
|
721
|
+
* history (the web's "Learn your writing voice") and `ugc voice save` keeps what
|
|
722
|
+
* they write themselves. Only an ask that names the creator context routes here:
|
|
723
|
+
* "learn my writing voice from my LinkedIn messages" is just as likely the
|
|
724
|
+
* workspace voice `knowledge synthesize --kind voice` learns from sent LinkedIn and
|
|
725
|
+
* WhatsApp messages, and brand voice stays with the Knowledge Graph.
|
|
726
|
+
*/
|
|
727
|
+
function isUgcVoiceIntent(query) {
|
|
728
|
+
return /\bcreators?'?s? voice\b/.test(query)
|
|
729
|
+
|| (/\bvoice\b/.test(query) && /\b(creator|ugc|ambassador)s?\b/.test(query) && !/\bbrand voice\b/.test(query));
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Who approves a creator's post. It is the creator, or the program organizer when
|
|
733
|
+
* the creator delegated approval; a brand can never require its own approval of
|
|
734
|
+
* creator posts (Philipp, 2026-09-29).
|
|
735
|
+
* The MCP acceptance run's "brand approval of creator post before publish" routed
|
|
736
|
+
* to the Knowledge Graph, so the rule it needed never reached it. Only an
|
|
737
|
+
* approval word next to a creator's post counts: "find LinkedIn creator posts
|
|
738
|
+
* about AI" stays a keyword post search.
|
|
739
|
+
*/
|
|
740
|
+
function isUgcPostApprovalIntent(query) {
|
|
741
|
+
return /\b(?:approv\w*|review\w*|sign[- ]?off)\b.{0,40}\b(?:creator|ugc|ambassador)s?'?\s+posts?\b/.test(query)
|
|
742
|
+
|| /\b(?:creator|ugc|ambassador)s?'?\s+posts?\b.{0,40}\b(?:approv\w*|review\w*|sign[- ]?off)\b/.test(query)
|
|
743
|
+
|| /\bbrand (?:approval|review)\b.{0,40}\b(?:creators?|ugc|ambassadors?)\b/.test(query);
|
|
744
|
+
}
|
|
745
|
+
function isUgcIntent(query) {
|
|
746
|
+
return isUgcProgramIntent(query) || isUgcCreatorSideIntent(query) || isUgcVoiceIntent(query)
|
|
747
|
+
|| isUgcPostApprovalIntent(query) || ugcProgramLifecycleVerb(query) !== null;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* UGC is Publishing's creator-program subpage (OXYGEN_PRODUCT.md § UGC programs): a
|
|
751
|
+
* creator-program ask keeps the publishing id, primitive, endpoint sections and
|
|
752
|
+
* gateways, but must not read Publishing's post-scheduler ownership back (UGC E2E pass, 2026-09-29).
|
|
753
|
+
*/
|
|
754
|
+
const UGC_ROUTE_VIEW = {
|
|
755
|
+
owns: "UGC creator programs: brand programs, creator invitations and applications, participations, creator voice, program post drafts and approvals, sponsorship, results, and bounded amplification.",
|
|
756
|
+
notFor: "Scheduling the workspace's own posts, one-to-one messaging, outreach campaigns, or keyword post search.",
|
|
757
|
+
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.",
|
|
758
|
+
posture: "mixed",
|
|
759
|
+
skills: ["oxygen-ugc", "oxygen-linkedin-marketing"],
|
|
760
|
+
};
|
|
761
|
+
/** The card a route reports: Publishing's own, or its UGC view for a creator-program ask. */
|
|
762
|
+
function routeCardFor(card, query) {
|
|
763
|
+
if (card.primitive === "publishing" && isUgcIntent(query))
|
|
764
|
+
return { ...card, ...UGC_ROUTE_VIEW };
|
|
765
|
+
if (card.id === "knowledge-graph" && isWorkspaceVoiceIntent(query))
|
|
766
|
+
return { ...card, execution: WORKSPACE_VOICE_EXECUTION };
|
|
767
|
+
return card;
|
|
768
|
+
}
|
|
672
769
|
function isLinkedInKeywordPostIntent(query) {
|
|
673
770
|
return /\blinkedin\b/.test(query)
|
|
674
771
|
&& /\b(posts?|posting)\b/.test(query)
|
|
@@ -700,6 +797,103 @@ function isOwnLinkedInNetworkIntent(query) {
|
|
|
700
797
|
const bothHalves = /\bconnections?\b/.test(query) && /\bfollowers?\b/.test(query);
|
|
701
798
|
return (ownNetwork || bothHalves) && !isNetNewLinkedInInitiation(query);
|
|
702
799
|
}
|
|
800
|
+
/**
|
|
801
|
+
* The workspace's own voice. `knowledge synthesize --kind voice` learns a writing
|
|
802
|
+
* voice from the workspace's SENT LinkedIn and WhatsApp messages; brand voice is a
|
|
803
|
+
* wiki page. "learn my writing voice from my LinkedIn history" scored higher on
|
|
804
|
+
* the connected-LinkedIn card ("my linkedin") and was offered `linkedin network
|
|
805
|
+
* setup` (UGC E2E pass, 2026-09-29, OXP-1.15.12). A creator's voice is UGC and is
|
|
806
|
+
* claimed first (isUgcVoiceIntent); phone voice (numbers, calls) never matches.
|
|
807
|
+
* The blind baseline also searched "writing style" and "voice learn".
|
|
808
|
+
*/
|
|
809
|
+
function isWorkspaceVoiceIntent(query) {
|
|
810
|
+
if (/\b(?:calls?|calling|dial\w*|phone|softphone|voicemail|numbers?)\b/.test(query))
|
|
811
|
+
return false;
|
|
812
|
+
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)
|
|
813
|
+
|| /\b(?:learn|capture|mimic|copy|match)\w*\b.{0,30}\bvoice\b/.test(query)
|
|
814
|
+
|| /\bvoice\b.{0,30}\b(?:learn|capture|mimic|copy|match)\w*\b/.test(query);
|
|
815
|
+
}
|
|
816
|
+
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`.";
|
|
817
|
+
/** One schedule for every channel of a Sequence, the same as Launch's Sending schedule. */
|
|
818
|
+
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";
|
|
819
|
+
/** Launch's unsubscribe switch: the visible link and the one-click headers together. */
|
|
820
|
+
export const SEQUENCE_UNSUBSCRIBE_COMMAND = "oxygen sequences update <sequence> --include-unsubscribe --json";
|
|
821
|
+
/**
|
|
822
|
+
* Every exact CLI command a route hands out, as the MCP tool call that sends the
|
|
823
|
+
* same request. The MCP surface serves these in place of the command: an MCP
|
|
824
|
+
* agent handed a CLI command had to translate it (blind user eval, 2026-09-29).
|
|
825
|
+
* Each was traced to the same `/api/cli/*` request as its command; the
|
|
826
|
+
* mcp-server test proves every tool exists and declares every argument.
|
|
827
|
+
* Factories, so a caller can edit the object it gets.
|
|
828
|
+
*/
|
|
829
|
+
export const CAPABILITY_ROUTE_MCP_CALLS = {
|
|
830
|
+
sequenceSendingSchedule: () => ({
|
|
831
|
+
tool: "oxygen_sequences_update",
|
|
832
|
+
arguments: { sequence: "<sequence>", sending_schedule: { timezone: "<IANA zone>", days: [1, 2, 3, 4, 5], start: "09:00", end: "17:00" } },
|
|
833
|
+
}),
|
|
834
|
+
sequenceUnsubscribe: () => ({ tool: "oxygen_sequences_update", arguments: { sequence: "<sequence>", include_unsubscribe: true } }),
|
|
835
|
+
ugcProgramCreate: () => ({ tool: "oxygen_ugc_program_create", arguments: { product_url: "<your product site>" } }),
|
|
836
|
+
// The first comment rides in `content`, as the CLI's --first-comment does.
|
|
837
|
+
publishingFirstComment: () => ({
|
|
838
|
+
tool: "oxygen_publishing_posts_create",
|
|
839
|
+
arguments: { content_text: "<post>", publish_at: "<iso>", sender_account_id: "<sender_id>", content: { first_comment: "<comment, e.g. the link>" } },
|
|
840
|
+
}),
|
|
841
|
+
companyUrlRecovery: () => ({
|
|
842
|
+
tool: "oxygen_companies_search_plan",
|
|
843
|
+
arguments: { prompt: "Find the websites for these companies: <company names>", source_intent: "url_recovery" },
|
|
844
|
+
}),
|
|
845
|
+
// `records` is a number the tool drops when it is not one; 1000 is its default.
|
|
846
|
+
companyPageFollowers: () => ({
|
|
847
|
+
tool: "oxygen_tables_followers",
|
|
848
|
+
arguments: { action: "preview", company_url: "<linkedin company page URL>", records: 1000 },
|
|
849
|
+
}),
|
|
850
|
+
localBusinessScrape: () => ({
|
|
851
|
+
tool: "oxygen_companies_search_preview",
|
|
852
|
+
arguments: { action: "preview", source: "google_maps", filters: { q: "<category> in <city, region>" } },
|
|
853
|
+
}),
|
|
854
|
+
// Like the CLI command, it leaves the LinkedIn risk acknowledgement to the user.
|
|
855
|
+
senderConnect: () => ({ tool: "oxygen_senders_connect", arguments: { country: "<cc>" } }),
|
|
856
|
+
rowClassification: () => ({ tool: "oxygen_columns_catalog", arguments: { family: "classification" } }),
|
|
857
|
+
// The tool has no `family`: it always searches linkedin_posts, which the API reads as the same source.
|
|
858
|
+
linkedinKeywordPosts: () => ({
|
|
859
|
+
tool: "oxygen_signals_search_preview",
|
|
860
|
+
arguments: {
|
|
861
|
+
action: "preview",
|
|
862
|
+
filters: { keywords: ["<any of>"], all_keywords: ["<all of>"], exclude_keywords: ["<none of>"], ai_filter: "<posts to keep>" },
|
|
863
|
+
},
|
|
864
|
+
}),
|
|
865
|
+
};
|
|
866
|
+
/** The catalogue row's `enrichment catalog --intent <id>` as an MCP call. */
|
|
867
|
+
export function catalogueIntentMcpCall(intentId) {
|
|
868
|
+
return { tool: "oxygen_enrichment_catalog", arguments: { intent: intentId } };
|
|
869
|
+
}
|
|
870
|
+
// A Sequence's sending schedule or send window. Workflow language still wins
|
|
871
|
+
// ("a workflow that changes the send window nightly" is automation), and
|
|
872
|
+
// "sending hours" stays with the connected account's working hours.
|
|
873
|
+
function isSequenceScheduleIntent(query) {
|
|
874
|
+
return /\bsend(?:ing)? (?:schedule|window)s?\b/.test(query)
|
|
875
|
+
&& !/\b(workflow|automation|automate|webhook|cron)\b/.test(query);
|
|
876
|
+
}
|
|
877
|
+
// The unsubscribe option on a Sequence's emails, not the list of people who
|
|
878
|
+
// opted out: a list, import or do-not-contact ask stays with suppressions.
|
|
879
|
+
function isSequenceUnsubscribeIntent(query) {
|
|
880
|
+
return /\bunsubscribe\b/.test(query)
|
|
881
|
+
&& !/\b(workflow|automation|automate|webhook|cron)\b/.test(query)
|
|
882
|
+
&& !/\b(unsubscribed|lists?|import\w*|suppress\w*|block ?list|blocklist|do not contact|dnc)\b/.test(query);
|
|
883
|
+
}
|
|
884
|
+
/** A post's first comment is set with the post; `publishing posts update` takes the same flag. */
|
|
885
|
+
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';
|
|
886
|
+
/**
|
|
887
|
+
* The comment OXYGEN posts right after a post goes out ("put the link in the first
|
|
888
|
+
* comment") is part of the post, not a reply to one. The 2026-09-29 baselines found
|
|
889
|
+
* no flag, guessed `content.first_comment`, and searched in vain.
|
|
890
|
+
*/
|
|
891
|
+
function isFirstCommentIntent(query) {
|
|
892
|
+
return /\bfirst comments?\b/.test(query)
|
|
893
|
+
&& !/\b(repl(?:y|ies|ied)|respond\w*|answer\w*)\b/.test(query)
|
|
894
|
+
// Reading comments people left is owned-post comment work, not setting one.
|
|
895
|
+
&& !/\b(show|list|read|see|view|who|whose)\b/.test(query);
|
|
896
|
+
}
|
|
703
897
|
export const COMPANY_PAGE_FOLLOWERS_COMMAND = "oxygen tables followers check --company <linkedin company page URL> --records <n> --json";
|
|
704
898
|
/**
|
|
705
899
|
* "The people who follow HubSpot on LinkedIn" / "a competitor's followers" is a
|
|
@@ -735,8 +929,10 @@ function isOpenLinkedInRequestsIntent(query) {
|
|
|
735
929
|
function explicitCapabilityIntent(query) {
|
|
736
930
|
if (/\b(infographic|graphic designer|render html|carousel pages|visual design|gtm flow image)\b/.test(query))
|
|
737
931
|
return ROUTE_BY_ID.get("visual-rendering") ?? null;
|
|
738
|
-
if (
|
|
932
|
+
if (isUgcIntent(query))
|
|
739
933
|
return ROUTE_BY_PRIMITIVE.get("publishing") ?? null;
|
|
934
|
+
if (isWorkspaceVoiceIntent(query))
|
|
935
|
+
return ROUTE_BY_ID.get("knowledge-graph") ?? null;
|
|
740
936
|
if (isOwnLinkedInNetworkIntent(query) || isOpenLinkedInRequestsIntent(query))
|
|
741
937
|
return ROUTE_BY_ID.get("connected-linkedin") ?? null;
|
|
742
938
|
if (isCompanyPageFollowersIntent(query))
|
|
@@ -750,7 +946,7 @@ function explicitCapabilityIntent(query) {
|
|
|
750
946
|
// names every attached channel (LinkedIn + WhatsApp + email). Resolve this
|
|
751
947
|
// before public LinkedIn research, whose generic "profile" wording would
|
|
752
948
|
// otherwise route an account-readiness question to scraper tools.
|
|
753
|
-
if (isSenderProfileIntent(query)) {
|
|
949
|
+
if (isSenderProfileIntent(query) || isSenderCopyVariableIntent(query)) {
|
|
754
950
|
return ROUTE_BY_PRIMITIVE.get("sequences") ?? null;
|
|
755
951
|
}
|
|
756
952
|
if (isInboxAvatarIntent(query)) {
|
|
@@ -773,6 +969,10 @@ function explicitCapabilityIntent(query) {
|
|
|
773
969
|
// enrichment baseline).
|
|
774
970
|
if (/\bprofile viewers?\b/.test(query))
|
|
775
971
|
return ROUTE_BY_ID.get("connected-linkedin") ?? null;
|
|
972
|
+
// Ahead of the keyword-post, workflow and owned-post-comment rules below, which
|
|
973
|
+
// claimed "a first comment on my LinkedIn post" and "... automatically".
|
|
974
|
+
if (isFirstCommentIntent(query))
|
|
975
|
+
return ROUTE_BY_PRIMITIVE.get("publishing") ?? null;
|
|
776
976
|
if (isLinkedInKeywordPostIntent(query))
|
|
777
977
|
return ROUTE_BY_ID.get("signals") ?? null;
|
|
778
978
|
// 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 +981,12 @@ function explicitCapabilityIntent(query) {
|
|
|
781
981
|
// publish" to the connected-account catalog.
|
|
782
982
|
if (isFailedPostIntent(query))
|
|
783
983
|
return ROUTE_BY_PRIMITIVE.get("publishing") ?? null;
|
|
984
|
+
// When and how a Sequence sends: its sending schedule and its unsubscribe
|
|
985
|
+
// option. Ahead of the workflow rule, whose bare "schedule" sent the 2026-09-29
|
|
986
|
+
// unified-launch acceptance agent's "sending schedule" search to Workflows and
|
|
987
|
+
// hid every Sequences command.
|
|
988
|
+
if (isSequenceScheduleIntent(query) || isSequenceUnsubscribeIntent(query))
|
|
989
|
+
return ROUTE_BY_PRIMITIVE.get("sequences") ?? null;
|
|
784
990
|
if (isHostedWorkflowIntent(query))
|
|
785
991
|
return ROUTE_BY_PRIMITIVE.get("workflows") ?? null;
|
|
786
992
|
if (isOwnedPostCommentIntent(query))
|
|
@@ -972,17 +1178,38 @@ function isLocalBusinessIntent(query) {
|
|
|
972
1178
|
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
1179
|
}
|
|
974
1180
|
function recommendationsFor(card, query) {
|
|
1181
|
+
// A program's lifecycle and a creator's voice are UGC work the Publishing card
|
|
1182
|
+
// also claims ("archive program", "creator voice"); before these they got the
|
|
1183
|
+
// post tools (UGC E2E pass, 2026-09-29).
|
|
1184
|
+
const lifecycle = card.primitive === "publishing" ? UGC_PROGRAM_LIFECYCLE[ugcProgramLifecycleVerb(query) ?? ""] : undefined;
|
|
1185
|
+
if (lifecycle)
|
|
1186
|
+
return { tools: [lifecycle[1], "oxygen_ugc_get"], commands: [lifecycle[0], "ugc programs list"] };
|
|
975
1187
|
if (card.primitive === "publishing" && isUgcCreatorSideIntent(query)) {
|
|
976
1188
|
return {
|
|
977
1189
|
tools: ["oxygen_ugc_get", "oxygen_ugc_application_create", "oxygen_ugc_invite_accept"],
|
|
978
1190
|
commands: ["ugc programs discover", "ugc programs apply", "ugc creators accept"],
|
|
979
1191
|
};
|
|
980
1192
|
}
|
|
1193
|
+
if (card.primitive === "publishing" && isUgcVoiceIntent(query)) {
|
|
1194
|
+
return {
|
|
1195
|
+
tools: ["oxygen_ugc_voice_import", "oxygen_ugc_voice_save", "oxygen_ugc_get"],
|
|
1196
|
+
commands: ["ugc voice import", "ugc voice save", "ugc voice get"],
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
981
1199
|
if (card.primitive === "publishing" && isUgcProgramIntent(query)) {
|
|
982
1200
|
return {
|
|
983
1201
|
tools: ["oxygen_ugc_program_create", "oxygen_ugc_invite_create", "oxygen_ugc_get"],
|
|
984
1202
|
commands: ["ugc programs create", "ugc creators invite", "ugc programs list"],
|
|
985
1203
|
exactCommand: "oxygen ugc programs create --product-url <your product site> --json",
|
|
1204
|
+
exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.ugcProgramCreate(),
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
if (card.primitive === "publishing" && isFirstCommentIntent(query)) {
|
|
1208
|
+
return {
|
|
1209
|
+
tools: ["oxygen_publishing_posts_create", "oxygen_publishing_posts_update", "oxygen_publishing_posts_get"],
|
|
1210
|
+
commands: ["publishing posts create", "publishing posts update", "publishing posts get"],
|
|
1211
|
+
exactCommand: PUBLISHING_FIRST_COMMENT_COMMAND,
|
|
1212
|
+
exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.publishingFirstComment(),
|
|
986
1213
|
};
|
|
987
1214
|
}
|
|
988
1215
|
if (card.id === "sourcing-and-provider-tools" && isCompanyUrlRecoveryIntent(query)) {
|
|
@@ -990,6 +1217,7 @@ function recommendationsFor(card, query) {
|
|
|
990
1217
|
tools: ["oxygen_companies_search_plan", "oxygen_companies_search_run"],
|
|
991
1218
|
commands: ["companies search plan", "companies search run"],
|
|
992
1219
|
exactCommand: COMPANY_URL_RECOVERY_COMMAND,
|
|
1220
|
+
exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.companyUrlRecovery(),
|
|
993
1221
|
};
|
|
994
1222
|
}
|
|
995
1223
|
if (card.primitive === "tables" && isCompanyPageFollowersIntent(query)) {
|
|
@@ -997,6 +1225,7 @@ function recommendationsFor(card, query) {
|
|
|
997
1225
|
tools: ["oxygen_tables_followers", "oxygen_tables_sources"],
|
|
998
1226
|
commands: ["tables followers check", "tables followers order", "tables sources"],
|
|
999
1227
|
exactCommand: COMPANY_PAGE_FOLLOWERS_COMMAND,
|
|
1228
|
+
exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.companyPageFollowers(),
|
|
1000
1229
|
};
|
|
1001
1230
|
}
|
|
1002
1231
|
if (card.id === "sourcing-and-provider-tools" && isLocalBusinessIntent(query)) {
|
|
@@ -1004,6 +1233,7 @@ function recommendationsFor(card, query) {
|
|
|
1004
1233
|
tools: ["oxygen_companies_search_preview", "oxygen_companies_search_run"],
|
|
1005
1234
|
commands: ["companies search filters", "companies search preview", "companies search run"],
|
|
1006
1235
|
exactCommand: LOCAL_BUSINESS_SCRAPE_COMMAND,
|
|
1236
|
+
exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.localBusinessScrape(),
|
|
1007
1237
|
};
|
|
1008
1238
|
}
|
|
1009
1239
|
if (card.id === "connected-linkedin" && isLinkedInAccountConnectIntent(query)) {
|
|
@@ -1011,6 +1241,7 @@ function recommendationsFor(card, query) {
|
|
|
1011
1241
|
tools: ["oxygen_senders_connect", "oxygen_senders_list", "oxygen_billing_commitments"],
|
|
1012
1242
|
commands: ["senders connect", "senders list", "billing commitments"],
|
|
1013
1243
|
exactCommand: "oxygen senders connect --country <cc> --json",
|
|
1244
|
+
exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.senderConnect(),
|
|
1014
1245
|
};
|
|
1015
1246
|
}
|
|
1016
1247
|
if (card.id === "connected-linkedin" && isLinkedInAccountLimitsIntent(query)) {
|
|
@@ -1122,6 +1353,7 @@ function recommendationsFor(card, query) {
|
|
|
1122
1353
|
tools: ["oxygen_columns_catalog", "oxygen_columns_add", "oxygen_columns_run", "oxygen_tables_query"],
|
|
1123
1354
|
commands: ["columns catalog", "columns add", "columns run", "tables query"],
|
|
1124
1355
|
exactCommand: ROW_CLASSIFICATION_COMMAND,
|
|
1356
|
+
exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.rowClassification(),
|
|
1125
1357
|
};
|
|
1126
1358
|
}
|
|
1127
1359
|
if (isCompanyResearchQuestionIntent(query)) {
|
|
@@ -1184,6 +1416,16 @@ function recommendationsFor(card, query) {
|
|
|
1184
1416
|
}
|
|
1185
1417
|
}
|
|
1186
1418
|
if (card.primitive === "sequences") {
|
|
1419
|
+
if (isSequenceScheduleIntent(query) || isSequenceUnsubscribeIntent(query)) {
|
|
1420
|
+
return {
|
|
1421
|
+
tools: ["oxygen_sequences_update", "oxygen_sequences_get"],
|
|
1422
|
+
commands: ["sequences update", "sequences get"],
|
|
1423
|
+
exactCommand: isSequenceScheduleIntent(query) ? SEQUENCE_SENDING_SCHEDULE_COMMAND : SEQUENCE_UNSUBSCRIBE_COMMAND,
|
|
1424
|
+
exactMcpCall: isSequenceScheduleIntent(query)
|
|
1425
|
+
? CAPABILITY_ROUTE_MCP_CALLS.sequenceSendingSchedule()
|
|
1426
|
+
: CAPABILITY_ROUTE_MCP_CALLS.sequenceUnsubscribe(),
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1187
1429
|
if (isSenderProfileIntent(query)) {
|
|
1188
1430
|
return {
|
|
1189
1431
|
tools: ["oxygen_senders_profiles_list", "oxygen_senders_profiles_get"],
|
|
@@ -1282,6 +1524,7 @@ function recommendationsFor(card, query) {
|
|
|
1282
1524
|
tools: ["oxygen_signals_search_preview", "oxygen_signals_search_run"],
|
|
1283
1525
|
commands: ["signals search preview", "signals search run"],
|
|
1284
1526
|
exactCommand: LINKEDIN_KEYWORD_POST_COMMAND,
|
|
1527
|
+
exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.linkedinKeywordPosts(),
|
|
1285
1528
|
};
|
|
1286
1529
|
}
|
|
1287
1530
|
if (card.primitive === "signals" && /\blinkedin\b.{0,40}\bengagers?\b/.test(query)) {
|
|
@@ -1296,6 +1539,13 @@ function recommendationsFor(card, query) {
|
|
|
1296
1539
|
commands: ["whatsapp accounts", "whatsapp get", "whatsapp limits get", "whatsapp connect"],
|
|
1297
1540
|
};
|
|
1298
1541
|
}
|
|
1542
|
+
// Brand voice is read from its wiki page; only a writing voice is synthesized.
|
|
1543
|
+
if (card.id === "knowledge-graph" && isWorkspaceVoiceIntent(query) && !/\bbrand voice\b/.test(query)) {
|
|
1544
|
+
return {
|
|
1545
|
+
tools: ["oxygen_context_synthesize", "oxygen_knowledge_search", "oxygen_knowledge_page_get"],
|
|
1546
|
+
commands: ["knowledge synthesize", "knowledge search", "knowledge page get"],
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1299
1549
|
return { tools: [...card.gatewayTools], commands: [...card.gatewayCommands] };
|
|
1300
1550
|
}
|
|
1301
1551
|
// An email-VERIFICATION ask names a grading verb next to the addresses, or the
|
|
@@ -1375,6 +1625,18 @@ function isSenderProfileIntent(query) {
|
|
|
1375
1625
|
|| (/\bprofiles?\b.{0,64}\b(?:linkedin|whatsapp|mailboxes?|inboxes?|email)\b/.test(query)
|
|
1376
1626
|
&& /\b(?:sender|sending|unif(?:y|ied))\b/.test(query));
|
|
1377
1627
|
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Naming whoever sends a step inside its copy ({{sender_first_name}}, a sign-off
|
|
1630
|
+
* with the sender's name) is Sequence step copy. A 2026-09-29 blind user asked
|
|
1631
|
+
* "preview rendered message per sender sign-off variable" and was routed to
|
|
1632
|
+
* comments and approvals, and "sign every email with …" reads as a one-off
|
|
1633
|
+
* Message. A bare "sender name" stays out: renaming an inbox's From is not copy.
|
|
1634
|
+
*/
|
|
1635
|
+
function isSenderCopyVariableIntent(query) {
|
|
1636
|
+
return /\bsender (?:first|last|full) name\b/.test(query)
|
|
1637
|
+
|| /\bsign off (?:variable|with|per sender)\b|\bper sender sign off\b/.test(query)
|
|
1638
|
+
|| /\bsign (?:every|each|all) (?:emails?|messages?|dms?|notes?) with\b/.test(query);
|
|
1639
|
+
}
|
|
1378
1640
|
/** The account's own working hours or daily caps (`senders limits`), not its network. */
|
|
1379
1641
|
/**
|
|
1380
1642
|
* 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;
|
|
@@ -45,6 +45,10 @@ export const COLLAB_SUBJECT_KINDS = [
|
|
|
45
45
|
"project",
|
|
46
46
|
"sender_profile",
|
|
47
47
|
"voice_number",
|
|
48
|
+
"signal_type",
|
|
49
|
+
"dashboard",
|
|
50
|
+
"agent",
|
|
51
|
+
"ad_search_collection",
|
|
48
52
|
];
|
|
49
53
|
export function isCollabSubjectKind(value) {
|
|
50
54
|
return typeof value === "string" && COLLAB_SUBJECT_KINDS.includes(value);
|
|
@@ -74,6 +78,10 @@ export const COLLAB_SUBJECT_LABELS = {
|
|
|
74
78
|
project: { one: "project", many: "projects" },
|
|
75
79
|
sender_profile: { one: "sender profile", many: "sender profiles" },
|
|
76
80
|
voice_number: { one: "phone number", many: "phone numbers" },
|
|
81
|
+
signal_type: { one: "signal type", many: "signal types" },
|
|
82
|
+
dashboard: { one: "dashboard", many: "dashboards" },
|
|
83
|
+
agent: { one: "agent", many: "agents" },
|
|
84
|
+
ad_search_collection: { one: "saved Ads search", many: "saved Ads searches" },
|
|
77
85
|
};
|
|
78
86
|
/**
|
|
79
87
|
* What a thread on each kind is usually about, for `--help` and MCP tool
|
|
@@ -100,6 +108,10 @@ export const COLLAB_SUBJECT_PROSE = {
|
|
|
100
108
|
project: "a project's scope",
|
|
101
109
|
sender_profile: "a sending identity's accounts and inboxes",
|
|
102
110
|
voice_number: "a dialing number's caps and warmup",
|
|
111
|
+
signal_type: "a signal type's classification and use",
|
|
112
|
+
dashboard: "a dashboard's layout and measures",
|
|
113
|
+
agent: "an Agent's configuration and runs",
|
|
114
|
+
ad_search_collection: "a saved Ads search's targeting query",
|
|
103
115
|
};
|
|
104
116
|
/** Oxford-comma list: ["a"] -> "a"; ["a","b"] -> "a and b"; ["a","b","c"] -> "a, b, and c". */
|
|
105
117
|
function formatProseList(items) {
|
|
@@ -213,11 +213,15 @@ export function columnDecisionOutputSchema(decision) {
|
|
|
213
213
|
const properties = {
|
|
214
214
|
[decision.field]: answer,
|
|
215
215
|
...(decision.mode === "score"
|
|
216
|
-
? { level: { type: ["string", "null"], enum: [...(decision.levels ?? []), null], description: "
|
|
216
|
+
? { level: { type: ["string", "null"], enum: [...(decision.levels ?? []), null], description: "Level nearest the numeric score; it can differ from the most probable level." } }
|
|
217
217
|
: {}),
|
|
218
|
-
confidence: { type: "number", description:
|
|
218
|
+
confidence: { type: "number", description: decision.mode === "score"
|
|
219
|
+
? "System One confidence, 0 to 1. It is separate from level probabilities, not their top share or the gap between the top two."
|
|
220
|
+
: "How sure the classifier is, 0 to 1." },
|
|
219
221
|
uncertain: { type: "boolean", description: "True when confidence is below the column's floor." },
|
|
220
|
-
reason: { type: "string", description:
|
|
222
|
+
reason: { type: "string", description: decision.mode === "score"
|
|
223
|
+
? "Readable level, confidence, and top probabilities; it does not explain the row evidence."
|
|
224
|
+
: "The answer and its confidence in words." },
|
|
221
225
|
};
|
|
222
226
|
return {
|
|
223
227
|
type: "object",
|