@oxygen-agent/cli 1.1010.905 → 1.1010.952

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +1 -1
  2. package/dist/contract-flags.d.ts +42 -0
  3. package/dist/contract-flags.js +220 -0
  4. package/dist/index.js +43 -184
  5. package/dist/sender-limits-flags.d.ts +14 -0
  6. package/dist/sender-limits-flags.js +157 -0
  7. package/dist/ugc-commands.js +16 -2
  8. package/node_modules/@oxygen/cli-ugc/dist/commands.js +3 -3
  9. package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +4 -0
  10. package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +20 -8
  11. package/node_modules/@oxygen/cli-ugc/dist/index.d.ts +1 -0
  12. package/node_modules/@oxygen/cli-ugc/dist/index.js +1 -0
  13. package/node_modules/@oxygen/shared/dist/capability-discovery.js +2 -2
  14. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +39 -0
  15. package/node_modules/@oxygen/shared/dist/contracts/operations.js +11 -0
  16. package/node_modules/@oxygen/shared/dist/contracts/schema.d.ts +186 -0
  17. package/node_modules/@oxygen/shared/dist/contracts/schema.js +260 -0
  18. package/node_modules/@oxygen/shared/dist/contracts/sender-limits.d.ts +67 -0
  19. package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +88 -0
  20. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +11 -0
  21. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +41 -0
  22. package/node_modules/@oxygen/shared/dist/product-briefing-rules.js +4 -2
  23. package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +2 -1
  24. package/node_modules/@oxygen/shared/dist/table-capacity.js +18 -3
  25. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  26. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  27. package/node_modules/@oxygen/shared/dist/version.js +3 -0
  28. package/package.json +1 -1
package/README.md CHANGED
@@ -34,4 +34,4 @@ oxygen update
34
34
 
35
35
  For product documentation, visit https://oxygen-agent.com/docs. For support, visit https://oxygen-agent.com.
36
36
 
37
- Version: 1.1010.905
37
+ Version: 1.1010.952
@@ -0,0 +1,42 @@
1
+ import { type Command } from "commander";
2
+ import { type ContractObject } from "@oxygen/shared/contracts/schema";
3
+ export type ContractFlagOverlayEntry = {
4
+ /** Replaces the derived flag name (no leading dashes). */
5
+ flag?: string;
6
+ help?: string;
7
+ /** Value placeholder, e.g. `<n>`; defaults to `<value>` (`<json>` for a JSON flag). */
8
+ valueName?: string;
9
+ /** Further accepted spellings, hidden from help. */
10
+ aliases?: readonly string[];
11
+ /** Boolean: help for the `--no-<flag>` form. */
12
+ negateHelp?: string;
13
+ /**
14
+ * Boolean as two flags, e.g. `--warmup-restart` (true) / `--warmup-disable`
15
+ * (false). When both are given, `off` wins.
16
+ */
17
+ pair?: {
18
+ on: string;
19
+ onHelp: string;
20
+ off: string;
21
+ offHelp: string;
22
+ };
23
+ /** Replaces the typed parse; the returned value is sent as is (the route validates it). */
24
+ parse?: (raw: string) => unknown;
25
+ /** Object: its fields become flags without the object's prefix. */
26
+ flatten?: boolean;
27
+ /** Nullable: do not generate the `--clear-<flag>` form. */
28
+ noClear?: boolean;
29
+ /** Leave the field off this command entirely. */
30
+ omit?: boolean;
31
+ };
32
+ /** Keyed by dotted contract path, e.g. `working_hours.timezone`. */
33
+ export type ContractFlagOverlay = Readonly<Record<string, ContractFlagOverlayEntry>>;
34
+ /** Registers the contract's flags on `command`, in contract order. */
35
+ export declare function addContractFlags(command: Command, contract: ContractObject, overlay?: ContractFlagOverlay): Command;
36
+ /**
37
+ * The request body the parsed flags describe: only the fields given, nested as
38
+ * the contract nests them. An object none of whose flags was given is absent.
39
+ */
40
+ export declare function readContractFlags(options: Record<string, unknown>, contract: ContractObject, overlay?: ContractFlagOverlay): Record<string, unknown>;
41
+ /** Every long flag `addContractFlags` registers, in order (for tests and docs). */
42
+ export declare function contractFlagNames(contract: ContractObject, overlay?: ContractFlagOverlay): string[];
@@ -0,0 +1,220 @@
1
+ // The CLI half of a shared resource contract (@oxygen/shared/contracts/schema).
2
+ // A command's flags and the request body they build are generated from the same
3
+ // contract that generates the MCP input schema and validates the route, so a
4
+ // settable field cannot exist on one surface and not the other.
5
+ //
6
+ // Derivation (the overlay only renames, re-words, aliases or hides):
7
+ // scalar --<kebab-name> <value> numbers parsed and range-checked
8
+ // enum --<kebab-name> <value> membership checked
9
+ // boolean --<x> / --no-<x> tri-state: absent = not sent
10
+ // array of scalars --<kebab-name> <a,b,c>
11
+ // object, <= 6 scalar fields --<obj>-<field> per field
12
+ // larger object --<obj>-json <json>
13
+ // nullable also --clear-<x>, which sends null
14
+ // Every value is checked inside the action (never in a Commander parser), so a
15
+ // bad value comes back as the JSON error envelope, not a raw stack trace.
16
+ import { Option } from "commander";
17
+ import { OxygenError } from "@oxygen/shared/cli-result";
18
+ import { unwrapNullable } from "@oxygen/shared/contracts/schema";
19
+ const MAX_SPLAT_FIELDS = 6;
20
+ function kebab(name) {
21
+ return name.replace(/_/g, "-");
22
+ }
23
+ function attribute(flag) {
24
+ return flag.replace(/-([a-z0-9])/g, (_match, char) => char.toUpperCase());
25
+ }
26
+ function isObject(property) {
27
+ return "type" in property && property.type === "object" && property.properties !== undefined;
28
+ }
29
+ function contractFlagSpecs(properties, overlay, parentPath = [], parentFlag = "") {
30
+ const specs = [];
31
+ for (const [name, raw] of Object.entries(properties)) {
32
+ const path = [...parentPath, name];
33
+ const entry = overlay[path.join(".")] ?? {};
34
+ if (entry.omit)
35
+ continue;
36
+ const { property, nullable } = unwrapNullable(raw);
37
+ const flag = entry.flag ?? `${parentFlag}${kebab(name)}`;
38
+ if (isObject(property)) {
39
+ const fields = Object.values(property.properties ?? {});
40
+ const splat = entry.flatten || (fields.length <= MAX_SPLAT_FIELDS && !fields.some((field) => isObject(unwrapNullable(field).property)));
41
+ if (splat)
42
+ specs.push(...contractFlagSpecs(property.properties ?? {}, overlay, path, entry.flatten ? "" : `${flag}-`));
43
+ else
44
+ specs.push({ kind: "json", path, flag: `${flag}-json`, entry });
45
+ }
46
+ else if ("type" in property && property.type === "boolean") {
47
+ specs.push(entry.pair ? { kind: "pair", path, on: entry.pair.on, off: entry.pair.off, entry } : { kind: "boolean", path, flag, entry });
48
+ }
49
+ else {
50
+ specs.push({ kind: "value", path, flag, property, entry });
51
+ }
52
+ if (nullable && !entry.noClear)
53
+ specs.push({ kind: "clear", path, flag: `clear-${flag}`, target: flag });
54
+ }
55
+ return specs;
56
+ }
57
+ function helpFor(spec, property) {
58
+ if (spec.entry.help !== undefined)
59
+ return spec.entry.help;
60
+ const description = property && "description" in property ? property.description ?? "" : "";
61
+ const values = property && "enum" in property && property.enum ? ` One of: ${property.enum.join(", ")}.` : "";
62
+ return `${description}${values}`.trim();
63
+ }
64
+ function propertyAt(contract, path) {
65
+ let current = contract;
66
+ for (const key of path) {
67
+ const unwrapped = current ? unwrapNullable(current).property : undefined;
68
+ current = unwrapped && isObject(unwrapped) ? unwrapped.properties?.[key] : undefined;
69
+ }
70
+ return current ? unwrapNullable(current).property : undefined;
71
+ }
72
+ /** Registers the contract's flags on `command`, in contract order. */
73
+ export function addContractFlags(command, contract, overlay = {}) {
74
+ for (const spec of contractFlagSpecs(contract.properties, overlay)) {
75
+ const property = "path" in spec ? propertyAt(contract, spec.path) : undefined;
76
+ if (spec.kind === "pair") {
77
+ command.addOption(new Option(`--${spec.on}`, spec.entry.pair?.onHelp));
78
+ command.addOption(new Option(`--${spec.off}`, spec.entry.pair?.offHelp));
79
+ continue;
80
+ }
81
+ if (spec.kind === "clear") {
82
+ command.addOption(new Option(`--${spec.flag}`, `Clear it (sends null). Not with --${spec.target}.`));
83
+ continue;
84
+ }
85
+ if (spec.kind === "boolean") {
86
+ command.addOption(new Option(`--${spec.flag}`, helpFor(spec, property)));
87
+ command.addOption(new Option(`--no-${spec.flag}`, spec.entry.negateHelp ?? `Sends ${spec.path.join(".")}: false.`));
88
+ continue;
89
+ }
90
+ const valueName = spec.entry.valueName ?? (spec.kind === "json" ? "<json>" : "<value>");
91
+ command.addOption(new Option(`--${spec.flag} ${valueName}`, helpFor(spec, property)));
92
+ for (const alias of spec.entry.aliases ?? [])
93
+ command.addOption(new Option(`--${alias} ${valueName}`).hideHelp());
94
+ }
95
+ return command;
96
+ }
97
+ function invalid(flag, value, message, code = "invalid_number") {
98
+ return new OxygenError(code, message, { details: { flag: `--${flag}`, value }, exitCode: 1 });
99
+ }
100
+ function parseScalar(property, raw, flag) {
101
+ if (!("type" in property))
102
+ return raw;
103
+ if (property.type === "integer" || property.type === "number") {
104
+ const parsed = Number(raw);
105
+ const whole = property.type === "integer";
106
+ const minimum = property.minimum;
107
+ const ok = whole ? Number.isSafeInteger(parsed) : Number.isFinite(parsed);
108
+ if (!ok || (minimum !== undefined && parsed < minimum)) {
109
+ throw invalid(flag, raw, minimum === 0
110
+ ? `Expected a non-negative ${whole ? "integer" : "number"}.`
111
+ : `Expected ${whole ? "an integer" : "a number"}${minimum !== undefined ? ` of at least ${minimum}` : ""}.`);
112
+ }
113
+ // A clamped field is lowered (and reported) by the server; anything else over the maximum is refused here.
114
+ if (property.maximum !== undefined && parsed > property.maximum && !property["x-clamp"]) {
115
+ throw invalid(flag, raw, `Expected at most ${property.maximum}.`);
116
+ }
117
+ return parsed;
118
+ }
119
+ if (property.type === "string" && property.enum && !property.enum.includes(raw)) {
120
+ throw invalid(flag, raw, `--${flag} must be one of: ${property.enum.join(", ")}.`, "invalid_option");
121
+ }
122
+ return raw;
123
+ }
124
+ function readValue(spec, raw) {
125
+ if (spec.entry.parse)
126
+ return spec.entry.parse(raw);
127
+ const property = spec.property;
128
+ if ("type" in property && property.type === "array") {
129
+ return raw.split(",").map((part) => part.trim()).filter(Boolean)
130
+ .map((part) => parseScalar(unwrapNullable(property.items).property, part, spec.flag));
131
+ }
132
+ return parseScalar(property, raw, spec.flag);
133
+ }
134
+ function setPath(body, path, value) {
135
+ let cursor = body;
136
+ for (const key of path.slice(0, -1)) {
137
+ const next = cursor[key];
138
+ if (typeof next === "object" && next !== null && !Array.isArray(next))
139
+ cursor = next;
140
+ else
141
+ cursor = (cursor[key] = {});
142
+ }
143
+ cursor[path[path.length - 1]] = value;
144
+ }
145
+ function stringOption(options, flag, aliases = []) {
146
+ for (const name of [flag, ...aliases]) {
147
+ const value = options[attribute(name)];
148
+ if (typeof value === "string" && value.trim())
149
+ return value.trim();
150
+ }
151
+ return undefined;
152
+ }
153
+ /**
154
+ * The request body the parsed flags describe: only the fields given, nested as
155
+ * the contract nests them. An object none of whose flags was given is absent.
156
+ */
157
+ export function readContractFlags(options, contract, overlay = {}) {
158
+ const body = {};
159
+ const specs = contractFlagSpecs(contract.properties, overlay);
160
+ const given = new Set();
161
+ for (const spec of specs) {
162
+ if (spec.kind === "value") {
163
+ const raw = stringOption(options, spec.flag, spec.entry.aliases);
164
+ if (raw === undefined)
165
+ continue;
166
+ setPath(body, spec.path, readValue(spec, raw));
167
+ given.add(spec.flag);
168
+ }
169
+ else if (spec.kind === "json") {
170
+ const raw = stringOption(options, spec.flag, spec.entry.aliases);
171
+ if (raw === undefined)
172
+ continue;
173
+ let parsed;
174
+ try {
175
+ parsed = JSON.parse(raw);
176
+ }
177
+ catch {
178
+ throw invalid(spec.flag, raw, `--${spec.flag} must be a JSON object.`, "invalid_json");
179
+ }
180
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
181
+ throw invalid(spec.flag, raw, `--${spec.flag} must be a JSON object.`, "invalid_json");
182
+ }
183
+ setPath(body, spec.path, parsed);
184
+ given.add(spec.flag);
185
+ }
186
+ else if (spec.kind === "boolean") {
187
+ const value = options[attribute(spec.flag)];
188
+ if (typeof value === "boolean")
189
+ setPath(body, spec.path, value);
190
+ }
191
+ else if (spec.kind === "pair") {
192
+ if (options[attribute(spec.off)] === true)
193
+ setPath(body, spec.path, false);
194
+ else if (options[attribute(spec.on)] === true)
195
+ setPath(body, spec.path, true);
196
+ }
197
+ else if (options[attribute(spec.flag)] === true) {
198
+ if (given.has(spec.target)) {
199
+ throw new OxygenError("invalid_request", `Pass either --${spec.target} or --${spec.flag}, not both.`, { exitCode: 1 });
200
+ }
201
+ setPath(body, spec.path, null);
202
+ }
203
+ }
204
+ return body;
205
+ }
206
+ /** Every long flag `addContractFlags` registers, in order (for tests and docs). */
207
+ export function contractFlagNames(contract, overlay = {}) {
208
+ return contractFlagSpecs(contract.properties, overlay).flatMap((spec) => {
209
+ switch (spec.kind) {
210
+ case "pair":
211
+ return [`--${spec.on}`, `--${spec.off}`];
212
+ case "boolean":
213
+ return [`--${spec.flag}`, `--no-${spec.flag}`];
214
+ case "clear":
215
+ return [`--${spec.flag}`];
216
+ default:
217
+ return [`--${spec.flag}`, ...(spec.entry.aliases ?? []).map((alias) => `--${alias}`)];
218
+ }
219
+ });
220
+ }
package/dist/index.js CHANGED
@@ -21,6 +21,10 @@ import { TAG_COLORS } from "@oxygen/shared/select-options";
21
21
  import { PUBLIC_WEBHOOK_INGRESS_PER_TARGET_PER_MINUTE } from "@oxygen/shared/plan-band";
22
22
  import { AI_COLUMN_CREDITS, AI_COLUMN_WEB_SEARCH_TYPICAL_CREDITS } from "@oxygen/shared/pricing-sheet";
23
23
  import { LINKEDIN_SENDER_LIMIT_DEFAULTS, LINKEDIN_SENDER_LIMIT_MAXIMUMS, } from "@oxygen/shared/linkedin-sequences";
24
+ import { operationPath } from "@oxygen/shared/contracts/operations";
25
+ import { SENDER_LIMITS_SET_OPERATION } from "@oxygen/shared/contracts/sender-limits";
26
+ import { addContractFlags } from "./contract-flags.js";
27
+ import { buildSenderLimitsBody, parseWorkingDaysOption, SENDER_LIMITS_FLAG_OVERLAY } from "./sender-limits-flags.js";
24
28
  import { readColumnDecisionFlags } from "./column-decision-options.js";
25
29
  import { MANAGED_DATA_SUPPLIERS } from "@oxygen/shared/data-suppliers";
26
30
  import { MAX_BUFFERED_IMPORT_PARSE_BYTES, assertImportFormatWithinBufferedLimit, inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, parseXlsxWorkbookBuffer, } from "@oxygen/shared/file-import";
@@ -3965,9 +3969,12 @@ Examples:
3965
3969
  }));
3966
3970
  }))
3967
3971
  .addCommand(new Command("file")
3968
- .description("Create a structured, titled Plain support Thread immediately. Prefer `support chat` for an ordinary conversation; use this zero-credit filing path when explicit subject, operation, error, run, or table metadata helps triage. Check `support list --status open` first because a new filing can create a second Thread.")
3972
+ .description("Create a structured Plain support conversation with a subject and required message. Prefer `support chat` for an ordinary conversation; use this zero-credit filing path when operation, error, run, or table metadata helps triage. A selected local transcript can be attached with --transcript-file, --session-id, or --include-transcript. Check `support list --status open` first because a new filing can create a second Thread.")
3969
3973
  .requiredOption("--subject <subject>", "One-line summary of the problem.")
3970
- .option("--body <body>", "What you were doing, what happened, and what you tried.")
3974
+ .requiredOption("--body <body>", "Required message: what you were doing, what happened, and what you tried.")
3975
+ .option("--include-transcript", "Attach the most recent local agent transcript after redaction.")
3976
+ .option("--session-id <id>", "Attach this local agent session transcript.")
3977
+ .option("--transcript-file <path>", "Attach the last 2 MB of a selected text/JSONL file after best-effort credential redaction; review it first.")
3971
3978
  .option("--severity <severity>", "low | normal | high. Defaults to normal.")
3972
3979
  .option("--category <category>", "Request type: question, bug, feature_request, billing, security_data, configuration, agency_directory, or other.")
3973
3980
  .option("--source <source>", "Where the ticket originated. Only slack is supported today.")
@@ -4235,7 +4242,7 @@ The final readback must show ticket.status triaging, plain_status TODO, the In p
4235
4242
  .description("Retired legacy write; always returns the Plain-cutover error (staff only).")
4236
4243
  .requiredOption("--organization <organization>", "Target organization id or slug the ticket belongs to.")
4237
4244
  .requiredOption("--subject <subject>", "One-line summary of the customer's request.")
4238
- .option("--body <body>", "The customer's request in their words, plus any staff notes.")
4245
+ .requiredOption("--body <body>", "The customer's request in their words, plus any staff notes.")
4239
4246
  .option("--severity <severity>", "low | normal | high. Defaults to normal.")
4240
4247
  .option("--category <category>", "Optional category label, e.g. bug, feature, question.")
4241
4248
  .option("--source <source>", "Where the ticket originated. Only slack is supported today.")
@@ -11064,7 +11071,7 @@ Examples:
11064
11071
  }));
11065
11072
  })))
11066
11073
  .addCommand(new Command("invoices")
11067
- .description("List every invoice Stripe has issued for this workspace, newest first, each with a direct PDF link. Covers ALL charges, not just the plan — sending seats, managed inboxes, and credit top-ups appear here too, which is why this works on the free tier: a workspace with no plan can still be a paying customer. A workspace linked to another organization (`orgs billing-link`) reads its billing at that owner, so it sees an empty list and shared_billing=true; switch to the billing owner to read its invoices. pdf_url and hosted_url are Stripe's own links. Read-only, 0 Oxygen credits.")
11074
+ .description("List every invoice Stripe has issued for this workspace, newest first, each with a direct PDF link. Covers ALL charges, not just the plan — sending seats, managed inboxes, and credit top-ups appear here too, which is why this works on the free tier: a workspace with no plan can still be a paying customer. A workspace linked to another organization (`orgs billing-link`) reads its billing at that owner, so it sees an empty list and shared_billing=true; switch to the billing owner to read its invoices. pdf_url and hosted_url are Stripe's own links. VAT or tax ID: open the web_url this command returns and choose VAT or tax ID on the Invoices card (top-up checkout also asks for it); Stripe prints it on every invoice issued after you save it, never on an earlier one. Read-only, 0 Oxygen credits.")
11068
11075
  .option("--json", "Print a JSON envelope.")
11069
11076
  .action(async (options) => {
11070
11077
  await handleAsyncAction("billing invoices", options, () => requestOxygen("/api/cli/billing/invoices"));
@@ -11204,7 +11211,7 @@ Examples:
11204
11211
  }));
11205
11212
  }))
11206
11213
  .addCommand(new Command("cancel")
11207
- .description("Schedule the plan subscription to cancel at period end. During a trial the card is never charged; undo any time before then with billing resume.")
11214
+ .description("Schedule the plan subscription to cancel at period end. Free (no fee, no credits); the plan stays usable until then and the card is not charged again (during a trial, never). Prints the end date; undo any time before then with billing resume.")
11208
11215
  .option("--json", "Print a JSON envelope.")
11209
11216
  .action(async (options) => {
11210
11217
  await handleAsyncAction("billing cancel", options, () => requestOxygen("/api/cli/billing/cancel", {
@@ -11213,7 +11220,7 @@ Examples:
11213
11220
  }));
11214
11221
  }))
11215
11222
  .addCommand(new Command("resume")
11216
- .description("Remove a scheduled cancellation so the subscription renews (or the trial converts) normally.")
11223
+ .description("Remove a scheduled cancellation so the subscription renews (or the trial converts) normally. Free (no fee, no credits). Prints the next charge date and amount.")
11217
11224
  .option("--json", "Print a JSON envelope.")
11218
11225
  .action(async (options) => {
11219
11226
  await handleAsyncAction("billing resume", options, () => requestOxygen("/api/cli/billing/cancel", {
@@ -13501,54 +13508,15 @@ Examples:
13501
13508
  .action(async (id, options) => {
13502
13509
  await handleAsyncAction("senders limits get", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/limits`));
13503
13510
  }))
13504
- .addCommand(new Command("set")
13505
- .description("Adjust per-account action limits and working hours. LinkedIn actions only run inside the account's working hours (default 07:00-22:00 every day, in the account's timezone); work due outside them is deferred to the next window, not failed. Set them with --timezone, --working-days, --working-hours-start and --working-hours-end; flags you omit keep their current value. A sequence's own schedule (sequences update --linkedin-timezone ...) can narrow the window for its LinkedIn steps, never widen it; email steps follow the sequence's email send window instead. Every limit flag shows its default and hard maximum; values above the maximum are clamped, and higher caps raise the risk of a LinkedIn restriction. Each action has its own cap (InMails per day and per rolling 30 days, profile views, profile lookups, endorsements, invitation withdrawals, posts), and every send except posts also counts toward --total-actions-per-day. Daily send caps run at half on Saturday and Sunday in the account's timezone unless --weekend-full-volume is set. A withdrawn invitation cannot be re-sent to the same person for 21 days. Own-feed posts and your own Unibox replies ignore working hours. A refused action names its reason (outside_working_hours defers it; linkedin_rate_limited names the limit and resets_at). Changing limits is free. <id> accepts a sender account id, connection id, or Unipile account id.")
13506
- .argument("<id>", "Sender account id, connection id, or Unipile account id.")
13507
- .option("--invites-per-day <n>", linkedInLimitHelp("invites_per_day", "Daily LinkedIn connection invites cap"))
13508
- .option("--invites-per-week <n>", linkedInLimitHelp("invites_per_week", "Weekly LinkedIn connection invites cap"))
13509
- .option("--messages-per-day <n>", linkedInLimitHelp("messages_per_day", "Daily direct messages cap"))
13510
- .option("--inmails-per-day <n>", linkedInLimitHelp("inmails_per_day", "Daily InMail cap"))
13511
- .option("--inmails-per-month <n>", linkedInLimitHelp("inmails_per_month", "InMail cap over a rolling 30 days"))
13512
- .option("--profile-views-per-day <n>", linkedInLimitHelp("profile_views_per_day", "Daily profile views cap; the member is notified"))
13513
- .option("--profile-lookups-per-day <n>", linkedInLimitHelp("profile_lookups_per_day", "Daily invisible profile and company lookups cap"))
13514
- .option("--endorsements-per-day <n>", linkedInLimitHelp("endorsements_per_day", "Daily skill endorsements cap"))
13515
- .option("--withdrawals-per-day <n>", linkedInLimitHelp("withdrawals_per_day", "Daily cap on withdrawing sent invitations; reading the sent list uses the read budgets"))
13516
- .option("--posts-per-day <n>", linkedInLimitHelp("posts_per_day", "Daily posts to the account's own feed, from every caller; not counted in the total"))
13517
- .option("--follows-per-day <n>", linkedInLimitHelp("follows_per_day", "Daily follows cap"))
13518
- .option("--likes-per-day <n>", linkedInLimitHelp("likes_per_day", "Daily likes cap"))
13519
- .option("--comments-per-day <n>", linkedInLimitHelp("comments_per_day", "Daily comments cap"))
13520
- .option("--total-actions-per-day <n>", linkedInLimitHelp("total_actions_per_day", "Daily cap across all send/action types except posts"))
13521
- .option("--relations-reads-per-day <n>", linkedInLimitHelp("relations_reads_per_day", "Daily cap on relations/connections list reads, the scrape ban vector"))
13522
- .option("--messages-reads-per-day <n>", linkedInLimitHelp("messages_reads_per_day", "Daily cap on chat and message-history reads"))
13523
- .option("--searches-per-day <n>", linkedInLimitHelp("searches_per_day", "Daily cap on LinkedIn search executions"))
13524
- .option("--sales-nav-search-results-per-day <n>", linkedInLimitHelp("sales_nav_search_results_per_day", "Daily cap on Sales Navigator search result rows fetched"))
13525
- .option("--api-reads-per-day <n>", linkedInLimitHelp("api_reads_per_day", "Daily cap on all other LinkedIn API reads"))
13526
- .option("--total-reads-per-day <n>", linkedInLimitHelp("total_reads_per_day", "Daily cap across all read units"))
13527
- .option("--min-spacing-seconds <n>", linkedInLimitHelp("min_action_spacing_seconds", "Minimum seconds between actions"))
13528
- .option("--spacing-jitter-seconds <n>", linkedInLimitHelp("action_spacing_jitter_seconds", "Random jitter seconds added to action spacing"))
13529
- .option("--interactive-min-spacing-seconds <n>", linkedInLimitHelp("interactive_min_spacing_seconds", "Minimum seconds between human-sent Unibox actions"))
13530
- .option("--interactive-spacing-jitter-seconds <n>", linkedInLimitHelp("interactive_spacing_jitter_seconds", "Random jitter seconds added to human-sent Unibox action spacing"))
13531
- .option("--interactive-sends-per-hour <n>", linkedInLimitHelp("interactive_sends_per_hour", "Hourly cap on human-sent Unibox actions"))
13532
- .option("--timezone <tz>", "IANA timezone for the working hours and the daily counter reset, e.g. Europe/Berlin. Defaults to the zone of the country chosen at connect.")
13533
- .option("--working-days <days>", "Days LinkedIn actions may run: mon-fri, mon,wed,fri, or ISO numbers 1-7 (1 = Monday). A weekday-only window leaves a visible two-day gap every week.")
13534
- .option("--working-hours-start <HH:MM>", "Local time working hours start, e.g. 09:00.")
13535
- .option("--working-hours-end <HH:MM>", "Local time working hours end (exclusive), e.g. 18:00. Must be after the start.")
13536
- .option("--warmup-restart", "Start (or restart) the warm-up ramp now — gradually raises this account's invite + message caps to full over ~2 weeks.")
13537
- .option("--warmup-disable", "Turn off warm-up for this account (treat it as already warm and use its full configured caps).")
13538
- .option("--read-warmup-disable", "Turn off the READ warm-up ramp for this account. Network capture (`oxygen linkedin network setup`) eases a newly armed account in at 5 \u2192 10 \u2192 20 reads a day over two weeks; this runs it at the full read budget immediately. Separate from --warmup-disable, which governs invites and messages.")
13539
- .option("--read-warmup-restart", "Restart the READ warm-up ramp from today, so network capture eases back in from 5 reads a day. Also clears --read-warmup-disable.")
13540
- .option("--warmup-start-date <date>", "Set the warm-up start date (ISO, e.g. 2026-01-31). A past date credits prior warming and advances the ramp.")
13541
- .option("--warmup-preset <preset>", "Warm-up ramp curve: conservative (~3 weeks), standard (~2 weeks, default), or fast (~1 week for an aged account).")
13542
- .option("--randomize-caps", "Randomise this account's daily send caps to 80-100% of configured so daily volume varies instead of hitting the same cap every day.")
13543
- .option("--no-randomize-caps", "Turn off randomised daily caps (use the exact configured caps every day).")
13544
- .option("--weekend-full-volume", "Keep full daily send caps on Saturday and Sunday (by default they run at half).")
13545
- .option("--no-weekend-full-volume", "Run daily send caps at half on Saturday and Sunday again (the default).")
13511
+ .addCommand(addContractFlags(new Command(SENDER_LIMITS_SET_OPERATION.cli.path.at(-1) ?? "set")
13512
+ .description(SENDER_LIMITS_SET_OPERATION.cli.summary)
13513
+ .argument("<id>", SENDER_LIMITS_SET_OPERATION.cli.argument?.description), SENDER_LIMITS_SET_OPERATION.contract, SENDER_LIMITS_FLAG_OVERLAY)
13546
13514
  .option("--json", "Print a JSON envelope.")
13547
13515
  .action(async (id, options) => {
13548
13516
  await handleAsyncAction("senders limits set", options, () => {
13549
- const body = buildLinkedinLimitsBody(options);
13550
- return requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/limits`, {
13551
- method: "PATCH",
13517
+ const body = buildSenderLimitsBody(options);
13518
+ return requestOxygen(operationPath(SENDER_LIMITS_SET_OPERATION, { id }), {
13519
+ method: SENDER_LIMITS_SET_OPERATION.method,
13552
13520
  body,
13553
13521
  });
13554
13522
  });
@@ -14408,7 +14376,7 @@ Examples:
14408
14376
  .option("--json", "Print a JSON envelope.")
14409
14377
  .action(async (id, options) => {
14410
14378
  await handleAsyncAction("whatsapp limits set", options, () => {
14411
- const body = buildLinkedinLimitsBody(options);
14379
+ const body = buildSenderLimitsBody(options);
14412
14380
  return requestOxygen(`/api/cli/whatsapp/accounts/${encodeURIComponent(id)}/limits`, {
14413
14381
  method: "PATCH",
14414
14382
  body,
@@ -27572,9 +27540,29 @@ function buildFeedbackBody(options) {
27572
27540
  return body;
27573
27541
  }
27574
27542
  function buildSupportTicketBody(options) {
27575
- const body = { subject: readOption(options.subject) };
27576
- if (readOption(options.body))
27577
- body.body = readOption(options.body);
27543
+ const message = readOption(options.body);
27544
+ if (!message) {
27545
+ throw new OxygenError("invalid_input", 'Provide a support message with --body "...".', { exitCode: 1 });
27546
+ }
27547
+ const body = { subject: readOption(options.subject), body: message };
27548
+ const sessionId = readOption(options.sessionId);
27549
+ const file = readOption(options.transcriptFile);
27550
+ if (options.includeTranscript || sessionId || file) {
27551
+ try {
27552
+ const captured = captureCurrentTranscript({ sessionId, file });
27553
+ if (!captured) {
27554
+ throw new OxygenError("transcript_not_found", "No local agent transcript was found. Choose --session-id or --transcript-file.", { exitCode: 1 });
27555
+ }
27556
+ const { path: _path, ...payload } = captured;
27557
+ body.transcript = payload;
27558
+ }
27559
+ catch (error) {
27560
+ if (error instanceof TranscriptCaptureError) {
27561
+ throw new OxygenError("transcript_not_found", error.message, { exitCode: 1 });
27562
+ }
27563
+ throw error;
27564
+ }
27565
+ }
27578
27566
  if (readOption(options.severity))
27579
27567
  body.severity = readOption(options.severity);
27580
27568
  if (readOption(options.category))
@@ -28692,110 +28680,6 @@ table, options) {
28692
28680
  ...(options.onlyMissing ? { only_missing: true } : {}),
28693
28681
  };
28694
28682
  }
28695
- /** "<text> (default N, max M)." for a LinkedIn sender limit flag, from the enforced constants. */
28696
- function linkedInLimitHelp(key, text) {
28697
- return `${text} (default ${LINKEDIN_SENDER_LIMIT_DEFAULTS[key]}, max ${LINKEDIN_SENDER_LIMIT_MAXIMUMS[key]}).`;
28698
- }
28699
- function buildLinkedinLimitsBody(// skipcq: JS-R1005 -- CLI body builder maps per-account limit flags + reset timezone.
28700
- options) {
28701
- const limits = {};
28702
- const setLimit = (key, value) => {
28703
- const parsed = readNonNegativeInt(value);
28704
- if (parsed !== undefined)
28705
- limits[key] = parsed;
28706
- };
28707
- setLimit("invites_per_day", options.invitesPerDay);
28708
- setLimit("invites_per_week", options.invitesPerWeek);
28709
- setLimit("messages_per_day", options.messagesPerDay);
28710
- setLimit("inmails_per_day", options.inmailsPerDay);
28711
- setLimit("inmails_per_month", options.inmailsPerMonth);
28712
- setLimit("profile_views_per_day", options.profileViewsPerDay);
28713
- setLimit("profile_lookups_per_day", options.profileLookupsPerDay);
28714
- setLimit("endorsements_per_day", options.endorsementsPerDay);
28715
- setLimit("withdrawals_per_day", options.withdrawalsPerDay);
28716
- setLimit("posts_per_day", options.postsPerDay);
28717
- setLimit("follows_per_day", options.followsPerDay);
28718
- setLimit("likes_per_day", options.likesPerDay);
28719
- setLimit("comments_per_day", options.commentsPerDay);
28720
- setLimit("total_actions_per_day", options.totalActionsPerDay);
28721
- setLimit("relations_reads_per_day", options.relationsReadsPerDay);
28722
- setLimit("messages_reads_per_day", options.messagesReadsPerDay);
28723
- setLimit("searches_per_day", options.searchesPerDay);
28724
- setLimit("sales_nav_search_results_per_day", options.salesNavSearchResultsPerDay);
28725
- setLimit("api_reads_per_day", options.apiReadsPerDay);
28726
- setLimit("total_reads_per_day", options.totalReadsPerDay);
28727
- setLimit("min_action_spacing_seconds", options.minSpacingSeconds);
28728
- setLimit("action_spacing_jitter_seconds", options.spacingJitterSeconds);
28729
- setLimit("interactive_min_spacing_seconds", options.interactiveMinSpacingSeconds);
28730
- setLimit("interactive_spacing_jitter_seconds", options.interactiveSpacingJitterSeconds);
28731
- setLimit("interactive_sends_per_hour", options.interactiveSendsPerHour);
28732
- // Working hours: only the given fields are sent; the server merges them into
28733
- // the stored window and validates the result (invalid_working_hours).
28734
- const workingHours = {};
28735
- const timezone = readOption(options.timezone);
28736
- if (timezone)
28737
- workingHours.timezone = timezone;
28738
- const workingDays = readOption(options.workingDays);
28739
- if (workingDays)
28740
- workingHours.days = parseWorkingDaysOption(workingDays);
28741
- const workingHoursStart = readOption(options.workingHoursStart);
28742
- if (workingHoursStart)
28743
- workingHours.start = workingHoursStart;
28744
- const workingHoursEnd = readOption(options.workingHoursEnd);
28745
- if (workingHoursEnd)
28746
- workingHours.end = workingHoursEnd;
28747
- // Warm-up override (LinkedIn only). A start-date wins over the booleans; the
28748
- // WhatsApp `set` command doesn't expose these flags, so this stays empty there.
28749
- let warmup;
28750
- const warmupStartDate = readOption(options.warmupStartDate);
28751
- if (warmupStartDate) {
28752
- warmup = { start_date: warmupStartDate };
28753
- }
28754
- else if (options.warmupDisable) {
28755
- warmup = { enabled: false };
28756
- }
28757
- else if (options.warmupRestart) {
28758
- warmup = { enabled: true };
28759
- }
28760
- // Opinionated-safety knobs (P3): the warm-up preset curve + randomised daily caps.
28761
- const warmupPreset = readOption(options.warmupPreset);
28762
- const hasPreset = warmupPreset !== null && warmupPreset !== undefined;
28763
- const hasRandomize = options.randomizeCaps !== undefined;
28764
- const hasWeekendFullVolume = options.weekendFullVolume !== undefined;
28765
- // The READ ramp is a separate axis from the send-side warm-up above: it is
28766
- // default-on, anchored on when network capture was armed, and governs read
28767
- // budgets only. Disabling one must never disable the other.
28768
- const readWarmup = options.readWarmupDisable
28769
- ? false
28770
- : options.readWarmupRestart
28771
- ? true
28772
- : undefined;
28773
- const hasLimits = Object.keys(limits).length > 0;
28774
- const hasWorkingHours = Object.keys(workingHours).length > 0;
28775
- const hasWarmup = warmup !== undefined;
28776
- const hasReadWarmup = readWarmup !== undefined;
28777
- if (!hasLimits && !hasWorkingHours && !hasWarmup && !hasPreset && !hasRandomize && !hasReadWarmup && !hasWeekendFullVolume) {
28778
- throw new OxygenError("invalid_request", "Pass at least one limit flag (e.g. --invites-per-day), a working-hours flag (--timezone, --working-days, --working-hours-start, --working-hours-end), a warm-up flag (--warmup-restart / --warmup-disable / --warmup-start-date), --read-warmup-disable / --read-warmup-restart, --warmup-preset, --randomize-caps, or --weekend-full-volume.", { exitCode: 1 });
28779
- }
28780
- return {
28781
- ...(hasLimits ? { limits } : {}),
28782
- ...(hasWorkingHours ? { working_hours: workingHours } : {}),
28783
- ...(hasWarmup ? { warmup } : {}),
28784
- ...(hasPreset ? { warmup_preset: warmupPreset } : {}),
28785
- ...(hasRandomize ? { randomize_daily_caps: options.randomizeCaps } : {}),
28786
- ...(hasWeekendFullVolume ? { weekend_full_volume: options.weekendFullVolume } : {}),
28787
- ...(hasReadWarmup ? { read_warmup: readWarmup } : {}),
28788
- };
28789
- }
28790
- const WORKING_DAY_NAMES = {
28791
- mon: 1, monday: 1, tue: 2, tues: 2, tuesday: 2, wed: 3, wednesday: 3, thu: 4, thur: 4, thurs: 4, thursday: 4,
28792
- fri: 5, friday: 5, sat: 6, saturday: 6, sun: 7, sunday: 7,
28793
- };
28794
- /**
28795
- * `--working-days` → ISO weekday numbers. Accepts day names or 1-7, comma
28796
- * separated, and ranges (`mon-fri`, `1-5`). A token it cannot read is sent as
28797
- * given so the server's invalid_working_hours error names it.
28798
- */
28799
28683
  // The four --linkedin-* flags write settings.schedule, the per-sequence window
28800
28684
  // the LinkedIn/WhatsApp dispatcher enforces on top of each sender's working
28801
28685
  // hours. The server stores the window whole, so the four flags go together.
@@ -28811,31 +28695,6 @@ function readLinkedinScheduleOptions(options) {
28811
28695
  }
28812
28696
  return { timezone, days: parseWorkingDaysOption(days), start, end };
28813
28697
  }
28814
- function parseWorkingDaysOption(value) {
28815
- const day = (token) => {
28816
- const lower = token.trim().toLowerCase();
28817
- if (/^[1-7]$/.test(lower))
28818
- return Number(lower);
28819
- return WORKING_DAY_NAMES[lower] ?? null;
28820
- };
28821
- const days = [];
28822
- for (const token of value.split(",").map((part) => part.trim()).filter(Boolean)) {
28823
- const [from, to, ...rest] = token.split("-");
28824
- const start = from !== undefined ? day(from) : null;
28825
- const end = to !== undefined ? day(to) : null;
28826
- if (rest.length === 0 && start !== null && end !== null && start <= end) {
28827
- for (let current = start; current <= end; current += 1)
28828
- days.push(current);
28829
- }
28830
- else if (to === undefined && start !== null) {
28831
- days.push(start);
28832
- }
28833
- else {
28834
- days.push(token);
28835
- }
28836
- }
28837
- return days;
28838
- }
28839
28698
  /**
28840
28699
  * A whole seat count. Unlike readPositiveNumber, ZERO IS VALID and meaningful:
28841
28700
  * `--quantity 0` is how a customer gives back every seat of a channel, and
@@ -0,0 +1,14 @@
1
+ import { type ContractFlagOverlay } from "./contract-flags.js";
2
+ /**
3
+ * `--working-days` → ISO weekday numbers. Accepts day names or 1-7, comma
4
+ * separated, and ranges (`mon-fri`, `1-5`). A token it cannot read is sent as
5
+ * given so the server's invalid_working_hours error names it.
6
+ */
7
+ export declare function parseWorkingDaysOption(value: string): unknown[];
8
+ export declare const SENDER_LIMITS_FLAG_OVERLAY: ContractFlagOverlay;
9
+ /**
10
+ * The limits PATCH body for the flags given. A warm-up start date wins over
11
+ * --warmup-restart/--warmup-disable (the route reads it first anyway), so the
12
+ * body carries only the one it will act on. Throws when no flag was given.
13
+ */
14
+ export declare function buildSenderLimitsBody(options: Record<string, unknown>): Record<string, unknown>;