@oxygen-agent/cli 1.1014.86 → 1.1014.119

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.
Files changed (28) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +4 -1
  3. package/dist/help.js +5 -0
  4. package/dist/index.js +133 -80
  5. package/dist/skills.js +2 -0
  6. package/node_modules/@oxygen/shared/dist/capability-discovery.js +122 -0
  7. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +14 -4
  8. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +6 -5
  9. package/node_modules/@oxygen/shared/dist/contracts/knowledge-sync.d.ts +19 -0
  10. package/node_modules/@oxygen/shared/dist/contracts/knowledge-sync.js +8 -0
  11. package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.d.ts +7 -0
  12. package/node_modules/@oxygen/shared/dist/contracts/schema.d.ts +3 -0
  13. package/node_modules/@oxygen/shared/dist/contracts/schema.js +3 -0
  14. package/node_modules/@oxygen/shared/dist/contracts/table-feed-read.d.ts +1 -0
  15. package/node_modules/@oxygen/shared/dist/contracts/table-row-read.d.ts +2 -0
  16. package/node_modules/@oxygen/shared/dist/credit-gate.js +2 -1
  17. package/node_modules/@oxygen/shared/dist/mcp-event-contract.d.ts +12 -0
  18. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +1 -1
  19. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +1 -0
  20. package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +9 -2
  21. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +14 -1
  22. package/node_modules/@oxygen/shared/dist/sending-schedule.d.ts +7 -0
  23. package/node_modules/@oxygen/shared/dist/sending-schedule.js +10 -0
  24. package/node_modules/@oxygen/shared/dist/user-capability-routing.d.ts +2 -0
  25. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +8 -0
  26. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  27. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  28. package/package.json +1 -1
@@ -1018,6 +1018,18 @@ const CRM_ENRICHMENT_READ_VIEW = {
1018
1018
  };
1019
1019
  /** The card a route reports: Publishing's own, or its UGC view for a creator-program ask. */
1020
1020
  function routeCardFor(card, query) {
1021
+ const placementGuide = card.id === "sending-infrastructure" ? placementGuideRecommendation(query) : null;
1022
+ if (placementGuide)
1023
+ card = { ...card, posture: "read_only", execution: placementGuide.execution };
1024
+ const lookup = card.id === "tables" ? linkedinUrlFindingRecommendation(query) : null;
1025
+ if (lookup)
1026
+ card = { ...card, execution: lookup.execution };
1027
+ const pageResearch = card.id === "sourcing-and-provider-tools" ? pageScrapingRecommendation(query) : null;
1028
+ if (pageResearch)
1029
+ card = { ...card, execution: pageResearch.execution };
1030
+ const collaboration = card.id === "collaboration" ? collaborationRecommendation(query) : null;
1031
+ if (collaboration)
1032
+ card = { ...card, execution: collaboration.execution, posture: collaboration.posture };
1021
1033
  if (card.id === "records" && isCrmEnrichmentConfigurationRead(query))
1022
1034
  return { ...card, ...CRM_ENRICHMENT_READ_VIEW };
1023
1035
  const crmMaintenance = card.id === "records" ? crmMaintenanceRecommendation(query) : null;
@@ -1115,6 +1127,8 @@ function routeCardFor(card, query) {
1115
1127
  /** Search surfaces share the selected skill owner; a general router is fallback. */
1116
1128
  function preferredSkillForIntent(card, query) {
1117
1129
  if (card.id === "tables") {
1130
+ if (linkedinUrlFindingRecommendation(query))
1131
+ return "oxygen-enrichment";
1118
1132
  if (/\bclay\b/.test(query) && /\b(import|export|migrat\w*|mov\w*)\b/.test(query))
1119
1133
  return "oxygen-clay-migration";
1120
1134
  if (isTableMaintenanceIntent(query))
@@ -1926,6 +1940,8 @@ function explicitCapabilityIntent(query) {
1926
1940
  if (isWorkspacePagesIntent(query) || isWorkspaceSettingsIntent(query)) {
1927
1941
  return ROUTE_BY_ID.get("workspace-access") ?? null;
1928
1942
  }
1943
+ if (collaborationRecommendation(query))
1944
+ return ROUTE_BY_ID.get("collaboration") ?? null;
1929
1945
  if (/\btables?\b/.test(query) && isSecondPartyDecisionIntent(query))
1930
1946
  return ROUTE_BY_ID.get("collaboration") ?? null;
1931
1947
  if (tableReadRecommendation(query) || tableMaintenanceRecommendation(query))
@@ -2009,6 +2025,8 @@ function explicitCapabilityIntent(query) {
2009
2025
  }
2010
2026
  if (isHostedWorkflowIntent(query))
2011
2027
  return ROUTE_BY_PRIMITIVE.get("workflows") ?? null;
2028
+ if (placementGuideRecommendation(query))
2029
+ return ROUTE_BY_ID.get("sending-infrastructure") ?? null;
2012
2030
  if (isCompanySourcingIntent(query))
2013
2031
  return ROUTE_BY_ID.get("sourcing-and-provider-tools") ?? null;
2014
2032
  if (isOwnedPostCommentIntent(query))
@@ -2053,6 +2071,8 @@ function explicitCapabilityIntent(query) {
2053
2071
  // research input.
2054
2072
  if (isNetNewLinkedInInitiation(query))
2055
2073
  return ROUTE_BY_PRIMITIVE.get("sequences") ?? null;
2074
+ if (linkedinUrlFindingRecommendation(query))
2075
+ return ROUTE_BY_PRIMITIVE.get("tables") ?? null;
2056
2076
  if (isPublicLinkedInRead(query))
2057
2077
  return ROUTE_BY_ID.get("linkedin-public-research") ?? null;
2058
2078
  if (/\b(my|our|own)\b.{0,40}\blinkedin\b.{0,40}\b(post )?engagers?\b/.test(query)) {
@@ -2134,6 +2154,10 @@ function explicitCapabilityIntent(query) {
2134
2154
  // company_enrich preset instead.
2135
2155
  if (isCompanyUrlRecoveryIntent(query))
2136
2156
  return ROUTE_BY_ID.get("sourcing-and-provider-tools") ?? null;
2157
+ if (pageScrapingRecommendation(query))
2158
+ return ROUTE_BY_ID.get("sourcing-and-provider-tools") ?? null;
2159
+ if (/\bagency partners?\b/.test(query))
2160
+ return ROUTE_BY_ID.get("agency-directory") ?? null;
2137
2161
  // A category-plus-place ask ("dental practices in Austin", "local businesses",
2138
2162
  // "scrape Google Maps") is the companies-search `local` intent. A 2026-09-21
2139
2163
  // blind user typing "find local businesses like dental practices in a city"
@@ -2212,6 +2236,18 @@ function isPublishingPostIntent(query) {
2212
2236
  && !(/\b(find|search|scrape|research)\b/.test(query) && /\b(about|mention|containing)\b/.test(query));
2213
2237
  }
2214
2238
  function recommendationsFor(card, query) {
2239
+ const placementGuide = card.id === "sending-infrastructure" ? placementGuideRecommendation(query) : null;
2240
+ if (placementGuide)
2241
+ return placementGuide;
2242
+ const lookup = card.id === "tables" ? linkedinUrlFindingRecommendation(query) : null;
2243
+ if (lookup)
2244
+ return lookup;
2245
+ const pageResearch = card.id === "sourcing-and-provider-tools" ? pageScrapingRecommendation(query) : null;
2246
+ if (pageResearch)
2247
+ return pageResearch;
2248
+ const collaboration = card.id === "collaboration" ? collaborationRecommendation(query) : null;
2249
+ if (collaboration)
2250
+ return collaboration;
2215
2251
  if (card.id === "knowledge-graph" && isWorkspaceSkillIntent(query)) {
2216
2252
  return { tools: ["oxygen_workspace_skills_list", "oxygen_workspace_skills_create", "oxygen_workspace_skills_import", "oxygen_workspace_skills_get", "oxygen_workspace_skills_update", "oxygen_workspace_skills_archive"],
2217
2253
  commands: ["skills workspace list", "skills workspace create", "skills workspace import", "skills workspace get", "skills workspace update", "skills workspace archive"] };
@@ -2930,6 +2966,48 @@ function isRowClassificationIntent(query) {
2930
2966
  function isCompanyResearchQuestionIntent(query) {
2931
2967
  return /\b(founders?|founded by|parent compan(?:y|ies)|subsidiar(?:y|ies)|cloud provider|naics|column templates?|template catalog|ready[- ]made|research question|company research|offers? demos?|gives? demos?)\b/.test(query);
2932
2968
  }
2969
+ /** Supporting placement instructions belong to deliverability, even when the ask names an inbox. */
2970
+ function placementGuideRecommendation(query) {
2971
+ if (!/\bplacement(?:[- ]tests?)?\b/.test(query)
2972
+ || !/\b(guidance|guide|instructions|supporting|read|docs?|documentation)\b/.test(query)
2973
+ || /\b(run|send|create|connect|enable)\b/.test(query))
2974
+ return null;
2975
+ return {
2976
+ tools: ["oxygen_skills_get"],
2977
+ commands: ["skills get"],
2978
+ exactCommand: "oxygen skills get oxygen-deliverability --file references/placement.md --json",
2979
+ exactMcpCall: { tool: "oxygen_skills_get", arguments: { name: "oxygen-deliverability", file: "references/placement.md" } },
2980
+ execution: "Read oxygen-deliverability, references/placement.md for placement-test guidance and references/emailguard.md for inbox monitoring. Instruction reads do not run a test or enroll a mailbox. Keep the live preview, sender scope, credit ceiling and separate approvals for provider provisioning and test sends.",
2981
+ };
2982
+ }
2983
+ /** OXP-43.72: pricing qualifies the lookup or scrape, rather than changing its owner. */
2984
+ function linkedinUrlFindingRecommendation(query) {
2985
+ if (!/\blinkedin\b/.test(query) || !/\b(urls?|links?)\b/.test(query)
2986
+ || !/\b(find|finding|lookup|look up|resolve|missing)\b/.test(query)
2987
+ || /\b(scrape\w*|posts?|comments?|engagers?|followers?|connect|inbox|send\w*|publish\w*)\b/.test(query)
2988
+ || /\bcompany pages?\b/.test(query))
2989
+ return null;
2990
+ return {
2991
+ tools: ["oxygen_tools_search", "oxygen_columns_add", "oxygen_enrich_column_preview"],
2992
+ commands: ["tools search", "columns add", "enrich-column preview"],
2993
+ exactCommand: "oxygen tools search find_linkedin_url --json",
2994
+ exactMcpCall: { tool: "oxygen_tools_search", arguments: { query: "find_linkedin_url" } },
2995
+ execution: "Inspect the native LinkedIn URL waterfall and current pricing with tools search find_linkedin_url. Read oxygen-enrichment and its references/linkedin-url.md. Add the linkedin_url capability to existing rows, then preview the exact row scope for free before an approved run with a credit ceiling. Known-profile scraping is a separate operation.",
2996
+ };
2997
+ }
2998
+ function pageScrapingRecommendation(query) {
2999
+ if (!/\b(scrap(?:e|ing)|crawl\w*|fetch|extract)\b/.test(query)
3000
+ || !/\b(pages?|websites?|web ?sites?|web content|urls?)\b/.test(query)
3001
+ || /\b(linkedin|emails?|phones?|crm|records?)\b/.test(query))
3002
+ return null;
3003
+ return {
3004
+ tools: ["oxygen_tools_search", "oxygen_tools_get"],
3005
+ commands: ["tools search", "tools get"],
3006
+ exactCommand: "oxygen tools search firecrawl --json",
3007
+ exactMcpCall: { tool: "oxygen_tools_search", arguments: { query: "firecrawl" } },
3008
+ execution: "Discover public-page operations with tools search firecrawl, then hydrate one exact tool and its pricing. Read oxygen-sourcing and provider-runbooks/firecrawl.md. Preview the selected URLs and price before execution; live scraping requires approval for the exact URL scope and credit ceiling. For durable per-row research, use native Tables research columns. Discovery does not run a scrape.",
3009
+ };
3010
+ }
2933
3011
  function isCompanyUrlRecoveryIntent(query) {
2934
3012
  const urlNoun = /\b(websites?|web ?sites?|website urls?|domains?|urls?|homepages?)\b/;
2935
3013
  const companyNoun = /\b(compan(?:y|ies)|accounts?|organi[sz]ations?|brands?|vendors?)\b/;
@@ -3109,6 +3187,50 @@ function isNetNewLinkedInInitiation(query) {
3109
3187
  return false;
3110
3188
  return !/\b(sequence|campaign|cadence|enroll|nurture|multi[ -]step|multi[ -]recipient|leads|contacts|recipients|audience|rotate senders?|stop on reply|day\s*\d+)\b/.test(query);
3111
3189
  }
3190
+ /** Recommendations point to the mounted native owners, without conferring their write authority. */
3191
+ function collaborationRecommendation(query) {
3192
+ const memberRead = { tools: ["oxygen_workspace_members_list"], commands: ["approvals client list"],
3193
+ execution: "Read workspace member roles, Clerk profiles and pending invitations. This does not invite anyone or change access. Free.", posture: "read_only" };
3194
+ const member = /\b(members?|memberships?)\b/.test(query);
3195
+ const workspace = /\b(workspace|organization|org)\b/.test(query);
3196
+ const clientRole = /\bclient (?:role|access|login)\b/.test(query);
3197
+ const roleWrite = /\b(set|change|grant|clear|remove|revoke|restrict|promote|demote)\b/.test(query);
3198
+ if (clientRole && roleWrite && !/\b(crm|records?|contacts?|companies|api|oauth)\b/.test(query)) {
3199
+ const clear = /\b(clear|remove|revoke)\b/.test(query);
3200
+ return { tools: ["oxygen_members_client_role_set", ...memberRead.tools], commands: [clear ? "approvals client revoke" : "approvals client grant", ...memberRead.commands],
3201
+ execution: "An admin can set or clear the restricted client overlay; a pending invite is checked in Clerk. This sends no invitation and does not change the Clerk workspace role. Free.", posture: "workspace_write" };
3202
+ }
3203
+ if (workspace && member && roleWrite && /\b(roles?|admin|administrator)\b/.test(query))
3204
+ return {
3205
+ tools: ["oxygen_members_workspace_role_set", ...memberRead.tools], commands: ["orgs member-role", ...memberRead.commands],
3206
+ execution: "An admin changes a member's workspace role in Clerk before Oxygen mirrors it. Self-role changes and the last administrator are protected; the client overlay is separate. Free.", posture: "workspace_write",
3207
+ };
3208
+ if (((workspace && member) || clientRole || /\bwho is in (?:my|our|this|the) workspace\b/.test(query))
3209
+ && !/\b(invite|add|delete|cancel|enable|disable|set|change|grant|clear|remove|revoke|restrict|promote|demote)\b/.test(query))
3210
+ return memberRead;
3211
+ if (/\b(?:approval|launch|sign ?off) gates?\b/.test(query)) {
3212
+ const write = /\b(arm|set|enable|disable|require|change|turn)\b/.test(query);
3213
+ return { tools: write ? ["oxygen_approvals_gate_set", "oxygen_approval_gates_list"] : ["oxygen_approval_gates_list", "oxygen_approvals_gate_set"],
3214
+ commands: write ? ["approvals gate set", "approvals gate list"] : ["approvals gate list", "approvals gate set"],
3215
+ execution: write ? "An admin configures the approval gate; this grants no approval. Launch gates a sequence; signoff gates a table. The owning primitive enforces it. Free."
3216
+ : "Read configured approval gates and effective state for an exact subject. Reading a gate grants no approval. Free.", posture: write ? "workspace_write" : "read_only" };
3217
+ }
3218
+ if (/\bcomments?\b/.test(query) && /\b(internal|workspace|teammate|client)\b/.test(query)
3219
+ && !/\b(linkedin|public|published|posts?|publishing)\b/.test(query)) {
3220
+ if (/\b(edit|update|correct)\b/.test(query))
3221
+ return { tools: ["oxygen_comments_edit", "oxygen_comments_list"], commands: ["comments edit", "comments list"],
3222
+ execution: "Edit your own internal workspace comment by comment id; the author check and edit time are enforced by its native owner. Free.", posture: "workspace_write" };
3223
+ if (/\b(delete|retract|remove)\b/.test(query))
3224
+ return { tools: ["oxygen_comments_delete", "oxygen_comments_list"], commands: ["comments delete", "comments list"],
3225
+ execution: "Retract your own internal comment by comment id. Its audit placeholder remains; this does not delete a public post comment. Free.", posture: "workspace_write" };
3226
+ }
3227
+ if (/\b(cancel|withdraw|retract)\b/.test(query) && /\b(?:approval|sign ?off) requests?\b/.test(query))
3228
+ return {
3229
+ tools: ["oxygen_approvals_cancel", "oxygen_approvals_list"], commands: ["approvals cancel", "approvals list"],
3230
+ execution: "Withdraw an approval request you created, by request id; this grants no approval. The requester check remains enforced by the native owner. Free.", posture: "workspace_write",
3231
+ };
3232
+ return null;
3233
+ }
3112
3234
  function isSecondPartyDecisionIntent(query) {
3113
3235
  const decisionLanguage = /\b(sign ?off|signoff|approval request|request approval|approval gate|go no go)\b/.test(query);
3114
3236
  const clientDecision = /\bclient\b.{0,20}\b(approv\w*|sign ?off|review|decide|decision|login|role)\b/.test(query)
@@ -97,15 +97,25 @@ export const BROWSER_OPERATIONS = [
97
97
  operation("tables.resource.runs.cancel", `${run}/cancel`, "POST", "tables runs stop", "oxygen_table_resource_run_stop", "runs", "Cancel one durable Table run; never repeats external provider effects."),
98
98
  operation("tables.resource.runs.pause", `${run}/pause`, "POST", "tables runs pause", "oxygen_table_resource_run_pause", "runs", "Pause one durable Table run without discarding its approved scope."),
99
99
  operation("tables.resource.runs.resume", `${run}/resume`, "POST", "tables runs resume", "oxygen_table_resource_run_resume", "runs", "Resume a paused Table run within its existing approved scope and credit ceiling."),
100
- operation("tables.resource.runs.retry", `${run}/retry-failed`, "POST", "tables runs retry", "oxygen_table_resource_run_retry", "runs", "Retry failed cells within a Table run's approved scope. Uncertain external effects require approved_effect_unknown=true.", { approved_effect_unknown: bool() }, [], { spends: true }),
100
+ operation("tables.resource.runs.retry", `${run}/retry-failed`, "POST", "tables runs retry", "oxygen_table_resource_run_retry", "runs", "Retry failed cells; a fresh allowance requires approved, retry_plan_hash and max_additional_credits. Uncertain effects require separate approval.", { approved_effect_unknown: bool(), approved: bool(), retry_plan_hash: text(), max_additional_credits: num({ minimum: 0 }) }, [], { spends: true }),
101
101
  operation("tables.resource.runs.provider_summary", `${run}/provider-summary`, "GET", "tables runs provider-summary", "oxygen_table_resource_run_provider_summary", "runs", "Read a Table run's provider outcomes, fulfilled rows, costs and timing."),
102
102
  operation("tables.resource.ingestions.cancel", `${table}/ingestions/{ingestion_run}/cancel`, "POST", "tables ingestion stop", "oxygen_table_resource_ingestion_stop", "runs", "Cancel this Table's active hosted ingestion run."),
103
103
  operation("tables.resource.ingestions.retry", `${table}/ingestions/{ingestion_run}/retry-failed`, "POST", "tables ingestion retry", "oxygen_table_resource_ingestion_retry", "runs", "Retry failed items in this Table's hosted ingestion run."),
104
104
  operation("tables.resource.import", "/api/cli/tables/import", "POST", "tables upload", "oxygen_table_resource_upload", "tables", "Preview or import small uploaded CSV, JSON or XLSX bytes through the exact browser import contract. Preview writes no rows.", upload, ["content_base64", "file_name"], { multipart }),
105
105
  operation("billing.activate", "/api/cli/billing/activate", "POST", "billing activate", "oxygen_billing_activate", "billing", "Activate a trial subscription now and charge its saved payment method; administrator approval is required. An optional tier reprices the active trial in the same call.", { approved: bool(), tier: text() }, ["approved"], { spends: true }),
106
- operation("billing.checkout", "/api/cli/billing/checkout", "POST", "billing checkout", "oxygen_billing_checkout", "billing", "Open the same hosted plan checkout as the browser; the returned URL carries the payment approval step.", { plan: text(), interval: enumOf(["month", "year"]), term: enumOf(["month", "year"]), terms_version: text(), currency: text(), trial: bool(), return_to: text(), source: text(), blocked: text() }, ["plan"]),
107
- operation("billing.confirm", "/api/cli/billing/confirm", "POST", "billing confirm", "oxygen_billing_confirm", "billing", "Confirm an exact unexpired plan-change preview. approved accepts its payment, term and effective date; workspace administrator required.", { token: text(), approved: bool() }, ["token", "approved"], { spends: true }),
108
- operation("billing.portal", "/api/cli/billing/portal", "POST", "billing portal", "oxygen_billing_portal", "billing", "Create the workspace's authenticated Stripe billing-portal URL; requires workspace administrator authorization.", { returnUrl: text() }),
106
+ operation("billing.checkout", "/api/cli/billing/checkout", "POST", "billing checkout", "oxygen_billing_checkout", "billing", "Open browser Checkout; payment approves purchase. First purchase: admin/owner. New yearly plans use interval month, term year and the current terms_version.", {
107
+ plan: text("Plan key from billing balance available_plans, for example oxygen_49."),
108
+ interval: enumOf(["month", "year"], { description: "New purchases use month. year is legacy prepaid cadence, refused for new purchases." }),
109
+ term: enumOf(["month", "year"], { description: "year commits to twelve discounted monthly payments; month is the default." }),
110
+ terms_version: text("Required for term year: annual_monthly_terms.terms_version from billing balance."),
111
+ currency: text("Billing currency: usd (default)."),
112
+ trial: bool({ description: "Retired compatibility option; omit for a new paid subscription." }),
113
+ return_to: text("Same-origin path to return to after Checkout, for example /home."),
114
+ source: text("Optional upgrade-entry attribution; omit if unknown."),
115
+ blocked: text("Optional capability that led to the upgrade; omit if unknown."),
116
+ }, ["plan"]),
117
+ operation("billing.confirm", "/api/cli/billing/confirm", "POST", "billing confirm", "oxygen_billing_confirm", "billing", "Confirm a plan-change preview within 15 minutes; approved accepts payment, term and effective date. Unrestricted members allowed; restricted clients denied.", { token: text(), approved: bool() }, ["token", "approved"], { spends: true }),
118
+ operation("billing.portal", "/api/cli/billing/portal", "POST", "billing portal", "oxygen_billing_portal", "billing", "Create a Stripe billing-portal URL. Unrestricted members allowed; restricted clients denied.", { returnUrl: text() }),
109
119
  operation("billing.ownership.get", "/api/cli/billing/ownership", "GET", "billing ownership get", "oxygen_billing_ownership_get", "billing", "Read the active workspace's shared billing owner, candidate owners and credit cap."),
110
120
  operation("billing.ownership.set", "/api/cli/billing/ownership", "POST", "billing ownership set", "oxygen_billing_ownership_set", "billing", "Set the billing owner and optional monthly credit cap using the browser's administrator and workspace eligibility checks.", { billing_owner_organization_id: text(), monthly_credit_cap: nullable(num({ minimum: 0 })) }, ["billing_owner_organization_id"]),
111
121
  operation("billing.ownership.clear", "/api/cli/billing/ownership", "DELETE", "billing ownership clear", "oxygen_billing_ownership_clear", "billing", "Restore this workspace's own billing ownership after the same administrator checks as the browser."),
@@ -8,6 +8,7 @@ import { COPILOT_SESSIONS_LIST_CONTRACT } from "./copilot-sessions.js";
8
8
  import { CRM_FILE_ATTACH_CONTRACT, CRM_FILE_ATTACH_DESCRIPTION } from "./crm-support.js";
9
9
  import { spintaxContract } from "./spintax.js";
10
10
  import { betaFeaturesUpdateContract, emailDomainSuppressionListContract, emailDomainSuppressionRemoveContract, emailSuppressionRemoveContract, providerLimitListContract, providerLimitSelectorContract, providerLimitUpdateContract } from "./workspace-utilities.js";
11
+ import { knowledgeSyncReadContract } from "./knowledge-sync.js";
11
12
  const text = (description) => str(description === undefined ? {} : { description });
12
13
  const list = () => arr(str());
13
14
  const json = { type: "object", additionalProperties: true };
@@ -34,8 +35,8 @@ export const CUSTOMER_OPERATIONS = [
34
35
  operation("comments.edit", "/api/cli/collab/comments/{comment_id}", "PATCH", "comments edit", "oxygen_comments_edit", "collaboration", "Edit your own internal workspace comment; records the edit time.", { body: text() }, ["body"], mcpOnly),
35
36
  operation("comments.delete", "/api/cli/collab/comments/{comment_id}", "DELETE", "comments delete", "oxygen_comments_delete", "collaboration", "Retract your own internal workspace comment while retaining its audit trail.", {}, [], mcpOnly),
36
37
  operation("approvals.cancel", "/api/cli/collab/requests/{request_id}/cancel", "POST", "approvals cancel", "oxygen_approvals_cancel", "collaboration", "Withdraw an approval request you created; grants no approval.", { note: text() }, [], mcpOnly),
37
- operation("members.client_role", "/api/cli/collab/members/role", "POST", "approvals client grant", "oxygen_members_client_role_set", "collaboration", "Set or clear a member's restricted client role; workspace admin only.", { email: text(), role: nullable(enumOf(["client"])) }, ["email", "role"], mcpOnly),
38
- operation("members.workspace_role", "/api/cli/collab/members/workspace-role", "POST", "orgs member-role", "oxygen_members_workspace_role_set", "collaboration", "Change a member's workspace role; admin only and the last administrator is protected.", { email: text(), role: enumOf(["admin", "member"]) }, ["email", "role"], mcpOnly),
38
+ operation("members.client_role", "/api/cli/collab/members/role", "POST", "approvals client grant", "oxygen_members_client_role_set", "collaboration", "Set or clear the restricted client overlay; admin only. May read pending Clerk invitations; sends no invitation. Free.", { email: text(), role: nullable(enumOf(["client"])) }, ["email", "role"], { ...mcpOnly, annotations: { openWorldHint: true } }),
39
+ operation("members.workspace_role", "/api/cli/collab/members/workspace-role", "POST", "orgs member-role", "oxygen_members_workspace_role_set", "collaboration", "Change a member's workspace role in Clerk; admin only, with self-role and last-administrator protection. Free.", { email: text(), role: enumOf(["admin", "member"]) }, ["email", "role"], { ...mcpOnly, annotations: { openWorldHint: true } }),
39
40
  operation("copilot.sessions.list", "/api/cli/copilot/sessions", "GET", "copilot list", "oxygen_copilot_sessions_list", "agents", "List attended Copilot sessions in this workspace.", COPILOT_SESSIONS_LIST_CONTRACT.properties, [], mcpOnly),
40
41
  operation("copilot.sessions.update", "/api/cli/copilot/sessions/{session_id}", "PATCH", "copilot policy set", "oxygen_copilot_session_update", "agents", "Set an attended Copilot session's approval and screen-follow policies. Paid auto-approval is explicit standing authorization.", { auto_approve_policy: enumOf(["off", "paid"]), surface_policy: enumOf(["off", "follow"]) }, [], { spends: true }),
41
42
  operation("copilot.attachments.list", "/api/cli/copilot/sessions/{session_id}/attachments", "GET", "copilot attachments list", "oxygen_copilot_attachments_list", "agents", "List sent and unsent attachments in one Copilot session.", {}, []),
@@ -64,8 +65,8 @@ export const CUSTOMER_OPERATIONS = [
64
65
  operation("directory.logo", "/api/cli/directory/listing/logo", "POST", "directory logo upload", "oxygen_directory_logo_upload", "crm", "Upload a public agency-directory logo and read back the updated listing; PNG, JPEG, WebP or SVG, at most 2 MB.", { content_base64: text(), file_name: text(), content_type: enumOf(["image/png", "image/jpeg", "image/webp", "image/svg+xml"]) }, ["content_base64", "file_name", "content_type"], { multipart: { contentField: "content_base64", nameField: "file_name", typeField: "content_type", fileField: "file" } }),
65
66
  operation("domains.delete", "/api/cli/domains/delete", "POST", "domains delete", "oxygen_domains_delete", "deliverability", "Preview domain removal and renewal cancellation; execute only with the exact plan hash and confirmation.", { domains: list(), approved: bool(), plan_hash: text(), confirmation: text() }, ["domains"], mcpOnly),
66
67
  operation("knowledge.pages.archive", "/api/cli/knowledge/pages/archive", "POST", "knowledge page archive", "oxygen_knowledge_page_archive", "knowledge", "Archive a knowledge page; content/history remain, links become unresolved. Pass expected_revision for the version you reviewed.", { slug: text(), id: text(), expected_revision: int({ minimum: 1 }) }, [], mcpOnly),
67
- operation("knowledge.sync.manifest", "/api/cli/knowledge/sync/manifest", "POST", "knowledge sync-manifest", "oxygen_knowledge_sync_manifest", "knowledge", "Read the wiki revision and content-hash inventory without downloading instruction bodies.", {}, [], { mutates: false }),
68
- operation("knowledge.sync.delta", "/api/cli/knowledge/sync/delta", "POST", "knowledge sync", "oxygen_knowledge_sync_delta", "knowledge", "Read one resumable batch of wiki pages and tombstones, bounded by the server's byte budget.", { cursor: text(), limit: int({ minimum: 1, maximum: 200 }) }, [], { ...mcpOnly, mutates: false }),
68
+ operation("knowledge.sync.manifest", "/api/cli/knowledge/sync/manifest", "POST", "knowledge sync-manifest", "oxygen_knowledge_sync_manifest", "knowledge", "Read wiki revision/hash metadata. Follow next_cursor for remaining pages; scope is a live keyset, not a snapshot.", {}, [], { contract: knowledgeSyncReadContract, mutates: false }),
69
+ operation("knowledge.sync.delta", "/api/cli/knowledge/sync/delta", "POST", "knowledge sync-delta", "oxygen_knowledge_sync_delta", "knowledge", "Read complete wiki pages/tombstones. First call seeds missing starter pages. Follow next_cursor; an oversized page is refused without skipping. 0 credits.", {}, [], { contract: knowledgeSyncReadContract }),
69
70
  operation("limits.provider.get", "/api/cli/limits/provider", "GET", "limits provider list", "oxygen_provider_limits_list", "billing", "Read provider request limits and BYOK warning defaults. MCP defaults to bounded summary pages; follow paging.next_offset with the same filter. HTTP/CLI omit pagination for the full legacy list. 0 credits.", {}, [], { contract: providerLimitListContract, generated: { cli: false, mcp: false } }),
70
71
  operation("limits.provider.set", "/api/cli/limits/provider", "POST", "limits provider set", "oxygen_provider_limits_set", "billing", "Set an enforced provider request limit; admin or owner only. Approve the exact provider, operation and window: raising a cap may permit more provider use. 0 credits.", {}, [], { ...mcpOnly, contract: providerLimitUpdateContract }),
71
72
  operation("limits.provider.delete", "/api/cli/limits/provider", "DELETE", "limits provider delete", "oxygen_provider_limits_delete", "billing", "Clear one provider request-limit override; admin or owner only. Approve the exact selector. Other operation overrides remain; the provider returns to its defaults, whose BYOK threshold may be observe-only. 0 credits.", {}, [], { ...mcpOnly, contract: providerLimitSelectorContract, queryFields: ["provider", "operation"] }),
@@ -107,7 +108,7 @@ export const CUSTOMER_OPERATIONS = [
107
108
  operation("billing.invoices", "/api/cli/billing/invoices", "GET", "billing invoices", "oxygen_billing_invoices", "billing", "Read this workspace's billing invoices and their hosted payment links.", {}, [], mcpOnly),
108
109
  operation("billing.seats.set", "/api/cli/billing/seats", "POST", "billing seats set", "oxygen_billing_seats_set", "billing", "Set purchased legacy sending-seat quantity. Retired seats cannot increase; reducing existing seats lowers the next renewal charge.", { seat_key: enumOf(["linkedin", "whatsapp", "phone_number", "email_sender"]), quantity: int({ minimum: 0 }) }, ["seat_key", "quantity"], mcpOnly),
109
110
  operation("collaboration.gates.list", "/api/cli/collab/gates", "GET", "approvals gate list", "oxygen_approval_gates_list", "collaboration", "Read this workspace's approval gates, optionally for one exact subject.", { subject: text() }, [], mcpOnly),
110
- operation("collaboration.members.list", "/api/cli/collab/members", "GET", "approvals client list", "oxygen_workspace_members_list", "collaboration", "Read workspace members and their collaboration roles.", {}, [], mcpOnly),
111
+ operation("collaboration.members.list", "/api/cli/collab/members", "GET", "approvals client list", "oxygen_workspace_members_list", "collaboration", "Read workspace member roles, with Clerk profiles and pending invitations. Free.", {}, [], { ...mcpOnly, annotations: { openWorldHint: true } }),
111
112
  operation("crm.connections", "/api/cli/crm/objects/{object}/records/{row_id}/connections", "GET", "crm connections", "oxygen_crm_connections", "crm", "Read a CRM record's connected conversations, sequence enrollments and related activity.", { conversation_limit: int({ minimum: 1 }), sequence_limit: int({ minimum: 1 }) }, [], mcpOnly),
112
113
  operation("crm.tasks.list", "/api/cli/crm/tasks", "GET", "crm tasks list", "oxygen_crm_tasks_queue", "crm", "Read the workspace task queue by status, assignee and cursor.", { status: text(), assignee: text(), cursor: text(), limit: int({ minimum: 1 }) }, [], mcpOnly),
113
114
  operation("crm.files.download", "/api/cli/crm/files/{attachment_id}/download", "GET", "crm files download", "oxygen_crm_file_download", "crm", "Download a tenant-owned record attachment by attachment_id. The native authenticated response contains its exact bytes.", {}, [], { binary: { maxBytes: 50 * 1024 * 1024 } }),
@@ -0,0 +1,19 @@
1
+ /** Both sync readers expose complete native pages; no formatter may trim a
2
+ * page after its continuation position has been issued. */
3
+ export declare const knowledgeSyncReadContract: import("./schema.js").ContractObject<{
4
+ readonly cursor: import("./schema.js").StringProperty;
5
+ readonly limit: {
6
+ type: "integer";
7
+ description?: string;
8
+ minimum?: number;
9
+ maximum?: number;
10
+ "x-clamp"?: true;
11
+ };
12
+ readonly max_chars: {
13
+ type: "integer";
14
+ description?: string;
15
+ minimum?: number;
16
+ maximum?: number;
17
+ "x-clamp"?: true;
18
+ };
19
+ }, never>;
@@ -0,0 +1,8 @@
1
+ import { int, obj, str } from "./schema.js";
2
+ /** Both sync readers expose complete native pages; no formatter may trim a
3
+ * page after its continuation position has been issued. */
4
+ export const knowledgeSyncReadContract = obj({
5
+ cursor: str({ minLength: 1, maxLength: 256 }),
6
+ limit: int({ minimum: 1, maximum: 200 }),
7
+ max_chars: int({ minimum: 1000, maximum: 48000, description: "Complete receipt window; surface cap applies." }),
8
+ });
@@ -20,6 +20,7 @@ export declare const publishingReadProjectionContract: ContractObject<{
20
20
  readonly description?: string;
21
21
  readonly minLength?: number;
22
22
  readonly maxLength?: number;
23
+ readonly pattern?: string;
23
24
  readonly enum: readonly ["full", "summary", "value"];
24
25
  };
25
26
  readonly value_path: import("./schema.js").StringProperty;
@@ -40,6 +41,7 @@ export declare const publishingAmplificationListQueryContract: ContractObject<{
40
41
  readonly description?: string;
41
42
  readonly minLength?: number;
42
43
  readonly maxLength?: number;
44
+ readonly pattern?: string;
43
45
  readonly enum: readonly ["full", "summary", "value"];
44
46
  };
45
47
  readonly value_path: import("./schema.js").StringProperty;
@@ -57,6 +59,7 @@ export declare const publishingAmplificationListQueryContract: ContractObject<{
57
59
  readonly description?: string;
58
60
  readonly minLength?: number;
59
61
  readonly maxLength?: number;
62
+ readonly pattern?: string;
60
63
  readonly enum: readonly ["all", "tag", "post", "ugc_program", "ugc_peer_program"];
61
64
  };
62
65
  readonly limit: {
@@ -73,6 +76,7 @@ export declare const publishingAmplificationActionsQueryContract: ContractObject
73
76
  readonly description?: string;
74
77
  readonly minLength?: number;
75
78
  readonly maxLength?: number;
79
+ readonly pattern?: string;
76
80
  readonly enum: readonly ["full", "summary", "value"];
77
81
  };
78
82
  readonly value_path: import("./schema.js").StringProperty;
@@ -99,6 +103,7 @@ export declare const publishingNotesQueryContract: ContractObject<{
99
103
  readonly description?: string;
100
104
  readonly minLength?: number;
101
105
  readonly maxLength?: number;
106
+ readonly pattern?: string;
102
107
  readonly enum: readonly ["full", "summary", "value"];
103
108
  };
104
109
  readonly value_path: import("./schema.js").StringProperty;
@@ -127,6 +132,7 @@ export declare const publishingRevisionsQueryContract: ContractObject<{
127
132
  readonly description?: string;
128
133
  readonly minLength?: number;
129
134
  readonly maxLength?: number;
135
+ readonly pattern?: string;
130
136
  readonly enum: readonly ["full", "summary", "value"];
131
137
  };
132
138
  readonly value_path: import("./schema.js").StringProperty;
@@ -166,6 +172,7 @@ export declare const publishingAnalyticsQueryContract: ContractObject<{
166
172
  readonly description?: string;
167
173
  readonly minLength?: number;
168
174
  readonly maxLength?: number;
175
+ readonly pattern?: string;
169
176
  readonly enum: readonly ["full", "summary", "value"];
170
177
  };
171
178
  readonly value_path: import("./schema.js").StringProperty;
@@ -15,6 +15,8 @@ export type StringProperty = {
15
15
  enum?: readonly string[];
16
16
  minLength?: number;
17
17
  maxLength?: number;
18
+ /** JSON Schema regular expression, evaluated with Unicode semantics. */
19
+ pattern?: string;
18
20
  };
19
21
  export type NumberProperty = {
20
22
  type: "number" | "integer";
@@ -99,6 +101,7 @@ export declare function jsonValue(options?: Described): {
99
101
  export declare function str(options?: Described & {
100
102
  minLength?: number;
101
103
  maxLength?: number;
104
+ pattern?: string;
102
105
  }): StringProperty;
103
106
  export declare function enumOf<const V extends string>(values: readonly V[], options?: Described): {
104
107
  type: "string";
@@ -210,6 +210,9 @@ function walkValue(schema, value, path, walk) {
210
210
  else if (schema.maxLength !== undefined && value.length > schema.maxLength) {
211
211
  walk.issues.push({ path, message: `${path} must be at most ${schema.maxLength} characters.` });
212
212
  }
213
+ else if (schema.pattern !== undefined && !new RegExp(schema.pattern, "u").test(value)) {
214
+ walk.issues.push({ path, message: `${path} must match the required pattern.` });
215
+ }
213
216
  return value;
214
217
  }
215
218
  case "integer":
@@ -55,6 +55,7 @@ export declare function tableFeedWindowProperties(contract: typeof TABLE_FEED_GE
55
55
  enum?: readonly string[];
56
56
  minLength?: number;
57
57
  maxLength?: number;
58
+ pattern?: string;
58
59
  } | {
59
60
  type: "integer";
60
61
  minimum?: number;
@@ -11,6 +11,7 @@ export declare const TABLE_ROW_READ_WINDOW_FIELDS: {
11
11
  readonly enum?: readonly string[];
12
12
  readonly minLength?: number;
13
13
  readonly maxLength?: number;
14
+ readonly pattern?: string;
14
15
  readonly description: "JSON Pointer; empty=root.";
15
16
  };
16
17
  readonly value_cursor: {
@@ -18,6 +19,7 @@ export declare const TABLE_ROW_READ_WINDOW_FIELDS: {
18
19
  readonly enum?: readonly string[];
19
20
  readonly minLength?: number;
20
21
  readonly maxLength?: number;
22
+ readonly pattern?: string;
21
23
  readonly description: "Resume with identical selection/path.";
22
24
  };
23
25
  readonly max_chars: {
@@ -7,7 +7,8 @@
7
7
  // on, a connect is admitted when the spendable balance covers the account's
8
8
  // first monthly credit reservation, refused with `insufficient_credits` when it
9
9
  // does not, and `billing seats` reports that seats are retired (S21). The plan
10
- // wall removal (S22) sits behind the same flag.
10
+ // wall removal (S22) sits behind the same flag; a LinkedIn account keeps its
11
+ // plan wall (`linkedin_connect`, founder 2026-10-12).
11
12
  //
12
13
  // WHY IT IS ITS OWN FLAG, AND WHY IT IS NOT ENOUGH ALONE. A balance check debits
13
14
  // nothing: while commitments are dark, a workspace holding 3,400 credits would
@@ -94,6 +94,7 @@ declare const REPLY_CLASSIFIED_PAYLOAD: ContractObject<{
94
94
  enum?: readonly string[];
95
95
  minLength?: number;
96
96
  maxLength?: number;
97
+ pattern?: string;
97
98
  format: "date-time";
98
99
  };
99
100
  readonly url: {
@@ -102,6 +103,7 @@ declare const REPLY_CLASSIFIED_PAYLOAD: ContractObject<{
102
103
  enum?: readonly string[];
103
104
  minLength?: number;
104
105
  maxLength?: number;
106
+ pattern?: string;
105
107
  format: "uri";
106
108
  };
107
109
  }, "channel" | "classification" | "conversation_id" | "enrollment_id" | "occurred_at" | "record_id" | "sequence_id" | "url">;
@@ -143,6 +145,7 @@ declare const ENROLLMENT_ENDED_PAYLOAD: ContractObject<{
143
145
  enum?: readonly string[];
144
146
  minLength?: number;
145
147
  maxLength?: number;
148
+ pattern?: string;
146
149
  format: "date-time";
147
150
  };
148
151
  readonly url: {
@@ -151,6 +154,7 @@ declare const ENROLLMENT_ENDED_PAYLOAD: ContractObject<{
151
154
  enum?: readonly string[];
152
155
  minLength?: number;
153
156
  maxLength?: number;
157
+ pattern?: string;
154
158
  format: "uri";
155
159
  };
156
160
  }, "enrollment_id" | "occurred_at" | "outcome" | "sequence_id" | "steps_sent" | "url">;
@@ -205,6 +209,7 @@ declare const MEETING_COMPLETED_PAYLOAD: ContractObject<{
205
209
  enum?: readonly string[];
206
210
  minLength?: number;
207
211
  maxLength?: number;
212
+ pattern?: string;
208
213
  format: "date-time";
209
214
  };
210
215
  readonly url: {
@@ -213,6 +218,7 @@ declare const MEETING_COMPLETED_PAYLOAD: ContractObject<{
213
218
  enum?: readonly string[];
214
219
  minLength?: number;
215
220
  maxLength?: number;
221
+ pattern?: string;
216
222
  format: "uri";
217
223
  };
218
224
  }, "activity_id" | "duration_seconds" | "occurred_at" | "record_ids" | "session_id" | "url">;
@@ -277,6 +283,7 @@ declare const RECORD_STAGE_CHANGED_PAYLOAD: ContractObject<{
277
283
  enum?: readonly string[];
278
284
  minLength?: number;
279
285
  maxLength?: number;
286
+ pattern?: string;
280
287
  format: "date-time";
281
288
  };
282
289
  readonly url: {
@@ -285,6 +292,7 @@ declare const RECORD_STAGE_CHANGED_PAYLOAD: ContractObject<{
285
292
  enum?: readonly string[];
286
293
  minLength?: number;
287
294
  maxLength?: number;
295
+ pattern?: string;
288
296
  format: "uri";
289
297
  };
290
298
  }, "activity_id" | "from_stage_id" | "object" | "occurred_at" | "pipeline_id" | "record_id" | "to_stage_id" | "url">;
@@ -323,6 +331,7 @@ declare const RUN_SETTLED_PAYLOAD: ContractObject<{
323
331
  enum?: readonly string[];
324
332
  minLength?: number;
325
333
  maxLength?: number;
334
+ pattern?: string;
326
335
  format: "date-time";
327
336
  };
328
337
  readonly url: {
@@ -331,6 +340,7 @@ declare const RUN_SETTLED_PAYLOAD: ContractObject<{
331
340
  enum?: readonly string[];
332
341
  minLength?: number;
333
342
  maxLength?: number;
343
+ pattern?: string;
334
344
  format: "uri";
335
345
  };
336
346
  }, "credits_used" | "kind" | "occurred_at" | "run_id" | "status" | "url">;
@@ -362,6 +372,7 @@ declare const APPROVAL_REQUESTED_PAYLOAD: ContractObject<{
362
372
  enum?: readonly string[];
363
373
  minLength?: number;
364
374
  maxLength?: number;
375
+ pattern?: string;
365
376
  format: "date-time";
366
377
  };
367
378
  readonly url: {
@@ -370,6 +381,7 @@ declare const APPROVAL_REQUESTED_PAYLOAD: ContractObject<{
370
381
  enum?: readonly string[];
371
382
  minLength?: number;
372
383
  maxLength?: number;
384
+ pattern?: string;
373
385
  format: "uri";
374
386
  };
375
387
  }, "approval_id" | "kind" | "occurred_at" | "run_id" | "url">;
@@ -60,7 +60,7 @@ export type OtlpLogSinkOptions = {
60
60
  * `worker.log_shipper.stats` is the Axiom shipper's once-a-minute loss accounting,
61
61
  * which by design never reaches Axiom (OXP-1.6.126).
62
62
  */
63
- export declare const OTLP_MONITOR_INFO_EVENTS: readonly ["diagnostics.capture_stored", "diagnostics.invocation_completed", "diagnostics.canary_completed", "diagnostics.digest_completed", "tenant.migrations.cron.done", "outbound_send.sent", "worker_workflow_cron_overlap_skipped", "worker_workflow_cron_enqueued", "copilot.turn.finished", "worker.queue.heartbeat", "worker.queue.ledger", "worker.queue.redis", "worker.queue.unregistered_kind", "automation_usage.reconcile_completed", "provider_health.cron_completed", "provider_balances.cron_completed", "cost_snapshot.cron_completed", "worker.egress_order_reconcile_sweep", "posthog_canary.round_trip_ok", "web.log_volume", "worker.log_shipper.stats"];
63
+ export declare const OTLP_MONITOR_INFO_EVENTS: readonly ["diagnostics.capture_stored", "diagnostics.invocation_completed", "diagnostics.canary_completed", "diagnostics.digest_completed", "tenant.migrations.cron.done", "outbound_send.sent", "worker_workflow_cron_overlap_skipped", "worker_workflow_cron_enqueued", "copilot.turn.finished", "worker.queue.heartbeat", "worker.queue.depth", "worker.queue.ledger", "worker.queue.redis", "worker.queue.unregistered_kind", "automation_usage.reconcile_completed", "provider_health.cron_completed", "provider_balances.cron_completed", "cost_snapshot.cron_completed", "worker.egress_order_reconcile_sweep", "posthog_canary.round_trip_ok", "web.log_volume", "worker.log_shipper.stats"];
64
64
  export type OtlpLogSinkHandle = {
65
65
  sink: LogSink;
66
66
  flush: () => Promise<void>;
@@ -22,6 +22,7 @@ export const OTLP_MONITOR_INFO_EVENTS = [
22
22
  "worker_workflow_cron_enqueued",
23
23
  "copilot.turn.finished",
24
24
  "worker.queue.heartbeat",
25
+ "worker.queue.depth",
25
26
  "worker.queue.ledger",
26
27
  "worker.queue.redis",
27
28
  "worker.queue.unregistered_kind",
@@ -37,11 +37,16 @@
37
37
  * billed that way (the credit gate, `OXYGEN_CREDIT_GATE_ENABLED`) credits are the
38
38
  * only gate and the sending walls retire: `CREDIT_GATE_RETIRED_CAPABILITIES`.
39
39
  * Until the gate is in force everything above still holds.
40
+ *
41
+ * EXCEPT LINKEDIN (founder, current chat, 2026-10-12: "you can only connect a
42
+ * linkedin account on a plan"). `linkedin_connect` is not retired by the credit
43
+ * gate, so a free workspace is told to subscribe before any balance check,
44
+ * however many credits it holds.
40
45
  */
41
46
  import type { PlanTier, PricingPlanDefinition } from "./billing.js";
42
47
  import { CREDIT_GATE_RETIRED_CAPABILITIES, isCapabilityRetiredByCreditGate } from "./credit-gate.js";
43
48
  import { type RepricingOptions } from "./repricing.js";
44
- export declare const PLAN_GATED_CAPABILITIES: readonly ["sender_connect", "sequence_dispatch", "publishing_connect", "publishing_deliver", "managed_email_infrastructure", "byok_provider_keys"];
49
+ export declare const PLAN_GATED_CAPABILITIES: readonly ["sender_connect", "linkedin_connect", "sequence_dispatch", "publishing_connect", "publishing_deliver", "managed_email_infrastructure", "byok_provider_keys"];
45
50
  export type PlanGatedCapability = (typeof PLAN_GATED_CAPABILITIES)[number];
46
51
  /**
47
52
  * Every tier except `free` unblocks every gated capability.
@@ -75,7 +80,9 @@ export declare function isPlanGatedCapability(value: unknown): value is PlanGate
75
80
  * (`assertCreditGateArmed`). Until then every one of them stays walled exactly
76
81
  * as before.
77
82
  *
78
- * Two capabilities stay walled under the gate:
83
+ * Three capabilities stay walled under the gate:
84
+ * - `linkedin_connect`: a LinkedIn account connects only on a plan (founder,
85
+ * current chat, 2026-10-12), whatever the balance.
79
86
  * - `byok_provider_keys`: bringing your own keys needs a plan that includes it
80
87
  * ($99 or above from the repricing's effective date).
81
88
  * - `publishing_connect`: the only publishing connects that still reach it are
@@ -37,11 +37,17 @@
37
37
  * billed that way (the credit gate, `OXYGEN_CREDIT_GATE_ENABLED`) credits are the
38
38
  * only gate and the sending walls retire: `CREDIT_GATE_RETIRED_CAPABILITIES`.
39
39
  * Until the gate is in force everything above still holds.
40
+ *
41
+ * EXCEPT LINKEDIN (founder, current chat, 2026-10-12: "you can only connect a
42
+ * linkedin account on a plan"). `linkedin_connect` is not retired by the credit
43
+ * gate, so a free workspace is told to subscribe before any balance check,
44
+ * however many credits it holds.
40
45
  */
41
46
  import { CREDIT_GATE_RETIRED_CAPABILITIES, isCapabilityRetiredByCreditGate } from "./credit-gate.js";
42
47
  import { OXYGEN_49_BYOK_REPRICING, readRepricingSwitch, repricedValue, } from "./repricing.js";
43
48
  export const PLAN_GATED_CAPABILITIES = [
44
49
  "sender_connect",
50
+ "linkedin_connect",
45
51
  "sequence_dispatch",
46
52
  "publishing_connect",
47
53
  "publishing_deliver",
@@ -71,6 +77,11 @@ export const PLAN_GATED_CAPABILITY_COPY = {
71
77
  freeAlternative: "You can build and preview the whole motion on Free — sequences, steps, and enrollment all work. Connecting a mailbox, LinkedIn account, or WhatsApp number is what needs a plan.",
72
78
  deepLinkPath: "/settings/connections",
73
79
  },
80
+ linkedin_connect: {
81
+ label: "Connecting a LinkedIn account",
82
+ freeAlternative: "Leads, sequences and enrollment all work on Free. To connect a LinkedIn account, subscribe to the Oxygen plan; its monthly credits pay for the account.",
83
+ deepLinkPath: "/settings/connections",
84
+ },
74
85
  sequence_dispatch: {
75
86
  label: "Sending a sequence",
76
87
  freeAlternative: "Authoring, versioning, enrolling, and previewing a sequence are all free. Dispatching to real recipients is what needs a plan.",
@@ -113,7 +124,9 @@ export function isPlanGatedCapability(value) {
113
124
  * (`assertCreditGateArmed`). Until then every one of them stays walled exactly
114
125
  * as before.
115
126
  *
116
- * Two capabilities stay walled under the gate:
127
+ * Three capabilities stay walled under the gate:
128
+ * - `linkedin_connect`: a LinkedIn account connects only on a plan (founder,
129
+ * current chat, 2026-10-12), whatever the balance.
117
130
  * - `byok_provider_keys`: bringing your own keys needs a plan that includes it
118
131
  * ($99 or above from the repricing's effective date).
119
132
  * - `publishing_connect`: the only publishing connects that still reach it are