@oxygen-agent/cli 1.1014.1696 → 1.1014.1763

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 (96) hide show
  1. package/README.md +1 -1
  2. package/dist/command-families/program.js +4 -0
  3. package/dist/command-manifest.js +14 -2
  4. package/dist/command-support.js +1 -1
  5. package/dist/commands/admin.js +82 -1
  6. package/dist/commands/billing.js +1 -1
  7. package/dist/commands/copilot.js +29 -7
  8. package/dist/commands/domains.js +7 -0
  9. package/dist/commands/email-assets.d.ts +3 -0
  10. package/dist/commands/email-assets.js +78 -0
  11. package/dist/commands/email-templates.d.ts +3 -0
  12. package/dist/commands/email-templates.js +101 -0
  13. package/dist/commands/sequences.js +47 -12
  14. package/dist/commands/subscriptions.d.ts +2 -4
  15. package/dist/commands/subscriptions.js +297 -51
  16. package/dist/sequence-start-at.d.ts +7 -0
  17. package/dist/sequence-start-at.js +21 -0
  18. package/dist/sequences-brand-kit-command.d.ts +28 -0
  19. package/dist/sequences-brand-kit-command.js +83 -0
  20. package/dist/sequences-content-commands.d.ts +28 -0
  21. package/dist/sequences-content-commands.js +176 -0
  22. package/dist/sequences-newsletter-commands.d.ts +6 -0
  23. package/dist/sequences-newsletter-commands.js +108 -0
  24. package/dist/sequences-test-send-command.d.ts +3 -0
  25. package/dist/sequences-test-send-command.js +47 -0
  26. package/node_modules/@oxygen/formula/dist/evaluate.js +2 -4
  27. package/node_modules/@oxygen/formula/dist/expression.d.ts +7 -5
  28. package/node_modules/@oxygen/formula/dist/expression.js +26 -4
  29. package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +6 -0
  30. package/node_modules/@oxygen/formula/dist/formula-functions.js +55 -0
  31. package/node_modules/@oxygen/shared/dist/billing.d.ts +2 -2
  32. package/node_modules/@oxygen/shared/dist/billing.js +5 -7
  33. package/node_modules/@oxygen/shared/dist/bulk-email-pacing.d.ts +105 -0
  34. package/node_modules/@oxygen/shared/dist/bulk-email-pacing.js +125 -0
  35. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +29 -0
  36. package/node_modules/@oxygen/shared/dist/capability-discovery.js +174 -10
  37. package/node_modules/@oxygen/shared/dist/contracts/clay-migration.d.ts +1 -0
  38. package/node_modules/@oxygen/shared/dist/contracts/clay-migration.js +28 -2
  39. package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.d.ts +24 -0
  40. package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.js +7 -0
  41. package/node_modules/@oxygen/shared/dist/contracts/teammates.js +6 -1
  42. package/node_modules/@oxygen/shared/dist/email-brand-kit.d.ts +108 -0
  43. package/node_modules/@oxygen/shared/dist/email-brand-kit.js +214 -0
  44. package/node_modules/@oxygen/shared/dist/email-delivery.d.ts +10 -0
  45. package/node_modules/@oxygen/shared/dist/email-delivery.js +14 -0
  46. package/node_modules/@oxygen/shared/dist/email-documents.d.ts +61 -0
  47. package/node_modules/@oxygen/shared/dist/email-documents.js +143 -0
  48. package/node_modules/@oxygen/shared/dist/email-dsn.js +52 -10
  49. package/node_modules/@oxygen/shared/dist/email-html-sanitize.d.ts +23 -0
  50. package/node_modules/@oxygen/shared/dist/email-html-sanitize.js +96 -0
  51. package/node_modules/@oxygen/shared/dist/email-subscriptions.d.ts +63 -1
  52. package/node_modules/@oxygen/shared/dist/email-subscriptions.js +149 -1
  53. package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.d.ts +41 -2
  54. package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.js +50 -1
  55. package/node_modules/@oxygen/shared/dist/newsletter-analytics.d.ts +90 -0
  56. package/node_modules/@oxygen/shared/dist/newsletter-analytics.js +28 -0
  57. package/node_modules/@oxygen/shared/dist/newsletter-test-send.d.ts +20 -0
  58. package/node_modules/@oxygen/shared/dist/newsletter-test-send.js +46 -0
  59. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +11 -0
  60. package/node_modules/@oxygen/shared/dist/object-storage.js +12 -0
  61. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +9 -0
  62. package/node_modules/@oxygen/shared/dist/plan-band.js +17 -0
  63. package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +4 -2
  64. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +12 -1
  65. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +2 -0
  66. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +3 -0
  67. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +5 -0
  68. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +5 -0
  69. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +29 -0
  70. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +29 -0
  71. package/node_modules/@oxygen/shared/dist/sending-domain-faults.d.ts +21 -0
  72. package/node_modules/@oxygen/shared/dist/sending-domain-faults.js +53 -0
  73. package/node_modules/@oxygen/shared/dist/sending-domain-rollout.d.ts +24 -1
  74. package/node_modules/@oxygen/shared/dist/sending-domain-rollout.js +51 -4
  75. package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +10 -1
  76. package/node_modules/@oxygen/shared/dist/sequence-failures.js +68 -6
  77. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.d.ts +34 -3
  78. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.js +57 -4
  79. package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +17 -0
  80. package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
  81. package/node_modules/@oxygen/shared/dist/sequences.d.ts +41 -0
  82. package/node_modules/@oxygen/shared/dist/sequences.js +86 -4
  83. package/node_modules/@oxygen/shared/dist/ses-identity-policy.d.ts +19 -0
  84. package/node_modules/@oxygen/shared/dist/ses-identity-policy.js +54 -0
  85. package/node_modules/@oxygen/shared/dist/skill-play-routing.d.ts +22 -0
  86. package/node_modules/@oxygen/shared/dist/skill-play-routing.js +75 -0
  87. package/node_modules/@oxygen/shared/dist/skill-plays.generated.d.ts +5 -0
  88. package/node_modules/@oxygen/shared/dist/skill-plays.generated.js +269 -0
  89. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +7 -0
  90. package/node_modules/@oxygen/shared/dist/spend-safety.js +7 -0
  91. package/node_modules/@oxygen/shared/dist/teammates.js +2 -1
  92. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +15 -1
  93. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  94. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  95. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +2 -2
  96. package/package.json +2 -2
@@ -2,6 +2,8 @@ import { findEnrichmentIntentByAlias } from "./enrichment-intents.js";
2
2
  import { networkMiningEnabled } from "./network-mining.js";
3
3
  import { nativeChatEnabled, withNativeChatDiscovery } from "./native-chat-discovery.js";
4
4
  import { RECIPE_PRIMITIVES } from "./recipes.js";
5
+ import { matchSkillPlay } from "./skill-play-routing.js";
6
+ export { matchSkillPlay, SKILL_PLAY_TRIGGERS } from "./skill-play-routing.js";
5
7
  // One compact semantic index covers every top-level /api/cli endpoint section.
6
8
  // It is navigation, never execution: live schemas, prices, readiness, and
7
9
  // approval enforcement remain on the exact command/tool selected from here.
@@ -298,7 +300,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
298
300
  primitive: "signals",
299
301
  owns: "Durable typed pulse, engagement, and product events resolved onto records.",
300
302
  notFor: "Message bodies, post artifacts, or ad-hoc provider webhooks as private automation state.",
301
- execution: "Read the hosted signal stream and type registry; tag a registered type with tags attach. Feed subscriptions, Sequences, or Workflows.",
303
+ execution: "Read the hosted signal stream and type registry; tag a registered type with tags attach. Feed subscriptions, Sequences, or Workflows. A visit or view the user reports goes on the person with oxygen_crm_activity_log (activity_type website_visit, direction inbound, live), after creating a missing person with oxygen_crm_record_assert (free, no approval); oxygen_crm_signal_record is for a provider webhook.",
302
304
  posture: "workspace_write",
303
305
  gatewayTools: ["oxygen_signals_list", "oxygen_signals_leads_today", "oxygen_signals_registry", "oxygen_tags_attach"],
304
306
  gatewayCommands: ["signals list", "signals leads-today", "signals registry", "tags attach"],
@@ -375,7 +377,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
375
377
  gatewayTools: ["oxygen_sequences_send", "oxygen_sequences_create", "oxygen_sequences_enroll", "oxygen_sequences_start", "oxygen_sequences_list", "oxygen_sequences_update", "oxygen_sequences_get", "oxygen_sequences_push_external", "oxygen_sequences_company_pause"],
376
378
  gatewayCommands: ["sequences send", "sequences create", "sequences enroll", "sequences start", "sequences list", "sequences update", "sequences get", "sequences push-external", "sequences pause-company"],
377
379
  skills: ["oxygen-sequencer"],
378
- endpointSections: ["senders", "sequencer", "sequences", "spintax", "subscriptions", "suppressions", "voice"],
380
+ endpointSections: ["email-assets", "email-documents", "senders", "sequencer", "sequences", "spintax", "subscriptions", "suppressions", "voice"],
379
381
  // "sender identity", "sender profile" and "phone number" are here because a
380
382
  // blind-user eval (2026-08-09) asked for "our sender identity" and got routed
381
383
  // to workspace-access — "identity" matched the AUTH sense. The nouns a user
@@ -411,7 +413,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
411
413
  primitive: "workflows",
412
414
  owns: "Deterministic hosted automation definitions, versions, triggers, branching, retries, approvals, modes, and durable runs.",
413
415
  notFor: "Local cron, client-side execution, adaptive Agent goals, or outreach cadence already owned by Sequences.",
414
- execution: "Author locally if useful, then schema/lint/apply and call the Workflow on OXYGEN workers; outbound uses internal oxygen.sequence_enroll against an already active bounded Sequence. Start from the graph schema's examples[0] (oxygen_workflows_schema subject graph): a minimal valid manifest whose tool node adds a Table row with oxygen_table_rows_insert. A draft that is not saved exists only in your reply, so show its manifest there. To find why a definition fails, lint it as written first. Saving without running is apply with publish false and workflow and trigger status disabled.",
416
+ execution: "Author locally if useful, then schema/lint/apply and call the Workflow on OXYGEN workers; outbound uses internal oxygen.sequence_enroll against an already active bounded Sequence. Start from the graph schema's examples[0] (oxygen_workflows_schema subject graph): a minimal valid manifest whose tool node adds a Table row with oxygen_table_rows_insert. To change existing rows, a tool node runs oxygen_tables_query with filters, a loop goes over steps.<node>.output.rows (body edge source_handle loop_body, plus an edge back to the loop), and the body calls oxygen_rows_update with row_id loop.each.item._row_id and values. A fixed change on a schedule stays a Workflow: draft and lint it rather than offering a formula column instead; a routine is for checks that need judgment. A cron time is in the person's time zone: set trigger.timezone, and ask for it in your first question when you do not know it. To find why a definition fails, lint it as written first. Saving without running is apply with publish false and workflow and trigger status disabled. If apply is refused, nothing was saved: say so once, never call it saved or a draft in OXYGEN, and never offer another way around the refusal (such as writing the rows directly). Then say in plain words what it would do once saved, and include the full manifest below that: show its manifest every time, since it exists only in your reply.",
415
417
  posture: "mixed",
416
418
  gatewayTools: ["oxygen_workflows_schema", "oxygen_workflows_lint", "oxygen_workflows_apply", "oxygen_workflows_call", "oxygen_workflows_tail"],
417
419
  gatewayCommands: ["workflows schema", "workflows lint", "workflows apply", "workflows call", "workflows tail"],
@@ -459,7 +461,11 @@ export const OXYGEN_CAPABILITY_ROUTES = [
459
461
  gatewayCommands: ["companies search plan", "people search plan", "tools search", "find company"],
460
462
  skills: ["oxygen-sourcing", "oxygen-enrichment"],
461
463
  endpointSections: ["companies", "find", "lead-sourcing", "people", "search", "sourcing", "tools", "verify"],
462
- 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"],
464
+ // Contacts at accounts are a people search: "merge two lists then find
465
+ // contacts" scored Records on "merge"/"contact", and "who we should reach at
466
+ // each company ... cost" scored Billing on "cost".
467
+ 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", "find contacts", "contacts per account", "contacts per company", "contacts at each", "reach at each", "people at each"],
468
+ negativeTerms: ["in my crm", "in our crm", "in the crm", "existing contacts"],
463
469
  },
464
470
  {
465
471
  id: "linkedin-public-research",
@@ -637,7 +643,10 @@ export function inferCapabilityRoute(query) {
637
643
  // Card scoring stays the owner of routing; the catalogue only breaks a
638
644
  // no-match tie ("get the funding round for these accounts" names no card
639
645
  // term) and decorates the data cards it belongs to.
640
- const card = explicit ?? highestScoringRoute(normalized) ?? (catalogRow ? cardForCatalogueIntent(catalogRow) : null);
646
+ // A feature-skill play the ask matches votes for the cards its skill owns and,
647
+ // when the chosen card lists that skill, travels on the route as the file to open.
648
+ const play = matchSkillPlay(normalized) ?? sourcingPlaybookFor(normalized);
649
+ const card = explicit ?? highestScoringRoute(normalized, play?.skill) ?? (catalogRow ? cardForCatalogueIntent(catalogRow) : null);
641
650
  if (!card)
642
651
  return null;
643
652
  const recommendations = recommendationsFor(card, normalized);
@@ -647,7 +656,7 @@ export function inferCapabilityRoute(query) {
647
656
  // Sourcing filters belong to the planner, not per-company enrichment.
648
657
  const catalogue = catalogRow && CATALOGUE_CARD_IDS.has(card.id) && !isCompanySourcingIntent(normalized)
649
658
  ? catalogueRecommendations(catalogRow, card) : null;
650
- return withRecordBrief({
659
+ return withSkillPlay(withRecordBrief({
651
660
  card: routeCardFor(card, normalized),
652
661
  recommendedTools: recommendations.tools,
653
662
  recommendedCommands: recommendations.commands,
@@ -656,7 +665,30 @@ export function inferCapabilityRoute(query) {
656
665
  ...(catalogue && catalogRow
657
666
  ? { catalogIntent: { id: catalogRow.id, label: catalogRow.label, status: catalogRow.status, group: catalogRow.group, tools: catalogue.tools, commands: catalogue.commands, exactCommand: catalogue.exactCommand, exactMcpCall: catalogue.exactMcpCall } }
658
667
  : {}),
659
- }, networkMiningEnabled(runtimeEnv()));
668
+ }, networkMiningEnabled(runtimeEnv())), play);
669
+ }
670
+ /**
671
+ * Contacts at accounts has no play of its own: oxygen-sourcing's SKILL.md sends
672
+ * "merging sources, setting contacts per account" to its playbook reference,
673
+ * so the route points there (2026-10-22 suite: the merge-then-contacts case
674
+ * missed two contacts per account and the second-source rule without it).
675
+ * Contacts already in the CRM stay with Records.
676
+ */
677
+ function sourcingPlaybookFor(query) {
678
+ return /\b(find contacts|contacts (per|at each) (account|company)|reach at each|people at each)\b/.test(query)
679
+ && !/\b(crm|existing contacts)\b/.test(query)
680
+ ? { skill: "oxygen-sourcing", slug: "playbook", file: "references/playbook.md" }
681
+ : null;
682
+ }
683
+ /** The play rides only on a card that lists its skill, and that skill is named first. */
684
+ function withSkillPlay(route, play) {
685
+ if (!play || !route.card.skills.includes(play.skill))
686
+ return route;
687
+ return {
688
+ ...route,
689
+ card: { ...route.card, skills: [play.skill, ...route.card.skills.filter((skill) => skill !== play.skill)] },
690
+ play,
691
+ };
660
692
  }
661
693
  /**
662
694
  * A data-type ask ("tech stack", "decision makers at each account", "funding
@@ -818,6 +850,8 @@ export function serializeCapabilityRoute(route, surfaceOrOptions = "cli", option
818
850
  return null;
819
851
  const nativeChat = (typeof surfaceOrOptions === "string" ? options.nativeChat : surfaceOrOptions.nativeChat) ?? nativeChatEnabled(runtimeEnv());
820
852
  route = withNativeChatDiscovery(route, nativeChat);
853
+ const play = route.play && (isPublicCapabilitySkill(route.play.skill) || visibleSkills?.includes(route.play.skill))
854
+ ? route.play : null;
821
855
  return {
822
856
  capability_id: route.card.id,
823
857
  layer: route.card.layer,
@@ -827,6 +861,15 @@ export function serializeCapabilityRoute(route, surfaceOrOptions = "cli", option
827
861
  execution: route.card.execution,
828
862
  posture: route.card.posture,
829
863
  skills: route.card.skills.filter((name) => isPublicCapabilitySkill(name) || visibleSkills?.includes(name)),
864
+ // The play to open before planning, in both read forms like `hydrate` below.
865
+ ...(play ? {
866
+ play: {
867
+ skill: play.skill,
868
+ file: play.file,
869
+ read_cli: `oxygen skills get ${play.skill} --file ${play.file} --json`,
870
+ read_mcp: { tool: "oxygen_skills_get", arguments: { name: play.skill, file: play.file } },
871
+ },
872
+ } : {}),
830
873
  endpoint_sections: route.card.endpointSections,
831
874
  recommended_commands: route.recommendedCommands,
832
875
  recommended_tools: route.recommendedTools,
@@ -1312,6 +1355,10 @@ function routeCardFor(card, query) {
1312
1355
  return { ...card, execution: feed.execution };
1313
1356
  if (card.primitive === "publishing" && isUgcIntent(query))
1314
1357
  return { ...card, ...UGC_ROUTE_VIEW };
1358
+ if (card.primitive === "publishing" && isPostDraftIntent(query))
1359
+ return { ...card, execution: POST_DRAFT_EXECUTION };
1360
+ if (card.primitive === "signals" && isSignalCaptureIntent(query))
1361
+ return { ...card, execution: SIGNAL_CAPTURE_EXECUTION, skills: ["oxygen-signals", "oxygen-crm"] };
1315
1362
  const contactFind = card.id === "sourcing-and-provider-tools" ? oneShotContactFindingRecommendation(query) : null;
1316
1363
  if (contactFind)
1317
1364
  card = { ...card, execution: contactFind.execution };
@@ -1352,6 +1399,8 @@ function routeCardFor(card, query) {
1352
1399
  card = { ...card, posture: "read_only", execution: CANONICAL_COPY_READ_EXECUTION };
1353
1400
  else if (card.id === "knowledge-graph" && isWorkspaceVoiceIntent(query))
1354
1401
  card = { ...card, execution: WORKSPACE_VOICE_EXECUTION };
1402
+ if (card.primitive === "agents" && isAdaptiveWatchIntent(query))
1403
+ card = { ...card, execution: ADAPTIVE_WATCH_EXECUTION };
1355
1404
  if (card.primitive === "workflows" && isRecurringCompanyMonitoringIntent(query))
1356
1405
  card = {
1357
1406
  ...card,
@@ -1396,6 +1445,9 @@ function routeCardFor(card, query) {
1396
1445
  const insert = card.primitive === "tables" ? tableRowInsertRecommendation(query) : null;
1397
1446
  if (insert)
1398
1447
  card = { ...card, posture: "workspace_write", execution: insert.execution };
1448
+ const update = card.primitive === "tables" ? tableRowUpdateRecommendation(query) : null;
1449
+ if (update)
1450
+ card = { ...card, posture: "workspace_write", execution: update.execution };
1399
1451
  const sent = card.id === "connected-linkedin" ? sentLinkedInInvitationReadRecommendation(query) : null;
1400
1452
  if (sent)
1401
1453
  card = { ...card, execution: sent.execution };
@@ -1554,6 +1606,14 @@ export const CAPABILITY_ROUTE_MCP_CALLS = {
1554
1606
  sequenceReplyToStop: () => ({ tool: "oxygen_sequences_update", arguments: { sequence: "<sequence>", reply_to_stop_text: "Reply STOP to opt out." } }),
1555
1607
  ugcProgramCreate: () => ({ tool: "oxygen_ugc_program_create", arguments: { product_url: "<your product site>" } }),
1556
1608
  // The first comment rides in `content`, as the CLI's --first-comment does.
1609
+ publishingPostDraft: () => ({
1610
+ tool: "oxygen_publishing_posts_create",
1611
+ arguments: { content_text: "<post>", publish_at: "<iso>", status: "draft" },
1612
+ }),
1613
+ signalCapture: () => ({
1614
+ tool: "oxygen_crm_activity_log",
1615
+ arguments: { object: "people", row_id: "<person rowId or email>", activity_type: "website_visit", direction: "inbound", summary: "<what they viewed>", occurred_at: "<iso>", mode: "live" },
1616
+ }),
1557
1617
  publishingFirstComment: () => ({
1558
1618
  tool: "oxygen_publishing_posts_create",
1559
1619
  arguments: { content_text: "<post>", publish_at: "<iso>", sender_account_id: "<sender_id>", content: { first_comment: "<comment, e.g. the link>" } },
@@ -1649,6 +1709,8 @@ function isSequenceUnsubscribeIntent(query) {
1649
1709
  && !/\b(unsubscribed|lists?|import\w*|suppress\w*|block ?list|blocklist|do not contact|dnc)\b/.test(query);
1650
1710
  }
1651
1711
  /** A post's first comment is set with the post; `publishing posts update` takes the same flag. */
1712
+ export const POST_DRAFT_COMMAND = 'oxygen publishing posts create --draft --publish-at <iso> --text "<post>" --json';
1713
+ export const SIGNAL_CAPTURE_COMMAND = 'oxygen crm activity log people <rowId or email> --type website_visit --direction inbound --summary "<what they viewed>" --live --json';
1652
1714
  export const PUBLISHING_FIRST_COMMENT_COMMAND = 'oxygen publishing posts create --publish-at <iso> --sender <sender_id> --text "<post>" --first-comment "<comment, e.g. the link>" --json';
1653
1715
  /**
1654
1716
  * The comment OXYGEN posts right after a post goes out ("put the link in the first
@@ -1906,7 +1968,7 @@ function tableRowRemovalRecommendation(query) {
1906
1968
  */
1907
1969
  function tableRowInsertRecommendation(query) {
1908
1970
  if (!/\brows?\b/.test(query) || !/\b(add|adding|insert\w*|append\w*|put|enter|write)\b/.test(query)
1909
- || /\b(delete|remove|update|edit|change|set|mark|import\w*|csv|crm|records?|enrich\w*|sync\w*|workflows?|agents?|campaigns?|sequences?|webhooks?|feeds?|functions?|callables?|cells?|dedup\w*|duplicates?)\b/.test(query)
1971
+ || /\b(delete|remove|update|edit|change|set|mark|existing|import\w*|csv|crm|records?|enrich\w*|sync\w*|workflows?|agents?|campaigns?|sequences?|webhooks?|feeds?|functions?|callables?|cells?|dedup\w*|duplicates?)\b/.test(query)
1910
1972
  || /\b(?:add|insert|new)\s+(?:an?\s+|the\s+)?(?:\w+\s+)?columns?\b/.test(query)
1911
1973
  || isSecondPartyDecisionIntent(query))
1912
1974
  return null;
@@ -1916,6 +1978,25 @@ function tableRowInsertRecommendation(query) {
1916
1978
  execution: "Add literal rows with tables rows insert (oxygen_table_rows_insert): table plus rows, an array of {column_key: value} objects. tables create (oxygen_tables_create) makes the columns but takes no rows, so insert after it. Manual rows are a free internal write. Read the rows back with tables query (oxygen_tables_query).",
1917
1979
  };
1918
1980
  }
1981
+ /**
1982
+ * Changing values on rows that already exist is oxygen_rows_update. Without this
1983
+ * card "update rows" / "set a cell" ranked create and enrichment tools, and an
1984
+ * Oxygent concluded no row update existed and offered a formula column instead of
1985
+ * the weekly Workflow it was asked for (System 1 routing case, 2026-10-22).
1986
+ */
1987
+ function tableRowUpdateRecommendation(query) {
1988
+ if (!/\b(rows?|cells?)\b/.test(query)
1989
+ || !(/\b(update\w*|edit\w*|chang\w*|sets?|setting|overwrit\w*|mark\w*)\b/.test(query) || /\bexisting (?:table )?rows?\b/.test(query))
1990
+ || /\b(delete|remove|insert\w*|append\w*|import\w*|csv|crm|records?|enrich\w*|sync\w*|campaigns?|sequences?|functions?|callables?|dedup\w*|duplicates?|history|provenance|rerun|re-run|retry|columns? run|run columns?|formulas?)\b/.test(query)
1991
+ || /\b(?:add|insert|new|create)\s+(?:an?\s+|the\s+)?(?:\w+\s+)?(?:columns?|rows?)\b/.test(query)
1992
+ || isSecondPartyDecisionIntent(query))
1993
+ return null;
1994
+ return {
1995
+ tools: ["oxygen_rows_update", "oxygen_tables_query", "oxygen_rows_upsert"],
1996
+ commands: ["tables update", "tables query", "tables upsert"],
1997
+ execution: "Change values on existing rows with tables update (oxygen_rows_update): table, row_id and values as {column_key: value}, one row per call, a free internal write. Find the rows and their _row_id with tables query (oxygen_tables_query) and filters first; tables upsert (oxygen_rows_upsert) writes by a key column instead. In a Workflow, loop over the query's rows and call oxygen_rows_update with row_id loop.each.item._row_id. Read the rows back after writing.",
1998
+ };
1999
+ }
1919
2000
  /** Reads of grid values/state are separate from executing or resolving actions. */
1920
2001
  function tableReadRecommendation(query) {
1921
2002
  if (isFunctionIntent(query) || isSecondPartyDecisionIntent(query)
@@ -2270,6 +2351,12 @@ function explicitCapabilityIntent(query) {
2270
2351
  return ROUTE_BY_ID.get(readOnly.capabilityId) ?? null;
2271
2352
  if (isOxygentChatIntent(query))
2272
2353
  return ROUTE_BY_ID.get("agents") ?? null;
2354
+ // Values on existing Table rows, ahead of the sourcing rule whose "prospect"
2355
+ // claimed "set a value on rows where stage is prospect". Scheduled or tag
2356
+ // wording keeps its Workflow and Tags owners.
2357
+ if (tableRowUpdateRecommendation(query)
2358
+ && !/\b(workflows?|agents?|routines?|automat\w*|schedul\w*|weekly|daily|nightly|hourly|every|cron|tags?|tagg\w*)\b/.test(query))
2359
+ return ROUTE_BY_ID.get("tables") ?? null;
2273
2360
  // Importing an AI answer is GEO evidence, rather than a Table file import.
2274
2361
  if (/\b(?:geo|aeo|ai (?:(?:search|answer) )?visibility|consumer ai answers?|answer engine optimization|generative engine optimization)\b/.test(query))
2275
2362
  return ROUTE_BY_ID.get("geo") ?? null;
@@ -2385,6 +2472,15 @@ function explicitCapabilityIntent(query) {
2385
2472
  // claimed "a first comment on my LinkedIn post" and "... automatically".
2386
2473
  if (isFirstCommentIntent(query))
2387
2474
  return ROUTE_BY_PRIMITIVE.get("publishing") ?? null;
2475
+ // Writing the workspace's own post is a Publishing draft, ahead of the keyword
2476
+ // post search ("a LinkedIn post about ...") and the public LinkedIn read owner,
2477
+ // which claimed "write a LinkedIn post and save it as a draft".
2478
+ if (isPostDraftIntent(query))
2479
+ return ROUTE_BY_PRIMITIVE.get("publishing") ?? null;
2480
+ // One event the user reports ("log that she visited our pricing page") is a
2481
+ // Signal on the person, not a CRM note or a table row.
2482
+ if (isSignalCaptureIntent(query))
2483
+ return ROUTE_BY_PRIMITIVE.get("signals") ?? null;
2388
2484
  // Publish/approve are writes even when the noun is a public LinkedIn post.
2389
2485
  // The 2026-09-30 connector baseline offered the cookieless read owner first.
2390
2486
  if (isPublishingPostIntent(query))
@@ -2406,6 +2502,8 @@ function explicitCapabilityIntent(query) {
2406
2502
  if (isMailboxVolumeIntent(query) && !/\b(workflow|automation|automate|webhook|cron)\b/.test(query)) {
2407
2503
  return ROUTE_BY_ID.get("sending-infrastructure") ?? null;
2408
2504
  }
2505
+ if (isAdaptiveWatchIntent(query))
2506
+ return ROUTE_BY_PRIMITIVE.get("agents") ?? null;
2409
2507
  if (isHostedWorkflowIntent(query))
2410
2508
  return ROUTE_BY_PRIMITIVE.get("workflows") ?? null;
2411
2509
  if (placementGuideRecommendation(query))
@@ -2582,10 +2680,16 @@ function matchesIntentTerm(query, term) {
2582
2680
  }
2583
2681
  return false;
2584
2682
  }
2585
- function highestScoringRoute(query) {
2683
+ /**
2684
+ * A matched play's skill counts as one multi-word term for each card that names
2685
+ * it first: "cold email the whole market" matched the whole-market play, yet its
2686
+ * one card term ("email") picked Messages over Sequences.
2687
+ */
2688
+ const PLAY_SKILL_TERM_SCORE = 6;
2689
+ function highestScoringRoute(query, playSkill) {
2586
2690
  let best = null;
2587
2691
  for (const card of OXYGEN_CAPABILITY_ROUTES) {
2588
- let score = 0;
2692
+ let score = playSkill !== undefined && card.skills[0] === playSkill ? PLAY_SKILL_TERM_SCORE : 0;
2589
2693
  for (const term of card.intentTerms) {
2590
2694
  if (matchesIntentTerm(query, term))
2591
2695
  score += term.includes(" ") ? 6 : 2;
@@ -2612,6 +2716,25 @@ export const LOCAL_BUSINESS_SCRAPE_COMMAND = `oxygen companies search preview --
2612
2716
  function isLocalBusinessIntent(query) {
2613
2717
  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);
2614
2718
  }
2719
+ /** Writing or saving the workspace's own post, or asking to post it: a Publishing draft. */
2720
+ function isPostDraftIntent(query) {
2721
+ if (/\b(engagers?|engag(?:ed|ing|ement)|comments?|commenters?|react\w*|harvest|scrape|competitors?|creators?|repl(?:y|ies)|tables?|rows?|workflows?|automat\w*|sequences?|campaigns?)\b/.test(query))
2722
+ return false;
2723
+ if (/\b(find|search|research|monitor|watch)\b/.test(query))
2724
+ return false;
2725
+ return (/\b(write|draft|drafts|drafted|compose|save|saved)\b/.test(query) && /\bposts?\b/.test(query))
2726
+ || /\bpost (?:it|this|that)\b|\bpost (?:on|to) (?:linkedin|x|twitter)\b/.test(query);
2727
+ }
2728
+ /** Recording one reported event (a visit, a view) on a person: a Signal, logged on the Record. */
2729
+ function isSignalCaptureIntent(query) {
2730
+ return /\b(record|log|logs|logged|track|note|save|add|capture)\b/.test(query)
2731
+ && /\b(visit(?:s|ed|or)?|viewed|profile views?|signals?)\b/.test(query)
2732
+ // Outreach wording ("set up an email follow-up ... add the people") is a
2733
+ // Sequence for signalled people, not one reported event.
2734
+ && !/\b(list|show|read|which|who|rank|leads? to contact|registry|search|find|inbox|webhook|automat\w*|workflows?|sequences?|enrich\w*|tables?|columns?|feeds?|follow ups?|campaigns?|cadences?|outreach|emails)\b/.test(query);
2735
+ }
2736
+ const POST_DRAFT_EXECUTION = "Read the voice page first (oxygen_knowledge_page_get, slug voice) and use only numbers a page states. Save the post with oxygen_publishing_posts_create, status draft and no approved flag: it needs no sender, publishes nothing and opens in Publishing for review. A post written only in the reply is not saved. Publishing it is a separate oxygen_publishing_posts_approve after the user says yes; a missing connected sender is the one blocker to name. End by saying it is a draft, not live, with its web_url; if the save is refused, say the post exists only in this chat.";
2737
+ const SIGNAL_CAPTURE_EXECUTION = "Find the person with oxygen_crm_record_search; if there is no record, create it with oxygen_crm_record_assert (identity email, values full_name and company_domain when known; free, no card). Then log the event on that record with oxygen_crm_activity_log: row_id the rowId the assert or search returned (not object.id) or her email, activity_type website_visit (or profile_view, post_reaction, new_follower), direction inbound, occurred_at, a one-line summary such as the page viewed. It then shows in the Signals feed and on the person's timeline. Not a note or a table row; contacting nobody. End by saying it is recorded as a website-visit Signal and where it shows (the record's web_url); if a write is refused, say nothing was recorded.";
2615
2738
  function isPublishingPostIntent(query) {
2616
2739
  return /\b(posts?|publishing|content calendar)\b/.test(query)
2617
2740
  && /\b(publish|publishing|schedule|scheduled|approve|approval)\b/.test(query)
@@ -2753,6 +2876,22 @@ function recommendationsFor(card, query) {
2753
2876
  exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.ugcProgramCreate(),
2754
2877
  };
2755
2878
  }
2879
+ if (card.primitive === "publishing" && isPostDraftIntent(query)) {
2880
+ return {
2881
+ tools: ["oxygen_knowledge_page_get", "oxygen_publishing_posts_create", "oxygen_publishing_posts_get"],
2882
+ commands: ["knowledge page get", "publishing posts create", "publishing posts get"],
2883
+ exactCommand: POST_DRAFT_COMMAND,
2884
+ exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.publishingPostDraft(),
2885
+ };
2886
+ }
2887
+ if (card.primitive === "signals" && isSignalCaptureIntent(query)) {
2888
+ return {
2889
+ tools: ["oxygen_crm_record_search", "oxygen_crm_record_assert", "oxygen_crm_activity_log", "oxygen_signals_list"],
2890
+ commands: ["crm search", "crm assert", "crm activity log", "signals list"],
2891
+ exactCommand: SIGNAL_CAPTURE_COMMAND,
2892
+ exactMcpCall: CAPABILITY_ROUTE_MCP_CALLS.signalCapture(),
2893
+ };
2894
+ }
2756
2895
  if (card.primitive === "publishing" && isFirstCommentIntent(query)) {
2757
2896
  return {
2758
2897
  tools: ["oxygen_publishing_posts_create", "oxygen_publishing_posts_update", "oxygen_publishing_posts_get"],
@@ -2971,6 +3110,9 @@ function recommendationsFor(card, query) {
2971
3110
  const insert = tableRowInsertRecommendation(query);
2972
3111
  if (insert)
2973
3112
  return insert;
3113
+ const update = tableRowUpdateRecommendation(query);
3114
+ if (update)
3115
+ return update;
2974
3116
  const read = tableReadRecommendation(query);
2975
3117
  if (read)
2976
3118
  return read;
@@ -3278,6 +3420,14 @@ function recommendationsFor(card, query) {
3278
3420
  commands: ["workflows templates search", "workflows templates describe", "workflows templates preflight", "workflows templates apply"],
3279
3421
  };
3280
3422
  }
3423
+ // A scheduled change to existing rows: the query and row-update tools the
3424
+ // graph's nodes call rank beside the authoring gateway.
3425
+ if (/\b(rows?|cells?)\b/.test(query) && /\b(updat\w*|edit\w*|chang\w*|sets?|setting|tag\w*|mark\w*|writ\w*)\b/.test(query)
3426
+ && !/\b(insert\w*|add\w*|append\w*|new rows?|delete\w*|remove\w*)\b/.test(query))
3427
+ return {
3428
+ tools: ["oxygen_workflows_schema", "oxygen_workflows_lint", "oxygen_tables_query", "oxygen_rows_update", "oxygen_workflows_apply"],
3429
+ commands: ["workflows schema", "workflows lint", "tables query", "tables update", "workflows apply"],
3430
+ };
3281
3431
  if (isNetNewLinkedInInitiation(query) || /\b(outreach|sequence|enroll|message)\b/.test(query)) {
3282
3432
  return {
3283
3433
  tools: ["oxygen_workflows_schema", "oxygen_workflows_lint", "oxygen_workflows_apply", "oxygen_tools_get", "oxygen_sequences_list", "oxygen_sequences_start"],
@@ -3733,6 +3883,20 @@ function isHostedWorkflowIntent(query) {
3733
3883
  return /\b(nightly|hourly|daily|weekly|every (?:monday|tuesday|wednesday|thursday|friday|saturday|sunday|day|night|week|month))\b/.test(query)
3734
3884
  || (/\bschedule(?:d)?\b/.test(query) && !/\bpost|content\b/.test(query));
3735
3885
  }
3886
+ /**
3887
+ * Watching items that change and working out what each one needs is an Agent's
3888
+ * judgment, not a Workflow's fixed rule: "keep an eye on open deals and suggest
3889
+ * what to do next for each" routed to Records reads, and the Oxygent proposed a
3890
+ * routine without saying why (System 1 routing case, 2026-10-22).
3891
+ */
3892
+ function isAdaptiveWatchIntent(query) {
3893
+ if (/\b(workflows?|webhooks?|cron|deterministic|fixed rule|sequences?|campaigns?|posts?|mailbox\w*|deliverability|bounce\w*)\b/.test(query))
3894
+ return false;
3895
+ return /\b(keeps? an eye on|keeps? tabs on|keeps? track of|watch(?:es|ing)?|monitor(?:s|ing)?|look(?:s|ing)? after)\b/.test(query)
3896
+ && /\b(suggest\w*|decid\w*|works? out|figures? out|recommend\w*|judg\w*|what to do|next steps?|next moves?)\b/.test(query)
3897
+ && /\b(each|every|individual\w*|differently|deals?|pipeline|opportunit(?:y|ies)|accounts?)\b/.test(query);
3898
+ }
3899
+ const ADAPTIVE_WATCH_EXECUTION = "Watching items that change and deciding what fits each one is an Agent's job: each run looks at every item and works out what it needs, while a Workflow repeats one fixed rule. Tell the person that in one plain line. Set it up as an ongoing Agent session with per-run and rolling 24-hour credit caps; in an Oxygent chat, propose a routine and put that line in its why. Read the items first so the goal names real fields (for deals: stage, amount, last activity). Size the caps to one check, about 10 credits a run for reading a few dozen records and writing suggestions, and say so when that is above a limit the person gave. Suggestions stay inside OXYGEN (a task or a note); contacting anyone needs its own approval.";
3736
3900
  function isRecurringCompanyMonitoringIntent(query) {
3737
3901
  return /\b(companies|company|competitors?)\b/.test(query)
3738
3902
  && /\b(monitor(?:ing)?|watch(?:ing|list)?|track(?:ing)?)\b/.test(query)
@@ -58,6 +58,7 @@ export declare const CLAY_MIGRATION_CONTRACT: import("./schema.js").ContractObje
58
58
  enum: readonly ("columns" | "fields" | "graph" | "metadata" | "record_ids" | "records" | "row" | "rows" | "sequence")[];
59
59
  description?: string;
60
60
  };
61
+ readonly full: import("./schema.js").BooleanProperty;
61
62
  readonly path: import("./schema.js").StringProperty;
62
63
  readonly text_offset: {
63
64
  type: "integer";
@@ -2,6 +2,10 @@ import { arr, bool, enumOf, int, obj, str } from "./schema.js";
2
2
  export const CLAY_MIGRATION_ACTIONS = ["connect", "status", "discover", "read", "transfer", "checkpoint", "report", "disconnect"];
3
3
  export const CLAY_RESOURCE_TYPES = ["workspace", "folder", "workbook", "table", "audience", "workflow", "function", "claygent", "knowledge", "schedule", "campaign"];
4
4
  export const CLAY_RESOURCE_DISPOSITIONS = ["pending", "copied", "rebuilt", "needs_decision", "unsupported", "inaccessible", "excluded", "failed"];
5
+ const FULL_READ_SECTIONS = {
6
+ table: ["metadata", "columns", "rows", "row"], audience: ["metadata", "fields", "record_ids", "records"],
7
+ workflow: ["metadata", "graph"], campaign: ["metadata", "sequence"],
8
+ };
5
9
  export const CLAY_GLOBAL_AUDIENCE_SCOPES = {
6
10
  "global-people": { entity_type: "people", global: true },
7
11
  "global-companies": { entity_type: "companies", global: true },
@@ -21,6 +25,7 @@ export const CLAY_MIGRATION_CONTRACT = obj({
21
25
  resource_id: str({ minLength: 1, maxLength: 256 }),
22
26
  row_id: str({ minLength: 1, maxLength: 200, pattern: "^[a-zA-Z0-9][a-zA-Z0-9_-]*$", description: "Source row ID for table read section=row; retains structured cell fields separately from displayed values." }),
23
27
  section: enumOf(["metadata", "columns", "rows", "row", "graph", "fields", "record_ids", "records", "sequence"]),
28
+ full: bool({ description: "Export one entire adapter response (8 MiB data + 1 MiB envelope); config credential redaction applies. No path/text projection. Discover omits derived resources; use report for dispositions/targets." }),
24
29
  path: str({ minLength: 1, maxLength: 1000, description: "Dotted path or JSON pointer into a source response, e.g. cells.f_1.fields." }),
25
30
  text_offset: int({ minimum: 0, maximum: 8388608, description: "Continue a large scalar read at next_text_offset." }),
26
31
  entity_type: enumOf(["people", "companies", "deals"]),
@@ -57,6 +62,27 @@ export function clayMigrationInputIssues(args) {
57
62
  }
58
63
  if (args.action === "checkpoint")
59
64
  requireField("disposition");
65
+ if (args.full !== undefined && !["read", "discover"].includes(String(args.action)))
66
+ issues.push({ path: "full", message: "full is only available for read or discover." });
67
+ if (args.full === true) {
68
+ if (args.action === "discover") {
69
+ requireField("resource_type");
70
+ for (const key of ["section", "resource_id", "record_ids"]) {
71
+ if (args[key] !== undefined)
72
+ issues.push({ path: key, message: `${key} is unavailable for a complete catalog response; use read for an individual resource section.` });
73
+ }
74
+ }
75
+ const sections = Object.hasOwn(FULL_READ_SECTIONS, String(args.resource_type)) ? FULL_READ_SECTIONS[args.resource_type] : ["metadata"];
76
+ if (args.action === "read" && !sections.includes(String(args.section ?? "metadata"))) {
77
+ issues.push({ path: "section", message: "This resource does not expose that section for complete export." });
78
+ }
79
+ if (args.entity_type !== undefined && args.resource_type !== "audience")
80
+ issues.push({ path: "entity_type", message: "entity_type only identifies Audience source responses." });
81
+ for (const key of ["path", "text_offset"]) {
82
+ if (args[key] !== undefined)
83
+ issues.push({ path: key, message: `${key} cannot be combined with full; export the entire response or inspect a projection.` });
84
+ }
85
+ }
60
86
  if (args.section === "records") {
61
87
  requireField("entity_type");
62
88
  requireField("record_ids");
@@ -82,7 +108,7 @@ export const CLAY_MIGRATION_OPERATION = {
82
108
  id: "clay.migration", route: "/api/cli/migrations/clay", method: "POST",
83
109
  contract: CLAY_MIGRATION_CONTRACT, inputCheck: clayMigrationInputIssues,
84
110
  generated: { cli: false, mcp: false },
85
- cli: { path: ["clay", "migration"], summary: "Migrate a Clay workspace into Oxygen: connect, discover source configuration, inspect bounded samples, transfer table and Audience rows, checkpoint native rebuilds and reconcile coverage. Creates no paid runs or active schedules." },
86
- mcp: { name: "oxygen_clay_migration", title: "Oxygen: Migrate from Clay", description: "Connect to Clay with browser sign-in, discover resources, read source configuration and samples, transfer table and Audience rows to existing Oxygen tables, checkpoint rebuilds and report coverage. Use native Oxygen tools to create targets and rebuild behavior. Follow cursors; report unsupported or inaccessible resources. Never activates schedules or paid runs.", pack: "integrations", annotations: { openWorldHint: true, destructiveHint: false, idempotentHint: false } },
111
+ cli: { path: ["clay", "migration"], summary: "Migrate a Clay workspace into Oxygen: connect, discover configuration, inspect samples or export complete responses, transfer table and Audience rows, checkpoint native rebuilds and reconcile coverage. Creates no paid runs or active schedules. Complete exports (--full): 8 MiB data + 1 MiB envelope. Full discovery omits derived resources; report retains dispositions and targets." },
112
+ mcp: { name: "oxygen_clay_migration", title: "Oxygen: Migrate from Clay", description: "Connect to Clay with browser sign-in, discover resources, inspect or export complete source responses, transfer table and Audience rows to existing Oxygen tables, checkpoint rebuilds and report coverage. Use native Oxygen tools to create targets and rebuild behavior. Follow cursors; report unsupported or inaccessible resources. Never activates schedules or paid runs. Full exports: 8 MiB data + 1 MiB envelope; discover omits derived resources, report retains dispositions and targets.", pack: "integrations", annotations: { openWorldHint: true, destructiveHint: false, idempotentHint: false } },
87
113
  mutates: true, spends: false,
88
114
  };
@@ -0,0 +1,24 @@
1
+ export declare const COPILOT_ATTACHMENT_READ_CONTRACT: import("./schema.js").ContractObject<{
2
+ readonly offset: {
3
+ type: "integer";
4
+ description?: string;
5
+ minimum?: number;
6
+ maximum?: number;
7
+ "x-clamp"?: true;
8
+ };
9
+ readonly limit: {
10
+ type: "integer";
11
+ description?: string;
12
+ minimum?: number;
13
+ maximum?: number;
14
+ "x-clamp"?: true;
15
+ };
16
+ readonly version: import("./schema.js").StringProperty;
17
+ readonly max_chars: {
18
+ type: "integer";
19
+ description?: string;
20
+ minimum?: number;
21
+ maximum?: number;
22
+ "x-clamp"?: true;
23
+ };
24
+ }, never>;
@@ -0,0 +1,7 @@
1
+ import { int, obj, str } from "./schema.js";
2
+ export const COPILOT_ATTACHMENT_READ_CONTRACT = obj({
3
+ offset: int({ minimum: 0, maximum: 512 * 1024 * 1024, description: "Byte offset; default 0. Resume at next_offset." }),
4
+ limit: int({ minimum: 1, maximum: 2048, description: "Original bytes requested; default 2048. Context bounds may return fewer." }),
5
+ version: str({ minLength: 64, maxLength: 64, description: "Opaque file revision returned by the first read; required when resuming." }),
6
+ max_chars: int({ minimum: 1024, maximum: 40_000, description: "Complete serialized byte-window size." }),
7
+ });
@@ -136,7 +136,12 @@ export const TEAMMATE_OPERATIONS = [
136
136
  make_admin: GROUP_PEOPLE("People in it to make admins, by workspace email."),
137
137
  remove_admin: GROUP_PEOPLE("Admins to make members again, by workspace email; a group keeps at least one admin."),
138
138
  }),
139
- teammateGroupOperation("teammate_groups.message", "group_message", "POST", "/{group_id}/messages", "Send a message in a group chat. The Oxygents it @mentions answer, in order; with no mention, the Oxygents it is for answer, or, in a group of only you, whoever answered last. A message that @mentions only people goes to them. In a group with other people, each answer waits for the one before it, and paid or external actions ask whoever sent the message. Each answer is a turn billed from workspace credits.", { message: str({ minLength: 1, maxLength: 16000 }) }, ["message"], true),
139
+ teammateGroupOperation("teammate_groups.message", "group_message", "POST", "/{group_id}/messages", "Send a message and uploaded files in a group chat. group_id is its Copilot session id: upload with oxygen copilot attach <group_id> <path>, inspect files with copilot attachments (oxygen_copilot_attachments), then bind attachment_ids here; read messages with copilot get (oxygen_copilot_get). The Oxygents it @mentions answer in order; otherwise those it is for answer, or whoever answered last in a group of only you. A mention of only people goes to them. Answers wait for earlier ones and are billed from workspace credits; paid or external actions ask the sender.", {
140
+ message: str({ minLength: 1, maxLength: 16000 }),
141
+ attachment_ids: arr(str({ pattern: "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" }), {
142
+ description: "File UUIDs uploaded by you to this group session and ready to send. Omit for text only.",
143
+ }),
144
+ }, ["message"], true),
140
145
  teammateGroupOperation("teammate_groups.read", "group_read", "POST", "/{group_id}/read", "Mark a group chat read up to its newest turn, or up to turn_index.", { turn_index: int({ minimum: 0 }) }),
141
146
  teammateGroupOperation("teammate_groups.leave", "group_leave", "POST", "/{group_id}/leave", "Leave a group chat: it disappears for you and its Oxygents stop answering your waiting messages. If you were its last admin, whoever has been in it longest becomes one; if you were the last person, it ends."),
142
147
  teammateGroupOperation("teammate_groups.end", "group_end", "POST", "/{group_id}/end", "End a group chat for everyone in it, as one of its admins: nobody answers in it again, and its messages stay readable."),
@@ -0,0 +1,108 @@
1
+ /**
2
+ * The workspace's newsletter brand kit (tenant table ox_sequencer.email_brand_kit,
3
+ * migration 0559): the logo, colours, width and font newsletters are designed
4
+ * with, the company name and postal address every bulk email carries in its
5
+ * footer, social links and the preference page's title.
6
+ *
7
+ * One parser serves the `/api/cli/sequencer/brand-kit` route (web, CLI, MCP):
8
+ * a patch names only the fields it changes, `null` clears one (a colour, width
9
+ * or font falls back to its default). Pure and browser-safe.
10
+ */
11
+ export declare const EMAIL_BRAND_FONT_FAMILIES: readonly ["system", "georgia", "arial", "helvetica", "verdana"];
12
+ export type EmailBrandFontFamily = typeof EMAIL_BRAND_FONT_FAMILIES[number];
13
+ export declare const EMAIL_BRAND_FONT_LABELS: Record<EmailBrandFontFamily, string>;
14
+ /** The CSS font stack each allowlisted family renders with in an email. */
15
+ export declare const EMAIL_BRAND_FONT_STACKS: Record<EmailBrandFontFamily, string>;
16
+ export declare const EMAIL_BRAND_SOCIAL_NETWORKS: readonly ["website", "linkedin", "x", "facebook", "instagram", "youtube", "tiktok", "github"];
17
+ export type EmailBrandSocialNetwork = typeof EMAIL_BRAND_SOCIAL_NETWORKS[number];
18
+ export declare const EMAIL_BRAND_COLOR_FIELDS: readonly ["background_color", "content_background_color", "text_color", "link_color", "button_color", "button_text_color"];
19
+ export type EmailBrandColorField = typeof EMAIL_BRAND_COLOR_FIELDS[number];
20
+ export declare const EMAIL_BRAND_LIMITS: {
21
+ readonly containerWidthMin: 480;
22
+ readonly containerWidthMax: 800;
23
+ readonly logoWidthMin: 40;
24
+ readonly logoWidthMax: 600;
25
+ readonly urlMax: 2048;
26
+ readonly companyNameMax: 120;
27
+ readonly postalAddressMax: 500;
28
+ readonly socialLinksMax: 10;
29
+ readonly preferencePageTitleMax: 120;
30
+ readonly preferencePageDescriptionMax: 500;
31
+ };
32
+ export type EmailBrandSocialLink = {
33
+ network: EmailBrandSocialNetwork;
34
+ url: string;
35
+ };
36
+ /** The kit as every surface reads it (snake_case wire shape), defaults applied. */
37
+ export type EmailBrandKit = {
38
+ logo_url: string | null;
39
+ logo_width: number | null;
40
+ background_color: string;
41
+ content_background_color: string;
42
+ text_color: string;
43
+ link_color: string;
44
+ button_color: string;
45
+ button_text_color: string;
46
+ container_width: number;
47
+ font_family: EmailBrandFontFamily;
48
+ company_name: string | null;
49
+ postal_address: string | null;
50
+ social_links: EmailBrandSocialLink[];
51
+ preference_page_title: string | null;
52
+ preference_page_description: string | null;
53
+ updated_by: string | null;
54
+ updated_at: string | null;
55
+ };
56
+ export declare const DEFAULT_EMAIL_BRAND_COLORS: Record<EmailBrandColorField, string>;
57
+ export declare const DEFAULT_EMAIL_BRAND_KIT: EmailBrandKit;
58
+ /** Every editable field; a patch names a subset. */
59
+ export declare const EMAIL_BRAND_KIT_FIELDS: readonly ["logo_url", "logo_width", "background_color", "content_background_color", "text_color", "link_color", "button_color", "button_text_color", "container_width", "font_family", "company_name", "postal_address", "social_links", "preference_page_title", "preference_page_description"];
60
+ export type EmailBrandKitField = typeof EMAIL_BRAND_KIT_FIELDS[number];
61
+ /** A validated change: a present key is written, `null` clears it. */
62
+ export type EmailBrandKitPatch = {
63
+ logo_url?: string | null;
64
+ logo_width?: number | null;
65
+ background_color?: string | null;
66
+ content_background_color?: string | null;
67
+ text_color?: string | null;
68
+ link_color?: string | null;
69
+ button_color?: string | null;
70
+ button_text_color?: string | null;
71
+ container_width?: number | null;
72
+ font_family?: EmailBrandFontFamily | null;
73
+ company_name?: string | null;
74
+ postal_address?: string | null;
75
+ social_links?: EmailBrandSocialLink[] | null;
76
+ preference_page_title?: string | null;
77
+ preference_page_description?: string | null;
78
+ };
79
+ export declare class EmailBrandKitInputError extends Error {
80
+ readonly field: string;
81
+ readonly code = "invalid_brand_kit";
82
+ constructor(field: string, message: string);
83
+ }
84
+ /** `#rrggbb` lowercase from `#rrggbb`, `rrggbb` or `#rgb`; throws on anything else. */
85
+ export declare function normalizeBrandColor(value: unknown, field: string): string;
86
+ /** An absolute https URL; email clients block or warn on plain http images and links. */
87
+ export declare function normalizeBrandUrl(value: unknown, field: string): string;
88
+ export declare function isEmailBrandFontFamily(value: unknown): value is EmailBrandFontFamily;
89
+ /**
90
+ * Validate a brand kit change. Only the named fields are returned; `null` (or
91
+ * an empty string for text) clears one. Throws `EmailBrandKitInputError` naming
92
+ * the field, and refuses a body that names no field.
93
+ */
94
+ export declare function parseEmailBrandKitPatch(body: Record<string, unknown>): EmailBrandKitPatch;
95
+ /** A newsletter may go live only once the kit carries a postal address. */
96
+ export declare function brandKitHasPostalAddress(kit: Pick<EmailBrandKit, "postal_address"> | null | undefined): boolean;
97
+ /** The email editor's global styles, as the composer and its preview consume them. */
98
+ export type EmailBrandGlobalStyles = {
99
+ background: string;
100
+ contentBackground: string;
101
+ text: string;
102
+ link: string;
103
+ button: string;
104
+ buttonText: string;
105
+ containerWidth: number;
106
+ fontFamily: string;
107
+ };
108
+ export declare function brandKitGlobalStyles(kit: Partial<EmailBrandKit> | null | undefined): EmailBrandGlobalStyles;