@oxygen-agent/cli 1.1011.687 → 1.1011.767
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/column-run-notices.js +2 -0
- package/dist/help.d.ts +2 -0
- package/dist/help.js +5 -0
- package/dist/index.js +139 -91
- package/dist/workflow-run-list-flags.d.ts +3 -0
- package/dist/workflow-run-list-flags.js +21 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +42 -13
- package/node_modules/@oxygen/shared/dist/contracts/crm-enrichment-read.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +8 -6
- package/node_modules/@oxygen/shared/dist/contracts/schema.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/contracts/schema.js +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/sequence-analytics-read.d.ts +38 -0
- package/node_modules/@oxygen/shared/dist/contracts/sequence-analytics-read.js +22 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-list.d.ts +31 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-list.js +22 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-operations.d.ts +58 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-operations.js +30 -0
- package/node_modules/@oxygen/shared/dist/contracts/workflow-run-list.d.ts +79 -0
- package/node_modules/@oxygen/shared/dist/contracts/workflow-run-list.js +38 -0
- package/node_modules/@oxygen/shared/dist/copilot-journeys.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.js +2 -2
- package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.d.ts +3 -3
- package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.js +3 -3
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +24 -0
- package/node_modules/@oxygen/shared/dist/recipes.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/recipes.js +4 -9
- 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/workflows/dist/graph/expression.d.ts +1 -0
- package/node_modules/@oxygen/workflows/dist/graph/expression.js +15 -0
- package/node_modules/@oxygen/workflows/dist/graph/lint.js +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,9 +33,11 @@ export function formatAiPromptPreviewNotice(data) {
|
|
|
33
33
|
const state = isRecord(preview.state) && isRecord(preview.state.row)
|
|
34
34
|
? JSON.stringify(preview.state.row, null, 2)
|
|
35
35
|
: null;
|
|
36
|
+
const searchQuery = readRecordString(preview, "search_query");
|
|
36
37
|
return [
|
|
37
38
|
`prompt preview — row ${rowId} (${source}), before grounding${truncation}:`,
|
|
38
39
|
shown,
|
|
40
|
+
...(searchQuery ? [`web search query for this row: ${searchQuery}`] : []),
|
|
39
41
|
...(state ? [`row values sent separately in classification state:\n${state.slice(0, 4000)}${state.length > 4000 ? "… (truncated)" : ""}`] : []),
|
|
40
42
|
].join("\n");
|
|
41
43
|
}
|
package/dist/help.d.ts
CHANGED
|
@@ -55,5 +55,7 @@ export declare function commanderUsageFailure(program: Command, argv: readonly s
|
|
|
55
55
|
envelope: CliFailure;
|
|
56
56
|
exitCode: number;
|
|
57
57
|
} | null;
|
|
58
|
+
/** The command path argv resolves to, without the program name (`tables auto-run set`). */
|
|
59
|
+
export declare function commandNameForArgv(program: Command, argv: readonly string[]): string;
|
|
58
60
|
/** `--json` anywhere before a `--` terminator, where commander still reads options. */
|
|
59
61
|
export declare function argvRequestsJson(argv: readonly string[]): boolean;
|
package/dist/help.js
CHANGED
|
@@ -414,6 +414,11 @@ export function commanderUsageFailure(program, argv, error, hint) {
|
|
|
414
414
|
exitCode: exitCodeForErrorCode(code),
|
|
415
415
|
};
|
|
416
416
|
}
|
|
417
|
+
/** The command path argv resolves to, without the program name (`tables auto-run set`). */
|
|
418
|
+
export function commandNameForArgv(program, argv) {
|
|
419
|
+
const { path } = resolveCommandPath(program, argv);
|
|
420
|
+
return path.split(" ").slice(1).join(" ") || program.name();
|
|
421
|
+
}
|
|
417
422
|
/** `--json` anywhere before a `--` terminator, where commander still reads options. */
|
|
418
423
|
export function argvRequestsJson(argv) {
|
|
419
424
|
const end = argv.indexOf("--");
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,9 @@ import { WHATSAPP_ACCOUNT_FLAGS, whatsappAccountQuery } from "./whatsapp-account
|
|
|
9
9
|
import { BILLING_OWNER_READ_CONTRACT } from "@oxygen/shared/contracts/billing-owner-read";
|
|
10
10
|
import { publishingAmplificationListQueryContract, publishingAmplificationActionsQueryContract, publishingNotesQueryContract, publishingRevisionsQueryContract, publishingAnalyticsQueryContract, publishingReadQueryParams, } from "@oxygen/shared/contracts/publishing-reads";
|
|
11
11
|
import { COPILOT_SESSIONS_LIST_CONTRACT } from "@oxygen/shared/contracts/copilot-sessions";
|
|
12
|
+
import { SEQUENCE_ANALYTICS_READ_CONTRACT } from "@oxygen/shared/contracts/sequence-analytics-read";
|
|
13
|
+
import { WORKFLOW_RUN_LIST_CONTRACT, workflowRunListQueryParams } from "@oxygen/shared/contracts/workflow-run-list";
|
|
14
|
+
import { WORKFLOW_RUN_LIST_FLAG_OVERLAY, readWorkflowRunListFlags } from "./workflow-run-list-flags.js";
|
|
12
15
|
import { registerCrmSupportFlags, readCrmNoteEditFields, readCrmPhotoFields, readCrmFileLabel, buildCrmObjectsPath, buildCrmConnectionsPath } from "./crm-support-flags.js";
|
|
13
16
|
import { addToolCatalogSearchWindowFlag, readToolCatalogSearchWindow } from "./tool-catalog-search-window.js";
|
|
14
17
|
import { createToolCatalogDetailCommand } from "./tool-catalog-detail-command.js";
|
|
@@ -31,7 +34,7 @@ import { registerVisualCommands } from "./visual-commands.js";
|
|
|
31
34
|
import { renderPrimaryProviderBoard } from "./admin-primary-providers-render.js";
|
|
32
35
|
import { registerFunctionsCommands } from "./functions-commands.js";
|
|
33
36
|
import { STREAMED_FILE_IMPORT_MIN_BYTES, putImportFileStream, rowsForStreamedImportTarget, scanStreamedImportFile, shouldStreamFileImport, } from "./streamed-file-import.js";
|
|
34
|
-
import { applyOxygenHelp, argvRequestsJson, commanderUsageFailure, enableCommandSuggestions, unknownCommandHint, unknownOptionHint, withoutHelpForUnknownCommand, validateRequiredOptionValues, } from "./help.js";
|
|
37
|
+
import { applyOxygenHelp, argvRequestsJson, commanderUsageFailure, commandNameForArgv, enableCommandSuggestions, unknownCommandHint, unknownOptionHint, withoutHelpForUnknownCommand, validateRequiredOptionValues, } from "./help.js";
|
|
35
38
|
import { buildCommandManifest, getCommandManifestEntry, searchCommandManifest, suggestCommandNames, } from "./command-manifest.js";
|
|
36
39
|
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";
|
|
37
40
|
import { PURCHASABLE_PLAN_KEYS, planVolumeBonusPercent, resolveBasePricingPlan } from "@oxygen/shared/billing";
|
|
@@ -49,6 +52,7 @@ import { INTEGRATION_CONNECT_ACCOUNT_CONTRACT } from "@oxygen/shared/contracts/i
|
|
|
49
52
|
import { CRM_ENRICHMENT_READ_CONTRACT } from "@oxygen/shared/contracts/crm-enrichment-read";
|
|
50
53
|
import { INBOX_DRAFTS_READ_CONTRACT } from "@oxygen/shared/contracts/inbox-drafts-read";
|
|
51
54
|
import { SEQUENCE_LIST_PROJECTION_CONTRACT } from "@oxygen/shared/contracts/sequence-list";
|
|
55
|
+
import { TABLE_LIST_QUERY_CONTRACT, tableListQueryParams } from "@oxygen/shared/contracts/table-list";
|
|
52
56
|
import { buildSenderLimitsBody, parseWorkingDaysOption, SENDER_LIMITS_FLAG_OVERLAY } from "./sender-limits-flags.js";
|
|
53
57
|
import { readColumnDecisionFlags } from "./column-decision-options.js";
|
|
54
58
|
import { MANAGED_DATA_SUPPLIERS } from "@oxygen/shared/data-suppliers";
|
|
@@ -2036,6 +2040,7 @@ function buildCrmSetupBody(options) {
|
|
|
2036
2040
|
objects: readCrmSetupObjects(options.objects),
|
|
2037
2041
|
mode: resolveLiveDryRunMode(options),
|
|
2038
2042
|
...(options.withEnrichment === true ? { with_enrichment: true } : {}),
|
|
2043
|
+
...(options.withoutEnrichment === true ? { without_enrichment: true } : {}),
|
|
2039
2044
|
...(readOption(options.project) ? { project: readOption(options.project) } : {}),
|
|
2040
2045
|
};
|
|
2041
2046
|
}
|
|
@@ -5716,9 +5721,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5716
5721
|
.command("crm")
|
|
5717
5722
|
.description("Agent-native CRM object setup, records, automatic enrichment, and metadata commands. Who to contact first today: crm signals leads-today lists people ranked by their strongest intent signal in the last 7 days (website visit > profile view > post reaction > new follower), most recent first within a type.")
|
|
5718
5723
|
.addCommand(new Command("setup")
|
|
5719
|
-
.description("Create or repair standard CRM object-backed tables
|
|
5724
|
+
.description("Create or repair standard CRM object-backed tables; defaults to dry-run. A live run that creates the companies or people list also switches on automatic paid enrichment for every record added later: the dry run says what it costs, and --without-enrichment skips it.")
|
|
5720
5725
|
.option("--objects <objects>", "Comma-separated standard CRM objects to set up. Defaults to companies,people,deals.")
|
|
5721
5726
|
.option("--project <project>", "Project id or slug for created CRM tables.")
|
|
5727
|
+
.option("--without-enrichment", "Set up the lists without switching on automatic enrichment, not even on a freshly created table. Wins over --with-enrichment. Switch an enrichment on later with `oxygen crm enrichment enable`.")
|
|
5722
5728
|
.option("--with-enrichment", "Also arm the standing cheap-enrichment auto-run on objects that already existed (fresh-created tables arm it by default). New records then auto-enrich within the per-batch credit cap, whether they arrive by import, insert, upsert, or `crm assert`. If your plan runs fewer columns per record than the defaults, the columns left out are listed under autoRunColumnsOverLimit. Pick which enrichments run with `oxygen crm enrichment`.")
|
|
5723
5729
|
.option("--dry-run", "Preview CRM setup without creating or repairing tables.")
|
|
5724
5730
|
.option("--live", "Apply CRM setup changes. Default is dry-run.")
|
|
@@ -5877,7 +5883,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5877
5883
|
.description("Create or update one CRM record by object identity. Defaults to dry-run. A live assert fires the object's standing auto-run (automatic enrichment) for the written record, exactly like `tables insert|upsert|import` — so new records enrich hands-free within the per-batch credit cap. Inspect or change what runs with `oxygen crm enrichment list`.")
|
|
5878
5884
|
.argument("<object>", "CRM object slug, such as companies or people.")
|
|
5879
5885
|
.requiredOption("--identity <key=value>", "Identity key/value, for example domain=acme.com or email=ceo@acme.com.")
|
|
5880
|
-
.option("--values-json <json>", "JSON object of CRM attribute values keyed by column key. RELATIONSHIP columns (a deal's Associated Company and Associated People, a person's Company) are not set here
|
|
5886
|
+
.option("--values-json <json>", "JSON object of CRM attribute values keyed by column key. RELATIONSHIP columns (a deal's Associated Company and Associated People, a person's Company) are not set here; a value for one is left out and reported in `warnings`. Link by the target's identity with `company_domain` (people and deals) or `primary_contact_email` (deals), or attach an existing record with `oxygen crm relationships upsert`.")
|
|
5881
5887
|
.option("--dry-run", "Preview the assert without writing a row.")
|
|
5882
5888
|
.option("--live", "Apply the assert. Default is dry-run.")
|
|
5883
5889
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -7104,29 +7110,20 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7104
7110
|
.action(async (options) => {
|
|
7105
7111
|
await handleAsyncAction("tables import-bundle", options, () => importTableBundle(options, binaryName));
|
|
7106
7112
|
}))
|
|
7107
|
-
.addCommand(new Command("list")
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
+
.addCommand(addContractFlags(new Command("list").description("List workspace tables in the current tenant database."), TABLE_LIST_QUERY_CONTRACT, {
|
|
7114
|
+
project: { help: "Project id or slug to filter by." },
|
|
7115
|
+
tag: { help: "Only tables carrying this workspace tag (see `oxygen tags list`)." },
|
|
7116
|
+
include_archived: {
|
|
7117
|
+
help: "Also list archived, delete-scheduled and purging tables (the trash; each carries a lifecycle with purge_scheduled_at, and one with purgeStartedAt is being purged and can no longer be restored).",
|
|
7118
|
+
},
|
|
7119
|
+
limit: { help: "Maximum tables to list. Defaults to 200; hard cap is 1000.", valueName: "<n>" },
|
|
7120
|
+
})
|
|
7113
7121
|
.option("--all", "List every table instead of the newest window.")
|
|
7114
7122
|
.option("--json", "Print a JSON envelope.")
|
|
7115
|
-
.addHelpText("after", "\nLists the 200 newest tables by default
|
|
7123
|
+
.addHelpText("after", "\nLists the 200 newest tables by default. Follow nextCursor with --cursor and the same filters, or pass --all. --query searches names across every table; --projection summary keeps results compact. rowCount is null when not reconciled; measure unknown counts with `tables describe <table> --stats`.\n")
|
|
7116
7124
|
.action(async (options) => {
|
|
7117
7125
|
await handleAsyncAction("tables list", options, async () => {
|
|
7118
|
-
const params =
|
|
7119
|
-
const project = readOption(options.project);
|
|
7120
|
-
if (project)
|
|
7121
|
-
params.set("project", project);
|
|
7122
|
-
const tag = readOption(options.tag);
|
|
7123
|
-
if (tag)
|
|
7124
|
-
params.set("tag", tag);
|
|
7125
|
-
if (options.includeArchived)
|
|
7126
|
-
params.set("include_archived", "true");
|
|
7127
|
-
const limit = readPositiveInt(options.limit);
|
|
7128
|
-
if (limit !== undefined)
|
|
7129
|
-
params.set("limit", String(limit));
|
|
7126
|
+
const params = tableListQueryParams(readContractFlags(options, TABLE_LIST_QUERY_CONTRACT));
|
|
7130
7127
|
if (options.all)
|
|
7131
7128
|
params.set("all", "true");
|
|
7132
7129
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
@@ -7134,7 +7131,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7134
7131
|
// A capped list is where a blind agent filtered 200 rows locally to find
|
|
7135
7132
|
// one table by name (acceptance run, 2026-09-18); name the native lookup.
|
|
7136
7133
|
if (!options.json)
|
|
7137
|
-
writeListCapNotice(data, "table(s)", "
|
|
7134
|
+
writeListCapNotice(data, "table(s)", "continue with --cursor <nextCursor>, or find a name with --query <text>");
|
|
7138
7135
|
return data;
|
|
7139
7136
|
});
|
|
7140
7137
|
}))
|
|
@@ -7229,6 +7226,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7229
7226
|
.argument("<table>", "Table id or slug.")
|
|
7230
7227
|
.option("--include-archived", "Include archived columns, and read a table that is archived, scheduled for deletion or being purged.")
|
|
7231
7228
|
.option("--stats", "Include stats.rowCount (also summary.rowCount) and per-column fill rates. Row count is exact; above 100 rows fill rates use a 100-row sample (stats.statsSampled). For whole-table fill rates on a bigger list use `tables preview <table> --summary-only` \u2014 exact up to 20,000 rows, 2,000-row sample above. 0 credits.")
|
|
7229
|
+
.option("--column <key>", "Describe just this one column (its kind, definition and whether auto-run includes it) instead of every column.")
|
|
7232
7230
|
.option("--json", "Print a JSON envelope.")
|
|
7233
7231
|
.action(async (table, options) => {
|
|
7234
7232
|
await handleAsyncAction("tables describe", options, () => requestOxygen("/api/cli/tables/describe", {
|
|
@@ -7237,6 +7235,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7237
7235
|
table,
|
|
7238
7236
|
...(options.includeArchived ? { include_archived: true } : {}),
|
|
7239
7237
|
...(options.stats ? { include_stats: true } : {}),
|
|
7238
|
+
...(readOption(options.column) ? { column: readOption(options.column) } : {}),
|
|
7240
7239
|
},
|
|
7241
7240
|
}));
|
|
7242
7241
|
}))
|
|
@@ -9472,7 +9471,12 @@ Examples:
|
|
|
9472
9471
|
].join("\n"));
|
|
9473
9472
|
program
|
|
9474
9473
|
.command("recipes")
|
|
9475
|
-
|
|
9474
|
+
// Customers meet these as "plays" (the web app's Plays page and every
|
|
9475
|
+
// copied play prompt), so that is the noun their agent types first. A
|
|
9476
|
+
// production audit on 2026-10-04 saw all 33 blind runs open with
|
|
9477
|
+
// `oxygen plays` and lose a call to "unknown command".
|
|
9478
|
+
.alias("plays")
|
|
9479
|
+
.description("GTM plays: proven motions with prerequisites, credit posture, and approval gates spelled out, to borrow from and adapt. Alias: `plays`. To design a motion for your company, use the oxygen-gtm-consultant skill: oxygen skills get oxygen-gtm-consultant --json.")
|
|
9476
9480
|
.addCommand(new Command("list")
|
|
9477
9481
|
.description("Find a GTM play for a goal: recipes ranked by relevance to your words; --view summary for a compact index. Filter by business-case category, journey stage, or audience.")
|
|
9478
9482
|
.argument("[query]", "Your goal in plain words, e.g. \"more pipeline from linkedin\" (ranked against title, trigger phrases, tags, and business case).")
|
|
@@ -11049,13 +11053,13 @@ Examples:
|
|
|
11049
11053
|
.addCommand(new Command("search")
|
|
11050
11054
|
.description("Plan, dry-run, or queue provider-backed people/contact search.")
|
|
11051
11055
|
.addCommand(new Command("filters")
|
|
11052
|
-
.description("List Find people filter fields and options without a provider call: people.* (title, level, function, location, education) and company.* (industry, headcount, location, funding, up to 50 company LinkedIn URLs). Every enum field ships its full accepted-value list, so no value has to be guessed. Shape rule: only a field whose own key ends in .include or .exclude takes {include, exclude} — every other list field takes the strings directly.
|
|
11056
|
+
.description("List Find people filter fields and options without a provider call: people.* (title, level, function, location, education) and company.* (industry, headcount, location, funding, up to 50 company LinkedIn URLs). Every enum field ships its full accepted-value list, so no value has to be guessed. Shape rule: only a field whose own key ends in .include or .exclude takes {include, exclude} — every other list field takes the strings directly. For the companies in a table you already have, sample them free with people search preview --from-table <table> --linkedin-url-column <key>, then people search plan|run --from-table to search each company and write a company relation back.")
|
|
11053
11057
|
.option("--json", "Print a JSON envelope.")
|
|
11054
11058
|
.action(async (options) => {
|
|
11055
11059
|
await handleAsyncAction("people search filters", options, () => requestOxygen("/api/cli/people/search/preview"));
|
|
11056
11060
|
}))
|
|
11057
11061
|
.addCommand(new Command("preview")
|
|
11058
|
-
.description("Preview up to 50 people free, using native filters from people search filters.
|
|
11062
|
+
.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.")
|
|
11059
11063
|
.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.')
|
|
11060
11064
|
.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.")
|
|
11061
11065
|
.option("--linkedin-url-column <key>", "Column of --from-table that holds each company's LinkedIn URL.")
|
|
@@ -11389,7 +11393,7 @@ Examples:
|
|
|
11389
11393
|
}))
|
|
11390
11394
|
.addCommand(new Command("usage")
|
|
11391
11395
|
.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 Unipile 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.")
|
|
11392
|
-
.option("--meter <meter>", "credits, automation_actions, byok_calls, or provider_seats (rolling-30d peak connected accounts and
|
|
11396
|
+
.option("--meter <meter>", "credits, automation_actions, byok_calls, or provider_seats (rolling-30d peak connected accounts and the flat per-account charge; each account's reservation is listed by `billing commitments`). Defaults to credits.")
|
|
11393
11397
|
.option("--days <n>", "Lookback window in days. Defaults to 30.")
|
|
11394
11398
|
.option("--from <iso>", "Only include ledger events at or after this ISO timestamp.")
|
|
11395
11399
|
.option("--to <iso>", "Only include ledger events at or before this ISO timestamp.")
|
|
@@ -13670,7 +13674,7 @@ Examples:
|
|
|
13670
13674
|
await handleAsyncAction("senders checkpoints", options, () => requestOxygen("/api/cli/senders/checkpoints"));
|
|
13671
13675
|
}))
|
|
13672
13676
|
.addCommand(new Command("connect")
|
|
13673
|
-
.description("Get a Unipile hosted-auth URL to connect a new LinkedIn account (or reconnect with --reconnect). This command creates a pending provider authorization; it does not preview a price. Each connected LinkedIn account has one monthly charge, never two: an existing dollar seat until it ends at renewal, or a monthly credit reservation after seats retire.
|
|
13677
|
+
.description("Get a Unipile hosted-auth URL to connect a new LinkedIn account (or reconnect with --reconnect). This command creates a pending provider authorization; it does not preview a price. Each connected LinkedIn account has one monthly charge, never two: an existing dollar seat until it ends at renewal, or a monthly credit reservation after seats retire. Once the workspace is repriced that reservation is 3,400 credits ($34) monthly; a connect the balance cannot cover fails with insufficient_credits. Invites, messages and other LinkedIn actions cost 0 credits on top. `oxygen billing seats --json` reports the charge that applies to this workspace now, 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 inside Unipile's hosted wizard; their secrets never pass through OXYGEN. Use --count for bulk onboarding; --count above 1 needs --owner-consent (each owner connects their own account). Links are shareable and valid for 10 minutes.")
|
|
13674
13678
|
.option("--reconnect <connection_id>", "Reconnect an existing connection instead of creating a new one. Accepts a connection id.")
|
|
13675
13679
|
.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).")
|
|
13676
13680
|
.option("--sales-nav", "Request Classic + Sales Navigator access during Unipile hosted authentication.")
|
|
@@ -15857,35 +15861,39 @@ Examples:
|
|
|
15857
15861
|
});
|
|
15858
15862
|
});
|
|
15859
15863
|
})))
|
|
15860
|
-
.addCommand(new Command("analytics")
|
|
15864
|
+
.addCommand(addContractFlags(new Command("analytics")
|
|
15861
15865
|
.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.")
|
|
15862
|
-
.option("--
|
|
15863
|
-
.option("--
|
|
15864
|
-
|
|
15865
|
-
|
|
15866
|
-
|
|
15867
|
-
|
|
15866
|
+
.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, [])
|
|
15867
|
+
.option("--json", "Print a JSON envelope."), SEQUENCE_ANALYTICS_READ_CONTRACT, {
|
|
15868
|
+
range: { valueName: "<preset>", parse: readOption },
|
|
15869
|
+
from: { valueName: "<date>", parse: readOption }, to: { valueName: "<date>", parse: readOption },
|
|
15870
|
+
sequence: { valueName: "<sequence>", parse: readOption },
|
|
15871
|
+
campaigns: { valueName: "<ids>", parse: readOption }, senders: { valueName: "<ids>", parse: readOption },
|
|
15872
|
+
})
|
|
15868
15873
|
.action(async (options) => {
|
|
15869
|
-
|
|
15870
|
-
const
|
|
15871
|
-
|
|
15872
|
-
|
|
15873
|
-
|
|
15874
|
-
const
|
|
15875
|
-
const senders = (options.sender ?? []).flatMap((value) => readCsvOption(value));
|
|
15876
|
-
if (range)
|
|
15877
|
-
params.set("range", range);
|
|
15878
|
-
if (from)
|
|
15879
|
-
params.set("from", from);
|
|
15880
|
-
if (to)
|
|
15881
|
-
params.set("to", to);
|
|
15882
|
-
if (sequence)
|
|
15883
|
-
params.set("sequence", sequence);
|
|
15874
|
+
const fetchAnalytics = () => {
|
|
15875
|
+
const query = readContractFlags(options, SEQUENCE_ANALYTICS_READ_CONTRACT, {
|
|
15876
|
+
range: { parse: readOption }, from: { parse: readOption }, to: { parse: readOption },
|
|
15877
|
+
sequence: { parse: readOption }, campaigns: { parse: readOption }, senders: { parse: readOption },
|
|
15878
|
+
});
|
|
15879
|
+
const senders = [...readCsvOption(typeof query.senders === "string" ? query.senders : undefined), ...(options.sender ?? []).flatMap(value => readCsvOption(value))];
|
|
15884
15880
|
if (senders.length > 0)
|
|
15885
|
-
|
|
15881
|
+
query.senders = senders.join(",");
|
|
15882
|
+
else
|
|
15883
|
+
delete query.senders;
|
|
15884
|
+
const params = new URLSearchParams();
|
|
15885
|
+
for (const [key, value] of Object.entries(query)) {
|
|
15886
|
+
if (value !== undefined && value !== null)
|
|
15887
|
+
params.set(key, String(value));
|
|
15888
|
+
}
|
|
15886
15889
|
const qs = params.toString();
|
|
15887
15890
|
return requestOxygen(`/api/cli/sequences/analytics${qs ? `?${qs}` : ""}`);
|
|
15888
|
-
}
|
|
15891
|
+
};
|
|
15892
|
+
const windowRequested = ["projection", "path", "valueCursor", "maxChars"].some(key => options[key] !== undefined);
|
|
15893
|
+
if (windowRequested)
|
|
15894
|
+
await handleAsyncAction("sequences analytics", options, fetchAnalytics);
|
|
15895
|
+
else
|
|
15896
|
+
await handleReadActionWithLens("sequences analytics", options, fetchAnalytics, formatSequenceAnalyticsHealth);
|
|
15889
15897
|
}))
|
|
15890
15898
|
.addCommand(new Command("create")
|
|
15891
15899
|
.description("Create a draft multichannel sequence from a steps JSON file. Supports LinkedIn, email, WhatsApp, human call_task, and connected-CRM crm_task journeys. Assign LinkedIn senders with --senders (optional at create — a draft can sit senderless, but enroll/start require at least one for LinkedIn journeys). Native email sends through connected Google/Microsoft mailboxes, including Microsoft SMTP transport; optional --email-* flags bind an Instantly campaign. Install/read the oxygen-sequencer skill for per-step senders (references/email.md, references/linkedin.md), the launch ladder and complete mapped CRM task examples (references/calls-and-crm-tasks.md).")
|
|
@@ -15895,6 +15903,7 @@ Examples:
|
|
|
15895
15903
|
.requiredOption("--steps-file <path>", "Path to a JSON file: { \"steps\": [...] }. LinkedIn steps (visit_profile | invite | wait_for_connection | message | inmail | follow | like_post | comment_post | withdraw_invite), email steps (email_send | email_enroll | email_move | email_stop), WhatsApp (whatsapp_message), human call tasks (call_task), connected-CRM tasks (crm_task; channel crm; exact identity record_mappings + explicit create/update policy + record_links + dynamic task property_mappings/associations), and control steps (wait | wait_for_signal | branch | stop), each with an `id`. Copy templates support {{column}} interpolation from the lead's row, {{column|fallback}} inline fallbacks, deterministic spintax — {{RANDOM|a|b|c}} or industry-standard bare {Hi|Hey|Hello}, nestable like {Would {Tuesday|Thursday} work|next week?} — and {% if column %}…{% endif %} conditionals; a bare {…} region is spintax only when it contains a top-level |, so literal braces (CSS/JSON) pass through. Every step's copy can also use {{sender_first_name}}, {{sender_last_name}} and {{sender_full_name}} (alias {{sender_name}}): the name of the person who sends that step (their sender profile, else the account or inbox display name), with no table column needed; email steps add {{sender_email}} (the from address). A row column of the same name WINS on collision, and a missing name renders empty. comment_post takes text_template and/or ai_prompt — a KG-grounded comment generated at send time (a paid AI call) that falls back to text_template if generation fails. A `branch` routes on signals (then/else) or the legacy connection_accepted/already_connected/open_profile sugar (then_id/else_id). A signal condition can also branch on the LEAD'S DATA: a data leaf { has_column: \"email\" } is true when that row_values column has a non-empty value (add present:false for \"missing\") — e.g. route leads that have an email down an email arm and the rest down a LinkedIn arm. MINIMAL EMAIL STEP SHAPE: { \"id\": \"s1\", \"channel\": \"email\", \"kind\": \"email_send\", \"subject_template\": \"...\", \"body_template\": \"...\" } — body_template is REQUIRED on email_send; subject_template is OPTIONAL and is the one control that decides threading: give a step its own subject and it goes out as a NEW email, omit it and the step continues the lead's previous email in the same thread, going out as \"Re: <the previous email's subject>\" (what the retired email_reply kind used to be — still accepted on input, rewritten into this shape, and reported under `warnings` in the response). The FIRST email step of a sequence must carry a subject, since it has no earlier thread to continue. MINIMAL WHATSAPP STEP SHAPE: { \"id\": \"s1\", \"channel\": \"whatsapp\", \"kind\": \"whatsapp_message\", \"template\": \"Hi {{first_name|there}} …\" } — template is REQUIRED, and a live WhatsApp start also needs the sequence's --whatsapp-cold-initiate opt-in. MINIMAL WAIT STEP SHAPE: { \"id\": \"w1\", \"kind\": \"wait\", \"days\": 3 } — days and/or hours, at least 1 hour in total. A/B tests use an explicit `variants` array of copy partials on the step (up to 25 alternates, a–z; spintax varies wording INSIDE one variant and is not A/B-tracked). For the complete replay-safe crm_task upsert mapping shape, read `oxygen skills get oxygen-sequencer --file references/calls-and-crm-tasks.md`.")
|
|
15896
15904
|
.option("--channels <list>", "Comma-separated channels: linkedin,email,whatsapp,call,crm. Defaults to the channels the journey touches. crm_task currently supports HubSpot.")
|
|
15897
15905
|
.option("--whatsapp-cold-initiate", "WhatsApp: allow cold-initiating new chats (no prior conversation). Required to start a WhatsApp sequence live — WhatsApp via Unipile is unofficial WhatsApp Web, so cold-initiating is an explicit ban-risk opt-in.")
|
|
15906
|
+
.addOption(new Option("--whatsapp-duplicate-scope <scope>", "Prevent duplicate WhatsApp copy in this campaign (default) or all campaigns.").choices(["campaign", "workspace"]))
|
|
15898
15907
|
.option("--phone-column-key <key>", "WhatsApp: row_values key holding each lead's phone number (else falls back to phone/phone_number/mobile).")
|
|
15899
15908
|
.option("--senders <ids>", "Comma-separated LinkedIn or WhatsApp sender account ids (or connection / Unipile ids; `senders list` / `whatsapp accounts` list them). Optional at create; enroll and start require at least one when the journey has LinkedIn or WhatsApp steps (attach later with `sequences update --senders`).")
|
|
15900
15909
|
.option("--table <id>", "Source table id or slug whose rows supply {{column}} template values.")
|
|
@@ -16015,6 +16024,7 @@ Examples:
|
|
|
16015
16024
|
.option("--steps-file <path>", "Draft only: path to a JSON file: { \"steps\": [...] } replacing the journey. Copy supports {{column}} interpolation; every step also takes {{sender_first_name}} / {{sender_last_name}} / {{sender_full_name}} for the person who sends it, and email adds {{sender_email}} (a same-named row column wins). A `branch` can also route on the lead's data with a data leaf { has_column: \"email\" } (true when that row_values column is non-empty; present:false for \"missing\").")
|
|
16016
16025
|
.option("--channels <list>", "Draft only: comma-separated channels: linkedin,email,whatsapp,call,crm.")
|
|
16017
16026
|
.option("--whatsapp-cold-initiate", "WhatsApp: allow cold-initiating new chats (no prior conversation). Required to start a WhatsApp sequence live (draft-editable).")
|
|
16027
|
+
.addOption(new Option("--whatsapp-duplicate-scope <scope>", "Prevent duplicate WhatsApp copy in this campaign (default) or all campaigns.").choices(["campaign", "workspace"]))
|
|
16018
16028
|
.option("--phone-column-key <key>", "WhatsApp: row_values key holding each lead's phone number (else falls back to phone/phone_number/mobile).")
|
|
16019
16029
|
.option("--email-column-key <key>", "Native email: row_values key holding each lead's email (else falls back to email/email_address/work_email). Maps a non-English header (e.g. E-Mail).")
|
|
16020
16030
|
.option("--linkedin-url-column-key <key>", "LinkedIn: row_values key holding each lead's profile URL (else falls back to linkedin_url/linkedinUrl/linkedin/profile_url). \"\" clears it back to auto-detect.")
|
|
@@ -16136,7 +16146,7 @@ Examples:
|
|
|
16136
16146
|
await handleAsyncAction("sequences get", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}${options.readiness ? "?include=readiness" : ""}`));
|
|
16137
16147
|
}))
|
|
16138
16148
|
.addCommand(new Command("enroll")
|
|
16139
|
-
.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). 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`.")
|
|
16149
|
+
.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`.")
|
|
16140
16150
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16141
16151
|
.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.")
|
|
16142
16152
|
.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.")
|
|
@@ -17565,9 +17575,12 @@ Examples:
|
|
|
17565
17575
|
.option("--locals <list>", "Comma-separated local parts to create for --sender, e.g. `--locals talia,talia.rosen,t.rosen` on send.acme.com orders talia@, talia.rosen@, and t.rosen@ — all owned by that one person. Requires --sender, because a bare local part carries no name and the vendor's stamp is permanent.")
|
|
17566
17576
|
.option("--mailboxes <json>", "JSON array of mailboxes: [{\"username\",\"first_name\",\"last_name\",\"profile_picture_url\"}], or [{\"username\",\"sender_profile_id\"}] to take the identity from a sender profile per item (mix freely). profile_picture_url is optional and must be a PUBLIC, PERMANENT https URL — the vendor fetches it server-side at provisioning, so an expiring CDN link (e.g. a media.licdn.com URL with e=<epoch>) ships the mailbox faceless. Use `oxygen managed-inboxes upload-avatar <path>` to host one, or --sender / sender_profile_id, whose photo Oxygen already mirrors.")
|
|
17567
17577
|
.option("--file <path>", "Path to a JSON file { \"mailboxes\": [...] } (alternative to --mailboxes).")
|
|
17568
|
-
.addOption(new Option("--preview", "Explicit read-only quote: connects nothing, buys nothing and charges nothing. Cannot be combined with
|
|
17578
|
+
.addOption(new Option("--preview", "Explicit read-only quote: connects nothing, buys nothing and charges nothing. Cannot be combined with purchase or DNS cleanup approval.").conflicts(["approved", "dnsCleanupApproved"]))
|
|
17569
17579
|
.option("--existing-domain", "Use an existing Cloudflare domain from oxygen domains list; never register or transfer it.")
|
|
17570
17580
|
.option("--max-per-domain <n>", "Save a stricter mailbox limit with the approved order, including existing addresses (e.g. 2). Preview does not save it.")
|
|
17581
|
+
.option("--dns-cleanup-records <ids>", "Preview deletion of these comma-separated exact email DNS record IDs on an existing domain; connects and buys nothing.")
|
|
17582
|
+
.addOption(new Option("--dns-cleanup-approved", "Delete only the reviewed email records; requires --dns-cleanup-records and --dns-cleanup-hash. Request a fresh purchase quote afterward.").conflicts("approved"))
|
|
17583
|
+
.option("--dns-cleanup-hash <hash>", "The plan_hash from the exact DNS cleanup preview, required to delete records.")
|
|
17571
17584
|
.option("--years <n>", "Years to register a new domain for (1-10). Omit for existing domains.")
|
|
17572
17585
|
.option("--redirect-url <url>", "Where the domain's web root redirects. Defaults to your workspace's company website; editable later with `oxygen domains forwarding set`.")
|
|
17573
17586
|
.option("--billing <path>", "Path to a JSON file with the registrant/WHOIS contact (first_name, last_name, phone, country, city, state, address_line_one, postal_code). Required on your FIRST order; later orders reuse the registrant already on file.")
|
|
@@ -17597,6 +17610,13 @@ Examples:
|
|
|
17597
17610
|
const billingPath = readOption(options.billing);
|
|
17598
17611
|
const billing = billingPath ? readJsonFileValue(resolve(billingPath), "--billing") : undefined;
|
|
17599
17612
|
const quote = readOption(options.quote);
|
|
17613
|
+
const cleanupRecords = readOption(options.dnsCleanupRecords);
|
|
17614
|
+
const cleanupHash = readOption(options.dnsCleanupHash);
|
|
17615
|
+
if ((cleanupRecords && !options.existingDomain)
|
|
17616
|
+
|| ((options.dnsCleanupApproved || cleanupHash) && !cleanupRecords)
|
|
17617
|
+
|| (options.dnsCleanupApproved && !cleanupHash)) {
|
|
17618
|
+
throw new OxygenError("invalid_request", "DNS cleanup requires --existing-domain and --dns-cleanup-records; deletion also requires its fresh --dns-cleanup-hash.", { exitCode: 2 });
|
|
17619
|
+
}
|
|
17600
17620
|
return requestOxygen("/api/cli/managed-inboxes/subscribe", {
|
|
17601
17621
|
method: "POST",
|
|
17602
17622
|
body: {
|
|
@@ -17607,6 +17627,11 @@ Examples:
|
|
|
17607
17627
|
...(years ? { years: Number(years) } : {}),
|
|
17608
17628
|
...(options.existingDomain ? { existing_domain: true } : {}),
|
|
17609
17629
|
...(options.maxPerDomain !== undefined ? { max_mailboxes_per_domain: Number(options.maxPerDomain) } : {}),
|
|
17630
|
+
...(cleanupRecords ? { dns_cleanup: {
|
|
17631
|
+
record_ids: cleanupRecords.split(",").map((id) => id.trim()),
|
|
17632
|
+
approved: options.dnsCleanupApproved === true,
|
|
17633
|
+
...(cleanupHash ? { plan_hash: cleanupHash } : {}),
|
|
17634
|
+
} } : {}),
|
|
17610
17635
|
...(redirectUrl ? { redirect_url: redirectUrl } : {}),
|
|
17611
17636
|
...(billing ? { billing } : {}),
|
|
17612
17637
|
// Warm-up is on unless --no-warmup. Placement is opt-in: it is sent only
|
|
@@ -19666,34 +19691,21 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
19666
19691
|
}), options);
|
|
19667
19692
|
});
|
|
19668
19693
|
})))
|
|
19669
|
-
.addCommand(new Command("runs")
|
|
19670
|
-
.description("List workflow runs.")
|
|
19671
|
-
.option("--workflow <workflow>", "Workflow id, slug, or name.")
|
|
19672
|
-
.option("--workflow-id <workflow_id>", "Workflow id or slug.")
|
|
19673
|
-
.option("--status <status>", "Filter by queued, running, waiting, awaiting_approval, completed, failed, canceling, or canceled.")
|
|
19674
|
-
.option("--mode <mode>", "Filter by execution mode: smoke_test, dry_run, or live.")
|
|
19675
|
-
.option("--cursor <cursor>", "Continue from a previous page. Pass the nextCursor the last response returned.")
|
|
19676
|
-
.option("--limit <n>", "Maximum runs to return. Defaults to 50.")
|
|
19694
|
+
.addCommand(addContractFlags(new Command("runs")
|
|
19695
|
+
.description("List stored workflow runs. Optional windows preserve complete public values without executing a workflow."), WORKFLOW_RUN_LIST_CONTRACT, WORKFLOW_RUN_LIST_FLAG_OVERLAY)
|
|
19677
19696
|
.option("--include-bundle", "Include durable recipe bundles in JSON output.")
|
|
19678
19697
|
.option("--json", "Print a JSON envelope.")
|
|
19679
|
-
.addHelpText("after", "\
|
|
19698
|
+
.addHelpText("after", "\nFor complete bounded history use --projection summary/full; echo nextCursor with the same filters. Unqualified legacy reads issue a continuation only with an explicit --limit. Value windows require --run-id and reauthorize the exact filtered public row.\n")
|
|
19680
19699
|
.action(async (options) => {
|
|
19681
19700
|
await handleAsyncAction("workflows runs", options, async () => {
|
|
19682
|
-
const
|
|
19683
|
-
const
|
|
19684
|
-
if (workflow)
|
|
19685
|
-
query.set("workflow", workflow);
|
|
19686
|
-
if (readOption(options.status))
|
|
19687
|
-
query.set("status", readOption(options.status) ?? "");
|
|
19688
|
-
if (readOption(options.mode))
|
|
19689
|
-
query.set("mode", readOption(options.mode) ?? "");
|
|
19690
|
-
if (readOption(options.cursor))
|
|
19691
|
-
query.set("cursor", readOption(options.cursor) ?? "");
|
|
19692
|
-
const limit = readPositiveInt(options.limit);
|
|
19693
|
-
if (limit)
|
|
19694
|
-
query.set("limit", String(limit));
|
|
19701
|
+
const args = readWorkflowRunListFlags(options);
|
|
19702
|
+
const query = workflowRunListQueryParams(args);
|
|
19695
19703
|
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
19696
|
-
|
|
19704
|
+
const data = await requestOxygen(`/api/cli/workflows/runs${suffix}`);
|
|
19705
|
+
// Opt-in windows are lossless public native receipts. Legacy bundle
|
|
19706
|
+
// stripping/error aliases still apply only to the unqualified read.
|
|
19707
|
+
return ["projection", "path", "value_cursor", "max_chars"].some(key => args[key] !== undefined)
|
|
19708
|
+
? data : prepareWorkflowCliOutput(data, options);
|
|
19697
19709
|
});
|
|
19698
19710
|
}))
|
|
19699
19711
|
.addCommand(new Command("failures")
|
|
@@ -22776,8 +22788,23 @@ function assertNotPeopleSearchKind(kind) {
|
|
|
22776
22788
|
});
|
|
22777
22789
|
}
|
|
22778
22790
|
}
|
|
22791
|
+
// The id a `search plan` envelope prints (search_plan_…, company_search_plan_…,
|
|
22792
|
+
// signal_search_plan_…). `--plan` takes the plan itself, not its id; a blind
|
|
22793
|
+
// acceptance run on 2026-10-04 passed the id and got only "Input must be valid
|
|
22794
|
+
// JSON" with no way forward.
|
|
22795
|
+
const SEARCH_PLAN_ID_PATTERN = /^[a-z_]*search_plan_[A-Za-z0-9]+$/;
|
|
22779
22796
|
function readSearchPlanJson(value) {
|
|
22780
|
-
const
|
|
22797
|
+
const raw = readFileIfPresent(value);
|
|
22798
|
+
if (raw === value && SEARCH_PLAN_ID_PATTERN.test(value.trim())) {
|
|
22799
|
+
throw new OxygenError("invalid_json", `--plan takes the plan JSON returned by the plan command, not its id ${value.trim()}.`, {
|
|
22800
|
+
details: {
|
|
22801
|
+
plan_id: value.trim(),
|
|
22802
|
+
next_step: "Re-run the plan command with --json and save its output to a file (for example plan.json), then pass --plan plan.json.",
|
|
22803
|
+
},
|
|
22804
|
+
exitCode: 1,
|
|
22805
|
+
});
|
|
22806
|
+
}
|
|
22807
|
+
const parsed = parseJsonObject(raw);
|
|
22781
22808
|
const data = parsed.data;
|
|
22782
22809
|
return data && typeof data === "object" && !Array.isArray(data)
|
|
22783
22810
|
? data
|
|
@@ -22785,7 +22812,7 @@ function readSearchPlanJson(value) {
|
|
|
22785
22812
|
}
|
|
22786
22813
|
// Table-driven Employee Finder help, written once and shared by `people search
|
|
22787
22814
|
// plan` and `people search run` so the price shape reads the same on both.
|
|
22788
|
-
const PEOPLE_SEARCH_FROM_TABLE_HELP = "Search every company LinkedIn URL in a table column for the people you describe — --titles, --exclude-titles, --seniorities, --job-functions, --education, location and --min-connections all apply per company. 1 credit per company per page; rows land in a new people table with a company relation back to each source row. Pass the table id or slug plus --linkedin-url-column, and bound it with --company-limit. For up to 50 companies this is the expensive way: people search preview
|
|
22815
|
+
const PEOPLE_SEARCH_FROM_TABLE_HELP = "Search every company LinkedIn URL in a table column for the people you describe — --titles, --exclude-titles, --seniorities, --job-functions, --education, location and --min-connections all apply per company. 1 credit per company per page; rows land in a new people table with a company relation back to each source row. Pass the table id or slug plus --linkedin-url-column, and bound it with --company-limit. For up to 50 companies this is the expensive way: people search preview --from-table samples them free and people search run --source people_search charges one page across all of them.";
|
|
22789
22816
|
const PEOPLE_SEARCH_LINKEDIN_COLUMN_HELP = "Column key in --from-table holding each company's LinkedIn URL.";
|
|
22790
22817
|
const PEOPLE_SEARCH_COMPANY_OFFSET_HELP = "Skip this many companies in --from-table. Use it to source the rest after a first-company preview.";
|
|
22791
22818
|
const PEOPLE_SEARCH_COMPANY_LIMIT_HELP = "Fetch people for at most this many companies from --from-table. Start with --company-limit 1.";
|
|
@@ -27798,7 +27825,7 @@ function formatCrmEnrichment(data) {
|
|
|
27798
27825
|
return lines.join("\n");
|
|
27799
27826
|
}
|
|
27800
27827
|
function pushCrmEnrichmentObject(lines, styles, view) {
|
|
27801
|
-
lines.push("", `${styles.bold(`Automatic enrichment — ${view.object}`)} ${view
|
|
27828
|
+
lines.push("", `${styles.bold(`Automatic enrichment — ${view.object}`)} ${formatCrmEnrichmentState(view, styles)}`, ` ${styles.dim("Standing permission: records written to this object enrich automatically, with no per-record approval, bounded by the per-batch ceiling below.")}`, "");
|
|
27802
27829
|
const labelWidth = Math.max(...view.presets.map((preset) => preset.label.length), 0);
|
|
27803
27830
|
const armed = new Set(view.presets.filter((preset) => preset.enabled).map((preset) => preset.id));
|
|
27804
27831
|
for (const preset of view.presets) {
|
|
@@ -27820,7 +27847,10 @@ function pushCrmEnrichmentObject(lines, styles, view) {
|
|
|
27820
27847
|
if (view.custom_columns.length > 0) {
|
|
27821
27848
|
// A hand-tuned config carries columns no preset owns, so the per-record
|
|
27822
27849
|
// figure above can be a floor rather than the bill.
|
|
27823
|
-
|
|
27850
|
+
// Uncosted only when one of them may bill; a formula needs no run and is free.
|
|
27851
|
+
const note = view.custom_columns_uncosted
|
|
27852
|
+
? ` ${styles.yellow("(cost not included above)")}`
|
|
27853
|
+
: ` ${styles.dim("(free: no run needed)")}`;
|
|
27824
27854
|
lines.push(` ${styles.dim("Extra".padEnd(summaryWidth))} ${view.custom_columns.join(", ")}${note}`);
|
|
27825
27855
|
}
|
|
27826
27856
|
// The half the config alone cannot show: an armed set says nothing about the
|
|
@@ -27831,12 +27861,6 @@ function pushCrmEnrichmentObject(lines, styles, view) {
|
|
|
27831
27861
|
if (backfillLine) {
|
|
27832
27862
|
lines.push(` ${styles.dim("Not enriched".padEnd(summaryWidth))} ${backfillLine}`);
|
|
27833
27863
|
}
|
|
27834
|
-
if (view.custom_columns.length > 0) {
|
|
27835
|
-
// A hand-tuned config carries columns no preset owns, so the per-record
|
|
27836
|
-
// figure above can be a floor rather than the bill.
|
|
27837
|
-
const note = view.custom_columns_uncosted ? ` ${styles.yellow("(cost not included above)")}` : "";
|
|
27838
|
-
lines.push(` ${styles.dim("Extra".padEnd(summaryWidth))} ${view.custom_columns.join(", ")}${note}`);
|
|
27839
|
-
}
|
|
27840
27864
|
if (view.changed) {
|
|
27841
27865
|
lines.push("", ...formatCrmEnrichmentChange(view.changed, styles));
|
|
27842
27866
|
}
|
|
@@ -27882,9 +27906,23 @@ function pushCrmEnrichmentPreset(lines, styles, preset, labelWidth, armed) {
|
|
|
27882
27906
|
}
|
|
27883
27907
|
}
|
|
27884
27908
|
function formatCrmEnrichmentCredits(credits, styles, unit) {
|
|
27885
|
-
|
|
27886
|
-
|
|
27887
|
-
|
|
27909
|
+
if (credits === 0)
|
|
27910
|
+
return styles.green("free");
|
|
27911
|
+
// "1 credits/record" read wrong in a blind pass (2026-10-04); the unit is
|
|
27912
|
+
// "credits" or "credits/record", so singularize its leading word.
|
|
27913
|
+
const label = credits === 1 ? unit.replace(/^credits\b/, "credit") : unit;
|
|
27914
|
+
return styles.yellow(`${credits.toLocaleString("en-US")} ${label}`);
|
|
27915
|
+
}
|
|
27916
|
+
// "on" alone reads as "the enrichments listed below are on" even when every one
|
|
27917
|
+
// of them is off and only a hand-added column is armed.
|
|
27918
|
+
function formatCrmEnrichmentState(view, styles) {
|
|
27919
|
+
if (!view.enabled)
|
|
27920
|
+
return styles.dim("off");
|
|
27921
|
+
const anyPreset = view.presets.some((preset) => preset.enabled);
|
|
27922
|
+
if (!anyPreset && view.custom_columns.length > 0) {
|
|
27923
|
+
return `${styles.green("on")} ${styles.dim("— your own columns only")}`;
|
|
27924
|
+
}
|
|
27925
|
+
return styles.green("on");
|
|
27888
27926
|
}
|
|
27889
27927
|
function formatCrmEnrichmentCeiling(view, styles) {
|
|
27890
27928
|
if (view.max_credits === null)
|
|
@@ -27892,7 +27930,8 @@ function formatCrmEnrichmentCeiling(view, styles) {
|
|
|
27892
27930
|
const source = view.max_credits_source === "explicit"
|
|
27893
27931
|
? "you set this"
|
|
27894
27932
|
: "your plan's per-delivery default";
|
|
27895
|
-
|
|
27933
|
+
const unit = view.max_credits === 1 ? "credit" : "credits";
|
|
27934
|
+
return `${view.max_credits.toLocaleString("en-US")} ${unit} ${styles.dim(`(${source})`)}`;
|
|
27896
27935
|
}
|
|
27897
27936
|
function formatCrmEnrichmentSources(sources) {
|
|
27898
27937
|
if (sources.length === 0)
|
|
@@ -29780,6 +29819,9 @@ function readSequenceSettings(options) {
|
|
|
29780
29819
|
if (options.whatsappColdInitiate === true) {
|
|
29781
29820
|
settings.whatsapp_cold_initiate = true;
|
|
29782
29821
|
}
|
|
29822
|
+
const whatsappDuplicateScope = readOption(options.whatsappDuplicateScope);
|
|
29823
|
+
if (whatsappDuplicateScope)
|
|
29824
|
+
settings.whatsapp_duplicate_scope = whatsappDuplicateScope;
|
|
29783
29825
|
const phoneColumnKey = readOption(options.phoneColumnKey);
|
|
29784
29826
|
if (phoneColumnKey) {
|
|
29785
29827
|
settings.phone_column_key = phoneColumnKey;
|
|
@@ -29964,6 +30006,12 @@ catch (error) {
|
|
|
29964
30006
|
writeJson(usage.envelope);
|
|
29965
30007
|
process.exitCode = usage?.exitCode ?? error.exitCode;
|
|
29966
30008
|
}
|
|
30009
|
+
else if (error instanceof OxygenError) {
|
|
30010
|
+
// An option a command reads before its handleAsyncAction (`tables auto-run
|
|
30011
|
+
// set --max-credits 0` -> readPositiveNumber) throws outside every handler,
|
|
30012
|
+
// and used to escape as a raw Node stack with no envelope, even with --json.
|
|
30013
|
+
emitCliFailure(commandNameForArgv(program, argv.slice(2)), error);
|
|
30014
|
+
}
|
|
29967
30015
|
else {
|
|
29968
30016
|
throw error;
|
|
29969
30017
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { WORKFLOW_RUN_LIST_CONTRACT } from "@oxygen/shared/contracts/workflow-run-list";
|
|
2
|
+
import { readContractFlags } from "./contract-flags.js";
|
|
3
|
+
import { readPositiveInt } from "./cli-values.js";
|
|
4
|
+
import { readOption } from "./util.js";
|
|
5
|
+
export const WORKFLOW_RUN_LIST_FLAG_OVERLAY = {
|
|
6
|
+
workflow: { valueName: "<workflow>", parse: readOption },
|
|
7
|
+
workflow_id: { valueName: "<workflow_id>", help: "Workflow id or slug; legacy CLI --workflow takes precedence.", parse: readOption },
|
|
8
|
+
status: { valueName: "<status>", parse: readOption },
|
|
9
|
+
mode: { valueName: "<mode>", parse: readOption },
|
|
10
|
+
cursor: { valueName: "<cursor>", parse: readOption },
|
|
11
|
+
// Preserve the shipped CLI parser: native HTTP owns the upper-500 refusal.
|
|
12
|
+
limit: { valueName: "<n>", parse: readPositiveInt },
|
|
13
|
+
};
|
|
14
|
+
export function readWorkflowRunListFlags(options) {
|
|
15
|
+
const args = readContractFlags(options, WORKFLOW_RUN_LIST_CONTRACT, WORKFLOW_RUN_LIST_FLAG_OVERLAY);
|
|
16
|
+
const workflow = args.workflow ?? args.workflow_id;
|
|
17
|
+
delete args.workflow_id;
|
|
18
|
+
if (workflow !== undefined)
|
|
19
|
+
args.workflow = workflow;
|
|
20
|
+
return args;
|
|
21
|
+
}
|