@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.
- package/README.md +1 -1
- package/dist/command-families/program.js +4 -0
- package/dist/command-manifest.js +14 -2
- package/dist/command-support.js +1 -1
- package/dist/commands/admin.js +82 -1
- package/dist/commands/billing.js +1 -1
- package/dist/commands/copilot.js +29 -7
- package/dist/commands/domains.js +7 -0
- package/dist/commands/email-assets.d.ts +3 -0
- package/dist/commands/email-assets.js +78 -0
- package/dist/commands/email-templates.d.ts +3 -0
- package/dist/commands/email-templates.js +101 -0
- package/dist/commands/sequences.js +47 -12
- package/dist/commands/subscriptions.d.ts +2 -4
- package/dist/commands/subscriptions.js +297 -51
- package/dist/sequence-start-at.d.ts +7 -0
- package/dist/sequence-start-at.js +21 -0
- package/dist/sequences-brand-kit-command.d.ts +28 -0
- package/dist/sequences-brand-kit-command.js +83 -0
- package/dist/sequences-content-commands.d.ts +28 -0
- package/dist/sequences-content-commands.js +176 -0
- package/dist/sequences-newsletter-commands.d.ts +6 -0
- package/dist/sequences-newsletter-commands.js +108 -0
- package/dist/sequences-test-send-command.d.ts +3 -0
- package/dist/sequences-test-send-command.js +47 -0
- package/node_modules/@oxygen/formula/dist/evaluate.js +2 -4
- package/node_modules/@oxygen/formula/dist/expression.d.ts +7 -5
- package/node_modules/@oxygen/formula/dist/expression.js +26 -4
- package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +6 -0
- package/node_modules/@oxygen/formula/dist/formula-functions.js +55 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/billing.js +5 -7
- package/node_modules/@oxygen/shared/dist/bulk-email-pacing.d.ts +105 -0
- package/node_modules/@oxygen/shared/dist/bulk-email-pacing.js +125 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +174 -10
- package/node_modules/@oxygen/shared/dist/contracts/clay-migration.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/clay-migration.js +28 -2
- package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.js +7 -0
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +6 -1
- package/node_modules/@oxygen/shared/dist/email-brand-kit.d.ts +108 -0
- package/node_modules/@oxygen/shared/dist/email-brand-kit.js +214 -0
- package/node_modules/@oxygen/shared/dist/email-delivery.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/email-delivery.js +14 -0
- package/node_modules/@oxygen/shared/dist/email-documents.d.ts +61 -0
- package/node_modules/@oxygen/shared/dist/email-documents.js +143 -0
- package/node_modules/@oxygen/shared/dist/email-dsn.js +52 -10
- package/node_modules/@oxygen/shared/dist/email-html-sanitize.d.ts +23 -0
- package/node_modules/@oxygen/shared/dist/email-html-sanitize.js +96 -0
- package/node_modules/@oxygen/shared/dist/email-subscriptions.d.ts +63 -1
- package/node_modules/@oxygen/shared/dist/email-subscriptions.js +149 -1
- package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.d.ts +41 -2
- package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.js +50 -1
- package/node_modules/@oxygen/shared/dist/newsletter-analytics.d.ts +90 -0
- package/node_modules/@oxygen/shared/dist/newsletter-analytics.js +28 -0
- package/node_modules/@oxygen/shared/dist/newsletter-test-send.d.ts +20 -0
- package/node_modules/@oxygen/shared/dist/newsletter-test-send.js +46 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +12 -0
- package/node_modules/@oxygen/shared/dist/plan-band.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/plan-band.js +17 -0
- package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +4 -2
- package/node_modules/@oxygen/shared/dist/plan-capabilities.js +12 -1
- package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.js +3 -0
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +5 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +29 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-faults.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-faults.js +53 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-rollout.d.ts +24 -1
- package/node_modules/@oxygen/shared/dist/sending-domain-rollout.js +51 -4
- package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +10 -1
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +68 -6
- package/node_modules/@oxygen/shared/dist/sequence-lifecycle.d.ts +34 -3
- package/node_modules/@oxygen/shared/dist/sequence-lifecycle.js +57 -4
- package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +41 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +86 -4
- package/node_modules/@oxygen/shared/dist/ses-identity-policy.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/ses-identity-policy.js +54 -0
- package/node_modules/@oxygen/shared/dist/skill-play-routing.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/skill-play-routing.js +75 -0
- package/node_modules/@oxygen/shared/dist/skill-plays.generated.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/skill-plays.generated.js +269 -0
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/spend-safety.js +7 -0
- package/node_modules/@oxygen/shared/dist/teammates.js +2 -1
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +15 -1
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +2 -2
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
86
|
-
mcp: { name: "oxygen_clay_migration", title: "Oxygen: Migrate from Clay", description: "Connect to Clay with browser sign-in, discover resources,
|
|
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.
|
|
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;
|