@oxygen-agent/cli 1.638.1 → 1.677.10

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.
@@ -14,6 +14,7 @@ export const RETYPEABLE_DATA_TYPES = [
14
14
  "text",
15
15
  "numeric",
16
16
  "boolean",
17
+ "jsonb",
17
18
  "timestamptz",
18
19
  ];
19
20
  // Excel/Sheets store dates as a serial number of days since 1899-12-30.
@@ -134,7 +135,17 @@ value, dataType) {
134
135
  }
135
136
  switch (dataType) { // skipcq: JS-0047
136
137
  case "text":
137
- return { ok: true, value: typeof value === "string" ? value : String(value) };
138
+ if (typeof value === "string")
139
+ return { ok: true, value };
140
+ if (typeof value === "object") {
141
+ try {
142
+ return { ok: true, value: JSON.stringify(value) };
143
+ }
144
+ catch {
145
+ return { ok: false, reason: "not JSON-serializable text" };
146
+ }
147
+ }
148
+ return { ok: true, value: String(value) };
138
149
  case "timestamptz": {
139
150
  const iso = parseDateValueToIso(value, { allowExcelSerial: true });
140
151
  return iso
@@ -157,5 +168,19 @@ value, dataType) {
157
168
  }
158
169
  return { ok: false, reason: "not a recognized boolean" };
159
170
  }
171
+ case "jsonb": {
172
+ try {
173
+ if (typeof value === "string") {
174
+ return { ok: true, value: JSON.parse(value.trim()) };
175
+ }
176
+ const serialized = JSON.stringify(value);
177
+ if (serialized === undefined)
178
+ return { ok: false, reason: "not valid JSON" };
179
+ return { ok: true, value: JSON.parse(serialized) };
180
+ }
181
+ catch {
182
+ return { ok: false, reason: "not valid JSON" };
183
+ }
184
+ }
160
185
  }
161
186
  }
@@ -5,9 +5,18 @@ export type CreditGuidance = {
5
5
  available_credits: number | null;
6
6
  credit_posture: CreditPosture;
7
7
  credit_guidance: string;
8
+ /** Present only when the caller supplied an expected (average) figure. */
9
+ expected_credits?: number;
8
10
  };
9
11
  export declare function buildCreditGuidance(input: {
10
12
  estimatedCredits: number | null | undefined;
11
13
  availableCredits: number | null | undefined;
12
14
  headroomMultiplier?: number;
15
+ /** The average a run actually costs. When given, the ceiling is sized from this. */
16
+ expectedCredits?: number | null | undefined;
17
+ /** Dearest single lane a row can reach — the tail the band covers. */
18
+ worstCaseCreditsPerRow?: number | null | undefined;
19
+ rowCount?: number | null | undefined;
20
+ /** True when estimatedCredits is a genuine per-row bound (bill-on-hit lanes). */
21
+ worstCaseIsHardBound?: boolean;
13
22
  }): CreditGuidance;
@@ -1,10 +1,66 @@
1
1
  const DEFAULT_HEADROOM_MULTIPLIER = 1.25;
2
+ /**
3
+ * Size a ceiling from the EXPECTED cost plus a variance band, rather than from
4
+ * the worst case.
5
+ *
6
+ * A waterfall lane bills only on a hit and a row stops at the first lane that
7
+ * resolves it, so the worst case is the dearest lane on EVERY row — a tail event,
8
+ * not a price. Quoting it made a 4.8k job read as 31.6k and told workspaces with
9
+ * ample balance they were "tight".
10
+ *
11
+ * The band is `expected + worst_row x tailRows`, where `tailRows` scales like
12
+ * sqrt(n): unlucky rows are a sample of the tail, and their count grows with the
13
+ * square root of the run, not linearly. `+1` guarantees a run can always afford at
14
+ * least one row above the mean, and `min(n, ...)` collapses the whole thing to the
15
+ * true worst case on tiny runs, where it is cheap to just cover everything.
16
+ *
17
+ * `expected x 1.25` remains the floor, so a flat/cheap chain with no meaningful
18
+ * tail keeps exactly the old headroom.
19
+ */
20
+ function expectedCreditCeiling(input) {
21
+ const floor = input.expectedCredits * input.headroomMultiplier;
22
+ let ceiling = floor;
23
+ if (input.worstCaseCreditsPerRow !== null && input.rowCount !== null && input.rowCount > 0) {
24
+ const tailRows = Math.min(input.rowCount, Math.ceil(Math.sqrt(input.rowCount)) + 1);
25
+ ceiling = Math.max(floor, input.expectedCredits + input.worstCaseCreditsPerRow * tailRows);
26
+ }
27
+ // Only clamp where the worst case is a REAL per-row bound — bill-on-hit lanes,
28
+ // where one lane is the most a row can cost. An AI/tool column's "estimate" is a
29
+ // token guess that real usage can exceed, so clamping there would reintroduce
30
+ // mid-run truncation on exactly the columns that have no ceiling of their own.
31
+ //
32
+ // Clamp to the worst case WITH its headroom, not to the bare figure: the row
33
+ // count is itself an estimate for a filtered selection, and the multiplier is
34
+ // what absorbs an undercount. Clamping to the bare total would make a small run
35
+ // strictly tighter than before this change, which is the opposite of the point.
36
+ if (input.worstCaseIsHardBound && input.worstCaseTotal !== null) {
37
+ ceiling = Math.min(ceiling, input.worstCaseTotal * input.headroomMultiplier);
38
+ }
39
+ return Math.max(1, Math.ceil(ceiling));
40
+ }
2
41
  export function buildCreditGuidance(input) {
3
42
  const estimatedCredits = normalizeCreditValue(input.estimatedCredits);
4
43
  const availableCredits = normalizeCreditValue(input.availableCredits);
5
- const recommendedMaxCredits = recommendedCreditCeiling(estimatedCredits, input.headroomMultiplier ?? DEFAULT_HEADROOM_MULTIPLIER);
44
+ const expectedCredits = normalizeCreditValue(input.expectedCredits);
45
+ const headroomMultiplier = normalizeMultiplier(input.headroomMultiplier);
46
+ // No expected figure supplied => byte-identical to the pre-existing behaviour,
47
+ // so publishing / sequences / AI callers are untouched by this change.
48
+ const recommendedMaxCredits = expectedCredits === null
49
+ ? recommendedCreditCeiling(estimatedCredits, headroomMultiplier)
50
+ : expectedCreditCeiling({
51
+ expectedCredits,
52
+ worstCaseCreditsPerRow: normalizeCreditValue(input.worstCaseCreditsPerRow),
53
+ worstCaseTotal: estimatedCredits,
54
+ rowCount: typeof input.rowCount === "number" && Number.isFinite(input.rowCount)
55
+ ? Math.max(0, Math.floor(input.rowCount))
56
+ : null,
57
+ headroomMultiplier,
58
+ worstCaseIsHardBound: input.worstCaseIsHardBound === true,
59
+ });
60
+ const expectedFields = expectedCredits === null ? {} : { expected_credits: expectedCredits };
6
61
  if (estimatedCredits === null) {
7
62
  return {
63
+ ...expectedFields,
8
64
  estimated_credits: null,
9
65
  recommended_max_credits: null,
10
66
  available_credits: availableCredits,
@@ -14,6 +70,7 @@ export function buildCreditGuidance(input) {
14
70
  }
15
71
  if (availableCredits === null) {
16
72
  return {
73
+ ...expectedFields,
17
74
  estimated_credits: estimatedCredits,
18
75
  recommended_max_credits: recommendedMaxCredits,
19
76
  available_credits: null,
@@ -22,7 +79,30 @@ export function buildCreditGuidance(input) {
22
79
  };
23
80
  }
24
81
  if (recommendedMaxCredits !== null && recommendedMaxCredits <= availableCredits) {
82
+ // When an expected figure drove the ceiling, the copy has to describe THAT,
83
+ // not "the estimate plus 25% headroom" — the old sentence would misstate how
84
+ // the number was derived. The tail warning is added only when a full
85
+ // fall-through really could outrun the balance; saying it otherwise invents
86
+ // a risk that does not exist.
87
+ if (expectedCredits !== null) {
88
+ const tailWarning = estimatedCredits > availableCredits
89
+ ? " Only a run where nearly every row falls through to the dearest provider could exceed your"
90
+ + " balance — the run keeps going and stops when the balance runs out, keeping every row it"
91
+ + " has already completed."
92
+ : "";
93
+ return {
94
+ ...expectedFields,
95
+ estimated_credits: estimatedCredits,
96
+ recommended_max_credits: recommendedMaxCredits,
97
+ available_credits: availableCredits,
98
+ credit_posture: "sufficient",
99
+ credit_guidance: `Credits cover the expected cost (≈${formatCreditsForCopy(expectedCredits)} credits) with `
100
+ + "headroom for the rows that fall through to a pricier provider."
101
+ + `${tailWarning} Unused credits are not spent.`,
102
+ };
103
+ }
25
104
  return {
105
+ ...expectedFields,
26
106
  estimated_credits: estimatedCredits,
27
107
  recommended_max_credits: recommendedMaxCredits,
28
108
  available_credits: availableCredits,
@@ -33,6 +113,7 @@ export function buildCreditGuidance(input) {
33
113
  };
34
114
  }
35
115
  return {
116
+ ...expectedFields,
36
117
  estimated_credits: estimatedCredits,
37
118
  recommended_max_credits: availableCredits > 0 ? roundCreditValue(availableCredits) : null,
38
119
  available_credits: availableCredits,
@@ -51,6 +132,14 @@ function recommendedCreditCeiling(estimatedCredits, headroomMultiplier = DEFAULT
51
132
  : DEFAULT_HEADROOM_MULTIPLIER;
52
133
  return Math.max(1, Math.ceil(normalized * multiplier));
53
134
  }
135
+ function normalizeMultiplier(value) {
136
+ return Number.isFinite(value) && (value ?? 0) > 0
137
+ ? value
138
+ : DEFAULT_HEADROOM_MULTIPLIER;
139
+ }
140
+ function formatCreditsForCopy(value) {
141
+ return Math.round(value).toLocaleString("en-US");
142
+ }
54
143
  function normalizeCreditValue(value) {
55
144
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
56
145
  return null;
@@ -11,6 +11,7 @@ export * from "./cell-format.js";
11
11
  export * from "./cli-envelope.js";
12
12
  export * from "./cli-login-code.js";
13
13
  export * from "./cli-result.js";
14
+ export * from "./collab.js";
14
15
  export * from "./crm-reply-events.js";
15
16
  export * from "./crm-activity-events.js";
16
17
  export * from "./column-types.js";
@@ -45,6 +46,7 @@ export * from "./sequences.js";
45
46
  export * from "./suppression-entries.js";
46
47
  export * from "./table-limits.js";
47
48
  export * from "./log.js";
49
+ export * from "./axiom-field-budget.js";
48
50
  export { sanitizeLogFields } from "./redaction.js";
49
51
  export * from "./provider-request-outcomes.js";
50
52
  export * from "./schedule-label.js";
@@ -11,6 +11,7 @@ export * from "./cell-format.js";
11
11
  export * from "./cli-envelope.js";
12
12
  export * from "./cli-login-code.js";
13
13
  export * from "./cli-result.js";
14
+ export * from "./collab.js";
14
15
  export * from "./crm-reply-events.js";
15
16
  export * from "./crm-activity-events.js";
16
17
  export * from "./column-types.js";
@@ -45,6 +46,7 @@ export * from "./sequences.js";
45
46
  export * from "./suppression-entries.js";
46
47
  export * from "./table-limits.js";
47
48
  export * from "./log.js";
49
+ export * from "./axiom-field-budget.js";
48
50
  // Narrow, deliberate export (ADR 0014): lets telemetry emitters regression-test
49
51
  // their field names against the REAL log sanitizer — the unanchored
50
52
  // SECRET_KEY_PATTERN redacts any name containing "token", which mocked loggers
@@ -90,6 +90,36 @@ export declare const VOICE_CREDITS_PER_AMD_CALL = 38;
90
90
  /** Managed domains: registrar at-cost × 1.25 (≈ a $10.44 .com → ~13,050 credits/yr). */
91
91
  export declare const MANAGED_DOMAIN_MARKUP = 1.25;
92
92
  export declare const MANAGED_DOMAIN_CREDITS_PER_YEAR_TYPICAL = 13050;
93
+ /**
94
+ * Inbox deliverability (EmailGuard placement seat). A flat monthly price per
95
+ * SUBSCRIBED inbox covering continuous deliverability monitoring (blacklist,
96
+ * SPF/DKIM/DMARC, domain reputation) plus an included allowance of
97
+ * inbox-placement (spam) tests; tests beyond the allowance bill the per-test
98
+ * price.
99
+ *
100
+ * All three are DISPLAY mirrors, per this file's contract. The canonical values
101
+ * live where they are enforced, and seed-parity.test.ts binds them so a mirror
102
+ * can never quietly drift from the number actually billed:
103
+ * seat DELIVERABILITY_UNIT_MONTHLY_CREDITS (@oxygen/control-db,
104
+ * also PRICING_SEED commitment.deliverability_unit) — 1,000 credits
105
+ * ($1.00), restored by the founder on 2026-07-30 after reviewing the
106
+ * Agency-plan unit economics, superseding the 2026-07-26 signature
107
+ * of 2,000.
108
+ * allowance DELIVERABILITY_INCLUDED_TESTS_PER_INBOX (@oxygen/control-db)
109
+ * overage EMAILGUARD_MANAGED_PLACEMENT_CREDITS (@oxygen/providers,
110
+ * derived from the Agency plan rather than typed in)
111
+ *
112
+ * THE ALLOWANCE IS LOAD-BEARING, not a perk. Placement COGS is per TEST
113
+ * ($0.1327 on EmailGuard's Agency plan), not per inbox, so an unbounded seat
114
+ * inverts its own margin: at $1.00/inbox-month the EIGHTH test in a cycle is
115
+ * already sold at a loss. Two included tests cost $0.265 against $1.00 of
116
+ * revenue — a ~73% margin — and everything past them bills
117
+ * PLACEMENT_TEST_OVERAGE_CREDITS. Orgs on their own EmailGuard key (BYOK) pay 0.
118
+ */
119
+ export declare const DELIVERABILITY_SEAT_CREDITS_PER_MONTH = 1000;
120
+ export declare const DELIVERABILITY_INCLUDED_TESTS_PER_INBOX = 2;
121
+ /** Overage placement test: $0.1327 EmailGuard COGS × 1.25, rounded up. */
122
+ export declare const PLACEMENT_TEST_OVERAGE_CREDITS = 166;
93
123
  /**
94
124
  * LinkedIn accounts: founder decision (2026-07-25) — a PURE seat model. The
95
125
  * flat monthly connection fee per connected account is now the WHOLE LinkedIn
@@ -82,6 +82,36 @@ export const VOICE_CREDITS_PER_AMD_CALL = 38;
82
82
  /** Managed domains: registrar at-cost × 1.25 (≈ a $10.44 .com → ~13,050 credits/yr). */
83
83
  export const MANAGED_DOMAIN_MARKUP = 1.25;
84
84
  export const MANAGED_DOMAIN_CREDITS_PER_YEAR_TYPICAL = 13_050;
85
+ /**
86
+ * Inbox deliverability (EmailGuard placement seat). A flat monthly price per
87
+ * SUBSCRIBED inbox covering continuous deliverability monitoring (blacklist,
88
+ * SPF/DKIM/DMARC, domain reputation) plus an included allowance of
89
+ * inbox-placement (spam) tests; tests beyond the allowance bill the per-test
90
+ * price.
91
+ *
92
+ * All three are DISPLAY mirrors, per this file's contract. The canonical values
93
+ * live where they are enforced, and seed-parity.test.ts binds them so a mirror
94
+ * can never quietly drift from the number actually billed:
95
+ * seat DELIVERABILITY_UNIT_MONTHLY_CREDITS (@oxygen/control-db,
96
+ * also PRICING_SEED commitment.deliverability_unit) — 1,000 credits
97
+ * ($1.00), restored by the founder on 2026-07-30 after reviewing the
98
+ * Agency-plan unit economics, superseding the 2026-07-26 signature
99
+ * of 2,000.
100
+ * allowance DELIVERABILITY_INCLUDED_TESTS_PER_INBOX (@oxygen/control-db)
101
+ * overage EMAILGUARD_MANAGED_PLACEMENT_CREDITS (@oxygen/providers,
102
+ * derived from the Agency plan rather than typed in)
103
+ *
104
+ * THE ALLOWANCE IS LOAD-BEARING, not a perk. Placement COGS is per TEST
105
+ * ($0.1327 on EmailGuard's Agency plan), not per inbox, so an unbounded seat
106
+ * inverts its own margin: at $1.00/inbox-month the EIGHTH test in a cycle is
107
+ * already sold at a loss. Two included tests cost $0.265 against $1.00 of
108
+ * revenue — a ~73% margin — and everything past them bills
109
+ * PLACEMENT_TEST_OVERAGE_CREDITS. Orgs on their own EmailGuard key (BYOK) pay 0.
110
+ */
111
+ export const DELIVERABILITY_SEAT_CREDITS_PER_MONTH = 1_000;
112
+ export const DELIVERABILITY_INCLUDED_TESTS_PER_INBOX = 2;
113
+ /** Overage placement test: $0.1327 EmailGuard COGS × 1.25, rounded up. */
114
+ export const PLACEMENT_TEST_OVERAGE_CREDITS = 166;
85
115
  /**
86
116
  * LinkedIn accounts: founder decision (2026-07-25) — a PURE seat model. The
87
117
  * flat monthly connection fee per connected account is now the WHOLE LinkedIn
@@ -2,13 +2,12 @@
2
2
  * Social channel capability registry.
3
3
  *
4
4
  * Channel, provider rail, and operation support are deliberately independent.
5
- * LinkedIn is the only registered channel today, but callers resolve capabilities
6
- * through this contract instead of branching on `channel === "linkedin"`. Adding a
7
- * channel later means registering a real adapter profile, not adding placeholder UI.
5
+ * Callers resolve capabilities through this contract instead of branching on a
6
+ * channel name. A channel is registered only when a real adapter exists.
8
7
  */
9
- export declare const SOCIAL_CHANNELS: readonly ["linkedin"];
8
+ export declare const SOCIAL_CHANNELS: readonly ["linkedin", "x"];
10
9
  export type SocialChannel = (typeof SOCIAL_CHANNELS)[number];
11
- export declare const SOCIAL_PROVIDER_RAILS: readonly ["unipile"];
10
+ export declare const SOCIAL_PROVIDER_RAILS: readonly ["unipile", "composio"];
12
11
  export type SocialProviderRail = (typeof SOCIAL_PROVIDER_RAILS)[number];
13
12
  export declare const SOCIAL_OPERATIONS: readonly ["list_owned_posts", "read_post_metrics", "list_comments", "reply_to_comment"];
14
13
  export type SocialOperation = (typeof SOCIAL_OPERATIONS)[number];
@@ -24,7 +23,7 @@ export type SocialOperationCapability = {
24
23
  limitation: string | null;
25
24
  };
26
25
  export type SocialMetricCapability = {
27
- status: "available" | "unavailable";
26
+ status: SocialCapabilityStatus;
28
27
  provider_field: string | null;
29
28
  reason: string | null;
30
29
  };
@@ -41,3 +40,5 @@ export type SocialCapabilityProfile = {
41
40
  };
42
41
  export declare function listSocialCapabilityProfiles(): readonly SocialCapabilityProfile[];
43
42
  export declare function getSocialCapabilityProfile(channel: string, providerRail: string): SocialCapabilityProfile | null;
43
+ /** Resolve the first configured rail that can perform an operation for a channel. */
44
+ export declare function getSocialCapabilityProfileForOperation(channel: string, operation: SocialOperation): SocialCapabilityProfile | null;
@@ -2,12 +2,11 @@
2
2
  * Social channel capability registry.
3
3
  *
4
4
  * Channel, provider rail, and operation support are deliberately independent.
5
- * LinkedIn is the only registered channel today, but callers resolve capabilities
6
- * through this contract instead of branching on `channel === "linkedin"`. Adding a
7
- * channel later means registering a real adapter profile, not adding placeholder UI.
5
+ * Callers resolve capabilities through this contract instead of branching on a
6
+ * channel name. A channel is registered only when a real adapter exists.
8
7
  */
9
- export const SOCIAL_CHANNELS = ["linkedin"];
10
- export const SOCIAL_PROVIDER_RAILS = ["unipile"];
8
+ export const SOCIAL_CHANNELS = ["linkedin", "x"];
9
+ export const SOCIAL_PROVIDER_RAILS = ["unipile", "composio"];
11
10
  export const SOCIAL_OPERATIONS = [
12
11
  "list_owned_posts",
13
12
  "read_post_metrics",
@@ -61,9 +60,9 @@ const LINKEDIN_UNIPILE_CAPABILITIES = {
61
60
  },
62
61
  metrics: {
63
62
  impressions: { status: "unavailable", provider_field: null, reason: NOT_EXPOSED_BY_UNIPILE },
64
- reactions: { status: "available", provider_field: "reaction_count", reason: null },
65
- comments: { status: "available", provider_field: "comment_count", reason: null },
66
- reposts: { status: "available", provider_field: "repost_count", reason: null },
63
+ reactions: { status: "available", provider_field: "reaction_counter", reason: null },
64
+ comments: { status: "available", provider_field: "comment_counter", reason: null },
65
+ reposts: { status: "available", provider_field: "repost_counter", reason: null },
67
66
  saves: { status: "unavailable", provider_field: null, reason: NOT_EXPOSED_BY_UNIPILE },
68
67
  sends: { status: "unavailable", provider_field: null, reason: NOT_EXPOSED_BY_UNIPILE },
69
68
  clicks: { status: "unavailable", provider_field: null, reason: NOT_EXPOSED_BY_UNIPILE },
@@ -74,8 +73,66 @@ const LINKEDIN_UNIPILE_CAPABILITIES = {
74
73
  webhooks: false,
75
74
  },
76
75
  };
76
+ const X_COMPOSIO_CAPABILITIES = {
77
+ version: 1,
78
+ channel: "x",
79
+ provider_rail: "composio",
80
+ operations: {
81
+ list_owned_posts: {
82
+ status: "unavailable",
83
+ source: "provider",
84
+ provider_operation: null,
85
+ external_write: false,
86
+ limitation: "oxygen_published_posts_only",
87
+ },
88
+ read_post_metrics: {
89
+ status: "limited",
90
+ source: "provider",
91
+ provider_operation: "TWITTER_POST_LOOKUP_BY_POST_ID",
92
+ external_write: false,
93
+ limitation: "connected_user_oauth_owned_posts",
94
+ },
95
+ list_comments: {
96
+ status: "unavailable",
97
+ source: "provider",
98
+ provider_operation: null,
99
+ external_write: false,
100
+ limitation: "not_implemented_for_x",
101
+ },
102
+ reply_to_comment: {
103
+ status: "unavailable",
104
+ source: "provider",
105
+ provider_operation: null,
106
+ external_write: true,
107
+ limitation: "not_implemented_for_x",
108
+ },
109
+ },
110
+ metrics: {
111
+ impressions: { status: "available", provider_field: "public_metrics.impression_count", reason: null },
112
+ reactions: { status: "available", provider_field: "public_metrics.like_count", reason: null },
113
+ comments: { status: "available", provider_field: "public_metrics.reply_count", reason: null },
114
+ reposts: { status: "available", provider_field: "public_metrics.retweet_count", reason: null },
115
+ saves: { status: "available", provider_field: "public_metrics.bookmark_count", reason: null },
116
+ sends: { status: "unavailable", provider_field: null, reason: "not_exposed_by_x_post_lookup" },
117
+ clicks: {
118
+ status: "limited",
119
+ provider_field: "non_public_metrics.url_link_clicks",
120
+ reason: "owned_posts_user_context_30_day_window",
121
+ },
122
+ video_views: {
123
+ status: "limited",
124
+ provider_field: "includes.media[].public_metrics.view_count",
125
+ reason: "video_media_only",
126
+ },
127
+ },
128
+ ingestion: {
129
+ polling: true,
130
+ webhooks: false,
131
+ },
132
+ };
77
133
  const SOCIAL_CAPABILITY_PROFILES = [
78
134
  LINKEDIN_UNIPILE_CAPABILITIES,
135
+ X_COMPOSIO_CAPABILITIES,
79
136
  ];
80
137
  export function listSocialCapabilityProfiles() {
81
138
  return SOCIAL_CAPABILITY_PROFILES;
@@ -83,3 +140,7 @@ export function listSocialCapabilityProfiles() {
83
140
  export function getSocialCapabilityProfile(channel, providerRail) {
84
141
  return SOCIAL_CAPABILITY_PROFILES.find((profile) => profile.channel === channel && profile.provider_rail === providerRail) ?? null;
85
142
  }
143
+ /** Resolve the first configured rail that can perform an operation for a channel. */
144
+ export function getSocialCapabilityProfileForOperation(channel, operation) {
145
+ return SOCIAL_CAPABILITY_PROFILES.find((profile) => profile.channel === channel && profile.operations[operation].status !== "unavailable") ?? null;
146
+ }
@@ -1,6 +1,6 @@
1
1
  import type { TagColor } from "./select-options.js";
2
2
  /** Primitive kinds that participate in the workspace tag union today. */
3
- export declare const TAG_KINDS: readonly ["knowledge_page", "publishing_post", "sequence", "table", "workflow", "recipe", "conversation", "mailbox", "sender", "record", "domain", "project", "engagement_watch"];
3
+ export declare const TAG_KINDS: readonly ["knowledge_page", "publishing_post", "sequence", "table", "workflow", "recipe", "conversation", "mailbox", "sender", "record", "domain", "project", "sender_profile", "voice_number"];
4
4
  export type TagKind = (typeof TAG_KINDS)[number];
5
5
  /**
6
6
  * The taggable kinds in prose, for CLI `--help` and MCP tool descriptions.
@@ -8,7 +8,7 @@ export type TagKind = (typeof TAG_KINDS)[number];
8
8
  * Lives next to {@link TAG_KINDS} because those descriptions are the *discovery*
9
9
  * surface: a blind-user eval (2026-07-21) found the tags help still advertising
10
10
  * the original seven kinds, so a user reading `tags --help` had no way to learn
11
- * that domains, projects, or engagement watches were taggable at all — they had
11
+ * that domains or projects were taggable at all — they had
12
12
  * to stumble on each owning group's own help. One string, one import, and
13
13
  * `TAG_KIND_PROSE_MARKERS` fails the build/test if a new kind skips it.
14
14
  */
@@ -24,7 +24,8 @@ export const TAG_KINDS = [
24
24
  "record",
25
25
  "domain",
26
26
  "project",
27
- "engagement_watch",
27
+ "sender_profile",
28
+ "voice_number",
28
29
  ];
29
30
  /**
30
31
  * The taggable kinds in prose, for CLI `--help` and MCP tool descriptions.
@@ -32,13 +33,13 @@ export const TAG_KINDS = [
32
33
  * Lives next to {@link TAG_KINDS} because those descriptions are the *discovery*
33
34
  * surface: a blind-user eval (2026-07-21) found the tags help still advertising
34
35
  * the original seven kinds, so a user reading `tags --help` had no way to learn
35
- * that domains, projects, or engagement watches were taggable at all — they had
36
+ * that domains or projects were taggable at all — they had
36
37
  * to stumble on each owning group's own help. One string, one import, and
37
38
  * `TAG_KIND_PROSE_MARKERS` fails the build/test if a new kind skips it.
38
39
  */
39
40
  export const TAG_KINDS_PROSE = "publishing posts, knowledge pages, sequences, tables, workflows, recipes, " +
40
41
  "inbox conversations, CRM records, mailboxes, sender accounts, sending domains, " +
41
- "projects, and engagement watches";
42
+ "projects, sender profiles, and phone numbers";
42
43
  /**
43
44
  * The phrase each kind must contribute to {@link TAG_KINDS_PROSE}. Typed as a
44
45
  * total map over TagKind, so adding a kind is a compile error until it is named
@@ -57,7 +58,8 @@ export const TAG_KIND_PROSE_MARKERS = {
57
58
  record: "CRM record",
58
59
  domain: "sending domain",
59
60
  project: "project",
60
- engagement_watch: "engagement watch",
61
+ sender_profile: "sender profile",
62
+ voice_number: "phone number",
61
63
  };
62
64
  export function isTagKind(value) {
63
65
  return typeof value === "string" && TAG_KINDS.includes(value);
@@ -87,7 +89,8 @@ export const TAG_KIND_LABELS = {
87
89
  record: { one: "CRM record", many: "CRM records" },
88
90
  domain: { one: "domain", many: "domains" },
89
91
  project: { one: "project", many: "projects" },
90
- engagement_watch: { one: "engagement watch", many: "engagement watches" },
92
+ sender_profile: { one: "sender profile", many: "sender profiles" },
93
+ voice_number: { one: "phone number", many: "phone numbers" },
91
94
  };
92
95
  /** Inline label for a count: `tagKindLabel("sequence", 1)` → "sequence". */
93
96
  export function tagKindLabel(kind, count) {
@@ -1,3 +1,3 @@
1
- export declare const OXYGEN_VERSION = "1.638.1";
1
+ export declare const OXYGEN_VERSION = "1.677.10";
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
3
3
  export declare const MANAGED_INBOX_MINIMUM_CLI_VERSION = "1.326.2";
@@ -1,4 +1,4 @@
1
- export const OXYGEN_VERSION = "1.638.1";
1
+ export const OXYGEN_VERSION = "1.677.10";
2
2
  // The GLOBAL CLI compatibility floor: the oldest CLI allowed to call any
3
3
  // operational route. Raising it hard-rejects every older CLI from the entire
4
4
  // product, so it obeys one law, enforced by scripts/ci/cli-min-version-gate.mjs:
@@ -111,6 +111,11 @@
111
111
  "import": "./dist/tags.js",
112
112
  "default": "./dist/tags.js"
113
113
  },
114
+ "./collab": {
115
+ "types": "./dist/collab.d.ts",
116
+ "import": "./dist/collab.js",
117
+ "default": "./dist/collab.js"
118
+ },
114
119
  "./schedule-label": {
115
120
  "types": "./dist/schedule-label.d.ts",
116
121
  "import": "./dist/schedule-label.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.638.1",
3
+ "version": "1.677.10",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",