@oxygen-agent/cli 1.1010.721 → 1.1010.950
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/auto-update.d.ts +129 -0
- package/dist/auto-update.js +392 -0
- package/dist/command-manifest.js +14 -0
- package/dist/contract-flags.d.ts +42 -0
- package/dist/contract-flags.js +220 -0
- package/dist/credentials.d.ts +2 -0
- package/dist/credentials.js +6 -3
- package/dist/functions-commands.js +1 -1
- package/dist/http-client.js +28 -4
- package/dist/index.js +622 -325
- package/dist/run-wait.d.ts +3 -1
- package/dist/run-wait.js +19 -5
- package/dist/sender-limits-flags.d.ts +14 -0
- package/dist/sender-limits-flags.js +157 -0
- package/dist/streamed-file-import.d.ts +58 -0
- package/dist/streamed-file-import.js +115 -0
- package/dist/ugc-commands.js +16 -2
- package/dist/update.d.ts +29 -0
- package/dist/update.js +62 -16
- package/dist/workflow-plan-limit-notices.d.ts +8 -0
- package/dist/workflow-plan-limit-notices.js +28 -0
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +6 -6
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +4 -0
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +20 -8
- package/node_modules/@oxygen/cli-ugc/dist/index.d.ts +1 -0
- package/node_modules/@oxygen/cli-ugc/dist/index.js +1 -0
- package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/billing-anchors.js +27 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +191 -35
- package/node_modules/@oxygen/shared/dist/billing.js +333 -42
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +55 -5
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +11 -0
- package/node_modules/@oxygen/shared/dist/contracts/schema.d.ts +186 -0
- package/node_modules/@oxygen/shared/dist/contracts/schema.js +260 -0
- package/node_modules/@oxygen/shared/dist/contracts/sender-limits.d.ts +67 -0
- package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +88 -0
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
- package/node_modules/@oxygen/shared/dist/cost-estimate-view.d.ts +50 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +90 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +167 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate.js +361 -0
- package/node_modules/@oxygen/shared/dist/credit-gate.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/credit-gate.js +65 -0
- package/node_modules/@oxygen/shared/dist/email-deliverability-policy.d.ts +51 -0
- package/node_modules/@oxygen/shared/dist/email-deliverability-policy.js +101 -0
- package/node_modules/@oxygen/shared/dist/email-hard-bounce.d.ts +3 -1
- package/node_modules/@oxygen/shared/dist/email-hard-bounce.js +3 -3
- package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -1
- package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/feature-gates.js +5 -0
- package/node_modules/@oxygen/shared/dist/file-import.d.ts +13 -1
- package/node_modules/@oxygen/shared/dist/file-import.js +33 -6
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +73 -3
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +246 -24
- package/node_modules/@oxygen/shared/dist/import-limits.d.ts +25 -1
- package/node_modules/@oxygen/shared/dist/import-limits.js +35 -2
- package/node_modules/@oxygen/shared/dist/index.d.ts +2 -22
- package/node_modules/@oxygen/shared/dist/index.js +2 -42
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +41 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +17 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +41 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.js +55 -0
- package/node_modules/@oxygen/shared/dist/plan-band.d.ts +117 -1
- package/node_modules/@oxygen/shared/dist/plan-band.js +175 -10
- package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +77 -7
- package/node_modules/@oxygen/shared/dist/plan-capabilities.js +87 -7
- package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +219 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.js +330 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +204 -6
- package/node_modules/@oxygen/shared/dist/plan-limits.js +197 -15
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +80 -36
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +80 -31
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +38 -20
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +47 -34
- package/node_modules/@oxygen/shared/dist/product-briefing-rules.js +4 -2
- package/node_modules/@oxygen/shared/dist/provider-http-error.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/provider-http-error.js +27 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +127 -0
- package/node_modules/@oxygen/shared/dist/repricing.js +407 -6
- package/node_modules/@oxygen/shared/dist/semver.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/semver.js +41 -0
- package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +5 -7
- package/node_modules/@oxygen/shared/dist/sending-limits.js +10 -16
- package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +18 -15
- package/node_modules/@oxygen/shared/dist/sending-seats.js +22 -17
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +57 -8
- package/node_modules/@oxygen/shared/dist/spend-safety.js +64 -11
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +15 -7
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +18 -5
- package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +35 -5
- package/node_modules/@oxygen/shared/dist/table-capacity.js +40 -8
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +13 -5
- package/node_modules/@oxygen/shared/dist/telemetry-resource.d.ts +40 -0
- package/node_modules/@oxygen/shared/dist/telemetry-resource.js +35 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +5 -0
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/ugc.js +29 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -3
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +17 -27
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +5 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +3 -3
- package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +15 -1
- package/node_modules/@oxygen/workflows/dist/graph/types.js +15 -1
- package/node_modules/@oxygen/workflows/dist/index.d.ts +45 -0
- package/node_modules/@oxygen/workflows/dist/index.js +152 -2
- package/node_modules/@oxygen/workflows/dist/usage-estimate.d.ts +10 -1
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +33 -29
- package/package.json +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One entry per customer operation that edits a resource: the route it calls,
|
|
3
|
+
* the contract its body satisfies, and the names it carries on the CLI and MCP.
|
|
4
|
+
* The CLI command, the MCP tool and the route read the same spec, so the three
|
|
5
|
+
* cannot disagree on which fields exist. Leaf and browser-safe.
|
|
6
|
+
*/
|
|
7
|
+
import type { ContractObject } from "./schema.js";
|
|
8
|
+
export type OperationSpec<C extends ContractObject = ContractObject> = {
|
|
9
|
+
/** Stable id, `<resource>.<verb>`. */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Route template; `{name}` segments are path parameters. */
|
|
12
|
+
route: string;
|
|
13
|
+
method: "POST" | "PATCH";
|
|
14
|
+
/** When set, the contract describes `body[bodyKey]` rather than the whole body. */
|
|
15
|
+
bodyKey?: string;
|
|
16
|
+
contract: C;
|
|
17
|
+
cli: {
|
|
18
|
+
/** Command words after `oxygen`, e.g. `["senders", "limits", "set"]`. */
|
|
19
|
+
path: readonly string[];
|
|
20
|
+
summary: string;
|
|
21
|
+
/** The positional argument that fills the route's single path parameter. */
|
|
22
|
+
argument?: {
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
mcp: {
|
|
28
|
+
name: string;
|
|
29
|
+
title: string;
|
|
30
|
+
description: string;
|
|
31
|
+
/** The MCP toolset domain (pack) the tool registers under. */
|
|
32
|
+
pack: string;
|
|
33
|
+
};
|
|
34
|
+
mutates: boolean;
|
|
35
|
+
spends: boolean;
|
|
36
|
+
};
|
|
37
|
+
/** The route with its `{param}` segments filled in, each URL-encoded. */
|
|
38
|
+
export declare function operationPath(spec: Pick<OperationSpec, "route">, params: Readonly<Record<string, string>>): string;
|
|
39
|
+
export declare const OPERATION_SPECS: readonly OperationSpec[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { SENDER_LIMITS_SET_OPERATION } from "./sender-limits.js";
|
|
2
|
+
/** The route with its `{param}` segments filled in, each URL-encoded. */
|
|
3
|
+
export function operationPath(spec, params) {
|
|
4
|
+
return spec.route.replace(/\{(\w+)\}/g, (_match, name) => {
|
|
5
|
+
const value = params[name];
|
|
6
|
+
if (value === undefined)
|
|
7
|
+
throw new Error(`Missing path parameter ${name} for ${spec.route}`);
|
|
8
|
+
return encodeURIComponent(value);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export const OPERATION_SPECS = [SENDER_LIMITS_SET_OPERATION];
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One contract per editable resource, in the plain JSON-schema dialect MCP tool
|
|
3
|
+
* input schemas already use (founder decision 2026-09-28: no zod, no new
|
|
4
|
+
* dependency). The same object generates the CLI flags
|
|
5
|
+
* (`packages/cli/src/contract-flags.ts`), the MCP input schema
|
|
6
|
+
* (`packages/mcp-server/src/tools/contract-tool.ts`) and validates the API
|
|
7
|
+
* route body (`validateContract`), so a field cannot exist on one surface and
|
|
8
|
+
* not another.
|
|
9
|
+
*
|
|
10
|
+
* Leaf and browser-safe: no imports, no IO.
|
|
11
|
+
*/
|
|
12
|
+
export type StringProperty = {
|
|
13
|
+
type: "string";
|
|
14
|
+
description?: string;
|
|
15
|
+
enum?: readonly string[];
|
|
16
|
+
minLength?: number;
|
|
17
|
+
maxLength?: number;
|
|
18
|
+
};
|
|
19
|
+
export type NumberProperty = {
|
|
20
|
+
type: "number" | "integer";
|
|
21
|
+
description?: string;
|
|
22
|
+
minimum?: number;
|
|
23
|
+
maximum?: number;
|
|
24
|
+
exclusiveMinimum?: number;
|
|
25
|
+
/**
|
|
26
|
+
* The route clamps a value above `maximum` down to it instead of refusing
|
|
27
|
+
* it, when `validateContract` runs with `overMax: "clamp"`. Annotation only:
|
|
28
|
+
* `contractToolProperties` strips it from the MCP schema.
|
|
29
|
+
*/
|
|
30
|
+
"x-clamp"?: true;
|
|
31
|
+
};
|
|
32
|
+
export type BooleanProperty = {
|
|
33
|
+
type: "boolean";
|
|
34
|
+
description?: string;
|
|
35
|
+
};
|
|
36
|
+
export type NullProperty = {
|
|
37
|
+
type: "null";
|
|
38
|
+
};
|
|
39
|
+
export interface ArrayProperty<I extends JsonProperty = JsonProperty> {
|
|
40
|
+
type: "array";
|
|
41
|
+
description?: string;
|
|
42
|
+
items: I;
|
|
43
|
+
minItems?: number;
|
|
44
|
+
maxItems?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface ObjectProperty<P extends Record<string, JsonProperty> = Record<string, JsonProperty>> {
|
|
47
|
+
type: "object";
|
|
48
|
+
description?: string;
|
|
49
|
+
additionalProperties?: boolean | JsonProperty;
|
|
50
|
+
properties?: P;
|
|
51
|
+
required?: readonly string[];
|
|
52
|
+
}
|
|
53
|
+
export interface OneOfProperty {
|
|
54
|
+
oneOf: JsonProperty[];
|
|
55
|
+
description?: string;
|
|
56
|
+
}
|
|
57
|
+
export type JsonProperty = {
|
|
58
|
+
$ref: string;
|
|
59
|
+
} | StringProperty | NumberProperty | BooleanProperty | NullProperty | ObjectProperty | ArrayProperty | OneOfProperty;
|
|
60
|
+
/** The top level of a contract: a closed object. */
|
|
61
|
+
export type ContractObject<P extends Record<string, JsonProperty> = Record<string, JsonProperty>, R extends string = string> = {
|
|
62
|
+
type: "object";
|
|
63
|
+
description?: string;
|
|
64
|
+
additionalProperties: false;
|
|
65
|
+
properties: P;
|
|
66
|
+
required?: readonly R[];
|
|
67
|
+
};
|
|
68
|
+
type Described = {
|
|
69
|
+
description?: string;
|
|
70
|
+
};
|
|
71
|
+
type NumberOptions = Described & {
|
|
72
|
+
minimum?: number;
|
|
73
|
+
maximum?: number;
|
|
74
|
+
clamp?: boolean;
|
|
75
|
+
};
|
|
76
|
+
export declare function int(options?: NumberOptions): {
|
|
77
|
+
type: "integer";
|
|
78
|
+
description?: string;
|
|
79
|
+
minimum?: number;
|
|
80
|
+
maximum?: number;
|
|
81
|
+
"x-clamp"?: true;
|
|
82
|
+
};
|
|
83
|
+
export declare function num(options?: NumberOptions): {
|
|
84
|
+
type: "number";
|
|
85
|
+
description?: string;
|
|
86
|
+
minimum?: number;
|
|
87
|
+
maximum?: number;
|
|
88
|
+
"x-clamp"?: true;
|
|
89
|
+
};
|
|
90
|
+
export declare function bool(options?: Described): BooleanProperty;
|
|
91
|
+
export declare function str(options?: Described & {
|
|
92
|
+
minLength?: number;
|
|
93
|
+
maxLength?: number;
|
|
94
|
+
}): StringProperty;
|
|
95
|
+
export declare function enumOf<const V extends string>(values: readonly V[], options?: Described): {
|
|
96
|
+
type: "string";
|
|
97
|
+
enum: readonly V[];
|
|
98
|
+
description?: string;
|
|
99
|
+
};
|
|
100
|
+
export declare function arr<const I extends JsonProperty>(items: I, options?: Described & {
|
|
101
|
+
minItems?: number;
|
|
102
|
+
maxItems?: number;
|
|
103
|
+
}): ArrayProperty<I>;
|
|
104
|
+
export declare function obj<const P extends Record<string, JsonProperty>, const R extends keyof P & string = never>(properties: P, options?: Described & {
|
|
105
|
+
required?: readonly R[];
|
|
106
|
+
}): ContractObject<P, R>;
|
|
107
|
+
/** `null` is accepted and passed through; it means "clear this field". */
|
|
108
|
+
export declare function nullable<const P extends JsonProperty>(property: P): {
|
|
109
|
+
oneOf: [P, NullProperty];
|
|
110
|
+
description?: string;
|
|
111
|
+
};
|
|
112
|
+
type Simplify<T> = {
|
|
113
|
+
[K in keyof T]: T[K];
|
|
114
|
+
} & {};
|
|
115
|
+
type InferObject<M, R> = Simplify<{
|
|
116
|
+
[K in keyof M as K extends R ? K : never]: Infer<M[K]>;
|
|
117
|
+
} & {
|
|
118
|
+
[K in keyof M as K extends R ? never : K]?: Infer<M[K]>;
|
|
119
|
+
}>;
|
|
120
|
+
/** The TypeScript type of a value that passes `validateContract`. Fields not in `required` are optional. */
|
|
121
|
+
export type Infer<P> = P extends {
|
|
122
|
+
oneOf: readonly [infer A, NullProperty];
|
|
123
|
+
} ? Infer<A> | null : P extends {
|
|
124
|
+
type: "integer" | "number";
|
|
125
|
+
} ? number : P extends {
|
|
126
|
+
type: "boolean";
|
|
127
|
+
} ? boolean : P extends {
|
|
128
|
+
type: "string";
|
|
129
|
+
enum: readonly (infer E)[];
|
|
130
|
+
} ? E : P extends {
|
|
131
|
+
type: "string";
|
|
132
|
+
} ? string : P extends {
|
|
133
|
+
type: "array";
|
|
134
|
+
items: infer I;
|
|
135
|
+
} ? Infer<I>[] : P extends {
|
|
136
|
+
type: "object";
|
|
137
|
+
properties: infer M;
|
|
138
|
+
} ? InferObject<M, P extends {
|
|
139
|
+
required?: readonly (infer R)[];
|
|
140
|
+
} ? R : never> : unknown;
|
|
141
|
+
/** A property's non-null branch, and whether null is allowed. */
|
|
142
|
+
export declare function unwrapNullable(property: JsonProperty): {
|
|
143
|
+
property: JsonProperty;
|
|
144
|
+
nullable: boolean;
|
|
145
|
+
};
|
|
146
|
+
export type ContractLeaf = {
|
|
147
|
+
/** Dotted path, e.g. `limits.invites_per_day`. */
|
|
148
|
+
key: string;
|
|
149
|
+
path: readonly string[];
|
|
150
|
+
/** The leaf's non-null schema. */
|
|
151
|
+
property: JsonProperty;
|
|
152
|
+
nullable: boolean;
|
|
153
|
+
};
|
|
154
|
+
/** Every non-object field of a contract, depth first in declaration order. */
|
|
155
|
+
export declare function contractLeafPaths(contract: ContractObject | ObjectProperty, prefix?: readonly string[]): ContractLeaf[];
|
|
156
|
+
export type ContractIssue = {
|
|
157
|
+
path: string;
|
|
158
|
+
message: string;
|
|
159
|
+
};
|
|
160
|
+
export type ValidateContractOptions = {
|
|
161
|
+
/**
|
|
162
|
+
* `warn`: an unknown key is listed in `unknownKeys` AND kept in `value`, so a
|
|
163
|
+
* route that still reads it behaves exactly as before the contract existed.
|
|
164
|
+
* `reject`: an unknown key is an issue.
|
|
165
|
+
*/
|
|
166
|
+
unknown: "warn" | "reject";
|
|
167
|
+
/** `clamp` lowers an over-maximum value to the maximum, only where the field carries `x-clamp`. */
|
|
168
|
+
overMax: "clamp" | "reject";
|
|
169
|
+
};
|
|
170
|
+
export type ContractValidation<T> = {
|
|
171
|
+
ok: true;
|
|
172
|
+
value: T;
|
|
173
|
+
unknownKeys: string[];
|
|
174
|
+
clamped: string[];
|
|
175
|
+
} | {
|
|
176
|
+
ok: false;
|
|
177
|
+
issues: ContractIssue[];
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Check a request body against its contract. Absent (`undefined`) fields are
|
|
181
|
+
* skipped, `null` passes only where the field is `nullable`, and every issue is
|
|
182
|
+
* collected rather than stopping at the first. On success `value` is a copy with
|
|
183
|
+
* clamps applied (see the options for what happens to unknown keys).
|
|
184
|
+
*/
|
|
185
|
+
export declare function validateContract<C extends ContractObject>(contract: C, value: unknown, options: ValidateContractOptions): ContractValidation<Infer<C>>;
|
|
186
|
+
export {};
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One contract per editable resource, in the plain JSON-schema dialect MCP tool
|
|
3
|
+
* input schemas already use (founder decision 2026-09-28: no zod, no new
|
|
4
|
+
* dependency). The same object generates the CLI flags
|
|
5
|
+
* (`packages/cli/src/contract-flags.ts`), the MCP input schema
|
|
6
|
+
* (`packages/mcp-server/src/tools/contract-tool.ts`) and validates the API
|
|
7
|
+
* route body (`validateContract`), so a field cannot exist on one surface and
|
|
8
|
+
* not another.
|
|
9
|
+
*
|
|
10
|
+
* Leaf and browser-safe: no imports, no IO.
|
|
11
|
+
*/
|
|
12
|
+
function described(shape, description) {
|
|
13
|
+
return description === undefined ? shape : { ...shape, description };
|
|
14
|
+
}
|
|
15
|
+
function numeric(type, options = {}) {
|
|
16
|
+
const { description, minimum, maximum, clamp } = options;
|
|
17
|
+
return {
|
|
18
|
+
type,
|
|
19
|
+
...(minimum !== undefined ? { minimum } : {}),
|
|
20
|
+
...(maximum !== undefined ? { maximum } : {}),
|
|
21
|
+
...(clamp ? { "x-clamp": true } : {}),
|
|
22
|
+
...(description !== undefined ? { description } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function int(options) {
|
|
26
|
+
return numeric("integer", options);
|
|
27
|
+
}
|
|
28
|
+
export function num(options) {
|
|
29
|
+
return numeric("number", options);
|
|
30
|
+
}
|
|
31
|
+
export function bool(options = {}) {
|
|
32
|
+
return described({ type: "boolean" }, options.description);
|
|
33
|
+
}
|
|
34
|
+
export function str(options = {}) {
|
|
35
|
+
const { description, ...rest } = options;
|
|
36
|
+
return described({ type: "string", ...rest }, description);
|
|
37
|
+
}
|
|
38
|
+
export function enumOf(values, options = {}) {
|
|
39
|
+
return described({ type: "string", enum: values }, options.description);
|
|
40
|
+
}
|
|
41
|
+
export function arr(items, options = {}) {
|
|
42
|
+
const { description, ...rest } = options;
|
|
43
|
+
return described({ type: "array", items, ...rest }, description);
|
|
44
|
+
}
|
|
45
|
+
export function obj(properties, options = {}) {
|
|
46
|
+
const { description, required } = options;
|
|
47
|
+
return {
|
|
48
|
+
type: "object",
|
|
49
|
+
...(description !== undefined ? { description } : {}),
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
properties,
|
|
52
|
+
...(required && required.length > 0 ? { required } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** `null` is accepted and passed through; it means "clear this field". */
|
|
56
|
+
export function nullable(property) {
|
|
57
|
+
const { description, ...inner } = property;
|
|
58
|
+
return {
|
|
59
|
+
oneOf: [inner, { type: "null" }],
|
|
60
|
+
...(description !== undefined ? { description } : {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** A property's non-null branch, and whether null is allowed. */
|
|
64
|
+
export function unwrapNullable(property) {
|
|
65
|
+
if ("oneOf" in property && property.oneOf.length === 2) {
|
|
66
|
+
const nonNull = property.oneOf.filter((branch) => !("type" in branch && branch.type === "null"));
|
|
67
|
+
if (nonNull.length === 1) {
|
|
68
|
+
const inner = nonNull[0];
|
|
69
|
+
return {
|
|
70
|
+
property: property.description !== undefined && !("$ref" in inner)
|
|
71
|
+
? { ...inner, description: property.description }
|
|
72
|
+
: inner,
|
|
73
|
+
nullable: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { property, nullable: false };
|
|
78
|
+
}
|
|
79
|
+
/** Every non-object field of a contract, depth first in declaration order. */
|
|
80
|
+
export function contractLeafPaths(contract, prefix = []) {
|
|
81
|
+
const leaves = [];
|
|
82
|
+
for (const [name, raw] of Object.entries(contract.properties ?? {})) {
|
|
83
|
+
const path = [...prefix, name];
|
|
84
|
+
const { property, nullable } = unwrapNullable(raw);
|
|
85
|
+
if ("type" in property && property.type === "object" && property.properties) {
|
|
86
|
+
leaves.push(...contractLeafPaths(property, path));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
leaves.push({ key: path.join("."), path, property, nullable });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return leaves;
|
|
93
|
+
}
|
|
94
|
+
function kindOf(value) {
|
|
95
|
+
if (value === null)
|
|
96
|
+
return "null";
|
|
97
|
+
if (Array.isArray(value))
|
|
98
|
+
return "array";
|
|
99
|
+
return typeof value;
|
|
100
|
+
}
|
|
101
|
+
function isRecord(value) {
|
|
102
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
103
|
+
}
|
|
104
|
+
const TYPE_NOUN = {
|
|
105
|
+
integer: "a whole number",
|
|
106
|
+
number: "a number",
|
|
107
|
+
string: "a string",
|
|
108
|
+
boolean: "true or false",
|
|
109
|
+
array: "a list",
|
|
110
|
+
object: "an object",
|
|
111
|
+
};
|
|
112
|
+
function childPath(parent, key) {
|
|
113
|
+
return parent ? `${parent}.${key}` : key;
|
|
114
|
+
}
|
|
115
|
+
function walkObject(schema, value, path, walk) {
|
|
116
|
+
const out = {};
|
|
117
|
+
const properties = schema.properties ?? {};
|
|
118
|
+
for (const key of schema.required ?? []) {
|
|
119
|
+
if (value[key] === undefined)
|
|
120
|
+
walk.issues.push({ path: childPath(path, key), message: `${childPath(path, key)} is required.` });
|
|
121
|
+
}
|
|
122
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
123
|
+
if (entry === undefined)
|
|
124
|
+
continue;
|
|
125
|
+
const at = childPath(path, key);
|
|
126
|
+
const property = properties[key];
|
|
127
|
+
if (property !== undefined) {
|
|
128
|
+
out[key] = walkValue(property, entry, at, walk);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const extra = schema.additionalProperties;
|
|
132
|
+
if (extra === false) {
|
|
133
|
+
if (walk.options.unknown === "reject") {
|
|
134
|
+
walk.issues.push({ path: at, message: `${at} is not a recognised field.` });
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
walk.unknownKeys.push(at);
|
|
138
|
+
out[key] = entry;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else if (extra && typeof extra === "object") {
|
|
142
|
+
out[key] = walkValue(extra, entry, at, walk);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
out[key] = entry;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
function walkValue(schema, value, path, walk) {
|
|
151
|
+
if ("$ref" in schema)
|
|
152
|
+
return value;
|
|
153
|
+
if ("oneOf" in schema) {
|
|
154
|
+
const { property, nullable } = unwrapNullable(schema);
|
|
155
|
+
if (value === null) {
|
|
156
|
+
if (nullable)
|
|
157
|
+
return null;
|
|
158
|
+
walk.issues.push({ path, message: `${path} must not be null.` });
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
if (nullable)
|
|
162
|
+
return walkValue(property, value, path, walk);
|
|
163
|
+
for (const branch of schema.oneOf) {
|
|
164
|
+
const trial = { options: walk.options, issues: [], unknownKeys: [], clamped: [] };
|
|
165
|
+
const result = walkValue(branch, value, path, trial);
|
|
166
|
+
if (trial.issues.length === 0) {
|
|
167
|
+
walk.unknownKeys.push(...trial.unknownKeys);
|
|
168
|
+
walk.clamped.push(...trial.clamped);
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
walk.issues.push({ path, message: `${path} does not match any accepted shape.` });
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
if (schema.type === "null") {
|
|
176
|
+
if (value !== null)
|
|
177
|
+
walk.issues.push({ path, message: `${path} must be null.` });
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
if (value === null) {
|
|
181
|
+
walk.issues.push({ path, message: `${path} must not be null.` });
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
const mismatch = () => {
|
|
185
|
+
walk.issues.push({ path, message: `${path} must be ${TYPE_NOUN[schema.type]}, not ${kindOf(value)}.` });
|
|
186
|
+
return value;
|
|
187
|
+
};
|
|
188
|
+
switch (schema.type) {
|
|
189
|
+
case "boolean":
|
|
190
|
+
return typeof value === "boolean" ? value : mismatch();
|
|
191
|
+
case "string": {
|
|
192
|
+
if (typeof value !== "string")
|
|
193
|
+
return mismatch();
|
|
194
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
195
|
+
walk.issues.push({ path, message: `${path} must be one of: ${schema.enum.join(", ")}.` });
|
|
196
|
+
}
|
|
197
|
+
else if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
198
|
+
walk.issues.push({ path, message: `${path} must be at least ${schema.minLength} characters.` });
|
|
199
|
+
}
|
|
200
|
+
else if (schema.maxLength !== undefined && value.length > schema.maxLength) {
|
|
201
|
+
walk.issues.push({ path, message: `${path} must be at most ${schema.maxLength} characters.` });
|
|
202
|
+
}
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
case "integer":
|
|
206
|
+
case "number": {
|
|
207
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
208
|
+
return mismatch();
|
|
209
|
+
if (schema.type === "integer" && !Number.isInteger(value))
|
|
210
|
+
return mismatch();
|
|
211
|
+
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
212
|
+
walk.issues.push({ path, message: `${path} must be at least ${schema.minimum}.` });
|
|
213
|
+
return value;
|
|
214
|
+
}
|
|
215
|
+
if (schema.exclusiveMinimum !== undefined && value <= schema.exclusiveMinimum) {
|
|
216
|
+
walk.issues.push({ path, message: `${path} must be greater than ${schema.exclusiveMinimum}.` });
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
220
|
+
if (schema["x-clamp"] && walk.options.overMax === "clamp") {
|
|
221
|
+
walk.clamped.push(path);
|
|
222
|
+
return schema.maximum;
|
|
223
|
+
}
|
|
224
|
+
walk.issues.push({ path, message: `${path} must be at most ${schema.maximum}.` });
|
|
225
|
+
}
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
228
|
+
case "array": {
|
|
229
|
+
if (!Array.isArray(value))
|
|
230
|
+
return mismatch();
|
|
231
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
232
|
+
walk.issues.push({ path, message: `${path} must list at least ${schema.minItems}.` });
|
|
233
|
+
}
|
|
234
|
+
else if (schema.maxItems !== undefined && value.length > schema.maxItems) {
|
|
235
|
+
walk.issues.push({ path, message: `${path} must list at most ${schema.maxItems}.` });
|
|
236
|
+
}
|
|
237
|
+
return value.map((item, index) => walkValue(schema.items, item, `${path}[${index}]`, walk));
|
|
238
|
+
}
|
|
239
|
+
case "object":
|
|
240
|
+
return isRecord(value) ? walkObject(schema, value, path, walk) : mismatch();
|
|
241
|
+
default:
|
|
242
|
+
return value;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Check a request body against its contract. Absent (`undefined`) fields are
|
|
247
|
+
* skipped, `null` passes only where the field is `nullable`, and every issue is
|
|
248
|
+
* collected rather than stopping at the first. On success `value` is a copy with
|
|
249
|
+
* clamps applied (see the options for what happens to unknown keys).
|
|
250
|
+
*/
|
|
251
|
+
export function validateContract(contract, value, options) {
|
|
252
|
+
const walk = { options, issues: [], unknownKeys: [], clamped: [] };
|
|
253
|
+
if (!isRecord(value)) {
|
|
254
|
+
return { ok: false, issues: [{ path: "", message: "The request body must be a JSON object." }] };
|
|
255
|
+
}
|
|
256
|
+
const out = walkObject(contract, value, "", walk);
|
|
257
|
+
if (walk.issues.length > 0)
|
|
258
|
+
return { ok: false, issues: walk.issues };
|
|
259
|
+
return { ok: true, value: out, unknownKeys: walk.unknownKeys, clamped: walk.clamped };
|
|
260
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { OperationSpec } from "./operations.js";
|
|
2
|
+
import { type Infer } from "./schema.js";
|
|
3
|
+
export declare const SENDER_LIMITS_CONTRACT: import("./schema.js").ContractObject<Record<"action_spacing_jitter_seconds" | "api_ingest_reads_per_day" | "api_reads_per_day" | "comments_per_day" | "endorsements_per_day" | "follows_per_day" | "inmails_per_day" | "inmails_per_month" | "interactive_min_spacing_seconds" | "interactive_sends_per_hour" | "interactive_spacing_jitter_seconds" | "invites_per_day" | "invites_per_week" | "likes_per_day" | "messages_ingest_reads_per_day" | "messages_per_day" | "messages_reads_per_day" | "min_action_spacing_seconds" | "network_backfill_reads_per_day" | "network_delta_reads_per_day" | "posts_per_day" | "profile_lookups_per_day" | "profile_views_per_day" | "relations_ingest_reads_per_day" | "relations_reads_per_day" | "sales_nav_search_results_per_day" | "searches_per_day" | "total_actions_per_day" | "total_reads_per_day" | "withdrawals_per_day", {
|
|
4
|
+
type: "integer";
|
|
5
|
+
description?: string;
|
|
6
|
+
minimum?: number;
|
|
7
|
+
maximum?: number;
|
|
8
|
+
"x-clamp"?: true;
|
|
9
|
+
}>, never>;
|
|
10
|
+
export declare const SENDER_WORKING_HOURS_CONTRACT: import("./schema.js").ContractObject<{
|
|
11
|
+
readonly timezone: import("./schema.js").StringProperty;
|
|
12
|
+
readonly days: import("./schema.js").ArrayProperty<{
|
|
13
|
+
type: "integer";
|
|
14
|
+
description?: string;
|
|
15
|
+
minimum?: number;
|
|
16
|
+
maximum?: number;
|
|
17
|
+
"x-clamp"?: true;
|
|
18
|
+
}>;
|
|
19
|
+
readonly start: import("./schema.js").StringProperty;
|
|
20
|
+
readonly end: import("./schema.js").StringProperty;
|
|
21
|
+
}, never>;
|
|
22
|
+
export declare const SENDER_WARMUP_CONTRACT: import("./schema.js").ContractObject<{
|
|
23
|
+
readonly enabled: import("./schema.js").BooleanProperty;
|
|
24
|
+
readonly start_date: {
|
|
25
|
+
oneOf: [import("./schema.js").StringProperty, import("./schema.js").NullProperty];
|
|
26
|
+
description?: string;
|
|
27
|
+
};
|
|
28
|
+
}, never>;
|
|
29
|
+
export declare const LINKEDIN_WARMUP_PRESETS: readonly ["conservative", "standard", "fast"];
|
|
30
|
+
export declare const SENDER_LIMITS_PATCH_CONTRACT: import("./schema.js").ContractObject<{
|
|
31
|
+
readonly limits: import("./schema.js").ContractObject<Record<"action_spacing_jitter_seconds" | "api_ingest_reads_per_day" | "api_reads_per_day" | "comments_per_day" | "endorsements_per_day" | "follows_per_day" | "inmails_per_day" | "inmails_per_month" | "interactive_min_spacing_seconds" | "interactive_sends_per_hour" | "interactive_spacing_jitter_seconds" | "invites_per_day" | "invites_per_week" | "likes_per_day" | "messages_ingest_reads_per_day" | "messages_per_day" | "messages_reads_per_day" | "min_action_spacing_seconds" | "network_backfill_reads_per_day" | "network_delta_reads_per_day" | "posts_per_day" | "profile_lookups_per_day" | "profile_views_per_day" | "relations_ingest_reads_per_day" | "relations_reads_per_day" | "sales_nav_search_results_per_day" | "searches_per_day" | "total_actions_per_day" | "total_reads_per_day" | "withdrawals_per_day", {
|
|
32
|
+
type: "integer";
|
|
33
|
+
description?: string;
|
|
34
|
+
minimum?: number;
|
|
35
|
+
maximum?: number;
|
|
36
|
+
"x-clamp"?: true;
|
|
37
|
+
}>, never>;
|
|
38
|
+
readonly working_hours: import("./schema.js").ContractObject<{
|
|
39
|
+
readonly timezone: import("./schema.js").StringProperty;
|
|
40
|
+
readonly days: import("./schema.js").ArrayProperty<{
|
|
41
|
+
type: "integer";
|
|
42
|
+
description?: string;
|
|
43
|
+
minimum?: number;
|
|
44
|
+
maximum?: number;
|
|
45
|
+
"x-clamp"?: true;
|
|
46
|
+
}>;
|
|
47
|
+
readonly start: import("./schema.js").StringProperty;
|
|
48
|
+
readonly end: import("./schema.js").StringProperty;
|
|
49
|
+
}, never>;
|
|
50
|
+
readonly warmup: import("./schema.js").ContractObject<{
|
|
51
|
+
readonly enabled: import("./schema.js").BooleanProperty;
|
|
52
|
+
readonly start_date: {
|
|
53
|
+
oneOf: [import("./schema.js").StringProperty, import("./schema.js").NullProperty];
|
|
54
|
+
description?: string;
|
|
55
|
+
};
|
|
56
|
+
}, never>;
|
|
57
|
+
readonly read_warmup: import("./schema.js").BooleanProperty;
|
|
58
|
+
readonly warmup_preset: {
|
|
59
|
+
type: "string";
|
|
60
|
+
enum: readonly ("conservative" | "fast" | "standard")[];
|
|
61
|
+
description?: string;
|
|
62
|
+
};
|
|
63
|
+
readonly randomize_daily_caps: import("./schema.js").BooleanProperty;
|
|
64
|
+
readonly weekend_full_volume: import("./schema.js").BooleanProperty;
|
|
65
|
+
}, never>;
|
|
66
|
+
export type SenderLimitsPatch = Infer<typeof SENDER_LIMITS_PATCH_CONTRACT>;
|
|
67
|
+
export declare const SENDER_LIMITS_SET_OPERATION: OperationSpec<typeof SENDER_LIMITS_PATCH_CONTRACT>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PATCH /api/cli/senders/{id}/limits`: per-account LinkedIn caps, working
|
|
3
|
+
* hours, the warm-up ramp and the account's safety knobs. Mirrors exactly what
|
|
4
|
+
* the route accepts; the route's own validators (working-hours rules, warm-up
|
|
5
|
+
* date parsing) still run after the contract.
|
|
6
|
+
*/
|
|
7
|
+
import { LINKEDIN_SENDER_LIMIT_DEFAULTS, LINKEDIN_SENDER_LIMIT_KEYS, LINKEDIN_SENDER_LIMIT_MAXIMUMS, } from "../linkedin-sequences.js";
|
|
8
|
+
import { arr, bool, enumOf, int, nullable, obj, str } from "./schema.js";
|
|
9
|
+
/** Short MCP-facing notes; the CLI carries its own longer help per flag. */
|
|
10
|
+
const LIMIT_NOTES = {
|
|
11
|
+
inmails_per_month: "Rolling 30 days.",
|
|
12
|
+
profile_lookups_per_day: "Invisible profile/company lookups.",
|
|
13
|
+
withdrawals_per_day: "Sent-invite withdrawals.",
|
|
14
|
+
posts_per_day: "Own-feed posts.",
|
|
15
|
+
total_actions_per_day: "All sends except posts.",
|
|
16
|
+
relations_reads_per_day: "Connection-list reads.",
|
|
17
|
+
messages_reads_per_day: "Message-history reads.",
|
|
18
|
+
sales_nav_search_results_per_day: "Result rows.",
|
|
19
|
+
api_reads_per_day: "Other API reads.",
|
|
20
|
+
total_reads_per_day: "All reads.",
|
|
21
|
+
interactive_min_spacing_seconds: "Human Unibox send spacing.",
|
|
22
|
+
interactive_spacing_jitter_seconds: "Jitter on that spacing.",
|
|
23
|
+
interactive_sends_per_hour: "Hourly human send cap.",
|
|
24
|
+
relations_ingest_reads_per_day: "Background connection/follower list reads.",
|
|
25
|
+
api_ingest_reads_per_day: "Background engagement and post-metric reads.",
|
|
26
|
+
messages_ingest_reads_per_day: "Background message-history backfill.",
|
|
27
|
+
network_backfill_reads_per_day: "Network capture, first walk.",
|
|
28
|
+
network_delta_reads_per_day: "Network capture, once walked.",
|
|
29
|
+
};
|
|
30
|
+
function limit(key) {
|
|
31
|
+
const max = LINKEDIN_SENDER_LIMIT_MAXIMUMS[key];
|
|
32
|
+
const note = LIMIT_NOTES[key];
|
|
33
|
+
return int({
|
|
34
|
+
minimum: 0,
|
|
35
|
+
maximum: max,
|
|
36
|
+
clamp: true,
|
|
37
|
+
description: `Max ${max} (default ${LINKEDIN_SENDER_LIMIT_DEFAULTS[key]}).${note ? ` ${note}` : ""}`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export const SENDER_LIMITS_CONTRACT = obj(Object.fromEntries(LINKEDIN_SENDER_LIMIT_KEYS.map((key) => [key, limit(key)])), { description: "Per-account caps for sends and reads. Values above the maximum are clamped server-side." });
|
|
41
|
+
// Merged into the stored window server-side, so a partial edit keeps the rest.
|
|
42
|
+
export const SENDER_WORKING_HOURS_CONTRACT = obj({
|
|
43
|
+
timezone: str({ description: "IANA zone for hours and daily reset, e.g. Europe/Berlin." }),
|
|
44
|
+
days: arr(int({ minimum: 1, maximum: 7 }), { description: "ISO weekdays, 1=Mon." }),
|
|
45
|
+
start: str({ description: "HH:MM, e.g. 09:00." }),
|
|
46
|
+
end: str({ description: "HH:MM, after start." }),
|
|
47
|
+
}, { description: "When LinkedIn actions may run (default 07:00-22:00 daily); work outside waits. Omitted fields keep their value." });
|
|
48
|
+
export const SENDER_WARMUP_CONTRACT = obj({
|
|
49
|
+
enabled: bool({ description: "true = start/restart the ramp at day 0; false = disable warm-up (account becomes warm)." }),
|
|
50
|
+
start_date: nullable(str({
|
|
51
|
+
description: "ISO date (e.g. 2026-01-31) to anchor the ramp start. A past date credits prior warming and advances the ramp day; a future date holds at day 0 until it arrives. null disables warm-up.",
|
|
52
|
+
})),
|
|
53
|
+
}, {
|
|
54
|
+
description: "Warm-up ramp override for this account. Warm-up gradually raises invite + message caps to full over ~2 weeks and is auto-enabled at first connect (pre-existing accounts are treated as already warm). Use { enabled: true } to start/restart the ramp now, { enabled: false } to disable it (account uses its full configured caps), or { start_date } to anchor the ramp to a specific day.",
|
|
55
|
+
});
|
|
56
|
+
export const LINKEDIN_WARMUP_PRESETS = ["conservative", "standard", "fast"];
|
|
57
|
+
export const SENDER_LIMITS_PATCH_CONTRACT = obj({
|
|
58
|
+
limits: SENDER_LIMITS_CONTRACT,
|
|
59
|
+
working_hours: SENDER_WORKING_HOURS_CONTRACT,
|
|
60
|
+
warmup: SENDER_WARMUP_CONTRACT,
|
|
61
|
+
read_warmup: bool({ description: "Network-capture read ramp: false runs full read budget now; true restarts it from today." }),
|
|
62
|
+
warmup_preset: enumOf(LINKEDIN_WARMUP_PRESETS, { description: "Warm-up ramp: ~3 weeks, ~2 (default), ~1 for an aged account." }),
|
|
63
|
+
randomize_daily_caps: bool({ description: "Vary each day's caps a little so volume is not identical daily." }),
|
|
64
|
+
weekend_full_volume: bool({
|
|
65
|
+
description: "Send caps halve on Saturday and Sunday in the account's timezone. true keeps full volume; false restores the halving.",
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
const inferMatchesRoute = true;
|
|
69
|
+
void inferMatchesRoute;
|
|
70
|
+
export const SENDER_LIMITS_SET_OPERATION = {
|
|
71
|
+
id: "senders.limits.set",
|
|
72
|
+
route: "/api/cli/senders/{id}/limits",
|
|
73
|
+
method: "PATCH",
|
|
74
|
+
contract: SENDER_LIMITS_PATCH_CONTRACT,
|
|
75
|
+
cli: {
|
|
76
|
+
path: ["senders", "limits", "set"],
|
|
77
|
+
summary: "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.",
|
|
78
|
+
argument: { name: "id", description: "Sender account id, connection id, or Unipile account id." },
|
|
79
|
+
},
|
|
80
|
+
mcp: {
|
|
81
|
+
name: "oxygen_senders_limits_set",
|
|
82
|
+
title: "Oxygen: Set LinkedIn Account Limits",
|
|
83
|
+
description: "Updates per-account daily limits for sends (invites/messages/InMails) AND reads (relations lists, message history, searches, profile views), working hours, and the warm-up ramp (enable/disable/anchor). Values are clamped to safe maximums to protect the LinkedIn account. LinkedIn actions run only inside working hours.",
|
|
84
|
+
pack: "deliverability",
|
|
85
|
+
},
|
|
86
|
+
mutates: true,
|
|
87
|
+
spends: false,
|
|
88
|
+
};
|