@oxygen-agent/cli 1.152.15 → 1.160.18
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 +2 -2
- package/dist/index.js +841 -127
- package/dist/transcript.d.ts +21 -0
- package/dist/transcript.js +208 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/index.js +2 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +54 -31
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +15 -219
- package/node_modules/@oxygen/shared/dist/log.js +12 -4
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +238 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +501 -0
- package/node_modules/@oxygen/shared/dist/sql-error.d.ts +43 -0
- package/node_modules/@oxygen/shared/dist/sql-error.js +318 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +26 -3
- package/node_modules/@oxygen/shared/dist/version.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/version.js +5 -2
- package/node_modules/@oxygen/workflows/dist/index.d.ts +0 -19
- package/node_modules/@oxygen/workflows/dist/index.js +16 -19
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,13 +10,14 @@ import { stdin as input, stdout as output } from "node:process";
|
|
|
10
10
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
11
|
import { Command, Option } from "commander";
|
|
12
12
|
import { formatCellForDisplay, OXYGEN_VERSION, OxygenError, success, toFailure } from "@oxygen/shared";
|
|
13
|
-
import { inferRowsFileFormat, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, } from "@oxygen/shared/file-import";
|
|
13
|
+
import { inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, } from "@oxygen/shared/file-import";
|
|
14
14
|
import { assertRecipeBundleSafe, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowManifest, } from "@oxygen/workflows";
|
|
15
15
|
import { isRecipeDefinition } from "@oxygen/recipe-sdk";
|
|
16
16
|
import { createBrowserLoginSession, openBrowser } from "./browser-login.js";
|
|
17
17
|
import { clearCredentials, defaultApiUrl, listCredentialProfiles, normalizeApiUrl, pickProfileNameForIdentity, pickProfileNameForUserSession, resolveActiveProfile, saveCredentials, switchCredentialProfile, updateActiveOrganizationForProfile, } from "./credentials.js";
|
|
18
18
|
import { ensureFreshCliForApiUrl, requestOxygen } from "./http-client.js";
|
|
19
19
|
import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
|
|
20
|
+
import { captureCurrentTranscript, collectFeedbackEnvironment, TranscriptCaptureError, } from "./transcript.js";
|
|
20
21
|
import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, updateSessionStep, } from "./session.js";
|
|
21
22
|
import { doctorAgentSkills, installAgentSkills, listAgentSkills, runAutomaticSkillsInstall, } from "./skills.js";
|
|
22
23
|
import { resolveCliBinaryName } from "./runtime.js";
|
|
@@ -426,6 +427,100 @@ export function createProgram() {
|
|
|
426
427
|
},
|
|
427
428
|
}));
|
|
428
429
|
}));
|
|
430
|
+
program
|
|
431
|
+
.command("support")
|
|
432
|
+
.description("File and track Oxygen support tickets.")
|
|
433
|
+
.addCommand(new Command("file")
|
|
434
|
+
.description("File a support ticket. Use when you're stuck on an Oxygen operation.")
|
|
435
|
+
.requiredOption("--subject <subject>", "One-line summary of the problem.")
|
|
436
|
+
.option("--body <body>", "What you were doing, what happened, and what you tried.")
|
|
437
|
+
.option("--severity <severity>", "low | normal | high. Defaults to normal.")
|
|
438
|
+
.option("--category <category>", "Optional category label.")
|
|
439
|
+
.option("--operation <operation>", "The operation/tool that failed, e.g. oxygen_columns_run.")
|
|
440
|
+
.option("--error-code <code>", "The error envelope code you received.")
|
|
441
|
+
.option("--run-ids <ids>", "Comma-separated run ids to attach.")
|
|
442
|
+
.option("--table-ids <ids>", "Comma-separated table ids to attach.")
|
|
443
|
+
.option("--deep-links <urls>", "Comma-separated https://oxygen-agent.com/... links.")
|
|
444
|
+
.option("--json", "Print a JSON envelope.")
|
|
445
|
+
.action(async (options) => {
|
|
446
|
+
await handleAsyncAction("support ticket create", options, () => requestOxygen("/api/cli/support/tickets", {
|
|
447
|
+
method: "POST",
|
|
448
|
+
body: buildSupportTicketBody(options),
|
|
449
|
+
}));
|
|
450
|
+
}))
|
|
451
|
+
.addCommand(new Command("list")
|
|
452
|
+
.description("List support tickets for the active organization.")
|
|
453
|
+
.option("--status <status>", "Filter by status (open, triaging, waiting_on_user, resolved, closed).")
|
|
454
|
+
.option("--limit <n>", "Max tickets to return.")
|
|
455
|
+
.option("--json", "Print a JSON envelope.")
|
|
456
|
+
.action(async (options) => {
|
|
457
|
+
await handleAsyncAction("support tickets list", options, () => requestOxygen(withSupportListQuery("/api/cli/support/tickets", options)));
|
|
458
|
+
}))
|
|
459
|
+
.addCommand(new Command("get")
|
|
460
|
+
.description("Show one support ticket with its message thread.")
|
|
461
|
+
.argument("<ticketId>", "Ticket UUID.")
|
|
462
|
+
.option("--json", "Print a JSON envelope.")
|
|
463
|
+
.action(async (ticketId, options) => {
|
|
464
|
+
await handleAsyncAction("support ticket get", options, () => requestOxygen(`/api/cli/support/tickets/${encodeURIComponent(ticketId)}`));
|
|
465
|
+
}))
|
|
466
|
+
.addCommand(new Command("reply")
|
|
467
|
+
.description("Add a message to a support ticket. Reopens a resolved ticket.")
|
|
468
|
+
.argument("<ticketId>", "Ticket UUID.")
|
|
469
|
+
.requiredOption("--body <body>", "Your reply.")
|
|
470
|
+
.option("--json", "Print a JSON envelope.")
|
|
471
|
+
.action(async (ticketId, options) => {
|
|
472
|
+
await handleAsyncAction("support ticket reply", options, () => requestOxygen(`/api/cli/support/tickets/${encodeURIComponent(ticketId)}/messages`, {
|
|
473
|
+
method: "POST",
|
|
474
|
+
body: { body: readOption(options.body) },
|
|
475
|
+
}));
|
|
476
|
+
}))
|
|
477
|
+
.addCommand(new Command("ack")
|
|
478
|
+
.description("Mark a resolved ticket as seen so it leaves the unread count.")
|
|
479
|
+
.argument("<ticketId>", "Ticket UUID.")
|
|
480
|
+
.option("--json", "Print a JSON envelope.")
|
|
481
|
+
.action(async (ticketId, options) => {
|
|
482
|
+
await handleAsyncAction("support ticket ack", options, () => requestOxygen(`/api/cli/support/tickets/${encodeURIComponent(ticketId)}/ack`, {
|
|
483
|
+
method: "POST",
|
|
484
|
+
body: {},
|
|
485
|
+
}));
|
|
486
|
+
}))
|
|
487
|
+
.addCommand(new Command("admin")
|
|
488
|
+
.description("Staff-only support triage commands.")
|
|
489
|
+
.addCommand(new Command("list")
|
|
490
|
+
.description("List support tickets across all organizations (staff only).")
|
|
491
|
+
.option("--status <status>", "Filter by status.")
|
|
492
|
+
.option("--limit <n>", "Max tickets to return.")
|
|
493
|
+
.option("--json", "Print a JSON envelope.")
|
|
494
|
+
.action(async (options) => {
|
|
495
|
+
await handleAsyncAction("support admin list", options, () => requestOxygen(withSupportListQuery("/api/cli/admin/support/tickets", options)));
|
|
496
|
+
}))
|
|
497
|
+
.addCommand(new Command("resolve")
|
|
498
|
+
.description("Resolve a support ticket and notify the opener (staff only).")
|
|
499
|
+
.argument("<ticketId>", "Ticket UUID.")
|
|
500
|
+
.requiredOption("--resolution <text>", "Resolution message sent to the opener.")
|
|
501
|
+
.option("--json", "Print a JSON envelope.")
|
|
502
|
+
.action(async (ticketId, options) => {
|
|
503
|
+
await handleAsyncAction("support admin resolve", options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}/resolve`, {
|
|
504
|
+
method: "POST",
|
|
505
|
+
body: { resolution: readOption(options.resolution) },
|
|
506
|
+
}));
|
|
507
|
+
})));
|
|
508
|
+
program
|
|
509
|
+
.command("feedback")
|
|
510
|
+
.description("Send feedback or a bug report to the Oxygen team, including your current chat transcript and environment info. Files a tracked ticket; you're notified of the reply by email and on your next session.")
|
|
511
|
+
.option("-m, --message <message>", "Your feedback or bug report.")
|
|
512
|
+
.option("--severity <severity>", "low | normal | high. Defaults to normal.")
|
|
513
|
+
.option("--category <category>", "Optional category label. Defaults to 'feedback'.")
|
|
514
|
+
.option("--session-id <id>", "Attach a specific local session transcript by id. Defaults to the most recently active session.")
|
|
515
|
+
.option("--file <path>", "Attach a specific transcript file instead of auto-detecting the current session.")
|
|
516
|
+
.option("--no-transcript", "Send your note only, without attaching any chat transcript.")
|
|
517
|
+
.option("--json", "Print a JSON envelope.")
|
|
518
|
+
.action(async (options) => {
|
|
519
|
+
await handleAsyncAction("feedback send", options, () => requestOxygen("/api/cli/feedback", {
|
|
520
|
+
method: "POST",
|
|
521
|
+
body: buildFeedbackBody(options),
|
|
522
|
+
}));
|
|
523
|
+
});
|
|
429
524
|
program
|
|
430
525
|
.command("db")
|
|
431
526
|
.description("Tenant database commands.")
|
|
@@ -2288,6 +2383,75 @@ export function createProgram() {
|
|
|
2288
2383
|
body: readCompaniesEnrichBody(table, options),
|
|
2289
2384
|
}));
|
|
2290
2385
|
})));
|
|
2386
|
+
program
|
|
2387
|
+
.command("people")
|
|
2388
|
+
.description("People and contact prospecting workflows.")
|
|
2389
|
+
.addCommand(new Command("search")
|
|
2390
|
+
.description("Plan, dry-run, or queue provider-backed people/contact search.")
|
|
2391
|
+
.addCommand(new Command("plan")
|
|
2392
|
+
.description("Compile a people-search prompt and optional typed persona filters into ordered provider routes without provider calls.")
|
|
2393
|
+
.requiredOption("--prompt <text-or-file>", "People-search prompt, or a path to a prompt file.")
|
|
2394
|
+
.option("--target-count <n>", "Desired contact count for routing and estimates.")
|
|
2395
|
+
.option("--source-intent <intent>", "Override detected intent: persona_search, account_contacts, audience_sizing, profile_lookup, concept_persona, or fallback_broad.")
|
|
2396
|
+
.option("--filters-json <json-or-file>", "PeopleSearchFilters JSON inline or a @file/path; wins over individual flags per top-level filter path.")
|
|
2397
|
+
.option("--titles <csv>", "Comma-separated job titles to include.")
|
|
2398
|
+
.option("--adjacent-titles <csv>", "Comma-separated adjacent/looser titles to accept.")
|
|
2399
|
+
.option("--exclude-titles <csv>", "Comma-separated job titles to exclude.")
|
|
2400
|
+
.option("--seniorities <csv>", "Comma-separated seniority levels: C-Suite, VP, Director, Manager, Staff.")
|
|
2401
|
+
.option("--departments <csv>", "Comma-separated departments/functions: Sales, Marketing, Engineering, ...")
|
|
2402
|
+
.option("--countries <csv>", "Comma-separated ISO 3166-1 alpha-2 person country codes.")
|
|
2403
|
+
.option("--keywords <csv>", "Comma-separated free-text persona keywords.")
|
|
2404
|
+
.option("--company-domains <csv>", "Comma-separated company domains to scope contacts to.")
|
|
2405
|
+
.option("--company-names <csv>", "Comma-separated company names to scope contacts to.")
|
|
2406
|
+
.option("--employees <range>", "Company employee count range: 20-200, 500+, or -50.")
|
|
2407
|
+
.option("--require-email", "Only return people with a work email available (provider-dependent).")
|
|
2408
|
+
.option("--require-phone", "Only return people with a phone/mobile available (provider-dependent).")
|
|
2409
|
+
.option("--max-per-company <n>", "Cap on contacts per company for account-anchored searches.")
|
|
2410
|
+
.option("--estimate", "Run a free count probe for an estimated match count.")
|
|
2411
|
+
.option("--materialize-preview", "Create a preview table with route rows.")
|
|
2412
|
+
.option("--json", "Print a JSON envelope.")
|
|
2413
|
+
.action(async (options) => {
|
|
2414
|
+
await handleAsyncAction("people search plan", options, () => requestOxygen("/api/cli/people/search/plan", {
|
|
2415
|
+
method: "POST",
|
|
2416
|
+
body: readPeopleSearchPlanBody(options),
|
|
2417
|
+
}));
|
|
2418
|
+
}))
|
|
2419
|
+
.addCommand(new Command("run")
|
|
2420
|
+
.description("Return a dry-run request or queue a live people-search ingestion run. Upsert dedup is on linkedin_url.")
|
|
2421
|
+
.option("--prompt <text-or-file>", "People-search prompt, or a path to a prompt file.")
|
|
2422
|
+
.option("--plan-json <json-or-file>", "Plan JSON returned by people search plan, or a path to a JSON file.")
|
|
2423
|
+
.option("--route-id <id>", "Route id from the plan to execute.")
|
|
2424
|
+
.option("--tool-id <tool>", "Tool id from the plan to execute.")
|
|
2425
|
+
.option("--table <table>", "Existing table id or slug to receive rows. If omitted for live, Oxygen creates a table.")
|
|
2426
|
+
.option("--upsert-key <column>", "Column key used for live upsert. Must match the plan upsert key (linkedin_url).")
|
|
2427
|
+
.option("--mode <mode>", "dry_run or live. Defaults to dry_run.")
|
|
2428
|
+
.option("--max-pages <n>", "Maximum provider pages to ingest. Defaults to the route estimate.")
|
|
2429
|
+
.option("--max-credits <n>", "Required credit ceiling for live runs.")
|
|
2430
|
+
.option("--target-count <n>", "Desired contact count when planning from --prompt.")
|
|
2431
|
+
.option("--source-intent <intent>", "Override detected intent when planning from --prompt.")
|
|
2432
|
+
.option("--filters-json <json-or-file>", "PeopleSearchFilters JSON inline or a @file/path when planning from --prompt; wins over individual flags per top-level filter path.")
|
|
2433
|
+
.option("--titles <csv>", "Comma-separated job titles to include when planning from --prompt.")
|
|
2434
|
+
.option("--adjacent-titles <csv>", "Comma-separated adjacent titles when planning from --prompt.")
|
|
2435
|
+
.option("--exclude-titles <csv>", "Comma-separated job titles to exclude when planning from --prompt.")
|
|
2436
|
+
.option("--seniorities <csv>", "Comma-separated seniority levels when planning from --prompt.")
|
|
2437
|
+
.option("--departments <csv>", "Comma-separated departments/functions when planning from --prompt.")
|
|
2438
|
+
.option("--countries <csv>", "Comma-separated ISO 3166-1 alpha-2 person country codes when planning from --prompt.")
|
|
2439
|
+
.option("--keywords <csv>", "Comma-separated persona keywords when planning from --prompt.")
|
|
2440
|
+
.option("--company-domains <csv>", "Comma-separated company domains when planning from --prompt.")
|
|
2441
|
+
.option("--company-names <csv>", "Comma-separated company names when planning from --prompt.")
|
|
2442
|
+
.option("--employees <range>", "Company employee count range when planning from --prompt.")
|
|
2443
|
+
.option("--require-email", "Only return people with a work email available when planning from --prompt.")
|
|
2444
|
+
.option("--require-phone", "Only return people with a phone available when planning from --prompt.")
|
|
2445
|
+
.option("--max-per-company <n>", "Cap on contacts per company when planning from --prompt.")
|
|
2446
|
+
.option("--estimate", "Run a free count probe when planning from --prompt.")
|
|
2447
|
+
.option("--approved", "Required for live runs after inspecting dry-run output.")
|
|
2448
|
+
.option("--json", "Print a JSON envelope.")
|
|
2449
|
+
.action(async (options) => {
|
|
2450
|
+
await handleAsyncAction("people search run", options, () => requestOxygen("/api/cli/people/search/run", {
|
|
2451
|
+
method: "POST",
|
|
2452
|
+
body: readPeopleSearchRunBody(options),
|
|
2453
|
+
}));
|
|
2454
|
+
})));
|
|
2291
2455
|
program
|
|
2292
2456
|
.command("worker")
|
|
2293
2457
|
.description("Background worker commands.")
|
|
@@ -2792,7 +2956,7 @@ export function createProgram() {
|
|
|
2792
2956
|
.description("Run an executable Oxygen tool through the HTTPS API.")
|
|
2793
2957
|
.argument("<tool_id>", "Tool id.")
|
|
2794
2958
|
.requiredOption("--input-json <json>", "Tool input as a JSON object.")
|
|
2795
|
-
.option("--mode <mode>", "Execution mode: live
|
|
2959
|
+
.option("--mode <mode>", "Execution mode: live or dry-run. Mutating tools default to dry-run.")
|
|
2796
2960
|
.option("--credential-mode <mode>", "Credential mode: managed, user_api_key, or user_oauth.")
|
|
2797
2961
|
.option("--org <organization_id>", "Optional organization id. Must match the stored CLI token.")
|
|
2798
2962
|
.option("--org-id <organization_id>", "Optional organization id. Must match the stored CLI token.")
|
|
@@ -2841,7 +3005,8 @@ export function createProgram() {
|
|
|
2841
3005
|
.option("--provider-order <providers>", "Comma-separated provider order. Overrides the default cost-aware waterfall profile.")
|
|
2842
3006
|
.option("--email-waterfall-profile <profile>", "Work-email waterfall profile: auto, name_domain, linkedin_url, or first_last_domain. Defaults prefer BlitzAPI/RocketReach/Prospeo where inputs fit.")
|
|
2843
3007
|
.option("--email-pattern-validation <mode>", "Work-email pattern pre-step: leadmagic_valid_only or disabled.")
|
|
2844
|
-
.option("--
|
|
3008
|
+
.option("--phone-waterfall-profile <profile>", "Phone waterfall profile: auto (input-aware), linkedin_url, email, or name_domain. Auto picks the cheapest cost-ordered provider set for each row's inputs (mobile match rate ~30-60%).")
|
|
3009
|
+
.option("--verify-phone", "Validate found phone numbers with ClearoutPhone (adds phone_line_type + phone_carrier; filter phone_line_type=mobile for mobile-only).")
|
|
2845
3010
|
.option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
|
|
2846
3011
|
.option("--limit <n>", "Rows to estimate. Defaults to 10.")
|
|
2847
3012
|
.option("--all", "Estimate all rows.")
|
|
@@ -2874,7 +3039,8 @@ export function createProgram() {
|
|
|
2874
3039
|
.option("--provider-order <providers>", "Comma-separated provider order. Overrides the default cost-aware waterfall profile.")
|
|
2875
3040
|
.option("--email-waterfall-profile <profile>", "Work-email waterfall profile: auto, name_domain, linkedin_url, or first_last_domain. Defaults prefer BlitzAPI/RocketReach/Prospeo where inputs fit.")
|
|
2876
3041
|
.option("--email-pattern-validation <mode>", "Work-email pattern pre-step: leadmagic_valid_only or disabled.")
|
|
2877
|
-
.option("--
|
|
3042
|
+
.option("--phone-waterfall-profile <profile>", "Phone waterfall profile: auto (input-aware), linkedin_url, email, or name_domain. Auto picks the cheapest cost-ordered provider set for each row's inputs (mobile match rate ~30-60%).")
|
|
3043
|
+
.option("--verify-phone", "Validate found phone numbers with ClearoutPhone (adds phone_line_type + phone_carrier; filter phone_line_type=mobile for mobile-only).")
|
|
2878
3044
|
.option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
|
|
2879
3045
|
.option("--limit <n>", "Rows to queue.")
|
|
2880
3046
|
.option("--all", "Queue all rows.")
|
|
@@ -3019,20 +3185,20 @@ export function createProgram() {
|
|
|
3019
3185
|
});
|
|
3020
3186
|
})))
|
|
3021
3187
|
.addCommand(new Command("list")
|
|
3022
|
-
.description("List supported
|
|
3188
|
+
.description("List supported integrations and this org's connections.")
|
|
3023
3189
|
.option("--json", "Print a JSON envelope.")
|
|
3024
3190
|
.action(async (options) => {
|
|
3025
3191
|
await handleAsyncAction("integrations list", options, async () => requestOxygen("/api/cli/integrations/composio/list"));
|
|
3026
3192
|
}))
|
|
3027
3193
|
.addCommand(new Command("connect")
|
|
3028
|
-
.description("Connect
|
|
3194
|
+
.description("Connect an integration. OAuth toolkits return a redirect URL; API-key integrations accept --api-key.")
|
|
3029
3195
|
.argument("<integration_id>", "Integration id, such as 'slack' or 'serpapi'.")
|
|
3030
3196
|
.option("--api-key <value>", "API key for Composio API-key toolkits (e.g. SerpAPI, Resend).")
|
|
3031
3197
|
.option("--json", "Print a JSON envelope.")
|
|
3032
3198
|
.action(async (integrationId, options) => {
|
|
3033
3199
|
await handleAsyncAction("integrations connect", options, async () => {
|
|
3034
3200
|
const apiKey = readOption(options.apiKey)?.trim();
|
|
3035
|
-
return requestOxygen("/api/cli/integrations/
|
|
3201
|
+
return requestOxygen("/api/cli/integrations/connect", {
|
|
3036
3202
|
method: "POST",
|
|
3037
3203
|
body: {
|
|
3038
3204
|
integration_id: integrationId,
|
|
@@ -3042,13 +3208,13 @@ export function createProgram() {
|
|
|
3042
3208
|
});
|
|
3043
3209
|
}))
|
|
3044
3210
|
.addCommand(new Command("disconnect")
|
|
3045
|
-
.description("Disconnect
|
|
3211
|
+
.description("Disconnect an integration.")
|
|
3046
3212
|
.argument("<integration_id>", "Integration id, such as 'slack'.")
|
|
3047
3213
|
.option("--connection-id <id>", "Disconnect a specific connected account when multiple exist.")
|
|
3048
3214
|
.option("--json", "Print a JSON envelope.")
|
|
3049
3215
|
.action(async (integrationId, options) => {
|
|
3050
3216
|
const connectionId = readOption(options.connectionId)?.trim();
|
|
3051
|
-
await handleAsyncAction("integrations disconnect", options, () => requestOxygen("/api/cli/integrations/
|
|
3217
|
+
await handleAsyncAction("integrations disconnect", options, () => requestOxygen("/api/cli/integrations/disconnect", {
|
|
3052
3218
|
method: "POST",
|
|
3053
3219
|
body: {
|
|
3054
3220
|
integration_id: integrationId,
|
|
@@ -3092,18 +3258,15 @@ export function createProgram() {
|
|
|
3092
3258
|
});
|
|
3093
3259
|
});
|
|
3094
3260
|
}));
|
|
3095
|
-
program
|
|
3096
|
-
.
|
|
3097
|
-
.description("LinkedIn sender account management for the native LinkedIn sequencer (accounts, limits, usage).")
|
|
3098
|
-
.addCommand(new Command("accounts")
|
|
3099
|
-
.description("Manage the org's connected LinkedIn sender accounts: list, connect, sync, inspect, disconnect, and tune rate limits.")
|
|
3261
|
+
program.addCommand(new Command("senders")
|
|
3262
|
+
.description("Manage the org's connected LinkedIn sender accounts for Sequencer: list, connect, sync, inspect, disconnect, and tune rate limits.")
|
|
3100
3263
|
.addCommand(new Command("list")
|
|
3101
3264
|
.description("List connected LinkedIn sender accounts with health status, rate limits, and today's usage.")
|
|
3102
3265
|
.option("--status <status>", "Filter by sender status: active, paused, disconnected, restricted, or credentials_required.")
|
|
3103
3266
|
.option("--no-usage", "Skip today's per-account usage counts for a faster, lighter response.")
|
|
3104
3267
|
.option("--json", "Print a JSON envelope.")
|
|
3105
3268
|
.action(async (options) => {
|
|
3106
|
-
await handleAsyncAction("
|
|
3269
|
+
await handleAsyncAction("senders list", options, () => {
|
|
3107
3270
|
const params = new URLSearchParams();
|
|
3108
3271
|
if (options.usage !== false)
|
|
3109
3272
|
params.set("include_usage", "true");
|
|
@@ -3111,7 +3274,7 @@ export function createProgram() {
|
|
|
3111
3274
|
if (status)
|
|
3112
3275
|
params.set("status", status);
|
|
3113
3276
|
const suffix = params.toString();
|
|
3114
|
-
return requestOxygen(`/api/cli/
|
|
3277
|
+
return requestOxygen(`/api/cli/senders${suffix ? `?${suffix}` : ""}`);
|
|
3115
3278
|
});
|
|
3116
3279
|
}))
|
|
3117
3280
|
.addCommand(new Command("connect")
|
|
@@ -3119,9 +3282,9 @@ export function createProgram() {
|
|
|
3119
3282
|
.option("--reconnect <connection_id>", "Reconnect an existing connection instead of creating a new one. Accepts a connection id.")
|
|
3120
3283
|
.option("--json", "Print a JSON envelope.")
|
|
3121
3284
|
.action(async (options) => {
|
|
3122
|
-
await handleAsyncAction("
|
|
3285
|
+
await handleAsyncAction("senders connect", options, () => {
|
|
3123
3286
|
const reconnect = readOption(options.reconnect);
|
|
3124
|
-
return requestOxygen("/api/cli/
|
|
3287
|
+
return requestOxygen("/api/cli/senders/connect", {
|
|
3125
3288
|
method: "POST",
|
|
3126
3289
|
body: {
|
|
3127
3290
|
...(reconnect ? { reconnect_connection_id: reconnect } : {}),
|
|
@@ -3130,20 +3293,20 @@ export function createProgram() {
|
|
|
3130
3293
|
});
|
|
3131
3294
|
}))
|
|
3132
3295
|
.addCommand(new Command("get")
|
|
3133
|
-
.description("Get one LinkedIn sender account with status, limits,
|
|
3296
|
+
.description("Get one LinkedIn sender account with status, limits, daily-reset timezone, and usage. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
3134
3297
|
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
3135
3298
|
.option("--json", "Print a JSON envelope.")
|
|
3136
3299
|
.action(async (id, options) => {
|
|
3137
|
-
await handleAsyncAction("
|
|
3300
|
+
await handleAsyncAction("senders get", options, async () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}`));
|
|
3138
3301
|
}))
|
|
3139
3302
|
.addCommand(new Command("sync")
|
|
3140
3303
|
.description("Refresh LinkedIn account state (status, profile) from Unipile. Pass --connection-id to sync one account, or omit it to sync all.")
|
|
3141
3304
|
.option("--connection-id <id>", "Sync a specific connection id. Defaults to syncing all connected accounts.")
|
|
3142
3305
|
.option("--json", "Print a JSON envelope.")
|
|
3143
3306
|
.action(async (options) => {
|
|
3144
|
-
await handleAsyncAction("
|
|
3307
|
+
await handleAsyncAction("senders sync", options, () => {
|
|
3145
3308
|
const connectionId = readOption(options.connectionId);
|
|
3146
|
-
return requestOxygen("/api/cli/
|
|
3309
|
+
return requestOxygen("/api/cli/senders/sync", {
|
|
3147
3310
|
method: "POST",
|
|
3148
3311
|
body: {
|
|
3149
3312
|
...(connectionId ? { connection_id: connectionId } : {}),
|
|
@@ -3156,22 +3319,22 @@ export function createProgram() {
|
|
|
3156
3319
|
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
3157
3320
|
.option("--json", "Print a JSON envelope.")
|
|
3158
3321
|
.action(async (id, options) => {
|
|
3159
|
-
await handleAsyncAction("
|
|
3322
|
+
await handleAsyncAction("senders disconnect", options, async () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/disconnect`, {
|
|
3160
3323
|
method: "POST",
|
|
3161
3324
|
}));
|
|
3162
3325
|
}))
|
|
3163
3326
|
.addCommand(new Command("limits")
|
|
3164
|
-
.description("View and adjust per-account daily action limits and
|
|
3327
|
+
.description("View and adjust per-account daily action limits and the daily-reset timezone.")
|
|
3165
3328
|
.option("--json", "Print a JSON envelope.")
|
|
3166
3329
|
.addCommand(new Command("get")
|
|
3167
|
-
.description("Show current limits, overrides,
|
|
3330
|
+
.description("Show current limits, overrides, daily-reset timezone, defaults, and safe maximums for an account. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
3168
3331
|
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
3169
3332
|
.option("--json", "Print a JSON envelope.")
|
|
3170
3333
|
.action(async (id, options) => {
|
|
3171
|
-
await handleAsyncAction("
|
|
3334
|
+
await handleAsyncAction("senders limits get", options, async () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/limits`));
|
|
3172
3335
|
}))
|
|
3173
3336
|
.addCommand(new Command("set")
|
|
3174
|
-
.description("Adjust per-account daily action limits and
|
|
3337
|
+
.description("Adjust per-account daily action limits and the daily-reset timezone. Values are clamped to safe maximums (e.g. max 80 invites/day). Send windows (time of day) are set per sequence in the campaign schedule, not per account. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
3175
3338
|
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
3176
3339
|
.option("--invites-per-day <n>", "Daily LinkedIn connection invites cap.")
|
|
3177
3340
|
.option("--invites-per-week <n>", "Weekly LinkedIn connection invites cap.")
|
|
@@ -3188,21 +3351,18 @@ export function createProgram() {
|
|
|
3188
3351
|
.option("--total-reads-per-day <n>", "Daily cap across all read types.")
|
|
3189
3352
|
.option("--min-spacing-seconds <n>", "Minimum seconds between actions.")
|
|
3190
3353
|
.option("--spacing-jitter-seconds <n>", "Random jitter seconds added to action spacing.")
|
|
3191
|
-
.option("--timezone <tz>", "IANA timezone
|
|
3192
|
-
.option("--working-days <days>", "Comma-separated ISO weekdays the account sends, e.g. 1,2,3,4,5 (1=Mon..7=Sun).")
|
|
3193
|
-
.option("--working-start <HH:MM>", "Working hours start time, e.g. 09:00.")
|
|
3194
|
-
.option("--working-end <HH:MM>", "Working hours end time, e.g. 17:00.")
|
|
3354
|
+
.option("--timezone <tz>", "IANA timezone the daily action counters reset in, e.g. America/New_York.")
|
|
3195
3355
|
.option("--json", "Print a JSON envelope.")
|
|
3196
3356
|
.action(async (id, options) => {
|
|
3197
|
-
await handleAsyncAction("
|
|
3357
|
+
await handleAsyncAction("senders limits set", options, () => {
|
|
3198
3358
|
const body = buildLinkedinLimitsBody(options);
|
|
3199
|
-
return requestOxygen(`/api/cli/
|
|
3359
|
+
return requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/limits`, {
|
|
3200
3360
|
method: "PATCH",
|
|
3201
3361
|
body,
|
|
3202
3362
|
});
|
|
3203
3363
|
});
|
|
3204
|
-
}))))
|
|
3205
|
-
|
|
3364
|
+
}))));
|
|
3365
|
+
program.addCommand(new Command("inbox")
|
|
3206
3366
|
.description("LinkedIn unified inbox (unibox): scan conversations across all sender accounts, read threads, and reply.")
|
|
3207
3367
|
.addCommand(new Command("list")
|
|
3208
3368
|
.description("List LinkedIn conversations across all connected accounts, newest first.")
|
|
@@ -3213,7 +3373,7 @@ export function createProgram() {
|
|
|
3213
3373
|
.option("--limit <n>", "Maximum conversations to return (1-200). Defaults to 50.")
|
|
3214
3374
|
.option("--json", "Print a JSON envelope.")
|
|
3215
3375
|
.action(async (options) => {
|
|
3216
|
-
await handleAsyncAction("
|
|
3376
|
+
await handleAsyncAction("inbox list", options, () => {
|
|
3217
3377
|
const params = new URLSearchParams();
|
|
3218
3378
|
const account = readOption(options.account);
|
|
3219
3379
|
if (account)
|
|
@@ -3229,7 +3389,7 @@ export function createProgram() {
|
|
|
3229
3389
|
if (limit)
|
|
3230
3390
|
params.set("limit", limit);
|
|
3231
3391
|
const suffix = params.toString();
|
|
3232
|
-
return requestOxygen(`/api/cli/
|
|
3392
|
+
return requestOxygen(`/api/cli/inbox${suffix ? `?${suffix}` : ""}`);
|
|
3233
3393
|
});
|
|
3234
3394
|
}))
|
|
3235
3395
|
.addCommand(new Command("get")
|
|
@@ -3238,13 +3398,13 @@ export function createProgram() {
|
|
|
3238
3398
|
.option("--message-limit <n>", "Maximum messages to return (1-500). Defaults to 100.")
|
|
3239
3399
|
.option("--json", "Print a JSON envelope.")
|
|
3240
3400
|
.action(async (conversation, options) => {
|
|
3241
|
-
await handleAsyncAction("
|
|
3401
|
+
await handleAsyncAction("inbox get", options, () => {
|
|
3242
3402
|
const params = new URLSearchParams();
|
|
3243
3403
|
const messageLimit = readOption(options.messageLimit);
|
|
3244
3404
|
if (messageLimit)
|
|
3245
3405
|
params.set("message_limit", messageLimit);
|
|
3246
3406
|
const suffix = params.toString();
|
|
3247
|
-
return requestOxygen(`/api/cli/
|
|
3407
|
+
return requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}${suffix ? `?${suffix}` : ""}`);
|
|
3248
3408
|
});
|
|
3249
3409
|
}))
|
|
3250
3410
|
.addCommand(new Command("send")
|
|
@@ -3254,7 +3414,7 @@ export function createProgram() {
|
|
|
3254
3414
|
.option("--approved", "Approve and send the message. Without this flag, returns a preview only.")
|
|
3255
3415
|
.option("--json", "Print a JSON envelope.")
|
|
3256
3416
|
.action(async (conversation, options) => {
|
|
3257
|
-
await handleAsyncAction("
|
|
3417
|
+
await handleAsyncAction("inbox send", options, () => requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}/send`, {
|
|
3258
3418
|
method: "POST",
|
|
3259
3419
|
body: {
|
|
3260
3420
|
text: readOption(options.text),
|
|
@@ -3267,7 +3427,7 @@ export function createProgram() {
|
|
|
3267
3427
|
.argument("<conversation>", "Conversation id or Unipile chat id.")
|
|
3268
3428
|
.option("--json", "Print a JSON envelope.")
|
|
3269
3429
|
.action(async (conversation, options) => {
|
|
3270
|
-
await handleAsyncAction("
|
|
3430
|
+
await handleAsyncAction("inbox mark-read", options, () => requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}/read`, {
|
|
3271
3431
|
method: "POST",
|
|
3272
3432
|
}));
|
|
3273
3433
|
}))
|
|
@@ -3277,7 +3437,7 @@ export function createProgram() {
|
|
|
3277
3437
|
.option("--message-limit <n>", "Maximum messages to sync per chat. Defaults to 20.")
|
|
3278
3438
|
.option("--json", "Print a JSON envelope.")
|
|
3279
3439
|
.action(async (options) => {
|
|
3280
|
-
await handleAsyncAction("
|
|
3440
|
+
await handleAsyncAction("inbox sync", options, () => {
|
|
3281
3441
|
const body = {};
|
|
3282
3442
|
const chatLimit = readOption(options.chatLimit);
|
|
3283
3443
|
if (chatLimit)
|
|
@@ -3285,77 +3445,160 @@ export function createProgram() {
|
|
|
3285
3445
|
const messageLimit = readOption(options.messageLimit);
|
|
3286
3446
|
if (messageLimit)
|
|
3287
3447
|
body.message_limit = Number(messageLimit);
|
|
3288
|
-
return requestOxygen("/api/cli/
|
|
3448
|
+
return requestOxygen("/api/cli/inbox/sync", { method: "POST", body });
|
|
3289
3449
|
});
|
|
3290
|
-
})))
|
|
3291
|
-
|
|
3292
|
-
.description("
|
|
3450
|
+
})));
|
|
3451
|
+
program.addCommand(new Command("sequences")
|
|
3452
|
+
.description("Multichannel outreach sequences: one enrollment per lead spans LinkedIn + email over a journey. LinkedIn steps dispatch natively (rate-limited, credit-capped); email steps send natively or place/move/stop the lead in a bound Instantly campaign (BYOK). Cross-channel reply-stop is intrinsic. A LinkedIn-only sequence behaves exactly like the original sequencer.")
|
|
3293
3453
|
.addCommand(new Command("list")
|
|
3294
|
-
.description("List
|
|
3454
|
+
.description("List sequences with status, channels, and credit usage.")
|
|
3295
3455
|
.option("--status <status>", "Filter by status: draft, active, paused, or archived.")
|
|
3296
3456
|
.option("--json", "Print a JSON envelope.")
|
|
3297
3457
|
.action(async (options) => {
|
|
3298
|
-
await handleAsyncAction("
|
|
3458
|
+
await handleAsyncAction("sequences list", options, () => {
|
|
3299
3459
|
const params = new URLSearchParams();
|
|
3300
3460
|
const status = readOption(options.status);
|
|
3301
3461
|
if (status)
|
|
3302
3462
|
params.set("status", status);
|
|
3303
3463
|
const suffix = params.toString();
|
|
3304
|
-
return requestOxygen(`/api/cli/
|
|
3464
|
+
return requestOxygen(`/api/cli/sequences${suffix ? `?${suffix}` : ""}`);
|
|
3465
|
+
});
|
|
3466
|
+
}))
|
|
3467
|
+
.addCommand(new Command("analytics")
|
|
3468
|
+
.description("Show organization-level sequencer analytics plus per-sequence funnels.")
|
|
3469
|
+
.option("--range <range>", "Preset range: 7d, 14d, 28d, or 30d. Defaults to 14d.")
|
|
3470
|
+
.option("--from <date>", "Custom start date (YYYY-MM-DD).")
|
|
3471
|
+
.option("--to <date>", "Custom end date (YYYY-MM-DD).")
|
|
3472
|
+
.option("--sequence <id-or-slug>", "Limit analytics to one sequence.")
|
|
3473
|
+
.option("--json", "Print a JSON envelope.")
|
|
3474
|
+
.action(async (options) => {
|
|
3475
|
+
await handleAsyncAction("sequences analytics", options, () => {
|
|
3476
|
+
const params = new URLSearchParams();
|
|
3477
|
+
const range = readOption(options.range);
|
|
3478
|
+
const from = readOption(options.from);
|
|
3479
|
+
const to = readOption(options.to);
|
|
3480
|
+
const sequence = readOption(options.sequence);
|
|
3481
|
+
if (range)
|
|
3482
|
+
params.set("range", range);
|
|
3483
|
+
if (from)
|
|
3484
|
+
params.set("from", from);
|
|
3485
|
+
if (to)
|
|
3486
|
+
params.set("to", to);
|
|
3487
|
+
if (sequence)
|
|
3488
|
+
params.set("sequence", sequence);
|
|
3489
|
+
const qs = params.toString();
|
|
3490
|
+
return requestOxygen(`/api/cli/sequences/analytics${qs ? `?${qs}` : ""}`);
|
|
3305
3491
|
});
|
|
3306
3492
|
}))
|
|
3307
3493
|
.addCommand(new Command("create")
|
|
3308
|
-
.description("Create a draft sequence from a steps JSON file. Assign
|
|
3494
|
+
.description("Create a draft multichannel sequence from a steps JSON file. Assign LinkedIn senders with --senders (required when the journey has LinkedIn steps); bind an Instantly email track with --email-*.")
|
|
3309
3495
|
.requiredOption("--name <name>", "Human-readable sequence name.")
|
|
3310
3496
|
.requiredOption("--slug <slug>", "Unique slug for the sequence.")
|
|
3311
|
-
.requiredOption("--steps-file <path>", "Path to a JSON file: { \"steps\": [...] }.")
|
|
3312
|
-
.
|
|
3497
|
+
.requiredOption("--steps-file <path>", "Path to a JSON file: { \"steps\": [...] }. LinkedIn steps (invite | message | wait_for_connection | inmail), email steps (email_send | email_reply | email_enroll | email_move | email_stop), and control steps (wait | wait_for_signal | branch | stop), each with an `id`. A `branch` routes on signals (then/else) or the legacy connection_accepted/already_connected sugar (then_id/else_id).")
|
|
3498
|
+
.option("--channels <list>", "Comma-separated channels: linkedin,email. Defaults to the channels the journey touches.")
|
|
3499
|
+
.option("--senders <ids>", "Comma-separated LinkedIn sender account ids (or connection / Unipile ids). Required when the journey has LinkedIn steps.")
|
|
3313
3500
|
.option("--table <id>", "Source table id whose rows supply {{column}} template values.")
|
|
3314
3501
|
.option("--url-column <key>", "Column key holding each lead's LinkedIn URL/provider id.")
|
|
3315
|
-
.option("--
|
|
3502
|
+
.option("--email-provider <provider>", "Email provider for the email track. Only 'instantly' is supported.")
|
|
3503
|
+
.option("--email-connection <id>", "Instantly connection id for the email track. Defaults to the org's active Instantly connection.")
|
|
3504
|
+
.option("--email-definition-file <path>", "Path to a JSON file with the email content spec (subjects/bodies/delays/subsequences) compiled to an Instantly campaign on start.")
|
|
3505
|
+
.option("--max-credits <n>", "Credit cap for the LinkedIn track (also set when starting).")
|
|
3316
3506
|
.option("--json", "Print a JSON envelope.")
|
|
3317
3507
|
.action(async (options) => {
|
|
3318
|
-
await handleAsyncAction("
|
|
3508
|
+
await handleAsyncAction("sequences create", options, () => {
|
|
3319
3509
|
const stepsPath = readOption(options.stepsFile);
|
|
3320
3510
|
if (!stepsPath)
|
|
3321
3511
|
throw new Error("--steps-file is required.");
|
|
3322
3512
|
const raw = readFileSync(resolve(stepsPath), "utf8");
|
|
3323
3513
|
const definition = JSON.parse(raw);
|
|
3324
|
-
const
|
|
3514
|
+
const channels = readCsvOption(options.channels);
|
|
3515
|
+
const senders = readCsvOption(options.senders);
|
|
3516
|
+
const email = readCampaignEmailBinding(options);
|
|
3325
3517
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
3326
|
-
return requestOxygen("/api/cli/
|
|
3518
|
+
return requestOxygen("/api/cli/sequences", {
|
|
3327
3519
|
method: "POST",
|
|
3328
3520
|
body: {
|
|
3329
3521
|
name: readOption(options.name),
|
|
3330
3522
|
slug: readOption(options.slug),
|
|
3331
3523
|
definition,
|
|
3332
|
-
|
|
3524
|
+
...(channels.length > 0 ? { channels } : {}),
|
|
3525
|
+
...(senders.length > 0 ? { senders } : {}),
|
|
3333
3526
|
...(readOption(options.table) ? { source_table_id: readOption(options.table) } : {}),
|
|
3334
3527
|
...(readOption(options.urlColumn) ? { linkedin_url_column_key: readOption(options.urlColumn) } : {}),
|
|
3528
|
+
...(email ? { email } : {}),
|
|
3335
3529
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
3336
3530
|
},
|
|
3337
3531
|
});
|
|
3338
3532
|
});
|
|
3533
|
+
}))
|
|
3534
|
+
.addCommand(new Command("update")
|
|
3535
|
+
.description("Update a DRAFT sequence's name, journey, channels, senders, email binding, or LinkedIn credit cap. The journey is re-validated; the email binding stays editable only while the sequence is a draft. --max-credits is locked once the sequence has been started (re-run `sequences start`). Pass only the fields you want to change.")
|
|
3536
|
+
.argument("<sequence>", "Sequence id or slug.")
|
|
3537
|
+
.option("--name <name>", "New human-readable sequence name.")
|
|
3538
|
+
.option("--steps-file <path>", "Path to a JSON file: { \"steps\": [...] } replacing the journey.")
|
|
3539
|
+
.option("--channels <list>", "Comma-separated channels: linkedin,email.")
|
|
3540
|
+
.option("--senders <ids>", "Comma-separated LinkedIn sender account ids (or connection / Unipile ids).")
|
|
3541
|
+
.option("--email-provider <provider>", "Email provider for the email track. Only 'instantly' is supported.")
|
|
3542
|
+
.option("--email-connection <id>", "Instantly connection id for the email track.")
|
|
3543
|
+
.option("--email-definition-file <path>", "Path to a JSON file with the email content spec (subjects/bodies/delays/subsequences).")
|
|
3544
|
+
.option("--clear-email", "Remove the email binding from the sequence (draft only).")
|
|
3545
|
+
.option("--max-credits <n>", "Credit cap for the LinkedIn track (draft only).")
|
|
3546
|
+
.option("--json", "Print a JSON envelope.")
|
|
3547
|
+
.action(async (sequence, options) => {
|
|
3548
|
+
await handleAsyncAction("sequences update", options, () => {
|
|
3549
|
+
const body = {};
|
|
3550
|
+
const name = readOption(options.name);
|
|
3551
|
+
if (name)
|
|
3552
|
+
body.name = name;
|
|
3553
|
+
const stepsPath = readOption(options.stepsFile);
|
|
3554
|
+
if (stepsPath) {
|
|
3555
|
+
body.definition = JSON.parse(readFileSync(resolve(stepsPath), "utf8"));
|
|
3556
|
+
}
|
|
3557
|
+
const channels = readCsvOption(options.channels);
|
|
3558
|
+
if (channels.length > 0)
|
|
3559
|
+
body.channels = channels;
|
|
3560
|
+
const senders = readCsvOption(options.senders);
|
|
3561
|
+
if (senders.length > 0)
|
|
3562
|
+
body.senders = senders;
|
|
3563
|
+
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
3564
|
+
if (maxCredits !== undefined)
|
|
3565
|
+
body.max_credits = maxCredits;
|
|
3566
|
+
if (options.clearEmail) {
|
|
3567
|
+
body.email = null;
|
|
3568
|
+
}
|
|
3569
|
+
else {
|
|
3570
|
+
const email = readCampaignEmailBinding(options);
|
|
3571
|
+
if (email)
|
|
3572
|
+
body.email = email;
|
|
3573
|
+
}
|
|
3574
|
+
if (Object.keys(body).length === 0) {
|
|
3575
|
+
throw new Error("Provide at least one field to update (--name, --steps-file, --channels, --senders, --email-*, --clear-email, or --max-credits).");
|
|
3576
|
+
}
|
|
3577
|
+
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}`, {
|
|
3578
|
+
method: "PATCH",
|
|
3579
|
+
body,
|
|
3580
|
+
});
|
|
3581
|
+
});
|
|
3339
3582
|
}))
|
|
3340
3583
|
.addCommand(new Command("get")
|
|
3341
3584
|
.description("Get a sequence's definition, senders, status, and credit usage.")
|
|
3342
3585
|
.argument("<sequence>", "Sequence id or slug.")
|
|
3343
3586
|
.option("--json", "Print a JSON envelope.")
|
|
3344
3587
|
.action(async (sequence, options) => {
|
|
3345
|
-
await handleAsyncAction("
|
|
3588
|
+
await handleAsyncAction("sequences get", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}`));
|
|
3346
3589
|
}))
|
|
3347
3590
|
.addCommand(new Command("enroll")
|
|
3348
|
-
.description("Enroll leads into a sequence from a JSON file of { leads: [...] }. Idempotent per table row.")
|
|
3591
|
+
.description("Enroll leads into a sequence from a JSON file of { leads: [...] }. When the sequence is bound to a source table, a lead's table_row_id auto-snapshots that row's columns (incl. AI/tool outputs) into row_values for {{column}} copy — explicit row_values win. Idempotent per table row.")
|
|
3349
3592
|
.argument("<sequence>", "Sequence id or slug.")
|
|
3350
|
-
.requiredOption("--leads-file <path>", "Path to a JSON file: { \"leads\": [{ lead_provider_id, lead_name, row_values }] }.")
|
|
3593
|
+
.requiredOption("--leads-file <path>", "Path to a JSON file: { \"leads\": [{ lead_provider_id, lead_name, table_row_id, row_values }] }.")
|
|
3351
3594
|
.option("--json", "Print a JSON envelope.")
|
|
3352
3595
|
.action(async (sequence, options) => {
|
|
3353
|
-
await handleAsyncAction("
|
|
3596
|
+
await handleAsyncAction("sequences enroll", options, () => {
|
|
3354
3597
|
const leadsPath = readOption(options.leadsFile);
|
|
3355
3598
|
if (!leadsPath)
|
|
3356
3599
|
throw new Error("--leads-file is required.");
|
|
3357
3600
|
const parsed = JSON.parse(readFileSync(resolve(leadsPath), "utf8"));
|
|
3358
|
-
return requestOxygen(`/api/cli/
|
|
3601
|
+
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/enroll`, {
|
|
3359
3602
|
method: "POST",
|
|
3360
3603
|
body: { leads: parsed.leads ?? [] },
|
|
3361
3604
|
});
|
|
@@ -3369,9 +3612,9 @@ export function createProgram() {
|
|
|
3369
3612
|
.option("--dry-run", "Activate in dry-run mode: advance every step with simulated sends, no LinkedIn actions, no credits.")
|
|
3370
3613
|
.option("--json", "Print a JSON envelope.")
|
|
3371
3614
|
.action(async (sequence, options) => {
|
|
3372
|
-
await handleAsyncAction("
|
|
3615
|
+
await handleAsyncAction("sequences start", options, () => {
|
|
3373
3616
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
3374
|
-
return requestOxygen(`/api/cli/
|
|
3617
|
+
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/start`, {
|
|
3375
3618
|
method: "POST",
|
|
3376
3619
|
body: {
|
|
3377
3620
|
...(options.dryRun ? { dry_run: true } : {}),
|
|
@@ -3386,7 +3629,7 @@ export function createProgram() {
|
|
|
3386
3629
|
.argument("<sequence>", "Sequence id or slug.")
|
|
3387
3630
|
.option("--json", "Print a JSON envelope.")
|
|
3388
3631
|
.action(async (sequence, options) => {
|
|
3389
|
-
await handleAsyncAction("
|
|
3632
|
+
await handleAsyncAction("sequences pause", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/status`, {
|
|
3390
3633
|
method: "POST", body: { status: "paused" },
|
|
3391
3634
|
}));
|
|
3392
3635
|
}))
|
|
@@ -3395,7 +3638,7 @@ export function createProgram() {
|
|
|
3395
3638
|
.argument("<sequence>", "Sequence id or slug.")
|
|
3396
3639
|
.option("--json", "Print a JSON envelope.")
|
|
3397
3640
|
.action(async (sequence, options) => {
|
|
3398
|
-
await handleAsyncAction("
|
|
3641
|
+
await handleAsyncAction("sequences resume", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/status`, {
|
|
3399
3642
|
method: "POST", body: { status: "active" },
|
|
3400
3643
|
}));
|
|
3401
3644
|
}))
|
|
@@ -3404,7 +3647,7 @@ export function createProgram() {
|
|
|
3404
3647
|
.argument("<sequence>", "Sequence id or slug.")
|
|
3405
3648
|
.option("--json", "Print a JSON envelope.")
|
|
3406
3649
|
.action(async (sequence, options) => {
|
|
3407
|
-
await handleAsyncAction("
|
|
3650
|
+
await handleAsyncAction("sequences archive", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/status`, {
|
|
3408
3651
|
method: "POST", body: { status: "archived" },
|
|
3409
3652
|
}));
|
|
3410
3653
|
}))
|
|
@@ -3415,7 +3658,7 @@ export function createProgram() {
|
|
|
3415
3658
|
.option("--limit <n>", "Maximum enrollments to return (1-500).")
|
|
3416
3659
|
.option("--json", "Print a JSON envelope.")
|
|
3417
3660
|
.action(async (sequence, options) => {
|
|
3418
|
-
await handleAsyncAction("
|
|
3661
|
+
await handleAsyncAction("sequences enrollments", options, () => {
|
|
3419
3662
|
const params = new URLSearchParams();
|
|
3420
3663
|
const status = readOption(options.status);
|
|
3421
3664
|
if (status)
|
|
@@ -3424,7 +3667,7 @@ export function createProgram() {
|
|
|
3424
3667
|
if (limit)
|
|
3425
3668
|
params.set("limit", limit);
|
|
3426
3669
|
const suffix = params.toString();
|
|
3427
|
-
return requestOxygen(`/api/cli/
|
|
3670
|
+
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/enrollments${suffix ? `?${suffix}` : ""}`);
|
|
3428
3671
|
});
|
|
3429
3672
|
}))
|
|
3430
3673
|
.addCommand(new Command("stats")
|
|
@@ -3432,8 +3675,142 @@ export function createProgram() {
|
|
|
3432
3675
|
.argument("<sequence>", "Sequence id or slug.")
|
|
3433
3676
|
.option("--json", "Print a JSON envelope.")
|
|
3434
3677
|
.action(async (sequence, options) => {
|
|
3435
|
-
await handleAsyncAction("
|
|
3678
|
+
await handleAsyncAction("sequences stats", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/stats`));
|
|
3436
3679
|
})));
|
|
3680
|
+
program.addCommand(new Command("mailboxes")
|
|
3681
|
+
.description("Native email sending pool: register/refresh Google/Microsoft mailboxes a campaign rotates over, pause/disable inboxes, and delegate warmup to Instantly (BYOK — Instantly bills your account, 0 Oxygen credits).")
|
|
3682
|
+
.addCommand(new Command("list")
|
|
3683
|
+
.description("List the org's sending mailboxes with provider, status, warmup state, and a pool overview.")
|
|
3684
|
+
.option("--status <status>", "Filter by status: active, paused, or disabled.")
|
|
3685
|
+
.option("--json", "Print a JSON envelope.")
|
|
3686
|
+
.action(async (options) => {
|
|
3687
|
+
await handleAsyncAction("mailboxes list", options, () => {
|
|
3688
|
+
const params = new URLSearchParams();
|
|
3689
|
+
const status = readOption(options.status);
|
|
3690
|
+
if (status)
|
|
3691
|
+
params.set("status", status);
|
|
3692
|
+
const suffix = params.toString();
|
|
3693
|
+
return requestOxygen(`/api/cli/mailboxes${suffix ? `?${suffix}` : ""}`);
|
|
3694
|
+
});
|
|
3695
|
+
}))
|
|
3696
|
+
.addCommand(new Command("import")
|
|
3697
|
+
.description("Register (or refresh) sending mailboxes in bulk. Provide --file (a JSON file: { \"mailboxes\": [{ email_address, provider, workspace_external_id? }] }) or --from zapmail to pull the connected Zapmail workspace's mailbox list. Upsert is keyed by address, so re-importing never duplicates a mailbox.")
|
|
3698
|
+
.option("--file <path>", "Path to a JSON file: { \"mailboxes\": [{ email_address, provider, workspace_external_id? }] }.")
|
|
3699
|
+
.option("--from <source>", "Import source: 'zapmail' to pull the connected Zapmail workspace's mailboxes.")
|
|
3700
|
+
.option("--connection <id>", "Zapmail connection id (--from zapmail). Defaults to the org's active Zapmail connection.")
|
|
3701
|
+
.option("--json", "Print a JSON envelope.")
|
|
3702
|
+
.action(async (options) => {
|
|
3703
|
+
await handleAsyncAction("mailboxes import", options, () => {
|
|
3704
|
+
const from = readOption(options.from);
|
|
3705
|
+
const filePath = readOption(options.file);
|
|
3706
|
+
const connection = readOption(options.connection);
|
|
3707
|
+
if (from === "zapmail") {
|
|
3708
|
+
return requestOxygen("/api/cli/mailboxes", {
|
|
3709
|
+
method: "POST",
|
|
3710
|
+
body: { source: "zapmail", ...(connection ? { connection_id: connection } : {}) },
|
|
3711
|
+
});
|
|
3712
|
+
}
|
|
3713
|
+
if (!filePath)
|
|
3714
|
+
throw new Error("Provide --file <path> (a { \"mailboxes\": [...] } JSON file) or --from zapmail.");
|
|
3715
|
+
const parsed = JSON.parse(readFileSync(resolve(filePath), "utf8"));
|
|
3716
|
+
return requestOxygen("/api/cli/mailboxes", {
|
|
3717
|
+
method: "POST",
|
|
3718
|
+
body: { mailboxes: parsed.mailboxes ?? [] },
|
|
3719
|
+
});
|
|
3720
|
+
});
|
|
3721
|
+
}))
|
|
3722
|
+
.addCommand(new Command("status")
|
|
3723
|
+
.description("Set a sending mailbox's status. Pausing/disabling takes the inbox out of the rotation pool without losing its warmup state.")
|
|
3724
|
+
.argument("<mailbox>", "Mailbox id or email address.")
|
|
3725
|
+
.requiredOption("--status <status>", "New status: active, paused, or disabled.")
|
|
3726
|
+
.option("--json", "Print a JSON envelope.")
|
|
3727
|
+
.action(async (mailbox, options) => {
|
|
3728
|
+
await handleAsyncAction("mailboxes status", options, () => {
|
|
3729
|
+
const status = readOption(options.status);
|
|
3730
|
+
if (!status)
|
|
3731
|
+
throw new Error("--status is required.");
|
|
3732
|
+
return requestOxygen(`/api/cli/mailboxes/${encodeURIComponent(mailbox)}/status`, {
|
|
3733
|
+
method: "POST",
|
|
3734
|
+
body: { status },
|
|
3735
|
+
});
|
|
3736
|
+
});
|
|
3737
|
+
}))
|
|
3738
|
+
.addCommand(new Command("connect-oauth")
|
|
3739
|
+
.description("Provision Zapmail Custom OAuth across the mailbox pool: registers Oxygen's own OAuth app with Zapmail, which authorizes it across the Workspaces so native send needs no per-Workspace delegation. BYOK — Zapmail bills your account, 0 Oxygen credits. Pass --status <export_id> to poll a run.")
|
|
3740
|
+
.option("--provider <provider>", "Mailbox provider to provision: google or microsoft.")
|
|
3741
|
+
.option("--mailboxes <list>", "Comma-separated mailbox addresses to provision. Omit to provision the whole pool for the provider.")
|
|
3742
|
+
.option("--connection <id>", "Zapmail connection id. Defaults to the org's active Zapmail connection.")
|
|
3743
|
+
.option("--status <export_id>", "Poll a previously started Custom OAuth export instead of starting a new one.")
|
|
3744
|
+
.option("--json", "Print a JSON envelope.")
|
|
3745
|
+
.action(async (options) => {
|
|
3746
|
+
await handleAsyncAction("mailboxes connect-oauth", options, () => {
|
|
3747
|
+
const exportId = readOption(options.status);
|
|
3748
|
+
if (exportId) {
|
|
3749
|
+
const params = new URLSearchParams({ export_id: exportId });
|
|
3750
|
+
const pollConnection = readOption(options.connection);
|
|
3751
|
+
if (pollConnection)
|
|
3752
|
+
params.set("connection_id", pollConnection);
|
|
3753
|
+
return requestOxygen(`/api/cli/mailboxes/connect-oauth?${params.toString()}`);
|
|
3754
|
+
}
|
|
3755
|
+
const provider = readOption(options.provider);
|
|
3756
|
+
if (!provider) {
|
|
3757
|
+
throw new Error("--provider <google|microsoft> is required (or pass --status <export_id> to poll a run).");
|
|
3758
|
+
}
|
|
3759
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
3760
|
+
const connection = readOption(options.connection);
|
|
3761
|
+
return requestOxygen("/api/cli/mailboxes/connect-oauth", {
|
|
3762
|
+
method: "POST",
|
|
3763
|
+
body: {
|
|
3764
|
+
provider,
|
|
3765
|
+
...(mailboxes.length > 0 ? { mailboxes } : {}),
|
|
3766
|
+
...(connection ? { connection_id: connection } : {}),
|
|
3767
|
+
},
|
|
3768
|
+
});
|
|
3769
|
+
});
|
|
3770
|
+
}))
|
|
3771
|
+
.addCommand(new Command("warmup")
|
|
3772
|
+
.description("Mailbox warmup via Instantly (BYOK — Instantly bills your account, 0 Oxygen credits).")
|
|
3773
|
+
.addCommand(new Command("enable")
|
|
3774
|
+
.description("Enable Instantly warmup for sending mailboxes and stamp each mailbox's warmup state. Targets the whole pool unless --mailboxes is given.")
|
|
3775
|
+
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to warm. Omit to warm the whole pool.")
|
|
3776
|
+
.option("--connection <id>", "Instantly connection id. Defaults to the org's active Instantly connection.")
|
|
3777
|
+
.option("--dry-run", "Simulate without calling Instantly (mailboxes marked pending).")
|
|
3778
|
+
.option("--json", "Print a JSON envelope.")
|
|
3779
|
+
.action(async (options) => {
|
|
3780
|
+
await handleAsyncAction("mailboxes warmup enable", options, () => {
|
|
3781
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
3782
|
+
const connection = readOption(options.connection);
|
|
3783
|
+
return requestOxygen("/api/cli/mailboxes/warmup", {
|
|
3784
|
+
method: "POST",
|
|
3785
|
+
body: {
|
|
3786
|
+
...(mailboxes.length > 0 ? { mailboxes } : {}),
|
|
3787
|
+
...(connection ? { connection_id: connection } : {}),
|
|
3788
|
+
...(options.dryRun ? { dry_run: true } : {}),
|
|
3789
|
+
},
|
|
3790
|
+
});
|
|
3791
|
+
});
|
|
3792
|
+
}))
|
|
3793
|
+
.addCommand(new Command("status")
|
|
3794
|
+
.description("Sync Instantly's warmup analytics into the pool, updating each mailbox's warmup state. Targets the whole pool unless --mailboxes is given.")
|
|
3795
|
+
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to sync. Omit to sync the whole pool.")
|
|
3796
|
+
.option("--connection <id>", "Instantly connection id. Defaults to the org's active Instantly connection.")
|
|
3797
|
+
.option("--dry-run", "Skip the Instantly call (mailboxes marked pending).")
|
|
3798
|
+
.option("--json", "Print a JSON envelope.")
|
|
3799
|
+
.action(async (options) => {
|
|
3800
|
+
await handleAsyncAction("mailboxes warmup status", options, () => {
|
|
3801
|
+
const params = new URLSearchParams();
|
|
3802
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
3803
|
+
if (mailboxes.length > 0)
|
|
3804
|
+
params.set("mailboxes", mailboxes.join(","));
|
|
3805
|
+
const connection = readOption(options.connection);
|
|
3806
|
+
if (connection)
|
|
3807
|
+
params.set("connection_id", connection);
|
|
3808
|
+
if (options.dryRun)
|
|
3809
|
+
params.set("dry_run", "true");
|
|
3810
|
+
const suffix = params.toString();
|
|
3811
|
+
return requestOxygen(`/api/cli/mailboxes/warmup/status${suffix ? `?${suffix}` : ""}`);
|
|
3812
|
+
});
|
|
3813
|
+
}))));
|
|
3437
3814
|
program
|
|
3438
3815
|
.command("workflows")
|
|
3439
3816
|
.description("Durable workflow automation commands.")
|
|
@@ -4548,6 +4925,112 @@ function readCompanySearchPlanJson(value) {
|
|
|
4548
4925
|
? data
|
|
4549
4926
|
: parsed;
|
|
4550
4927
|
}
|
|
4928
|
+
function readPeopleSearchPlanBody(options) {
|
|
4929
|
+
const targetCount = readPositiveInt(options.targetCount);
|
|
4930
|
+
const filters = readPeopleSearchFilters(options);
|
|
4931
|
+
return {
|
|
4932
|
+
prompt: readFileIfPresent(options.prompt),
|
|
4933
|
+
...(targetCount !== undefined ? { target_count: targetCount } : {}),
|
|
4934
|
+
...(options.sourceIntent ? { source_intent: options.sourceIntent } : {}),
|
|
4935
|
+
...(filters ? { filters } : {}),
|
|
4936
|
+
...(options.estimate ? { estimate: true } : {}),
|
|
4937
|
+
...(options.materializePreview ? { materialize_preview: true } : {}),
|
|
4938
|
+
};
|
|
4939
|
+
}
|
|
4940
|
+
function readPeopleSearchRunBody(options) {
|
|
4941
|
+
const prompt = options.prompt ? readFileIfPresent(options.prompt) : null;
|
|
4942
|
+
const plan = options.planJson ? readPeopleSearchPlanJson(options.planJson) : null;
|
|
4943
|
+
if (!prompt && !plan) {
|
|
4944
|
+
throw new OxygenError("invalid_request", "Pass --prompt or --plan-json.", { exitCode: 1 });
|
|
4945
|
+
}
|
|
4946
|
+
const maxPages = readPositiveInt(options.maxPages);
|
|
4947
|
+
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
4948
|
+
const targetCount = readPositiveInt(options.targetCount);
|
|
4949
|
+
const filters = prompt ? readPeopleSearchFilters(options) : null;
|
|
4950
|
+
return {
|
|
4951
|
+
...(prompt ? { prompt } : {}),
|
|
4952
|
+
...(plan ? { plan } : {}),
|
|
4953
|
+
...(options.routeId ? { route_id: options.routeId } : {}),
|
|
4954
|
+
...(options.toolId ? { tool_id: options.toolId } : {}),
|
|
4955
|
+
...(options.table ? { table: options.table } : {}),
|
|
4956
|
+
...(options.upsertKey ? { upsert_key: options.upsertKey } : {}),
|
|
4957
|
+
...(options.mode ? { mode: options.mode } : {}),
|
|
4958
|
+
...(maxPages !== undefined ? { max_pages: maxPages } : {}),
|
|
4959
|
+
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
4960
|
+
...(targetCount !== undefined ? { target_count: targetCount } : {}),
|
|
4961
|
+
...(options.sourceIntent ? { source_intent: options.sourceIntent } : {}),
|
|
4962
|
+
...(filters ? { filters } : {}),
|
|
4963
|
+
...(prompt && options.estimate ? { estimate: true } : {}),
|
|
4964
|
+
...(options.approved ? { approved: true } : {}),
|
|
4965
|
+
};
|
|
4966
|
+
}
|
|
4967
|
+
// PeopleSearchFilters mirror (structural — the server validates the shape). Builds a
|
|
4968
|
+
// filters object from individual --titles/--seniorities/etc. flags, then lets --filters-json
|
|
4969
|
+
// win per top-level filter path so an agent can pass a precise object while still using
|
|
4970
|
+
// convenience flags for the rest.
|
|
4971
|
+
function readPeopleSearchFilters(options) {
|
|
4972
|
+
const filters = {};
|
|
4973
|
+
const titles = {};
|
|
4974
|
+
const titleInclude = readCsvOption(options.titles);
|
|
4975
|
+
if (titleInclude.length > 0)
|
|
4976
|
+
titles.include = titleInclude;
|
|
4977
|
+
const adjacent = readCsvOption(options.adjacentTitles);
|
|
4978
|
+
if (adjacent.length > 0)
|
|
4979
|
+
titles.include_adjacent = adjacent;
|
|
4980
|
+
const titleExclude = readCsvOption(options.excludeTitles);
|
|
4981
|
+
if (titleExclude.length > 0)
|
|
4982
|
+
titles.exclude = titleExclude;
|
|
4983
|
+
if (Object.keys(titles).length > 0)
|
|
4984
|
+
filters.titles = titles;
|
|
4985
|
+
const seniorities = readCsvOption(options.seniorities);
|
|
4986
|
+
if (seniorities.length > 0)
|
|
4987
|
+
filters.seniorities = { include: seniorities };
|
|
4988
|
+
const departments = readCsvOption(options.departments);
|
|
4989
|
+
if (departments.length > 0)
|
|
4990
|
+
filters.departments = { include: departments };
|
|
4991
|
+
const keywords = readCsvOption(options.keywords);
|
|
4992
|
+
if (keywords.length > 0)
|
|
4993
|
+
filters.keywords = { include: keywords };
|
|
4994
|
+
const countries = readCsvOption(options.countries);
|
|
4995
|
+
if (countries.length > 0)
|
|
4996
|
+
filters.geo = { countries };
|
|
4997
|
+
const contactability = {};
|
|
4998
|
+
if (options.requireEmail)
|
|
4999
|
+
contactability.require_work_email = true;
|
|
5000
|
+
if (options.requirePhone)
|
|
5001
|
+
contactability.require_mobile = true;
|
|
5002
|
+
if (Object.keys(contactability).length > 0)
|
|
5003
|
+
filters.contactability = contactability;
|
|
5004
|
+
const company = {};
|
|
5005
|
+
const companyDomains = readCsvOption(options.companyDomains);
|
|
5006
|
+
if (companyDomains.length > 0)
|
|
5007
|
+
company.domains = { include: companyDomains };
|
|
5008
|
+
const companyNames = readCsvOption(options.companyNames);
|
|
5009
|
+
if (companyNames.length > 0)
|
|
5010
|
+
company.names = { include: companyNames };
|
|
5011
|
+
const employees = readRangeOption(options.employees, "--employees");
|
|
5012
|
+
if (employees)
|
|
5013
|
+
company.employee_count = employees;
|
|
5014
|
+
if (Object.keys(company).length > 0)
|
|
5015
|
+
filters.company = company;
|
|
5016
|
+
const maxPerCompany = readPositiveInt(options.maxPerCompany);
|
|
5017
|
+
if (maxPerCompany !== undefined)
|
|
5018
|
+
filters.max_results_per_company = maxPerCompany;
|
|
5019
|
+
const explicit = options.filtersJson ? parseJsonObject(readFileIfPresent(options.filtersJson)) : null;
|
|
5020
|
+
if (explicit) {
|
|
5021
|
+
for (const [key, value] of Object.entries(explicit)) {
|
|
5022
|
+
filters[key] = value;
|
|
5023
|
+
}
|
|
5024
|
+
}
|
|
5025
|
+
return Object.keys(filters).length > 0 ? filters : null;
|
|
5026
|
+
}
|
|
5027
|
+
function readPeopleSearchPlanJson(value) {
|
|
5028
|
+
const parsed = parseJsonObject(readFileIfPresent(value));
|
|
5029
|
+
const data = parsed.data;
|
|
5030
|
+
return data && typeof data === "object" && !Array.isArray(data)
|
|
5031
|
+
? data
|
|
5032
|
+
: parsed;
|
|
5033
|
+
}
|
|
4551
5034
|
function readCompaniesEnrichBody(table, options) {
|
|
4552
5035
|
const body = { table };
|
|
4553
5036
|
const fields = readCsvOption(options.missingFields);
|
|
@@ -4668,6 +5151,9 @@ async function importRows(table, options) {
|
|
|
4668
5151
|
const sourceHash = hashImportFile(options.file);
|
|
4669
5152
|
const shouldUseBackground = options.background
|
|
4670
5153
|
|| (!options.sync && parsedRows.length > LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD);
|
|
5154
|
+
const preparedBackgroundTarget = shouldUseBackground && table && !options.create
|
|
5155
|
+
? await prepareImportTarget(table, options, parsedRows)
|
|
5156
|
+
: null;
|
|
4671
5157
|
if (shouldUseBackground) {
|
|
4672
5158
|
// Prefer the object-storage fast path: upload the raw file once via a
|
|
4673
5159
|
// presigned URL (no request-body limit) so the worker COPY-loads it.
|
|
@@ -4675,13 +5161,13 @@ async function importRows(table, options) {
|
|
|
4675
5161
|
const staged = await tryEnqueueStagedFileImport(table, options, format, parsedRows, {
|
|
4676
5162
|
autoBackground: !options.background,
|
|
4677
5163
|
sourceHash,
|
|
4678
|
-
});
|
|
5164
|
+
}, preparedBackgroundTarget);
|
|
4679
5165
|
if (staged)
|
|
4680
5166
|
return staged;
|
|
4681
5167
|
return enqueueImportFile(table, options, format, batchSize, {
|
|
4682
5168
|
autoBackground: !options.background,
|
|
4683
5169
|
sourceHash,
|
|
4684
|
-
});
|
|
5170
|
+
}, preparedBackgroundTarget);
|
|
4685
5171
|
}
|
|
4686
5172
|
const target = await prepareImportTarget(table, options, parsedRows);
|
|
4687
5173
|
let rowCount = 0;
|
|
@@ -4691,28 +5177,30 @@ async function importRows(table, options) {
|
|
|
4691
5177
|
const warnings = [];
|
|
4692
5178
|
let warningsTruncated = false;
|
|
4693
5179
|
let lastResult = null;
|
|
5180
|
+
let writeRequestCount = 0;
|
|
5181
|
+
let requestTooLargeRetries = 0;
|
|
5182
|
+
let minimumBatchSizeUsed = null;
|
|
4694
5183
|
for (const batch of chunk(target.rows, batchSize)) {
|
|
4695
|
-
const result = await
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
table: target.tableRef,
|
|
4700
|
-
rows: batch,
|
|
4701
|
-
...(target.upsertKey ? { key: target.upsertKey } : {}),
|
|
4702
|
-
},
|
|
5184
|
+
const result = await writeImportBatchWithAutoSplit({
|
|
5185
|
+
tableRef: target.tableRef,
|
|
5186
|
+
upsertKey: target.upsertKey,
|
|
5187
|
+
rows: batch,
|
|
4703
5188
|
});
|
|
4704
|
-
rowCount +=
|
|
4705
|
-
insertedCount +=
|
|
4706
|
-
updatedCount +=
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
const batchWarnings = Array.isArray(result.warnings) ? result.warnings : [];
|
|
4710
|
-
for (const warning of batchWarnings) {
|
|
5189
|
+
rowCount += result.rowCount;
|
|
5190
|
+
insertedCount += result.insertedCount;
|
|
5191
|
+
updatedCount += result.updatedCount;
|
|
5192
|
+
warningCount += result.warningCount;
|
|
5193
|
+
for (const warning of result.warnings) {
|
|
4711
5194
|
if (warnings.length < 20)
|
|
4712
5195
|
warnings.push(warning);
|
|
4713
5196
|
}
|
|
4714
|
-
warningsTruncated = warningsTruncated || result.warningsTruncated
|
|
4715
|
-
|
|
5197
|
+
warningsTruncated = warningsTruncated || result.warningsTruncated || warningCount > warnings.length;
|
|
5198
|
+
writeRequestCount += result.writeRequestCount;
|
|
5199
|
+
requestTooLargeRetries += result.requestTooLargeRetries;
|
|
5200
|
+
minimumBatchSizeUsed = minimumBatchSizeUsed === null
|
|
5201
|
+
? result.minimumBatchSizeUsed
|
|
5202
|
+
: Math.min(minimumBatchSizeUsed, result.minimumBatchSizeUsed);
|
|
5203
|
+
lastResult = result.lastResult;
|
|
4716
5204
|
}
|
|
4717
5205
|
return {
|
|
4718
5206
|
table: lastResult?.table ?? null,
|
|
@@ -4725,8 +5213,12 @@ async function importRows(table, options) {
|
|
|
4725
5213
|
warningCount,
|
|
4726
5214
|
...(warningsTruncated ? { warningsTruncated: true } : {}),
|
|
4727
5215
|
} : {}),
|
|
4728
|
-
batchCount:
|
|
5216
|
+
batchCount: writeRequestCount,
|
|
4729
5217
|
batchSize,
|
|
5218
|
+
...(requestTooLargeRetries > 0 ? {
|
|
5219
|
+
requestTooLargeRetries,
|
|
5220
|
+
minimumBatchSizeUsed,
|
|
5221
|
+
} : {}),
|
|
4730
5222
|
...(target.tableWebUrl ? { table_web_url: target.tableWebUrl } : {}),
|
|
4731
5223
|
...(readRecordString(lastResult, "web_url") ? { web_url: readRecordString(lastResult, "web_url") } : {}),
|
|
4732
5224
|
};
|
|
@@ -4743,13 +5235,14 @@ async function prepareImportTarget(table, options, parsedRows) {
|
|
|
4743
5235
|
});
|
|
4744
5236
|
}
|
|
4745
5237
|
if (!options.create) {
|
|
5238
|
+
const keyMapping = await resolveExistingTableImportKeyMapping(table, parsedRows, readOption(options.upsertKey) ?? undefined);
|
|
4746
5239
|
return {
|
|
4747
5240
|
tableRef: table,
|
|
4748
|
-
rows: parsedRows,
|
|
5241
|
+
rows: remapImportRows(parsedRows, keyMapping.sourceKeyMap),
|
|
4749
5242
|
createdTable: null,
|
|
4750
5243
|
tableWebUrl: null,
|
|
4751
|
-
upsertKey:
|
|
4752
|
-
sourceKeyMap:
|
|
5244
|
+
upsertKey: keyMapping.upsertKey,
|
|
5245
|
+
sourceKeyMap: keyMapping.sourceKeyMap,
|
|
4753
5246
|
};
|
|
4754
5247
|
}
|
|
4755
5248
|
const normalized = normalizeRowsForNewTable(parsedRows);
|
|
@@ -4779,8 +5272,137 @@ async function prepareImportTarget(table, options, parsedRows) {
|
|
|
4779
5272
|
sourceKeyMap: normalized.keyBySource,
|
|
4780
5273
|
};
|
|
4781
5274
|
}
|
|
5275
|
+
async function resolveExistingTableImportKeyMapping(table, rows, upsertKey) {
|
|
5276
|
+
const describe = await requestOxygen("/api/cli/tables/describe", { method: "POST", body: { table } });
|
|
5277
|
+
const resolver = buildExistingTableColumnResolver(describe.columns ?? []);
|
|
5278
|
+
const sourceKeyMap = {};
|
|
5279
|
+
for (const sourceKey of inferImportColumnLabels(rows)) {
|
|
5280
|
+
const resolved = resolveImportSourceKey(sourceKey, resolver);
|
|
5281
|
+
if (resolved && resolved !== sourceKey)
|
|
5282
|
+
sourceKeyMap[sourceKey] = resolved;
|
|
5283
|
+
}
|
|
5284
|
+
return {
|
|
5285
|
+
sourceKeyMap: Object.keys(sourceKeyMap).length > 0 ? sourceKeyMap : null,
|
|
5286
|
+
upsertKey: upsertKey
|
|
5287
|
+
? resolveImportSourceKey(upsertKey, resolver) ?? upsertKey
|
|
5288
|
+
: undefined,
|
|
5289
|
+
};
|
|
5290
|
+
}
|
|
5291
|
+
function buildExistingTableColumnResolver(columns) {
|
|
5292
|
+
const exact = new Map();
|
|
5293
|
+
const aliases = new Map();
|
|
5294
|
+
for (const column of columns) {
|
|
5295
|
+
if (!column.key)
|
|
5296
|
+
continue;
|
|
5297
|
+
exact.set(column.key, column.key);
|
|
5298
|
+
addImportColumnAlias(aliases, column.label, column.key);
|
|
5299
|
+
addImportColumnAlias(aliases, normalizeImportColumnKey(column.key), column.key);
|
|
5300
|
+
if (column.label)
|
|
5301
|
+
addImportColumnAlias(aliases, normalizeImportColumnKey(column.label), column.key);
|
|
5302
|
+
}
|
|
5303
|
+
return { exact, aliases };
|
|
5304
|
+
}
|
|
5305
|
+
function addImportColumnAlias(aliases, alias, key) {
|
|
5306
|
+
const normalizedAlias = alias?.trim();
|
|
5307
|
+
if (!normalizedAlias)
|
|
5308
|
+
return;
|
|
5309
|
+
const existing = aliases.get(normalizedAlias);
|
|
5310
|
+
if (existing === undefined) {
|
|
5311
|
+
aliases.set(normalizedAlias, key);
|
|
5312
|
+
return;
|
|
5313
|
+
}
|
|
5314
|
+
if (existing !== key)
|
|
5315
|
+
aliases.set(normalizedAlias, null);
|
|
5316
|
+
}
|
|
5317
|
+
function resolveImportSourceKey(sourceKey, resolver) {
|
|
5318
|
+
const trimmed = sourceKey.trim();
|
|
5319
|
+
return resolver.exact.get(sourceKey)
|
|
5320
|
+
?? resolver.exact.get(trimmed)
|
|
5321
|
+
?? readImportColumnAlias(resolver.aliases, sourceKey)
|
|
5322
|
+
?? readImportColumnAlias(resolver.aliases, trimmed)
|
|
5323
|
+
?? readImportColumnAlias(resolver.aliases, normalizeImportColumnKey(sourceKey));
|
|
5324
|
+
}
|
|
5325
|
+
function readImportColumnAlias(aliases, alias) {
|
|
5326
|
+
return aliases.get(alias) ?? null;
|
|
5327
|
+
}
|
|
5328
|
+
function remapImportRows(rows, sourceKeyMap) {
|
|
5329
|
+
if (!sourceKeyMap)
|
|
5330
|
+
return rows;
|
|
5331
|
+
return rows.map((row) => {
|
|
5332
|
+
const remapped = {};
|
|
5333
|
+
for (const [key, value] of Object.entries(row)) {
|
|
5334
|
+
remapped[sourceKeyMap[key] ?? key] = value;
|
|
5335
|
+
}
|
|
5336
|
+
return remapped;
|
|
5337
|
+
});
|
|
5338
|
+
}
|
|
5339
|
+
async function writeImportBatchWithAutoSplit(input) {
|
|
5340
|
+
try {
|
|
5341
|
+
const result = await requestOxygen(input.upsertKey ? "/api/cli/tables/rows/upsert" : "/api/cli/tables/rows", {
|
|
5342
|
+
method: "POST",
|
|
5343
|
+
timeoutMs: 300_000,
|
|
5344
|
+
body: {
|
|
5345
|
+
table: input.tableRef,
|
|
5346
|
+
rows: input.rows,
|
|
5347
|
+
...(input.upsertKey ? { key: input.upsertKey } : {}),
|
|
5348
|
+
},
|
|
5349
|
+
});
|
|
5350
|
+
return summarizeImportBatchWrite(result, input.rows.length);
|
|
5351
|
+
}
|
|
5352
|
+
catch (error) {
|
|
5353
|
+
if (!isRequestTooLargeError(error) || input.rows.length <= 1)
|
|
5354
|
+
throw error;
|
|
5355
|
+
const midpoint = Math.ceil(input.rows.length / 2);
|
|
5356
|
+
process.stderr.write(`note: import batch of ${input.rows.length} rows exceeded the request limit; `
|
|
5357
|
+
+ `retrying as ${midpoint} and ${input.rows.length - midpoint} row batches.\n`);
|
|
5358
|
+
const first = await writeImportBatchWithAutoSplit({
|
|
5359
|
+
...input,
|
|
5360
|
+
rows: input.rows.slice(0, midpoint),
|
|
5361
|
+
});
|
|
5362
|
+
const second = await writeImportBatchWithAutoSplit({
|
|
5363
|
+
...input,
|
|
5364
|
+
rows: input.rows.slice(midpoint),
|
|
5365
|
+
});
|
|
5366
|
+
return combineImportBatchWriteSummaries(first, second, 1);
|
|
5367
|
+
}
|
|
5368
|
+
}
|
|
5369
|
+
function summarizeImportBatchWrite(result, batchSize) {
|
|
5370
|
+
const warningCount = readCount(result.warningCount);
|
|
5371
|
+
const batchWarnings = Array.isArray(result.warnings) ? result.warnings : [];
|
|
5372
|
+
return {
|
|
5373
|
+
rowCount: readCount(result.rowCount),
|
|
5374
|
+
insertedCount: readCount(result.insertedCount),
|
|
5375
|
+
updatedCount: readCount(result.updatedCount),
|
|
5376
|
+
warningCount,
|
|
5377
|
+
warnings: batchWarnings.slice(0, 20),
|
|
5378
|
+
warningsTruncated: result.warningsTruncated === true || warningCount > batchWarnings.length,
|
|
5379
|
+
writeRequestCount: 1,
|
|
5380
|
+
requestTooLargeRetries: 0,
|
|
5381
|
+
minimumBatchSizeUsed: batchSize,
|
|
5382
|
+
lastResult: result,
|
|
5383
|
+
};
|
|
5384
|
+
}
|
|
5385
|
+
function combineImportBatchWriteSummaries(first, second, extraRequestTooLargeRetries) {
|
|
5386
|
+
const warnings = [...first.warnings, ...second.warnings].slice(0, 20);
|
|
5387
|
+
const warningCount = first.warningCount + second.warningCount;
|
|
5388
|
+
return {
|
|
5389
|
+
rowCount: first.rowCount + second.rowCount,
|
|
5390
|
+
insertedCount: first.insertedCount + second.insertedCount,
|
|
5391
|
+
updatedCount: first.updatedCount + second.updatedCount,
|
|
5392
|
+
warningCount,
|
|
5393
|
+
warnings,
|
|
5394
|
+
warningsTruncated: first.warningsTruncated || second.warningsTruncated || warningCount > warnings.length,
|
|
5395
|
+
writeRequestCount: first.writeRequestCount + second.writeRequestCount,
|
|
5396
|
+
requestTooLargeRetries: first.requestTooLargeRetries + second.requestTooLargeRetries + extraRequestTooLargeRetries,
|
|
5397
|
+
minimumBatchSizeUsed: Math.min(first.minimumBatchSizeUsed, second.minimumBatchSizeUsed),
|
|
5398
|
+
lastResult: second.lastResult ?? first.lastResult,
|
|
5399
|
+
};
|
|
5400
|
+
}
|
|
5401
|
+
function isRequestTooLargeError(error) {
|
|
5402
|
+
return error instanceof OxygenError && error.code === "request_too_large";
|
|
5403
|
+
}
|
|
4782
5404
|
async function tryEnqueueStagedFileImport(// skipcq: JS-R1005
|
|
4783
|
-
table, options, format, parsedRows, context) {
|
|
5405
|
+
table, options, format, parsedRows, context, preparedTarget = null) {
|
|
4784
5406
|
if (options.create && table) {
|
|
4785
5407
|
throw new OxygenError("invalid_import_target", "Pass either a table argument or --create, not both.", {
|
|
4786
5408
|
exitCode: 1,
|
|
@@ -4817,7 +5439,7 @@ table, options, format, parsedRows, context) {
|
|
|
4817
5439
|
return null;
|
|
4818
5440
|
// Create the table for --create (or resolve the existing ref) before the
|
|
4819
5441
|
// upload so a presign success always pairs with a real target.
|
|
4820
|
-
const target = await prepareImportTarget(table, options, parsedRows);
|
|
5442
|
+
const target = preparedTarget ?? await prepareImportTarget(table, options, parsedRows);
|
|
4821
5443
|
const controller = new AbortController();
|
|
4822
5444
|
const timer = setTimeout(() => controller.abort(), 300_000);
|
|
4823
5445
|
let putResponse;
|
|
@@ -4862,7 +5484,7 @@ table, options, format, parsedRows, context) {
|
|
|
4862
5484
|
storage_provider: storageProvider,
|
|
4863
5485
|
};
|
|
4864
5486
|
}
|
|
4865
|
-
async function enqueueImportFile(table, options, format, batchSize, context) {
|
|
5487
|
+
async function enqueueImportFile(table, options, format, batchSize, context, preparedTarget = null) {
|
|
4866
5488
|
if (options.create && table) {
|
|
4867
5489
|
throw new OxygenError("invalid_import_target", "Pass either a table argument or --create, not both.", {
|
|
4868
5490
|
exitCode: 1,
|
|
@@ -4883,16 +5505,20 @@ async function enqueueImportFile(table, options, format, batchSize, context) {
|
|
|
4883
5505
|
form.append("batch_size", String(batchSize));
|
|
4884
5506
|
form.append("auto_background", String(context.autoBackground));
|
|
4885
5507
|
form.append("source_hash", context.sourceHash);
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
5508
|
+
const targetTable = preparedTarget?.tableRef ?? table;
|
|
5509
|
+
if (targetTable)
|
|
5510
|
+
form.append("table", targetTable);
|
|
5511
|
+
if (!preparedTarget && options.create)
|
|
4889
5512
|
form.append("table_name", options.create);
|
|
4890
5513
|
const project = readOption(options.project);
|
|
4891
5514
|
if (project)
|
|
4892
5515
|
form.append("project", project);
|
|
4893
|
-
const upsertKey = readOption(options.upsertKey);
|
|
5516
|
+
const upsertKey = preparedTarget?.upsertKey ?? readOption(options.upsertKey);
|
|
4894
5517
|
if (upsertKey)
|
|
4895
5518
|
form.append("upsert_key", upsertKey);
|
|
5519
|
+
if (preparedTarget?.sourceKeyMap) {
|
|
5520
|
+
form.append("source_key_map", JSON.stringify(preparedTarget.sourceKeyMap));
|
|
5521
|
+
}
|
|
4896
5522
|
if (options.sheet)
|
|
4897
5523
|
form.append("sheet", options.sheet);
|
|
4898
5524
|
const maxConcurrency = readPositiveInt(options.maxConcurrency);
|
|
@@ -5500,7 +6126,7 @@ function formatRows(rows, format, columns) {
|
|
|
5500
6126
|
const lines = [renderRow(headers), separator, ...formattedRows.map(renderRow)];
|
|
5501
6127
|
if (rescuedCount > 0) {
|
|
5502
6128
|
lines.push("");
|
|
5503
|
-
lines.push(`# Note: reformatted ${rescuedCount} cell${rescuedCount === 1 ? "" : "s"} that look numeric in text columns. Run \`oxygen columns retype --
|
|
6129
|
+
lines.push(`# Note: reformatted ${rescuedCount} cell${rescuedCount === 1 ? "" : "s"} that look numeric in text columns. Run \`oxygen columns retype <table> <column> --data-type numeric\` to make this permanent.`);
|
|
5504
6130
|
}
|
|
5505
6131
|
return { content: `${lines.join("\n")}\n`, rescuedCount };
|
|
5506
6132
|
}
|
|
@@ -6731,6 +7357,93 @@ function ansi(enabled) {
|
|
|
6731
7357
|
function readOption(value) {
|
|
6732
7358
|
return value?.trim() ? value.trim() : null;
|
|
6733
7359
|
}
|
|
7360
|
+
// Assemble the POST body for `oxygen feedback`. Reads the local chat transcript
|
|
7361
|
+
// (unless --no-transcript) and attaches a non-sensitive environment snapshot so
|
|
7362
|
+
// the Oxygen team can triage. The transcript read happens here, inside the
|
|
7363
|
+
// command action, so any failure surfaces in the JSON envelope.
|
|
7364
|
+
function buildFeedbackBody(options) {
|
|
7365
|
+
const message = readOption(options.message);
|
|
7366
|
+
if (!message) {
|
|
7367
|
+
throw new OxygenError("invalid_input", 'Provide your feedback with --message "...".', { exitCode: 1 });
|
|
7368
|
+
}
|
|
7369
|
+
const body = {
|
|
7370
|
+
message,
|
|
7371
|
+
category: readOption(options.category) ?? "feedback",
|
|
7372
|
+
context: {
|
|
7373
|
+
cli_version: OXYGEN_VERSION,
|
|
7374
|
+
environment: collectFeedbackEnvironment(),
|
|
7375
|
+
},
|
|
7376
|
+
};
|
|
7377
|
+
if (readOption(options.severity))
|
|
7378
|
+
body.severity = readOption(options.severity);
|
|
7379
|
+
if (options.transcript !== false) {
|
|
7380
|
+
try {
|
|
7381
|
+
const captured = captureCurrentTranscript({
|
|
7382
|
+
sessionId: readOption(options.sessionId),
|
|
7383
|
+
file: readOption(options.file),
|
|
7384
|
+
});
|
|
7385
|
+
if (captured) {
|
|
7386
|
+
const { path: _path, ...payload } = captured;
|
|
7387
|
+
body.transcript = payload;
|
|
7388
|
+
}
|
|
7389
|
+
else {
|
|
7390
|
+
process.stderr.write("note: no local chat transcript found; sending your note without one.\n");
|
|
7391
|
+
}
|
|
7392
|
+
}
|
|
7393
|
+
catch (error) {
|
|
7394
|
+
if (error instanceof TranscriptCaptureError) {
|
|
7395
|
+
throw new OxygenError("transcript_not_found", error.message, { exitCode: 1 });
|
|
7396
|
+
}
|
|
7397
|
+
throw error;
|
|
7398
|
+
}
|
|
7399
|
+
}
|
|
7400
|
+
return body;
|
|
7401
|
+
}
|
|
7402
|
+
function splitCsvOption(value) {
|
|
7403
|
+
if (!value)
|
|
7404
|
+
return [];
|
|
7405
|
+
return value
|
|
7406
|
+
.split(",")
|
|
7407
|
+
.map((entry) => entry.trim())
|
|
7408
|
+
.filter((entry) => entry.length > 0);
|
|
7409
|
+
}
|
|
7410
|
+
function buildSupportTicketBody(options) {
|
|
7411
|
+
const body = { subject: readOption(options.subject) };
|
|
7412
|
+
if (readOption(options.body))
|
|
7413
|
+
body.body = readOption(options.body);
|
|
7414
|
+
if (readOption(options.severity))
|
|
7415
|
+
body.severity = readOption(options.severity);
|
|
7416
|
+
if (readOption(options.category))
|
|
7417
|
+
body.category = readOption(options.category);
|
|
7418
|
+
const context = {};
|
|
7419
|
+
if (readOption(options.operation))
|
|
7420
|
+
context.operation = readOption(options.operation);
|
|
7421
|
+
if (readOption(options.errorCode))
|
|
7422
|
+
context.error_code = readOption(options.errorCode);
|
|
7423
|
+
const runIds = splitCsvOption(options.runIds);
|
|
7424
|
+
const tableIds = splitCsvOption(options.tableIds);
|
|
7425
|
+
const deepLinks = splitCsvOption(options.deepLinks);
|
|
7426
|
+
if (runIds.length)
|
|
7427
|
+
context.run_ids = runIds;
|
|
7428
|
+
if (tableIds.length)
|
|
7429
|
+
context.table_ids = tableIds;
|
|
7430
|
+
if (deepLinks.length)
|
|
7431
|
+
context.deep_links = deepLinks;
|
|
7432
|
+
if (Object.keys(context).length)
|
|
7433
|
+
body.context = context;
|
|
7434
|
+
return body;
|
|
7435
|
+
}
|
|
7436
|
+
function withSupportListQuery(path, options) {
|
|
7437
|
+
const params = new URLSearchParams();
|
|
7438
|
+
const status = readOption(options.status);
|
|
7439
|
+
const limit = readOption(options.limit);
|
|
7440
|
+
if (status)
|
|
7441
|
+
params.set("status", status);
|
|
7442
|
+
if (limit)
|
|
7443
|
+
params.set("limit", limit);
|
|
7444
|
+
const query = params.toString();
|
|
7445
|
+
return query ? `${path}?${query}` : path;
|
|
7446
|
+
}
|
|
6734
7447
|
function readCsvOption(value) {
|
|
6735
7448
|
const option = readOption(value);
|
|
6736
7449
|
if (!option)
|
|
@@ -6746,6 +7459,26 @@ function splitCsv(value) {
|
|
|
6746
7459
|
.map((entry) => entry.trim())
|
|
6747
7460
|
.filter(Boolean);
|
|
6748
7461
|
}
|
|
7462
|
+
// Assemble the optional campaign email binding from the --email-* flags. The
|
|
7463
|
+
// content spec (--email-definition-file) is the author-provided email sequence
|
|
7464
|
+
// that the API compiles to an Instantly campaign on start; provider/connection
|
|
7465
|
+
// pick which Instantly account to bind. Returns undefined when no email flags
|
|
7466
|
+
// are set (a LinkedIn-only campaign).
|
|
7467
|
+
function readCampaignEmailBinding(options) {
|
|
7468
|
+
const provider = readOption(options.emailProvider);
|
|
7469
|
+
const connectionId = readOption(options.emailConnection);
|
|
7470
|
+
const definitionPath = readOption(options.emailDefinitionFile);
|
|
7471
|
+
if (!provider && !connectionId && !definitionPath)
|
|
7472
|
+
return undefined;
|
|
7473
|
+
const definition = definitionPath
|
|
7474
|
+
? JSON.parse(readFileSync(resolve(definitionPath), "utf8"))
|
|
7475
|
+
: undefined;
|
|
7476
|
+
return {
|
|
7477
|
+
...(provider ? { provider } : {}),
|
|
7478
|
+
...(connectionId ? { connection_id: connectionId } : {}),
|
|
7479
|
+
...(definition !== undefined ? { definition } : {}),
|
|
7480
|
+
};
|
|
7481
|
+
}
|
|
6749
7482
|
function collectMultiple(value, previous) {
|
|
6750
7483
|
return [...previous, value];
|
|
6751
7484
|
}
|
|
@@ -6908,6 +7641,9 @@ table, options) {
|
|
|
6908
7641
|
...(readOption(options.emailPatternValidation)
|
|
6909
7642
|
? { email_pattern_validation: readOption(options.emailPatternValidation) }
|
|
6910
7643
|
: {}),
|
|
7644
|
+
...(readOption(options.phoneWaterfallProfile)
|
|
7645
|
+
? { phone_waterfall_profile: readOption(options.phoneWaterfallProfile) }
|
|
7646
|
+
: {}),
|
|
6911
7647
|
...(options.verifyPhone ? { verify_phone: true } : {}),
|
|
6912
7648
|
...(readOption(options.phoneVerificationCredentialMode)
|
|
6913
7649
|
? { phone_verification_credential_mode: readOption(options.phoneVerificationCredentialMode) }
|
|
@@ -6919,22 +7655,7 @@ table, options) {
|
|
|
6919
7655
|
...(options.onlyMissing ? { only_missing: true } : {}),
|
|
6920
7656
|
};
|
|
6921
7657
|
}
|
|
6922
|
-
function
|
|
6923
|
-
if (!readOption(value))
|
|
6924
|
-
return undefined;
|
|
6925
|
-
const days = readCsvOption(value).map((entry) => {
|
|
6926
|
-
const parsed = Number(entry);
|
|
6927
|
-
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 7) {
|
|
6928
|
-
throw new OxygenError("invalid_request", "--working-days must be comma-separated ISO weekdays 1-7 (1=Mon..7=Sun).", {
|
|
6929
|
-
details: { value },
|
|
6930
|
-
exitCode: 1,
|
|
6931
|
-
});
|
|
6932
|
-
}
|
|
6933
|
-
return parsed;
|
|
6934
|
-
});
|
|
6935
|
-
return days;
|
|
6936
|
-
}
|
|
6937
|
-
function buildLinkedinLimitsBody(// skipcq: JS-R1005 -- CLI body builder maps per-account limit and working-hours flags.
|
|
7658
|
+
function buildLinkedinLimitsBody(// skipcq: JS-R1005 -- CLI body builder maps per-account limit flags + reset timezone.
|
|
6938
7659
|
options) {
|
|
6939
7660
|
const limits = {};
|
|
6940
7661
|
const setLimit = (key, value) => {
|
|
@@ -6957,23 +7678,16 @@ options) {
|
|
|
6957
7678
|
setLimit("total_reads_per_day", options.totalReadsPerDay);
|
|
6958
7679
|
setLimit("min_action_spacing_seconds", options.minSpacingSeconds);
|
|
6959
7680
|
setLimit("action_spacing_jitter_seconds", options.spacingJitterSeconds);
|
|
7681
|
+
// Send windows are campaign-scoped; the only per-account time setting is the
|
|
7682
|
+
// timezone the daily counters reset in.
|
|
6960
7683
|
const workingHours = {};
|
|
6961
7684
|
const timezone = readOption(options.timezone);
|
|
6962
7685
|
if (timezone)
|
|
6963
7686
|
workingHours.timezone = timezone;
|
|
6964
|
-
const days = readWorkingDaysOption(options.workingDays);
|
|
6965
|
-
if (days !== undefined)
|
|
6966
|
-
workingHours.days = days;
|
|
6967
|
-
const start = readOption(options.workingStart);
|
|
6968
|
-
if (start)
|
|
6969
|
-
workingHours.start = start;
|
|
6970
|
-
const end = readOption(options.workingEnd);
|
|
6971
|
-
if (end)
|
|
6972
|
-
workingHours.end = end;
|
|
6973
7687
|
const hasLimits = Object.keys(limits).length > 0;
|
|
6974
7688
|
const hasWorkingHours = Object.keys(workingHours).length > 0;
|
|
6975
7689
|
if (!hasLimits && !hasWorkingHours) {
|
|
6976
|
-
throw new OxygenError("invalid_request", "Pass at least one limit flag (e.g. --invites-per-day) or
|
|
7690
|
+
throw new OxygenError("invalid_request", "Pass at least one limit flag (e.g. --invites-per-day) or --timezone.", { exitCode: 1 });
|
|
6977
7691
|
}
|
|
6978
7692
|
return {
|
|
6979
7693
|
...(hasLimits ? { limits } : {}),
|