@oxygen-agent/cli 1.152.15 → 1.162.10
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 +938 -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";
|
|
@@ -136,6 +137,82 @@ function parseJsonArray(value) {
|
|
|
136
137
|
}
|
|
137
138
|
return parsed;
|
|
138
139
|
}
|
|
140
|
+
async function readDeleteRowIdsOption(options) {
|
|
141
|
+
const rowIdsJson = readOption(options.rowIdsJson);
|
|
142
|
+
const rowIdsFile = readOption(options.rowIdsFile);
|
|
143
|
+
if (rowIdsJson && rowIdsFile) {
|
|
144
|
+
throw new OxygenError("invalid_request", "Pass either --row-ids-json or --row-ids-file, not both.", {
|
|
145
|
+
exitCode: 1,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (!rowIdsJson && !rowIdsFile) {
|
|
149
|
+
throw new OxygenError("invalid_request", "Pass --row-ids-json or --row-ids-file.", {
|
|
150
|
+
exitCode: 1,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (rowIdsJson)
|
|
154
|
+
return normalizeDeleteRowIds(parseJsonArray(rowIdsJson));
|
|
155
|
+
const filePath = resolve(rowIdsFile ?? "");
|
|
156
|
+
const buffer = readFileSync(filePath);
|
|
157
|
+
const format = normalizeRowsFormat(options.format, inferRowsFileFormat(filePath));
|
|
158
|
+
if (format === "json") {
|
|
159
|
+
const text = buffer.toString("utf8");
|
|
160
|
+
const inlineIds = tryParseJsonStringArray(text);
|
|
161
|
+
if (inlineIds)
|
|
162
|
+
return normalizeDeleteRowIds(inlineIds);
|
|
163
|
+
if (!options.format && !text.trimStart().startsWith("[")) {
|
|
164
|
+
return normalizeDeleteRowIds(parsePlainRowIdList(text));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const sheet = readOption(options.sheet);
|
|
168
|
+
const rows = await parseRowsFileBuffer(buffer, format, sheet ? { sheet } : {});
|
|
169
|
+
const rowIdColumn = readOption(options.rowIdColumn) ?? "_row_id";
|
|
170
|
+
return normalizeDeleteRowIds(rows.map((row, index) => {
|
|
171
|
+
const value = row[rowIdColumn];
|
|
172
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
173
|
+
throw new OxygenError("invalid_request", `Row id column "${rowIdColumn}" must contain row UUID strings.`, {
|
|
174
|
+
details: { row_number: index + 1, column: rowIdColumn },
|
|
175
|
+
exitCode: 1,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return value.trim();
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
function tryParseJsonStringArray(value) {
|
|
182
|
+
let parsed;
|
|
183
|
+
try {
|
|
184
|
+
parsed = JSON.parse(value);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
if (!Array.isArray(parsed) || !parsed.every((entry) => typeof entry === "string"))
|
|
190
|
+
return null;
|
|
191
|
+
return parsed;
|
|
192
|
+
}
|
|
193
|
+
function parsePlainRowIdList(value) {
|
|
194
|
+
return value
|
|
195
|
+
.split(/[\n,]/)
|
|
196
|
+
.map((entry) => entry.trim())
|
|
197
|
+
.filter(Boolean);
|
|
198
|
+
}
|
|
199
|
+
function normalizeDeleteRowIds(values) {
|
|
200
|
+
const rowIds = values.map((value, index) => {
|
|
201
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
202
|
+
throw new OxygenError("invalid_request", "Row IDs must be non-empty strings.", {
|
|
203
|
+
details: { index },
|
|
204
|
+
exitCode: 1,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return value.trim();
|
|
208
|
+
});
|
|
209
|
+
if (rowIds.length === 0) {
|
|
210
|
+
throw new OxygenError("invalid_request", "Row ID list cannot be empty.", {
|
|
211
|
+
exitCode: 1,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return rowIds;
|
|
215
|
+
}
|
|
139
216
|
function readCustomIntegrationManifest(options) {
|
|
140
217
|
const manifestPath = readOption(options.manifest);
|
|
141
218
|
const manifestJson = readOption(options.manifestJson);
|
|
@@ -426,6 +503,100 @@ export function createProgram() {
|
|
|
426
503
|
},
|
|
427
504
|
}));
|
|
428
505
|
}));
|
|
506
|
+
program
|
|
507
|
+
.command("support")
|
|
508
|
+
.description("File and track Oxygen support tickets.")
|
|
509
|
+
.addCommand(new Command("file")
|
|
510
|
+
.description("File a support ticket. Use when you're stuck on an Oxygen operation.")
|
|
511
|
+
.requiredOption("--subject <subject>", "One-line summary of the problem.")
|
|
512
|
+
.option("--body <body>", "What you were doing, what happened, and what you tried.")
|
|
513
|
+
.option("--severity <severity>", "low | normal | high. Defaults to normal.")
|
|
514
|
+
.option("--category <category>", "Optional category label.")
|
|
515
|
+
.option("--operation <operation>", "The operation/tool that failed, e.g. oxygen_columns_run.")
|
|
516
|
+
.option("--error-code <code>", "The error envelope code you received.")
|
|
517
|
+
.option("--run-ids <ids>", "Comma-separated run ids to attach.")
|
|
518
|
+
.option("--table-ids <ids>", "Comma-separated table ids to attach.")
|
|
519
|
+
.option("--deep-links <urls>", "Comma-separated https://oxygen-agent.com/... links.")
|
|
520
|
+
.option("--json", "Print a JSON envelope.")
|
|
521
|
+
.action(async (options) => {
|
|
522
|
+
await handleAsyncAction("support ticket create", options, () => requestOxygen("/api/cli/support/tickets", {
|
|
523
|
+
method: "POST",
|
|
524
|
+
body: buildSupportTicketBody(options),
|
|
525
|
+
}));
|
|
526
|
+
}))
|
|
527
|
+
.addCommand(new Command("list")
|
|
528
|
+
.description("List support tickets for the active organization.")
|
|
529
|
+
.option("--status <status>", "Filter by status (open, triaging, waiting_on_user, resolved, closed).")
|
|
530
|
+
.option("--limit <n>", "Max tickets to return.")
|
|
531
|
+
.option("--json", "Print a JSON envelope.")
|
|
532
|
+
.action(async (options) => {
|
|
533
|
+
await handleAsyncAction("support tickets list", options, () => requestOxygen(withSupportListQuery("/api/cli/support/tickets", options)));
|
|
534
|
+
}))
|
|
535
|
+
.addCommand(new Command("get")
|
|
536
|
+
.description("Show one support ticket with its message thread.")
|
|
537
|
+
.argument("<ticketId>", "Ticket UUID.")
|
|
538
|
+
.option("--json", "Print a JSON envelope.")
|
|
539
|
+
.action(async (ticketId, options) => {
|
|
540
|
+
await handleAsyncAction("support ticket get", options, () => requestOxygen(`/api/cli/support/tickets/${encodeURIComponent(ticketId)}`));
|
|
541
|
+
}))
|
|
542
|
+
.addCommand(new Command("reply")
|
|
543
|
+
.description("Add a message to a support ticket. Reopens a resolved ticket.")
|
|
544
|
+
.argument("<ticketId>", "Ticket UUID.")
|
|
545
|
+
.requiredOption("--body <body>", "Your reply.")
|
|
546
|
+
.option("--json", "Print a JSON envelope.")
|
|
547
|
+
.action(async (ticketId, options) => {
|
|
548
|
+
await handleAsyncAction("support ticket reply", options, () => requestOxygen(`/api/cli/support/tickets/${encodeURIComponent(ticketId)}/messages`, {
|
|
549
|
+
method: "POST",
|
|
550
|
+
body: { body: readOption(options.body) },
|
|
551
|
+
}));
|
|
552
|
+
}))
|
|
553
|
+
.addCommand(new Command("ack")
|
|
554
|
+
.description("Mark a resolved ticket as seen so it leaves the unread count.")
|
|
555
|
+
.argument("<ticketId>", "Ticket UUID.")
|
|
556
|
+
.option("--json", "Print a JSON envelope.")
|
|
557
|
+
.action(async (ticketId, options) => {
|
|
558
|
+
await handleAsyncAction("support ticket ack", options, () => requestOxygen(`/api/cli/support/tickets/${encodeURIComponent(ticketId)}/ack`, {
|
|
559
|
+
method: "POST",
|
|
560
|
+
body: {},
|
|
561
|
+
}));
|
|
562
|
+
}))
|
|
563
|
+
.addCommand(new Command("admin")
|
|
564
|
+
.description("Staff-only support triage commands.")
|
|
565
|
+
.addCommand(new Command("list")
|
|
566
|
+
.description("List support tickets across all organizations (staff only).")
|
|
567
|
+
.option("--status <status>", "Filter by status.")
|
|
568
|
+
.option("--limit <n>", "Max tickets to return.")
|
|
569
|
+
.option("--json", "Print a JSON envelope.")
|
|
570
|
+
.action(async (options) => {
|
|
571
|
+
await handleAsyncAction("support admin list", options, () => requestOxygen(withSupportListQuery("/api/cli/admin/support/tickets", options)));
|
|
572
|
+
}))
|
|
573
|
+
.addCommand(new Command("resolve")
|
|
574
|
+
.description("Resolve a support ticket and notify the opener (staff only).")
|
|
575
|
+
.argument("<ticketId>", "Ticket UUID.")
|
|
576
|
+
.requiredOption("--resolution <text>", "Resolution message sent to the opener.")
|
|
577
|
+
.option("--json", "Print a JSON envelope.")
|
|
578
|
+
.action(async (ticketId, options) => {
|
|
579
|
+
await handleAsyncAction("support admin resolve", options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}/resolve`, {
|
|
580
|
+
method: "POST",
|
|
581
|
+
body: { resolution: readOption(options.resolution) },
|
|
582
|
+
}));
|
|
583
|
+
})));
|
|
584
|
+
program
|
|
585
|
+
.command("feedback")
|
|
586
|
+
.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.")
|
|
587
|
+
.option("-m, --message <message>", "Your feedback or bug report.")
|
|
588
|
+
.option("--severity <severity>", "low | normal | high. Defaults to normal.")
|
|
589
|
+
.option("--category <category>", "Optional category label. Defaults to 'feedback'.")
|
|
590
|
+
.option("--session-id <id>", "Attach a specific local session transcript by id. Defaults to the most recently active session.")
|
|
591
|
+
.option("--file <path>", "Attach a specific transcript file instead of auto-detecting the current session.")
|
|
592
|
+
.option("--no-transcript", "Send your note only, without attaching any chat transcript.")
|
|
593
|
+
.option("--json", "Print a JSON envelope.")
|
|
594
|
+
.action(async (options) => {
|
|
595
|
+
await handleAsyncAction("feedback send", options, () => requestOxygen("/api/cli/feedback", {
|
|
596
|
+
method: "POST",
|
|
597
|
+
body: buildFeedbackBody(options),
|
|
598
|
+
}));
|
|
599
|
+
});
|
|
429
600
|
program
|
|
430
601
|
.command("db")
|
|
431
602
|
.description("Tenant database commands.")
|
|
@@ -671,6 +842,27 @@ export function createProgram() {
|
|
|
671
842
|
row_id: rowId,
|
|
672
843
|
},
|
|
673
844
|
}));
|
|
845
|
+
}))
|
|
846
|
+
.addCommand(new Command("delete-rows")
|
|
847
|
+
.description("Delete multiple workspace table rows.")
|
|
848
|
+
.argument("<table>", "Table id or slug.")
|
|
849
|
+
.option("--row-ids-json <json>", "JSON array of workspace row UUIDs.")
|
|
850
|
+
.option("--row-ids-file <path>", "File containing row UUIDs or rows with a _row_id column.")
|
|
851
|
+
.option("--row-id-column <key>", "Column to read from --row-ids-file. Defaults to _row_id.")
|
|
852
|
+
.option("--format <format>", "File format for --row-ids-file: json, jsonl, csv, or xlsx.")
|
|
853
|
+
.option("--sheet <name>", "Worksheet name when reading row IDs from an XLSX file.")
|
|
854
|
+
.option("--json", "Print a JSON envelope.")
|
|
855
|
+
.action(async (table, options) => {
|
|
856
|
+
await handleAsyncAction("tables delete-rows", options, async () => {
|
|
857
|
+
const rowIds = await readDeleteRowIdsOption(options);
|
|
858
|
+
return requestOxygen("/api/cli/tables/rows/delete", {
|
|
859
|
+
method: "POST",
|
|
860
|
+
body: {
|
|
861
|
+
table,
|
|
862
|
+
row_ids: rowIds,
|
|
863
|
+
},
|
|
864
|
+
});
|
|
865
|
+
});
|
|
674
866
|
}))
|
|
675
867
|
.addCommand(new Command("upsert")
|
|
676
868
|
.description("Insert or update rows in a workspace table by a column key.")
|
|
@@ -2288,6 +2480,75 @@ export function createProgram() {
|
|
|
2288
2480
|
body: readCompaniesEnrichBody(table, options),
|
|
2289
2481
|
}));
|
|
2290
2482
|
})));
|
|
2483
|
+
program
|
|
2484
|
+
.command("people")
|
|
2485
|
+
.description("People and contact prospecting workflows.")
|
|
2486
|
+
.addCommand(new Command("search")
|
|
2487
|
+
.description("Plan, dry-run, or queue provider-backed people/contact search.")
|
|
2488
|
+
.addCommand(new Command("plan")
|
|
2489
|
+
.description("Compile a people-search prompt and optional typed persona filters into ordered provider routes without provider calls.")
|
|
2490
|
+
.requiredOption("--prompt <text-or-file>", "People-search prompt, or a path to a prompt file.")
|
|
2491
|
+
.option("--target-count <n>", "Desired contact count for routing and estimates.")
|
|
2492
|
+
.option("--source-intent <intent>", "Override detected intent: persona_search, account_contacts, audience_sizing, profile_lookup, concept_persona, or fallback_broad.")
|
|
2493
|
+
.option("--filters-json <json-or-file>", "PeopleSearchFilters JSON inline or a @file/path; wins over individual flags per top-level filter path.")
|
|
2494
|
+
.option("--titles <csv>", "Comma-separated job titles to include.")
|
|
2495
|
+
.option("--adjacent-titles <csv>", "Comma-separated adjacent/looser titles to accept.")
|
|
2496
|
+
.option("--exclude-titles <csv>", "Comma-separated job titles to exclude.")
|
|
2497
|
+
.option("--seniorities <csv>", "Comma-separated seniority levels: C-Suite, VP, Director, Manager, Staff.")
|
|
2498
|
+
.option("--departments <csv>", "Comma-separated departments/functions: Sales, Marketing, Engineering, ...")
|
|
2499
|
+
.option("--countries <csv>", "Comma-separated ISO 3166-1 alpha-2 person country codes.")
|
|
2500
|
+
.option("--keywords <csv>", "Comma-separated free-text persona keywords.")
|
|
2501
|
+
.option("--company-domains <csv>", "Comma-separated company domains to scope contacts to.")
|
|
2502
|
+
.option("--company-names <csv>", "Comma-separated company names to scope contacts to.")
|
|
2503
|
+
.option("--employees <range>", "Company employee count range: 20-200, 500+, or -50.")
|
|
2504
|
+
.option("--require-email", "Only return people with a work email available (provider-dependent).")
|
|
2505
|
+
.option("--require-phone", "Only return people with a phone/mobile available (provider-dependent).")
|
|
2506
|
+
.option("--max-per-company <n>", "Cap on contacts per company for account-anchored searches.")
|
|
2507
|
+
.option("--estimate", "Run a free count probe for an estimated match count.")
|
|
2508
|
+
.option("--materialize-preview", "Create a preview table with route rows.")
|
|
2509
|
+
.option("--json", "Print a JSON envelope.")
|
|
2510
|
+
.action(async (options) => {
|
|
2511
|
+
await handleAsyncAction("people search plan", options, () => requestOxygen("/api/cli/people/search/plan", {
|
|
2512
|
+
method: "POST",
|
|
2513
|
+
body: readPeopleSearchPlanBody(options),
|
|
2514
|
+
}));
|
|
2515
|
+
}))
|
|
2516
|
+
.addCommand(new Command("run")
|
|
2517
|
+
.description("Return a dry-run request or queue a live people-search ingestion run. Upsert dedup is on linkedin_url.")
|
|
2518
|
+
.option("--prompt <text-or-file>", "People-search prompt, or a path to a prompt file.")
|
|
2519
|
+
.option("--plan-json <json-or-file>", "Plan JSON returned by people search plan, or a path to a JSON file.")
|
|
2520
|
+
.option("--route-id <id>", "Route id from the plan to execute.")
|
|
2521
|
+
.option("--tool-id <tool>", "Tool id from the plan to execute.")
|
|
2522
|
+
.option("--table <table>", "Existing table id or slug to receive rows. If omitted for live, Oxygen creates a table.")
|
|
2523
|
+
.option("--upsert-key <column>", "Column key used for live upsert. Must match the plan upsert key (linkedin_url).")
|
|
2524
|
+
.option("--mode <mode>", "dry_run or live. Defaults to dry_run.")
|
|
2525
|
+
.option("--max-pages <n>", "Maximum provider pages to ingest. Defaults to the route estimate.")
|
|
2526
|
+
.option("--max-credits <n>", "Required credit ceiling for live runs.")
|
|
2527
|
+
.option("--target-count <n>", "Desired contact count when planning from --prompt.")
|
|
2528
|
+
.option("--source-intent <intent>", "Override detected intent when planning from --prompt.")
|
|
2529
|
+
.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.")
|
|
2530
|
+
.option("--titles <csv>", "Comma-separated job titles to include when planning from --prompt.")
|
|
2531
|
+
.option("--adjacent-titles <csv>", "Comma-separated adjacent titles when planning from --prompt.")
|
|
2532
|
+
.option("--exclude-titles <csv>", "Comma-separated job titles to exclude when planning from --prompt.")
|
|
2533
|
+
.option("--seniorities <csv>", "Comma-separated seniority levels when planning from --prompt.")
|
|
2534
|
+
.option("--departments <csv>", "Comma-separated departments/functions when planning from --prompt.")
|
|
2535
|
+
.option("--countries <csv>", "Comma-separated ISO 3166-1 alpha-2 person country codes when planning from --prompt.")
|
|
2536
|
+
.option("--keywords <csv>", "Comma-separated persona keywords when planning from --prompt.")
|
|
2537
|
+
.option("--company-domains <csv>", "Comma-separated company domains when planning from --prompt.")
|
|
2538
|
+
.option("--company-names <csv>", "Comma-separated company names when planning from --prompt.")
|
|
2539
|
+
.option("--employees <range>", "Company employee count range when planning from --prompt.")
|
|
2540
|
+
.option("--require-email", "Only return people with a work email available when planning from --prompt.")
|
|
2541
|
+
.option("--require-phone", "Only return people with a phone available when planning from --prompt.")
|
|
2542
|
+
.option("--max-per-company <n>", "Cap on contacts per company when planning from --prompt.")
|
|
2543
|
+
.option("--estimate", "Run a free count probe when planning from --prompt.")
|
|
2544
|
+
.option("--approved", "Required for live runs after inspecting dry-run output.")
|
|
2545
|
+
.option("--json", "Print a JSON envelope.")
|
|
2546
|
+
.action(async (options) => {
|
|
2547
|
+
await handleAsyncAction("people search run", options, () => requestOxygen("/api/cli/people/search/run", {
|
|
2548
|
+
method: "POST",
|
|
2549
|
+
body: readPeopleSearchRunBody(options),
|
|
2550
|
+
}));
|
|
2551
|
+
})));
|
|
2291
2552
|
program
|
|
2292
2553
|
.command("worker")
|
|
2293
2554
|
.description("Background worker commands.")
|
|
@@ -2792,7 +3053,7 @@ export function createProgram() {
|
|
|
2792
3053
|
.description("Run an executable Oxygen tool through the HTTPS API.")
|
|
2793
3054
|
.argument("<tool_id>", "Tool id.")
|
|
2794
3055
|
.requiredOption("--input-json <json>", "Tool input as a JSON object.")
|
|
2795
|
-
.option("--mode <mode>", "Execution mode: live
|
|
3056
|
+
.option("--mode <mode>", "Execution mode: live or dry-run. Mutating tools default to dry-run.")
|
|
2796
3057
|
.option("--credential-mode <mode>", "Credential mode: managed, user_api_key, or user_oauth.")
|
|
2797
3058
|
.option("--org <organization_id>", "Optional organization id. Must match the stored CLI token.")
|
|
2798
3059
|
.option("--org-id <organization_id>", "Optional organization id. Must match the stored CLI token.")
|
|
@@ -2841,7 +3102,8 @@ export function createProgram() {
|
|
|
2841
3102
|
.option("--provider-order <providers>", "Comma-separated provider order. Overrides the default cost-aware waterfall profile.")
|
|
2842
3103
|
.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
3104
|
.option("--email-pattern-validation <mode>", "Work-email pattern pre-step: leadmagic_valid_only or disabled.")
|
|
2844
|
-
.option("--
|
|
3105
|
+
.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%).")
|
|
3106
|
+
.option("--verify-phone", "Validate found phone numbers with ClearoutPhone (adds phone_line_type + phone_carrier; filter phone_line_type=mobile for mobile-only).")
|
|
2845
3107
|
.option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
|
|
2846
3108
|
.option("--limit <n>", "Rows to estimate. Defaults to 10.")
|
|
2847
3109
|
.option("--all", "Estimate all rows.")
|
|
@@ -2874,7 +3136,8 @@ export function createProgram() {
|
|
|
2874
3136
|
.option("--provider-order <providers>", "Comma-separated provider order. Overrides the default cost-aware waterfall profile.")
|
|
2875
3137
|
.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
3138
|
.option("--email-pattern-validation <mode>", "Work-email pattern pre-step: leadmagic_valid_only or disabled.")
|
|
2877
|
-
.option("--
|
|
3139
|
+
.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%).")
|
|
3140
|
+
.option("--verify-phone", "Validate found phone numbers with ClearoutPhone (adds phone_line_type + phone_carrier; filter phone_line_type=mobile for mobile-only).")
|
|
2878
3141
|
.option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
|
|
2879
3142
|
.option("--limit <n>", "Rows to queue.")
|
|
2880
3143
|
.option("--all", "Queue all rows.")
|
|
@@ -3019,20 +3282,20 @@ export function createProgram() {
|
|
|
3019
3282
|
});
|
|
3020
3283
|
})))
|
|
3021
3284
|
.addCommand(new Command("list")
|
|
3022
|
-
.description("List supported
|
|
3285
|
+
.description("List supported integrations and this org's connections.")
|
|
3023
3286
|
.option("--json", "Print a JSON envelope.")
|
|
3024
3287
|
.action(async (options) => {
|
|
3025
3288
|
await handleAsyncAction("integrations list", options, async () => requestOxygen("/api/cli/integrations/composio/list"));
|
|
3026
3289
|
}))
|
|
3027
3290
|
.addCommand(new Command("connect")
|
|
3028
|
-
.description("Connect
|
|
3291
|
+
.description("Connect an integration. OAuth toolkits return a redirect URL; API-key integrations accept --api-key.")
|
|
3029
3292
|
.argument("<integration_id>", "Integration id, such as 'slack' or 'serpapi'.")
|
|
3030
3293
|
.option("--api-key <value>", "API key for Composio API-key toolkits (e.g. SerpAPI, Resend).")
|
|
3031
3294
|
.option("--json", "Print a JSON envelope.")
|
|
3032
3295
|
.action(async (integrationId, options) => {
|
|
3033
3296
|
await handleAsyncAction("integrations connect", options, async () => {
|
|
3034
3297
|
const apiKey = readOption(options.apiKey)?.trim();
|
|
3035
|
-
return requestOxygen("/api/cli/integrations/
|
|
3298
|
+
return requestOxygen("/api/cli/integrations/connect", {
|
|
3036
3299
|
method: "POST",
|
|
3037
3300
|
body: {
|
|
3038
3301
|
integration_id: integrationId,
|
|
@@ -3042,13 +3305,13 @@ export function createProgram() {
|
|
|
3042
3305
|
});
|
|
3043
3306
|
}))
|
|
3044
3307
|
.addCommand(new Command("disconnect")
|
|
3045
|
-
.description("Disconnect
|
|
3308
|
+
.description("Disconnect an integration.")
|
|
3046
3309
|
.argument("<integration_id>", "Integration id, such as 'slack'.")
|
|
3047
3310
|
.option("--connection-id <id>", "Disconnect a specific connected account when multiple exist.")
|
|
3048
3311
|
.option("--json", "Print a JSON envelope.")
|
|
3049
3312
|
.action(async (integrationId, options) => {
|
|
3050
3313
|
const connectionId = readOption(options.connectionId)?.trim();
|
|
3051
|
-
await handleAsyncAction("integrations disconnect", options, () => requestOxygen("/api/cli/integrations/
|
|
3314
|
+
await handleAsyncAction("integrations disconnect", options, () => requestOxygen("/api/cli/integrations/disconnect", {
|
|
3052
3315
|
method: "POST",
|
|
3053
3316
|
body: {
|
|
3054
3317
|
integration_id: integrationId,
|
|
@@ -3092,18 +3355,15 @@ export function createProgram() {
|
|
|
3092
3355
|
});
|
|
3093
3356
|
});
|
|
3094
3357
|
}));
|
|
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.")
|
|
3358
|
+
program.addCommand(new Command("senders")
|
|
3359
|
+
.description("Manage the org's connected LinkedIn sender accounts for Sequencer: list, connect, sync, inspect, disconnect, and tune rate limits.")
|
|
3100
3360
|
.addCommand(new Command("list")
|
|
3101
3361
|
.description("List connected LinkedIn sender accounts with health status, rate limits, and today's usage.")
|
|
3102
3362
|
.option("--status <status>", "Filter by sender status: active, paused, disconnected, restricted, or credentials_required.")
|
|
3103
3363
|
.option("--no-usage", "Skip today's per-account usage counts for a faster, lighter response.")
|
|
3104
3364
|
.option("--json", "Print a JSON envelope.")
|
|
3105
3365
|
.action(async (options) => {
|
|
3106
|
-
await handleAsyncAction("
|
|
3366
|
+
await handleAsyncAction("senders list", options, () => {
|
|
3107
3367
|
const params = new URLSearchParams();
|
|
3108
3368
|
if (options.usage !== false)
|
|
3109
3369
|
params.set("include_usage", "true");
|
|
@@ -3111,7 +3371,7 @@ export function createProgram() {
|
|
|
3111
3371
|
if (status)
|
|
3112
3372
|
params.set("status", status);
|
|
3113
3373
|
const suffix = params.toString();
|
|
3114
|
-
return requestOxygen(`/api/cli/
|
|
3374
|
+
return requestOxygen(`/api/cli/senders${suffix ? `?${suffix}` : ""}`);
|
|
3115
3375
|
});
|
|
3116
3376
|
}))
|
|
3117
3377
|
.addCommand(new Command("connect")
|
|
@@ -3119,9 +3379,9 @@ export function createProgram() {
|
|
|
3119
3379
|
.option("--reconnect <connection_id>", "Reconnect an existing connection instead of creating a new one. Accepts a connection id.")
|
|
3120
3380
|
.option("--json", "Print a JSON envelope.")
|
|
3121
3381
|
.action(async (options) => {
|
|
3122
|
-
await handleAsyncAction("
|
|
3382
|
+
await handleAsyncAction("senders connect", options, () => {
|
|
3123
3383
|
const reconnect = readOption(options.reconnect);
|
|
3124
|
-
return requestOxygen("/api/cli/
|
|
3384
|
+
return requestOxygen("/api/cli/senders/connect", {
|
|
3125
3385
|
method: "POST",
|
|
3126
3386
|
body: {
|
|
3127
3387
|
...(reconnect ? { reconnect_connection_id: reconnect } : {}),
|
|
@@ -3130,20 +3390,20 @@ export function createProgram() {
|
|
|
3130
3390
|
});
|
|
3131
3391
|
}))
|
|
3132
3392
|
.addCommand(new Command("get")
|
|
3133
|
-
.description("Get one LinkedIn sender account with status, limits,
|
|
3393
|
+
.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
3394
|
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
3135
3395
|
.option("--json", "Print a JSON envelope.")
|
|
3136
3396
|
.action(async (id, options) => {
|
|
3137
|
-
await handleAsyncAction("
|
|
3397
|
+
await handleAsyncAction("senders get", options, async () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}`));
|
|
3138
3398
|
}))
|
|
3139
3399
|
.addCommand(new Command("sync")
|
|
3140
3400
|
.description("Refresh LinkedIn account state (status, profile) from Unipile. Pass --connection-id to sync one account, or omit it to sync all.")
|
|
3141
3401
|
.option("--connection-id <id>", "Sync a specific connection id. Defaults to syncing all connected accounts.")
|
|
3142
3402
|
.option("--json", "Print a JSON envelope.")
|
|
3143
3403
|
.action(async (options) => {
|
|
3144
|
-
await handleAsyncAction("
|
|
3404
|
+
await handleAsyncAction("senders sync", options, () => {
|
|
3145
3405
|
const connectionId = readOption(options.connectionId);
|
|
3146
|
-
return requestOxygen("/api/cli/
|
|
3406
|
+
return requestOxygen("/api/cli/senders/sync", {
|
|
3147
3407
|
method: "POST",
|
|
3148
3408
|
body: {
|
|
3149
3409
|
...(connectionId ? { connection_id: connectionId } : {}),
|
|
@@ -3156,22 +3416,22 @@ export function createProgram() {
|
|
|
3156
3416
|
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
3157
3417
|
.option("--json", "Print a JSON envelope.")
|
|
3158
3418
|
.action(async (id, options) => {
|
|
3159
|
-
await handleAsyncAction("
|
|
3419
|
+
await handleAsyncAction("senders disconnect", options, async () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/disconnect`, {
|
|
3160
3420
|
method: "POST",
|
|
3161
3421
|
}));
|
|
3162
3422
|
}))
|
|
3163
3423
|
.addCommand(new Command("limits")
|
|
3164
|
-
.description("View and adjust per-account daily action limits and
|
|
3424
|
+
.description("View and adjust per-account daily action limits and the daily-reset timezone.")
|
|
3165
3425
|
.option("--json", "Print a JSON envelope.")
|
|
3166
3426
|
.addCommand(new Command("get")
|
|
3167
|
-
.description("Show current limits, overrides,
|
|
3427
|
+
.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
3428
|
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
3169
3429
|
.option("--json", "Print a JSON envelope.")
|
|
3170
3430
|
.action(async (id, options) => {
|
|
3171
|
-
await handleAsyncAction("
|
|
3431
|
+
await handleAsyncAction("senders limits get", options, async () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/limits`));
|
|
3172
3432
|
}))
|
|
3173
3433
|
.addCommand(new Command("set")
|
|
3174
|
-
.description("Adjust per-account daily action limits and
|
|
3434
|
+
.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
3435
|
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
3176
3436
|
.option("--invites-per-day <n>", "Daily LinkedIn connection invites cap.")
|
|
3177
3437
|
.option("--invites-per-week <n>", "Weekly LinkedIn connection invites cap.")
|
|
@@ -3188,21 +3448,18 @@ export function createProgram() {
|
|
|
3188
3448
|
.option("--total-reads-per-day <n>", "Daily cap across all read types.")
|
|
3189
3449
|
.option("--min-spacing-seconds <n>", "Minimum seconds between actions.")
|
|
3190
3450
|
.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.")
|
|
3451
|
+
.option("--timezone <tz>", "IANA timezone the daily action counters reset in, e.g. America/New_York.")
|
|
3195
3452
|
.option("--json", "Print a JSON envelope.")
|
|
3196
3453
|
.action(async (id, options) => {
|
|
3197
|
-
await handleAsyncAction("
|
|
3454
|
+
await handleAsyncAction("senders limits set", options, () => {
|
|
3198
3455
|
const body = buildLinkedinLimitsBody(options);
|
|
3199
|
-
return requestOxygen(`/api/cli/
|
|
3456
|
+
return requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/limits`, {
|
|
3200
3457
|
method: "PATCH",
|
|
3201
3458
|
body,
|
|
3202
3459
|
});
|
|
3203
3460
|
});
|
|
3204
|
-
}))))
|
|
3205
|
-
|
|
3461
|
+
}))));
|
|
3462
|
+
program.addCommand(new Command("inbox")
|
|
3206
3463
|
.description("LinkedIn unified inbox (unibox): scan conversations across all sender accounts, read threads, and reply.")
|
|
3207
3464
|
.addCommand(new Command("list")
|
|
3208
3465
|
.description("List LinkedIn conversations across all connected accounts, newest first.")
|
|
@@ -3213,7 +3470,7 @@ export function createProgram() {
|
|
|
3213
3470
|
.option("--limit <n>", "Maximum conversations to return (1-200). Defaults to 50.")
|
|
3214
3471
|
.option("--json", "Print a JSON envelope.")
|
|
3215
3472
|
.action(async (options) => {
|
|
3216
|
-
await handleAsyncAction("
|
|
3473
|
+
await handleAsyncAction("inbox list", options, () => {
|
|
3217
3474
|
const params = new URLSearchParams();
|
|
3218
3475
|
const account = readOption(options.account);
|
|
3219
3476
|
if (account)
|
|
@@ -3229,7 +3486,7 @@ export function createProgram() {
|
|
|
3229
3486
|
if (limit)
|
|
3230
3487
|
params.set("limit", limit);
|
|
3231
3488
|
const suffix = params.toString();
|
|
3232
|
-
return requestOxygen(`/api/cli/
|
|
3489
|
+
return requestOxygen(`/api/cli/inbox${suffix ? `?${suffix}` : ""}`);
|
|
3233
3490
|
});
|
|
3234
3491
|
}))
|
|
3235
3492
|
.addCommand(new Command("get")
|
|
@@ -3238,13 +3495,13 @@ export function createProgram() {
|
|
|
3238
3495
|
.option("--message-limit <n>", "Maximum messages to return (1-500). Defaults to 100.")
|
|
3239
3496
|
.option("--json", "Print a JSON envelope.")
|
|
3240
3497
|
.action(async (conversation, options) => {
|
|
3241
|
-
await handleAsyncAction("
|
|
3498
|
+
await handleAsyncAction("inbox get", options, () => {
|
|
3242
3499
|
const params = new URLSearchParams();
|
|
3243
3500
|
const messageLimit = readOption(options.messageLimit);
|
|
3244
3501
|
if (messageLimit)
|
|
3245
3502
|
params.set("message_limit", messageLimit);
|
|
3246
3503
|
const suffix = params.toString();
|
|
3247
|
-
return requestOxygen(`/api/cli/
|
|
3504
|
+
return requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}${suffix ? `?${suffix}` : ""}`);
|
|
3248
3505
|
});
|
|
3249
3506
|
}))
|
|
3250
3507
|
.addCommand(new Command("send")
|
|
@@ -3254,7 +3511,7 @@ export function createProgram() {
|
|
|
3254
3511
|
.option("--approved", "Approve and send the message. Without this flag, returns a preview only.")
|
|
3255
3512
|
.option("--json", "Print a JSON envelope.")
|
|
3256
3513
|
.action(async (conversation, options) => {
|
|
3257
|
-
await handleAsyncAction("
|
|
3514
|
+
await handleAsyncAction("inbox send", options, () => requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}/send`, {
|
|
3258
3515
|
method: "POST",
|
|
3259
3516
|
body: {
|
|
3260
3517
|
text: readOption(options.text),
|
|
@@ -3267,7 +3524,7 @@ export function createProgram() {
|
|
|
3267
3524
|
.argument("<conversation>", "Conversation id or Unipile chat id.")
|
|
3268
3525
|
.option("--json", "Print a JSON envelope.")
|
|
3269
3526
|
.action(async (conversation, options) => {
|
|
3270
|
-
await handleAsyncAction("
|
|
3527
|
+
await handleAsyncAction("inbox mark-read", options, () => requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}/read`, {
|
|
3271
3528
|
method: "POST",
|
|
3272
3529
|
}));
|
|
3273
3530
|
}))
|
|
@@ -3277,7 +3534,7 @@ export function createProgram() {
|
|
|
3277
3534
|
.option("--message-limit <n>", "Maximum messages to sync per chat. Defaults to 20.")
|
|
3278
3535
|
.option("--json", "Print a JSON envelope.")
|
|
3279
3536
|
.action(async (options) => {
|
|
3280
|
-
await handleAsyncAction("
|
|
3537
|
+
await handleAsyncAction("inbox sync", options, () => {
|
|
3281
3538
|
const body = {};
|
|
3282
3539
|
const chatLimit = readOption(options.chatLimit);
|
|
3283
3540
|
if (chatLimit)
|
|
@@ -3285,77 +3542,160 @@ export function createProgram() {
|
|
|
3285
3542
|
const messageLimit = readOption(options.messageLimit);
|
|
3286
3543
|
if (messageLimit)
|
|
3287
3544
|
body.message_limit = Number(messageLimit);
|
|
3288
|
-
return requestOxygen("/api/cli/
|
|
3545
|
+
return requestOxygen("/api/cli/inbox/sync", { method: "POST", body });
|
|
3289
3546
|
});
|
|
3290
|
-
})))
|
|
3291
|
-
|
|
3292
|
-
.description("
|
|
3547
|
+
})));
|
|
3548
|
+
program.addCommand(new Command("sequences")
|
|
3549
|
+
.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
3550
|
.addCommand(new Command("list")
|
|
3294
|
-
.description("List
|
|
3551
|
+
.description("List sequences with status, channels, and credit usage.")
|
|
3295
3552
|
.option("--status <status>", "Filter by status: draft, active, paused, or archived.")
|
|
3296
3553
|
.option("--json", "Print a JSON envelope.")
|
|
3297
3554
|
.action(async (options) => {
|
|
3298
|
-
await handleAsyncAction("
|
|
3555
|
+
await handleAsyncAction("sequences list", options, () => {
|
|
3299
3556
|
const params = new URLSearchParams();
|
|
3300
3557
|
const status = readOption(options.status);
|
|
3301
3558
|
if (status)
|
|
3302
3559
|
params.set("status", status);
|
|
3303
3560
|
const suffix = params.toString();
|
|
3304
|
-
return requestOxygen(`/api/cli/
|
|
3561
|
+
return requestOxygen(`/api/cli/sequences${suffix ? `?${suffix}` : ""}`);
|
|
3562
|
+
});
|
|
3563
|
+
}))
|
|
3564
|
+
.addCommand(new Command("analytics")
|
|
3565
|
+
.description("Show organization-level sequencer analytics plus per-sequence funnels.")
|
|
3566
|
+
.option("--range <range>", "Preset range: 7d, 14d, 28d, or 30d. Defaults to 14d.")
|
|
3567
|
+
.option("--from <date>", "Custom start date (YYYY-MM-DD).")
|
|
3568
|
+
.option("--to <date>", "Custom end date (YYYY-MM-DD).")
|
|
3569
|
+
.option("--sequence <id-or-slug>", "Limit analytics to one sequence.")
|
|
3570
|
+
.option("--json", "Print a JSON envelope.")
|
|
3571
|
+
.action(async (options) => {
|
|
3572
|
+
await handleAsyncAction("sequences analytics", options, () => {
|
|
3573
|
+
const params = new URLSearchParams();
|
|
3574
|
+
const range = readOption(options.range);
|
|
3575
|
+
const from = readOption(options.from);
|
|
3576
|
+
const to = readOption(options.to);
|
|
3577
|
+
const sequence = readOption(options.sequence);
|
|
3578
|
+
if (range)
|
|
3579
|
+
params.set("range", range);
|
|
3580
|
+
if (from)
|
|
3581
|
+
params.set("from", from);
|
|
3582
|
+
if (to)
|
|
3583
|
+
params.set("to", to);
|
|
3584
|
+
if (sequence)
|
|
3585
|
+
params.set("sequence", sequence);
|
|
3586
|
+
const qs = params.toString();
|
|
3587
|
+
return requestOxygen(`/api/cli/sequences/analytics${qs ? `?${qs}` : ""}`);
|
|
3305
3588
|
});
|
|
3306
3589
|
}))
|
|
3307
3590
|
.addCommand(new Command("create")
|
|
3308
|
-
.description("Create a draft sequence from a steps JSON file. Assign
|
|
3591
|
+
.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
3592
|
.requiredOption("--name <name>", "Human-readable sequence name.")
|
|
3310
3593
|
.requiredOption("--slug <slug>", "Unique slug for the sequence.")
|
|
3311
|
-
.requiredOption("--steps-file <path>", "Path to a JSON file: { \"steps\": [...] }.")
|
|
3312
|
-
.
|
|
3594
|
+
.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).")
|
|
3595
|
+
.option("--channels <list>", "Comma-separated channels: linkedin,email. Defaults to the channels the journey touches.")
|
|
3596
|
+
.option("--senders <ids>", "Comma-separated LinkedIn sender account ids (or connection / Unipile ids). Required when the journey has LinkedIn steps.")
|
|
3313
3597
|
.option("--table <id>", "Source table id whose rows supply {{column}} template values.")
|
|
3314
3598
|
.option("--url-column <key>", "Column key holding each lead's LinkedIn URL/provider id.")
|
|
3315
|
-
.option("--
|
|
3599
|
+
.option("--email-provider <provider>", "Email provider for the email track. Only 'instantly' is supported.")
|
|
3600
|
+
.option("--email-connection <id>", "Instantly connection id for the email track. Defaults to the org's active Instantly connection.")
|
|
3601
|
+
.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.")
|
|
3602
|
+
.option("--max-credits <n>", "Credit cap for the LinkedIn track (also set when starting).")
|
|
3316
3603
|
.option("--json", "Print a JSON envelope.")
|
|
3317
3604
|
.action(async (options) => {
|
|
3318
|
-
await handleAsyncAction("
|
|
3605
|
+
await handleAsyncAction("sequences create", options, () => {
|
|
3319
3606
|
const stepsPath = readOption(options.stepsFile);
|
|
3320
3607
|
if (!stepsPath)
|
|
3321
3608
|
throw new Error("--steps-file is required.");
|
|
3322
3609
|
const raw = readFileSync(resolve(stepsPath), "utf8");
|
|
3323
3610
|
const definition = JSON.parse(raw);
|
|
3324
|
-
const
|
|
3611
|
+
const channels = readCsvOption(options.channels);
|
|
3612
|
+
const senders = readCsvOption(options.senders);
|
|
3613
|
+
const email = readCampaignEmailBinding(options);
|
|
3325
3614
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
3326
|
-
return requestOxygen("/api/cli/
|
|
3615
|
+
return requestOxygen("/api/cli/sequences", {
|
|
3327
3616
|
method: "POST",
|
|
3328
3617
|
body: {
|
|
3329
3618
|
name: readOption(options.name),
|
|
3330
3619
|
slug: readOption(options.slug),
|
|
3331
3620
|
definition,
|
|
3332
|
-
|
|
3621
|
+
...(channels.length > 0 ? { channels } : {}),
|
|
3622
|
+
...(senders.length > 0 ? { senders } : {}),
|
|
3333
3623
|
...(readOption(options.table) ? { source_table_id: readOption(options.table) } : {}),
|
|
3334
3624
|
...(readOption(options.urlColumn) ? { linkedin_url_column_key: readOption(options.urlColumn) } : {}),
|
|
3625
|
+
...(email ? { email } : {}),
|
|
3335
3626
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
3336
3627
|
},
|
|
3337
3628
|
});
|
|
3338
3629
|
});
|
|
3630
|
+
}))
|
|
3631
|
+
.addCommand(new Command("update")
|
|
3632
|
+
.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.")
|
|
3633
|
+
.argument("<sequence>", "Sequence id or slug.")
|
|
3634
|
+
.option("--name <name>", "New human-readable sequence name.")
|
|
3635
|
+
.option("--steps-file <path>", "Path to a JSON file: { \"steps\": [...] } replacing the journey.")
|
|
3636
|
+
.option("--channels <list>", "Comma-separated channels: linkedin,email.")
|
|
3637
|
+
.option("--senders <ids>", "Comma-separated LinkedIn sender account ids (or connection / Unipile ids).")
|
|
3638
|
+
.option("--email-provider <provider>", "Email provider for the email track. Only 'instantly' is supported.")
|
|
3639
|
+
.option("--email-connection <id>", "Instantly connection id for the email track.")
|
|
3640
|
+
.option("--email-definition-file <path>", "Path to a JSON file with the email content spec (subjects/bodies/delays/subsequences).")
|
|
3641
|
+
.option("--clear-email", "Remove the email binding from the sequence (draft only).")
|
|
3642
|
+
.option("--max-credits <n>", "Credit cap for the LinkedIn track (draft only).")
|
|
3643
|
+
.option("--json", "Print a JSON envelope.")
|
|
3644
|
+
.action(async (sequence, options) => {
|
|
3645
|
+
await handleAsyncAction("sequences update", options, () => {
|
|
3646
|
+
const body = {};
|
|
3647
|
+
const name = readOption(options.name);
|
|
3648
|
+
if (name)
|
|
3649
|
+
body.name = name;
|
|
3650
|
+
const stepsPath = readOption(options.stepsFile);
|
|
3651
|
+
if (stepsPath) {
|
|
3652
|
+
body.definition = JSON.parse(readFileSync(resolve(stepsPath), "utf8"));
|
|
3653
|
+
}
|
|
3654
|
+
const channels = readCsvOption(options.channels);
|
|
3655
|
+
if (channels.length > 0)
|
|
3656
|
+
body.channels = channels;
|
|
3657
|
+
const senders = readCsvOption(options.senders);
|
|
3658
|
+
if (senders.length > 0)
|
|
3659
|
+
body.senders = senders;
|
|
3660
|
+
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
3661
|
+
if (maxCredits !== undefined)
|
|
3662
|
+
body.max_credits = maxCredits;
|
|
3663
|
+
if (options.clearEmail) {
|
|
3664
|
+
body.email = null;
|
|
3665
|
+
}
|
|
3666
|
+
else {
|
|
3667
|
+
const email = readCampaignEmailBinding(options);
|
|
3668
|
+
if (email)
|
|
3669
|
+
body.email = email;
|
|
3670
|
+
}
|
|
3671
|
+
if (Object.keys(body).length === 0) {
|
|
3672
|
+
throw new Error("Provide at least one field to update (--name, --steps-file, --channels, --senders, --email-*, --clear-email, or --max-credits).");
|
|
3673
|
+
}
|
|
3674
|
+
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}`, {
|
|
3675
|
+
method: "PATCH",
|
|
3676
|
+
body,
|
|
3677
|
+
});
|
|
3678
|
+
});
|
|
3339
3679
|
}))
|
|
3340
3680
|
.addCommand(new Command("get")
|
|
3341
3681
|
.description("Get a sequence's definition, senders, status, and credit usage.")
|
|
3342
3682
|
.argument("<sequence>", "Sequence id or slug.")
|
|
3343
3683
|
.option("--json", "Print a JSON envelope.")
|
|
3344
3684
|
.action(async (sequence, options) => {
|
|
3345
|
-
await handleAsyncAction("
|
|
3685
|
+
await handleAsyncAction("sequences get", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}`));
|
|
3346
3686
|
}))
|
|
3347
3687
|
.addCommand(new Command("enroll")
|
|
3348
|
-
.description("Enroll leads into a sequence from a JSON file of { leads: [...] }. Idempotent per table row.")
|
|
3688
|
+
.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
3689
|
.argument("<sequence>", "Sequence id or slug.")
|
|
3350
|
-
.requiredOption("--leads-file <path>", "Path to a JSON file: { \"leads\": [{ lead_provider_id, lead_name, row_values }] }.")
|
|
3690
|
+
.requiredOption("--leads-file <path>", "Path to a JSON file: { \"leads\": [{ lead_provider_id, lead_name, table_row_id, row_values }] }.")
|
|
3351
3691
|
.option("--json", "Print a JSON envelope.")
|
|
3352
3692
|
.action(async (sequence, options) => {
|
|
3353
|
-
await handleAsyncAction("
|
|
3693
|
+
await handleAsyncAction("sequences enroll", options, () => {
|
|
3354
3694
|
const leadsPath = readOption(options.leadsFile);
|
|
3355
3695
|
if (!leadsPath)
|
|
3356
3696
|
throw new Error("--leads-file is required.");
|
|
3357
3697
|
const parsed = JSON.parse(readFileSync(resolve(leadsPath), "utf8"));
|
|
3358
|
-
return requestOxygen(`/api/cli/
|
|
3698
|
+
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/enroll`, {
|
|
3359
3699
|
method: "POST",
|
|
3360
3700
|
body: { leads: parsed.leads ?? [] },
|
|
3361
3701
|
});
|
|
@@ -3369,9 +3709,9 @@ export function createProgram() {
|
|
|
3369
3709
|
.option("--dry-run", "Activate in dry-run mode: advance every step with simulated sends, no LinkedIn actions, no credits.")
|
|
3370
3710
|
.option("--json", "Print a JSON envelope.")
|
|
3371
3711
|
.action(async (sequence, options) => {
|
|
3372
|
-
await handleAsyncAction("
|
|
3712
|
+
await handleAsyncAction("sequences start", options, () => {
|
|
3373
3713
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
3374
|
-
return requestOxygen(`/api/cli/
|
|
3714
|
+
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/start`, {
|
|
3375
3715
|
method: "POST",
|
|
3376
3716
|
body: {
|
|
3377
3717
|
...(options.dryRun ? { dry_run: true } : {}),
|
|
@@ -3386,7 +3726,7 @@ export function createProgram() {
|
|
|
3386
3726
|
.argument("<sequence>", "Sequence id or slug.")
|
|
3387
3727
|
.option("--json", "Print a JSON envelope.")
|
|
3388
3728
|
.action(async (sequence, options) => {
|
|
3389
|
-
await handleAsyncAction("
|
|
3729
|
+
await handleAsyncAction("sequences pause", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/status`, {
|
|
3390
3730
|
method: "POST", body: { status: "paused" },
|
|
3391
3731
|
}));
|
|
3392
3732
|
}))
|
|
@@ -3395,7 +3735,7 @@ export function createProgram() {
|
|
|
3395
3735
|
.argument("<sequence>", "Sequence id or slug.")
|
|
3396
3736
|
.option("--json", "Print a JSON envelope.")
|
|
3397
3737
|
.action(async (sequence, options) => {
|
|
3398
|
-
await handleAsyncAction("
|
|
3738
|
+
await handleAsyncAction("sequences resume", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/status`, {
|
|
3399
3739
|
method: "POST", body: { status: "active" },
|
|
3400
3740
|
}));
|
|
3401
3741
|
}))
|
|
@@ -3404,7 +3744,7 @@ export function createProgram() {
|
|
|
3404
3744
|
.argument("<sequence>", "Sequence id or slug.")
|
|
3405
3745
|
.option("--json", "Print a JSON envelope.")
|
|
3406
3746
|
.action(async (sequence, options) => {
|
|
3407
|
-
await handleAsyncAction("
|
|
3747
|
+
await handleAsyncAction("sequences archive", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/status`, {
|
|
3408
3748
|
method: "POST", body: { status: "archived" },
|
|
3409
3749
|
}));
|
|
3410
3750
|
}))
|
|
@@ -3415,7 +3755,7 @@ export function createProgram() {
|
|
|
3415
3755
|
.option("--limit <n>", "Maximum enrollments to return (1-500).")
|
|
3416
3756
|
.option("--json", "Print a JSON envelope.")
|
|
3417
3757
|
.action(async (sequence, options) => {
|
|
3418
|
-
await handleAsyncAction("
|
|
3758
|
+
await handleAsyncAction("sequences enrollments", options, () => {
|
|
3419
3759
|
const params = new URLSearchParams();
|
|
3420
3760
|
const status = readOption(options.status);
|
|
3421
3761
|
if (status)
|
|
@@ -3424,7 +3764,7 @@ export function createProgram() {
|
|
|
3424
3764
|
if (limit)
|
|
3425
3765
|
params.set("limit", limit);
|
|
3426
3766
|
const suffix = params.toString();
|
|
3427
|
-
return requestOxygen(`/api/cli/
|
|
3767
|
+
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/enrollments${suffix ? `?${suffix}` : ""}`);
|
|
3428
3768
|
});
|
|
3429
3769
|
}))
|
|
3430
3770
|
.addCommand(new Command("stats")
|
|
@@ -3432,8 +3772,142 @@ export function createProgram() {
|
|
|
3432
3772
|
.argument("<sequence>", "Sequence id or slug.")
|
|
3433
3773
|
.option("--json", "Print a JSON envelope.")
|
|
3434
3774
|
.action(async (sequence, options) => {
|
|
3435
|
-
await handleAsyncAction("
|
|
3775
|
+
await handleAsyncAction("sequences stats", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/stats`));
|
|
3436
3776
|
})));
|
|
3777
|
+
program.addCommand(new Command("mailboxes")
|
|
3778
|
+
.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).")
|
|
3779
|
+
.addCommand(new Command("list")
|
|
3780
|
+
.description("List the org's sending mailboxes with provider, status, warmup state, and a pool overview.")
|
|
3781
|
+
.option("--status <status>", "Filter by status: active, paused, or disabled.")
|
|
3782
|
+
.option("--json", "Print a JSON envelope.")
|
|
3783
|
+
.action(async (options) => {
|
|
3784
|
+
await handleAsyncAction("mailboxes list", options, () => {
|
|
3785
|
+
const params = new URLSearchParams();
|
|
3786
|
+
const status = readOption(options.status);
|
|
3787
|
+
if (status)
|
|
3788
|
+
params.set("status", status);
|
|
3789
|
+
const suffix = params.toString();
|
|
3790
|
+
return requestOxygen(`/api/cli/mailboxes${suffix ? `?${suffix}` : ""}`);
|
|
3791
|
+
});
|
|
3792
|
+
}))
|
|
3793
|
+
.addCommand(new Command("import")
|
|
3794
|
+
.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.")
|
|
3795
|
+
.option("--file <path>", "Path to a JSON file: { \"mailboxes\": [{ email_address, provider, workspace_external_id? }] }.")
|
|
3796
|
+
.option("--from <source>", "Import source: 'zapmail' to pull the connected Zapmail workspace's mailboxes.")
|
|
3797
|
+
.option("--connection <id>", "Zapmail connection id (--from zapmail). Defaults to the org's active Zapmail connection.")
|
|
3798
|
+
.option("--json", "Print a JSON envelope.")
|
|
3799
|
+
.action(async (options) => {
|
|
3800
|
+
await handleAsyncAction("mailboxes import", options, () => {
|
|
3801
|
+
const from = readOption(options.from);
|
|
3802
|
+
const filePath = readOption(options.file);
|
|
3803
|
+
const connection = readOption(options.connection);
|
|
3804
|
+
if (from === "zapmail") {
|
|
3805
|
+
return requestOxygen("/api/cli/mailboxes", {
|
|
3806
|
+
method: "POST",
|
|
3807
|
+
body: { source: "zapmail", ...(connection ? { connection_id: connection } : {}) },
|
|
3808
|
+
});
|
|
3809
|
+
}
|
|
3810
|
+
if (!filePath)
|
|
3811
|
+
throw new Error("Provide --file <path> (a { \"mailboxes\": [...] } JSON file) or --from zapmail.");
|
|
3812
|
+
const parsed = JSON.parse(readFileSync(resolve(filePath), "utf8"));
|
|
3813
|
+
return requestOxygen("/api/cli/mailboxes", {
|
|
3814
|
+
method: "POST",
|
|
3815
|
+
body: { mailboxes: parsed.mailboxes ?? [] },
|
|
3816
|
+
});
|
|
3817
|
+
});
|
|
3818
|
+
}))
|
|
3819
|
+
.addCommand(new Command("status")
|
|
3820
|
+
.description("Set a sending mailbox's status. Pausing/disabling takes the inbox out of the rotation pool without losing its warmup state.")
|
|
3821
|
+
.argument("<mailbox>", "Mailbox id or email address.")
|
|
3822
|
+
.requiredOption("--status <status>", "New status: active, paused, or disabled.")
|
|
3823
|
+
.option("--json", "Print a JSON envelope.")
|
|
3824
|
+
.action(async (mailbox, options) => {
|
|
3825
|
+
await handleAsyncAction("mailboxes status", options, () => {
|
|
3826
|
+
const status = readOption(options.status);
|
|
3827
|
+
if (!status)
|
|
3828
|
+
throw new Error("--status is required.");
|
|
3829
|
+
return requestOxygen(`/api/cli/mailboxes/${encodeURIComponent(mailbox)}/status`, {
|
|
3830
|
+
method: "POST",
|
|
3831
|
+
body: { status },
|
|
3832
|
+
});
|
|
3833
|
+
});
|
|
3834
|
+
}))
|
|
3835
|
+
.addCommand(new Command("connect-oauth")
|
|
3836
|
+
.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.")
|
|
3837
|
+
.option("--provider <provider>", "Mailbox provider to provision: google or microsoft.")
|
|
3838
|
+
.option("--mailboxes <list>", "Comma-separated mailbox addresses to provision. Omit to provision the whole pool for the provider.")
|
|
3839
|
+
.option("--connection <id>", "Zapmail connection id. Defaults to the org's active Zapmail connection.")
|
|
3840
|
+
.option("--status <export_id>", "Poll a previously started Custom OAuth export instead of starting a new one.")
|
|
3841
|
+
.option("--json", "Print a JSON envelope.")
|
|
3842
|
+
.action(async (options) => {
|
|
3843
|
+
await handleAsyncAction("mailboxes connect-oauth", options, () => {
|
|
3844
|
+
const exportId = readOption(options.status);
|
|
3845
|
+
if (exportId) {
|
|
3846
|
+
const params = new URLSearchParams({ export_id: exportId });
|
|
3847
|
+
const pollConnection = readOption(options.connection);
|
|
3848
|
+
if (pollConnection)
|
|
3849
|
+
params.set("connection_id", pollConnection);
|
|
3850
|
+
return requestOxygen(`/api/cli/mailboxes/connect-oauth?${params.toString()}`);
|
|
3851
|
+
}
|
|
3852
|
+
const provider = readOption(options.provider);
|
|
3853
|
+
if (!provider) {
|
|
3854
|
+
throw new Error("--provider <google|microsoft> is required (or pass --status <export_id> to poll a run).");
|
|
3855
|
+
}
|
|
3856
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
3857
|
+
const connection = readOption(options.connection);
|
|
3858
|
+
return requestOxygen("/api/cli/mailboxes/connect-oauth", {
|
|
3859
|
+
method: "POST",
|
|
3860
|
+
body: {
|
|
3861
|
+
provider,
|
|
3862
|
+
...(mailboxes.length > 0 ? { mailboxes } : {}),
|
|
3863
|
+
...(connection ? { connection_id: connection } : {}),
|
|
3864
|
+
},
|
|
3865
|
+
});
|
|
3866
|
+
});
|
|
3867
|
+
}))
|
|
3868
|
+
.addCommand(new Command("warmup")
|
|
3869
|
+
.description("Mailbox warmup via Instantly (BYOK — Instantly bills your account, 0 Oxygen credits).")
|
|
3870
|
+
.addCommand(new Command("enable")
|
|
3871
|
+
.description("Enable Instantly warmup for sending mailboxes and stamp each mailbox's warmup state. Targets the whole pool unless --mailboxes is given.")
|
|
3872
|
+
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to warm. Omit to warm the whole pool.")
|
|
3873
|
+
.option("--connection <id>", "Instantly connection id. Defaults to the org's active Instantly connection.")
|
|
3874
|
+
.option("--dry-run", "Simulate without calling Instantly (mailboxes marked pending).")
|
|
3875
|
+
.option("--json", "Print a JSON envelope.")
|
|
3876
|
+
.action(async (options) => {
|
|
3877
|
+
await handleAsyncAction("mailboxes warmup enable", options, () => {
|
|
3878
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
3879
|
+
const connection = readOption(options.connection);
|
|
3880
|
+
return requestOxygen("/api/cli/mailboxes/warmup", {
|
|
3881
|
+
method: "POST",
|
|
3882
|
+
body: {
|
|
3883
|
+
...(mailboxes.length > 0 ? { mailboxes } : {}),
|
|
3884
|
+
...(connection ? { connection_id: connection } : {}),
|
|
3885
|
+
...(options.dryRun ? { dry_run: true } : {}),
|
|
3886
|
+
},
|
|
3887
|
+
});
|
|
3888
|
+
});
|
|
3889
|
+
}))
|
|
3890
|
+
.addCommand(new Command("status")
|
|
3891
|
+
.description("Sync Instantly's warmup analytics into the pool, updating each mailbox's warmup state. Targets the whole pool unless --mailboxes is given.")
|
|
3892
|
+
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to sync. Omit to sync the whole pool.")
|
|
3893
|
+
.option("--connection <id>", "Instantly connection id. Defaults to the org's active Instantly connection.")
|
|
3894
|
+
.option("--dry-run", "Skip the Instantly call (mailboxes marked pending).")
|
|
3895
|
+
.option("--json", "Print a JSON envelope.")
|
|
3896
|
+
.action(async (options) => {
|
|
3897
|
+
await handleAsyncAction("mailboxes warmup status", options, () => {
|
|
3898
|
+
const params = new URLSearchParams();
|
|
3899
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
3900
|
+
if (mailboxes.length > 0)
|
|
3901
|
+
params.set("mailboxes", mailboxes.join(","));
|
|
3902
|
+
const connection = readOption(options.connection);
|
|
3903
|
+
if (connection)
|
|
3904
|
+
params.set("connection_id", connection);
|
|
3905
|
+
if (options.dryRun)
|
|
3906
|
+
params.set("dry_run", "true");
|
|
3907
|
+
const suffix = params.toString();
|
|
3908
|
+
return requestOxygen(`/api/cli/mailboxes/warmup/status${suffix ? `?${suffix}` : ""}`);
|
|
3909
|
+
});
|
|
3910
|
+
}))));
|
|
3437
3911
|
program
|
|
3438
3912
|
.command("workflows")
|
|
3439
3913
|
.description("Durable workflow automation commands.")
|
|
@@ -4548,6 +5022,112 @@ function readCompanySearchPlanJson(value) {
|
|
|
4548
5022
|
? data
|
|
4549
5023
|
: parsed;
|
|
4550
5024
|
}
|
|
5025
|
+
function readPeopleSearchPlanBody(options) {
|
|
5026
|
+
const targetCount = readPositiveInt(options.targetCount);
|
|
5027
|
+
const filters = readPeopleSearchFilters(options);
|
|
5028
|
+
return {
|
|
5029
|
+
prompt: readFileIfPresent(options.prompt),
|
|
5030
|
+
...(targetCount !== undefined ? { target_count: targetCount } : {}),
|
|
5031
|
+
...(options.sourceIntent ? { source_intent: options.sourceIntent } : {}),
|
|
5032
|
+
...(filters ? { filters } : {}),
|
|
5033
|
+
...(options.estimate ? { estimate: true } : {}),
|
|
5034
|
+
...(options.materializePreview ? { materialize_preview: true } : {}),
|
|
5035
|
+
};
|
|
5036
|
+
}
|
|
5037
|
+
function readPeopleSearchRunBody(options) {
|
|
5038
|
+
const prompt = options.prompt ? readFileIfPresent(options.prompt) : null;
|
|
5039
|
+
const plan = options.planJson ? readPeopleSearchPlanJson(options.planJson) : null;
|
|
5040
|
+
if (!prompt && !plan) {
|
|
5041
|
+
throw new OxygenError("invalid_request", "Pass --prompt or --plan-json.", { exitCode: 1 });
|
|
5042
|
+
}
|
|
5043
|
+
const maxPages = readPositiveInt(options.maxPages);
|
|
5044
|
+
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
5045
|
+
const targetCount = readPositiveInt(options.targetCount);
|
|
5046
|
+
const filters = prompt ? readPeopleSearchFilters(options) : null;
|
|
5047
|
+
return {
|
|
5048
|
+
...(prompt ? { prompt } : {}),
|
|
5049
|
+
...(plan ? { plan } : {}),
|
|
5050
|
+
...(options.routeId ? { route_id: options.routeId } : {}),
|
|
5051
|
+
...(options.toolId ? { tool_id: options.toolId } : {}),
|
|
5052
|
+
...(options.table ? { table: options.table } : {}),
|
|
5053
|
+
...(options.upsertKey ? { upsert_key: options.upsertKey } : {}),
|
|
5054
|
+
...(options.mode ? { mode: options.mode } : {}),
|
|
5055
|
+
...(maxPages !== undefined ? { max_pages: maxPages } : {}),
|
|
5056
|
+
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
5057
|
+
...(targetCount !== undefined ? { target_count: targetCount } : {}),
|
|
5058
|
+
...(options.sourceIntent ? { source_intent: options.sourceIntent } : {}),
|
|
5059
|
+
...(filters ? { filters } : {}),
|
|
5060
|
+
...(prompt && options.estimate ? { estimate: true } : {}),
|
|
5061
|
+
...(options.approved ? { approved: true } : {}),
|
|
5062
|
+
};
|
|
5063
|
+
}
|
|
5064
|
+
// PeopleSearchFilters mirror (structural — the server validates the shape). Builds a
|
|
5065
|
+
// filters object from individual --titles/--seniorities/etc. flags, then lets --filters-json
|
|
5066
|
+
// win per top-level filter path so an agent can pass a precise object while still using
|
|
5067
|
+
// convenience flags for the rest.
|
|
5068
|
+
function readPeopleSearchFilters(options) {
|
|
5069
|
+
const filters = {};
|
|
5070
|
+
const titles = {};
|
|
5071
|
+
const titleInclude = readCsvOption(options.titles);
|
|
5072
|
+
if (titleInclude.length > 0)
|
|
5073
|
+
titles.include = titleInclude;
|
|
5074
|
+
const adjacent = readCsvOption(options.adjacentTitles);
|
|
5075
|
+
if (adjacent.length > 0)
|
|
5076
|
+
titles.include_adjacent = adjacent;
|
|
5077
|
+
const titleExclude = readCsvOption(options.excludeTitles);
|
|
5078
|
+
if (titleExclude.length > 0)
|
|
5079
|
+
titles.exclude = titleExclude;
|
|
5080
|
+
if (Object.keys(titles).length > 0)
|
|
5081
|
+
filters.titles = titles;
|
|
5082
|
+
const seniorities = readCsvOption(options.seniorities);
|
|
5083
|
+
if (seniorities.length > 0)
|
|
5084
|
+
filters.seniorities = { include: seniorities };
|
|
5085
|
+
const departments = readCsvOption(options.departments);
|
|
5086
|
+
if (departments.length > 0)
|
|
5087
|
+
filters.departments = { include: departments };
|
|
5088
|
+
const keywords = readCsvOption(options.keywords);
|
|
5089
|
+
if (keywords.length > 0)
|
|
5090
|
+
filters.keywords = { include: keywords };
|
|
5091
|
+
const countries = readCsvOption(options.countries);
|
|
5092
|
+
if (countries.length > 0)
|
|
5093
|
+
filters.geo = { countries };
|
|
5094
|
+
const contactability = {};
|
|
5095
|
+
if (options.requireEmail)
|
|
5096
|
+
contactability.require_work_email = true;
|
|
5097
|
+
if (options.requirePhone)
|
|
5098
|
+
contactability.require_mobile = true;
|
|
5099
|
+
if (Object.keys(contactability).length > 0)
|
|
5100
|
+
filters.contactability = contactability;
|
|
5101
|
+
const company = {};
|
|
5102
|
+
const companyDomains = readCsvOption(options.companyDomains);
|
|
5103
|
+
if (companyDomains.length > 0)
|
|
5104
|
+
company.domains = { include: companyDomains };
|
|
5105
|
+
const companyNames = readCsvOption(options.companyNames);
|
|
5106
|
+
if (companyNames.length > 0)
|
|
5107
|
+
company.names = { include: companyNames };
|
|
5108
|
+
const employees = readRangeOption(options.employees, "--employees");
|
|
5109
|
+
if (employees)
|
|
5110
|
+
company.employee_count = employees;
|
|
5111
|
+
if (Object.keys(company).length > 0)
|
|
5112
|
+
filters.company = company;
|
|
5113
|
+
const maxPerCompany = readPositiveInt(options.maxPerCompany);
|
|
5114
|
+
if (maxPerCompany !== undefined)
|
|
5115
|
+
filters.max_results_per_company = maxPerCompany;
|
|
5116
|
+
const explicit = options.filtersJson ? parseJsonObject(readFileIfPresent(options.filtersJson)) : null;
|
|
5117
|
+
if (explicit) {
|
|
5118
|
+
for (const [key, value] of Object.entries(explicit)) {
|
|
5119
|
+
filters[key] = value;
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
5122
|
+
return Object.keys(filters).length > 0 ? filters : null;
|
|
5123
|
+
}
|
|
5124
|
+
function readPeopleSearchPlanJson(value) {
|
|
5125
|
+
const parsed = parseJsonObject(readFileIfPresent(value));
|
|
5126
|
+
const data = parsed.data;
|
|
5127
|
+
return data && typeof data === "object" && !Array.isArray(data)
|
|
5128
|
+
? data
|
|
5129
|
+
: parsed;
|
|
5130
|
+
}
|
|
4551
5131
|
function readCompaniesEnrichBody(table, options) {
|
|
4552
5132
|
const body = { table };
|
|
4553
5133
|
const fields = readCsvOption(options.missingFields);
|
|
@@ -4668,6 +5248,9 @@ async function importRows(table, options) {
|
|
|
4668
5248
|
const sourceHash = hashImportFile(options.file);
|
|
4669
5249
|
const shouldUseBackground = options.background
|
|
4670
5250
|
|| (!options.sync && parsedRows.length > LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD);
|
|
5251
|
+
const preparedBackgroundTarget = shouldUseBackground && table && !options.create
|
|
5252
|
+
? await prepareImportTarget(table, options, parsedRows)
|
|
5253
|
+
: null;
|
|
4671
5254
|
if (shouldUseBackground) {
|
|
4672
5255
|
// Prefer the object-storage fast path: upload the raw file once via a
|
|
4673
5256
|
// presigned URL (no request-body limit) so the worker COPY-loads it.
|
|
@@ -4675,13 +5258,13 @@ async function importRows(table, options) {
|
|
|
4675
5258
|
const staged = await tryEnqueueStagedFileImport(table, options, format, parsedRows, {
|
|
4676
5259
|
autoBackground: !options.background,
|
|
4677
5260
|
sourceHash,
|
|
4678
|
-
});
|
|
5261
|
+
}, preparedBackgroundTarget);
|
|
4679
5262
|
if (staged)
|
|
4680
5263
|
return staged;
|
|
4681
5264
|
return enqueueImportFile(table, options, format, batchSize, {
|
|
4682
5265
|
autoBackground: !options.background,
|
|
4683
5266
|
sourceHash,
|
|
4684
|
-
});
|
|
5267
|
+
}, preparedBackgroundTarget);
|
|
4685
5268
|
}
|
|
4686
5269
|
const target = await prepareImportTarget(table, options, parsedRows);
|
|
4687
5270
|
let rowCount = 0;
|
|
@@ -4691,28 +5274,30 @@ async function importRows(table, options) {
|
|
|
4691
5274
|
const warnings = [];
|
|
4692
5275
|
let warningsTruncated = false;
|
|
4693
5276
|
let lastResult = null;
|
|
5277
|
+
let writeRequestCount = 0;
|
|
5278
|
+
let requestTooLargeRetries = 0;
|
|
5279
|
+
let minimumBatchSizeUsed = null;
|
|
4694
5280
|
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
|
-
},
|
|
5281
|
+
const result = await writeImportBatchWithAutoSplit({
|
|
5282
|
+
tableRef: target.tableRef,
|
|
5283
|
+
upsertKey: target.upsertKey,
|
|
5284
|
+
rows: batch,
|
|
4703
5285
|
});
|
|
4704
|
-
rowCount +=
|
|
4705
|
-
insertedCount +=
|
|
4706
|
-
updatedCount +=
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
const batchWarnings = Array.isArray(result.warnings) ? result.warnings : [];
|
|
4710
|
-
for (const warning of batchWarnings) {
|
|
5286
|
+
rowCount += result.rowCount;
|
|
5287
|
+
insertedCount += result.insertedCount;
|
|
5288
|
+
updatedCount += result.updatedCount;
|
|
5289
|
+
warningCount += result.warningCount;
|
|
5290
|
+
for (const warning of result.warnings) {
|
|
4711
5291
|
if (warnings.length < 20)
|
|
4712
5292
|
warnings.push(warning);
|
|
4713
5293
|
}
|
|
4714
|
-
warningsTruncated = warningsTruncated || result.warningsTruncated
|
|
4715
|
-
|
|
5294
|
+
warningsTruncated = warningsTruncated || result.warningsTruncated || warningCount > warnings.length;
|
|
5295
|
+
writeRequestCount += result.writeRequestCount;
|
|
5296
|
+
requestTooLargeRetries += result.requestTooLargeRetries;
|
|
5297
|
+
minimumBatchSizeUsed = minimumBatchSizeUsed === null
|
|
5298
|
+
? result.minimumBatchSizeUsed
|
|
5299
|
+
: Math.min(minimumBatchSizeUsed, result.minimumBatchSizeUsed);
|
|
5300
|
+
lastResult = result.lastResult;
|
|
4716
5301
|
}
|
|
4717
5302
|
return {
|
|
4718
5303
|
table: lastResult?.table ?? null,
|
|
@@ -4725,8 +5310,12 @@ async function importRows(table, options) {
|
|
|
4725
5310
|
warningCount,
|
|
4726
5311
|
...(warningsTruncated ? { warningsTruncated: true } : {}),
|
|
4727
5312
|
} : {}),
|
|
4728
|
-
batchCount:
|
|
5313
|
+
batchCount: writeRequestCount,
|
|
4729
5314
|
batchSize,
|
|
5315
|
+
...(requestTooLargeRetries > 0 ? {
|
|
5316
|
+
requestTooLargeRetries,
|
|
5317
|
+
minimumBatchSizeUsed,
|
|
5318
|
+
} : {}),
|
|
4730
5319
|
...(target.tableWebUrl ? { table_web_url: target.tableWebUrl } : {}),
|
|
4731
5320
|
...(readRecordString(lastResult, "web_url") ? { web_url: readRecordString(lastResult, "web_url") } : {}),
|
|
4732
5321
|
};
|
|
@@ -4743,13 +5332,14 @@ async function prepareImportTarget(table, options, parsedRows) {
|
|
|
4743
5332
|
});
|
|
4744
5333
|
}
|
|
4745
5334
|
if (!options.create) {
|
|
5335
|
+
const keyMapping = await resolveExistingTableImportKeyMapping(table, parsedRows, readOption(options.upsertKey) ?? undefined);
|
|
4746
5336
|
return {
|
|
4747
5337
|
tableRef: table,
|
|
4748
|
-
rows: parsedRows,
|
|
5338
|
+
rows: remapImportRows(parsedRows, keyMapping.sourceKeyMap),
|
|
4749
5339
|
createdTable: null,
|
|
4750
5340
|
tableWebUrl: null,
|
|
4751
|
-
upsertKey:
|
|
4752
|
-
sourceKeyMap:
|
|
5341
|
+
upsertKey: keyMapping.upsertKey,
|
|
5342
|
+
sourceKeyMap: keyMapping.sourceKeyMap,
|
|
4753
5343
|
};
|
|
4754
5344
|
}
|
|
4755
5345
|
const normalized = normalizeRowsForNewTable(parsedRows);
|
|
@@ -4779,8 +5369,137 @@ async function prepareImportTarget(table, options, parsedRows) {
|
|
|
4779
5369
|
sourceKeyMap: normalized.keyBySource,
|
|
4780
5370
|
};
|
|
4781
5371
|
}
|
|
5372
|
+
async function resolveExistingTableImportKeyMapping(table, rows, upsertKey) {
|
|
5373
|
+
const describe = await requestOxygen("/api/cli/tables/describe", { method: "POST", body: { table } });
|
|
5374
|
+
const resolver = buildExistingTableColumnResolver(describe.columns ?? []);
|
|
5375
|
+
const sourceKeyMap = {};
|
|
5376
|
+
for (const sourceKey of inferImportColumnLabels(rows)) {
|
|
5377
|
+
const resolved = resolveImportSourceKey(sourceKey, resolver);
|
|
5378
|
+
if (resolved && resolved !== sourceKey)
|
|
5379
|
+
sourceKeyMap[sourceKey] = resolved;
|
|
5380
|
+
}
|
|
5381
|
+
return {
|
|
5382
|
+
sourceKeyMap: Object.keys(sourceKeyMap).length > 0 ? sourceKeyMap : null,
|
|
5383
|
+
upsertKey: upsertKey
|
|
5384
|
+
? resolveImportSourceKey(upsertKey, resolver) ?? upsertKey
|
|
5385
|
+
: undefined,
|
|
5386
|
+
};
|
|
5387
|
+
}
|
|
5388
|
+
function buildExistingTableColumnResolver(columns) {
|
|
5389
|
+
const exact = new Map();
|
|
5390
|
+
const aliases = new Map();
|
|
5391
|
+
for (const column of columns) {
|
|
5392
|
+
if (!column.key)
|
|
5393
|
+
continue;
|
|
5394
|
+
exact.set(column.key, column.key);
|
|
5395
|
+
addImportColumnAlias(aliases, column.label, column.key);
|
|
5396
|
+
addImportColumnAlias(aliases, normalizeImportColumnKey(column.key), column.key);
|
|
5397
|
+
if (column.label)
|
|
5398
|
+
addImportColumnAlias(aliases, normalizeImportColumnKey(column.label), column.key);
|
|
5399
|
+
}
|
|
5400
|
+
return { exact, aliases };
|
|
5401
|
+
}
|
|
5402
|
+
function addImportColumnAlias(aliases, alias, key) {
|
|
5403
|
+
const normalizedAlias = alias?.trim();
|
|
5404
|
+
if (!normalizedAlias)
|
|
5405
|
+
return;
|
|
5406
|
+
const existing = aliases.get(normalizedAlias);
|
|
5407
|
+
if (existing === undefined) {
|
|
5408
|
+
aliases.set(normalizedAlias, key);
|
|
5409
|
+
return;
|
|
5410
|
+
}
|
|
5411
|
+
if (existing !== key)
|
|
5412
|
+
aliases.set(normalizedAlias, null);
|
|
5413
|
+
}
|
|
5414
|
+
function resolveImportSourceKey(sourceKey, resolver) {
|
|
5415
|
+
const trimmed = sourceKey.trim();
|
|
5416
|
+
return resolver.exact.get(sourceKey)
|
|
5417
|
+
?? resolver.exact.get(trimmed)
|
|
5418
|
+
?? readImportColumnAlias(resolver.aliases, sourceKey)
|
|
5419
|
+
?? readImportColumnAlias(resolver.aliases, trimmed)
|
|
5420
|
+
?? readImportColumnAlias(resolver.aliases, normalizeImportColumnKey(sourceKey));
|
|
5421
|
+
}
|
|
5422
|
+
function readImportColumnAlias(aliases, alias) {
|
|
5423
|
+
return aliases.get(alias) ?? null;
|
|
5424
|
+
}
|
|
5425
|
+
function remapImportRows(rows, sourceKeyMap) {
|
|
5426
|
+
if (!sourceKeyMap)
|
|
5427
|
+
return rows;
|
|
5428
|
+
return rows.map((row) => {
|
|
5429
|
+
const remapped = {};
|
|
5430
|
+
for (const [key, value] of Object.entries(row)) {
|
|
5431
|
+
remapped[sourceKeyMap[key] ?? key] = value;
|
|
5432
|
+
}
|
|
5433
|
+
return remapped;
|
|
5434
|
+
});
|
|
5435
|
+
}
|
|
5436
|
+
async function writeImportBatchWithAutoSplit(input) {
|
|
5437
|
+
try {
|
|
5438
|
+
const result = await requestOxygen(input.upsertKey ? "/api/cli/tables/rows/upsert" : "/api/cli/tables/rows", {
|
|
5439
|
+
method: "POST",
|
|
5440
|
+
timeoutMs: 300_000,
|
|
5441
|
+
body: {
|
|
5442
|
+
table: input.tableRef,
|
|
5443
|
+
rows: input.rows,
|
|
5444
|
+
...(input.upsertKey ? { key: input.upsertKey } : {}),
|
|
5445
|
+
},
|
|
5446
|
+
});
|
|
5447
|
+
return summarizeImportBatchWrite(result, input.rows.length);
|
|
5448
|
+
}
|
|
5449
|
+
catch (error) {
|
|
5450
|
+
if (!isRequestTooLargeError(error) || input.rows.length <= 1)
|
|
5451
|
+
throw error;
|
|
5452
|
+
const midpoint = Math.ceil(input.rows.length / 2);
|
|
5453
|
+
process.stderr.write(`note: import batch of ${input.rows.length} rows exceeded the request limit; `
|
|
5454
|
+
+ `retrying as ${midpoint} and ${input.rows.length - midpoint} row batches.\n`);
|
|
5455
|
+
const first = await writeImportBatchWithAutoSplit({
|
|
5456
|
+
...input,
|
|
5457
|
+
rows: input.rows.slice(0, midpoint),
|
|
5458
|
+
});
|
|
5459
|
+
const second = await writeImportBatchWithAutoSplit({
|
|
5460
|
+
...input,
|
|
5461
|
+
rows: input.rows.slice(midpoint),
|
|
5462
|
+
});
|
|
5463
|
+
return combineImportBatchWriteSummaries(first, second, 1);
|
|
5464
|
+
}
|
|
5465
|
+
}
|
|
5466
|
+
function summarizeImportBatchWrite(result, batchSize) {
|
|
5467
|
+
const warningCount = readCount(result.warningCount);
|
|
5468
|
+
const batchWarnings = Array.isArray(result.warnings) ? result.warnings : [];
|
|
5469
|
+
return {
|
|
5470
|
+
rowCount: readCount(result.rowCount),
|
|
5471
|
+
insertedCount: readCount(result.insertedCount),
|
|
5472
|
+
updatedCount: readCount(result.updatedCount),
|
|
5473
|
+
warningCount,
|
|
5474
|
+
warnings: batchWarnings.slice(0, 20),
|
|
5475
|
+
warningsTruncated: result.warningsTruncated === true || warningCount > batchWarnings.length,
|
|
5476
|
+
writeRequestCount: 1,
|
|
5477
|
+
requestTooLargeRetries: 0,
|
|
5478
|
+
minimumBatchSizeUsed: batchSize,
|
|
5479
|
+
lastResult: result,
|
|
5480
|
+
};
|
|
5481
|
+
}
|
|
5482
|
+
function combineImportBatchWriteSummaries(first, second, extraRequestTooLargeRetries) {
|
|
5483
|
+
const warnings = [...first.warnings, ...second.warnings].slice(0, 20);
|
|
5484
|
+
const warningCount = first.warningCount + second.warningCount;
|
|
5485
|
+
return {
|
|
5486
|
+
rowCount: first.rowCount + second.rowCount,
|
|
5487
|
+
insertedCount: first.insertedCount + second.insertedCount,
|
|
5488
|
+
updatedCount: first.updatedCount + second.updatedCount,
|
|
5489
|
+
warningCount,
|
|
5490
|
+
warnings,
|
|
5491
|
+
warningsTruncated: first.warningsTruncated || second.warningsTruncated || warningCount > warnings.length,
|
|
5492
|
+
writeRequestCount: first.writeRequestCount + second.writeRequestCount,
|
|
5493
|
+
requestTooLargeRetries: first.requestTooLargeRetries + second.requestTooLargeRetries + extraRequestTooLargeRetries,
|
|
5494
|
+
minimumBatchSizeUsed: Math.min(first.minimumBatchSizeUsed, second.minimumBatchSizeUsed),
|
|
5495
|
+
lastResult: second.lastResult ?? first.lastResult,
|
|
5496
|
+
};
|
|
5497
|
+
}
|
|
5498
|
+
function isRequestTooLargeError(error) {
|
|
5499
|
+
return error instanceof OxygenError && error.code === "request_too_large";
|
|
5500
|
+
}
|
|
4782
5501
|
async function tryEnqueueStagedFileImport(// skipcq: JS-R1005
|
|
4783
|
-
table, options, format, parsedRows, context) {
|
|
5502
|
+
table, options, format, parsedRows, context, preparedTarget = null) {
|
|
4784
5503
|
if (options.create && table) {
|
|
4785
5504
|
throw new OxygenError("invalid_import_target", "Pass either a table argument or --create, not both.", {
|
|
4786
5505
|
exitCode: 1,
|
|
@@ -4817,7 +5536,7 @@ table, options, format, parsedRows, context) {
|
|
|
4817
5536
|
return null;
|
|
4818
5537
|
// Create the table for --create (or resolve the existing ref) before the
|
|
4819
5538
|
// upload so a presign success always pairs with a real target.
|
|
4820
|
-
const target = await prepareImportTarget(table, options, parsedRows);
|
|
5539
|
+
const target = preparedTarget ?? await prepareImportTarget(table, options, parsedRows);
|
|
4821
5540
|
const controller = new AbortController();
|
|
4822
5541
|
const timer = setTimeout(() => controller.abort(), 300_000);
|
|
4823
5542
|
let putResponse;
|
|
@@ -4862,7 +5581,7 @@ table, options, format, parsedRows, context) {
|
|
|
4862
5581
|
storage_provider: storageProvider,
|
|
4863
5582
|
};
|
|
4864
5583
|
}
|
|
4865
|
-
async function enqueueImportFile(table, options, format, batchSize, context) {
|
|
5584
|
+
async function enqueueImportFile(table, options, format, batchSize, context, preparedTarget = null) {
|
|
4866
5585
|
if (options.create && table) {
|
|
4867
5586
|
throw new OxygenError("invalid_import_target", "Pass either a table argument or --create, not both.", {
|
|
4868
5587
|
exitCode: 1,
|
|
@@ -4883,16 +5602,20 @@ async function enqueueImportFile(table, options, format, batchSize, context) {
|
|
|
4883
5602
|
form.append("batch_size", String(batchSize));
|
|
4884
5603
|
form.append("auto_background", String(context.autoBackground));
|
|
4885
5604
|
form.append("source_hash", context.sourceHash);
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
5605
|
+
const targetTable = preparedTarget?.tableRef ?? table;
|
|
5606
|
+
if (targetTable)
|
|
5607
|
+
form.append("table", targetTable);
|
|
5608
|
+
if (!preparedTarget && options.create)
|
|
4889
5609
|
form.append("table_name", options.create);
|
|
4890
5610
|
const project = readOption(options.project);
|
|
4891
5611
|
if (project)
|
|
4892
5612
|
form.append("project", project);
|
|
4893
|
-
const upsertKey = readOption(options.upsertKey);
|
|
5613
|
+
const upsertKey = preparedTarget?.upsertKey ?? readOption(options.upsertKey);
|
|
4894
5614
|
if (upsertKey)
|
|
4895
5615
|
form.append("upsert_key", upsertKey);
|
|
5616
|
+
if (preparedTarget?.sourceKeyMap) {
|
|
5617
|
+
form.append("source_key_map", JSON.stringify(preparedTarget.sourceKeyMap));
|
|
5618
|
+
}
|
|
4896
5619
|
if (options.sheet)
|
|
4897
5620
|
form.append("sheet", options.sheet);
|
|
4898
5621
|
const maxConcurrency = readPositiveInt(options.maxConcurrency);
|
|
@@ -5500,7 +6223,7 @@ function formatRows(rows, format, columns) {
|
|
|
5500
6223
|
const lines = [renderRow(headers), separator, ...formattedRows.map(renderRow)];
|
|
5501
6224
|
if (rescuedCount > 0) {
|
|
5502
6225
|
lines.push("");
|
|
5503
|
-
lines.push(`# Note: reformatted ${rescuedCount} cell${rescuedCount === 1 ? "" : "s"} that look numeric in text columns. Run \`oxygen columns retype --
|
|
6226
|
+
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
6227
|
}
|
|
5505
6228
|
return { content: `${lines.join("\n")}\n`, rescuedCount };
|
|
5506
6229
|
}
|
|
@@ -6731,6 +7454,93 @@ function ansi(enabled) {
|
|
|
6731
7454
|
function readOption(value) {
|
|
6732
7455
|
return value?.trim() ? value.trim() : null;
|
|
6733
7456
|
}
|
|
7457
|
+
// Assemble the POST body for `oxygen feedback`. Reads the local chat transcript
|
|
7458
|
+
// (unless --no-transcript) and attaches a non-sensitive environment snapshot so
|
|
7459
|
+
// the Oxygen team can triage. The transcript read happens here, inside the
|
|
7460
|
+
// command action, so any failure surfaces in the JSON envelope.
|
|
7461
|
+
function buildFeedbackBody(options) {
|
|
7462
|
+
const message = readOption(options.message);
|
|
7463
|
+
if (!message) {
|
|
7464
|
+
throw new OxygenError("invalid_input", 'Provide your feedback with --message "...".', { exitCode: 1 });
|
|
7465
|
+
}
|
|
7466
|
+
const body = {
|
|
7467
|
+
message,
|
|
7468
|
+
category: readOption(options.category) ?? "feedback",
|
|
7469
|
+
context: {
|
|
7470
|
+
cli_version: OXYGEN_VERSION,
|
|
7471
|
+
environment: collectFeedbackEnvironment(),
|
|
7472
|
+
},
|
|
7473
|
+
};
|
|
7474
|
+
if (readOption(options.severity))
|
|
7475
|
+
body.severity = readOption(options.severity);
|
|
7476
|
+
if (options.transcript !== false) {
|
|
7477
|
+
try {
|
|
7478
|
+
const captured = captureCurrentTranscript({
|
|
7479
|
+
sessionId: readOption(options.sessionId),
|
|
7480
|
+
file: readOption(options.file),
|
|
7481
|
+
});
|
|
7482
|
+
if (captured) {
|
|
7483
|
+
const { path: _path, ...payload } = captured;
|
|
7484
|
+
body.transcript = payload;
|
|
7485
|
+
}
|
|
7486
|
+
else {
|
|
7487
|
+
process.stderr.write("note: no local chat transcript found; sending your note without one.\n");
|
|
7488
|
+
}
|
|
7489
|
+
}
|
|
7490
|
+
catch (error) {
|
|
7491
|
+
if (error instanceof TranscriptCaptureError) {
|
|
7492
|
+
throw new OxygenError("transcript_not_found", error.message, { exitCode: 1 });
|
|
7493
|
+
}
|
|
7494
|
+
throw error;
|
|
7495
|
+
}
|
|
7496
|
+
}
|
|
7497
|
+
return body;
|
|
7498
|
+
}
|
|
7499
|
+
function splitCsvOption(value) {
|
|
7500
|
+
if (!value)
|
|
7501
|
+
return [];
|
|
7502
|
+
return value
|
|
7503
|
+
.split(",")
|
|
7504
|
+
.map((entry) => entry.trim())
|
|
7505
|
+
.filter((entry) => entry.length > 0);
|
|
7506
|
+
}
|
|
7507
|
+
function buildSupportTicketBody(options) {
|
|
7508
|
+
const body = { subject: readOption(options.subject) };
|
|
7509
|
+
if (readOption(options.body))
|
|
7510
|
+
body.body = readOption(options.body);
|
|
7511
|
+
if (readOption(options.severity))
|
|
7512
|
+
body.severity = readOption(options.severity);
|
|
7513
|
+
if (readOption(options.category))
|
|
7514
|
+
body.category = readOption(options.category);
|
|
7515
|
+
const context = {};
|
|
7516
|
+
if (readOption(options.operation))
|
|
7517
|
+
context.operation = readOption(options.operation);
|
|
7518
|
+
if (readOption(options.errorCode))
|
|
7519
|
+
context.error_code = readOption(options.errorCode);
|
|
7520
|
+
const runIds = splitCsvOption(options.runIds);
|
|
7521
|
+
const tableIds = splitCsvOption(options.tableIds);
|
|
7522
|
+
const deepLinks = splitCsvOption(options.deepLinks);
|
|
7523
|
+
if (runIds.length)
|
|
7524
|
+
context.run_ids = runIds;
|
|
7525
|
+
if (tableIds.length)
|
|
7526
|
+
context.table_ids = tableIds;
|
|
7527
|
+
if (deepLinks.length)
|
|
7528
|
+
context.deep_links = deepLinks;
|
|
7529
|
+
if (Object.keys(context).length)
|
|
7530
|
+
body.context = context;
|
|
7531
|
+
return body;
|
|
7532
|
+
}
|
|
7533
|
+
function withSupportListQuery(path, options) {
|
|
7534
|
+
const params = new URLSearchParams();
|
|
7535
|
+
const status = readOption(options.status);
|
|
7536
|
+
const limit = readOption(options.limit);
|
|
7537
|
+
if (status)
|
|
7538
|
+
params.set("status", status);
|
|
7539
|
+
if (limit)
|
|
7540
|
+
params.set("limit", limit);
|
|
7541
|
+
const query = params.toString();
|
|
7542
|
+
return query ? `${path}?${query}` : path;
|
|
7543
|
+
}
|
|
6734
7544
|
function readCsvOption(value) {
|
|
6735
7545
|
const option = readOption(value);
|
|
6736
7546
|
if (!option)
|
|
@@ -6746,6 +7556,26 @@ function splitCsv(value) {
|
|
|
6746
7556
|
.map((entry) => entry.trim())
|
|
6747
7557
|
.filter(Boolean);
|
|
6748
7558
|
}
|
|
7559
|
+
// Assemble the optional campaign email binding from the --email-* flags. The
|
|
7560
|
+
// content spec (--email-definition-file) is the author-provided email sequence
|
|
7561
|
+
// that the API compiles to an Instantly campaign on start; provider/connection
|
|
7562
|
+
// pick which Instantly account to bind. Returns undefined when no email flags
|
|
7563
|
+
// are set (a LinkedIn-only campaign).
|
|
7564
|
+
function readCampaignEmailBinding(options) {
|
|
7565
|
+
const provider = readOption(options.emailProvider);
|
|
7566
|
+
const connectionId = readOption(options.emailConnection);
|
|
7567
|
+
const definitionPath = readOption(options.emailDefinitionFile);
|
|
7568
|
+
if (!provider && !connectionId && !definitionPath)
|
|
7569
|
+
return undefined;
|
|
7570
|
+
const definition = definitionPath
|
|
7571
|
+
? JSON.parse(readFileSync(resolve(definitionPath), "utf8"))
|
|
7572
|
+
: undefined;
|
|
7573
|
+
return {
|
|
7574
|
+
...(provider ? { provider } : {}),
|
|
7575
|
+
...(connectionId ? { connection_id: connectionId } : {}),
|
|
7576
|
+
...(definition !== undefined ? { definition } : {}),
|
|
7577
|
+
};
|
|
7578
|
+
}
|
|
6749
7579
|
function collectMultiple(value, previous) {
|
|
6750
7580
|
return [...previous, value];
|
|
6751
7581
|
}
|
|
@@ -6908,6 +7738,9 @@ table, options) {
|
|
|
6908
7738
|
...(readOption(options.emailPatternValidation)
|
|
6909
7739
|
? { email_pattern_validation: readOption(options.emailPatternValidation) }
|
|
6910
7740
|
: {}),
|
|
7741
|
+
...(readOption(options.phoneWaterfallProfile)
|
|
7742
|
+
? { phone_waterfall_profile: readOption(options.phoneWaterfallProfile) }
|
|
7743
|
+
: {}),
|
|
6911
7744
|
...(options.verifyPhone ? { verify_phone: true } : {}),
|
|
6912
7745
|
...(readOption(options.phoneVerificationCredentialMode)
|
|
6913
7746
|
? { phone_verification_credential_mode: readOption(options.phoneVerificationCredentialMode) }
|
|
@@ -6919,22 +7752,7 @@ table, options) {
|
|
|
6919
7752
|
...(options.onlyMissing ? { only_missing: true } : {}),
|
|
6920
7753
|
};
|
|
6921
7754
|
}
|
|
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.
|
|
7755
|
+
function buildLinkedinLimitsBody(// skipcq: JS-R1005 -- CLI body builder maps per-account limit flags + reset timezone.
|
|
6938
7756
|
options) {
|
|
6939
7757
|
const limits = {};
|
|
6940
7758
|
const setLimit = (key, value) => {
|
|
@@ -6957,23 +7775,16 @@ options) {
|
|
|
6957
7775
|
setLimit("total_reads_per_day", options.totalReadsPerDay);
|
|
6958
7776
|
setLimit("min_action_spacing_seconds", options.minSpacingSeconds);
|
|
6959
7777
|
setLimit("action_spacing_jitter_seconds", options.spacingJitterSeconds);
|
|
7778
|
+
// Send windows are campaign-scoped; the only per-account time setting is the
|
|
7779
|
+
// timezone the daily counters reset in.
|
|
6960
7780
|
const workingHours = {};
|
|
6961
7781
|
const timezone = readOption(options.timezone);
|
|
6962
7782
|
if (timezone)
|
|
6963
7783
|
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
7784
|
const hasLimits = Object.keys(limits).length > 0;
|
|
6974
7785
|
const hasWorkingHours = Object.keys(workingHours).length > 0;
|
|
6975
7786
|
if (!hasLimits && !hasWorkingHours) {
|
|
6976
|
-
throw new OxygenError("invalid_request", "Pass at least one limit flag (e.g. --invites-per-day) or
|
|
7787
|
+
throw new OxygenError("invalid_request", "Pass at least one limit flag (e.g. --invites-per-day) or --timezone.", { exitCode: 1 });
|
|
6977
7788
|
}
|
|
6978
7789
|
return {
|
|
6979
7790
|
...(hasLimits ? { limits } : {}),
|