@oxygen-agent/cli 1.142.4 → 1.146.1

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 CHANGED
@@ -34,4 +34,4 @@ oxygen update
34
34
 
35
35
  For product documentation and support, visit https://oxygen-agent.com.
36
36
 
37
- Version: 1.142.4
37
+ Version: 1.146.1
@@ -1,4 +1,4 @@
1
- import { OXYGEN_VERSION, OxygenError } from "@oxygen/shared";
1
+ import { OXYGEN_VERSION, OxygenError, isVersionGreater } from "@oxygen/shared";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { defaultApiUrl, loadCredentials } from "./credentials.js";
4
4
  import { resolveCliUpdateGuidance } from "./runtime.js";
@@ -24,6 +24,9 @@ path, options = {}) {
24
24
  const headers = {
25
25
  Accept: "application/json",
26
26
  "X-Oxygen-Trace-Id": traceId,
27
+ // Advertise the CLI version so the server can enforce its minimum-CLI floor
28
+ // even against clients that predate the client-side envelope gate.
29
+ "X-Oxygen-Client-Version": OXYGEN_VERSION,
27
30
  };
28
31
  addVercelProtectionBypassHeader(apiUrl, headers);
29
32
  if (credentials?.token) {
@@ -225,31 +228,6 @@ function withTraceDetails(details, traceId, compatibility, apiUrl) {
225
228
  ...fields,
226
229
  };
227
230
  }
228
- function isVersionGreater(left, right) {
229
- const leftParts = parseSemver(left);
230
- const rightParts = parseSemver(right);
231
- if (!leftParts || !rightParts)
232
- return false;
233
- for (let index = 0; index < leftParts.length; index += 1) {
234
- const leftPart = leftParts[index] ?? 0;
235
- const rightPart = rightParts[index] ?? 0;
236
- if (leftPart > rightPart)
237
- return true;
238
- if (leftPart < rightPart)
239
- return false;
240
- }
241
- return false;
242
- }
243
- function parseSemver(value) {
244
- const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(value);
245
- if (!match)
246
- return null;
247
- return [
248
- Number(match[1]),
249
- Number(match[2]),
250
- Number(match[3]),
251
- ];
252
- }
253
231
  function addVercelProtectionBypassHeader(apiUrl, headers) {
254
232
  const secret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET?.trim();
255
233
  if (!secret)
package/dist/index.js CHANGED
@@ -45,6 +45,10 @@ const OXYGEN_WORDMARK = [
45
45
  const LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD = 500;
46
46
  const TABLE_ACTION_RUN_WAIT_DEFAULT_TIMEOUT_SECONDS = 600;
47
47
  const TABLE_ACTION_RUN_WAIT_DEFAULT_INTERVAL_SECONDS = 5;
48
+ // Single-row paid runs are auto-backgrounded server-side; the CLI waits this
49
+ // long for the cell to finish before handing back the queued run envelope.
50
+ const SINGLE_ROW_COLUMN_RUN_WAIT_DEFAULT_TIMEOUT_SECONDS = 90;
51
+ const SINGLE_ROW_COLUMN_RUN_WAIT_DEFAULT_INTERVAL_SECONDS = 2;
48
52
  const TABLE_INGESTION_WAIT_DEFAULT_TIMEOUT_SECONDS = 600;
49
53
  const TABLE_INGESTION_WAIT_DEFAULT_INTERVAL_SECONDS = 5;
50
54
  const WORKFLOW_TAIL_DEFAULT_TIMEOUT_SECONDS = 600;
@@ -74,9 +78,28 @@ async function handleAsyncAction(command, options, action) {
74
78
  catch (error) {
75
79
  const failure = toFailure(command, error);
76
80
  writeJson(failure);
81
+ writeMaxCreditsHint(error);
77
82
  process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
78
83
  }
79
84
  }
85
+ // Paid multi-row column runs require an explicit --max-credits spend cap; the
86
+ // server rejects them with max_credits_required plus a recommended cap. Surface
87
+ // that as a one-line stderr hint so users don't have to dig the value out of the
88
+ // JSON envelope (stderr keeps --json stdout machine-clean).
89
+ function writeMaxCreditsHint(error) {
90
+ if (!(error instanceof OxygenError) || error.code !== "max_credits_required")
91
+ return;
92
+ const recommended = readRecommendedMaxCredits(error.details);
93
+ if (recommended === null)
94
+ return;
95
+ process.stderr.write(`hint: re-run with --max-credits ${recommended} to approve the spend cap\n`);
96
+ }
97
+ function readRecommendedMaxCredits(details) {
98
+ if (!details || typeof details !== "object" || Array.isArray(details))
99
+ return null;
100
+ const value = details.recommended_max_credits;
101
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
102
+ }
80
103
  function parseJsonObject(value) {
81
104
  let parsed;
82
105
  try {
@@ -479,6 +502,32 @@ export function createProgram() {
479
502
  },
480
503
  });
481
504
  });
505
+ }))
506
+ .addCommand(new Command("backfill-upsert-indexes")
507
+ .description("Create missing upsert-key secondary indexes on customer tables. Staff only; defaults to dry-run.")
508
+ .option("--dry-run", "Report the index backfill plan without creating indexes. This is the default.")
509
+ .option("--apply", "Create the missing indexes (CONCURRENTLY). Requires --confirm.")
510
+ .option("--confirm", "Confirm --apply for index creation.")
511
+ .option("--org <id>", "Limit the backfill to a single organization id.")
512
+ .option("--limit <n>", "Maximum ready tenants to inspect in this batch. Defaults to 25; hard cap is 200.")
513
+ .option("--json", "Print a JSON envelope.")
514
+ .action(async (options) => {
515
+ await handleAsyncAction("db backfill-upsert-indexes", options, () => {
516
+ if (options.apply && !options.confirm) {
517
+ throw new OxygenError("confirmation_required", "Refusing to create upsert-key indexes without --confirm.", { exitCode: 1 });
518
+ }
519
+ const limit = readPositiveInt(options.limit);
520
+ return requestOxygen("/api/cli/db/backfill-upsert-indexes", {
521
+ method: "POST",
522
+ body: {
523
+ apply: Boolean(options.apply),
524
+ dry_run: !options.apply,
525
+ confirm: Boolean(options.confirm),
526
+ ...(options.org ? { organization_id: options.org } : {}),
527
+ ...(limit !== undefined ? { limit } : {}),
528
+ },
529
+ });
530
+ });
482
531
  }))
483
532
  .addCommand(new Command("cost-policy")
484
533
  .description("Show tenant database cost controls and reconciliation status.")
@@ -2416,7 +2465,11 @@ export function createProgram() {
2416
2465
  .description("Redacted operation event commands for the current organization.")
2417
2466
  .addCommand(new Command("events")
2418
2467
  .description("List recent redacted operation events and failures.")
2419
- .option("--status <status>", "Filter by completed, queued, completed_with_errors, or failed.")
2468
+ // Keep in sync with OBSERVABILITY_STATUS_FILTERS in
2469
+ // apps/web/src/lib/observability.ts and the MCP tool enum in
2470
+ // packages/mcp-server/src/tools/observability.ts. The API rejects any
2471
+ // other value with invalid_request so a typo fails loudly.
2472
+ .option("--status <status>", "Filter by success, error, completed, failed, completed_with_errors, queued, skipped, or blocked.")
2420
2473
  .option("--trace-id <trace_id>", "Filter by trace id.")
2421
2474
  .option("--run-id <run_id>", "Filter by workspace run id.")
2422
2475
  .option("--limit <n>", "Maximum events to return. Defaults to 50.")
@@ -3010,6 +3063,197 @@ export function createProgram() {
3010
3063
  });
3011
3064
  });
3012
3065
  }));
3066
+ program
3067
+ .command("linkedin")
3068
+ .description("LinkedIn sender account management for the native LinkedIn sequencer (accounts, limits, usage).")
3069
+ .addCommand(new Command("accounts")
3070
+ .description("Manage the org's connected LinkedIn sender accounts: list, connect, sync, inspect, disconnect, and tune rate limits.")
3071
+ .addCommand(new Command("list")
3072
+ .description("List connected LinkedIn sender accounts with health status, rate limits, and today's usage.")
3073
+ .option("--status <status>", "Filter by sender status: active, paused, disconnected, restricted, or credentials_required.")
3074
+ .option("--no-usage", "Skip today's per-account usage counts for a faster, lighter response.")
3075
+ .option("--json", "Print a JSON envelope.")
3076
+ .action(async (options) => {
3077
+ await handleAsyncAction("linkedin accounts list", options, () => {
3078
+ const params = new URLSearchParams();
3079
+ if (options.usage !== false)
3080
+ params.set("include_usage", "true");
3081
+ const status = readOption(options.status);
3082
+ if (status)
3083
+ params.set("status", status);
3084
+ const suffix = params.toString();
3085
+ return requestOxygen(`/api/cli/linkedin/accounts${suffix ? `?${suffix}` : ""}`);
3086
+ });
3087
+ }))
3088
+ .addCommand(new Command("connect")
3089
+ .description("Get a Unipile hosted-auth URL to connect a new LinkedIn account (or reconnect with --reconnect). Open the URL in a browser to complete authentication.")
3090
+ .option("--reconnect <connection_id>", "Reconnect an existing connection instead of creating a new one. Accepts a connection id.")
3091
+ .option("--json", "Print a JSON envelope.")
3092
+ .action(async (options) => {
3093
+ await handleAsyncAction("linkedin accounts connect", options, () => {
3094
+ const reconnect = readOption(options.reconnect);
3095
+ return requestOxygen("/api/cli/linkedin/accounts/connect", {
3096
+ method: "POST",
3097
+ body: {
3098
+ ...(reconnect ? { reconnect_connection_id: reconnect } : {}),
3099
+ },
3100
+ });
3101
+ });
3102
+ }))
3103
+ .addCommand(new Command("get")
3104
+ .description("Get one LinkedIn sender account with status, limits, working hours, and usage. <id> accepts a sender account id, connection id, or Unipile account id.")
3105
+ .argument("<id>", "Sender account id, connection id, or Unipile account id.")
3106
+ .option("--json", "Print a JSON envelope.")
3107
+ .action(async (id, options) => {
3108
+ await handleAsyncAction("linkedin accounts get", options, async () => requestOxygen(`/api/cli/linkedin/accounts/${encodeURIComponent(id)}`));
3109
+ }))
3110
+ .addCommand(new Command("sync")
3111
+ .description("Refresh LinkedIn account state (status, profile) from Unipile. Pass --connection-id to sync one account, or omit it to sync all.")
3112
+ .option("--connection-id <id>", "Sync a specific connection id. Defaults to syncing all connected accounts.")
3113
+ .option("--json", "Print a JSON envelope.")
3114
+ .action(async (options) => {
3115
+ await handleAsyncAction("linkedin accounts sync", options, () => {
3116
+ const connectionId = readOption(options.connectionId);
3117
+ return requestOxygen("/api/cli/linkedin/accounts/sync", {
3118
+ method: "POST",
3119
+ body: {
3120
+ ...(connectionId ? { connection_id: connectionId } : {}),
3121
+ },
3122
+ });
3123
+ });
3124
+ }))
3125
+ .addCommand(new Command("disconnect")
3126
+ .description("Disconnect a LinkedIn sender account so it stops sending. <id> accepts a sender account id, connection id, or Unipile account id.")
3127
+ .argument("<id>", "Sender account id, connection id, or Unipile account id.")
3128
+ .option("--json", "Print a JSON envelope.")
3129
+ .action(async (id, options) => {
3130
+ await handleAsyncAction("linkedin accounts disconnect", options, async () => requestOxygen(`/api/cli/linkedin/accounts/${encodeURIComponent(id)}/disconnect`, {
3131
+ method: "POST",
3132
+ }));
3133
+ }))
3134
+ .addCommand(new Command("limits")
3135
+ .description("View and adjust per-account daily action limits and working hours.")
3136
+ .option("--json", "Print a JSON envelope.")
3137
+ .addCommand(new Command("get")
3138
+ .description("Show current limits, overrides, working hours, defaults, and safe maximums for an account. <id> accepts a sender account id, connection id, or Unipile account id.")
3139
+ .argument("<id>", "Sender account id, connection id, or Unipile account id.")
3140
+ .option("--json", "Print a JSON envelope.")
3141
+ .action(async (id, options) => {
3142
+ await handleAsyncAction("linkedin accounts limits get", options, async () => requestOxygen(`/api/cli/linkedin/accounts/${encodeURIComponent(id)}/limits`));
3143
+ }))
3144
+ .addCommand(new Command("set")
3145
+ .description("Adjust per-account daily action limits and working hours. Values are clamped to safe maximums (e.g. max 80 invites/day). <id> accepts a sender account id, connection id, or Unipile account id.")
3146
+ .argument("<id>", "Sender account id, connection id, or Unipile account id.")
3147
+ .option("--invites-per-day <n>", "Daily LinkedIn connection invites cap.")
3148
+ .option("--invites-per-week <n>", "Weekly LinkedIn connection invites cap.")
3149
+ .option("--messages-per-day <n>", "Daily direct messages cap.")
3150
+ .option("--inmails-per-day <n>", "Daily InMail cap.")
3151
+ .option("--profile-views-per-day <n>", "Daily profile views cap.")
3152
+ .option("--follows-per-day <n>", "Daily follows cap.")
3153
+ .option("--likes-per-day <n>", "Daily likes cap.")
3154
+ .option("--total-actions-per-day <n>", "Daily cap across all action types.")
3155
+ .option("--min-spacing-seconds <n>", "Minimum seconds between actions.")
3156
+ .option("--spacing-jitter-seconds <n>", "Random jitter seconds added to action spacing.")
3157
+ .option("--timezone <tz>", "IANA timezone for working hours, e.g. America/New_York.")
3158
+ .option("--working-days <days>", "Comma-separated ISO weekdays the account sends, e.g. 1,2,3,4,5 (1=Mon..7=Sun).")
3159
+ .option("--working-start <HH:MM>", "Working hours start time, e.g. 09:00.")
3160
+ .option("--working-end <HH:MM>", "Working hours end time, e.g. 17:00.")
3161
+ .option("--json", "Print a JSON envelope.")
3162
+ .action(async (id, options) => {
3163
+ await handleAsyncAction("linkedin accounts limits set", options, () => {
3164
+ const body = buildLinkedinLimitsBody(options);
3165
+ return requestOxygen(`/api/cli/linkedin/accounts/${encodeURIComponent(id)}/limits`, {
3166
+ method: "PATCH",
3167
+ body,
3168
+ });
3169
+ });
3170
+ }))))
3171
+ .addCommand(new Command("inbox")
3172
+ .description("LinkedIn unified inbox (unibox): scan conversations across all sender accounts, read threads, and reply.")
3173
+ .addCommand(new Command("list")
3174
+ .description("List LinkedIn conversations across all connected accounts, newest first.")
3175
+ .option("--account <id>", "Filter to one sender account (sender id, connection id, or Unipile account id).")
3176
+ .option("--unread", "Only show conversations with unread messages.")
3177
+ .option("--search <text>", "Filter by attendee name or last-message text.")
3178
+ .option("--include-archived", "Include archived conversations.")
3179
+ .option("--limit <n>", "Maximum conversations to return (1-200). Defaults to 50.")
3180
+ .option("--json", "Print a JSON envelope.")
3181
+ .action(async (options) => {
3182
+ await handleAsyncAction("linkedin inbox list", options, () => {
3183
+ const params = new URLSearchParams();
3184
+ const account = readOption(options.account);
3185
+ if (account)
3186
+ params.set("account", account);
3187
+ if (options.unread)
3188
+ params.set("unread", "true");
3189
+ const search = readOption(options.search);
3190
+ if (search)
3191
+ params.set("search", search);
3192
+ if (options.includeArchived)
3193
+ params.set("include_archived", "true");
3194
+ const limit = readOption(options.limit);
3195
+ if (limit)
3196
+ params.set("limit", limit);
3197
+ const suffix = params.toString();
3198
+ return requestOxygen(`/api/cli/linkedin/inbox${suffix ? `?${suffix}` : ""}`);
3199
+ });
3200
+ }))
3201
+ .addCommand(new Command("get")
3202
+ .description("Get one conversation with its full message thread. <conversation> accepts a conversation id or Unipile chat id.")
3203
+ .argument("<conversation>", "Conversation id or Unipile chat id.")
3204
+ .option("--message-limit <n>", "Maximum messages to return (1-500). Defaults to 100.")
3205
+ .option("--json", "Print a JSON envelope.")
3206
+ .action(async (conversation, options) => {
3207
+ await handleAsyncAction("linkedin inbox get", options, () => {
3208
+ const params = new URLSearchParams();
3209
+ const messageLimit = readOption(options.messageLimit);
3210
+ if (messageLimit)
3211
+ params.set("message_limit", messageLimit);
3212
+ const suffix = params.toString();
3213
+ return requestOxygen(`/api/cli/linkedin/inbox/${encodeURIComponent(conversation)}${suffix ? `?${suffix}` : ""}`);
3214
+ });
3215
+ }))
3216
+ .addCommand(new Command("send")
3217
+ .description("Reply into a LinkedIn conversation. Sends a real LinkedIn message — requires --approved. Without it, returns a preview.")
3218
+ .argument("<conversation>", "Conversation id or Unipile chat id.")
3219
+ .requiredOption("--text <message>", "Reply text to send.")
3220
+ .option("--approved", "Approve and send the message. Without this flag, returns a preview only.")
3221
+ .option("--json", "Print a JSON envelope.")
3222
+ .action(async (conversation, options) => {
3223
+ await handleAsyncAction("linkedin inbox send", options, () => requestOxygen(`/api/cli/linkedin/inbox/${encodeURIComponent(conversation)}/send`, {
3224
+ method: "POST",
3225
+ body: {
3226
+ text: readOption(options.text),
3227
+ ...(options.approved ? { approved: true } : {}),
3228
+ },
3229
+ }));
3230
+ }))
3231
+ .addCommand(new Command("mark-read")
3232
+ .description("Mark a conversation and all its messages as read.")
3233
+ .argument("<conversation>", "Conversation id or Unipile chat id.")
3234
+ .option("--json", "Print a JSON envelope.")
3235
+ .action(async (conversation, options) => {
3236
+ await handleAsyncAction("linkedin inbox mark-read", options, () => requestOxygen(`/api/cli/linkedin/inbox/${encodeURIComponent(conversation)}/read`, {
3237
+ method: "POST",
3238
+ }));
3239
+ }))
3240
+ .addCommand(new Command("sync")
3241
+ .description("Force a backstop inbox sync from Unipile for all active sender accounts (pulls recent chats + messages into the unibox).")
3242
+ .option("--chat-limit <n>", "Maximum chats to sync per account. Defaults to 30.")
3243
+ .option("--message-limit <n>", "Maximum messages to sync per chat. Defaults to 20.")
3244
+ .option("--json", "Print a JSON envelope.")
3245
+ .action(async (options) => {
3246
+ await handleAsyncAction("linkedin inbox sync", options, () => {
3247
+ const body = {};
3248
+ const chatLimit = readOption(options.chatLimit);
3249
+ if (chatLimit)
3250
+ body.chat_limit = Number(chatLimit);
3251
+ const messageLimit = readOption(options.messageLimit);
3252
+ if (messageLimit)
3253
+ body.message_limit = Number(messageLimit);
3254
+ return requestOxygen("/api/cli/linkedin/inbox/sync", { method: "POST", body });
3255
+ });
3256
+ })));
3013
3257
  program
3014
3258
  .command("workflows")
3015
3259
  .description("Durable workflow automation commands.")
@@ -3849,21 +4093,94 @@ function tableRunsListPath(options) {
3849
4093
  }
3850
4094
  async function requestColumnsRun(body, table, options) {
3851
4095
  const traceId = randomUUID();
4096
+ let result;
3852
4097
  try {
3853
- return await requestOxygen("/api/cli/tables/columns/run", {
4098
+ result = await requestOxygen("/api/cli/tables/columns/run", {
3854
4099
  method: "POST",
3855
4100
  body,
3856
4101
  traceId,
3857
4102
  });
3858
4103
  }
3859
4104
  catch (error) {
3860
- if (!options.background || !isNetworkTimeoutError(error))
4105
+ // Paid runs are durable background runs server-side (even single-row since
4106
+ // v1.144.0), so a network timeout is always recoverable by locating the
4107
+ // created run via its trace id - not only when --background was passed.
4108
+ // Inline (formula) runs have no run to recover; recovery returns null and
4109
+ // the original error propagates.
4110
+ if (!isNetworkTimeoutError(error))
3861
4111
  throw error;
3862
4112
  const recovered = await recoverBackgroundColumnRun(table, traceId);
3863
4113
  if (!recovered)
3864
4114
  throw error;
3865
4115
  return recovered;
3866
4116
  }
4117
+ // "Run this cell" still resolves to the finished value in one command: when
4118
+ // the server auto-backgrounds a single-row paid run (caller did not pass
4119
+ // --background), wait for the created run and attach its item output.
4120
+ // Inline (formula) results carry no action_run_id and pass through as-is.
4121
+ if (!options.background && typeof body.row_id === "string" && isRecord(result)) {
4122
+ const actionRunId = readRecordString(result, "action_run_id");
4123
+ if (actionRunId)
4124
+ return resolveSingleRowColumnRun(result, actionRunId);
4125
+ }
4126
+ return result;
4127
+ }
4128
+ /**
4129
+ * Wait for an auto-backgrounded single-row column run to finish and return the
4130
+ * terminal run merged with its item output (the cell value). On wait timeout
4131
+ * the queued run envelope is returned unchanged, plus the follow-up command -
4132
+ * the run keeps executing server-side either way.
4133
+ */
4134
+ async function resolveSingleRowColumnRun(envelope, actionRunId) {
4135
+ const timeoutSeconds = readEnvPositiveInt("OXYGEN_COLUMN_RUN_WAIT_TIMEOUT_SECONDS")
4136
+ ?? SINGLE_ROW_COLUMN_RUN_WAIT_DEFAULT_TIMEOUT_SECONDS;
4137
+ const intervalSeconds = readEnvPositiveInt("OXYGEN_COLUMN_RUN_WAIT_INTERVAL_SECONDS")
4138
+ ?? SINGLE_ROW_COLUMN_RUN_WAIT_DEFAULT_INTERVAL_SECONDS;
4139
+ let waited;
4140
+ try {
4141
+ waited = await waitForTableActionRun(actionRunId, {
4142
+ timeoutSeconds: String(timeoutSeconds),
4143
+ intervalSeconds: String(intervalSeconds),
4144
+ });
4145
+ }
4146
+ catch (error) {
4147
+ if (error instanceof OxygenError && error.code === "table_action_run_wait_timeout") {
4148
+ return {
4149
+ ...envelope,
4150
+ auto_wait_timed_out: true,
4151
+ next_step: `oxygen table-runs wait ${actionRunId}`,
4152
+ };
4153
+ }
4154
+ throw error;
4155
+ }
4156
+ const finalRun = isRecord(waited.actionRun) ? waited.actionRun : envelope;
4157
+ const items = await listColumnRunItems(actionRunId);
4158
+ return {
4159
+ ...finalRun,
4160
+ ...(items ? { items } : {}),
4161
+ auto_waited: {
4162
+ polls: waited.polls,
4163
+ elapsed_ms: waited.elapsedMs,
4164
+ },
4165
+ };
4166
+ }
4167
+ async function listColumnRunItems(actionRunId) {
4168
+ try {
4169
+ const response = await requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(actionRunId)}/items?limit=5`);
4170
+ const items = Array.isArray(response.items) ? response.items.filter(isRecord) : [];
4171
+ return items.length > 0 ? items : null;
4172
+ }
4173
+ catch {
4174
+ // The terminal run state is the answer; item output is best-effort.
4175
+ return null;
4176
+ }
4177
+ }
4178
+ function readEnvPositiveInt(name) {
4179
+ const value = process.env[name]?.trim();
4180
+ if (!value)
4181
+ return undefined;
4182
+ const parsed = Number(value);
4183
+ return Number.isInteger(parsed) && parsed >= 1 ? parsed : undefined;
3867
4184
  }
3868
4185
  async function recoverBackgroundColumnRun(table, traceId) {
3869
4186
  for (let attempt = 0; attempt < 3; attempt += 1) {
@@ -6306,6 +6623,62 @@ table, options) {
6306
6623
  ...(options.onlyMissing ? { only_missing: true } : {}),
6307
6624
  };
6308
6625
  }
6626
+ function readWorkingDaysOption(value) {
6627
+ if (!readOption(value))
6628
+ return undefined;
6629
+ const days = readCsvOption(value).map((entry) => {
6630
+ const parsed = Number(entry);
6631
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 7) {
6632
+ throw new OxygenError("invalid_request", "--working-days must be comma-separated ISO weekdays 1-7 (1=Mon..7=Sun).", {
6633
+ details: { value },
6634
+ exitCode: 1,
6635
+ });
6636
+ }
6637
+ return parsed;
6638
+ });
6639
+ return days;
6640
+ }
6641
+ function buildLinkedinLimitsBody(// skipcq: JS-R1005 -- CLI body builder maps per-account limit and working-hours flags.
6642
+ options) {
6643
+ const limits = {};
6644
+ const setLimit = (key, value) => {
6645
+ const parsed = readNonNegativeInt(value);
6646
+ if (parsed !== undefined)
6647
+ limits[key] = parsed;
6648
+ };
6649
+ setLimit("invites_per_day", options.invitesPerDay);
6650
+ setLimit("invites_per_week", options.invitesPerWeek);
6651
+ setLimit("messages_per_day", options.messagesPerDay);
6652
+ setLimit("inmails_per_day", options.inmailsPerDay);
6653
+ setLimit("profile_views_per_day", options.profileViewsPerDay);
6654
+ setLimit("follows_per_day", options.followsPerDay);
6655
+ setLimit("likes_per_day", options.likesPerDay);
6656
+ setLimit("total_actions_per_day", options.totalActionsPerDay);
6657
+ setLimit("min_action_spacing_seconds", options.minSpacingSeconds);
6658
+ setLimit("action_spacing_jitter_seconds", options.spacingJitterSeconds);
6659
+ const workingHours = {};
6660
+ const timezone = readOption(options.timezone);
6661
+ if (timezone)
6662
+ workingHours.timezone = timezone;
6663
+ const days = readWorkingDaysOption(options.workingDays);
6664
+ if (days !== undefined)
6665
+ workingHours.days = days;
6666
+ const start = readOption(options.workingStart);
6667
+ if (start)
6668
+ workingHours.start = start;
6669
+ const end = readOption(options.workingEnd);
6670
+ if (end)
6671
+ workingHours.end = end;
6672
+ const hasLimits = Object.keys(limits).length > 0;
6673
+ const hasWorkingHours = Object.keys(workingHours).length > 0;
6674
+ if (!hasLimits && !hasWorkingHours) {
6675
+ throw new OxygenError("invalid_request", "Pass at least one limit flag (e.g. --invites-per-day) or working-hours flag (e.g. --timezone, --working-days).", { exitCode: 1 });
6676
+ }
6677
+ return {
6678
+ ...(hasLimits ? { limits } : {}),
6679
+ ...(hasWorkingHours ? { working_hours: workingHours } : {}),
6680
+ };
6681
+ }
6309
6682
  function readPositiveInt(value) {
6310
6683
  const trimmed = value?.trim();
6311
6684
  if (!trimmed)
@@ -1,4 +1,5 @@
1
1
  export { OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_VERSION } from "./version.js";
2
+ export { WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS, clearWorkflowTriggerAutoPauseMetadata, } from "./workflow-trigger-metadata.js";
2
3
  export * from "./billing.js";
3
4
  export * from "./cell-format.js";
4
5
  export * from "./column-types.js";
@@ -46,3 +47,24 @@ export declare function failure(command: string, error: {
46
47
  details?: unknown;
47
48
  }, version?: string, minimumCliVersion?: string): CliFailure;
48
49
  export declare function toFailure(command: string, error: unknown, version?: string): CliFailure;
50
+ export type SemanticVersion = {
51
+ major: number;
52
+ minor: number;
53
+ patch: number;
54
+ };
55
+ /**
56
+ * Parse a three-segment semantic version (e.g. `1.142.17`). Pre-release and
57
+ * build metadata suffixes (`-rc.1`, `+build`) are tolerated but ignored.
58
+ * Returns `null` when the input is not a parseable `major.minor.patch` string.
59
+ */
60
+ export declare function parseSemver(version: string): SemanticVersion | null;
61
+ /**
62
+ * Compare two semantic versions. Returns -1 when `a < b`, 1 when `a > b`, and
63
+ * 0 when they are equal. Unparseable inputs compare as equal (0) so callers
64
+ * fail open rather than misordering garbage.
65
+ */
66
+ export declare function compareSemver(a: string, b: string): -1 | 0 | 1;
67
+ /** True when `a` is a strictly greater semantic version than `b`. */
68
+ export declare function isVersionGreater(a: string, b: string): boolean;
69
+ /** True when `a` is a strictly lesser semantic version than `b`. */
70
+ export declare function isVersionLess(a: string, b: string): boolean;
@@ -1,5 +1,6 @@
1
1
  import { OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_VERSION } from "./version.js";
2
2
  export { OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_VERSION } from "./version.js";
3
+ export { WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS, clearWorkflowTriggerAutoPauseMetadata, } from "./workflow-trigger-metadata.js";
3
4
  export * from "./billing.js";
4
5
  export * from "./cell-format.js";
5
6
  export * from "./column-types.js";
@@ -51,3 +52,44 @@ export function toFailure(command, error, version = OXYGEN_VERSION) {
51
52
  }
52
53
  return failure(command, { code: "unexpected_error", message: "An unexpected error occurred." }, version);
53
54
  }
55
+ /**
56
+ * Parse a three-segment semantic version (e.g. `1.142.17`). Pre-release and
57
+ * build metadata suffixes (`-rc.1`, `+build`) are tolerated but ignored.
58
+ * Returns `null` when the input is not a parseable `major.minor.patch` string.
59
+ */
60
+ export function parseSemver(version) {
61
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version);
62
+ if (!match)
63
+ return null;
64
+ return {
65
+ major: Number(match[1]),
66
+ minor: Number(match[2]),
67
+ patch: Number(match[3]),
68
+ };
69
+ }
70
+ /**
71
+ * Compare two semantic versions. Returns -1 when `a < b`, 1 when `a > b`, and
72
+ * 0 when they are equal. Unparseable inputs compare as equal (0) so callers
73
+ * fail open rather than misordering garbage.
74
+ */
75
+ export function compareSemver(a, b) {
76
+ const left = parseSemver(a);
77
+ const right = parseSemver(b);
78
+ if (!left || !right)
79
+ return 0;
80
+ for (const key of ["major", "minor", "patch"]) {
81
+ if (left[key] > right[key])
82
+ return 1;
83
+ if (left[key] < right[key])
84
+ return -1;
85
+ }
86
+ return 0;
87
+ }
88
+ /** True when `a` is a strictly greater semantic version than `b`. */
89
+ export function isVersionGreater(a, b) {
90
+ return compareSemver(a, b) > 0;
91
+ }
92
+ /** True when `a` is a strictly lesser semantic version than `b`. */
93
+ export function isVersionLess(a, b) {
94
+ return compareSemver(a, b) < 0;
95
+ }
@@ -1,5 +1,8 @@
1
1
  export type TelemetryAttributes = Record<string, unknown>;
2
- export declare function withTelemetrySpan<T>(tracerName: string, name: string, attributes: TelemetryAttributes | undefined, fn: () => Promise<T>): Promise<T>;
2
+ export type WithTelemetrySpanOptions = {
3
+ isTransient?: (error: unknown) => boolean;
4
+ };
5
+ export declare function withTelemetrySpan<T>(tracerName: string, name: string, attributes: TelemetryAttributes | undefined, fn: () => Promise<T>, options?: WithTelemetrySpanOptions): Promise<T>;
3
6
  export declare function setActiveTelemetryAttributes(attributes: TelemetryAttributes): void;
4
7
  export declare function markActiveTelemetryError(message: string, attributes?: TelemetryAttributes): void;
5
8
  export declare function addTelemetryEvent(name: string, attributes?: TelemetryAttributes): void;
@@ -4,7 +4,7 @@ import { normalizeTelemetryAttributes } from "./redaction.js";
4
4
  import { OXYGEN_VERSION } from "./version.js";
5
5
  const counterCache = new Map();
6
6
  const histogramCache = new Map();
7
- export async function withTelemetrySpan(tracerName, name, attributes, fn) {
7
+ export async function withTelemetrySpan(tracerName, name, attributes, fn, options) {
8
8
  const tracer = trace.getTracer(tracerName, OXYGEN_VERSION);
9
9
  return tracer.startActiveSpan(name, { attributes: normalizeTelemetryAttributes(commonTelemetryAttributes(attributes)) }, async (span) => {
10
10
  try {
@@ -12,8 +12,16 @@ export async function withTelemetrySpan(tracerName, name, attributes, fn) {
12
12
  }
13
13
  catch (error) {
14
14
  span.recordException(error instanceof Error ? error : new Error(String(error)));
15
- span.setStatus({ code: SpanStatusCode.ERROR });
16
- span.setAttributes(normalizeTelemetryAttributes(errorTelemetryAttributes(error)));
15
+ if (options?.isTransient?.(error) === true) {
16
+ span.setAttributes(normalizeTelemetryAttributes({
17
+ ...errorTelemetryAttributes(error),
18
+ outcome: "transient_error",
19
+ }));
20
+ }
21
+ else {
22
+ span.setStatus({ code: SpanStatusCode.ERROR });
23
+ span.setAttributes(normalizeTelemetryAttributes(errorTelemetryAttributes(error)));
24
+ }
17
25
  throw error;
18
26
  }
19
27
  finally {
@@ -97,14 +105,18 @@ function getHistogram(name) {
97
105
  }
98
106
  function errorTelemetryAttributes(error) {
99
107
  if (error instanceof Error) {
108
+ // pg connect timeouts (and some driver errors) surface with an empty
109
+ // message; fall back to the error name so spans are never message-less.
110
+ const message = error.message.trim() ? error.message : error.name || "unknown_error";
100
111
  return {
101
112
  "error.id": errorId(error),
102
113
  "error.name": error.name,
103
- "error.message": error.message,
114
+ "error.message": message,
104
115
  };
105
116
  }
117
+ const text = String(error).trim();
106
118
  return {
107
119
  "error.id": "non_error",
108
- "error.message": String(error),
120
+ "error.message": text || "unknown_error",
109
121
  };
110
122
  }
@@ -1,2 +1,2 @@
1
- export declare const OXYGEN_VERSION = "1.142.4";
1
+ export declare const OXYGEN_VERSION = "1.146.1";
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.135.0";
@@ -1,3 +1,3 @@
1
- export const OXYGEN_VERSION = "1.142.4";
1
+ export const OXYGEN_VERSION = "1.146.1";
2
2
  // Bump this only when deployed CLI/API contracts require a newer CLI.
3
3
  export const OXYGEN_MINIMUM_CLI_VERSION = "1.135.0";
@@ -0,0 +1,2 @@
1
+ export declare const WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS: readonly ["consecutive_failure_count", "last_failure_code", "last_failure_at", "auto_paused_at", "auto_pause_reason"];
2
+ export declare function clearWorkflowTriggerAutoPauseMetadata(metadata: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,14 @@
1
+ export const WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS = [
2
+ "consecutive_failure_count",
3
+ "last_failure_code",
4
+ "last_failure_at",
5
+ "auto_paused_at",
6
+ "auto_pause_reason",
7
+ ];
8
+ export function clearWorkflowTriggerAutoPauseMetadata(metadata) {
9
+ const next = { ...metadata };
10
+ for (const key of WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS) {
11
+ delete next[key];
12
+ }
13
+ return next;
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.142.4",
3
+ "version": "1.146.1",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",