@oxygen-agent/cli 1.987.20 → 1.1010.1
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.d.ts +0 -2
- package/dist/admin-primary-providers-render.js +1 -1
- package/dist/browser-login.js +1 -4
- package/dist/column-decision-options.d.ts +20 -0
- package/dist/column-decision-options.js +54 -0
- package/dist/command-manifest.d.ts +3 -2
- package/dist/command-manifest.js +25 -2
- package/dist/credentials.d.ts +1 -1
- package/dist/functions-commands.js +13 -9
- package/dist/help.d.ts +8 -0
- package/dist/help.js +46 -0
- package/dist/index.js +2751 -242
- package/dist/knowledge-mirror.d.ts +2 -2
- package/dist/runtime.d.ts +0 -15
- package/dist/runtime.js +1 -1
- package/dist/search-ai-filter-notice.d.ts +17 -0
- package/dist/search-ai-filter-notice.js +38 -0
- package/dist/session.d.ts +4 -3
- package/dist/skills.d.ts +8 -7
- package/dist/skills.js +58 -20
- package/dist/transcript.d.ts +2 -1
- package/dist/util.d.ts +10 -1
- package/dist/util.js +14 -2
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +296 -140
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +9 -0
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +34 -0
- package/node_modules/@oxygen/formula/dist/coerce.d.ts +10 -0
- package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
- package/node_modules/@oxygen/formula/dist/formula-functions.js +65 -0
- package/node_modules/@oxygen/formula/dist/hash.d.ts +19 -0
- package/node_modules/@oxygen/formula/dist/hash.js +199 -0
- package/node_modules/@oxygen/formula/dist/value-cleaners.d.ts +6 -1
- package/node_modules/@oxygen/formula/dist/value-cleaners.js +10 -26
- package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +13 -0
- package/node_modules/@oxygen/shared/dist/array-utils.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/array-utils.js +11 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +99 -22
- package/node_modules/@oxygen/shared/dist/billing.js +195 -40
- package/node_modules/@oxygen/shared/dist/byok-connect.js +5 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +27 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +311 -28
- package/node_modules/@oxygen/shared/dist/cli-http-error.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/cli-http-error.js +8 -0
- package/node_modules/@oxygen/shared/dist/cli-result.js +1 -0
- package/node_modules/@oxygen/shared/dist/column-autofill.d.ts +52 -0
- package/node_modules/@oxygen/shared/dist/column-autofill.js +62 -0
- package/node_modules/@oxygen/shared/dist/column-decision.d.ts +50 -0
- package/node_modules/@oxygen/shared/dist/column-decision.js +228 -0
- package/node_modules/@oxygen/shared/dist/column-output-fields.js +2 -6
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +113 -0
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +548 -0
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +6 -6
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +6 -6
- package/node_modules/@oxygen/shared/dist/cutover-freeze.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/cutover-freeze.js +52 -0
- package/node_modules/@oxygen/shared/dist/data-suppliers.d.ts +57 -0
- package/node_modules/@oxygen/shared/dist/data-suppliers.js +59 -0
- package/node_modules/@oxygen/shared/dist/deploy-env.d.ts +74 -0
- package/node_modules/@oxygen/shared/dist/deploy-env.js +82 -0
- package/node_modules/@oxygen/shared/dist/dnc-rules.d.ts +130 -0
- package/node_modules/@oxygen/shared/dist/dnc-rules.js +221 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +107 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +809 -0
- package/node_modules/@oxygen/shared/dist/error-message.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/error-message.js +3 -0
- package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -3
- package/node_modules/@oxygen/shared/dist/external-write-policy.d.ts +33 -0
- package/node_modules/@oxygen/shared/dist/external-write-policy.js +68 -0
- package/node_modules/@oxygen/shared/dist/format-percent.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/format-percent.js +13 -0
- package/node_modules/@oxygen/shared/dist/freemail-domains.d.ts +81 -0
- package/node_modules/@oxygen/shared/dist/freemail-domains.js +157 -0
- package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.js +1 -1
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +60 -4
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +125 -10
- package/node_modules/@oxygen/shared/dist/index.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/index.js +15 -0
- package/node_modules/@oxygen/shared/dist/json-path.js +1 -3
- package/node_modules/@oxygen/shared/dist/knowledge-bases.js +1 -3
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +2 -2
- package/node_modules/@oxygen/shared/dist/langfuse.d.ts +44 -1
- package/node_modules/@oxygen/shared/dist/langfuse.js +416 -14
- package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +33 -0
- package/node_modules/@oxygen/shared/dist/linkedin-countries.js +361 -0
- package/node_modules/@oxygen/shared/dist/linkedin-country-timezones.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/linkedin-country-timezones.js +276 -0
- package/node_modules/@oxygen/shared/dist/linkedin-message-deletion.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/linkedin-message-deletion.js +5 -0
- package/node_modules/@oxygen/shared/dist/linkedin-post-keywords.d.ts +44 -0
- package/node_modules/@oxygen/shared/dist/linkedin-post-keywords.js +116 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +96 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +123 -0
- package/node_modules/@oxygen/shared/dist/llm-durable-capture.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/llm-durable-capture.js +89 -0
- package/node_modules/@oxygen/shared/dist/llm-prompts.d.ts +75 -0
- package/node_modules/@oxygen/shared/dist/llm-prompts.js +161 -0
- package/node_modules/@oxygen/shared/dist/log-sink-selector.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/log-sink-selector.js +56 -0
- package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/log.js +6 -1
- package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.d.ts +90 -0
- package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.js +130 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +21 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.js +73 -0
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +79 -0
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +366 -0
- package/node_modules/@oxygen/shared/dist/plan-capabilities.js +1 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +23 -22
- package/node_modules/@oxygen/shared/dist/plan-limits.js +45 -18
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +48 -41
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +36 -25
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +22 -22
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +40 -34
- package/node_modules/@oxygen/shared/dist/product-analytics-environment.js +9 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +36 -2
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +36 -1
- package/node_modules/@oxygen/shared/dist/rate-window.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/rate-window.js +8 -0
- package/node_modules/@oxygen/shared/dist/redaction.js +4 -1
- package/node_modules/@oxygen/shared/dist/research-output-contract.js +1 -3
- package/node_modules/@oxygen/shared/dist/scraper-lane-credential.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/scraper-lane-credential.js +23 -0
- package/node_modules/@oxygen/shared/dist/search-vocab.js +4 -5
- package/node_modules/@oxygen/shared/dist/select-options.js +6 -1
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +1 -5
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +23 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +115 -5
- package/node_modules/@oxygen/shared/dist/signup-lead-payload.d.ts +80 -0
- package/node_modules/@oxygen/shared/dist/signup-lead-payload.js +198 -0
- package/node_modules/@oxygen/shared/dist/social-capabilities.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/social-capabilities.js +25 -16
- package/node_modules/@oxygen/shared/dist/social-post-metrics-core.d.ts +32 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-core.js +32 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-linkedin.d.ts +31 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-linkedin.js +103 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-series.d.ts +96 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-series.js +213 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-x.d.ts +13 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-x.js +78 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics.d.ts +36 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics.js +51 -0
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +22 -10
- package/node_modules/@oxygen/shared/dist/spend-safety.js +15 -21
- package/node_modules/@oxygen/shared/dist/sql-rows.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/sql-rows.js +3 -0
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +36 -0
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +184 -0
- package/node_modules/@oxygen/shared/dist/stripe-subscription-kind.d.ts +41 -0
- package/node_modules/@oxygen/shared/dist/stripe-subscription-kind.js +44 -0
- package/node_modules/@oxygen/shared/dist/table-limits.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/table-limits.js +3 -0
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +94 -0
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +298 -0
- package/node_modules/@oxygen/shared/dist/telemetry.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +28 -2
- package/node_modules/@oxygen/shared/dist/type-guards.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/type-guards.js +35 -0
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +22 -11
- package/node_modules/@oxygen/shared/dist/ugc.js +10 -0
- package/node_modules/@oxygen/shared/dist/value-readers.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/value-readers.js +59 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/version.generated.js +2 -0
- package/node_modules/@oxygen/shared/dist/version.js +8 -1
- package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +0 -23
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +31 -0
- package/node_modules/@oxygen/shared/package.json +59 -0
- package/node_modules/@oxygen/workflows/dist/graph/expression.js +2 -5
- package/node_modules/@oxygen/workflows/dist/graph/params.js +1 -1
- package/package.json +3 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { findEnrichmentIntentByAlias } from "./enrichment-intents.js";
|
|
1
2
|
import { RECIPE_PRIMITIVES } from "./recipes.js";
|
|
2
3
|
// One compact semantic index covers every top-level /api/cli endpoint section.
|
|
3
4
|
// It is navigation, never execution: live schemas, prices, readiness, and
|
|
@@ -194,15 +195,18 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
194
195
|
id: "records",
|
|
195
196
|
layer: "Data",
|
|
196
197
|
primitive: "records",
|
|
197
|
-
owns: "Canonical companies, people, deals, identities, relationships, activities, merges, and CRM sync state.",
|
|
198
|
+
owns: "Canonical companies, people, deals, identities, relationships, activities, notes, tasks (reminders), files, merges, and CRM sync state.",
|
|
198
199
|
notFor: "Disposable sourcing, enrichment, or scoring work; use Tables before promotion.",
|
|
199
|
-
execution: "Operate the hosted native CRM and preserve field/relationship provenance.",
|
|
200
|
+
execution: "Operate the hosted native CRM and preserve field/relationship provenance. Importing FROM a connected CRM lands here via `crm sync import <provider> --object contacts|companies`, which pulls the whole object and has no list/segment filter. To import ONE HubSpot saved list or ONE Pipedrive saved filter into a new table the user can filter and enrich — optionally suppressing a do-not-contact list/filter first — use `tables hubspot import` or `tables pipedrive import` instead. Open work across the workspace is one read: `crm tasks list`.",
|
|
200
201
|
posture: "workspace_write",
|
|
201
202
|
gatewayTools: ["oxygen_crm_record_search", "oxygen_crm_record_get"],
|
|
202
|
-
gatewayCommands: ["crm search", "crm get", "crm describe"],
|
|
203
|
+
gatewayCommands: ["crm search", "crm get", "crm describe", "crm tasks list", "crm notes list"],
|
|
203
204
|
skills: ["oxygen-gtm"],
|
|
204
205
|
endpointSections: ["crm"],
|
|
205
|
-
|
|
206
|
+
// "reminder"/"to-do"/"open tasks" routed to workspace-access before these
|
|
207
|
+
// terms existed (blind acceptance, 2026-09-22): the customer's phrasing for
|
|
208
|
+
// the Tasks read has to land on the primitive that owns it.
|
|
209
|
+
intentTerms: ["record", "records", "crm", "company record", "contact", "person record", "deal", "timeline", "merge", "relationship", "note", "notes", "task", "tasks", "reminder", "reminders", "to-do", "open tasks", "follow-up", "attachment", "files on a record"],
|
|
206
210
|
},
|
|
207
211
|
{
|
|
208
212
|
id: "tables",
|
|
@@ -210,10 +214,10 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
210
214
|
primitive: "tables",
|
|
211
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.",
|
|
212
216
|
notFor: "Canonical CRM truth, message cadence, or an off-platform spreadsheet runtime.",
|
|
213
|
-
execution: "Create and run work in hosted OXYGEN Tables; validate a small sample before bounded paid runs. 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.",
|
|
214
218
|
posture: "mixed",
|
|
215
|
-
gatewayTools: ["oxygen_tables_create", "oxygen_columns_add", "oxygen_columns_catalog", "oxygen_enrich_column_preview", "oxygen_tables_link_bulk", "oxygen_callables_manage"],
|
|
216
|
-
gatewayCommands: ["tables create", "columns add", "columns catalog", "enrich-column preview", "tables link", "functions list", "functions draft"],
|
|
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
|
+
gatewayCommands: ["tables create", "tools search", "columns add", "columns catalog", "enrich-column preview", "tables link", "functions list", "functions draft"],
|
|
217
221
|
skills: ["oxygen-gtm", "oxygen-table-tidy", "oxygen-diagnostics", "oxygen-clay-migration", "oxygen-linkedin-marketing"],
|
|
218
222
|
endpointSections: ["action-columns", "callables", "functions", "columns", "company-enrichment", "enrich-column", "enrichment", "projects", "supabase", "table-action-items", "table-action-runs", "table-ingestion-runs", "tables"],
|
|
219
223
|
// "link"/"join"/"connect"/"relate" route here for `tables link`. Added after a
|
|
@@ -221,12 +225,19 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
221
225
|
// `tables create` / `columns add` / `enrich-column preview` — none of which
|
|
222
226
|
// do it. The agent only found the right command by grepping the raw 25k-line
|
|
223
227
|
// command manifest, which is not a discovery path a customer has.
|
|
228
|
+
// "hubspot list" / "saved segment" / "list import" route here for
|
|
229
|
+
// `tables hubspot import`. Added after the 2026-09-21 blind ACCEPTANCE run:
|
|
230
|
+
// the capability shipped that same day, the agent searched "import hubspot
|
|
231
|
+
// contacts", was routed to `records`, read `crm sync import --help`, found
|
|
232
|
+
// no --list flag, and reported segment-scoped import as a MISSING native
|
|
233
|
+
// capability. The records execution text now names the table lane too,
|
|
234
|
+
// because that is where this phrasing legitimately lands.
|
|
224
235
|
// "founders" / "parent company" / "cloud provider" / "company research" route
|
|
225
236
|
// here for `columns catalog`. Added after the 2026-09-16 blind baseline asked
|
|
226
237
|
// `capabilities search "founders of a company"` and got `route: null` with a
|
|
227
238
|
// generic sourcing pointer, then hand-wrote four research prompts for
|
|
228
239
|
// questions the template catalog now answers by key.
|
|
229
|
-
intentTerms: ["table", "spreadsheet", "rows", "column", "columns", "dataset", "csv", "import", "enrich", "enrichment", "waterfall", "score", "formula", "ai column", "lookup", "link", "link tables", "join", "connect", "relate", "relationship", "function", "functions", "reusable function", "function draft", "function version", "callable", "column template", "skill set", "grad school", "podcast appearance", "keynote", "github profile", "job fit", "founders", "parent company", "subsidiaries", "cloud provider", "company research", "research question", "research column", "web research", "template catalog", "ready-made", "offers demos", "naics"],
|
|
240
|
+
intentTerms: ["table", "spreadsheet", "rows", "column", "columns", "dataset", "csv", "import", "hubspot list", "hubspot segment", "pipedrive filter", "pipedrive", "saved list", "saved segment", "saved filter", "crm segment", "list import", "segment import", "filter import", "import a segment", "import a list", "import a filter", "enrich", "enrichment", "waterfall", "score", "formula", "ai column", "lookup", "link", "link tables", "join", "connect", "relate", "relationship", "function", "functions", "reusable function", "function draft", "function version", "callable", "column template", "skill set", "grad school", "podcast appearance", "keynote", "github profile", "job fit", "founders", "parent company", "subsidiaries", "cloud provider", "company research", "research question", "research column", "web research", "template catalog", "ready-made", "offers demos", "naics", "cost per row", "price per row", "what can i enrich", "enrichment cost", "credits per row"],
|
|
230
241
|
},
|
|
231
242
|
{
|
|
232
243
|
id: "messages",
|
|
@@ -274,15 +285,26 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
274
285
|
id: "dashboards",
|
|
275
286
|
layer: "Data",
|
|
276
287
|
primitive: "dashboards",
|
|
277
|
-
owns: "
|
|
278
|
-
notFor: "Arbitrary BI modeling or a separate analytics store.",
|
|
279
|
-
execution: "Compute hosted curated views over Records, Messages, Signals, Sequences
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
288
|
+
owns: "The curated funnel, channel, spend and needs-action reporting, PLUS the workspace's own saved dashboards: named pages of chart / view / iframe / rich-text / code / live-source widgets over tables and CRM objects.",
|
|
289
|
+
notFor: "Arbitrary BI modeling or a separate analytics store. Outreach volume and replies are not chartable yet — group a table or a CRM object, or read the curated funnel with `dashboards summary`.",
|
|
290
|
+
execution: "Compute hosted curated views over Records, Messages, Signals, Sequences and runs; store and serve user-defined dashboards from ox_dashboards.",
|
|
291
|
+
// Was `read_only` until v1.1002.0, which is when a workspace could first SAVE a
|
|
292
|
+
// dashboard. A blind user eval (2026-09-21) caught the drift the expensive way:
|
|
293
|
+
// the agent asked this router for "dashboard", was told read_only and
|
|
294
|
+
// "not for BI modeling", and only found the eight mutating commands by
|
|
295
|
+
// separately searching the command index. This record is step 4 of the CLI's
|
|
296
|
+
// own quickstart, so a stale posture here routes users away from what shipped.
|
|
297
|
+
// `mixed`, not workspace_write: this capability now spans a read (summary,
|
|
298
|
+
// widget data), a workspace write (dashboards, tabs, widgets) AND one
|
|
299
|
+
// external-reach spending call (a live_source refresh calls its bound
|
|
300
|
+
// provider). "read_write" is not a member of CapabilityPosture -- inventing
|
|
301
|
+
// it is what broke the shared build on the first attempt.
|
|
302
|
+
posture: "mixed",
|
|
303
|
+
gatewayTools: ["oxygen_dashboard_summary", "oxygen_dashboards_list", "oxygen_dashboards_create", "oxygen_dashboards_widget_kinds", "oxygen_dashboards_widget_add", "oxygen_dashboards_widget_data"],
|
|
304
|
+
gatewayCommands: ["dashboards summary", "dashboards list", "dashboards create", "dashboards widgets kinds", "dashboards widgets add", "dashboards widgets data"],
|
|
283
305
|
skills: ["oxygen-gtm"],
|
|
284
306
|
endpointSections: ["dashboards"],
|
|
285
|
-
intentTerms: ["dashboard", "report", "reporting", "funnel", "metrics", "kpi", "performance", "pipeline summary"],
|
|
307
|
+
intentTerms: ["dashboard", "report", "reporting", "funnel", "metrics", "kpi", "performance", "pipeline summary", "save", "saved dashboard", "custom dashboard", "widget", "chart", "weekly report", "monday", "come back to"],
|
|
286
308
|
},
|
|
287
309
|
{
|
|
288
310
|
id: "ads",
|
|
@@ -318,10 +340,15 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
318
340
|
primitive: "sequences",
|
|
319
341
|
owns: "Net-new LinkedIn outreach plus versioned multichannel programs, cadence, enrollment, sender limits, suppression, reply-stop, and dispatch.",
|
|
320
342
|
notFor: "A reply in an existing conversation or a direct one-off email; use Messages. Not for general deterministic automation.",
|
|
321
|
-
|
|
343
|
+
// The external-campaign sentence exists because a 2026-09-16 blind user asked
|
|
344
|
+
// to "push table rows to smartlead campaign", landed here, read the native
|
|
345
|
+
// email track's "Only 'instantly' is supported" and concluded Smartlead was
|
|
346
|
+
// unreachable. The governed path for a campaign the customer already runs
|
|
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. 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); the native email track of a Sequence stays Instantly-bound.",
|
|
322
349
|
posture: "external_write",
|
|
323
|
-
gatewayTools: ["oxygen_sequences_send", "oxygen_sequences_create", "oxygen_sequences_enroll", "oxygen_sequences_start", "oxygen_sequences_list"],
|
|
324
|
-
gatewayCommands: ["sequences send", "sequences create", "sequences enroll", "sequences start", "sequences list"],
|
|
350
|
+
gatewayTools: ["oxygen_sequences_send", "oxygen_sequences_create", "oxygen_sequences_enroll", "oxygen_sequences_start", "oxygen_sequences_list", "oxygen_sequences_push_external"],
|
|
351
|
+
gatewayCommands: ["sequences send", "sequences create", "sequences enroll", "sequences start", "sequences list", "sequences push-external"],
|
|
325
352
|
skills: ["oxygen-sequencer"],
|
|
326
353
|
endpointSections: ["senders", "sequencer", "sequences", "spintax", "suppressions", "voice"],
|
|
327
354
|
// "sender identity", "sender profile" and "phone number" are here because a
|
|
@@ -329,7 +356,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
329
356
|
// to workspace-access — "identity" matched the AUTH sense. The nouns a user
|
|
330
357
|
// reaches for when naming a sending identity or the dialing pool have to land
|
|
331
358
|
// on the group that actually owns `senders profiles` and `voice numbers`.
|
|
332
|
-
intentTerms: ["sequence", "campaign", "cadence", "enroll", "outreach", "linkedin message", "linkedin dm", "nurture", "follow up", "sender rotation", "stop on reply", "sender identity", "sending identity", "sender profile", "phone number", "dialing", "dialer", "call", "cold call"],
|
|
359
|
+
intentTerms: ["sequence", "campaign", "cadence", "enroll", "outreach", "linkedin message", "linkedin dm", "nurture", "follow up", "sender rotation", "stop on reply", "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"],
|
|
333
360
|
},
|
|
334
361
|
{
|
|
335
362
|
id: "publishing",
|
|
@@ -346,7 +373,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
346
373
|
// "remove program" / "delete program" landed on route: null in a blind eval
|
|
347
374
|
// (2026-09-10); a program is a UGC noun here, and its lifecycle verbs must
|
|
348
375
|
// resolve to the group that owns `ugc programs archive|delete|duplicate`.
|
|
349
|
-
intentTerms: ["publish", "publishing", "schedule post", "content calendar", "posting calendar", "approve post", "social publishing", "ugc", "creator program", "user generated content", "creator voice", "sponsored creator", "ugc program", "archive program", "delete program", "remove program", "duplicate program", "copy program"],
|
|
376
|
+
intentTerms: ["publish", "publishing", "schedule post", "content calendar", "posting calendar", "approve post", "social publishing", "video cover", "video thumbnail", "cover image", "post preview", "ugc", "creator program", "user generated content", "creator voice", "sponsored creator", "ugc program", "archive program", "delete program", "remove program", "duplicate program", "copy program"],
|
|
350
377
|
},
|
|
351
378
|
{
|
|
352
379
|
id: "workflows",
|
|
@@ -402,7 +429,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
402
429
|
gatewayCommands: ["search plan", "sourcing plan", "find company", "tools search"],
|
|
403
430
|
skills: ["oxygen-gtm"],
|
|
404
431
|
endpointSections: ["companies", "find", "lead-sourcing", "people", "search", "sourcing", "tools", "verify"],
|
|
405
|
-
intentTerms: ["find companies", "find people", "source leads", "build a list", "tam", "prospect", "provider", "tool catalog", "find email", "find phone"],
|
|
432
|
+
intentTerms: ["find companies", "find people", "source leads", "build a list", "tam", "prospect", "provider", "tool catalog", "find email", "find phone", "local businesses", "local business", "google maps", "storefronts"],
|
|
406
433
|
},
|
|
407
434
|
{
|
|
408
435
|
id: "linkedin-public-research",
|
|
@@ -470,15 +497,15 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
470
497
|
id: "connected-linkedin",
|
|
471
498
|
layer: "External",
|
|
472
499
|
primitive: null,
|
|
473
|
-
owns: "The workspace LinkedIn account network/private data, relationship context such as mutual connections, and connected-account writes used by Signals, Messages, Sequences, and Publishing.",
|
|
500
|
+
owns: "The workspace LinkedIn account network/private data — everyone you are connected to and everyone who follows you, captured into one self-refreshing 'LinkedIn Network' table — relationship context such as mutual connections, and connected-account writes used by Signals, Messages, Sequences, and Publishing.",
|
|
474
501
|
notFor: "Ordinary public or third-party LinkedIn research; use the cookieless native scraper.",
|
|
475
|
-
execution: "For mutual/shared connection counts, resolve the connected account with `oxygen senders list --json`, inspect `oxygen tools get linkedin.users_get --json`, then run `oxygen tools run linkedin.users_get --input-json '{\"identifier\":\"<profile-url>\",\"account_id\":\"<connected-account-id>\",\"notify\":false}' --mode live --json`. This read is non-notifying and no-bill. An unavailable count is explicit and never means zero; retrying the same account/profile pair is not expected to reveal a provider-hidden count. Otherwise use the owning primitive gateway; the connected account provides identity and transport, not a competing product runtime.",
|
|
502
|
+
execution: "To put your own network in a table — connections and followers, one row per person with a Relationship column, kept current in the background — run `oxygen linkedin network setup --json` (add --account <ref> for one account; `oxygen linkedin network status` reports the pace and each account's state, `oxygen linkedin network pause` switches it off). Open connection requests — received and sent, on every connected account, in one table that marks each one accepted or no_longer_pending when it disappears — are `oxygen linkedin invitations setup --json` (`status` and `pause` beside it). Profile viewers are the same per account: `oxygen viewers import --account <ref>` switches one on, `oxygen viewers pause --account <ref>` off, and `oxygen viewers status` lists every account's state and `viewed_account` (whose profile each person viewed). `oxygen viewers enrichment --on` enriches each NEW viewer's full profile automatically at the per-person price `viewers status` quotes (existing viewers never). It is free, read-only on the account, and paced under its own daily read budget. For mutual/shared connection counts, resolve the connected account with `oxygen senders list --json`, inspect `oxygen tools get linkedin.users_get --json`, then run `oxygen tools run linkedin.users_get --input-json '{\"identifier\":\"<profile-url>\",\"account_id\":\"<connected-account-id>\",\"notify\":false}' --mode live --json`. This read is non-notifying and no-bill. An unavailable count is explicit and never means zero; retrying the same account/profile pair is not expected to reveal a provider-hidden count. Otherwise use the owning primitive gateway; the connected account provides identity and transport, not a competing product runtime.",
|
|
476
503
|
posture: "mixed",
|
|
477
|
-
gatewayTools: ["oxygen_senders_list", "oxygen_linkedin_connections_import", "oxygen_inbox_list"],
|
|
478
|
-
gatewayCommands: ["senders list", "connections import", "inbox list"],
|
|
504
|
+
gatewayTools: ["oxygen_linkedin_network_setup", "oxygen_linkedin_invitations_setup", "oxygen_senders_list", "oxygen_linkedin_connections_import", "oxygen_inbox_list"],
|
|
505
|
+
gatewayCommands: ["linkedin network setup", "linkedin invitations setup", "senders list", "connections import", "inbox list"],
|
|
479
506
|
skills: ["oxygen-linkedin-marketing", "oxygen-sequencer", "oxygen-unibox"],
|
|
480
507
|
endpointSections: ["linkedin"],
|
|
481
|
-
intentTerms: ["my linkedin", "our linkedin", "own linkedin", "connected linkedin", "connections", "mutual connections", "shared connections", "connections in common", "followers", "profile viewers", "sales navigator", "recruiter", "linkedin inbox"],
|
|
508
|
+
intentTerms: ["my linkedin", "our linkedin", "own linkedin", "connected linkedin", "connections", "mutual connections", "shared connections", "connections in common", "followers", "my network", "whole network", "linkedin network", "connections and followers", "profile viewers", "sales navigator", "recruiter", "linkedin inbox"],
|
|
482
509
|
},
|
|
483
510
|
{
|
|
484
511
|
id: "connected-whatsapp",
|
|
@@ -498,6 +525,8 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
498
525
|
export const OXYGEN_PRIMITIVE_ROUTES = OXYGEN_CAPABILITY_ROUTES
|
|
499
526
|
.filter((card) => card.primitive !== null);
|
|
500
527
|
const ROUTE_BY_ID = new Map(OXYGEN_CAPABILITY_ROUTES.map((card) => [card.id, card]));
|
|
528
|
+
/** Cards a catalogue row may decorate: the data surfaces, never sequences/posts/billing. */
|
|
529
|
+
const CATALOGUE_CARD_IDS = new Set(["tables", "sourcing-and-provider-tools", "signals"]);
|
|
501
530
|
const ROUTE_BY_PRIMITIVE = new Map(OXYGEN_PRIMITIVE_ROUTES.map((card) => [card.primitive, card]));
|
|
502
531
|
const NORMALIZED_PRIMITIVES = [...RECIPE_PRIMITIVES].sort();
|
|
503
532
|
const NORMALIZED_ROUTE_PRIMITIVES = OXYGEN_PRIMITIVE_ROUTES.map((card) => card.primitive).sort();
|
|
@@ -523,15 +552,62 @@ export function inferCapabilityRoute(query) {
|
|
|
523
552
|
if (!normalized)
|
|
524
553
|
return null;
|
|
525
554
|
const explicit = explicitCapabilityIntent(normalized);
|
|
526
|
-
const
|
|
555
|
+
const catalogRow = findEnrichmentIntentByAlias(normalized);
|
|
556
|
+
// Card scoring stays the owner of routing; the catalogue only breaks a
|
|
557
|
+
// no-match tie ("get the funding round for these accounts" names no card
|
|
558
|
+
// term) and decorates the data cards it belongs to.
|
|
559
|
+
const card = explicit ?? highestScoringRoute(normalized) ?? (catalogRow ? cardForCatalogueIntent(catalogRow) : null);
|
|
527
560
|
if (!card)
|
|
528
561
|
return null;
|
|
529
562
|
const recommendations = recommendationsFor(card, normalized);
|
|
563
|
+
// The card's own recommendations are a pinned contract (tool order, no
|
|
564
|
+
// exact command on plain enrichment asks); the catalogue path travels
|
|
565
|
+
// beside them in `catalogIntent`, never inside them.
|
|
566
|
+
const catalogue = catalogRow && CATALOGUE_CARD_IDS.has(card.id) ? catalogueRecommendations(catalogRow, card) : null;
|
|
530
567
|
return {
|
|
531
568
|
card,
|
|
532
569
|
recommendedTools: recommendations.tools,
|
|
533
570
|
recommendedCommands: recommendations.commands,
|
|
534
571
|
...(recommendations.exactCommand ? { exactCommand: recommendations.exactCommand } : {}),
|
|
572
|
+
...(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 } }
|
|
574
|
+
: {}),
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* A data-type ask ("tech stack", "decision makers at each account", "funding
|
|
579
|
+
* events") owns its card by taxonomy group: person/contact/company rows are
|
|
580
|
+
* Tables columns, decision-maker rows are the sourcing tools, signal rows are
|
|
581
|
+
* the signals card when one exists.
|
|
582
|
+
*/
|
|
583
|
+
function cardForCatalogueIntent(row) {
|
|
584
|
+
if (row.group === "decision_maker")
|
|
585
|
+
return ROUTE_BY_ID.get("sourcing-and-provider-tools") ?? null;
|
|
586
|
+
if (row.group === "signal")
|
|
587
|
+
return ROUTE_BY_ID.get("signals") ?? ROUTE_BY_PRIMITIVE.get("signals") ?? null;
|
|
588
|
+
return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
|
|
589
|
+
}
|
|
590
|
+
function catalogueRecommendations(row, card) {
|
|
591
|
+
const exactCommand = `oxygen enrichment catalog --intent ${row.id}`;
|
|
592
|
+
if (row.group === "decision_maker" || card.id === "sourcing-and-provider-tools") {
|
|
593
|
+
return {
|
|
594
|
+
tools: ["oxygen_enrichment_catalog", "oxygen_people_search_plan", "oxygen_people_search_run"],
|
|
595
|
+
commands: ["enrichment catalog", "people search plan", "people search run"],
|
|
596
|
+
exactCommand,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
if (row.group === "signal") {
|
|
600
|
+
return {
|
|
601
|
+
tools: ["oxygen_enrichment_catalog", "oxygen_signals_search_plan", "oxygen_signals_search_run"],
|
|
602
|
+
commands: ["enrichment catalog", "signals search plan", "signals search run"],
|
|
603
|
+
exactCommand,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
const find = row.surfaces.find ? [`oxygen_${row.surfaces.find.replace(/[\s-]+/g, "_")}`] : [];
|
|
607
|
+
return {
|
|
608
|
+
tools: ["oxygen_enrichment_catalog", "oxygen_columns_add", "oxygen_enrich_column_preview", ...find],
|
|
609
|
+
commands: ["enrichment catalog", "columns add", "enrich-column preview", ...(row.surfaces.find ? [row.surfaces.find] : [])],
|
|
610
|
+
exactCommand,
|
|
535
611
|
};
|
|
536
612
|
}
|
|
537
613
|
export function inferPrimitiveRoute(query) {
|
|
@@ -556,6 +632,9 @@ export function serializeCapabilityRoute(route) {
|
|
|
556
632
|
recommended_commands: route.recommendedCommands,
|
|
557
633
|
recommended_tools: route.recommendedTools,
|
|
558
634
|
...(route.exactCommand ? { exact_command: route.exactCommand } : {}),
|
|
635
|
+
...(route.catalogIntent
|
|
636
|
+
? { catalog_intent: { id: route.catalogIntent.id, label: route.catalogIntent.label, status: route.catalogIntent.status, group: route.catalogIntent.group, tools: route.catalogIntent.tools, commands: route.catalogIntent.commands, exact_command: route.catalogIntent.exactCommand } }
|
|
637
|
+
: {}),
|
|
559
638
|
hydrate: {
|
|
560
639
|
cli: "oxygen commands get <exact-command> --json",
|
|
561
640
|
mcp: "oxygen_capabilities_schema",
|
|
@@ -565,6 +644,23 @@ export function serializeCapabilityRoute(route) {
|
|
|
565
644
|
function normalizeIntent(query) {
|
|
566
645
|
return query.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
567
646
|
}
|
|
647
|
+
/**
|
|
648
|
+
* "LinkedIn posts about X" / "posts mentioning Y" / "monitor LinkedIn posts by
|
|
649
|
+
* keyword" is keyword post search, a Signals source with all-of / any-of /
|
|
650
|
+
* exclude rules. A blind user eval (2026-09-23) asked exactly this and was
|
|
651
|
+
* routed to Workflows, never learning `signals search --family linkedin_posts`
|
|
652
|
+
* exists. Posts FROM named profiles stay with the Profile Watcher, and the
|
|
653
|
+
* workspace's own posts stay with Posts. The free preview leads: it shows real
|
|
654
|
+
* posts, with the AI filter applied, before a table exists.
|
|
655
|
+
*/
|
|
656
|
+
const LINKEDIN_KEYWORD_POST_COMMAND = 'oxygen signals search preview --family linkedin_posts --keywords "<any of>" --all-keywords "<all of>" --exclude-keywords "<none of>" --ai-filter "<posts to keep>" --json';
|
|
657
|
+
function isLinkedInKeywordPostIntent(query) {
|
|
658
|
+
return /\blinkedin\b/.test(query)
|
|
659
|
+
&& /\b(posts?|posting)\b/.test(query)
|
|
660
|
+
&& /\b(keywords?|about|mention\w*|topics?|niche|talk\w* about|viral|trending)\b/.test(query)
|
|
661
|
+
&& !/\bprofiles?\b/.test(query)
|
|
662
|
+
&& !/\b(my|our) (own )?posts?\b/.test(query);
|
|
663
|
+
}
|
|
568
664
|
function isLinkedInProfileWatcherIntent(query) {
|
|
569
665
|
return /\blinkedin\b/.test(query)
|
|
570
666
|
&& /\bprofiles?\b/.test(query)
|
|
@@ -572,9 +668,62 @@ function isLinkedInProfileWatcherIntent(query) {
|
|
|
572
668
|
&& (/\b(engagers?|engaging|engagement|reactions?|comments?|posts?)\b/.test(query) || /\bprofile watcher\b/.test(query))
|
|
573
669
|
&& !isNetNewLinkedInInitiation(query);
|
|
574
670
|
}
|
|
671
|
+
/**
|
|
672
|
+
* "My LinkedIn connections and followers as a table" is the connected account's
|
|
673
|
+
* own network, not a Tables question. Without this the phrase "into a table"
|
|
674
|
+
* routed a blind user to the generic Tables card, which never names
|
|
675
|
+
* `linkedin network setup` (blind eval, 2026-09-22).
|
|
676
|
+
*/
|
|
677
|
+
function isOwnLinkedInNetworkIntent(query) {
|
|
678
|
+
if (!/\blinkedin\b/.test(query))
|
|
679
|
+
return false;
|
|
680
|
+
if (/\b(mutual|shared|in common|public|third party|cookieless|post engagers?)\b/.test(query))
|
|
681
|
+
return false;
|
|
682
|
+
const ownNetwork = /\b(my|our|own|whole|entire|existing) (linkedin )?(network|connections|followers)\b/.test(query)
|
|
683
|
+
|| /\b(everyone|everybody|people) (i am|i'm|we are|we're) connected (to|with)\b/.test(query)
|
|
684
|
+
|| /\blinkedin network\b/.test(query);
|
|
685
|
+
const bothHalves = /\bconnections?\b/.test(query) && /\bfollowers?\b/.test(query);
|
|
686
|
+
return (ownNetwork || bothHalves) && !isNetNewLinkedInInitiation(query);
|
|
687
|
+
}
|
|
688
|
+
export const COMPANY_PAGE_FOLLOWERS_COMMAND = "oxygen tables followers check --company <linkedin company page URL> --records <n> --json";
|
|
689
|
+
/**
|
|
690
|
+
* "The people who follow HubSpot on LinkedIn" / "a competitor's followers" is a
|
|
691
|
+
* vendor order of a company page's audience (`tables followers`), not the
|
|
692
|
+
* workspace's own network and not a signal. The Signals card claims the bare
|
|
693
|
+
* word "follower", so without this a blind user (2026-09-24) got only
|
|
694
|
+
* `signals *` from `commands search "company followers"` and priced the order
|
|
695
|
+
* through raw `tools run` instead. The own-network ask stays with its owner.
|
|
696
|
+
*/
|
|
697
|
+
function isCompanyPageFollowersIntent(query) {
|
|
698
|
+
const followers = /\bfollowers?\b/.test(query) || /\b(who|that) follows?\b/.test(query);
|
|
699
|
+
if (!followers || isOwnLinkedInNetworkIntent(query))
|
|
700
|
+
return false;
|
|
701
|
+
if (/\bfollows? (me|us)\b/.test(query) || /\b(my|our) (own )?(linkedin )?followers\b/.test(query))
|
|
702
|
+
return false;
|
|
703
|
+
return /\b(compan(y|ies)|competitors?|pages?|brands?|organi[sz]ations?)\b/.test(query)
|
|
704
|
+
|| (/\blinkedin\b/.test(query) && /\bfollows? (?!me\b|us\b)[a-z0-9]/.test(query));
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Open connection requests on the workspace's own accounts are the connected
|
|
708
|
+
* account's data. A blind user searching "pending LinkedIn connection requests"
|
|
709
|
+
* was routed nowhere near `linkedin invitations` (blind eval, 2026-09-24).
|
|
710
|
+
*/
|
|
711
|
+
function isOpenLinkedInRequestsIntent(query) {
|
|
712
|
+
if (!/\blinkedin\b/.test(query))
|
|
713
|
+
return false;
|
|
714
|
+
// Sending requests to leads is Sequences, not this table.
|
|
715
|
+
if (/\b(send|sending|enroll|sequence|campaign|outreach|cadence)\b/.test(query))
|
|
716
|
+
return false;
|
|
717
|
+
return /\b(connection|invitation|invite|connect) requests?\b/.test(query)
|
|
718
|
+
|| /\b(pending|open|unanswered|outstanding|received|sent) (linkedin )?(invitations?|invites?)\b/.test(query);
|
|
719
|
+
}
|
|
575
720
|
function explicitCapabilityIntent(query) {
|
|
576
721
|
if (/\b(infographic|graphic designer|render html|carousel pages|visual design|gtm flow image)\b/.test(query))
|
|
577
722
|
return ROUTE_BY_ID.get("visual-rendering") ?? null;
|
|
723
|
+
if (isOwnLinkedInNetworkIntent(query) || isOpenLinkedInRequestsIntent(query))
|
|
724
|
+
return ROUTE_BY_ID.get("connected-linkedin") ?? null;
|
|
725
|
+
if (isCompanyPageFollowersIntent(query))
|
|
726
|
+
return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
|
|
578
727
|
// A whole-motion ask ("set up inbound-led outbound for my posts") is a
|
|
579
728
|
// Recipe kit + playbook, not the connected-LinkedIn signals card that the
|
|
580
729
|
// "my LinkedIn … engagers" rule below would otherwise claim. Resolve it first.
|
|
@@ -598,6 +747,21 @@ function explicitCapabilityIntent(query) {
|
|
|
598
747
|
}
|
|
599
748
|
if (isLinkedInProfileWatcherIntent(query))
|
|
600
749
|
return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
|
|
750
|
+
// Profile viewers exist only as a connected account's data, and their capture
|
|
751
|
+
// owns its own on/off, pause and automatic enrichment. "Automatically enrich
|
|
752
|
+
// new profile viewers" reads as workflow language, but a hand-built workflow is
|
|
753
|
+
// the wrong answer when `viewers enrichment --on` is the switch (2026-09-23
|
|
754
|
+
// enrichment baseline).
|
|
755
|
+
if (/\bprofile viewers?\b/.test(query))
|
|
756
|
+
return ROUTE_BY_ID.get("connected-linkedin") ?? null;
|
|
757
|
+
if (isLinkedInKeywordPostIntent(query))
|
|
758
|
+
return ROUTE_BY_ID.get("signals") ?? null;
|
|
759
|
+
// 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
|
|
760
|
+
// unlock path: the 2026-09-25 stuck-post baseline sent "publishing failed post
|
|
761
|
+
// retry outcome uncertain" to Workflows and "why did my LinkedIn post fail to
|
|
762
|
+
// publish" to the connected-account catalog.
|
|
763
|
+
if (isFailedPostIntent(query))
|
|
764
|
+
return ROUTE_BY_PRIMITIVE.get("publishing") ?? null;
|
|
601
765
|
if (isHostedWorkflowIntent(query))
|
|
602
766
|
return ROUTE_BY_PRIMITIVE.get("workflows") ?? null;
|
|
603
767
|
if (isOwnedPostCommentIntent(query))
|
|
@@ -619,6 +783,17 @@ function explicitCapabilityIntent(query) {
|
|
|
619
783
|
if (isMutualLinkedInConnectionsIntent(query)) {
|
|
620
784
|
return ROUTE_BY_ID.get("connected-linkedin") ?? null;
|
|
621
785
|
}
|
|
786
|
+
// Authoring a post's video cover or previewing a draft is Publishing work even
|
|
787
|
+
// when the ask says "LinkedIn post"; the public-read rule below otherwise sent
|
|
788
|
+
// "choose video cover frame for linkedin post" to the paid cookieless research
|
|
789
|
+
// catalog (blind acceptance run, 2026-09-24).
|
|
790
|
+
if (isPostCoverOrPreviewIntent(query))
|
|
791
|
+
return ROUTE_BY_PRIMITIVE.get("publishing") ?? null;
|
|
792
|
+
// An X thread, poll, alt text or reply control is how an X post is authored, not
|
|
793
|
+
// a conversation thread (Messages): the 2026-09-25 X-formats baseline routed
|
|
794
|
+
// "thread" to Messages and "X poll post" nowhere.
|
|
795
|
+
if (isXPostFormatIntent(query))
|
|
796
|
+
return ROUTE_BY_PRIMITIVE.get("publishing") ?? null;
|
|
622
797
|
// A deferred acquisition motion starts with the public-data owner. This keeps
|
|
623
798
|
// "find/qualify now, contact later" from skipping straight to the final write
|
|
624
799
|
// step; the recommendation still hands the eventual initiation to Sequences.
|
|
@@ -647,6 +822,10 @@ function explicitCapabilityIntent(query) {
|
|
|
647
822
|
// Monday verify new signups" still lands on Workflows.
|
|
648
823
|
if (isEmailVerificationIntent(query))
|
|
649
824
|
return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
|
|
825
|
+
// "verify these phone numbers" is a verification column on a table, not a
|
|
826
|
+
// dialer action; without this the sequences card's call terms win.
|
|
827
|
+
if (/\b(verify|validate|check)\b[^.]{0,40}\b(phone|mobile)\b/.test(query))
|
|
828
|
+
return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
|
|
650
829
|
if (/\b(recipe|playbook|proven play|what should i do)\b/.test(query)) {
|
|
651
830
|
return ROUTE_BY_PRIMITIVE.get("recipes") ?? null;
|
|
652
831
|
}
|
|
@@ -655,6 +834,13 @@ function explicitCapabilityIntent(query) {
|
|
|
655
834
|
}
|
|
656
835
|
if (/\b(message )?reviews?\b/.test(query))
|
|
657
836
|
return ROUTE_BY_PRIMITIVE.get("messages") ?? null;
|
|
837
|
+
// Sorting each row into categories ("classify companies by industry with
|
|
838
|
+
// confidence", "label every row as agency, SaaS or other") is a Tables
|
|
839
|
+
// column, not a workspace Tag. Before this rule the 2026-09-25 blind baseline
|
|
840
|
+
// got `route: null` for "classify companies by industry with confidence",
|
|
841
|
+
// and the "label" wording fell to the Tags card below.
|
|
842
|
+
if (isRowClassificationIntent(query))
|
|
843
|
+
return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
|
|
658
844
|
if (/\b(tag|tags|label|labels)\b/.test(query))
|
|
659
845
|
return ROUTE_BY_PRIMITIVE.get("tags") ?? null;
|
|
660
846
|
if (/\b(disposable|working)\b.{0,30}\b(list|dataset|table)\b|\b(prospect|sourcing) table\b/.test(query)) {
|
|
@@ -695,6 +881,12 @@ function explicitCapabilityIntent(query) {
|
|
|
695
881
|
// company_enrich preset instead.
|
|
696
882
|
if (isCompanyUrlRecoveryIntent(query))
|
|
697
883
|
return ROUTE_BY_ID.get("sourcing-and-provider-tools") ?? null;
|
|
884
|
+
// A category-plus-place ask ("dental practices in Austin", "local businesses",
|
|
885
|
+
// "scrape Google Maps") is the companies-search `local` intent. A 2026-09-21
|
|
886
|
+
// blind user typing "find local businesses like dental practices in a city"
|
|
887
|
+
// got NO route and fell back to browsing every command group's --help.
|
|
888
|
+
if (isLocalBusinessIntent(query))
|
|
889
|
+
return ROUTE_BY_ID.get("sourcing-and-provider-tools") ?? null;
|
|
698
890
|
if (/\b(find emails?|find phones?|find companies|find people|source (?:leads?|a list|companies|people)|build (?:me )?a list|tam|prospect)\b/.test(query)) {
|
|
699
891
|
return ROUTE_BY_ID.get("sourcing-and-provider-tools") ?? null;
|
|
700
892
|
}
|
|
@@ -749,6 +941,17 @@ function highestScoringRoute(query) {
|
|
|
749
941
|
}
|
|
750
942
|
/** The exact `companies search plan` invocation for the url_recovery intent. */
|
|
751
943
|
export const COMPANY_URL_RECOVERY_COMMAND = 'oxygen companies search plan --prompt "Find the websites for these companies: <company names>" --source-intent url_recovery';
|
|
944
|
+
/** The free native preview for Google Maps businesses; import follows its reviewed quote. */
|
|
945
|
+
export const LOCAL_BUSINESS_SCRAPE_COMMAND = `oxygen companies search preview --source google_maps --filters-json '{"q":"<category> in <city, region>"}'`;
|
|
946
|
+
/**
|
|
947
|
+
* Storefronts and service businesses by category and place — the Scrape Google
|
|
948
|
+
* Maps Businesses table source. Named business categories are the everyday
|
|
949
|
+
* phrasing ("dentists in Austin"), so a short list of them is matched beside the
|
|
950
|
+
* generic words; "near me" and "in/around <place>" carry the same meaning.
|
|
951
|
+
*/
|
|
952
|
+
function isLocalBusinessIntent(query) {
|
|
953
|
+
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);
|
|
954
|
+
}
|
|
752
955
|
function recommendationsFor(card, query) {
|
|
753
956
|
if (card.id === "sourcing-and-provider-tools" && isCompanyUrlRecoveryIntent(query)) {
|
|
754
957
|
return {
|
|
@@ -757,6 +960,20 @@ function recommendationsFor(card, query) {
|
|
|
757
960
|
exactCommand: COMPANY_URL_RECOVERY_COMMAND,
|
|
758
961
|
};
|
|
759
962
|
}
|
|
963
|
+
if (card.primitive === "tables" && isCompanyPageFollowersIntent(query)) {
|
|
964
|
+
return {
|
|
965
|
+
tools: ["oxygen_tables_followers", "oxygen_tables_sources"],
|
|
966
|
+
commands: ["tables followers check", "tables followers order", "tables sources"],
|
|
967
|
+
exactCommand: COMPANY_PAGE_FOLLOWERS_COMMAND,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
if (card.id === "sourcing-and-provider-tools" && isLocalBusinessIntent(query)) {
|
|
971
|
+
return {
|
|
972
|
+
tools: ["oxygen_companies_search_preview", "oxygen_companies_search_run"],
|
|
973
|
+
commands: ["companies search filters", "companies search preview", "companies search run"],
|
|
974
|
+
exactCommand: LOCAL_BUSINESS_SCRAPE_COMMAND,
|
|
975
|
+
};
|
|
976
|
+
}
|
|
760
977
|
if (card.id === "connected-linkedin" && isMutualLinkedInConnectionsIntent(query)) {
|
|
761
978
|
return {
|
|
762
979
|
tools: ["oxygen_tools_get", "oxygen_tools_run_live", "oxygen_senders_list"],
|
|
@@ -843,6 +1060,17 @@ function recommendationsFor(card, query) {
|
|
|
843
1060
|
commands: ["tables watcher preview", "tables watcher create", "tables watcher get", "tables watcher update", "tables watcher pause", "tables watcher resume"],
|
|
844
1061
|
};
|
|
845
1062
|
}
|
|
1063
|
+
if (isRowClassificationIntent(query)) {
|
|
1064
|
+
// The classification list (free) names the template production has and,
|
|
1065
|
+
// where the deployment runs it, the native Classify column; any other
|
|
1066
|
+
// scheme is a prompt column (free to add). The run's dry run prices it,
|
|
1067
|
+
// and the labelled rows are filtered natively with `tables query`.
|
|
1068
|
+
return {
|
|
1069
|
+
tools: ["oxygen_columns_catalog", "oxygen_columns_add", "oxygen_columns_run", "oxygen_tables_query"],
|
|
1070
|
+
commands: ["columns catalog", "columns add", "columns run", "tables query"],
|
|
1071
|
+
exactCommand: ROW_CLASSIFICATION_COMMAND,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
846
1074
|
if (isCompanyResearchQuestionIntent(query)) {
|
|
847
1075
|
// A fact the template catalog already answers by key: list the keys first
|
|
848
1076
|
// (free), add the column by key (free), then price and run it. Ahead of
|
|
@@ -996,6 +1224,13 @@ function recommendationsFor(card, query) {
|
|
|
996
1224
|
};
|
|
997
1225
|
}
|
|
998
1226
|
}
|
|
1227
|
+
if (card.primitive === "signals" && isLinkedInKeywordPostIntent(query)) {
|
|
1228
|
+
return {
|
|
1229
|
+
tools: ["oxygen_signals_search_preview", "oxygen_signals_search_run"],
|
|
1230
|
+
commands: ["signals search preview", "signals search run"],
|
|
1231
|
+
exactCommand: LINKEDIN_KEYWORD_POST_COMMAND,
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
999
1234
|
if (card.primitive === "signals" && /\blinkedin\b.{0,40}\bengagers?\b/.test(query)) {
|
|
1000
1235
|
return {
|
|
1001
1236
|
tools: ["oxygen_engagement_list_engagers", "oxygen_signals_list", "oxygen_signals_leads_today"],
|
|
@@ -1020,6 +1255,36 @@ function isEmailVerificationIntent(query) {
|
|
|
1020
1255
|
const addressThenGrade = /\b(e ?mails?|addresses)\b.{0,40}\b(verif(?:y|ied|ication)|validat(?:e|ed|ion)|valid|invalid|deliverable|(?:will|would|might) bounce|bounce risk|risky|safe to (?:send|email)|catch ?all|accept ?all)\b/;
|
|
1021
1256
|
return gradeThenAddress.test(query) || addressThenGrade.test(query);
|
|
1022
1257
|
}
|
|
1258
|
+
/**
|
|
1259
|
+
* The free list of classification columns. Production answers it with its one
|
|
1260
|
+
* classification template (B2B or B2C); a deployment that runs the native
|
|
1261
|
+
* Classify column lists that entry here too. Any other scheme is an AI prompt
|
|
1262
|
+
* column through `columns add`, which the route recommends next.
|
|
1263
|
+
*/
|
|
1264
|
+
export const ROW_CLASSIFICATION_COMMAND = "oxygen columns catalog --family classification";
|
|
1265
|
+
/** Nouns another primitive owns: a classify ask that names one keeps that owner. */
|
|
1266
|
+
const ROW_CLASSIFICATION_OTHER_OWNER = /\b(inbox|repl(?:y|ies)|conversations?|threads?|messages?|dms?|e-?mails?|bounces?|signals?|visitors?|workflows?|deals?|crm|records?|icp|knowledge|wiki|pages?|ads|dashboards?|metrics?|agents?|runs)\b/;
|
|
1267
|
+
/** What one table row is: the noun a per-row classification is about. */
|
|
1268
|
+
const ROW_CLASSIFICATION_ROW_NOUN = /\b(rows?|columns?|tables?|compan(?:y|ies)|accounts?|leads?|contacts?|domains?|people|persons?)\b/;
|
|
1269
|
+
/**
|
|
1270
|
+
* Put every row of a table into one of a few buckets: classify or categorize
|
|
1271
|
+
* rows, companies, accounts, leads, contacts, domains or people (or anything
|
|
1272
|
+
* "with confidence"), "label every row as …", or a yes/no question asked per
|
|
1273
|
+
* row. The verb alone is not enough: "classify my records", "categorize my
|
|
1274
|
+
* workflows" or "classify leads into ICP tiers" name the primitive that owns
|
|
1275
|
+
* them, and conversation triage ("classify my inbox replies") keeps Messages.
|
|
1276
|
+
* A bare "tag these contacts" stays with Tags.
|
|
1277
|
+
*/
|
|
1278
|
+
function isRowClassificationIntent(query) {
|
|
1279
|
+
if (ROW_CLASSIFICATION_OTHER_OWNER.test(query))
|
|
1280
|
+
return false;
|
|
1281
|
+
const classifyVerb = /\b(classif(?:y|ies|ied|ying|ication)|categori[sz](?:e|es|ed|ing|ation))\b/.test(query);
|
|
1282
|
+
return (classifyVerb && (ROW_CLASSIFICATION_ROW_NOUN.test(query) || /\bconfidence\b/.test(query)))
|
|
1283
|
+
|| /\blabel(?:s|led|ed|ling|ing)?\b.{0,40}\b(?:each|every|all|per)\s+(?:rows?|compan(?:y|ies)|accounts?|leads?|contacts?|domains?)\b/.test(query)
|
|
1284
|
+
|| /\blabel(?:s|led|ed|ling|ing)?\b.{0,80}\bconfidence\b/.test(query)
|
|
1285
|
+
|| /\byes(?: or |\/)no\b.{0,60}\b(?:each|every|per)\s+(?:rows?|compan(?:y|ies)|accounts?|leads?|contacts?)\b/.test(query)
|
|
1286
|
+
|| /\b(?:each|every|per)\s+(?:rows?|compan(?:y|ies)|accounts?|leads?|contacts?)\b.{0,60}\byes(?: or |\/)no\b/.test(query);
|
|
1287
|
+
}
|
|
1023
1288
|
// "get company website URL from company name", "find the websites for this
|
|
1024
1289
|
// list of company names", "recover domains for company names" — a URL-ish
|
|
1025
1290
|
// noun, a company noun, a recovery verb, and the hint that the input is NAMES
|
|
@@ -1104,6 +1369,24 @@ function isSecondPartyDecisionIntent(query) {
|
|
|
1104
1369
|
&& !/\b(linkedin|public|published|post|posts|publishing)\b/.test(query);
|
|
1105
1370
|
return decisionLanguage || clientDecision || internalThread;
|
|
1106
1371
|
}
|
|
1372
|
+
/** An X (Twitter) post format: thread, poll, alt text, who-can-reply, quote, photo tags. */
|
|
1373
|
+
function isXPostFormatIntent(query) {
|
|
1374
|
+
return /\b(x|twitter|tweets?)\b/.test(query)
|
|
1375
|
+
&& /\b(thread|poll|alt text|image descriptions?|reply settings?|who can reply|quote|photo tags?|tag (?:people|someone|accounts?) in)\b/.test(query);
|
|
1376
|
+
}
|
|
1377
|
+
/** A post's video cover (thumbnail) or a preview of a post being authored. Competitor ad creative stays with Ads. */
|
|
1378
|
+
function isFailedPostIntent(query) {
|
|
1379
|
+
if (/\b(ads?|competitors?|runs?|workflows?|sequences?|tables?|comments?)\b/.test(query))
|
|
1380
|
+
return false;
|
|
1381
|
+
return /\b(posts?|tweets?)\b/.test(query)
|
|
1382
|
+
&& /\b(fail(s|ed|ing|ure)?|stuck|locked|uncertain|refused|rejected|retry|reschedul\w*)\b/.test(query);
|
|
1383
|
+
}
|
|
1384
|
+
function isPostCoverOrPreviewIntent(query) {
|
|
1385
|
+
if (/\b(ads?|competitors?)\b/.test(query))
|
|
1386
|
+
return false;
|
|
1387
|
+
return (/\b(cover|thumbnail)\b/.test(query) && /\b(post|video|draft)\b/.test(query))
|
|
1388
|
+
|| /\bpreview\b.{0,40}\b(post|draft)\b/.test(query);
|
|
1389
|
+
}
|
|
1107
1390
|
function isOwnedPostCommentIntent(query) {
|
|
1108
1391
|
if (!/\bcomments?\b/.test(query) || !/\b(linkedin|posts?|publishing)\b/.test(query))
|
|
1109
1392
|
return false;
|
|
@@ -1131,7 +1414,7 @@ function isPublicLinkedInRead(query) {
|
|
|
1131
1414
|
return false;
|
|
1132
1415
|
const disclaimsConnectedAccount = /\b(without|no|not using|does not require)\b.{0,24}\bconnected(?: linkedin)? account\b/.test(query);
|
|
1133
1416
|
const connectedOwnership = !disclaimsConnectedAccount
|
|
1134
|
-
&& /\b(my|our|own|owned|connected|connections|followers|inbox|messages|recruiter|sales\s*navigator|salesnav|unipile|viewers)\b/.test(query);
|
|
1417
|
+
&& /\b(my|our|own|owned|connected|connections|followers|inbox|messages|recruiter|sales\s*navigator|salesnav|unipile|viewers?)\b/.test(query);
|
|
1135
1418
|
if (connectedOwnership)
|
|
1136
1419
|
return false;
|
|
1137
1420
|
return /\b(comments?|companies|company|competitor|cookieless|engagers?|harvest|posts?|profiles?|public|reactions?|reactors?|scrape|scraper|search|third[ -]party)\b/.test(query);
|
|
@@ -51,6 +51,7 @@ export function exitCodeForErrorCode(code) {
|
|
|
51
51
|
case "max_credits_required":
|
|
52
52
|
case "approval_required":
|
|
53
53
|
case "effect_unknown_approval_required":
|
|
54
|
+
case "amplification_risk_acknowledgement_required":
|
|
54
55
|
case "spend_cap_required":
|
|
55
56
|
case "spend_cap_too_low":
|
|
56
57
|
case "insufficient_credits":
|