@oxygen-agent/cli 1.1011.855 → 1.1011.1091
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/admin-lead-magnet-commands.d.ts +7 -0
- package/dist/admin-lead-magnet-commands.js +128 -0
- package/dist/command-manifest.js +29 -0
- package/dist/crm-support-flags.d.ts +1 -0
- package/dist/crm-support-flags.js +5 -1
- package/dist/customer-operation-commands.js +30 -1
- package/dist/functions-commands.js +1 -1
- package/dist/help.js +10 -2
- package/dist/http-client.d.ts +4 -0
- package/dist/http-client.js +21 -10
- package/dist/index.js +444 -208
- package/dist/sender-limits-flags.d.ts +7 -0
- package/dist/sender-limits-flags.js +13 -0
- package/dist/workspace-utility-flags.d.ts +7 -0
- package/dist/workspace-utility-flags.js +45 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/attempt-provenance.js +83 -0
- package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/billing-anchors.js +7 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +279 -13
- package/node_modules/@oxygen/shared/dist/cli-result.js +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +21 -9
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.d.ts +7 -1
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.js +5 -1
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +14 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +13 -11
- package/node_modules/@oxygen/shared/dist/contracts/limits-read.js +11 -1
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/schema.js +10 -6
- package/node_modules/@oxygen/shared/dist/contracts/spintax.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/contracts/spintax.js +11 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.js +21 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-utilities.d.ts +162 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-utilities.js +46 -0
- package/node_modules/@oxygen/shared/dist/credit-topup-review.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/credit-topup-review.js +3 -0
- package/node_modules/@oxygen/shared/dist/emailguard-placement-probe.d.ts +70 -0
- package/node_modules/@oxygen/shared/dist/emailguard-placement-probe.js +161 -0
- package/node_modules/@oxygen/shared/dist/future-signup-events.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/future-signup-events.js +4 -0
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +2 -2
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +48 -0
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +94 -0
- package/node_modules/@oxygen/shared/dist/partner-coupon-bonus.d.ts +45 -0
- package/node_modules/@oxygen/shared/dist/partner-coupon-bonus.js +57 -0
- package/node_modules/@oxygen/shared/dist/provider-balance-signal.js +3 -2
- package/node_modules/@oxygen/shared/dist/table-filter-operators.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/table-filter-operators.js +50 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +4 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- 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/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.d.ts +42 -0
- package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.js +68 -0
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +33 -0
- package/node_modules/@oxygen/shared/package.json +5 -5
- package/node_modules/@oxygen/workflows/dist/index.d.ts +1 -0
- package/node_modules/@oxygen/workflows/dist/index.js +1 -0
- package/node_modules/@oxygen/workflows/dist/recipe-timestamp.d.ts +22 -0
- package/node_modules/@oxygen/workflows/dist/recipe-timestamp.js +72 -0
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +9 -5
- package/package.json +1 -1
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.d.ts +0 -9
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.js +0 -33
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registerAdminLeadMagnetCommands } from "./admin-lead-magnet-commands.js";
|
|
1
2
|
import { renderPrimaryProviderBoard } from "./admin-primary-providers-render.js";
|
|
2
3
|
import { isTruthyEnv, markStdinConsumed, maybeScheduleBackgroundUpdate, readAutoUpdateStatus, runBackgroundUpdate, } from "./auto-update.js";
|
|
3
4
|
import { createBrowserLoginSession, createRelayLoginSession, openBrowser } from "./browser-login.js";
|
|
@@ -9,7 +10,7 @@ import { registerConnectorsCommands } from "./connectors-commands.js";
|
|
|
9
10
|
import { addContractFlags, readContractFlags } from "./contract-flags.js";
|
|
10
11
|
import { clearCredentials, defaultApiUrl, listCredentialProfiles, loadCredentials, normalizeApiUrl, pickProfileNameForIdentity, pickProfileNameForUserSession, readEnvToken, resolveActiveProfile, saveCredentials, switchCredentialProfile, updateActiveOrganizationForProfile, } from "./credentials.js";
|
|
11
12
|
import { registerCrmFileReadCommands } from "./crm-file-read-commands.js";
|
|
12
|
-
import { registerCrmSupportFlags, readCrmNoteEditFields, readCrmPhotoFields, readCrmFileLabel, buildCrmObjectsPath, buildCrmConnectionsPath } from "./crm-support-flags.js";
|
|
13
|
+
import { registerCrmSupportFlags, readCrmNoteEditFields, readCrmPhotoFields, readCrmFileLabel, buildCrmObjectsPath, buildCrmConnectionsPath, buildCrmRecordPath } from "./crm-support-flags.js";
|
|
13
14
|
import { registerCustomerOperationCommands } from "./customer-operation-commands.js";
|
|
14
15
|
import { DATABASE_QUERY_FLAG_OVERLAY } from "./database-query-flags.js";
|
|
15
16
|
import { addFeedReadFlags, feedReadWindowParams } from "./feed-read-flags.js";
|
|
@@ -25,7 +26,7 @@ import { formatMailboxRampNotice } from "./mailbox-ramp-notice.js";
|
|
|
25
26
|
import { waitForCliRun } from "./run-wait.js";
|
|
26
27
|
import { resolveCliBinaryName } from "./runtime.js";
|
|
27
28
|
import { formatSearchAiFilterTotalsNotice } from "./search-ai-filter-notice.js";
|
|
28
|
-
import { buildSenderLimitsBody, parseWorkingDaysOption, SENDER_LIMITS_FLAG_OVERLAY } from "./sender-limits-flags.js";
|
|
29
|
+
import { addWhatsAppLimitFlags, buildSenderLimitsBody, parseWorkingDaysOption, SENDER_LIMITS_FLAG_OVERLAY } from "./sender-limits-flags.js";
|
|
29
30
|
import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, updateSessionStep, } from "./session.js";
|
|
30
31
|
import { registerSettingsCommands } from "./settings-commands.js";
|
|
31
32
|
import { doctorAgentSkills, getAgentSkill, installAgentSkills, listAgentSkills, runAutomaticSkillsInstall, searchAgentSkills, } from "./skills.js";
|
|
@@ -40,6 +41,7 @@ import { registerVisualCommands } from "./visual-commands.js";
|
|
|
40
41
|
import { WHATSAPP_ACCOUNT_FLAGS, whatsappAccountQuery } from "./whatsapp-account-flags.js";
|
|
41
42
|
import { formatWorkflowPlanLimitNotices } from "./workflow-plan-limit-notices.js";
|
|
42
43
|
import { WORKFLOW_RUN_LIST_FLAG_OVERLAY, readWorkflowRunListFlags } from "./workflow-run-list-flags.js";
|
|
44
|
+
import { addProviderLimitFlags, addSuppressionListFlags, providerLimitBody, providerLimitQuery, suppressionListQuery } from "./workspace-utility-flags.js";
|
|
43
45
|
import { isRecipeDefinition } from "@oxygen/recipe-sdk";
|
|
44
46
|
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";
|
|
45
47
|
import { PURCHASABLE_PLAN_KEYS, planVolumeBonusPercent, resolveBasePricingPlan } from "@oxygen/shared/billing";
|
|
@@ -66,12 +68,14 @@ import { WHATSAPP_ACCOUNTS_READ_CONTRACT } from "@oxygen/shared/contracts/whatsa
|
|
|
66
68
|
import { WORKFLOW_RUN_LIST_CONTRACT, workflowRunListQueryParams } from "@oxygen/shared/contracts/workflow-run-list";
|
|
67
69
|
import { MANAGED_DATA_SUPPLIERS } from "@oxygen/shared/data-suppliers";
|
|
68
70
|
import { MAX_BUFFERED_IMPORT_PARSE_BYTES, assertImportFormatWithinBufferedLimit, inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, parseXlsxWorkbookBuffer, } from "@oxygen/shared/file-import";
|
|
71
|
+
import { KNOWLEDGE_PAGE_TYPES } from "@oxygen/shared/knowledge-constants";
|
|
69
72
|
import { LINKEDIN_SENDER_LIMIT_DEFAULTS, LINKEDIN_SENDER_LIMIT_MAXIMUMS, } from "@oxygen/shared/linkedin-sequences";
|
|
70
73
|
import { MAILBOX_IMPORT_FILE_MAX_BYTES as SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES, MAILBOX_IMPORT_ROW_LIMIT as SHARED_MAILBOX_IMPORT_ROW_LIMIT, normalizeMailboxImportFile as normalizeSharedMailboxImportFile, normalizeMailboxImportVendor as normalizeSharedMailboxImportVendor, normalizeMailboxWorkbookRows, parseMailboxImportText, summarizeMailboxImportValidation as summarizeSharedMailboxImportValidation, } from "@oxygen/shared/mailbox-import";
|
|
71
74
|
import { PUBLIC_WEBHOOK_INGRESS_PER_TARGET_PER_MINUTE } from "@oxygen/shared/plan-band";
|
|
72
75
|
import { AI_COLUMN_CREDITS, AI_COLUMN_WEB_SEARCH_TYPICAL_CREDITS } from "@oxygen/shared/pricing-sheet";
|
|
73
76
|
import { TAG_COLORS } from "@oxygen/shared/select-options";
|
|
74
77
|
import { SEQUENCE_LIFECYCLES, sequenceDisplayStatus } from "@oxygen/shared/sequence-lifecycle";
|
|
78
|
+
import { FILTER_TREE_OPERATORS } from "@oxygen/shared/table-filter-operators";
|
|
75
79
|
import { assertRecipeBundleSafe, assertPortableWorkflowDefinition, assertWorkflowGraphManifest, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowGraphManifest, isWorkflowManifest, hashPortableWorkflowGraphManifest, WORKFLOW_GRAPH_COMPILER_VERSION, WORKFLOW_GRAPH_MANIFEST_VERSION, } from "@oxygen/workflows";
|
|
76
80
|
import { Command, CommanderError, Option } from "commander";
|
|
77
81
|
import { execFileSync } from "node:child_process";
|
|
@@ -352,6 +356,20 @@ function suppressionListParams(options) {
|
|
|
352
356
|
// feed): `translateScheduleEvery` in apps/web resolves all four forms, so this
|
|
353
357
|
// string is the single place the CLI spells them out.
|
|
354
358
|
const CADENCE_FLAG_DESCRIPTION = "Shorthand schedule: hourly, every_6h, daily@<hour>, or weekly@<day><hour> (e.g. weekly@mon9). Hours 0-23 UTC.";
|
|
359
|
+
/** A --value for the pricing book: a number, true/false for a charge's active flag, or "none" for no plan limit. */
|
|
360
|
+
function readPriceBookValue(raw) {
|
|
361
|
+
if (raw === undefined)
|
|
362
|
+
return undefined;
|
|
363
|
+
const value = raw.trim();
|
|
364
|
+
if (value === "true")
|
|
365
|
+
return true;
|
|
366
|
+
if (value === "false")
|
|
367
|
+
return false;
|
|
368
|
+
if (value === "none" || value === "null")
|
|
369
|
+
return null;
|
|
370
|
+
const number = Number(value);
|
|
371
|
+
return value !== "" && Number.isFinite(number) ? number : value;
|
|
372
|
+
}
|
|
355
373
|
const BROWSER_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
356
374
|
const BROWSER_LOGIN_FALLBACK_HINT_MS = 15 * 1000;
|
|
357
375
|
const OXYGEN_SPINNER_INTERVAL_MS = 90;
|
|
@@ -1703,6 +1721,15 @@ function writeMaxCreditsHint(error) {
|
|
|
1703
1721
|
process.stderr.write(`hint: the estimate is ${estimated} credits; re-run with --max-credits ${estimated} or higher\n`);
|
|
1704
1722
|
return;
|
|
1705
1723
|
}
|
|
1724
|
+
case "copilot_send_timeout": {
|
|
1725
|
+
const nextStep = readDetailsString(error.details, "next_step");
|
|
1726
|
+
const interrupt = readDetailsString(error.details, "interrupt_command");
|
|
1727
|
+
if (nextStep)
|
|
1728
|
+
process.stderr.write(`hint: the turn is still running; read it with \`${nextStep}\`\n`);
|
|
1729
|
+
if (interrupt)
|
|
1730
|
+
process.stderr.write(`hint: stop it (and its spend) with \`${interrupt}\`\n`);
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1706
1733
|
default:
|
|
1707
1734
|
}
|
|
1708
1735
|
}
|
|
@@ -3798,6 +3825,7 @@ export function createProgram() {
|
|
|
3798
3825
|
const workspaceCommand = program
|
|
3799
3826
|
.command("workspace")
|
|
3800
3827
|
.description("Inspect this workspace or end its managed external infrastructure.")
|
|
3828
|
+
.addHelpText("before", "\nActive workspace and identity: oxygen whoami --summary-json (read-only, 0 credits).\n")
|
|
3801
3829
|
.addCommand(new Command("map")
|
|
3802
3830
|
.description("What this workspace actually contains: the newest tables, CRM objects, sequences, workflows, agents, wiki pages, posts and projects, each with its id, last activity, tags and link. Read-only, 0 credits. Capability search tells you what OXYGEN can do; this tells you what is here.")
|
|
3803
3831
|
.option("--kind <kinds>", "Comma-separated kinds to include: table, crm_object, sequence, workflow, agent, knowledge_page, post, project.")
|
|
@@ -4013,7 +4041,7 @@ export function createProgram() {
|
|
|
4013
4041
|
});
|
|
4014
4042
|
}))
|
|
4015
4043
|
.addCommand(new Command("billing-link")
|
|
4016
|
-
.description("Cover a workspace with
|
|
4044
|
+
.description("Cover a workspace with another organization's paid plan instead of buying a second subscription. Omit --owner when exactly one owner is eligible; orgs billing-owners lists them. You must administer both organizations, the workspace must not hold its own live subscription, and an org-bound key can link only its workspace. The owner funds credits; data access stays unchanged. billing estimate reports sender_billing: credit_reservations uses the owner's credits, including when plan_tier is free; legacy sending_seats uses the owner's paid plan and seats. BYOK and Instagram/TikTok/YouTube/Facebook publishing still need a paid plan. billing usage --group-by workspace reports each linked workspace's spend.")
|
|
4017
4045
|
.option("--owner <organization>", "Billing owner organization id, Clerk org id, or slug. Optional — when omitted, Oxygen uses your one eligible organization, and refuses to pick if there is more than one.")
|
|
4018
4046
|
.option("--organization <organization>", "Workspace organization to link. Defaults to the active organization.")
|
|
4019
4047
|
.option("--organization-id <id>", "Alias for --organization.")
|
|
@@ -4041,7 +4069,7 @@ export function createProgram() {
|
|
|
4041
4069
|
});
|
|
4042
4070
|
}))
|
|
4043
4071
|
.addCommand(new Command("billing-unlink")
|
|
4044
|
-
.description("Return a workspace to
|
|
4072
|
+
.description("Return a workspace to its own billing owner and credit balance. Its own subscription state, plan and sender_billing model determine which actions need a plan.")
|
|
4045
4073
|
.option("--organization <organization>", "Workspace organization to unlink. Defaults to the active organization.")
|
|
4046
4074
|
.option("--organization-id <id>", "Alias for --organization.")
|
|
4047
4075
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -4188,11 +4216,12 @@ Examples:
|
|
|
4188
4216
|
await handleAsyncAction("support admin list", options, () => requestOxygen(withSupportListQuery("/api/cli/admin/support/tickets", options)));
|
|
4189
4217
|
}))
|
|
4190
4218
|
.addCommand(new Command("get")
|
|
4191
|
-
.description("Show one live Plain Thread, its routing metadata, and customer-visible messages (staff only).")
|
|
4219
|
+
.description("Show one live Plain Thread, its routing metadata, and customer-visible messages (staff only). Add --include-notes to also read back the Thread's team-only notes and staged reply drafts as `internal_notes` (kind note, reply_draft, suggested_reply_provenance or undelivered_customer_report); read-only, never shown to the customer.")
|
|
4192
4220
|
.argument("<ticketId>", "Plain Thread ID (th_...) or Thread ref (T-<n>), as shown by `oxygen support admin list`.")
|
|
4221
|
+
.option("--include-notes", "Also return team-only internal notes and staged drafts.")
|
|
4193
4222
|
.option("--json", "Print a JSON envelope.")
|
|
4194
4223
|
.action(async (ticketId, options) => {
|
|
4195
|
-
await handleAsyncAction("support admin get", options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}`));
|
|
4224
|
+
await handleAsyncAction("support admin get", options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}${options.includeNotes ? "?include_notes=1" : ""}`));
|
|
4196
4225
|
}))
|
|
4197
4226
|
.addCommand(new Command("open")
|
|
4198
4227
|
.description("Return the exact Plain Inbox URL for one live Thread (staff only).")
|
|
@@ -4756,7 +4785,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4756
4785
|
}));
|
|
4757
4786
|
}))
|
|
4758
4787
|
.addCommand(new Command("color")
|
|
4759
|
-
.description("Set a
|
|
4788
|
+
.description("Set a folder's colour in the Tables sidebar (rail) for everyone in the workspace. `--color auto` restores the automatic colour. Free.")
|
|
4760
4789
|
.argument("<project>", "Project slug or id.")
|
|
4761
4790
|
.requiredOption("--color <color>", "Colour name (gray, red, orange, amber, yellow, green, teal, blue, indigo, purple, pink) or `auto` to clear.")
|
|
4762
4791
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -5189,9 +5218,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5189
5218
|
}));
|
|
5190
5219
|
})))
|
|
5191
5220
|
.addCommand(new Command("comments")
|
|
5192
|
-
.description("Publishing Community for public comments on every recent post owned by a connected LinkedIn account, including posts created directly on LinkedIn. Start with `oxygen publishing comments list --view unanswered --json`. These are network conversations, not workspace review notes. The local queue targets a six-hour background discovery + comment poll; provider quota/backoff can delay it, and
|
|
5221
|
+
.description("Publishing Community for public comments on every recent post owned by a connected LinkedIn account, including posts created directly on LinkedIn. Start with `oxygen publishing comments list --view unanswered --json`. These are network conversations, not workspace review notes. The local queue targets a six-hour background discovery + comment poll; provider quota/backoff can delay it. Read freshness, empty_queue_is_current and guidance before trusting an empty queue. automatic_retry_at reports a future retry; next_sync_at alone does not confirm one. An unconfirmed provider read can pause refresh; contact support when guidance says to. No manual retry is available. Queue reads use 0 credits and make no provider call; only explicit approval can post a public reply.")
|
|
5193
5222
|
.addCommand(new Command("list")
|
|
5194
|
-
.description("List the Publishing Community work queue, oldest first, with polling freshness/backoff even when empty. Defaults to the Unanswered view: needs_reply, draft, awaiting_approval, and action_unavailable. All
|
|
5223
|
+
.description("List the Publishing Community work queue, oldest first, with polling freshness/backoff even when empty. Defaults to the Unanswered view: needs_reply, draft, awaiting_approval, and action_unavailable. All covers every lifecycle state on published owned posts within 30 days plus UGC-linked posts, not lifetime LinkedIn history. Completed coverage counts earlier successful polls and can overlap failed coverage after a later error. Read-only: no provider call, 0 credits.")
|
|
5195
5224
|
.option("--view <view>", "Community view: unanswered (default) or all; all is bounded to recent owned posts.")
|
|
5196
5225
|
.option("--status <status>", "Advanced exact-state filter: needs_reply, draft, awaiting_approval, replied, resolved, ignored, spam, or action_unavailable. Cannot be combined with --view.")
|
|
5197
5226
|
.option("--post <post_id>", "Only comments on one scheduled Publishing post.")
|
|
@@ -5287,7 +5316,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5287
5316
|
}));
|
|
5288
5317
|
})))
|
|
5289
5318
|
.addCommand(new Command("analytics")
|
|
5290
|
-
.description("Stored LinkedIn and X engagement snapshots synced in the background. LinkedIn includes owned posts discovered from the provider; X includes only posts published through OXYGEN. Reading analytics makes no provider call and uses 0 credits. A snapshotted post completed a sync pass; use per-metric coverage to see which values were populated. Null is unknown, never zero.
|
|
5319
|
+
.description("Stored LinkedIn and X engagement snapshots synced in the background. LinkedIn includes owned posts discovered from the provider; X includes only posts published through OXYGEN. Reading analytics makes no provider call and uses 0 credits. A snapshotted post completed a sync pass; use per-metric coverage to see which values were populated. Null is unknown, never zero. Read freshness and guidance for delayed refresh; an unconfirmed provider read can pause it. No manual provider retry is available.")
|
|
5291
5320
|
.addCommand(new Command("summary")
|
|
5292
5321
|
.description("Workspace LinkedIn or X analytics. LinkedIn includes owned posts discovered from the provider; X currently includes posts published through OXYGEN. Uses each post's latest stored snapshot; snapshotted_posts (legacy measured_posts) counts sync passes, while coverage counts non-null values. Read-only: no provider call, 0 credits.")
|
|
5293
5322
|
.option("--channel <channel>", "Channel to summarize: linkedin (default) or x.")
|
|
@@ -5344,7 +5373,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5344
5373
|
await handleAsyncAction("publishing analytics post", options, () => requestOxygen(publishingReadQueryPath(buildPublishingAnalyticsPath(`/api/cli/publishing/analytics/posts/${encodeURIComponent(postId)}`, { since: readOption(options.since) }), publishingAnalyticsQueryContract, options)));
|
|
5345
5374
|
}))
|
|
5346
5375
|
.addCommand(addContractFlags(new Command("account"), publishingAnalyticsQueryContract)
|
|
5347
|
-
.description("Stored per-account metric series (followers) for connected LinkedIn and X publishing accounts, read once a day in the background. Read-only: no provider call, 0 credits.")
|
|
5376
|
+
.description("Stored per-account metric series (followers) for connected LinkedIn and X publishing accounts, read once a day in the background. Each account reports its snapshot freshness; stale values remain available. Read-only: no provider call, 0 credits.")
|
|
5348
5377
|
.option("--provider <provider>", "Channel to read: linkedin or x. Omit for every connected channel.")
|
|
5349
5378
|
.option("--since <date>", "ISO date to start the series from.")
|
|
5350
5379
|
.option("--account <key>", PUBLISHING_ANALYTICS_ACCOUNT_HELP, collectRepeatable, [])
|
|
@@ -5513,7 +5542,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5513
5542
|
.command("dashboards")
|
|
5514
5543
|
.description("Your saved dashboards, plus the built-in GTM funnel. `summary` reads the built-in Command Center funnel across the sequencer, unibox and CRM; everything else creates and edits dashboards you save yourself and open again later, from charts, saved table views, notes, embeds and live sources.")
|
|
5515
5544
|
.addCommand(new Command("summary")
|
|
5516
|
-
.description("Show the GTM funnel summary: touches → replies → positive → meetings → deals → won, with this-period revenue, channel split, and needs-action triage counts.")
|
|
5545
|
+
.description("Show the GTM funnel summary: touches → replies → positive → meetings → deals → won, with this-period revenue, channel split, and current needs-action triage. Unread replies across channels and interested email conversations are current inbox counts, independent of the selected range.")
|
|
5517
5546
|
.option("--range <range>", "Preset range: 7d, 14d, 28d, 30d, 90d, 180d, 365d, or all. Defaults to 14d.")
|
|
5518
5547
|
.option("--from <date>", "Custom start date (YYYY-MM-DD).")
|
|
5519
5548
|
.option("--to <date>", "Custom end date (YYYY-MM-DD).")
|
|
@@ -5805,16 +5834,16 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5805
5834
|
// and options.json is undefined here. optsWithGlobals() reads --json
|
|
5806
5835
|
// wherever it landed, so the envelope is emitted like every other
|
|
5807
5836
|
// crm_* command instead of printing the bare result.
|
|
5808
|
-
await handleAsyncAction("crm
|
|
5837
|
+
await handleAsyncAction("crm objects create", { json: Boolean(command.optsWithGlobals().json) }, () => requestOxygen("/api/cli/crm/objects", {
|
|
5809
5838
|
method: "POST",
|
|
5810
5839
|
body: buildCrmObjectCreateBody(options),
|
|
5811
5840
|
}));
|
|
5812
5841
|
}))
|
|
5813
5842
|
.addCommand(new Command("delete")
|
|
5814
|
-
.description("Delete a CUSTOM CRM object: removes the object and schedules its backing table for deletion (restorable for 14 days). Standard objects (companies, people, deals) are permanent and refused. Defaults to dry-run; --live requires --confirm. Re-run on an object a teammate already deleted, it reports alreadyArchived with the table's deletion state and purge date.")
|
|
5843
|
+
.description("Delete a CUSTOM CRM object: removes the object and schedules its backing table for deletion (restorable for 14 days). Standard objects (companies, people, deals) are permanent and refused. Defaults to dry-run; --live requires --confirm. Re-run on an object a teammate already deleted, it reports alreadyArchived with the table's deletion state and purge date. Restoring that table returns its rows as an ordinary table; the object stays deleted. Purging it also removes the object's archived definition.")
|
|
5815
5844
|
.argument("<object>", "Custom CRM object slug, such as projects or tasks.")
|
|
5816
|
-
.option("--keep-table", "Deregister the object but keep its table as an ordinary workspace table (no delete timer).")
|
|
5817
|
-
.option("--purge-after <days>", "Days until the backing table's physical purge. Defaults to 14; restorable via `tables restore` until then.")
|
|
5845
|
+
.option("--keep-table", "Deregister the object but keep its table as an ordinary workspace table (no delete timer). Ignored once the table is already scheduled.")
|
|
5846
|
+
.option("--purge-after <days>", "Days until the backing table's physical purge. Defaults to 14; restorable via `tables restore` until then. Ignored once the table is already scheduled.")
|
|
5818
5847
|
.option("--dry-run", "Preview the object, rows, relationships, and edges the delete would take. Default.")
|
|
5819
5848
|
.option("--live", "Apply the delete. Requires --confirm.")
|
|
5820
5849
|
.option("--confirm", "Confirm the live delete after inspecting the dry-run impact.")
|
|
@@ -5822,7 +5851,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5822
5851
|
.action(async (object, options, command) => {
|
|
5823
5852
|
// See `objects create` above: optsWithGlobals() recovers --json,
|
|
5824
5853
|
// which the parent command otherwise swallows.
|
|
5825
|
-
await handleAsyncAction("crm
|
|
5854
|
+
await handleAsyncAction("crm objects delete", { json: Boolean(command.optsWithGlobals().json) }, () => requestOxygen(`/api/cli/crm/objects/${encodeURIComponent(object)}`, {
|
|
5826
5855
|
method: "DELETE",
|
|
5827
5856
|
body: buildCrmObjectDeleteBody(options),
|
|
5828
5857
|
}));
|
|
@@ -5838,7 +5867,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5838
5867
|
.action(async (object, options, command) => {
|
|
5839
5868
|
// See `objects create` above: optsWithGlobals() recovers --json,
|
|
5840
5869
|
// which the parent command otherwise swallows.
|
|
5841
|
-
await handleAsyncAction("crm
|
|
5870
|
+
await handleAsyncAction("crm objects add-attr", { json: Boolean(command.optsWithGlobals().json) }, () => requestOxygen(`/api/cli/crm/objects/${encodeURIComponent(object)}/attributes`, {
|
|
5842
5871
|
method: "POST",
|
|
5843
5872
|
body: buildCrmObjectAddAttrBody(options),
|
|
5844
5873
|
}));
|
|
@@ -5855,7 +5884,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5855
5884
|
.option("--live", "Apply the change. Default is dry-run.")
|
|
5856
5885
|
.option("--json", "Print a JSON envelope.")
|
|
5857
5886
|
.action(async (object, attribute, options, command) => {
|
|
5858
|
-
await handleAsyncAction("crm
|
|
5887
|
+
await handleAsyncAction("crm objects update-attr", { json: Boolean(command.optsWithGlobals().json) }, () => requestOxygen(`/api/cli/crm/objects/${encodeURIComponent(object)}/attributes/${encodeURIComponent(attribute)}`, { method: "PATCH", body: buildCrmObjectUpdateAttrBody(options) }));
|
|
5859
5888
|
}))
|
|
5860
5889
|
.addCommand(new Command("remove-attr")
|
|
5861
5890
|
.description("Remove a field you added to a CRM object. The column is archived (restorable with `columns restore <table> <key>`, which brings the field back; no cell destroyed), the object stops listing it, and the key is free for a new field at once. Oxygen-shipped fields, the title field, identity fields and relations cannot be removed. Defaults to dry-run.")
|
|
@@ -5865,7 +5894,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5865
5894
|
.option("--live", "Remove the field. Default is dry-run.")
|
|
5866
5895
|
.option("--json", "Print a JSON envelope.")
|
|
5867
5896
|
.action(async (object, attribute, options, command) => {
|
|
5868
|
-
await handleAsyncAction("crm
|
|
5897
|
+
await handleAsyncAction("crm objects remove-attr", { json: Boolean(command.optsWithGlobals().json) }, () => requestOxygen(`/api/cli/crm/objects/${encodeURIComponent(object)}/attributes/${encodeURIComponent(attribute)}`, { method: "DELETE", body: { mode: resolveLiveDryRunMode(options) } }));
|
|
5869
5898
|
})))
|
|
5870
5899
|
.addCommand(new Command("search")
|
|
5871
5900
|
.description("Search CRM records by identity or record label. Takes ONE query; narrow the objects with --object, not a second argument.")
|
|
@@ -5901,7 +5930,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5901
5930
|
.argument("<row_id>", "CRM record row id.")
|
|
5902
5931
|
.option("--json", "Print a JSON envelope.")
|
|
5903
5932
|
.action(async (object, rowId, options) => {
|
|
5904
|
-
await handleAsyncAction("crm get", options, () => requestOxygen(
|
|
5933
|
+
await handleAsyncAction("crm get", options, () => requestOxygen(buildCrmRecordPath(object, rowId, options)));
|
|
5905
5934
|
}))
|
|
5906
5935
|
.addCommand(new Command("connections")
|
|
5907
5936
|
.description("Show the conversations and sequence enrollments connected to one CRM record, resolved through its people and identities \u2014 never by fuzzy name or domain.")
|
|
@@ -5926,7 +5955,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5926
5955
|
await handleAsyncAction("crm files list", options, () => requestOxygen(`/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(rowId)}/files`));
|
|
5927
5956
|
}))
|
|
5928
5957
|
.addCommand(new Command("attach")
|
|
5929
|
-
.description("Upload a local file and attach it to a CRM record.
|
|
5958
|
+
.description("Upload a local file and attach it to a CRM record. Each call creates a new file and attachment, including identical retries. If a response is lost, inspect the record's files before retrying. No credits.")
|
|
5930
5959
|
.argument("<object>", "CRM object slug, such as companies or people.")
|
|
5931
5960
|
.argument("<record>", "CRM record row id, or an identity value such as acme.com or sarah@acme.com.")
|
|
5932
5961
|
.argument("<path>", "Local file path.")
|
|
@@ -6161,6 +6190,14 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6161
6190
|
},
|
|
6162
6191
|
});
|
|
6163
6192
|
});
|
|
6193
|
+
}))
|
|
6194
|
+
.addCommand(new Command("keep")
|
|
6195
|
+
.description("Keep one sample record as real. A new workspace starts with sample companies, CEOs and deals: their reads carry `_sample: true` (`sample: true` on `crm pipeline` deals) and pipeline totals leave them out. Keep one only when it is real; it then counts like any record. Linked records stay as they are: a kept sample deal's company and contact stay samples until each is kept. Free and idempotent, no undo.")
|
|
6196
|
+
.argument("<object>", "CRM object slug: companies, people or deals.")
|
|
6197
|
+
.argument("<record>", "Row id (`crm pipeline` lists each deal's), or an identity value such as stripe.com.")
|
|
6198
|
+
.option("--json", "Print a JSON envelope.")
|
|
6199
|
+
.action(async (object, record, options) => {
|
|
6200
|
+
await handleAsyncAction("crm keep", options, () => requestOxygen(`/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(record)}/keep`, { method: "POST" }));
|
|
6164
6201
|
}))
|
|
6165
6202
|
.addCommand(new Command("relationships")
|
|
6166
6203
|
.description("Define CRM relationships and manage record relationship edges.")
|
|
@@ -6259,7 +6296,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6259
6296
|
await handleAsyncAction("crm activity timeline", options, () => requestOxygen(buildCrmTimelinePath(object, rowId, options)));
|
|
6260
6297
|
})))
|
|
6261
6298
|
.addCommand(new Command("describe")
|
|
6262
|
-
.description("Describe one configured CRM object: its fields, identities and relationships. Pass --field <key> to read just one field (its name, type, choices, flags) instead of the whole object. A deleted custom object answers
|
|
6299
|
+
.description("Describe one configured CRM object: its fields, identities and relationships. Pass --field <key> to read just one field (its name, type, choices, flags) instead of the whole object. Custom slugs accept hyphens. A deleted custom object answers crm_object_not_found with state archived; its next step, `crm objects delete <slug> --dry-run`, shows the purge time.")
|
|
6263
6300
|
.argument("<object>", "CRM object slug, such as companies or people.")
|
|
6264
6301
|
.option("--field <key>", "Narrow the answer to one field by its key, e.g. --field pipeline_stage. Also accepted as --attribute.")
|
|
6265
6302
|
.option("--attribute <key>", "Alias for --field.")
|
|
@@ -6293,7 +6330,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6293
6330
|
}));
|
|
6294
6331
|
}))
|
|
6295
6332
|
.addCommand(new Command("pipeline")
|
|
6296
|
-
.description("Show the deals pipeline grouped by stage, with per-stage counts and amount totals.")
|
|
6333
|
+
.description("Show the deals pipeline grouped by stage, with per-stage counts and amount totals. Seeded sample deals are listed with `sample: true` but never counted; `samples` reports them.")
|
|
6297
6334
|
.option("--pipeline <name>", "Filter to one pipeline, such as sales, expansion, or renewal.")
|
|
6298
6335
|
.option("--limit <limit>", "Maximum deals to return per stage.")
|
|
6299
6336
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -6371,7 +6408,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6371
6408
|
.requiredOption("--object <object>", "Provider object: hubspot contacts|companies, attio people|companies, or pipedrive persons|organizations|deals. Import Pipedrive persons and organizations before deals so each deal links to them.")
|
|
6372
6409
|
.option("--into <object>", "Oxygen CRM object slug to import into. Defaults to the natural mapping.")
|
|
6373
6410
|
.option("--limit <n>", "Max records to pull this run.")
|
|
6374
|
-
.option("--max-credits <n>", "OXYGEN
|
|
6411
|
+
.option("--max-credits <n>", "OXYGEN credit ceiling for the run. Review the current quote: own-API-key calls cost 0.05 credits each after repricing takes effect; OAuth follows its tool quote. Workflow steps count toward the ceiling after repricing takes effect. Use --limit for rows; provider billing and API quota are separate.")
|
|
6375
6412
|
.option("--approved", "Confirm a live import after inspecting a dry run.")
|
|
6376
6413
|
.option("--dry-run", "Preview the import without pulling. Default.")
|
|
6377
6414
|
.option("--live", "Run the live import. Requires --approved and --max-credits.")
|
|
@@ -6390,7 +6427,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6390
6427
|
.option("--cron <cron>", "Cron expression for the scheduled reconciliation cadence, e.g. '*/15 * * * *'.")
|
|
6391
6428
|
.option("--timezone <tz>", "IANA timezone the cron schedule runs in, e.g. America/New_York. Defaults to UTC.")
|
|
6392
6429
|
.option("--max-rows <n>", "Per-cycle row cap, separate from the --max-credits OXYGEN spend ceiling. OAuth provider calls follow their tool quote; own-API-key calls and live Workflow steps have separate charges.")
|
|
6393
|
-
.option("--max-credits <n>", "
|
|
6430
|
+
.option("--max-credits <n>", "OXYGEN credit ceiling per cycle. Provider/data-read fees count toward it; Workflow steps count after repricing takes effect. Provider billing and API quota are separate.")
|
|
6394
6431
|
.option("--field-mappings-json <json>", "HubSpot custom-property mappings as a JSON array of {oxygen_field, provider_field, direction}. Pass [] to clear; canonical identity/name mappings stay fixed.")
|
|
6395
6432
|
.option("--approved", "Confirm a live configuration after inspecting a dry run.")
|
|
6396
6433
|
.option("--dry-run", "Preview the configuration and next cycle without writing. Inbound/bidirectional previews perform bounded CRM reads and may use external API quota. Default.")
|
|
@@ -6409,8 +6446,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6409
6446
|
.option("--direction <direction>", "Override this cycle's direction: inbound, outbound, or bidirectional.")
|
|
6410
6447
|
.option("--since <iso>", "Read provider changes since this watermark.")
|
|
6411
6448
|
.option("--config-id <id>", "Use this standing CRM sync configuration for the cycle.")
|
|
6412
|
-
.option("--max-rows <n>", "Per-cycle row cap
|
|
6413
|
-
.option("--max-credits <n>", "
|
|
6449
|
+
.option("--max-rows <n>", "Per-cycle row cap, separate from the --max-credits OXYGEN spend ceiling. OAuth provider calls follow their tool quote; own-API-key calls and live Workflow steps have separate charges.")
|
|
6450
|
+
.option("--max-credits <n>", "OXYGEN credit ceiling per cycle. Provider/data-read fees count toward it; Workflow steps count after repricing takes effect. Provider billing and API quota are separate.")
|
|
6414
6451
|
.option("--approved", "Confirm a live cycle after inspecting a dry run.")
|
|
6415
6452
|
.option("--dry-run", "Preview the cycle without pulling or pushing. Default.")
|
|
6416
6453
|
.option("--live", "Run the live reconciliation cycle. Requires --approved and --max-credits.")
|
|
@@ -6429,7 +6466,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6429
6466
|
.option("--cron <cron>", "Cron expression for the cadence, e.g. '*/15 * * * *'. Defaults to every 15 minutes.")
|
|
6430
6467
|
.option("--timezone <tz>", "IANA timezone the cron runs in. Defaults to UTC.")
|
|
6431
6468
|
.requiredOption("--max-rows <n>", "Per-cycle row cap — the volume brake on provider writes (required).")
|
|
6432
|
-
.requiredOption("--max-credits <n>", "
|
|
6469
|
+
.requiredOption("--max-credits <n>", "OXYGEN credit ceiling per cycle. Provider/data-read fees count toward it; Workflow steps count after repricing takes effect. Provider billing and API quota are separate.")
|
|
6433
6470
|
.option("--field-mappings-json <json>", "HubSpot custom-property mappings as a JSON array of {oxygen_field, provider_field, direction}. Omit to preserve the configured mappings.")
|
|
6434
6471
|
.option("--approved", "Confirm enabling scheduled provider writes after inspecting a dry run.")
|
|
6435
6472
|
.option("--no-run-now", "Do not run an immediate first cycle after enabling.")
|
|
@@ -6466,7 +6503,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6466
6503
|
await handleAsyncAction("crm sync status", resolved, () => requestOxygen(buildCrmSyncConfigStatusPath(resolved)));
|
|
6467
6504
|
}))
|
|
6468
6505
|
.addCommand(new Command("links")
|
|
6469
|
-
.description("Show provider record links (HubSpot/Attio ids) mapped to one Oxygen CRM record.")
|
|
6506
|
+
.description("Show provider record links (HubSpot/Attio/Pipedrive ids) mapped to one Oxygen CRM record.")
|
|
6470
6507
|
.argument("<object>", "Oxygen CRM object slug, such as companies or people.")
|
|
6471
6508
|
.argument("<row_id>", "Oxygen CRM record row id.")
|
|
6472
6509
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -6705,6 +6742,61 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6705
6742
|
return requestOxygen(`/api/cli/tables/sources${qs}`);
|
|
6706
6743
|
});
|
|
6707
6744
|
}))
|
|
6745
|
+
.addCommand(new Command("records")
|
|
6746
|
+
.description("Copy a Records object or saved view into a working Table, once or kept in sync. One-way: Records stay unchanged, the Table is keyed by record_id, cells edited in the Table are kept, and records that leave the source stay as rows with in_records=false.")
|
|
6747
|
+
.addCommand(new Command("import")
|
|
6748
|
+
.description("Create a Table from a Records object or saved view. --dry-run counts the records and lists the columns without creating anything. --refresh sync also arms a schedule (a Workflow you can pause) that repeats the copy. The copy costs 0 credits and calls no provider; each scheduled sync bills one workflow step (credits_per_scheduled_sync).")
|
|
6749
|
+
.requiredOption("--object <slug>", "Records object slug, such as companies, people or deals (`crm objects`).")
|
|
6750
|
+
.option("--view <id|name>", "Saved view on that object, by id or exact name (`tables views list <object table>`). Omit for every record.")
|
|
6751
|
+
.option("--name <table>", "Name for the new Table. Defaults to the object and view name.")
|
|
6752
|
+
.option("--project <project>", "Project id or slug. Defaults to General.")
|
|
6753
|
+
.option("--refresh <once|sync>", "once (default) copies now; sync also keeps it in sync (needs --approved).")
|
|
6754
|
+
.option("--every <cadence>", "With --refresh sync: hourly, every_6h, daily@<hour> or weekly@<day><hour> (UTC). Default daily@6.")
|
|
6755
|
+
.option("--dry-run", "Preview without creating the Table.")
|
|
6756
|
+
.option("--approved", "Approve the standing per-sync charge that --refresh sync arms.")
|
|
6757
|
+
.option("--json", "Print a JSON envelope.")
|
|
6758
|
+
.action(async (options) => {
|
|
6759
|
+
await handleAsyncAction("tables records import", options, () => requestOxygen("/api/cli/tables/records-import", {
|
|
6760
|
+
method: "POST",
|
|
6761
|
+
body: {
|
|
6762
|
+
object: options.object,
|
|
6763
|
+
...(readOption(options.view) ? { view: readOption(options.view) } : {}),
|
|
6764
|
+
...(readOption(options.name) ? { name: readOption(options.name) } : {}),
|
|
6765
|
+
...(readOption(options.project) ? { project: readOption(options.project) } : {}),
|
|
6766
|
+
...(readOption(options.refresh) ? { refresh: readOption(options.refresh) } : {}),
|
|
6767
|
+
...(readOption(options.every) ? { every: readOption(options.every) } : {}),
|
|
6768
|
+
...(options.dryRun ? { dry_run: true } : {}),
|
|
6769
|
+
...(options.approved ? { approved: true } : {}),
|
|
6770
|
+
},
|
|
6771
|
+
}));
|
|
6772
|
+
}))
|
|
6773
|
+
.addCommand(new Command("sync")
|
|
6774
|
+
.description("Copy a Records-imported Table's source again now: new records are added, changed cells updated, edited cells kept, and departed records marked in_records=false (they flip back if they return). Without --refresh the schedule stays as it is; --refresh sync --approved arms or retimes it, --refresh once removes it. preserved_cells counts copied-field cells left as someone edited them in the Table; columns you added are never touched and not counted. Running it now costs 0 credits; each scheduled sync bills one workflow step (credits_per_scheduled_sync in an import dry run).")
|
|
6775
|
+
.argument("<table>", "Table id, slug or URL.")
|
|
6776
|
+
.option("--refresh <once|sync>", "Omit to keep the schedule; sync (with --approved) arms or retimes it; once removes it.")
|
|
6777
|
+
.option("--every <cadence>", "With --refresh sync: hourly, every_6h, daily@<hour> or weekly@<day><hour> (UTC). Default daily@6.")
|
|
6778
|
+
.option("--dry-run", "Count what would change without writing.")
|
|
6779
|
+
.option("--approved", "Approve the standing per-sync charge that --refresh sync arms.")
|
|
6780
|
+
.option("--json", "Print a JSON envelope.")
|
|
6781
|
+
.action(async (table, options) => {
|
|
6782
|
+
await handleAsyncAction("tables records sync", options, () => requestOxygen("/api/cli/tables/records-sync", {
|
|
6783
|
+
method: "POST",
|
|
6784
|
+
body: {
|
|
6785
|
+
table,
|
|
6786
|
+
...(readOption(options.refresh) ? { refresh: readOption(options.refresh) } : {}),
|
|
6787
|
+
...(readOption(options.every) ? { every: readOption(options.every) } : {}),
|
|
6788
|
+
...(options.dryRun ? { dry_run: true } : {}),
|
|
6789
|
+
...(options.approved ? { approved: true } : {}),
|
|
6790
|
+
},
|
|
6791
|
+
}));
|
|
6792
|
+
}))
|
|
6793
|
+
.addCommand(new Command("status")
|
|
6794
|
+
.description("Show a Records-imported Table's source object and view, the last sync's counts and its schedule. Read-only, 0 credits.")
|
|
6795
|
+
.argument("<table>", "Table id, slug or URL.")
|
|
6796
|
+
.option("--json", "Print a JSON envelope.")
|
|
6797
|
+
.action(async (table, options) => {
|
|
6798
|
+
await handleAsyncAction("tables records status", options, () => requestOxygen(`/api/cli/tables/records-sync?table=${encodeURIComponent(table)}`));
|
|
6799
|
+
})))
|
|
6708
6800
|
.addCommand(new Command("hubspot")
|
|
6709
6801
|
.description("Import HubSpot contacts or companies into a new table or Records, optionally from a saved list and with do-not-contact suppression.")
|
|
6710
6802
|
.addCommand(new Command("lists")
|
|
@@ -7004,7 +7096,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7004
7096
|
});
|
|
7005
7097
|
}))
|
|
7006
7098
|
.addCommand(new Command("delete-rows")
|
|
7007
|
-
.description("Delete
|
|
7099
|
+
.description("Delete exact workspace row IDs. For a matching filtered selection, use tables rows remove-selection. Campaign enrollments stay enrolled; the response names those campaigns.")
|
|
7100
|
+
.addHelpText("after", "\nFor a matching filtered selection, use tables rows remove-selection --help.")
|
|
7008
7101
|
.argument("<table>", "Table id or slug.")
|
|
7009
7102
|
.option("--row-ids-json <json>", "JSON array of workspace row UUIDs.")
|
|
7010
7103
|
.option("--row-ids-file <path>", "File containing row UUIDs or rows with a _row_id column.")
|
|
@@ -7147,7 +7240,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7147
7240
|
.option("--offset <n>", "Skip this many rows and start there — jump straight to a row without paging to it. Mutually exclusive with --cursor.")
|
|
7148
7241
|
.option("--fields <columns>", "Comma-separated column keys or ids to include.")
|
|
7149
7242
|
.option("--filter-json <json>", "Legacy filter object or array, e.g. '{\"column\":\"mobile_phone_e164\",\"op\":\"is_null\"}'. Mutually exclusive with --filter-tree-json/--sort-json.")
|
|
7150
|
-
.option("--filter-tree-json <json>",
|
|
7243
|
+
.option("--filter-tree-json <json>", `Airtable-style filter group, e.g. '{\"type\":\"group\",\"conjunction\":\"and\",\"children\":[{\"type\":\"leaf\",\"columnKey\":\"stage\",\"operator\":\"is\",\"value\":\"won\"}]}'. Add \"path\":\"a.b\" to a leaf to match a key inside a JSON cell, e.g. the rows one Function run touched: {\"columnKey\":\"<function_column>\",\"path\":\"action_run_id\",\"operator\":\"is\",\"value\":\"<run_id>\"}. Leaf operators: ${FILTER_TREE_OPERATORS.join(", ")} (is_true/is_false need a checkbox column; --filter-json's is_null/is_not_null are is_empty/is_not_empty here). Mutually exclusive with --filter-json.`)
|
|
7151
7244
|
.option("--search <text>", "Find rows by one piece of text, matched against every text, number, date and checkbox column at once — the grid's magnifier. Combines with --filter-tree-json (both must hold). Use --filter-tree-json when you know which column to match.")
|
|
7152
7245
|
.option("--formula-values <mode>", "Formula filters are refused by default because displayed formulas are evaluated live. Refresh the formula's stored cells with `columns run <table> <column> --force` (0 credits), then pass 'materialized'; filtering uses that stored snapshot and returns a freshness note.")
|
|
7153
7246
|
.option("--sort-json <json>", "Ordered sort rules, e.g. '[{\"columnKey\":\"_created_at\",\"direction\":\"desc\"}]'. Earlier rules dominate. Mutually exclusive with --filter-json.")
|
|
@@ -7431,13 +7524,13 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7431
7524
|
});
|
|
7432
7525
|
}
|
|
7433
7526
|
const followersCommand = tablesCommand.command("followers")
|
|
7434
|
-
.description("Order a LinkedIn company page's followers as a table of leads — a competitor's audience. `check` is free and quotes the
|
|
7527
|
+
.description("Order a LinkedIn company page's followers as a table of leads — a competitor's audience. `check` is free and quotes the order; fee-positive own-key previews leave the follower count unknown. `order` places it. The order is fulfilled by ScrapeLi, so the table is created immediately and its rows land later; there is no published delivery time.");
|
|
7435
7528
|
followersCommand.command("check")
|
|
7436
|
-
.description("Free
|
|
7529
|
+
.description("Free order preview. Managed quotes the follower cost; your own key quotes placement and later check fees, with a total ceiling you choose. No spend or table; `orderable: false` means the order is unavailable.")
|
|
7437
7530
|
.requiredOption("--company <url>", "LinkedIn company page URL, e.g. https://www.linkedin.com/company/hubspot.")
|
|
7438
7531
|
.option("--records <n>", "How many followers to price. Minimum 100; defaults to 1000.")
|
|
7439
7532
|
.option("--package <package>", "Basic (default): profile, job title, location, employer. Advanced (10x the credits): also a verified work email and region.")
|
|
7440
|
-
.option("--credential-mode <mode>", "managed (default): Oxygen credits. user_api_key: your
|
|
7533
|
+
.option("--credential-mode <mode>", "managed (default): Oxygen credits. user_api_key: your saved ScrapeLi key, billed by ScrapeLi, plus Oxygen placement/recovery platform fees.")
|
|
7441
7534
|
.option("--json", "Print a JSON envelope.")
|
|
7442
7535
|
.action(async (options) => {
|
|
7443
7536
|
await handleAsyncAction("tables followers check", options, () => requestOxygen("/api/cli/tables/linkedin-company-followers", {
|
|
@@ -7452,16 +7545,16 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7452
7545
|
}));
|
|
7453
7546
|
});
|
|
7454
7547
|
followersCommand.command("order")
|
|
7455
|
-
.description("Place the follower order after checking it. Requires --approved
|
|
7548
|
+
.description("Place the follower order after checking it. Requires --approved and --max-credits: managed uses the quoted order cost; own-key uses a TOTAL placement/recovery platform-fee ceiling. A ceiling equal to placement cannot recover rows. Budget exhaustion lands no partial rows; refetched pages consume the same ceiling. Creates the Table now; rows arrive later.")
|
|
7456
7549
|
.requiredOption("--approved", "Approve this purchase. Required, as on every command that buys data.")
|
|
7457
7550
|
.requiredOption("--company <url>", "LinkedIn company page URL whose followers to order.")
|
|
7458
|
-
.option("--max-credits <credits>", "
|
|
7459
|
-
.requiredOption("--request-id <uuid>", "One stable UUID
|
|
7551
|
+
.option("--max-credits <credits>", "Approved order ceiling. On your key, TOTAL platform fees for placement and later checks; exhaustion stops recovery. At least the quoted placement fee. ScrapeLi charges separately.")
|
|
7552
|
+
.requiredOption("--request-id <uuid>", "One stable UUID and identical order payload for retries. A new ID can buy the audience again.")
|
|
7460
7553
|
.option("--records <n>", "How many followers to buy. Minimum 100; defaults to 1000.")
|
|
7461
7554
|
.option("--package <package>", "Basic (default) or Advanced (10x the credits; adds a verified work email and region).")
|
|
7462
7555
|
.option("--name <name>", "Table name. Defaults to \"<Company> Followers\".")
|
|
7463
7556
|
.option("--project <project>", "Project id or slug; defaults to General.")
|
|
7464
|
-
.option("--credential-mode <mode>", "managed (default): Oxygen credits. user_api_key: your
|
|
7557
|
+
.option("--credential-mode <mode>", "managed (default): Oxygen credits. user_api_key: your saved ScrapeLi key, billed by ScrapeLi, plus Oxygen placement/recovery platform fees.")
|
|
7465
7558
|
.option("--json", "Print a JSON envelope.")
|
|
7466
7559
|
.action(async (options) => {
|
|
7467
7560
|
await handleAsyncAction("tables followers order", options, () => requestOxygen("/api/cli/tables/linkedin-company-followers", {
|
|
@@ -7480,6 +7573,23 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7480
7573
|
},
|
|
7481
7574
|
}));
|
|
7482
7575
|
});
|
|
7576
|
+
followersCommand.command("status")
|
|
7577
|
+
.description("Read a fee-budgeted own-key follower order's total fee ceiling, allocated attempts, recorded fees and recovery outcome by its original request ID, including interrupted setup. Optional --table-id verifies the saved Table binding. Recorded fees may settle later. Makes no provider call.")
|
|
7578
|
+
.requiredOption("--request-id <uuid>", "Original order request ID.")
|
|
7579
|
+
.option("--table-id <id>", "Optional saved Table ID; verifies the order belongs to that Table.")
|
|
7580
|
+
.option("--json", "Print a JSON envelope.")
|
|
7581
|
+
.action(async (options) => {
|
|
7582
|
+
await handleAsyncAction("tables followers status", options, () => {
|
|
7583
|
+
const requestId = readOption(options.requestId);
|
|
7584
|
+
if (!requestId)
|
|
7585
|
+
throw new OxygenError("invalid_input", "--request-id must be the original nonblank order request ID.", { exitCode: 2 });
|
|
7586
|
+
const query = new URLSearchParams({ request_id: requestId });
|
|
7587
|
+
const tableId = readOption(options.tableId);
|
|
7588
|
+
if (tableId)
|
|
7589
|
+
query.set("table_id", tableId);
|
|
7590
|
+
return requestOxygen(`/api/cli/tables/linkedin-company-followers?${query}`, { method: "GET" });
|
|
7591
|
+
});
|
|
7592
|
+
});
|
|
7483
7593
|
tablesCommand.addCommand(new Command("relate")
|
|
7484
7594
|
.description("Relate two tables: define empty Tables-owned relation columns on the source and target. Then use `oxygen tables link` to populate row-to-row edges. Works on any workspace table; plain tables stay plain and are never registered as CRM objects. Defaults to dry-run.")
|
|
7485
7595
|
.argument("<table>", "Source table id or slug.")
|
|
@@ -8329,7 +8439,7 @@ Examples:
|
|
|
8329
8439
|
.option("--body <text>", "Asset body text.")
|
|
8330
8440
|
.option("--data-json <json>", "Flexible JSON object for structured asset data.")
|
|
8331
8441
|
.option("--asset-json <json>", "Full asset JSON object. CLI flags override matching fields.")
|
|
8332
|
-
.option("--default", "
|
|
8442
|
+
.option("--default", "Ask to pin as the canonical default for its type (auto-applied to copy generation). For brand, voice, positioning and playbooks the asset saves unpinned and the pin waits for approval of a proposal (pin.ok false; oxygen knowledge proposals approve <id>); other types pin at once.")
|
|
8333
8443
|
.option("--json", "Print a JSON envelope.")
|
|
8334
8444
|
.action(async (options) => {
|
|
8335
8445
|
await handleAsyncAction("context asset upsert", options, () => requestOxygen("/api/cli/context/assets/upsert", {
|
|
@@ -8338,7 +8448,7 @@ Examples:
|
|
|
8338
8448
|
}));
|
|
8339
8449
|
}))
|
|
8340
8450
|
.addCommand(new Command("set-default")
|
|
8341
|
-
.description("Pin a context asset as the canonical default for its type. The canonical brand/voice/positioning docs are auto-applied to AI copy generation.")
|
|
8451
|
+
.description("Pin a context asset as the canonical default for its type. The canonical brand/voice/positioning docs are auto-applied to AI copy generation. For brand, voice, positioning and playbooks a pin or unpin files a proposal a human approves (oxygen knowledge proposals approve <id>) and fails with knowledge_approval_required; other types pin at once.")
|
|
8342
8452
|
.argument("<asset_id>", "Context asset UUID.")
|
|
8343
8453
|
.option("--off", "Unpin instead of pin (clears the canonical default for this type).")
|
|
8344
8454
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -8349,7 +8459,7 @@ Examples:
|
|
|
8349
8459
|
}));
|
|
8350
8460
|
}))
|
|
8351
8461
|
.addCommand(new Command("archive")
|
|
8352
|
-
.description("Archive a context asset.")
|
|
8462
|
+
.description("Archive a context asset. Archiving the canonical brand, voice, positioning or playbook files an approval proposal and fails with knowledge_approval_required.")
|
|
8353
8463
|
.argument("<asset_id>", "Context asset UUID.")
|
|
8354
8464
|
.option("--json", "Print a JSON envelope.")
|
|
8355
8465
|
.action(async (assetId, options) => {
|
|
@@ -8371,7 +8481,7 @@ Examples:
|
|
|
8371
8481
|
.option("--live", "Authorize this manual run to spend and write. Requires --max-credits. Without it the command previews and nothing is billed.")
|
|
8372
8482
|
.option("--approved", "Same as --live. Accepted because the global help footer names --approved as the way to authorize a credit-spending command.")
|
|
8373
8483
|
.option("--max-credits <credits>", `Hard credit ceiling for the whole pass. Required with --live, and clamped server-side to ${KNOWLEDGE_BOOTSTRAP_MAX_CREDITS}.`)
|
|
8374
|
-
.option("--force", "
|
|
8484
|
+
.option("--force", "Re-arm past an existing once-ever marker, the one deliberate way to research the same workspace twice. Without --live it only previews the re-arm for free: nothing is cleared or written. With --live it replaces the marker with a new pass and spends again.")
|
|
8375
8485
|
.option("--json", "Print a JSON envelope.")
|
|
8376
8486
|
.action(async (options) => {
|
|
8377
8487
|
await handleAsyncAction("knowledge bootstrap", options, () => requestOxygen("/api/cli/knowledge/bootstrap", {
|
|
@@ -8442,14 +8552,14 @@ Examples:
|
|
|
8442
8552
|
.addCommand(new Command("page")
|
|
8443
8553
|
.description("Knowledge wiki pages (slug-addressed, revision-guarded).")
|
|
8444
8554
|
.addCommand(new Command("upsert")
|
|
8445
|
-
.description("Create or update a knowledge wiki page.")
|
|
8446
|
-
.option("--slug <slug>", "Stable page
|
|
8447
|
-
.option("--folder <folder_id>", "Folder UUID, or root to
|
|
8555
|
+
.description("Create or update a knowledge wiki page. Free internal write; canonical edits require proposal approval.")
|
|
8556
|
+
.option("--slug <slug>", "Stable page key. Omit to derive from title; an existing slug updates unless --create-only is set.")
|
|
8557
|
+
.option("--folder <folder_id>", "Folder UUID, or root to clear placement; omit to preserve. Moving creates a revision and changes data.knowledge_folder_id.")
|
|
8448
8558
|
.option("--create-only", "Fail if the page already exists instead of updating it.")
|
|
8449
|
-
.option("--id <page_id>", "Existing page UUID to update.
|
|
8450
|
-
.option("--type <type>",
|
|
8559
|
+
.option("--id <page_id>", "Existing page UUID to update. For a new file, omit and use --create-only.")
|
|
8560
|
+
.option("--type <type>", `Page type: ${KNOWLEDGE_PAGE_TYPES.join(", ")}. voice, brand and positioning are canonical copy once pinned. Defaults to other on create.`)
|
|
8451
8561
|
.option("--title <title>", "Page title. Required on create.")
|
|
8452
|
-
.option("--status <status>", "draft, active, or archived.")
|
|
8562
|
+
.option("--status <status>", "draft, active, or archived. Restore with draft and --expected-revision; omit --body to retain content.")
|
|
8453
8563
|
.option("--tags <csv>", "Comma-separated page tags.")
|
|
8454
8564
|
.option("--summary <text>", "Short page summary.")
|
|
8455
8565
|
.option("--body <text>", "Page body (Markdown, supports [[wikilinks]]).")
|
|
@@ -8468,7 +8578,7 @@ Examples:
|
|
|
8468
8578
|
});
|
|
8469
8579
|
}))
|
|
8470
8580
|
.addCommand(addContractFlags(new Command("get")
|
|
8471
|
-
.description("Read one knowledge wiki page by slug or UUID. Read-only, 0 credits.")
|
|
8581
|
+
.description("Read one knowledge wiki page by slug or UUID. Use --view sources for its evidence ledger or --view full for content, links and sources. Read-only, 0 credits.")
|
|
8472
8582
|
.argument("<slug_or_id>", "Page slug or UUID.")
|
|
8473
8583
|
.option("--json", "Print a JSON envelope."), knowledgePageReadContract, { slug: { omit: true }, id: { omit: true } })
|
|
8474
8584
|
.action(async (slugOrId, options) => {
|
|
@@ -8488,14 +8598,22 @@ Examples:
|
|
|
8488
8598
|
}));
|
|
8489
8599
|
}))
|
|
8490
8600
|
.addCommand(new Command("archive")
|
|
8491
|
-
.description("Archive a knowledge wiki page.")
|
|
8601
|
+
.description("Archive a knowledge wiki page while retaining its content and history. Links become unresolved. Restore with page upsert --status draft and the current --expected-revision.")
|
|
8492
8602
|
.argument("<slug_or_id>", "Page slug or UUID.")
|
|
8603
|
+
.option("--expected-revision <n>", "Fail if the current page revision differs from the version you reviewed.")
|
|
8493
8604
|
.option("--json", "Print a JSON envelope.")
|
|
8494
8605
|
.action(async (slugOrId, options) => {
|
|
8495
8606
|
await handleAsyncAction("knowledge page archive", options, async () => {
|
|
8607
|
+
const expectedRevision = readPositiveInt(options.expectedRevision);
|
|
8608
|
+
if (options.expectedRevision !== undefined && (expectedRevision === undefined || !Number.isSafeInteger(expectedRevision))) {
|
|
8609
|
+
throw new OxygenError("invalid_request", "--expected-revision must be a positive integer.", { exitCode: 2 });
|
|
8610
|
+
}
|
|
8496
8611
|
const data = await requestOxygen("/api/cli/knowledge/pages/archive", {
|
|
8497
8612
|
method: "POST",
|
|
8498
|
-
body:
|
|
8613
|
+
body: {
|
|
8614
|
+
...knowledgePageRefBody(slugOrId),
|
|
8615
|
+
...(expectedRevision !== undefined ? { expected_revision: expectedRevision } : {}),
|
|
8616
|
+
},
|
|
8499
8617
|
});
|
|
8500
8618
|
await markKnowledgeMirrorStaleAfterWrite();
|
|
8501
8619
|
return data;
|
|
@@ -8565,7 +8683,7 @@ Examples:
|
|
|
8565
8683
|
.description("Search wiki pages by meaning and keywords. Read-only, 0 credits. Plain language uses hybrid retrieval when available; check match_type and semantic_status. Quotes, OR, and exclusions stay lexical. If the platform-funded embedding allowance is unavailable, keyword search still works.")
|
|
8566
8684
|
.argument("<query>", "Search text. Supports quoted phrases, OR, and -exclude.")
|
|
8567
8685
|
.option("--type <type>", "Filter by page type.")
|
|
8568
|
-
.option("--status <status>", "Filter by draft, active, or archived.")
|
|
8686
|
+
.option("--status <status>", "Filter by draft, active, or archived. Omitted, archived pages are excluded.")
|
|
8569
8687
|
.option("--tags <csv>", "Comma-separated tags that must be present.")
|
|
8570
8688
|
.option("--limit <n>", "Maximum pages to return.")
|
|
8571
8689
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -8737,13 +8855,14 @@ Examples:
|
|
|
8737
8855
|
}))
|
|
8738
8856
|
.addCommand(knowledgeBootstrapCommand)
|
|
8739
8857
|
.addCommand(new Command("synthesize")
|
|
8740
|
-
.description("Synthesize knowledge from workspace evidence (one low-cost AI call, priced as one low-reasoning AI column cell): campaign learnings from auto-filed outreach outcomes, or your writing voice (a voice guide) learned from your sent LinkedIn and WhatsApp messages. Every run spends credits, the preview included, unless fewer than 3 sent messages exist (insufficient_source_messages, nothing charged); without --approved nothing is saved. To keep the guide a preview showed, pass its body back with --approved --body (no new AI call); --approved alone synthesizes and charges again. A
|
|
8858
|
+
.description("Synthesize knowledge from workspace evidence (one low-cost AI call, priced as one low-reasoning AI column cell): campaign learnings from auto-filed outreach outcomes, or your writing voice (a voice guide) learned from your sent LinkedIn and WhatsApp messages. Every run spends credits, the preview included, unless fewer than 3 sent messages exist (insufficient_source_messages, nothing charged); without --approved nothing is saved. To keep the guide a preview showed, pass its body back with --approved --body (no new AI call); --approved alone synthesizes and charges again. A saved voice guide becomes the canonical voice once a human approves its pin proposal (oxygen knowledge proposals approve <id>); it then grounds message, reply, sequence and post drafts and AI columns.")
|
|
8741
8859
|
.option("--kind <kind>", "What to synthesize: campaign_learnings (default) or voice (your writing voice from sent LinkedIn and WhatsApp messages).", "campaign_learnings")
|
|
8742
8860
|
.option("--sequence <sequence_id>", "Scope campaign learnings to one sequence's learnings page. Omit for the org-wide [[outreach-learnings]] page.")
|
|
8743
|
-
.option("--approved", "Write the synthesized result (learnings page revision /
|
|
8744
|
-
.option("--body <markdown>", "With --approved: save this body verbatim (the guide a preview showed, or your edit) with no new AI call.")
|
|
8861
|
+
.option("--approved", "Write the synthesized result (learnings page revision / voice guide saved with a pin proposal) instead of previewing.")
|
|
8862
|
+
.option("--body <markdown>", "With --approved: save this body verbatim (the guide a preview showed, or your edit) with no new AI call, at 0 credits.")
|
|
8745
8863
|
.option("--title <title>", "Optional title for a newly created page/asset.")
|
|
8746
8864
|
.option("--limit <n>", "Voice only: how many recent sent messages to learn from (max 200).")
|
|
8865
|
+
.option("--max-credits <n>", "Hard cap OXYGEN enforces on this call: it is charged exactly its estimate, so never more than n. A higher estimate is refused before any AI call, with nothing charged, and the refusal reports the estimate (--max-credits 0 shows it for free). --approved --body makes no AI call and needs none.")
|
|
8747
8866
|
.option("--json", "Print a JSON envelope.")
|
|
8748
8867
|
.action(async (options) => {
|
|
8749
8868
|
await handleAsyncAction("knowledge synthesize", options, () => requestOxygen("/api/cli/knowledge/synthesize", {
|
|
@@ -8755,6 +8874,7 @@ Examples:
|
|
|
8755
8874
|
...(readOption(options.body) ? { body: readOption(options.body) } : {}),
|
|
8756
8875
|
...(readOption(options.title) ? { title: readOption(options.title) } : {}),
|
|
8757
8876
|
...(readPositiveInt(options.limit) !== undefined ? { limit: readPositiveInt(options.limit) } : {}),
|
|
8877
|
+
...(readNonNegativeNumber(options.maxCredits) !== undefined ? { max_credits: readNonNegativeNumber(options.maxCredits) } : {}),
|
|
8758
8878
|
},
|
|
8759
8879
|
}));
|
|
8760
8880
|
}))
|
|
@@ -9715,8 +9835,8 @@ Examples:
|
|
|
9715
9835
|
.option("--reasoning-level <level>", REASONING_LEVEL_OPTION_HELP)
|
|
9716
9836
|
.option("--run-condition <formula>", "Formula expression gating whether the AI column runs per row. A formula over bare column keys (eu_israel = true), not {{tokens}}.")
|
|
9717
9837
|
.option("--run-condition-columns <csv>", "Comma-separated column keys referenced by --run-condition.")
|
|
9718
|
-
.option("--no-structured-output", "Keep an AI column answering in plain text even when its prompt names output sections. Use this for copy — an email body is one answer, not a set of fields.")
|
|
9719
9838
|
.option("--structured-output", "Undo --no-structured-output: let the prompt's named sections shape the answer again.")
|
|
9839
|
+
.option("--no-structured-output", "Keep an AI column answering in plain text even when its prompt names output sections. Use this for copy — an email body is one answer, not a set of fields.")
|
|
9720
9840
|
.option("--output-schema-json <json>", "AI column output JSON schema as inline JSON.")
|
|
9721
9841
|
.option("--output-schema-file <path>", "AI column output JSON schema read from a file path.")
|
|
9722
9842
|
.addOption(new Option("--classify <labels>", "Classification column: label every row with one of these comma-separated labels (2-20), answering --prompt. Each cell holds the label, a 0-1 confidence and uncertain:true below --min-confidence (the best guess is kept). Flat ~0.075 credits/row; see `columns run --dry-run`."))
|
|
@@ -10161,8 +10281,10 @@ Examples:
|
|
|
10161
10281
|
.option("--reasoning-level <level>", REASONING_LEVEL_OPTION_HELP)
|
|
10162
10282
|
.option("--run-condition <formula>", "Formula expression gating whether the AI column runs per row. A formula over bare column keys (eu_israel = true), not {{tokens}}.")
|
|
10163
10283
|
.option("--run-condition-columns <csv>", "Comma-separated column keys referenced by --run-condition.")
|
|
10284
|
+
// Positive first keeps omission undefined; negative first defaults true
|
|
10285
|
+
// and silently overwrites a saved prose choice on an unrelated update.
|
|
10286
|
+
.option("--structured-output", "Restore the prompt's named JSON fields. Existing AI columns must store jsonb; for a text column, add a new AI column with --data-type jsonb.")
|
|
10164
10287
|
.option("--no-structured-output", "Keep an AI column answering in plain text even when its prompt names output sections. Use this for copy — an email body is one answer, not a set of fields.")
|
|
10165
|
-
.option("--structured-output", "Undo --no-structured-output: let the prompt's named sections shape the answer again.")
|
|
10166
10288
|
.option("--output-schema-json <json>", "AI column output JSON schema as inline JSON.")
|
|
10167
10289
|
.option("--output-schema-file <path>", "AI column output JSON schema read from a file path.")
|
|
10168
10290
|
.option("--research-query <template>", "Research columns: replace the per-row web search query template, e.g. \"{{company_name}} pricing page\".")
|
|
@@ -10189,7 +10311,8 @@ Examples:
|
|
|
10189
10311
|
readOption(options.reasoningLevel) ||
|
|
10190
10312
|
readOption(options.runCondition) ||
|
|
10191
10313
|
readOption(options.outputSchemaJson) ||
|
|
10192
|
-
readOption(options.outputSchemaFile)
|
|
10314
|
+
readOption(options.outputSchemaFile) ||
|
|
10315
|
+
typeof options.structuredOutput === "boolean";
|
|
10193
10316
|
let definition = options.definitionJson
|
|
10194
10317
|
? parseJsonObject(options.definitionJson)
|
|
10195
10318
|
: undefined;
|
|
@@ -10951,9 +11074,10 @@ Examples:
|
|
|
10951
11074
|
await handleAsyncAction("companies search filters", options, () => requestOxygen(`/api/cli/companies/search/preview${readCompanySourceQuery(options.source)}`));
|
|
10952
11075
|
}))
|
|
10953
11076
|
.addCommand(new Command("preview")
|
|
10954
|
-
.description("Preview up to 50 rows free, using native filters from companies search filters. --source google_maps previews 20 businesses with a q business-and-area filter; total_results is unavailable when the API publishes no total. --source hiring previews matching job postings with an exact total.")
|
|
11077
|
+
.description("Preview up to 50 rows free, using native filters from companies search filters. --source google_maps previews 20 businesses with a q business-and-area filter; total_results is unavailable when the API publishes no total. --source hiring previews matching job postings with an exact total. Totals, notes and the priced next step come first and the rows last; each row is compact (name, domain, LinkedIn, headcount, industry, location, a one-line description) unless --view full.")
|
|
10955
11078
|
.requiredOption("--filters-json <json-or-file>", 'Native filters as JSON or @file/path. Google Maps requires q, placeId or cid; other sources accept {} for all results.')
|
|
10956
11079
|
.option("--source <source>", "company_search (default), hiring or google_maps. Filters for hiring nest under job and company, e.g. {\"job\":{\"title\":{\"include\":[\"Account Executive\"]}}}.")
|
|
11080
|
+
.option("--view <view>", "compact (default) or full: full adds each row's provider record as raw_source, about 2.5 KB a company.")
|
|
10957
11081
|
.option("--json", "Print a JSON envelope.")
|
|
10958
11082
|
.action(async (options) => {
|
|
10959
11083
|
await handleAsyncAction("companies search preview", options, () => requestOxygen("/api/cli/companies/search/preview", {
|
|
@@ -10961,6 +11085,7 @@ Examples:
|
|
|
10961
11085
|
body: {
|
|
10962
11086
|
filters: parseJsonObject(readFileIfPresent(options.filtersJson)),
|
|
10963
11087
|
...(readCompanySearchSource(options.source) !== "company_search" ? { source: readCompanySearchSource(options.source) } : {}),
|
|
11088
|
+
...(options.view !== undefined ? { view: options.view } : {}),
|
|
10964
11089
|
},
|
|
10965
11090
|
}));
|
|
10966
11091
|
}))
|
|
@@ -11082,7 +11207,7 @@ Examples:
|
|
|
11082
11207
|
await handleAsyncAction("people search filters", options, () => requestOxygen("/api/cli/people/search/preview"));
|
|
11083
11208
|
}))
|
|
11084
11209
|
.addCommand(new Command("preview")
|
|
11085
|
-
.description("Preview up to 50 people free, using native filters from people search filters. For the companies in one of your tables, pass --from-table with --linkedin-url-column; for a few named companies, put their LinkedIn company URLs in company.linkedin_url. Then people search run --source people_search --source-filters-json <same filters> for a quoted, approved table fill; people search run --from-table instead runs one paid search per company in a table and covers each one.")
|
|
11210
|
+
.description("Preview up to 50 people free, using native filters from people search filters. For the companies in one of your tables, pass --from-table with --linkedin-url-column; for a few named companies, put their LinkedIn company URLs in company.linkedin_url. Totals, per-company counts, company_fit (what a person row cannot tell you about fit) and the priced next step come first, the rows last. Then people search run --source people_search --source-filters-json <same filters> for a quoted, approved table fill; people search run --from-table instead runs one paid search per company in a table and covers each one.")
|
|
11086
11211
|
.requiredOption("--filters-json <json-or-file>", 'Nested native filters as JSON or @file/path, e.g. {"people":{"job_title":{"include":["VP Sales"]}},"company":{"linkedin_url":["https://linkedin.com/company/acme"]}}; {} searches all.')
|
|
11087
11212
|
.option("--from-table <table>", "Free sample of Find decision makers at companies: search the first 50 companies in this table in one free page. Pair with --linkedin-url-column.")
|
|
11088
11213
|
.option("--linkedin-url-column <key>", "Column of --from-table that holds each company's LinkedIn URL.")
|
|
@@ -11269,49 +11394,32 @@ Examples:
|
|
|
11269
11394
|
}))
|
|
11270
11395
|
.addCommand(new Command("provider")
|
|
11271
11396
|
.description("Per-provider call-rate overrides and BYOK daily warning thresholds. With no subcommand, lists policies (same as `limits provider list`).")
|
|
11272
|
-
.addCommand(new Command("list")
|
|
11273
|
-
.description("List enforced provider overrides and the plan-tier BYOK daily threshold with its enforcement mode.")
|
|
11274
|
-
.option("--provider <provider>", "Filter saved overrides to one provider id; plan-tier defaults are always returned.")
|
|
11397
|
+
.addCommand(addProviderLimitFlags(new Command("list")
|
|
11398
|
+
.description("List enforced provider overrides and the plan-tier BYOK daily threshold with its enforcement mode."), "list")
|
|
11275
11399
|
.option("--json", "Print a JSON envelope.")
|
|
11276
11400
|
.action(async (options) => {
|
|
11277
|
-
const
|
|
11278
|
-
if (readOption(options.provider))
|
|
11279
|
-
query.set("provider", readOption(options.provider) ?? "");
|
|
11280
|
-
const suffix = query.toString();
|
|
11401
|
+
const suffix = providerLimitQuery(options, "list");
|
|
11281
11402
|
await handleAsyncAction("limits provider list", options, () => requestOxygen(`/api/cli/limits/provider${suffix ? `?${suffix}` : ""}`));
|
|
11282
11403
|
}), { isDefault: true })
|
|
11283
|
-
.addCommand(new Command("set")
|
|
11284
|
-
.description("Set an enforced
|
|
11285
|
-
.requiredOption("--provider <provider>", "Provider id (e.g. hubspot, leadmagic).")
|
|
11286
|
-
.requiredOption("--limit <n>", "Calls allowed per window.")
|
|
11287
|
-
.requiredOption("--window-seconds <n>", "Window size in seconds (86400 = one day).")
|
|
11288
|
-
.option("--operation <operation>", "Scope to one operation; omitted = provider-wide.")
|
|
11404
|
+
.addCommand(addProviderLimitFlags(new Command("set")
|
|
11405
|
+
.description("Set an enforced provider request limit; admin/owner only. Approve the exact provider, operation and window before running; execution saves the policy. Raising a cap can permit more use. 0 credits."), "set")
|
|
11289
11406
|
.option("--json", "Print a JSON envelope.")
|
|
11290
11407
|
.action(async (options) => {
|
|
11291
11408
|
await handleAsyncAction("limits provider set", options, () => requestOxygen("/api/cli/limits/provider", {
|
|
11292
11409
|
method: "POST",
|
|
11293
|
-
body:
|
|
11294
|
-
provider: options.provider,
|
|
11295
|
-
limit: Number(options.limit),
|
|
11296
|
-
window_seconds: Number(options.windowSeconds),
|
|
11297
|
-
...(options.operation ? { operation: options.operation } : {}),
|
|
11298
|
-
},
|
|
11410
|
+
body: providerLimitBody(options),
|
|
11299
11411
|
}));
|
|
11300
11412
|
}))
|
|
11301
|
-
.addCommand(new Command("delete")
|
|
11302
|
-
.description("Remove
|
|
11303
|
-
.requiredOption("--provider <provider>", "Provider id.")
|
|
11304
|
-
.option("--operation <operation>", "Operation scope; omitted targets the provider-wide row.")
|
|
11413
|
+
.addCommand(addProviderLimitFlags(new Command("delete")
|
|
11414
|
+
.description("Remove one provider override; admin/owner only. Approve its selector before running; execution deletes the policy. Other overrides remain; BYOK defaults may be observe-only. 0 credits."), "delete")
|
|
11305
11415
|
.option("--json", "Print a JSON envelope.")
|
|
11306
11416
|
.action(async (options) => {
|
|
11307
|
-
const query =
|
|
11308
|
-
if (options.operation)
|
|
11309
|
-
query.set("operation", options.operation);
|
|
11417
|
+
const query = providerLimitQuery(options, "delete");
|
|
11310
11418
|
await handleAsyncAction("limits provider delete", options, () => requestOxygen(`/api/cli/limits/provider?${query.toString()}`, { method: "DELETE" }));
|
|
11311
11419
|
})));
|
|
11312
11420
|
program
|
|
11313
11421
|
.command("billing")
|
|
11314
|
-
.description("Plan and managed credit commands. Spend splits into FLEXIBLE (ad-hoc: enrichment, AI, automation — drawn from your free-to-spend balance) and FIXED recurring per-resource monthly commitments blocked out of it; see `billing commitments`. THREE CLOCKS, deliberately different: the CREDIT CYCLE that `billing allowance` reports against (your plan's monthly grant window); each resource's own COMMITMENT RENEWAL, anchored to the day you connected it, so `billing commitments --json` next_due_at rarely matches the cycle end; and your SUBSCRIPTION PERIOD in `billing balance` (annual plans span many credit cycles). A number from one clock will not reconcile against another. On a plan that pays for several workspaces, `billing usage --group-by workspace` totals what each one spent. To price a month of outreach before building it (leads, enrichment, sequence steps, senders), run `billing estimate`. Failed-payment grace, suspension, and recovery: https://oxygen-agent.com/docs/safety/billing.")
|
|
11422
|
+
.description("Plan and managed credit commands. Credits reserved but not yet charged are listed hold by hold with `billing usage --meter holds`. Spend splits into FLEXIBLE (ad-hoc: enrichment, AI, automation — drawn from your free-to-spend balance) and FIXED recurring per-resource monthly commitments blocked out of it; see `billing commitments`. THREE CLOCKS, deliberately different: the CREDIT CYCLE that `billing allowance` reports against (your plan's monthly grant window); each resource's own COMMITMENT RENEWAL, anchored to the day you connected it, so `billing commitments --json` next_due_at rarely matches the cycle end; and your SUBSCRIPTION PERIOD in `billing balance` (annual plans span many credit cycles). A number from one clock will not reconcile against another. On a plan that pays for several workspaces, `billing usage --group-by workspace` totals what each one spent. To price a month of outreach before building it (leads, enrichment, sequence steps, senders), run `billing estimate`. Failed-payment grace, suspension, and recovery: https://oxygen-agent.com/docs/safety/billing.")
|
|
11315
11423
|
.addCommand(new Command("change")
|
|
11316
11424
|
.description("Create an approval link (`approval_url`) to start or change a plan; nothing changes until an organization admin confirms. Compare all six prices, credits and yearly terms with `billing balance` first. Use --interval month --term year for twelve discounted monthly payments, renewing for another year: the $99 plan costs $82.50 per month before tax, not $990 upfront. Credits arrive monthly after successful payment. On a workspace that has never had a plan, the Checkout for any monthly plan from $99 up carries the first-month offer, half its first month, while its 24-hour window is open (`offer`; `billing balance` lists every size under `first_month_offer.plans`): `amount_due_now` is then the discounted first month and `next_invoice_total` the regular price, both before tax. Creating that Checkout starts the window if nothing has started it yet. Upgrades during an annual commitment apply immediately with proration and keep its end date; downgrades and switching to a monthly commitment wait for the term end. Switching from a monthly commitment to yearly starts at the next monthly boundary. Existing prepaid yearly plans keep their cadence unless a new term is selected; transitions wait until their paid period ends. Omit interval and term to preserve the current contract. For an existing subscription, a yearly preview returns charges, start and term-end dates; nothing changes until separate confirmation.")
|
|
11317
11425
|
.requiredOption("--to <plan>",
|
|
@@ -11345,16 +11453,17 @@ Examples:
|
|
|
11345
11453
|
}));
|
|
11346
11454
|
}))
|
|
11347
11455
|
.addCommand(new Command("balance")
|
|
11348
|
-
.description("Compare all six subscription sizes in `available_plans`: monthly prices, yearly paid-monthly prices, credit allowances and savings. `annual_monthly_billing_available` says whether yearly paid monthly is offered; `annual_monthly_terms` explains the 12-month commitment, annual renewal, remaining payments after cancellation and plan-change timing. `first_month_offer` is the first-month offer this workspace can still claim: every size it applies to (`plans`, from `minimum_plan` up) with its first-month and regular price before tax, and `ends_at`, null until its 24-hour window starts. The window starts the first time the workspace meets the offer: the app showing it, signup from a browser that saw it on the website, or creating its Checkout with `billing change`. Also show the current plan, commitment, subscription entitlement, and managed credit balance: available and reserved credits, FIXED credits committed to recurring per-resource charges, and the FLEXIBLE free-to-spend remainder. `next_renewal` answers \"will I be charged?\": read `will_charge` first — true only when this workspace's own Stripe plan is going to charge its card, in which case `charge_at` and the subscription's recurring amount in money say when and roughly how much (the exact amount, with any promotion code or tax, is on the invoice); `period_ends_at` is the trial or period end either way, `cancellation_scheduled` says whether a cancellation is already set, and `stop_command` names the one command that changes it (`billing cancel`, undone by `billing resume`) or is null when nothing here can stop it (staff-invoiced, billed through another workspace, not active). `billing_interval` is payment cadence; `commitment` distinguishes yearly paid monthly from monthly and legacy prepaid yearly plans. Credits are granted monthly; `next_credit_grant_at` is the next grant. For a yearly commitment, cancellation stops annual renewal but remaining monthly payments continue until commitment.ends_at. `renewals_past_due` lists infrastructure renewals (managed-inbox domains, warm-ups) OXYGEN could NOT charge this month, what they cost, and the exact top-up that clears them — nothing is cancelled and billing retries automatically after a top-up. `warnings` also flags a recurring_shortfall when your connected infrastructure costs more per month than the plan grants. `partner_bonus` says whether this workspace earns the agency partner bonus (+15% credits a month for a published directory partner on $99 or above), how many credits, and why not when it does not; the bonus arrives as its own ledger line (`billing usage --category partner_bonus`), never inside plan.monthly_credits. A workspace linked to another organization (`orgs billing-link`) reads its billing at that owner: the balance is the owner's pool, `shared_billing` is true and `billing_owner_organization_id` names the owner. After failed-payment grace expires, mutations stop while read/export and billing recovery remain available. Recovery: https://oxygen-agent.com/billing. Policy: https://oxygen-agent.com/docs/safety/billing.")
|
|
11456
|
+
.description("Compare all six subscription sizes in `available_plans`: monthly prices, yearly paid-monthly prices, credit allowances and savings. `annual_monthly_billing_available` says whether yearly paid monthly is offered; `annual_monthly_terms` explains the 12-month commitment, annual renewal, remaining payments after cancellation and plan-change timing. `first_month_offer` is the first-month offer this workspace can still claim: every size it applies to (`plans`, from `minimum_plan` up) with its first-month and regular price before tax, and `ends_at`, null until its 24-hour window starts. The window starts the first time the workspace meets the offer: the app showing it, signup from a browser that saw it on the website, or creating its Checkout with `billing change`. Also show the current plan, commitment, subscription entitlement, and managed credit balance: available and reserved credits, FIXED credits committed to recurring per-resource charges, and the FLEXIBLE free-to-spend remainder. `next_renewal` answers \"will I be charged?\": read `will_charge` first — true only when this workspace's own Stripe plan is going to charge its card, in which case `charge_at` and the subscription's recurring amount in money say when and roughly how much (the exact amount, with any promotion code or tax, is on the invoice); `period_ends_at` is the trial or period end either way, `cancellation_scheduled` says whether a cancellation is already set, and `stop_command` names the one command that changes it (`billing cancel`, undone by `billing resume`) or is null when nothing here can stop it (staff-invoiced, billed through another workspace, not active). `billing_interval` is payment cadence; `commitment` distinguishes yearly paid monthly from monthly and legacy prepaid yearly plans. Credits are granted monthly; `next_credit_grant_at` is the next grant. For a yearly commitment, cancellation stops annual renewal but remaining monthly payments continue until commitment.ends_at. `renewals_past_due` lists infrastructure renewals (managed-inbox domains, warm-ups) OXYGEN could NOT charge this month, what they cost, and the exact top-up that clears them — nothing is cancelled and billing retries automatically after a top-up. `warnings` also flags a recurring_shortfall when your connected infrastructure costs more per month than the plan grants. `partner_bonus` says whether this workspace earns the agency partner bonus (+15% credits a month for a published directory partner on $99 or above), how many credits, and why not when it does not; the bonus arrives as its own ledger line (`billing usage --category partner_bonus`), never inside plan.monthly_credits. A workspace linked to another organization (`orgs billing-link`) reads its billing at that owner: the balance is the owner's pool, `shared_billing` is true and `billing_owner_organization_id` names the owner. After failed-payment grace expires, mutations stop while read/export and billing recovery remain available. Recovery: https://oxygen-agent.com/billing. Policy: https://oxygen-agent.com/docs/safety/billing. 0 credits; reserves nothing. Due plan grants or credit expiry may apply.")
|
|
11349
11457
|
.option("--json", "Print a JSON envelope.")
|
|
11350
11458
|
.action(async (options) => {
|
|
11351
11459
|
await handleAsyncAction("billing balance", options, () => requestOxygen("/api/cli/billing/balance"));
|
|
11352
11460
|
}))
|
|
11353
11461
|
.addCommand(new Command("commitments")
|
|
11354
|
-
.description("List
|
|
11462
|
+
.description("List recurring commitments, or inspect monthly connection prices and first-debit timing with --view connection-prices without creating authorization. Default: list the fixed monthly credit commitments — per connected sending mailbox, OXYGEN-sold mailbox, warm-up, deliverability, connected LinkedIn, WhatsApp and X account, and rented phone number — with unit price, quantity, and next-due date. When your balance cannot cover an account's renewal, sending from that account pauses at once and it disconnects 7 days later (a phone number is released after 30): paused_quantity, past_due_since, disconnects_at and topup_url say which kind, since when and by when to top up; a top-up resumes it within minutes. This is the FORWARD run-rate: what your currently-connected resources will cost at their next renewal. It is NOT this cycle's charges — compare `billing allowance` fixed_spent_credits for that, and expect the two to differ. Each resource renews on its OWN anchor (the day it was connected), so next_due_at is per-resource and rarely lines up with the credit cycle. The top-level count is the number of individual resource rows; the commitments list groups them by kind. A past next_due_at is an unpaid renewal still due. To stop all infrastructure renewals while retaining records, preview `workspace infrastructure-exit`; plan cancellation alone does not stop these resources. Read-only, 0 Oxygen credits.")
|
|
11463
|
+
.addOption(new Option("--view <view>", "Inspect connection-prices before authorizing a new account, or current commitments (default).").choices(["commitments", "connection-prices"]))
|
|
11355
11464
|
.option("--json", "Print a JSON envelope.")
|
|
11356
11465
|
.action(async (options) => {
|
|
11357
|
-
await handleAsyncAction("billing commitments", options, () => requestOxygen(
|
|
11466
|
+
await handleAsyncAction("billing commitments", options, () => requestOxygen(`/api/cli/billing/commitments${options.view ? `?view=${encodeURIComponent(options.view)}` : ""}`));
|
|
11358
11467
|
}))
|
|
11359
11468
|
.addCommand(new Command("estimate")
|
|
11360
11469
|
.description("Estimate what a month of outreach costs before you build it: leads × enrichment × sequence steps × senders. Returns one line per cost (enrichment credits expected and worst case, each mailbox and LinkedIn account at its monthly price, sends at 0 credits), `totals`, `plan_fit` (does your plan cover it, and the top-up if not), `recommended_plan` (the cheapest plan plus top-ups that covers it) and `limits` (emails per mailbox, LinkedIn invites and messages per account; `limits_hit` names each one the scenario exceeds and `senders_needed` how many senders it takes). Steps are counted per channel: a 3-step sequence of two emails and one LinkedIn invite is `--email-steps 2 --linkedin-steps 1`, and `summary` restates what was priced. `monthly_usd` is all in: plan, top-ups and dollar seats. Prices are the ones in force; `sender_billing` says whether senders are paid as dollar seats or monthly credit reservations. Example: `oxygen billing estimate --leads 500 --enrich work_email --email-steps 2 --linkedin-steps 1 --mailboxes 1 --linkedin-accounts 1 --json`. `web_url` opens the same estimate in Settings → Plan limits. Read-only, 0 Oxygen credits.")
|
|
@@ -11383,13 +11492,13 @@ Examples:
|
|
|
11383
11492
|
}));
|
|
11384
11493
|
}))
|
|
11385
11494
|
.addCommand(new Command("seats")
|
|
11386
|
-
.description("Show sending
|
|
11495
|
+
.description("Show sending seats, current billing-period end and next-connection eligibility (null: unknown, with a reason). Includes purchased, grandfathered (null: unlimited), connected and left per channel. Seats belong to the billing owner. Email mailbox allocation is workspace-scoped; unreadable mailbox data leaves allocated/available/can_connect null. When retired, held_seats_end_at_reason and held_seats_end_at_message distinguish no paying subscription from an unknown end date; after_seats_end gives monthly credit prices (null: unconfirmed). connect_eligibility explains a single new connection's credit affordability, sponsorship and attachment capacity; paying-seat coverage may remain unknown. Existing accounts and reconnects differ. Charges 0 credits and creates no reservation; like billing balance, it may apply a due plan grant or already-due sponsored-credit expiry.")
|
|
11387
11496
|
.option("--json", "Print a JSON envelope.")
|
|
11388
11497
|
.action(async (options) => {
|
|
11389
11498
|
await handleAsyncAction("billing seats", options, () => requestOxygen("/api/cli/billing/seats"));
|
|
11390
11499
|
})
|
|
11391
11500
|
.addCommand(new Command("set")
|
|
11392
|
-
.description("
|
|
11501
|
+
.description("Manage legacy dollar sending seats for one channel; check `billing seats` for retirement status. The quantity is ABSOLUTE, not a delta: `--quantity 3` means you end up with three, so a retried command cannot buy extra. Increasing charges the card pro rata immediately; decreasing credits you pro rata. Reducing below the number of senders you have connected is REFUSED and names how many to disconnect first, because an orphaned sender is one you keep paying a provider for while Oxygen has stopped honouring it. An email_sender reduction is refused with `seat_allocation_unavailable` (503) while the workspace's mailbox count cannot be read; retry it in a moment. Once seats are retired, any increase is refused with `seats_retired` and lowering held seats still works. Charges real money.")
|
|
11393
11502
|
.requiredOption("--seat-key <kind>", "linkedin, whatsapp, phone_number, or email_sender.")
|
|
11394
11503
|
.requiredOption("--quantity <n>", "Total seats to hold for this channel, 0 or more.")
|
|
11395
11504
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -11409,14 +11518,14 @@ Examples:
|
|
|
11409
11518
|
await handleAsyncAction("billing invoices", options, () => requestOxygen("/api/cli/billing/invoices"));
|
|
11410
11519
|
}))
|
|
11411
11520
|
.addCommand(new Command("allowance")
|
|
11412
|
-
.description("Show this billing cycle's credit pool as one breakdown: fixed spend, flexible spend, credits committed to the next renewal, and what is free to spend — the numbers behind the bar on the credit-usage page. The parts always sum to the total, with ONE deliberate exception: `renewals_past_due` (and its region=past_due segment) reports renewals that FAILED to charge, which is money not taken and therefore outside total_credits. fixed_spent_credits is what has ALREADY been charged this cycle, NOT your monthly run-rate — for the forward figure billed at the next renewal see `billing commitments`, which is normally much larger. For organization wallet scope, reserved_credits is in-flight spend already removed from the available balance: do not subtract it again from free_to_spend_credits. Reserved credits can exceed the remaining free balance without implying an overdraft. For workspace_cap scope, free_to_spend_credits is remaining allowance, not the billing owner's wallet; live execution still checks that wallet and spend policies. Every segment carries region=fixed|flexible.
|
|
11521
|
+
.description("Show this billing cycle's credit pool as one breakdown: fixed spend, flexible spend, credits committed to the next renewal, and what is free to spend — the numbers behind the bar on the credit-usage page. The parts always sum to the total, with ONE deliberate exception: `renewals_past_due` (and its region=past_due segment) reports renewals that FAILED to charge, which is money not taken and therefore outside total_credits. fixed_spent_credits is what has ALREADY been charged this cycle, NOT your monthly run-rate — for the forward figure billed at the next renewal see `billing commitments`, which is normally much larger. For organization wallet scope, reserved_credits is in-flight spend already removed from the available balance: do not subtract it again from free_to_spend_credits. Reserved credits can exceed the remaining free balance without implying an overdraft. For workspace_cap scope, free_to_spend_credits is remaining allowance, not the billing owner's wallet; live execution still checks that wallet and spend policies. Every segment carries region=fixed|flexible. 0 credits; reserves nothing. Due plan grants or credit expiry may apply.")
|
|
11413
11522
|
.option("--json", "Print a JSON envelope.")
|
|
11414
11523
|
.action(async (options) => {
|
|
11415
11524
|
await handleAsyncAction("billing allowance", options, () => requestOxygen("/api/cli/billing/allowance"));
|
|
11416
11525
|
}))
|
|
11417
11526
|
.addCommand(new Command("usage")
|
|
11418
|
-
.description("Show credit ledger events (each tagged with its FIXED/FLEXIBLE spend_class), workflow step usage (automation_actions: credits per step, credits billed; writing rows is free), calls with your own API keys (byok_calls: the platform fee per call and the fee credits this month; your provider bills the call itself), or connected LinkedIn and WhatsApp accounts (provider_seats: the rolling-30d peak and its implied
|
|
11419
|
-
.option("--meter <meter>", "credits, automation_actions, byok_calls,
|
|
11527
|
+
.description("Show credit ledger events (each tagged with its FIXED/FLEXIBLE spend_class), workflow step usage (automation_actions: credits per step, credits billed; writing rows is free), calls with your own API keys (byok_calls: the platform fee per call and the fee credits this month; your provider bills the call itself), or connected LinkedIn and WhatsApp accounts (provider_seats: the rolling-30d peak and its implied connectivity cost; what each account is charged is its credit reservation in `billing commitments`). On a plan that pays for several workspaces (an agency covering client workspaces), `--group-by workspace` totals the credits each workspace spent (`by_workspace`) and `--workspace <id|slug>` narrows the ledger to one of them; a linked client workspace sees only its own spend. managed_credits_used counts captured credits; for where credits went by category, provider or tool, with shares, run `billing audit`.")
|
|
11528
|
+
.option("--meter <meter>", "credits, automation_actions, byok_calls, provider_seats (rolling-30d peak connected accounts and the flat per-account charge; each account's reservation is listed by `billing commitments`), or holds (each held reservation behind reserved_credits in this workspace: credits, run, why it is held (an unmarked LinkedIn lookup hold cannot say whether its result was unknown or delivered), and automatic_return: only a LinkedIn lookup hold from 24 hours to 30 days old is eligible, only while that is switched on, and an hourly release returns it on a later pass with no fixed deadline; read-only, newest first, --days up to 365 and --limit apply). Defaults to credits.")
|
|
11420
11529
|
.option("--days <n>", "Lookback window in days. Defaults to 30.")
|
|
11421
11530
|
.option("--from <iso>", "Only include ledger events at or after this ISO timestamp.")
|
|
11422
11531
|
.option("--to <iso>", "Only include ledger events at or before this ISO timestamp.")
|
|
@@ -11444,7 +11553,7 @@ Examples:
|
|
|
11444
11553
|
});
|
|
11445
11554
|
}))
|
|
11446
11555
|
.addCommand(new Command("audit")
|
|
11447
|
-
.description("Summarize where managed credits were granted, reserved, captured, released, or spent.
|
|
11556
|
+
.description("Summarize where managed credits were granted, reserved, captured, released, or spent. Every result lists spend_by_category (each spend area's credits and share of the window's spend, largest first) and totals fixed_credits_spent/flexible_credits_spent over the whole window, whatever --group-by is. groups_truncated says when --limit cut grouped rows, and managed_credits_spent_outside_groups how much they carried. Spent is reserved minus released in the window, so it differs from totals.managed_credits_captured (`billing usage`'s managed_credits_used) by holds still open (reserved_credits) and holds settling across the window edge. byok_usage_events counts only nonzero rows unless --type byok. 0 credits; reserves nothing. Due plan grants or credit expiry may apply.")
|
|
11448
11557
|
.option("--days <n>", "Lookback window in days. Defaults to 365.")
|
|
11449
11558
|
.option("--from <iso>", "Only include ledger events at or after this ISO timestamp.")
|
|
11450
11559
|
.option("--to <iso>", "Only include ledger events at or before this ISO timestamp.")
|
|
@@ -11816,8 +11925,8 @@ Examples:
|
|
|
11816
11925
|
.command("admin")
|
|
11817
11926
|
.description("Staff-only commands.")
|
|
11818
11927
|
.addCommand(new Command("kpis")
|
|
11819
|
-
.description("Show company
|
|
11820
|
-
.addOption(new Option("--range <range>", "Range for acquisition,
|
|
11928
|
+
.description("Show the company conversion funnel (visitors → signups → agent connected → CLI/MCP activation → paid, with step rates), self-reported signup-channel attribution, new and positive-paid MRR/ARR, and paid churn separated from trial loss. OXYGEN staff only; workspace admin role alone does not grant access.")
|
|
11929
|
+
.addOption(new Option("--range <range>", "Range for acquisition, the funnel's signup cohort, new revenue, and churn; current MRR/ARR and scheduled cancellations remain point-in-time.")
|
|
11821
11930
|
.choices(["7d", "30d", "90d"])
|
|
11822
11931
|
.default("30d"))
|
|
11823
11932
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -11853,6 +11962,49 @@ Examples:
|
|
|
11853
11962
|
return requestOxygen(`/api/cli/admin/costs${suffix}`);
|
|
11854
11963
|
});
|
|
11855
11964
|
}))
|
|
11965
|
+
.addCommand(new Command("price-book")
|
|
11966
|
+
.description("The runtime prices charging reads from the control DB: recurring charges, plan allowances and scalars, each with its committed default and whether staff may edit it. An edit reaches every instance within 60 seconds. Staff only.")
|
|
11967
|
+
.addCommand(new Command("list")
|
|
11968
|
+
.description("List every pricing-book row with its stored value, what charging bills now, the committed default, and who reads it. Read-only.")
|
|
11969
|
+
.option("--json", "Print a JSON envelope.")
|
|
11970
|
+
.action(async (options) => {
|
|
11971
|
+
await handleAsyncAction("admin price-book list", options, () => requestOxygen("/api/cli/admin/costs?section=pricing_book"));
|
|
11972
|
+
}))
|
|
11973
|
+
.addCommand(new Command("set")
|
|
11974
|
+
.description("Set one editable pricing-book field. A charge takes --field credits or active; a plan takes monthly_credits, weekly_credits_limit or rollover_cap (value none for no limit); a scalar takes value. Rows charging does not read from the book are refused.")
|
|
11975
|
+
.requiredOption("--table <table>", "charge, plan, or scalar.")
|
|
11976
|
+
.requiredOption("--key <key>", "The row key, e.g. addon.egress_dedicated_ip, oxygen_499, markup.copilot_inference.")
|
|
11977
|
+
.requiredOption("--field <field>", "credits | active (charge); monthly_credits | weekly_credits_limit | rollover_cap (plan); value (scalar).")
|
|
11978
|
+
.requiredOption("--value <value>", "A non-negative number, true/false for active, or none for no plan limit.")
|
|
11979
|
+
.option("--json", "Print a JSON envelope.")
|
|
11980
|
+
.action(async (options) => {
|
|
11981
|
+
await handleAsyncAction("admin price-book set", options, () => requestOxygen("/api/cli/admin/costs", {
|
|
11982
|
+
method: "POST",
|
|
11983
|
+
body: {
|
|
11984
|
+
price_table: options.table,
|
|
11985
|
+
price_key: options.key,
|
|
11986
|
+
price_field: options.field,
|
|
11987
|
+
value: readPriceBookValue(options.value),
|
|
11988
|
+
},
|
|
11989
|
+
}));
|
|
11990
|
+
}))
|
|
11991
|
+
.addCommand(new Command("restore")
|
|
11992
|
+
.description("Put a pricing-book row back to its committed default. For a plan, --field restores one allowance field; without it all three are restored.")
|
|
11993
|
+
.requiredOption("--table <table>", "charge, plan, or scalar.")
|
|
11994
|
+
.requiredOption("--key <key>", "The row key.")
|
|
11995
|
+
.option("--field <field>", "Plan only: monthly_credits, weekly_credits_limit or rollover_cap.")
|
|
11996
|
+
.option("--json", "Print a JSON envelope.")
|
|
11997
|
+
.action(async (options) => {
|
|
11998
|
+
await handleAsyncAction("admin price-book restore", options, () => requestOxygen("/api/cli/admin/costs", {
|
|
11999
|
+
method: "POST",
|
|
12000
|
+
body: {
|
|
12001
|
+
price_table: options.table,
|
|
12002
|
+
price_key: options.key,
|
|
12003
|
+
...(options.field ? { price_field: options.field } : {}),
|
|
12004
|
+
restore: true,
|
|
12005
|
+
},
|
|
12006
|
+
}));
|
|
12007
|
+
})))
|
|
11856
12008
|
.addCommand(new Command("primary-providers")
|
|
11857
12009
|
.description("Board of the PRIMARY managed external data providers (enrichment, people/company search, web search, scraping, signals, AI-column web grounding, LLM inference): per provider health probe, 7d traffic, balance, 30d/7d COGS, rate policy, spend ceilings, breaker, and posture, plus the platform runaway guards and how old each snapshot is. Reads the snapshots the crons write; --refresh re-runs those same zero-credit snapshots first. Staff only.")
|
|
11858
12010
|
.option("--refresh", "Re-run the zero-credit snapshot work the three crons run (provider health probes, managed balances, cost snapshot) before reading the board. No paid provider call and no credit spend; takes up to a few minutes.")
|
|
@@ -12478,7 +12630,7 @@ Examples:
|
|
|
12478
12630
|
.description("Start an attended Workspace Copilot session on Oxygen Auto — Oxygen picks the model, keeps one conversation on one model, and runs its own errands on a cheaper one. Starting is free; the next send starts inference. No inference budget is required; inference bills at 5x actual model cost under a 3,000-credit internal platform safety backstop. Legacy budget and tier flags remain accepted for older clients but do not authorize or cap spend. Paid or external actions require separate approval.")
|
|
12479
12631
|
.option("--budget-credits <number>", "Legacy compatibility only: accepted and clamped, but does not authorize or cap attended Copilot inference.")
|
|
12480
12632
|
.option("--tier <auto|low|medium|high>", "Legacy. Sessions run on Oxygen Auto; Oxygen picks the model. The three older tiers are still accepted so existing scripts keep working.")
|
|
12481
|
-
.option("--model <id>", "Pin a specific priced model id instead of letting Oxygen choose.")
|
|
12633
|
+
.option("--model <id>", "Pin a specific priced model id instead of letting Oxygen choose. An id Oxygen does not price is ignored and the session runs on Oxygen Auto.")
|
|
12482
12634
|
.option("--per-turn-ceiling <number>", "Legacy compatibility only: accepted and clamped, but does not cap an attended turn.")
|
|
12483
12635
|
.option("--journey <slug>", "Optional journey slug to seed the session's goal and context.")
|
|
12484
12636
|
.option("--title <text>", "Optional human title for the session.")
|
|
@@ -12669,8 +12821,8 @@ Examples:
|
|
|
12669
12821
|
.command("runs")
|
|
12670
12822
|
.description("Workspace run ledger and provenance. `runs list --table <table>` is one table's full run history: imports, row writes and column runs.")
|
|
12671
12823
|
.addCommand(new Command("list")
|
|
12672
|
-
.description("List
|
|
12673
|
-
.option("--table <table>", "
|
|
12824
|
+
.description("List workspace runs, newest first; --table gives one table's full run history.")
|
|
12825
|
+
.option("--table <table>", "One table's full run history (id, slug or table URL): imports, row writes, column runs and Function calls.")
|
|
12674
12826
|
.option("--limit <n>", "Maximum runs to return. Defaults to 50.")
|
|
12675
12827
|
.option("--json", "Print a JSON envelope.")
|
|
12676
12828
|
.action(async (options) => {
|
|
@@ -13065,7 +13217,7 @@ Examples:
|
|
|
13065
13217
|
.option("--return <mode>", "Legacy response shape: raw, compact, or summary. Defaults to raw.")
|
|
13066
13218
|
.option("--return-mode <mode>", "Response shape: raw, compact, or summary. Prefer summary for large search responses.")
|
|
13067
13219
|
.option("--oxygen-cursor <cursor>", "Short Oxygen cursor returned as oxygen_next_cursor by a previous tool run.")
|
|
13068
|
-
.option("--max-credits <n>", "Credit ceiling for live paid tools; not needed for no-bill tools, and
|
|
13220
|
+
.option("--max-credits <n>", "Credit ceiling for live paid tools; not needed for no-bill tools. 0 runs no-bill operations, which may call providers, and reuses a managed lookup's cached result for the exact same input for free (the dry run's preview.cache says whether one exists). A paid cache miss at 0 calls and charges nothing.")
|
|
13069
13221
|
.option("--approved", "Required for live paid tools and external writes after inspecting dry-run output.")
|
|
13070
13222
|
.option("--json", "Print a JSON envelope.")
|
|
13071
13223
|
.action(async (toolId, options) => {
|
|
@@ -13198,7 +13350,7 @@ Examples:
|
|
|
13198
13350
|
.option("--company-domain <domain>", "Company apex domain, e.g. acme.com.")
|
|
13199
13351
|
.option("--company-name <name>", "Company name.")
|
|
13200
13352
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
13201
|
-
.option("--max-credits <credits>", "Spend ceiling. Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. When the resolved chain runs the guessed-address pass (the name+domain and first/last+domain profiles, not the LinkedIn-URL one) the block also carries estimate.pattern_guess_verification_credits — the verification checks that pass bills before any named provider is dialled. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13353
|
+
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits; a lane that runs on your own provider key bills your provider account and is outside it (cap that with `oxygen limits provider set`). Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. When the resolved chain runs the guessed-address pass (the name+domain and first/last+domain profiles, not the LinkedIn-URL one) the block also carries estimate.pattern_guess_verification_credits — the verification checks that pass bills before any named provider is dialled. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13202
13354
|
.option("--json", "Print a JSON envelope.")
|
|
13203
13355
|
.action(async (options) => {
|
|
13204
13356
|
await handleAsyncAction("find email", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("email", options) }));
|
|
@@ -13211,7 +13363,7 @@ Examples:
|
|
|
13211
13363
|
.option("--company-domain <domain>", "Company apex domain, e.g. acme.com.")
|
|
13212
13364
|
.option("--company-name <name>", "Company name.")
|
|
13213
13365
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
13214
|
-
.option("--max-credits <credits>", "Spend ceiling. Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13366
|
+
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits; a lane that runs on your own provider key bills your provider account and is outside it (cap that with `oxygen limits provider set`). Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13215
13367
|
.option("--json", "Print a JSON envelope.")
|
|
13216
13368
|
.action(async (options) => {
|
|
13217
13369
|
await handleAsyncAction("find personal-email", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("personal_email", options) }));
|
|
@@ -13225,7 +13377,7 @@ Examples:
|
|
|
13225
13377
|
.option("--company-name <name>", "Company name.")
|
|
13226
13378
|
.option("--verify", "Verify the found number with ClearoutPhone — adds line_type/carrier and keeps the number on a non-verdict (only a genuine 'not valid' is discarded).")
|
|
13227
13379
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
13228
|
-
.option("--max-credits <credits>", "Spend ceiling. Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13380
|
+
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits; a lane that runs on your own provider key bills your provider account and is outside it (cap that with `oxygen limits provider set`). Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13229
13381
|
.option("--json", "Print a JSON envelope.")
|
|
13230
13382
|
.action(async (options) => {
|
|
13231
13383
|
await handleAsyncAction("find phone", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("phone", options) }));
|
|
@@ -13238,15 +13390,15 @@ Examples:
|
|
|
13238
13390
|
.option("--company-name <name>", "Company name.")
|
|
13239
13391
|
.option("--company-linkedin-url <url>", "Company LinkedIn URL.")
|
|
13240
13392
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
13241
|
-
.option("--max-credits <credits>", "Spend ceiling. Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13393
|
+
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits; a lane that runs on your own provider key bills your provider account and is outside it (cap that with `oxygen limits provider set`). Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13242
13394
|
.option("--json", "Print a JSON envelope.")
|
|
13243
13395
|
.action(async (options) => {
|
|
13244
13396
|
await handleAsyncAction("find linkedin", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("linkedin", options) }));
|
|
13245
13397
|
}))
|
|
13246
13398
|
.addCommand(new Command("person")
|
|
13247
|
-
.description("Enrich one person from a LinkedIn URL, a LinkedIn public id, a Sales Navigator link, or an email / name + company: one managed profile lookup returns the profile block — first, last and full name, current company and its LinkedIn URL, job title, job start date, education and school, headline, bio, location and follower count — the same fields the person_enrich table preset extracts for free. An email or name is first resolved to the profile through the LinkedIn URL waterfall (priced as an identity prerequisite). A Sales Navigator link
|
|
13399
|
+
.description("Enrich one person from a LinkedIn URL, a LinkedIn public id or member id, a Sales Navigator link, or an email / name + company: one managed profile lookup returns the profile block — first, last and full name, current company and its LinkedIn URL, job title, job start date, education and school, headline, bio, location and follower count — the same fields the person_enrich table preset extracts for free. An email or name is first resolved to the profile through the LinkedIn URL waterfall (priced as an identity prerequisite). A member id uses managed enrichment. A Sales Navigator link resolves on the workspace's own HarvestAPI key. dry_run previews the lookup and its price for free.")
|
|
13248
13400
|
.option("--linkedin-url <url>", "Person LinkedIn profile URL (linkedin.com/in/<handle>).")
|
|
13249
|
-
.option("--linkedin-id <id>", "LinkedIn public identifier (the part after /in/), or
|
|
13401
|
+
.option("--linkedin-id <id>", "LinkedIn public identifier (the part after /in/), or a member ID beginning ACo/ACw; both support managed enrichment. Preview checks input shape and price without resolving the ID.")
|
|
13250
13402
|
.option("--sales-navigator-url <url>", "Sales Navigator lead or people link; resolved on the workspace's own HarvestAPI key.")
|
|
13251
13403
|
.option("--email <email>", "Known email; resolved to the profile through the LinkedIn URL waterfall first.")
|
|
13252
13404
|
.option("--full-name <name>", "Person full name; with --company-domain or --company-name, resolved through the LinkedIn URL waterfall first.")
|
|
@@ -13256,7 +13408,7 @@ Examples:
|
|
|
13256
13408
|
.option("--company-name <name>", "Company name.")
|
|
13257
13409
|
.option("--include-raw", "Also return the provider's raw profile payload (profile_raw) for fields the profile block does not carry.")
|
|
13258
13410
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the profile.")
|
|
13259
|
-
.option("--max-credits <credits>", "Spend ceiling. Required for --mode live. Run the free dry_run first: estimate.recommended_max_credits funds the profile lookup plus, for an email or name input, every lane of the LinkedIn URL waterfall that resolves it.")
|
|
13411
|
+
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits; a lane that runs on your own provider key bills your provider account and is outside it (cap that with `oxygen limits provider set`). Required for --mode live. Run the free dry_run first: estimate.recommended_max_credits funds the profile lookup plus, for an email or name input, every lane of the LinkedIn URL waterfall that resolves it.")
|
|
13260
13412
|
.option("--json", "Print a JSON envelope.")
|
|
13261
13413
|
.action(async (options) => {
|
|
13262
13414
|
await handleAsyncAction("find person", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("person", options) }));
|
|
@@ -13270,7 +13422,7 @@ Examples:
|
|
|
13270
13422
|
.option("--list-fields", "Print the field catalog (category, description, provider count, per-row credits: free / fixed / ~estimated) and exit. Needs no identity; spends nothing.")
|
|
13271
13423
|
.option("--check-technologies <names>", "Comma-separated technology names for the technology_check field, answered yes/no against the detected stack (e.g. hubspot,salesforce).")
|
|
13272
13424
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
13273
|
-
.option("--max-credits <credits>", "Spend ceiling. Required for --mode live; 0
|
|
13425
|
+
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits; a lane that runs on your own provider key bills your provider account and is outside it (cap that with `oxygen limits provider set`). Required for --mode live; 0 can still call providers on zero-credit lanes and reuse any priced lane this workspace already has a cached answer for (the other priced lanes are skipped as credit_ceiling_reached). Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that funds every lane in the plan, and estimate.min_credits, what the plan costs if each field is answered by its first lane.")
|
|
13274
13426
|
.option("--json", "Print a JSON envelope.")
|
|
13275
13427
|
.action(async (options) => {
|
|
13276
13428
|
await handleAsyncAction("find company", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("company", options) }));
|
|
@@ -13537,17 +13689,21 @@ Examples:
|
|
|
13537
13689
|
});
|
|
13538
13690
|
})))
|
|
13539
13691
|
.addCommand(new Command("list")
|
|
13540
|
-
.description("List supported integrations and this org's connections.")
|
|
13692
|
+
.description("List supported integrations and this org's connections. Add --integration <id> to read one integration and its connection state.")
|
|
13693
|
+
.option("--integration <id>", "Return only this integration and its connections, such as hubspot.")
|
|
13541
13694
|
.option("--json", "Print a JSON envelope.")
|
|
13542
13695
|
.action(async (options) => {
|
|
13543
|
-
await handleAsyncAction("integrations list", options, () =>
|
|
13696
|
+
await handleAsyncAction("integrations list", options, () => {
|
|
13697
|
+
const integration = readOption(options.integration)?.trim();
|
|
13698
|
+
return requestOxygen(`/api/cli/integrations/composio/list${integration ? `?integration=${encodeURIComponent(integration)}` : ""}`);
|
|
13699
|
+
});
|
|
13544
13700
|
}))
|
|
13545
13701
|
.addCommand(addContractFlags(new Command("connect")
|
|
13546
|
-
.description("Connect an integration. For OAuth toolkits, running this command immediately creates a pending provider authorization and returns a redirect URL; account access starts only if you approve on the provider's page. This is not a price preview. On a repriced workspace, a connected X account reserves 1,000 credits ($10) monthly; inspect
|
|
13702
|
+
.description("Connect an integration. For OAuth toolkits, running this command immediately creates a pending provider authorization and returns a redirect URL; account access starts only if you approve on the provider's page. This is not a price preview. On a repriced workspace, a connected X account reserves 1,000 credits ($10) monthly; inspect current connection prices and first billing timing with `oxygen billing commitments --view connection-prices --json` before connecting. API-key integrations accept --api-key. Run without credentials only for an API-key integration to get a 0-credit preview of its exact fields (mode: needs_api_key): no credential is submitted, nothing is stored, and no provider call is made. Use repeatable --credential name=value when a provider also requires a host, subdomain, or other named value. To keep a secret out of shell history and process arguments, open the preview's web_url and submit it in Connections; --api-key is for controlled noninteractive use. Credential submission creates or replaces the connection immediately and needs no separate --approved flag. LinkedIn and WhatsApp return a hosted sign-in link instead (`oxygen senders connect` / `oxygen whatsapp connect` are the canonical paths).")
|
|
13547
13703
|
.argument("<integration_id>", "Integration id, such as 'slack' or 'serpapi'.")
|
|
13548
13704
|
.option("--api-key <value>", "API key for controlled noninteractive use. Literal values can appear in shell history and process arguments; prefer the preview web_url for manual entry.")
|
|
13549
13705
|
.option("--credential <name=value>", "Named provider credential field. Repeat for multi-field integrations (for example, PostHog: --credential subdomain=us).", collectRepeatable, [])
|
|
13550
|
-
.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).")
|
|
13706
|
+
.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 from the 56 countries OXYGEN has LinkedIn proxies in).")
|
|
13551
13707
|
.option("--pairing-phone-number <e164>", "WhatsApp only: pair by code sent to this E.164 number instead of showing a QR.")
|
|
13552
13708
|
.addOption(new Option("--acknowledge-risk", "Deprecated: LinkedIn connect no longer needs it. Still accepted so older scripts keep running.").hideHelp())
|
|
13553
13709
|
.option("--return-to <path>", "In-product destination after OAuth approval.")
|
|
@@ -13594,7 +13750,7 @@ Examples:
|
|
|
13594
13750
|
}));
|
|
13595
13751
|
}))
|
|
13596
13752
|
.addCommand(new Command("disconnect")
|
|
13597
|
-
.description("Disconnect an integration. LinkedIn and WhatsApp remove the
|
|
13753
|
+
.description("Disconnect an integration. LinkedIn and WhatsApp remove the connected account; pass --connection-id when more than one is connected.")
|
|
13598
13754
|
.argument("<integration_id>", "Integration id, such as 'slack'.")
|
|
13599
13755
|
.option("--connection-id <id>", "Disconnect a specific connected account when multiple exist.")
|
|
13600
13756
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -13705,12 +13861,12 @@ Examples:
|
|
|
13705
13861
|
await handleAsyncAction("senders checkpoints", options, () => requestOxygen("/api/cli/senders/checkpoints"));
|
|
13706
13862
|
}))
|
|
13707
13863
|
.addCommand(new Command("connect")
|
|
13708
|
-
.description("Get a
|
|
13864
|
+
.description("Get a hosted sign-in link to connect a new LinkedIn account (or reconnect with --reconnect). This command creates a pending provider authorization. Inspect prices first with `oxygen billing commitments --view connection-prices --json`, which creates no authorization. A covering dollar seat and a credit reservation are alternative billing rails. Read the seat coverage and actual commitment before relying on the next monthly billing date, especially on reconnect. Once the workspace is repriced that reservation is 3,400 credits ($34) monthly; with the credit gate active, a connect the balance cannot cover fails with insufficient_credits. Invites, messages and other LinkedIn actions cost 0 credits on top. `oxygen billing seats --json` shows held seats and next-connection eligibility; paying-seat coverage may remain unknown. Inspect `oxygen billing commitments --json` for an account’s actual reservation, and `oxygen billing estimate --linkedin-accounts <n> --json` prices n accounts against your plan. 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 in the hosted sign-in 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.")
|
|
13709
13865
|
.option("--reconnect <connection_id>", "Reconnect an existing connection instead of creating a new one. Accepts a connection id.")
|
|
13710
|
-
.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).")
|
|
13711
|
-
.option("--sales-nav", "Request Classic + Sales Navigator access during
|
|
13712
|
-
.option("--cookie-auth", "Offer LinkedIn cookie authentication in
|
|
13713
|
-
.option("--custom-proxy", "Let the account owner enter a custom proxy in
|
|
13866
|
+
.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); one of the 56 countries OXYGEN has LinkedIn proxies in.")
|
|
13867
|
+
.option("--sales-nav", "Request Classic + Sales Navigator access during hosted sign-in.")
|
|
13868
|
+
.option("--cookie-auth", "Offer LinkedIn cookie authentication in the hosted sign-in wizard. The account owner enters li_at/li_a there; OXYGEN never receives them.")
|
|
13869
|
+
.option("--custom-proxy", "Let the account owner enter a custom proxy in the hosted sign-in wizard. OXYGEN never receives the proxy credentials.")
|
|
13714
13870
|
.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.")
|
|
13715
13871
|
.option("--share-link", "Return a shareable connection link for one account; its owner completes authentication.")
|
|
13716
13872
|
.option("--inbox-scope <scope>", "LinkedIn inbox privacy for new accounts: oxygen_initiated (default) or all. Reconnect preserves the existing account setting.")
|
|
@@ -13762,6 +13918,17 @@ Examples:
|
|
|
13762
13918
|
body: { scope: options.scope },
|
|
13763
13919
|
}));
|
|
13764
13920
|
})))
|
|
13921
|
+
.addCommand(new Command("proxy-country")
|
|
13922
|
+
.description("Move a LinkedIn sender's automatic proxy to another country without reconnecting. The account picks a proxy there at once when one is free; otherwise its sequences stay paused and each `senders sync` asks again. Zero credits.")
|
|
13923
|
+
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
13924
|
+
.requiredOption("--country <code>", "ISO 3166-1 alpha-2 country where the account owner logs in; one of the 56 countries OXYGEN has LinkedIn proxies in (others are refused with the list).")
|
|
13925
|
+
.option("--json", "Print a JSON envelope.")
|
|
13926
|
+
.action(async (id, options) => {
|
|
13927
|
+
await handleAsyncAction("senders proxy-country", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/proxy-country`, {
|
|
13928
|
+
method: "PATCH",
|
|
13929
|
+
body: { country: options.country.trim().toUpperCase() },
|
|
13930
|
+
}));
|
|
13931
|
+
}))
|
|
13765
13932
|
.addCommand(new Command("get")
|
|
13766
13933
|
.description("Get one LinkedIn sender account with status, limits, daily-reset timezone, and usage. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
13767
13934
|
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
@@ -13781,7 +13948,7 @@ Examples:
|
|
|
13781
13948
|
}));
|
|
13782
13949
|
}))
|
|
13783
13950
|
.addCommand(new Command("health")
|
|
13784
|
-
.description("Show a one-call health snapshot for a LinkedIn sender: status, the latest error reason, any open security checkpoint, today's usage, the warm-up ramp,
|
|
13951
|
+
.description("Show a one-call health snapshot for a LinkedIn sender: status, the latest error reason, any open security checkpoint, today's usage, the warm-up ramp, when the daily quota resets, and inbox sync (last sync, latest failure, back-off). Read-only — no provider call, no credits. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
13785
13952
|
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
13786
13953
|
.option("--json", "Print a JSON envelope.")
|
|
13787
13954
|
.action(async (id, options) => {
|
|
@@ -13820,7 +13987,7 @@ Examples:
|
|
|
13820
13987
|
});
|
|
13821
13988
|
}))
|
|
13822
13989
|
.addCommand(new Command("disconnect")
|
|
13823
|
-
.description("Disconnect a LinkedIn sender account so it stops sending
|
|
13990
|
+
.description("Disconnect a LinkedIn sender account so it stops sending. Works on an account that is already disconnected. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
13824
13991
|
.argument("<id>", "Sender account id, connection id, or Unipile account id.")
|
|
13825
13992
|
.option("--json", "Print a JSON envelope.")
|
|
13826
13993
|
.action(async (id, options) => {
|
|
@@ -14445,7 +14612,7 @@ Examples:
|
|
|
14445
14612
|
program.addCommand(new Command("linkedin")
|
|
14446
14613
|
.description("Put your own LinkedIn network in a table and keep it current. `linkedin network setup` captures everyone you are connected to and everyone who follows you into one self-refreshing 'LinkedIn Network' table (free, read-only on your account, paced so the account is never at risk); `linkedin network status` reports the pace and `linkedin network pause` switches it off. `linkedin invitations setup` does the same for open connection requests — received and sent — in one self-updating table. Also home to `linkedin intent` (engagement capture) and `linkedin ingestion status` (every LinkedIn read drip and each account's remaining read budget).")
|
|
14447
14614
|
.addCommand(new Command("network")
|
|
14448
|
-
.description("Capture everyone already in your LinkedIn network \u2014 the people you are connected to and the people who follow you \u2014 into one self-refreshing workspace table you can filter, enrich and enroll from. READ-ONLY on your LinkedIn account: it lists your own connections and followers and nothing else. It sends no messages, no connection requests, and does not visit anyone's profile. Runs as a slow background drip on its OWN per-account read budget, so it cannot slow down or starve anything else the account does: about 2,000 people a day while it first walks your network (40 reads of up to 50 people), then a light check a few times a day for new arrivals. Only inside each account's own 07:00-22:00 working hours, spaced minutes apart, never in a burst; a newly armed account eases in over its first two weeks. LinkedIn does not say how big a network is until the walk has been through it, so a large network takes days \u2014 `status` reports the rate, not a completion date it cannot know. Free \u2014 no Oxygen credits; it uses
|
|
14615
|
+
.description("Capture everyone already in your LinkedIn network \u2014 the people you are connected to and the people who follow you \u2014 into one self-refreshing workspace table you can filter, enrich and enroll from. READ-ONLY on your LinkedIn account: it lists your own connections and followers and nothing else. It sends no messages, no connection requests, and does not visit anyone's profile. Runs as a slow background drip on its OWN per-account read budget, so it cannot slow down or starve anything else the account does: about 2,000 people a day while it first walks your network (40 reads of up to 50 people), then a light check a few times a day for new arrivals. Only inside each account's own 07:00-22:00 working hours, spaced minutes apart, never in a burst; a newly armed account eases in over its first two weeks. LinkedIn does not say how big a network is until the walk has been through it, so a large network takes days \u2014 `status` reports the rate, not a completion date it cannot know. Free \u2014 no Oxygen credits for this read; it uses your connected LinkedIn account, whose seat or credit reservation has its own terms.")
|
|
14449
14616
|
.addCommand(new Command("setup")
|
|
14450
14617
|
.description("Arm (or re-arm) network capture. Creates or reuses one merged 'LinkedIn Network' table and starts a connections drip and a followers drip per account, both writing into that one table deduped on LinkedIn member id \u2014 somebody who is both shows as 'both' in the Relationship column. This is a STANDING permission: capture keeps running until you switch it off with `linkedin network pause`, and a LinkedIn account you connect later is armed automatically. A newly connected account eases into full read speed over its first two weeks. Nothing is sent and no credits are charged. The response names each account it armed (`senders[].display_name`) \u2014 check it is yours before leaving it running.")
|
|
14451
14618
|
.option("--account <ref>", "Account to capture (sender id, connection id, or Unipile account id). Omit to capture every connected LinkedIn account.")
|
|
@@ -14654,7 +14821,7 @@ Examples:
|
|
|
14654
14821
|
});
|
|
14655
14822
|
}))));
|
|
14656
14823
|
program.addCommand(new Command("whatsapp")
|
|
14657
|
-
.description("Manage the org's connected WhatsApp accounts for Sequencer: list, connect, sync, inspect, disconnect, tune daily limits, and warm-send into an existing conversation. Cold outbound runs through a WhatsApp sequence (sequences create --channels whatsapp).")
|
|
14824
|
+
.description("Manage the org's connected WhatsApp accounts for Sequencer: accounts (list), connect, sync, inspect, disconnect, tune daily limits, and warm-send into an existing conversation. Cold outbound runs through a WhatsApp sequence (sequences create --channels whatsapp).")
|
|
14658
14825
|
.addCommand(addContractFlags(new Command("accounts")
|
|
14659
14826
|
.description("Read stored WhatsApp account health. Disconnected accounts are hidden by default; --include-disconnected includes retained accounts, --status selects one status. Presence is not send readiness. Use summary and exact account-id/value for long public metadata."), WHATSAPP_ACCOUNTS_READ_CONTRACT, WHATSAPP_ACCOUNT_FLAGS)
|
|
14660
14827
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -14665,7 +14832,7 @@ Examples:
|
|
|
14665
14832
|
});
|
|
14666
14833
|
}))
|
|
14667
14834
|
.addCommand(new Command("connect")
|
|
14668
|
-
.description("Get a
|
|
14835
|
+
.description("Get a hosted sign-in link to connect a new WhatsApp account by scanning a QR in WhatsApp → Linked Devices (or reconnect with --reconnect). Running this command creates a pending provider authorization. Inspect the current monthly price and first billing timing with `oxygen billing commitments --view connection-prices --json` before connecting. Newly connected accounts start a mandatory warm-up ramp.")
|
|
14669
14836
|
.option("--reconnect <connection_id>", "Reconnect an existing connection id instead of linking a new account.")
|
|
14670
14837
|
.option("--pairing-phone-number <phone>", "Offer phone-number pairing instead of QR where supported; use your international number.")
|
|
14671
14838
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -14703,7 +14870,7 @@ Examples:
|
|
|
14703
14870
|
});
|
|
14704
14871
|
}))
|
|
14705
14872
|
.addCommand(new Command("disconnect")
|
|
14706
|
-
.description("Disconnect a WhatsApp account so it stops sending
|
|
14873
|
+
.description("Disconnect a WhatsApp account so it stops sending. Works on an account that is already disconnected. <id> accepts an account id, connection id, or Unipile account id.")
|
|
14707
14874
|
.argument("<id>", "WhatsApp account id, connection id, or Unipile account id.")
|
|
14708
14875
|
.option("--json", "Print a JSON envelope.")
|
|
14709
14876
|
.action(async (id, options) => {
|
|
@@ -14718,14 +14885,9 @@ Examples:
|
|
|
14718
14885
|
.action(async (id, options) => {
|
|
14719
14886
|
await handleAsyncAction("whatsapp limits get", options, () => requestOxygen(`/api/cli/whatsapp/accounts/${encodeURIComponent(id)}/limits`));
|
|
14720
14887
|
}))
|
|
14721
|
-
.addCommand(new Command("set")
|
|
14888
|
+
.addCommand(addWhatsAppLimitFlags(new Command("set")
|
|
14722
14889
|
.description("Adjust per-account WhatsApp daily limits and the daily-reset timezone. Values are clamped to safe maximums; the warm-up ceiling still applies to direct and Sequence traffic. <id> accepts an account id, connection id, or Unipile account id.")
|
|
14723
|
-
.argument("<id>", "WhatsApp account id, connection id, or Unipile account id.")
|
|
14724
|
-
.option("--messages-per-day <n>", "Daily WhatsApp messages cap across direct and Sequence traffic (subject to the warm-up ceiling).")
|
|
14725
|
-
.option("--total-actions-per-day <n>", "Daily cap across all WhatsApp action types.")
|
|
14726
|
-
.option("--messages-reads-per-day <n>", "Daily cap on chat and message-history reads.")
|
|
14727
|
-
.option("--min-spacing-seconds <n>", "Minimum seconds between actions.")
|
|
14728
|
-
.option("--spacing-jitter-seconds <n>", "Random jitter seconds added to action spacing.")
|
|
14890
|
+
.argument("<id>", "WhatsApp account id, connection id, or Unipile account id."))
|
|
14729
14891
|
.option("--working-days <days>", "Allowed weekdays, e.g. mon-fri or 1,3,5.")
|
|
14730
14892
|
.option("--working-hours-start <HH:MM>", "Local start of the allowed sending window.")
|
|
14731
14893
|
.option("--working-hours-end <HH:MM>", "Local end of the allowed sending window.")
|
|
@@ -14900,7 +15062,8 @@ Examples:
|
|
|
14900
15062
|
.option("--domain <domains>", "Email only: comma-separated counterpart domains to include.")
|
|
14901
15063
|
.option("--exclude-domain <domains>", "Email only: comma-separated counterpart domains to exclude.")
|
|
14902
15064
|
.option("--mailbox-id <ids>", "Email only: comma-separated mailbox ids.")
|
|
14903
|
-
.option("--sender-profile <ids>", "Comma-separated sender profile ids (people
|
|
15065
|
+
.option("--sender-profile-id <ids>", "Comma-separated sender profile ids (people; `unibox metadata` and `senders profiles list` list them): only conversations on their own inboxes and LinkedIn/WhatsApp accounts. The same `sender_profile_id` key a saved view's --filters takes. Merged stream only; narrow channels with --channels.")
|
|
15066
|
+
.addOption(new Option("--sender-profile <ids>", "Alias of --sender-profile-id.").hideHelp())
|
|
14904
15067
|
.option("--search <text>", "Fuzzy search (typos, word order, prefixes) over names, addresses, subjects and the text of every message in EVERY non-warmup conversation: all channels unless --channel narrows, archived and sent-only threads included. --segment, --unanswered and the Primary tab's negative-tier exclusion are ignored while set; explicit facets such as --status still narrow. For structured filters use --status or --tag, not a text search.")
|
|
14905
15068
|
.option("--include-archived", "Include archived conversations.")
|
|
14906
15069
|
.option("--limit <n>", "Maximum conversations to return (1-200). Defaults to 50.")
|
|
@@ -14938,6 +15101,7 @@ Examples:
|
|
|
14938
15101
|
["excludeDomain", "exclude_domain"],
|
|
14939
15102
|
["mailboxId", "mailbox_id"],
|
|
14940
15103
|
["senderProfile", "sender_profile_id"],
|
|
15104
|
+
["senderProfileId", "sender_profile_id"],
|
|
14941
15105
|
["cursor", "cursor"],
|
|
14942
15106
|
]) {
|
|
14943
15107
|
const value = readOption(options[flag]);
|
|
@@ -14961,7 +15125,7 @@ Examples:
|
|
|
14961
15125
|
});
|
|
14962
15126
|
}))
|
|
14963
15127
|
.addCommand(new Command("get")
|
|
14964
|
-
.description("Get one conversation with its full message thread. <conversation> accepts a conversation id, Unipile chat id, or (email) Zapbox thread id, and is resolved on any channel by default (email first, then LinkedIn/WhatsApp). Opening a WhatsApp thread may fetch provider history and
|
|
15128
|
+
.description("Get one conversation with its full message thread. <conversation> accepts a conversation id, Unipile chat id, or (email) Zapbox thread id, and is resolved on any channel by default (email first, then LinkedIn/WhatsApp). Opening a LinkedIn or WhatsApp thread may fetch provider history and update the local mirror. WhatsApp also merges provider-confirmed phone/contact identities. This uses no Oxygen credits and sends no message; old conversation references remain usable. For stored-only inspection, use messages query.")
|
|
14965
15129
|
.argument("<conversation>", "Conversation id, Unipile chat id, or Zapbox thread id.")
|
|
14966
15130
|
.option("--channel <channel>", "Inbox channel: all (default, resolves the id on any channel), email, linkedin, or whatsapp.")
|
|
14967
15131
|
.option("--message-limit <n>", "Maximum messages to return (1-500). Defaults to 100.")
|
|
@@ -15095,7 +15259,7 @@ Examples:
|
|
|
15095
15259
|
}));
|
|
15096
15260
|
}))
|
|
15097
15261
|
.addCommand(new Command("mark-read")
|
|
15098
|
-
.description("Mark a conversation and all its messages as read.")
|
|
15262
|
+
.description("Mark a conversation and all its messages as read (LinkedIn by default; use --channel email or whatsapp for those channels).")
|
|
15099
15263
|
.argument("<conversation>", "Conversation id, Unipile chat id, or (email) Zapbox thread id.")
|
|
15100
15264
|
.option("--channel <channel>", "Inbox channel: linkedin (default), whatsapp, or email.")
|
|
15101
15265
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -15120,7 +15284,8 @@ Examples:
|
|
|
15120
15284
|
.option("--exclude-domain <domains>", "Email only: comma-separated counterpart domains to exclude.")
|
|
15121
15285
|
.option("--mailbox-id <ids>", "Email only: comma-separated mailbox ids.")
|
|
15122
15286
|
.option("--sender-account-id <ids>", "DMs only: comma-separated OXYGEN sender UUIDs from `oxygen senders list --json` (not LinkedIn member ids).")
|
|
15123
|
-
.option("--sender-profile <ids>", "Comma-separated sender profile ids (people
|
|
15287
|
+
.option("--sender-profile-id <ids>", "Comma-separated sender profile ids (people; `unibox metadata` lists them): only their own inboxes and LinkedIn/WhatsApp accounts, as in `inbox list --sender-profile-id`.")
|
|
15288
|
+
.addOption(new Option("--sender-profile <ids>", "Alias of --sender-profile-id.").hideHelp())
|
|
15124
15289
|
.option("--since <iso>", "Only conversations whose last message is on/after this ISO date/timestamp. Excludes conversations without a last-message date. Cross-channel.")
|
|
15125
15290
|
.option("--until <iso>", "Only conversations whose last message is on/before this ISO date/timestamp. Excludes conversations without a last-message date. Cross-channel.")
|
|
15126
15291
|
.option("--search <text>", "Fuzzy search (typos, word order, prefixes) over names, addresses, subjects and the text of every message in EVERY non-warmup conversation: all channels unless --channel narrows, archived and sent-only threads included. --segment, --unanswered and the Primary tab's negative-tier exclusion are ignored while set; explicit facets such as --status still narrow.")
|
|
@@ -15151,6 +15316,7 @@ Examples:
|
|
|
15151
15316
|
["mailboxId", "mailbox_id"],
|
|
15152
15317
|
["senderAccountId", "sender_account_id"],
|
|
15153
15318
|
["senderProfile", "sender_profile_id"],
|
|
15319
|
+
["senderProfileId", "sender_profile_id"],
|
|
15154
15320
|
["since", "since"],
|
|
15155
15321
|
["until", "until"],
|
|
15156
15322
|
["search", "search"],
|
|
@@ -15169,6 +15335,7 @@ Examples:
|
|
|
15169
15335
|
params.set("needs_reply", "true");
|
|
15170
15336
|
return requestOxygen(`/api/cli/inbox/read-all?${params.toString()}`, {
|
|
15171
15337
|
method: "POST",
|
|
15338
|
+
previewOnly: !options.yes,
|
|
15172
15339
|
// Approval rides in the body: a bodyless POST reads as
|
|
15173
15340
|
// approved=false server-side, i.e. a preview that mutates
|
|
15174
15341
|
// nothing. --yes (not --approved) because this spends no
|
|
@@ -15331,6 +15498,7 @@ Examples:
|
|
|
15331
15498
|
.filter(Boolean);
|
|
15332
15499
|
return requestOxygen("/api/cli/inbox/bulk", {
|
|
15333
15500
|
method: "POST",
|
|
15501
|
+
previewOnly: !options.yes,
|
|
15334
15502
|
body: {
|
|
15335
15503
|
action,
|
|
15336
15504
|
channel,
|
|
@@ -15352,7 +15520,7 @@ Examples:
|
|
|
15352
15520
|
.addCommand(new Command("create")
|
|
15353
15521
|
.description("Save a view for the team.")
|
|
15354
15522
|
.requiredOption("--name <name>", "View name, unique in the workspace (up to 80 characters).")
|
|
15355
|
-
.option("--filters <url-or-query>", "A Unibox URL or its query string, e.g. 'sender_profile_id=<id>&channels=linkedin&status=interested'
|
|
15523
|
+
.option("--filters <url-or-query>", "A Unibox URL or its query string, e.g. 'sender_profile_id=<id>&channels=linkedin&status=interested'. Keys are the `inbox list` filters (sender_profile_id is --sender-profile-id); a view without segment opens on the Primary tab, segment=all on every conversation. Omit for every conversation.")
|
|
15356
15524
|
.option("--default", "Also make it your own default view.")
|
|
15357
15525
|
.option("--json", "Print a JSON envelope.")
|
|
15358
15526
|
.action(async (options) => {
|
|
@@ -15369,7 +15537,7 @@ Examples:
|
|
|
15369
15537
|
.description("Rename, re-filter or reorder a view, or set it as (or stop using it as) your own default.")
|
|
15370
15538
|
.argument("<view>", "View id (from `inbox views list`).")
|
|
15371
15539
|
.option("--name <name>", "New name.")
|
|
15372
|
-
.option("--filters <url-or-query>", "Replace the view's filters: a Unibox URL or its query string.")
|
|
15540
|
+
.option("--filters <url-or-query>", "Replace the view's filters: a Unibox URL or its query string (the `inbox list` filter keys; without segment the view opens on the Primary tab).")
|
|
15373
15541
|
.option("--position <n>", "Zero-based order in the team's list.")
|
|
15374
15542
|
.option("--default", "Make it your own default view.")
|
|
15375
15543
|
.option("--no-default", "Stop using it as your default view (you open on the built-in view).")
|
|
@@ -15948,7 +16116,7 @@ Examples:
|
|
|
15948
16116
|
});
|
|
15949
16117
|
})))
|
|
15950
16118
|
.addCommand(addContractFlags(new Command("analytics")
|
|
15951
|
-
.description("Show selected-window sequencer analytics with persisted status kept separate from operational state, fleet marked-active versus operationally-live counts, current open work, sent/terminal-failed/deferral totals, failure classes, and action-kind breakdowns. Also includes per-sequence funnels and native email attribution under analytics.emailAttribution: byMailbox, byDomain, and explicit unattributed facts. With --sequence, analytics.senderPerformance groups channel-conditional counts and rates by sending person; analytics.messagePerformance reports each copy-bearing deterministic step variant, target/actual distribution, contacted recipients, and type-specific accepted/reply/positive outcomes (absent for note-less connect-only campaigns). Message-level reply outcomes require preserved step/variant provenance, so older unattributed replies remain in campaign totals without being guessed into a message row. Provider-owned campaigns appear only after their telemetry is normalized into Oxygen's native Sequence ledgers. Also reports whether the reply → CRM automation is armed, with a link to the workflow.")
|
|
16119
|
+
.description("Show selected-window sequencer analytics with persisted status kept separate from operational state, fleet marked-active versus operationally-live counts, current open work, sent/terminal-failed/deferral totals, failure classes, and action-kind breakdowns. Also includes per-sequence funnels and native email attribution under analytics.emailAttribution: byMailbox, byDomain, and explicit unattributed facts. With --sequence, analytics.senderPerformance groups channel-conditional counts and rates by sending person; analytics.messagePerformance reports each copy-bearing deterministic step variant, target/actual distribution, contacted recipients, and type-specific accepted/reply/positive outcomes (absent for note-less connect-only campaigns). Message-level reply outcomes require preserved step/variant provenance, so older unattributed replies remain in campaign totals without being guessed into a message row. Provider-owned campaigns appear only after their telemetry is normalized into Oxygen's native Sequence ledgers. Also reports whether the reply → CRM automation is armed, with a link to the workflow. Omit --projection for all sections in one read; --projection value --path /analytics selects all analytics sections. value_json is escaped JSON text, paged losslessly with value_cursor.")
|
|
15952
16120
|
.option("--sender <id>", "Filter to one sender from `oxygen senders list` or `oxygen senders profiles list`, or a key from this output's senders list. Repeatable.", collectRepeatable, [])
|
|
15953
16121
|
.option("--json", "Print a JSON envelope."), SEQUENCE_ANALYTICS_READ_CONTRACT, {
|
|
15954
16122
|
range: { valueName: "<preset>", parse: readOption },
|
|
@@ -16224,7 +16392,7 @@ Examples:
|
|
|
16224
16392
|
});
|
|
16225
16393
|
}))
|
|
16226
16394
|
.addCommand(new Command("get")
|
|
16227
|
-
.description("Get a sequence's definition, senders, status, and credit usage. --readiness adds the launch check — whether it CAN go live, why not, and the mailbox pool it would send from — without touching it; `sequences
|
|
16395
|
+
.description("Get a sequence's definition, senders, status, and credit usage. --readiness adds the launch check — whether it CAN go live, why not, and the mailbox pool it would send from — without touching it; `sequences preview <sequence>` (a read; 0 credits) shows who sends each step (sender_journey).")
|
|
16228
16396
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16229
16397
|
.option("--readiness", "Add launch_readiness: blockers (e.g. a missing send cap), warnings, and the resolved sending mailboxes. Read-only — sends nothing, changes nothing, spends nothing. Skips the LinkedIn rendered-copy scan, which stays in `sequences start` without --approved.")
|
|
16230
16398
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -16232,7 +16400,7 @@ Examples:
|
|
|
16232
16400
|
await handleAsyncAction("sequences get", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}${options.readiness ? "?include=readiness" : ""}`));
|
|
16233
16401
|
}))
|
|
16234
16402
|
.addCommand(new Command("enroll")
|
|
16235
|
-
.description("Enroll leads into a sequence. Enrolling spends no credits. An active sequence contacts the new leads on its next dispatch tick; a draft or paused one holds them until `sequences start`. --from-table enrolls every not-yet-enrolled row of the sequence's bound source table (up to 500 per run; the response's from_table.has_more says whether to run again) — for a LinkedIn sequence the profile is read from the bound table's URL column, where a full /in/ URL and a bare public handle carrying a hyphen or digit (ada-lovelace) both work, so no pre-resolved provider ids are needed; each handle Oxygen canonicalized is echoed in linkedin_handles_canonicalized (value + url) to check before start, and every row that still cannot be resolved is named in unresolved_linkedin_lead_details (row, value, reason) rather than only counted in unresolved_linkedin_leads. --leads-file enrolls an explicit JSON list { leads: [...] }: for an email/WhatsApp sequence a lead identified only by an email (row_values.email) or phone (row_values.phone) is auto-filed as a row in the sequence's leads table (auto-creating and binding one if there is none), deduped by email/phone, so no table has to exist first (the table is returned as leads_table with a web_url); a LinkedIn lead needs a lead_provider_id, a lead_profile_url (a /in/ URL or a bare public handle with a hyphen or digit), or a table_row_id whose row carries one of those. A CRM-only lead needs a stable table_row_id or lead_provider_id in addition to mapped row_values; a HubSpot contact id used in crm_task associations identifies the destination record, not the Oxygen enrollment. When the sequence is bound to a source table, a lead's table_row_id auto-snapshots that row's columns (incl. AI/tool outputs) into row_values for {{column}} copy — explicit row_values win. Idempotent per table row (and per email/phone for auto-filed leads). WhatsApp-only campaigns also enroll each normalized number once across all source rows and enrollment statuses; mixed campaigns keep other channels and skip duplicate WhatsApp actions. Duplicate protection defaults to this campaign; --whatsapp-duplicate-scope on create/update can additionally block identical copy across all campaigns. The org do-not-contact list is always enforced; --exclude-contacted and --suppress-list add further opt-in skips (reported under skipped_by_reason). Leads already owned by a sender account (from an earlier real send) are routed back to that same account; a lead owned by a sender NOT on this sequence is skipped (bound_to_other_sender) unless --ignore-sender-bindings. This enrolls into an OXYGEN Sequence; to put the same rows into a campaign you already run in Smartlead, Instantly, lemlist or HeyReach, use `sequences push-external`.")
|
|
16403
|
+
.description("Enroll leads into a sequence. Enrolling spends no credits. An active sequence contacts the new leads on its next dispatch tick; a draft or paused one holds them until `sequences start`. --from-table enrolls every not-yet-enrolled row of the sequence's bound source table (up to 500 per run; the response's from_table.has_more says whether to run again) — for a LinkedIn sequence the profile is read from the bound table's URL column, where a full /in/ URL and a bare public handle carrying a hyphen or digit (ada-lovelace) both work, so no pre-resolved provider ids are needed; each handle Oxygen canonicalized is echoed in linkedin_handles_canonicalized (value + url) to check before start, and every row that still cannot be resolved is named in unresolved_linkedin_lead_details (row, value, reason) rather than only counted in unresolved_linkedin_leads. In a LinkedIn + email sequence a row with no usable profile but a recipient email is still enrolled for the email steps (its LinkedIn steps are skipped as no_linkedin_profile, a connection wait passes at once and a connection branch takes its not-connected path) and is listed in linkedin_unavailable_lead_details instead; unresolved_linkedin_leads names only rows that were not enrolled. --leads-file enrolls an explicit JSON list { leads: [...] }: for an email/WhatsApp sequence a lead identified only by an email (row_values.email) or phone (row_values.phone) is auto-filed as a row in the sequence's leads table (auto-creating and binding one if there is none), deduped by email/phone, so no table has to exist first (the table is returned as leads_table with a web_url); a LinkedIn + email sequence auto-files an email-only lead the same way; a LinkedIn lead needs a lead_provider_id, a lead_profile_url (a /in/ URL or a bare public handle with a hyphen or digit), or a table_row_id whose row carries one of those. A CRM-only lead needs a stable table_row_id or lead_provider_id in addition to mapped row_values; a HubSpot contact id used in crm_task associations identifies the destination record, not the Oxygen enrollment. When the sequence is bound to a source table, a lead's table_row_id auto-snapshots that row's columns (incl. AI/tool outputs) into row_values for {{column}} copy — explicit row_values win. Idempotent per table row (and per email/phone for auto-filed leads). WhatsApp-only campaigns also enroll each normalized number once across all source rows and enrollment statuses; mixed campaigns keep other channels and skip duplicate WhatsApp actions. Duplicate protection defaults to this campaign; --whatsapp-duplicate-scope on create/update can additionally block identical copy across all campaigns. The org do-not-contact list is always enforced; --exclude-contacted and --suppress-list add further opt-in skips (reported under skipped_by_reason). Leads already owned by a sender account (from an earlier real send) are routed back to that same account; a lead owned by a sender NOT on this sequence is skipped (bound_to_other_sender) unless --ignore-sender-bindings. This enrolls into an OXYGEN Sequence; to put the same rows into a campaign you already run in Smartlead, Instantly, lemlist or HeyReach, use `sequences push-external`. esp.pool_scope identifies the active workspace inventory used for routing and strict_deferred_leads estimates; these estimates omit sequence sender restrictions, readiness and daily limits. A zero estimate does not guarantee readiness. Review sequences preview before launch.")
|
|
16236
16404
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16237
16405
|
.option("--leads-file <path>", "Path to a JSON file: { \"leads\": [{ row_values: { email }, lead_name }] } for cold email; { lead_provider_id, lead_name, table_row_id, row_values } for LinkedIn/existing rows; CRM-only leads require table_row_id or a stable lead_provider_id alongside mapped row_values. A CRM association object id is not the enrollment identity. Exactly one of --leads-file or --from-table.")
|
|
16238
16406
|
.option("--from-table", "Enroll every not-yet-enrolled row of the sequence's bound source table (up to 500 per run; re-run to continue). Exactly one of --leads-file or --from-table.")
|
|
@@ -16323,7 +16491,7 @@ Examples:
|
|
|
16323
16491
|
}, formatSequencePushExternal);
|
|
16324
16492
|
}))
|
|
16325
16493
|
.addCommand(new Command("start")
|
|
16326
|
-
.description("Preview or start a sequence. Without --approved this is the launch check and changes nothing: it returns launch_readiness (blockers
|
|
16494
|
+
.description("Preview or start a sequence. Without --approved this is the launch check and changes nothing: it returns launch_readiness (blockers and warnings), sender_journey (each step's From, candidate inboxes and assignment; no leads needed), rendered pending-lead LinkedIn/WhatsApp copy, character counts/limits, copy blockers, per-kind scope, sender/mailbox capacity, CRM readiness and safety caps. Email copy is not rendered; inspect its saved templates and unsubscribe settings with sequences get. Live start requires --approved; --max-credits is optional. Email/WhatsApp/CRM-task journeys additionally require --max-live-sends because their external actions cost 0 Oxygen credits. --dry-run starts the sequence in simulated mode (it leaves draft), with no send, provider campaign, CRM task or credits. Live start enables the standing reply → CRM automation only if it has never been configured. Planned counts include only leads each step's channel can reach; skipped_actions_by_reason and channel_coverage show the rest.")
|
|
16327
16495
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16328
16496
|
.option("--approved", "Approve and activate live. Without this flag, returns a preview only, including copy_preview rendered samples and blockers.")
|
|
16329
16497
|
.option("--max-credits <n>", "Optional positive credit ceiling. Omit for the free launch preview. Live start uses the saved ceiling when omitted; an unset ceiling means unbounded credits, not recipient approval.")
|
|
@@ -16344,6 +16512,15 @@ Examples:
|
|
|
16344
16512
|
},
|
|
16345
16513
|
});
|
|
16346
16514
|
});
|
|
16515
|
+
}))
|
|
16516
|
+
.addCommand(new Command("preview")
|
|
16517
|
+
.description("The free launch check, as a read: the same preview sequences start returns without --approved. It returns launch_readiness (blockers and warnings), sender_journey (who sends each step and how an inbox is picked), copy_preview (rendered pending-lead LinkedIn/WhatsApp copy, character counts and scan), caps and estimated credits. Email copy is not rendered; inspect its saved templates and unsubscribe settings with sequences get. Changes nothing and costs 0 credits; it never starts, resumes or dry-runs the sequence. Start with sequences start <sequence> --approved after review. Planned counts include only leads each step's channel can reach; skipped_actions_by_reason, channel_coverage and copy_preview.skipped_leads show the rest.")
|
|
16518
|
+
.argument("<sequence>", "Sequence id or slug.")
|
|
16519
|
+
.option("--json", "Print a JSON envelope.")
|
|
16520
|
+
.action(async (sequence, options) => {
|
|
16521
|
+
await handleAsyncAction("sequences preview", options, () =>
|
|
16522
|
+
// The start route's preview branch: no approved, no dry_run, no ceilings.
|
|
16523
|
+
requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/start`, { method: "POST", body: {} }));
|
|
16347
16524
|
}))
|
|
16348
16525
|
.addCommand(new Command("signal")
|
|
16349
16526
|
.description("Record an external GTM signal (e.g. linkedin_connected, email_replied, company_raised_funds, job_change, web_visit, intent) onto a running sequence's enrollment(s) to drive signal-triggered branch/wait control. Target an enrollment by --enrollment or --lead (at least one is required); the server validates the signal name.")
|
|
@@ -16375,7 +16552,7 @@ Examples:
|
|
|
16375
16552
|
}));
|
|
16376
16553
|
}))
|
|
16377
16554
|
.addCommand(new Command("resume")
|
|
16378
|
-
.description("Resume a paused sequence.")
|
|
16555
|
+
.description("Resume a paused sequence. A campaign paused at its live send cap pauses again at once: raise the cap and resume in one step with `sequences start <sequence> --max-live-sends <n>` (preview, then --approved).")
|
|
16379
16556
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16380
16557
|
.option("--json", "Print a JSON envelope.")
|
|
16381
16558
|
.action(async (sequence, options) => {
|
|
@@ -16392,10 +16569,15 @@ Examples:
|
|
|
16392
16569
|
await handleAsyncAction("sequences delete", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}${options.approved ? "?approved=true" : ""}`, { method: "DELETE" }));
|
|
16393
16570
|
}))
|
|
16394
16571
|
.addCommand(new Command("enrollments")
|
|
16395
|
-
.description("List per-lead enrollment state plus the latest action status, error, attempts, and retry time. missing_source_row is true for a lead whose source-table row was deleted: still enrolled and contacted (remove with `contacts-remove --missing-source-rows`); null when the campaign has no live source table to check against, or the check failed.")
|
|
16572
|
+
.description("List per-lead enrollment state plus the latest action status, error, attempts, and retry time. --view compact answers 'when is each lead's next message due' per lead: lead, status, invite (accepted, pending, not_invited), accepted_at, current_step and next_due. next_due.summary says it in words, at_local/gives_up_at_local give the time in the campaign's timezone (its schedule's, else UTC) and at/gives_up_at in UTC. Reasons: step_scheduled (planned send time; held_by names what holds it, e.g. send_cap_reached), planner_due (when the planner next looks at the lead), not_started (begins when the campaign is started or resumed), nothing_scheduled (enrolled, no step will run), waiting_for_connection / waiting_for_signal (no send time; acceptance makes the next step due at once; gives_up_at is when the wait ends, and on_timeout says whether the lead then leaves or moves on), finished. A failed lead carries failure: the cause in words (cause_recorded false when OXYGEN never recorded one) and provider_contact (made; none = the step never started; nothing_sent; unknown). missing_source_row is true for a lead whose source-table row was deleted: still enrolled and contacted (remove with `contacts-remove --missing-source-rows`); null when the campaign has no live source table to check against, or the check failed. channels says which of the sequence's channels reach each lead: usable, and unavailable with a reason (no_linkedin_profile = enrolled for email only, its LinkedIn steps are skipped; no_recipient_email; no_phone_number).")
|
|
16396
16573
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16397
|
-
.option("--status <status>", "Filter by enrollment status (active, waiting_connection, replied, completed, stopped, failed).")
|
|
16574
|
+
.option("--status <status>", "Filter by enrollment status (pending, active, waiting_connection, waiting_signal, replied, completed, stopped, failed, excluded). pending = enrolled, not started (next_due not_started); waiting_connection = invite sent, not accepted yet. Status is the lead's progress, not its invite: use --invite accepted for the leads that accepted.")
|
|
16575
|
+
.option("--step <n>", "Only leads currently on this step (0-based index, as in the sequence's steps).")
|
|
16576
|
+
.option("--invite <state>", "Only leads in this LinkedIn invite state: accepted (invited and connected), pending (invited, not accepted yet) or not_invited (no invite: already connected before, suppressed or email-only).")
|
|
16577
|
+
.option("--view <view>", "full (default, every enrollment field) or compact (one short row per lead with next_due in words and local time).")
|
|
16578
|
+
.option("--order <order>", "created (default) or due: soonest next_due first, then waiting leads by gives_up_at, then leads with nothing due.")
|
|
16398
16579
|
.option("--limit <n>", "Maximum enrollments to return (1-500).")
|
|
16580
|
+
.option("--offset <n>", "Skip this many enrollments; pass next_offset from the previous page to continue.")
|
|
16399
16581
|
.option("--json", "Print a JSON envelope.")
|
|
16400
16582
|
.action(async (sequence, options) => {
|
|
16401
16583
|
await handleAsyncAction("sequences enrollments", options, () => {
|
|
@@ -16403,9 +16585,24 @@ Examples:
|
|
|
16403
16585
|
const status = readOption(options.status);
|
|
16404
16586
|
if (status)
|
|
16405
16587
|
params.set("status", status);
|
|
16588
|
+
const step = readOption(options.step);
|
|
16589
|
+
if (step)
|
|
16590
|
+
params.set("step", step);
|
|
16591
|
+
const invite = readOption(options.invite);
|
|
16592
|
+
if (invite)
|
|
16593
|
+
params.set("invite", invite);
|
|
16594
|
+
const view = readOption(options.view);
|
|
16595
|
+
if (view)
|
|
16596
|
+
params.set("view", view);
|
|
16597
|
+
const order = readOption(options.order);
|
|
16598
|
+
if (order)
|
|
16599
|
+
params.set("order", order);
|
|
16406
16600
|
const limit = readOption(options.limit);
|
|
16407
16601
|
if (limit)
|
|
16408
16602
|
params.set("limit", limit);
|
|
16603
|
+
const offset = readOption(options.offset);
|
|
16604
|
+
if (offset)
|
|
16605
|
+
params.set("offset", offset);
|
|
16409
16606
|
const suffix = params.toString();
|
|
16410
16607
|
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/enrollments${suffix ? `?${suffix}` : ""}`);
|
|
16411
16608
|
});
|
|
@@ -16581,7 +16778,7 @@ Examples:
|
|
|
16581
16778
|
});
|
|
16582
16779
|
}))
|
|
16583
16780
|
.addCommand(new Command("reconcile")
|
|
16584
|
-
.description("Safely reconcile one ambiguous LinkedIn invite/message from provider reads. Preview-only unless --approved; never resends.")
|
|
16781
|
+
.description("Safely reconcile one ambiguous LinkedIn invite/message from provider reads. Zero credits. Preview-only unless --approved; never resends.")
|
|
16585
16782
|
.argument("<sequence>", "Sequence id or slug. The sequence must be paused.")
|
|
16586
16783
|
.requiredOption("--action <id>", "Failed/canceled live action UUID to reconcile.")
|
|
16587
16784
|
.option("--approved", "Apply an exact provider match to local state. No provider write or resend occurs.")
|
|
@@ -17494,55 +17691,21 @@ Examples:
|
|
|
17494
17691
|
});
|
|
17495
17692
|
});
|
|
17496
17693
|
})))
|
|
17497
|
-
.addCommand(new Command("domains")
|
|
17498
|
-
.description("List the org's whole-domain email blocks, newest first. Filter by --reason (manual | complaint | policy) or --search (case-insensitive substring of the domain).")
|
|
17499
|
-
.option("--reason <reason>", "Filter by reason: manual, complaint, policy.")
|
|
17500
|
-
.option("--search <text>", "Case-insensitive substring match on the domain.")
|
|
17501
|
-
.option("--limit <n>", "Maximum domain blocks to return (1-500; default 100).")
|
|
17502
|
-
.option("--offset <n>", "Pagination offset (0-based).")
|
|
17694
|
+
.addCommand(addSuppressionListFlags(new Command("domains")
|
|
17695
|
+
.description("List the org's whole-domain email blocks, newest first. Filter by --reason (manual | complaint | policy) or --search (case-insensitive substring of the domain)."), "domains")
|
|
17503
17696
|
.option("--json", "Print a JSON envelope.")
|
|
17504
17697
|
.action(async (options) => {
|
|
17505
17698
|
await handleAsyncAction("suppressions domains", options, () => {
|
|
17506
|
-
const
|
|
17507
|
-
const reason = readOption(options.reason);
|
|
17508
|
-
if (reason)
|
|
17509
|
-
params.set("reason", reason);
|
|
17510
|
-
const search = readOption(options.search);
|
|
17511
|
-
if (search)
|
|
17512
|
-
params.set("search", search);
|
|
17513
|
-
const limit = readOption(options.limit);
|
|
17514
|
-
if (limit)
|
|
17515
|
-
params.set("limit", limit);
|
|
17516
|
-
const offset = readOption(options.offset);
|
|
17517
|
-
if (offset)
|
|
17518
|
-
params.set("offset", offset);
|
|
17519
|
-
const suffix = params.toString();
|
|
17699
|
+
const suffix = suppressionListQuery(options, "domains");
|
|
17520
17700
|
return requestOxygen(`/api/cli/suppressions/domains${suffix ? `?${suffix}` : ""}`);
|
|
17521
17701
|
});
|
|
17522
17702
|
}))
|
|
17523
|
-
.addCommand(new Command("addresses")
|
|
17524
|
-
.description("List the org's blocked email ADDRESSES, newest first (bounces, unsubscribes, and `suppressions import` land here). Filter by --reason (hard_bounce | complaint | unsubscribe | manual) or --search (case-insensitive substring of the address). Rows carry a CRM person name when one matches.")
|
|
17525
|
-
.option("--reason <reason>", "Filter by reason: hard_bounce, complaint, unsubscribe, manual.")
|
|
17526
|
-
.option("--search <text>", "Case-insensitive substring match on the address.")
|
|
17527
|
-
.option("--limit <n>", "Maximum addresses to return (1-500; default 100).")
|
|
17528
|
-
.option("--offset <n>", "Pagination offset (0-based).")
|
|
17703
|
+
.addCommand(addSuppressionListFlags(new Command("addresses")
|
|
17704
|
+
.description("List the org's blocked email ADDRESSES, newest first (bounces, unsubscribes, and `suppressions import` land here). Filter by --reason (hard_bounce | complaint | unsubscribe | manual) or --search (case-insensitive substring of the address). Rows carry a CRM person name when one matches."), "addresses")
|
|
17529
17705
|
.option("--json", "Print a JSON envelope.")
|
|
17530
17706
|
.action(async (options) => {
|
|
17531
17707
|
await handleAsyncAction("suppressions addresses", options, () => {
|
|
17532
|
-
const
|
|
17533
|
-
const reason = readOption(options.reason);
|
|
17534
|
-
if (reason)
|
|
17535
|
-
params.set("reason", reason);
|
|
17536
|
-
const search = readOption(options.search);
|
|
17537
|
-
if (search)
|
|
17538
|
-
params.set("search", search);
|
|
17539
|
-
const limit = readOption(options.limit);
|
|
17540
|
-
if (limit)
|
|
17541
|
-
params.set("limit", limit);
|
|
17542
|
-
const offset = readOption(options.offset);
|
|
17543
|
-
if (offset)
|
|
17544
|
-
params.set("offset", offset);
|
|
17545
|
-
const suffix = params.toString();
|
|
17708
|
+
const suffix = suppressionListQuery(options, "addresses");
|
|
17546
17709
|
return requestOxygen(`/api/cli/suppressions/addresses${suffix ? `?${suffix}` : ""}`);
|
|
17547
17710
|
});
|
|
17548
17711
|
}))
|
|
@@ -18262,7 +18425,7 @@ Examples:
|
|
|
18262
18425
|
: requestOxygen(`/api/cli/mailboxes/sync?${new URLSearchParams({ mailbox }).toString()}`));
|
|
18263
18426
|
}))
|
|
18264
18427
|
.addCommand(new Command("connect-oauth")
|
|
18265
|
-
.description("Connect Gmail/Microsoft 365 with --vendor composio to import all mail into Unibox and business activity into CRM.
|
|
18428
|
+
.description("Connect Gmail/Microsoft 365 with --vendor composio to import all mail into Unibox and business activity into CRM. The listed recurring rate is 100 credits per mailbox per month including history, sync and analysis; previews cost 0, with no extra execution fee. Before consent, inspect current monthly connection prices and first-debit timing with billing commitments --view connection-prices --json. Quote a mailbox count with billing estimate --mailboxes <n> --json; the managed preview shows the listed rate; an unavailable managed connection directs you to OXYGEN support. Managed OAuth requires provider rollout readiness and exact registered addresses, max 10. Use mailboxes sync for progress and pause/resume/disconnect. Composio uses individual owner consent only. Native --vendor oxygen retains its existing polling; full history/CRM requires managed reconnection. Warm-up retains its existing eligibility and pricing. Preview by default; pass --approved only after reviewing exact candidates. --vendor oxygen requires an exact address list and never discovers a domain directory. For --vendor oxygen only, Microsoft supports --authorization-mode tenant (one administrator approval, up to 500 exact selected addresses, recommended for dedicated sending tenants) or individual (one account sign-in per mailbox, max 10 per run, recommended for ordinary company/personal mailboxes). For Microsoft's built-in roles, tenant mode requires a Global Administrator or Privileged Role Administrator because it requests the Microsoft Graph application Mail.Send and Mail.Read permissions; Application Administrator and Cloud Application Administrator cannot consent to Microsoft Graph application roles. Microsoft's application grant is tenant-wide at the provider; OXYGEN—not Microsoft—enforces the selected-address boundary at verification and send time unless the tenant separately configures Exchange Application RBAC. For non-managed Microsoft mailboxes, native-send approval does not authorize OXYGEN Warm-up: every mailbox still needs its own `mailboxes warmup microsoft` Outlook consent before standalone enrollment. Connecting authorization does not change whether a mailbox is active or disabled. Google uses individual OAuth. Source tokens, passwords, authenticator seeds, and one-time codes never transfer. Managed authorization paths remain available through their vendor options.")
|
|
18266
18429
|
.addHelpText("after", "\nDocs: https://oxygen-agent.com/docs/providers/mailboxes\n")
|
|
18267
18430
|
.option("--provider <provider>", "Mailbox provider: google or microsoft.")
|
|
18268
18431
|
.option("--vendor <vendor>", "Authorization path: composio for customer-owned Gmail/Microsoft 365 with managed consent, history and CRM; oxygen for native/tenant consent; zapmail (default) or inboxkit for provisioned inboxes.")
|
|
@@ -18455,7 +18618,7 @@ Examples:
|
|
|
18455
18618
|
.option("--approved", "Execute the PRICED enrollment. Without it the response is a priced preview and nothing is enrolled or billed.")
|
|
18456
18619
|
.option("--plan <hash>", "Hash from the fresh preview (required with --approved).")
|
|
18457
18620
|
.option("--max-credits <n>", "Hard first-cycle credit cap from the preview (required with --approved).")
|
|
18458
|
-
.option("--ramp-weeks <n>", "
|
|
18621
|
+
.option("--ramp-weeks <n>", "Weeks the warm-up volume climb is spread over (default 4). A paid enrollment completes its ramp after 28 days with a provider-confirmed warm-up send, then keeps warming at 10 a day until you disable it.")
|
|
18459
18622
|
.option("--dry-run", "Simulate without calling the provider (mailboxes marked pending).")
|
|
18460
18623
|
.option("--json", "Print a JSON envelope.")
|
|
18461
18624
|
.action(async (options) => {
|
|
@@ -18492,6 +18655,29 @@ Examples:
|
|
|
18492
18655
|
},
|
|
18493
18656
|
});
|
|
18494
18657
|
});
|
|
18658
|
+
}))
|
|
18659
|
+
.addCommand(new Command("google")
|
|
18660
|
+
.description("Authorize one to ten connected Google inboxes for OXYGEN Warm-up. Separate from native sending: no passwords or OXYGEN tokens transfer. Preview costs 0 credits and creates no link. Approve its hash with --max-credits 0, open each consent_url as that exact inbox, then poll --status. Google shows the warm-up provider's consent application. Warmup stays off until a separately approved warmup enable; its fresh preview shows the price, including 0 extra credits when included in the mailbox connection.")
|
|
18661
|
+
.requiredOption("--mailboxes <list>", "Exact Google inbox ids or addresses, comma-separated; maximum ten.")
|
|
18662
|
+
.option("--approved", "Create the previewed authorization links.")
|
|
18663
|
+
.option("--plan <hash>", "Fresh preview hash; required with approval.")
|
|
18664
|
+
.option("--max-credits <n>", "Exactly 0; required with approval.")
|
|
18665
|
+
.option("--status", "Verify completed Google authorizations; never starts warmup.")
|
|
18666
|
+
.option("--json", "Print a JSON envelope.")
|
|
18667
|
+
.action(async (options) => {
|
|
18668
|
+
await handleAsyncAction("mailboxes warmup google", options, () => {
|
|
18669
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
18670
|
+
const planHash = readOption(options.plan);
|
|
18671
|
+
const maxCredits = options.maxCredits === undefined ? undefined : readNonNegativeNumber(options.maxCredits);
|
|
18672
|
+
if (options.approved && options.status)
|
|
18673
|
+
throw new Error("Status and approval cannot be combined.");
|
|
18674
|
+
if (options.approved && (!planHash || maxCredits !== 0))
|
|
18675
|
+
throw new Error("Approval requires a fresh --plan and --max-credits 0.");
|
|
18676
|
+
return requestOxygen("/api/cli/mailboxes/warmup/google", { method: "POST", body: {
|
|
18677
|
+
mailboxes, ...(options.approved ? { approved: true } : {}), ...(options.status ? { status: true } : {}),
|
|
18678
|
+
...(planHash ? { plan_hash: planHash } : {}), ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
18679
|
+
} });
|
|
18680
|
+
});
|
|
18495
18681
|
}))
|
|
18496
18682
|
.addCommand(new Command("microsoft")
|
|
18497
18683
|
.description("NON-INBOXKIT FALLBACK ONLY: authorize eligible Microsoft/Outlook mailboxes for OXYGEN Warm-up with ONE browser consent PER MAILBOX, never a password or app password. Do not run this for managed InboxKit mailboxes: fresh Microsoft/Azure orders activate native InboxKit warm-up exports automatically when the approved order includes warmup; opted-out or later separate managed enrollment uses standalone `warmup enable`. For the non-InboxKit fallback there is no tenant-admin shortcut: a 40-inbox Microsoft pool means 40 separate authorizations, each opened by whoever can sign in to that mailbox. The preview names the exact fallback mailboxes and outstanding links at 0 credits. Approve that fresh hash with --max-credits 0, open every returned consent_url, then re-run with --status. Warmup itself stays OFF until standalone `warmup enable`.")
|
|
@@ -18758,6 +18944,7 @@ Examples:
|
|
|
18758
18944
|
.addOption(new Option("--provider <provider>", "Health provider: emailguard, zapmail, or sendkit. Omit to auto-resolve EmailGuard/Zapmail. SendKit is never auto-selected and requires the configured dev canary sender.").choices(["emailguard", "zapmail", "sendkit"]))
|
|
18759
18945
|
.option("--subject <subject>", "Seed-message subject. Required with --provider sendkit; Zapmail ignores it beyond labeling the test.")
|
|
18760
18946
|
.option("--body <text>", "Plain-text seed-message body. Required with --provider sendkit; ignored by Zapmail.")
|
|
18947
|
+
.option("--allow-paused-probe", "Opt in to one diagnostic probe while preserving campaign pause; repeat at both create and send approvals. Never permits disabled senders.")
|
|
18761
18948
|
.option("--approved", "Actually run the test (otherwise a preview is returned).")
|
|
18762
18949
|
.option("--max-credits <n>", "Credit cap the caller accepts (must be >= credits_required for managed runs).")
|
|
18763
18950
|
.option("--plan <hash>", "plan_hash from a fresh create preview (required with --approved).")
|
|
@@ -18777,6 +18964,7 @@ Examples:
|
|
|
18777
18964
|
? { body_text: readOption(options.body) }
|
|
18778
18965
|
: {}),
|
|
18779
18966
|
...(options.approved ? { approved: true } : {}),
|
|
18967
|
+
...(options.allowPausedProbe ? { allow_paused_probe: true } : {}),
|
|
18780
18968
|
...(readOption(options.maxCredits)
|
|
18781
18969
|
? {
|
|
18782
18970
|
max_credits: Number(readOption(options.maxCredits)),
|
|
@@ -18791,6 +18979,7 @@ Examples:
|
|
|
18791
18979
|
.addCommand(new Command("send")
|
|
18792
18980
|
.description("Preview or send the exact EmailGuard seed message for a provider-created test. Preview returns sender, all recipients, exact content, and plan_hash. Live send requires --approved --plan <hash>, costs 0 additional credits, and sends one message only.")
|
|
18793
18981
|
.argument("<id>", "Oxygen placement-test id returned by `placement-test run`.")
|
|
18982
|
+
.option("--allow-paused-probe", "Opt in to one diagnostic probe while preserving campaign pause; repeat at both create and send approvals. Never permits disabled senders.")
|
|
18794
18983
|
.option("--approved", "Send the exact previewed seed message (otherwise preview only).")
|
|
18795
18984
|
.option("--plan <hash>", "plan_hash from a fresh send preview (required with --approved).")
|
|
18796
18985
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -18799,6 +18988,7 @@ Examples:
|
|
|
18799
18988
|
method: "POST",
|
|
18800
18989
|
body: {
|
|
18801
18990
|
...(options.approved ? { approved: true } : {}),
|
|
18991
|
+
...(options.allowPausedProbe ? { allow_paused_probe: true } : {}),
|
|
18802
18992
|
...(readOption(options.plan)
|
|
18803
18993
|
? { plan_hash: readOption(options.plan) }
|
|
18804
18994
|
: {}),
|
|
@@ -18825,11 +19015,11 @@ Examples:
|
|
|
18825
19015
|
program.addCommand(new Command("domains")
|
|
18826
19016
|
.description("Cold-email domains: sync zones from your own Cloudflare account (BYOK), inspect age/warmup/DNS health, check availability and pricing, buy domains, and archive or delete domains with their inboxes (OXYGEN-bought domains included). Your own Cloudflare account bills purchases to its payment method; domains registered by OXYGEN cost credits at the vendor price. Preview `domains buy` before approval.")
|
|
18827
19017
|
.addCommand(new Command("list")
|
|
18828
|
-
.description("List the org's cached Cloudflare domains with cold-email metadata (age, mailboxes, warmup, sending volume, DNS health). Each row's `dns` object explains the status (ok, issues, not checked, check failed, partial check, no zone) with a reason and fix path; `managed_domains` lists InboxKit-managed bundle domains (vendor-registered + vendor-DNS'd — manage via `managed-inboxes`), each carrying `internal_billing_status` (ok|past_due) and `last_billed_cycle_key` — a domain the vendor keeps live can still be one OXYGEN could not renew. Reads the cache only — run `domains sync` to refresh.")
|
|
19018
|
+
.description("List the org's cached Cloudflare domains with cold-email metadata (age, mailboxes, warmup, sending volume, DNS health). Each row's `dns` object explains the status (ok, issues, not checked, check failed, partial check, no zone) with a reason and fix path; `managed_domains` lists InboxKit-managed bundle domains (vendor-registered + vendor-DNS'd — manage via `managed-inboxes`), each carrying `internal_billing_status` (ok|past_due) and `last_billed_cycle_key` — a domain the vendor keeps live can still be one OXYGEN could not renew. summary.by_dns_state gives all DNS-state counts in one read. volume.outbound_30d counts outbound email messages in the synced inbox ledger, including mail outside Sequences. Reads the cache only — run `domains sync` to refresh.")
|
|
18829
19019
|
.option("--status <status>", "Filter by zone status: unknown, initializing, pending, active, moved, or deleted.")
|
|
18830
19020
|
.option("--registrar <registrar>", "Filter by registrar name.")
|
|
18831
19021
|
.option("--q <text>", "Filter by domain substring.")
|
|
18832
|
-
.option("--dns <state>", "Filter by
|
|
19022
|
+
.option("--dns <state>", "Filter by one derived DNS state: ok, issues, not_checked, check_failed, check_incomplete, or no_zone; summary.by_dns_state gives all state counts.")
|
|
18833
19023
|
.option("--expiring", "Only non-deleted domains expiring within 60 days.")
|
|
18834
19024
|
.option("--has-mailboxes <bool>", "true → only domains with sending mailboxes; false → only domains without.")
|
|
18835
19025
|
.option("--include-archived", "Include archived domains (hidden by default).")
|
|
@@ -19240,6 +19430,14 @@ Examples:
|
|
|
19240
19430
|
.action(async (callable, options) => {
|
|
19241
19431
|
await handleAsyncAction("callables disable", options, () => requestOxygen("/api/cli/callables/status", { method: "POST", body: { callable, status: "disabled" } }));
|
|
19242
19432
|
}));
|
|
19433
|
+
const workflowRunReadbackHelp = `
|
|
19434
|
+
Stored run values:
|
|
19435
|
+
oxygen workflows runs --run-id <run_id> --projection value --path /runs/0/output --json
|
|
19436
|
+
Canonical graph output is a steps map keyed by node id.
|
|
19437
|
+
Read the observed cost receipt with oxygen workflows run <run_id> --json.
|
|
19438
|
+
For large values, follow next_value_cursor with --value-cursor and the same selection.
|
|
19439
|
+
Append value_json fragments in offset order until next_value_cursor is null, then parse the combined JSON.
|
|
19440
|
+
`;
|
|
19243
19441
|
program
|
|
19244
19442
|
.command("workflows")
|
|
19245
19443
|
.description("Durable workflow automation commands.")
|
|
@@ -19269,8 +19467,10 @@ without --draft publishes the revision; calling or enabling it remains a separat
|
|
|
19269
19467
|
execution/authorization step.
|
|
19270
19468
|
|
|
19271
19469
|
Run completion:
|
|
19272
|
-
Calls enqueue asynchronously.
|
|
19470
|
+
Calls without --preview enqueue asynchronously. Use the returned data.run.id as <run_id>:
|
|
19273
19471
|
oxygen workflows tail <run_id>
|
|
19472
|
+
Preview calls return data.preview and create no run.
|
|
19473
|
+
${workflowRunReadbackHelp}
|
|
19274
19474
|
`)
|
|
19275
19475
|
.addCommand(new Command("templates")
|
|
19276
19476
|
.description("Deprecated aliases for `oxygen blueprints` — these route through the canonical blueprint surface (built-in seed templates plus this workspace's saved blueprints).")
|
|
@@ -19501,7 +19701,7 @@ Run completion:
|
|
|
19501
19701
|
});
|
|
19502
19702
|
}))
|
|
19503
19703
|
.addCommand(new Command("get")
|
|
19504
|
-
.description("Get one workflow automation, including any cron slowdown to your plan's minimum interval.")
|
|
19704
|
+
.description("Get one workflow automation, including any cron slowdown to your plan's minimum interval and `draftRevision` when a saved revision is newer than the published one. The top-level `status` is the live state; `manifest.workflow.status` is what the revision was authored with and does not change on enable or disable.")
|
|
19505
19705
|
.argument("<workflow>", "Workflow id, slug, or name.")
|
|
19506
19706
|
.option("--include-bundle", "Include durable recipe bundles in JSON output.")
|
|
19507
19707
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -19509,6 +19709,7 @@ Run completion:
|
|
|
19509
19709
|
await handleAsyncAction("workflows get", options, async () => {
|
|
19510
19710
|
const data = await requestOxygen("/api/cli/workflows/get", {
|
|
19511
19711
|
method: "POST",
|
|
19712
|
+
readOnly: true,
|
|
19512
19713
|
body: { workflow },
|
|
19513
19714
|
});
|
|
19514
19715
|
if (!options.json) {
|
|
@@ -19551,12 +19752,12 @@ Run completion:
|
|
|
19551
19752
|
}), options));
|
|
19552
19753
|
}))
|
|
19553
19754
|
.addCommand(new Command("call")
|
|
19554
|
-
.description("Run a workflow — or preview it first. `--preview` shows every step that leaves Oxygen, the accounts it would use, and what one run costs
|
|
19755
|
+
.description("Run a workflow — or preview it first. `--preview` returns data.preview without creating a run: it shows every step that leaves Oxygen, the accounts it would use, and what one run costs. Calls without --preview enqueue asynchronously without simulating the trigger; use the returned data.run.id as <run_id> in `oxygen workflows tail <run_id>`.")
|
|
19555
19756
|
.argument("[workflow]", "Workflow id, slug, or name.")
|
|
19556
19757
|
.option("--workflow <workflow>", "Workflow id, slug, or name.")
|
|
19557
19758
|
.option("--workflow-id <workflow_id>", "Workflow id or slug.")
|
|
19558
19759
|
.option("--workflow-name <workflow_name>", "Workflow name.")
|
|
19559
|
-
.option("--input-json <json>", "Workflow input object. Defaults to {}.")
|
|
19760
|
+
.option("--input-json <json>", "Workflow input object in every mode, including smoke-test. Defaults to {}.")
|
|
19560
19761
|
.option("--mode <mode>", "Execution mode. smoke-test/dry-run: 0 credits, no paid provider calls or external writes; internal reads still execute. live: may spend/write and requires approval + cap. Not needed with --preview, which runs nothing.")
|
|
19561
19762
|
.option("--idempotency-key <key>", "Optional idempotency key.")
|
|
19562
19763
|
.option("--max-credits <n>", "Required credit ceiling for live calls.")
|
|
@@ -19567,7 +19768,8 @@ Run completion:
|
|
|
19567
19768
|
.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.")
|
|
19568
19769
|
.option("--include-bundle", "Include durable recipe bundles in JSON output.")
|
|
19569
19770
|
.option("--json", "Print a JSON envelope.")
|
|
19570
|
-
.addHelpText("after", "\nSafety: smoke_test and dry_run share one boundary: 0 credits, no paid provider calls, no external writes. Oxygen internal reads use current workspace data, and oxygen.http_json_request may make a real outbound GET. Every mode creates an inspectable Workflow run record.\n\n--node scopes a test to one step. Oxygen plans the slice on the server from the exact saved version and records it on the run, so the run shows precisely which nodes were allowed to execute; you never submit a node list. Selecting the trigger, a disabled node, or a node unreachable from the trigger is refused with the reason. Only canonical graph workflows support it.\n")
|
|
19771
|
+
.addHelpText("after", "\nSafety: smoke_test and dry_run share one boundary: 0 credits, no paid provider calls, no external writes. Oxygen internal reads use current workspace data, and oxygen.http_json_request may make a real outbound GET. Every execution mode creates an inspectable Workflow run record; --preview creates none.\n\n--node scopes a test to one step. Oxygen plans the slice on the server from the exact saved version and records it on the run, so the run shows precisely which nodes were allowed to execute; you never submit a node list. Selecting the trigger, a disabled node, or a node unreachable from the trigger is refused with the reason. Only canonical graph workflows support it.\n")
|
|
19772
|
+
.addHelpText("after", workflowRunReadbackHelp)
|
|
19571
19773
|
.action(async (workflowArg, options) => {
|
|
19572
19774
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
19573
19775
|
const revisionVersion = readPositiveNumber(options.revision);
|
|
@@ -19581,15 +19783,18 @@ Run completion:
|
|
|
19581
19783
|
if (!options.preview && !readOption(options.mode)) {
|
|
19582
19784
|
throw new OxygenError("invalid_request", "--mode is required. Use smoke-test, dry-run, or live. To see what a live run would do without running it, use --preview — it runs nothing and needs no mode.", { exitCode: 2 });
|
|
19583
19785
|
}
|
|
19786
|
+
const idempotencyKey = readOption(options.idempotencyKey);
|
|
19584
19787
|
return prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/call", {
|
|
19585
19788
|
method: "POST",
|
|
19789
|
+
...(options.preview ? { previewOnly: true } : {}),
|
|
19790
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
19586
19791
|
body: {
|
|
19587
19792
|
workflow: readOption(workflowArg) ?? readOption(options.workflow),
|
|
19588
19793
|
...(readOption(options.workflowId) ? { workflow_id: readOption(options.workflowId) } : {}),
|
|
19589
19794
|
...(readOption(options.workflowName) ? { workflow_name: readOption(options.workflowName) } : {}),
|
|
19590
19795
|
input: options.inputJson ? parseJsonObject(options.inputJson) : {},
|
|
19591
19796
|
...(readOption(options.mode) ? { mode: readOption(options.mode) } : {}),
|
|
19592
|
-
...(
|
|
19797
|
+
...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
|
|
19593
19798
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
19594
19799
|
...(options.approved ? { approved: true } : {}),
|
|
19595
19800
|
...(revisionVersion !== undefined ? { revision_version: revisionVersion } : {}),
|
|
@@ -19601,7 +19806,7 @@ Run completion:
|
|
|
19601
19806
|
});
|
|
19602
19807
|
}))
|
|
19603
19808
|
.addCommand(new Command("webhooks")
|
|
19604
|
-
.description("Workflow webhook trigger utilities. A trigger's inbound URL and the deliveries a minute it accepts (`rate_limit`) are in `oxygen workflows get <workflow>`.")
|
|
19809
|
+
.description("Workflow webhook trigger utilities. A trigger's inbound URL and the deliveries a minute it accepts (`rate_limit`) are in `oxygen workflows get <workflow>`. Senders authenticate by sending the trigger's shared secret in the `x-oxygen-workflow-secret` header; a missing or wrong secret is recorded as a `rejected` delivery that starts no run and costs nothing.")
|
|
19605
19810
|
.addCommand(new Command("rotate")
|
|
19606
19811
|
.description("Replace a workflow webhook's shared secret. Previews by default; the old sender secret stops working immediately after approval.")
|
|
19607
19812
|
.argument("<workflow>", "Webhook workflow id, slug, or name.")
|
|
@@ -19609,6 +19814,7 @@ Run completion:
|
|
|
19609
19814
|
.option("--json", "Print a JSON envelope.")
|
|
19610
19815
|
.action((workflow, options) => handleAsyncAction("workflows webhooks rotate", options, () => requestOxygen("/api/cli/workflows/webhooks/rotate", {
|
|
19611
19816
|
method: "POST",
|
|
19817
|
+
previewOnly: !options.approved,
|
|
19612
19818
|
body: {
|
|
19613
19819
|
workflow,
|
|
19614
19820
|
...(options.approved ? { approved: true } : {}),
|
|
@@ -19627,9 +19833,9 @@ Run completion:
|
|
|
19627
19833
|
},
|
|
19628
19834
|
}))))
|
|
19629
19835
|
.addCommand(new Command("deliveries")
|
|
19630
|
-
.description("List inbound workflow webhook deliveries: what arrived, whether it verified, and the run it started. Replays never append here; only a verified outcome=ran delivery can be replayed.")
|
|
19836
|
+
.description("List inbound workflow webhook deliveries: what arrived, whether it verified, and the run it started. Stored headers never include `x-oxygen-workflow-secret`, so its absence does not show whether a rejected sender's secret was missing or wrong. Replays never append here; only a verified outcome=ran delivery can be replayed.")
|
|
19631
19837
|
.argument("[trigger_id]", "Optional webhook trigger name, such as lead-created.")
|
|
19632
|
-
.option("--workflow-id <id>", "Filter to deliveries that started
|
|
19838
|
+
.option("--workflow-id <id>", "Filter to this workflow's deliveries, including rejected and blocked ones that started no run.")
|
|
19633
19839
|
.option("--outcome <outcome>", "Filter by rejected (sender authentication failed), blocked (authenticated, no run), or ran.")
|
|
19634
19840
|
.option("--limit <n>", "Maximum deliveries to return. Defaults to 50.")
|
|
19635
19841
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -19835,6 +20041,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
19835
20041
|
.action(async (runId, options) => {
|
|
19836
20042
|
await handleAsyncAction("workflows run", options, async () => prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/run", {
|
|
19837
20043
|
method: "POST",
|
|
20044
|
+
readOnly: true,
|
|
19838
20045
|
body: { run_id: runId },
|
|
19839
20046
|
}), options));
|
|
19840
20047
|
}))
|
|
@@ -19850,6 +20057,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
19850
20057
|
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
19851
20058
|
return prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/retry", {
|
|
19852
20059
|
method: "POST",
|
|
20060
|
+
previewOnly: !options.approved,
|
|
19853
20061
|
body: {
|
|
19854
20062
|
run_id: runId,
|
|
19855
20063
|
...(options.approved ? { approved: true } : {}),
|
|
@@ -19860,11 +20068,12 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
19860
20068
|
}))
|
|
19861
20069
|
.addCommand(new Command("tail")
|
|
19862
20070
|
.description("Poll until a workflow finishes or parks in waiting/awaiting_approval; clock waits resume automatically.")
|
|
19863
|
-
.argument("<run_id>", "Workflow run UUID.")
|
|
20071
|
+
.argument("<run_id>", "Workflow run UUID returned as data.run.id by workflows call; distinct from data.run.runId.")
|
|
19864
20072
|
.option("--timeout-seconds <n>", "Maximum time to wait. Defaults to 600.")
|
|
19865
20073
|
.option("--interval-seconds <n>", "Polling interval. Defaults to 2.")
|
|
19866
20074
|
.option("--include-bundle", "Include durable recipe bundles in JSON output.")
|
|
19867
20075
|
.option("--json", "Print a JSON envelope.")
|
|
20076
|
+
.addHelpText("after", workflowRunReadbackHelp)
|
|
19868
20077
|
.action(async (runId, options) => {
|
|
19869
20078
|
await handleAsyncAction("workflows tail", options, async () => prepareWorkflowCliOutput(await waitForWorkflowRun(runId, options), options));
|
|
19870
20079
|
}))
|
|
@@ -19877,6 +20086,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
19877
20086
|
.action(async (runId, options) => {
|
|
19878
20087
|
await handleAsyncAction("workflows cancel", options, () => requestOxygen("/api/cli/workflows/cancel", {
|
|
19879
20088
|
method: "POST",
|
|
20089
|
+
...(options.preview ? { previewOnly: true } : {}),
|
|
19880
20090
|
body: {
|
|
19881
20091
|
run_id: runId,
|
|
19882
20092
|
...(options.preview ? { preview: true } : {}),
|
|
@@ -20016,6 +20226,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
20016
20226
|
const single = workflows.length === 1;
|
|
20017
20227
|
const data = await requestOxygen("/api/cli/workflows/delete", {
|
|
20018
20228
|
method: "POST",
|
|
20229
|
+
previewOnly: !options.yes,
|
|
20019
20230
|
body: {
|
|
20020
20231
|
...(single ? { workflow: workflows[0] } : { workflows }),
|
|
20021
20232
|
...(options.yes ? { approved: true } : {}),
|
|
@@ -20171,6 +20382,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
20171
20382
|
registerFunctionsCommands(program, handleAsyncAction);
|
|
20172
20383
|
registerUgcCommands(program, handleAsyncAction);
|
|
20173
20384
|
registerKnowledgeRepositoryCommands(program, handleAsyncAction);
|
|
20385
|
+
registerAdminLeadMagnetCommands(program, handleAsyncAction);
|
|
20174
20386
|
registerConnectorsCommands(program, handleAsyncAction);
|
|
20175
20387
|
registerSettingsCommands(program, handleAsyncAction);
|
|
20176
20388
|
registerCrmSupportFlags(program);
|
|
@@ -20748,6 +20960,7 @@ function waitForWorkflowRun(runId, options) {
|
|
|
20748
20960
|
fetchRun: async () => {
|
|
20749
20961
|
latestEnvelope = await requestOxygen("/api/cli/workflows/run", {
|
|
20750
20962
|
method: "POST",
|
|
20963
|
+
readOnly: true,
|
|
20751
20964
|
body: { run_id: runId },
|
|
20752
20965
|
});
|
|
20753
20966
|
return isRecord(latestEnvelope.run) ? latestEnvelope.run : latestEnvelope;
|
|
@@ -21056,8 +21269,17 @@ function waitForCopilotTurn(options) {
|
|
|
21056
21269
|
isTerminal: isTerminalCopilotTurnStatus,
|
|
21057
21270
|
shapeTerminal: () => latestData,
|
|
21058
21271
|
timeoutCode: "copilot_send_timeout",
|
|
21059
|
-
|
|
21272
|
+
// Only the CLI stops waiting. The turn keeps running on the server and keeps
|
|
21273
|
+
// billing until it finishes or is interrupted, so the timeout says so and names
|
|
21274
|
+
// the commands that read and stop it.
|
|
21275
|
+
timeoutMessage: "Stopped waiting for the copilot turn. It is still running and keeps spending credits until it finishes or you interrupt it.",
|
|
21060
21276
|
timeoutDetailIdKey: "turn_id",
|
|
21277
|
+
timeoutExtraDetails: () => ({
|
|
21278
|
+
session_id: options.sessionId,
|
|
21279
|
+
turn_still_running: true,
|
|
21280
|
+
next_step: `oxygen copilot get ${options.sessionId}`,
|
|
21281
|
+
interrupt_command: `oxygen copilot interrupt ${options.sessionId} ${options.turnId}`,
|
|
21282
|
+
}),
|
|
21061
21283
|
});
|
|
21062
21284
|
}
|
|
21063
21285
|
function workflowTemplateActionBody(templateId, options) {
|
|
@@ -26639,11 +26861,23 @@ function formatCrmObjects(data) {
|
|
|
26639
26861
|
// System attributes are bookkeeping, not fields anyone chose.
|
|
26640
26862
|
const fieldCount = attributes.filter((attribute) => attribute.attributeKind !== "system").length;
|
|
26641
26863
|
const primary = identities.find((identity) => identity.isPrimary === true) ?? identities[0];
|
|
26864
|
+
// A compact projection carries no attributes or identities (and an older
|
|
26865
|
+
// server no standardKey): print what it does carry, never "custom, 0
|
|
26866
|
+
// fields, can duplicate" for companies/people/deals.
|
|
26867
|
+
const compact = !Array.isArray(object.attributes);
|
|
26868
|
+
const kind = object.standardKey ? "standard" : object.standardKey === null || !compact ? "custom" : "—";
|
|
26869
|
+
let identity = describeCrmIdentity(primary, attributes);
|
|
26870
|
+
if (!Array.isArray(object.identities) && Object.hasOwn(object, "primaryIdentitySlug")) {
|
|
26871
|
+
identity = typeof object.primaryIdentitySlug === "string" ? object.primaryIdentitySlug : "nothing — can duplicate";
|
|
26872
|
+
}
|
|
26873
|
+
else if (compact && !Array.isArray(object.identities)) {
|
|
26874
|
+
identity = "—";
|
|
26875
|
+
}
|
|
26642
26876
|
return [
|
|
26643
26877
|
String(object.pluralName || object.displayName || object.slug || "—"),
|
|
26644
|
-
|
|
26645
|
-
String(fieldCount),
|
|
26646
|
-
|
|
26878
|
+
kind,
|
|
26879
|
+
compact ? "—" : String(fieldCount),
|
|
26880
|
+
identity,
|
|
26647
26881
|
String(object.slug ?? "—"),
|
|
26648
26882
|
];
|
|
26649
26883
|
});
|
|
@@ -29412,8 +29646,10 @@ async function pushOneMirrorPage(dir, state, candidate) {
|
|
|
29412
29646
|
slug,
|
|
29413
29647
|
kind,
|
|
29414
29648
|
status: "proposal_created",
|
|
29415
|
-
|
|
29416
|
-
...(typeof details.
|
|
29649
|
+
// The API sends snake_case details; camelCase never matched, so push hid the proposal.
|
|
29650
|
+
...(typeof details.proposal_id === "string" ? { proposal_id: details.proposal_id } : {}),
|
|
29651
|
+
...(typeof details.proposal_slug === "string" ? { proposal_slug: details.proposal_slug } : {}),
|
|
29652
|
+
...(typeof details.proposal_web_url === "string" ? { proposal_web_url: details.proposal_web_url } : {}),
|
|
29417
29653
|
reason: typeof details.reason === "string" ? details.reason : "sensitive_page",
|
|
29418
29654
|
};
|
|
29419
29655
|
}
|