@oxygen-agent/cli 1.1014.136 → 1.1014.618
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/admin-primary-providers-render.js +27 -19
- package/dist/cli-output.d.ts +12 -0
- package/dist/cli-output.js +395 -0
- package/dist/cli-paths.d.ts +1 -0
- package/dist/cli-paths.js +3 -0
- package/dist/cli-profile-context.d.ts +3 -0
- package/dist/cli-profile-context.js +13 -0
- package/dist/command-manifest.d.ts +15 -0
- package/dist/command-manifest.js +23 -1
- package/dist/command-support.d.ts +438 -0
- package/dist/command-support.js +1610 -0
- package/dist/commands/action-column.d.ts +3 -0
- package/dist/commands/action-column.js +43 -0
- package/dist/commands/activation.d.ts +3 -0
- package/dist/commands/activation.js +24 -0
- package/dist/commands/admin.d.ts +3 -0
- package/dist/commands/admin.js +347 -0
- package/dist/commands/ads.d.ts +3 -0
- package/dist/commands/ads.js +62 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.js +274 -0
- package/dist/commands/api-keys.d.ts +3 -0
- package/dist/commands/api-keys.js +77 -0
- package/dist/commands/approvals.d.ts +3 -0
- package/dist/commands/approvals.js +518 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.js +230 -0
- package/dist/commands/billing.d.ts +3 -0
- package/dist/commands/billing.js +357 -0
- package/dist/commands/blueprints.d.ts +3 -0
- package/dist/commands/blueprints.js +312 -0
- package/dist/commands/budget.d.ts +3 -0
- package/dist/commands/budget.js +55 -0
- package/dist/commands/callables.d.ts +3 -0
- package/dist/commands/callables.js +111 -0
- package/dist/commands/capabilities.d.ts +3 -0
- package/dist/commands/capabilities.js +44 -0
- package/dist/commands/cells.d.ts +3 -0
- package/dist/commands/cells.js +94 -0
- package/dist/commands/columns.d.ts +3 -0
- package/dist/commands/columns.js +1628 -0
- package/dist/commands/commands.d.ts +3 -0
- package/dist/commands/commands.js +45 -0
- package/dist/commands/comments.d.ts +3 -0
- package/dist/commands/comments.js +188 -0
- package/dist/commands/companies.d.ts +3 -0
- package/dist/commands/companies.js +312 -0
- package/dist/commands/connections.d.ts +3 -0
- package/dist/commands/connections.js +39 -0
- package/dist/commands/context.d.ts +3 -0
- package/dist/commands/context.js +170 -0
- package/dist/commands/copilot.d.ts +3 -0
- package/dist/commands/copilot.js +565 -0
- package/dist/commands/crm.d.ts +3 -0
- package/dist/commands/crm.js +1669 -0
- package/dist/commands/custom-integrations.d.ts +3 -0
- package/dist/commands/custom-integrations.js +140 -0
- package/dist/commands/dashboards.d.ts +3 -0
- package/dist/commands/dashboards.js +240 -0
- package/dist/commands/db.d.ts +3 -0
- package/dist/commands/db.js +190 -0
- package/dist/commands/deliverability.d.ts +3 -0
- package/dist/commands/deliverability.js +90 -0
- package/dist/commands/directory.d.ts +3 -0
- package/dist/commands/directory.js +101 -0
- package/dist/commands/domains.d.ts +3 -0
- package/dist/commands/domains.js +604 -0
- package/dist/commands/egress.d.ts +3 -0
- package/dist/commands/egress.js +130 -0
- package/dist/commands/email.d.ts +3 -0
- package/dist/commands/email.js +49 -0
- package/dist/commands/engagement.d.ts +3 -0
- package/dist/commands/engagement.js +189 -0
- package/dist/commands/enrich-column.d.ts +3 -0
- package/dist/commands/enrich-column.js +149 -0
- package/dist/commands/enrichment.d.ts +3 -0
- package/dist/commands/enrichment.js +49 -0
- package/dist/commands/feedback.d.ts +3 -0
- package/dist/commands/feedback.js +96 -0
- package/dist/commands/feeds.d.ts +3 -0
- package/dist/commands/feeds.js +486 -0
- package/dist/commands/find.d.ts +3 -0
- package/dist/commands/find.js +159 -0
- package/dist/commands/followers.d.ts +3 -0
- package/dist/commands/followers.js +39 -0
- package/dist/commands/formulas.d.ts +3 -0
- package/dist/commands/formulas.js +43 -0
- package/dist/commands/home.d.ts +3 -0
- package/dist/commands/home.js +22 -0
- package/dist/commands/inbox.d.ts +3 -0
- package/dist/commands/inbox.js +779 -0
- package/dist/commands/integrations.d.ts +3 -0
- package/dist/commands/integrations.js +290 -0
- package/dist/commands/knowledge.d.ts +3 -0
- package/dist/commands/knowledge.js +874 -0
- package/dist/commands/lead-sourcing.d.ts +3 -0
- package/dist/commands/lead-sourcing.js +52 -0
- package/dist/commands/limits.d.ts +3 -0
- package/dist/commands/limits.js +54 -0
- package/dist/commands/linkedin.d.ts +3 -0
- package/dist/commands/linkedin.js +275 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.js +347 -0
- package/dist/commands/logout.d.ts +3 -0
- package/dist/commands/logout.js +12 -0
- package/dist/commands/mailboxes.d.ts +3 -0
- package/dist/commands/mailboxes.js +1290 -0
- package/dist/commands/managed-inboxes.d.ts +3 -0
- package/dist/commands/managed-inboxes.js +361 -0
- package/dist/commands/messages.d.ts +3 -0
- package/dist/commands/messages.js +78 -0
- package/dist/commands/notetaker.d.ts +3 -0
- package/dist/commands/notetaker.js +79 -0
- package/dist/commands/observability.d.ts +3 -0
- package/dist/commands/observability.js +125 -0
- package/dist/commands/orgs.d.ts +3 -0
- package/dist/commands/orgs.js +237 -0
- package/dist/commands/people.d.ts +3 -0
- package/dist/commands/people.js +353 -0
- package/dist/commands/posts.d.ts +3 -0
- package/dist/commands/posts.js +139 -0
- package/dist/commands/profiles.d.ts +3 -0
- package/dist/commands/profiles.js +250 -0
- package/dist/commands/projects.d.ts +3 -0
- package/dist/commands/projects.js +94 -0
- package/dist/commands/prompts.d.ts +3 -0
- package/dist/commands/prompts.js +84 -0
- package/dist/commands/publishing.d.ts +3 -0
- package/dist/commands/publishing.js +1419 -0
- package/dist/commands/recipes.d.ts +3 -0
- package/dist/commands/recipes.js +120 -0
- package/dist/commands/reviews.d.ts +3 -0
- package/dist/commands/reviews.js +75 -0
- package/dist/commands/rows.d.ts +3 -0
- package/dist/commands/rows.js +27 -0
- package/dist/commands/runs.d.ts +3 -0
- package/dist/commands/runs.js +37 -0
- package/dist/commands/search.d.ts +3 -0
- package/dist/commands/search.js +182 -0
- package/dist/commands/senders.d.ts +3 -0
- package/dist/commands/senders.js +393 -0
- package/dist/commands/sequences.d.ts +3 -0
- package/dist/commands/sequences.js +2322 -0
- package/dist/commands/session.d.ts +3 -0
- package/dist/commands/session.js +109 -0
- package/dist/commands/signals.d.ts +3 -0
- package/dist/commands/signals.js +289 -0
- package/dist/commands/signup-leads.d.ts +3 -0
- package/dist/commands/signup-leads.js +46 -0
- package/dist/commands/skills.d.ts +3 -0
- package/dist/commands/skills.js +50 -0
- package/dist/commands/sourcing.d.ts +3 -0
- package/dist/commands/sourcing.js +27 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +51 -0
- package/dist/commands/supabase.d.ts +3 -0
- package/dist/commands/supabase.js +128 -0
- package/dist/commands/support.d.ts +3 -0
- package/dist/commands/support.js +651 -0
- package/dist/commands/suppressions.d.ts +3 -0
- package/dist/commands/suppressions.js +577 -0
- package/dist/commands/table-ingestions.d.ts +3 -0
- package/dist/commands/table-ingestions.js +145 -0
- package/dist/commands/table-runs.d.ts +3 -0
- package/dist/commands/table-runs.js +277 -0
- package/dist/commands/tables.d.ts +3 -0
- package/dist/commands/tables.js +3492 -0
- package/dist/commands/tags.d.ts +3 -0
- package/dist/commands/tags.js +147 -0
- package/dist/commands/tools.d.ts +3 -0
- package/dist/commands/tools.js +246 -0
- package/dist/commands/update.d.ts +3 -0
- package/dist/commands/update.js +106 -0
- package/dist/commands/verify.d.ts +3 -0
- package/dist/commands/verify.js +27 -0
- package/dist/commands/viewers.d.ts +3 -0
- package/dist/commands/viewers.js +72 -0
- package/dist/commands/voice.d.ts +3 -0
- package/dist/commands/voice.js +351 -0
- package/dist/commands/whatsapp.d.ts +3 -0
- package/dist/commands/whatsapp.js +226 -0
- package/dist/commands/whoami.d.ts +3 -0
- package/dist/commands/whoami.js +110 -0
- package/dist/commands/worker.d.ts +3 -0
- package/dist/commands/worker.js +60 -0
- package/dist/commands/workflows.d.ts +3 -0
- package/dist/commands/workflows.js +1752 -0
- package/dist/commands/workspace.d.ts +3 -0
- package/dist/commands/workspace.js +94 -0
- package/dist/compact-list-flags.d.ts +7 -0
- package/dist/compact-list-flags.js +12 -0
- package/dist/crm-support-flags.js +14 -5
- package/dist/customer-operation-commands.js +8 -6
- package/dist/diagnostic-uploader.d.ts +3 -1
- package/dist/diagnostic-uploader.js +29 -8
- package/dist/help.d.ts +5 -0
- package/dist/help.js +22 -0
- package/dist/index.d.ts +98 -1
- package/dist/index.js +331 -30719
- package/dist/local-module-loader.d.ts +4 -0
- package/dist/local-module-loader.js +156 -0
- package/dist/visual-commands.js +9 -5
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +2 -2
- package/node_modules/@oxygen/shared/dist/agent-session-schedule.d.ts +36 -0
- package/node_modules/@oxygen/shared/dist/agent-session-schedule.js +69 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +14 -5
- package/node_modules/@oxygen/shared/dist/agent-sessions.js +14 -3
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +175 -14
- package/node_modules/@oxygen/shared/dist/compact-list-result.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/compact-list-result.js +27 -0
- package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.d.ts +12 -1
- package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +5 -2
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +4 -2
- package/node_modules/@oxygen/shared/dist/contracts/compact-list-reads.d.ts +57 -0
- package/node_modules/@oxygen/shared/dist/contracts/compact-list-reads.js +23 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +52 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +43 -2
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +17 -14
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-proposal-reads.d.ts +49 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-proposal-reads.js +14 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-sync.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/limits-read.js +60 -7
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +9 -1
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +13 -5
- package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/teammates.d.ts +16 -0
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +115 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +21 -3
- package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +31 -1
- package/node_modules/@oxygen/shared/dist/cost-estimate.js +56 -15
- package/node_modules/@oxygen/shared/dist/diagnostic-credentials.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-credentials.js +2 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-lock.js +71 -44
- package/node_modules/@oxygen/shared/dist/diagnostic-spool.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-spool.js +348 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage-failure.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage-failure.js +42 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +59 -213
- package/node_modules/@oxygen/shared/dist/diagnostics.js +3 -2
- package/node_modules/@oxygen/shared/dist/email-dsn.d.ts +16 -5
- package/node_modules/@oxygen/shared/dist/email-dsn.js +97 -1
- package/node_modules/@oxygen/shared/dist/emailguard-placement-probe.js +5 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +4 -5
- package/node_modules/@oxygen/shared/dist/file-import.js +5 -2
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +20 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +6 -2
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.d.ts +17 -7
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +81 -17
- package/node_modules/@oxygen/shared/dist/langfuse.js +8 -4
- package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +6 -3
- package/node_modules/@oxygen/shared/dist/mailbox-import.js +9 -9
- package/node_modules/@oxygen/shared/dist/mcp-server-info.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/mcp-server-info.js +10 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +14 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.js +17 -1
- 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/provider-selection.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/provider-selection.js +3 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/repricing.js +2 -2
- package/node_modules/@oxygen/shared/dist/schedule-label.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/schedule-label.js +12 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +20 -6
- package/node_modules/@oxygen/shared/dist/teammate-groups.d.ts +55 -0
- package/node_modules/@oxygen/shared/dist/teammate-groups.js +74 -0
- package/node_modules/@oxygen/shared/dist/teammate-messages.d.ts +52 -0
- package/node_modules/@oxygen/shared/dist/teammate-messages.js +88 -0
- package/node_modules/@oxygen/shared/dist/teammate-notes.d.ts +58 -0
- package/node_modules/@oxygen/shared/dist/teammate-notes.js +73 -0
- package/node_modules/@oxygen/shared/dist/teammate-routines.d.ts +95 -0
- package/node_modules/@oxygen/shared/dist/teammate-routines.js +266 -0
- package/node_modules/@oxygen/shared/dist/teammate-tasks.d.ts +56 -0
- package/node_modules/@oxygen/shared/dist/teammate-tasks.js +114 -0
- package/node_modules/@oxygen/shared/dist/teammates.d.ts +78 -0
- package/node_modules/@oxygen/shared/dist/teammates.js +140 -0
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +120 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/visual-render.d.ts +9 -1
- package/node_modules/@oxygen/shared/dist/visual-render.js +12 -5
- package/node_modules/@oxygen/shared/dist/warmup-metrics.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/warmup-metrics.js +44 -0
- package/package.json +1 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { emitCliFailure, writeJson } from "../cli-output.js";
|
|
2
|
+
import { ansi, buildPostLoginHint, createCredentialFingerprint, formatFingerprint, formatLoginSuccessForResolved, pickProfileForLogin, readEnvProfileName, resolveActiveProfileWithSource, resolveLoginIdentity } from "../command-support.js";
|
|
3
|
+
import { defaultApiUrl, normalizeApiUrl, saveCredentials } from "../credentials.js";
|
|
4
|
+
import { ensureFreshCliForApiUrl, requestOxygen } from "../http-client.js";
|
|
5
|
+
import { readOption } from "../util.js";
|
|
6
|
+
import { exitCodeForOxygenError, OxygenError, success, toFailure } from "@oxygen/shared/cli-result";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { stdout as output } from "node:process";
|
|
9
|
+
async function handleAuthUseTokenAction(options) {
|
|
10
|
+
try {
|
|
11
|
+
const data = await applyAuthToken(options);
|
|
12
|
+
if (options.json) {
|
|
13
|
+
writeJson(success("auth use-token", {
|
|
14
|
+
logged_in: true,
|
|
15
|
+
profile: data.profile,
|
|
16
|
+
api_url: data.credentials.apiUrl,
|
|
17
|
+
user: data.loginIdentity.user,
|
|
18
|
+
organization: data.loginIdentity.activeOrganization,
|
|
19
|
+
organizations: data.loginIdentity.organizations,
|
|
20
|
+
selection_required: data.loginIdentity.selectionRequired,
|
|
21
|
+
renamed: data.renamed,
|
|
22
|
+
}));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
process.stdout.write(formatLoginSuccessForResolved(data.loginIdentity, data.profile, { renamed: data.renamed }));
|
|
26
|
+
const hint = await buildPostLoginHint(data.profile);
|
|
27
|
+
if (hint)
|
|
28
|
+
process.stdout.write(hint);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const failure = toFailure("auth use-token", error);
|
|
32
|
+
if (options.json)
|
|
33
|
+
writeJson(failure);
|
|
34
|
+
else
|
|
35
|
+
process.stderr.write(`${failure.error.message}\n`);
|
|
36
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function handleAuthDoctorAction(options) {
|
|
40
|
+
try {
|
|
41
|
+
const data = await runAuthDoctor();
|
|
42
|
+
if (options.json) {
|
|
43
|
+
writeJson(success("auth doctor", data));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
process.stdout.write(formatAuthDoctor(data));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
emitCliFailure("auth doctor", error);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function runAuthDoctor() {
|
|
53
|
+
let context = null;
|
|
54
|
+
let profileError = null;
|
|
55
|
+
try {
|
|
56
|
+
context = await resolveActiveProfileWithSource();
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
const failure = toFailure("profiles current", error);
|
|
60
|
+
profileError = {
|
|
61
|
+
code: failure.error.code,
|
|
62
|
+
message: failure.error.message,
|
|
63
|
+
...(failure.error.details !== undefined ? { details: failure.error.details } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const credentials = context?.resolution.credentials ?? null;
|
|
67
|
+
const apiUrl = credentials?.apiUrl ?? defaultApiUrl();
|
|
68
|
+
const healthCredentials = credentials ?? { token: "", apiUrl };
|
|
69
|
+
const health = await runAuthDoctorCheck(() => requestOxygen("/api/health", {
|
|
70
|
+
credentials: healthCredentials,
|
|
71
|
+
requireAuth: false,
|
|
72
|
+
enforceMinimumCliVersion: false,
|
|
73
|
+
}));
|
|
74
|
+
const identity = credentials
|
|
75
|
+
? await runAuthDoctorCheck(() => requestOxygen("/api/cli/whoami", {
|
|
76
|
+
credentials,
|
|
77
|
+
enforceMinimumCliVersion: false,
|
|
78
|
+
}))
|
|
79
|
+
: { ok: false, skipped: true, reason: "no_credentials" };
|
|
80
|
+
const cachedIdentity = credentials?.identity ?? null;
|
|
81
|
+
const cachedOrganization = credentials?.activeOrganization ?? cachedIdentity?.organization ?? null;
|
|
82
|
+
// The live read wins over the stored `authKind`: a profile written by an older
|
|
83
|
+
// CLI can carry no kind at all, and the server always knows.
|
|
84
|
+
const liveAuthType = "ok" in identity && identity.ok && typeof identity.data.authType === "string"
|
|
85
|
+
? identity.data.authType
|
|
86
|
+
: null;
|
|
87
|
+
return {
|
|
88
|
+
profile: context?.resolution.exists ? context.resolution.name : null,
|
|
89
|
+
profile_source: context?.source ?? null,
|
|
90
|
+
stored: Boolean(context?.resolution.exists),
|
|
91
|
+
api_url: apiUrl,
|
|
92
|
+
auth_kind: credentials?.authKind ?? null,
|
|
93
|
+
token_fingerprint: credentials ? formatFingerprint(createCredentialFingerprint(credentials.token)) : null,
|
|
94
|
+
cached_organization: cachedOrganization,
|
|
95
|
+
cached_user: cachedIdentity?.user ?? null,
|
|
96
|
+
profile_error: profileError,
|
|
97
|
+
org_switching: {
|
|
98
|
+
switchable: resolveOrgSwitchability(liveAuthType ?? credentials?.authKind ?? null),
|
|
99
|
+
current_organization: cachedOrganization,
|
|
100
|
+
},
|
|
101
|
+
health,
|
|
102
|
+
identity,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function resolveOrgSwitchability(authKind) {
|
|
106
|
+
if (authKind === "user_session" || authKind === "oauth")
|
|
107
|
+
return true;
|
|
108
|
+
if (authKind === "org_api_key")
|
|
109
|
+
return false;
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
async function runAuthDoctorCheck(fn) {
|
|
113
|
+
try {
|
|
114
|
+
return { ok: true, data: await fn() };
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const failure = toFailure("auth doctor", error);
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
error: {
|
|
121
|
+
code: failure.error.code,
|
|
122
|
+
message: failure.error.message,
|
|
123
|
+
...(failure.error.details !== undefined ? { details: failure.error.details } : {}),
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function applyAuthToken(options) {
|
|
129
|
+
const token = readOption(options.token);
|
|
130
|
+
if (!token) {
|
|
131
|
+
throw new OxygenError("missing_token", "Pass a CLI API token with --token.", {
|
|
132
|
+
exitCode: 1,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
|
|
136
|
+
await ensureFreshCliForApiUrl(apiUrl);
|
|
137
|
+
const credentials = {
|
|
138
|
+
token,
|
|
139
|
+
apiUrl,
|
|
140
|
+
};
|
|
141
|
+
const loginIdentity = await resolveLoginIdentity(token, credentials.apiUrl);
|
|
142
|
+
const explicitProfile = readOption(options.profile) ?? readEnvProfileName();
|
|
143
|
+
let chosenProfile;
|
|
144
|
+
let renamed = false;
|
|
145
|
+
if (explicitProfile) {
|
|
146
|
+
chosenProfile = explicitProfile;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
const picked = await pickProfileForLogin(loginIdentity);
|
|
150
|
+
chosenProfile = picked.name;
|
|
151
|
+
renamed = picked.renamed;
|
|
152
|
+
}
|
|
153
|
+
const profile = await saveCredentials(loginIdentity.credentials, process.env, {
|
|
154
|
+
profile: chosenProfile,
|
|
155
|
+
allowHostChange: options.force === true,
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
identity: loginIdentity.whoami,
|
|
159
|
+
loginIdentity,
|
|
160
|
+
credentials: loginIdentity.credentials,
|
|
161
|
+
api_url: loginIdentity.credentials.apiUrl,
|
|
162
|
+
profile,
|
|
163
|
+
renamed,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function formatAuthDoctor(data) {
|
|
167
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
168
|
+
const lines = [
|
|
169
|
+
"",
|
|
170
|
+
styles.bold("Oxygen Auth Doctor"),
|
|
171
|
+
"",
|
|
172
|
+
` ${styles.dim("Profile")} ${data.profile ?? "(none)"}${data.profile_source ? ` (${data.profile_source})` : ""}`,
|
|
173
|
+
` ${styles.dim("API")} ${data.api_url}`,
|
|
174
|
+
` ${styles.dim("Stored")} ${data.stored ? "yes" : "no"}`,
|
|
175
|
+
];
|
|
176
|
+
if (data.auth_kind)
|
|
177
|
+
lines.push(` ${styles.dim("Auth kind")} ${data.auth_kind}`);
|
|
178
|
+
if (data.token_fingerprint)
|
|
179
|
+
lines.push(` ${styles.dim("Fingerprint")} ${data.token_fingerprint}`);
|
|
180
|
+
if (data.cached_organization) {
|
|
181
|
+
const org = data.cached_organization.slug ?? data.cached_organization.id;
|
|
182
|
+
lines.push(` ${styles.dim("Cached org")} ${data.cached_organization.name} (${org})`);
|
|
183
|
+
}
|
|
184
|
+
lines.push(` ${styles.dim("Org switching")} ${formatOrgSwitching(data.org_switching)}`);
|
|
185
|
+
if (data.profile_error) {
|
|
186
|
+
lines.push(` ${styles.dim("Profile error")} ${data.profile_error.code}: ${data.profile_error.message}`);
|
|
187
|
+
}
|
|
188
|
+
lines.push(` ${styles.dim("Health")} ${data.health.ok ? "ok" : `failed (${data.health.error.code})`}`);
|
|
189
|
+
if ("skipped" in data.identity) {
|
|
190
|
+
lines.push(` ${styles.dim("Identity")} skipped (${data.identity.reason})`);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
lines.push(` ${styles.dim("Identity")} ${data.identity.ok ? "ok" : `failed (${data.identity.error.code})`}`);
|
|
194
|
+
if (!data.identity.ok) {
|
|
195
|
+
lines.push(` ${styles.dim("Identity error")} ${data.identity.error.message}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
lines.push("");
|
|
199
|
+
return lines.join("\n");
|
|
200
|
+
}
|
|
201
|
+
function formatOrgSwitching(orgSwitching) {
|
|
202
|
+
if (orgSwitching.switchable === true)
|
|
203
|
+
return "available — `oxygen orgs use <organization>`";
|
|
204
|
+
if (orgSwitching.switchable === false) {
|
|
205
|
+
const name = orgSwitching.current_organization?.name;
|
|
206
|
+
return `locked to ${name ?? "this workspace"} — \`oxygen login\` for a switchable user key`;
|
|
207
|
+
}
|
|
208
|
+
return "unknown";
|
|
209
|
+
}
|
|
210
|
+
export function registerAuthFamily(program, handleAsyncAction) {
|
|
211
|
+
program
|
|
212
|
+
.command("auth")
|
|
213
|
+
.description("Authentication helpers for scripts and agents (non-interactive token use). Day to day, `oxygen login` and `oxygen whoami` are all you need.")
|
|
214
|
+
.addCommand(new Command("use-token")
|
|
215
|
+
.description("Store a CLI API token non-interactively.")
|
|
216
|
+
.requiredOption("--token <token>", "CLI API token.")
|
|
217
|
+
.option("--api-url <url>", "Oxygen API URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
|
|
218
|
+
.option("--profile <name>", "Store credentials under a named CLI profile and make it active.")
|
|
219
|
+
.option("--force", "Repoint an existing profile at a different API host. Refused without this flag.")
|
|
220
|
+
.option("--json", "Print a JSON envelope.")
|
|
221
|
+
.action(async (options) => {
|
|
222
|
+
await handleAuthUseTokenAction(options);
|
|
223
|
+
}))
|
|
224
|
+
.addCommand(new Command("doctor")
|
|
225
|
+
.description("Diagnose the active Oxygen CLI credentials without printing token material.")
|
|
226
|
+
.option("--json", "Print a JSON envelope.")
|
|
227
|
+
.action(async (options) => {
|
|
228
|
+
await handleAuthDoctorAction(options);
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { parseJsonObject, readPositiveInt } from "../cli-values.js";
|
|
2
|
+
import { collectRepeatable, readPositiveNumber } from "../command-support.js";
|
|
3
|
+
import { defaultApiUrl } from "../credentials.js";
|
|
4
|
+
import { oxygenAppUrl } from "../help.js";
|
|
5
|
+
import { requestOxygen } from "../http-client.js";
|
|
6
|
+
import { readOption } from "../util.js";
|
|
7
|
+
import { PURCHASABLE_PLAN_KEYS, planVolumeBonusPercent, resolveBasePricingPlan } from "@oxygen/shared/billing";
|
|
8
|
+
import { OxygenError } from "@oxygen/shared/cli-result";
|
|
9
|
+
import { Command, Option } from "commander";
|
|
10
|
+
function buildBillingLedgerParams(options) {
|
|
11
|
+
const params = new URLSearchParams();
|
|
12
|
+
const days = readPositiveInt(options.days);
|
|
13
|
+
const limit = readPositiveInt(options.limit);
|
|
14
|
+
if (days)
|
|
15
|
+
params.set("days", String(days));
|
|
16
|
+
if (limit)
|
|
17
|
+
params.set("limit", String(limit));
|
|
18
|
+
if (readOption(options.meter))
|
|
19
|
+
params.set("meter", readOption(options.meter));
|
|
20
|
+
if (readOption(options.from))
|
|
21
|
+
params.set("from", readOption(options.from));
|
|
22
|
+
if (readOption(options.to))
|
|
23
|
+
params.set("to", readOption(options.to));
|
|
24
|
+
if (readOption(options.cursor))
|
|
25
|
+
params.set("cursor", readOption(options.cursor));
|
|
26
|
+
if (readOption(options.type))
|
|
27
|
+
params.set("type", readOption(options.type));
|
|
28
|
+
if (readOption(options.category))
|
|
29
|
+
params.set("category", readOption(options.category));
|
|
30
|
+
if (readOption(options.provider))
|
|
31
|
+
params.set("provider", readOption(options.provider));
|
|
32
|
+
const sourceId = readOption(options.sourceId) ?? readOption(options.source);
|
|
33
|
+
if (sourceId)
|
|
34
|
+
params.set("source_id", sourceId);
|
|
35
|
+
if (readOption(options.runId))
|
|
36
|
+
params.set("run_id", readOption(options.runId));
|
|
37
|
+
if (options.nonzero)
|
|
38
|
+
params.set("nonzero", "true");
|
|
39
|
+
return params;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A whole seat count. Unlike readPositiveNumber, ZERO IS VALID and meaningful:
|
|
43
|
+
* `--quantity 0` is how a customer gives back every seat of a channel, and
|
|
44
|
+
* rejecting it would leave the only way to stop paying being a support ticket.
|
|
45
|
+
*/
|
|
46
|
+
function readPositiveNumberOrZero(value) {
|
|
47
|
+
const trimmed = value?.trim();
|
|
48
|
+
if (!trimmed)
|
|
49
|
+
return undefined;
|
|
50
|
+
const parsed = Number(trimmed);
|
|
51
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
52
|
+
throw new OxygenError("invalid_number", "Expected a whole number of 0 or more.", {
|
|
53
|
+
details: { value },
|
|
54
|
+
exitCode: 1,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
export function registerBillingFamily(program, handleAsyncAction, billingDocsUrl) {
|
|
60
|
+
program
|
|
61
|
+
.command("billing")
|
|
62
|
+
.description("Plan and managed credit commands. Credits reserved but not yet charged are listed hold by hold with `billing usage --meter holds`. Spend splits into FLEXIBLE (ad-hoc: enrichment, AI, automation — drawn from your free-to-spend balance) and FIXED recurring per-resource monthly commitments blocked out of it; see `billing commitments`. THREE CLOCKS, deliberately different: the CREDIT CYCLE that `billing allowance` reports against (your plan's monthly grant window); each resource's own COMMITMENT RENEWAL, anchored to the day you connected it, so `billing commitments --json` next_due_at rarely matches the cycle end; and your SUBSCRIPTION PERIOD in `billing balance` (annual plans span many credit cycles). A number from one clock will not reconcile against another. On a plan that pays for several workspaces, `billing usage --group-by workspace` totals what each one spent. To price a month of outreach before building it (leads, enrichment, sequence steps, senders), run `billing estimate`. Failed-payment grace, suspension, and recovery: " + billingDocsUrl + ".")
|
|
63
|
+
.addCommand(new Command("change")
|
|
64
|
+
.description("Create an approval link (`approval_url`) to start or change a plan; no charge or plan change until confirmation. First purchase needs an admin/owner; existing-plan changes allow any unrestricted member. Compare all six prices, credits and yearly terms with `billing balance` first. Use --interval month --term year for twelve discounted monthly payments, renewing for another year: the $99 plan costs $82.50 per month before tax, not $990 upfront. Credits arrive monthly after successful payment. On a workspace that has never had a plan, the Checkout for any monthly plan from $99 up carries the first-month offer, half its first month, while its 24-hour window is open (`offer`; `billing balance` lists every size under `first_month_offer.plans`): `amount_due_now` is then the discounted first month and `next_invoice_total` the regular price, both before tax. Creating that Checkout starts the window if nothing has started it yet. Upgrades during an annual commitment apply immediately with proration and keep its end date; downgrades and switching to a monthly commitment wait for the term end. Switching from a monthly commitment to yearly starts at the next monthly boundary. Legacy prepaid yearly upgrades that keep their cadence apply immediately with proration; downgrades and term or cadence changes wait until the paid year ends. Omit interval and term to preserve the current contract. For an existing subscription, a yearly preview returns charges, start and term-end dates; nothing changes until separate confirmation.")
|
|
65
|
+
.requiredOption("--to <plan>",
|
|
66
|
+
// Derived from the catalog, never retyped: this help text is the only
|
|
67
|
+
// place a customer's agent learns which plans exist, and it named
|
|
68
|
+
// three retired ones for as long as it was a literal. The price sits
|
|
69
|
+
// beside each key because nothing else on the CLI prints a
|
|
70
|
+
// plan-to-price table (blind baseline, 2026-09-22). The credits sit
|
|
71
|
+
// there too, bonus included, because the $499+ rungs are no longer
|
|
72
|
+
// price x 100 (repricing 2026-09, decision 1.3).
|
|
73
|
+
`Target plan: ${PURCHASABLE_PLAN_KEYS.map((key) => {
|
|
74
|
+
const plan = resolveBasePricingPlan(key);
|
|
75
|
+
const cents = plan?.monthlyPriceCents;
|
|
76
|
+
if (typeof cents !== "number")
|
|
77
|
+
return key;
|
|
78
|
+
const credits = plan?.monthlyCredits;
|
|
79
|
+
const bonus = planVolumeBonusPercent(key);
|
|
80
|
+
const creditText = typeof credits === "number"
|
|
81
|
+
? `, ${credits.toLocaleString("en-US")} credits${bonus > 0 ? ` incl. a ${bonus}% volume bonus` : ""}`
|
|
82
|
+
: "";
|
|
83
|
+
return `${key} ($${cents / 100}/mo${creditText})`;
|
|
84
|
+
}).join(", ")}.`)
|
|
85
|
+
.option("--interval <interval>", "Payment cadence. Use month for new plans; choose --term year for a yearly commitment paid monthly. Omit to preserve an existing subscription's cadence, including prepaid yearly plans.")
|
|
86
|
+
.option("--term <term>", "Commitment: month or year. Year means twelve discounted monthly payments, renewing annually. Downgrades wait for the term end; omit to preserve the current term.")
|
|
87
|
+
.option("--json", "Print a JSON envelope.")
|
|
88
|
+
.action(async (options) => {
|
|
89
|
+
await handleAsyncAction("billing change", options, () => requestOxygen("/api/cli/billing/change", {
|
|
90
|
+
method: "POST",
|
|
91
|
+
// Sent only when asked: the server keeps the subscriber's interval otherwise.
|
|
92
|
+
body: { tier: options.to, ...(options.interval ? { interval: options.interval } : {}), ...(options.term ? { term: options.term } : {}) },
|
|
93
|
+
}));
|
|
94
|
+
}))
|
|
95
|
+
.addCommand(new Command("balance")
|
|
96
|
+
.description("May apply due plan grants or credit expiry. Compare all six subscription sizes in `available_plans`: monthly prices, yearly paid-monthly prices, credit allowances and savings. `annual_monthly_billing_available` says whether yearly paid monthly is offered; `annual_monthly_terms` explains the 12-month commitment, annual renewal, remaining payments after cancellation and plan-change timing. `first_month_offer` is the first-month offer this workspace can still claim: every size it applies to (`plans`, from `minimum_plan` up) with its first-month and regular price before tax, and `ends_at`, null until its 24-hour window starts. The window starts the first time the workspace meets the offer: the app showing it, signup from a browser that saw it on the website, or creating its Checkout with `billing change`. Also show the current plan, commitment, subscription entitlement, and managed credit balance: available and reserved credits, FIXED credits committed to recurring per-resource charges, and the FLEXIBLE free-to-spend remainder. `next_renewal` answers \"will I be charged?\": read `will_charge` first — true only when this workspace's own Stripe plan is going to charge its card, in which case `charge_at` and the subscription's recurring amount in money say when and roughly how much (the exact amount, with any promotion code or tax, is on the invoice); `period_ends_at` is the trial or period end either way, `cancellation_scheduled` says whether a cancellation is already set, and `stop_command` names the one command that changes it (`billing cancel`, undone by `billing resume`) or is null when nothing here can stop it (staff-invoiced, billed through another workspace, not active). `billing_interval` is payment cadence; `commitment` distinguishes yearly paid monthly from monthly and legacy prepaid yearly plans. Credits are granted monthly; `next_credit_grant_at` is the next grant. For a yearly commitment, cancellation stops annual renewal but remaining monthly payments continue until commitment.ends_at. `renewals_past_due` lists infrastructure renewals (managed-inbox domains, warm-ups) OXYGEN could NOT charge this month, what they cost, and the exact top-up that clears them — nothing is cancelled and billing retries automatically after a top-up. `warnings` also flags a recurring_shortfall when your connected infrastructure costs more per month than the plan grants. `partner_bonus` says whether this workspace earns the agency partner bonus (+15% credits a month for a published directory partner on $99 or above), how many credits, and why not when it does not; the bonus arrives as its own ledger line (`billing usage --category partner_bonus`), never inside plan.monthly_credits. Its basis is the plan face value; `first_grant_timing` explains unknown processing time separately from the next scheduled grant. PARTNERS2026 ends on cancellation or effective plan change; `partner_bonus.conditions` and `coupon_authority` describe the separate prospective exclusion when it applies. See the agency-partner-bonus section of the billing policy. A workspace linked to another organization (`orgs billing-link`) reads its billing at that owner: the balance is the owner's pool, `shared_billing` is true and `billing_owner_organization_id` names the owner. After failed-payment grace expires, mutations stop while read/export and billing recovery remain available. Recovery: " + oxygenAppUrl(defaultApiUrl(), "/billing") + ". Policy: " + billingDocsUrl + ". 0 credits; reserves nothing.")
|
|
97
|
+
.option("--json", "Print a JSON envelope.")
|
|
98
|
+
.action(async (options) => {
|
|
99
|
+
await handleAsyncAction("billing balance", options, () => requestOxygen("/api/cli/billing/balance"));
|
|
100
|
+
}))
|
|
101
|
+
.addCommand(new Command("commitments")
|
|
102
|
+
.description("List recurring commitments, or inspect monthly connection prices and first-debit timing with --view connection-prices without creating authorization. Default: list the fixed monthly credit commitments — per connected sending mailbox, OXYGEN-sold mailbox, warm-up, deliverability, connected LinkedIn, WhatsApp and X account, and rented phone number — with unit price, quantity, and next-due date. When your balance cannot cover an account's renewal, sending from that account pauses at once and it disconnects 7 days later (a phone number is released after 30): paused_quantity, past_due_since, disconnects_at and topup_url say which kind, since when and by when to top up; a top-up resumes it within minutes. This is the FORWARD run-rate: what your currently-connected resources will cost at their next renewal. It is NOT this cycle's charges — compare `billing allowance` fixed_spent_credits for that, and expect the two to differ. Each resource renews on its OWN anchor (the day it was connected), so next_due_at is per-resource and rarely lines up with the credit cycle. The top-level count is the number of individual resource rows; the commitments list groups them by kind. A past next_due_at is an unpaid renewal still due. To stop all infrastructure renewals while retaining records, preview `workspace infrastructure-exit`; plan cancellation alone does not stop these resources. Read-only, 0 Oxygen credits.")
|
|
103
|
+
.addOption(new Option("--view <view>", "Inspect connection-prices before authorizing a new account, or current commitments (default).").choices(["commitments", "connection-prices"]))
|
|
104
|
+
.option("--json", "Print a JSON envelope.")
|
|
105
|
+
.action(async (options) => {
|
|
106
|
+
await handleAsyncAction("billing commitments", options, () => requestOxygen(`/api/cli/billing/commitments${options.view ? `?view=${encodeURIComponent(options.view)}` : ""}`));
|
|
107
|
+
}))
|
|
108
|
+
.addCommand(new Command("estimate")
|
|
109
|
+
.description("Quote prospective enrichment, senders, X/WhatsApp, EmailGuard tests and an unsaved Workflow without provider calls, writes or reservations. Work-email figures are labelled conservative ceilings until matching measured populations exist; LinkedIn finder prices distinguish found results, misses and declared averages. Current workspace plan_fit uses stored spendable balance and balance-only months; --plan selects a hypothetical monthly allowance instead. Unknown/floor costs suppress complete plan recommendations. Workflow quotes include step floors, 30-day cadence and a separate declared ceiling; no execution authority. Own-key test vendor charges are separate. Prices are in force; opening the quote costs 0 credits. Example: oxygen billing estimate --leads 50 --enrich linkedin_url --whatsapp-accounts 2 --x-accounts 1 --json.")
|
|
110
|
+
.option("--leads <n>", "Leads you work through a month.")
|
|
111
|
+
.option("--enrich <id>", "An enrichment run on every lead, by id from `oxygen enrichment catalog --json` (work_email, mobile_phone, person_enrich, capability:verify_email …). Repeat for several.", collectRepeatable, [])
|
|
112
|
+
.option("--email-steps <n>", "Email steps per lead (only the email ones; LinkedIn steps go in --linkedin-steps).")
|
|
113
|
+
.option("--linkedin-steps <n>", "LinkedIn steps per lead; the first is a connection invite, later ones messages.")
|
|
114
|
+
.option("--mailboxes <n>", "Mailboxes you connect yourself (Google, Microsoft, SMTP).")
|
|
115
|
+
.option("--managed-mailboxes <n>", "Mailboxes you buy from OXYGEN.")
|
|
116
|
+
.option("--linkedin-accounts <n>", "LinkedIn accounts you connect.")
|
|
117
|
+
.option("--x-accounts <n>", "X accounts; prospective scenario quantity.")
|
|
118
|
+
.option("--whatsapp-accounts <n>", "WhatsApp accounts; prospective scenario quantity.")
|
|
119
|
+
.option("--emailguard-inboxes <n>", "Managed EmailGuard inboxes; prospective scenario quantity.")
|
|
120
|
+
.option("--emailguard-tests-per-inbox <n>", "Managed tests per inbox per cycle; prospective scenario quantity.")
|
|
121
|
+
.option("--emailguard-byok-inboxes <n>", "Own-key EmailGuard inboxes; prospective scenario quantity.")
|
|
122
|
+
.option("--emailguard-byok-tests-per-inbox <n>", "Own-key tests per inbox per cycle; prospective scenario quantity.")
|
|
123
|
+
.option("--workflow-manifest <json>", "Unsaved oxygen-workflows-v2 draft JSON. Saves nothing.")
|
|
124
|
+
.option("--workflow-cron <cron>", "Five-field schedule for a 30-day step-cost floor; arms nothing.")
|
|
125
|
+
.option("--workflow-max-credits <n>", "Declared ceiling for comparison only; grants no approval or authority.")
|
|
126
|
+
.option("--plan <plan>", `Check against this plan instead of your own: free, ${PURCHASABLE_PLAN_KEYS.join(", ")}.`)
|
|
127
|
+
.option("--json", "Print a JSON envelope.")
|
|
128
|
+
.action(async (options) => {
|
|
129
|
+
await handleAsyncAction("billing estimate", options, () => requestOxygen("/api/cli/billing/estimate", {
|
|
130
|
+
method: "POST",
|
|
131
|
+
body: {
|
|
132
|
+
leads_per_month: readPositiveNumberOrZero(options.leads) ?? 0,
|
|
133
|
+
enrichments: options.enrich.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean),
|
|
134
|
+
email_steps: readPositiveNumberOrZero(options.emailSteps) ?? 0,
|
|
135
|
+
linkedin_steps: readPositiveNumberOrZero(options.linkedinSteps) ?? 0,
|
|
136
|
+
mailboxes: readPositiveNumberOrZero(options.mailboxes) ?? 0,
|
|
137
|
+
managed_mailboxes: readPositiveNumberOrZero(options.managedMailboxes) ?? 0,
|
|
138
|
+
linkedin_accounts: readPositiveNumberOrZero(options.linkedinAccounts) ?? 0,
|
|
139
|
+
x_accounts: options.xAccounts === undefined ? 0 : Number(options.xAccounts),
|
|
140
|
+
whatsapp_accounts: options.whatsappAccounts === undefined ? 0 : Number(options.whatsappAccounts),
|
|
141
|
+
emailguard_inboxes: options.emailguardInboxes === undefined ? 0 : Number(options.emailguardInboxes),
|
|
142
|
+
emailguard_tests_per_inbox: options.emailguardTestsPerInbox === undefined ? 0 : Number(options.emailguardTestsPerInbox),
|
|
143
|
+
emailguard_byok_inboxes: options.emailguardByokInboxes === undefined ? 0 : Number(options.emailguardByokInboxes),
|
|
144
|
+
emailguard_byok_tests_per_inbox: options.emailguardByokTestsPerInbox === undefined ? 0 : Number(options.emailguardByokTestsPerInbox),
|
|
145
|
+
...(options.workflowManifest ? { workflow_manifest: parseJsonObject(options.workflowManifest) } : {}),
|
|
146
|
+
...(options.workflowCron ? { workflow_cron: options.workflowCron } : {}),
|
|
147
|
+
...(options.workflowMaxCredits === undefined ? {} : { workflow_max_credits: Number(options.workflowMaxCredits) }),
|
|
148
|
+
...(readOption(options.plan) ? { plan: readOption(options.plan) } : {}),
|
|
149
|
+
},
|
|
150
|
+
}));
|
|
151
|
+
}))
|
|
152
|
+
.addCommand(new Command("seats")
|
|
153
|
+
.description("Show sending seats, current billing-period end and next-connection eligibility (null: unknown, with a reason). Includes purchased, grandfathered (null: unlimited), connected and left per channel. Seats belong to the billing owner. Email mailbox allocation is workspace-scoped; unreadable mailbox data leaves allocated/available/can_connect null. When retired, held_seats_end_at_reason and held_seats_end_at_message distinguish no paying subscription from an unknown end date; after_seats_end gives monthly credit prices (null: unconfirmed). connect_eligibility explains a single new connection's credit affordability, sponsorship and attachment capacity; paying-seat coverage may remain unknown. Existing accounts and reconnects differ. Charges 0 credits and creates no reservation; like billing balance, it may apply a due plan grant or already-due sponsored-credit expiry.")
|
|
154
|
+
.option("--json", "Print a JSON envelope.")
|
|
155
|
+
.action(async (options) => {
|
|
156
|
+
await handleAsyncAction("billing seats", options, () => requestOxygen("/api/cli/billing/seats"));
|
|
157
|
+
})
|
|
158
|
+
.addCommand(new Command("set")
|
|
159
|
+
.description("Manage legacy dollar sending seats for one channel; check `billing seats` for retirement status. The quantity is ABSOLUTE, not a delta: `--quantity 3` means you end up with three, so a retried command cannot buy extra. Increasing charges the card pro rata immediately; decreasing credits you pro rata. Reducing below the number of senders you have connected is REFUSED and names how many to disconnect first, because an orphaned sender is one you keep paying a provider for while Oxygen has stopped honouring it. An email_sender reduction is refused with `seat_allocation_unavailable` (503) while the workspace's mailbox count cannot be read; retry it in a moment. Once seats are retired, any increase is refused with `seats_retired` and lowering held seats still works. Charges real money.")
|
|
160
|
+
.requiredOption("--seat-key <kind>", "linkedin, whatsapp, phone_number, or email_sender.")
|
|
161
|
+
.requiredOption("--quantity <n>", "Total seats to hold for this channel, 0 or more.")
|
|
162
|
+
.option("--json", "Print a JSON envelope.")
|
|
163
|
+
.action(async (options) => {
|
|
164
|
+
await handleAsyncAction("billing seats set", options, () => requestOxygen("/api/cli/billing/seats", {
|
|
165
|
+
method: "POST",
|
|
166
|
+
body: {
|
|
167
|
+
seat_key: readOption(options.seatKey),
|
|
168
|
+
quantity: readPositiveNumberOrZero(options.quantity),
|
|
169
|
+
},
|
|
170
|
+
}));
|
|
171
|
+
})))
|
|
172
|
+
.addCommand(new Command("invoices")
|
|
173
|
+
.description("List every invoice Stripe has issued for this workspace, newest first, each with a direct PDF link. Covers ALL charges, not just the plan — sending seats, managed inboxes, and credit top-ups appear here too, which is why this works on the free tier: a workspace with no plan can still be a paying customer. A workspace linked to another organization (`orgs billing-link`) reads its billing at that owner, so it sees an empty list and shared_billing=true; switch to the billing owner to read its invoices. pdf_url and hosted_url are Stripe's own links. VAT or tax ID: open the web_url this command returns and choose VAT or tax ID on the Invoices card (top-up checkout also asks for it); Stripe prints it on every invoice issued after you save it, never on an earlier one. Read-only, 0 Oxygen credits.")
|
|
174
|
+
.option("--json", "Print a JSON envelope.")
|
|
175
|
+
.action(async (options) => {
|
|
176
|
+
await handleAsyncAction("billing invoices", options, () => requestOxygen("/api/cli/billing/invoices"));
|
|
177
|
+
}))
|
|
178
|
+
.addCommand(new Command("allowance")
|
|
179
|
+
.description("Show this billing cycle's credit pool as one breakdown: fixed spend, flexible spend, credits committed to the next renewal, and what is free to spend — the numbers behind the bar on the credit-usage page. The parts always sum to the total, with ONE deliberate exception: `renewals_past_due` (and its region=past_due segment) reports renewals that FAILED to charge, which is money not taken and therefore outside total_credits. fixed_spent_credits is what has ALREADY been charged this cycle, NOT your monthly run-rate — for the forward figure billed at the next renewal see `billing commitments`, which is normally much larger. For organization wallet scope, reserved_credits is in-flight spend already removed from the available balance: do not subtract it again from free_to_spend_credits. Reserved credits can exceed the remaining free balance without implying an overdraft. For workspace_cap scope, free_to_spend_credits is remaining allowance, not the billing owner's wallet; live execution still checks that wallet and spend policies. Every segment carries region=fixed|flexible. 0 credits; reserves nothing. Due plan grants or credit expiry may apply.")
|
|
180
|
+
.option("--json", "Print a JSON envelope.")
|
|
181
|
+
.action(async (options) => {
|
|
182
|
+
await handleAsyncAction("billing allowance", options, () => requestOxygen("/api/cli/billing/allowance"));
|
|
183
|
+
}))
|
|
184
|
+
.addCommand(new Command("usage")
|
|
185
|
+
.description("Credit-ledger and BYOK views may apply due plan grants or credit expiry. 0 credits. Show credit ledger events (each tagged with its FIXED/FLEXIBLE spend_class), workflow step usage (automation_actions: credits per step, credits billed; writing rows is free), calls with your own API keys (byok_calls: the platform fee per successful call and the fee credits this month; vendor charges follow your provider's terms), or connected LinkedIn and WhatsApp accounts (provider_seats: the rolling-30d peak and its implied connectivity cost; what each account is charged is its credit reservation in `billing commitments`). On a plan that pays for several workspaces (an agency covering client workspaces), `--group-by workspace` totals the credits each workspace spent (`by_workspace`) and `--workspace <id|slug>` narrows the ledger to one of them; a linked client workspace sees only its own spend. managed_credits_used counts captured credits; for where credits went by category, provider or tool, with shares, run `billing audit`.")
|
|
186
|
+
.option("--meter <meter>", "credits, automation_actions, byok_calls, provider_seats (rolling-30d peak connected accounts and the flat per-account charge; each account's reservation is listed by `billing commitments`), or holds (each held reservation behind reserved_credits in this workspace: credits, run, why it is held (an unmarked LinkedIn lookup hold cannot say whether its result was unknown or delivered), and automatic_return: LinkedIn lookup holds from 24 hours to 30 days old and explicitly unknown LeadMagic email-validation holds older than 24 hours are eligible while automatic return is switched on, and an hourly release returns it on a later pass with no fixed deadline; read-only, newest first, --days up to 365 and --limit apply). Defaults to credits.")
|
|
187
|
+
.option("--days <n>", "Lookback window in days. Defaults to 30.")
|
|
188
|
+
.option("--from <iso>", "Only include ledger events at or after this ISO timestamp.")
|
|
189
|
+
.option("--to <iso>", "Only include ledger events at or before this ISO timestamp.")
|
|
190
|
+
.option("--limit <n>", "Maximum events to return. Defaults to 50.")
|
|
191
|
+
.option("--cursor <cursor>", "Opaque cursor from a previous usage response.")
|
|
192
|
+
.option("--type <type>", "Filter by transaction type, such as reserve, capture, release, grant, or byok_usage (alias byok).")
|
|
193
|
+
.option("--category <category>", "Filter by transaction category, such as managed_enrichment, managed_ai, byok, subscription, partner_bonus (the agency partner bonus), or admin.")
|
|
194
|
+
.option("--provider <provider>", "Filter by provider id. With --meter provider_seats, selects the seat provider: linkedin (default) or whatsapp.")
|
|
195
|
+
.option("--source <source_id>", "Filter by source_id/provider operation.")
|
|
196
|
+
.option("--source-id <source_id>", "Alias for --source.")
|
|
197
|
+
.option("--run-id <run_id>", "Filter by run id recorded in ledger metadata. New settlements preserve the reserve's run id; historical settled charges can omit it. For those older runs, use `table-runs get <run>` (creditsUsed) or `table-runs items`.")
|
|
198
|
+
.option("--nonzero", "Only include events that changed available or reserved credits.")
|
|
199
|
+
.option("--workspace <id|slug>", "Only one workspace's spend on a plan that pays for several (credits meter).")
|
|
200
|
+
.option("--group-by <key>", "workspace: credits spent per workspace on a pooled plan, in by_workspace, for the same filters (credits meter).")
|
|
201
|
+
.option("--json", "Print a JSON envelope.")
|
|
202
|
+
.action(async (options) => {
|
|
203
|
+
await handleAsyncAction("billing usage", options, () => {
|
|
204
|
+
const params = buildBillingLedgerParams(options);
|
|
205
|
+
if (readOption(options.workspace))
|
|
206
|
+
params.set("workspace", readOption(options.workspace));
|
|
207
|
+
if (readOption(options.groupBy))
|
|
208
|
+
params.set("group_by", readOption(options.groupBy));
|
|
209
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
210
|
+
return requestOxygen(`/api/cli/billing/usage${suffix}`);
|
|
211
|
+
});
|
|
212
|
+
}))
|
|
213
|
+
.addCommand(new Command("audit")
|
|
214
|
+
.description("Summarize where managed credits were granted, reserved, captured, released, or spent. Every result lists spend_by_category (each spend area's credits and share of the window's spend, largest first) and totals fixed_credits_spent/flexible_credits_spent over the whole window, whatever --group-by is. groups_truncated says when --limit cut grouped rows, and managed_credits_spent_outside_groups how much they carried. Spent is reserved minus released in the window, so it differs from totals.managed_credits_captured (`billing usage`'s managed_credits_used) because of holds at the window edges and adjustments to reserved credits. Historical events retain their original credit units; windows spanning a re-denomination can mix units. totals.grant_credits is gross grants, excluding refunds and adjustments. byok_usage_events counts only nonzero rows unless --type byok. 0 credits; reserves nothing. Due plan grants or credit expiry may apply.")
|
|
215
|
+
.option("--days <n>", "Lookback window in days. Defaults to 365.")
|
|
216
|
+
.option("--from <iso>", "Only include ledger events at or after this ISO timestamp.")
|
|
217
|
+
.option("--to <iso>", "Only include ledger events at or before this ISO timestamp.")
|
|
218
|
+
.option("--limit <n>", "Maximum grouped rows to return. Defaults to 100.")
|
|
219
|
+
.option("--group-by <keys>", "Comma-separated grouping keys (day, type, category, provider, source, tool, model, run, workspace). Defaults to provider,source.")
|
|
220
|
+
.option("--type <type>", "Filter by transaction type, such as reserve, capture, release, grant, or byok_usage (alias byok).")
|
|
221
|
+
.option("--category <category>", "Filter by transaction category, such as managed_enrichment, managed_ai, byok, subscription, partner_bonus (the agency partner bonus), or admin.")
|
|
222
|
+
.option("--provider <provider>", "Filter by provider id.")
|
|
223
|
+
.option("--source <source_id>", "Filter by source_id/provider operation.")
|
|
224
|
+
.option("--source-id <source_id>", "Alias for --source.")
|
|
225
|
+
.option("--run-id <run_id>", "Filter by run id recorded in ledger metadata. New settlements preserve the reserve's run id; historical settled charges can omit it. For those older runs, use `table-runs get <run>` (creditsUsed) or `table-runs items`.")
|
|
226
|
+
.option("--nonzero", "Only include events that changed available or reserved credits. Default for audit except BYOK filters.")
|
|
227
|
+
.option("--json", "Print a JSON envelope.")
|
|
228
|
+
.action(async (options) => {
|
|
229
|
+
await handleAsyncAction("billing audit", options, () => {
|
|
230
|
+
const params = buildBillingLedgerParams(options);
|
|
231
|
+
if (readOption(options.groupBy))
|
|
232
|
+
params.set("group_by", readOption(options.groupBy));
|
|
233
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
234
|
+
return requestOxygen(`/api/cli/billing/audit${suffix}`);
|
|
235
|
+
});
|
|
236
|
+
}))
|
|
237
|
+
.addCommand(new Command("grant")
|
|
238
|
+
.description("Grant managed credits to the current organization (staff only).")
|
|
239
|
+
.requiredOption("--credits <credits>", "Managed credits to grant.")
|
|
240
|
+
.requiredOption("--idempotency-key <key>", "Stable key that makes retries safe.")
|
|
241
|
+
.option("--organization-id <id>", "Organization id to credit. Defaults to the current organization.")
|
|
242
|
+
.option("--org-id <id>", "Alias for --organization-id.")
|
|
243
|
+
.option("--description <text>", "Ledger description for the admin grant.")
|
|
244
|
+
.option("--json", "Print a JSON envelope.")
|
|
245
|
+
.action(async (options) => {
|
|
246
|
+
await handleAsyncAction("billing grant", options, () => requestOxygen("/api/cli/billing/grant", {
|
|
247
|
+
method: "POST",
|
|
248
|
+
body: {
|
|
249
|
+
credits: readPositiveNumber(options.credits),
|
|
250
|
+
idempotency_key: readOption(options.idempotencyKey),
|
|
251
|
+
...(readOption(options.organizationId) || readOption(options.orgId)
|
|
252
|
+
? { organization_id: readOption(options.organizationId) ?? readOption(options.orgId) }
|
|
253
|
+
: {}),
|
|
254
|
+
...(readOption(options.description) ? { description: readOption(options.description) } : {}),
|
|
255
|
+
},
|
|
256
|
+
}));
|
|
257
|
+
}))
|
|
258
|
+
.addCommand(new Command("reconcile-paid-period")
|
|
259
|
+
.description("Find active subscriptions whose current billing period was paid in Stripe but never had its plan credits minted, and restore them (staff only). Detect-only unless --apply: every repair is proven by a Stripe paid invoice covering that exact period, so it can only return credits the customer already bought. Re-running is safe — an already-granted period is never granted twice.")
|
|
260
|
+
.option("--organization-id <id>", "Restrict the sweep to one organization. Defaults to every active subscription.")
|
|
261
|
+
.option("--org-id <id>", "Alias for --organization-id.")
|
|
262
|
+
.option("--apply", "Mint the missing grants. Without it, report what would be repaired and change nothing.")
|
|
263
|
+
.option("--json", "Print a JSON envelope.")
|
|
264
|
+
.action(async (options) => {
|
|
265
|
+
await handleAsyncAction("billing reconcile-paid-period", options, () => requestOxygen("/api/cli/billing/reconcile-paid-period", {
|
|
266
|
+
method: "POST",
|
|
267
|
+
body: {
|
|
268
|
+
apply: Boolean(options.apply),
|
|
269
|
+
...(readOption(options.organizationId) || readOption(options.orgId)
|
|
270
|
+
? { organization_id: readOption(options.organizationId) ?? readOption(options.orgId) }
|
|
271
|
+
: {}),
|
|
272
|
+
},
|
|
273
|
+
}));
|
|
274
|
+
}))
|
|
275
|
+
.addCommand(new Command("set-plan")
|
|
276
|
+
.description("Create or update an off-Stripe custom plan for an organization (staff only).")
|
|
277
|
+
.requiredOption("--organization-id <id>", "Organization id to put on the custom plan.")
|
|
278
|
+
.requiredOption("--credits <n>", "Monthly managed credits included in the plan.")
|
|
279
|
+
.option("--org-id <id>", "Alias for --organization-id.")
|
|
280
|
+
.option("--name <text>", "Display name for the plan, e.g. \"Custom Contract - $250/mo\".")
|
|
281
|
+
.option("--price <usd>", "Monthly contract price in USD (recorded for display; not charged off-Stripe).")
|
|
282
|
+
.option("--weekly-credits <n>", "Weekly spend throttle. Defaults to the monthly credits (no throttle).")
|
|
283
|
+
.option("--rollover <n>", "Max balance credits roll over to. Defaults to the monthly credits.")
|
|
284
|
+
.option("--base-tier <tier>", "Base pricing tier to inherit feature flags from. Defaults to growth_250.")
|
|
285
|
+
.option("--byok", "Enable bring-your-own-key integrations for the plan.")
|
|
286
|
+
.option("--note <text>", "Internal note stored on the contract.")
|
|
287
|
+
.option("--json", "Print a JSON envelope.")
|
|
288
|
+
.action(async (options) => {
|
|
289
|
+
await handleAsyncAction("billing set-plan", options, () => requestOxygen("/api/cli/billing/set-plan", {
|
|
290
|
+
method: "POST",
|
|
291
|
+
body: {
|
|
292
|
+
organization_id: readOption(options.organizationId) ?? readOption(options.orgId),
|
|
293
|
+
monthly_credits: readPositiveNumber(options.credits),
|
|
294
|
+
...(readOption(options.name) ? { name: readOption(options.name) } : {}),
|
|
295
|
+
...(readOption(options.price) ? { price: readPositiveNumber(options.price) } : {}),
|
|
296
|
+
...(readOption(options.weeklyCredits)
|
|
297
|
+
? { weekly_credits_limit: readPositiveNumber(options.weeklyCredits) }
|
|
298
|
+
: {}),
|
|
299
|
+
...(readOption(options.rollover)
|
|
300
|
+
? { rollover_cap: readPositiveNumber(options.rollover) }
|
|
301
|
+
: {}),
|
|
302
|
+
...(readOption(options.baseTier) ? { base_tier: readOption(options.baseTier) } : {}),
|
|
303
|
+
...(options.byok ? { byok: true } : {}),
|
|
304
|
+
...(readOption(options.note) ? { note: readOption(options.note) } : {}),
|
|
305
|
+
},
|
|
306
|
+
}));
|
|
307
|
+
}))
|
|
308
|
+
.addCommand(new Command("cancel")
|
|
309
|
+
.description("Turn off subscription renewal. For a yearly commitment paid monthly, remaining monthly payments continue through commitment_ends_at; the plan stays usable until then. Other plans end at their paid period or trial end with no further subscription renewal charge. This command costs no Oxygen credits. Separately funded external resources keep renewing; preview `workspace infrastructure-exit` to end those too. Prints the end date; undo before then with billing resume.")
|
|
310
|
+
.option("--comment <text>", "Written cancellation feedback (1–2,000 characters).")
|
|
311
|
+
.option("--json", "Print a JSON envelope.")
|
|
312
|
+
.action(async (options) => {
|
|
313
|
+
await handleAsyncAction("billing cancel", options, () => requestOxygen("/api/cli/billing/cancel", {
|
|
314
|
+
method: "POST",
|
|
315
|
+
body: { action: "cancel", ...(options.comment !== undefined ? { comment: options.comment } : {}) },
|
|
316
|
+
}));
|
|
317
|
+
}))
|
|
318
|
+
.addCommand(new Command("resume")
|
|
319
|
+
.description("Remove a scheduled cancellation so the subscription renews (or the trial converts) normally. Free (no fee, no credits). Prints the next charge date and amount.")
|
|
320
|
+
.option("--json", "Print a JSON envelope.")
|
|
321
|
+
.action(async (options) => {
|
|
322
|
+
await handleAsyncAction("billing resume", options, () => requestOxygen("/api/cli/billing/cancel", {
|
|
323
|
+
method: "POST",
|
|
324
|
+
body: { action: "resume" },
|
|
325
|
+
}));
|
|
326
|
+
}))
|
|
327
|
+
.addCommand(new Command("retention-offer")
|
|
328
|
+
.description("Preview a one-month discount before cancelling your monthly subscription. Reports eligibility, the next invoice price and normal price. Reads only; no discount is applied.")
|
|
329
|
+
.option("--json", "Print a JSON envelope.")
|
|
330
|
+
.action(async (options) => {
|
|
331
|
+
await handleAsyncAction("billing retention-offer", options, () => requestOxygen("/api/cli/billing/cancel", { method: "POST", body: { action: "offer" } }));
|
|
332
|
+
}))
|
|
333
|
+
.addCommand(new Command("retain")
|
|
334
|
+
.description("Stay subscribed with the one-month retention discount from billing retention-offer. Restores renewal if cancellation was scheduled. No charge today; the normal price returns afterwards.")
|
|
335
|
+
.requiredOption("--offer <id>", "The offer_id from the retention offer preview.")
|
|
336
|
+
.option("--json", "Print a JSON envelope.")
|
|
337
|
+
.action(async (options) => {
|
|
338
|
+
await handleAsyncAction("billing retain", options, () => requestOxygen("/api/cli/billing/cancel", { method: "POST", body: { action: "retain", offer_id: options.offer } }));
|
|
339
|
+
}))
|
|
340
|
+
.addCommand(new Command("topup")
|
|
341
|
+
.description("No amount: read-only price quote, no checkout. With --credits or a pack: create Stripe Checkout and possibly billing resources; no charge until payment. Read tax_basis and review tax and the final total in Checkout. Prices are per 100 credits (`usd_per_100_credits`); credits arrive after confirmed payment and never expire. Plan credits cost less. `usd_per_1k_credits` is a deprecated name for the same per-100 price, not a price per 1,000 credits. `default_credits` is the web form's initial custom selection, not a purchase or minimum.")
|
|
342
|
+
.argument("[pack]", "A pack id from the list shown when you run without an amount.")
|
|
343
|
+
.option("--credits <n>", "Credits to buy (800-100,000 in 100-credit increments).")
|
|
344
|
+
.option("--json", "Print a JSON envelope.")
|
|
345
|
+
.action(async (pack, options) => {
|
|
346
|
+
const credits = readOption(options.credits);
|
|
347
|
+
await handleAsyncAction("billing topup", options, () => pack || credits
|
|
348
|
+
? requestOxygen("/api/cli/billing/topup", {
|
|
349
|
+
method: "POST",
|
|
350
|
+
body: {
|
|
351
|
+
...(pack ? { pack } : {}),
|
|
352
|
+
...(credits ? { credits: readPositiveNumber(credits) } : {}),
|
|
353
|
+
},
|
|
354
|
+
})
|
|
355
|
+
: requestOxygen("/api/cli/billing/topup"));
|
|
356
|
+
}));
|
|
357
|
+
}
|