@oxygen-agent/cli 1.1014.619 → 1.1014.771

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 (65) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +15 -0
  3. package/dist/command-support.d.ts +5 -5
  4. package/dist/command-support.js +7 -7
  5. package/dist/commands/billing.js +12 -6
  6. package/dist/commands/columns.js +2 -2
  7. package/dist/commands/companies.js +1 -1
  8. package/dist/commands/copilot.js +1 -1
  9. package/dist/commands/dashboards.js +1 -1
  10. package/dist/commands/enrich-column.js +2 -2
  11. package/dist/commands/find.js +2 -2
  12. package/dist/commands/mailboxes.js +36 -0
  13. package/dist/commands/recipes.js +1 -1
  14. package/dist/commands/sequences.js +6 -6
  15. package/dist/commands/tables.js +1 -1
  16. package/dist/commands/verify.js +112 -11
  17. package/dist/http-client.js +1 -70
  18. package/dist/index.js +1 -1
  19. package/dist/refactor-internals/binary-download.d.ts +5 -0
  20. package/dist/refactor-internals/binary-download.js +71 -0
  21. package/node_modules/@oxygen/formula/dist/evaluate.js +2 -82
  22. package/node_modules/@oxygen/formula/dist/refactor-internals/novelty-score.d.ts +2 -0
  23. package/node_modules/@oxygen/formula/dist/refactor-internals/novelty-score.js +87 -0
  24. package/node_modules/@oxygen/shared/dist/billing.d.ts +15 -3
  25. package/node_modules/@oxygen/shared/dist/billing.js +19 -7
  26. package/node_modules/@oxygen/shared/dist/cancellation-reasons.d.ts +35 -0
  27. package/node_modules/@oxygen/shared/dist/cancellation-reasons.js +21 -0
  28. package/node_modules/@oxygen/shared/dist/capability-discovery.js +27 -1
  29. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +6 -6
  30. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -1
  31. package/node_modules/@oxygen/shared/dist/contracts/limits-read.js +1 -0
  32. package/node_modules/@oxygen/shared/dist/contracts/teammates.js +41 -17
  33. package/node_modules/@oxygen/shared/dist/email-verification-batch.d.ts +26 -0
  34. package/node_modules/@oxygen/shared/dist/email-verification-batch.js +55 -0
  35. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +3 -3
  36. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +3 -3
  37. package/node_modules/@oxygen/shared/dist/knowledge-brain-draft.d.ts +70 -0
  38. package/node_modules/@oxygen/shared/dist/knowledge-brain-draft.js +42 -0
  39. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +1 -1
  40. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
  41. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +3 -1
  42. package/node_modules/@oxygen/shared/dist/product-doctrine.js +1 -1
  43. package/node_modules/@oxygen/shared/dist/sequence-internals/esp-policy.d.ts +225 -0
  44. package/node_modules/@oxygen/shared/dist/sequence-internals/esp-policy.js +578 -0
  45. package/node_modules/@oxygen/shared/dist/sequences.d.ts +1 -226
  46. package/node_modules/@oxygen/shared/dist/sequences.js +2 -576
  47. package/node_modules/@oxygen/shared/dist/teammate-groups.d.ts +103 -24
  48. package/node_modules/@oxygen/shared/dist/teammate-groups.js +125 -32
  49. package/node_modules/@oxygen/shared/dist/teammate-messages.d.ts +1 -1
  50. package/node_modules/@oxygen/shared/dist/teammates.d.ts +28 -2
  51. package/node_modules/@oxygen/shared/dist/teammates.js +32 -2
  52. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +40 -8
  53. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  54. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  55. package/node_modules/@oxygen/workflows/dist/graph/expression.d.ts +1 -16
  56. package/node_modules/@oxygen/workflows/dist/graph/expression.js +4 -224
  57. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +2 -2
  58. package/node_modules/@oxygen/workflows/dist/index.d.ts +1 -63
  59. package/node_modules/@oxygen/workflows/dist/index.js +1 -193
  60. package/node_modules/@oxygen/workflows/dist/refactor-internals/condition-comparison.d.ts +18 -0
  61. package/node_modules/@oxygen/workflows/dist/refactor-internals/condition-comparison.js +222 -0
  62. package/node_modules/@oxygen/workflows/dist/refactor-internals/execution-policy.d.ts +64 -0
  63. package/node_modules/@oxygen/workflows/dist/refactor-internals/execution-policy.js +204 -0
  64. package/node_modules/@oxygen/workflows/package.json +4 -0
  65. package/package.json +1 -1
@@ -2,7 +2,7 @@ import { readGeoExportPagination } from "@oxygen/shared/geo";
2
2
  import { OXYGEN_VERSION, OxygenError, isCliResult, isVersionGreater, parseSemver, readEnvelopeCompatibility, vercelProtectionBypassHeaders, withRetryAfterDetails, } from "@oxygen/shared";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { cliDiagnosticHeaders, cliDiagnosticInvocation, recordCliDiagnosticRequest } from "./diagnostics.js";
5
- import { open, unlink } from "node:fs/promises";
5
+ import { readBoundedBinaryResponse, streamBinaryResponse } from "./refactor-internals/binary-download.js";
6
6
  import { recordMutatingRequestAnswered, recoverFromCliFloorRejection } from "./auto-update.js";
7
7
  import { defaultApiUrl, loadCredentialsWithEndpoint } from "./credentials.js";
8
8
  import { assertResolvedApiUrl, cliEndpointDetails, describeCliEndpoint, isProdApiUrl, resolveCliUpdateGuidance, } from "./runtime.js";
@@ -200,75 +200,6 @@ path, options = {}) {
200
200
  recordMutatingRequestAnswered();
201
201
  return envelope.data;
202
202
  }
203
- async function readBoundedBinaryResponse(response, maxBytes) {
204
- if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 50 * 1024 * 1024)
205
- throw new OxygenError("invalid_request", "Invalid file download limit.");
206
- const length = response.headers.get("content-length");
207
- if (length && Number(length) > maxBytes)
208
- throw new OxygenError("file_too_large", "The file exceeds its download limit.");
209
- const reader = response.body?.getReader();
210
- if (!reader)
211
- throw new OxygenError("invalid_response", "The download had no body.");
212
- const chunks = [];
213
- let size = 0;
214
- try {
215
- for (;;) {
216
- const { value, done } = await reader.read();
217
- if (done)
218
- break;
219
- size += value.byteLength;
220
- if (size > maxBytes)
221
- throw new OxygenError("file_too_large", "The file exceeds its download limit.");
222
- chunks.push(value);
223
- }
224
- }
225
- finally {
226
- await reader.cancel();
227
- }
228
- const bytes = new Uint8Array(size);
229
- let offset = 0;
230
- for (const chunk of chunks) {
231
- bytes.set(chunk, offset);
232
- offset += chunk.byteLength;
233
- }
234
- return bytes;
235
- }
236
- async function streamBinaryResponse(response, output) {
237
- let file;
238
- try {
239
- file = await open(output, "wx");
240
- }
241
- catch (error) {
242
- await response.body?.cancel();
243
- if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
244
- throw new OxygenError("output_exists", "The output file already exists. Choose another --output path.", { exitCode: 2 });
245
- }
246
- throw error;
247
- }
248
- const reader = response.body?.getReader();
249
- let bytes = 0;
250
- try {
251
- if (!reader)
252
- throw new OxygenError("invalid_response", "The download had no body.");
253
- for (;;) {
254
- const chunk = await reader.read();
255
- if (chunk.done)
256
- break;
257
- await file.writeFile(chunk.value);
258
- bytes += chunk.value.byteLength;
259
- }
260
- await file.close();
261
- return { path: output, bytes };
262
- }
263
- catch (error) {
264
- await file.close().catch(() => { });
265
- await unlink(output).catch(() => { });
266
- throw error;
267
- }
268
- finally {
269
- await reader?.cancel().catch(() => { });
270
- }
271
- }
272
203
  export async function ensureFreshCliForApiUrl(apiUrl, options = {}) {
273
204
  const checkKey = `${apiUrl}|${OXYGEN_VERSION}`;
274
205
  if (cliCompatibilityCheckKeys.has(checkKey))
package/dist/index.js CHANGED
@@ -214,7 +214,7 @@ export function createProgram() {
214
214
  const billingDocsUrl = oxygenDocsUrl(defaultApiUrl(), "/safety/billing");
215
215
  program
216
216
  .name(binaryName)
217
- .description("Revenue infrastructure for B2B startups — agent-operated GTM: tables, enrichment, sequences, workflows, CRM, knowledge. Everything here also runs in the web app and over MCP; every state-changing action returns a web_url deep-link.")
217
+ .description("The agentic GTM platform for B2B GTM teams: tables, enrichment, sequences, workflows, CRM, knowledge. Everything here also runs in the web app and over MCP; every state-changing action returns a web_url deep-link.")
218
218
  .version(OXYGEN_VERSION)
219
219
  .option("--pretty", "Indent JSON for human reading; omit for compact agent output.")
220
220
  .option("--profile <name>", "Use a stored CLI profile for this command.")
@@ -0,0 +1,5 @@
1
+ export declare function readBoundedBinaryResponse(response: Response, maxBytes: number): Promise<Uint8Array>;
2
+ export declare function streamBinaryResponse(response: Response, output: string): Promise<{
3
+ path: string;
4
+ bytes: number;
5
+ }>;
@@ -0,0 +1,71 @@
1
+ import { OxygenError } from "@oxygen/shared/cli-result";
2
+ import { open, unlink } from "node:fs/promises";
3
+ export async function readBoundedBinaryResponse(response, maxBytes) {
4
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 50 * 1024 * 1024)
5
+ throw new OxygenError("invalid_request", "Invalid file download limit.");
6
+ const length = response.headers.get("content-length");
7
+ if (length && Number(length) > maxBytes)
8
+ throw new OxygenError("file_too_large", "The file exceeds its download limit.");
9
+ const reader = response.body?.getReader();
10
+ if (!reader)
11
+ throw new OxygenError("invalid_response", "The download had no body.");
12
+ const chunks = [];
13
+ let size = 0;
14
+ try {
15
+ for (;;) {
16
+ const { value, done } = await reader.read();
17
+ if (done)
18
+ break;
19
+ size += value.byteLength;
20
+ if (size > maxBytes)
21
+ throw new OxygenError("file_too_large", "The file exceeds its download limit.");
22
+ chunks.push(value);
23
+ }
24
+ }
25
+ finally {
26
+ await reader.cancel();
27
+ }
28
+ const bytes = new Uint8Array(size);
29
+ let offset = 0;
30
+ for (const chunk of chunks) {
31
+ bytes.set(chunk, offset);
32
+ offset += chunk.byteLength;
33
+ }
34
+ return bytes;
35
+ }
36
+ export async function streamBinaryResponse(response, output) {
37
+ let file;
38
+ try {
39
+ file = await open(output, "wx");
40
+ }
41
+ catch (error) {
42
+ await response.body?.cancel();
43
+ if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
44
+ throw new OxygenError("output_exists", "The output file already exists. Choose another --output path.", { exitCode: 2 });
45
+ }
46
+ throw error;
47
+ }
48
+ const reader = response.body?.getReader();
49
+ let bytes = 0;
50
+ try {
51
+ if (!reader)
52
+ throw new OxygenError("invalid_response", "The download had no body.");
53
+ for (;;) {
54
+ const chunk = await reader.read();
55
+ if (chunk.done)
56
+ break;
57
+ await file.writeFile(chunk.value);
58
+ bytes += chunk.value.byteLength;
59
+ }
60
+ await file.close();
61
+ return { path: output, bytes };
62
+ }
63
+ catch (error) {
64
+ await file.close().catch(() => { });
65
+ await unlink(output).catch(() => { });
66
+ throw error;
67
+ }
68
+ finally {
69
+ await reader?.cancel().catch(() => { });
70
+ }
71
+ }
@@ -1,5 +1,5 @@
1
1
  import { OxygenError } from "@oxygen/shared/cli-result";
2
- import { isRecord } from "./coerce.js";
2
+ import { evaluateNoveltyScore } from "./refactor-internals/novelty-score.js";
3
3
  import { CROSS_ROW_FUNCTIONS, parseFormulaExpression, } from "./expression.js";
4
4
  import { FORMULA_FUNCTION_REGISTRY, checkFormulaFunctionArity, compareFormulaValues, formulaExpressionError, formulaValuesEqual, readFormulaNumber, toFormulaBoolean, } from "./formula-functions.js";
5
5
  export function evaluateFormulaInScope(expression, scope, options = {}) {
@@ -86,11 +86,7 @@ function evaluateCrossRowFunction(input) {
86
86
  exitCode: 1,
87
87
  });
88
88
  }
89
- const cellValue = stringifyNoveltyValue(readOptionalValue(input.scope, targetColumnKey));
90
- if (!cellValue)
91
- return null;
92
- const noveltyScore = computeNoveltyScore(cellValue, corpus);
93
- return Math.round(noveltyScore * 10_000) / 10_000;
89
+ return evaluateNoveltyScore(readOptionalValue(input.scope, targetColumnKey), corpus);
94
90
  }
95
91
  /**
96
92
  * Cross-row functions take a column key, not an in-scope identifier: a column
@@ -117,82 +113,6 @@ function readCrossRowColumnArg(node, functionName) {
117
113
  function: functionName,
118
114
  });
119
115
  }
120
- function stringifyNoveltyValue(value) {
121
- if (value === null || value === undefined)
122
- return "";
123
- if (typeof value === "string")
124
- return value.trim();
125
- if (typeof value === "number" || typeof value === "boolean")
126
- return String(value).trim();
127
- if (Array.isArray(value)) {
128
- return value
129
- .map((entry) => stringifyNoveltyValue(entry))
130
- .filter((entry) => entry.length > 0)
131
- .join(" ")
132
- .trim();
133
- }
134
- if (isRecord(value)) {
135
- const preferred = value.body ?? value.text ?? value.email ?? value.message;
136
- if (typeof preferred === "string")
137
- return preferred.trim();
138
- return JSON.stringify(value);
139
- }
140
- return "";
141
- }
142
- const NOVELTY_SHINGLE_SIZE = 5;
143
- function computeNoveltyScore(cellValue, corpus) {
144
- const cellShingles = buildShingleSet(cellValue);
145
- if (cellShingles.size === 0)
146
- return 1;
147
- let maxOverlap = 0;
148
- let skippedSelfMatch = false;
149
- for (const other of corpus) {
150
- if (other === cellValue && !skippedSelfMatch) {
151
- skippedSelfMatch = true;
152
- continue;
153
- }
154
- const otherShingles = buildShingleSet(other);
155
- if (otherShingles.size === 0)
156
- continue;
157
- const overlap = jaccardSimilarity(cellShingles, otherShingles);
158
- if (overlap > maxOverlap)
159
- maxOverlap = overlap;
160
- }
161
- const novelty = 1 - maxOverlap;
162
- if (novelty < 0)
163
- return 0;
164
- if (novelty > 1)
165
- return 1;
166
- return novelty;
167
- }
168
- function buildShingleSet(value) {
169
- const normalized = value
170
- .toLowerCase()
171
- .replace(/[^a-z0-9\s]/g, " ")
172
- .replace(/\s+/g, " ")
173
- .trim();
174
- const tokens = normalized ? normalized.split(" ") : [];
175
- const shingles = new Set();
176
- if (tokens.length < NOVELTY_SHINGLE_SIZE) {
177
- if (tokens.length > 0)
178
- shingles.add(tokens.join(" "));
179
- return shingles;
180
- }
181
- for (let i = 0; i <= tokens.length - NOVELTY_SHINGLE_SIZE; i += 1) {
182
- shingles.add(tokens.slice(i, i + NOVELTY_SHINGLE_SIZE).join(" "));
183
- }
184
- return shingles;
185
- }
186
- function jaccardSimilarity(left, right) {
187
- let intersection = 0;
188
- const [smaller, larger] = left.size <= right.size ? [left, right] : [right, left];
189
- for (const entry of smaller) {
190
- if (larger.has(entry))
191
- intersection += 1;
192
- }
193
- const union = left.size + right.size - intersection;
194
- return union === 0 ? 0 : intersection / union;
195
- }
196
116
  function evaluateUnary(operator, value) {
197
117
  switch (operator) {
198
118
  case "+":
@@ -0,0 +1,2 @@
1
+ /** Score a value against a supplied column corpus, excluding one exact self row. */
2
+ export declare function evaluateNoveltyScore(value: unknown, corpus: readonly string[]): number | null;
@@ -0,0 +1,87 @@
1
+ // Corpus scoring is independent of scope resolution, expression dispatch and
2
+ // corpus availability errors. The evaluator owns those host-facing contracts.
3
+ import { isRecord } from "../coerce.js";
4
+ /** Score a value against a supplied column corpus, excluding one exact self row. */
5
+ export function evaluateNoveltyScore(value, corpus) {
6
+ const cellValue = stringifyNoveltyValue(value);
7
+ if (!cellValue)
8
+ return null;
9
+ const noveltyScore = computeNoveltyScore(cellValue, corpus);
10
+ return Math.round(noveltyScore * 10_000) / 10_000;
11
+ }
12
+ function stringifyNoveltyValue(value) {
13
+ if (value === null || value === undefined)
14
+ return "";
15
+ if (typeof value === "string")
16
+ return value.trim();
17
+ if (typeof value === "number" || typeof value === "boolean")
18
+ return String(value).trim();
19
+ if (Array.isArray(value)) {
20
+ return value
21
+ .map((entry) => stringifyNoveltyValue(entry))
22
+ .filter((entry) => entry.length > 0)
23
+ .join(" ")
24
+ .trim();
25
+ }
26
+ if (isRecord(value)) {
27
+ const preferred = value.body ?? value.text ?? value.email ?? value.message;
28
+ if (typeof preferred === "string")
29
+ return preferred.trim();
30
+ return JSON.stringify(value);
31
+ }
32
+ return "";
33
+ }
34
+ const NOVELTY_SHINGLE_SIZE = 5;
35
+ function computeNoveltyScore(cellValue, corpus) {
36
+ const cellShingles = buildShingleSet(cellValue);
37
+ if (cellShingles.size === 0)
38
+ return 1;
39
+ let maxOverlap = 0;
40
+ let skippedSelfMatch = false;
41
+ for (const other of corpus) {
42
+ if (other === cellValue && !skippedSelfMatch) {
43
+ skippedSelfMatch = true;
44
+ continue;
45
+ }
46
+ const otherShingles = buildShingleSet(other);
47
+ if (otherShingles.size === 0)
48
+ continue;
49
+ const overlap = jaccardSimilarity(cellShingles, otherShingles);
50
+ if (overlap > maxOverlap)
51
+ maxOverlap = overlap;
52
+ }
53
+ const novelty = 1 - maxOverlap;
54
+ if (novelty < 0)
55
+ return 0;
56
+ if (novelty > 1)
57
+ return 1;
58
+ return novelty;
59
+ }
60
+ function buildShingleSet(value) {
61
+ const normalized = value
62
+ .toLowerCase()
63
+ .replace(/[^a-z0-9\s]/g, " ")
64
+ .replace(/\s+/g, " ")
65
+ .trim();
66
+ const tokens = normalized ? normalized.split(" ") : [];
67
+ const shingles = new Set();
68
+ if (tokens.length < NOVELTY_SHINGLE_SIZE) {
69
+ if (tokens.length > 0)
70
+ shingles.add(tokens.join(" "));
71
+ return shingles;
72
+ }
73
+ for (let i = 0; i <= tokens.length - NOVELTY_SHINGLE_SIZE; i += 1) {
74
+ shingles.add(tokens.slice(i, i + NOVELTY_SHINGLE_SIZE).join(" "));
75
+ }
76
+ return shingles;
77
+ }
78
+ function jaccardSimilarity(left, right) {
79
+ let intersection = 0;
80
+ const [smaller, larger] = left.size <= right.size ? [left, right] : [right, left];
81
+ for (const entry of smaller) {
82
+ if (larger.has(entry))
83
+ intersection += 1;
84
+ }
85
+ const union = left.size + right.size - intersection;
86
+ return union === 0 ? 0 : intersection / union;
87
+ }
@@ -532,12 +532,12 @@ export declare const ANNUAL_BILLING_MONTHS_CHARGED = 10;
532
532
  export declare function annualPlanPriceCents(monthlyPriceCents: number): number;
533
533
  /** How often a plan Price bills. Credits are granted monthly on both. */
534
534
  export type PlanBillingInterval = "month" | "year";
535
- /** Commitment and payment frequency are separate: annual commitments bill monthly. */
535
+ /** Commitment and payment frequency are separate for legacy annual installments. */
536
536
  export type PlanCommitmentTerm = "month" | "year";
537
537
  export declare const ANNUAL_MONTHLY_TERMS_VERSION = "2026-10-02";
538
538
  export declare function annualMonthlyPriceCents(monthlyPriceCents: number): number;
539
- /** Read-only comparison; annual totals reflect twelve rounded monthly payments. */
540
- export declare function purchasablePlanComparison(annualMonthlyAvailable: boolean): {
539
+ /** Read-only comparison of monthly payments and the full prepaid annual charge. */
540
+ export declare function purchasablePlanComparison(annualAvailable: boolean): {
541
541
  plan_key: "oxygen_199" | "oxygen_1999" | "oxygen_49" | "oxygen_499" | "oxygen_99" | "oxygen_999";
542
542
  currency: string;
543
543
  monthly_credits: number;
@@ -552,10 +552,22 @@ export declare function purchasablePlanComparison(annualMonthlyAvailable: boolea
552
552
  billing_interval: string;
553
553
  commitment_months: number;
554
554
  total_payment_cents: number;
555
+ monthly_equivalent_cents: number;
555
556
  saving_cents: number;
556
557
  prices_before_tax: boolean;
557
558
  } | null;
558
559
  }[];
560
+ export declare const ANNUAL_PLAN_TERMS: {
561
+ readonly billing_interval: "year";
562
+ readonly commitment_months: 12;
563
+ readonly payment: "Full discounted year charged upfront, then annually.";
564
+ readonly credits_granted: "monthly_during_paid_year";
565
+ readonly credit_schedule: "The first monthly slice starts with the paid annual period once payment is confirmed. Later slice windows start on that period's UTC day and time each month, clamped to the month's last day. These are scheduled boundaries, not guaranteed processing times.";
566
+ readonly cancellation: "Stops annual renewal; access and monthly credits continue through the paid year.";
567
+ readonly switch_from_monthly: "Starts at the next paid-period boundary.";
568
+ readonly downgrades_or_switch_to_monthly: "Take effect at the paid year end.";
569
+ };
570
+ /** Historical terms retained for existing installment subscriptions only. */
559
571
  export declare const ANNUAL_MONTHLY_PLAN_TERMS: {
560
572
  readonly terms_version: "2026-10-02";
561
573
  readonly commitment_months: 12;
@@ -1063,29 +1063,41 @@ export const ANNUAL_MONTHLY_TERMS_VERSION = "2026-10-02";
1063
1063
  export function annualMonthlyPriceCents(monthlyPriceCents) {
1064
1064
  return Math.round(annualPlanPriceCents(monthlyPriceCents) / 12);
1065
1065
  }
1066
- /** Read-only comparison; annual totals reflect twelve rounded monthly payments. */
1067
- export function purchasablePlanComparison(annualMonthlyAvailable) {
1066
+ /** Read-only comparison of monthly payments and the full prepaid annual charge. */
1067
+ export function purchasablePlanComparison(annualAvailable) {
1068
1068
  return PURCHASABLE_PLAN_KEYS.map((key) => {
1069
1069
  const plan = resolveBasePricingPlan(key);
1070
1070
  const monthly = plan.monthlyPriceCents;
1071
- const annualPayment = annualMonthlyPriceCents(monthly);
1071
+ const annualPayment = annualPlanPriceCents(monthly);
1072
1072
  return {
1073
1073
  plan_key: key,
1074
1074
  currency: "usd",
1075
1075
  monthly_credits: plan.monthlyCredits,
1076
1076
  volume_bonus_percent: planVolumeBonusPercent(key),
1077
1077
  monthly: { payment_cents: monthly, billing_interval: "month", commitment_months: 1 },
1078
- yearly: annualMonthlyAvailable ? {
1078
+ yearly: annualAvailable ? {
1079
1079
  payment_cents: annualPayment,
1080
- billing_interval: "month",
1080
+ billing_interval: "year",
1081
1081
  commitment_months: 12,
1082
- total_payment_cents: annualPayment * 12,
1083
- saving_cents: monthly * 12 - annualPayment * 12,
1082
+ total_payment_cents: annualPayment,
1083
+ monthly_equivalent_cents: planMonthlyEquivalentCents(annualPayment, "year"),
1084
+ saving_cents: monthly * 12 - annualPayment,
1084
1085
  prices_before_tax: true,
1085
1086
  } : null,
1086
1087
  };
1087
1088
  });
1088
1089
  }
1090
+ export const ANNUAL_PLAN_TERMS = {
1091
+ billing_interval: "year",
1092
+ commitment_months: 12,
1093
+ payment: "Full discounted year charged upfront, then annually.",
1094
+ credits_granted: "monthly_during_paid_year",
1095
+ credit_schedule: "The first monthly slice starts with the paid annual period once payment is confirmed. Later slice windows start on that period's UTC day and time each month, clamped to the month's last day. These are scheduled boundaries, not guaranteed processing times.",
1096
+ cancellation: "Stops annual renewal; access and monthly credits continue through the paid year.",
1097
+ switch_from_monthly: "Starts at the next paid-period boundary.",
1098
+ downgrades_or_switch_to_monthly: "Take effect at the paid year end.",
1099
+ };
1100
+ /** Historical terms retained for existing installment subscriptions only. */
1089
1101
  export const ANNUAL_MONTHLY_PLAN_TERMS = {
1090
1102
  terms_version: ANNUAL_MONTHLY_TERMS_VERSION,
1091
1103
  commitment_months: 12,
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Why a customer cancels their plan (Tim, 2026-10-14): one choice, counted on the KPI board.
3
+ *
4
+ * The values are Stripe's own `cancellation_details.feedback` vocabulary, so a reason chosen
5
+ * in OXYGEN and one picked in the Stripe Billing Portal's survey land in the same field and
6
+ * group together. The labels are what the customer reads; the order is the order they see.
7
+ */
8
+ export declare const CANCELLATION_REASONS: readonly [{
9
+ readonly value: "too_expensive";
10
+ readonly label: "Too expensive";
11
+ }, {
12
+ readonly value: "missing_features";
13
+ readonly label: "Missing a feature I need";
14
+ }, {
15
+ readonly value: "switched_service";
16
+ readonly label: "Switched to another tool";
17
+ }, {
18
+ readonly value: "unused";
19
+ readonly label: "Not using it enough";
20
+ }, {
21
+ readonly value: "too_complex";
22
+ readonly label: "Too hard to use";
23
+ }, {
24
+ readonly value: "low_quality";
25
+ readonly label: "Results weren't good enough";
26
+ }, {
27
+ readonly value: "customer_service";
28
+ readonly label: "Support didn't help";
29
+ }, {
30
+ readonly value: "other";
31
+ readonly label: "Something else";
32
+ }];
33
+ export type CancellationReason = (typeof CANCELLATION_REASONS)[number]["value"];
34
+ export declare const CANCELLATION_REASON_VALUES: readonly CancellationReason[];
35
+ export declare function isCancellationReason(value: unknown): value is CancellationReason;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Why a customer cancels their plan (Tim, 2026-10-14): one choice, counted on the KPI board.
3
+ *
4
+ * The values are Stripe's own `cancellation_details.feedback` vocabulary, so a reason chosen
5
+ * in OXYGEN and one picked in the Stripe Billing Portal's survey land in the same field and
6
+ * group together. The labels are what the customer reads; the order is the order they see.
7
+ */
8
+ export const CANCELLATION_REASONS = [
9
+ { value: "too_expensive", label: "Too expensive" },
10
+ { value: "missing_features", label: "Missing a feature I need" },
11
+ { value: "switched_service", label: "Switched to another tool" },
12
+ { value: "unused", label: "Not using it enough" },
13
+ { value: "too_complex", label: "Too hard to use" },
14
+ { value: "low_quality", label: "Results weren't good enough" },
15
+ { value: "customer_service", label: "Support didn't help" },
16
+ { value: "other", label: "Something else" },
17
+ ];
18
+ export const CANCELLATION_REASON_VALUES = CANCELLATION_REASONS.map((reason) => reason.value);
19
+ export function isCancellationReason(value) {
20
+ return typeof value === "string" && CANCELLATION_REASON_VALUES.includes(value);
21
+ }
@@ -423,7 +423,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
423
423
  primitive: "agents",
424
424
  owns: "Adaptive governed worker definitions, versions, authorization, context, triggers, threads, checkpoints, inference, and first-class runs.",
425
425
  notFor: "A known deterministic graph that belongs in Workflows or an opaque uploaded runtime.",
426
- execution: "Publish and run a governed Agent with explicit tool authority and bounded spend. Staff on dev can create ongoing sessions: draft, exact resource scope, per-run and rolling 24-hour caps, preview/activate, continuing conversation and bounded self-scheduling. Inspect agent session templates; pause/resume preserves history. Staff on dev also talk to Oxygents (AI teammates): the workspace's first one (New Bot until named) and any others added, one ongoing private chat each.",
426
+ execution: "Publish and run a governed Agent with explicit tool authority and bounded spend. Staff on dev can create ongoing sessions: draft, exact resource scope, per-run and rolling 24-hour caps, preview/activate, continuing conversation and bounded self-scheduling. Inspect agent session templates; pause/resume preserves history. Staff on dev also talk to Oxygents (AI teammates): the workspace's first one (New Bot until named) and any others added, one ongoing private chat each. A new one is its owner's until shared with everyone in the workspace or named people, as collaborators (a chat of their own) or managers (also its profile and who has access). Group chats hold up to six Oxygents and, if you like, teammates: their admins change who is in them, and each message goes to the Oxygents it @mentions or is for.",
427
427
  posture: "mixed",
428
428
  gatewayTools: ["oxygen_agent_list", "oxygen_agent_create", "oxygen_agent_run"],
429
429
  gatewayCommands: ["agent list", "agent create", "agent run"],
@@ -1052,6 +1052,17 @@ function readInspectionRecommendation(query) {
1052
1052
  if (action)
1053
1053
  return null;
1054
1054
  const stored = positive.replace(/\bwithout\s+(?:changing|approving|rejecting|publishing|running|executing|refreshing)\b(?:\s+(?:widget\s+)?data\b)?/g, "");
1055
+ // A trigger diagnosis needs the live definition, not run tailing or a call.
1056
+ // Keep execution, authoring and run-history requests with their own owners.
1057
+ if (/\bworkflows?\b/.test(stored)
1058
+ && /\b(?:get|read|inspect|show|diagnose|details)\b/.test(stored)
1059
+ && !/\b(?:runs?|tail|logs?|templates?|blueprints?|recipes?|schemas?|knowledge|wiki|messages?|inbox|dashboards?|functions?|create|build|author|design|write|edit|update|change|delete|remove|archive|save|approve|reject|publish|running|execute|call|cancel|retry|resume|pause|restore|reset|duplicate|copy|materialize|send|refresh|launch|enable|disable|apply|import|sync|schedule|enroll|add|set)\b/.test(stored))
1060
+ return {
1061
+ capabilityId: "workflows",
1062
+ tools: ["oxygen_workflows_get", "oxygen_workflows_revisions", "oxygen_workflows_runs"],
1063
+ commands: ["workflows get", "workflows revisions", "workflows runs"],
1064
+ execution: "Read the named Workflow's live trigger status and scheduler_last_error_code with oxygen_workflows_get; revisions show authored definitions and runs show execution history. A stored refusal explains the current block, not when platform policy changed. External MCP uses oxygen_capabilities_read. Inspect before separately approving enable or execution; discovery grants no standing authority or credit spend.",
1065
+ };
1055
1066
  const storedOnly = !isHostedWorkflowIntent(stored) && !isSecondPartyDecisionIntent(stored)
1056
1067
  && !/\b(?:create|write|edit|update|change|delete|remove|archive|save|approve|reject|publish|run|running|execute|send|refresh|launch|enable|disable|apply|materialize|import|sync|schedule|enroll|add|set)\b/.test(stored);
1057
1068
  // The connector BEFORE's explicit Knowledge requests were claimed by the
@@ -1192,6 +1203,11 @@ function routeCardFor(card, query) {
1192
1203
  ...card, posture: "read_only",
1193
1204
  execution: "Read partner_bonus eligibility, amount, conditions and timing with billing summary (oxygen_billing_summary), without grants or expiry. Captured coupon evidence is conditional and dated; a future bonus still requires a fresh Stripe check. Balance and usage can reconcile credits. Publication and plan changes need separate approval.",
1194
1205
  };
1206
+ if (card.id === "billing-and-budgets" && isSeatTransitionReadIntent(query))
1207
+ card = {
1208
+ ...card, posture: "read_only",
1209
+ execution: "Read sending_seats with billing summary (oxygen_billing_summary), using the saved wallet without grants or expiry. It reports retired status, held-seat period-end meaning, channel capacity, current reservation prices and snapshot eligibility. Coverage of a specific account and its first future debit may remain unknown. A connection rechecks eligibility; this read reserves nothing.",
1210
+ };
1195
1211
  const blockRead = card.primitive === "sequences" ? sendingBlockReadRecommendation(query) : null;
1196
1212
  if (blockRead)
1197
1213
  card = { ...card, posture: "read_only", execution: blockRead.execution };
@@ -2039,6 +2055,8 @@ function explicitCapabilityIntent(query) {
2039
2055
  return ROUTE_BY_ID.get("geo") ?? null;
2040
2056
  if (isSubscriptionRetentionIntent(query))
2041
2057
  return ROUTE_BY_ID.get("billing-and-budgets") ?? null;
2058
+ if (isSeatTransitionReadIntent(query))
2059
+ return ROUTE_BY_ID.get("billing-and-budgets") ?? null;
2042
2060
  if (isConnectionPriceIntent(query))
2043
2061
  return ROUTE_BY_ID.get("billing-and-budgets") ?? null;
2044
2062
  if (sentLinkedInInvitationReadRecommendation(query))
@@ -2380,6 +2398,9 @@ function recommendationsFor(card, query) {
2380
2398
  if (card.id === "billing-and-budgets" && isPartnerBonusReadIntent(query)) {
2381
2399
  return { tools: ["oxygen_billing_summary"], commands: ["billing summary"] };
2382
2400
  }
2401
+ if (card.id === "billing-and-budgets" && isSeatTransitionReadIntent(query)) {
2402
+ return { tools: ["oxygen_billing_summary"], commands: ["billing summary"] };
2403
+ }
2383
2404
  const readOnly = readInspectionRecommendation(query);
2384
2405
  if (readOnly?.capabilityId === card.id)
2385
2406
  return readOnly;
@@ -3230,6 +3251,11 @@ function isSenderCopyVariableIntent(query) {
3230
3251
  || /\bsign off (?:variable|with|per sender)\b|\bper sender sign off\b/.test(query)
3231
3252
  || /\bsign (?:every|each|all) (?:emails?|messages?|dms?|notes?) with\b/.test(query);
3232
3253
  }
3254
+ /** Retired sending-seat questions need a stored read before any financial maintenance. */
3255
+ function isSeatTransitionReadIntent(query) {
3256
+ return /\b(sending seats?|seat subscriptions?|retired seats?|held seats?)\b/.test(query)
3257
+ && !/\b(buy|purchase|set|increase|decrease|lower|reduce|cancel|connect|disconnect)\b/.test(query);
3258
+ }
3233
3259
  /** OXP-43.36: inspect recurring account prices before creating authorization. */
3234
3260
  function isConnectionPriceIntent(query) {
3235
3261
  if (!/\b(costs?|prices?|credits?|reser(?:ve[sd]?|vations?)|charges?|charged|fees?|pay|quote)\b/.test(query))
@@ -49,7 +49,7 @@ export const BROWSER_OPERATIONS = [
49
49
  operation("billing.allowance.view", "/api/cli/billing/allowance/view", "GET", "billing allowance view", "oxygen_billing_allowance_view", "billing", "Inspect the current-cycle allowance, billing owner and partner-bonus eligibility; due plan grants or credit expiry may apply. 0 credits; reserves nothing.", {}, [], { mutates: true }),
50
50
  operation("billing.commitments.view", "/api/cli/billing/commitments/view", "GET", "billing commitments view", "oxygen_billing_commitments_view", "billing", "Read the complete fixed-credit commitment view and linked billing-owner display, without enumerating a child's sibling resources."),
51
51
  operation("billing.invoices.view", "/api/cli/billing/invoices/view", "GET", "billing invoices view", "oxygen_billing_invoices_view", "billing", "Read the browser invoice-history projection, including exact native timestamps, amounts and signed Stripe invoice links."),
52
- operation("billing.summary", "/api/cli/billing/summary", "GET", "billing summary", "oxygen_billing_summary", "billing", "Read the saved wallet balance, plan and billing owner; trialCreditGrant is the trial budget. Grant records establish actual signup credits. Includes partner-bonus eligibility/timing; applies no grants or expiry."),
52
+ operation("billing.summary", "/api/cli/billing/summary", "GET", "billing summary", "oxygen_billing_summary", "billing", "Read saved wallet/plan, partner bonus and sending_seats status, period-end meaning, channel capacity, prices and snapshot eligibility. No grants or expiry. Connection actions recheck eligibility; first debit may remain unknown. trialCreditGrant is a budget, not a recorded grant."),
53
53
  operation("billing.usage.view", "/api/cli/billing/usage/view", "GET", "billing usage view", "oxygen_billing_usage_view", "billing", "Credit spend over 7, 30 or 90 days by category and provider, each with its share, plus permitted linked-workspace rollups. totalSpent is reserved minus released; billing usage managed_credits_used counts captured credits only. Other windows or groupings: billing audit.", { days: enumOf(["7", "30", "90"]), workspace: text() }),
54
54
  operation("integrations.managed_endpoints", "/api/cli/integrations/composio/endpoints", "GET", "integrations managed-endpoints", "oxygen_integration_managed_endpoints", "integrations", "Read a managed integration's full action catalog and parameter schemas.", { integration_id: text() }, ["integration_id"]),
55
55
  operation("integrations.openrouter_models", "/api/cli/integrations/openrouter/models", "GET", "integrations openrouter-models", "oxygen_openrouter_models", "integrations", "Read model and pricing metadata using this workspace's connected OpenRouter account; credentials are never returned."),
@@ -105,11 +105,11 @@ export const BROWSER_OPERATIONS = [
105
105
  operation("tables.resource.ingestions.retry", `${table}/ingestions/{ingestion_run}/retry-failed`, "POST", "tables ingestion retry", "oxygen_table_resource_ingestion_retry", "runs", "Retry failed items in this Table's hosted ingestion run."),
106
106
  operation("tables.resource.import", "/api/cli/tables/import", "POST", "tables upload", "oxygen_table_resource_upload", "tables", "Preview or import small uploaded CSV, JSON or XLSX bytes through the exact browser import contract. Preview writes no rows.", upload, ["content_base64", "file_name"], { multipart }),
107
107
  operation("billing.activate", "/api/cli/billing/activate", "POST", "billing activate", "oxygen_billing_activate", "billing", "Activate a trial subscription now and charge its saved payment method; administrator approval is required. An optional tier reprices the active trial in the same call.", { approved: bool(), tier: text() }, ["approved"], { spends: true }),
108
- operation("billing.checkout", "/api/cli/billing/checkout", "POST", "billing checkout", "oxygen_billing_checkout", "billing", "Open browser Checkout; payment approves purchase. First purchase: admin/owner. New yearly plans use interval month, term year and the current terms_version.", {
108
+ operation("billing.checkout", "/api/cli/billing/checkout", "POST", "billing checkout", "oxygen_billing_checkout", "billing", "Checkout approval link; admin/owner for first purchase. Yearly plans charge the full discounted year upfront: use interval year, term year. Credits arrive monthly.", {
109
109
  plan: text("Plan key from billing balance available_plans, for example oxygen_49."),
110
- interval: enumOf(["month", "year"], { description: "New purchases use month. year is legacy prepaid cadence, refused for new purchases." }),
111
- term: enumOf(["month", "year"], { description: "year commits to twelve discounted monthly payments; month is the default." }),
112
- terms_version: text("Required for term year: annual_monthly_terms.terms_version from billing balance."),
110
+ interval: enumOf(["month", "year"], { description: "month bills monthly; year charges the full discounted year upfront." }),
111
+ term: enumOf(["month", "year"], { description: "year pays the full discounted year upfront; month is the default." }),
112
+ terms_version: text("Historical installment terms version; new yearly purchases use interval year."),
113
113
  currency: text("Billing currency: usd (default)."),
114
114
  trial: bool({ description: "Retired compatibility option; omit for a new paid subscription." }),
115
115
  return_to: text("Same-origin path to return to after Checkout, for example /home."),
@@ -133,6 +133,6 @@ export const BROWSER_OPERATIONS = [
133
133
  operation("notetaker.calendar.schedule", "/api/cli/notetaker/calendar", "POST", "notetaker calendar schedule", "oxygen_notetaker_calendar_schedule", "crm", "Schedule recording for an existing calendar event after approval; an external CLI or MCP caller must supply an explicit max_credits ceiling.", { event_id: text(), approved: bool(), max_credits: num({ minimum: 0 }) }, ["event_id", "approved"], { defaults: { action: "schedule" }, spends: true }),
134
134
  operation("notetaker.note.edit", "/api/cli/notetaker/sessions/{session}/note", "PATCH", "notetaker notes edit", "oxygen_notetaker_note_edit", "crm", "Save the markdown note for one meeting session and return the committed note.", { markdown: text() }, ["markdown"]),
135
135
  operation("orgs.create", "/api/cli/orgs/create", "POST", "orgs create", "oxygen_workspace_create", "workspace", "Create a workspace for the authenticated person's account and begin native company setup. Workspace API keys cannot create personal workspaces.", { name: text(), company_domain: text("Company website or domain, or its LinkedIn company page URL when it has no website."), linkedin_profile_url: text("Creator's LinkedIn profile URL, separate from the company's page.") }, ["name", "company_domain"]),
136
- operation("onboarding.complete", "/api/cli/onboarding/complete", "POST", "onboarding complete", "oxygen_onboarding_complete", "workspace", "Complete the active workspace's onboarding step and record its discovery-source choices. The other choice requires source_other.", { sources: arr(enumOf(["linkedin_content", "linkedin_dm", "linkedin_ads", "x", "youtube", "google", "email", "word_of_mouth", "referral", "event", "other"])), source_other: text(), redirect_url: text() }, ["sources"]),
136
+ operation("onboarding.complete", "/api/cli/onboarding/complete", "POST", "onboarding complete", "oxygen_onboarding_complete", "workspace", "Complete the active workspace's onboarding step and record its discovery-source choices. The other choice requires source_other.", { sources: arr(enumOf(["linkedin_content", "linkedin_dm", "linkedin_ads", "x", "youtube", "google", "ai_assistant", "email", "word_of_mouth", "referral", "event", "other"])), source_other: text(), redirect_url: text() }, ["sources"]),
137
137
  operation("onboarding.start", "/api/cli/onboarding/start", "POST", "onboarding start", "oxygen_onboarding_start", "workspace", "Answer onboarding's choice card. own_agent continues to connecting your own agent. copilot opens the Workspace Copilot with the onboarding conversation already started, billed in credits like any Copilot turn; choosing again reopens that conversation.", { path: enumOf(["own_agent", "copilot"]), play: text("A play slug carried to the install step.") }, ["path"], { spends: true }),
138
138
  ];