@oxygen-agent/cli 1.1010.650 → 1.1010.905

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 (114) hide show
  1. package/README.md +1 -1
  2. package/dist/auto-update.d.ts +129 -0
  3. package/dist/auto-update.js +392 -0
  4. package/dist/command-manifest.js +15 -1
  5. package/dist/credentials.d.ts +2 -0
  6. package/dist/credentials.js +6 -3
  7. package/dist/functions-commands.js +1 -1
  8. package/dist/http-client.js +28 -4
  9. package/dist/inbox-needs-reply-notice.d.ts +12 -0
  10. package/dist/inbox-needs-reply-notice.js +51 -0
  11. package/dist/index.js +756 -177
  12. package/dist/run-wait.d.ts +3 -1
  13. package/dist/run-wait.js +19 -5
  14. package/dist/skills.js +48 -22
  15. package/dist/streamed-file-import.d.ts +58 -0
  16. package/dist/streamed-file-import.js +115 -0
  17. package/dist/update.d.ts +29 -0
  18. package/dist/update.js +62 -16
  19. package/dist/workflow-plan-limit-notices.d.ts +8 -0
  20. package/dist/workflow-plan-limit-notices.js +28 -0
  21. package/node_modules/@oxygen/cli-ugc/dist/commands.js +3 -3
  22. package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +50 -2
  23. package/node_modules/@oxygen/shared/dist/billing-anchors.js +94 -2
  24. package/node_modules/@oxygen/shared/dist/billing.d.ts +247 -37
  25. package/node_modules/@oxygen/shared/dist/billing.js +418 -45
  26. package/node_modules/@oxygen/shared/dist/capability-discovery.js +66 -6
  27. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +6 -6
  28. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +6 -6
  29. package/node_modules/@oxygen/shared/dist/cost-estimate-view.d.ts +50 -0
  30. package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +90 -0
  31. package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +167 -0
  32. package/node_modules/@oxygen/shared/dist/cost-estimate.js +361 -0
  33. package/node_modules/@oxygen/shared/dist/credit-gate.d.ts +26 -0
  34. package/node_modules/@oxygen/shared/dist/credit-gate.js +65 -0
  35. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.d.ts +51 -0
  36. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.js +101 -0
  37. package/node_modules/@oxygen/shared/dist/email-hard-bounce.d.ts +27 -0
  38. package/node_modules/@oxygen/shared/dist/email-hard-bounce.js +27 -0
  39. package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +1 -1
  40. package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -1
  41. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +10 -1
  42. package/node_modules/@oxygen/shared/dist/feature-gates.js +12 -1
  43. package/node_modules/@oxygen/shared/dist/file-import.d.ts +13 -1
  44. package/node_modules/@oxygen/shared/dist/file-import.js +33 -6
  45. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +73 -3
  46. package/node_modules/@oxygen/shared/dist/hosted-ai.js +246 -24
  47. package/node_modules/@oxygen/shared/dist/import-limits.d.ts +25 -1
  48. package/node_modules/@oxygen/shared/dist/import-limits.js +35 -2
  49. package/node_modules/@oxygen/shared/dist/index.d.ts +4 -23
  50. package/node_modules/@oxygen/shared/dist/index.js +4 -43
  51. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +114 -0
  52. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +150 -0
  53. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +9 -0
  54. package/node_modules/@oxygen/shared/dist/object-storage.js +17 -0
  55. package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +41 -0
  56. package/node_modules/@oxygen/shared/dist/operational-telemetry.js +55 -0
  57. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +19 -2
  58. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +234 -0
  59. package/node_modules/@oxygen/shared/dist/plan-band.js +312 -0
  60. package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +77 -7
  61. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +87 -7
  62. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +219 -0
  63. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +330 -0
  64. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +335 -126
  65. package/node_modules/@oxygen/shared/dist/plan-limits.js +277 -86
  66. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +158 -49
  67. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +139 -41
  68. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +42 -23
  69. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +56 -37
  70. package/node_modules/@oxygen/shared/dist/process-resource.d.ts +4 -0
  71. package/node_modules/@oxygen/shared/dist/process-resource.js +25 -0
  72. package/node_modules/@oxygen/shared/dist/provider-http-error.d.ts +10 -0
  73. package/node_modules/@oxygen/shared/dist/provider-http-error.js +27 -0
  74. package/node_modules/@oxygen/shared/dist/repricing.d.ts +257 -0
  75. package/node_modules/@oxygen/shared/dist/repricing.js +721 -0
  76. package/node_modules/@oxygen/shared/dist/semver.d.ts +21 -0
  77. package/node_modules/@oxygen/shared/dist/semver.js +41 -0
  78. package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +30 -0
  79. package/node_modules/@oxygen/shared/dist/sending-limits.js +43 -0
  80. package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +18 -15
  81. package/node_modules/@oxygen/shared/dist/sending-seats.js +22 -17
  82. package/node_modules/@oxygen/shared/dist/sequence-failures.js +4 -1
  83. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +57 -8
  84. package/node_modules/@oxygen/shared/dist/spend-safety.js +64 -11
  85. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +33 -1
  86. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +71 -1
  87. package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +68 -10
  88. package/node_modules/@oxygen/shared/dist/table-capacity.js +85 -4
  89. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +6 -0
  90. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +13 -5
  91. package/node_modules/@oxygen/shared/dist/telemetry-resource.d.ts +40 -0
  92. package/node_modules/@oxygen/shared/dist/telemetry-resource.js +35 -0
  93. package/node_modules/@oxygen/shared/dist/telemetry.d.ts +9 -0
  94. package/node_modules/@oxygen/shared/dist/telemetry.js +41 -2
  95. package/node_modules/@oxygen/shared/dist/trace-context.d.ts +29 -0
  96. package/node_modules/@oxygen/shared/dist/trace-context.js +88 -0
  97. package/node_modules/@oxygen/shared/dist/ugc.d.ts +15 -0
  98. package/node_modules/@oxygen/shared/dist/ugc.js +29 -0
  99. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -3
  100. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  101. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  102. package/node_modules/@oxygen/shared/dist/version.js +14 -27
  103. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +5 -0
  104. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +5 -0
  105. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +3 -3
  106. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +15 -1
  107. package/node_modules/@oxygen/workflows/dist/graph/types.js +15 -1
  108. package/node_modules/@oxygen/workflows/dist/index.d.ts +45 -0
  109. package/node_modules/@oxygen/workflows/dist/index.js +152 -2
  110. package/node_modules/@oxygen/workflows/dist/usage-estimate.d.ts +10 -1
  111. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +33 -29
  112. package/package.json +1 -1
  113. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.d.ts +0 -64
  114. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.js +0 -90
@@ -13,9 +13,27 @@
13
13
  /**
14
14
  * Formats that must be held in memory to parse at all — a JSON array has no
15
15
  * record boundary to stream from, and XLSX is a zip container. CSV and JSONL
16
- * stream, so only these two are capped this low.
16
+ * stream from object storage up to the plan's file limit, so only these two
17
+ * carry this lower ceiling (raised from 10 MiB by decision F.8, 2026-09-27).
18
+ *
19
+ * It also bounds every path that holds a whole file in one request: the inline
20
+ * multipart import used when object storage is not configured.
17
21
  */
18
22
  export declare const MAX_BUFFERED_IMPORT_PARSE_BYTES: number;
23
+ /**
24
+ * A link import (`--url`, `oxygen_tables_import_url`, the wizard's link card)
25
+ * is downloaded and parsed inside the web request, so it keeps its own small
26
+ * ceiling. It is not a plan limit: a bigger file is downloaded and imported
27
+ * with `tables import --file`, which stages it in object storage.
28
+ */
29
+ export declare const MAX_URL_IMPORT_BYTES: number;
30
+ /**
31
+ * The largest object one presigned PUT may carry on an S3-compatible store (S3
32
+ * and Ceph-based stores such as Hetzner Object Storage cap a single PUT at
33
+ * 5 GiB). A staged import is one PUT, so no plan's import file limit may
34
+ * exceed it.
35
+ */
36
+ export declare const IMPORT_OBJECT_SINGLE_PUT_MAX_BYTES: number;
19
37
  /**
20
38
  * The platform's request-body ceiling — infrastructure, not a plan limit.
21
39
  *
@@ -26,3 +44,9 @@ export declare const MAX_BUFFERED_IMPORT_PARSE_BYTES: number;
26
44
  export declare const VERCEL_REQUEST_BODY_LIMIT_BYTES: number;
27
45
  /** True for the formats that cannot stream and so carry the buffered ceiling. */
28
46
  export declare function isBufferedOnlyImportFormat(format: string): boolean;
47
+ /**
48
+ * The refusal every surface gives a JSON or XLSX file over the buffered
49
+ * ceiling. It names the ceiling and the two formats that take a larger file,
50
+ * so the reader knows the fix is a format change, not a plan change.
51
+ */
52
+ export declare function bufferedImportTooLargeMessage(format: string, fileBytes?: number | null): string;
@@ -10,12 +10,31 @@
10
10
  * constants live here alone, and both of those modules re-export them, keeping
11
11
  * one definition rather than a copy that drifts.
12
12
  */
13
+ const MIB = 1024 * 1024;
13
14
  /**
14
15
  * Formats that must be held in memory to parse at all — a JSON array has no
15
16
  * record boundary to stream from, and XLSX is a zip container. CSV and JSONL
16
- * stream, so only these two are capped this low.
17
+ * stream from object storage up to the plan's file limit, so only these two
18
+ * carry this lower ceiling (raised from 10 MiB by decision F.8, 2026-09-27).
19
+ *
20
+ * It also bounds every path that holds a whole file in one request: the inline
21
+ * multipart import used when object storage is not configured.
22
+ */
23
+ export const MAX_BUFFERED_IMPORT_PARSE_BYTES = 100 * MIB;
24
+ /**
25
+ * A link import (`--url`, `oxygen_tables_import_url`, the wizard's link card)
26
+ * is downloaded and parsed inside the web request, so it keeps its own small
27
+ * ceiling. It is not a plan limit: a bigger file is downloaded and imported
28
+ * with `tables import --file`, which stages it in object storage.
29
+ */
30
+ export const MAX_URL_IMPORT_BYTES = 10 * MIB;
31
+ /**
32
+ * The largest object one presigned PUT may carry on an S3-compatible store (S3
33
+ * and Ceph-based stores such as Hetzner Object Storage cap a single PUT at
34
+ * 5 GiB). A staged import is one PUT, so no plan's import file limit may
35
+ * exceed it.
17
36
  */
18
- export const MAX_BUFFERED_IMPORT_PARSE_BYTES = 10 * 1024 * 1024;
37
+ export const IMPORT_OBJECT_SINGLE_PUT_MAX_BYTES = 5 * 1024 * MIB;
19
38
  /**
20
39
  * The platform's request-body ceiling — infrastructure, not a plan limit.
21
40
  *
@@ -28,3 +47,17 @@ export const VERCEL_REQUEST_BODY_LIMIT_BYTES = 4 * 1024 * 1024;
28
47
  export function isBufferedOnlyImportFormat(format) {
29
48
  return format === "json" || format === "xlsx";
30
49
  }
50
+ /**
51
+ * The refusal every surface gives a JSON or XLSX file over the buffered
52
+ * ceiling. It names the ceiling and the two formats that take a larger file,
53
+ * so the reader knows the fix is a format change, not a plan change.
54
+ */
55
+ export function bufferedImportTooLargeMessage(format, fileBytes) {
56
+ const label = format.toUpperCase();
57
+ const size = typeof fileBytes === "number" ? ` This one is ${formatImportMib(fileBytes)}.` : "";
58
+ return `${label} files are read whole, so they are capped at ${formatImportMib(MAX_BUFFERED_IMPORT_PARSE_BYTES)}.${size} Save it as CSV or JSONL, which import up to your plan's file-size limit.`;
59
+ }
60
+ function formatImportMib(bytes) {
61
+ const mib = bytes / MIB;
62
+ return `${mib >= 10 ? Math.round(mib).toLocaleString("en-US") : mib.toFixed(1)} MiB`;
63
+ }
@@ -1,4 +1,4 @@
1
- export { MANAGED_INBOX_MINIMUM_CLI_VERSION, OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_VERSION, SUPPORT_AGENT_REPLY_MINIMUM_CLI_VERSION, } from "./version.js";
1
+ export { OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_VERSION, } from "./version.js";
2
2
  export { WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS, clearWorkflowTriggerAutoPauseMetadata, } from "./workflow-trigger-metadata.js";
3
3
  export { WORKFLOW_STATUS_CHANGE_METADATA_KEY, type WorkflowStatusChange, type WorkflowStatusChangeActor, type WorkflowStatusChangeSource, describeWorkflowStatusChange, formatWorkflowStatusChangeTimestamp, parseWorkflowStatusChange, readWorkflowStatusChange, } from "./workflow-status-change.js";
4
4
  export * from "./billing.js";
@@ -10,6 +10,7 @@ export * from "./capability-discovery.js";
10
10
  export * from "./user-capability-routing.js";
11
11
  export * from "./plan-capabilities.js";
12
12
  export * from "./plan-limits.js";
13
+ export * from "./plan-band.js";
13
14
  export * from "./sending-seats.js";
14
15
  export * from "./sending-seat-capacity.js";
15
16
  export * from "./plain-support-events.js";
@@ -37,7 +38,7 @@ export * from "./credit-guidance.js";
37
38
  export * from "./deploy-env.js";
38
39
  export * from "./directory.js";
39
40
  export * from "./email-dsn.js";
40
- export * from "./email-warmup-readiness.js";
41
+ export * from "./email-hard-bounce.js";
41
42
  export * from "./email-tracking-token.js";
42
43
  export * from "./email-unsubscribe-token.js";
43
44
  export * from "./error-redaction.js";
@@ -110,27 +111,7 @@ export * from "./workspace-event-catalog.js";
110
111
  export * from "./notetaker-events.js";
111
112
  export * from "./workspace-agents.js";
112
113
  export declare const MAX_ROW_LOOP_WRITE_ROWS = 500;
113
- export type SemanticVersion = {
114
- major: number;
115
- minor: number;
116
- patch: number;
117
- };
118
- /**
119
- * Parse a three-segment semantic version (e.g. `1.142.17`). Pre-release and
120
- * build metadata suffixes (`-rc.1`, `+build`) are tolerated but ignored.
121
- * Returns `null` when the input is not a parseable `major.minor.patch` string.
122
- */
123
- export declare function parseSemver(version: string): SemanticVersion | null;
124
- /**
125
- * Compare two semantic versions. Returns -1 when `a < b`, 1 when `a > b`, and
126
- * 0 when they are equal. Unparseable inputs compare as equal (0) so callers
127
- * fail open rather than misordering garbage.
128
- */
129
- export declare function compareSemver(a: string, b: string): -1 | 0 | 1;
130
- /** True when `a` is a strictly greater semantic version than `b`. */
131
- export declare function isVersionGreater(a: string, b: string): boolean;
132
- /** True when `a` is a strictly lesser semantic version than `b`. */
133
- export declare function isVersionLess(a: string, b: string): boolean;
114
+ export * from "./semver.js";
134
115
  export * from "./ugc.js";
135
116
  export * from "./ugc-amplification-identity.js";
136
117
  export * from "./knowledge-repository.js";
@@ -1,4 +1,4 @@
1
- export { MANAGED_INBOX_MINIMUM_CLI_VERSION, OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_VERSION, SUPPORT_AGENT_REPLY_MINIMUM_CLI_VERSION, } from "./version.js";
1
+ export { OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_VERSION, } from "./version.js";
2
2
  export { WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS, clearWorkflowTriggerAutoPauseMetadata, } from "./workflow-trigger-metadata.js";
3
3
  export { WORKFLOW_STATUS_CHANGE_METADATA_KEY, describeWorkflowStatusChange, formatWorkflowStatusChangeTimestamp, parseWorkflowStatusChange, readWorkflowStatusChange, } from "./workflow-status-change.js";
4
4
  export * from "./billing.js";
@@ -10,6 +10,7 @@ export * from "./capability-discovery.js";
10
10
  export * from "./user-capability-routing.js";
11
11
  export * from "./plan-capabilities.js";
12
12
  export * from "./plan-limits.js";
13
+ export * from "./plan-band.js";
13
14
  export * from "./sending-seats.js";
14
15
  export * from "./sending-seat-capacity.js";
15
16
  export * from "./plain-support-events.js";
@@ -37,7 +38,7 @@ export * from "./credit-guidance.js";
37
38
  export * from "./deploy-env.js";
38
39
  export * from "./directory.js";
39
40
  export * from "./email-dsn.js";
40
- export * from "./email-warmup-readiness.js";
41
+ export * from "./email-hard-bounce.js";
41
42
  export * from "./email-tracking-token.js";
42
43
  export * from "./email-unsubscribe-token.js";
43
44
  export * from "./error-redaction.js";
@@ -126,47 +127,7 @@ export * from "./workspace-agents.js";
126
127
  // the COPY-based bulk engine. Tenant-db enforces this and the CLI/API row caps
127
128
  // reference it so they never advertise a batch the row-loop will reject.
128
129
  export const MAX_ROW_LOOP_WRITE_ROWS = 500;
129
- /**
130
- * Parse a three-segment semantic version (e.g. `1.142.17`). Pre-release and
131
- * build metadata suffixes (`-rc.1`, `+build`) are tolerated but ignored.
132
- * Returns `null` when the input is not a parseable `major.minor.patch` string.
133
- */
134
- export function parseSemver(version) {
135
- const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version);
136
- if (!match)
137
- return null;
138
- return {
139
- major: Number(match[1]),
140
- minor: Number(match[2]),
141
- patch: Number(match[3]),
142
- };
143
- }
144
- /**
145
- * Compare two semantic versions. Returns -1 when `a < b`, 1 when `a > b`, and
146
- * 0 when they are equal. Unparseable inputs compare as equal (0) so callers
147
- * fail open rather than misordering garbage.
148
- */
149
- export function compareSemver(a, b) {
150
- const left = parseSemver(a);
151
- const right = parseSemver(b);
152
- if (!left || !right)
153
- return 0;
154
- for (const key of ["major", "minor", "patch"]) {
155
- if (left[key] > right[key])
156
- return 1;
157
- if (left[key] < right[key])
158
- return -1;
159
- }
160
- return 0;
161
- }
162
- /** True when `a` is a strictly greater semantic version than `b`. */
163
- export function isVersionGreater(a, b) {
164
- return compareSemver(a, b) > 0;
165
- }
166
- /** True when `a` is a strictly lesser semantic version than `b`. */
167
- export function isVersionLess(a, b) {
168
- return compareSemver(a, b) < 0;
169
- }
130
+ export * from "./semver.js";
170
131
  export * from "./ugc.js";
171
132
  export * from "./ugc-amplification-identity.js";
172
133
  export * from "./knowledge-repository.js";
@@ -247,4 +247,118 @@ export declare function truncateLinkedInInviteNote(note: string, limit: number):
247
247
  truncated: boolean;
248
248
  originalLength: number;
249
249
  };
250
+ export type LinkedInSenderLimits = {
251
+ invites_per_day: number;
252
+ invites_per_week: number;
253
+ messages_per_day: number;
254
+ inmails_per_day: number;
255
+ /**
256
+ * InMails in a rolling 30 days (today and the 29 before it, in the account's
257
+ * day). Checked under the same sender row lock as the weekly invite cap, so
258
+ * concurrent claims cannot both pass a stale sum.
259
+ */
260
+ inmails_per_month: number;
261
+ profile_views_per_day: number;
262
+ /**
263
+ * Invisible profile and company lookups (`users_get` with notify:false,
264
+ * `company_get`). The member is never notified, so these are reads, but
265
+ * Unipile relays LinkedIn's ~100 profile retrievals a day for them.
266
+ */
267
+ profile_lookups_per_day: number;
268
+ /** Skill endorsements. Used to share the follow cap. */
269
+ endorsements_per_day: number;
270
+ /**
271
+ * Sent-invitation withdrawals (the withdraw action itself, not the reads of
272
+ * the sent-invite list). LinkedIn blocks re-inviting a withdrawn member for
273
+ * three weeks; see the re-invite guard.
274
+ */
275
+ withdrawals_per_day: number;
276
+ /** Posts to the member's own feed, from every caller. Timing-exempt, never uncapped. */
277
+ posts_per_day: number;
278
+ follows_per_day: number;
279
+ likes_per_day: number;
280
+ comments_per_day: number;
281
+ total_actions_per_day: number;
282
+ min_action_spacing_seconds: number;
283
+ action_spacing_jitter_seconds: number;
284
+ interactive_min_spacing_seconds: number;
285
+ interactive_spacing_jitter_seconds: number;
286
+ /**
287
+ * Backstop on the interactive lane: how many human-paced sends an account may
288
+ * make in a rolling hour.
289
+ *
290
+ * Mostly redundant with messages_per_day (whose maximum is 40), and that is
291
+ * deliberate — it bounds BURST SHAPE rather than volume. Volume is already
292
+ * capped; this stops the whole daily allowance being spent in four minutes if
293
+ * the interactive classification is ever wrong, which is the one failure mode
294
+ * the daily cap cannot see.
295
+ */
296
+ interactive_sends_per_hour: number;
297
+ relations_reads_per_day: number;
298
+ messages_reads_per_day: number;
299
+ searches_per_day: number;
300
+ sales_nav_search_results_per_day: number;
301
+ api_reads_per_day: number;
302
+ total_reads_per_day: number;
303
+ relations_ingest_reads_per_day: number;
304
+ api_ingest_reads_per_day: number;
305
+ messages_ingest_reads_per_day: number;
306
+ /**
307
+ * Network-capture read budgets: the two-phase allowance for enumerating an
308
+ * account's OWN 1st-degree connections and followers into a workspace table.
309
+ *
310
+ * Two keys, not one, because Unipile's own guidance for the relations list is
311
+ * explicitly two-phase: a heavier initial sync, then "retrieving the first page
312
+ * only a few times a day with randomly spaced intervals". One flat number would
313
+ * either make the first walk take weeks or leave a steady-state drip reading far
314
+ * more than it needs forever. Both still meter as `relations_ingest_read` against
315
+ * one shared daily counter, so an account cannot spend both budgets at once.
316
+ */
317
+ network_backfill_reads_per_day: number;
318
+ network_delta_reads_per_day: number;
319
+ };
320
+ /**
321
+ * Safe defaults for LinkedIn action limits.
322
+ *
323
+ * PROVENANCE, because these numbers get copied and should not be laundered into
324
+ * authority they do not have:
325
+ *
326
+ * - The 20-25/day connection-request range and the 40/day message posture come
327
+ * from third-party operator playbooks describing HeyReach-style outreach, NOT
328
+ * from HeyReach. HeyReach's own sending-limits article publishes no numbers at
329
+ * all — it only says you configure per-action daily limits and that it "will
330
+ * always try to reach those limits". An earlier version of this comment cited
331
+ * HeyReach directly; that attribution was wrong.
332
+ * - The READ budgets (relations/api/messages, ingest and network capture) are
333
+ * anchored on Unipile's documented per-route recommendations, which relay
334
+ * LinkedIn's own limits: ~80-100 invitations/day, ~100 profile retrievals/day,
335
+ * and 100/day per account for "all other routes" — the bucket users_relations
336
+ * and users_followers fall into. Unipile enforces none of it; cadence is ours
337
+ * to choose, and exceeding returns 429/500 and can disconnect the account.
338
+ * See developer.unipile.com/docs/provider-limits-and-restrictions.
339
+ *
340
+ * The weekly invite and 200-total-action guards stay enforced independently.
341
+ * Users can lower limits per account, but never raise them above
342
+ * LINKEDIN_SENDER_LIMIT_MAXIMUMS.
343
+ */
344
+ export declare const LINKEDIN_SENDER_LIMIT_DEFAULTS: LinkedInSenderLimits;
345
+ export declare const LINKEDIN_SENDER_LIMIT_MAXIMUMS: LinkedInSenderLimits;
346
+ /**
347
+ * LinkedIn refuses a new invitation to a member whose earlier invitation from
348
+ * the same account was withdrawn, for about three weeks. OXYGEN records every
349
+ * withdrawal it makes and refuses the re-invite itself for this long, so the
350
+ * refusal is ours (with a reset time) instead of a provider 422.
351
+ */
352
+ export declare const LINKEDIN_REINVITE_COOLDOWN_DAYS = 21;
353
+ /**
354
+ * Whether a refused invitation is LinkedIn's re-invite cooldown: Unipile answers
355
+ * HTTP 422 with the `cannot_resend_yet` error type. Anchored on the type, with
356
+ * the message as a fallback for the provider shapes that carry the type only in
357
+ * the sentence. Pure and total.
358
+ */
359
+ export declare function isLinkedInReinviteCooldownRefusal(input: {
360
+ status: number | null;
361
+ providerType: string | null;
362
+ providerMessage: string | null;
363
+ }): boolean;
250
364
  export {};
@@ -196,3 +196,153 @@ export function truncateLinkedInInviteNote(note, limit) {
196
196
  const kept = cut === 0 ? head : head.slice(0, cut);
197
197
  return { note: kept.join("").trimEnd(), truncated: true, originalLength };
198
198
  }
199
+ /**
200
+ * Safe defaults for LinkedIn action limits.
201
+ *
202
+ * PROVENANCE, because these numbers get copied and should not be laundered into
203
+ * authority they do not have:
204
+ *
205
+ * - The 20-25/day connection-request range and the 40/day message posture come
206
+ * from third-party operator playbooks describing HeyReach-style outreach, NOT
207
+ * from HeyReach. HeyReach's own sending-limits article publishes no numbers at
208
+ * all — it only says you configure per-action daily limits and that it "will
209
+ * always try to reach those limits". An earlier version of this comment cited
210
+ * HeyReach directly; that attribution was wrong.
211
+ * - The READ budgets (relations/api/messages, ingest and network capture) are
212
+ * anchored on Unipile's documented per-route recommendations, which relay
213
+ * LinkedIn's own limits: ~80-100 invitations/day, ~100 profile retrievals/day,
214
+ * and 100/day per account for "all other routes" — the bucket users_relations
215
+ * and users_followers fall into. Unipile enforces none of it; cadence is ours
216
+ * to choose, and exceeding returns 429/500 and can disconnect the account.
217
+ * See developer.unipile.com/docs/provider-limits-and-restrictions.
218
+ *
219
+ * The weekly invite and 200-total-action guards stay enforced independently.
220
+ * Users can lower limits per account, but never raise them above
221
+ * LINKEDIN_SENDER_LIMIT_MAXIMUMS.
222
+ */
223
+ export const LINKEDIN_SENDER_LIMIT_DEFAULTS = {
224
+ invites_per_day: 20,
225
+ invites_per_week: 100,
226
+ messages_per_day: 40,
227
+ inmails_per_day: 10,
228
+ // Competitors cap open-profile InMail near 800/month; 400 leaves headroom.
229
+ inmails_per_month: 400,
230
+ profile_views_per_day: 50,
231
+ profile_lookups_per_day: 80,
232
+ endorsements_per_day: 5,
233
+ withdrawals_per_day: 10,
234
+ // Matches the order of magnitude of a busy human poster; the publishing
235
+ // scheduler's own 25-per-24h ceiling stays a second guard.
236
+ posts_per_day: 10,
237
+ follows_per_day: 20,
238
+ likes_per_day: 20,
239
+ comments_per_day: 5,
240
+ total_actions_per_day: 150,
241
+ min_action_spacing_seconds: 300,
242
+ action_spacing_jitter_seconds: 300,
243
+ // 1s, no jitter: a person working their own inbox should never meet this gate.
244
+ // It exists only to stop a genuine burst — a stuck key, a runaway client — from
245
+ // firing an account's whole hour in a second, and to keep the per-account row
246
+ // lock (and with it the hourly counter) in play. Jitter is 0 deliberately: the
247
+ // variance in human sending is the human, and adding 0-5s on top only makes the
248
+ // product feel slow. Non-zero spacing is still required — see the fast-path note
249
+ // in reserveLinkedInSenderActionSlot.
250
+ interactive_min_spacing_seconds: 1,
251
+ interactive_spacing_jitter_seconds: 0,
252
+ // The primary bound on human sending now that it no longer claims the daily
253
+ // message cap. 200/hour is ~3/minute sustained: never felt by someone answering
254
+ // their inbox, and still a hard ceiling if the interactive classification is
255
+ // ever wrong.
256
+ interactive_sends_per_hour: 200,
257
+ // Conservative read budgets: roughly human-speed usage. Bulk scraping
258
+ // (full contact-list exports, whole-inbox pulls, search harvesting) is
259
+ // impossible by default — the pattern that gets accounts banned.
260
+ relations_reads_per_day: 20,
261
+ messages_reads_per_day: 200,
262
+ searches_per_day: 20,
263
+ sales_nav_search_results_per_day: 1000,
264
+ api_reads_per_day: 200,
265
+ total_reads_per_day: 1000,
266
+ // Conservative ingestion drips: a large network/history import spreads over
267
+ // many days rather than bursting, and stays well within total_reads_per_day.
268
+ // History backfill is the slowest drip (full thread walks are the most
269
+ // ban-sensitive bulk read), so its default is the tightest of the three.
270
+ relations_ingest_reads_per_day: 15,
271
+ api_ingest_reads_per_day: 20,
272
+ messages_ingest_reads_per_day: 10,
273
+ // Network capture, phase 1: 40% of Unipile's documented ~100/day recommendation
274
+ // for these routes. At 50 members a page that is ~2,000 people/day, so a typical
275
+ // network lands in days rather than weeks while leaving most of the account's
276
+ // daily read headroom unused.
277
+ network_backfill_reads_per_day: 40,
278
+ // Phase 2, once the walk is exhausted: Unipile's "first page a few times a day".
279
+ // New connections and followers arrive at the TOP of both listings, so a handful
280
+ // of first-page reads catches them all; the drip re-walks deeper only if it finds
281
+ // a full page of new people.
282
+ network_delta_reads_per_day: 4,
283
+ };
284
+ export const LINKEDIN_SENDER_LIMIT_MAXIMUMS = {
285
+ invites_per_day: 30,
286
+ invites_per_week: 150,
287
+ messages_per_day: 40,
288
+ // Lowered 40 → 25 and 150 → 100 (2026-09-26, founder decision "add missing
289
+ // caps + tighten"): operator playbooks put InMail at ≤25/day and profile views
290
+ // at 50-100. Stored overrides above these clamp at read time.
291
+ inmails_per_day: 25,
292
+ inmails_per_month: 800,
293
+ profile_views_per_day: 100,
294
+ // Unipile relays ~100 profile retrievals a day as LinkedIn's own ceiling.
295
+ profile_lookups_per_day: 100,
296
+ endorsements_per_day: 10,
297
+ withdrawals_per_day: 20,
298
+ posts_per_day: 25,
299
+ follows_per_day: 50,
300
+ likes_per_day: 50,
301
+ // Lowered 50 → 20 (2026-09-25): public comments are the most visible automated
302
+ // signal an account emits, and the one LinkedIn's inauthentic-engagement
303
+ // enforcement keys on. Stored overrides above this clamp at read time.
304
+ comments_per_day: 20,
305
+ total_actions_per_day: 200,
306
+ min_action_spacing_seconds: 3600,
307
+ action_spacing_jitter_seconds: 1800,
308
+ interactive_min_spacing_seconds: 3600,
309
+ interactive_spacing_jitter_seconds: 1800,
310
+ // Lowered 1000 → 300 (2026-09-25): 5/minute sustained is still far above
311
+ // anyone answering their own inbox, and bounds a misclassified bulk send.
312
+ interactive_sends_per_hour: 300,
313
+ relations_reads_per_day: 100,
314
+ messages_reads_per_day: 600,
315
+ searches_per_day: 100,
316
+ sales_nav_search_results_per_day: 1000,
317
+ api_reads_per_day: 1000,
318
+ total_reads_per_day: 2000,
319
+ relations_ingest_reads_per_day: 100,
320
+ api_ingest_reads_per_day: 200,
321
+ messages_ingest_reads_per_day: 200,
322
+ // 100/day is Unipile's own documented recommendation for "all other routes",
323
+ // which is the bucket users_relations and users_followers fall into. Our hard
324
+ // maximum is exactly the vendor's number and must not be raised above it.
325
+ network_backfill_reads_per_day: 100,
326
+ network_delta_reads_per_day: 20,
327
+ };
328
+ // ===== Re-invite cooldown =====
329
+ /**
330
+ * LinkedIn refuses a new invitation to a member whose earlier invitation from
331
+ * the same account was withdrawn, for about three weeks. OXYGEN records every
332
+ * withdrawal it makes and refuses the re-invite itself for this long, so the
333
+ * refusal is ours (with a reset time) instead of a provider 422.
334
+ */
335
+ export const LINKEDIN_REINVITE_COOLDOWN_DAYS = 21;
336
+ /**
337
+ * Whether a refused invitation is LinkedIn's re-invite cooldown: Unipile answers
338
+ * HTTP 422 with the `cannot_resend_yet` error type. Anchored on the type, with
339
+ * the message as a fallback for the provider shapes that carry the type only in
340
+ * the sentence. Pure and total.
341
+ */
342
+ export function isLinkedInReinviteCooldownRefusal(input) {
343
+ if (input.status !== 422)
344
+ return false;
345
+ if (typeof input.providerType === "string" && /(?:^|\/)cannot_resend_yet$/u.test(input.providerType.trim()))
346
+ return true;
347
+ return typeof input.providerMessage === "string" && /\bcannot_resend_yet\b/u.test(input.providerMessage);
348
+ }
@@ -62,6 +62,15 @@ export type PresignedImportUpload = {
62
62
  expiresInSeconds: number;
63
63
  };
64
64
  export type PresignedPublishingMediaUpload = PresignedImportUpload;
65
+ /**
66
+ * Presign the one PUT that stages an import file under `imports/<org>/`.
67
+ *
68
+ * Import staging is deliberately NOT a workspace file (`workspace-file-storage`):
69
+ * it is transient input the worker streams once and deletes, so neither the
70
+ * 100 MiB per-file nor the 1 GiB retained-files ceiling of that store applies.
71
+ * Its ceiling is the caller's plan import limit, checked by the route before it
72
+ * asks, plus the single-PUT maximum below, which no plan exceeds.
73
+ */
65
74
  export declare function presignImportUpload(input: {
66
75
  organizationId: string;
67
76
  fileName?: string | null;
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client, } from "@aws-sdk/client-s3";
3
3
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
4
4
  import { OxygenError } from "./cli-result.js";
5
+ import { IMPORT_OBJECT_SINGLE_PUT_MAX_BYTES } from "./import-limits.js";
5
6
  // S3-compatible object storage for large CSV/file imports. The CLI uploads the
6
7
  // raw file straight to the bucket via a presigned PUT URL (bypassing Vercel's
7
8
  // ~4.5MB request-body limit), then the Fly worker downloads it and COPY-loads
@@ -141,7 +142,23 @@ export function isCopilotAttachmentObjectKeyForOrganization(key, organizationId)
141
142
  export function isPublishingMediaObjectKeyForOrganization(key, organizationId) {
142
143
  return key.startsWith(`publishing-media/${organizationId}/`);
143
144
  }
145
+ /**
146
+ * Presign the one PUT that stages an import file under `imports/<org>/`.
147
+ *
148
+ * Import staging is deliberately NOT a workspace file (`workspace-file-storage`):
149
+ * it is transient input the worker streams once and deletes, so neither the
150
+ * 100 MiB per-file nor the 1 GiB retained-files ceiling of that store applies.
151
+ * Its ceiling is the caller's plan import limit, checked by the route before it
152
+ * asks, plus the single-PUT maximum below, which no plan exceeds.
153
+ */
144
154
  export async function presignImportUpload(input) {
155
+ if (!Number.isSafeInteger(input.contentLength) || input.contentLength < 1
156
+ || input.contentLength > IMPORT_OBJECT_SINGLE_PUT_MAX_BYTES) {
157
+ throw new OxygenError("request_too_large", "Import file is too large to stage in one upload.", {
158
+ details: { file_bytes: input.contentLength, max_file_bytes: IMPORT_OBJECT_SINGLE_PUT_MAX_BYTES },
159
+ exitCode: 1,
160
+ });
161
+ }
145
162
  const { client, config } = resolveClient();
146
163
  const storageKey = buildImportObjectKey({
147
164
  organizationId: input.organizationId,
@@ -21,4 +21,45 @@ export declare function operationalSpanSnapshot<T extends {
21
21
  instrumentationScope?: AttributeContainer;
22
22
  spanContext(): unknown;
23
23
  }>(span: T): T;
24
+ type ExportResource = {
25
+ attributes: Record<string, unknown>;
26
+ };
27
+ /**
28
+ * Maps the SDK resource onto the resource the collector destination exports,
29
+ * without touching the one every other destination reads. `rewrite` receives the
30
+ * SDK resource's attributes and returns the collector's; the result is
31
+ * sanitized once and memoized per source resource, so every span in a batch
32
+ * keeps sharing one resource object and the OTLP encoder still groups them into
33
+ * a single ResourceSpans entry.
34
+ *
35
+ * A resource whose async detectors are still pending (`asyncAttributesPending`)
36
+ * is mapped but not memoized, so attributes that settle later are picked up on
37
+ * a later export instead of an incomplete overlay being kept for the process.
38
+ */
39
+ export declare function collectorResourceMapper(rewrite: (attributes: Record<string, unknown>) => Record<string, unknown>): (resource: MappableResource | undefined) => ExportResource;
40
+ type MappableResource = {
41
+ attributes?: Record<string, unknown>;
42
+ asyncAttributesPending?: boolean;
43
+ };
44
+ type SnapshotSpan = Parameters<typeof operationalSpanSnapshot>[0];
45
+ /**
46
+ * The collector's per-span export transform: the sanitized snapshot, with the
47
+ * resource swapped for the collector's when a mapper is given. Without one it
48
+ * is exactly `operationalSpanSnapshot`.
49
+ */
50
+ export declare function collectorSpanSnapshot(resourceFor?: (resource: MappableResource | undefined) => ExportResource): <T extends SnapshotSpan>(span: T) => T;
51
+ type MetricExporterLike = {
52
+ export(metrics: unknown, resultCallback: (result: {
53
+ code: number;
54
+ error?: Error | undefined;
55
+ }) => void): void;
56
+ shutdown(): Promise<void>;
57
+ };
58
+ /**
59
+ * Wraps a metric exporter so each ResourceMetrics batch carries the collector's
60
+ * resource. The reader-facing methods (temporality/aggregation selection,
61
+ * forceFlush) are forwarded only when the inner exporter has them, so the
62
+ * PeriodicExportingMetricReader sees exactly what it would have unwrapped.
63
+ */
64
+ export declare function withCollectorMetricResource<E extends MetricExporterLike>(inner: E, resourceFor: (resource: MappableResource | undefined) => ExportResource): E;
24
65
  export {};
@@ -1,4 +1,5 @@
1
1
  import { normalizeTelemetryAttributes, redactSecretsInString } from "./redaction.js";
2
+ import { forwardExporterMethods } from "./telemetry-export-observer.js";
2
3
  // Namespaced semantic-convention fields evade the generic exact-key log policy.
3
4
  // Numeric usage remains operational data, subject to the shared secret policy.
4
5
  const PAYLOAD_KEY = /(?:^|[._-])(?:prompts?|completions?|inputs?|outputs?|payloads?|rows?|messages?|content|body|customer_data)(?:$|[._-])|^(?:request|response)$/i;
@@ -71,3 +72,57 @@ function stripOperationalUrl(value) {
71
72
  return value.split(/[?#]/, 1)[0] ?? "";
72
73
  }
73
74
  }
75
+ /**
76
+ * Maps the SDK resource onto the resource the collector destination exports,
77
+ * without touching the one every other destination reads. `rewrite` receives the
78
+ * SDK resource's attributes and returns the collector's; the result is
79
+ * sanitized once and memoized per source resource, so every span in a batch
80
+ * keeps sharing one resource object and the OTLP encoder still groups them into
81
+ * a single ResourceSpans entry.
82
+ *
83
+ * A resource whose async detectors are still pending (`asyncAttributesPending`)
84
+ * is mapped but not memoized, so attributes that settle later are picked up on
85
+ * a later export instead of an incomplete overlay being kept for the process.
86
+ */
87
+ export function collectorResourceMapper(rewrite) {
88
+ const cache = new WeakMap();
89
+ let withoutSource;
90
+ return (resource) => {
91
+ if (!resource)
92
+ return withoutSource ??= { attributes: operationalAttributes(rewrite({})) };
93
+ const cached = cache.get(resource);
94
+ if (cached)
95
+ return cached;
96
+ const mapped = { attributes: operationalAttributes(rewrite(resource.attributes ?? {})) };
97
+ if (!resource.asyncAttributesPending)
98
+ cache.set(resource, mapped);
99
+ return mapped;
100
+ };
101
+ }
102
+ /**
103
+ * The collector's per-span export transform: the sanitized snapshot, with the
104
+ * resource swapped for the collector's when a mapper is given. Without one it
105
+ * is exactly `operationalSpanSnapshot`.
106
+ */
107
+ export function collectorSpanSnapshot(resourceFor) {
108
+ if (!resourceFor)
109
+ return (span) => operationalSpanSnapshot(span);
110
+ return (span) => ({ ...operationalSpanSnapshot(span), resource: resourceFor(span.resource) });
111
+ }
112
+ /**
113
+ * Wraps a metric exporter so each ResourceMetrics batch carries the collector's
114
+ * resource. The reader-facing methods (temporality/aggregation selection,
115
+ * forceFlush) are forwarded only when the inner exporter has them, so the
116
+ * PeriodicExportingMetricReader sees exactly what it would have unwrapped.
117
+ */
118
+ export function withCollectorMetricResource(inner, resourceFor) {
119
+ const wrapped = {
120
+ export(metrics, resultCallback) {
121
+ const batch = metrics;
122
+ inner.export({ ...batch, resource: resourceFor(batch.resource) }, resultCallback);
123
+ },
124
+ shutdown: () => inner.shutdown(),
125
+ };
126
+ forwardExporterMethods(inner, wrapped);
127
+ return wrapped;
128
+ }