@oxygen-agent/cli 1.1011.687 → 1.1011.800
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 +245 -158
- 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 +65 -14
- 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/operations.js +2 -1
- 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-feed-update.d.ts +222 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-feed-update.js +55 -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/dist/index.js
CHANGED
|
@@ -1,81 +1,86 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { renderPrimaryProviderBoard } from "./admin-primary-providers-render.js";
|
|
2
|
+
import { markStdinConsumed, maybeScheduleBackgroundUpdate, readAutoUpdateStatus, runBackgroundUpdate, } from "./auto-update.js";
|
|
3
|
+
import { createBrowserLoginSession, createRelayLoginSession, openBrowser } from "./browser-login.js";
|
|
4
|
+
import { assertModeFlagsExclusive, parseKeyValuePairs, parseJsonObject, readJsonObjectOption, readNonNegativeInt, readPositiveInt, readRecordString, resolveLiveDryRunMode, } from "./cli-values.js";
|
|
5
|
+
import { readColumnDecisionFlags } from "./column-decision-options.js";
|
|
6
|
+
import { formatAiPromptPreviewNotice, formatColumnReferenceNotices, formatEnrichmentInputMappingNotice, formatProviderLaneNotices, } from "./column-run-notices.js";
|
|
7
|
+
import { buildCommandManifest, getCommandManifestEntry, searchCommandManifest, suggestCommandNames, } from "./command-manifest.js";
|
|
8
|
+
import { registerConnectorsCommands } from "./connectors-commands.js";
|
|
9
|
+
import { addContractFlags, readContractFlags } from "./contract-flags.js";
|
|
10
|
+
import { clearCredentials, defaultApiUrl, listCredentialProfiles, loadCredentials, normalizeApiUrl, pickProfileNameForIdentity, pickProfileNameForUserSession, resolveActiveProfile, saveCredentials, switchCredentialProfile, updateActiveOrganizationForProfile, } from "./credentials.js";
|
|
6
11
|
import { registerCrmFileReadCommands } from "./crm-file-read-commands.js";
|
|
7
|
-
import { WHATSAPP_ACCOUNTS_READ_CONTRACT } from "@oxygen/shared/contracts/whatsapp-reads";
|
|
8
|
-
import { WHATSAPP_ACCOUNT_FLAGS, whatsappAccountQuery } from "./whatsapp-account-flags.js";
|
|
9
|
-
import { BILLING_OWNER_READ_CONTRACT } from "@oxygen/shared/contracts/billing-owner-read";
|
|
10
|
-
import { publishingAmplificationListQueryContract, publishingAmplificationActionsQueryContract, publishingNotesQueryContract, publishingRevisionsQueryContract, publishingAnalyticsQueryContract, publishingReadQueryParams, } from "@oxygen/shared/contracts/publishing-reads";
|
|
11
|
-
import { COPILOT_SESSIONS_LIST_CONTRACT } from "@oxygen/shared/contracts/copilot-sessions";
|
|
12
12
|
import { registerCrmSupportFlags, readCrmNoteEditFields, readCrmPhotoFields, readCrmFileLabel, buildCrmObjectsPath, buildCrmConnectionsPath } from "./crm-support-flags.js";
|
|
13
|
-
import { addToolCatalogSearchWindowFlag, readToolCatalogSearchWindow } from "./tool-catalog-search-window.js";
|
|
14
|
-
import { createToolCatalogDetailCommand } from "./tool-catalog-detail-command.js";
|
|
15
|
-
import { LIMITS_READ_QUERY_CONTRACT, limitsReadQueryIssue } from "@oxygen/shared/contracts/limits-read";
|
|
16
|
-
import { execFileSync } from "node:child_process";
|
|
17
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
18
|
-
import { closeSync, createReadStream, createWriteStream, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
19
|
-
import { tmpdir } from "node:os";
|
|
20
|
-
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
21
|
-
import { createInterface } from "node:readline/promises";
|
|
22
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
23
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
24
|
-
import { Command, CommanderError, Option } from "commander";
|
|
25
|
-
import { registerUgcCommands } from "./ugc-commands.js";
|
|
26
|
-
import { registerConnectorsCommands } from "./connectors-commands.js";
|
|
27
|
-
import { registerSettingsCommands } from "./settings-commands.js";
|
|
28
13
|
import { registerCustomerOperationCommands } from "./customer-operation-commands.js";
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import { renderPrimaryProviderBoard } from "./admin-primary-providers-render.js";
|
|
14
|
+
import { DATABASE_QUERY_FLAG_OVERLAY } from "./database-query-flags.js";
|
|
15
|
+
import { addFeedReadFlags, feedReadWindowParams } from "./feed-read-flags.js";
|
|
32
16
|
import { registerFunctionsCommands } from "./functions-commands.js";
|
|
17
|
+
import { applyOxygenHelp, argvRequestsJson, commanderUsageFailure, commandNameForArgv, enableCommandSuggestions, unknownCommandHint, unknownOptionHint, withoutHelpForUnknownCommand, validateRequiredOptionValues, } from "./help.js";
|
|
18
|
+
import { ensureFreshCliForApiUrl, requestOxygen } from "./http-client.js";
|
|
19
|
+
import { formatInboxNeedsReplyNotice } from "./inbox-needs-reply-notice.js";
|
|
20
|
+
import { acquireMirrorLock, clearConflictFiles, deletePageFile, emptyMirrorState, findMirrorSlugByPageId, isFileDirty, listConflictFiles, listLocalMirrors, localPageSha256, markMirrorStale, mirrorExists, pageFilePath, planMirrorPush, purgeMirror, resetMirrorForFullResync, quarantineDirtyFile, readMirrorState, releaseMirrorLock, resolveDefaultConfigDir, resolveMirrorDir, writeGeneratedIndexFile, writeGeneratedLogFile, writeMirrorState, writePageFile, } from "./knowledge-mirror.js";
|
|
21
|
+
import { registerKnowledgeRepositoryCommands } from "./knowledge-repository-commands.js";
|
|
22
|
+
import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
|
|
23
|
+
import { formatMailboxRampNotice } from "./mailbox-ramp-notice.js";
|
|
24
|
+
import { waitForCliRun } from "./run-wait.js";
|
|
25
|
+
import { resolveCliBinaryName } from "./runtime.js";
|
|
26
|
+
import { formatSearchAiFilterTotalsNotice } from "./search-ai-filter-notice.js";
|
|
27
|
+
import { buildSenderLimitsBody, parseWorkingDaysOption, SENDER_LIMITS_FLAG_OVERLAY } from "./sender-limits-flags.js";
|
|
28
|
+
import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, updateSessionStep, } from "./session.js";
|
|
29
|
+
import { registerSettingsCommands } from "./settings-commands.js";
|
|
30
|
+
import { doctorAgentSkills, getAgentSkill, installAgentSkills, listAgentSkills, runAutomaticSkillsInstall, searchAgentSkills, } from "./skills.js";
|
|
33
31
|
import { STREAMED_FILE_IMPORT_MIN_BYTES, putImportFileStream, rowsForStreamedImportTarget, scanStreamedImportFile, shouldStreamFileImport, } from "./streamed-file-import.js";
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
32
|
+
import { createToolCatalogDetailCommand } from "./tool-catalog-detail-command.js";
|
|
33
|
+
import { addToolCatalogSearchWindowFlag, readToolCatalogSearchWindow } from "./tool-catalog-search-window.js";
|
|
34
|
+
import { captureCurrentTranscript, collectFeedbackEnvironment, redactTranscriptSecrets, TranscriptCaptureError, } from "./transcript.js";
|
|
35
|
+
import { registerUgcCommands } from "./ugc-commands.js";
|
|
36
|
+
import { updateCli } from "./update.js";
|
|
37
|
+
import { isRecord, readClearableOption, readErrorMessage, readOption } from "./util.js";
|
|
38
|
+
import { registerVisualCommands } from "./visual-commands.js";
|
|
39
|
+
import { WHATSAPP_ACCOUNT_FLAGS, whatsappAccountQuery } from "./whatsapp-account-flags.js";
|
|
40
|
+
import { formatWorkflowPlanLimitNotices } from "./workflow-plan-limit-notices.js";
|
|
41
|
+
import { WORKFLOW_RUN_LIST_FLAG_OVERLAY, readWorkflowRunListFlags } from "./workflow-run-list-flags.js";
|
|
42
|
+
import { isRecipeDefinition } from "@oxygen/recipe-sdk";
|
|
36
43
|
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
44
|
import { PURCHASABLE_PLAN_KEYS, planVolumeBonusPercent, resolveBasePricingPlan } from "@oxygen/shared/billing";
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import { LINKEDIN_SENDER_LIMIT_DEFAULTS, LINKEDIN_SENDER_LIMIT_MAXIMUMS, } from "@oxygen/shared/linkedin-sequences";
|
|
42
|
-
import { SEQUENCE_LIFECYCLES, sequenceDisplayStatus } from "@oxygen/shared/sequence-lifecycle";
|
|
43
|
-
import { operationPath } from "@oxygen/shared/contracts/operations";
|
|
44
|
-
import { SENDER_LIMITS_SET_OPERATION } from "@oxygen/shared/contracts/sender-limits";
|
|
45
|
-
import { addContractFlags, readContractFlags } from "./contract-flags.js";
|
|
46
|
-
import { addFeedReadFlags, feedReadWindowParams } from "./feed-read-flags.js";
|
|
47
|
-
import { TABLE_QUERY_GRID_CONTRACT } from "@oxygen/shared/contracts/table-query-grid";
|
|
48
|
-
import { INTEGRATION_CONNECT_ACCOUNT_CONTRACT } from "@oxygen/shared/contracts/integration-connect";
|
|
45
|
+
import { COMPANY_REPLY_PAUSE_DEFAULT_STATUSES, COMPANY_REPLY_PAUSE_LEGACY_STATUSES, COMPANY_REPLY_PAUSE_STATUS_KEY_PATTERN, } from "@oxygen/shared/company-reply-pause";
|
|
46
|
+
import { BILLING_OWNER_READ_CONTRACT } from "@oxygen/shared/contracts/billing-owner-read";
|
|
47
|
+
import { COPILOT_SESSIONS_LIST_CONTRACT } from "@oxygen/shared/contracts/copilot-sessions";
|
|
49
48
|
import { CRM_ENRICHMENT_READ_CONTRACT } from "@oxygen/shared/contracts/crm-enrichment-read";
|
|
49
|
+
import { DATABASE_ATTACH_CONTRACT, DATABASE_MIGRATE_CONTRACT, DATABASE_QUERY_CONTRACT } from "@oxygen/shared/contracts/database";
|
|
50
50
|
import { INBOX_DRAFTS_READ_CONTRACT } from "@oxygen/shared/contracts/inbox-drafts-read";
|
|
51
|
+
import { INTEGRATION_CONNECT_ACCOUNT_CONTRACT } from "@oxygen/shared/contracts/integration-connect";
|
|
52
|
+
import { knowledgeIndexReadContract, knowledgePageReadContract } from "@oxygen/shared/contracts/knowledge-content-reads";
|
|
53
|
+
import { LIMITS_READ_QUERY_CONTRACT, limitsReadQueryIssue } from "@oxygen/shared/contracts/limits-read";
|
|
54
|
+
import { LINKEDIN_SENT_INVITES_QUERY_CONTRACT } from "@oxygen/shared/contracts/linkedin-invites-list";
|
|
55
|
+
import { operationPath } from "@oxygen/shared/contracts/operations";
|
|
56
|
+
import { PIPEDRIVE_CATALOG_QUERY_CONTRACT, pipedriveCatalogQueryParams } from "@oxygen/shared/contracts/pipedrive-catalog";
|
|
57
|
+
import { publishingAmplificationListQueryContract, publishingAmplificationActionsQueryContract, publishingNotesQueryContract, publishingRevisionsQueryContract, publishingAnalyticsQueryContract, publishingReadQueryParams, } from "@oxygen/shared/contracts/publishing-reads";
|
|
58
|
+
import { SENDER_LIMITS_SET_OPERATION } from "@oxygen/shared/contracts/sender-limits";
|
|
59
|
+
import { SEQUENCE_ANALYTICS_READ_CONTRACT } from "@oxygen/shared/contracts/sequence-analytics-read";
|
|
51
60
|
import { SEQUENCE_LIST_PROJECTION_CONTRACT } from "@oxygen/shared/contracts/sequence-list";
|
|
52
|
-
import {
|
|
53
|
-
import {
|
|
61
|
+
import { TABLE_FEED_UPDATE_CONTRACT, tableFeedUpdateIssues } from "@oxygen/shared/contracts/table-feed-update";
|
|
62
|
+
import { TABLE_LIST_QUERY_CONTRACT, tableListQueryParams } from "@oxygen/shared/contracts/table-list";
|
|
63
|
+
import { TABLE_QUERY_GRID_CONTRACT } from "@oxygen/shared/contracts/table-query-grid";
|
|
64
|
+
import { WHATSAPP_ACCOUNTS_READ_CONTRACT } from "@oxygen/shared/contracts/whatsapp-reads";
|
|
65
|
+
import { WORKFLOW_RUN_LIST_CONTRACT, workflowRunListQueryParams } from "@oxygen/shared/contracts/workflow-run-list";
|
|
54
66
|
import { MANAGED_DATA_SUPPLIERS } from "@oxygen/shared/data-suppliers";
|
|
55
|
-
import { COMPANY_REPLY_PAUSE_DEFAULT_STATUSES, COMPANY_REPLY_PAUSE_LEGACY_STATUSES, COMPANY_REPLY_PAUSE_STATUS_KEY_PATTERN, } from "@oxygen/shared/company-reply-pause";
|
|
56
67
|
import { MAX_BUFFERED_IMPORT_PARSE_BYTES, assertImportFormatWithinBufferedLimit, inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, parseXlsxWorkbookBuffer, } from "@oxygen/shared/file-import";
|
|
68
|
+
import { LINKEDIN_SENDER_LIMIT_DEFAULTS, LINKEDIN_SENDER_LIMIT_MAXIMUMS, } from "@oxygen/shared/linkedin-sequences";
|
|
57
69
|
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";
|
|
70
|
+
import { PUBLIC_WEBHOOK_INGRESS_PER_TARGET_PER_MINUTE } from "@oxygen/shared/plan-band";
|
|
71
|
+
import { AI_COLUMN_CREDITS, AI_COLUMN_WEB_SEARCH_TYPICAL_CREDITS } from "@oxygen/shared/pricing-sheet";
|
|
72
|
+
import { TAG_COLORS } from "@oxygen/shared/select-options";
|
|
73
|
+
import { SEQUENCE_LIFECYCLES, sequenceDisplayStatus } from "@oxygen/shared/sequence-lifecycle";
|
|
58
74
|
import { assertRecipeBundleSafe, assertPortableWorkflowDefinition, assertWorkflowGraphManifest, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowGraphManifest, isWorkflowManifest, hashPortableWorkflowGraphManifest, WORKFLOW_GRAPH_COMPILER_VERSION, WORKFLOW_GRAPH_MANIFEST_VERSION, } from "@oxygen/workflows";
|
|
59
|
-
import {
|
|
60
|
-
import {
|
|
61
|
-
import {
|
|
62
|
-
import {
|
|
63
|
-
import {
|
|
64
|
-
import {
|
|
65
|
-
import {
|
|
66
|
-
import {
|
|
67
|
-
import {
|
|
68
|
-
import { formatSearchAiFilterTotalsNotice } from "./search-ai-filter-notice.js";
|
|
69
|
-
import { formatInboxNeedsReplyNotice } from "./inbox-needs-reply-notice.js";
|
|
70
|
-
import { formatMailboxRampNotice } from "./mailbox-ramp-notice.js";
|
|
71
|
-
import { formatWorkflowPlanLimitNotices } from "./workflow-plan-limit-notices.js";
|
|
72
|
-
import { captureCurrentTranscript, collectFeedbackEnvironment, redactTranscriptSecrets, TranscriptCaptureError, } from "./transcript.js";
|
|
73
|
-
import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, updateSessionStep, } from "./session.js";
|
|
74
|
-
import { doctorAgentSkills, getAgentSkill, installAgentSkills, listAgentSkills, runAutomaticSkillsInstall, searchAgentSkills, } from "./skills.js";
|
|
75
|
-
import { resolveCliBinaryName } from "./runtime.js";
|
|
76
|
-
import { updateCli } from "./update.js";
|
|
77
|
-
import { markStdinConsumed, maybeScheduleBackgroundUpdate, readAutoUpdateStatus, runBackgroundUpdate, } from "./auto-update.js";
|
|
78
|
-
import { isRecord, readClearableOption, readErrorMessage, readOption } from "./util.js";
|
|
75
|
+
import { Command, CommanderError, Option } from "commander";
|
|
76
|
+
import { execFileSync } from "node:child_process";
|
|
77
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
78
|
+
import { closeSync, createReadStream, createWriteStream, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
79
|
+
import { tmpdir } from "node:os";
|
|
80
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
81
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
82
|
+
import { createInterface } from "node:readline/promises";
|
|
83
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
79
84
|
const AGENT_MODEL_POLICY_HELP = "Model policy JSON: level low (Fast), medium (Balanced, default), or high (Max); credential_mode managed or organization_byok.";
|
|
80
85
|
const AGENT_TOOL_POLICY_HELP = "Tool policy JSON. Effects: read, internal_write, paid, external_write. Ask-before-changes preset: {\"mode\":\"scoped\",\"rules\":[{\"decision\":\"allow\",\"effect\":\"read\"},{\"decision\":\"ask\",\"effect\":\"internal_write\"},{\"decision\":\"ask\",\"effect\":\"paid\"},{\"decision\":\"ask\",\"effect\":\"external_write\"}]}. Full mode requires --confirm-full-access.";
|
|
81
86
|
/**
|
|
@@ -2036,6 +2041,7 @@ function buildCrmSetupBody(options) {
|
|
|
2036
2041
|
objects: readCrmSetupObjects(options.objects),
|
|
2037
2042
|
mode: resolveLiveDryRunMode(options),
|
|
2038
2043
|
...(options.withEnrichment === true ? { with_enrichment: true } : {}),
|
|
2044
|
+
...(options.withoutEnrichment === true ? { without_enrichment: true } : {}),
|
|
2039
2045
|
...(readOption(options.project) ? { project: readOption(options.project) } : {}),
|
|
2040
2046
|
};
|
|
2041
2047
|
}
|
|
@@ -5716,9 +5722,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5716
5722
|
.command("crm")
|
|
5717
5723
|
.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
5724
|
.addCommand(new Command("setup")
|
|
5719
|
-
.description("Create or repair standard CRM object-backed tables
|
|
5725
|
+
.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
5726
|
.option("--objects <objects>", "Comma-separated standard CRM objects to set up. Defaults to companies,people,deals.")
|
|
5721
5727
|
.option("--project <project>", "Project id or slug for created CRM tables.")
|
|
5728
|
+
.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
5729
|
.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
5730
|
.option("--dry-run", "Preview CRM setup without creating or repairing tables.")
|
|
5724
5731
|
.option("--live", "Apply CRM setup changes. Default is dry-run.")
|
|
@@ -5803,7 +5810,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5803
5810
|
}));
|
|
5804
5811
|
}))
|
|
5805
5812
|
.addCommand(new Command("delete")
|
|
5806
|
-
.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.")
|
|
5813
|
+
.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.")
|
|
5807
5814
|
.argument("<object>", "Custom CRM object slug, such as projects or tasks.")
|
|
5808
5815
|
.option("--keep-table", "Deregister the object but keep its table as an ordinary workspace table (no delete timer).")
|
|
5809
5816
|
.option("--purge-after <days>", "Days until the backing table's physical purge. Defaults to 14; restorable via `tables restore` until then.")
|
|
@@ -5877,7 +5884,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5877
5884
|
.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
5885
|
.argument("<object>", "CRM object slug, such as companies or people.")
|
|
5879
5886
|
.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
|
|
5887
|
+
.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
5888
|
.option("--dry-run", "Preview the assert without writing a row.")
|
|
5882
5889
|
.option("--live", "Apply the assert. Default is dry-run.")
|
|
5883
5890
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -6251,7 +6258,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6251
6258
|
await handleAsyncAction("crm activity timeline", options, () => requestOxygen(buildCrmTimelinePath(object, rowId, options)));
|
|
6252
6259
|
})))
|
|
6253
6260
|
.addCommand(new Command("describe")
|
|
6254
|
-
.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.")
|
|
6261
|
+
.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 not found; `crm objects delete <slug> --dry-run` shows its archived state.")
|
|
6255
6262
|
.argument("<object>", "CRM object slug, such as companies or people.")
|
|
6256
6263
|
.option("--field <key>", "Narrow the answer to one field by its key, e.g. --field pipeline_stage. Also accepted as --attribute.")
|
|
6257
6264
|
.option("--attribute <key>", "Alias for --field.")
|
|
@@ -7104,29 +7111,20 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7104
7111
|
.action(async (options) => {
|
|
7105
7112
|
await handleAsyncAction("tables import-bundle", options, () => importTableBundle(options, binaryName));
|
|
7106
7113
|
}))
|
|
7107
|
-
.addCommand(new Command("list")
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7114
|
+
.addCommand(addContractFlags(new Command("list").description("List workspace tables in the current tenant database."), TABLE_LIST_QUERY_CONTRACT, {
|
|
7115
|
+
project: { help: "Project id or slug to filter by." },
|
|
7116
|
+
tag: { help: "Only tables carrying this workspace tag (see `oxygen tags list`)." },
|
|
7117
|
+
include_archived: {
|
|
7118
|
+
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).",
|
|
7119
|
+
},
|
|
7120
|
+
limit: { help: "Maximum tables to list. Defaults to 200; hard cap is 1000.", valueName: "<n>" },
|
|
7121
|
+
})
|
|
7113
7122
|
.option("--all", "List every table instead of the newest window.")
|
|
7114
7123
|
.option("--json", "Print a JSON envelope.")
|
|
7115
|
-
.addHelpText("after", "\nLists the 200 newest tables by default
|
|
7124
|
+
.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
7125
|
.action(async (options) => {
|
|
7117
7126
|
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));
|
|
7127
|
+
const params = tableListQueryParams(readContractFlags(options, TABLE_LIST_QUERY_CONTRACT));
|
|
7130
7128
|
if (options.all)
|
|
7131
7129
|
params.set("all", "true");
|
|
7132
7130
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
@@ -7134,7 +7132,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7134
7132
|
// A capped list is where a blind agent filtered 200 rows locally to find
|
|
7135
7133
|
// one table by name (acceptance run, 2026-09-18); name the native lookup.
|
|
7136
7134
|
if (!options.json)
|
|
7137
|
-
writeListCapNotice(data, "table(s)", "
|
|
7135
|
+
writeListCapNotice(data, "table(s)", "continue with --cursor <nextCursor>, or find a name with --query <text>");
|
|
7138
7136
|
return data;
|
|
7139
7137
|
});
|
|
7140
7138
|
}))
|
|
@@ -7229,6 +7227,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7229
7227
|
.argument("<table>", "Table id or slug.")
|
|
7230
7228
|
.option("--include-archived", "Include archived columns, and read a table that is archived, scheduled for deletion or being purged.")
|
|
7231
7229
|
.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.")
|
|
7230
|
+
.option("--column <key>", "Describe just this one column (its kind, definition and whether auto-run includes it) instead of every column.")
|
|
7232
7231
|
.option("--json", "Print a JSON envelope.")
|
|
7233
7232
|
.action(async (table, options) => {
|
|
7234
7233
|
await handleAsyncAction("tables describe", options, () => requestOxygen("/api/cli/tables/describe", {
|
|
@@ -7237,6 +7236,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7237
7236
|
table,
|
|
7238
7237
|
...(options.includeArchived ? { include_archived: true } : {}),
|
|
7239
7238
|
...(options.stats ? { include_stats: true } : {}),
|
|
7239
|
+
...(readOption(options.column) ? { column: readOption(options.column) } : {}),
|
|
7240
7240
|
},
|
|
7241
7241
|
}));
|
|
7242
7242
|
}))
|
|
@@ -8077,6 +8077,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8077
8077
|
.addCommand(addFeedReadFlags(new Command("get")
|
|
8078
8078
|
.description("Get one table feed. Optional summary or lossless value JSON windows recover every public setting; resume value-cursor with the same feed/path/include-cursor. Read-only.")
|
|
8079
8079
|
.argument("<feed>", "Feed id or endpoint id.")
|
|
8080
|
+
.option("--include-route", "Include the complete frozen request and fallback lanes for an in-place update.")
|
|
8080
8081
|
.option("--include-cursor", "Include the opaque provider page token in the cursor block.")
|
|
8081
8082
|
.option("--json", "Print a JSON envelope.")
|
|
8082
8083
|
.action((feed, options) => handleAsyncAction("feeds get", options, () => requestOxygen(feedsGetPath(feed, options)))), true))
|
|
@@ -8158,6 +8159,26 @@ Examples:
|
|
|
8158
8159
|
return data;
|
|
8159
8160
|
});
|
|
8160
8161
|
}))
|
|
8162
|
+
.addCommand(addContractFlags(new Command("update")
|
|
8163
|
+
.description("Update an existing feed in place. Inspect feeds get --include-route first. Source replacement resets its cursor; cadence, cap and source changes keep native standing authorization and budget checks. Configuration spends nothing.")
|
|
8164
|
+
.argument("<feed>", "Feed id or endpoint id.")
|
|
8165
|
+
.option("--json", "Print a JSON envelope."), TABLE_FEED_UPDATE_CONTRACT, {
|
|
8166
|
+
feed: { omit: true }, sync: { flatten: true },
|
|
8167
|
+
"sync.max_credits_per_cycle": { flag: "max-credits" },
|
|
8168
|
+
"sync.max_rows_per_cycle": { flag: "max-rows" },
|
|
8169
|
+
approved: { noNegate: true },
|
|
8170
|
+
})
|
|
8171
|
+
.action((feed, options) => handleAsyncAction("feeds update", options, () => {
|
|
8172
|
+
const body = { ...readContractFlags(options, TABLE_FEED_UPDATE_CONTRACT, {
|
|
8173
|
+
feed: { omit: true }, sync: { flatten: true },
|
|
8174
|
+
"sync.max_credits_per_cycle": { flag: "max-credits" },
|
|
8175
|
+
"sync.max_rows_per_cycle": { flag: "max-rows" }, approved: { noNegate: true },
|
|
8176
|
+
}), feed };
|
|
8177
|
+
const errors = tableFeedUpdateIssues(body);
|
|
8178
|
+
if (errors.length)
|
|
8179
|
+
throw new OxygenError("invalid_request", errors.join(" "), { exitCode: 1 });
|
|
8180
|
+
return requestOxygen("/api/cli/tables/feeds", { method: "PATCH", body });
|
|
8181
|
+
})))
|
|
8161
8182
|
.addCommand(new Command("pause")
|
|
8162
8183
|
.description("Pause a feed: the cron stays armed but every cycle it fires is refused, so nothing is fetched and nothing is billed until you resume. Free.")
|
|
8163
8184
|
.argument("<feed>", "Feed id or endpoint id.")
|
|
@@ -9472,7 +9493,12 @@ Examples:
|
|
|
9472
9493
|
].join("\n"));
|
|
9473
9494
|
program
|
|
9474
9495
|
.command("recipes")
|
|
9475
|
-
|
|
9496
|
+
// Customers meet these as "plays" (the web app's Plays page and every
|
|
9497
|
+
// copied play prompt), so that is the noun their agent types first. A
|
|
9498
|
+
// production audit on 2026-10-04 saw all 33 blind runs open with
|
|
9499
|
+
// `oxygen plays` and lose a call to "unknown command".
|
|
9500
|
+
.alias("plays")
|
|
9501
|
+
.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
9502
|
.addCommand(new Command("list")
|
|
9477
9503
|
.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
9504
|
.argument("[query]", "Your goal in plain words, e.g. \"more pipeline from linkedin\" (ranked against title, trigger phrases, tags, and business case).")
|
|
@@ -11049,13 +11075,13 @@ Examples:
|
|
|
11049
11075
|
.addCommand(new Command("search")
|
|
11050
11076
|
.description("Plan, dry-run, or queue provider-backed people/contact search.")
|
|
11051
11077
|
.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.
|
|
11078
|
+
.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
11079
|
.option("--json", "Print a JSON envelope.")
|
|
11054
11080
|
.action(async (options) => {
|
|
11055
11081
|
await handleAsyncAction("people search filters", options, () => requestOxygen("/api/cli/people/search/preview"));
|
|
11056
11082
|
}))
|
|
11057
11083
|
.addCommand(new Command("preview")
|
|
11058
|
-
.description("Preview up to 50 people free, using native filters from people search filters.
|
|
11084
|
+
.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
11085
|
.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
11086
|
.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
11087
|
.option("--linkedin-url-column <key>", "Column of --from-table that holds each company's LinkedIn URL.")
|
|
@@ -11389,7 +11415,7 @@ Examples:
|
|
|
11389
11415
|
}))
|
|
11390
11416
|
.addCommand(new Command("usage")
|
|
11391
11417
|
.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
|
|
11418
|
+
.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
11419
|
.option("--days <n>", "Lookback window in days. Defaults to 30.")
|
|
11394
11420
|
.option("--from <iso>", "Only include ledger events at or after this ISO timestamp.")
|
|
11395
11421
|
.option("--to <iso>", "Only include ledger events at or before this ISO timestamp.")
|
|
@@ -13670,7 +13696,7 @@ Examples:
|
|
|
13670
13696
|
await handleAsyncAction("senders checkpoints", options, () => requestOxygen("/api/cli/senders/checkpoints"));
|
|
13671
13697
|
}))
|
|
13672
13698
|
.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.
|
|
13699
|
+
.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
13700
|
.option("--reconnect <connection_id>", "Reconnect an existing connection instead of creating a new one. Accepts a connection id.")
|
|
13675
13701
|
.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
13702
|
.option("--sales-nav", "Request Classic + Sales Navigator access during Unipile hosted authentication.")
|
|
@@ -15857,35 +15883,39 @@ Examples:
|
|
|
15857
15883
|
});
|
|
15858
15884
|
});
|
|
15859
15885
|
})))
|
|
15860
|
-
.addCommand(new Command("analytics")
|
|
15886
|
+
.addCommand(addContractFlags(new Command("analytics")
|
|
15861
15887
|
.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
|
-
|
|
15888
|
+
.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, [])
|
|
15889
|
+
.option("--json", "Print a JSON envelope."), SEQUENCE_ANALYTICS_READ_CONTRACT, {
|
|
15890
|
+
range: { valueName: "<preset>", parse: readOption },
|
|
15891
|
+
from: { valueName: "<date>", parse: readOption }, to: { valueName: "<date>", parse: readOption },
|
|
15892
|
+
sequence: { valueName: "<sequence>", parse: readOption },
|
|
15893
|
+
campaigns: { valueName: "<ids>", parse: readOption }, senders: { valueName: "<ids>", parse: readOption },
|
|
15894
|
+
})
|
|
15868
15895
|
.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);
|
|
15896
|
+
const fetchAnalytics = () => {
|
|
15897
|
+
const query = readContractFlags(options, SEQUENCE_ANALYTICS_READ_CONTRACT, {
|
|
15898
|
+
range: { parse: readOption }, from: { parse: readOption }, to: { parse: readOption },
|
|
15899
|
+
sequence: { parse: readOption }, campaigns: { parse: readOption }, senders: { parse: readOption },
|
|
15900
|
+
});
|
|
15901
|
+
const senders = [...readCsvOption(typeof query.senders === "string" ? query.senders : undefined), ...(options.sender ?? []).flatMap(value => readCsvOption(value))];
|
|
15884
15902
|
if (senders.length > 0)
|
|
15885
|
-
|
|
15903
|
+
query.senders = senders.join(",");
|
|
15904
|
+
else
|
|
15905
|
+
delete query.senders;
|
|
15906
|
+
const params = new URLSearchParams();
|
|
15907
|
+
for (const [key, value] of Object.entries(query)) {
|
|
15908
|
+
if (value !== undefined && value !== null)
|
|
15909
|
+
params.set(key, String(value));
|
|
15910
|
+
}
|
|
15886
15911
|
const qs = params.toString();
|
|
15887
15912
|
return requestOxygen(`/api/cli/sequences/analytics${qs ? `?${qs}` : ""}`);
|
|
15888
|
-
}
|
|
15913
|
+
};
|
|
15914
|
+
const windowRequested = ["projection", "path", "valueCursor", "maxChars"].some(key => options[key] !== undefined);
|
|
15915
|
+
if (windowRequested)
|
|
15916
|
+
await handleAsyncAction("sequences analytics", options, fetchAnalytics);
|
|
15917
|
+
else
|
|
15918
|
+
await handleReadActionWithLens("sequences analytics", options, fetchAnalytics, formatSequenceAnalyticsHealth);
|
|
15889
15919
|
}))
|
|
15890
15920
|
.addCommand(new Command("create")
|
|
15891
15921
|
.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 +15925,7 @@ Examples:
|
|
|
15895
15925
|
.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
15926
|
.option("--channels <list>", "Comma-separated channels: linkedin,email,whatsapp,call,crm. Defaults to the channels the journey touches. crm_task currently supports HubSpot.")
|
|
15897
15927
|
.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.")
|
|
15928
|
+
.addOption(new Option("--whatsapp-duplicate-scope <scope>", "Prevent duplicate WhatsApp copy in this campaign (default) or all campaigns.").choices(["campaign", "workspace"]))
|
|
15898
15929
|
.option("--phone-column-key <key>", "WhatsApp: row_values key holding each lead's phone number (else falls back to phone/phone_number/mobile).")
|
|
15899
15930
|
.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
15931
|
.option("--table <id>", "Source table id or slug whose rows supply {{column}} template values.")
|
|
@@ -16015,6 +16046,7 @@ Examples:
|
|
|
16015
16046
|
.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
16047
|
.option("--channels <list>", "Draft only: comma-separated channels: linkedin,email,whatsapp,call,crm.")
|
|
16017
16048
|
.option("--whatsapp-cold-initiate", "WhatsApp: allow cold-initiating new chats (no prior conversation). Required to start a WhatsApp sequence live (draft-editable).")
|
|
16049
|
+
.addOption(new Option("--whatsapp-duplicate-scope <scope>", "Prevent duplicate WhatsApp copy in this campaign (default) or all campaigns.").choices(["campaign", "workspace"]))
|
|
16018
16050
|
.option("--phone-column-key <key>", "WhatsApp: row_values key holding each lead's phone number (else falls back to phone/phone_number/mobile).")
|
|
16019
16051
|
.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
16052
|
.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 +16168,7 @@ Examples:
|
|
|
16136
16168
|
await handleAsyncAction("sequences get", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}${options.readiness ? "?include=readiness" : ""}`));
|
|
16137
16169
|
}))
|
|
16138
16170
|
.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`.")
|
|
16171
|
+
.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
16172
|
.argument("<sequence>", "Sequence id or slug.")
|
|
16141
16173
|
.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
16174
|
.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 +17597,12 @@ Examples:
|
|
|
17565
17597
|
.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
17598
|
.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
17599
|
.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
|
|
17600
|
+
.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
17601
|
.option("--existing-domain", "Use an existing Cloudflare domain from oxygen domains list; never register or transfer it.")
|
|
17570
17602
|
.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.")
|
|
17603
|
+
.option("--dns-cleanup-records <ids>", "Preview deletion of these comma-separated exact email DNS record IDs on an existing domain; connects and buys nothing.")
|
|
17604
|
+
.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"))
|
|
17605
|
+
.option("--dns-cleanup-hash <hash>", "The plan_hash from the exact DNS cleanup preview, required to delete records.")
|
|
17571
17606
|
.option("--years <n>", "Years to register a new domain for (1-10). Omit for existing domains.")
|
|
17572
17607
|
.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
17608
|
.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 +17632,13 @@ Examples:
|
|
|
17597
17632
|
const billingPath = readOption(options.billing);
|
|
17598
17633
|
const billing = billingPath ? readJsonFileValue(resolve(billingPath), "--billing") : undefined;
|
|
17599
17634
|
const quote = readOption(options.quote);
|
|
17635
|
+
const cleanupRecords = readOption(options.dnsCleanupRecords);
|
|
17636
|
+
const cleanupHash = readOption(options.dnsCleanupHash);
|
|
17637
|
+
if ((cleanupRecords && !options.existingDomain)
|
|
17638
|
+
|| ((options.dnsCleanupApproved || cleanupHash) && !cleanupRecords)
|
|
17639
|
+
|| (options.dnsCleanupApproved && !cleanupHash)) {
|
|
17640
|
+
throw new OxygenError("invalid_request", "DNS cleanup requires --existing-domain and --dns-cleanup-records; deletion also requires its fresh --dns-cleanup-hash.", { exitCode: 2 });
|
|
17641
|
+
}
|
|
17600
17642
|
return requestOxygen("/api/cli/managed-inboxes/subscribe", {
|
|
17601
17643
|
method: "POST",
|
|
17602
17644
|
body: {
|
|
@@ -17607,6 +17649,11 @@ Examples:
|
|
|
17607
17649
|
...(years ? { years: Number(years) } : {}),
|
|
17608
17650
|
...(options.existingDomain ? { existing_domain: true } : {}),
|
|
17609
17651
|
...(options.maxPerDomain !== undefined ? { max_mailboxes_per_domain: Number(options.maxPerDomain) } : {}),
|
|
17652
|
+
...(cleanupRecords ? { dns_cleanup: {
|
|
17653
|
+
record_ids: cleanupRecords.split(",").map((id) => id.trim()),
|
|
17654
|
+
approved: options.dnsCleanupApproved === true,
|
|
17655
|
+
...(cleanupHash ? { plan_hash: cleanupHash } : {}),
|
|
17656
|
+
} } : {}),
|
|
17610
17657
|
...(redirectUrl ? { redirect_url: redirectUrl } : {}),
|
|
17611
17658
|
...(billing ? { billing } : {}),
|
|
17612
17659
|
// Warm-up is on unless --no-warmup. Placement is opt-in: it is sent only
|
|
@@ -19666,34 +19713,21 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
19666
19713
|
}), options);
|
|
19667
19714
|
});
|
|
19668
19715
|
})))
|
|
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.")
|
|
19716
|
+
.addCommand(addContractFlags(new Command("runs")
|
|
19717
|
+
.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
19718
|
.option("--include-bundle", "Include durable recipe bundles in JSON output.")
|
|
19678
19719
|
.option("--json", "Print a JSON envelope.")
|
|
19679
|
-
.addHelpText("after", "\
|
|
19720
|
+
.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
19721
|
.action(async (options) => {
|
|
19681
19722
|
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));
|
|
19723
|
+
const args = readWorkflowRunListFlags(options);
|
|
19724
|
+
const query = workflowRunListQueryParams(args);
|
|
19695
19725
|
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
19696
|
-
|
|
19726
|
+
const data = await requestOxygen(`/api/cli/workflows/runs${suffix}`);
|
|
19727
|
+
// Opt-in windows are lossless public native receipts. Legacy bundle
|
|
19728
|
+
// stripping/error aliases still apply only to the unqualified read.
|
|
19729
|
+
return ["projection", "path", "value_cursor", "max_chars"].some(key => args[key] !== undefined)
|
|
19730
|
+
? data : prepareWorkflowCliOutput(data, options);
|
|
19697
19731
|
});
|
|
19698
19732
|
}))
|
|
19699
19733
|
.addCommand(new Command("failures")
|
|
@@ -22203,8 +22237,9 @@ function feedsListPath(options, tableArgument) {
|
|
|
22203
22237
|
function feedsGetPath(feed, options) {
|
|
22204
22238
|
const params = feedReadWindowParams(options, true);
|
|
22205
22239
|
params.set("feed", feed);
|
|
22206
|
-
|
|
22207
|
-
|
|
22240
|
+
const include = [options.includeCursor ? "cursor" : null, options.includeRoute ? "route" : null].filter(Boolean);
|
|
22241
|
+
if (include.length)
|
|
22242
|
+
params.set("include", include.join(","));
|
|
22208
22243
|
return `/api/cli/tables/feeds?${params.toString()}`;
|
|
22209
22244
|
}
|
|
22210
22245
|
// The bind body mirrors the route's reader: everything the provider is asked for
|
|
@@ -22776,8 +22811,23 @@ function assertNotPeopleSearchKind(kind) {
|
|
|
22776
22811
|
});
|
|
22777
22812
|
}
|
|
22778
22813
|
}
|
|
22814
|
+
// The id a `search plan` envelope prints (search_plan_…, company_search_plan_…,
|
|
22815
|
+
// signal_search_plan_…). `--plan` takes the plan itself, not its id; a blind
|
|
22816
|
+
// acceptance run on 2026-10-04 passed the id and got only "Input must be valid
|
|
22817
|
+
// JSON" with no way forward.
|
|
22818
|
+
const SEARCH_PLAN_ID_PATTERN = /^[a-z_]*search_plan_[A-Za-z0-9]+$/;
|
|
22779
22819
|
function readSearchPlanJson(value) {
|
|
22780
|
-
const
|
|
22820
|
+
const raw = readFileIfPresent(value);
|
|
22821
|
+
if (raw === value && SEARCH_PLAN_ID_PATTERN.test(value.trim())) {
|
|
22822
|
+
throw new OxygenError("invalid_json", `--plan takes the plan JSON returned by the plan command, not its id ${value.trim()}.`, {
|
|
22823
|
+
details: {
|
|
22824
|
+
plan_id: value.trim(),
|
|
22825
|
+
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.",
|
|
22826
|
+
},
|
|
22827
|
+
exitCode: 1,
|
|
22828
|
+
});
|
|
22829
|
+
}
|
|
22830
|
+
const parsed = parseJsonObject(raw);
|
|
22781
22831
|
const data = parsed.data;
|
|
22782
22832
|
return data && typeof data === "object" && !Array.isArray(data)
|
|
22783
22833
|
? data
|
|
@@ -22785,7 +22835,7 @@ function readSearchPlanJson(value) {
|
|
|
22785
22835
|
}
|
|
22786
22836
|
// Table-driven Employee Finder help, written once and shared by `people search
|
|
22787
22837
|
// 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
|
|
22838
|
+
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
22839
|
const PEOPLE_SEARCH_LINKEDIN_COLUMN_HELP = "Column key in --from-table holding each company's LinkedIn URL.";
|
|
22790
22840
|
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
22841
|
const PEOPLE_SEARCH_COMPANY_LIMIT_HELP = "Fetch people for at most this many companies from --from-table. Start with --company-limit 1.";
|
|
@@ -25302,10 +25352,24 @@ async function handleOrgUseAction(organization, options, command) {
|
|
|
25302
25352
|
credentials: context.resolution.credentials,
|
|
25303
25353
|
organization: data.currentOrganization,
|
|
25304
25354
|
});
|
|
25355
|
+
// OXYGEN_ORG outranks the stored selection on every later command (the
|
|
25356
|
+
// oxygen-dev wrapper exports one), so a selection it contradicts is saved
|
|
25357
|
+
// but never used. Reported 2026-09-22: three "successful" selects in a row.
|
|
25358
|
+
const envOrganization = process.env.OXYGEN_ORG?.trim();
|
|
25359
|
+
const selected = data.currentOrganization;
|
|
25360
|
+
const environmentOverride = envOrganization && selected
|
|
25361
|
+
&& envOrganization !== selected.id && envOrganization !== selected.slug
|
|
25362
|
+
? {
|
|
25363
|
+
variable: "OXYGEN_ORG",
|
|
25364
|
+
organization: envOrganization,
|
|
25365
|
+
message: `OXYGEN_ORG=${envOrganization} is set, so later commands still use that workspace. Unset it, set it to ${selected.slug ?? selected.id}, or pass --org.`,
|
|
25366
|
+
}
|
|
25367
|
+
: null;
|
|
25305
25368
|
const result = {
|
|
25306
25369
|
...data,
|
|
25307
25370
|
profile,
|
|
25308
25371
|
selection_persisted: selectionPersisted,
|
|
25372
|
+
...(environmentOverride ? { environment_override: environmentOverride } : {}),
|
|
25309
25373
|
};
|
|
25310
25374
|
if (options.json) {
|
|
25311
25375
|
writeJson(success(command, result));
|
|
@@ -25314,6 +25378,8 @@ async function handleOrgUseAction(organization, options, command) {
|
|
|
25314
25378
|
writeJson(result);
|
|
25315
25379
|
}
|
|
25316
25380
|
writeCredentialNotice(data.credential);
|
|
25381
|
+
if (environmentOverride)
|
|
25382
|
+
process.stderr.write(`${environmentOverride.message}\n`);
|
|
25317
25383
|
}
|
|
25318
25384
|
catch (error) {
|
|
25319
25385
|
emitCliFailure(command, error);
|
|
@@ -27798,7 +27864,7 @@ function formatCrmEnrichment(data) {
|
|
|
27798
27864
|
return lines.join("\n");
|
|
27799
27865
|
}
|
|
27800
27866
|
function pushCrmEnrichmentObject(lines, styles, view) {
|
|
27801
|
-
lines.push("", `${styles.bold(`Automatic enrichment — ${view.object}`)} ${view
|
|
27867
|
+
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
27868
|
const labelWidth = Math.max(...view.presets.map((preset) => preset.label.length), 0);
|
|
27803
27869
|
const armed = new Set(view.presets.filter((preset) => preset.enabled).map((preset) => preset.id));
|
|
27804
27870
|
for (const preset of view.presets) {
|
|
@@ -27820,7 +27886,10 @@ function pushCrmEnrichmentObject(lines, styles, view) {
|
|
|
27820
27886
|
if (view.custom_columns.length > 0) {
|
|
27821
27887
|
// A hand-tuned config carries columns no preset owns, so the per-record
|
|
27822
27888
|
// figure above can be a floor rather than the bill.
|
|
27823
|
-
|
|
27889
|
+
// Uncosted only when one of them may bill; a formula needs no run and is free.
|
|
27890
|
+
const note = view.custom_columns_uncosted
|
|
27891
|
+
? ` ${styles.yellow("(cost not included above)")}`
|
|
27892
|
+
: ` ${styles.dim("(free: no run needed)")}`;
|
|
27824
27893
|
lines.push(` ${styles.dim("Extra".padEnd(summaryWidth))} ${view.custom_columns.join(", ")}${note}`);
|
|
27825
27894
|
}
|
|
27826
27895
|
// The half the config alone cannot show: an armed set says nothing about the
|
|
@@ -27831,12 +27900,6 @@ function pushCrmEnrichmentObject(lines, styles, view) {
|
|
|
27831
27900
|
if (backfillLine) {
|
|
27832
27901
|
lines.push(` ${styles.dim("Not enriched".padEnd(summaryWidth))} ${backfillLine}`);
|
|
27833
27902
|
}
|
|
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
27903
|
if (view.changed) {
|
|
27841
27904
|
lines.push("", ...formatCrmEnrichmentChange(view.changed, styles));
|
|
27842
27905
|
}
|
|
@@ -27882,9 +27945,23 @@ function pushCrmEnrichmentPreset(lines, styles, preset, labelWidth, armed) {
|
|
|
27882
27945
|
}
|
|
27883
27946
|
}
|
|
27884
27947
|
function formatCrmEnrichmentCredits(credits, styles, unit) {
|
|
27885
|
-
|
|
27886
|
-
|
|
27887
|
-
|
|
27948
|
+
if (credits === 0)
|
|
27949
|
+
return styles.green("free");
|
|
27950
|
+
// "1 credits/record" read wrong in a blind pass (2026-10-04); the unit is
|
|
27951
|
+
// "credits" or "credits/record", so singularize its leading word.
|
|
27952
|
+
const label = credits === 1 ? unit.replace(/^credits\b/, "credit") : unit;
|
|
27953
|
+
return styles.yellow(`${credits.toLocaleString("en-US")} ${label}`);
|
|
27954
|
+
}
|
|
27955
|
+
// "on" alone reads as "the enrichments listed below are on" even when every one
|
|
27956
|
+
// of them is off and only a hand-added column is armed.
|
|
27957
|
+
function formatCrmEnrichmentState(view, styles) {
|
|
27958
|
+
if (!view.enabled)
|
|
27959
|
+
return styles.dim("off");
|
|
27960
|
+
const anyPreset = view.presets.some((preset) => preset.enabled);
|
|
27961
|
+
if (!anyPreset && view.custom_columns.length > 0) {
|
|
27962
|
+
return `${styles.green("on")} ${styles.dim("— your own columns only")}`;
|
|
27963
|
+
}
|
|
27964
|
+
return styles.green("on");
|
|
27888
27965
|
}
|
|
27889
27966
|
function formatCrmEnrichmentCeiling(view, styles) {
|
|
27890
27967
|
if (view.max_credits === null)
|
|
@@ -27892,7 +27969,8 @@ function formatCrmEnrichmentCeiling(view, styles) {
|
|
|
27892
27969
|
const source = view.max_credits_source === "explicit"
|
|
27893
27970
|
? "you set this"
|
|
27894
27971
|
: "your plan's per-delivery default";
|
|
27895
|
-
|
|
27972
|
+
const unit = view.max_credits === 1 ? "credit" : "credits";
|
|
27973
|
+
return `${view.max_credits.toLocaleString("en-US")} ${unit} ${styles.dim(`(${source})`)}`;
|
|
27896
27974
|
}
|
|
27897
27975
|
function formatCrmEnrichmentSources(sources) {
|
|
27898
27976
|
if (sources.length === 0)
|
|
@@ -29780,6 +29858,9 @@ function readSequenceSettings(options) {
|
|
|
29780
29858
|
if (options.whatsappColdInitiate === true) {
|
|
29781
29859
|
settings.whatsapp_cold_initiate = true;
|
|
29782
29860
|
}
|
|
29861
|
+
const whatsappDuplicateScope = readOption(options.whatsappDuplicateScope);
|
|
29862
|
+
if (whatsappDuplicateScope)
|
|
29863
|
+
settings.whatsapp_duplicate_scope = whatsappDuplicateScope;
|
|
29783
29864
|
const phoneColumnKey = readOption(options.phoneColumnKey);
|
|
29784
29865
|
if (phoneColumnKey) {
|
|
29785
29866
|
settings.phone_column_key = phoneColumnKey;
|
|
@@ -29964,6 +30045,12 @@ catch (error) {
|
|
|
29964
30045
|
writeJson(usage.envelope);
|
|
29965
30046
|
process.exitCode = usage?.exitCode ?? error.exitCode;
|
|
29966
30047
|
}
|
|
30048
|
+
else if (error instanceof OxygenError) {
|
|
30049
|
+
// An option a command reads before its handleAsyncAction (`tables auto-run
|
|
30050
|
+
// set --max-credits 0` -> readPositiveNumber) throws outside every handler,
|
|
30051
|
+
// and used to escape as a raw Node stack with no envelope, even with --json.
|
|
30052
|
+
emitCliFailure(commandNameForArgv(program, argv.slice(2)), error);
|
|
30053
|
+
}
|
|
29967
30054
|
else {
|
|
29968
30055
|
throw error;
|
|
29969
30056
|
}
|