@oxygen-agent/cli 1.1011.326 → 1.1011.440
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/command-manifest.js +15 -4
- package/dist/connectors-commands.d.ts +4 -1
- package/dist/connectors-commands.js +84 -10
- package/dist/contract-flags.d.ts +2 -0
- package/dist/contract-flags.js +26 -11
- package/dist/crm-support-flags.d.ts +11 -0
- package/dist/crm-support-flags.js +60 -0
- package/dist/customer-operation-commands.d.ts +5 -0
- package/dist/customer-operation-commands.js +101 -0
- package/dist/database-query-flags.d.ts +2 -0
- package/dist/database-query-flags.js +6 -0
- package/dist/help.d.ts +3 -1
- package/dist/help.js +43 -0
- package/dist/http-client.d.ts +2 -0
- package/dist/http-client.js +41 -1
- package/dist/index.js +640 -234
- package/dist/knowledge-repository-commands.js +12 -14
- package/dist/mailbox-ramp-notice.d.ts +11 -0
- package/dist/mailbox-ramp-notice.js +53 -0
- package/dist/settings-commands.d.ts +11 -0
- package/dist/settings-commands.js +73 -0
- package/dist/skills.d.ts +5 -4
- package/dist/skills.js +106 -79
- package/dist/tool-catalog-detail-command.d.ts +7 -0
- package/dist/tool-catalog-detail-command.js +19 -0
- package/dist/tool-catalog-search-window.d.ts +6 -0
- package/dist/tool-catalog-search-window.js +12 -0
- package/dist/transcript.js +1 -0
- package/node_modules/@oxygen/shared/dist/bounded-read-result.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/bounded-read-result.js +25 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +47 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +625 -39
- package/node_modules/@oxygen/shared/dist/connection-approval.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/connection-approval.js +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +110 -0
- package/node_modules/@oxygen/shared/dist/contracts/connectors.d.ts +32 -6
- package/node_modules/@oxygen/shared/dist/contracts/connectors.js +56 -4
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.js +39 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-enrichment-read.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-enrichment-read.js +15 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +117 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +43 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +131 -0
- package/node_modules/@oxygen/shared/dist/contracts/dashboard-tabs.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/contracts/dashboard-tabs.js +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/database.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/contracts/database.js +29 -0
- package/node_modules/@oxygen/shared/dist/contracts/integration-connect.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/integration-connect.js +7 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.d.ts +43 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.js +20 -0
- package/node_modules/@oxygen/shared/dist/contracts/linkedin-invites-list.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/contracts/linkedin-invites-list.js +8 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +48 -2
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +68 -2
- package/node_modules/@oxygen/shared/dist/contracts/table-grid-query.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-grid-query.js +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.js +27 -0
- package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-detail.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-detail.js +10 -0
- package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-search-window.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-search-window.js +7 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.js +49 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger-parser.d.ts +32 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger-parser.js +303 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger.generated.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger.generated.js +717 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger.js +20 -0
- package/node_modules/@oxygen/shared/dist/email-campaign-ramp.d.ts +104 -0
- package/node_modules/@oxygen/shared/dist/email-campaign-ramp.js +250 -0
- package/node_modules/@oxygen/shared/dist/mcp-event-contract.d.ts +10 -7
- package/node_modules/@oxygen/shared/dist/mcp-event-contract.js +11 -7
- package/node_modules/@oxygen/shared/dist/mcp-table-widget.d.ts +50 -0
- package/node_modules/@oxygen/shared/dist/mcp-table-widget.js +24 -0
- package/node_modules/@oxygen/shared/dist/plan-band.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/plan-band.js +12 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +20 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.js +41 -0
- package/node_modules/@oxygen/shared/dist/provider-input-value.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/provider-input-value.js +10 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +33 -4
- package/node_modules/@oxygen/shared/dist/sequences.js +48 -13
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +17 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/package.json +8 -6
- package/package.json +1 -1
- package/node_modules/@oxygen/shared/dist/copilot-skills.d.ts +0 -15
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +0 -31
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +0 -41
- package/node_modules/@oxygen/shared/dist/copilot-skills.js +0 -6
package/dist/index.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { knowledgeIndexReadContract, knowledgePageReadContract } from "@oxygen/shared/contracts/knowledge-content-reads";
|
|
2
|
+
import { DATABASE_ATTACH_CONTRACT, DATABASE_MIGRATE_CONTRACT, DATABASE_QUERY_CONTRACT } from "@oxygen/shared/contracts/database";
|
|
3
|
+
import { DATABASE_QUERY_FLAG_OVERLAY } from "./database-query-flags.js";
|
|
4
|
+
import { LINKEDIN_SENT_INVITES_QUERY_CONTRACT } from "@oxygen/shared/contracts/linkedin-invites-list";
|
|
5
|
+
import { registerCrmSupportFlags, readCrmNoteEditFields, readCrmPhotoFields, readCrmFileLabel, buildCrmObjectsPath, buildCrmConnectionsPath } from "./crm-support-flags.js";
|
|
6
|
+
import { addToolCatalogSearchWindowFlag, readToolCatalogSearchWindow } from "./tool-catalog-search-window.js";
|
|
7
|
+
import { createToolCatalogDetailCommand } from "./tool-catalog-detail-command.js";
|
|
1
8
|
import { execFileSync } from "node:child_process";
|
|
2
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
10
|
import { closeSync, createReadStream, createWriteStream, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
@@ -9,12 +16,14 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
9
16
|
import { Command, CommanderError, Option } from "commander";
|
|
10
17
|
import { registerUgcCommands } from "./ugc-commands.js";
|
|
11
18
|
import { registerConnectorsCommands } from "./connectors-commands.js";
|
|
19
|
+
import { registerSettingsCommands } from "./settings-commands.js";
|
|
20
|
+
import { registerCustomerOperationCommands } from "./customer-operation-commands.js";
|
|
12
21
|
import { registerKnowledgeRepositoryCommands } from "./knowledge-repository-commands.js";
|
|
13
22
|
import { registerVisualCommands } from "./visual-commands.js";
|
|
14
23
|
import { renderPrimaryProviderBoard } from "./admin-primary-providers-render.js";
|
|
15
24
|
import { registerFunctionsCommands } from "./functions-commands.js";
|
|
16
25
|
import { STREAMED_FILE_IMPORT_MIN_BYTES, putImportFileStream, rowsForStreamedImportTarget, scanStreamedImportFile, shouldStreamFileImport, } from "./streamed-file-import.js";
|
|
17
|
-
import { applyOxygenHelp, argvRequestsJson, commanderUsageFailure, enableCommandSuggestions, unknownCommandHint, unknownOptionHint, withoutHelpForUnknownCommand, } from "./help.js";
|
|
26
|
+
import { applyOxygenHelp, argvRequestsJson, commanderUsageFailure, enableCommandSuggestions, unknownCommandHint, unknownOptionHint, withoutHelpForUnknownCommand, validateRequiredOptionValues, } from "./help.js";
|
|
18
27
|
import { buildCommandManifest, getCommandManifestEntry, searchCommandManifest, suggestCommandNames, } from "./command-manifest.js";
|
|
19
28
|
import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, COLLAB_GATE_KINDS, COLLAB_GATE_PROSE, COLLAB_SUBJECT_KINDS, COLLAB_SUBJECT_KINDS_PROSE, COLLAB_SUBJECT_LABELS, COLLAB_SUBJECT_PROSE, GATE_KIND_SUBJECTS, describeWorkflowStatusChange, formatCellForDisplay, formatCopilotPlanDuration, formatCopilotPlanSeconds, formatPublicBudgetScopes, SUBJECT_PATH_FORMS_PROSE, formatSubjectPath, exitCodeForOxygenError, parseSubjectPath, parseSubjectRef, parseWorkflowStatusChange, chunk, isVersionGreater, isVersionLess, KNOWLEDGE_BOOTSTRAP_MAX_CREDITS, MAX_CLI_JSON_BODY_BYTES, MAX_MCP_TOOL_NAME_LENGTH, normalizeCopilotPlanStepStatus, OXYGEN_CAPABILITY_ROUTES, OXYGEN_VERSION, OxygenError, getCapabilityRouteMatch, inferUserCapabilityRoute, parseKnowledgePageMarkdown, PLAN_LIMITS, serializeCapabilityRoute, sleep, success, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
|
|
20
29
|
import { PURCHASABLE_PLAN_KEYS, planVolumeBonusPercent, resolveBasePricingPlan } from "@oxygen/shared/billing";
|
|
@@ -25,7 +34,10 @@ import { LINKEDIN_SENDER_LIMIT_DEFAULTS, LINKEDIN_SENDER_LIMIT_MAXIMUMS, } from
|
|
|
25
34
|
import { SEQUENCE_LIFECYCLES, sequenceDisplayStatus } from "@oxygen/shared/sequence-lifecycle";
|
|
26
35
|
import { operationPath } from "@oxygen/shared/contracts/operations";
|
|
27
36
|
import { SENDER_LIMITS_SET_OPERATION } from "@oxygen/shared/contracts/sender-limits";
|
|
28
|
-
import { addContractFlags } from "./contract-flags.js";
|
|
37
|
+
import { addContractFlags, readContractFlags } from "./contract-flags.js";
|
|
38
|
+
import { TABLE_QUERY_GRID_CONTRACT } from "@oxygen/shared/contracts/table-query-grid";
|
|
39
|
+
import { INTEGRATION_CONNECT_ACCOUNT_CONTRACT } from "@oxygen/shared/contracts/integration-connect";
|
|
40
|
+
import { CRM_ENRICHMENT_READ_CONTRACT } from "@oxygen/shared/contracts/crm-enrichment-read";
|
|
29
41
|
import { buildSenderLimitsBody, parseWorkingDaysOption, SENDER_LIMITS_FLAG_OVERLAY } from "./sender-limits-flags.js";
|
|
30
42
|
import { readColumnDecisionFlags } from "./column-decision-options.js";
|
|
31
43
|
import { MANAGED_DATA_SUPPLIERS } from "@oxygen/shared/data-suppliers";
|
|
@@ -44,8 +56,9 @@ import { formatAiPromptPreviewNotice, formatColumnReferenceNotices, formatEnrich
|
|
|
44
56
|
import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
|
|
45
57
|
import { formatSearchAiFilterTotalsNotice } from "./search-ai-filter-notice.js";
|
|
46
58
|
import { formatInboxNeedsReplyNotice } from "./inbox-needs-reply-notice.js";
|
|
59
|
+
import { formatMailboxRampNotice } from "./mailbox-ramp-notice.js";
|
|
47
60
|
import { formatWorkflowPlanLimitNotices } from "./workflow-plan-limit-notices.js";
|
|
48
|
-
import { captureCurrentTranscript, collectFeedbackEnvironment, TranscriptCaptureError, } from "./transcript.js";
|
|
61
|
+
import { captureCurrentTranscript, collectFeedbackEnvironment, redactTranscriptSecrets, TranscriptCaptureError, } from "./transcript.js";
|
|
49
62
|
import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, updateSessionStep, } from "./session.js";
|
|
50
63
|
import { doctorAgentSkills, getAgentSkill, installAgentSkills, listAgentSkills, runAutomaticSkillsInstall, searchAgentSkills, } from "./skills.js";
|
|
51
64
|
import { resolveCliBinaryName } from "./runtime.js";
|
|
@@ -279,6 +292,11 @@ function buildFindBody(capability, options) {
|
|
|
279
292
|
body.verify = true;
|
|
280
293
|
return body;
|
|
281
294
|
}
|
|
295
|
+
const LINKEDIN_SENT_INVITES_FLAG_OVERLAY = {
|
|
296
|
+
account: { valueName: "<ref>", help: "Scope to one sender account (sender id, connection id, or Unipile account id).", parse: readOption },
|
|
297
|
+
limit: { valueName: "<n>", help: "Maximum invitations to return.", parse: readPositiveInt },
|
|
298
|
+
cursor: { valueName: "<cursor>", parse: readOption },
|
|
299
|
+
};
|
|
282
300
|
/** The shared search/buy filters, in the snake_case the voice numbers route reads. */
|
|
283
301
|
function voiceNumberFilters(options) {
|
|
284
302
|
const areaCode = readOption(options.areaCode);
|
|
@@ -290,6 +308,13 @@ function voiceNumberFilters(options) {
|
|
|
290
308
|
...(country ? { iso_country: country.toUpperCase() } : {}),
|
|
291
309
|
};
|
|
292
310
|
}
|
|
311
|
+
function readSuppressionBulkKeys(value, flag) {
|
|
312
|
+
const entries = splitCommaList(value);
|
|
313
|
+
if (entries.length === 0 || entries.length > 500) {
|
|
314
|
+
throw new OxygenError("invalid_request", `${flag} requires 1-500 nonblank entries.`, { exitCode: 2 });
|
|
315
|
+
}
|
|
316
|
+
return entries;
|
|
317
|
+
}
|
|
293
318
|
function suppressionListParams(options) {
|
|
294
319
|
const params = new URLSearchParams();
|
|
295
320
|
const reason = readOption(options.reason);
|
|
@@ -852,6 +877,7 @@ async function handleAsyncAction(command, options, action) {
|
|
|
852
877
|
writeCreditsReceipt(data);
|
|
853
878
|
writeSearchAiFilterTotalsNotice(data);
|
|
854
879
|
writeInboxNeedsReplyNotice(data);
|
|
880
|
+
writeMailboxRampNotice(data);
|
|
855
881
|
}
|
|
856
882
|
catch (error) {
|
|
857
883
|
emitCliFailure(command, error);
|
|
@@ -1162,6 +1188,11 @@ function writeInboxNeedsReplyNotice(data) {
|
|
|
1162
1188
|
if (notice)
|
|
1163
1189
|
process.stderr.write(`${notice}\n`);
|
|
1164
1190
|
}
|
|
1191
|
+
function writeMailboxRampNotice(data) {
|
|
1192
|
+
const notice = formatMailboxRampNotice(data);
|
|
1193
|
+
if (notice)
|
|
1194
|
+
process.stderr.write(`${notice}\n`);
|
|
1195
|
+
}
|
|
1165
1196
|
function writeAiPromptPreviewNotice(data) {
|
|
1166
1197
|
const notice = formatAiPromptPreviewNotice(data);
|
|
1167
1198
|
if (notice)
|
|
@@ -2143,6 +2174,9 @@ function buildCrmSyncRunBody(options) {
|
|
|
2143
2174
|
provider: options.provider,
|
|
2144
2175
|
object: options.object,
|
|
2145
2176
|
mode: resolveLiveDryRunMode(options),
|
|
2177
|
+
...(readOption(options.direction) ? { direction: readOption(options.direction) } : {}),
|
|
2178
|
+
...(readOption(options.since) ? { since: readOption(options.since) } : {}),
|
|
2179
|
+
...(readOption(options.configId) ? { config_id: readOption(options.configId) } : {}),
|
|
2146
2180
|
...(readPositiveInt(options.maxRows) !== undefined ? { max_rows_per_cycle: readPositiveInt(options.maxRows) } : {}),
|
|
2147
2181
|
...(readPositiveNumber(options.maxCredits) !== undefined ? { max_credits: readPositiveNumber(options.maxCredits) } : {}),
|
|
2148
2182
|
...(options.approved ? { approved: true } : {}),
|
|
@@ -2196,11 +2230,16 @@ function buildCrmSyncLinksPath(object, rowId) {
|
|
|
2196
2230
|
params.set("row_id", rowId);
|
|
2197
2231
|
return `/api/cli/crm/sync/links?${params.toString()}`;
|
|
2198
2232
|
}
|
|
2233
|
+
const CRM_ENRICHMENT_READ_FLAGS = {
|
|
2234
|
+
object: { valueName: "<object>", help: "CRM object slug: companies or people. Omit for every object this workspace has.", parse: readOption },
|
|
2235
|
+
};
|
|
2199
2236
|
function buildCrmEnrichmentListPath(options) {
|
|
2200
|
-
const
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2237
|
+
const fields = readContractFlags(options, CRM_ENRICHMENT_READ_CONTRACT, CRM_ENRICHMENT_READ_FLAGS);
|
|
2238
|
+
const params = new URLSearchParams();
|
|
2239
|
+
for (const [key, value] of Object.entries(fields))
|
|
2240
|
+
if (value !== undefined && value !== null)
|
|
2241
|
+
params.set(key, String(value));
|
|
2242
|
+
return params.size ? `/api/cli/crm/enrichment?${params.toString()}` : "/api/cli/crm/enrichment";
|
|
2204
2243
|
}
|
|
2205
2244
|
// The delta form: flip one named enrichment and leave every other decision
|
|
2206
2245
|
// (the rest of the presets, the ceiling, force, the trigger set) exactly as it
|
|
@@ -2263,6 +2302,8 @@ function buildNotetakerSetupBody(options) {
|
|
|
2263
2302
|
capture_mode: "invited_bot",
|
|
2264
2303
|
auto_record_scope: "manual",
|
|
2265
2304
|
...(readOption(options.botName) ? { bot_name: readOption(options.botName) } : {}),
|
|
2305
|
+
...(options.botAvatarUrl !== undefined ? { bot_avatar_url: readOption(options.botAvatarUrl) ?? null } : {}),
|
|
2306
|
+
...(options.consentDisclosure !== undefined ? { consent_disclosure: readOption(options.consentDisclosure) ?? null } : {}),
|
|
2266
2307
|
...(readPositiveInt(options.defaultMaxMinutes) ? { default_max_minutes: readPositiveInt(options.defaultMaxMinutes) } : {}),
|
|
2267
2308
|
...(readPositiveNumber(options.defaultMaxCredits) ? { default_max_credits: readPositiveNumber(options.defaultMaxCredits) } : {}),
|
|
2268
2309
|
...(readPositiveInt(options.retentionDays) ? { retention_days: readPositiveInt(options.retentionDays) } : {}),
|
|
@@ -2349,6 +2390,7 @@ function buildPublishingPostCreateBody(options) {
|
|
|
2349
2390
|
const title = readOption(options.title);
|
|
2350
2391
|
const timezone = readOption(options.timezone);
|
|
2351
2392
|
const ugcParticipationId = readOption(options.ugcParticipationId);
|
|
2393
|
+
const idempotencyKey = readOption(options.idempotencyKey);
|
|
2352
2394
|
const content = buildPublishingContent(options);
|
|
2353
2395
|
if (provider)
|
|
2354
2396
|
body.provider = provider;
|
|
@@ -2362,6 +2404,10 @@ function buildPublishingPostCreateBody(options) {
|
|
|
2362
2404
|
body.title = title;
|
|
2363
2405
|
if (timezone)
|
|
2364
2406
|
body.timezone = timezone;
|
|
2407
|
+
if (idempotencyKey)
|
|
2408
|
+
body.idempotency_key = idempotencyKey;
|
|
2409
|
+
if (options.tags !== undefined)
|
|
2410
|
+
body.tags = splitCommaList(options.tags);
|
|
2365
2411
|
if (ugcParticipationId) {
|
|
2366
2412
|
if (options.approved) {
|
|
2367
2413
|
throw new OxygenError("ugc_draft_required", "A program-bound personal post must be saved unapproved. Remove --approved, then review it in Publishing → UGC.", { exitCode: 1 });
|
|
@@ -2579,9 +2625,9 @@ async function uploadPublishingMediaFile(filePath, options) {
|
|
|
2579
2625
|
},
|
|
2580
2626
|
});
|
|
2581
2627
|
const uploadUrl = readRecordString(ticket, "upload_url");
|
|
2582
|
-
const
|
|
2583
|
-
if (!uploadUrl || !
|
|
2584
|
-
throw new OxygenError("publishing_media_upload_failed", "The server did not return an upload URL for that file.", { exitCode: 1 });
|
|
2628
|
+
const mediaId = isRecord(ticket.media) ? readRecordString(ticket.media, "id") : null;
|
|
2629
|
+
if (!uploadUrl || !mediaId) {
|
|
2630
|
+
throw new OxygenError("publishing_media_upload_failed", "The server did not return an upload URL and media ID for that file.", { exitCode: 1 });
|
|
2585
2631
|
}
|
|
2586
2632
|
const headers = ticket.upload_headers && typeof ticket.upload_headers === "object"
|
|
2587
2633
|
? Object.fromEntries(Object.entries(ticket.upload_headers).map(([key, value]) => [key, String(value)]))
|
|
@@ -2590,7 +2636,7 @@ async function uploadPublishingMediaFile(filePath, options) {
|
|
|
2590
2636
|
if (!put.ok) {
|
|
2591
2637
|
throw new OxygenError("publishing_media_upload_failed", `Object storage rejected the upload (${put.status}).`, { exitCode: 1 });
|
|
2592
2638
|
}
|
|
2593
|
-
return await requestOxygen(
|
|
2639
|
+
return await requestOxygen(`/api/cli/publishing/media/${encodeURIComponent(mediaId)}/uploaded`, { method: "POST", body: {} });
|
|
2594
2640
|
}
|
|
2595
2641
|
function buildPublishingMediaUploadUrlBody(options) {
|
|
2596
2642
|
const fileName = readOption(options.fileName);
|
|
@@ -3066,6 +3112,7 @@ function publishingAmplificationTunables(options) {
|
|
|
3066
3112
|
const commentMin = readNonNegativeInt(options.commentDelayMin);
|
|
3067
3113
|
const commentMax = readNonNegativeInt(options.commentDelayMax);
|
|
3068
3114
|
return {
|
|
3115
|
+
...(options.metadataJson !== undefined ? { metadata: parseJsonObject(readFileIfPresent(options.metadataJson)) } : {}),
|
|
3069
3116
|
...(maxActions !== undefined ? { max_actions_per_post: maxActions } : {}),
|
|
3070
3117
|
...(commentPool ? { comment_source: "pool", comment_pool: commentPool } : {}),
|
|
3071
3118
|
...(reactionMin !== undefined ? { reaction_delay_min_seconds: reactionMin } : {}),
|
|
@@ -3126,6 +3173,7 @@ function buildPublishingBoostBody(postId, options) {
|
|
|
3126
3173
|
const maxActions = readPositiveInt(options.maxActionsPerPost);
|
|
3127
3174
|
const commentPool = readPublishingCommentPool(options);
|
|
3128
3175
|
return {
|
|
3176
|
+
...(readOption(options.name) ? { name: readOption(options.name) } : {}),
|
|
3129
3177
|
senders,
|
|
3130
3178
|
actions,
|
|
3131
3179
|
max_credits: maxCredits,
|
|
@@ -3570,7 +3618,8 @@ export function createProgram() {
|
|
|
3570
3618
|
.version(OXYGEN_VERSION)
|
|
3571
3619
|
.option("--profile <name>", "Use a stored CLI profile for this command.")
|
|
3572
3620
|
.option("--org <organization>", "Use an organization id, Clerk org id, or slug for this command.");
|
|
3573
|
-
program.hook("preAction", () => {
|
|
3621
|
+
program.hook("preAction", (_root, selected) => {
|
|
3622
|
+
validateRequiredOptionValues(program, selected);
|
|
3574
3623
|
const options = program.opts();
|
|
3575
3624
|
if (options.profile) {
|
|
3576
3625
|
globalProfileFlag = options.profile;
|
|
@@ -4001,6 +4050,8 @@ export function createProgram() {
|
|
|
4001
4050
|
.requiredOption("-m, --message <message>", "Your message to OXYGEN support.")
|
|
4002
4051
|
.option("--severity <severity>", "New conversation only: low | normal | high. Defaults to normal.")
|
|
4003
4052
|
.option("--category <category>", "New conversation only: question, bug, feature_request, billing, security_data, configuration, agency_directory, or other. Defaults to question.")
|
|
4053
|
+
.option("--transcript-text <text>", "New conversation only: explicitly share a selected transcript excerpt of 1–200,000 bytes after credential redaction.")
|
|
4054
|
+
.option("--no-transcript", "Omit any selected transcript excerpt.")
|
|
4004
4055
|
.option("--json", "Print a JSON envelope.")
|
|
4005
4056
|
.addHelpText("after", `
|
|
4006
4057
|
Examples:
|
|
@@ -4010,29 +4061,42 @@ Examples:
|
|
|
4010
4061
|
.action(async (threadId, options) => {
|
|
4011
4062
|
const message = readOption(options.message);
|
|
4012
4063
|
if (threadId) {
|
|
4013
|
-
await handleAsyncAction("support chat", options, () =>
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4064
|
+
await handleAsyncAction("support chat", options, () => {
|
|
4065
|
+
if (options.transcript !== false && options.transcriptText !== undefined) {
|
|
4066
|
+
throw new OxygenError("invalid_transcript", "Transcript excerpts can only be attached when starting a new conversation. Omit the Thread ID or use support file.", { exitCode: 1 });
|
|
4067
|
+
}
|
|
4068
|
+
return requestOxygen(`/api/cli/support/tickets/${encodeURIComponent(threadId)}/messages`, {
|
|
4069
|
+
method: "POST",
|
|
4070
|
+
body: { body: message },
|
|
4071
|
+
});
|
|
4072
|
+
});
|
|
4017
4073
|
return;
|
|
4018
4074
|
}
|
|
4019
4075
|
const severity = readOption(options.severity);
|
|
4020
|
-
await handleAsyncAction("support chat", options, () =>
|
|
4021
|
-
|
|
4022
|
-
body: {
|
|
4076
|
+
await handleAsyncAction("support chat", options, () => {
|
|
4077
|
+
const body = {
|
|
4023
4078
|
message,
|
|
4024
4079
|
category: readOption(options.category) ?? "question",
|
|
4025
4080
|
...(severity ? { severity } : {}),
|
|
4026
|
-
}
|
|
4027
|
-
|
|
4081
|
+
};
|
|
4082
|
+
if (options.transcript !== false && options.transcriptText !== undefined) {
|
|
4083
|
+
body.transcript_text = readSelectedTranscriptExcerpt(options.transcriptText);
|
|
4084
|
+
}
|
|
4085
|
+
return requestOxygen("/api/cli/support/tickets", {
|
|
4086
|
+
method: "POST",
|
|
4087
|
+
body,
|
|
4088
|
+
});
|
|
4089
|
+
});
|
|
4028
4090
|
}))
|
|
4029
4091
|
.addCommand(new Command("file")
|
|
4030
|
-
.description("Create a structured Plain support conversation with a subject and required message. Prefer `support chat` for an ordinary conversation; use this zero-credit filing path when operation, error, run, or table metadata helps triage.
|
|
4092
|
+
.description("Create a structured Plain support conversation with a subject and required message. Prefer `support chat` for an ordinary conversation; use this zero-credit filing path when operation, error, run, or table metadata helps triage. Share a selected excerpt with --transcript-text, or a local transcript with --transcript-file, --session-id, or --include-transcript. Check `support list --status open` first because a new filing can create a second Thread.")
|
|
4031
4093
|
.requiredOption("--subject <subject>", "One-line summary of the problem.")
|
|
4032
4094
|
.requiredOption("--body <body>", "Required message: what you were doing, what happened, and what you tried.")
|
|
4033
4095
|
.option("--include-transcript", "Attach the most recent local agent transcript after redaction.")
|
|
4034
4096
|
.option("--session-id <id>", "Attach this local agent session transcript.")
|
|
4035
4097
|
.option("--transcript-file <path>", "Attach the last 2 MB of a selected text/JSONL file after best-effort credential redaction; review it first.")
|
|
4098
|
+
.option("--transcript-text <text>", "Explicitly share a selected transcript excerpt of 1–200,000 bytes after credential redaction, instead of a local transcript.")
|
|
4099
|
+
.option("--no-transcript", "Omit any transcript, including explicitly selected content.")
|
|
4036
4100
|
.option("--severity <severity>", "low | normal | high. Defaults to normal.")
|
|
4037
4101
|
.option("--category <category>", "Request type: question, bug, feature_request, billing, security_data, configuration, agency_directory, or other.")
|
|
4038
4102
|
.option("--source <source>", "Where the ticket originated. Only slack is supported today.")
|
|
@@ -4044,6 +4108,7 @@ Examples:
|
|
|
4044
4108
|
.option("--slack-permalink <url>", "Slack message permalink (https://*.slack.com/...) to attach as context.")
|
|
4045
4109
|
.option("--slack-channel-id <id>", "Slack channel id (e.g. C0123ABC456) the report came from.")
|
|
4046
4110
|
.option("--customer-workspace <label>", "Customer workspace (org name/slug) a forwarded report concerns.")
|
|
4111
|
+
.option("--cli-version <version>", "CLI version associated with the reported problem.")
|
|
4047
4112
|
.option("--json", "Print a JSON envelope.")
|
|
4048
4113
|
.action(async (options) => {
|
|
4049
4114
|
await handleAsyncAction("support ticket create", options, () => requestOxygen("/api/cli/support/tickets", {
|
|
@@ -4390,6 +4455,15 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4390
4455
|
.option("-m, --message <message>", "Your feedback or bug report.")
|
|
4391
4456
|
.option("--severity <severity>", "low | normal | high. Defaults to normal.")
|
|
4392
4457
|
.option("--category <category>", "Optional category label. Defaults to 'feedback'.")
|
|
4458
|
+
.option("--deep-links <urls>", "Comma-separated workspace links to include in the report context.")
|
|
4459
|
+
.option("--operation <operation>", "The operation that failed.")
|
|
4460
|
+
.option("--error-code <code>", "Error envelope code from the failed operation.")
|
|
4461
|
+
.option("--run-ids <ids>", "Comma-separated affected run ids.")
|
|
4462
|
+
.option("--table-ids <ids>", "Comma-separated affected table ids.")
|
|
4463
|
+
.option("--slack-permalink <url>", "Customer-selected Slack message permalink for a forwarded report.")
|
|
4464
|
+
.option("--slack-channel-id <id>", "Slack channel associated with the forwarded report.")
|
|
4465
|
+
.option("--customer-workspace <label>", "Workspace name or slug associated with a forwarded report.")
|
|
4466
|
+
.option("--transcript-text <text>", "Explicitly attach a selected excerpt (1-200,000 bytes) after best-effort secret redaction; exclusive with local transcript options.")
|
|
4393
4467
|
.option("--session-id <id>", "Explicitly attach a specific local session transcript by id.")
|
|
4394
4468
|
.option("--file <path>", "Explicitly attach a specific local transcript file.")
|
|
4395
4469
|
.option("--include-transcript", "Explicitly attach the most recently active local session after best-effort secret redaction. Review it for sensitive customer data first.")
|
|
@@ -4416,14 +4490,13 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4416
4490
|
.action(async (options) => {
|
|
4417
4491
|
await handleAsyncAction("db provision", options, () => requestOxygen("/api/cli/db/provision", { method: "POST", body: {} }));
|
|
4418
4492
|
}))
|
|
4419
|
-
.addCommand(new Command("migrate")
|
|
4420
|
-
.description("Apply pending tenant database migrations for the current organization.")
|
|
4421
|
-
.option("--rotate-credentials", "Rotate the tenant runtime/read DB passwords and rewrite stored credentials. Use only to repair a tenant whose stored credentials are out of sync; routine migrations do not need it.")
|
|
4493
|
+
.addCommand(addContractFlags(new Command("migrate")
|
|
4494
|
+
.description("Apply pending tenant database migrations for the current organization."), DATABASE_MIGRATE_CONTRACT, { rotate_credentials: { noNegate: true } })
|
|
4422
4495
|
.option("--json", "Print a JSON envelope.")
|
|
4423
4496
|
.action(async (options) => {
|
|
4424
4497
|
await handleAsyncAction("db migrate", options, () => requestOxygen("/api/cli/db/migrate", {
|
|
4425
4498
|
method: "POST",
|
|
4426
|
-
body: options
|
|
4499
|
+
body: readContractFlags(options, DATABASE_MIGRATE_CONTRACT, { rotate_credentials: { noNegate: true } }),
|
|
4427
4500
|
}));
|
|
4428
4501
|
}))
|
|
4429
4502
|
.addCommand(new Command("migrate-all")
|
|
@@ -4557,29 +4630,27 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4557
4630
|
},
|
|
4558
4631
|
}));
|
|
4559
4632
|
}))
|
|
4560
|
-
.addCommand(new Command("attach")
|
|
4633
|
+
.addCommand(addContractFlags(new Command("attach")
|
|
4561
4634
|
.description("Attach an existing Postgres database as the current organization's tenant database.")
|
|
4562
|
-
.requiredOption("--database-url <url>", "Owner connection string for the tenant Postgres database.")
|
|
4635
|
+
.requiredOption("--database-url <url>", "Owner connection string for the tenant Postgres database."), DATABASE_ATTACH_CONTRACT, { database_url: { omit: true } })
|
|
4563
4636
|
.option("--json", "Print a JSON envelope.")
|
|
4564
4637
|
.action(async (options) => {
|
|
4565
4638
|
await handleAsyncAction("db attach", options, () => requestOxygen("/api/cli/db/attach", {
|
|
4566
4639
|
method: "POST",
|
|
4567
|
-
body:
|
|
4640
|
+
body: readContractFlags(options, DATABASE_ATTACH_CONTRACT),
|
|
4568
4641
|
}));
|
|
4569
4642
|
}))
|
|
4570
|
-
.addCommand(new Command("query")
|
|
4643
|
+
.addCommand(addContractFlags(new Command("query")
|
|
4571
4644
|
.description("Run one read-only SQL statement against the current tenant database.")
|
|
4572
|
-
.requiredOption("--sql <sql>", "Single SELECT, WITH, or EXPLAIN statement.")
|
|
4573
|
-
.option("--max-rows <n>", "Maximum rows to return. Defaults to 100; hard cap is 1000.")
|
|
4645
|
+
.requiredOption("--sql <sql>", "Single SELECT, WITH, or EXPLAIN statement."), DATABASE_QUERY_CONTRACT, DATABASE_QUERY_FLAG_OVERLAY)
|
|
4574
4646
|
.option("--json", "Print a JSON envelope.")
|
|
4575
4647
|
.action(async (options) => {
|
|
4576
4648
|
await handleAsyncAction("db query", options, () => {
|
|
4577
|
-
const maxRows = readPositiveInt(options.maxRows);
|
|
4578
4649
|
return requestOxygen("/api/cli/db/query", {
|
|
4579
4650
|
method: "POST",
|
|
4580
4651
|
body: {
|
|
4581
4652
|
sql: options.sql,
|
|
4582
|
-
...(
|
|
4653
|
+
...readContractFlags(options, DATABASE_QUERY_CONTRACT, DATABASE_QUERY_FLAG_OVERLAY),
|
|
4583
4654
|
},
|
|
4584
4655
|
});
|
|
4585
4656
|
});
|
|
@@ -4676,6 +4747,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4676
4747
|
.addCommand(new Command("setup")
|
|
4677
4748
|
.description("Configure the Recall-backed Oxygen Notetaker. Defaults to dry-run.")
|
|
4678
4749
|
.option("--bot-name <name>", "Meeting bot display name. Defaults to Oxygen Notetaker.")
|
|
4750
|
+
.option("--bot-avatar-url <url>", "Meeting bot avatar URL; pass an empty string to clear it.")
|
|
4751
|
+
.option("--consent-disclosure <text>", "Recording disclosure; pass an empty string to restore the default disclosure.")
|
|
4679
4752
|
.option("--disabled", "Save the notetaker as disabled when used with --live.")
|
|
4680
4753
|
.option("--default-max-minutes <n>", "Default maximum meeting minutes to reserve. Defaults to 60.")
|
|
4681
4754
|
.option("--default-max-credits <n>", "Default spend cap for live schedules.")
|
|
@@ -4771,6 +4844,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4771
4844
|
.option("--draft", "Create as a draft instead of scheduled.")
|
|
4772
4845
|
.option("--ugc-participation-id <id>", "Also enroll this personal post in one active creator participation. It is always saved as an unapproved draft; omit --approved.")
|
|
4773
4846
|
.option("--approved", "Mark an ordinary post approved for the scheduler. Invalid with --ugc-participation-id.")
|
|
4847
|
+
.option("--idempotency-key <key>", "Reuse one key when retrying the same post creation to avoid a duplicate.")
|
|
4848
|
+
.option("--tags <csv>", "Workspace tags to attach to the new post.")
|
|
4774
4849
|
.option("--thread-post <text>", "X: text of the next post in a thread (posts 2, 3, ... in order). Repeatable.", collectRepeatable, [])
|
|
4775
4850
|
.option("--thread-media <position=media_id>", "X: attach an uploaded media id to thread post N (2 or later), e.g. 2=<media_id>. Repeatable.", collectRepeatable, [])
|
|
4776
4851
|
.option("--poll-option <text>", "X: a poll option on the first post (2-4 options, 25 characters each). Repeatable.", collectRepeatable, [])
|
|
@@ -4909,6 +4984,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4909
4984
|
.addCommand(new Command("boost")
|
|
4910
4985
|
.description("Boost ONE post: your teammates' connected accounts react to and comment on it, once. These are REAL public LinkedIn writes from their accounts and they SPEND CREDITS — refused (exit 7) without --approved, --max-credits and --acknowledge-risk. Creates a post-scoped policy that is already enabled and plans its actions immediately. For a standing rule across many posts, use `publishing amplification create`.")
|
|
4911
4986
|
.argument("<post_id>", "Scheduled post id.")
|
|
4987
|
+
.option("--name <name>", "Name the post-scoped amplification policy.")
|
|
4912
4988
|
.requiredOption("--senders <ids>", "Comma-separated connected sender account ids that will engage.")
|
|
4913
4989
|
.requiredOption("--actions <list>", "Comma-separated actions: reaction, comment.")
|
|
4914
4990
|
.option("--max-actions-per-post <n>", "Hard cap on how many actions are planned (at most 12).")
|
|
@@ -4972,12 +5048,13 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4972
5048
|
});
|
|
4973
5049
|
}))
|
|
4974
5050
|
.addCommand(new Command("resolve-note")
|
|
4975
|
-
.description("
|
|
5051
|
+
.description("Resolve a review note, or reopen it.")
|
|
4976
5052
|
.argument("<post_id>", "Scheduled post id.")
|
|
4977
5053
|
.requiredOption("--comment <comment_id>", "Note id from `publishing posts notes`.")
|
|
5054
|
+
.option("--reopen", "Reopen the review note instead of resolving it.")
|
|
4978
5055
|
.option("--json", "Print a JSON envelope.")
|
|
4979
5056
|
.action(async (postId, options) => {
|
|
4980
|
-
await handleAsyncAction("publishing posts resolve-note", options, () => requestOxygen(`/api/cli/publishing/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(options.comment)}/resolve`, { method: "POST" }));
|
|
5057
|
+
await handleAsyncAction("publishing posts resolve-note", options, () => requestOxygen(`/api/cli/publishing/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(options.comment)}/resolve`, { method: "POST", body: { resolved: options.reopen !== true } }));
|
|
4981
5058
|
})))
|
|
4982
5059
|
.addCommand(new Command("drafts")
|
|
4983
5060
|
.description("Review the AI post-draft queue before anything reaches the publish queue.")
|
|
@@ -5298,6 +5375,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5298
5375
|
.option("--max-credits <n>", "Hard credit cap per cycle (required — this policy spends credits).")
|
|
5299
5376
|
.option("--comment-pool <text>", "A comment to draw from. Repeatable. Required when --actions includes comment.", collectRepeatable, [])
|
|
5300
5377
|
.option("--comment-pool-file <path>", "Read the comment pool from a file, one comment per line.")
|
|
5378
|
+
.option("--metadata-json <json-or-file>", "Policy metadata; {} clears it on update.")
|
|
5301
5379
|
.option("--reaction-delay-min <seconds>", "Lower bound of the randomized reaction delay.")
|
|
5302
5380
|
.option("--reaction-delay-max <seconds>", "Upper bound of the randomized reaction delay.")
|
|
5303
5381
|
.option("--comment-delay-min <seconds>", "Lower bound of the randomized comment delay.")
|
|
@@ -5328,6 +5406,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5328
5406
|
.option("--max-credits <n>", "Hard credit cap per cycle.")
|
|
5329
5407
|
.option("--comment-pool <text>", "A comment to draw from. Repeatable.", collectRepeatable, [])
|
|
5330
5408
|
.option("--comment-pool-file <path>", "Read the comment pool from a file, one comment per line.")
|
|
5409
|
+
.option("--metadata-json <json-or-file>", "Policy metadata; {} clears it on update.")
|
|
5331
5410
|
.option("--reaction-delay-min <seconds>", "Lower bound of the randomized reaction delay.")
|
|
5332
5411
|
.option("--reaction-delay-max <seconds>", "Upper bound of the randomized reaction delay.")
|
|
5333
5412
|
.option("--comment-delay-min <seconds>", "Lower bound of the randomized comment delay.")
|
|
@@ -5641,7 +5720,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5641
5720
|
.action(async (options) => {
|
|
5642
5721
|
await handleAsyncAction("crm photos", options, () => requestOxygen("/api/cli/crm/photos", {
|
|
5643
5722
|
method: "POST",
|
|
5644
|
-
body:
|
|
5723
|
+
body: readCrmPhotoFields(options),
|
|
5645
5724
|
}));
|
|
5646
5725
|
}))
|
|
5647
5726
|
.addCommand(new Command("fields")
|
|
@@ -5679,7 +5758,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5679
5758
|
.description("List configured CRM objects, or create/extend custom objects.")
|
|
5680
5759
|
.option("--json", "Print a JSON envelope.")
|
|
5681
5760
|
.action(async (options) => {
|
|
5682
|
-
await handleReadActionWithLens("crm objects", options, () => requestOxygen(
|
|
5761
|
+
await handleReadActionWithLens("crm objects", options, () => requestOxygen(buildCrmObjectsPath(options)), formatCrmObjects);
|
|
5683
5762
|
})
|
|
5684
5763
|
.addCommand(new Command("create")
|
|
5685
5764
|
.description("Create or repair a custom CRM object. Defaults to dry-run.")
|
|
@@ -5807,14 +5886,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5807
5886
|
.option("--json", "Print a JSON envelope.")
|
|
5808
5887
|
.action(async (object, rowId, options) => {
|
|
5809
5888
|
await handleAsyncAction("crm connections", options, () => {
|
|
5810
|
-
|
|
5811
|
-
if (options.conversationLimit)
|
|
5812
|
-
params.set("conversation_limit", options.conversationLimit);
|
|
5813
|
-
if (options.sequenceLimit)
|
|
5814
|
-
params.set("sequence_limit", options.sequenceLimit);
|
|
5815
|
-
const query = params.toString();
|
|
5816
|
-
const base = `/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(rowId)}/connections`;
|
|
5817
|
-
return requestOxygen(query ? `${base}?${query}` : base);
|
|
5889
|
+
return requestOxygen(buildCrmConnectionsPath(object, rowId, options));
|
|
5818
5890
|
});
|
|
5819
5891
|
}))
|
|
5820
5892
|
.addCommand(new Command("files")
|
|
@@ -5855,9 +5927,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5855
5927
|
},
|
|
5856
5928
|
});
|
|
5857
5929
|
const uploadUrl = readRecordString(ticket, "upload_url");
|
|
5858
|
-
const
|
|
5859
|
-
|
|
5860
|
-
|
|
5930
|
+
const fileId = isRecord(ticket.file) ? readRecordString(ticket.file, "id") : null;
|
|
5931
|
+
const reservedRowId = isRecord(ticket.record) ? readRecordString(ticket.record, "row_id") : null;
|
|
5932
|
+
if (!uploadUrl || !fileId || !reservedRowId) {
|
|
5933
|
+
throw new OxygenError("attachment_upload_failed", "The server did not return an upload URL, file ID and resolved record ID for that file.", { exitCode: 1 });
|
|
5861
5934
|
}
|
|
5862
5935
|
const controller = new AbortController();
|
|
5863
5936
|
const timer = setTimeout(() => controller.abort(), 300_000);
|
|
@@ -5876,9 +5949,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5876
5949
|
if (!put.ok) {
|
|
5877
5950
|
throw new OxygenError("attachment_upload_failed", `Object storage rejected the upload (${put.status}).`, { exitCode: 1 });
|
|
5878
5951
|
}
|
|
5879
|
-
return requestOxygen(
|
|
5952
|
+
return requestOxygen(`/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(reservedRowId)}/files/${encodeURIComponent(fileId)}/uploaded`, {
|
|
5880
5953
|
method: "POST",
|
|
5881
|
-
body:
|
|
5954
|
+
body: readCrmFileLabel(options),
|
|
5882
5955
|
});
|
|
5883
5956
|
});
|
|
5884
5957
|
}))
|
|
@@ -6018,16 +6091,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6018
6091
|
.option("--json", "Print a JSON envelope.")
|
|
6019
6092
|
.action(async (noteId, options) => {
|
|
6020
6093
|
await handleAsyncAction("crm notes update", options, () => {
|
|
6021
|
-
// Absent --title leaves it; --title "" clears it.
|
|
6022
|
-
const title = readClearableOption(options.title);
|
|
6023
6094
|
return requestOxygen(`/api/cli/crm/notes/${encodeURIComponent(noteId)}`, {
|
|
6024
6095
|
method: "PATCH",
|
|
6025
|
-
body:
|
|
6026
|
-
...(readOption(options.body) ? { body: readOption(options.body) } : {}),
|
|
6027
|
-
...(title !== undefined ? { title } : {}),
|
|
6028
|
-
...(options.pin ? { pinned: true } : {}),
|
|
6029
|
-
...(options.unpin ? { pinned: false } : {}),
|
|
6030
|
-
},
|
|
6096
|
+
body: readCrmNoteEditFields(options),
|
|
6031
6097
|
});
|
|
6032
6098
|
});
|
|
6033
6099
|
}))
|
|
@@ -6314,6 +6380,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6314
6380
|
.description("Run one bidirectional reconciliation cycle for a configured CRM object pair as a durable workflow run. Defaults to dry-run.")
|
|
6315
6381
|
.requiredOption("--provider <provider>", "Connected CRM: hubspot or attio.")
|
|
6316
6382
|
.requiredOption("--object <object>", "Provider object: hubspot contacts|companies, or attio people|companies.")
|
|
6383
|
+
.option("--direction <direction>", "Override this cycle's direction: inbound, outbound, or bidirectional.")
|
|
6384
|
+
.option("--since <iso>", "Read provider changes since this watermark.")
|
|
6385
|
+
.option("--config-id <id>", "Use this standing CRM sync configuration for the cycle.")
|
|
6317
6386
|
.option("--max-rows <n>", "Per-cycle row cap. The real volume brake on provider writes (BYOK writes cost 0 Oxygen credits).")
|
|
6318
6387
|
.option("--max-credits <n>", "Managed-credit + external-quota cap for this cycle.")
|
|
6319
6388
|
.option("--approved", "Confirm a live cycle after inspecting a dry run.")
|
|
@@ -6380,12 +6449,14 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6380
6449
|
})))
|
|
6381
6450
|
.addCommand(new Command("enrichment")
|
|
6382
6451
|
.description("Choose which named enrichments run automatically on new CRM records. The saved set is a STANDING PERMISSION: every record written to the object (imports, inserts, upserts, and `crm assert`) enriches without per-record approval, bounded by the per-batch credit ceiling. Same storage as `oxygen tables auto-run`, in founder words.")
|
|
6383
|
-
.addCommand(new Command("list")
|
|
6452
|
+
.addCommand(addContractFlags(new Command("list")
|
|
6384
6453
|
.description("Show the enrichments available per CRM object: which are armed, what each costs per record, the per-record total, the per-batch credit ceiling, how much of the column allowance is spent, and how many records already in the object have never been enriched. Read-only, free.")
|
|
6385
|
-
.option("--
|
|
6386
|
-
.option("--json", "Print a JSON envelope.")
|
|
6454
|
+
.option("--json", "Print a JSON envelope."), CRM_ENRICHMENT_READ_CONTRACT, CRM_ENRICHMENT_READ_FLAGS)
|
|
6387
6455
|
.action(async (options) => {
|
|
6388
|
-
|
|
6456
|
+
if (options.projection === "summary" || options.projection === "value")
|
|
6457
|
+
await handleAsyncAction("crm enrichment list", options, () => requestOxygen(buildCrmEnrichmentListPath(options)));
|
|
6458
|
+
else
|
|
6459
|
+
await handleCrmEnrichmentAction("crm enrichment list", options, () => requestOxygen(buildCrmEnrichmentListPath(options)));
|
|
6389
6460
|
}))
|
|
6390
6461
|
.addCommand(new Command("backfill")
|
|
6391
6462
|
.description("Enrich the records you ALREADY have. Arming an enrichment only covers records written from that moment on, so the companies and people already in your CRM stay blank until you run this. Previews for free by default: what is still missing, what one batch costs, and what finishing costs. Add --live --approved --max-credits to actually run a batch; repeat it and the next batch picks up where the last one stopped.")
|
|
@@ -6552,6 +6623,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6552
6623
|
.argument("[prompt]", "Prompt text or @file (same as --prompt); requires --family. The --prompt flag wins if both are given.")
|
|
6553
6624
|
.option("--prompt <text-or-file>", "Signal-sourcing prompt, or a path to a prompt file. Requires --family.")
|
|
6554
6625
|
.option("--plan-json <json-or-file>", "Plan JSON returned by signals search plan, or a path to a JSON file.")
|
|
6626
|
+
.option("--source <linkedin_posts>", "Run the native post-search lane using --filters-json or --keywords; no prompt needed.")
|
|
6555
6627
|
.option("--family <family>", "Signal family when planning from --prompt: hiring, tech, funding, acquisition, news, job_change, or linkedin_posts.")
|
|
6556
6628
|
.option("--scope <scope>", "market or watch_list when planning from --prompt.")
|
|
6557
6629
|
.option("--route-id <id>", "Route id from the plan to execute.")
|
|
@@ -6822,7 +6894,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6822
6894
|
});
|
|
6823
6895
|
})))
|
|
6824
6896
|
.addCommand(new Command("create")
|
|
6825
|
-
.description("Create a
|
|
6897
|
+
.description("Create a table private to the active workspace. Free — 0 Oxygen credits. To create a table and import a file in one step, use `tables import --create <name>`.")
|
|
6826
6898
|
.argument("<name>", "Display name for the table.")
|
|
6827
6899
|
.requiredOption("--columns-json <json>", 'Required nonempty JSON array of column definitions, e.g. [{"key":"name","label":"Name","dataType":"text"},{"key":"domain","label":"Domain","dataType":"text"}]. Inspect an existing shape with `oxygen tables describe <table>`.')
|
|
6828
6900
|
.option("--project <project>", "Project id or slug. Defaults to General.")
|
|
@@ -6844,10 +6916,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6844
6916
|
.option("--schema-only", "Duplicate columns and definitions without copying row values.")
|
|
6845
6917
|
.option("--json", "Print a JSON envelope.")
|
|
6846
6918
|
.action(async (table, options) => {
|
|
6847
|
-
await handleAsyncAction("tables duplicate", options, () => requestOxygen(
|
|
6919
|
+
await handleAsyncAction("tables duplicate", options, () => requestOxygen(`/api/cli/tables/${encodeURIComponent(table)}/duplicate`, {
|
|
6848
6920
|
method: "POST",
|
|
6849
6921
|
body: {
|
|
6850
|
-
table,
|
|
6851
6922
|
...(readOption(options.name) ? { name: readOption(options.name) } : {}),
|
|
6852
6923
|
...(options.schemaOnly ? { include_rows: false } : {}),
|
|
6853
6924
|
},
|
|
@@ -6886,38 +6957,50 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6886
6957
|
}));
|
|
6887
6958
|
}))
|
|
6888
6959
|
.addCommand(new Command("delete-row")
|
|
6889
|
-
.description("Delete one workspace table row.")
|
|
6960
|
+
.description("Delete one workspace table row. A lead enrolled in a campaign stays enrolled; the response names those campaigns.")
|
|
6890
6961
|
.argument("<table>", "Table id or slug.")
|
|
6891
6962
|
.argument("<row_id>", "Workspace row UUID.")
|
|
6963
|
+
.option("--metadata-json <json-or-file>", "Durable row deletion provenance.")
|
|
6892
6964
|
.option("--json", "Print a JSON envelope.")
|
|
6893
6965
|
.action(async (table, rowId, options) => {
|
|
6894
|
-
await handleAsyncAction("tables delete-row", options, () =>
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6966
|
+
await handleAsyncAction("tables delete-row", options, async () => {
|
|
6967
|
+
const data = await requestOxygen("/api/cli/tables/rows/delete", {
|
|
6968
|
+
method: "POST",
|
|
6969
|
+
body: {
|
|
6970
|
+
table,
|
|
6971
|
+
row_id: rowId,
|
|
6972
|
+
...(options.metadataJson !== undefined ? { metadata: parseJsonObject(readFileIfPresent(options.metadataJson)) } : {}),
|
|
6973
|
+
},
|
|
6974
|
+
});
|
|
6975
|
+
if (!options.json)
|
|
6976
|
+
writeRowDeleteWarnings(data);
|
|
6977
|
+
return data;
|
|
6978
|
+
});
|
|
6901
6979
|
}))
|
|
6902
6980
|
.addCommand(new Command("delete-rows")
|
|
6903
|
-
.description("Delete multiple workspace table rows.")
|
|
6981
|
+
.description("Delete multiple workspace table rows. Leads enrolled in a campaign stay enrolled; the response names those campaigns.")
|
|
6904
6982
|
.argument("<table>", "Table id or slug.")
|
|
6905
6983
|
.option("--row-ids-json <json>", "JSON array of workspace row UUIDs.")
|
|
6906
6984
|
.option("--row-ids-file <path>", "File containing row UUIDs or rows with a _row_id column.")
|
|
6907
6985
|
.option("--row-id-column <key>", "Column to read from --row-ids-file. Defaults to _row_id.")
|
|
6908
6986
|
.option("--format <format>", "File format for --row-ids-file: json, jsonl, csv, or xlsx.")
|
|
6909
6987
|
.option("--sheet <name>", "Worksheet name when reading row IDs from an XLSX file.")
|
|
6988
|
+
.option("--metadata-json <json-or-file>", "Durable row deletion provenance.")
|
|
6910
6989
|
.option("--json", "Print a JSON envelope.")
|
|
6911
6990
|
.action(async (table, options) => {
|
|
6912
6991
|
await handleAsyncAction("tables delete-rows", options, async () => {
|
|
6913
6992
|
const rowIds = await readDeleteRowIdsOption(options);
|
|
6914
|
-
|
|
6993
|
+
const data = await requestOxygen("/api/cli/tables/rows/delete", {
|
|
6915
6994
|
method: "POST",
|
|
6916
6995
|
body: {
|
|
6917
6996
|
table,
|
|
6918
6997
|
row_ids: rowIds,
|
|
6998
|
+
...(options.metadataJson !== undefined ? { metadata: parseJsonObject(readFileIfPresent(options.metadataJson)) } : {}),
|
|
6919
6999
|
},
|
|
6920
7000
|
});
|
|
7001
|
+
if (!options.json)
|
|
7002
|
+
writeRowDeleteWarnings(data);
|
|
7003
|
+
return data;
|
|
6921
7004
|
});
|
|
6922
7005
|
}))
|
|
6923
7006
|
.addCommand(new Command("upsert")
|
|
@@ -6967,6 +7050,19 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6967
7050
|
.option("--limit <n>", "Maximum rows to export. Defaults to 100; hard cap is 1000.")
|
|
6968
7051
|
.option("--json", "Print a JSON envelope.")
|
|
6969
7052
|
.action(async (table, options) => {
|
|
7053
|
+
if (!options.json && !options.output && options.format?.trim().toLowerCase() === "table") {
|
|
7054
|
+
// The human-readable table itself, not a JSON object holding it as one
|
|
7055
|
+
// escaped string (blind user evals, 2026-09-30). --json keeps the envelope.
|
|
7056
|
+
try {
|
|
7057
|
+
const { content } = await exportRows(table, options);
|
|
7058
|
+
const text = typeof content === "string" ? content : "";
|
|
7059
|
+
process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
7060
|
+
}
|
|
7061
|
+
catch (error) {
|
|
7062
|
+
emitCliFailure("tables export", error);
|
|
7063
|
+
}
|
|
7064
|
+
return;
|
|
7065
|
+
}
|
|
6970
7066
|
await handleAsyncAction("tables export", options, () => exportRows(table, options));
|
|
6971
7067
|
}))
|
|
6972
7068
|
.addCommand(new Command("export-bundle")
|
|
@@ -7024,8 +7120,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7024
7120
|
return data;
|
|
7025
7121
|
});
|
|
7026
7122
|
}))
|
|
7027
|
-
.addCommand(new Command("query")
|
|
7028
|
-
|
|
7123
|
+
.addCommand(addContractFlags(new Command("query"), TABLE_QUERY_GRID_CONTRACT, {
|
|
7124
|
+
format: { valueName: "<format>", help: "Read the web grid projection ('grid'): defaults to 50 rows, capped at 200, with table, limit, cursor and optional --include-stats/--include-fill. Omit for a filtered row query." },
|
|
7125
|
+
})
|
|
7126
|
+
.description("Query raw table rows or use --format grid for display previews, cell state and native cursors. Output is a JSON envelope; for a typed rendering of raw rows use `tables export <table> --format table`. Grid takes table/limit/cursor; rowCountMode labels registry/exact/skipped. Optional stats/fill may be unavailable.")
|
|
7029
7127
|
.argument("<table>", "Table id or slug.")
|
|
7030
7128
|
.option("--limit <n>", "Maximum rows to return. Defaults to 100; hard cap is 1000.")
|
|
7031
7129
|
.option("--cursor <cursor>", "Pagination cursor returned by a previous query. Walks forward one page at a time; mutually exclusive with --offset.")
|
|
@@ -7040,6 +7138,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7040
7138
|
.option("--json", "Print a JSON envelope.")
|
|
7041
7139
|
.action(async (table, options) => {
|
|
7042
7140
|
await handleAsyncAction("tables query", options, () => {
|
|
7141
|
+
const gridArgs = readContractFlags(options, TABLE_QUERY_GRID_CONTRACT);
|
|
7142
|
+
if (gridArgs.format !== "grid" && Object.keys(gridArgs).length > 0) {
|
|
7143
|
+
throw new OxygenError("invalid_request", "Grid options require --format grid.", { exitCode: 1 });
|
|
7144
|
+
}
|
|
7043
7145
|
const limit = readPositiveInt(options.limit);
|
|
7044
7146
|
const filters = readFilterJsonOption(options.filterJson);
|
|
7045
7147
|
const filterTree = readJsonObjectOption(options.filterTreeJson);
|
|
@@ -7055,10 +7157,15 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7055
7157
|
if (readOption(options.cursor) && offset !== undefined) {
|
|
7056
7158
|
throw new OxygenError("invalid_request", "Pass either --cursor or --offset, not both.", { exitCode: 1 });
|
|
7057
7159
|
}
|
|
7160
|
+
if (options.format === "grid" && (readCsvOption(options.fields).length > 0 || filters?.length || filterTree ||
|
|
7161
|
+
readOption(options.search) || formulaValues || sorts?.length || (offset !== undefined && offset > 0))) {
|
|
7162
|
+
throw new OxygenError("invalid_request", '--format grid takes only table, --limit, --cursor and optional --include-stats/--include-fill; omit --format for filters, fields, search, sorts or offsets.', { exitCode: 1 });
|
|
7163
|
+
}
|
|
7058
7164
|
return requestOxygen("/api/cli/tables/query", {
|
|
7059
7165
|
method: "POST",
|
|
7060
7166
|
body: {
|
|
7061
7167
|
table,
|
|
7168
|
+
...gridArgs,
|
|
7062
7169
|
...(limit ? { limit } : {}),
|
|
7063
7170
|
...(readOption(options.cursor) ? { cursor: readOption(options.cursor) } : {}),
|
|
7064
7171
|
...(offset !== undefined ? { offset } : {}),
|
|
@@ -7165,9 +7272,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7165
7272
|
.argument("<table>", "Table id or slug.")
|
|
7166
7273
|
.option("--json", "Print a JSON envelope.")
|
|
7167
7274
|
.action(async (table, options) => {
|
|
7168
|
-
await handleAsyncAction("tables archive", options, () => requestOxygen(
|
|
7169
|
-
method: "
|
|
7170
|
-
body: { table },
|
|
7275
|
+
await handleAsyncAction("tables archive", options, () => requestOxygen(`/api/cli/tables/${encodeURIComponent(table)}`, {
|
|
7276
|
+
method: "DELETE",
|
|
7171
7277
|
}));
|
|
7172
7278
|
}))
|
|
7173
7279
|
.addCommand(new Command("delete")
|
|
@@ -7201,13 +7307,19 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7201
7307
|
}))
|
|
7202
7308
|
.addCommand(new Command("restore")
|
|
7203
7309
|
.description("Restore an archived or delete-scheduled table: cancels the purge timer and reclaims its slug.")
|
|
7204
|
-
.argument("
|
|
7310
|
+
.argument("[table]", "Table id or slug (the pre-delete slug works).")
|
|
7311
|
+
.option("--tables <refs>", "Restore a comma-separated batch; alternative to the table argument.")
|
|
7205
7312
|
.option("--json", "Print a JSON envelope.")
|
|
7206
7313
|
.action(async (table, options) => {
|
|
7207
|
-
await handleAsyncAction("tables restore", options, () =>
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7314
|
+
await handleAsyncAction("tables restore", options, () => {
|
|
7315
|
+
const refs = splitCommaList(options.tables);
|
|
7316
|
+
if ((!table && refs.length === 0) || (table && refs.length > 0))
|
|
7317
|
+
throw new OxygenError("invalid_selection", "Pass one table argument or --tables, not both.", { exitCode: 2 });
|
|
7318
|
+
return requestOxygen("/api/cli/tables/restore", {
|
|
7319
|
+
method: "POST",
|
|
7320
|
+
body: { ...(options.tables !== undefined ? { tables: splitCommaList(options.tables) } : { table }) },
|
|
7321
|
+
});
|
|
7322
|
+
});
|
|
7211
7323
|
}))
|
|
7212
7324
|
.addCommand(new Command("move")
|
|
7213
7325
|
.description("Move one or more workspace tables into a project (folder) in one call. Rows, columns, runs, and provenance are preserved. Free; reverse it by moving the table back.")
|
|
@@ -7749,7 +7861,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7749
7861
|
.description("List a table's saved views.")
|
|
7750
7862
|
.argument("<table>", "Table id or slug.")
|
|
7751
7863
|
.option("--json", "Print a JSON envelope.")
|
|
7752
|
-
.action((table, options) => handleAsyncAction("tables views list", options, () => requestOxygen(`/api/cli/tables
|
|
7864
|
+
.action((table, options) => handleAsyncAction("tables views list", options, () => requestOxygen(`/api/cli/tables/${encodeURIComponent(table)}/views`))))
|
|
7753
7865
|
.addCommand(new Command("get")
|
|
7754
7866
|
.description("Get one saved view by id.")
|
|
7755
7867
|
.argument("<table>", "Table id or slug.")
|
|
@@ -7764,18 +7876,20 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7764
7876
|
.option("--group-by <column>", "Kanban: the editable select/status column key the board splits on, e.g. --group-by lifecycle_stage. Unknown or non-groupable keys are refused with the groupable candidates. The response's effective_group_by is the column the board actually uses.")
|
|
7765
7877
|
.option("--config <json>", TABLE_VIEW_CONFIG_HELP)
|
|
7766
7878
|
.option("--default", "Make this the table's default view.")
|
|
7879
|
+
.option("--no-default", "Clear this view's default status.")
|
|
7880
|
+
.option("--position <n>", "Zero-based order among the table's saved views.")
|
|
7767
7881
|
.option("--json", "Print a JSON envelope.")
|
|
7768
|
-
.action((table, options) => handleAsyncAction("tables views create", options, () => requestOxygen(
|
|
7882
|
+
.action((table, options) => handleAsyncAction("tables views create", options, () => requestOxygen(`/api/cli/tables/${encodeURIComponent(table)}/views`, {
|
|
7769
7883
|
method: "POST",
|
|
7770
7884
|
body: {
|
|
7771
|
-
table,
|
|
7772
7885
|
name: readOption(options.name),
|
|
7773
7886
|
...(readOption(options.viewType) ? { view_type: readOption(options.viewType) } : {}),
|
|
7774
7887
|
...(readOption(options.groupBy) ? { group_by: readOption(options.groupBy) } : {}),
|
|
7775
7888
|
...(readOption(options.config)
|
|
7776
7889
|
? { config: parseJsonValue(readOption(options.config) ?? "", "--config") }
|
|
7777
7890
|
: {}),
|
|
7778
|
-
...(options.default ? { is_default:
|
|
7891
|
+
...(typeof options.default === "boolean" ? { is_default: options.default } : {}),
|
|
7892
|
+
...(readOption(options.position) !== null ? { position: readNonNegativeInt(options.position) } : {}),
|
|
7779
7893
|
},
|
|
7780
7894
|
}))))
|
|
7781
7895
|
.addCommand(new Command("update")
|
|
@@ -7787,20 +7901,19 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7787
7901
|
.option("--group-by <column>", "Kanban: change the select/status column the board splits on; folds into the stored config and keeps the rest of it. Refused for a grid view unless --view-type kanban is passed with it.")
|
|
7788
7902
|
.option("--config <json>", `${TABLE_VIEW_CONFIG_HELP} Replaces the stored config.`)
|
|
7789
7903
|
.option("--default", "Make this the table's default view.")
|
|
7904
|
+
.option("--no-default", "Clear this view's default status.")
|
|
7790
7905
|
.option("--position <n>", "0-based order among the table's views.")
|
|
7791
7906
|
.option("--json", "Print a JSON envelope.")
|
|
7792
|
-
.action((table, view, options) => handleAsyncAction("tables views update", options, () => requestOxygen(
|
|
7907
|
+
.action((table, view, options) => handleAsyncAction("tables views update", options, () => requestOxygen(`/api/cli/tables/${encodeURIComponent(table)}/views/${encodeURIComponent(view)}`, {
|
|
7793
7908
|
method: "PATCH",
|
|
7794
7909
|
body: {
|
|
7795
|
-
table,
|
|
7796
|
-
view,
|
|
7797
7910
|
...(readOption(options.name) ? { name: readOption(options.name) } : {}),
|
|
7798
7911
|
...(readOption(options.viewType) ? { view_type: readOption(options.viewType) } : {}),
|
|
7799
7912
|
...(readOption(options.groupBy) ? { group_by: readOption(options.groupBy) } : {}),
|
|
7800
7913
|
...(readOption(options.config)
|
|
7801
7914
|
? { config: parseJsonValue(readOption(options.config) ?? "", "--config") }
|
|
7802
7915
|
: {}),
|
|
7803
|
-
...(options.default ? { is_default:
|
|
7916
|
+
...(typeof options.default === "boolean" ? { is_default: options.default } : {}),
|
|
7804
7917
|
...(readOption(options.position) ? { position: Number(readOption(options.position)) } : {}),
|
|
7805
7918
|
},
|
|
7806
7919
|
}))))
|
|
@@ -7809,7 +7922,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7809
7922
|
.argument("<table>", "Table id or slug.")
|
|
7810
7923
|
.argument("<view>", "View id.")
|
|
7811
7924
|
.option("--json", "Print a JSON envelope.")
|
|
7812
|
-
.action((table, view, options) => handleAsyncAction("tables views delete", options, () => requestOxygen(`/api/cli/tables
|
|
7925
|
+
.action((table, view, options) => handleAsyncAction("tables views delete", options, () => requestOxygen(`/api/cli/tables/${encodeURIComponent(table)}/views/${encodeURIComponent(view)}`, { method: "DELETE" })))));
|
|
7813
7926
|
tablesCommand.addCommand(new Command("auto-run")
|
|
7814
7927
|
.description("Inspect and manage a table's standing auto-run configuration (columns queued automatically when new rows are written).")
|
|
7815
7928
|
.addCommand(new Command("get")
|
|
@@ -7958,6 +8071,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7958
8071
|
.option("--schema-json <json-or-file>", "Bind a PUSH feed instead: the JSON Schema (an object with type, properties, required, additionalProperties) every delivered row must match, inline or a path to a JSON file.")
|
|
7959
8072
|
.option("--source <source>", `Bind a WEB INTENT push feed instead: ${FEED_BIND_WEB_INTENT_SOURCES.join(", ")} (rb2b = the website-visit reveals OXYGEN already receives for this workspace, also kept as website_visit Signals). Fixed row shape, no schema, no cadence, 0 credits per reveal. Takes a table or --create <name>, plus --name and --upsert-key.`)
|
|
7960
8073
|
.option("--create <name>", "Push or web-intent feed only. Create a new table with this name — its columns derived from --schema-json, or the fixed reveal columns for --source rb2b — instead of binding an existing table.")
|
|
8074
|
+
.option("--project <project>", "Workspace project id or slug for the new --create table.")
|
|
7961
8075
|
.option("--items-path <path>", "Push feed only. Dot path to an array of row objects in the delivered payload.")
|
|
7962
8076
|
.option("--event-id-path <path>", "Push feed only. Dot path to the event id. Defaults to event_id, eventId, id, or a payload hash.")
|
|
7963
8077
|
.option("--event-type-path <path>", "Push feed only. Dot path to the event type. Defaults to type or event.")
|
|
@@ -7978,6 +8092,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7978
8092
|
.option("--intent-id <id>", "Routing-registry intent id from the plan.")
|
|
7979
8093
|
.option("--input-shape <shape>", "Routing-registry input shape from the plan, e.g. market or watch_list.")
|
|
7980
8094
|
.option("--route-id <id>", "Route id from the plan this feed was bound from.")
|
|
8095
|
+
.option("--registry-version <version>", "Routing-registry revision from the plan, retained with the frozen feed provenance.")
|
|
7981
8096
|
.option("--source-prompt <text-or-file>", "The prompt this feed was planned from, or a path to a prompt file.")
|
|
7982
8097
|
.option("--approved", "Approve the recurring paid pull. Required with --every and --max-credits on a paid provider.")
|
|
7983
8098
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -8312,15 +8427,12 @@ Examples:
|
|
|
8312
8427
|
return data;
|
|
8313
8428
|
});
|
|
8314
8429
|
}))
|
|
8315
|
-
.addCommand(new Command("get")
|
|
8430
|
+
.addCommand(addContractFlags(new Command("get")
|
|
8316
8431
|
.description("Read one knowledge wiki page by slug or UUID. Read-only, 0 credits.")
|
|
8317
8432
|
.argument("<slug_or_id>", "Page slug or UUID.")
|
|
8318
|
-
.option("--json", "Print a JSON envelope.")
|
|
8433
|
+
.option("--json", "Print a JSON envelope."), knowledgePageReadContract, { slug: { omit: true }, id: { omit: true } })
|
|
8319
8434
|
.action(async (slugOrId, options) => {
|
|
8320
|
-
await handleAsyncAction("knowledge page get", options, () => requestOxygen(
|
|
8321
|
-
method: "POST",
|
|
8322
|
-
body: { slug: slugOrId },
|
|
8323
|
-
}));
|
|
8435
|
+
await handleAsyncAction("knowledge page get", options, () => requestOxygen(`/api/cli/knowledge/pages/get?${new URLSearchParams(Object.entries({ slug: slugOrId, ...readContractFlags(options, knowledgePageReadContract, { slug: { omit: true }, id: { omit: true } }) }).map(([key, value]) => [key, String(value)]))}`));
|
|
8324
8436
|
}))
|
|
8325
8437
|
.addCommand(new Command("list")
|
|
8326
8438
|
.description("List knowledge wiki pages. Read-only, 0 credits.")
|
|
@@ -8423,11 +8535,11 @@ Examples:
|
|
|
8423
8535
|
body: buildKnowledgeSearchBody(query, options),
|
|
8424
8536
|
}));
|
|
8425
8537
|
}))
|
|
8426
|
-
.addCommand(new Command("index")
|
|
8538
|
+
.addCommand(addContractFlags(new Command("index")
|
|
8427
8539
|
.description("Compact wiki index: each page's slug, one-liner, tags, link degree, plus type/status counts over the whole wiki. Lists the 200 most recently updated pages by default. Read-only, 0 credits. semantic.budget is Oxygen's platform-funded embedding safety allowance; its dollar and query counters are Oxygen's spend, never charges against your credits.")
|
|
8428
8540
|
.option("--limit <n>", "Maximum pages to list. Defaults to 200; hard cap is 1000. Counts always cover the whole wiki.")
|
|
8429
8541
|
.option("--all", "List every page instead of the most recently updated window.")
|
|
8430
|
-
.option("--json", "Print a JSON envelope.")
|
|
8542
|
+
.option("--json", "Print a JSON envelope."), knowledgeIndexReadContract, { limit: { omit: true } })
|
|
8431
8543
|
.action(async (options) => {
|
|
8432
8544
|
await handleAsyncAction("knowledge index", options, async () => {
|
|
8433
8545
|
const params = new URLSearchParams();
|
|
@@ -8436,6 +8548,8 @@ Examples:
|
|
|
8436
8548
|
params.set("limit", String(limit));
|
|
8437
8549
|
if (options.all)
|
|
8438
8550
|
params.set("all", "true");
|
|
8551
|
+
for (const [key, value] of Object.entries(readContractFlags(options, knowledgeIndexReadContract, { limit: { omit: true } })))
|
|
8552
|
+
params.set(key, String(value));
|
|
8439
8553
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
8440
8554
|
const data = await requestOxygen(`/api/cli/knowledge/index${qs}`);
|
|
8441
8555
|
if (!options.json)
|
|
@@ -9377,11 +9491,12 @@ Examples:
|
|
|
9377
9491
|
.alias("get")
|
|
9378
9492
|
.description("Show one recipe: its kit, the install status of each stage here, prerequisites, credits, approval gates, and the playbook body. Alias: `get`.")
|
|
9379
9493
|
.argument("<slug>", "Recipe slug, e.g. outbound-pilot-50.")
|
|
9494
|
+
.option("--section <heading>", "Read one recipe heading instead of the whole playbook.")
|
|
9380
9495
|
.option("--json", "Print a JSON envelope.")
|
|
9381
9496
|
.action(async (slug, options) => {
|
|
9382
9497
|
await handleAsyncAction("recipes show", options, () => requestOxygen("/api/cli/recipes/get", {
|
|
9383
9498
|
method: "POST",
|
|
9384
|
-
body: { slug },
|
|
9499
|
+
body: { slug, ...(readOption(options.section) ? { section: readOption(options.section) } : {}) },
|
|
9385
9500
|
}));
|
|
9386
9501
|
}))
|
|
9387
9502
|
.addCommand(new Command("install")
|
|
@@ -9808,6 +9923,7 @@ Examples:
|
|
|
9808
9923
|
.description("Run an executable AI, tool, formula, enrichment, bind, lookup, or custom HTTP column for one row or a bounded batch. Paid server-side columns always run durably in the background. Bind create-mode (onNoMatch=create) needs --approved.")
|
|
9809
9924
|
.argument("<table>", "Table id or slug.")
|
|
9810
9925
|
.argument("<column>", "Column id or key.")
|
|
9926
|
+
.option("--idempotency-key <key>", "Replay key for durable server-side run admission.")
|
|
9811
9927
|
.option("--row-id <row_id>", "Workspace row id to run. Get one from `oxygen tables query <table> --limit 1 --json` — the field is `_row_id`, not `id`.")
|
|
9812
9928
|
.option("--limit <n>", "Run the next N rows whose target cell is still empty (--force runs the first N regardless). Repeat until rowCount is 0 to page through a table. Defaults to 10; inline deterministic runs have a hard cap of 25.")
|
|
9813
9929
|
.option("--all", "Run all rows. Requires --background, except with --dry-run, which previews the background run without queueing it.")
|
|
@@ -9883,6 +9999,7 @@ Examples:
|
|
|
9883
9999
|
});
|
|
9884
10000
|
}
|
|
9885
10001
|
const body = {
|
|
10002
|
+
...(readOption(options.idempotencyKey) ? { idempotency_key: readOption(options.idempotencyKey) } : {}),
|
|
9886
10003
|
table,
|
|
9887
10004
|
column,
|
|
9888
10005
|
...(options.all ? { selection: { mode: "all" } } : {}),
|
|
@@ -9941,11 +10058,9 @@ Examples:
|
|
|
9941
10058
|
.action(async (table, sourceColumn, options) => {
|
|
9942
10059
|
const mappings = options.mappingsJson ? parseJsonArray(options.mappingsJson) : undefined;
|
|
9943
10060
|
const preset = readOption(options.preset);
|
|
9944
|
-
await handleAsyncAction("columns materialize", options, () => requestOxygen(
|
|
10061
|
+
await handleAsyncAction("columns materialize", options, () => requestOxygen(`/api/cli/tables/${encodeURIComponent(table)}/columns/${encodeURIComponent(sourceColumn)}/materialize`, {
|
|
9945
10062
|
method: "POST",
|
|
9946
10063
|
body: {
|
|
9947
|
-
table,
|
|
9948
|
-
source_column: sourceColumn,
|
|
9949
10064
|
...(mappings ? { mappings } : {}),
|
|
9950
10065
|
...(!mappings ? { preset: preset ?? "work_email" } : preset ? { preset } : {}),
|
|
9951
10066
|
create_if_missing: options.createIfMissing !== false,
|
|
@@ -9954,10 +10069,10 @@ Examples:
|
|
|
9954
10069
|
}));
|
|
9955
10070
|
}))
|
|
9956
10071
|
.addCommand(new Command("rename")
|
|
9957
|
-
.description("Rename a workspace column key
|
|
10072
|
+
.description("Rename a workspace column's stable key (requires --key), optionally updating its display label. Free (0 credits). For a label-only change, use columns update <table> <column> --label <label>. Warns when another column reads the old key; a rename does not rewrite those references.")
|
|
9958
10073
|
.argument("<table>", "Table id or slug.")
|
|
9959
10074
|
.argument("<column>", "Column id or key.")
|
|
9960
|
-
.requiredOption("--key <key>", "
|
|
10075
|
+
.requiredOption("--key <key>", "Required new stable column key.")
|
|
9961
10076
|
.option("--label <label>", "Optional new display label.")
|
|
9962
10077
|
.option("--json", "Print a JSON envelope.")
|
|
9963
10078
|
.action(async (table, column, options) => {
|
|
@@ -9982,6 +10097,8 @@ Examples:
|
|
|
9982
10097
|
.argument("<column>", "Column id or key.")
|
|
9983
10098
|
.option("--label <label>", "New display label.")
|
|
9984
10099
|
.option("--semantic-type <type>", "New semantic type.")
|
|
10100
|
+
.option("--clear-semantic-type", "Clear the column's semantic type.")
|
|
10101
|
+
.option("--position <n>", "Move the column to this zero-based position.")
|
|
9985
10102
|
.option("--definition-json <json>", "Definition metadata to shallow-merge into the column; arrays are replaced wholesale and missing keys leave existing fields unchanged.")
|
|
9986
10103
|
.option("--definition-unset <keys>", "Comma-separated definition keys to REMOVE after the merge (e.g. outputSchema,model) — a merge can only add or overwrite. Lifecycle and provenance keys are rejected.")
|
|
9987
10104
|
.option("--data-type <type>", "New output data type for a lookup column (text, numeric, boolean, jsonb, timestamptz). Discards the cached cells; other column kinds use `oxygen columns retype`.")
|
|
@@ -10061,7 +10178,8 @@ Examples:
|
|
|
10061
10178
|
column,
|
|
10062
10179
|
...(researchDepth ? { research_depth: researchDepth } : {}),
|
|
10063
10180
|
...(readOption(options.label) ? { label: readOption(options.label) } : {}),
|
|
10064
|
-
...(readOption(options.semanticType) ? { semantic_type: readOption(options.semanticType) } : {}),
|
|
10181
|
+
...(options.clearSemanticType ? { semantic_type: null } : readOption(options.semanticType) ? { semantic_type: readOption(options.semanticType) } : {}),
|
|
10182
|
+
...(readOption(options.position) !== null ? { position: readNonNegativeInt(options.position) } : {}),
|
|
10065
10183
|
...(definition ? { definition } : {}),
|
|
10066
10184
|
...(definitionUnset.length > 0 ? { definition_unset: definitionUnset } : {}),
|
|
10067
10185
|
...(readOption(options.dataType) ? { data_type: readOption(options.dataType) } : {}),
|
|
@@ -10116,11 +10234,9 @@ Examples:
|
|
|
10116
10234
|
.option("--dry-run", "Preview the conversion (row counts and samples) without writing.")
|
|
10117
10235
|
.option("--json", "Print a JSON envelope.")
|
|
10118
10236
|
.action(async (table, column, options) => {
|
|
10119
|
-
await handleAsyncAction("columns retype", options, () => requestOxygen(
|
|
10237
|
+
await handleAsyncAction("columns retype", options, () => requestOxygen(`/api/cli/tables/${encodeURIComponent(table)}/columns/${encodeURIComponent(column)}/retype`, {
|
|
10120
10238
|
method: "POST",
|
|
10121
10239
|
body: {
|
|
10122
|
-
table,
|
|
10123
|
-
column,
|
|
10124
10240
|
data_type: options.dataType,
|
|
10125
10241
|
...(options.dryRun ? { dry_run: true } : {}),
|
|
10126
10242
|
},
|
|
@@ -10132,9 +10248,9 @@ Examples:
|
|
|
10132
10248
|
.argument("<column>", "Column id or key.")
|
|
10133
10249
|
.option("--json", "Print a JSON envelope.")
|
|
10134
10250
|
.action(async (table, column, options) => {
|
|
10135
|
-
await handleAsyncAction("columns archive", options, () => requestOxygen(
|
|
10251
|
+
await handleAsyncAction("columns archive", options, () => requestOxygen(`/api/cli/tables/${encodeURIComponent(table)}/columns/${encodeURIComponent(column)}/archive`, {
|
|
10136
10252
|
method: "POST",
|
|
10137
|
-
body: {
|
|
10253
|
+
body: {},
|
|
10138
10254
|
}));
|
|
10139
10255
|
}))
|
|
10140
10256
|
.addCommand(new Command("delete")
|
|
@@ -10340,6 +10456,7 @@ Examples:
|
|
|
10340
10456
|
.option("--max-credits <n>", "Maximum managed/provider credits to reserve for this run.")
|
|
10341
10457
|
.option("--max-concurrency <n>", "Maximum concurrent row items for this run (1-250). Defaults to 50 (160 for Firecrawl scrape columns).")
|
|
10342
10458
|
.option("--metadata-json <json>", "Optional metadata object to attach to the run.")
|
|
10459
|
+
.option("--idempotency-key <key>", "Reuse the same scoped run when retrying this exact request.")
|
|
10343
10460
|
.option("--then-json <json>", "JSON array of sequential follow-up steps. Each step runs after the previous terminates with completed or completed_with_errors. Paid steps require selection and max_credits. Shape: [{actions:[{type:'tool_column',column}],selection,force,max_concurrency,max_credits,metadata,run_on_failure}].")
|
|
10344
10461
|
.option("--json", "Print a JSON envelope.")
|
|
10345
10462
|
.action(async (table, options) => {
|
|
@@ -10358,6 +10475,7 @@ Examples:
|
|
|
10358
10475
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
10359
10476
|
...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
|
|
10360
10477
|
...(options.metadataJson ? { metadata: parseJsonObject(options.metadataJson) } : {}),
|
|
10478
|
+
...(readOption(options.idempotencyKey) ? { idempotency_key: readOption(options.idempotencyKey) } : {}),
|
|
10361
10479
|
...(chainSteps.length > 0 ? { then: chainSteps } : {}),
|
|
10362
10480
|
},
|
|
10363
10481
|
}));
|
|
@@ -12720,7 +12838,7 @@ Examples:
|
|
|
12720
12838
|
program
|
|
12721
12839
|
.command("tools")
|
|
12722
12840
|
.description("The priced catalog: every provider operation and every OXYGEN column (enrichment bundles, waterfalls, templates, Functions) with its credit cost per row. Start with `tools search --for-table <table>` to see what a table's own columns can be enriched with and what each costs, before adding anything.")
|
|
12723
|
-
.addCommand(new Command("search")
|
|
12841
|
+
.addCommand(addToolCatalogSearchWindowFlag(new Command("search")
|
|
12724
12842
|
.description("Search the priced catalog (0 credits): provider operations in `tools`, and OXYGEN's own columns in `native_columns` (enrichment bundles, waterfalls, templates, Functions), each with `price_label`, `estimated_credits_per_row`, `pricing_kind` and the exact `cli_add` command. With --for-table <table> it reads that table's columns and returns `suggestions`: the enrichments those columns already support, priced per row. Hydrate one provider operation with tools get. A response listing partial_sources means an optional catalog source timed out and totals may be understated — rerun for the complete catalog.")
|
|
12725
12843
|
.argument("[query]", "Search text, e.g. `web` for the Web Research Agent column and its modes (each priced in `modes`), or `email`.")
|
|
12726
12844
|
.option("--verbosity <verbosity>", "minimal, summary, or full. Defaults to minimal; hydrate one result with tools get.")
|
|
@@ -12742,6 +12860,11 @@ Examples:
|
|
|
12742
12860
|
await handleAsyncAction("tools search", options, async () => {
|
|
12743
12861
|
const params = new URLSearchParams();
|
|
12744
12862
|
params.set("query", query ?? "");
|
|
12863
|
+
const window = readToolCatalogSearchWindow(options);
|
|
12864
|
+
if (window.max_chars !== undefined)
|
|
12865
|
+
params.set("max_chars", String(window.max_chars));
|
|
12866
|
+
if (window.include_native !== undefined)
|
|
12867
|
+
params.set("include_native", String(window.include_native));
|
|
12745
12868
|
const forTable = readOption(options.forTable);
|
|
12746
12869
|
if (forTable)
|
|
12747
12870
|
params.set("for_table", forTable);
|
|
@@ -12776,14 +12899,8 @@ Examples:
|
|
|
12776
12899
|
writeAccessCheckFailuresWarning(data);
|
|
12777
12900
|
return data;
|
|
12778
12901
|
});
|
|
12779
|
-
}))
|
|
12780
|
-
.addCommand(
|
|
12781
|
-
.description("Get a tool descriptor.")
|
|
12782
|
-
.argument("<tool_id>", "Tool id.")
|
|
12783
|
-
.option("--json", "Print a JSON envelope.")
|
|
12784
|
-
.action(async (toolId, options) => {
|
|
12785
|
-
await handleAsyncAction("tools get", options, () => requestOxygen(`/api/cli/tools/${encodeURIComponent(toolId)}`));
|
|
12786
|
-
}))
|
|
12902
|
+
})))
|
|
12903
|
+
.addCommand(createToolCatalogDetailCommand(handleAsyncAction))
|
|
12787
12904
|
.addCommand(new Command("favorite")
|
|
12788
12905
|
.description("Favourite one column-catalog entry for this workspace: a native column (find_email, preset:person_enrich, prompt:icp_fit_score_v1) or a vendor tool (tool:<tool_id>). Favourites lead every table's + Add column panel and `tools search --favorites`.")
|
|
12789
12906
|
.argument("<entry_id>", "Catalog entry id, e.g. find_email, preset:person_enrich, prompt:<key>, tool:<tool_id>.")
|
|
@@ -13336,21 +13453,25 @@ Examples:
|
|
|
13336
13453
|
.action(async (options) => {
|
|
13337
13454
|
await handleAsyncAction("integrations list", options, () => requestOxygen("/api/cli/integrations/composio/list"));
|
|
13338
13455
|
}))
|
|
13339
|
-
.addCommand(new Command("connect")
|
|
13456
|
+
.addCommand(addContractFlags(new Command("connect")
|
|
13340
13457
|
.description("Connect an integration. For OAuth toolkits, running this command immediately creates a pending provider authorization and returns a redirect URL; account access starts only if you approve on the provider's page. This is not a price preview. On a repriced workspace, a connected X account reserves 1,000 credits ($10) monthly; inspect the current pricing schedule with `oxygen limits show --json` before connecting. API-key integrations accept --api-key. Run without credentials only for an API-key integration to get a 0-credit preview of its exact fields (mode: needs_api_key): no credential is submitted, nothing is stored, and no provider call is made. Use repeatable --credential name=value when a provider also requires a host, subdomain, or other named value. To keep a secret out of shell history and process arguments, open the preview's web_url and submit it in Connections; --api-key is for controlled noninteractive use. Credential submission creates or replaces the connection immediately and needs no separate --approved flag. LinkedIn and WhatsApp return a Unipile hosted-auth URL instead (`oxygen senders connect` / `oxygen whatsapp connect` are the canonical paths).")
|
|
13341
13458
|
.argument("<integration_id>", "Integration id, such as 'slack' or 'serpapi'.")
|
|
13342
13459
|
.option("--api-key <value>", "API key for controlled noninteractive use. Literal values can appear in shell history and process arguments; prefer the preview web_url for manual entry.")
|
|
13343
13460
|
.option("--credential <name=value>", "Named provider credential field. Repeat for multi-field integrations (for example, PostHog: --credential subdomain=us).", collectRepeatable, [])
|
|
13344
|
-
.option("--account-id <id>", "Provider account id for integrations that span multiple accounts (e.g. Cloudflare when the token can access more than one account).")
|
|
13345
13461
|
.option("--country <code>", "ISO 3166-1 alpha-2 country where the LinkedIn account owner normally signs in (required for `connect linkedin`; picks the egress proxy).")
|
|
13346
13462
|
.option("--pairing-phone-number <e164>", "WhatsApp only: pair by code sent to this E.164 number instead of showing a QR.")
|
|
13347
|
-
.
|
|
13348
|
-
.option("--
|
|
13463
|
+
.addOption(new Option("--acknowledge-risk", "Deprecated: LinkedIn connect no longer needs it. Still accepted so older scripts keep running.").hideHelp())
|
|
13464
|
+
.option("--return-to <path>", "In-product destination after OAuth approval.")
|
|
13465
|
+
.option("--json", "Print a JSON envelope."), INTEGRATION_CONNECT_ACCOUNT_CONTRACT, {
|
|
13466
|
+
account_id: {
|
|
13467
|
+
valueName: "<id>",
|
|
13468
|
+
help: "Provider account id for integrations that span multiple accounts (e.g. Cloudflare when the token can access more than one account).",
|
|
13469
|
+
},
|
|
13470
|
+
})
|
|
13349
13471
|
.action(async (integrationId, options) => {
|
|
13350
13472
|
await handleAsyncAction("integrations connect", options, () => {
|
|
13351
13473
|
const apiKey = readOption(options.apiKey)?.trim();
|
|
13352
13474
|
const credentials = readNamedCredentials(options.credential);
|
|
13353
|
-
const accountId = readOption(options.accountId);
|
|
13354
13475
|
const country = readOption(options.country)?.trim();
|
|
13355
13476
|
const pairingPhoneNumber = readOption(options.pairingPhoneNumber)?.trim();
|
|
13356
13477
|
return requestOxygen("/api/cli/integrations/connect", {
|
|
@@ -13359,7 +13480,8 @@ Examples:
|
|
|
13359
13480
|
integration_id: integrationId,
|
|
13360
13481
|
...(apiKey ? { api_key: apiKey } : {}),
|
|
13361
13482
|
...(credentials ? { credentials } : {}),
|
|
13362
|
-
...(
|
|
13483
|
+
...readContractFlags(options, INTEGRATION_CONNECT_ACCOUNT_CONTRACT),
|
|
13484
|
+
...(readOption(options.returnTo) ? { return_to: readOption(options.returnTo) } : {}),
|
|
13363
13485
|
...(country ? { country } : {}),
|
|
13364
13486
|
...(pairingPhoneNumber ? { pairing_phone_number: pairingPhoneNumber } : {}),
|
|
13365
13487
|
...(options.acknowledgeRisk ? { acknowledge_linkedin_risk: true } : {}),
|
|
@@ -13433,6 +13555,8 @@ Examples:
|
|
|
13433
13555
|
.description("Run an action for a connected integration — Composio toolkits and native providers alike.")
|
|
13434
13556
|
.argument("<integration_id>", "Integration id, such as 'slack'.")
|
|
13435
13557
|
.argument("<action_slug>", "Composio action slug, such as 'SLACK_SEND_MESSAGE'.")
|
|
13558
|
+
.option("--connection-id <id>", "Select this workspace connection.")
|
|
13559
|
+
.option("--credential-mode <mode>", "managed, user_api_key, or user_oauth.")
|
|
13436
13560
|
.option("--input <json>", "Arguments as JSON object. Default: empty object.")
|
|
13437
13561
|
.option("--live", "Execute the action live. Default is dry-run.")
|
|
13438
13562
|
.option("--dry-run", "Force dry-run (no provider call).")
|
|
@@ -13453,6 +13577,8 @@ Examples:
|
|
|
13453
13577
|
integration_id: integrationId,
|
|
13454
13578
|
action_slug: actionSlug,
|
|
13455
13579
|
arguments: args,
|
|
13580
|
+
...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
|
|
13581
|
+
...(readOption(options.credentialMode) ? { credential_mode: readOption(options.credentialMode) } : {}),
|
|
13456
13582
|
mode,
|
|
13457
13583
|
...(options.approved ? { approved: true } : {}),
|
|
13458
13584
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
@@ -13490,15 +13616,16 @@ Examples:
|
|
|
13490
13616
|
await handleAsyncAction("senders checkpoints", options, () => requestOxygen("/api/cli/senders/checkpoints"));
|
|
13491
13617
|
}))
|
|
13492
13618
|
.addCommand(new Command("connect")
|
|
13493
|
-
.description("Get a Unipile hosted-auth URL to connect a new LinkedIn account (or reconnect with --reconnect). This command creates a pending provider authorization; it does not preview a price. Each connected LinkedIn account has one monthly charge, never two: an existing dollar seat until it ends at renewal, or a monthly credit reservation after seats retire. On a repriced workspace that reservation is 3,400 credits ($34) monthly; a connect the balance cannot cover fails with insufficient_credits. Invites, messages and other LinkedIn actions cost 0 credits on top. Check `oxygen billing seats --json` before connecting for the current charge and seat status. A workspace linked to another organization's plan is billed at that organization. New accounts require --country (the owner's normal LinkedIn login country) and default to syncing only conversations OXYGEN starts; use --inbox-scope all to opt into the full LinkedIn inbox. Use --cookie-auth and --custom-proxy to expose those inputs inside Unipile's hosted wizard; their secrets never pass through OXYGEN. Use --count for bulk onboarding
|
|
13619
|
+
.description("Get a Unipile hosted-auth URL to connect a new LinkedIn account (or reconnect with --reconnect). This command creates a pending provider authorization; it does not preview a price. Each connected LinkedIn account has one monthly charge, never two: an existing dollar seat until it ends at renewal, or a monthly credit reservation after seats retire. On a repriced workspace that reservation is 3,400 credits ($34) monthly; a connect the balance cannot cover fails with insufficient_credits. Invites, messages and other LinkedIn actions cost 0 credits on top. Check `oxygen billing seats --json` before connecting for the current charge and seat status. A workspace linked to another organization's plan is billed at that organization. New accounts require --country (the owner's normal LinkedIn login country) and default to syncing only conversations OXYGEN starts; use --inbox-scope all to opt into the full LinkedIn inbox. Use --cookie-auth and --custom-proxy to expose those inputs inside Unipile's hosted wizard; their secrets never pass through OXYGEN. Use --count for bulk onboarding; --count above 1 needs --owner-consent (each owner connects their own account). Links are shareable and valid for 10 minutes.")
|
|
13494
13620
|
.option("--reconnect <connection_id>", "Reconnect an existing connection instead of creating a new one. Accepts a connection id.")
|
|
13495
13621
|
.option("--country <code>", "Required for new accounts: ISO 3166-1 alpha-2 code for the account owner's normal LinkedIn login country (for example DE or US).")
|
|
13496
13622
|
.option("--sales-nav", "Request Classic + Sales Navigator access during Unipile hosted authentication.")
|
|
13497
13623
|
.option("--cookie-auth", "Offer LinkedIn cookie authentication in Unipile's hosted wizard. The account owner enters li_at/li_a there; OXYGEN never receives them.")
|
|
13498
13624
|
.option("--custom-proxy", "Let the account owner enter a custom proxy in Unipile's hosted wizard. OXYGEN never receives the proxy credentials.")
|
|
13499
13625
|
.option("--count <n>", "Mint N hosted-auth links in one call for bulk onboarding (1-25, default 1). Every link uses the same --country; use separate calls for different login countries. Ignored when reconnecting.")
|
|
13626
|
+
.option("--share-link", "Return a shareable connection link for one account; its owner completes authentication.")
|
|
13500
13627
|
.option("--inbox-scope <scope>", "LinkedIn inbox privacy for new accounts: oxygen_initiated (default) or all. Reconnect preserves the existing account setting.")
|
|
13501
|
-
.
|
|
13628
|
+
.addOption(new Option("--acknowledge-risk", "Deprecated: connecting no longer needs it. Still accepted so older scripts keep running.").hideHelp())
|
|
13502
13629
|
.option("--owner-consent", "Required with --count above 1: each account owner connects their own account.")
|
|
13503
13630
|
.option("--established", "The account is an established profile: skip the ~2-week warm-up a new account gets by default.")
|
|
13504
13631
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -13517,6 +13644,7 @@ Examples:
|
|
|
13517
13644
|
...(options.cookieAuth ? { cookie_auth: true } : {}),
|
|
13518
13645
|
...(options.customProxy ? { custom_proxy: true } : {}),
|
|
13519
13646
|
...(count ? { count } : {}),
|
|
13647
|
+
...(options.shareLink ? { share_link: true } : {}),
|
|
13520
13648
|
...(inboxScope ? { inbox_sync_scope: inboxScope } : {}),
|
|
13521
13649
|
...(options.acknowledgeRisk ? { acknowledge_linkedin_risk: true } : {}),
|
|
13522
13650
|
...(options.ownerConsent ? { owner_consent: true } : {}),
|
|
@@ -13701,6 +13829,8 @@ Examples:
|
|
|
13701
13829
|
.option("--first-name <name>", "The person's first name, as it should appear on mailboxes ordered for this sender. Defaults to splitting --name / the LinkedIn name on the first space.")
|
|
13702
13830
|
.option("--last-name <name>", "The person's last name, as it should appear on mailboxes ordered for this sender. Ordering managed inboxes for this sender needs both names; a one-word name leaves it blank and the order is refused rather than guessed.")
|
|
13703
13831
|
.option("--from-linkedin <senderId>", "Seed the profile from this LinkedIn/WhatsApp sender account id: derives the name and MIRRORS the LinkedIn photo into Oxygen storage (LinkedIn's own URL expires, so a mirrored copy is what an inbox vendor can still fetch hours later) and attaches the account.")
|
|
13832
|
+
.option("--avatar-from-linkedin", "Use the attached LinkedIn account's mirrored photo for this new profile.")
|
|
13833
|
+
.option("--avatar-upload-key <key>", "Use the storage_key returned by managed-inboxes upload-avatar; verified server-side.")
|
|
13704
13834
|
.option("--avatar-url <url>", "Use this image URL as the profile photo instead of the LinkedIn one. Must be a public https URL. An expiring link (e.g. media.licdn.com with e=<epoch>) is stored but marked non-durable and is NOT sent to an inbox vendor — use `set-photo --file` to host a permanent copy.")
|
|
13705
13835
|
.option("--status <status>", "Initial status: active (default), paused, or archived.")
|
|
13706
13836
|
.option("--senders <ids>", "Comma-separated sender account ids (LinkedIn/WhatsApp) to attach. A LinkedIn sender also supplies the profile's photo when it has none of its own.")
|
|
@@ -13714,6 +13844,8 @@ Examples:
|
|
|
13714
13844
|
...(readOption(options.firstName) ? { first_name: readOption(options.firstName) } : {}),
|
|
13715
13845
|
...(readOption(options.lastName) ? { last_name: readOption(options.lastName) } : {}),
|
|
13716
13846
|
...(readOption(options.fromLinkedin) ? { from_sender_account_id: readOption(options.fromLinkedin) } : {}),
|
|
13847
|
+
...(options.avatarFromLinkedin ? { avatar_from_linkedin: true } : {}),
|
|
13848
|
+
...(readOption(options.avatarUploadKey) ? { avatar_upload_key: readOption(options.avatarUploadKey) } : {}),
|
|
13717
13849
|
...(readOption(options.avatarUrl) ? { avatar_url: readOption(options.avatarUrl) } : {}),
|
|
13718
13850
|
...(readOption(options.status) ? { status: readOption(options.status) } : {}),
|
|
13719
13851
|
...(options.senders ? { sender_account_ids: splitCommaList(options.senders) } : {}),
|
|
@@ -14004,6 +14136,9 @@ Examples:
|
|
|
14004
14136
|
.option("--text-file <path>", "Read the post body from a file (alternative to --text).")
|
|
14005
14137
|
.option("--account <ref>", "Sender account to post from. Omit for the org default.")
|
|
14006
14138
|
.option("--as-organization <id>", "Publish as a company page the account administers (advanced; may require raw-route support).")
|
|
14139
|
+
.option("--repost <id>", "Quote or share this LinkedIn post.")
|
|
14140
|
+
.option("--include-job-posting <id>", "Embed a LinkedIn job preview.")
|
|
14141
|
+
.option("--attachments-json <json-or-file>", "Up to 20 {content:<base64>,content_type:<MIME>,filename:<name>} objects; use a local JSON file for media.")
|
|
14007
14142
|
.option("--external-link <url>", "Attach an external link to the post.")
|
|
14008
14143
|
.option("--approved", "Publish for real. Without this flag, prints a preview only.")
|
|
14009
14144
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -14024,6 +14159,9 @@ Examples:
|
|
|
14024
14159
|
...(account ? { account } : {}),
|
|
14025
14160
|
...(asOrganization ? { as_organization: asOrganization } : {}),
|
|
14026
14161
|
...(externalLink ? { external_link: externalLink } : {}),
|
|
14162
|
+
...(readOption(options.repost) ? { repost: readOption(options.repost) } : {}),
|
|
14163
|
+
...(readOption(options.includeJobPosting) ? { include_job_posting: readOption(options.includeJobPosting) } : {}),
|
|
14164
|
+
...(options.attachmentsJson !== undefined ? { attachments: parseJsonArray(readFileIfPresent(options.attachmentsJson)) } : {}),
|
|
14027
14165
|
...(options.approved ? { approved: true } : {}),
|
|
14028
14166
|
},
|
|
14029
14167
|
});
|
|
@@ -14185,6 +14323,7 @@ Examples:
|
|
|
14185
14323
|
.description("Comment on a LinkedIn post (or reply to a comment with --comment-id). A real external LinkedIn write that everyone can see and that counts against the sender account's daily action quota, gated behind approval: prints a preview by default and only posts when you pass --approved. --post is the composite post social_id from `oxygen posts get` (NOT the activity URN).")
|
|
14186
14324
|
.requiredOption("--post <social_id>", "Composite post social_id from `oxygen posts get` (NOT the activity URN).")
|
|
14187
14325
|
.requiredOption("--text <text>", "Comment text to post.")
|
|
14326
|
+
.option("--mentions-json <json-or-file>", "Provider comment mention objects.")
|
|
14188
14327
|
.option("--comment-id <id>", "Reply under this comment id instead of commenting on the post.")
|
|
14189
14328
|
.option("--as-organization <org>", "LinkedIn organization id to comment as a company page.")
|
|
14190
14329
|
.option("--external-link <url>", "URL to attach as a link preview on the comment.")
|
|
@@ -14203,6 +14342,7 @@ Examples:
|
|
|
14203
14342
|
method: "POST",
|
|
14204
14343
|
body: {
|
|
14205
14344
|
...(post ? { post } : {}),
|
|
14345
|
+
...(options.mentionsJson !== undefined ? { mentions: parseJsonArray(readFileIfPresent(options.mentionsJson)) } : {}),
|
|
14206
14346
|
...(text ? { text } : {}),
|
|
14207
14347
|
...(commentId ? { comment_id: commentId } : {}),
|
|
14208
14348
|
...(asOrganization ? { as_organization: asOrganization } : {}),
|
|
@@ -14410,20 +14550,16 @@ Examples:
|
|
|
14410
14550
|
});
|
|
14411
14551
|
});
|
|
14412
14552
|
}))
|
|
14413
|
-
.addCommand(new Command("list")
|
|
14414
|
-
.description("List the account's pending sent LinkedIn invitations with their invitation ids — the ids `linkedin invitations withdraw --ids` cancels. Read-only: no provider write
|
|
14415
|
-
.option("--account <ref>", "Scope to one sender account (sender id, connection id, or Unipile account id).")
|
|
14416
|
-
.option("--limit <n>", "Maximum invitations to return.")
|
|
14553
|
+
.addCommand(addContractFlags(new Command("list")
|
|
14554
|
+
.description("List the account's pending sent LinkedIn invitations with their invitation ids — the ids `linkedin invitations withdraw --ids` cancels. Read-only provider fetch: no provider write or credits. --cursor resumes the same sender's returned continuation. For received requests too, kept in a table that updates itself, use `linkedin invitations setup`."), LINKEDIN_SENT_INVITES_QUERY_CONTRACT, LINKEDIN_SENT_INVITES_FLAG_OVERLAY)
|
|
14417
14555
|
.option("--json", "Print a JSON envelope.")
|
|
14418
14556
|
.action(async (options) => {
|
|
14419
14557
|
await handleAsyncAction("linkedin invitations list", options, () => {
|
|
14420
|
-
const
|
|
14421
|
-
const limit = readPositiveInt(options.limit);
|
|
14558
|
+
const selectors = readContractFlags(options, LINKEDIN_SENT_INVITES_QUERY_CONTRACT, LINKEDIN_SENT_INVITES_FLAG_OVERLAY);
|
|
14422
14559
|
const params = new URLSearchParams();
|
|
14423
|
-
|
|
14424
|
-
|
|
14425
|
-
|
|
14426
|
-
params.set("limit", String(limit));
|
|
14560
|
+
for (const [key, value] of Object.entries(selectors))
|
|
14561
|
+
if (value !== undefined && value !== null)
|
|
14562
|
+
params.set(key, String(value));
|
|
14427
14563
|
const suffix = params.toString();
|
|
14428
14564
|
return requestOxygen(`/api/cli/linkedin/connections/invites-sent${suffix ? `?${suffix}` : ""}`);
|
|
14429
14565
|
});
|
|
@@ -14453,13 +14589,18 @@ Examples:
|
|
|
14453
14589
|
.addCommand(new Command("connect")
|
|
14454
14590
|
.description("Get a Unipile hosted-auth URL to connect a new WhatsApp account by scanning a QR in WhatsApp → Linked Devices (or reconnect with --reconnect). Newly connected accounts start a mandatory warm-up ramp.")
|
|
14455
14591
|
.option("--reconnect <connection_id>", "Reconnect an existing connection id instead of linking a new account.")
|
|
14592
|
+
.option("--pairing-phone-number <phone>", "Offer phone-number pairing instead of QR where supported; use your international number.")
|
|
14456
14593
|
.option("--json", "Print a JSON envelope.")
|
|
14457
14594
|
.action(async (options) => {
|
|
14458
14595
|
await handleAsyncAction("whatsapp connect", options, () => {
|
|
14459
14596
|
const reconnect = readOption(options.reconnect);
|
|
14597
|
+
const pairingPhoneNumber = readOption(options.pairingPhoneNumber);
|
|
14460
14598
|
return requestOxygen("/api/cli/whatsapp/connect", {
|
|
14461
14599
|
method: "POST",
|
|
14462
|
-
body: {
|
|
14600
|
+
body: {
|
|
14601
|
+
...(reconnect ? { reconnect_connection_id: reconnect } : {}),
|
|
14602
|
+
...(pairingPhoneNumber ? { pairing_phone_number: pairingPhoneNumber } : {}),
|
|
14603
|
+
},
|
|
14463
14604
|
});
|
|
14464
14605
|
});
|
|
14465
14606
|
}))
|
|
@@ -14507,6 +14648,9 @@ Examples:
|
|
|
14507
14648
|
.option("--messages-reads-per-day <n>", "Daily cap on chat and message-history reads.")
|
|
14508
14649
|
.option("--min-spacing-seconds <n>", "Minimum seconds between actions.")
|
|
14509
14650
|
.option("--spacing-jitter-seconds <n>", "Random jitter seconds added to action spacing.")
|
|
14651
|
+
.option("--working-days <days>", "Allowed weekdays, e.g. mon-fri or 1,3,5.")
|
|
14652
|
+
.option("--working-hours-start <HH:MM>", "Local start of the allowed sending window.")
|
|
14653
|
+
.option("--working-hours-end <HH:MM>", "Local end of the allowed sending window.")
|
|
14510
14654
|
.option("--timezone <tz>", "IANA timezone the daily action counters reset in, e.g. America/New_York.")
|
|
14511
14655
|
.option("--json", "Print a JSON envelope.")
|
|
14512
14656
|
.action(async (id, options) => {
|
|
@@ -15034,6 +15178,7 @@ Examples:
|
|
|
15034
15178
|
.description("Set a conversation's status (the Instantly-style tier). A manual override that locks out AI re-classification. --channel linkedin/whatsapp sets a DM's status.")
|
|
15035
15179
|
.argument("<conversation>", "Conversation id, Unipile chat id, or (email) Zapbox thread id.")
|
|
15036
15180
|
.argument("<status>", "A status label key (e.g. interested, meeting_booked, won, not_interested).")
|
|
15181
|
+
.option("--source <manual|ai>", "Classification provenance; manual locks out AI reclassification.")
|
|
15037
15182
|
.option("--channel <channel>", "Inbox channel: email (default), linkedin, or whatsapp.")
|
|
15038
15183
|
.option("--json", "Print a JSON envelope.")
|
|
15039
15184
|
.action(async (conversation, status, options) => {
|
|
@@ -15041,7 +15186,7 @@ Examples:
|
|
|
15041
15186
|
const channel = readOption(options.channel) ?? "email";
|
|
15042
15187
|
return requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}/status`, {
|
|
15043
15188
|
method: "POST",
|
|
15044
|
-
body: { status, channel },
|
|
15189
|
+
body: { status, channel, ...(readOption(options.source) ? { source: readOption(options.source) } : {}) },
|
|
15045
15190
|
});
|
|
15046
15191
|
});
|
|
15047
15192
|
}))
|
|
@@ -15126,6 +15271,9 @@ Examples:
|
|
|
15126
15271
|
.addCommand(new Command("create")
|
|
15127
15272
|
.description("Create a custom status label.")
|
|
15128
15273
|
.argument("<name>", "Display name.")
|
|
15274
|
+
.option("--key <key>", "Stable custom status key; defaults to the name slug.")
|
|
15275
|
+
.option("--icon <name>", "Label icon; an empty string clears it.")
|
|
15276
|
+
.option("--sort-order <n>", "Numeric position; 0 is valid.")
|
|
15129
15277
|
.option("--color <hex>", "Hex color (e.g. #2a78d6). Workspace palette: #2a78d6 #eb6834 #1baf7a #eda100 #e87ba4 #008300 #4a3aa7 #e34948 #0e9dc4 #9a4fd0.")
|
|
15130
15278
|
.option("--bucket <bucket>", "primary (default) or others.")
|
|
15131
15279
|
.option("--ai-settable", "Let the AI reply classifier assign this label.")
|
|
@@ -15141,6 +15289,9 @@ Examples:
|
|
|
15141
15289
|
body: {
|
|
15142
15290
|
name,
|
|
15143
15291
|
...(color ? { color } : {}),
|
|
15292
|
+
...(options.icon !== undefined ? { icon: options.icon } : {}),
|
|
15293
|
+
...(options.sortOrder !== undefined ? { sort_order: Number(options.sortOrder) } : {}),
|
|
15294
|
+
...(readOption(options.key) ? { key: readOption(options.key) } : {}),
|
|
15144
15295
|
...(bucket ? { bucket } : {}),
|
|
15145
15296
|
...(options.aiSettable ? { ai_settable: true } : {}),
|
|
15146
15297
|
...(description ? { description } : {}),
|
|
@@ -15152,6 +15303,8 @@ Examples:
|
|
|
15152
15303
|
.description("Update a status label's name, color, bucket, description, or (custom labels only) ai-settable flag.")
|
|
15153
15304
|
.argument("<key>", "The label key.")
|
|
15154
15305
|
.option("--name <name>", "New display name.")
|
|
15306
|
+
.option("--icon <name>", "Label icon; an empty string clears it.")
|
|
15307
|
+
.option("--sort-order <n>", "Numeric position; 0 is valid.")
|
|
15155
15308
|
.option("--color <hex>", "New hex color.")
|
|
15156
15309
|
.option("--bucket <bucket>", "New bucket (primary or others).")
|
|
15157
15310
|
.option("--ai-settable", "Let the AI classifier assign this label (custom labels only).")
|
|
@@ -15169,6 +15322,9 @@ Examples:
|
|
|
15169
15322
|
body: {
|
|
15170
15323
|
...(name ? { name } : {}),
|
|
15171
15324
|
...(color ? { color } : {}),
|
|
15325
|
+
...(options.icon !== undefined ? { icon: options.icon } : {}),
|
|
15326
|
+
...(options.sortOrder !== undefined ? { sort_order: Number(options.sortOrder) } : {}),
|
|
15327
|
+
...(readOption(options.key) ? { key: readOption(options.key) } : {}),
|
|
15172
15328
|
...(bucket ? { bucket } : {}),
|
|
15173
15329
|
...(options.aiSettable !== undefined ? { ai_settable: options.aiSettable } : {}),
|
|
15174
15330
|
...(description ? { description } : {}),
|
|
@@ -15601,6 +15757,7 @@ Examples:
|
|
|
15601
15757
|
.option("--contact-linkedin-property <name>", "Optional contact LinkedIn profile property internal name.")
|
|
15602
15758
|
.option("--contact-company-domain-property <name>", "Optional explicit company-domain property on contacts, copied to lead rows.")
|
|
15603
15759
|
.option("--contact-company-linkedin-property <name>", "Optional explicit company LinkedIn URL property on contacts.")
|
|
15760
|
+
.option("--contact-properties <list>", "Comma-separated extra HubSpot contact properties to hydrate (max 50).")
|
|
15604
15761
|
.option("--company-domain-property <name>", "Company-list domain property (default domain).", "domain")
|
|
15605
15762
|
.option("--company-linkedin-property <name>", "Optional company-list LinkedIn URL property.")
|
|
15606
15763
|
.option("--cron <expression>", "Standing cron cadence. Defaults to every 15 minutes.")
|
|
@@ -15637,6 +15794,7 @@ Examples:
|
|
|
15637
15794
|
...(readOption(options.contactLinkedinProperty) ? { contact_linkedin_property: readOption(options.contactLinkedinProperty) } : {}),
|
|
15638
15795
|
...(readOption(options.contactCompanyDomainProperty) ? { contact_company_domain_property: readOption(options.contactCompanyDomainProperty) } : {}),
|
|
15639
15796
|
...(readOption(options.contactCompanyLinkedinProperty) ? { contact_company_linkedin_property: readOption(options.contactCompanyLinkedinProperty) } : {}),
|
|
15797
|
+
...(options.contactProperties !== undefined ? { contact_properties: readCsvOption(options.contactProperties) } : {}),
|
|
15640
15798
|
...(readOption(options.companyDomainProperty) ? { company_domain_property: readOption(options.companyDomainProperty) } : {}),
|
|
15641
15799
|
...(readOption(options.companyLinkedinProperty) ? { company_linkedin_property: readOption(options.companyLinkedinProperty) } : {}),
|
|
15642
15800
|
mode: options.live ? "live" : "dry_run",
|
|
@@ -15683,11 +15841,11 @@ Examples:
|
|
|
15683
15841
|
}, formatSequenceAnalyticsHealth);
|
|
15684
15842
|
}))
|
|
15685
15843
|
.addCommand(new Command("create")
|
|
15686
|
-
.description("Create a draft multichannel sequence from a steps JSON file. Supports LinkedIn, email, WhatsApp, human call_task, and connected-CRM crm_task journeys. Assign LinkedIn senders with --senders (optional at create — a draft can sit senderless, but enroll/start require at least one for LinkedIn journeys). Native email sends through connected Google/Microsoft mailboxes, including Microsoft SMTP transport; optional --email-* flags bind an Instantly campaign. Install/read the oxygen-sequencer skill for complete mapped CRM task examples.")
|
|
15687
|
-
.addHelpText("after", "\nSenders: each new lead gets one of the --sender-profiles people, and its LinkedIn and email steps come from that person (email from one of their inboxes). Per step, in the steps file: email_send.
|
|
15844
|
+
.description("Create a draft multichannel sequence from a steps JSON file. Supports LinkedIn, email, WhatsApp, human call_task, and connected-CRM crm_task journeys. Assign LinkedIn senders with --senders (optional at create — a draft can sit senderless, but enroll/start require at least one for LinkedIn journeys). Native email sends through connected Google/Microsoft mailboxes, including Microsoft SMTP transport; optional --email-* flags bind an Instantly campaign. Install/read the oxygen-sequencer skill for per-step senders, the launch preview and complete mapped CRM task examples.")
|
|
15845
|
+
.addHelpText("after", "\nSenders: each new lead gets one of the --sender-profiles people, the one with the fewest of this sequence's leads (a lead already talking to one of your LinkedIn accounts keeps that account's person), and its LinkedIn and email steps come from that person (email from one of their inboxes). Per step, in the steps file: email_send.sender_profile_id sends that email from any inbox of one selected person (the normal inbox rotation spreads leads across that person's inboxes and each lead keeps its one; a lead already assigned to that person keeps its own inbox; with none usable the lead waits, never using a colleague's inbox), and email_send.sender_mailbox_id from one exact inbox (set one or neither); a LinkedIn step's sender_profile_id sends it from one selected person. Invite, message, inmail and withdraw_invite steps must share one sender_profile_id or none: that person then owns every lead's LinkedIn thread (LinkedIn allows one of your accounts per conversation). visit_profile, follow, like_post and comment_post may each name anyone selected. An email whose From changes, including the return to the lead's inbox, needs its own subject_template. To check who sends each step, run `sequences start <sequence>` without --approved; nothing changes.\n")
|
|
15688
15846
|
.requiredOption("--name <name>", "Human-readable sequence name.")
|
|
15689
15847
|
.requiredOption("--slug <slug>", "Unique slug for the sequence.")
|
|
15690
|
-
.requiredOption("--steps-file <path>", "Path to a JSON file: { \"steps\": [...] }. LinkedIn steps (visit_profile | invite | wait_for_connection | message | inmail | follow | like_post | comment_post | withdraw_invite), email steps (email_send | email_enroll | email_move | email_stop), WhatsApp (whatsapp_message), human call tasks (call_task), connected-CRM tasks (crm_task; channel crm; exact identity record_mappings + explicit create/update policy + record_links + dynamic task property_mappings/associations), and control steps (wait | wait_for_signal | branch | stop), each with an `id`. Copy templates support {{column}} interpolation from the lead's row, {{column|fallback}} inline fallbacks, deterministic spintax — {{RANDOM|a|b|c}} or industry-standard bare {Hi|Hey|Hello}, nestable like {Would {Tuesday|Thursday} work|next week?} — and {% if column %}…{% endif %} conditionals; a bare {…} region is spintax only when it contains a top-level |, so literal braces (CSS/JSON) pass through. Every step's copy can also use {{sender_first_name}}, {{sender_last_name}} and {{sender_full_name}} (alias {{sender_name}}): the name of the person who sends that step (their sender profile, else the account or inbox display name), with no table column needed; email steps add {{sender_email}} (the from address). A row column of the same name WINS on collision, and a missing name renders empty. comment_post takes text_template and/or ai_prompt — a KG-grounded comment generated at send time (a paid AI call) that falls back to text_template if generation fails. A `branch` routes on signals (then/else) or the legacy connection_accepted/already_connected/open_profile sugar (then_id/else_id). A signal condition can also branch on the LEAD'S DATA: a data leaf { has_column: \"email\" } is true when that row_values column has a non-empty value (add present:false for \"missing\") — e.g. route leads that have an email down an email arm and the rest down a LinkedIn arm. MINIMAL EMAIL STEP SHAPE: { \"id\": \"s1\", \"channel\": \"email\", \"kind\": \"email_send\", \"subject_template\": \"...\", \"body_template\": \"...\" } — body_template is REQUIRED on email_send; subject_template is OPTIONAL and is the one control that decides threading: give a step its own subject and it goes out as a NEW email, omit it and the step continues the lead's previous email in the same thread, going out as \"Re: <the previous email's subject>\" (what the retired email_reply kind used to be — still accepted on input, rewritten into this shape, and reported under `warnings` in the response). The FIRST email step of a sequence must carry a subject, since it has no earlier thread to continue. MINIMAL WHATSAPP STEP SHAPE: { \"id\": \"s1\", \"channel\": \"whatsapp\", \"kind\": \"whatsapp_message\", \"template\": \"Hi {{first_name|there}} …\" } — template is REQUIRED, and a live WhatsApp start also needs the sequence's --whatsapp-cold-initiate opt-in. A/B tests use an explicit `variants` array of copy partials on the step (up to 25 alternates, a–z; spintax varies wording INSIDE one variant and is not A/B-tracked). For the complete replay-safe crm_task upsert mapping shape, install/read the oxygen-sequencer skill.")
|
|
15848
|
+
.requiredOption("--steps-file <path>", "Path to a JSON file: { \"steps\": [...] }. LinkedIn steps (visit_profile | invite | wait_for_connection | message | inmail | follow | like_post | comment_post | withdraw_invite), email steps (email_send | email_enroll | email_move | email_stop), WhatsApp (whatsapp_message), human call tasks (call_task), connected-CRM tasks (crm_task; channel crm; exact identity record_mappings + explicit create/update policy + record_links + dynamic task property_mappings/associations), and control steps (wait | wait_for_signal | branch | stop), each with an `id`. Copy templates support {{column}} interpolation from the lead's row, {{column|fallback}} inline fallbacks, deterministic spintax — {{RANDOM|a|b|c}} or industry-standard bare {Hi|Hey|Hello}, nestable like {Would {Tuesday|Thursday} work|next week?} — and {% if column %}…{% endif %} conditionals; a bare {…} region is spintax only when it contains a top-level |, so literal braces (CSS/JSON) pass through. Every step's copy can also use {{sender_first_name}}, {{sender_last_name}} and {{sender_full_name}} (alias {{sender_name}}): the name of the person who sends that step (their sender profile, else the account or inbox display name), with no table column needed; email steps add {{sender_email}} (the from address). A row column of the same name WINS on collision, and a missing name renders empty. comment_post takes text_template and/or ai_prompt — a KG-grounded comment generated at send time (a paid AI call) that falls back to text_template if generation fails. A `branch` routes on signals (then/else) or the legacy connection_accepted/already_connected/open_profile sugar (then_id/else_id). A signal condition can also branch on the LEAD'S DATA: a data leaf { has_column: \"email\" } is true when that row_values column has a non-empty value (add present:false for \"missing\") — e.g. route leads that have an email down an email arm and the rest down a LinkedIn arm. MINIMAL EMAIL STEP SHAPE: { \"id\": \"s1\", \"channel\": \"email\", \"kind\": \"email_send\", \"subject_template\": \"...\", \"body_template\": \"...\" } — body_template is REQUIRED on email_send; subject_template is OPTIONAL and is the one control that decides threading: give a step its own subject and it goes out as a NEW email, omit it and the step continues the lead's previous email in the same thread, going out as \"Re: <the previous email's subject>\" (what the retired email_reply kind used to be — still accepted on input, rewritten into this shape, and reported under `warnings` in the response). The FIRST email step of a sequence must carry a subject, since it has no earlier thread to continue. MINIMAL WHATSAPP STEP SHAPE: { \"id\": \"s1\", \"channel\": \"whatsapp\", \"kind\": \"whatsapp_message\", \"template\": \"Hi {{first_name|there}} …\" } — template is REQUIRED, and a live WhatsApp start also needs the sequence's --whatsapp-cold-initiate opt-in. MINIMAL WAIT STEP SHAPE: { \"id\": \"w1\", \"kind\": \"wait\", \"days\": 3 } — days and/or hours, at least 1 hour in total. A/B tests use an explicit `variants` array of copy partials on the step (up to 25 alternates, a–z; spintax varies wording INSIDE one variant and is not A/B-tracked). For the complete replay-safe crm_task upsert mapping shape, install/read the oxygen-sequencer skill.")
|
|
15691
15849
|
.option("--channels <list>", "Comma-separated channels: linkedin,email,whatsapp,call,crm. Defaults to the channels the journey touches. crm_task currently supports HubSpot.")
|
|
15692
15850
|
.option("--whatsapp-cold-initiate", "WhatsApp: allow cold-initiating new chats (no prior conversation). Required to start a WhatsApp sequence live — WhatsApp via Unipile is unofficial WhatsApp Web, so cold-initiating is an explicit ban-risk opt-in.")
|
|
15693
15851
|
.option("--phone-column-key <key>", "WhatsApp: row_values key holding each lead's phone number (else falls back to phone/phone_number/mobile).")
|
|
@@ -15715,13 +15873,15 @@ Examples:
|
|
|
15715
15873
|
.option("--email-min-gap-minutes <n>", "Minimum same-mailbox gap, default 12 minutes (integer 0-720; 0 disables the extra gap). Sends are spread across the window and may be farther apart. Daily caps and windows still apply.")
|
|
15716
15874
|
.option("--opportunity-value <usd>", "Estimated USD value of one positive-reply opportunity (>= 0). Analytics-only: sequence stats multiply it by the positive-reply count to report pipeline $ (stats.opportunities). Never gates a send or spends a credit.")
|
|
15717
15875
|
.option("--mailboxes <ids>", "Per-sequence sending-account allowlist for native email (Instantly's 'Accounts to use'): comma-separated mailbox ids. Native email sends rotate ONLY over these mailboxes instead of the whole pool. Omit for the whole pool. Note: a narrow allowlist plus --esp-matching strict can starve sends when no allowed same-provider mailbox exists (surfaced as blocked defer reasons in stats).")
|
|
15718
|
-
.option("--sender-profiles <ids>", "Send from these sender profiles (unified LinkedIn/WhatsApp/inbox identities): comma-separated profile ids. The server resolves each into its senders + inboxes. Live start is blocked if a selected profile lacks an account for a channel the journey uses.")
|
|
15876
|
+
.option("--sender-profiles <ids>", "Send from these sender profiles (unified LinkedIn/WhatsApp/inbox identities): comma-separated profile ids (`oxygen senders profiles list` lists them). The server resolves each into its senders + inboxes. Live start is blocked if a selected profile lacks an account for a channel the journey uses.")
|
|
15719
15877
|
.option("--no-stop-on-bounce", "Keep a lead's enrollment running after a hard bounce (default: stop it). The bounce is still recorded as an email_bounced signal either way.")
|
|
15720
15878
|
.option("--exclude-contacted", "Default every enroll into this sequence to cross-campaign exclusion — skip leads any OTHER active sequence is already contacting. A per-enroll `sequences enroll --exclude-contacted` / `--no-exclude-contacted` always overrides this default. Off by default.")
|
|
15721
15879
|
.option("--no-exclude-contacted", "Turn the sequence-level exclude-contacted default back OFF (enrollments then exclude cross-campaign only when a call opts in).")
|
|
15722
15880
|
.option("--company-reply-pause <off|default|statuses>", "Pause the replier's company in this campaign when a reply has one of these statuses. Oxygen detects the company from work-email domains or explicit company fields; with no company identity, only the replier stops. New sequences start with this setting off. 'default' = interested,neutral,not_now,not_interested; or comma-separated Inbox status keys (`inbox labels list`); 'off' disables it. Read the saved setting with `sequences get`.")
|
|
15723
15881
|
.option("--stop-on-reply-scope <scope>", "(deprecated: use --company-reply-pause) 'company_domain' = --company-reply-pause interested,neutral,not_now,not_interested,wrong_person; 'lead' = off.")
|
|
15724
15882
|
.option("--bounce-protection <json>", "Sequence bounce policy JSON (partial updates merge): enabled=true, warning_rate=.02, pause_rate=.03, min_sends=20, window_days=7. Hard bounces / accepted sends in the rolling window; pauses this sequence, not the fleet. Rates are fractions; mailbox protection is separate.")
|
|
15883
|
+
.option("--auto-optimize", "Enable the sequence-wide A/B auto-winner (default on).")
|
|
15884
|
+
.option("--no-auto-optimize", "Disable the sequence-wide A/B auto-winner; manual variant decisions stay available.")
|
|
15725
15885
|
.option("--include-unsubscribe", "Add an unsubscribe option to emails, the same as Launch's switch: a visible recipient-bound link (plain text shows the URL, HTML a linked Unsubscribe label) plus one-click headers that mail apps show as an Unsubscribe button, both on first-touch cold emails; in-thread follow-ups carry neither. Zapbox sends (mailboxes imported from Zapmail, `transport: \"zapbox\"` in `mailboxes list --json`) carry the link only. Off by default. There is no separate opt-out text: write any reply-to-stop line into the email copy. `sequences update` can switch on the link or the headers alone.")
|
|
15726
15886
|
.option("--no-include-unsubscribe", "Turn off both the visible unsubscribe link and the one-click headers.")
|
|
15727
15887
|
.option("--tracking-opens", "Opt in to open tracking (default off). Requires a verified tracking domain and deployment tracking secret.")
|
|
@@ -15802,7 +15962,7 @@ Examples:
|
|
|
15802
15962
|
}))
|
|
15803
15963
|
.addCommand(new Command("update")
|
|
15804
15964
|
.description("Update a sequence. Journey structure/channels/email binding are draft-only so revised scope receives a fresh launch approval; a started sequence may only re-time waits it already has (clear a delay by zeroing it, never by deleting the step — enrollments track position by index); sender pools can change while draft or paused; launch caps change through `sequences start` after first start. Name, throttles, the sending schedule, tags, the unsubscribe option (off by default), and open/click tracking toggles remain editable as allowed by the server. Pass only the fields you want to change.")
|
|
15805
|
-
.addHelpText("after", "\nSenders: email_send.
|
|
15965
|
+
.addHelpText("after", "\nSenders: email_send.sender_profile_id sends one email from any inbox of a selected person (a lead already assigned to that person keeps its own inbox), and email_send.sender_mailbox_id from one exact inbox of a selected sender; a LinkedIn step's sender_profile_id sends it from a selected person (invite, message, inmail and withdraw_invite share one or none). Omit both to use the lead's assigned person. An email whose From changes needs its own subject_template. `sequences start <sequence>` without --approved shows who sends each step; nothing changes.\n")
|
|
15806
15966
|
.argument("<sequence>", "Sequence id or slug.")
|
|
15807
15967
|
.option("--name <name>", "New human-readable sequence name.")
|
|
15808
15968
|
.option("--steps-file <path>", "Draft only: path to a JSON file: { \"steps\": [...] } replacing the journey. Copy supports {{column}} interpolation; every step also takes {{sender_first_name}} / {{sender_last_name}} / {{sender_full_name}} for the person who sends it, and email adds {{sender_email}} (a same-named row column wins). A `branch` can also route on the lead's data with a data leaf { has_column: \"email\" } (true when that row_values column is non-empty; present:false for \"missing\").")
|
|
@@ -15838,13 +15998,15 @@ Examples:
|
|
|
15838
15998
|
.option("--email-min-gap-minutes <n>", "Minimum same-mailbox gap, default 12 minutes (integer 0-720; 0 disables the extra gap). Sends are spread across the window and may be farther apart. Daily caps and windows still apply.")
|
|
15839
15999
|
.option("--opportunity-value <usd>", "Estimated USD value of one positive-reply opportunity (>= 0). Analytics-only: sequence stats multiply it by the positive-reply count to report pipeline $ (stats.opportunities). Never gates a send or spends a credit.")
|
|
15840
16000
|
.option("--mailboxes <ids>", "Per-sequence sending-account allowlist for native email (Instantly's 'Accounts to use'): comma-separated mailbox ids. Native email sends rotate ONLY over these mailboxes instead of the whole pool. Note: a narrow allowlist plus --esp-matching strict can starve sends when no allowed same-provider mailbox exists (surfaced as blocked defer reasons in stats).")
|
|
15841
|
-
.option("--sender-profiles <ids>", "Send from these sender profiles (unified LinkedIn/WhatsApp/inbox identities): comma-separated profile ids. The server resolves each into its senders + inboxes. Live start is blocked if a selected profile lacks an account for a channel the journey uses.")
|
|
16001
|
+
.option("--sender-profiles <ids>", "Send from these sender profiles (unified LinkedIn/WhatsApp/inbox identities): comma-separated profile ids (`oxygen senders profiles list` lists them). The server resolves each into its senders + inboxes. Live start is blocked if a selected profile lacks an account for a channel the journey uses.")
|
|
15842
16002
|
.option("--no-stop-on-bounce", "Keep a lead's enrollment running after a hard bounce (default: stop it). The bounce is still recorded as an email_bounced signal either way.")
|
|
15843
16003
|
.option("--exclude-contacted", "Default every enroll into this sequence to cross-campaign exclusion — skip leads any OTHER active sequence is already contacting. A per-enroll `sequences enroll --exclude-contacted` / `--no-exclude-contacted` always overrides this default. Off by default.")
|
|
15844
16004
|
.option("--no-exclude-contacted", "Turn the sequence-level exclude-contacted default back OFF (enrollments then exclude cross-campaign only when a call opts in).")
|
|
15845
16005
|
.option("--company-reply-pause <off|default|statuses>", "Pause the replier's company in this campaign when a reply has one of these statuses. Oxygen detects the company from work-email domains or explicit company fields; with no company identity, only the replier stops. New sequences start with this setting off. 'default' = interested,neutral,not_now,not_interested; or comma-separated Inbox status keys (`inbox labels list`); 'off' disables it. Read the saved setting with `sequences get`.")
|
|
15846
16006
|
.option("--stop-on-reply-scope <scope>", "(deprecated: use --company-reply-pause) 'company_domain' = --company-reply-pause interested,neutral,not_now,not_interested,wrong_person; 'lead' = off.")
|
|
15847
16007
|
.option("--bounce-protection <json>", "Sequence bounce policy JSON (partial updates merge): enabled=true, warning_rate=.02, pause_rate=.03, min_sends=20, window_days=7. Hard bounces / accepted sends in the rolling window; pauses this sequence, not the fleet. Rates are fractions; mailbox protection is separate.")
|
|
16008
|
+
.option("--auto-optimize", "Enable the sequence-wide A/B auto-winner (default on).")
|
|
16009
|
+
.option("--no-auto-optimize", "Disable the sequence-wide A/B auto-winner; manual variant decisions stay available.")
|
|
15848
16010
|
.option("--include-unsubscribe", "Add an unsubscribe option to emails, the same as Launch's switch: a visible recipient-bound link (plain text shows the URL, HTML a linked Unsubscribe label) plus one-click headers that mail apps show as an Unsubscribe button, both on first-touch cold emails; in-thread follow-ups carry neither. Zapbox sends (mailboxes imported from Zapmail, `transport: \"zapbox\"` in `mailboxes list --json`) carry the link only. Off by default. There is no separate opt-out text: write any reply-to-stop line into the email copy.")
|
|
15849
16011
|
.option("--no-include-unsubscribe", "Turn off both the visible unsubscribe link and the one-click headers.")
|
|
15850
16012
|
.option("--include-unsubscribe-headers", "Only the one-click unsubscribe headers (default off); --include-unsubscribe turns on the headers and the visible link together.")
|
|
@@ -15919,7 +16081,7 @@ Examples:
|
|
|
15919
16081
|
});
|
|
15920
16082
|
}))
|
|
15921
16083
|
.addCommand(new Command("get")
|
|
15922
|
-
.description("Get a sequence's definition, senders, status, and credit usage. --readiness adds the launch check — whether it CAN go live, why not, and
|
|
16084
|
+
.description("Get a sequence's definition, senders, status, and credit usage. --readiness adds the launch check — whether it CAN go live, why not, and the mailbox pool it would send from — without touching it; `sequences start <sequence>` without --approved shows who sends each step (sender_journey).")
|
|
15923
16085
|
.argument("<sequence>", "Sequence id or slug.")
|
|
15924
16086
|
.option("--readiness", "Add launch_readiness: blockers (e.g. a missing send cap), warnings, and the resolved sending mailboxes. Read-only — sends nothing, changes nothing, spends nothing. Skips the LinkedIn rendered-copy scan, which stays in `sequences start` without --approved.")
|
|
15925
16087
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -15975,6 +16137,7 @@ Examples:
|
|
|
15975
16137
|
.requiredOption("--campaign <name-or-id>", "The campaign that already exists in that tool. A name is resolved against your connected account (exact id, then exact name, then a unique partial match).")
|
|
15976
16138
|
.requiredOption("--from-table <table>", "Table id or slug whose rows are pushed.")
|
|
15977
16139
|
.option("--rows <row_ids>", "Comma-separated row ids to push. Defaults to the whole table.")
|
|
16140
|
+
.option("--excluded-rows <row_ids>", "Comma-separated row ids omitted from the whole-table/filter selection; cannot combine with --rows.")
|
|
15978
16141
|
.option("--filter-json <json>", "Filter object or array selecting the rows, e.g. '{\"column\":\"email\",\"op\":\"is_not_null\"}'.")
|
|
15979
16142
|
.option("--mode <mode>", "dry_run (default) or live.")
|
|
15980
16143
|
.option("--approved", "Required for --mode live: confirms the external write after inspecting the dry run.")
|
|
@@ -15990,10 +16153,13 @@ Examples:
|
|
|
15990
16153
|
if (rows.length > 0 && filterJson) {
|
|
15991
16154
|
throw new Error("Pass either --rows or --filter-json, not both.");
|
|
15992
16155
|
}
|
|
16156
|
+
const excludedRowIds = splitCommaList(options.excludedRows);
|
|
16157
|
+
if (rows.length > 0 && excludedRowIds.length > 0)
|
|
16158
|
+
throw new OxygenError("invalid_selection", "--excluded-rows cannot combine with --rows.", { exitCode: 2 });
|
|
15993
16159
|
const selection = rows.length > 0
|
|
15994
16160
|
? { mode: "row_ids", row_ids: rows }
|
|
15995
|
-
: filterJson
|
|
15996
|
-
? { mode: "all_matching", filters: JSON.parse(filterJson) }
|
|
16161
|
+
: filterJson || excludedRowIds.length > 0
|
|
16162
|
+
? { mode: "all_matching", filters: filterJson ? JSON.parse(filterJson) : [], ...(excludedRowIds.length > 0 ? { excluded_row_ids: excludedRowIds } : {}) }
|
|
15997
16163
|
: null;
|
|
15998
16164
|
const fieldMappingJson = readOption(options.fieldMappingJson);
|
|
15999
16165
|
return requestOxygen("/api/cli/sequences/push-external", {
|
|
@@ -16014,12 +16180,12 @@ Examples:
|
|
|
16014
16180
|
}, formatSequencePushExternal);
|
|
16015
16181
|
}))
|
|
16016
16182
|
.addCommand(new Command("start")
|
|
16017
|
-
.description("Preview or start a sequence. Without --approved this is the launch check: it returns launch_readiness (blockers such as a missing send cap or the WhatsApp cold-initiate opt-in, plus warnings), scans pending LinkedIn copy, and returns exact rendered recipient samples, character counts/limits, copy blockers, per-kind scope, sender/mailbox capacity, CRM readiness, and safety caps. Live start requires --approved; --max-credits is optional. Email/WhatsApp/CRM-task journeys additionally require --max-live-sends because their external actions cost 0 Oxygen credits.
|
|
16183
|
+
.description("Preview or start a sequence. Without --approved this is the launch check and changes nothing: it returns launch_readiness (blockers such as a missing send cap or the WhatsApp cold-initiate opt-in, plus warnings), who sends each step (sender_journey: each step's From, its candidate inboxes and how a lead's inbox is picked; no leads needed), scans pending LinkedIn copy, and returns exact rendered recipient samples, character counts/limits, copy blockers, per-kind scope, sender/mailbox capacity, CRM readiness, and safety caps. Live start requires --approved; --max-credits is optional. Email/WhatsApp/CRM-task journeys additionally require --max-live-sends because their external actions cost 0 Oxygen credits. --dry-run is not this preview: it starts the sequence in simulated mode (it leaves draft), with no send, provider campaign, CRM task, or credits. The first live start also switches on the standing reply → CRM automation for the workspace.")
|
|
16018
16184
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16019
16185
|
.option("--approved", "Approve and activate live. Without this flag, returns a preview only, including copy_preview rendered samples and blockers.")
|
|
16020
16186
|
.option("--max-credits <n>", "Optional credit ceiling for the LinkedIn track (omit for an unbounded run).")
|
|
16021
16187
|
.option("--max-live-sends <n>", "Live external-action ceiling (positive integer). Required for email, WhatsApp, or crm_task steps.")
|
|
16022
|
-
.option("--dry-run", "Activate the sequence (status draft → active, startedAt set) in dry-run mode: every step advances with simulated actions — no sends, CRM writes, provider calls, or credits. This is NOT the preview: `sequences start` with no flags is the side-effect-free launch preview,
|
|
16188
|
+
.option("--dry-run", "Activate the sequence (status draft → active, startedAt set) in dry-run mode: every step advances with simulated actions — no sends, CRM writes, provider calls, or credits. This is NOT the preview: `sequences start` with no flags is the side-effect-free launch preview. A later live start (--approved) restarts those leads from the first step so they are contacted for real, except deleted source rows, a person another live campaign contacts under exclude_contacted, and older duplicates of a person.")
|
|
16023
16189
|
.option("--json", "Print a JSON envelope.")
|
|
16024
16190
|
.action(async (sequence, options) => {
|
|
16025
16191
|
await handleAsyncAction("sequences start", options, () => {
|
|
@@ -16083,7 +16249,7 @@ Examples:
|
|
|
16083
16249
|
await handleAsyncAction("sequences delete", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}${options.approved ? "?approved=true" : ""}`, { method: "DELETE" }));
|
|
16084
16250
|
}))
|
|
16085
16251
|
.addCommand(new Command("enrollments")
|
|
16086
|
-
.description("List per-lead enrollment state plus the latest action status, error, attempts, and retry time.")
|
|
16252
|
+
.description("List per-lead enrollment state plus the latest action status, error, attempts, and retry time. missing_source_row is true for a lead whose source-table row was deleted: still enrolled and contacted (remove with `contacts-remove --missing-source-rows`); null when the campaign has no live source table to check against, or the check failed.")
|
|
16087
16253
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16088
16254
|
.option("--status <status>", "Filter by enrollment status (active, waiting_connection, replied, completed, stopped, failed).")
|
|
16089
16255
|
.option("--limit <n>", "Maximum enrollments to return (1-500).")
|
|
@@ -16102,7 +16268,7 @@ Examples:
|
|
|
16102
16268
|
});
|
|
16103
16269
|
}))
|
|
16104
16270
|
.addCommand(new Command("contacts")
|
|
16105
|
-
.description("List a launched campaign's table-backed contacts with full source variables, contact tags, and factual or assigned sender. A contact carries every tag it reached: contacted means an invite, message or email was sent (a profile visit is not), and a positive reply is also replied and contacted. Contacts removed from this campaign are hidden; source rows remain intact.")
|
|
16271
|
+
.description("List a launched campaign's table-backed contacts with full source variables, contact tags, and factual or assigned sender. A contact carries every tag it reached: contacted means an invite, message or email was sent (a profile visit is not), and a positive reply is also replied and contacted. Contacts removed from this campaign are hidden; source rows remain intact. missing_source_row_count (first page only) counts enrolled leads whose source-table row was deleted: still contacted, never listed (remove them with `contacts-remove --missing-source-rows`).")
|
|
16106
16272
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16107
16273
|
.option("--contact-state <tags>", "Only contacts with any of these tags, comma-separated: not_contacted, contacted, connected, replied, positive_reply; or all.", "all")
|
|
16108
16274
|
.option("--exclude-contact-state <tags>", "Hide contacts with any of these tags, comma-separated.")
|
|
@@ -16152,27 +16318,39 @@ Examples:
|
|
|
16152
16318
|
});
|
|
16153
16319
|
}))
|
|
16154
16320
|
.addCommand(new Command("contacts-remove")
|
|
16155
|
-
.description("Remove selected source-table rows
|
|
16321
|
+
.description("Remove leads from this campaign only: selected source-table rows (--table-row-ids), or every enrolled lead whose source-table row was deleted (--missing-source-rows; deleting a row never removes its lead). Preview by default and names those leads; --approved stops pending enrollment work and queues delegated-provider cleanup while preserving source rows and historical analytics. Permanent: a removed lead cannot be re-enrolled in this campaign. Finished leads stay as they are, except one a dry run finished while the sequence is still in dry run, which a live start would otherwise restart.")
|
|
16156
16322
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16157
|
-
.
|
|
16323
|
+
.option("--table-row-ids <ids>", "Comma-separated source table row UUIDs (maximum 500).")
|
|
16324
|
+
.option("--missing-source-rows", "Select enrolled leads whose source-table row was deleted (up to 500 per call, oldest first).")
|
|
16158
16325
|
.option("--approved", "Apply the previewed campaign-only removal.")
|
|
16159
16326
|
.option("--json", "Print a JSON envelope.")
|
|
16160
16327
|
.action(async (sequence, options) => {
|
|
16161
|
-
await handleAsyncAction("sequences contacts-remove", options, () => {
|
|
16162
|
-
const
|
|
16163
|
-
|
|
16164
|
-
|
|
16165
|
-
|
|
16166
|
-
|
|
16167
|
-
|
|
16328
|
+
await handleAsyncAction("sequences contacts-remove", options, async () => {
|
|
16329
|
+
const rowIdsOption = readOption(options.tableRowIds);
|
|
16330
|
+
if (Boolean(rowIdsOption) === (options.missingSourceRows === true)) {
|
|
16331
|
+
throw new OxygenError("invalid_request", "Pass exactly one of --table-row-ids <ids> or --missing-source-rows.", { exitCode: 2 });
|
|
16332
|
+
}
|
|
16333
|
+
let selector = { missing_source_rows: true };
|
|
16334
|
+
if (rowIdsOption) {
|
|
16335
|
+
const tableRowIds = rowIdsOption
|
|
16336
|
+
.split(",")
|
|
16337
|
+
.map((value) => value.trim())
|
|
16338
|
+
.filter(Boolean);
|
|
16339
|
+
if (tableRowIds.length === 0 || tableRowIds.length > 500) {
|
|
16340
|
+
throw new Error("--table-row-ids must contain between 1 and 500 row UUIDs.");
|
|
16341
|
+
}
|
|
16342
|
+
selector = { table_row_ids: tableRowIds };
|
|
16168
16343
|
}
|
|
16169
|
-
|
|
16344
|
+
const data = await requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/contacts`, {
|
|
16170
16345
|
method: "POST",
|
|
16171
16346
|
body: {
|
|
16172
|
-
|
|
16347
|
+
...selector,
|
|
16173
16348
|
approved: options.approved === true,
|
|
16174
16349
|
},
|
|
16175
16350
|
});
|
|
16351
|
+
if (!options.json)
|
|
16352
|
+
writeContactsRemoveSummary(data);
|
|
16353
|
+
return data;
|
|
16176
16354
|
});
|
|
16177
16355
|
}))
|
|
16178
16356
|
.addCommand(new Command("pause-company")
|
|
@@ -16376,6 +16554,7 @@ Examples:
|
|
|
16376
16554
|
.option("--timezone <tz>", "IANA timezone (e.g. America/New_York). Calls are allowed 09:00-20:00 in the lead's local time; WITHOUT a timezone that window cannot be checked, so the guardrail fails closed and the lead cannot be dialed.")
|
|
16377
16555
|
.option("--override <guardrails>", "Comma-separated guardrails to waive for THIS lead: calling_window (the 09:00-20:00 local window), unknown_timezone, daily_cap. Do-not-call, credits, number health and destination coverage are never waivable.")
|
|
16378
16556
|
.option("--note <text>", "Talking points for the rep. Shown on the dial screen, never sent to the lead.")
|
|
16557
|
+
.option("--assigned-user-id <id>", "Assign the call task to this workspace user.")
|
|
16379
16558
|
.option("--source <source>", "Origin: manual (default), crm, table, reply, sequence.")
|
|
16380
16559
|
.option("--priority <n>", "Higher sorts first in the queue.")
|
|
16381
16560
|
.option("--due <iso>", "ISO-8601 timestamp; the task stays out of the queue until then.")
|
|
@@ -16411,6 +16590,7 @@ Examples:
|
|
|
16411
16590
|
method: "POST",
|
|
16412
16591
|
body: {
|
|
16413
16592
|
action: "queue",
|
|
16593
|
+
...(readOption(options.assignedUserId) ? { assigned_user_id: readOption(options.assignedUserId) } : {}),
|
|
16414
16594
|
lead_phone: phone,
|
|
16415
16595
|
...(name ? { lead_name: name } : {}),
|
|
16416
16596
|
...(timezone ? { lead_timezone: timezone } : {}),
|
|
@@ -16793,22 +16973,24 @@ Examples:
|
|
|
16793
16973
|
});
|
|
16794
16974
|
}))
|
|
16795
16975
|
.addCommand(new Command("add")
|
|
16796
|
-
.description("Add
|
|
16797
|
-
.
|
|
16976
|
+
.description("Add or refresh selected lead ids on the org do-not-contact list so the sequencer never enrolls them again. Use --lead or --leads (1-500). Idempotent; --reason defaults to manual.")
|
|
16977
|
+
.option("--lead <provider_id>", "One lead provider id to suppress; exclusive with --leads.")
|
|
16978
|
+
.option("--leads <ids>", "Comma-separated lead provider ids to suppress in one bounded batch (1-500).")
|
|
16798
16979
|
.option("--reason <reason>", "Why: manual (default), replied, unsubscribed, bounced, do_not_contact, friends.")
|
|
16799
16980
|
.option("--detail <text>", "Optional free-text note stored with the suppression.")
|
|
16800
16981
|
.option("--json", "Print a JSON envelope.")
|
|
16801
16982
|
.action(async (options) => {
|
|
16802
16983
|
await handleAsyncAction("suppressions add", options, () => {
|
|
16803
16984
|
const lead = readOption(options.lead);
|
|
16804
|
-
|
|
16805
|
-
|
|
16985
|
+
const leads = options.leads !== undefined ? readSuppressionBulkKeys(options.leads, "--leads") : undefined;
|
|
16986
|
+
if (Boolean(lead) === Boolean(leads))
|
|
16987
|
+
throw new OxygenError("invalid_request", "Pass exactly one of --lead or --leads.", { exitCode: 2 });
|
|
16806
16988
|
const reason = readOption(options.reason);
|
|
16807
16989
|
const detail = readOption(options.detail);
|
|
16808
16990
|
return requestOxygen("/api/cli/suppressions", {
|
|
16809
16991
|
method: "POST",
|
|
16810
16992
|
body: {
|
|
16811
|
-
lead_provider_id: lead,
|
|
16993
|
+
...(leads ? { lead_provider_ids: leads } : { lead_provider_id: lead }),
|
|
16812
16994
|
...(reason ? { reason } : {}),
|
|
16813
16995
|
...(detail ? { detail } : {}),
|
|
16814
16996
|
},
|
|
@@ -16816,11 +16998,27 @@ Examples:
|
|
|
16816
16998
|
});
|
|
16817
16999
|
}))
|
|
16818
17000
|
.addCommand(new Command("remove")
|
|
16819
|
-
.description("Remove
|
|
17001
|
+
.description("Remove selected leads or every identity of selected merged people from the org do-not-contact list. Choose one of --lead, --leads, --subject-id or --subject-ids; batches hold 1-500 ids. Read a person's metadata.dnc_subject_id in suppressions list, addresses, phones or companies --json.")
|
|
16820
17002
|
.option("--lead <provider_id>", "The lead provider id to un-suppress. Mutually exclusive with --subject-id.")
|
|
16821
17003
|
.option("--subject-id <id>", "DNC subject id (metadata.dnc_subject_id). Clears that person's email, phone, LinkedIn, and company suppressions in one transaction.")
|
|
17004
|
+
.option("--leads <ids>", "Comma-separated lead provider ids to remove in one bounded batch (1-500).")
|
|
17005
|
+
.option("--subject-ids <ids>", "Comma-separated merged-person subject ids to remove across every ledger (1-500).")
|
|
16822
17006
|
.option("--json", "Print a JSON envelope.")
|
|
16823
17007
|
.action(async (options) => {
|
|
17008
|
+
if (options.leads !== undefined || options.subjectIds !== undefined) {
|
|
17009
|
+
await handleAsyncAction("suppressions remove", options, () => {
|
|
17010
|
+
const selectors = [options.lead, options.subjectId, options.leads, options.subjectIds].filter((value) => value !== undefined);
|
|
17011
|
+
if (selectors.length !== 1)
|
|
17012
|
+
throw new OxygenError("invalid_request", "Pass exactly one of --lead, --leads, --subject-id or --subject-ids.", { exitCode: 2 });
|
|
17013
|
+
return requestOxygen("/api/cli/suppressions", {
|
|
17014
|
+
method: "DELETE",
|
|
17015
|
+
body: options.leads !== undefined
|
|
17016
|
+
? { lead_provider_ids: readSuppressionBulkKeys(options.leads, "--leads") }
|
|
17017
|
+
: { subject_ids: readSuppressionBulkKeys(options.subjectIds, "--subject-ids") },
|
|
17018
|
+
});
|
|
17019
|
+
});
|
|
17020
|
+
return;
|
|
17021
|
+
}
|
|
16824
17022
|
const subjectId = readOption(options.subjectId);
|
|
16825
17023
|
if (!subjectId) {
|
|
16826
17024
|
await handleAsyncAction("suppressions remove", options, () => {
|
|
@@ -16879,6 +17077,7 @@ Examples:
|
|
|
16879
17077
|
.requiredOption("--file <path>", "Path to a file of emails, bare domains, LinkedIn profile URLs, and/or LinkedIn member ids, separated by newlines, commas, or whitespace.")
|
|
16880
17078
|
.option("--reason <reason>", "Suppression reason. Emails: hard_bounce | complaint | unsubscribe | manual. Domains: manual | complaint | policy. LinkedIn contacts: manual | replied | unsubscribed | bounced | do_not_contact | friends. Required when the file contains any domains; must be valid for every entry type present (manual always is).")
|
|
16881
17079
|
.option("--source <text>", "Optional provenance note stored on every imported row (e.g. instantly_export).")
|
|
17080
|
+
.option("--metadata-json <json-or-file>", "Default metadata stored on imported identities.")
|
|
16882
17081
|
.option("--detail <text>", "Optional free-text note stored on every imported row.")
|
|
16883
17082
|
.option("--json", "Print a JSON envelope.")
|
|
16884
17083
|
.action(async (options) => {
|
|
@@ -16902,6 +17101,7 @@ Examples:
|
|
|
16902
17101
|
method: "POST",
|
|
16903
17102
|
body: {
|
|
16904
17103
|
entries,
|
|
17104
|
+
...(options.metadataJson !== undefined ? { metadata: parseJsonObject(readFileIfPresent(options.metadataJson)) } : {}),
|
|
16905
17105
|
...(reason ? { reason } : {}),
|
|
16906
17106
|
...(source ? { source } : {}),
|
|
16907
17107
|
...(detail ? { detail } : {}),
|
|
@@ -17029,11 +17229,18 @@ Examples:
|
|
|
17029
17229
|
});
|
|
17030
17230
|
}))
|
|
17031
17231
|
.addCommand(new Command("remove-phone")
|
|
17032
|
-
.description("
|
|
17033
|
-
.argument("
|
|
17232
|
+
.description("Remove one phone suppression or 1-500 exact E.164 numbers with --phones.")
|
|
17233
|
+
.argument("[phone]", "One strict E.164 number, e.g. +14155550123; exclusive with --phones.")
|
|
17234
|
+
.option("--phones <phones>", "Comma-separated strict E.164 numbers to remove (1-500).")
|
|
17034
17235
|
.option("--json", "Print a JSON envelope.")
|
|
17035
17236
|
.action(async (phone, options) => {
|
|
17036
|
-
await handleAsyncAction("suppressions remove-phone", options, () =>
|
|
17237
|
+
await handleAsyncAction("suppressions remove-phone", options, () => {
|
|
17238
|
+
if (Boolean(phone) === (options.phones !== undefined))
|
|
17239
|
+
throw new OxygenError("invalid_request", "Pass one phone or --phones.", { exitCode: 2 });
|
|
17240
|
+
return options.phones !== undefined
|
|
17241
|
+
? requestOxygen("/api/cli/suppressions/phones", { method: "DELETE", body: { phones: readSuppressionBulkKeys(options.phones, "--phones") } })
|
|
17242
|
+
: requestOxygen(`/api/cli/suppressions/phones?phone=${encodeURIComponent(phone)}`, { method: "DELETE" });
|
|
17243
|
+
});
|
|
17037
17244
|
}))
|
|
17038
17245
|
.addCommand(new Command("companies")
|
|
17039
17246
|
.description("List explicit company-domain and LinkedIn-company suppressions. These stop every Sequence channel.")
|
|
@@ -17054,14 +17261,23 @@ Examples:
|
|
|
17054
17261
|
});
|
|
17055
17262
|
}))
|
|
17056
17263
|
.addCommand(new Command("remove-company")
|
|
17057
|
-
.description("
|
|
17058
|
-
.
|
|
17059
|
-
.
|
|
17264
|
+
.description("Remove one explicit company suppression or 1-500 exact identities with --identities-json.")
|
|
17265
|
+
.option("--kind <kind>", "domain or linkedin_company; use with --value.")
|
|
17266
|
+
.option("--value <identity>", "One domain or canonical LinkedIn company URL.")
|
|
17267
|
+
.option("--identities-json <json>", "Exact batch of {kind,value} company identities to remove (1-500); exclusive with --kind/--value.")
|
|
17060
17268
|
.option("--json", "Print a JSON envelope.")
|
|
17061
17269
|
.action(async (options) => {
|
|
17062
17270
|
await handleAsyncAction("suppressions remove-company", options, () => {
|
|
17063
17271
|
const kind = readOption(options.kind);
|
|
17064
17272
|
const value = readOption(options.value);
|
|
17273
|
+
if (options.identitiesJson !== undefined) {
|
|
17274
|
+
if (options.kind !== undefined || options.value !== undefined)
|
|
17275
|
+
throw new OxygenError("invalid_request", "Pass --identities-json or --kind and --value.", { exitCode: 2 });
|
|
17276
|
+
const identities = parseJsonValue(options.identitiesJson, "--identities-json");
|
|
17277
|
+
if (!Array.isArray(identities) || identities.length < 1 || identities.length > 500)
|
|
17278
|
+
throw new OxygenError("invalid_request", "--identities-json must contain 1-500 {kind,value} objects.", { exitCode: 2 });
|
|
17279
|
+
return requestOxygen("/api/cli/suppressions/companies", { method: "DELETE", body: { identities } });
|
|
17280
|
+
}
|
|
17065
17281
|
if (!kind || !value)
|
|
17066
17282
|
throw new Error("--kind and --value are required.");
|
|
17067
17283
|
const params = new URLSearchParams({ kind, value });
|
|
@@ -17188,12 +17404,18 @@ Examples:
|
|
|
17188
17404
|
});
|
|
17189
17405
|
}))
|
|
17190
17406
|
.addCommand(new Command("remove-domain")
|
|
17191
|
-
.description("Remove
|
|
17192
|
-
.argument("
|
|
17407
|
+
.description("Remove whole-domain email blocks (re-enable sending). Use one domain or --domains for 1-500.")
|
|
17408
|
+
.argument("[domain]", "One bare domain to unblock; exclusive with --domains.")
|
|
17409
|
+
.option("--domains <domains>", "Comma-separated domains to remove (1-500).")
|
|
17193
17410
|
.option("--json", "Print a JSON envelope.")
|
|
17194
17411
|
.action(async (domain, options) => {
|
|
17195
17412
|
await handleAsyncAction("suppressions remove-domain", options, () => {
|
|
17196
17413
|
const value = domain?.trim();
|
|
17414
|
+
if (options.domains !== undefined) {
|
|
17415
|
+
if (value)
|
|
17416
|
+
throw new OxygenError("invalid_request", "Pass one domain or --domains.", { exitCode: 2 });
|
|
17417
|
+
return requestOxygen("/api/cli/suppressions/domains", { method: "DELETE", body: { domains: readSuppressionBulkKeys(options.domains, "--domains") } });
|
|
17418
|
+
}
|
|
17197
17419
|
if (!value)
|
|
17198
17420
|
throw new Error("A domain is required.");
|
|
17199
17421
|
return requestOxygen(`/api/cli/suppressions/domains?domain=${encodeURIComponent(value)}`, {
|
|
@@ -17202,11 +17424,23 @@ Examples:
|
|
|
17202
17424
|
});
|
|
17203
17425
|
}))
|
|
17204
17426
|
.addCommand(new Command("remove-address")
|
|
17205
|
-
.description("Remove
|
|
17206
|
-
.argument("
|
|
17427
|
+
.description("Remove blocked email addresses from the do-not-contact ledger (re-enable sending). Use one address or --emails for 1-500; whole-domain blocks use remove-domain.")
|
|
17428
|
+
.argument("[email]", "One blocked address, e.g. ada@acme.com; exclusive with --emails.")
|
|
17429
|
+
.option("--emails <emails>", "Comma-separated exact email addresses to remove (1-500).")
|
|
17207
17430
|
.option("--json", "Print a JSON envelope.")
|
|
17208
17431
|
.action(async (email, options) => {
|
|
17432
|
+
const emails = options.emails;
|
|
17433
|
+
if (emails !== undefined) {
|
|
17434
|
+
await handleAsyncAction("suppressions remove-address", options, () => {
|
|
17435
|
+
if (email)
|
|
17436
|
+
throw new OxygenError("invalid_request", "Pass one email or --emails.", { exitCode: 2 });
|
|
17437
|
+
return requestOxygen("/api/cli/suppressions/addresses", { method: "DELETE", body: { emails: readSuppressionBulkKeys(emails, "--emails") } });
|
|
17438
|
+
});
|
|
17439
|
+
return;
|
|
17440
|
+
}
|
|
17209
17441
|
try {
|
|
17442
|
+
if (!email)
|
|
17443
|
+
throw new OxygenError("invalid_request", "Pass one email or --emails.", { exitCode: 2 });
|
|
17210
17444
|
const data = await requestOxygen(`/api/cli/suppressions/addresses?email=${encodeURIComponent(email)}`, { method: "DELETE" });
|
|
17211
17445
|
if (options.json) {
|
|
17212
17446
|
writeJson(success("suppressions remove-address", data));
|
|
@@ -17275,7 +17509,7 @@ Examples:
|
|
|
17275
17509
|
await handleAsyncAction("managed-inboxes registrant", options, () => requestOxygen("/api/cli/managed-inboxes/registrant"));
|
|
17276
17510
|
}))
|
|
17277
17511
|
.addCommand(new Command("subscribe")
|
|
17278
|
-
.description("
|
|
17512
|
+
.description("Buy managed monthly inboxes on a new domain or your existing Cloudflare domain. Use --existing-domain for Cloudflare: the read-only preview shows ownership, occupancy, DNS scope and blockers, with zero registration charge. --max-per-domain saves a stricter limit with the approved order, including existing addresses; later additions honor it. Preview does not save the limit or change workspace data. WITHOUT --approved this prints a priced PREVIEW with a quote_id (nothing ordered, nothing charged); re-run with --approved --quote <id> to place the order. OXYGEN Warm-up defaults on for Google, Microsoft, and Azure when available: preview and approved responses carry `warmup_activation` with the exact `mailboxes` and `scope=warmup_only`; it is null when warmup is disabled or unavailable. For InboxKit, Google carries `transport=inboxkit_managed_google_handoff`; Microsoft/Azure carries `transport=inboxkit_sequencer_export`. Both carry `mailbox_credentials_required=false` and `native_send_oauth_separate=true`, so managed warm-up needs no customer-supplied OAuth or mailbox password. Google retrieves the existing managed credential only during activation and never persists or returns it; Microsoft/Azure uses native export. Native sending authorization is separate and may still be required before OXYGEN can send. Verify each exact address with `mailboxes oauth-health --json`; InboxKit's unattended Google domain approval usually lands within minutes and the worker keeps retrying, so a waiting row is not a reason to ask the customer to sign in. This order authorizes automatic OXYGEN Warm-up activation after provisioning, so no second `mailboxes warmup enable` approval is needed. The preview shows the price in force: from the 2026-09 repricing date a Google inbox is 500 credits a month all-in (a flat 400 mailbox line plus the 100-credit mailbox connection, warm-up included, which `connection_monthly_credits` shows and your balance reserves once the inbox connects); Microsoft and Azure mailboxes are priced from the vendor's LIVE rate card. Domains are priced from the vendor's LIVE domain price, and any price below vendor cost is refused. A domain you bought on its own through Oxygen (`domains buy`) takes its first inboxes here too: the preview says `domain_source: owned`, nothing is registered again and `one_off_credits` is 0 (Google or Microsoft; omit --years). To add mailboxes to a domain that already has inboxes use `managed-inboxes add-inboxes`.")
|
|
17279
17513
|
.argument("[domain]", "Sending domain to register + host the mailboxes (e.g. send.acme.com), or one you already bought through Oxygen. May also be passed as --domain.")
|
|
17280
17514
|
.option("--domain <domain>", "Sending domain (alternative to the positional argument).")
|
|
17281
17515
|
.requiredOption("--provider <provider>", "Mailbox PLATFORM: google, microsoft, or azure. (google/microsoft cap at 5 mailboxes per domain; azure allows 100.)")
|
|
@@ -17284,7 +17518,10 @@ Examples:
|
|
|
17284
17518
|
.option("--locals <list>", "Comma-separated local parts to create for --sender, e.g. `--locals talia,talia.rosen,t.rosen` on send.acme.com orders talia@, talia.rosen@, and t.rosen@ — all owned by that one person. Requires --sender, because a bare local part carries no name and the vendor's stamp is permanent.")
|
|
17285
17519
|
.option("--mailboxes <json>", "JSON array of mailboxes: [{\"username\",\"first_name\",\"last_name\",\"profile_picture_url\"}], or [{\"username\",\"sender_profile_id\"}] to take the identity from a sender profile per item (mix freely). profile_picture_url is optional and must be a PUBLIC, PERMANENT https URL — the vendor fetches it server-side at provisioning, so an expiring CDN link (e.g. a media.licdn.com URL with e=<epoch>) ships the mailbox faceless. Use `oxygen managed-inboxes upload-avatar <path>` to host one, or --sender / sender_profile_id, whose photo Oxygen already mirrors.")
|
|
17286
17520
|
.option("--file <path>", "Path to a JSON file { \"mailboxes\": [...] } (alternative to --mailboxes).")
|
|
17287
|
-
.
|
|
17521
|
+
.addOption(new Option("--preview", "Explicit read-only quote: connects nothing, buys nothing and charges nothing. Cannot be combined with --approved.").conflicts("approved"))
|
|
17522
|
+
.option("--existing-domain", "Use an existing Cloudflare domain from oxygen domains list; never register or transfer it.")
|
|
17523
|
+
.option("--max-per-domain <n>", "Save a stricter mailbox limit with the approved order, including existing addresses (e.g. 2). Preview does not save it.")
|
|
17524
|
+
.option("--years <n>", "Years to register a new domain for (1-10). Omit for existing domains.")
|
|
17288
17525
|
.option("--redirect-url <url>", "Where the domain's web root redirects. Defaults to your workspace's company website; editable later with `oxygen domains forwarding set`.")
|
|
17289
17526
|
.option("--billing <path>", "Path to a JSON file with the registrant/WHOIS contact (first_name, last_name, phone, country, city, state, address_line_one, postal_code). Required on your FIRST order; later orders reuse the registrant already on file.")
|
|
17290
17527
|
.option("--no-warmup", "Order WITHOUT automatic OXYGEN Warm-up. Warm-up is included by default and starts after provisioning under this order approval for the exact quoted Google/Microsoft/Azure addresses; opting out means using BYO warm-up or a later standalone warmup approval.")
|
|
@@ -17321,6 +17558,8 @@ Examples:
|
|
|
17321
17558
|
mailboxes,
|
|
17322
17559
|
...(vendor ? { vendor } : {}),
|
|
17323
17560
|
...(years ? { years: Number(years) } : {}),
|
|
17561
|
+
...(options.existingDomain ? { existing_domain: true } : {}),
|
|
17562
|
+
...(options.maxPerDomain !== undefined ? { max_mailboxes_per_domain: Number(options.maxPerDomain) } : {}),
|
|
17324
17563
|
...(redirectUrl ? { redirect_url: redirectUrl } : {}),
|
|
17325
17564
|
...(billing ? { billing } : {}),
|
|
17326
17565
|
// Warm-up is on unless --no-warmup. Placement is opt-in: it is sent only
|
|
@@ -17536,7 +17775,7 @@ Examples:
|
|
|
17536
17775
|
writeMailboxPoolOverview(result, { filtered: suffix.length > 0, filterFlags });
|
|
17537
17776
|
}))
|
|
17538
17777
|
.addCommand(new Command("get")
|
|
17539
|
-
.description("Get one sending mailbox's configuration/readiness detail (provider, status, daily cap, warmup state, auth mode, and source — managed vs bring-your-own) plus a one-row pool summary. This is not sent/replied/bounced performance; use `oxygen sequences analytics` for native Sequence attribution by mailbox/domain. An ineligible native-send transport includes transport_reason + transport_hint; it does not by itself block OXYGEN Warm-up or EmailGuard monitoring. <mailbox> accepts a mailbox id or email address.")
|
|
17778
|
+
.description("Get one sending mailbox's configuration/readiness detail (provider, status, daily cap, warmup state, auth mode, and source — managed vs bring-your-own) plus a one-row pool summary. effective_cap comes first: today's campaign limit (effective_daily_cap), the ramp-up mode and its ramp_schedule. This is not sent/replied/bounced performance; use `oxygen sequences analytics` for native Sequence attribution by mailbox/domain. An ineligible native-send transport includes transport_reason + transport_hint; it does not by itself block OXYGEN Warm-up or EmailGuard monitoring. <mailbox> accepts a mailbox id or email address.")
|
|
17540
17779
|
.argument("<mailbox>", "Mailbox id or email address.")
|
|
17541
17780
|
.option("--json", "Print a JSON envelope.")
|
|
17542
17781
|
.action(async (mailbox, options) => {
|
|
@@ -17549,6 +17788,7 @@ Examples:
|
|
|
17549
17788
|
.option("--plan-hash <hash>", "Fresh preview plan_hash.")
|
|
17550
17789
|
.option("--confirmation <phrase>", "Exact confirmation_phrase returned by the fresh preview.")
|
|
17551
17790
|
.option("--cascade", "Disconnect each mailbox's OWN warm-up and deliverability-monitoring add-ons as part of the delete, instead of failing closed until you tear them down by hand. Stops their recurring credits too. Pass it on the preview as well as the approval: the plan hash is bound to it, so an approval cannot turn it on. A live managed domain subscription still fails closed.")
|
|
17791
|
+
.option("--domains <list>", "Exact domains to cancel/archive with this mailbox deletion; included in the preview and approval hash.")
|
|
17552
17792
|
.option("--include-domains", "Also delete each domain whose inboxes are ALL selected: an OXYGEN-bought domain is cancelled, your own is archived (DNS and registrar untouched). Partly selected domains are kept and listed in preview.domains_kept. Pass it on the preview as well as the approval.")
|
|
17553
17793
|
.option("--json", "Print a JSON envelope.")
|
|
17554
17794
|
.action(async (options) => {
|
|
@@ -17566,6 +17806,7 @@ Examples:
|
|
|
17566
17806
|
method: "POST",
|
|
17567
17807
|
body: {
|
|
17568
17808
|
mailboxes,
|
|
17809
|
+
...(options.domains !== undefined ? { domains: readCsvOption(options.domains) } : {}),
|
|
17569
17810
|
...(options.approved === true ? { approved: true } : {}),
|
|
17570
17811
|
...(options.cascade === true ? { cascade: true } : {}),
|
|
17571
17812
|
...(options.includeDomains === true ? { include_domains: true } : {}),
|
|
@@ -17730,9 +17971,9 @@ Examples:
|
|
|
17730
17971
|
});
|
|
17731
17972
|
}))
|
|
17732
17973
|
.addCommand(new Command("cap")
|
|
17733
|
-
.description("View and adjust a single mailbox's daily send cap (throttle a problem inbox
|
|
17974
|
+
.description("View and adjust a single mailbox's daily send cap (throttle a problem inbox without pausing it). The campaign ramp-up that climbs to it is `mailboxes ramp set`.")
|
|
17734
17975
|
.addCommand(new Command("set")
|
|
17735
|
-
.description("Set one sending mailbox's daily send cap (sends per day; new mailboxes get 25). --daily-cap must be a positive whole number; `oxygen limits show` reports the maximum in force. The
|
|
17976
|
+
.description("Set one sending mailbox's daily send cap (sends per day; new mailboxes get 25). --daily-cap must be a positive whole number; `oxygen limits show` reports the maximum in force. The campaign ramp-up (standard 5, 10, then 20/day over the first nine sending days) climbs to this cap; customize or turn it off with `mailboxes ramp set`. Optional provider warm-up is configured with `mailboxes warmup config`. Consumes no Oxygen credits. <mailbox> accepts a mailbox id or email address.")
|
|
17736
17977
|
.argument("<mailbox>", "Mailbox id or email address.")
|
|
17737
17978
|
.requiredOption("--daily-cap <n>", "New daily send cap (a positive whole number of sends per day).")
|
|
17738
17979
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -17748,44 +17989,58 @@ Examples:
|
|
|
17748
17989
|
});
|
|
17749
17990
|
})))
|
|
17750
17991
|
.addCommand(new Command("ramp")
|
|
17751
|
-
.description("
|
|
17992
|
+
.description("Campaign ramp-up for one mailbox: how much campaign email it may send on each of its first sending days before its daily cap. Standard is 5, 10, then 20 a day over the first nine sending days; set a custom start and increase, or turn it off for an inbox already sending elsewhere. A sending day is a completed UTC day with accepted campaign sends. Provider warm-up is separate (`mailboxes warmup config`).")
|
|
17752
17993
|
.addCommand(new Command("set")
|
|
17753
|
-
.description("Set one mailbox's campaign
|
|
17994
|
+
.description("Set one mailbox's campaign ramp-up: a custom --start-per-day plus --step every --increase-every-days sending days until its daily cap, --off for the full daily cap from the first sending day, or --clear for the standard 5/10/20 ramp. Sending days are completed UTC days with accepted campaign sends. The optional --cap holds a custom ramp lower for good. effective_cap in the response reads back today's limit, the ramp_schedule and full_cap_on_sending_day. Free configuration write.")
|
|
17754
17995
|
.argument("<mailbox>", "Mailbox id or email address.")
|
|
17755
|
-
.option("--start-per-day <n>", "
|
|
17756
|
-
.option("--increase-every-days <n>", "
|
|
17757
|
-
.option("--step <n>", "
|
|
17758
|
-
.option("--cap <n>", "
|
|
17759
|
-
.option("--
|
|
17996
|
+
.option("--start-per-day <n>", "Campaign emails allowed on the first sending day (non-negative whole number).")
|
|
17997
|
+
.option("--increase-every-days <n>", "Sending days between increases (whole number >= 1).")
|
|
17998
|
+
.option("--step <n>", "Emails added at each increase (non-negative whole number).")
|
|
17999
|
+
.option("--cap <n>", "Optional permanent ceiling for the ramp; omit it to climb until the daily cap.")
|
|
18000
|
+
.option("--off", "Turn the ramp off: the full daily cap from the first sending day.")
|
|
18001
|
+
.option("--clear", "Restore the standard 5/10/20 ramp.")
|
|
17760
18002
|
.option("--json", "Print a JSON envelope.")
|
|
17761
18003
|
.action(async (mailbox, options) => {
|
|
17762
18004
|
await handleAsyncAction("mailboxes ramp set", options, () => {
|
|
18005
|
+
const knobs = [
|
|
18006
|
+
options.startPerDay,
|
|
18007
|
+
options.increaseEveryDays,
|
|
18008
|
+
options.step,
|
|
18009
|
+
options.cap,
|
|
18010
|
+
].some((value) => value !== undefined);
|
|
18011
|
+
if (options.off && options.clear) {
|
|
18012
|
+
throw new Error("Use --off or --clear, not both.");
|
|
18013
|
+
}
|
|
18014
|
+
if ((options.off || options.clear) && knobs) {
|
|
18015
|
+
throw new Error("--off and --clear take no ramp numbers; drop --start-per-day, --increase-every-days, --step and --cap.");
|
|
18016
|
+
}
|
|
18017
|
+
let rampConfig;
|
|
17763
18018
|
if (options.clear) {
|
|
17764
|
-
|
|
17765
|
-
|
|
17766
|
-
|
|
17767
|
-
}
|
|
18019
|
+
rampConfig = null;
|
|
18020
|
+
}
|
|
18021
|
+
else if (options.off) {
|
|
18022
|
+
rampConfig = { enabled: false };
|
|
17768
18023
|
}
|
|
17769
|
-
|
|
17770
|
-
|
|
17771
|
-
|
|
17772
|
-
|
|
17773
|
-
|
|
17774
|
-
|
|
17775
|
-
|
|
17776
|
-
|
|
17777
|
-
|
|
18024
|
+
else {
|
|
18025
|
+
const startPerDay = readNonNegativeInt(options.startPerDay);
|
|
18026
|
+
const increaseEveryDays = readPositiveInt(options.increaseEveryDays);
|
|
18027
|
+
const step = readNonNegativeInt(options.step);
|
|
18028
|
+
const cap = readNonNegativeInt(options.cap);
|
|
18029
|
+
if (startPerDay === undefined ||
|
|
18030
|
+
increaseEveryDays === undefined ||
|
|
18031
|
+
step === undefined) {
|
|
18032
|
+
throw new Error("Provide --start-per-day, --increase-every-days and --step (--cap is optional), or --off to turn the ramp off, or --clear for the standard ramp.");
|
|
18033
|
+
}
|
|
18034
|
+
rampConfig = {
|
|
18035
|
+
start_per_day: startPerDay,
|
|
18036
|
+
increase_every_days: increaseEveryDays,
|
|
18037
|
+
step,
|
|
18038
|
+
...(cap !== undefined ? { cap } : {}),
|
|
18039
|
+
};
|
|
17778
18040
|
}
|
|
17779
18041
|
return requestOxygen(`/api/cli/mailboxes/${encodeURIComponent(mailbox)}/ramp`, {
|
|
17780
18042
|
method: "PATCH",
|
|
17781
|
-
body: {
|
|
17782
|
-
ramp_config: {
|
|
17783
|
-
start_per_day: startPerDay,
|
|
17784
|
-
increase_every_days: increaseEveryDays,
|
|
17785
|
-
step,
|
|
17786
|
-
cap,
|
|
17787
|
-
},
|
|
17788
|
-
},
|
|
18043
|
+
body: { ramp_config: rampConfig },
|
|
17789
18044
|
});
|
|
17790
18045
|
});
|
|
17791
18046
|
})))
|
|
@@ -18553,10 +18808,11 @@ Examples:
|
|
|
18553
18808
|
.option("--dmarc-policy <policy>", "DMARC policy: none (default), quarantine, or reject.")
|
|
18554
18809
|
.option("--dmarc-rua <mailbox>", "DMARC aggregate-report mailbox (rua=mailto:...).")
|
|
18555
18810
|
.option("--tracking-host <host>", "Open/click tracking CNAME host (only planned when tracking is configured).")
|
|
18811
|
+
.option("--dkim-json <json>", "DKIM records as [{type:TXT|CNAME,name,content}]. Use identical records when applying the preview.")
|
|
18556
18812
|
.option("--no-auto-dkim", "Skip auto-deriving Microsoft DKIM selector1/selector2 CNAMEs (default: auto-derive them for microsoft when no DKIM is supplied).")
|
|
18557
18813
|
.option("--json", "Print a JSON envelope.")
|
|
18558
|
-
.action(async (domain, options) => {
|
|
18559
|
-
await handleAsyncAction("domains dns plan", options, () => runDomainsDnsPlan(domain, options));
|
|
18814
|
+
.action(async (domain, options, command) => {
|
|
18815
|
+
await handleAsyncAction("domains dns plan", { ...options, json: command.optsWithGlobals().json === true }, () => runDomainsDnsPlan(domain, options));
|
|
18560
18816
|
}))
|
|
18561
18817
|
.addCommand(new Command("apply")
|
|
18562
18818
|
.description("Write the planned cold-email DNS records into the domain's Cloudflare zone. Requires a fresh plan_hash from `dns plan`. No Oxygen credits (Cloudflare does not charge for DNS writes). Without --approved, re-previews the plan.")
|
|
@@ -18567,10 +18823,11 @@ Examples:
|
|
|
18567
18823
|
.option("--dmarc-policy <policy>", "DMARC policy: none (default), quarantine, or reject.")
|
|
18568
18824
|
.option("--dmarc-rua <mailbox>", "DMARC aggregate-report mailbox (rua=mailto:...).")
|
|
18569
18825
|
.option("--tracking-host <host>", "Open/click tracking CNAME host (only applied when tracking is configured).")
|
|
18826
|
+
.option("--dkim-json <json>", "DKIM records as [{type:TXT|CNAME,name,content}]. Use identical records when applying the preview.")
|
|
18570
18827
|
.option("--no-auto-dkim", "Skip auto-deriving Microsoft DKIM (must match the value used in `dns plan` so the plan_hash matches).")
|
|
18571
18828
|
.option("--json", "Print a JSON envelope.")
|
|
18572
|
-
.action(async (domain, options) => {
|
|
18573
|
-
await handleAsyncAction("domains dns apply", options, () => runDomainsDnsApply(domain, options));
|
|
18829
|
+
.action(async (domain, options, command) => {
|
|
18830
|
+
await handleAsyncAction("domains dns apply", { ...options, json: command.optsWithGlobals().json === true }, () => runDomainsDnsApply(domain, options));
|
|
18574
18831
|
})))
|
|
18575
18832
|
.addCommand(new Command("tracking")
|
|
18576
18833
|
.description("Open/click tracking domain for native cold email: provision the tracking CNAME on the domain's Cloudflare zone and check its verification status. 0 Oxygen credits.")
|
|
@@ -19120,6 +19377,7 @@ Run completion:
|
|
|
19120
19377
|
.option("--max-credits <n>", "Required credit ceiling for live calls.")
|
|
19121
19378
|
.option("--approved", "Required for live calls after inspecting a dry run.")
|
|
19122
19379
|
.option("--revision <n>", "Run a specific saved version instead of the live one. Dry-run and smoke-test only: publish a version to run it live.")
|
|
19380
|
+
.addOption(new Option("--test-scope <workflow|node>", "Create a durable non-live whole-workflow or node test; node requires --node.").choices(["workflow", "node"]))
|
|
19123
19381
|
.option("--node <node_id>", "Test one node: runs that node plus only the predecessors it needs, from the same saved graph. Dry-run and smoke-test only.")
|
|
19124
19382
|
.option("--preview", "Show what a live run of this exact version would do — every step that leaves Oxygen, the accounts it would use, and the minimum number of billable steps and credits it would cost — without running anything. AI and enrichment steps bill per row and can cost more than that floor. Needs no --mode: it always describes a live run.")
|
|
19125
19383
|
.option("--include-bundle", "Include durable recipe bundles in JSON output.")
|
|
@@ -19150,6 +19408,7 @@ Run completion:
|
|
|
19150
19408
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
19151
19409
|
...(options.approved ? { approved: true } : {}),
|
|
19152
19410
|
...(revisionVersion !== undefined ? { revision_version: revisionVersion } : {}),
|
|
19411
|
+
...(readOption(options.testScope) ? { test_scope: readOption(options.testScope) } : {}),
|
|
19153
19412
|
...(readOption(options.node) ? { test_node_id: readOption(options.node) } : {}),
|
|
19154
19413
|
...(options.preview ? { preview: true } : {}),
|
|
19155
19414
|
},
|
|
@@ -19712,6 +19971,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
19712
19971
|
.addCommand(new Command("get")
|
|
19713
19972
|
.description("Hydrate one exact Oxygen skill entrypoint and its document index.")
|
|
19714
19973
|
.argument("<skill-name>", "Exact name returned by skills search.")
|
|
19974
|
+
.option("--file <path>", "Read one manifest-listed supporting file relative to the selected skill.")
|
|
19715
19975
|
.option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
|
|
19716
19976
|
.option("--json", "Print a JSON envelope.")
|
|
19717
19977
|
.action(async (skillName, options) => {
|
|
@@ -19735,14 +19995,20 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
19735
19995
|
.action(async (options) => {
|
|
19736
19996
|
await handleAsyncAction("skills install", options, () => installAgentSkills(options));
|
|
19737
19997
|
}));
|
|
19738
|
-
applyOxygenHelp(program, binaryName);
|
|
19739
19998
|
registerVisualCommands(program, handleAsyncAction);
|
|
19740
19999
|
registerFunctionsCommands(program, handleAsyncAction);
|
|
19741
20000
|
registerUgcCommands(program, handleAsyncAction);
|
|
19742
20001
|
registerKnowledgeRepositoryCommands(program, handleAsyncAction);
|
|
19743
20002
|
registerConnectorsCommands(program, handleAsyncAction);
|
|
19744
|
-
|
|
19745
|
-
|
|
20003
|
+
registerSettingsCommands(program, handleAsyncAction);
|
|
20004
|
+
registerCrmSupportFlags(program);
|
|
20005
|
+
registerCustomerOperationCommands(program, handleAsyncAction);
|
|
20006
|
+
// After the register* helpers: help groups are assigned to the commands that
|
|
20007
|
+
// exist when this runs, and a family registered later fell into commander's
|
|
20008
|
+
// trailing default "Commands:" list (the 2026-09-30 blind agent read the first
|
|
20009
|
+
// 80 lines of help and missed `connectors` there).
|
|
20010
|
+
applyOxygenHelp(program, binaryName);
|
|
20011
|
+
// Last, so every command registered above is covered.
|
|
19746
20012
|
enableCommandSuggestions(program);
|
|
19747
20013
|
return program;
|
|
19748
20014
|
}
|
|
@@ -21917,6 +22183,7 @@ function readFeedBindBody(table, options) {
|
|
|
21917
22183
|
...(readOption(options.intentId) ? { intent_id: readOption(options.intentId) } : {}),
|
|
21918
22184
|
...(readOption(options.inputShape) ? { input_shape: readOption(options.inputShape) } : {}),
|
|
21919
22185
|
...(readOption(options.routeId) ? { route_id: readOption(options.routeId) } : {}),
|
|
22186
|
+
...(readOption(options.registryVersion) ? { registry_version: readOption(options.registryVersion) } : {}),
|
|
21920
22187
|
...(options.sourcePrompt ? { source_prompt: readFileIfPresent(options.sourcePrompt) } : {}),
|
|
21921
22188
|
...(options.approved ? { approved: true } : {}),
|
|
21922
22189
|
};
|
|
@@ -21941,11 +22208,13 @@ const FEED_BIND_PULL_ONLY_FLAGS = [
|
|
|
21941
22208
|
["intentId", "--intent-id"],
|
|
21942
22209
|
["inputShape", "--input-shape"],
|
|
21943
22210
|
["routeId", "--route-id"],
|
|
22211
|
+
["registryVersion", "--registry-version"],
|
|
21944
22212
|
["sourcePrompt", "--source-prompt"],
|
|
21945
22213
|
["approved", "--approved"],
|
|
21946
22214
|
];
|
|
21947
22215
|
const FEED_BIND_PUSH_ONLY_FLAGS = [
|
|
21948
22216
|
["create", "--create"],
|
|
22217
|
+
["project", "--project"],
|
|
21949
22218
|
["itemsPath", "--items-path"],
|
|
21950
22219
|
["eventIdPath", "--event-id-path"],
|
|
21951
22220
|
["eventTypePath", "--event-type-path"],
|
|
@@ -21962,7 +22231,7 @@ const FEED_BIND_WEB_INTENT_SOURCES = ["rb2b"];
|
|
|
21962
22231
|
const FEED_BIND_WEB_INTENT_REFUSED_FLAGS = [
|
|
21963
22232
|
["schemaJson", "--schema-json"],
|
|
21964
22233
|
...FEED_BIND_PULL_ONLY_FLAGS,
|
|
21965
|
-
...FEED_BIND_PUSH_ONLY_FLAGS.filter(([key]) => key !== "create"),
|
|
22234
|
+
...FEED_BIND_PUSH_ONLY_FLAGS.filter(([key]) => key !== "create" && key !== "project"),
|
|
21966
22235
|
];
|
|
21967
22236
|
// Pull mode keeps the contract commander enforced when --kind/--tool-id/
|
|
21968
22237
|
// --request-json were requiredOptions and <table> was required: same messages, same
|
|
@@ -22005,6 +22274,8 @@ function readFeedPushBindBody(table, options) {
|
|
|
22005
22274
|
const rowSchema = parseJsonObject(readFileIfPresent(schemaSource));
|
|
22006
22275
|
const tableRef = readOption(table);
|
|
22007
22276
|
const create = readOption(options.create);
|
|
22277
|
+
if (readOption(options.project) && !create)
|
|
22278
|
+
throw new OxygenError("invalid_request", "--project requires --create; the existing table keeps its project.", { exitCode: 2 });
|
|
22008
22279
|
if (tableRef && create) {
|
|
22009
22280
|
throw new OxygenError("invalid_request", "Pass a table or --create <name>, not both.", { exitCode: 1 });
|
|
22010
22281
|
}
|
|
@@ -22018,7 +22289,7 @@ function readFeedPushBindBody(table, options) {
|
|
|
22018
22289
|
return {
|
|
22019
22290
|
transport: "push",
|
|
22020
22291
|
row_schema: rowSchema,
|
|
22021
|
-
...(tableRef ? { table: tableRef } : { create_table: { name: create } }),
|
|
22292
|
+
...(tableRef ? { table: tableRef } : { create_table: { name: create, ...(readOption(options.project) ? { project: readOption(options.project) } : {}) } }),
|
|
22022
22293
|
...(readOption(options.name) ? { name: readOption(options.name) } : {}),
|
|
22023
22294
|
...(readOption(options.upsertKey) ? { upsert_key: readOption(options.upsertKey) } : {}),
|
|
22024
22295
|
...(readOption(options.itemsPath) ? { items_path: readOption(options.itemsPath) } : {}),
|
|
@@ -22044,6 +22315,8 @@ function readFeedWebIntentBindBody(table, options) {
|
|
|
22044
22315
|
}
|
|
22045
22316
|
const tableRef = readOption(table);
|
|
22046
22317
|
const create = readOption(options.create);
|
|
22318
|
+
if (readOption(options.project) && !create)
|
|
22319
|
+
throw new OxygenError("invalid_request", "--project requires --create; the existing table keeps its project.", { exitCode: 2 });
|
|
22047
22320
|
if (tableRef && create) {
|
|
22048
22321
|
throw new OxygenError("invalid_request", "Pass a table or --create <name>, not both.", { exitCode: 1 });
|
|
22049
22322
|
}
|
|
@@ -22053,7 +22326,7 @@ function readFeedWebIntentBindBody(table, options) {
|
|
|
22053
22326
|
return {
|
|
22054
22327
|
transport: "push",
|
|
22055
22328
|
feed_kind: source,
|
|
22056
|
-
...(tableRef ? { table: tableRef } : { create_table: { name: create } }),
|
|
22329
|
+
...(tableRef ? { table: tableRef } : { create_table: { name: create, ...(readOption(options.project) ? { project: readOption(options.project) } : {}) } }),
|
|
22057
22330
|
...(readOption(options.name) ? { name: readOption(options.name) } : {}),
|
|
22058
22331
|
...(readOption(options.upsertKey) ? { upsert_key: readOption(options.upsertKey) } : {}),
|
|
22059
22332
|
};
|
|
@@ -22163,7 +22436,7 @@ function readSignalsSearchRunBody(options, promptArg) {
|
|
|
22163
22436
|
const plan = options.planJson ? readSearchPlanJson(options.planJson) : null;
|
|
22164
22437
|
// A post search needs no prompt: its --keywords and filters are the plan,
|
|
22165
22438
|
// as they are for the free preview (the server names the table after them).
|
|
22166
|
-
const postSearchFilters = !prompt && !plan && readOption(options.family) === "linkedin_posts"
|
|
22439
|
+
const postSearchFilters = !prompt && !plan && (readOption(options.source) ?? readOption(options.family)) === "linkedin_posts"
|
|
22167
22440
|
? readSignalSearchFilters(options)
|
|
22168
22441
|
: null;
|
|
22169
22442
|
const postSearch = Array.isArray(postSearchFilters?.keywords) && postSearchFilters.keywords.length > 0;
|
|
@@ -22189,6 +22462,7 @@ function readSignalsSearchRunBody(options, promptArg) {
|
|
|
22189
22462
|
}
|
|
22190
22463
|
const filters = prompt ? typedFilters : postSearchFilters;
|
|
22191
22464
|
return {
|
|
22465
|
+
...(readOption(options.source) ? { source: readOption(options.source) } : {}),
|
|
22192
22466
|
...(prompt ? { prompt } : {}),
|
|
22193
22467
|
...(plan ? { plan } : {}),
|
|
22194
22468
|
...(readOption(options.family) ? { family: readOption(options.family) } : {}),
|
|
@@ -23582,6 +23856,19 @@ function domainsBuyRerunCommand(domains, quoteId, options, data) {
|
|
|
23582
23856
|
];
|
|
23583
23857
|
return `${resolveCliBinaryName()} domains buy ${domains.join(" ")} ${flags.join(" ")}`;
|
|
23584
23858
|
}
|
|
23859
|
+
function readDnsDkimOption(value) {
|
|
23860
|
+
let records;
|
|
23861
|
+
try {
|
|
23862
|
+
records = JSON.parse(value);
|
|
23863
|
+
}
|
|
23864
|
+
catch {
|
|
23865
|
+
throw new OxygenError("invalid_request", "--dkim-json must be a JSON array of DKIM records.", { exitCode: 2 });
|
|
23866
|
+
}
|
|
23867
|
+
if (!Array.isArray(records) || records.some((record) => !isRecord(record) || !["TXT", "CNAME"].includes(String(record.type).toUpperCase()) || !readRecordString(record, "name") || !readRecordString(record, "content"))) {
|
|
23868
|
+
throw new OxygenError("invalid_request", "Each --dkim-json record requires type TXT or CNAME, name, and content.", { exitCode: 2 });
|
|
23869
|
+
}
|
|
23870
|
+
return records;
|
|
23871
|
+
}
|
|
23585
23872
|
function dnsPlanBody(options) {
|
|
23586
23873
|
const provider = readOption(options.provider);
|
|
23587
23874
|
if (!provider) {
|
|
@@ -23593,6 +23880,7 @@ function dnsPlanBody(options) {
|
|
|
23593
23880
|
...(readOption(options.dmarcRua) ? { dmarc_rua: readOption(options.dmarcRua) } : {}),
|
|
23594
23881
|
...(readOption(options.trackingHost) ? { tracking_host: readOption(options.trackingHost) } : {}),
|
|
23595
23882
|
...(options.autoDkim === false ? { auto_dkim: false } : {}),
|
|
23883
|
+
...(options.dkimJson !== undefined ? { dkim: readDnsDkimOption(options.dkimJson) } : {}),
|
|
23596
23884
|
};
|
|
23597
23885
|
}
|
|
23598
23886
|
// Preview the cold-email DNS plan for a domain (free — no writes). Appends a
|
|
@@ -23602,7 +23890,12 @@ async function runDomainsDnsPlan(domain, options) {
|
|
|
23602
23890
|
const planHash = readRecordString(data, "plan_hash");
|
|
23603
23891
|
const provider = readOption(options.provider);
|
|
23604
23892
|
return planHash && provider
|
|
23605
|
-
? { ...data, rerun_command:
|
|
23893
|
+
? { ...data, rerun_command: [resolveCliBinaryName(), "domains dns apply", shellQuote(domain), "--provider", shellQuote(provider), "--approved --plan", shellQuote(planHash),
|
|
23894
|
+
...(options.dkimJson !== undefined ? ["--dkim-json", shellQuote(options.dkimJson)] : []),
|
|
23895
|
+
...(readOption(options.dmarcPolicy) ? ["--dmarc-policy", shellQuote(options.dmarcPolicy)] : []),
|
|
23896
|
+
...(readOption(options.dmarcRua) ? ["--dmarc-rua", shellQuote(options.dmarcRua)] : []),
|
|
23897
|
+
...(readOption(options.trackingHost) ? ["--tracking-host", shellQuote(options.trackingHost)] : []),
|
|
23898
|
+
...(options.autoDkim === false ? ["--no-auto-dkim"] : []),].join(" ") }
|
|
23606
23899
|
: data;
|
|
23607
23900
|
}
|
|
23608
23901
|
// Apply an approved DNS plan (writes records). Requires --plan <hash> with
|
|
@@ -26315,7 +26608,15 @@ function formatSequenceListHealth(data) {
|
|
|
26315
26608
|
lines.push("No sequences match these filters.");
|
|
26316
26609
|
}
|
|
26317
26610
|
else {
|
|
26318
|
-
lines.push(...renderTextTable(["SEQUENCE", "PERSISTED", "OPERATIONAL", "OPEN WORK", "SENT / FAILED", "REASONS"], sequences.map((sequence) =>
|
|
26611
|
+
lines.push(...renderTextTable(["SEQUENCE", "PERSISTED", "OPERATIONAL", "OPEN WORK", "SENT / FAILED", "REASONS"], sequences.map((sequence) => {
|
|
26612
|
+
// Warnings print below the table, so the row points at them: a reader
|
|
26613
|
+
// skimming the rows missed the deleted-row warning (T-220 acceptance).
|
|
26614
|
+
const row = sequenceHealthRow(sequence);
|
|
26615
|
+
const warningCount = sequenceRowWarnings([sequence]).length;
|
|
26616
|
+
if (warningCount > 0)
|
|
26617
|
+
row[row.length - 1] = `${row[row.length - 1]}; ${warningCount} warning${warningCount === 1 ? "" : "s"} below`;
|
|
26618
|
+
return row;
|
|
26619
|
+
})));
|
|
26319
26620
|
const warnings = sequenceRowWarnings(sequences);
|
|
26320
26621
|
if (warnings.length > 0)
|
|
26321
26622
|
lines.push("", ...warnings);
|
|
@@ -26466,7 +26767,10 @@ function formatSequenceStatsHealth(data) {
|
|
|
26466
26767
|
if (failureMix !== "—")
|
|
26467
26768
|
lines.push(`Failure mix: ${failureMix}`);
|
|
26468
26769
|
lines.push(...formatActionKindBreakdown(operational.failureAnalytics));
|
|
26469
|
-
|
|
26770
|
+
// Every lead ever enrolled, removed and finished included, so name the split
|
|
26771
|
+
// beside it: "Enrolled: 6" next to 4 in progress read as a miscount (T-220 acceptance).
|
|
26772
|
+
const byStatus = formatCountRecord(recordValue(stats.byStatus));
|
|
26773
|
+
lines.push("", `Enrolled (all time): ${numberText(stats.enrolled)}${byStatus === "—" ? "" : ` — ${byStatus}`}`);
|
|
26470
26774
|
const replyLine = formatSequenceReplyRollup(stats.replies);
|
|
26471
26775
|
if (replyLine)
|
|
26472
26776
|
lines.push(replyLine);
|
|
@@ -27881,12 +28185,26 @@ function buildFeedbackBody(options) {
|
|
|
27881
28185
|
context: {
|
|
27882
28186
|
cli_version: OXYGEN_VERSION,
|
|
27883
28187
|
environment: collectFeedbackEnvironment(),
|
|
28188
|
+
...(readCsvOption(options.deepLinks).length ? { deep_links: readCsvOption(options.deepLinks) } : {}),
|
|
28189
|
+
...(readOption(options.operation) ? { operation: readOption(options.operation) } : {}),
|
|
28190
|
+
...(readOption(options.errorCode) ? { error_code: readOption(options.errorCode) } : {}),
|
|
28191
|
+
...(readCsvOption(options.runIds).length ? { run_ids: readCsvOption(options.runIds) } : {}),
|
|
28192
|
+
...(readCsvOption(options.tableIds).length ? { table_ids: readCsvOption(options.tableIds) } : {}),
|
|
28193
|
+
...(readOption(options.slackPermalink) ? { slack_permalink: readOption(options.slackPermalink) } : {}),
|
|
28194
|
+
...(readOption(options.slackChannelId) ? { slack_channel_id: readOption(options.slackChannelId) } : {}),
|
|
28195
|
+
...(readOption(options.customerWorkspace) ? { customer_workspace: readOption(options.customerWorkspace) } : {}),
|
|
27884
28196
|
},
|
|
27885
28197
|
};
|
|
27886
28198
|
if (readOption(options.severity))
|
|
27887
28199
|
body.severity = readOption(options.severity);
|
|
27888
28200
|
const sessionId = readOption(options.sessionId);
|
|
27889
28201
|
const file = readOption(options.file);
|
|
28202
|
+
if (options.transcript !== false && options.transcriptText !== undefined) {
|
|
28203
|
+
if (sessionId || file || options.includeTranscript) {
|
|
28204
|
+
throw new OxygenError("invalid_input", "Choose --transcript-text or one local transcript option.", { exitCode: 1 });
|
|
28205
|
+
}
|
|
28206
|
+
body.transcript_text = readSelectedTranscriptExcerpt(options.transcriptText);
|
|
28207
|
+
}
|
|
27890
28208
|
const includeTranscript = options.transcript !== false &&
|
|
27891
28209
|
(options.includeTranscript === true || Boolean(sessionId) || Boolean(file));
|
|
27892
28210
|
if (includeTranscript) {
|
|
@@ -27920,7 +28238,13 @@ function buildSupportTicketBody(options) {
|
|
|
27920
28238
|
const body = { subject: readOption(options.subject), body: message };
|
|
27921
28239
|
const sessionId = readOption(options.sessionId);
|
|
27922
28240
|
const file = readOption(options.transcriptFile);
|
|
27923
|
-
if (options.
|
|
28241
|
+
if (options.transcript !== false && options.transcriptText !== undefined) {
|
|
28242
|
+
if (options.includeTranscript || sessionId || file) {
|
|
28243
|
+
throw new OxygenError("invalid_input", "Choose --transcript-text or one local transcript option.", { exitCode: 1 });
|
|
28244
|
+
}
|
|
28245
|
+
body.transcript_text = readSelectedTranscriptExcerpt(options.transcriptText);
|
|
28246
|
+
}
|
|
28247
|
+
if (options.transcript !== false && (options.includeTranscript || sessionId || file)) {
|
|
27924
28248
|
try {
|
|
27925
28249
|
const captured = captureCurrentTranscript({ sessionId, file });
|
|
27926
28250
|
if (!captured) {
|
|
@@ -27943,6 +28267,8 @@ function buildSupportTicketBody(options) {
|
|
|
27943
28267
|
if (readOption(options.source))
|
|
27944
28268
|
body.source = readOption(options.source);
|
|
27945
28269
|
const context = {};
|
|
28270
|
+
if (readOption(options.cliVersion))
|
|
28271
|
+
context.cli_version = readOption(options.cliVersion);
|
|
27946
28272
|
if (readOption(options.operation))
|
|
27947
28273
|
context.operation = readOption(options.operation);
|
|
27948
28274
|
if (readOption(options.errorCode))
|
|
@@ -27966,6 +28292,13 @@ function buildSupportTicketBody(options) {
|
|
|
27966
28292
|
body.context = context;
|
|
27967
28293
|
return body;
|
|
27968
28294
|
}
|
|
28295
|
+
function readSelectedTranscriptExcerpt(text) {
|
|
28296
|
+
const excerpt = text.trim();
|
|
28297
|
+
if (!excerpt || Buffer.byteLength(excerpt, "utf8") > 200_000) {
|
|
28298
|
+
throw new OxygenError("invalid_transcript", "Transcript excerpt must be between 1 and 200,000 bytes.", { exitCode: 1 });
|
|
28299
|
+
}
|
|
28300
|
+
return redactTranscriptSecrets(excerpt);
|
|
28301
|
+
}
|
|
27969
28302
|
function withSupportListQuery(path, options) {
|
|
27970
28303
|
const params = new URLSearchParams();
|
|
27971
28304
|
const status = readOption(options.status);
|
|
@@ -28402,7 +28735,10 @@ function buildContextAssetUpsertBody(options) {
|
|
|
28402
28735
|
}
|
|
28403
28736
|
function buildKnowledgePageUpsertBody(options) {
|
|
28404
28737
|
const tags = readCsvOption(options.tags);
|
|
28405
|
-
const expectedRevision =
|
|
28738
|
+
const expectedRevision = options.expectedRevision === undefined ? undefined : Number(options.expectedRevision);
|
|
28739
|
+
if (expectedRevision !== undefined && (!options.expectedRevision?.trim() || !Number.isInteger(expectedRevision) || expectedRevision < 0)) {
|
|
28740
|
+
throw new OxygenError("invalid_request", "--expected-revision must be a nonnegative integer.", { exitCode: 2 });
|
|
28741
|
+
}
|
|
28406
28742
|
const folder = readOption(options.folder);
|
|
28407
28743
|
return {
|
|
28408
28744
|
...(folder ? { folderId: folder === "root" ? null : folder } : {}),
|
|
@@ -29215,6 +29551,66 @@ function readCompanyIdentityOptions(options, verb) {
|
|
|
29215
29551
|
function readRepeatableIds(values) {
|
|
29216
29552
|
return [...new Set((values ?? []).flatMap((value) => readCsvOption(value)))];
|
|
29217
29553
|
}
|
|
29554
|
+
// `sequences contacts-remove` for a human reader: what the call selects, the
|
|
29555
|
+
// people it names (a --missing-source-rows preview exists so nobody approves a
|
|
29556
|
+
// removal by UUID), and the next step. stderr, like the other summaries.
|
|
29557
|
+
function writeContactsRemoveSummary(data) {
|
|
29558
|
+
const payload = asPayloadRecord(data);
|
|
29559
|
+
if (!payload)
|
|
29560
|
+
return;
|
|
29561
|
+
const count = (value) => (typeof value === "number" ? value : 0);
|
|
29562
|
+
const lines = [];
|
|
29563
|
+
const leads = asPayloadRecord(payload.leads);
|
|
29564
|
+
const approved = payload.approved === true;
|
|
29565
|
+
if (payload.mode === "missing_source_rows") {
|
|
29566
|
+
const total = count(leads?.total);
|
|
29567
|
+
// Counts recorded deletions only, so zero is never read as "none deleted".
|
|
29568
|
+
lines.push(`${total} enrolled lead${total === 1 ? " has" : "s have"} a recorded deletion from this campaign's source table.`);
|
|
29569
|
+
}
|
|
29570
|
+
lines.push(approved
|
|
29571
|
+
? `Removed ${count(payload.removed)} from the campaign.`
|
|
29572
|
+
: `Preview: ${count(payload.removable)} removable, ${count(payload.contacted)} already contacted, ${count(payload.skippedTerminal)} already finished.`);
|
|
29573
|
+
// What approving costs and touches, stated where the reader decides; with
|
|
29574
|
+
// nothing removable there is nothing to approve, so no line invites it.
|
|
29575
|
+
if (!approved && count(payload.removable) > 0 && typeof payload.credits_used === "number") {
|
|
29576
|
+
lines.push(`Cost: ${payload.credits_used} credits. Approving stops only these campaign enrollments: nothing is sent, and table rows are kept.`);
|
|
29577
|
+
}
|
|
29578
|
+
const items = Array.isArray(leads?.items) ? leads.items : [];
|
|
29579
|
+
for (const entry of items) {
|
|
29580
|
+
const lead = asPayloadRecord(entry);
|
|
29581
|
+
if (!lead)
|
|
29582
|
+
continue;
|
|
29583
|
+
const name = typeof lead.name === "string" && lead.name.trim() ? lead.name.trim() : "(unnamed lead)";
|
|
29584
|
+
// After approval the server reports each lead's new state; "stopped" is this removal.
|
|
29585
|
+
const state = approved
|
|
29586
|
+
? lead.status === "stopped" ? "removed" : `not removed (${String(lead.status ?? "unknown")})`
|
|
29587
|
+
: String(lead.status ?? "unknown");
|
|
29588
|
+
lines.push(` ${name} — ${state}${lead.contacted === true ? ", already contacted" : ""}`);
|
|
29589
|
+
}
|
|
29590
|
+
// One call covers at most per_call_limit leads (the oldest); the server's
|
|
29591
|
+
// next_step says to repeat for the rest, so count the unnamed ones in this call.
|
|
29592
|
+
const total = count(leads?.total);
|
|
29593
|
+
const covered = typeof payload.per_call_limit === "number" ? Math.min(total, payload.per_call_limit) : total;
|
|
29594
|
+
const unnamed = covered - items.length;
|
|
29595
|
+
if (unnamed > 0)
|
|
29596
|
+
lines.push(` …and ${unnamed} more${covered < total ? " in this call" : ""}`);
|
|
29597
|
+
if (typeof payload.next_step === "string")
|
|
29598
|
+
lines.push(payload.next_step);
|
|
29599
|
+
if (typeof payload.web_url === "string")
|
|
29600
|
+
lines.push(`Open: ${payload.web_url}`);
|
|
29601
|
+
process.stderr.write(`${lines.join("\n")}\n`);
|
|
29602
|
+
}
|
|
29603
|
+
// `tables delete-row|delete-rows` for a human reader: a deleted row's lead stays
|
|
29604
|
+
// enrolled in its campaigns, so name them where the delete was typed.
|
|
29605
|
+
function writeRowDeleteWarnings(data) {
|
|
29606
|
+
const payload = asPayloadRecord(data);
|
|
29607
|
+
const warnings = Array.isArray(payload?.warnings) ? payload.warnings : [];
|
|
29608
|
+
for (const entry of warnings) {
|
|
29609
|
+
const warning = asPayloadRecord(entry);
|
|
29610
|
+
if (warning && typeof warning.message === "string")
|
|
29611
|
+
process.stderr.write(`Warning: ${warning.message}\n`);
|
|
29612
|
+
}
|
|
29613
|
+
}
|
|
29218
29614
|
// Human-mode summary of a company pause/resume/list on stderr (stdout keeps the
|
|
29219
29615
|
// machine-readable envelope): what was held or resumed, per campaign, and where
|
|
29220
29616
|
// to see it.
|
|
@@ -29339,6 +29735,8 @@ function readSequenceSettings(options) {
|
|
|
29339
29735
|
// A visible footer mutates outbound copy, so it is opt-in and tri-state:
|
|
29340
29736
|
// absent leaves the stored value alone; either explicit flag persists the
|
|
29341
29737
|
// chosen boolean. Missing/false is interpreted as OFF by the dispatcher.
|
|
29738
|
+
if (options.autoOptimize !== undefined)
|
|
29739
|
+
settings.auto_optimize = options.autoOptimize;
|
|
29342
29740
|
if (options.bounceProtection !== undefined)
|
|
29343
29741
|
settings.bounce_protection = JSON.parse(options.bounceProtection);
|
|
29344
29742
|
if (options.includeUnsubscribeHeaders !== undefined)
|
|
@@ -29399,7 +29797,7 @@ function readSequenceSettings(options) {
|
|
|
29399
29797
|
// Sender profiles (--sender-profiles id1,id2): the sequence sends from these
|
|
29400
29798
|
// unified identities. The server resolves them into senderAccountIds + mailbox_ids.
|
|
29401
29799
|
const senderProfileIds = readCsvOption(options.senderProfiles);
|
|
29402
|
-
if (
|
|
29800
|
+
if (options.senderProfiles !== undefined)
|
|
29403
29801
|
settings.sender_profile_ids = senderProfileIds;
|
|
29404
29802
|
return Object.keys(settings).length > 0 ? settings : undefined;
|
|
29405
29803
|
}
|
|
@@ -29486,6 +29884,14 @@ catch (error) {
|
|
|
29486
29884
|
// envelope; agents parse stdout and branch on the exit code, and the exit
|
|
29487
29885
|
// table classes a usage error as 2 in every mode.
|
|
29488
29886
|
const usage = commanderUsageFailure(program, cliArgs, error, hints[0] ?? null);
|
|
29887
|
+
if (usage?.envelope.meta.command === "columns rename" &&
|
|
29888
|
+
usage.envelope.error.code === "missing_required_option" &&
|
|
29889
|
+
isRecord(usage.envelope.error.details) &&
|
|
29890
|
+
usage.envelope.error.details.option === "--key") {
|
|
29891
|
+
const hint = `Hint: --key <new-key> is required for \`${program.name()} columns rename <table> <column> --key <new-key>\`. To change only the display label, run \`${program.name()} columns update <table> <column> --label <label>\`.`;
|
|
29892
|
+
writeHint(hint);
|
|
29893
|
+
usage.envelope.error.details.next_step = hint.replace(/^Hint:\s*/, "");
|
|
29894
|
+
}
|
|
29489
29895
|
if (usage && argvRequestsJson(cliArgs))
|
|
29490
29896
|
writeJson(usage.envelope);
|
|
29491
29897
|
process.exitCode = usage?.exitCode ?? error.exitCode;
|