@oxygen-agent/cli 1.575.19 → 1.610.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/command-manifest.d.ts +18 -0
- package/dist/command-manifest.js +106 -5
- package/dist/help.js +3 -1
- package/dist/index.js +722 -138
- package/dist/skills.d.ts +6 -0
- package/dist/skills.js +200 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +36 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +766 -0
- package/node_modules/@oxygen/shared/dist/cli-result.js +4 -3
- package/node_modules/@oxygen/shared/dist/file-import.js +58 -11
- package/node_modules/@oxygen/shared/dist/index.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/index.js +2 -0
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
- package/node_modules/@oxygen/shared/dist/recipes.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/recipes.js +4 -2
- package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
- package/node_modules/@oxygen/shared/dist/sequence-terminal-events.d.ts +47 -0
- package/node_modules/@oxygen/shared/dist/sequence-terminal-events.js +70 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/workflows/dist/graph/lint.js +3 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +5 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +5 -0
- package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +2 -0
- package/node_modules/@oxygen/workflows/dist/index.d.ts +4 -0
- package/node_modules/@oxygen/workflows/dist/index.js +5 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,8 +8,8 @@ import { stdin as input, stdout as output } from "node:process";
|
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
9
|
import { Command, CommanderError, Option } from "commander";
|
|
10
10
|
import { applyOxygenHelp } from "./help.js";
|
|
11
|
-
import { buildCommandManifest } from "./command-manifest.js";
|
|
12
|
-
import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, describeWorkflowStatusChange, formatCellForDisplay, formatPublicBudgetScopes, exitCodeForOxygenError, parseWorkflowStatusChange, isVersionGreater, isVersionLess, MAX_MCP_TOOL_NAME_LENGTH, OXYGEN_VERSION, OxygenError, parseKnowledgePageMarkdown, PLAN_LIMITS, sleep, success, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
|
|
11
|
+
import { buildCommandManifest, getCommandManifestEntry, searchCommandManifest, suggestCommandNames, } from "./command-manifest.js";
|
|
12
|
+
import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, describeWorkflowStatusChange, formatCellForDisplay, formatPublicBudgetScopes, exitCodeForOxygenError, parseWorkflowStatusChange, isVersionGreater, isVersionLess, MAX_MCP_TOOL_NAME_LENGTH, OXYGEN_CAPABILITY_ROUTES, OXYGEN_VERSION, OxygenError, getCapabilityRouteMatch, inferCapabilityRoute, parseKnowledgePageMarkdown, PLAN_LIMITS, serializeCapabilityRoute, sleep, success, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
|
|
13
13
|
import { TAG_COLORS } from "@oxygen/shared/select-options";
|
|
14
14
|
import { inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, } from "@oxygen/shared/file-import";
|
|
15
15
|
import { assertRecipeBundleSafe, assertWorkflowGraphManifest, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowGraphManifest, isWorkflowManifest, } from "@oxygen/workflows";
|
|
@@ -24,7 +24,7 @@ import { formatAiPromptPreviewNotice } from "./column-run-notices.js";
|
|
|
24
24
|
import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
|
|
25
25
|
import { captureCurrentTranscript, collectFeedbackEnvironment, TranscriptCaptureError, } from "./transcript.js";
|
|
26
26
|
import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, updateSessionStep, } from "./session.js";
|
|
27
|
-
import { doctorAgentSkills, installAgentSkills, listAgentSkills, runAutomaticSkillsInstall, } from "./skills.js";
|
|
27
|
+
import { doctorAgentSkills, getAgentSkill, installAgentSkills, listAgentSkills, runAutomaticSkillsInstall, searchAgentSkills, } from "./skills.js";
|
|
28
28
|
import { resolveCliBinaryName } from "./runtime.js";
|
|
29
29
|
import { updateCli } from "./update.js";
|
|
30
30
|
import { isRecord, readErrorMessage, readOption } from "./util.js";
|
|
@@ -289,6 +289,7 @@ async function handleAsyncAction(command, options, action) {
|
|
|
289
289
|
try {
|
|
290
290
|
const data = await action();
|
|
291
291
|
emitSuccess(command, data, options);
|
|
292
|
+
writeDryRunNotice(data);
|
|
292
293
|
writeCreditsReceipt(data);
|
|
293
294
|
}
|
|
294
295
|
catch (error) {
|
|
@@ -304,6 +305,24 @@ function emitCliFailure(command, error) {
|
|
|
304
305
|
writeMaxCreditsHint(error);
|
|
305
306
|
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
306
307
|
}
|
|
308
|
+
// A dry run's stdout envelope looks like a successful result — same shape, same
|
|
309
|
+
// `ok: true` — so in a terminal the only tell that nothing was fetched was
|
|
310
|
+
// `meta.mode` buried inside the payload. That is how a working provider key gets
|
|
311
|
+
// reported as broken: the preview comes back empty and reads as a failed live
|
|
312
|
+
// call. Mirror the server's preview banner on stderr, the same stdout/stderr
|
|
313
|
+
// split the credits receipt uses, so the machine-read envelope stays clean.
|
|
314
|
+
function writeDryRunNotice(data) {
|
|
315
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
316
|
+
return;
|
|
317
|
+
const preview = data.preview;
|
|
318
|
+
if (!preview || typeof preview !== "object" || Array.isArray(preview))
|
|
319
|
+
return;
|
|
320
|
+
const block = preview;
|
|
321
|
+
if (typeof block.message === "string")
|
|
322
|
+
process.stderr.write(`${block.message}\n`);
|
|
323
|
+
if (typeof block.next_step === "string")
|
|
324
|
+
process.stderr.write(`${block.next_step}\n`);
|
|
325
|
+
}
|
|
307
326
|
// Paid envelopes (push 3 legibility) carry a `credits` block: quote on
|
|
308
327
|
// dry_run, receipt on live, remaining balance on both. Mirror it as one
|
|
309
328
|
// stderr line so spend stays visible in a terminal without polluting the
|
|
@@ -639,99 +658,217 @@ function readJsonFileValue(path, inputName) {
|
|
|
639
658
|
/**
|
|
640
659
|
* Whitelist mailbox import fields before the local file crosses the network.
|
|
641
660
|
* In particular, a credential cannot be silently sent through the ordinary
|
|
642
|
-
* inline path: the caller must choose --from
|
|
643
|
-
* the encrypted transfer-vault contract.
|
|
661
|
+
* inline path: the caller must choose --from credentials (or the Hypertide
|
|
662
|
+
* shortcut) so the server applies the encrypted transfer-vault contract.
|
|
644
663
|
*/
|
|
645
|
-
function normalizeMailboxImportFile(value,
|
|
664
|
+
function normalizeMailboxImportFile(value, mode) {
|
|
646
665
|
if (!Array.isArray(value) || value.length === 0) {
|
|
647
666
|
throw new OxygenError("invalid_request", "--file must contain a non-empty mailboxes array.", { exitCode: 1 });
|
|
648
667
|
}
|
|
649
|
-
|
|
668
|
+
const rows = value.map((entry, index) => {
|
|
650
669
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
651
670
|
throw new OxygenError("invalid_request", `mailboxes[${index}] must be an object.`, { exitCode: 1 });
|
|
652
671
|
}
|
|
653
|
-
|
|
654
|
-
const row = source === "hypertide"
|
|
655
|
-
? normalizeHypertideMailboxExportRow(rawRow, index)
|
|
656
|
-
: rawRow;
|
|
657
|
-
const hasCredential = typeof row.app_password === "string" || typeof row.password === "string";
|
|
658
|
-
if (source === "inline" && hasCredential) {
|
|
659
|
-
throw new OxygenError("invalid_request", "Mailbox credentials require --from hypertide; the ordinary inline import never accepts or forwards passwords.", { exitCode: 1 });
|
|
660
|
-
}
|
|
661
|
-
return {
|
|
662
|
-
...(typeof row.email_address === "string"
|
|
663
|
-
? { email_address: row.email_address }
|
|
664
|
-
: {}),
|
|
665
|
-
...(typeof row.provider === "string" ? { provider: row.provider } : {}),
|
|
666
|
-
...(typeof row.workspace_external_id === "string"
|
|
667
|
-
? { workspace_external_id: row.workspace_external_id }
|
|
668
|
-
: {}),
|
|
669
|
-
...(source === "hypertide" && typeof row.app_password === "string"
|
|
670
|
-
? { app_password: row.app_password }
|
|
671
|
-
: {}),
|
|
672
|
-
};
|
|
672
|
+
return normalizeMailboxExportRow(entry, index, mode);
|
|
673
673
|
});
|
|
674
|
+
return dedupeMailboxImportRows(rows);
|
|
675
|
+
}
|
|
676
|
+
function summarizeMailboxImportValidation(mailboxes, input) {
|
|
677
|
+
const providers = { google: 0, microsoft: 0 };
|
|
678
|
+
const infrastructurePlatforms = {};
|
|
679
|
+
let credentialRows = 0;
|
|
680
|
+
for (const mailbox of mailboxes) {
|
|
681
|
+
if (mailbox.provider === "google")
|
|
682
|
+
providers.google += 1;
|
|
683
|
+
if (mailbox.provider === "microsoft")
|
|
684
|
+
providers.microsoft += 1;
|
|
685
|
+
if (typeof mailbox.infrastructure_platform === "string") {
|
|
686
|
+
const platform = mailbox.infrastructure_platform;
|
|
687
|
+
infrastructurePlatforms[platform] =
|
|
688
|
+
(infrastructurePlatforms[platform] ?? 0) + 1;
|
|
689
|
+
}
|
|
690
|
+
if (typeof mailbox.app_password === "string")
|
|
691
|
+
credentialRows += 1;
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
valid: true,
|
|
695
|
+
source: input.source,
|
|
696
|
+
...(input.sourceProvider
|
|
697
|
+
? { source_provider: input.sourceProvider }
|
|
698
|
+
: {}),
|
|
699
|
+
rows: mailboxes.length,
|
|
700
|
+
providers,
|
|
701
|
+
infrastructure_platforms: infrastructurePlatforms,
|
|
702
|
+
credential_rows: credentialRows,
|
|
703
|
+
identity_only_rows: mailboxes.length - credentialRows,
|
|
704
|
+
limits: {
|
|
705
|
+
max_rows: MAILBOX_IMPORT_ROW_LIMIT,
|
|
706
|
+
max_file_bytes: MAILBOX_IMPORT_FILE_MAX_BYTES,
|
|
707
|
+
},
|
|
708
|
+
mutation: false,
|
|
709
|
+
network_request: false,
|
|
710
|
+
provider_call: false,
|
|
711
|
+
credits_used: 0,
|
|
712
|
+
next_action: "Re-run the same command without --validate-only to register these mailboxes.",
|
|
713
|
+
};
|
|
674
714
|
}
|
|
675
|
-
const
|
|
715
|
+
const MAILBOX_EMAIL_HEADERS = new Set([
|
|
676
716
|
"address",
|
|
677
717
|
"email",
|
|
678
718
|
"emailaddress",
|
|
719
|
+
"fromemail",
|
|
679
720
|
"mailbox",
|
|
680
721
|
"mailboxaddress",
|
|
681
722
|
"mailboxemail",
|
|
682
723
|
]);
|
|
683
|
-
const
|
|
724
|
+
const MAILBOX_PROVIDER_HEADERS = new Set([
|
|
684
725
|
"emailprovider",
|
|
726
|
+
"esp",
|
|
685
727
|
"mailboxprovider",
|
|
686
728
|
"platform",
|
|
687
729
|
"provider",
|
|
688
730
|
"serviceprovider",
|
|
689
731
|
"type",
|
|
690
732
|
]);
|
|
691
|
-
const
|
|
733
|
+
const MAILBOX_PASSWORD_HEADERS = new Set([
|
|
692
734
|
"apppassword",
|
|
735
|
+
"applicationpassword",
|
|
736
|
+
"googleapppassword",
|
|
693
737
|
"imappassword",
|
|
694
738
|
"mailboxpassword",
|
|
695
739
|
"password",
|
|
696
740
|
"smtppassword",
|
|
697
741
|
]);
|
|
698
|
-
const
|
|
742
|
+
const MAILBOX_WORKSPACE_HEADERS = new Set([
|
|
699
743
|
"externalaccountid",
|
|
744
|
+
"mailboxid",
|
|
745
|
+
"mailboxuid",
|
|
746
|
+
"sourceaccountid",
|
|
747
|
+
"sourcemailboxid",
|
|
748
|
+
"uid",
|
|
700
749
|
"workspaceexternalid",
|
|
701
750
|
]);
|
|
751
|
+
const MAILBOX_PLATFORM_HEADERS = new Set([
|
|
752
|
+
"infrastructure",
|
|
753
|
+
"infrastructureplatform",
|
|
754
|
+
"infraplatform",
|
|
755
|
+
]);
|
|
756
|
+
const MAILBOX_TENANT_HEADERS = new Set([
|
|
757
|
+
"azuretenantid",
|
|
758
|
+
"entratenantid",
|
|
759
|
+
"microsofttenantid",
|
|
760
|
+
"tenantid",
|
|
761
|
+
]);
|
|
762
|
+
const MAILBOX_NON_TRANSFERABLE_SECRET_HEADERS = new Set([
|
|
763
|
+
"accesstoken",
|
|
764
|
+
"applicationsecret",
|
|
765
|
+
"authenticatorsecret",
|
|
766
|
+
"authorization",
|
|
767
|
+
"authorizationcode",
|
|
768
|
+
"bearertoken",
|
|
769
|
+
"clientsecret",
|
|
770
|
+
"clientprivatekey",
|
|
771
|
+
"delegationkey",
|
|
772
|
+
"idtoken",
|
|
773
|
+
"mfacode",
|
|
774
|
+
"mfasecret",
|
|
775
|
+
"oauthaccesstoken",
|
|
776
|
+
"oauthcode",
|
|
777
|
+
"oauthrefreshtoken",
|
|
778
|
+
"oauthtoken",
|
|
779
|
+
"onetimepassword",
|
|
780
|
+
"otp",
|
|
781
|
+
"otpsecret",
|
|
782
|
+
"privatekey",
|
|
783
|
+
"refreshtoken",
|
|
784
|
+
"serviceaccountkey",
|
|
785
|
+
"serviceaccountjson",
|
|
786
|
+
"serviceaccountprivatekey",
|
|
787
|
+
"totp",
|
|
788
|
+
"totpsecret",
|
|
789
|
+
]);
|
|
702
790
|
const MAILBOX_IMPORT_FILE_MAX_BYTES = 5 * 1024 * 1024;
|
|
703
791
|
const MAILBOX_IMPORT_ROW_LIMIT = 500;
|
|
704
792
|
/**
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
* are
|
|
710
|
-
*
|
|
711
|
-
* boundary; Oxygen derives the standard Google/Microsoft endpoints itself.
|
|
793
|
+
* Normalize common mailbox-vendor export labels locally, then send only
|
|
794
|
+
* Oxygen's canonical fields. Passwords are accepted only in credential mode
|
|
795
|
+
* and only become Google app passwords; Microsoft passwords are dropped because
|
|
796
|
+
* tenant consent is its only warmup path. OAuth grants, MFA/TOTP seeds, and
|
|
797
|
+
* delegation keys are rejected in every mode. Host columns may prove the
|
|
798
|
+
* provider but never cross the request boundary.
|
|
712
799
|
*/
|
|
713
|
-
function
|
|
800
|
+
function normalizeMailboxExportRow(row, index, mode) {
|
|
714
801
|
const byHeader = new Map();
|
|
715
802
|
for (const [header, value] of Object.entries(row)) {
|
|
716
|
-
|
|
803
|
+
const normalizedHeader = normalizeMailboxExportHeader(header);
|
|
804
|
+
const existing = byHeader.get(normalizedHeader);
|
|
805
|
+
if (existing !== undefined &&
|
|
806
|
+
String(existing).trim() !== String(value ?? "").trim()) {
|
|
807
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting columns that normalize to ${normalizedHeader}.`, { exitCode: 1 });
|
|
808
|
+
}
|
|
809
|
+
byHeader.set(normalizedHeader, value);
|
|
810
|
+
}
|
|
811
|
+
const email = readUniqueMailboxExportString(byHeader, MAILBOX_EMAIL_HEADERS, index, "email_address", (value) => value.trim().toLowerCase()) ?? readEmailShapedMailboxUsername(byHeader);
|
|
812
|
+
const providerValues = readMailboxExportStrings(byHeader, MAILBOX_PROVIDER_HEADERS);
|
|
813
|
+
const provider = normalizeMailboxProvider(providerValues, byHeader, index);
|
|
814
|
+
if (!email ||
|
|
815
|
+
email.length > 320 ||
|
|
816
|
+
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
817
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].email_address must be a valid address of at most 320 characters.`, { exitCode: 1 });
|
|
717
818
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
819
|
+
if (!provider) {
|
|
820
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].provider could not be inferred. Oxygen imports Google Workspace and Microsoft 365/Entra identities; generic SMTP credentials are not transferable yet.`, { exitCode: 1 });
|
|
821
|
+
}
|
|
822
|
+
if (hasMailboxExportValue(byHeader, MAILBOX_NON_TRANSFERABLE_SECRET_HEADERS) ||
|
|
823
|
+
hasNonTransferableMailboxAuthLikeValue(byHeader)) {
|
|
824
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}] contains OAuth, MFA/TOTP, client-secret, or delegation material. Those credentials are never transferable; import the mailbox identity and complete the provider's administrator consent flow.`, { exitCode: 1 });
|
|
825
|
+
}
|
|
826
|
+
const passwords = readMailboxExportStrings(byHeader, MAILBOX_PASSWORD_HEADERS);
|
|
723
827
|
const distinctPasswords = [...new Set(passwords)];
|
|
828
|
+
if (distinctPasswords.some((password) => password.length > 1024)) {
|
|
829
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].app_password must be at most 1024 characters.`, { exitCode: 1 });
|
|
830
|
+
}
|
|
831
|
+
if (mode === "identity" && distinctPasswords.length > 0) {
|
|
832
|
+
throw new OxygenError("invalid_request", "Mailbox credentials require --from credentials --vendor <source> (or --from hypertide for a Hypertide export); keep --validate-only for a no-network preflight. An identity import never accepts or forwards passwords.", { exitCode: 1 });
|
|
833
|
+
}
|
|
724
834
|
if (provider !== "microsoft" && distinctPasswords.length > 1) {
|
|
725
835
|
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting SMTP/IMAP/app-password values.`, { exitCode: 1 });
|
|
726
836
|
}
|
|
727
|
-
|
|
837
|
+
if (mode === "credential" &&
|
|
838
|
+
provider === "google" &&
|
|
839
|
+
distinctPasswords.length === 0) {
|
|
840
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}] needs a Google app password for credential import. OAuth tokens and an ordinary account password are not portable.`, { exitCode: 1 });
|
|
841
|
+
}
|
|
842
|
+
const workspaceExternalId = readUniqueMailboxExportString(byHeader, MAILBOX_WORKSPACE_HEADERS, index, "workspace_external_id", (value) => value.trim());
|
|
843
|
+
if (workspaceExternalId && workspaceExternalId.length > 512) {
|
|
844
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].workspace_external_id must be at most 512 characters.`, { exitCode: 1 });
|
|
845
|
+
}
|
|
846
|
+
const tenantId = readUniqueMailboxExportString(byHeader, MAILBOX_TENANT_HEADERS, index, "tenant_id", (value) => value.trim().toLowerCase());
|
|
847
|
+
if (tenantId && provider !== "microsoft") {
|
|
848
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].tenant_id applies only to Microsoft mailboxes.`, { exitCode: 1 });
|
|
849
|
+
}
|
|
850
|
+
if (tenantId &&
|
|
851
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tenantId.trim())) {
|
|
852
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].tenant_id must be a Microsoft Entra tenant GUID.`, { exitCode: 1 });
|
|
853
|
+
}
|
|
854
|
+
const infrastructurePlatformRaw = readUniqueMailboxExportString(byHeader, MAILBOX_PLATFORM_HEADERS, index, "infrastructure_platform", (value) => normalizeMailboxExportHeader(value)) ??
|
|
855
|
+
(providerValues.some((value) => ["azure", "entra", "microsoftazure"].includes(normalizeMailboxExportHeader(value)))
|
|
856
|
+
? "microsoft_azure"
|
|
857
|
+
: null);
|
|
858
|
+
const infrastructurePlatform = normalizeMailboxInfrastructurePlatform(infrastructurePlatformRaw, provider, index);
|
|
728
859
|
return {
|
|
729
|
-
|
|
730
|
-
|
|
860
|
+
email_address: email.trim().toLowerCase(),
|
|
861
|
+
provider,
|
|
731
862
|
...(workspaceExternalId !== null
|
|
732
|
-
? { workspace_external_id: workspaceExternalId }
|
|
863
|
+
? { workspace_external_id: workspaceExternalId.trim() }
|
|
733
864
|
: {}),
|
|
734
|
-
...(
|
|
865
|
+
...(infrastructurePlatformRaw
|
|
866
|
+
? { infrastructure_platform: infrastructurePlatform }
|
|
867
|
+
: {}),
|
|
868
|
+
...(tenantId ? { tenant_id: tenantId.trim().toLowerCase() } : {}),
|
|
869
|
+
...(mode === "credential" &&
|
|
870
|
+
provider === "google" &&
|
|
871
|
+
distinctPasswords[0] !== undefined
|
|
735
872
|
? { app_password: distinctPasswords[0] }
|
|
736
873
|
: {}),
|
|
737
874
|
};
|
|
@@ -752,6 +889,16 @@ function readMailboxExportString(byHeader, headers) {
|
|
|
752
889
|
}
|
|
753
890
|
return null;
|
|
754
891
|
}
|
|
892
|
+
function readUniqueMailboxExportString(byHeader, headers, index, field, normalize) {
|
|
893
|
+
const values = readMailboxExportStrings(byHeader, headers);
|
|
894
|
+
if (values.length === 0)
|
|
895
|
+
return null;
|
|
896
|
+
const normalized = [...new Set(values.map(normalize))];
|
|
897
|
+
if (normalized.length > 1) {
|
|
898
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting ${field} aliases.`, { exitCode: 1 });
|
|
899
|
+
}
|
|
900
|
+
return normalized[0] ?? null;
|
|
901
|
+
}
|
|
755
902
|
function readMailboxExportStrings(byHeader, headers) {
|
|
756
903
|
const values = [];
|
|
757
904
|
for (const header of headers) {
|
|
@@ -761,24 +908,115 @@ function readMailboxExportStrings(byHeader, headers) {
|
|
|
761
908
|
}
|
|
762
909
|
return values;
|
|
763
910
|
}
|
|
911
|
+
function hasMailboxExportValue(byHeader, headers) {
|
|
912
|
+
for (const header of headers) {
|
|
913
|
+
const value = byHeader.get(header);
|
|
914
|
+
if (value !== undefined && value !== null && String(value).trim()) {
|
|
915
|
+
return true;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
function hasNonTransferableMailboxAuthLikeValue(byHeader) {
|
|
921
|
+
for (const [header, value] of byHeader) {
|
|
922
|
+
if (/(?:token|secret|privatekey|totp|passcode|authorization)/.test(header) &&
|
|
923
|
+
value !== undefined &&
|
|
924
|
+
value !== null &&
|
|
925
|
+
String(value).trim()) {
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
764
931
|
function readEmailShapedMailboxUsername(byHeader) {
|
|
765
932
|
for (const header of ["username", "imapusername", "smtpusername"]) {
|
|
766
933
|
const value = byHeader.get(header);
|
|
767
934
|
if (typeof value === "string" && value.includes("@"))
|
|
768
|
-
return value;
|
|
935
|
+
return value.trim().toLowerCase();
|
|
769
936
|
}
|
|
770
937
|
return null;
|
|
771
938
|
}
|
|
772
|
-
function
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
939
|
+
function normalizeMailboxProvider(providerValues, byHeader, index) {
|
|
940
|
+
const explicit = providerValues.map((value) => {
|
|
941
|
+
const normalized = normalizeMailboxExportHeader(value);
|
|
942
|
+
if (["google", "googleworkspace", "gmail", "gsuite"].includes(normalized)) {
|
|
943
|
+
return "google";
|
|
944
|
+
}
|
|
945
|
+
if ([
|
|
946
|
+
"azure",
|
|
947
|
+
"entra",
|
|
948
|
+
"m365",
|
|
949
|
+
"microsoft",
|
|
950
|
+
"microsoft365",
|
|
951
|
+
"microsoftazure",
|
|
952
|
+
"ms365",
|
|
953
|
+
"o365",
|
|
954
|
+
"office365",
|
|
955
|
+
"outlook",
|
|
956
|
+
].includes(normalized)) {
|
|
957
|
+
return "microsoft";
|
|
958
|
+
}
|
|
959
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].provider is unsupported; expected Google/Gmail/Workspace or Microsoft/Outlook/Office 365/Entra.`, { exitCode: 1 });
|
|
960
|
+
});
|
|
961
|
+
const explicitKinds = [...new Set(explicit)];
|
|
962
|
+
if (explicitKinds.length > 1) {
|
|
963
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting provider aliases.`, { exitCode: 1 });
|
|
964
|
+
}
|
|
965
|
+
const hostValues = [
|
|
966
|
+
byHeader.get("imaphost"),
|
|
967
|
+
byHeader.get("imaphostname"),
|
|
968
|
+
byHeader.get("imapserver"),
|
|
969
|
+
byHeader.get("smtphost"),
|
|
970
|
+
byHeader.get("smtphostname"),
|
|
971
|
+
byHeader.get("smtpserver"),
|
|
972
|
+
].filter((value) => typeof value === "string" && Boolean(value.trim()));
|
|
973
|
+
const hostKinds = hostValues.map((value) => {
|
|
974
|
+
const hostname = normalizeMailboxHost(value);
|
|
975
|
+
if (hostname === "smtp.gmail.com" || hostname === "imap.gmail.com") {
|
|
976
|
+
return "google";
|
|
977
|
+
}
|
|
978
|
+
if (hostname === "smtp.office365.com" ||
|
|
979
|
+
hostname === "outlook.office365.com") {
|
|
980
|
+
return "microsoft";
|
|
981
|
+
}
|
|
982
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}] contains a non-standard SMTP/IMAP host. Oxygen imports Google Workspace and Microsoft 365/Entra identities; generic SMTP credential transport is not supported.`, { exitCode: 1 });
|
|
983
|
+
});
|
|
984
|
+
const inferredKinds = [...new Set(hostKinds)];
|
|
985
|
+
if (inferredKinds.length > 1) {
|
|
986
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}] contains conflicting Google and Microsoft mail hosts.`, { exitCode: 1 });
|
|
987
|
+
}
|
|
988
|
+
const explicitKind = explicitKinds[0];
|
|
989
|
+
const inferredKind = inferredKinds[0];
|
|
990
|
+
if (explicitKind && inferredKind && explicitKind !== inferredKind) {
|
|
991
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}] has conflicting provider and SMTP/IMAP host evidence.`, { exitCode: 1 });
|
|
992
|
+
}
|
|
993
|
+
return explicitKind ?? inferredKind ?? null;
|
|
994
|
+
}
|
|
995
|
+
function normalizeMailboxHost(value) {
|
|
996
|
+
const withoutScheme = value
|
|
997
|
+
.trim()
|
|
998
|
+
.toLowerCase()
|
|
999
|
+
.replace(/^[a-z]+:\/\//, "");
|
|
1000
|
+
return withoutScheme.split(/[/:]/, 1)[0] ?? "";
|
|
1001
|
+
}
|
|
1002
|
+
function normalizeMailboxInfrastructurePlatform(raw, provider, index) {
|
|
1003
|
+
if (!raw) {
|
|
1004
|
+
return provider === "google" ? "google_workspace" : "microsoft_365";
|
|
1005
|
+
}
|
|
1006
|
+
const normalized = normalizeMailboxExportHeader(raw);
|
|
1007
|
+
if (["google", "googleworkspace", "gmail", "gsuite", "workspace"].includes(normalized)) {
|
|
1008
|
+
if (provider !== "google") {
|
|
1009
|
+
throw mailboxPlatformConflict(index, provider);
|
|
1010
|
+
}
|
|
1011
|
+
return "google_workspace";
|
|
1012
|
+
}
|
|
1013
|
+
if (["azure", "entra", "microsoftazure"].includes(normalized)) {
|
|
1014
|
+
if (provider !== "microsoft") {
|
|
1015
|
+
throw mailboxPlatformConflict(index, provider);
|
|
1016
|
+
}
|
|
1017
|
+
return "microsoft_azure";
|
|
778
1018
|
}
|
|
779
1019
|
if ([
|
|
780
|
-
"azure",
|
|
781
|
-
"entra",
|
|
782
1020
|
"m365",
|
|
783
1021
|
"microsoft",
|
|
784
1022
|
"microsoft365",
|
|
@@ -787,25 +1025,50 @@ function normalizeHypertideMailboxProvider(providerRaw, byHeader, index) {
|
|
|
787
1025
|
"office365",
|
|
788
1026
|
"outlook",
|
|
789
1027
|
].includes(normalized)) {
|
|
790
|
-
|
|
1028
|
+
if (provider !== "microsoft") {
|
|
1029
|
+
throw mailboxPlatformConflict(index, provider);
|
|
1030
|
+
}
|
|
1031
|
+
return "microsoft_365";
|
|
791
1032
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
.toLowerCase();
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1033
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].infrastructure_platform is unsupported; expected Google Workspace, Microsoft 365, Azure, or Entra.`, { exitCode: 1 });
|
|
1034
|
+
}
|
|
1035
|
+
function mailboxPlatformConflict(index, provider) {
|
|
1036
|
+
return new OxygenError("invalid_request", `mailboxes[${index}].infrastructure_platform conflicts with provider=${provider}.`, { exitCode: 1 });
|
|
1037
|
+
}
|
|
1038
|
+
function dedupeMailboxImportRows(rows) {
|
|
1039
|
+
const unique = new Map();
|
|
1040
|
+
for (const row of rows) {
|
|
1041
|
+
const address = String(row.email_address).toLowerCase();
|
|
1042
|
+
const existing = unique.get(address);
|
|
1043
|
+
if (!existing) {
|
|
1044
|
+
unique.set(address, row);
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
if (JSON.stringify(existing) !== JSON.stringify(row)) {
|
|
1048
|
+
throw new OxygenError("invalid_request", `Duplicate mailbox ${address} has conflicting provider, platform, tenant, external id, or credential fields.`, { exitCode: 1 });
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return [...unique.values()];
|
|
1052
|
+
}
|
|
1053
|
+
function normalizeMailboxImportVendor(raw, from) {
|
|
1054
|
+
if (from === "hypertide") {
|
|
1055
|
+
if (raw && raw.trim().toLowerCase() !== "hypertide") {
|
|
1056
|
+
throw new OxygenError("invalid_request", "--from hypertide is fixed to --vendor hypertide.", { exitCode: 1 });
|
|
1057
|
+
}
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
if (from === "credentials" && !raw) {
|
|
1061
|
+
throw new OxygenError("invalid_request", "--vendor <source> is required with --from credentials so the imported mailbox keeps its provenance.", { exitCode: 1 });
|
|
1062
|
+
}
|
|
1063
|
+
if (!raw)
|
|
807
1064
|
return null;
|
|
808
|
-
|
|
1065
|
+
const normalized = raw.trim().toLowerCase();
|
|
1066
|
+
if (normalized.length < 1 ||
|
|
1067
|
+
normalized.length > 64 ||
|
|
1068
|
+
!/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/.test(normalized)) {
|
|
1069
|
+
throw new OxygenError("invalid_request", "--vendor must be 1-64 lowercase letters, digits, dots, underscores, or hyphens.", { exitCode: 1 });
|
|
1070
|
+
}
|
|
1071
|
+
return normalized;
|
|
809
1072
|
}
|
|
810
1073
|
async function readMailboxImportFile(path) {
|
|
811
1074
|
let buffer;
|
|
@@ -813,7 +1076,7 @@ async function readMailboxImportFile(path) {
|
|
|
813
1076
|
buffer = readFileSync(path);
|
|
814
1077
|
}
|
|
815
1078
|
catch {
|
|
816
|
-
throw new OxygenError("mailbox_import_file_unreadable", `Couldn't read --file '${basename(path)}'. Check that the path exists and is readable.
|
|
1079
|
+
throw new OxygenError("mailbox_import_file_unreadable", `Couldn't read --file '${basename(path)}'. Check that the path exists and is readable. CSV/JSON/JSONL/XLSX identity files are accepted; credential exports require --from credentials (or --from hypertide). See https://oxygen-agent.com/docs/providers/mailbox-compatibility.`, { exitCode: 1 });
|
|
817
1080
|
}
|
|
818
1081
|
if (buffer.byteLength > MAILBOX_IMPORT_FILE_MAX_BYTES) {
|
|
819
1082
|
throw new OxygenError("invalid_request", `Mailbox import files must be ${MAILBOX_IMPORT_FILE_MAX_BYTES / 1024 / 1024} MB or smaller.`, { exitCode: 1 });
|
|
@@ -821,7 +1084,13 @@ async function readMailboxImportFile(path) {
|
|
|
821
1084
|
const format = inferRowsFileFormat(path);
|
|
822
1085
|
let rows;
|
|
823
1086
|
if (format === "json") {
|
|
824
|
-
|
|
1087
|
+
let parsed;
|
|
1088
|
+
try {
|
|
1089
|
+
parsed = JSON.parse(buffer.toString("utf8"));
|
|
1090
|
+
}
|
|
1091
|
+
catch {
|
|
1092
|
+
throw new OxygenError("invalid_json", "--file must contain valid JSON. File contents are omitted from this error because mailbox imports may contain credentials.", { exitCode: 1 });
|
|
1093
|
+
}
|
|
825
1094
|
if (Array.isArray(parsed)) {
|
|
826
1095
|
rows = parsed;
|
|
827
1096
|
}
|
|
@@ -836,7 +1105,12 @@ async function readMailboxImportFile(path) {
|
|
|
836
1105
|
}
|
|
837
1106
|
}
|
|
838
1107
|
else {
|
|
839
|
-
|
|
1108
|
+
try {
|
|
1109
|
+
rows = await parseRowsFileBuffer(buffer, format);
|
|
1110
|
+
}
|
|
1111
|
+
catch {
|
|
1112
|
+
throw new OxygenError("invalid_mailbox_import_file", `Couldn't parse the ${format.toUpperCase()} mailbox file. File contents are omitted from this error because mailbox imports may contain credentials.`, { exitCode: 1 });
|
|
1113
|
+
}
|
|
840
1114
|
}
|
|
841
1115
|
if (rows.length > MAILBOX_IMPORT_ROW_LIMIT) {
|
|
842
1116
|
throw new OxygenError("invalid_request", `Mailbox imports are limited to ${MAILBOX_IMPORT_ROW_LIMIT} rows per request.`, { exitCode: 1 });
|
|
@@ -1233,7 +1507,10 @@ function readCrmEnrichmentMaxCredits(value) {
|
|
|
1233
1507
|
return credits === undefined ? {} : { max_credits: credits };
|
|
1234
1508
|
}
|
|
1235
1509
|
function buildCrmSearchBody(query, options) {
|
|
1236
|
-
|
|
1510
|
+
// `crm search` is the only command in the family that took the plural, which
|
|
1511
|
+
// cost a failed invocation every time an agent reached for the sibling
|
|
1512
|
+
// spelling. Both are accepted; the plural still wins if somebody passes both.
|
|
1513
|
+
const objects = readCsvOption(options.objects ?? options.object);
|
|
1237
1514
|
const limit = readPositiveInt(options.limit);
|
|
1238
1515
|
return {
|
|
1239
1516
|
query,
|
|
@@ -2250,13 +2527,78 @@ export function createProgram() {
|
|
|
2250
2527
|
.action(async (options) => {
|
|
2251
2528
|
await handleAsyncAction("home standup", options, () => requestOxygen("/api/cli/home/standup"));
|
|
2252
2529
|
});
|
|
2253
|
-
program
|
|
2530
|
+
const commandsCommand = program
|
|
2254
2531
|
.command("commands")
|
|
2255
|
-
.description("
|
|
2532
|
+
.description("Discover CLI commands. With no subcommand, print the backwards-compatible full manifest.")
|
|
2256
2533
|
.option("--json", "Print a JSON envelope.")
|
|
2257
2534
|
.action(async (options) => {
|
|
2258
2535
|
await handleAsyncAction("commands", options, async () => buildCommandManifest(program, binaryName));
|
|
2259
2536
|
});
|
|
2537
|
+
commandsCommand
|
|
2538
|
+
.addCommand(new Command("search")
|
|
2539
|
+
.description("Find a bounded set of CLI commands from a natural-language outcome.")
|
|
2540
|
+
.argument("<query...>", "Outcome or capability to find.")
|
|
2541
|
+
.option("--limit <n>", "Maximum results (default 10, max 25).")
|
|
2542
|
+
.option("--json", "Print a JSON envelope.")
|
|
2543
|
+
.action(async (queryParts, options) => {
|
|
2544
|
+
const outputOptions = { ...options, json: options.json || Boolean(commandsCommand.opts().json) };
|
|
2545
|
+
await handleAsyncAction("commands search", outputOptions, async () => {
|
|
2546
|
+
const manifest = buildCommandManifest(program, binaryName);
|
|
2547
|
+
return searchCommandManifest(manifest, queryParts.join(" "), readPositiveInt(options.limit) ?? 10);
|
|
2548
|
+
});
|
|
2549
|
+
}))
|
|
2550
|
+
.addCommand(new Command("get")
|
|
2551
|
+
.description("Hydrate one exact CLI command with its arguments, flags, and safety markers.")
|
|
2552
|
+
.argument("<command...>", "Exact command name returned by commands search.")
|
|
2553
|
+
.option("--json", "Print a JSON envelope.")
|
|
2554
|
+
.action(async (commandParts, options) => {
|
|
2555
|
+
const outputOptions = { ...options, json: options.json || Boolean(commandsCommand.opts().json) };
|
|
2556
|
+
await handleAsyncAction("commands get", outputOptions, async () => {
|
|
2557
|
+
const manifest = buildCommandManifest(program, binaryName);
|
|
2558
|
+
const exactName = commandParts.join(" ");
|
|
2559
|
+
const command = getCommandManifestEntry(manifest, exactName);
|
|
2560
|
+
if (!command) {
|
|
2561
|
+
throw new OxygenError("command_not_found", `Unknown Oxygen command: ${exactName}`, {
|
|
2562
|
+
details: { did_you_mean: suggestCommandNames(manifest, exactName) },
|
|
2563
|
+
exitCode: 2,
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
return command;
|
|
2567
|
+
});
|
|
2568
|
+
}));
|
|
2569
|
+
program
|
|
2570
|
+
.command("capabilities")
|
|
2571
|
+
.description("Route an outcome to its owning OXYGEN layer and hosted primitive.")
|
|
2572
|
+
.addCommand(new Command("search")
|
|
2573
|
+
.description("Find the owning capability, boundary, gateways, and next hydration step.")
|
|
2574
|
+
.argument("<query...>", "Natural-language GTM outcome.")
|
|
2575
|
+
.option("--json", "Print a JSON envelope.")
|
|
2576
|
+
.action(async (queryParts, options) => {
|
|
2577
|
+
await handleAsyncAction("capabilities search", options, async () => {
|
|
2578
|
+
const query = queryParts.join(" ");
|
|
2579
|
+
return {
|
|
2580
|
+
query,
|
|
2581
|
+
route: serializeCapabilityRoute(inferCapabilityRoute(query)),
|
|
2582
|
+
hint: "Hydrate one exact CLI command with `oxygen commands get <exact-command> --json`, or one MCP tool with `oxygen_capabilities_schema`.",
|
|
2583
|
+
};
|
|
2584
|
+
});
|
|
2585
|
+
}))
|
|
2586
|
+
.addCommand(new Command("get")
|
|
2587
|
+
.description("Hydrate one exact capability card by id.")
|
|
2588
|
+
.argument("<capability-id>", "Capability id returned by capabilities search.")
|
|
2589
|
+
.option("--json", "Print a JSON envelope.")
|
|
2590
|
+
.action(async (capabilityId, options) => {
|
|
2591
|
+
await handleAsyncAction("capabilities get", options, async () => {
|
|
2592
|
+
const route = getCapabilityRouteMatch(capabilityId);
|
|
2593
|
+
if (!route) {
|
|
2594
|
+
throw new OxygenError("capability_not_found", `Unknown OXYGEN capability: ${capabilityId}`, {
|
|
2595
|
+
details: { available_ids: OXYGEN_CAPABILITY_ROUTES.map((card) => card.id) },
|
|
2596
|
+
exitCode: 2,
|
|
2597
|
+
});
|
|
2598
|
+
}
|
|
2599
|
+
return serializeCapabilityRoute(route);
|
|
2600
|
+
});
|
|
2601
|
+
}));
|
|
2260
2602
|
program
|
|
2261
2603
|
.command("status")
|
|
2262
2604
|
.description("Compare the local Oxygen CLI version against the active profile's deployed Oxygen API.")
|
|
@@ -2808,7 +3150,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2808
3150
|
.addCommand(new Command("resolve")
|
|
2809
3151
|
.description("Resolve @handles, LinkedIn profile/company links, provider ids, and URNs — through a connected sender (notify=false, free) or the managed scraper (--source scraper, paid, no sender/window needed).")
|
|
2810
3152
|
.option("--account <account>", "LinkedIn sender id, connection id, or Unipile account id. Required unless --source scraper.")
|
|
2811
|
-
.option("--source <source>", "Resolution source: sender (default, free) or scraper (managed cookieless lookup
|
|
3153
|
+
.option("--source <source>", "Resolution source: sender (default, free) or scraper (managed cookieless lookup; fetch current cost from tools get/dry-run; needs --approved --max-credits).")
|
|
2812
3154
|
.option("--approved", "Approve the paid scraper lookups (required with --source scraper).")
|
|
2813
3155
|
.option("--max-credits <n>", "Credit ceiling for scraper lookups (required with --source scraper).")
|
|
2814
3156
|
.option("--text <text>", "Post text containing @identifiers or LinkedIn URLs.")
|
|
@@ -3402,9 +3744,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
3402
3744
|
}));
|
|
3403
3745
|
})))
|
|
3404
3746
|
.addCommand(new Command("search")
|
|
3405
|
-
.description("Search CRM records by identity or record label.")
|
|
3406
|
-
.argument("<query>", "Domain, email, LinkedIn URL, or record name to search for.")
|
|
3747
|
+
.description("Search CRM records by identity or record label. Takes ONE query; narrow the objects with --object, not a second argument.")
|
|
3748
|
+
.argument("<query>", "Domain, email, LinkedIn URL, or record name to search for. Quote it if it contains spaces; to restrict to one object use --object companies.")
|
|
3407
3749
|
.option("--objects <objects>", "Comma-separated CRM object slugs to search. Defaults to all configured objects.")
|
|
3750
|
+
.option("--object <object>", "Alias for --objects; every sibling crm command spells it singular.")
|
|
3408
3751
|
.option("--limit <limit>", "Maximum records to return.")
|
|
3409
3752
|
.option("--json", "Print a JSON envelope.")
|
|
3410
3753
|
.action(async (query, options) => {
|
|
@@ -3545,9 +3888,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
3545
3888
|
}));
|
|
3546
3889
|
}))
|
|
3547
3890
|
.addCommand(new Command("timeline")
|
|
3548
|
-
.description("Show a CRM record's activity timeline, newest first.")
|
|
3891
|
+
.description("Show a CRM record's activity timeline, newest first. Address the record by row id OR by any of the object's identities \u2014 a company domain, a person's email or LinkedIn URL.")
|
|
3549
3892
|
.argument("<object>", "CRM object slug, such as companies or people.")
|
|
3550
|
-
.argument("<
|
|
3893
|
+
.argument("<record>", "CRM record row id, or an identity value such as acme.com or sarah@acme.com.")
|
|
3551
3894
|
.option("--limit <limit>", "Maximum activities to return.")
|
|
3552
3895
|
.option("--cursor <cursor>", "Pagination cursor from a previous page.")
|
|
3553
3896
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -5567,7 +5910,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5567
5910
|
});
|
|
5568
5911
|
}))
|
|
5569
5912
|
.addCommand(new Command("preflight")
|
|
5570
|
-
.description("Preflight a blueprint (slug, local file, or shared URL) against this workspace.")
|
|
5913
|
+
.description("Preflight a blueprint (slug, local file, or shared URL) against this workspace. Price-aware seeds return a current runtime descriptor upper bound, cap adequacy, and an exact zero-credit apply command.")
|
|
5571
5914
|
.argument("[slug]", "Blueprint slug (for stored or seed blueprints).")
|
|
5572
5915
|
.option("--file <path>", "Read a blueprint envelope from a local JSON file.")
|
|
5573
5916
|
.option("--from-url <url>", "Fetch a shared blueprint envelope from a public Oxygen share URL.")
|
|
@@ -5581,13 +5924,13 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5581
5924
|
});
|
|
5582
5925
|
}))
|
|
5583
5926
|
.addCommand(new Command("apply")
|
|
5584
|
-
.description("Apply a blueprint
|
|
5927
|
+
.description("Apply a blueprint: 0 credits and no provider calls or external writes, but creates workspace tables, columns, prompts, and a disabled workflow. It does not enable or run the workflow.")
|
|
5585
5928
|
.argument("[slug]", "Blueprint slug (for stored or seed blueprints).")
|
|
5586
5929
|
.option("--file <path>", "Read a blueprint envelope from a local JSON file.")
|
|
5587
5930
|
.option("--from-url <url>", "Fetch a shared blueprint envelope from a public Oxygen share URL.")
|
|
5588
5931
|
.option("--input-json <json>", "Seed blueprint input (parameters) as JSON.")
|
|
5589
5932
|
.option("--table-ref <ref=id...>", "Reuse an existing table for a blueprint ref (repeatable).", collectMultiple, [])
|
|
5590
|
-
.option("--workflow-id <id>", "
|
|
5933
|
+
.option("--workflow-id <id>", "Set the new workflow's manifest id/slug; this does not update or reuse an existing workflow.")
|
|
5591
5934
|
.option("--workflow-name <name>", "Override the resulting workflow name.")
|
|
5592
5935
|
.option("--json", "Print a JSON envelope.")
|
|
5593
5936
|
.action(async (slug, options) => {
|
|
@@ -5723,7 +6066,19 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5723
6066
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
5724
6067
|
return requestOxygen(`/api/blueprints/marketplace${qs}`, { requireAuth: false });
|
|
5725
6068
|
});
|
|
5726
|
-
}))
|
|
6069
|
+
}))
|
|
6070
|
+
.addHelpText("after", [
|
|
6071
|
+
"",
|
|
6072
|
+
"Safety:",
|
|
6073
|
+
" list, describe, and preflight use 0 credits, make no provider calls, and do not change workspace state.",
|
|
6074
|
+
" Price-aware preflights fetch runtime descriptor pricing, label upper bounds versus exact-shape dry runs,",
|
|
6075
|
+
" and return the exact apply command that preserves the validated inputs.",
|
|
6076
|
+
" export has the same workspace safety; --out only writes the named local file.",
|
|
6077
|
+
" apply also uses 0 credits and makes no provider calls or external writes, but it creates workspace",
|
|
6078
|
+
" tables, prompts, and a disabled workflow. Its response reports future per-run credit ceilings.",
|
|
6079
|
+
" save, archive, tag, share, publish, and their inverse commands change workspace or publication state.",
|
|
6080
|
+
"",
|
|
6081
|
+
].join("\n"));
|
|
5727
6082
|
program
|
|
5728
6083
|
.command("recipes")
|
|
5729
6084
|
.description("Business-case GTM playbooks: proven plays with prerequisites, credit posture, and approval gates spelled out.")
|
|
@@ -5856,7 +6211,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5856
6211
|
.command("columns")
|
|
5857
6212
|
.description("Workspace table column commands.")
|
|
5858
6213
|
.addCommand(new Command("add")
|
|
5859
|
-
.description("Add a nullable column to a workspace table.")
|
|
6214
|
+
.description("Add a nullable column to a workspace table. Writes the definition only — this never runs the column and never spends credits; use `columns run` for that, with --dry-run first to see the cost.")
|
|
5860
6215
|
.argument("<table>", "Table id or slug.")
|
|
5861
6216
|
.option("--label <label>", "Display label for the new column. Required unless --prompt-key supplies a default title.")
|
|
5862
6217
|
.option("--key <key>", "Optional stable column key. Defaults to a normalized label.")
|
|
@@ -6017,7 +6372,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6017
6372
|
.description("Run an executable AI, tool, formula, enrichment, bind, lookup, or local custom HTTP column for one row or a bounded batch. Paid server-side columns always run durably in the background. Bind create-mode (onNoMatch=create) needs --approved.")
|
|
6018
6373
|
.argument("<table>", "Table id or slug.")
|
|
6019
6374
|
.argument("<column>", "Column id or key.")
|
|
6020
|
-
.option("--row-id <row_id>", "Workspace row id to run.")
|
|
6375
|
+
.option("--row-id <row_id>", "Workspace row id to run. Get one from `oxygen tables query <table> --limit 1 --json` — the field is `_row_id`, not `id`.")
|
|
6021
6376
|
.option("--limit <n>", "Run the next N rows whose target cell is still empty (--force runs the first N regardless). Repeat until rowCount is 0 to page through a table. Defaults to 10; inline deterministic runs have a hard cap of 25.")
|
|
6022
6377
|
.option("--all", "Run all rows. Requires --background.")
|
|
6023
6378
|
.option("--filter-json <json>", "Row selector filter object or array for background runs. Do not combine with --all, --limit, or --row-id.")
|
|
@@ -6181,6 +6536,11 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6181
6536
|
.option("--research-mode <mode>", "Research columns: strict (answer only from the sources) or estimate (reason to a figure from them).")
|
|
6182
6537
|
.option("--research-results <n>", "Research columns: how many search results to ground each row on (1-25).")
|
|
6183
6538
|
.option("--research-engine <engine>", "Research columns: pin the search provider (exa, parallel, or firecrawl).")
|
|
6539
|
+
// Declaring BOTH forms leaves the default undefined (Commander only
|
|
6540
|
+
// defaults to true when --no- is declared alone), so an update that
|
|
6541
|
+
// doesn't mention visibility leaves it untouched.
|
|
6542
|
+
.option("--always-show", "Keep this column visible even when it holds no values.")
|
|
6543
|
+
.option("--no-always-show", "Stop pinning this column, so it collapses again while empty.")
|
|
6184
6544
|
.option("--dry-run", "Return the would-be merged definition without writing.")
|
|
6185
6545
|
.option("--json", "Print a JSON envelope.")
|
|
6186
6546
|
.action(async (table, column, options) => {
|
|
@@ -6225,6 +6585,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6225
6585
|
...(definition ? { definition } : {}),
|
|
6226
6586
|
...(definitionUnset.length > 0 ? { definition_unset: definitionUnset } : {}),
|
|
6227
6587
|
...(readOption(options.dataType) ? { data_type: readOption(options.dataType) } : {}),
|
|
6588
|
+
...(typeof options.alwaysShow === "boolean" ? { always_show: options.alwaysShow } : {}),
|
|
6228
6589
|
...(options.dryRun ? { dry_run: true } : {}),
|
|
6229
6590
|
},
|
|
6230
6591
|
});
|
|
@@ -7108,7 +7469,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7108
7469
|
})));
|
|
7109
7470
|
program
|
|
7110
7471
|
.command("billing")
|
|
7111
|
-
.description("Plan and managed credit commands. Spend splits into FLEXIBLE (ad-hoc: enrichment, AI, automation — drawn from your free-to-spend balance) and FIXED recurring per-resource monthly commitments blocked out of it; see `billing commitments`.")
|
|
7472
|
+
.description("Plan and managed credit commands. Spend splits into FLEXIBLE (ad-hoc: enrichment, AI, automation — drawn from your free-to-spend balance) and FIXED recurring per-resource monthly commitments blocked out of it; see `billing commitments`. THREE CLOCKS, deliberately different: the CREDIT CYCLE that `billing allowance` reports against (your plan's monthly grant window); each resource's own COMMITMENT RENEWAL, anchored to the day you connected it, so `billing commitments --json` next_due_at rarely matches the cycle end; and your SUBSCRIPTION PERIOD in `billing balance` (annual plans span many credit cycles). A number from one clock will not reconcile against another. Failed-payment grace, suspension, and recovery: https://oxygen-agent.com/docs/safety/billing.")
|
|
7112
7473
|
.addCommand(new Command("change")
|
|
7113
7474
|
.description("Preview an upgrade or downgrade and return a Stripe confirmation link. Nothing changes until confirmed in Stripe.")
|
|
7114
7475
|
.requiredOption("--to <tier>", "Target plan: starter, pro, or team.")
|
|
@@ -7120,19 +7481,19 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7120
7481
|
}));
|
|
7121
7482
|
}))
|
|
7122
7483
|
.addCommand(new Command("balance")
|
|
7123
|
-
.description("Show the current plan and managed credit balance: available and reserved credits, FIXED credits committed to recurring per-resource charges, and the FLEXIBLE free-to-spend remainder.
|
|
7484
|
+
.description("Show the current plan, subscription entitlement, and managed credit balance: available and reserved credits, FIXED credits committed to recurring per-resource charges, and the FLEXIBLE free-to-spend remainder. After failed-payment grace expires, mutations stop while read/export and billing recovery remain available. Recovery: https://oxygen-agent.com/billing. Policy: https://oxygen-agent.com/docs/safety/billing.")
|
|
7124
7485
|
.option("--json", "Print a JSON envelope.")
|
|
7125
7486
|
.action(async (options) => {
|
|
7126
7487
|
await handleAsyncAction("billing balance", options, () => requestOxygen("/api/cli/billing/balance"));
|
|
7127
7488
|
}))
|
|
7128
7489
|
.addCommand(new Command("commitments")
|
|
7129
|
-
.description("List the fixed monthly credit commitments blocked at your subscription renewal — per connected sending mailbox, OXYGEN-sold mailbox, warm-up, deliverability, and connected LinkedIn account — with unit price, quantity, and next-due date. Read-only, 0 Oxygen credits.")
|
|
7490
|
+
.description("List the fixed monthly credit commitments blocked at your subscription renewal — per connected sending mailbox, OXYGEN-sold mailbox, warm-up, deliverability, and connected LinkedIn account — with unit price, quantity, and next-due date. This is the FORWARD run-rate: what your currently-connected resources will cost at their next renewal. It is NOT this cycle's charges — compare `billing allowance` fixed_spent_credits for that, and expect the two to differ. Each resource renews on its OWN anchor (the day it was connected), so next_due_at is per-resource and rarely lines up with the credit cycle. Read-only, 0 Oxygen credits.")
|
|
7130
7491
|
.option("--json", "Print a JSON envelope.")
|
|
7131
7492
|
.action(async (options) => {
|
|
7132
7493
|
await handleAsyncAction("billing commitments", options, () => requestOxygen("/api/cli/billing/commitments"));
|
|
7133
7494
|
}))
|
|
7134
7495
|
.addCommand(new Command("allowance")
|
|
7135
|
-
.description("Show this billing cycle's credit pool as one breakdown: fixed spend, flexible spend, credits committed to the next renewal, and what is free to spend — the numbers behind the bar on the credit-usage page. The parts always sum to the total. Read-only, 0 Oxygen credits.")
|
|
7496
|
+
.description("Show this billing cycle's credit pool as one breakdown: fixed spend, flexible spend, credits committed to the next renewal, and what is free to spend — the numbers behind the bar on the credit-usage page. The parts always sum to the total. fixed_spent_credits is what has ALREADY been charged this cycle, NOT your monthly run-rate — for the forward figure billed at the next renewal see `billing commitments`, which is normally much larger. reserved_credits is in-flight spend already carved out of free_to_spend_credits, so treat free_to_spend as the ceiling and free_to_spend minus reserved as what is genuinely uncommitted. Every segment carries region=fixed|flexible. Read-only, 0 Oxygen credits.")
|
|
7136
7497
|
.option("--json", "Print a JSON envelope.")
|
|
7137
7498
|
.action(async (options) => {
|
|
7138
7499
|
await handleAsyncAction("billing allowance", options, () => requestOxygen("/api/cli/billing/allowance"));
|
|
@@ -7504,6 +7865,19 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7504
7865
|
program
|
|
7505
7866
|
.command("admin")
|
|
7506
7867
|
.description("Staff-only commands.")
|
|
7868
|
+
.addCommand(new Command("kpis")
|
|
7869
|
+
.description("Show company acquisition with self-reported signup-channel attribution, signup-to-CLI/MCP activation, matured seven-day trial conversion, positive-paid MRR/ARR, and paid churn separated from trial loss. OXYGEN staff only; workspace admin role alone does not grant access.")
|
|
7870
|
+
.addOption(new Option("--range <range>", "Range for acquisition, actor-and-surface-tracked activation, seven-day trial cohorts, and churn; current MRR/ARR and scheduled cancellations remain point-in-time.")
|
|
7871
|
+
.choices(["7d", "30d", "90d"])
|
|
7872
|
+
.default("30d"))
|
|
7873
|
+
.option("--json", "Print a JSON envelope.")
|
|
7874
|
+
.action(async (options) => {
|
|
7875
|
+
await handleAsyncAction("admin kpis", options, () => {
|
|
7876
|
+
const range = readOption(options.range);
|
|
7877
|
+
const suffix = range && range !== "30d" ? `?range=${encodeURIComponent(range)}` : "";
|
|
7878
|
+
return requestOxygen(`/api/cli/admin/kpis${suffix}`);
|
|
7879
|
+
});
|
|
7880
|
+
}))
|
|
7507
7881
|
.addCommand(new Command("costs")
|
|
7508
7882
|
.description("Show provider costs (COGS) per workspace. Staff only.")
|
|
7509
7883
|
.option("--top <n>", "Limit number of workspace columns. Defaults to all.")
|
|
@@ -8394,10 +8768,11 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8394
8768
|
.command("tools")
|
|
8395
8769
|
.description("Tool catalog commands.")
|
|
8396
8770
|
.addCommand(new Command("search")
|
|
8397
|
-
.description("Search
|
|
8771
|
+
.description("Search a bounded, compact provider-operation catalog; hydrate one result with tools get. A response listing partial_sources means an optional catalog source timed out and totals may be understated — rerun for the complete catalog.")
|
|
8398
8772
|
.argument("[query]", "Search text.")
|
|
8399
|
-
.option("--verbosity <verbosity>", "minimal, summary, or full. Defaults to
|
|
8773
|
+
.option("--verbosity <verbosity>", "minimal, summary, or full. Defaults to minimal; hydrate one result with tools get.")
|
|
8400
8774
|
.option("--terse", "Alias for --verbosity minimal.")
|
|
8775
|
+
.option("--all", "Return the complete matching catalog. Explicit because the default is bounded to 10.")
|
|
8401
8776
|
.option("--only-runnable", "Only return tools runnable by the active organization.")
|
|
8402
8777
|
.option("--workflow-eligible", "Only return tools a hosted workflow step may call, each annotated with the canonical `workflow_effect` its manifest step must declare. This is a narrower set than the table-column catalog — use it when authoring a workflow manifest so lint cannot reject a tool at apply time.")
|
|
8403
8778
|
.option("--no-access-check", "Skip per-tool availability checks for a fast complete-catalog listing. Tools are returned without availability info; pair with --terse for discovery sweeps.")
|
|
@@ -8413,6 +8788,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8413
8788
|
const verbosity = options.terse ? "minimal" : readOption(options.verbosity);
|
|
8414
8789
|
if (verbosity)
|
|
8415
8790
|
params.set("verbosity", verbosity);
|
|
8791
|
+
if (options.all)
|
|
8792
|
+
params.set("all", "true");
|
|
8416
8793
|
if (options.onlyRunnable)
|
|
8417
8794
|
params.set("only_runnable", "true");
|
|
8418
8795
|
if (options.workflowEligible)
|
|
@@ -9377,7 +9754,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9377
9754
|
});
|
|
9378
9755
|
})));
|
|
9379
9756
|
program.addCommand(new Command("engagement")
|
|
9380
|
-
.description("
|
|
9757
|
+
.description("Capture LinkedIn intent from one known post (harvest needs its public URL or the composite social_id from `oxygen posts get`) or from your connected account's viewers, followers, and connections. For recurring competitor-profile monitoring that discovers future posts, start with `oxygen recipes list competitor --json`. Harvests run as a slow, durable drip under a conservative read budget.")
|
|
9381
9758
|
.addCommand(new Command("harvest")
|
|
9382
9759
|
.description("Start (or re-arm) a harvest of a post's engagers into a workspace table you can enroll into a sequence. Engagers drip into the table over many ticks; poll `engagement status` to watch it fill. No messages are sent. Cookieless harvests spend Oxygen credits per scraper page and require --max-credits.")
|
|
9383
9760
|
.requiredOption("--post <social_id_or_url>", "Composite post social_id from `oxygen posts get` (NOT the activity URN), or the public LinkedIn post URL for cookieless.")
|
|
@@ -9493,6 +9870,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9493
9870
|
}))
|
|
9494
9871
|
.addCommand(new Command("watch")
|
|
9495
9872
|
.description("Declarative engagement watches (the signals wedge): stand up a watch on a post's engagers, 'who viewed my profile', or the sender's inbound network (new followers / new connections) that harvests people into a table and, under a standing approval, auto-enrolls them into a sequence. The watch materializes the harvest drip and enrolls newly-harvested people each cycle — the sequence still gates its own sends.")
|
|
9873
|
+
.addHelpText("after", "\nScope: this watches one known post or one connected-account signal. For daily discovery across one or more public profiles' recent posts, use `oxygen blueprints describe linkedin-profile-engager-monitor --json`.\n")
|
|
9496
9874
|
.addCommand(new Command("create")
|
|
9497
9875
|
.description("Arm an engagement watch. `--kind post` watches a post's reactors + commenters (needs --post social_id; a cookieless post also needs --post-url); `--kind profile_viewers` watches 'who viewed my profile' (source unipile); `--kind followers` / `--kind connections` watch the sender's inbound network — people NEW to the org's orbit stream into the watch table as they follow/connect (source unipile, no credits, reads metered against the account's daily ingest budget). With --auto-enroll it enrolls harvested people into --sequence, capped by --max-enrolls-per-day — the inbound-led-outbound loop. --max-credits-per-cycle is the standing per-cycle spend cap (required for --auto-enroll and cookieless). No messages are sent by the watch itself.")
|
|
9498
9876
|
.requiredOption("--kind <kind>", "post | profile_viewers | followers | connections.")
|
|
@@ -10578,24 +10956,57 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10578
10956
|
const suffix = params.toString();
|
|
10579
10957
|
return requestOxygen(`/api/cli/sequences${suffix ? `?${suffix}` : ""}`);
|
|
10580
10958
|
});
|
|
10959
|
+
}))
|
|
10960
|
+
.addCommand(new Command("send")
|
|
10961
|
+
.description("Initiate one net-new LinkedIn message through a one-recipient, one-step hosted Sequence. First call without a sequence id: pass --recipient, --text/--text-file, and --sender to create the inert draft and preview (no message, no credits). After showing that preview, re-run with the returned sequence id plus --approved --max-credits N. Existing-thread replies stay in `oxygen inbox send`.")
|
|
10962
|
+
.argument("[sequence]", "Sequence id returned by the preview call. Omit when creating the preview; required with --approved.")
|
|
10963
|
+
.option("--recipient <url-or-id>", "LinkedIn personal-profile URL or provider member id. Preview call only.")
|
|
10964
|
+
.option("--recipient-name <name>", "Optional display name stored on the enrollment. Preview call only.")
|
|
10965
|
+
.option("--text <text>", "Message body. Preview call only; use either --text or --text-file.")
|
|
10966
|
+
.option("--text-file <path>", "Read the message body from a file. Preview call only; use either --text-file or --text.")
|
|
10967
|
+
.option("--sender <ref>", "Connected LinkedIn sender id, connection id, or Unipile account id. List choices with `oxygen senders list`.")
|
|
10968
|
+
.option("--approved", "Approve the previewed Sequence for live dispatch. Requires the returned sequence id and --max-credits.")
|
|
10969
|
+
.option("--max-credits <n>", "Positive credit ceiling for the live LinkedIn dispatch.")
|
|
10970
|
+
.option("--json", "Print a JSON envelope.")
|
|
10971
|
+
.action(async (sequence, options) => {
|
|
10972
|
+
await handleAsyncAction("sequences send", options, () => {
|
|
10973
|
+
const sequenceId = readOption(sequence);
|
|
10974
|
+
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
10975
|
+
if (sequenceId) {
|
|
10976
|
+
if (readOption(options.recipient) || readOption(options.recipientName) || readOption(options.text) || readOption(options.textFile) || readOption(options.sender)) {
|
|
10977
|
+
throw new OxygenError("conflicting_flags", "With a sequence id, pass only --approved and --max-credits; recipient, copy, and sender are fixed by the previewed draft.", { exitCode: 1 });
|
|
10978
|
+
}
|
|
10979
|
+
return requestOxygen("/api/cli/sequences/send", { method: "POST", body: { sequence: sequenceId, ...(options.approved ? { approved: true } : {}), ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}) } });
|
|
10980
|
+
}
|
|
10981
|
+
if (options.approved)
|
|
10982
|
+
throw new OxygenError("sequence_preview_required", "Preview first without --approved; then pass the returned sequence id with --approved --max-credits N.", { exitCode: 1 });
|
|
10983
|
+
const recipient = readOption(options.recipient);
|
|
10984
|
+
const sender = readOption(options.sender);
|
|
10985
|
+
const text = readPublishingPostText(options, true);
|
|
10986
|
+
if (!recipient)
|
|
10987
|
+
throw new OxygenError("invalid_request", "--recipient is required for the preview call.", { exitCode: 1 });
|
|
10988
|
+
if (!sender)
|
|
10989
|
+
throw new OxygenError("invalid_request", "--sender is required for the preview call; list choices with `oxygen senders list`.", { exitCode: 1 });
|
|
10990
|
+
return requestOxygen("/api/cli/sequences/send", { method: "POST", body: { recipient, text, sender, ...(readOption(options.recipientName) ? { recipient_name: readOption(options.recipientName) } : {}) } });
|
|
10991
|
+
});
|
|
10581
10992
|
}))
|
|
10582
10993
|
.addCommand(new Command("workflows")
|
|
10583
|
-
.description("
|
|
10994
|
+
.description("Compatibility-only maintenance for existing sequencer presets. Do not create new Teams notifications here: use `oxygen workflows` (web: Sequence → Launch → Connected workflows) with a sequence event trigger and Microsoft Teams Send Message.")
|
|
10584
10995
|
.addCommand(new Command("list")
|
|
10585
|
-
.description("List
|
|
10996
|
+
.description("List the two legacy sequencer presets with armed state, editable config, and ordinary workflow deep-links.")
|
|
10586
10997
|
.option("--json", "Print a JSON envelope.")
|
|
10587
10998
|
.action(async (options) => {
|
|
10588
10999
|
await handleAsyncAction("sequences workflows list", options, () => requestOxygen("/api/cli/sequencer/reply-workflows"));
|
|
10589
11000
|
}))
|
|
10590
11001
|
.addCommand(new Command("configure")
|
|
10591
|
-
.description("Preview or
|
|
10592
|
-
.argument("<template>", "crm-lead-stage-router or sequencer-positive-reply-teams.")
|
|
11002
|
+
.description("Preview or maintain an existing legacy preset. New Teams notifications must use an ordinary connected workflow; this command remains only for compatibility. Omit --live for a zero-write preview. Live arming requires --armed --approved and a positive per-delivery --max-credits cap.")
|
|
11003
|
+
.argument("<template>", "crm-lead-stage-router or the legacy sequencer-positive-reply-teams preset (existing configurations only).")
|
|
10593
11004
|
.option("--config-file <path>", "JSON object containing the managed workflow configuration.")
|
|
10594
11005
|
.option("--armed", "Arm the workflow.")
|
|
10595
11006
|
.option("--disarmed", "Disarm the workflow.")
|
|
10596
11007
|
.option("--live", "Persist the configuration. Omit for a dry-run preview.")
|
|
10597
11008
|
.option("--approved", "Explicitly approve standing authority for this exact workflow revision.")
|
|
10598
|
-
.option("--max-credits <n>", "Hard per-delivery credit ceiling (Teams requires at least
|
|
11009
|
+
.option("--max-credits <n>", "Hard per-delivery credit ceiling (the legacy Teams preset requires at least 0.02).")
|
|
10599
11010
|
.option("--json", "Print a JSON envelope.")
|
|
10600
11011
|
.action(async (template, options) => {
|
|
10601
11012
|
await handleAsyncAction("sequences workflows configure", options, () => {
|
|
@@ -10624,15 +11035,15 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10624
11035
|
});
|
|
10625
11036
|
})))
|
|
10626
11037
|
.addCommand(new Command("hubspot-sync")
|
|
10627
|
-
.description("Map actual sequencer events to contact datetime fields in the connected HubSpot portal.
|
|
11038
|
+
.description("Map actual sequencer events to contact datetime fields in the connected HubSpot portal. Reuses native or provider-managed authorization and only requests known-missing property access if a preview needs new fields.")
|
|
10628
11039
|
.addCommand(new Command("show")
|
|
10629
|
-
.description("
|
|
11040
|
+
.description("Show the connected portal and fetch its writable contact datetime fields, supported sequencer events, current mappings, and enabled state.")
|
|
10630
11041
|
.option("--json", "Print a JSON envelope.")
|
|
10631
11042
|
.action(async (options) => {
|
|
10632
11043
|
await handleAsyncAction("sequences hubspot-sync show", options, () => requestOxygen("/api/cli/sequencer/hubspot-sync"));
|
|
10633
11044
|
}))
|
|
10634
11045
|
.addCommand(new Command("configure")
|
|
10635
|
-
.description("Preview or save event → HubSpot property mappings.
|
|
11046
|
+
.description("Preview or save event → HubSpot property mappings. Existing fields need no new grant; creating missing properties requires --approved and HubSpot property-creation access.")
|
|
10636
11047
|
.option("--mappings-file <path>", "JSON object mapping every sequencer event key to a HubSpot contact datetime-property internal name; use an empty string to disable an event.")
|
|
10637
11048
|
.option("--armed", "Enable future event delivery.")
|
|
10638
11049
|
.option("--disarmed", "Disable future event delivery.")
|
|
@@ -11109,7 +11520,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11109
11520
|
program.addCommand(new Command("voice")
|
|
11110
11521
|
.description("The call channel. `tasks` works the call queue — leads waiting for a HUMAN to dial, queued by sequence call steps, CRM records, table rows, and replies. Claiming takes a lease so two reps never dial the same prospect. Nothing here places a call: dialing is a separate, metered, guardrail-gated action. Consumes 0 credits.")
|
|
11111
11522
|
.addCommand(new Command("tasks")
|
|
11112
|
-
.description("Work the call queue: queue | list | claim | complete | skip | override.\n\nWhat blocks a dial, and what you can do about it:\n calling_window lead's local time outside 09:00-20:00 waivable per lead\n unknown_timezone no timezone on the lead, so the window\n cannot be checked (fails closed) waivable per lead\n daily_cap the chosen number hit today's dial cap waivable per lead\n suppressed on the workspace do-not-call list NEVER waivable\n destination_blocked your numbers cannot reach that country NEVER waivable\n\n`list` reports dialable, block_reason and block_detail per lead, plus\ndialable_count and blocked_by_reason for the page. Waive one with `override`.")
|
|
11523
|
+
.description("Work the call queue: queue | list | claim | complete | extend | skip | override.\n\nA call that nobody answers is dispositioned for you from the call itself, which\nalso resumes any sequence enrollment parked on it — so `complete` and `skip`\nreport already_recorded:true rather than failing when that has happened. You log\nonly what a person said.\n\nWhat blocks a dial, and what you can do about it:\n calling_window lead's local time outside 09:00-20:00 waivable per lead\n unknown_timezone no timezone on the lead, so the window\n cannot be checked (fails closed) waivable per lead\n daily_cap the chosen number hit today's dial cap waivable per lead\n suppressed on the workspace do-not-call list NEVER waivable\n destination_blocked your numbers cannot reach that country NEVER waivable\n\n`list` reports dialable, block_reason and block_detail per lead, plus\ndialable_count and blocked_by_reason for the page. Waive one with `override`.")
|
|
11113
11524
|
.addCommand(new Command("queue")
|
|
11114
11525
|
.description("Put a lead INTO the call queue. Idempotent: a lead who already has an open task comes back with created:false rather than a duplicate, because the queue guarantees one open call per person. Queuing is not dialing — it writes a row a human later acts on. Consumes 0 credits.")
|
|
11115
11526
|
.requiredOption("--phone <e164>", "The lead's number in E.164 (a leading + then 2-15 digits).")
|
|
@@ -11269,6 +11680,25 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11269
11680
|
},
|
|
11270
11681
|
});
|
|
11271
11682
|
});
|
|
11683
|
+
}))
|
|
11684
|
+
.addCommand(new Command("extend")
|
|
11685
|
+
.description("Renew a claimed task's 15-minute lease. Completing needs a live lease, so a longer call must heartbeat or its outcome and notes are rejected as stale.")
|
|
11686
|
+
.requiredOption("--task <id>", "The claimed task's id.")
|
|
11687
|
+
.requiredOption("--lease <token>", "The lease_token returned by `claim`.")
|
|
11688
|
+
.option("--json", "Print a JSON envelope.")
|
|
11689
|
+
.action(async (options) => {
|
|
11690
|
+
await handleAsyncAction("voice tasks extend", options, () => {
|
|
11691
|
+
const task = readOption(options.task);
|
|
11692
|
+
if (!task)
|
|
11693
|
+
throw new Error("--task is required.");
|
|
11694
|
+
const lease = readOption(options.lease);
|
|
11695
|
+
if (!lease)
|
|
11696
|
+
throw new Error("--lease is required.");
|
|
11697
|
+
return requestOxygen("/api/cli/voice/tasks", {
|
|
11698
|
+
method: "POST",
|
|
11699
|
+
body: { action: "extend", task_id: task, lease_token: lease },
|
|
11700
|
+
});
|
|
11701
|
+
});
|
|
11272
11702
|
}))
|
|
11273
11703
|
.addCommand(new Command("skip")
|
|
11274
11704
|
.description("Drop a task out of the queue without dialing (bad data, a rep pass, a guardrail block). Needs no lease — the guardrail gate skips tasks it never claimed.")
|
|
@@ -11567,7 +11997,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11567
11997
|
});
|
|
11568
11998
|
})));
|
|
11569
11999
|
program.addCommand(new Command("managed-inboxes")
|
|
11570
|
-
.description("Whitelabel sending inboxes bought through OXYGEN: subscribe a domain + N mailboxes (google/microsoft/azure) as a recurring MONTHLY subscription billed in USD to your Oxygen Email Infrastructure subscription, list/get your subscriptions, verify that Oxygen/Stripe/the vendor agree, and cancel. Subscribe/cancel are approval-gated (preview → re-run with --approved --quote). The vendor is chosen for you; --vendor pins one.")
|
|
12000
|
+
.description("Whitelabel sending inboxes bought through OXYGEN: subscribe a domain + N mailboxes (google/microsoft/azure) as a recurring MONTHLY subscription billed in USD to your Oxygen Email Infrastructure subscription, add-inboxes to a domain you already own, list/get your subscriptions, verify that Oxygen/Stripe/the vendor agree, and cancel. Subscribe/add-inboxes/cancel are approval-gated (preview → re-run with --approved --quote). The vendor is chosen for you; --vendor pins one.")
|
|
11571
12001
|
.addCommand(new Command("verify")
|
|
11572
12002
|
.description("Check that OXYGEN, STRIPE, and the VENDOR agree about what this org is buying. The truth about a managed inbox lives in three systems — what the customer asked for, what they are charged, and what is actually running — and a 200 from any one of them proves nothing. Reports every disagreement with WHO IS LOSING MONEY while it stands (customer_overbilled first, then oxygen_pays). Read-only, no writes, 0 Oxygen credits.")
|
|
11573
12003
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -11581,7 +12011,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11581
12011
|
await handleAsyncAction("managed-inboxes registrant", options, () => requestOxygen("/api/cli/managed-inboxes/registrant"));
|
|
11582
12012
|
}))
|
|
11583
12013
|
.addCommand(new Command("subscribe")
|
|
11584
|
-
.description("Subscribe a domain + mailboxes as a managed monthly inbox subscription. WITHOUT --approved this prints a priced PREVIEW with a quote_id (nothing ordered, nothing charged); re-run with --approved --quote <id> to place the order. Prices come from the vendor's LIVE rate card and LIVE domain price, and are refused if they sit below vendor cost. Fails closed until the vendor key + founder-signed per-platform pricing are configured.")
|
|
12014
|
+
.description("Subscribe a NEW domain + mailboxes as a managed monthly inbox subscription. WITHOUT --approved this prints a priced PREVIEW with a quote_id (nothing ordered, nothing charged); re-run with --approved --quote <id> to place the order. Prices come from the vendor's LIVE rate card and LIVE domain price, and are refused if they sit below vendor cost. Fails closed until the vendor key + founder-signed per-platform pricing are configured. To add mailboxes to a domain you ALREADY own use `managed-inboxes add-inboxes` — this command always registers a new domain and fails on one you own.")
|
|
11585
12015
|
.argument("[domain]", "Sending domain to register + host the mailboxes (e.g. send.acme.com). May also be passed as --domain.")
|
|
11586
12016
|
.option("--domain <domain>", "Sending domain (alternative to the positional argument).")
|
|
11587
12017
|
.requiredOption("--provider <provider>", "Mailbox PLATFORM: google, microsoft, or azure. (google/microsoft cap at 5 mailboxes per domain; azure allows 100.)")
|
|
@@ -11646,6 +12076,75 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11646
12076
|
},
|
|
11647
12077
|
});
|
|
11648
12078
|
});
|
|
12079
|
+
}))
|
|
12080
|
+
.addCommand(new Command("add-inboxes")
|
|
12081
|
+
.description("Add mailboxes to a managed domain you ALREADY own — no new domain is registered and there is NO domain registration charge, only the extra inboxes' monthly rate (plus their add-ons). WITHOUT --approved this prints a priced PREVIEW with a quote_id and orders nothing; re-run with --approved --quote <id> to place the order. Capped per domain by the platform the domain was bought on (5 google/microsoft, 100 azure) COUNTING the inboxes already on it. To register a NEW domain use `managed-inboxes subscribe` instead.")
|
|
12082
|
+
.argument("[domain]", "A managed domain this workspace already owns (e.g. send.acme.com). May also be passed as --domain.")
|
|
12083
|
+
.option("--domain <domain>", "The managed inbox domain (alternative to the positional argument).")
|
|
12084
|
+
.option("--mailboxes <json>", "JSON array of mailboxes: [{\"username\",\"first_name\",\"last_name\"}]. The vendor stamps the names on each mailbox, so real names belong here.")
|
|
12085
|
+
.option("--file <path>", "Path to a JSON file { \"mailboxes\": [...] } (alternative to --mailboxes).")
|
|
12086
|
+
.option("--count <n>", "Shorthand for --mailboxes: how many inboxes to add. Requires --prefix.")
|
|
12087
|
+
.option("--prefix <base>", "Shorthand username base for --count: `--count 3 --prefix ada` adds ada1, ada2, ada3 with placeholder names (Ada 1, Ada 2, Ada 3). Pass --mailboxes/--file instead when the inboxes need real human names.")
|
|
12088
|
+
.option("--approved", "Place the order (requires --quote). Without it, a priced preview is returned.")
|
|
12089
|
+
.option("--quote <id>", "The quote_id from a fresh preview. Required with --approved.")
|
|
12090
|
+
.option("--json", "Print a JSON envelope.")
|
|
12091
|
+
.action(async (domainArg, options) => {
|
|
12092
|
+
await handleAsyncAction("managed-inboxes add-inboxes", options, () => {
|
|
12093
|
+
const domain = requireDomainArg(domainArg, options.domain);
|
|
12094
|
+
let mailboxes;
|
|
12095
|
+
const mailboxesJson = readOption(options.mailboxes);
|
|
12096
|
+
const filePath = readOption(options.file);
|
|
12097
|
+
const prefix = readOption(options.prefix);
|
|
12098
|
+
const count = readPositiveInt(options.count);
|
|
12099
|
+
if (mailboxesJson) {
|
|
12100
|
+
mailboxes = JSON.parse(mailboxesJson);
|
|
12101
|
+
}
|
|
12102
|
+
else if (filePath) {
|
|
12103
|
+
const parsed = readJsonFileValue(resolve(filePath), "--file");
|
|
12104
|
+
mailboxes = parsed.mailboxes ?? [];
|
|
12105
|
+
}
|
|
12106
|
+
else if (count !== undefined || prefix) {
|
|
12107
|
+
if (count === undefined || !prefix) {
|
|
12108
|
+
throw new Error("--count and --prefix must be passed together (e.g. --count 3 --prefix ada).");
|
|
12109
|
+
}
|
|
12110
|
+
// The vendor stamps a first/last name on every mailbox, so there is no
|
|
12111
|
+
// name-less order shape to fall back on — the shorthand invents
|
|
12112
|
+
// placeholders rather than pretending names are optional. Anyone who
|
|
12113
|
+
// wants real human identities on the inboxes passes --mailboxes/--file.
|
|
12114
|
+
const base = prefix.toLowerCase();
|
|
12115
|
+
const label = `${base.charAt(0).toUpperCase()}${base.slice(1)}`;
|
|
12116
|
+
mailboxes = Array.from({ length: count }, (_entry, index) => ({
|
|
12117
|
+
username: `${base}${index + 1}`,
|
|
12118
|
+
first_name: label,
|
|
12119
|
+
last_name: String(index + 1),
|
|
12120
|
+
}));
|
|
12121
|
+
}
|
|
12122
|
+
else {
|
|
12123
|
+
throw new Error("Provide --mailboxes <json>, --file <path>, or --count <n> --prefix <base>.");
|
|
12124
|
+
}
|
|
12125
|
+
const quote = readOption(options.quote);
|
|
12126
|
+
// Refuse locally rather than spending a round trip on a PAID path: the
|
|
12127
|
+
// route requires the quote that priced this exact expansion, so
|
|
12128
|
+
// `--approved` alone can only ever come back as a 400.
|
|
12129
|
+
if (options.approved && !quote) {
|
|
12130
|
+
// Typed, not a bare Error: a bare throw surfaces as
|
|
12131
|
+
// `unexpected_error`, which an agent cannot tell apart from a
|
|
12132
|
+
// real failure on a PAID command. Refusing to order is an
|
|
12133
|
+
// ordinary, expected outcome and must say so in its code.
|
|
12134
|
+
throw new OxygenError("invalid_request", "--approved requires --quote <id> from a fresh preview. Re-run without --approved to get one.", { exitCode: 2 });
|
|
12135
|
+
}
|
|
12136
|
+
return requestOxygen(`/api/cli/managed-inboxes/${encodeURIComponent(domain)}/mailboxes`, {
|
|
12137
|
+
method: "POST",
|
|
12138
|
+
body: {
|
|
12139
|
+
// Always an explicit mailboxes[]: --count/--prefix is client-side
|
|
12140
|
+
// sugar, so the server sees exactly one shape and the preview it
|
|
12141
|
+
// prices is the list that gets ordered.
|
|
12142
|
+
mailboxes,
|
|
12143
|
+
...(options.approved ? { approved: true } : {}),
|
|
12144
|
+
...(quote ? { quote_id: quote } : {}),
|
|
12145
|
+
},
|
|
12146
|
+
});
|
|
12147
|
+
});
|
|
11649
12148
|
}))
|
|
11650
12149
|
.addCommand(new Command("list")
|
|
11651
12150
|
.description("List the org's managed inbox orders: vendor, platform, live inbox count, lifecycle status, internal billing posture, and each order's FULL monthly cost — the inbox line plus the warm-up and inbox-placement add-ons billing per inbox on top of it (`orders[].total_monthly_credits`, `total_monthly_credits` across the workspace). Read-only, 0 Oxygen credits.")
|
|
@@ -11697,9 +12196,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11697
12196
|
});
|
|
11698
12197
|
})));
|
|
11699
12198
|
program.addCommand(new Command("mailboxes")
|
|
11700
|
-
.description("Native email sending pool: register/refresh Google/Microsoft mailboxes (including secure local Hypertide transfer), pause/disable inboxes, connect EmailGuard monitoring, and run managed TrulyInbox warmup with explicit plans and credit caps.")
|
|
12199
|
+
.description("Native email sending pool: register/refresh Google/Microsoft mailboxes (including secure local Hypertide transfer), pause/disable inboxes, connect EmailGuard monitoring, and run managed TrulyInbox warmup with explicit plans and credit caps. Safe import preflight: run `oxygen mailboxes compatibility --catalog-only --json`, then `oxygen mailboxes import --file <path> --validate-only --json` (add the credential source flags shown by import help when the file contains credentials).")
|
|
11701
12200
|
.addCommand(new Command("list")
|
|
11702
|
-
.description("List the org's sending mailboxes with provider, status, warmup state, and a pool overview.")
|
|
12201
|
+
.description("List the org's sending mailboxes with provider, status, warmup state, source (managed = bought through Oxygen, byok = bring-your-own), and a pool overview (including counts by source).")
|
|
11703
12202
|
.option("--status <status>", "Filter by status: active, paused, or disabled.")
|
|
11704
12203
|
.option("--json", "Print a JSON envelope.")
|
|
11705
12204
|
.action(async (options) => {
|
|
@@ -11713,7 +12212,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11713
12212
|
});
|
|
11714
12213
|
}))
|
|
11715
12214
|
.addCommand(new Command("get")
|
|
11716
|
-
.description("Get one sending mailbox's detail (provider, status, daily cap, warmup state, auth mode) plus a one-row pool summary. An ineligible native-send transport includes transport_reason + transport_hint; it does not by itself block TrulyInbox warmup or EmailGuard monitoring. <mailbox> accepts a mailbox id or email address.")
|
|
12215
|
+
.description("Get one sending mailbox's detail (provider, status, daily cap, warmup state, auth mode, and source — managed vs bring-your-own) plus a one-row pool summary. An ineligible native-send transport includes transport_reason + transport_hint; it does not by itself block TrulyInbox warmup or EmailGuard monitoring. <mailbox> accepts a mailbox id or email address.")
|
|
11717
12216
|
.argument("<mailbox>", "Mailbox id or email address.")
|
|
11718
12217
|
.option("--json", "Print a JSON envelope.")
|
|
11719
12218
|
.action(async (mailbox, options) => {
|
|
@@ -11726,13 +12225,20 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11726
12225
|
await handleAsyncAction("mailboxes health", options, () => requestOxygen("/api/cli/mailboxes/health"));
|
|
11727
12226
|
}))
|
|
11728
12227
|
.addCommand(new Command("compatibility")
|
|
11729
|
-
.description("Read-only compatibility report for every selected mailbox: origin vendor, real infrastructure tier (including InboxKit Azure),
|
|
12228
|
+
.description("Read-only compatibility report for every selected mailbox: origin vendor, real infrastructure tier (including InboxKit Azure), OXYGEN native-send connection quality, TrulyInbox warmup path, EmailGuard monitoring path, and exact next actions. Native send distinguishes destination-bound OAuth, provider-managed API transport, admin delegation, and authorization_required. Use --catalog-only for the compact workspace-independent import contract. JSON data.compatibility contains checked rows; data.import_methods and data.import_fields describe accepted transfer inputs; data.provider_matrix is the complete current 18-pair product matrix; data.non_transferable_auth names credentials that must be reconnected; data.summary rolls up states; data.web_url opens the pool. Downstream states distinguish credential_required, consent_required, and vendor_blocked. Never decrypts a credential or calls a downstream provider.")
|
|
11730
12229
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
|
|
12230
|
+
.option("--catalog-only", "Return only the bounded import-method, field, auth-boundary, and 18-pair provider catalogs; do not read or return workspace mailbox rows.")
|
|
11731
12231
|
.option("--json", "Print a JSON envelope.")
|
|
11732
12232
|
.action(async (options) => {
|
|
11733
12233
|
await handleAsyncAction("mailboxes compatibility", options, () => {
|
|
11734
12234
|
const mailboxes = readCsvOption(options.mailboxes);
|
|
12235
|
+
if (options.catalogOnly === true && mailboxes.length > 0) {
|
|
12236
|
+
throw new OxygenError("invalid_scope", "--catalog-only cannot be combined with --mailboxes.", { exitCode: 1 });
|
|
12237
|
+
}
|
|
11735
12238
|
const params = new URLSearchParams();
|
|
12239
|
+
if (options.catalogOnly === true) {
|
|
12240
|
+
params.set("catalog_only", "true");
|
|
12241
|
+
}
|
|
11736
12242
|
if (mailboxes.length > 0) {
|
|
11737
12243
|
params.set("mailboxes", mailboxes.join(","));
|
|
11738
12244
|
}
|
|
@@ -11741,38 +12247,54 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11741
12247
|
});
|
|
11742
12248
|
}))
|
|
11743
12249
|
.addCommand(new Command("import")
|
|
11744
|
-
.description("Register (or refresh) sending mailboxes in bulk.
|
|
12250
|
+
.description("Register (or refresh) sending mailboxes in bulk. Use --validate-only with a local file for a non-mutating, no-network preflight; without it, import is a 0-credit state mutation whose upsert is idempotent by mailbox address. CSV/JSON/JSONL/XLSX identity files from any vendor use --file plus optional --vendor. Compatible Google app-password exports use --from credentials --vendor <source>; --from hypertide remains a shortcut. Connected Zapmail inventories use --from zapmail. In credential files, app_password is Google-only; Microsoft password fields are discarded locally. OAuth grants, MFA/TOTP seeds, and delegation keys never transfer: Microsoft warmup requires Entra tenant-admin consent and EmailGuard remains vendor_blocked for Microsoft. Google app passwords are sent only in the request body, encrypted server-side, and never returned.")
|
|
11745
12251
|
.addHelpText("after", [
|
|
11746
12252
|
"",
|
|
11747
|
-
"Ordinary identity file contract:",
|
|
11748
|
-
|
|
12253
|
+
"Ordinary identity file contract (CSV / JSON / JSONL / XLSX):",
|
|
12254
|
+
" Canonical fields: email_address, provider, workspace_external_id?, infrastructure_platform?, tenant_id?. Common vendor aliases such as Email, From Email, ESP, Mailbox ID, and Entra Tenant ID are mapped locally. Credentials are rejected.",
|
|
11749
12255
|
"",
|
|
11750
|
-
"
|
|
11751
|
-
" Limits: 500 rows / 5 MB.",
|
|
12256
|
+
"All local file imports:",
|
|
12257
|
+
" Limits: 500 rows / 5 MB for identity, credential, and Hypertide files.",
|
|
12258
|
+
"",
|
|
12259
|
+
"Credential file contract:",
|
|
11752
12260
|
' JSON: {"mailboxes":[{"email_address":"ada@send-acme.com","provider":"google","app_password":"<Google mailbox app password>"}]}',
|
|
11753
|
-
"
|
|
12261
|
+
" Only Google app passwords enter the encrypted seven-day transfer vault. Microsoft rows remain identity-only. Generic SMTP passwords and OAuth/MFA/delegation secrets are rejected.",
|
|
12262
|
+
" Validation: add --validate-only to parse the complete real file and return safe aggregate counts without authentication, a network request, or a workspace write. Any parse, shape, provider, platform, tenant, secret-policy, or duplicate-conflict error rejects the entire file before the first mailbox write and names mailboxes[index]; validation-only never writes. A later import infrastructure failure may interrupt the upsert; re-run the same file because import is idempotent by address.",
|
|
11754
12263
|
" Docs: https://oxygen-agent.com/docs/providers/mailbox-compatibility",
|
|
11755
12264
|
" Skill: oxygen-email-infra (`oxygen skills install --skill oxygen-email-infra`).",
|
|
11756
12265
|
"",
|
|
11757
12266
|
].join("\n"))
|
|
11758
|
-
.option("--file <path>", "Local
|
|
11759
|
-
.option("--from <source>", "Import source: '
|
|
12267
|
+
.option("--file <path>", "Local CSV/JSON/JSONL/XLSX file. Common mailbox-vendor headers are normalized locally; only canonical allowlisted fields cross the network.")
|
|
12268
|
+
.option("--from <source>", "Import source: 'credentials' for a compatible Google app-password export, 'hypertide' as its provider shortcut, or 'zapmail' to pull a connected workspace.")
|
|
12269
|
+
.option("--vendor <slug>", "Non-secret source provenance (for example instantly, mailforge, or smartlead). Required with --from credentials; optional for identity files.")
|
|
11760
12270
|
.option("--connection <id>", "Zapmail connection id (--from zapmail). Defaults to the org's active Zapmail connection.")
|
|
11761
12271
|
.option("--provider <provider>", "Zapmail pool to pull (--from zapmail): google or microsoft. Zapmail's mailbox list is provider-scoped, so the Microsoft pool is only reachable with --provider microsoft; Microsoft mailboxes get their Entra tenant id stamped on import.")
|
|
12272
|
+
.option("--validate-only", "Parse, normalize, and policy-check a local file, then return non-secret aggregate counts without authentication, network access, provider calls, or workspace writes.")
|
|
11762
12273
|
.option("--json", "Print a JSON envelope.")
|
|
11763
12274
|
.action(async (options) => {
|
|
11764
12275
|
await handleAsyncAction("mailboxes import", options, async () => {
|
|
11765
12276
|
const from = readOption(options.from);
|
|
11766
12277
|
const filePath = readOption(options.file);
|
|
12278
|
+
const vendor = readOption(options.vendor);
|
|
11767
12279
|
const connection = readOption(options.connection);
|
|
11768
12280
|
const provider = readOption(options.provider);
|
|
11769
|
-
|
|
11770
|
-
|
|
12281
|
+
const validateOnly = options.validateOnly === true;
|
|
12282
|
+
if (from &&
|
|
12283
|
+
from !== "zapmail" &&
|
|
12284
|
+
from !== "hypertide" &&
|
|
12285
|
+
from !== "credentials") {
|
|
12286
|
+
throw new Error("--from must be credentials, hypertide, or zapmail.");
|
|
11771
12287
|
}
|
|
11772
12288
|
if (from === "zapmail") {
|
|
12289
|
+
if (validateOnly) {
|
|
12290
|
+
throw new Error("--validate-only applies to local --file imports and cannot be combined with --from zapmail.");
|
|
12291
|
+
}
|
|
11773
12292
|
if (filePath) {
|
|
11774
12293
|
throw new Error("--file cannot be combined with --from zapmail.");
|
|
11775
12294
|
}
|
|
12295
|
+
if (vendor) {
|
|
12296
|
+
throw new Error("--vendor cannot be combined with --from zapmail; the connected integration is authoritative.");
|
|
12297
|
+
}
|
|
11776
12298
|
return requestOxygen("/api/cli/mailboxes", {
|
|
11777
12299
|
method: "POST",
|
|
11778
12300
|
body: {
|
|
@@ -11784,13 +12306,36 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11784
12306
|
}
|
|
11785
12307
|
if (provider)
|
|
11786
12308
|
throw new Error("--provider only applies with --from zapmail (inline files carry a per-mailbox provider).");
|
|
12309
|
+
if (connection) {
|
|
12310
|
+
throw new Error("--connection only applies with --from zapmail.");
|
|
12311
|
+
}
|
|
12312
|
+
const sourceProvider = normalizeMailboxImportVendor(vendor, from);
|
|
11787
12313
|
if (!filePath)
|
|
11788
|
-
throw new Error("Provide --file <path>, --from hypertide --file <path>, or --from zapmail.");
|
|
11789
|
-
const mailboxes = normalizeMailboxImportFile(await readMailboxImportFile(resolve(filePath)), from === "hypertide"
|
|
12314
|
+
throw new Error("Provide --file <path>, --from credentials --vendor <source> --file <path>, --from hypertide --file <path>, or --from zapmail.");
|
|
12315
|
+
const mailboxes = normalizeMailboxImportFile(await readMailboxImportFile(resolve(filePath)), from === "hypertide" || from === "credentials"
|
|
12316
|
+
? "credential"
|
|
12317
|
+
: "identity");
|
|
12318
|
+
if (validateOnly) {
|
|
12319
|
+
return summarizeMailboxImportValidation(mailboxes, {
|
|
12320
|
+
source: from === "hypertide"
|
|
12321
|
+
? "hypertide_file"
|
|
12322
|
+
: from === "credentials"
|
|
12323
|
+
? "credential_file"
|
|
12324
|
+
: "identity_file",
|
|
12325
|
+
sourceProvider,
|
|
12326
|
+
});
|
|
12327
|
+
}
|
|
11790
12328
|
return requestOxygen("/api/cli/mailboxes", {
|
|
11791
12329
|
method: "POST",
|
|
11792
12330
|
body: {
|
|
11793
|
-
...(from === "hypertide"
|
|
12331
|
+
...(from === "hypertide"
|
|
12332
|
+
? { source: "hypertide" }
|
|
12333
|
+
: from === "credentials"
|
|
12334
|
+
? { source: "credential_file" }
|
|
12335
|
+
: {}),
|
|
12336
|
+
...(sourceProvider
|
|
12337
|
+
? { source_provider: sourceProvider }
|
|
12338
|
+
: {}),
|
|
11794
12339
|
mailboxes,
|
|
11795
12340
|
},
|
|
11796
12341
|
});
|
|
@@ -11913,40 +12458,60 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11913
12458
|
});
|
|
11914
12459
|
}))
|
|
11915
12460
|
.addCommand(new Command("connect-oauth")
|
|
11916
|
-
.description("
|
|
12461
|
+
.description("Connect Google/Microsoft mailboxes to OXYGEN native send with fresh destination-bound OAuth. Preview by default; pass --approved only after reviewing exact candidates. --vendor oxygen handles imported, Hypertide, external, or manual mailboxes: it requires an exact list (max 10), returns one OXYGEN browser link per candidate, and stores a grant only after that exact mailbox completes provider consent/MFA. Source tokens, passwords, authenticator seeds, and one-time codes never transfer. --vendor zapmail (default) hands a provisioned pool to Zapmail Custom OAuth; --vendor inboxkit requests domain-gated consent with one canary on unproven domains and a 10-write live cap. All paths cost 0 Oxygen credits. Poll --status <id>; connected means an encrypted refresh token actually landed, never merely that a request was accepted.")
|
|
11917
12462
|
.option("--provider <provider>", "Mailbox provider to provision: google or microsoft.")
|
|
11918
|
-
.option("--
|
|
12463
|
+
.option("--vendor <vendor>", "Authorization path: oxygen for imported/manual mailboxes, zapmail (default), or inboxkit.")
|
|
12464
|
+
.option("--mailboxes <list>", "Comma-separated mailbox addresses. Required for vendor=oxygen (max 10); omit for vendor-provisioned whole-pool flows.")
|
|
12465
|
+
.option("--domains <list>", "Comma-separated sending domains to limit an inboxkit run to. Omit to cover every domain in the pool.")
|
|
12466
|
+
.option("--no-canary", "inboxkit only: fan out to every eligible mailbox on a domain that has never connected one, instead of firing a single canary first.")
|
|
11919
12467
|
.option("--connection <id>", "Zapmail connection id. Defaults to the org's active Zapmail connection.")
|
|
11920
|
-
.option("--status <
|
|
12468
|
+
.option("--status <id>", "Poll a previously started Zapmail, InboxKit, or OXYGEN direct-consent run.")
|
|
12469
|
+
.option("--dry-run", "Preview the requested mailbox/domain scope and external writes without changing vendor or ledger state. This is the default for starts; a successful preview can still have zero eligible mailboxes, so check InboxKit candidates with oauth-health before approval.")
|
|
12470
|
+
.option("--approved", "Start the reviewed authorization run. For vendor=oxygen this records expiring browser steps; provider consent still occurs only when the user opens each link.")
|
|
11921
12471
|
.option("--json", "Print a JSON envelope.")
|
|
11922
12472
|
.action(async (options) => {
|
|
11923
12473
|
await handleAsyncAction("mailboxes connect-oauth", options, () => {
|
|
12474
|
+
const vendor = readOption(options.vendor);
|
|
11924
12475
|
const exportId = readOption(options.status);
|
|
11925
12476
|
if (exportId) {
|
|
11926
12477
|
const params = new URLSearchParams({ export_id: exportId });
|
|
11927
12478
|
const pollConnection = readOption(options.connection);
|
|
11928
12479
|
if (pollConnection)
|
|
11929
12480
|
params.set("connection_id", pollConnection);
|
|
12481
|
+
// Omitted, the server reads the vendor off the id's shape (numeric
|
|
12482
|
+
// export id vs run uuid); this is the override for the rare id that
|
|
12483
|
+
// does not look like either.
|
|
12484
|
+
if (vendor)
|
|
12485
|
+
params.set("vendor", vendor);
|
|
11930
12486
|
return requestOxygen(`/api/cli/mailboxes/connect-oauth?${params.toString()}`);
|
|
11931
12487
|
}
|
|
11932
12488
|
const provider = readOption(options.provider);
|
|
11933
12489
|
if (!provider) {
|
|
11934
|
-
throw new Error("--provider <google|microsoft> is required (or pass --status <
|
|
12490
|
+
throw new Error("--provider <google|microsoft> is required (or pass --status <id> to poll a run).");
|
|
11935
12491
|
}
|
|
11936
12492
|
const mailboxes = readCsvOption(options.mailboxes);
|
|
12493
|
+
const domains = readCsvOption(options.domains);
|
|
11937
12494
|
const connection = readOption(options.connection);
|
|
11938
12495
|
return requestOxygen("/api/cli/mailboxes/connect-oauth", {
|
|
11939
12496
|
method: "POST",
|
|
11940
12497
|
body: {
|
|
11941
12498
|
provider,
|
|
12499
|
+
// Every key stays absent unless it was asked for, so a run
|
|
12500
|
+
// without the new flags is the request this command has always
|
|
12501
|
+
// sent — the server reads an absent vendor as zapmail.
|
|
12502
|
+
...(vendor ? { vendor } : {}),
|
|
11942
12503
|
...(mailboxes.length > 0 ? { mailboxes } : {}),
|
|
12504
|
+
...(domains.length > 0 ? { domains } : {}),
|
|
12505
|
+
...(options.canary === false ? { canary: false } : {}),
|
|
11943
12506
|
...(connection ? { connection_id: connection } : {}),
|
|
12507
|
+
...(options.dryRun ? { dry_run: true } : {}),
|
|
12508
|
+
...(options.approved ? { approved: true } : {}),
|
|
11944
12509
|
},
|
|
11945
12510
|
});
|
|
11946
12511
|
});
|
|
11947
12512
|
}))
|
|
11948
12513
|
.addCommand(new Command("oauth-health")
|
|
11949
|
-
.description("Show
|
|
12514
|
+
.description("Show every Google/Microsoft inbox with neither a per-mailbox OAuth token nor covered Google delegation. Remedies are connect_oauth for Zapmail, connect_oauth_inboxkit for InboxKit, and connect_oauth_oxygen for imported/manual mailboxes. Zapmail rows include their 3-per-7-day export budget; OXYGEN direct rows require exact browser consent and may invoke provider MFA. Read-only — 0 Oxygen credits.")
|
|
11950
12515
|
.option("--json", "Print a JSON envelope.")
|
|
11951
12516
|
.action(async (options) => {
|
|
11952
12517
|
await handleAsyncAction("mailboxes oauth-health", options, () => requestOxygen("/api/cli/mailboxes/oauth-health"));
|
|
@@ -12774,7 +13339,7 @@ Run completion:
|
|
|
12774
13339
|
.description("Deprecated alias for `oxygen blueprints apply`. Creates a disabled workflow (plus its tables, columns, and prompts) from a blueprint.")
|
|
12775
13340
|
.argument("<template_id>", "Blueprint slug (formerly workflow template id).")
|
|
12776
13341
|
.requiredOption("--input-json <json>", "Blueprint input as a JSON object.")
|
|
12777
|
-
.option("--workflow-id <workflow_id>", "
|
|
13342
|
+
.option("--workflow-id <workflow_id>", "Set the new workflow's manifest id/slug; this does not update or reuse an existing workflow.")
|
|
12778
13343
|
.option("--workflow-name <workflow_name>", "Override the resulting workflow name.")
|
|
12779
13344
|
.option("--mode <mode>", "Deprecated and ignored: the workflow is created disabled.")
|
|
12780
13345
|
.option("--max-credits <credits>", "Credit ceiling; folded into inputs.max_credits.")
|
|
@@ -12876,7 +13441,8 @@ Run completion:
|
|
|
12876
13441
|
params.set("tag", tag);
|
|
12877
13442
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
12878
13443
|
const data = await requestOxygen(`/api/cli/workflows${qs}`);
|
|
12879
|
-
|
|
13444
|
+
if (!options.json)
|
|
13445
|
+
writeDisabledWorkflowNotices(data);
|
|
12880
13446
|
return data;
|
|
12881
13447
|
});
|
|
12882
13448
|
}))
|
|
@@ -12891,7 +13457,8 @@ Run completion:
|
|
|
12891
13457
|
method: "POST",
|
|
12892
13458
|
body: { workflow },
|
|
12893
13459
|
});
|
|
12894
|
-
|
|
13460
|
+
if (!options.json)
|
|
13461
|
+
writeDisabledWorkflowNotices(data);
|
|
12895
13462
|
return prepareWorkflowCliOutput(data, options);
|
|
12896
13463
|
});
|
|
12897
13464
|
}))
|
|
@@ -13386,11 +13953,28 @@ Run completion:
|
|
|
13386
13953
|
.command("skills")
|
|
13387
13954
|
.description("Agent skill discovery and installation commands.")
|
|
13388
13955
|
.addCommand(new Command("list")
|
|
13389
|
-
.description("List Oxygen agent skills available from the
|
|
13956
|
+
.description("List Oxygen agent skills available from the hosted skill index.")
|
|
13390
13957
|
.option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
|
|
13391
13958
|
.option("--json", "Print a JSON envelope.")
|
|
13392
13959
|
.action(async (options) => {
|
|
13393
13960
|
await handleAsyncAction("skills list", options, () => listAgentSkills(options));
|
|
13961
|
+
}))
|
|
13962
|
+
.addCommand(new Command("search")
|
|
13963
|
+
.description("Find a bounded set of Oxygen skills and matching documents from an outcome.")
|
|
13964
|
+
.argument("<query...>", "Outcome, primitive, provider, or operating task.")
|
|
13965
|
+
.option("--limit <n>", "Maximum results (default 5, max 10).")
|
|
13966
|
+
.option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
|
|
13967
|
+
.option("--json", "Print a JSON envelope.")
|
|
13968
|
+
.action(async (queryParts, options) => {
|
|
13969
|
+
await handleAsyncAction("skills search", options, () => searchAgentSkills(queryParts.join(" "), options));
|
|
13970
|
+
}))
|
|
13971
|
+
.addCommand(new Command("get")
|
|
13972
|
+
.description("Hydrate one exact Oxygen skill entrypoint and its document index.")
|
|
13973
|
+
.argument("<skill-name>", "Exact name returned by skills search.")
|
|
13974
|
+
.option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
|
|
13975
|
+
.option("--json", "Print a JSON envelope.")
|
|
13976
|
+
.action(async (skillName, options) => {
|
|
13977
|
+
await handleAsyncAction("skills get", options, () => getAgentSkill(skillName, options));
|
|
13394
13978
|
}))
|
|
13395
13979
|
.addCommand(new Command("doctor")
|
|
13396
13980
|
.description("Check Oxygen skill index reachability and local installer prerequisites.")
|
|
@@ -13400,7 +13984,7 @@ Run completion:
|
|
|
13400
13984
|
await handleAsyncAction("skills doctor", options, () => doctorAgentSkills(options));
|
|
13401
13985
|
}))
|
|
13402
13986
|
.addCommand(new Command("install")
|
|
13403
|
-
.description("Install Oxygen agent skills into local agent skill directories.")
|
|
13987
|
+
.description("Install Oxygen agent skills into local agent skill directories only; uses 0 credits and does not change the Oxygen workspace or call a provider.")
|
|
13404
13988
|
.option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
|
|
13405
13989
|
.option("--agents <agents...>", "Space or comma separated agents. Defaults to codex, claude-code, and cursor.")
|
|
13406
13990
|
.option("--skill <skill>", "Skill name or '*'. Defaults to '*'.")
|