@oxygen-agent/cli 1.1011.405 → 1.1011.446
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 +8 -4
- package/dist/connectors-commands.js +3 -0
- package/dist/contract-flags.js +22 -9
- 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 +33 -0
- package/dist/http-client.d.ts +2 -0
- package/dist/http-client.js +41 -1
- package/dist/index.js +470 -174
- 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.js +5 -4
- 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 +1 -1
- package/node_modules/@oxygen/shared/dist/bounded-read-result.js +2 -2
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +17 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +359 -34
- 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/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/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/database.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/contracts/database.js +29 -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 +67 -7
- 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 +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.js +12 -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/plan-limits.d.ts +1 -2
- package/node_modules/@oxygen/shared/dist/plan-limits.js +2 -2
- 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/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/package.json +3 -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,4 +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";
|
|
1
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";
|
|
2
8
|
import { execFileSync } from "node:child_process";
|
|
3
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
10
|
import { closeSync, createReadStream, createWriteStream, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
@@ -11,12 +17,13 @@ import { Command, CommanderError, Option } from "commander";
|
|
|
11
17
|
import { registerUgcCommands } from "./ugc-commands.js";
|
|
12
18
|
import { registerConnectorsCommands } from "./connectors-commands.js";
|
|
13
19
|
import { registerSettingsCommands } from "./settings-commands.js";
|
|
20
|
+
import { registerCustomerOperationCommands } from "./customer-operation-commands.js";
|
|
14
21
|
import { registerKnowledgeRepositoryCommands } from "./knowledge-repository-commands.js";
|
|
15
22
|
import { registerVisualCommands } from "./visual-commands.js";
|
|
16
23
|
import { renderPrimaryProviderBoard } from "./admin-primary-providers-render.js";
|
|
17
24
|
import { registerFunctionsCommands } from "./functions-commands.js";
|
|
18
25
|
import { STREAMED_FILE_IMPORT_MIN_BYTES, putImportFileStream, rowsForStreamedImportTarget, scanStreamedImportFile, shouldStreamFileImport, } from "./streamed-file-import.js";
|
|
19
|
-
import { applyOxygenHelp, argvRequestsJson, commanderUsageFailure, enableCommandSuggestions, unknownCommandHint, unknownOptionHint, withoutHelpForUnknownCommand, } from "./help.js";
|
|
26
|
+
import { applyOxygenHelp, argvRequestsJson, commanderUsageFailure, enableCommandSuggestions, unknownCommandHint, unknownOptionHint, withoutHelpForUnknownCommand, validateRequiredOptionValues, } from "./help.js";
|
|
20
27
|
import { buildCommandManifest, getCommandManifestEntry, searchCommandManifest, suggestCommandNames, } from "./command-manifest.js";
|
|
21
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";
|
|
22
29
|
import { PURCHASABLE_PLAN_KEYS, planVolumeBonusPercent, resolveBasePricingPlan } from "@oxygen/shared/billing";
|
|
@@ -28,7 +35,9 @@ import { SEQUENCE_LIFECYCLES, sequenceDisplayStatus } from "@oxygen/shared/seque
|
|
|
28
35
|
import { operationPath } from "@oxygen/shared/contracts/operations";
|
|
29
36
|
import { SENDER_LIMITS_SET_OPERATION } from "@oxygen/shared/contracts/sender-limits";
|
|
30
37
|
import { addContractFlags, readContractFlags } from "./contract-flags.js";
|
|
38
|
+
import { TABLE_QUERY_GRID_CONTRACT } from "@oxygen/shared/contracts/table-query-grid";
|
|
31
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";
|
|
32
41
|
import { buildSenderLimitsBody, parseWorkingDaysOption, SENDER_LIMITS_FLAG_OVERLAY } from "./sender-limits-flags.js";
|
|
33
42
|
import { readColumnDecisionFlags } from "./column-decision-options.js";
|
|
34
43
|
import { MANAGED_DATA_SUPPLIERS } from "@oxygen/shared/data-suppliers";
|
|
@@ -47,8 +56,9 @@ import { formatAiPromptPreviewNotice, formatColumnReferenceNotices, formatEnrich
|
|
|
47
56
|
import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
|
|
48
57
|
import { formatSearchAiFilterTotalsNotice } from "./search-ai-filter-notice.js";
|
|
49
58
|
import { formatInboxNeedsReplyNotice } from "./inbox-needs-reply-notice.js";
|
|
59
|
+
import { formatMailboxRampNotice } from "./mailbox-ramp-notice.js";
|
|
50
60
|
import { formatWorkflowPlanLimitNotices } from "./workflow-plan-limit-notices.js";
|
|
51
|
-
import { captureCurrentTranscript, collectFeedbackEnvironment, TranscriptCaptureError, } from "./transcript.js";
|
|
61
|
+
import { captureCurrentTranscript, collectFeedbackEnvironment, redactTranscriptSecrets, TranscriptCaptureError, } from "./transcript.js";
|
|
52
62
|
import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, updateSessionStep, } from "./session.js";
|
|
53
63
|
import { doctorAgentSkills, getAgentSkill, installAgentSkills, listAgentSkills, runAutomaticSkillsInstall, searchAgentSkills, } from "./skills.js";
|
|
54
64
|
import { resolveCliBinaryName } from "./runtime.js";
|
|
@@ -282,6 +292,11 @@ function buildFindBody(capability, options) {
|
|
|
282
292
|
body.verify = true;
|
|
283
293
|
return body;
|
|
284
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
|
+
};
|
|
285
300
|
/** The shared search/buy filters, in the snake_case the voice numbers route reads. */
|
|
286
301
|
function voiceNumberFilters(options) {
|
|
287
302
|
const areaCode = readOption(options.areaCode);
|
|
@@ -293,6 +308,13 @@ function voiceNumberFilters(options) {
|
|
|
293
308
|
...(country ? { iso_country: country.toUpperCase() } : {}),
|
|
294
309
|
};
|
|
295
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
|
+
}
|
|
296
318
|
function suppressionListParams(options) {
|
|
297
319
|
const params = new URLSearchParams();
|
|
298
320
|
const reason = readOption(options.reason);
|
|
@@ -855,6 +877,7 @@ async function handleAsyncAction(command, options, action) {
|
|
|
855
877
|
writeCreditsReceipt(data);
|
|
856
878
|
writeSearchAiFilterTotalsNotice(data);
|
|
857
879
|
writeInboxNeedsReplyNotice(data);
|
|
880
|
+
writeMailboxRampNotice(data);
|
|
858
881
|
}
|
|
859
882
|
catch (error) {
|
|
860
883
|
emitCliFailure(command, error);
|
|
@@ -1165,6 +1188,11 @@ function writeInboxNeedsReplyNotice(data) {
|
|
|
1165
1188
|
if (notice)
|
|
1166
1189
|
process.stderr.write(`${notice}\n`);
|
|
1167
1190
|
}
|
|
1191
|
+
function writeMailboxRampNotice(data) {
|
|
1192
|
+
const notice = formatMailboxRampNotice(data);
|
|
1193
|
+
if (notice)
|
|
1194
|
+
process.stderr.write(`${notice}\n`);
|
|
1195
|
+
}
|
|
1168
1196
|
function writeAiPromptPreviewNotice(data) {
|
|
1169
1197
|
const notice = formatAiPromptPreviewNotice(data);
|
|
1170
1198
|
if (notice)
|
|
@@ -2146,6 +2174,9 @@ function buildCrmSyncRunBody(options) {
|
|
|
2146
2174
|
provider: options.provider,
|
|
2147
2175
|
object: options.object,
|
|
2148
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) } : {}),
|
|
2149
2180
|
...(readPositiveInt(options.maxRows) !== undefined ? { max_rows_per_cycle: readPositiveInt(options.maxRows) } : {}),
|
|
2150
2181
|
...(readPositiveNumber(options.maxCredits) !== undefined ? { max_credits: readPositiveNumber(options.maxCredits) } : {}),
|
|
2151
2182
|
...(options.approved ? { approved: true } : {}),
|
|
@@ -2199,11 +2230,16 @@ function buildCrmSyncLinksPath(object, rowId) {
|
|
|
2199
2230
|
params.set("row_id", rowId);
|
|
2200
2231
|
return `/api/cli/crm/sync/links?${params.toString()}`;
|
|
2201
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
|
+
};
|
|
2202
2236
|
function buildCrmEnrichmentListPath(options) {
|
|
2203
|
-
const
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
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";
|
|
2207
2243
|
}
|
|
2208
2244
|
// The delta form: flip one named enrichment and leave every other decision
|
|
2209
2245
|
// (the rest of the presets, the ceiling, force, the trigger set) exactly as it
|
|
@@ -2266,6 +2302,8 @@ function buildNotetakerSetupBody(options) {
|
|
|
2266
2302
|
capture_mode: "invited_bot",
|
|
2267
2303
|
auto_record_scope: "manual",
|
|
2268
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 } : {}),
|
|
2269
2307
|
...(readPositiveInt(options.defaultMaxMinutes) ? { default_max_minutes: readPositiveInt(options.defaultMaxMinutes) } : {}),
|
|
2270
2308
|
...(readPositiveNumber(options.defaultMaxCredits) ? { default_max_credits: readPositiveNumber(options.defaultMaxCredits) } : {}),
|
|
2271
2309
|
...(readPositiveInt(options.retentionDays) ? { retention_days: readPositiveInt(options.retentionDays) } : {}),
|
|
@@ -2352,6 +2390,7 @@ function buildPublishingPostCreateBody(options) {
|
|
|
2352
2390
|
const title = readOption(options.title);
|
|
2353
2391
|
const timezone = readOption(options.timezone);
|
|
2354
2392
|
const ugcParticipationId = readOption(options.ugcParticipationId);
|
|
2393
|
+
const idempotencyKey = readOption(options.idempotencyKey);
|
|
2355
2394
|
const content = buildPublishingContent(options);
|
|
2356
2395
|
if (provider)
|
|
2357
2396
|
body.provider = provider;
|
|
@@ -2365,6 +2404,10 @@ function buildPublishingPostCreateBody(options) {
|
|
|
2365
2404
|
body.title = title;
|
|
2366
2405
|
if (timezone)
|
|
2367
2406
|
body.timezone = timezone;
|
|
2407
|
+
if (idempotencyKey)
|
|
2408
|
+
body.idempotency_key = idempotencyKey;
|
|
2409
|
+
if (options.tags !== undefined)
|
|
2410
|
+
body.tags = splitCommaList(options.tags);
|
|
2368
2411
|
if (ugcParticipationId) {
|
|
2369
2412
|
if (options.approved) {
|
|
2370
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 });
|
|
@@ -2582,9 +2625,9 @@ async function uploadPublishingMediaFile(filePath, options) {
|
|
|
2582
2625
|
},
|
|
2583
2626
|
});
|
|
2584
2627
|
const uploadUrl = readRecordString(ticket, "upload_url");
|
|
2585
|
-
const
|
|
2586
|
-
if (!uploadUrl || !
|
|
2587
|
-
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 });
|
|
2588
2631
|
}
|
|
2589
2632
|
const headers = ticket.upload_headers && typeof ticket.upload_headers === "object"
|
|
2590
2633
|
? Object.fromEntries(Object.entries(ticket.upload_headers).map(([key, value]) => [key, String(value)]))
|
|
@@ -2593,7 +2636,7 @@ async function uploadPublishingMediaFile(filePath, options) {
|
|
|
2593
2636
|
if (!put.ok) {
|
|
2594
2637
|
throw new OxygenError("publishing_media_upload_failed", `Object storage rejected the upload (${put.status}).`, { exitCode: 1 });
|
|
2595
2638
|
}
|
|
2596
|
-
return await requestOxygen(
|
|
2639
|
+
return await requestOxygen(`/api/cli/publishing/media/${encodeURIComponent(mediaId)}/uploaded`, { method: "POST", body: {} });
|
|
2597
2640
|
}
|
|
2598
2641
|
function buildPublishingMediaUploadUrlBody(options) {
|
|
2599
2642
|
const fileName = readOption(options.fileName);
|
|
@@ -3069,6 +3112,7 @@ function publishingAmplificationTunables(options) {
|
|
|
3069
3112
|
const commentMin = readNonNegativeInt(options.commentDelayMin);
|
|
3070
3113
|
const commentMax = readNonNegativeInt(options.commentDelayMax);
|
|
3071
3114
|
return {
|
|
3115
|
+
...(options.metadataJson !== undefined ? { metadata: parseJsonObject(readFileIfPresent(options.metadataJson)) } : {}),
|
|
3072
3116
|
...(maxActions !== undefined ? { max_actions_per_post: maxActions } : {}),
|
|
3073
3117
|
...(commentPool ? { comment_source: "pool", comment_pool: commentPool } : {}),
|
|
3074
3118
|
...(reactionMin !== undefined ? { reaction_delay_min_seconds: reactionMin } : {}),
|
|
@@ -3129,6 +3173,7 @@ function buildPublishingBoostBody(postId, options) {
|
|
|
3129
3173
|
const maxActions = readPositiveInt(options.maxActionsPerPost);
|
|
3130
3174
|
const commentPool = readPublishingCommentPool(options);
|
|
3131
3175
|
return {
|
|
3176
|
+
...(readOption(options.name) ? { name: readOption(options.name) } : {}),
|
|
3132
3177
|
senders,
|
|
3133
3178
|
actions,
|
|
3134
3179
|
max_credits: maxCredits,
|
|
@@ -3573,7 +3618,8 @@ export function createProgram() {
|
|
|
3573
3618
|
.version(OXYGEN_VERSION)
|
|
3574
3619
|
.option("--profile <name>", "Use a stored CLI profile for this command.")
|
|
3575
3620
|
.option("--org <organization>", "Use an organization id, Clerk org id, or slug for this command.");
|
|
3576
|
-
program.hook("preAction", () => {
|
|
3621
|
+
program.hook("preAction", (_root, selected) => {
|
|
3622
|
+
validateRequiredOptionValues(program, selected);
|
|
3577
3623
|
const options = program.opts();
|
|
3578
3624
|
if (options.profile) {
|
|
3579
3625
|
globalProfileFlag = options.profile;
|
|
@@ -4004,6 +4050,8 @@ export function createProgram() {
|
|
|
4004
4050
|
.requiredOption("-m, --message <message>", "Your message to OXYGEN support.")
|
|
4005
4051
|
.option("--severity <severity>", "New conversation only: low | normal | high. Defaults to normal.")
|
|
4006
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.")
|
|
4007
4055
|
.option("--json", "Print a JSON envelope.")
|
|
4008
4056
|
.addHelpText("after", `
|
|
4009
4057
|
Examples:
|
|
@@ -4013,29 +4061,42 @@ Examples:
|
|
|
4013
4061
|
.action(async (threadId, options) => {
|
|
4014
4062
|
const message = readOption(options.message);
|
|
4015
4063
|
if (threadId) {
|
|
4016
|
-
await handleAsyncAction("support chat", options, () =>
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
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
|
+
});
|
|
4020
4073
|
return;
|
|
4021
4074
|
}
|
|
4022
4075
|
const severity = readOption(options.severity);
|
|
4023
|
-
await handleAsyncAction("support chat", options, () =>
|
|
4024
|
-
|
|
4025
|
-
body: {
|
|
4076
|
+
await handleAsyncAction("support chat", options, () => {
|
|
4077
|
+
const body = {
|
|
4026
4078
|
message,
|
|
4027
4079
|
category: readOption(options.category) ?? "question",
|
|
4028
4080
|
...(severity ? { severity } : {}),
|
|
4029
|
-
}
|
|
4030
|
-
|
|
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
|
+
});
|
|
4031
4090
|
}))
|
|
4032
4091
|
.addCommand(new Command("file")
|
|
4033
|
-
.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.")
|
|
4034
4093
|
.requiredOption("--subject <subject>", "One-line summary of the problem.")
|
|
4035
4094
|
.requiredOption("--body <body>", "Required message: what you were doing, what happened, and what you tried.")
|
|
4036
4095
|
.option("--include-transcript", "Attach the most recent local agent transcript after redaction.")
|
|
4037
4096
|
.option("--session-id <id>", "Attach this local agent session transcript.")
|
|
4038
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.")
|
|
4039
4100
|
.option("--severity <severity>", "low | normal | high. Defaults to normal.")
|
|
4040
4101
|
.option("--category <category>", "Request type: question, bug, feature_request, billing, security_data, configuration, agency_directory, or other.")
|
|
4041
4102
|
.option("--source <source>", "Where the ticket originated. Only slack is supported today.")
|
|
@@ -4047,6 +4108,7 @@ Examples:
|
|
|
4047
4108
|
.option("--slack-permalink <url>", "Slack message permalink (https://*.slack.com/...) to attach as context.")
|
|
4048
4109
|
.option("--slack-channel-id <id>", "Slack channel id (e.g. C0123ABC456) the report came from.")
|
|
4049
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.")
|
|
4050
4112
|
.option("--json", "Print a JSON envelope.")
|
|
4051
4113
|
.action(async (options) => {
|
|
4052
4114
|
await handleAsyncAction("support ticket create", options, () => requestOxygen("/api/cli/support/tickets", {
|
|
@@ -4393,6 +4455,15 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4393
4455
|
.option("-m, --message <message>", "Your feedback or bug report.")
|
|
4394
4456
|
.option("--severity <severity>", "low | normal | high. Defaults to normal.")
|
|
4395
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.")
|
|
4396
4467
|
.option("--session-id <id>", "Explicitly attach a specific local session transcript by id.")
|
|
4397
4468
|
.option("--file <path>", "Explicitly attach a specific local transcript file.")
|
|
4398
4469
|
.option("--include-transcript", "Explicitly attach the most recently active local session after best-effort secret redaction. Review it for sensitive customer data first.")
|
|
@@ -4419,14 +4490,13 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4419
4490
|
.action(async (options) => {
|
|
4420
4491
|
await handleAsyncAction("db provision", options, () => requestOxygen("/api/cli/db/provision", { method: "POST", body: {} }));
|
|
4421
4492
|
}))
|
|
4422
|
-
.addCommand(new Command("migrate")
|
|
4423
|
-
.description("Apply pending tenant database migrations for the current organization.")
|
|
4424
|
-
.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 } })
|
|
4425
4495
|
.option("--json", "Print a JSON envelope.")
|
|
4426
4496
|
.action(async (options) => {
|
|
4427
4497
|
await handleAsyncAction("db migrate", options, () => requestOxygen("/api/cli/db/migrate", {
|
|
4428
4498
|
method: "POST",
|
|
4429
|
-
body: options
|
|
4499
|
+
body: readContractFlags(options, DATABASE_MIGRATE_CONTRACT, { rotate_credentials: { noNegate: true } }),
|
|
4430
4500
|
}));
|
|
4431
4501
|
}))
|
|
4432
4502
|
.addCommand(new Command("migrate-all")
|
|
@@ -4560,29 +4630,27 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4560
4630
|
},
|
|
4561
4631
|
}));
|
|
4562
4632
|
}))
|
|
4563
|
-
.addCommand(new Command("attach")
|
|
4633
|
+
.addCommand(addContractFlags(new Command("attach")
|
|
4564
4634
|
.description("Attach an existing Postgres database as the current organization's tenant database.")
|
|
4565
|
-
.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 } })
|
|
4566
4636
|
.option("--json", "Print a JSON envelope.")
|
|
4567
4637
|
.action(async (options) => {
|
|
4568
4638
|
await handleAsyncAction("db attach", options, () => requestOxygen("/api/cli/db/attach", {
|
|
4569
4639
|
method: "POST",
|
|
4570
|
-
body:
|
|
4640
|
+
body: readContractFlags(options, DATABASE_ATTACH_CONTRACT),
|
|
4571
4641
|
}));
|
|
4572
4642
|
}))
|
|
4573
|
-
.addCommand(new Command("query")
|
|
4643
|
+
.addCommand(addContractFlags(new Command("query")
|
|
4574
4644
|
.description("Run one read-only SQL statement against the current tenant database.")
|
|
4575
|
-
.requiredOption("--sql <sql>", "Single SELECT, WITH, or EXPLAIN statement.")
|
|
4576
|
-
.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)
|
|
4577
4646
|
.option("--json", "Print a JSON envelope.")
|
|
4578
4647
|
.action(async (options) => {
|
|
4579
4648
|
await handleAsyncAction("db query", options, () => {
|
|
4580
|
-
const maxRows = readPositiveInt(options.maxRows);
|
|
4581
4649
|
return requestOxygen("/api/cli/db/query", {
|
|
4582
4650
|
method: "POST",
|
|
4583
4651
|
body: {
|
|
4584
4652
|
sql: options.sql,
|
|
4585
|
-
...(
|
|
4653
|
+
...readContractFlags(options, DATABASE_QUERY_CONTRACT, DATABASE_QUERY_FLAG_OVERLAY),
|
|
4586
4654
|
},
|
|
4587
4655
|
});
|
|
4588
4656
|
});
|
|
@@ -4679,6 +4747,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4679
4747
|
.addCommand(new Command("setup")
|
|
4680
4748
|
.description("Configure the Recall-backed Oxygen Notetaker. Defaults to dry-run.")
|
|
4681
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.")
|
|
4682
4752
|
.option("--disabled", "Save the notetaker as disabled when used with --live.")
|
|
4683
4753
|
.option("--default-max-minutes <n>", "Default maximum meeting minutes to reserve. Defaults to 60.")
|
|
4684
4754
|
.option("--default-max-credits <n>", "Default spend cap for live schedules.")
|
|
@@ -4774,6 +4844,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4774
4844
|
.option("--draft", "Create as a draft instead of scheduled.")
|
|
4775
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.")
|
|
4776
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.")
|
|
4777
4849
|
.option("--thread-post <text>", "X: text of the next post in a thread (posts 2, 3, ... in order). Repeatable.", collectRepeatable, [])
|
|
4778
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, [])
|
|
4779
4851
|
.option("--poll-option <text>", "X: a poll option on the first post (2-4 options, 25 characters each). Repeatable.", collectRepeatable, [])
|
|
@@ -4912,6 +4984,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4912
4984
|
.addCommand(new Command("boost")
|
|
4913
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`.")
|
|
4914
4986
|
.argument("<post_id>", "Scheduled post id.")
|
|
4987
|
+
.option("--name <name>", "Name the post-scoped amplification policy.")
|
|
4915
4988
|
.requiredOption("--senders <ids>", "Comma-separated connected sender account ids that will engage.")
|
|
4916
4989
|
.requiredOption("--actions <list>", "Comma-separated actions: reaction, comment.")
|
|
4917
4990
|
.option("--max-actions-per-post <n>", "Hard cap on how many actions are planned (at most 12).")
|
|
@@ -4975,12 +5048,13 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4975
5048
|
});
|
|
4976
5049
|
}))
|
|
4977
5050
|
.addCommand(new Command("resolve-note")
|
|
4978
|
-
.description("
|
|
5051
|
+
.description("Resolve a review note, or reopen it.")
|
|
4979
5052
|
.argument("<post_id>", "Scheduled post id.")
|
|
4980
5053
|
.requiredOption("--comment <comment_id>", "Note id from `publishing posts notes`.")
|
|
5054
|
+
.option("--reopen", "Reopen the review note instead of resolving it.")
|
|
4981
5055
|
.option("--json", "Print a JSON envelope.")
|
|
4982
5056
|
.action(async (postId, options) => {
|
|
4983
|
-
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 } }));
|
|
4984
5058
|
})))
|
|
4985
5059
|
.addCommand(new Command("drafts")
|
|
4986
5060
|
.description("Review the AI post-draft queue before anything reaches the publish queue.")
|
|
@@ -5301,6 +5375,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5301
5375
|
.option("--max-credits <n>", "Hard credit cap per cycle (required — this policy spends credits).")
|
|
5302
5376
|
.option("--comment-pool <text>", "A comment to draw from. Repeatable. Required when --actions includes comment.", collectRepeatable, [])
|
|
5303
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.")
|
|
5304
5379
|
.option("--reaction-delay-min <seconds>", "Lower bound of the randomized reaction delay.")
|
|
5305
5380
|
.option("--reaction-delay-max <seconds>", "Upper bound of the randomized reaction delay.")
|
|
5306
5381
|
.option("--comment-delay-min <seconds>", "Lower bound of the randomized comment delay.")
|
|
@@ -5331,6 +5406,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5331
5406
|
.option("--max-credits <n>", "Hard credit cap per cycle.")
|
|
5332
5407
|
.option("--comment-pool <text>", "A comment to draw from. Repeatable.", collectRepeatable, [])
|
|
5333
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.")
|
|
5334
5410
|
.option("--reaction-delay-min <seconds>", "Lower bound of the randomized reaction delay.")
|
|
5335
5411
|
.option("--reaction-delay-max <seconds>", "Upper bound of the randomized reaction delay.")
|
|
5336
5412
|
.option("--comment-delay-min <seconds>", "Lower bound of the randomized comment delay.")
|
|
@@ -5851,9 +5927,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5851
5927
|
},
|
|
5852
5928
|
});
|
|
5853
5929
|
const uploadUrl = readRecordString(ticket, "upload_url");
|
|
5854
|
-
const
|
|
5855
|
-
|
|
5856
|
-
|
|
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 });
|
|
5857
5934
|
}
|
|
5858
5935
|
const controller = new AbortController();
|
|
5859
5936
|
const timer = setTimeout(() => controller.abort(), 300_000);
|
|
@@ -5872,11 +5949,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5872
5949
|
if (!put.ok) {
|
|
5873
5950
|
throw new OxygenError("attachment_upload_failed", `Object storage rejected the upload (${put.status}).`, { exitCode: 1 });
|
|
5874
5951
|
}
|
|
5875
|
-
|
|
5876
|
-
const recordRowId = readRecordString(ticket.record, "row_id");
|
|
5877
|
-
if (!fileId || !recordRowId)
|
|
5878
|
-
throw new OxygenError("attachment_upload_failed", "The upload reservation has no file or record id.", { exitCode: 1 });
|
|
5879
|
-
return requestOxygen(`/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(recordRowId)}/files/${encodeURIComponent(fileId)}/uploaded`, {
|
|
5952
|
+
return requestOxygen(`/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(reservedRowId)}/files/${encodeURIComponent(fileId)}/uploaded`, {
|
|
5880
5953
|
method: "POST",
|
|
5881
5954
|
body: readCrmFileLabel(options),
|
|
5882
5955
|
});
|
|
@@ -6307,6 +6380,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6307
6380
|
.description("Run one bidirectional reconciliation cycle for a configured CRM object pair as a durable workflow run. Defaults to dry-run.")
|
|
6308
6381
|
.requiredOption("--provider <provider>", "Connected CRM: hubspot or attio.")
|
|
6309
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.")
|
|
6310
6386
|
.option("--max-rows <n>", "Per-cycle row cap. The real volume brake on provider writes (BYOK writes cost 0 Oxygen credits).")
|
|
6311
6387
|
.option("--max-credits <n>", "Managed-credit + external-quota cap for this cycle.")
|
|
6312
6388
|
.option("--approved", "Confirm a live cycle after inspecting a dry run.")
|
|
@@ -6373,12 +6449,14 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6373
6449
|
})))
|
|
6374
6450
|
.addCommand(new Command("enrichment")
|
|
6375
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.")
|
|
6376
|
-
.addCommand(new Command("list")
|
|
6452
|
+
.addCommand(addContractFlags(new Command("list")
|
|
6377
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.")
|
|
6378
|
-
.option("--
|
|
6379
|
-
.option("--json", "Print a JSON envelope.")
|
|
6454
|
+
.option("--json", "Print a JSON envelope."), CRM_ENRICHMENT_READ_CONTRACT, CRM_ENRICHMENT_READ_FLAGS)
|
|
6380
6455
|
.action(async (options) => {
|
|
6381
|
-
|
|
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)));
|
|
6382
6460
|
}))
|
|
6383
6461
|
.addCommand(new Command("backfill")
|
|
6384
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.")
|
|
@@ -6545,6 +6623,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6545
6623
|
.argument("[prompt]", "Prompt text or @file (same as --prompt); requires --family. The --prompt flag wins if both are given.")
|
|
6546
6624
|
.option("--prompt <text-or-file>", "Signal-sourcing prompt, or a path to a prompt file. Requires --family.")
|
|
6547
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.")
|
|
6548
6627
|
.option("--family <family>", "Signal family when planning from --prompt: hiring, tech, funding, acquisition, news, job_change, or linkedin_posts.")
|
|
6549
6628
|
.option("--scope <scope>", "market or watch_list when planning from --prompt.")
|
|
6550
6629
|
.option("--route-id <id>", "Route id from the plan to execute.")
|
|
@@ -6815,7 +6894,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6815
6894
|
});
|
|
6816
6895
|
})))
|
|
6817
6896
|
.addCommand(new Command("create")
|
|
6818
|
-
.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>`.")
|
|
6819
6898
|
.argument("<name>", "Display name for the table.")
|
|
6820
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>`.')
|
|
6821
6900
|
.option("--project <project>", "Project id or slug. Defaults to General.")
|
|
@@ -6837,10 +6916,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6837
6916
|
.option("--schema-only", "Duplicate columns and definitions without copying row values.")
|
|
6838
6917
|
.option("--json", "Print a JSON envelope.")
|
|
6839
6918
|
.action(async (table, options) => {
|
|
6840
|
-
await handleAsyncAction("tables duplicate", options, () => requestOxygen(
|
|
6919
|
+
await handleAsyncAction("tables duplicate", options, () => requestOxygen(`/api/cli/tables/${encodeURIComponent(table)}/duplicate`, {
|
|
6841
6920
|
method: "POST",
|
|
6842
6921
|
body: {
|
|
6843
|
-
table,
|
|
6844
6922
|
...(readOption(options.name) ? { name: readOption(options.name) } : {}),
|
|
6845
6923
|
...(options.schemaOnly ? { include_rows: false } : {}),
|
|
6846
6924
|
},
|
|
@@ -6882,6 +6960,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6882
6960
|
.description("Delete one workspace table row. A lead enrolled in a campaign stays enrolled; the response names those campaigns.")
|
|
6883
6961
|
.argument("<table>", "Table id or slug.")
|
|
6884
6962
|
.argument("<row_id>", "Workspace row UUID.")
|
|
6963
|
+
.option("--metadata-json <json-or-file>", "Durable row deletion provenance.")
|
|
6885
6964
|
.option("--json", "Print a JSON envelope.")
|
|
6886
6965
|
.action(async (table, rowId, options) => {
|
|
6887
6966
|
await handleAsyncAction("tables delete-row", options, async () => {
|
|
@@ -6890,6 +6969,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6890
6969
|
body: {
|
|
6891
6970
|
table,
|
|
6892
6971
|
row_id: rowId,
|
|
6972
|
+
...(options.metadataJson !== undefined ? { metadata: parseJsonObject(readFileIfPresent(options.metadataJson)) } : {}),
|
|
6893
6973
|
},
|
|
6894
6974
|
});
|
|
6895
6975
|
if (!options.json)
|
|
@@ -6905,6 +6985,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6905
6985
|
.option("--row-id-column <key>", "Column to read from --row-ids-file. Defaults to _row_id.")
|
|
6906
6986
|
.option("--format <format>", "File format for --row-ids-file: json, jsonl, csv, or xlsx.")
|
|
6907
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.")
|
|
6908
6989
|
.option("--json", "Print a JSON envelope.")
|
|
6909
6990
|
.action(async (table, options) => {
|
|
6910
6991
|
await handleAsyncAction("tables delete-rows", options, async () => {
|
|
@@ -6914,6 +6995,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6914
6995
|
body: {
|
|
6915
6996
|
table,
|
|
6916
6997
|
row_ids: rowIds,
|
|
6998
|
+
...(options.metadataJson !== undefined ? { metadata: parseJsonObject(readFileIfPresent(options.metadataJson)) } : {}),
|
|
6917
6999
|
},
|
|
6918
7000
|
});
|
|
6919
7001
|
if (!options.json)
|
|
@@ -7038,8 +7120,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7038
7120
|
return data;
|
|
7039
7121
|
});
|
|
7040
7122
|
}))
|
|
7041
|
-
.addCommand(new Command("query")
|
|
7042
|
-
|
|
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.")
|
|
7043
7127
|
.argument("<table>", "Table id or slug.")
|
|
7044
7128
|
.option("--limit <n>", "Maximum rows to return. Defaults to 100; hard cap is 1000.")
|
|
7045
7129
|
.option("--cursor <cursor>", "Pagination cursor returned by a previous query. Walks forward one page at a time; mutually exclusive with --offset.")
|
|
@@ -7054,6 +7138,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7054
7138
|
.option("--json", "Print a JSON envelope.")
|
|
7055
7139
|
.action(async (table, options) => {
|
|
7056
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
|
+
}
|
|
7057
7145
|
const limit = readPositiveInt(options.limit);
|
|
7058
7146
|
const filters = readFilterJsonOption(options.filterJson);
|
|
7059
7147
|
const filterTree = readJsonObjectOption(options.filterTreeJson);
|
|
@@ -7069,10 +7157,15 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7069
7157
|
if (readOption(options.cursor) && offset !== undefined) {
|
|
7070
7158
|
throw new OxygenError("invalid_request", "Pass either --cursor or --offset, not both.", { exitCode: 1 });
|
|
7071
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
|
+
}
|
|
7072
7164
|
return requestOxygen("/api/cli/tables/query", {
|
|
7073
7165
|
method: "POST",
|
|
7074
7166
|
body: {
|
|
7075
7167
|
table,
|
|
7168
|
+
...gridArgs,
|
|
7076
7169
|
...(limit ? { limit } : {}),
|
|
7077
7170
|
...(readOption(options.cursor) ? { cursor: readOption(options.cursor) } : {}),
|
|
7078
7171
|
...(offset !== undefined ? { offset } : {}),
|
|
@@ -7179,9 +7272,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7179
7272
|
.argument("<table>", "Table id or slug.")
|
|
7180
7273
|
.option("--json", "Print a JSON envelope.")
|
|
7181
7274
|
.action(async (table, options) => {
|
|
7182
|
-
await handleAsyncAction("tables archive", options, () => requestOxygen(
|
|
7183
|
-
method: "
|
|
7184
|
-
body: { table },
|
|
7275
|
+
await handleAsyncAction("tables archive", options, () => requestOxygen(`/api/cli/tables/${encodeURIComponent(table)}`, {
|
|
7276
|
+
method: "DELETE",
|
|
7185
7277
|
}));
|
|
7186
7278
|
}))
|
|
7187
7279
|
.addCommand(new Command("delete")
|
|
@@ -7215,13 +7307,19 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7215
7307
|
}))
|
|
7216
7308
|
.addCommand(new Command("restore")
|
|
7217
7309
|
.description("Restore an archived or delete-scheduled table: cancels the purge timer and reclaims its slug.")
|
|
7218
|
-
.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.")
|
|
7219
7312
|
.option("--json", "Print a JSON envelope.")
|
|
7220
7313
|
.action(async (table, options) => {
|
|
7221
|
-
await handleAsyncAction("tables restore", options, () =>
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
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
|
+
});
|
|
7225
7323
|
}))
|
|
7226
7324
|
.addCommand(new Command("move")
|
|
7227
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.")
|
|
@@ -7763,7 +7861,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7763
7861
|
.description("List a table's saved views.")
|
|
7764
7862
|
.argument("<table>", "Table id or slug.")
|
|
7765
7863
|
.option("--json", "Print a JSON envelope.")
|
|
7766
|
-
.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`))))
|
|
7767
7865
|
.addCommand(new Command("get")
|
|
7768
7866
|
.description("Get one saved view by id.")
|
|
7769
7867
|
.argument("<table>", "Table id or slug.")
|
|
@@ -7778,18 +7876,20 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7778
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.")
|
|
7779
7877
|
.option("--config <json>", TABLE_VIEW_CONFIG_HELP)
|
|
7780
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.")
|
|
7781
7881
|
.option("--json", "Print a JSON envelope.")
|
|
7782
|
-
.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`, {
|
|
7783
7883
|
method: "POST",
|
|
7784
7884
|
body: {
|
|
7785
|
-
table,
|
|
7786
7885
|
name: readOption(options.name),
|
|
7787
7886
|
...(readOption(options.viewType) ? { view_type: readOption(options.viewType) } : {}),
|
|
7788
7887
|
...(readOption(options.groupBy) ? { group_by: readOption(options.groupBy) } : {}),
|
|
7789
7888
|
...(readOption(options.config)
|
|
7790
7889
|
? { config: parseJsonValue(readOption(options.config) ?? "", "--config") }
|
|
7791
7890
|
: {}),
|
|
7792
|
-
...(options.default ? { is_default:
|
|
7891
|
+
...(typeof options.default === "boolean" ? { is_default: options.default } : {}),
|
|
7892
|
+
...(readOption(options.position) !== null ? { position: readNonNegativeInt(options.position) } : {}),
|
|
7793
7893
|
},
|
|
7794
7894
|
}))))
|
|
7795
7895
|
.addCommand(new Command("update")
|
|
@@ -7801,20 +7901,19 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7801
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.")
|
|
7802
7902
|
.option("--config <json>", `${TABLE_VIEW_CONFIG_HELP} Replaces the stored config.`)
|
|
7803
7903
|
.option("--default", "Make this the table's default view.")
|
|
7904
|
+
.option("--no-default", "Clear this view's default status.")
|
|
7804
7905
|
.option("--position <n>", "0-based order among the table's views.")
|
|
7805
7906
|
.option("--json", "Print a JSON envelope.")
|
|
7806
|
-
.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)}`, {
|
|
7807
7908
|
method: "PATCH",
|
|
7808
7909
|
body: {
|
|
7809
|
-
table,
|
|
7810
|
-
view,
|
|
7811
7910
|
...(readOption(options.name) ? { name: readOption(options.name) } : {}),
|
|
7812
7911
|
...(readOption(options.viewType) ? { view_type: readOption(options.viewType) } : {}),
|
|
7813
7912
|
...(readOption(options.groupBy) ? { group_by: readOption(options.groupBy) } : {}),
|
|
7814
7913
|
...(readOption(options.config)
|
|
7815
7914
|
? { config: parseJsonValue(readOption(options.config) ?? "", "--config") }
|
|
7816
7915
|
: {}),
|
|
7817
|
-
...(options.default ? { is_default:
|
|
7916
|
+
...(typeof options.default === "boolean" ? { is_default: options.default } : {}),
|
|
7818
7917
|
...(readOption(options.position) ? { position: Number(readOption(options.position)) } : {}),
|
|
7819
7918
|
},
|
|
7820
7919
|
}))))
|
|
@@ -7823,7 +7922,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7823
7922
|
.argument("<table>", "Table id or slug.")
|
|
7824
7923
|
.argument("<view>", "View id.")
|
|
7825
7924
|
.option("--json", "Print a JSON envelope.")
|
|
7826
|
-
.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" })))));
|
|
7827
7926
|
tablesCommand.addCommand(new Command("auto-run")
|
|
7828
7927
|
.description("Inspect and manage a table's standing auto-run configuration (columns queued automatically when new rows are written).")
|
|
7829
7928
|
.addCommand(new Command("get")
|
|
@@ -7972,6 +8071,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7972
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.")
|
|
7973
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.`)
|
|
7974
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.")
|
|
7975
8075
|
.option("--items-path <path>", "Push feed only. Dot path to an array of row objects in the delivered payload.")
|
|
7976
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.")
|
|
7977
8077
|
.option("--event-type-path <path>", "Push feed only. Dot path to the event type. Defaults to type or event.")
|
|
@@ -7992,6 +8092,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7992
8092
|
.option("--intent-id <id>", "Routing-registry intent id from the plan.")
|
|
7993
8093
|
.option("--input-shape <shape>", "Routing-registry input shape from the plan, e.g. market or watch_list.")
|
|
7994
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.")
|
|
7995
8096
|
.option("--source-prompt <text-or-file>", "The prompt this feed was planned from, or a path to a prompt file.")
|
|
7996
8097
|
.option("--approved", "Approve the recurring paid pull. Required with --every and --max-credits on a paid provider.")
|
|
7997
8098
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -8326,15 +8427,12 @@ Examples:
|
|
|
8326
8427
|
return data;
|
|
8327
8428
|
});
|
|
8328
8429
|
}))
|
|
8329
|
-
.addCommand(new Command("get")
|
|
8430
|
+
.addCommand(addContractFlags(new Command("get")
|
|
8330
8431
|
.description("Read one knowledge wiki page by slug or UUID. Read-only, 0 credits.")
|
|
8331
8432
|
.argument("<slug_or_id>", "Page slug or UUID.")
|
|
8332
|
-
.option("--json", "Print a JSON envelope.")
|
|
8433
|
+
.option("--json", "Print a JSON envelope."), knowledgePageReadContract, { slug: { omit: true }, id: { omit: true } })
|
|
8333
8434
|
.action(async (slugOrId, options) => {
|
|
8334
|
-
await handleAsyncAction("knowledge page get", options, () => requestOxygen(
|
|
8335
|
-
method: "POST",
|
|
8336
|
-
body: { slug: slugOrId },
|
|
8337
|
-
}));
|
|
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)]))}`));
|
|
8338
8436
|
}))
|
|
8339
8437
|
.addCommand(new Command("list")
|
|
8340
8438
|
.description("List knowledge wiki pages. Read-only, 0 credits.")
|
|
@@ -8437,11 +8535,11 @@ Examples:
|
|
|
8437
8535
|
body: buildKnowledgeSearchBody(query, options),
|
|
8438
8536
|
}));
|
|
8439
8537
|
}))
|
|
8440
|
-
.addCommand(new Command("index")
|
|
8538
|
+
.addCommand(addContractFlags(new Command("index")
|
|
8441
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.")
|
|
8442
8540
|
.option("--limit <n>", "Maximum pages to list. Defaults to 200; hard cap is 1000. Counts always cover the whole wiki.")
|
|
8443
8541
|
.option("--all", "List every page instead of the most recently updated window.")
|
|
8444
|
-
.option("--json", "Print a JSON envelope.")
|
|
8542
|
+
.option("--json", "Print a JSON envelope."), knowledgeIndexReadContract, { limit: { omit: true } })
|
|
8445
8543
|
.action(async (options) => {
|
|
8446
8544
|
await handleAsyncAction("knowledge index", options, async () => {
|
|
8447
8545
|
const params = new URLSearchParams();
|
|
@@ -8450,6 +8548,8 @@ Examples:
|
|
|
8450
8548
|
params.set("limit", String(limit));
|
|
8451
8549
|
if (options.all)
|
|
8452
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));
|
|
8453
8553
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
8454
8554
|
const data = await requestOxygen(`/api/cli/knowledge/index${qs}`);
|
|
8455
8555
|
if (!options.json)
|
|
@@ -9391,11 +9491,12 @@ Examples:
|
|
|
9391
9491
|
.alias("get")
|
|
9392
9492
|
.description("Show one recipe: its kit, the install status of each stage here, prerequisites, credits, approval gates, and the playbook body. Alias: `get`.")
|
|
9393
9493
|
.argument("<slug>", "Recipe slug, e.g. outbound-pilot-50.")
|
|
9494
|
+
.option("--section <heading>", "Read one recipe heading instead of the whole playbook.")
|
|
9394
9495
|
.option("--json", "Print a JSON envelope.")
|
|
9395
9496
|
.action(async (slug, options) => {
|
|
9396
9497
|
await handleAsyncAction("recipes show", options, () => requestOxygen("/api/cli/recipes/get", {
|
|
9397
9498
|
method: "POST",
|
|
9398
|
-
body: { slug },
|
|
9499
|
+
body: { slug, ...(readOption(options.section) ? { section: readOption(options.section) } : {}) },
|
|
9399
9500
|
}));
|
|
9400
9501
|
}))
|
|
9401
9502
|
.addCommand(new Command("install")
|
|
@@ -9822,6 +9923,7 @@ Examples:
|
|
|
9822
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.")
|
|
9823
9924
|
.argument("<table>", "Table id or slug.")
|
|
9824
9925
|
.argument("<column>", "Column id or key.")
|
|
9926
|
+
.option("--idempotency-key <key>", "Replay key for durable server-side run admission.")
|
|
9825
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`.")
|
|
9826
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.")
|
|
9827
9929
|
.option("--all", "Run all rows. Requires --background, except with --dry-run, which previews the background run without queueing it.")
|
|
@@ -9897,6 +9999,7 @@ Examples:
|
|
|
9897
9999
|
});
|
|
9898
10000
|
}
|
|
9899
10001
|
const body = {
|
|
10002
|
+
...(readOption(options.idempotencyKey) ? { idempotency_key: readOption(options.idempotencyKey) } : {}),
|
|
9900
10003
|
table,
|
|
9901
10004
|
column,
|
|
9902
10005
|
...(options.all ? { selection: { mode: "all" } } : {}),
|
|
@@ -9955,11 +10058,9 @@ Examples:
|
|
|
9955
10058
|
.action(async (table, sourceColumn, options) => {
|
|
9956
10059
|
const mappings = options.mappingsJson ? parseJsonArray(options.mappingsJson) : undefined;
|
|
9957
10060
|
const preset = readOption(options.preset);
|
|
9958
|
-
await handleAsyncAction("columns materialize", options, () => requestOxygen(
|
|
10061
|
+
await handleAsyncAction("columns materialize", options, () => requestOxygen(`/api/cli/tables/${encodeURIComponent(table)}/columns/${encodeURIComponent(sourceColumn)}/materialize`, {
|
|
9959
10062
|
method: "POST",
|
|
9960
10063
|
body: {
|
|
9961
|
-
table,
|
|
9962
|
-
source_column: sourceColumn,
|
|
9963
10064
|
...(mappings ? { mappings } : {}),
|
|
9964
10065
|
...(!mappings ? { preset: preset ?? "work_email" } : preset ? { preset } : {}),
|
|
9965
10066
|
create_if_missing: options.createIfMissing !== false,
|
|
@@ -9968,10 +10069,10 @@ Examples:
|
|
|
9968
10069
|
}));
|
|
9969
10070
|
}))
|
|
9970
10071
|
.addCommand(new Command("rename")
|
|
9971
|
-
.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.")
|
|
9972
10073
|
.argument("<table>", "Table id or slug.")
|
|
9973
10074
|
.argument("<column>", "Column id or key.")
|
|
9974
|
-
.requiredOption("--key <key>", "
|
|
10075
|
+
.requiredOption("--key <key>", "Required new stable column key.")
|
|
9975
10076
|
.option("--label <label>", "Optional new display label.")
|
|
9976
10077
|
.option("--json", "Print a JSON envelope.")
|
|
9977
10078
|
.action(async (table, column, options) => {
|
|
@@ -9996,6 +10097,8 @@ Examples:
|
|
|
9996
10097
|
.argument("<column>", "Column id or key.")
|
|
9997
10098
|
.option("--label <label>", "New display label.")
|
|
9998
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.")
|
|
9999
10102
|
.option("--definition-json <json>", "Definition metadata to shallow-merge into the column; arrays are replaced wholesale and missing keys leave existing fields unchanged.")
|
|
10000
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.")
|
|
10001
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`.")
|
|
@@ -10075,7 +10178,8 @@ Examples:
|
|
|
10075
10178
|
column,
|
|
10076
10179
|
...(researchDepth ? { research_depth: researchDepth } : {}),
|
|
10077
10180
|
...(readOption(options.label) ? { label: readOption(options.label) } : {}),
|
|
10078
|
-
...(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) } : {}),
|
|
10079
10183
|
...(definition ? { definition } : {}),
|
|
10080
10184
|
...(definitionUnset.length > 0 ? { definition_unset: definitionUnset } : {}),
|
|
10081
10185
|
...(readOption(options.dataType) ? { data_type: readOption(options.dataType) } : {}),
|
|
@@ -10130,11 +10234,9 @@ Examples:
|
|
|
10130
10234
|
.option("--dry-run", "Preview the conversion (row counts and samples) without writing.")
|
|
10131
10235
|
.option("--json", "Print a JSON envelope.")
|
|
10132
10236
|
.action(async (table, column, options) => {
|
|
10133
|
-
await handleAsyncAction("columns retype", options, () => requestOxygen(
|
|
10237
|
+
await handleAsyncAction("columns retype", options, () => requestOxygen(`/api/cli/tables/${encodeURIComponent(table)}/columns/${encodeURIComponent(column)}/retype`, {
|
|
10134
10238
|
method: "POST",
|
|
10135
10239
|
body: {
|
|
10136
|
-
table,
|
|
10137
|
-
column,
|
|
10138
10240
|
data_type: options.dataType,
|
|
10139
10241
|
...(options.dryRun ? { dry_run: true } : {}),
|
|
10140
10242
|
},
|
|
@@ -10146,9 +10248,9 @@ Examples:
|
|
|
10146
10248
|
.argument("<column>", "Column id or key.")
|
|
10147
10249
|
.option("--json", "Print a JSON envelope.")
|
|
10148
10250
|
.action(async (table, column, options) => {
|
|
10149
|
-
await handleAsyncAction("columns archive", options, () => requestOxygen(
|
|
10251
|
+
await handleAsyncAction("columns archive", options, () => requestOxygen(`/api/cli/tables/${encodeURIComponent(table)}/columns/${encodeURIComponent(column)}/archive`, {
|
|
10150
10252
|
method: "POST",
|
|
10151
|
-
body: {
|
|
10253
|
+
body: {},
|
|
10152
10254
|
}));
|
|
10153
10255
|
}))
|
|
10154
10256
|
.addCommand(new Command("delete")
|
|
@@ -10354,6 +10456,7 @@ Examples:
|
|
|
10354
10456
|
.option("--max-credits <n>", "Maximum managed/provider credits to reserve for this run.")
|
|
10355
10457
|
.option("--max-concurrency <n>", "Maximum concurrent row items for this run (1-250). Defaults to 50 (160 for Firecrawl scrape columns).")
|
|
10356
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.")
|
|
10357
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}].")
|
|
10358
10461
|
.option("--json", "Print a JSON envelope.")
|
|
10359
10462
|
.action(async (table, options) => {
|
|
@@ -10372,6 +10475,7 @@ Examples:
|
|
|
10372
10475
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
10373
10476
|
...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
|
|
10374
10477
|
...(options.metadataJson ? { metadata: parseJsonObject(options.metadataJson) } : {}),
|
|
10478
|
+
...(readOption(options.idempotencyKey) ? { idempotency_key: readOption(options.idempotencyKey) } : {}),
|
|
10375
10479
|
...(chainSteps.length > 0 ? { then: chainSteps } : {}),
|
|
10376
10480
|
},
|
|
10377
10481
|
}));
|
|
@@ -12734,7 +12838,7 @@ Examples:
|
|
|
12734
12838
|
program
|
|
12735
12839
|
.command("tools")
|
|
12736
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.")
|
|
12737
|
-
.addCommand(new Command("search")
|
|
12841
|
+
.addCommand(addToolCatalogSearchWindowFlag(new Command("search")
|
|
12738
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.")
|
|
12739
12843
|
.argument("[query]", "Search text, e.g. `web` for the Web Research Agent column and its modes (each priced in `modes`), or `email`.")
|
|
12740
12844
|
.option("--verbosity <verbosity>", "minimal, summary, or full. Defaults to minimal; hydrate one result with tools get.")
|
|
@@ -12756,6 +12860,11 @@ Examples:
|
|
|
12756
12860
|
await handleAsyncAction("tools search", options, async () => {
|
|
12757
12861
|
const params = new URLSearchParams();
|
|
12758
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));
|
|
12759
12868
|
const forTable = readOption(options.forTable);
|
|
12760
12869
|
if (forTable)
|
|
12761
12870
|
params.set("for_table", forTable);
|
|
@@ -12790,14 +12899,8 @@ Examples:
|
|
|
12790
12899
|
writeAccessCheckFailuresWarning(data);
|
|
12791
12900
|
return data;
|
|
12792
12901
|
});
|
|
12793
|
-
}))
|
|
12794
|
-
.addCommand(
|
|
12795
|
-
.description("Get a tool descriptor.")
|
|
12796
|
-
.argument("<tool_id>", "Tool id.")
|
|
12797
|
-
.option("--json", "Print a JSON envelope.")
|
|
12798
|
-
.action(async (toolId, options) => {
|
|
12799
|
-
await handleAsyncAction("tools get", options, () => requestOxygen(`/api/cli/tools/${encodeURIComponent(toolId)}`));
|
|
12800
|
-
}))
|
|
12902
|
+
})))
|
|
12903
|
+
.addCommand(createToolCatalogDetailCommand(handleAsyncAction))
|
|
12801
12904
|
.addCommand(new Command("favorite")
|
|
12802
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`.")
|
|
12803
12906
|
.argument("<entry_id>", "Catalog entry id, e.g. find_email, preset:person_enrich, prompt:<key>, tool:<tool_id>.")
|
|
@@ -13357,7 +13460,8 @@ Examples:
|
|
|
13357
13460
|
.option("--credential <name=value>", "Named provider credential field. Repeat for multi-field integrations (for example, PostHog: --credential subdomain=us).", collectRepeatable, [])
|
|
13358
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).")
|
|
13359
13462
|
.option("--pairing-phone-number <e164>", "WhatsApp only: pair by code sent to this E.164 number instead of showing a QR.")
|
|
13360
|
-
.
|
|
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.")
|
|
13361
13465
|
.option("--json", "Print a JSON envelope."), INTEGRATION_CONNECT_ACCOUNT_CONTRACT, {
|
|
13362
13466
|
account_id: {
|
|
13363
13467
|
valueName: "<id>",
|
|
@@ -13377,6 +13481,7 @@ Examples:
|
|
|
13377
13481
|
...(apiKey ? { api_key: apiKey } : {}),
|
|
13378
13482
|
...(credentials ? { credentials } : {}),
|
|
13379
13483
|
...readContractFlags(options, INTEGRATION_CONNECT_ACCOUNT_CONTRACT),
|
|
13484
|
+
...(readOption(options.returnTo) ? { return_to: readOption(options.returnTo) } : {}),
|
|
13380
13485
|
...(country ? { country } : {}),
|
|
13381
13486
|
...(pairingPhoneNumber ? { pairing_phone_number: pairingPhoneNumber } : {}),
|
|
13382
13487
|
...(options.acknowledgeRisk ? { acknowledge_linkedin_risk: true } : {}),
|
|
@@ -13450,6 +13555,8 @@ Examples:
|
|
|
13450
13555
|
.description("Run an action for a connected integration — Composio toolkits and native providers alike.")
|
|
13451
13556
|
.argument("<integration_id>", "Integration id, such as 'slack'.")
|
|
13452
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.")
|
|
13453
13560
|
.option("--input <json>", "Arguments as JSON object. Default: empty object.")
|
|
13454
13561
|
.option("--live", "Execute the action live. Default is dry-run.")
|
|
13455
13562
|
.option("--dry-run", "Force dry-run (no provider call).")
|
|
@@ -13470,6 +13577,8 @@ Examples:
|
|
|
13470
13577
|
integration_id: integrationId,
|
|
13471
13578
|
action_slug: actionSlug,
|
|
13472
13579
|
arguments: args,
|
|
13580
|
+
...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
|
|
13581
|
+
...(readOption(options.credentialMode) ? { credential_mode: readOption(options.credentialMode) } : {}),
|
|
13473
13582
|
mode,
|
|
13474
13583
|
...(options.approved ? { approved: true } : {}),
|
|
13475
13584
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
@@ -13507,15 +13616,16 @@ Examples:
|
|
|
13507
13616
|
await handleAsyncAction("senders checkpoints", options, () => requestOxygen("/api/cli/senders/checkpoints"));
|
|
13508
13617
|
}))
|
|
13509
13618
|
.addCommand(new Command("connect")
|
|
13510
|
-
.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.")
|
|
13511
13620
|
.option("--reconnect <connection_id>", "Reconnect an existing connection instead of creating a new one. Accepts a connection id.")
|
|
13512
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).")
|
|
13513
13622
|
.option("--sales-nav", "Request Classic + Sales Navigator access during Unipile hosted authentication.")
|
|
13514
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.")
|
|
13515
13624
|
.option("--custom-proxy", "Let the account owner enter a custom proxy in Unipile's hosted wizard. OXYGEN never receives the proxy credentials.")
|
|
13516
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.")
|
|
13517
13627
|
.option("--inbox-scope <scope>", "LinkedIn inbox privacy for new accounts: oxygen_initiated (default) or all. Reconnect preserves the existing account setting.")
|
|
13518
|
-
.
|
|
13628
|
+
.addOption(new Option("--acknowledge-risk", "Deprecated: connecting no longer needs it. Still accepted so older scripts keep running.").hideHelp())
|
|
13519
13629
|
.option("--owner-consent", "Required with --count above 1: each account owner connects their own account.")
|
|
13520
13630
|
.option("--established", "The account is an established profile: skip the ~2-week warm-up a new account gets by default.")
|
|
13521
13631
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -13534,6 +13644,7 @@ Examples:
|
|
|
13534
13644
|
...(options.cookieAuth ? { cookie_auth: true } : {}),
|
|
13535
13645
|
...(options.customProxy ? { custom_proxy: true } : {}),
|
|
13536
13646
|
...(count ? { count } : {}),
|
|
13647
|
+
...(options.shareLink ? { share_link: true } : {}),
|
|
13537
13648
|
...(inboxScope ? { inbox_sync_scope: inboxScope } : {}),
|
|
13538
13649
|
...(options.acknowledgeRisk ? { acknowledge_linkedin_risk: true } : {}),
|
|
13539
13650
|
...(options.ownerConsent ? { owner_consent: true } : {}),
|
|
@@ -13718,6 +13829,8 @@ Examples:
|
|
|
13718
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.")
|
|
13719
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.")
|
|
13720
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.")
|
|
13721
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.")
|
|
13722
13835
|
.option("--status <status>", "Initial status: active (default), paused, or archived.")
|
|
13723
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.")
|
|
@@ -13731,6 +13844,8 @@ Examples:
|
|
|
13731
13844
|
...(readOption(options.firstName) ? { first_name: readOption(options.firstName) } : {}),
|
|
13732
13845
|
...(readOption(options.lastName) ? { last_name: readOption(options.lastName) } : {}),
|
|
13733
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) } : {}),
|
|
13734
13849
|
...(readOption(options.avatarUrl) ? { avatar_url: readOption(options.avatarUrl) } : {}),
|
|
13735
13850
|
...(readOption(options.status) ? { status: readOption(options.status) } : {}),
|
|
13736
13851
|
...(options.senders ? { sender_account_ids: splitCommaList(options.senders) } : {}),
|
|
@@ -14021,6 +14136,9 @@ Examples:
|
|
|
14021
14136
|
.option("--text-file <path>", "Read the post body from a file (alternative to --text).")
|
|
14022
14137
|
.option("--account <ref>", "Sender account to post from. Omit for the org default.")
|
|
14023
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.")
|
|
14024
14142
|
.option("--external-link <url>", "Attach an external link to the post.")
|
|
14025
14143
|
.option("--approved", "Publish for real. Without this flag, prints a preview only.")
|
|
14026
14144
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -14041,6 +14159,9 @@ Examples:
|
|
|
14041
14159
|
...(account ? { account } : {}),
|
|
14042
14160
|
...(asOrganization ? { as_organization: asOrganization } : {}),
|
|
14043
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)) } : {}),
|
|
14044
14165
|
...(options.approved ? { approved: true } : {}),
|
|
14045
14166
|
},
|
|
14046
14167
|
});
|
|
@@ -14202,6 +14323,7 @@ Examples:
|
|
|
14202
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).")
|
|
14203
14324
|
.requiredOption("--post <social_id>", "Composite post social_id from `oxygen posts get` (NOT the activity URN).")
|
|
14204
14325
|
.requiredOption("--text <text>", "Comment text to post.")
|
|
14326
|
+
.option("--mentions-json <json-or-file>", "Provider comment mention objects.")
|
|
14205
14327
|
.option("--comment-id <id>", "Reply under this comment id instead of commenting on the post.")
|
|
14206
14328
|
.option("--as-organization <org>", "LinkedIn organization id to comment as a company page.")
|
|
14207
14329
|
.option("--external-link <url>", "URL to attach as a link preview on the comment.")
|
|
@@ -14220,6 +14342,7 @@ Examples:
|
|
|
14220
14342
|
method: "POST",
|
|
14221
14343
|
body: {
|
|
14222
14344
|
...(post ? { post } : {}),
|
|
14345
|
+
...(options.mentionsJson !== undefined ? { mentions: parseJsonArray(readFileIfPresent(options.mentionsJson)) } : {}),
|
|
14223
14346
|
...(text ? { text } : {}),
|
|
14224
14347
|
...(commentId ? { comment_id: commentId } : {}),
|
|
14225
14348
|
...(asOrganization ? { as_organization: asOrganization } : {}),
|
|
@@ -14427,20 +14550,16 @@ Examples:
|
|
|
14427
14550
|
});
|
|
14428
14551
|
});
|
|
14429
14552
|
}))
|
|
14430
|
-
.addCommand(new Command("list")
|
|
14431
|
-
.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
|
|
14432
|
-
.option("--account <ref>", "Scope to one sender account (sender id, connection id, or Unipile account id).")
|
|
14433
|
-
.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)
|
|
14434
14555
|
.option("--json", "Print a JSON envelope.")
|
|
14435
14556
|
.action(async (options) => {
|
|
14436
14557
|
await handleAsyncAction("linkedin invitations list", options, () => {
|
|
14437
|
-
const
|
|
14438
|
-
const limit = readPositiveInt(options.limit);
|
|
14558
|
+
const selectors = readContractFlags(options, LINKEDIN_SENT_INVITES_QUERY_CONTRACT, LINKEDIN_SENT_INVITES_FLAG_OVERLAY);
|
|
14439
14559
|
const params = new URLSearchParams();
|
|
14440
|
-
|
|
14441
|
-
|
|
14442
|
-
|
|
14443
|
-
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));
|
|
14444
14563
|
const suffix = params.toString();
|
|
14445
14564
|
return requestOxygen(`/api/cli/linkedin/connections/invites-sent${suffix ? `?${suffix}` : ""}`);
|
|
14446
14565
|
});
|
|
@@ -14470,13 +14589,18 @@ Examples:
|
|
|
14470
14589
|
.addCommand(new Command("connect")
|
|
14471
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.")
|
|
14472
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.")
|
|
14473
14593
|
.option("--json", "Print a JSON envelope.")
|
|
14474
14594
|
.action(async (options) => {
|
|
14475
14595
|
await handleAsyncAction("whatsapp connect", options, () => {
|
|
14476
14596
|
const reconnect = readOption(options.reconnect);
|
|
14597
|
+
const pairingPhoneNumber = readOption(options.pairingPhoneNumber);
|
|
14477
14598
|
return requestOxygen("/api/cli/whatsapp/connect", {
|
|
14478
14599
|
method: "POST",
|
|
14479
|
-
body: {
|
|
14600
|
+
body: {
|
|
14601
|
+
...(reconnect ? { reconnect_connection_id: reconnect } : {}),
|
|
14602
|
+
...(pairingPhoneNumber ? { pairing_phone_number: pairingPhoneNumber } : {}),
|
|
14603
|
+
},
|
|
14480
14604
|
});
|
|
14481
14605
|
});
|
|
14482
14606
|
}))
|
|
@@ -14524,6 +14648,9 @@ Examples:
|
|
|
14524
14648
|
.option("--messages-reads-per-day <n>", "Daily cap on chat and message-history reads.")
|
|
14525
14649
|
.option("--min-spacing-seconds <n>", "Minimum seconds between actions.")
|
|
14526
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.")
|
|
14527
14654
|
.option("--timezone <tz>", "IANA timezone the daily action counters reset in, e.g. America/New_York.")
|
|
14528
14655
|
.option("--json", "Print a JSON envelope.")
|
|
14529
14656
|
.action(async (id, options) => {
|
|
@@ -15051,6 +15178,7 @@ Examples:
|
|
|
15051
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.")
|
|
15052
15179
|
.argument("<conversation>", "Conversation id, Unipile chat id, or (email) Zapbox thread id.")
|
|
15053
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.")
|
|
15054
15182
|
.option("--channel <channel>", "Inbox channel: email (default), linkedin, or whatsapp.")
|
|
15055
15183
|
.option("--json", "Print a JSON envelope.")
|
|
15056
15184
|
.action(async (conversation, status, options) => {
|
|
@@ -15058,7 +15186,7 @@ Examples:
|
|
|
15058
15186
|
const channel = readOption(options.channel) ?? "email";
|
|
15059
15187
|
return requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}/status`, {
|
|
15060
15188
|
method: "POST",
|
|
15061
|
-
body: { status, channel },
|
|
15189
|
+
body: { status, channel, ...(readOption(options.source) ? { source: readOption(options.source) } : {}) },
|
|
15062
15190
|
});
|
|
15063
15191
|
});
|
|
15064
15192
|
}))
|
|
@@ -15143,6 +15271,9 @@ Examples:
|
|
|
15143
15271
|
.addCommand(new Command("create")
|
|
15144
15272
|
.description("Create a custom status label.")
|
|
15145
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.")
|
|
15146
15277
|
.option("--color <hex>", "Hex color (e.g. #2a78d6). Workspace palette: #2a78d6 #eb6834 #1baf7a #eda100 #e87ba4 #008300 #4a3aa7 #e34948 #0e9dc4 #9a4fd0.")
|
|
15147
15278
|
.option("--bucket <bucket>", "primary (default) or others.")
|
|
15148
15279
|
.option("--ai-settable", "Let the AI reply classifier assign this label.")
|
|
@@ -15158,6 +15289,9 @@ Examples:
|
|
|
15158
15289
|
body: {
|
|
15159
15290
|
name,
|
|
15160
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) } : {}),
|
|
15161
15295
|
...(bucket ? { bucket } : {}),
|
|
15162
15296
|
...(options.aiSettable ? { ai_settable: true } : {}),
|
|
15163
15297
|
...(description ? { description } : {}),
|
|
@@ -15169,6 +15303,8 @@ Examples:
|
|
|
15169
15303
|
.description("Update a status label's name, color, bucket, description, or (custom labels only) ai-settable flag.")
|
|
15170
15304
|
.argument("<key>", "The label key.")
|
|
15171
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.")
|
|
15172
15308
|
.option("--color <hex>", "New hex color.")
|
|
15173
15309
|
.option("--bucket <bucket>", "New bucket (primary or others).")
|
|
15174
15310
|
.option("--ai-settable", "Let the AI classifier assign this label (custom labels only).")
|
|
@@ -15186,6 +15322,9 @@ Examples:
|
|
|
15186
15322
|
body: {
|
|
15187
15323
|
...(name ? { name } : {}),
|
|
15188
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) } : {}),
|
|
15189
15328
|
...(bucket ? { bucket } : {}),
|
|
15190
15329
|
...(options.aiSettable !== undefined ? { ai_settable: options.aiSettable } : {}),
|
|
15191
15330
|
...(description ? { description } : {}),
|
|
@@ -15618,6 +15757,7 @@ Examples:
|
|
|
15618
15757
|
.option("--contact-linkedin-property <name>", "Optional contact LinkedIn profile property internal name.")
|
|
15619
15758
|
.option("--contact-company-domain-property <name>", "Optional explicit company-domain property on contacts, copied to lead rows.")
|
|
15620
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).")
|
|
15621
15761
|
.option("--company-domain-property <name>", "Company-list domain property (default domain).", "domain")
|
|
15622
15762
|
.option("--company-linkedin-property <name>", "Optional company-list LinkedIn URL property.")
|
|
15623
15763
|
.option("--cron <expression>", "Standing cron cadence. Defaults to every 15 minutes.")
|
|
@@ -15654,6 +15794,7 @@ Examples:
|
|
|
15654
15794
|
...(readOption(options.contactLinkedinProperty) ? { contact_linkedin_property: readOption(options.contactLinkedinProperty) } : {}),
|
|
15655
15795
|
...(readOption(options.contactCompanyDomainProperty) ? { contact_company_domain_property: readOption(options.contactCompanyDomainProperty) } : {}),
|
|
15656
15796
|
...(readOption(options.contactCompanyLinkedinProperty) ? { contact_company_linkedin_property: readOption(options.contactCompanyLinkedinProperty) } : {}),
|
|
15797
|
+
...(options.contactProperties !== undefined ? { contact_properties: readCsvOption(options.contactProperties) } : {}),
|
|
15657
15798
|
...(readOption(options.companyDomainProperty) ? { company_domain_property: readOption(options.companyDomainProperty) } : {}),
|
|
15658
15799
|
...(readOption(options.companyLinkedinProperty) ? { company_linkedin_property: readOption(options.companyLinkedinProperty) } : {}),
|
|
15659
15800
|
mode: options.live ? "live" : "dry_run",
|
|
@@ -15739,6 +15880,8 @@ Examples:
|
|
|
15739
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`.")
|
|
15740
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.")
|
|
15741
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.")
|
|
15742
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.")
|
|
15743
15886
|
.option("--no-include-unsubscribe", "Turn off both the visible unsubscribe link and the one-click headers.")
|
|
15744
15887
|
.option("--tracking-opens", "Opt in to open tracking (default off). Requires a verified tracking domain and deployment tracking secret.")
|
|
@@ -15862,6 +16005,8 @@ Examples:
|
|
|
15862
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`.")
|
|
15863
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.")
|
|
15864
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.")
|
|
15865
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.")
|
|
15866
16011
|
.option("--no-include-unsubscribe", "Turn off both the visible unsubscribe link and the one-click headers.")
|
|
15867
16012
|
.option("--include-unsubscribe-headers", "Only the one-click unsubscribe headers (default off); --include-unsubscribe turns on the headers and the visible link together.")
|
|
@@ -15992,6 +16137,7 @@ Examples:
|
|
|
15992
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).")
|
|
15993
16138
|
.requiredOption("--from-table <table>", "Table id or slug whose rows are pushed.")
|
|
15994
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.")
|
|
15995
16141
|
.option("--filter-json <json>", "Filter object or array selecting the rows, e.g. '{\"column\":\"email\",\"op\":\"is_not_null\"}'.")
|
|
15996
16142
|
.option("--mode <mode>", "dry_run (default) or live.")
|
|
15997
16143
|
.option("--approved", "Required for --mode live: confirms the external write after inspecting the dry run.")
|
|
@@ -16007,10 +16153,13 @@ Examples:
|
|
|
16007
16153
|
if (rows.length > 0 && filterJson) {
|
|
16008
16154
|
throw new Error("Pass either --rows or --filter-json, not both.");
|
|
16009
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 });
|
|
16010
16159
|
const selection = rows.length > 0
|
|
16011
16160
|
? { mode: "row_ids", row_ids: rows }
|
|
16012
|
-
: filterJson
|
|
16013
|
-
? { 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 } : {}) }
|
|
16014
16163
|
: null;
|
|
16015
16164
|
const fieldMappingJson = readOption(options.fieldMappingJson);
|
|
16016
16165
|
return requestOxygen("/api/cli/sequences/push-external", {
|
|
@@ -16405,6 +16554,7 @@ Examples:
|
|
|
16405
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.")
|
|
16406
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.")
|
|
16407
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.")
|
|
16408
16558
|
.option("--source <source>", "Origin: manual (default), crm, table, reply, sequence.")
|
|
16409
16559
|
.option("--priority <n>", "Higher sorts first in the queue.")
|
|
16410
16560
|
.option("--due <iso>", "ISO-8601 timestamp; the task stays out of the queue until then.")
|
|
@@ -16440,6 +16590,7 @@ Examples:
|
|
|
16440
16590
|
method: "POST",
|
|
16441
16591
|
body: {
|
|
16442
16592
|
action: "queue",
|
|
16593
|
+
...(readOption(options.assignedUserId) ? { assigned_user_id: readOption(options.assignedUserId) } : {}),
|
|
16443
16594
|
lead_phone: phone,
|
|
16444
16595
|
...(name ? { lead_name: name } : {}),
|
|
16445
16596
|
...(timezone ? { lead_timezone: timezone } : {}),
|
|
@@ -16822,22 +16973,24 @@ Examples:
|
|
|
16822
16973
|
});
|
|
16823
16974
|
}))
|
|
16824
16975
|
.addCommand(new Command("add")
|
|
16825
|
-
.description("Add
|
|
16826
|
-
.
|
|
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).")
|
|
16827
16979
|
.option("--reason <reason>", "Why: manual (default), replied, unsubscribed, bounced, do_not_contact, friends.")
|
|
16828
16980
|
.option("--detail <text>", "Optional free-text note stored with the suppression.")
|
|
16829
16981
|
.option("--json", "Print a JSON envelope.")
|
|
16830
16982
|
.action(async (options) => {
|
|
16831
16983
|
await handleAsyncAction("suppressions add", options, () => {
|
|
16832
16984
|
const lead = readOption(options.lead);
|
|
16833
|
-
|
|
16834
|
-
|
|
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 });
|
|
16835
16988
|
const reason = readOption(options.reason);
|
|
16836
16989
|
const detail = readOption(options.detail);
|
|
16837
16990
|
return requestOxygen("/api/cli/suppressions", {
|
|
16838
16991
|
method: "POST",
|
|
16839
16992
|
body: {
|
|
16840
|
-
lead_provider_id: lead,
|
|
16993
|
+
...(leads ? { lead_provider_ids: leads } : { lead_provider_id: lead }),
|
|
16841
16994
|
...(reason ? { reason } : {}),
|
|
16842
16995
|
...(detail ? { detail } : {}),
|
|
16843
16996
|
},
|
|
@@ -16845,11 +16998,27 @@ Examples:
|
|
|
16845
16998
|
});
|
|
16846
16999
|
}))
|
|
16847
17000
|
.addCommand(new Command("remove")
|
|
16848
|
-
.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.")
|
|
16849
17002
|
.option("--lead <provider_id>", "The lead provider id to un-suppress. Mutually exclusive with --subject-id.")
|
|
16850
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).")
|
|
16851
17006
|
.option("--json", "Print a JSON envelope.")
|
|
16852
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
|
+
}
|
|
16853
17022
|
const subjectId = readOption(options.subjectId);
|
|
16854
17023
|
if (!subjectId) {
|
|
16855
17024
|
await handleAsyncAction("suppressions remove", options, () => {
|
|
@@ -16908,6 +17077,7 @@ Examples:
|
|
|
16908
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.")
|
|
16909
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).")
|
|
16910
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.")
|
|
16911
17081
|
.option("--detail <text>", "Optional free-text note stored on every imported row.")
|
|
16912
17082
|
.option("--json", "Print a JSON envelope.")
|
|
16913
17083
|
.action(async (options) => {
|
|
@@ -16931,6 +17101,7 @@ Examples:
|
|
|
16931
17101
|
method: "POST",
|
|
16932
17102
|
body: {
|
|
16933
17103
|
entries,
|
|
17104
|
+
...(options.metadataJson !== undefined ? { metadata: parseJsonObject(readFileIfPresent(options.metadataJson)) } : {}),
|
|
16934
17105
|
...(reason ? { reason } : {}),
|
|
16935
17106
|
...(source ? { source } : {}),
|
|
16936
17107
|
...(detail ? { detail } : {}),
|
|
@@ -17058,11 +17229,18 @@ Examples:
|
|
|
17058
17229
|
});
|
|
17059
17230
|
}))
|
|
17060
17231
|
.addCommand(new Command("remove-phone")
|
|
17061
|
-
.description("
|
|
17062
|
-
.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).")
|
|
17063
17235
|
.option("--json", "Print a JSON envelope.")
|
|
17064
17236
|
.action(async (phone, options) => {
|
|
17065
|
-
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
|
+
});
|
|
17066
17244
|
}))
|
|
17067
17245
|
.addCommand(new Command("companies")
|
|
17068
17246
|
.description("List explicit company-domain and LinkedIn-company suppressions. These stop every Sequence channel.")
|
|
@@ -17083,14 +17261,23 @@ Examples:
|
|
|
17083
17261
|
});
|
|
17084
17262
|
}))
|
|
17085
17263
|
.addCommand(new Command("remove-company")
|
|
17086
|
-
.description("
|
|
17087
|
-
.
|
|
17088
|
-
.
|
|
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.")
|
|
17089
17268
|
.option("--json", "Print a JSON envelope.")
|
|
17090
17269
|
.action(async (options) => {
|
|
17091
17270
|
await handleAsyncAction("suppressions remove-company", options, () => {
|
|
17092
17271
|
const kind = readOption(options.kind);
|
|
17093
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
|
+
}
|
|
17094
17281
|
if (!kind || !value)
|
|
17095
17282
|
throw new Error("--kind and --value are required.");
|
|
17096
17283
|
const params = new URLSearchParams({ kind, value });
|
|
@@ -17217,12 +17404,18 @@ Examples:
|
|
|
17217
17404
|
});
|
|
17218
17405
|
}))
|
|
17219
17406
|
.addCommand(new Command("remove-domain")
|
|
17220
|
-
.description("Remove
|
|
17221
|
-
.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).")
|
|
17222
17410
|
.option("--json", "Print a JSON envelope.")
|
|
17223
17411
|
.action(async (domain, options) => {
|
|
17224
17412
|
await handleAsyncAction("suppressions remove-domain", options, () => {
|
|
17225
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
|
+
}
|
|
17226
17419
|
if (!value)
|
|
17227
17420
|
throw new Error("A domain is required.");
|
|
17228
17421
|
return requestOxygen(`/api/cli/suppressions/domains?domain=${encodeURIComponent(value)}`, {
|
|
@@ -17231,11 +17424,23 @@ Examples:
|
|
|
17231
17424
|
});
|
|
17232
17425
|
}))
|
|
17233
17426
|
.addCommand(new Command("remove-address")
|
|
17234
|
-
.description("Remove
|
|
17235
|
-
.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).")
|
|
17236
17430
|
.option("--json", "Print a JSON envelope.")
|
|
17237
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
|
+
}
|
|
17238
17441
|
try {
|
|
17442
|
+
if (!email)
|
|
17443
|
+
throw new OxygenError("invalid_request", "Pass one email or --emails.", { exitCode: 2 });
|
|
17239
17444
|
const data = await requestOxygen(`/api/cli/suppressions/addresses?email=${encodeURIComponent(email)}`, { method: "DELETE" });
|
|
17240
17445
|
if (options.json) {
|
|
17241
17446
|
writeJson(success("suppressions remove-address", data));
|
|
@@ -17570,7 +17775,7 @@ Examples:
|
|
|
17570
17775
|
writeMailboxPoolOverview(result, { filtered: suffix.length > 0, filterFlags });
|
|
17571
17776
|
}))
|
|
17572
17777
|
.addCommand(new Command("get")
|
|
17573
|
-
.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.")
|
|
17574
17779
|
.argument("<mailbox>", "Mailbox id or email address.")
|
|
17575
17780
|
.option("--json", "Print a JSON envelope.")
|
|
17576
17781
|
.action(async (mailbox, options) => {
|
|
@@ -17583,6 +17788,7 @@ Examples:
|
|
|
17583
17788
|
.option("--plan-hash <hash>", "Fresh preview plan_hash.")
|
|
17584
17789
|
.option("--confirmation <phrase>", "Exact confirmation_phrase returned by the fresh preview.")
|
|
17585
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.")
|
|
17586
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.")
|
|
17587
17793
|
.option("--json", "Print a JSON envelope.")
|
|
17588
17794
|
.action(async (options) => {
|
|
@@ -17600,6 +17806,7 @@ Examples:
|
|
|
17600
17806
|
method: "POST",
|
|
17601
17807
|
body: {
|
|
17602
17808
|
mailboxes,
|
|
17809
|
+
...(options.domains !== undefined ? { domains: readCsvOption(options.domains) } : {}),
|
|
17603
17810
|
...(options.approved === true ? { approved: true } : {}),
|
|
17604
17811
|
...(options.cascade === true ? { cascade: true } : {}),
|
|
17605
17812
|
...(options.includeDomains === true ? { include_domains: true } : {}),
|
|
@@ -17764,9 +17971,9 @@ Examples:
|
|
|
17764
17971
|
});
|
|
17765
17972
|
}))
|
|
17766
17973
|
.addCommand(new Command("cap")
|
|
17767
|
-
.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`.")
|
|
17768
17975
|
.addCommand(new Command("set")
|
|
17769
|
-
.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.")
|
|
17770
17977
|
.argument("<mailbox>", "Mailbox id or email address.")
|
|
17771
17978
|
.requiredOption("--daily-cap <n>", "New daily send cap (a positive whole number of sends per day).")
|
|
17772
17979
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -17782,44 +17989,58 @@ Examples:
|
|
|
17782
17989
|
});
|
|
17783
17990
|
})))
|
|
17784
17991
|
.addCommand(new Command("ramp")
|
|
17785
|
-
.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`).")
|
|
17786
17993
|
.addCommand(new Command("set")
|
|
17787
|
-
.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.")
|
|
17788
17995
|
.argument("<mailbox>", "Mailbox id or email address.")
|
|
17789
|
-
.option("--start-per-day <n>", "
|
|
17790
|
-
.option("--increase-every-days <n>", "
|
|
17791
|
-
.option("--step <n>", "
|
|
17792
|
-
.option("--cap <n>", "
|
|
17793
|
-
.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.")
|
|
17794
18002
|
.option("--json", "Print a JSON envelope.")
|
|
17795
18003
|
.action(async (mailbox, options) => {
|
|
17796
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;
|
|
17797
18018
|
if (options.clear) {
|
|
17798
|
-
|
|
17799
|
-
method: "PATCH",
|
|
17800
|
-
body: { ramp_config: null },
|
|
17801
|
-
});
|
|
18019
|
+
rampConfig = null;
|
|
17802
18020
|
}
|
|
17803
|
-
|
|
17804
|
-
|
|
17805
|
-
|
|
17806
|
-
|
|
17807
|
-
|
|
17808
|
-
increaseEveryDays
|
|
17809
|
-
step
|
|
17810
|
-
cap
|
|
17811
|
-
|
|
18021
|
+
else if (options.off) {
|
|
18022
|
+
rampConfig = { enabled: false };
|
|
18023
|
+
}
|
|
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
|
+
};
|
|
17812
18040
|
}
|
|
17813
18041
|
return requestOxygen(`/api/cli/mailboxes/${encodeURIComponent(mailbox)}/ramp`, {
|
|
17814
18042
|
method: "PATCH",
|
|
17815
|
-
body: {
|
|
17816
|
-
ramp_config: {
|
|
17817
|
-
start_per_day: startPerDay,
|
|
17818
|
-
increase_every_days: increaseEveryDays,
|
|
17819
|
-
step,
|
|
17820
|
-
cap,
|
|
17821
|
-
},
|
|
17822
|
-
},
|
|
18043
|
+
body: { ramp_config: rampConfig },
|
|
17823
18044
|
});
|
|
17824
18045
|
});
|
|
17825
18046
|
})))
|
|
@@ -18587,10 +18808,11 @@ Examples:
|
|
|
18587
18808
|
.option("--dmarc-policy <policy>", "DMARC policy: none (default), quarantine, or reject.")
|
|
18588
18809
|
.option("--dmarc-rua <mailbox>", "DMARC aggregate-report mailbox (rua=mailto:...).")
|
|
18589
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.")
|
|
18590
18812
|
.option("--no-auto-dkim", "Skip auto-deriving Microsoft DKIM selector1/selector2 CNAMEs (default: auto-derive them for microsoft when no DKIM is supplied).")
|
|
18591
18813
|
.option("--json", "Print a JSON envelope.")
|
|
18592
|
-
.action(async (domain, options) => {
|
|
18593
|
-
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));
|
|
18594
18816
|
}))
|
|
18595
18817
|
.addCommand(new Command("apply")
|
|
18596
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.")
|
|
@@ -18601,10 +18823,11 @@ Examples:
|
|
|
18601
18823
|
.option("--dmarc-policy <policy>", "DMARC policy: none (default), quarantine, or reject.")
|
|
18602
18824
|
.option("--dmarc-rua <mailbox>", "DMARC aggregate-report mailbox (rua=mailto:...).")
|
|
18603
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.")
|
|
18604
18827
|
.option("--no-auto-dkim", "Skip auto-deriving Microsoft DKIM (must match the value used in `dns plan` so the plan_hash matches).")
|
|
18605
18828
|
.option("--json", "Print a JSON envelope.")
|
|
18606
|
-
.action(async (domain, options) => {
|
|
18607
|
-
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));
|
|
18608
18831
|
})))
|
|
18609
18832
|
.addCommand(new Command("tracking")
|
|
18610
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.")
|
|
@@ -19154,6 +19377,7 @@ Run completion:
|
|
|
19154
19377
|
.option("--max-credits <n>", "Required credit ceiling for live calls.")
|
|
19155
19378
|
.option("--approved", "Required for live calls after inspecting a dry run.")
|
|
19156
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"]))
|
|
19157
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.")
|
|
19158
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.")
|
|
19159
19383
|
.option("--include-bundle", "Include durable recipe bundles in JSON output.")
|
|
@@ -19184,6 +19408,7 @@ Run completion:
|
|
|
19184
19408
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
19185
19409
|
...(options.approved ? { approved: true } : {}),
|
|
19186
19410
|
...(revisionVersion !== undefined ? { revision_version: revisionVersion } : {}),
|
|
19411
|
+
...(readOption(options.testScope) ? { test_scope: readOption(options.testScope) } : {}),
|
|
19187
19412
|
...(readOption(options.node) ? { test_node_id: readOption(options.node) } : {}),
|
|
19188
19413
|
...(options.preview ? { preview: true } : {}),
|
|
19189
19414
|
},
|
|
@@ -19746,6 +19971,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
19746
19971
|
.addCommand(new Command("get")
|
|
19747
19972
|
.description("Hydrate one exact Oxygen skill entrypoint and its document index.")
|
|
19748
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.")
|
|
19749
19975
|
.option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
|
|
19750
19976
|
.option("--json", "Print a JSON envelope.")
|
|
19751
19977
|
.action(async (skillName, options) => {
|
|
@@ -19776,6 +20002,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
19776
20002
|
registerConnectorsCommands(program, handleAsyncAction);
|
|
19777
20003
|
registerSettingsCommands(program, handleAsyncAction);
|
|
19778
20004
|
registerCrmSupportFlags(program);
|
|
20005
|
+
registerCustomerOperationCommands(program, handleAsyncAction);
|
|
19779
20006
|
// After the register* helpers: help groups are assigned to the commands that
|
|
19780
20007
|
// exist when this runs, and a family registered later fell into commander's
|
|
19781
20008
|
// trailing default "Commands:" list (the 2026-09-30 blind agent read the first
|
|
@@ -21956,6 +22183,7 @@ function readFeedBindBody(table, options) {
|
|
|
21956
22183
|
...(readOption(options.intentId) ? { intent_id: readOption(options.intentId) } : {}),
|
|
21957
22184
|
...(readOption(options.inputShape) ? { input_shape: readOption(options.inputShape) } : {}),
|
|
21958
22185
|
...(readOption(options.routeId) ? { route_id: readOption(options.routeId) } : {}),
|
|
22186
|
+
...(readOption(options.registryVersion) ? { registry_version: readOption(options.registryVersion) } : {}),
|
|
21959
22187
|
...(options.sourcePrompt ? { source_prompt: readFileIfPresent(options.sourcePrompt) } : {}),
|
|
21960
22188
|
...(options.approved ? { approved: true } : {}),
|
|
21961
22189
|
};
|
|
@@ -21980,11 +22208,13 @@ const FEED_BIND_PULL_ONLY_FLAGS = [
|
|
|
21980
22208
|
["intentId", "--intent-id"],
|
|
21981
22209
|
["inputShape", "--input-shape"],
|
|
21982
22210
|
["routeId", "--route-id"],
|
|
22211
|
+
["registryVersion", "--registry-version"],
|
|
21983
22212
|
["sourcePrompt", "--source-prompt"],
|
|
21984
22213
|
["approved", "--approved"],
|
|
21985
22214
|
];
|
|
21986
22215
|
const FEED_BIND_PUSH_ONLY_FLAGS = [
|
|
21987
22216
|
["create", "--create"],
|
|
22217
|
+
["project", "--project"],
|
|
21988
22218
|
["itemsPath", "--items-path"],
|
|
21989
22219
|
["eventIdPath", "--event-id-path"],
|
|
21990
22220
|
["eventTypePath", "--event-type-path"],
|
|
@@ -22001,7 +22231,7 @@ const FEED_BIND_WEB_INTENT_SOURCES = ["rb2b"];
|
|
|
22001
22231
|
const FEED_BIND_WEB_INTENT_REFUSED_FLAGS = [
|
|
22002
22232
|
["schemaJson", "--schema-json"],
|
|
22003
22233
|
...FEED_BIND_PULL_ONLY_FLAGS,
|
|
22004
|
-
...FEED_BIND_PUSH_ONLY_FLAGS.filter(([key]) => key !== "create"),
|
|
22234
|
+
...FEED_BIND_PUSH_ONLY_FLAGS.filter(([key]) => key !== "create" && key !== "project"),
|
|
22005
22235
|
];
|
|
22006
22236
|
// Pull mode keeps the contract commander enforced when --kind/--tool-id/
|
|
22007
22237
|
// --request-json were requiredOptions and <table> was required: same messages, same
|
|
@@ -22044,6 +22274,8 @@ function readFeedPushBindBody(table, options) {
|
|
|
22044
22274
|
const rowSchema = parseJsonObject(readFileIfPresent(schemaSource));
|
|
22045
22275
|
const tableRef = readOption(table);
|
|
22046
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 });
|
|
22047
22279
|
if (tableRef && create) {
|
|
22048
22280
|
throw new OxygenError("invalid_request", "Pass a table or --create <name>, not both.", { exitCode: 1 });
|
|
22049
22281
|
}
|
|
@@ -22057,7 +22289,7 @@ function readFeedPushBindBody(table, options) {
|
|
|
22057
22289
|
return {
|
|
22058
22290
|
transport: "push",
|
|
22059
22291
|
row_schema: rowSchema,
|
|
22060
|
-
...(tableRef ? { table: tableRef } : { create_table: { name: create } }),
|
|
22292
|
+
...(tableRef ? { table: tableRef } : { create_table: { name: create, ...(readOption(options.project) ? { project: readOption(options.project) } : {}) } }),
|
|
22061
22293
|
...(readOption(options.name) ? { name: readOption(options.name) } : {}),
|
|
22062
22294
|
...(readOption(options.upsertKey) ? { upsert_key: readOption(options.upsertKey) } : {}),
|
|
22063
22295
|
...(readOption(options.itemsPath) ? { items_path: readOption(options.itemsPath) } : {}),
|
|
@@ -22083,6 +22315,8 @@ function readFeedWebIntentBindBody(table, options) {
|
|
|
22083
22315
|
}
|
|
22084
22316
|
const tableRef = readOption(table);
|
|
22085
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 });
|
|
22086
22320
|
if (tableRef && create) {
|
|
22087
22321
|
throw new OxygenError("invalid_request", "Pass a table or --create <name>, not both.", { exitCode: 1 });
|
|
22088
22322
|
}
|
|
@@ -22092,7 +22326,7 @@ function readFeedWebIntentBindBody(table, options) {
|
|
|
22092
22326
|
return {
|
|
22093
22327
|
transport: "push",
|
|
22094
22328
|
feed_kind: source,
|
|
22095
|
-
...(tableRef ? { table: tableRef } : { create_table: { name: create } }),
|
|
22329
|
+
...(tableRef ? { table: tableRef } : { create_table: { name: create, ...(readOption(options.project) ? { project: readOption(options.project) } : {}) } }),
|
|
22096
22330
|
...(readOption(options.name) ? { name: readOption(options.name) } : {}),
|
|
22097
22331
|
...(readOption(options.upsertKey) ? { upsert_key: readOption(options.upsertKey) } : {}),
|
|
22098
22332
|
};
|
|
@@ -22202,7 +22436,7 @@ function readSignalsSearchRunBody(options, promptArg) {
|
|
|
22202
22436
|
const plan = options.planJson ? readSearchPlanJson(options.planJson) : null;
|
|
22203
22437
|
// A post search needs no prompt: its --keywords and filters are the plan,
|
|
22204
22438
|
// as they are for the free preview (the server names the table after them).
|
|
22205
|
-
const postSearchFilters = !prompt && !plan && readOption(options.family) === "linkedin_posts"
|
|
22439
|
+
const postSearchFilters = !prompt && !plan && (readOption(options.source) ?? readOption(options.family)) === "linkedin_posts"
|
|
22206
22440
|
? readSignalSearchFilters(options)
|
|
22207
22441
|
: null;
|
|
22208
22442
|
const postSearch = Array.isArray(postSearchFilters?.keywords) && postSearchFilters.keywords.length > 0;
|
|
@@ -22228,6 +22462,7 @@ function readSignalsSearchRunBody(options, promptArg) {
|
|
|
22228
22462
|
}
|
|
22229
22463
|
const filters = prompt ? typedFilters : postSearchFilters;
|
|
22230
22464
|
return {
|
|
22465
|
+
...(readOption(options.source) ? { source: readOption(options.source) } : {}),
|
|
22231
22466
|
...(prompt ? { prompt } : {}),
|
|
22232
22467
|
...(plan ? { plan } : {}),
|
|
22233
22468
|
...(readOption(options.family) ? { family: readOption(options.family) } : {}),
|
|
@@ -23621,6 +23856,19 @@ function domainsBuyRerunCommand(domains, quoteId, options, data) {
|
|
|
23621
23856
|
];
|
|
23622
23857
|
return `${resolveCliBinaryName()} domains buy ${domains.join(" ")} ${flags.join(" ")}`;
|
|
23623
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
|
+
}
|
|
23624
23872
|
function dnsPlanBody(options) {
|
|
23625
23873
|
const provider = readOption(options.provider);
|
|
23626
23874
|
if (!provider) {
|
|
@@ -23632,6 +23880,7 @@ function dnsPlanBody(options) {
|
|
|
23632
23880
|
...(readOption(options.dmarcRua) ? { dmarc_rua: readOption(options.dmarcRua) } : {}),
|
|
23633
23881
|
...(readOption(options.trackingHost) ? { tracking_host: readOption(options.trackingHost) } : {}),
|
|
23634
23882
|
...(options.autoDkim === false ? { auto_dkim: false } : {}),
|
|
23883
|
+
...(options.dkimJson !== undefined ? { dkim: readDnsDkimOption(options.dkimJson) } : {}),
|
|
23635
23884
|
};
|
|
23636
23885
|
}
|
|
23637
23886
|
// Preview the cold-email DNS plan for a domain (free — no writes). Appends a
|
|
@@ -23641,7 +23890,12 @@ async function runDomainsDnsPlan(domain, options) {
|
|
|
23641
23890
|
const planHash = readRecordString(data, "plan_hash");
|
|
23642
23891
|
const provider = readOption(options.provider);
|
|
23643
23892
|
return planHash && provider
|
|
23644
|
-
? { ...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(" ") }
|
|
23645
23899
|
: data;
|
|
23646
23900
|
}
|
|
23647
23901
|
// Apply an approved DNS plan (writes records). Requires --plan <hash> with
|
|
@@ -27931,12 +28185,26 @@ function buildFeedbackBody(options) {
|
|
|
27931
28185
|
context: {
|
|
27932
28186
|
cli_version: OXYGEN_VERSION,
|
|
27933
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) } : {}),
|
|
27934
28196
|
},
|
|
27935
28197
|
};
|
|
27936
28198
|
if (readOption(options.severity))
|
|
27937
28199
|
body.severity = readOption(options.severity);
|
|
27938
28200
|
const sessionId = readOption(options.sessionId);
|
|
27939
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
|
+
}
|
|
27940
28208
|
const includeTranscript = options.transcript !== false &&
|
|
27941
28209
|
(options.includeTranscript === true || Boolean(sessionId) || Boolean(file));
|
|
27942
28210
|
if (includeTranscript) {
|
|
@@ -27970,7 +28238,13 @@ function buildSupportTicketBody(options) {
|
|
|
27970
28238
|
const body = { subject: readOption(options.subject), body: message };
|
|
27971
28239
|
const sessionId = readOption(options.sessionId);
|
|
27972
28240
|
const file = readOption(options.transcriptFile);
|
|
27973
|
-
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)) {
|
|
27974
28248
|
try {
|
|
27975
28249
|
const captured = captureCurrentTranscript({ sessionId, file });
|
|
27976
28250
|
if (!captured) {
|
|
@@ -27993,6 +28267,8 @@ function buildSupportTicketBody(options) {
|
|
|
27993
28267
|
if (readOption(options.source))
|
|
27994
28268
|
body.source = readOption(options.source);
|
|
27995
28269
|
const context = {};
|
|
28270
|
+
if (readOption(options.cliVersion))
|
|
28271
|
+
context.cli_version = readOption(options.cliVersion);
|
|
27996
28272
|
if (readOption(options.operation))
|
|
27997
28273
|
context.operation = readOption(options.operation);
|
|
27998
28274
|
if (readOption(options.errorCode))
|
|
@@ -28016,6 +28292,13 @@ function buildSupportTicketBody(options) {
|
|
|
28016
28292
|
body.context = context;
|
|
28017
28293
|
return body;
|
|
28018
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
|
+
}
|
|
28019
28302
|
function withSupportListQuery(path, options) {
|
|
28020
28303
|
const params = new URLSearchParams();
|
|
28021
28304
|
const status = readOption(options.status);
|
|
@@ -28452,7 +28735,10 @@ function buildContextAssetUpsertBody(options) {
|
|
|
28452
28735
|
}
|
|
28453
28736
|
function buildKnowledgePageUpsertBody(options) {
|
|
28454
28737
|
const tags = readCsvOption(options.tags);
|
|
28455
|
-
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
|
+
}
|
|
28456
28742
|
const folder = readOption(options.folder);
|
|
28457
28743
|
return {
|
|
28458
28744
|
...(folder ? { folderId: folder === "root" ? null : folder } : {}),
|
|
@@ -29449,6 +29735,8 @@ function readSequenceSettings(options) {
|
|
|
29449
29735
|
// A visible footer mutates outbound copy, so it is opt-in and tri-state:
|
|
29450
29736
|
// absent leaves the stored value alone; either explicit flag persists the
|
|
29451
29737
|
// chosen boolean. Missing/false is interpreted as OFF by the dispatcher.
|
|
29738
|
+
if (options.autoOptimize !== undefined)
|
|
29739
|
+
settings.auto_optimize = options.autoOptimize;
|
|
29452
29740
|
if (options.bounceProtection !== undefined)
|
|
29453
29741
|
settings.bounce_protection = JSON.parse(options.bounceProtection);
|
|
29454
29742
|
if (options.includeUnsubscribeHeaders !== undefined)
|
|
@@ -29509,7 +29797,7 @@ function readSequenceSettings(options) {
|
|
|
29509
29797
|
// Sender profiles (--sender-profiles id1,id2): the sequence sends from these
|
|
29510
29798
|
// unified identities. The server resolves them into senderAccountIds + mailbox_ids.
|
|
29511
29799
|
const senderProfileIds = readCsvOption(options.senderProfiles);
|
|
29512
|
-
if (
|
|
29800
|
+
if (options.senderProfiles !== undefined)
|
|
29513
29801
|
settings.sender_profile_ids = senderProfileIds;
|
|
29514
29802
|
return Object.keys(settings).length > 0 ? settings : undefined;
|
|
29515
29803
|
}
|
|
@@ -29596,6 +29884,14 @@ catch (error) {
|
|
|
29596
29884
|
// envelope; agents parse stdout and branch on the exit code, and the exit
|
|
29597
29885
|
// table classes a usage error as 2 in every mode.
|
|
29598
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
|
+
}
|
|
29599
29895
|
if (usage && argvRequestsJson(cliArgs))
|
|
29600
29896
|
writeJson(usage.envelope);
|
|
29601
29897
|
process.exitCode = usage?.exitCode ?? error.exitCode;
|