@oxygen-agent/cli 1.139.10 → 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 +1 -1
- package/dist/http-client.js +4 -26
- package/dist/index.js +467 -12
- package/node_modules/@oxygen/shared/dist/index.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/index.js +42 -0
- package/node_modules/@oxygen/shared/dist/telemetry.d.ts +4 -1
- package/node_modules/@oxygen/shared/dist/telemetry.js +17 -5
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/http-client.js
CHANGED
|
@@ -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 {
|
|
@@ -420,9 +443,13 @@ export function createProgram() {
|
|
|
420
443
|
}))
|
|
421
444
|
.addCommand(new Command("migrate")
|
|
422
445
|
.description("Apply pending tenant database migrations for the current organization.")
|
|
446
|
+
.option("--rotate-credentials", "Rotate the tenant runtime/read DB passwords and rewrite stored credentials. Use only to repair a tenant whose stored credentials are out of sync; routine migrations do not need it.")
|
|
423
447
|
.option("--json", "Print a JSON envelope.")
|
|
424
448
|
.action(async (options) => {
|
|
425
|
-
await handleAsyncAction("db migrate", options, async () => requestOxygen("/api/cli/db/migrate", {
|
|
449
|
+
await handleAsyncAction("db migrate", options, async () => requestOxygen("/api/cli/db/migrate", {
|
|
450
|
+
method: "POST",
|
|
451
|
+
body: options.rotateCredentials ? { rotate_credentials: true } : {},
|
|
452
|
+
}));
|
|
426
453
|
}))
|
|
427
454
|
.addCommand(new Command("migrate-all")
|
|
428
455
|
.description("Apply pending tenant database migrations across all ready tenants (staff only).")
|
|
@@ -475,6 +502,32 @@ export function createProgram() {
|
|
|
475
502
|
},
|
|
476
503
|
});
|
|
477
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
|
+
});
|
|
478
531
|
}))
|
|
479
532
|
.addCommand(new Command("cost-policy")
|
|
480
533
|
.description("Show tenant database cost controls and reconciliation status.")
|
|
@@ -1732,10 +1785,12 @@ export function createProgram() {
|
|
|
1732
1785
|
.option("--max-credits <n>", "Maximum managed/provider credits to reserve for this run.")
|
|
1733
1786
|
.option("--max-concurrency <n>", "Maximum concurrent row items for this run.")
|
|
1734
1787
|
.option("--metadata-json <json>", "Optional metadata object to attach to the run.")
|
|
1788
|
+
.option("--then-json <json>", "JSON array of sequential follow-up steps. Each step runs after the previous terminates with completed or completed_with_errors. Paid steps require selection and max_credits. Shape: [{actions:[{type:'tool_column',column}],selection,force,max_concurrency,max_credits,metadata,run_on_failure}].")
|
|
1735
1789
|
.option("--json", "Print a JSON envelope.")
|
|
1736
1790
|
.action(async (table, options) => {
|
|
1737
1791
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
1738
1792
|
const maxConcurrency = readPositiveInt(options.maxConcurrency);
|
|
1793
|
+
const chainSteps = readChainStepsOption(options.thenJson);
|
|
1739
1794
|
await handleAsyncAction("table-runs create", options, async () => requestOxygen("/api/cli/table-action-runs", {
|
|
1740
1795
|
method: "POST",
|
|
1741
1796
|
body: {
|
|
@@ -1747,6 +1802,7 @@ export function createProgram() {
|
|
|
1747
1802
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
1748
1803
|
...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
|
|
1749
1804
|
...(options.metadataJson ? { metadata: parseJsonObject(options.metadataJson) } : {}),
|
|
1805
|
+
...(chainSteps.length > 0 ? { then: chainSteps } : {}),
|
|
1750
1806
|
},
|
|
1751
1807
|
}));
|
|
1752
1808
|
}))
|
|
@@ -2276,22 +2332,50 @@ export function createProgram() {
|
|
|
2276
2332
|
await handleAsyncAction("billing balance", options, async () => requestOxygen("/api/cli/billing/balance"));
|
|
2277
2333
|
}))
|
|
2278
2334
|
.addCommand(new Command("usage")
|
|
2279
|
-
.description("Show
|
|
2335
|
+
.description("Show credit ledger events.")
|
|
2280
2336
|
.option("--days <n>", "Lookback window in days. Defaults to 30.")
|
|
2337
|
+
.option("--from <iso>", "Only include ledger events at or after this ISO timestamp.")
|
|
2338
|
+
.option("--to <iso>", "Only include ledger events at or before this ISO timestamp.")
|
|
2281
2339
|
.option("--limit <n>", "Maximum events to return. Defaults to 50.")
|
|
2340
|
+
.option("--cursor <cursor>", "Opaque cursor from a previous usage response.")
|
|
2341
|
+
.option("--type <type>", "Filter by transaction type, such as reserve, capture, release, grant, or byok_usage.")
|
|
2342
|
+
.option("--category <category>", "Filter by transaction category, such as managed_enrichment, managed_ai, byok, subscription, or admin.")
|
|
2343
|
+
.option("--provider <provider>", "Filter by provider id.")
|
|
2344
|
+
.option("--source <source_id>", "Filter by source_id/provider operation.")
|
|
2345
|
+
.option("--source-id <source_id>", "Alias for --source.")
|
|
2346
|
+
.option("--run-id <run_id>", "Filter by run id recorded in ledger metadata.")
|
|
2347
|
+
.option("--nonzero", "Only include events that changed available or reserved credits.")
|
|
2282
2348
|
.option("--json", "Print a JSON envelope.")
|
|
2283
2349
|
.action(async (options) => {
|
|
2284
2350
|
await handleAsyncAction("billing usage", options, async () => {
|
|
2285
|
-
const params =
|
|
2286
|
-
const days = readPositiveInt(options.days);
|
|
2287
|
-
const limit = readPositiveInt(options.limit);
|
|
2288
|
-
if (days)
|
|
2289
|
-
params.set("days", String(days));
|
|
2290
|
-
if (limit)
|
|
2291
|
-
params.set("limit", String(limit));
|
|
2351
|
+
const params = buildBillingLedgerParams(options);
|
|
2292
2352
|
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
2293
2353
|
return requestOxygen(`/api/cli/billing/usage${suffix}`);
|
|
2294
2354
|
});
|
|
2355
|
+
}))
|
|
2356
|
+
.addCommand(new Command("audit")
|
|
2357
|
+
.description("Summarize where managed credits were granted, reserved, captured, released, or spent.")
|
|
2358
|
+
.option("--days <n>", "Lookback window in days. Defaults to 365.")
|
|
2359
|
+
.option("--from <iso>", "Only include ledger events at or after this ISO timestamp.")
|
|
2360
|
+
.option("--to <iso>", "Only include ledger events at or before this ISO timestamp.")
|
|
2361
|
+
.option("--limit <n>", "Maximum grouped rows to return. Defaults to 100.")
|
|
2362
|
+
.option("--group-by <keys>", "Comma-separated grouping keys. Defaults to provider,source.")
|
|
2363
|
+
.option("--type <type>", "Filter by transaction type, such as reserve, capture, release, grant, or byok_usage.")
|
|
2364
|
+
.option("--category <category>", "Filter by transaction category, such as managed_enrichment, managed_ai, byok, subscription, or admin.")
|
|
2365
|
+
.option("--provider <provider>", "Filter by provider id.")
|
|
2366
|
+
.option("--source <source_id>", "Filter by source_id/provider operation.")
|
|
2367
|
+
.option("--source-id <source_id>", "Alias for --source.")
|
|
2368
|
+
.option("--run-id <run_id>", "Filter by run id recorded in ledger metadata.")
|
|
2369
|
+
.option("--nonzero", "Only include events that changed available or reserved credits. Default for audit except BYOK filters.")
|
|
2370
|
+
.option("--json", "Print a JSON envelope.")
|
|
2371
|
+
.action(async (options) => {
|
|
2372
|
+
await handleAsyncAction("billing audit", options, async () => {
|
|
2373
|
+
const params = buildBillingLedgerParams(options);
|
|
2374
|
+
if (readOption(options.groupBy))
|
|
2375
|
+
params.set("group_by", readOption(options.groupBy));
|
|
2376
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
2377
|
+
return requestOxygen(`/api/cli/billing/audit${suffix}`);
|
|
2378
|
+
});
|
|
2295
2379
|
}))
|
|
2296
2380
|
.addCommand(new Command("grant")
|
|
2297
2381
|
.description("Grant managed credits to the current organization (staff only).")
|
|
@@ -2381,7 +2465,11 @@ export function createProgram() {
|
|
|
2381
2465
|
.description("Redacted operation event commands for the current organization.")
|
|
2382
2466
|
.addCommand(new Command("events")
|
|
2383
2467
|
.description("List recent redacted operation events and failures.")
|
|
2384
|
-
|
|
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.")
|
|
2385
2473
|
.option("--trace-id <trace_id>", "Filter by trace id.")
|
|
2386
2474
|
.option("--run-id <run_id>", "Filter by workspace run id.")
|
|
2387
2475
|
.option("--limit <n>", "Maximum events to return. Defaults to 50.")
|
|
@@ -2975,6 +3063,197 @@ export function createProgram() {
|
|
|
2975
3063
|
});
|
|
2976
3064
|
});
|
|
2977
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
|
+
})));
|
|
2978
3257
|
program
|
|
2979
3258
|
.command("workflows")
|
|
2980
3259
|
.description("Durable workflow automation commands.")
|
|
@@ -3753,6 +4032,24 @@ function readTableRunActions(options) {
|
|
|
3753
4032
|
exitCode: 1,
|
|
3754
4033
|
});
|
|
3755
4034
|
}
|
|
4035
|
+
// Parse --then-json into an array of chain steps. Server validates the shape;
|
|
4036
|
+
// we only enforce that the top-level value is an array of objects so users get
|
|
4037
|
+
// a clear local error before round-tripping to the API.
|
|
4038
|
+
function readChainStepsOption(value) {
|
|
4039
|
+
if (!value)
|
|
4040
|
+
return [];
|
|
4041
|
+
const parsed = parseJsonArray(value);
|
|
4042
|
+
for (let i = 0; i < parsed.length; i += 1) {
|
|
4043
|
+
const entry = parsed[i];
|
|
4044
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
4045
|
+
throw new OxygenError("invalid_table_run", "--then-json entries must be objects.", {
|
|
4046
|
+
details: { index: i },
|
|
4047
|
+
exitCode: 1,
|
|
4048
|
+
});
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
return parsed;
|
|
4052
|
+
}
|
|
3756
4053
|
function readTableRunSelection(options) {
|
|
3757
4054
|
const hasAll = Boolean(options.all);
|
|
3758
4055
|
const limit = readPositiveInt(options.limit);
|
|
@@ -3796,21 +4093,94 @@ function tableRunsListPath(options) {
|
|
|
3796
4093
|
}
|
|
3797
4094
|
async function requestColumnsRun(body, table, options) {
|
|
3798
4095
|
const traceId = randomUUID();
|
|
4096
|
+
let result;
|
|
3799
4097
|
try {
|
|
3800
|
-
|
|
4098
|
+
result = await requestOxygen("/api/cli/tables/columns/run", {
|
|
3801
4099
|
method: "POST",
|
|
3802
4100
|
body,
|
|
3803
4101
|
traceId,
|
|
3804
4102
|
});
|
|
3805
4103
|
}
|
|
3806
4104
|
catch (error) {
|
|
3807
|
-
|
|
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))
|
|
3808
4111
|
throw error;
|
|
3809
4112
|
const recovered = await recoverBackgroundColumnRun(table, traceId);
|
|
3810
4113
|
if (!recovered)
|
|
3811
4114
|
throw error;
|
|
3812
4115
|
return recovered;
|
|
3813
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;
|
|
3814
4184
|
}
|
|
3815
4185
|
async function recoverBackgroundColumnRun(table, traceId) {
|
|
3816
4186
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
@@ -6143,6 +6513,35 @@ function contextAssetsQuery(options) {
|
|
|
6143
6513
|
const value = query.toString();
|
|
6144
6514
|
return value ? `?${value}` : "";
|
|
6145
6515
|
}
|
|
6516
|
+
function buildBillingLedgerParams(options) {
|
|
6517
|
+
const params = new URLSearchParams();
|
|
6518
|
+
const days = readPositiveInt(options.days);
|
|
6519
|
+
const limit = readPositiveInt(options.limit);
|
|
6520
|
+
if (days)
|
|
6521
|
+
params.set("days", String(days));
|
|
6522
|
+
if (limit)
|
|
6523
|
+
params.set("limit", String(limit));
|
|
6524
|
+
if (readOption(options.from))
|
|
6525
|
+
params.set("from", readOption(options.from));
|
|
6526
|
+
if (readOption(options.to))
|
|
6527
|
+
params.set("to", readOption(options.to));
|
|
6528
|
+
if (readOption(options.cursor))
|
|
6529
|
+
params.set("cursor", readOption(options.cursor));
|
|
6530
|
+
if (readOption(options.type))
|
|
6531
|
+
params.set("type", readOption(options.type));
|
|
6532
|
+
if (readOption(options.category))
|
|
6533
|
+
params.set("category", readOption(options.category));
|
|
6534
|
+
if (readOption(options.provider))
|
|
6535
|
+
params.set("provider", readOption(options.provider));
|
|
6536
|
+
const sourceId = readOption(options.sourceId) ?? readOption(options.source);
|
|
6537
|
+
if (sourceId)
|
|
6538
|
+
params.set("source_id", sourceId);
|
|
6539
|
+
if (readOption(options.runId))
|
|
6540
|
+
params.set("run_id", readOption(options.runId));
|
|
6541
|
+
if (options.nonzero)
|
|
6542
|
+
params.set("nonzero", "true");
|
|
6543
|
+
return params;
|
|
6544
|
+
}
|
|
6146
6545
|
function buildContextResolveBody(options) {
|
|
6147
6546
|
const assetTypes = readCsvOption(options.assetType);
|
|
6148
6547
|
const tags = readCsvOption(options.tags);
|
|
@@ -6224,6 +6623,62 @@ table, options) {
|
|
|
6224
6623
|
...(options.onlyMissing ? { only_missing: true } : {}),
|
|
6225
6624
|
};
|
|
6226
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
|
+
}
|
|
6227
6682
|
function readPositiveInt(value) {
|
|
6228
6683
|
const trimmed = value?.trim();
|
|
6229
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
|
|
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
|
-
|
|
16
|
-
|
|
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":
|
|
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":
|
|
120
|
+
"error.message": text || "unknown_error",
|
|
109
121
|
};
|
|
110
122
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const OXYGEN_VERSION = "1.
|
|
1
|
+
export declare const OXYGEN_VERSION = "1.146.1";
|
|
2
2
|
export declare 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
|
+
}
|