@oxygen-agent/cli 1.1014.1600 → 1.1014.1763
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-families/program.js +4 -0
- package/dist/command-manifest.js +18 -3
- package/dist/command-support.js +1 -1
- package/dist/commands/admin.js +82 -1
- package/dist/commands/billing.js +1 -1
- package/dist/commands/copilot.js +29 -7
- package/dist/commands/custom-integrations.js +13 -0
- package/dist/commands/domains.js +7 -0
- package/dist/commands/email-assets.d.ts +3 -0
- package/dist/commands/email-assets.js +78 -0
- package/dist/commands/email-templates.d.ts +3 -0
- package/dist/commands/email-templates.js +101 -0
- package/dist/commands/inbox.js +1 -1
- package/dist/commands/integrations.js +5 -1
- package/dist/commands/mailboxes.js +26 -6
- package/dist/commands/managed-inboxes.js +2 -2
- package/dist/commands/senders.js +4 -4
- package/dist/commands/sequences.js +47 -12
- package/dist/commands/subscriptions.d.ts +2 -4
- package/dist/commands/subscriptions.js +297 -51
- package/dist/credential-storage/store.d.ts +10 -0
- package/dist/credential-storage/store.js +282 -0
- package/dist/credentials.d.ts +1 -3
- package/dist/credentials.js +3 -275
- package/dist/mailbox-fleet-output.js +11 -2
- package/dist/sequence-start-at.d.ts +7 -0
- package/dist/sequence-start-at.js +21 -0
- package/dist/sequences-brand-kit-command.d.ts +28 -0
- package/dist/sequences-brand-kit-command.js +83 -0
- package/dist/sequences-content-commands.d.ts +28 -0
- package/dist/sequences-content-commands.js +176 -0
- package/dist/sequences-newsletter-commands.d.ts +6 -0
- package/dist/sequences-newsletter-commands.js +108 -0
- package/dist/sequences-test-send-command.d.ts +3 -0
- package/dist/sequences-test-send-command.js +47 -0
- package/dist/workspace-placement-commands.js +3 -2
- package/node_modules/@oxygen/formula/dist/evaluate.js +2 -4
- package/node_modules/@oxygen/formula/dist/expression.d.ts +7 -5
- package/node_modules/@oxygen/formula/dist/expression.js +26 -4
- package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +6 -0
- package/node_modules/@oxygen/formula/dist/formula-functions.js +55 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/billing.js +5 -7
- package/node_modules/@oxygen/shared/dist/bulk-email-pacing.d.ts +105 -0
- package/node_modules/@oxygen/shared/dist/bulk-email-pacing.js +125 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +236 -18
- package/node_modules/@oxygen/shared/dist/clay-migration-auth-state.d.ts +7 -1
- package/node_modules/@oxygen/shared/dist/clay-migration-auth-state.js +6 -5
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/clay-migration.d.ts +40 -1
- package/node_modules/@oxygen/shared/dist/contracts/clay-migration.js +60 -6
- package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.js +7 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +2 -1
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +6 -1
- package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.js +2 -2
- package/node_modules/@oxygen/shared/dist/email-brand-kit.d.ts +108 -0
- package/node_modules/@oxygen/shared/dist/email-brand-kit.js +214 -0
- package/node_modules/@oxygen/shared/dist/email-classification-scope.d.ts +35 -0
- package/node_modules/@oxygen/shared/dist/email-classification-scope.js +40 -0
- package/node_modules/@oxygen/shared/dist/email-delivery.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/email-delivery.js +14 -0
- package/node_modules/@oxygen/shared/dist/email-documents.d.ts +61 -0
- package/node_modules/@oxygen/shared/dist/email-documents.js +143 -0
- package/node_modules/@oxygen/shared/dist/email-dsn.js +52 -10
- package/node_modules/@oxygen/shared/dist/email-html-sanitize.d.ts +23 -0
- package/node_modules/@oxygen/shared/dist/email-html-sanitize.js +96 -0
- package/node_modules/@oxygen/shared/dist/email-subscriptions.d.ts +63 -1
- package/node_modules/@oxygen/shared/dist/email-subscriptions.js +149 -1
- package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.d.ts +41 -2
- package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.js +50 -1
- package/node_modules/@oxygen/shared/dist/native-chat.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/native-chat.js +12 -1
- package/node_modules/@oxygen/shared/dist/newsletter-analytics.d.ts +90 -0
- package/node_modules/@oxygen/shared/dist/newsletter-analytics.js +28 -0
- package/node_modules/@oxygen/shared/dist/newsletter-test-send.d.ts +20 -0
- package/node_modules/@oxygen/shared/dist/newsletter-test-send.js +46 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +12 -0
- package/node_modules/@oxygen/shared/dist/onboarding-research.d.ts +9 -2
- package/node_modules/@oxygen/shared/dist/onboarding-research.js +9 -2
- package/node_modules/@oxygen/shared/dist/plan-band.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/plan-band.js +17 -0
- package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +4 -2
- package/node_modules/@oxygen/shared/dist/plan-capabilities.js +12 -1
- package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.js +3 -0
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +5 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +29 -0
- package/node_modules/@oxygen/shared/dist/recipes.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/recipes.js +13 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-faults.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-faults.js +53 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-rollout.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/sending-domain-rollout.js +75 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +10 -1
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +68 -6
- package/node_modules/@oxygen/shared/dist/sequence-lifecycle.d.ts +34 -3
- package/node_modules/@oxygen/shared/dist/sequence-lifecycle.js +57 -4
- package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +41 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +86 -4
- package/node_modules/@oxygen/shared/dist/ses-identity-policy.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/ses-identity-policy.js +54 -0
- package/node_modules/@oxygen/shared/dist/skill-play-routing.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/skill-play-routing.js +75 -0
- package/node_modules/@oxygen/shared/dist/skill-plays.generated.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/skill-plays.generated.js +269 -0
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/spend-safety.js +7 -0
- package/node_modules/@oxygen/shared/dist/teammates.js +2 -1
- package/node_modules/@oxygen/shared/dist/tool-call-arguments.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/tool-call-arguments.js +68 -0
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +15 -1
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +2 -2
- package/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement.js +11 -0
- package/package.json +2 -2
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { requestOxygen } from "../http-client.js";
|
|
2
2
|
import { isRecord, readOption } from "../util.js";
|
|
3
3
|
import { OxygenError } from "@oxygen/shared/cli-result";
|
|
4
|
+
import { EmailSubscriptionInputError, chunkContactImport, keepContactProperties, parseContactsCsv, } from "@oxygen/shared/email-subscriptions";
|
|
4
5
|
import { Command } from "commander";
|
|
5
6
|
import { readFileSync } from "node:fs";
|
|
6
7
|
import { resolve } from "node:path";
|
|
8
|
+
/** --description / --parent on update: absent leaves it, "" clears it (null). */
|
|
9
|
+
function clearableOption(value) {
|
|
10
|
+
if (value === undefined)
|
|
11
|
+
return {};
|
|
12
|
+
return { value: readOption(value) ?? null };
|
|
13
|
+
}
|
|
7
14
|
function evidenceBody(options) {
|
|
8
15
|
const noticeVersion = readOption(options.noticeVersion);
|
|
9
16
|
const noticeTextHash = readOption(options.noticeTextHash);
|
|
@@ -25,48 +32,25 @@ function requireSubscribeEvidence(options, status) {
|
|
|
25
32
|
throw new OxygenError("consent_evidence_required", "Subscribing needs consent evidence: --notice-version with --notice-text-hash, or --basis naming where consent came from.", { details: { field: "evidence" }, exitCode: 2 });
|
|
26
33
|
}
|
|
27
34
|
}
|
|
28
|
-
/** Split one CSV line, honouring double-quoted fields with "" escapes. */
|
|
29
|
-
function splitCsvLine(line) {
|
|
30
|
-
const cells = [];
|
|
31
|
-
let cell = "";
|
|
32
|
-
let quoted = false;
|
|
33
|
-
for (let index = 0; index < line.length; index += 1) {
|
|
34
|
-
const char = line[index];
|
|
35
|
-
if (quoted) {
|
|
36
|
-
if (char === "\"" && line[index + 1] === "\"") {
|
|
37
|
-
cell += "\"";
|
|
38
|
-
index += 1;
|
|
39
|
-
}
|
|
40
|
-
else if (char === "\"")
|
|
41
|
-
quoted = false;
|
|
42
|
-
else
|
|
43
|
-
cell += char;
|
|
44
|
-
}
|
|
45
|
-
else if (char === "\"")
|
|
46
|
-
quoted = true;
|
|
47
|
-
else if (char === ",") {
|
|
48
|
-
cells.push(cell.trim());
|
|
49
|
-
cell = "";
|
|
50
|
-
}
|
|
51
|
-
else
|
|
52
|
-
cell += char;
|
|
53
|
-
}
|
|
54
|
-
cells.push(cell.trim());
|
|
55
|
-
return cells;
|
|
56
|
-
}
|
|
57
35
|
function csvEntries(text) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const emailIndex = header.findIndex((cell) => cell === "email" || cell === "email_address" || cell === "e-mail");
|
|
61
|
-
if (emailIndex < 0) {
|
|
62
|
-
throw new OxygenError("invalid_request", "The CSV needs an email column in its header row.", { exitCode: 2 });
|
|
36
|
+
try {
|
|
37
|
+
return parseContactsCsv(text).entries;
|
|
63
38
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof EmailSubscriptionInputError)
|
|
41
|
+
throw new OxygenError("invalid_request", error.message, { details: { field: error.field }, exitCode: 2 });
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Keep only the columns that are contact properties of this workspace, so a
|
|
47
|
+
* stray column (a CRM export's "company", an index) is reported, not refused.
|
|
48
|
+
*/
|
|
49
|
+
async function keepKnownProperties(entries) {
|
|
50
|
+
if (!entries.some((entry) => entry.properties && Object.keys(entry.properties).length > 0))
|
|
51
|
+
return { entries, ignored: [] };
|
|
52
|
+
const known = await requestOxygen("/api/cli/subscriptions/properties");
|
|
53
|
+
return keepContactProperties(entries, new Set((known.properties ?? []).map((property) => property.key)));
|
|
70
54
|
}
|
|
71
55
|
function jsonEntries(value) {
|
|
72
56
|
const list = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.entries) ? value.entries : null;
|
|
@@ -77,7 +61,16 @@ function jsonEntries(value) {
|
|
|
77
61
|
if (typeof entry === "string")
|
|
78
62
|
return { email: entry };
|
|
79
63
|
if (isRecord(entry) && typeof entry.email === "string") {
|
|
80
|
-
|
|
64
|
+
const properties = isRecord(entry.properties)
|
|
65
|
+
? Object.fromEntries(Object.entries(entry.properties).flatMap(([key, value]) => (typeof value === "string" || typeof value === "number" ? [[key, String(value)]] : [])))
|
|
66
|
+
: {};
|
|
67
|
+
return {
|
|
68
|
+
email: entry.email,
|
|
69
|
+
...(typeof entry.person_id === "string" ? { person_id: entry.person_id } : {}),
|
|
70
|
+
...(typeof entry.first_name === "string" && entry.first_name ? { first_name: entry.first_name } : {}),
|
|
71
|
+
...(typeof entry.last_name === "string" && entry.last_name ? { last_name: entry.last_name } : {}),
|
|
72
|
+
...(Object.keys(properties).length > 0 ? { properties } : {}),
|
|
73
|
+
};
|
|
81
74
|
}
|
|
82
75
|
return { email: "" };
|
|
83
76
|
});
|
|
@@ -121,24 +114,133 @@ function runSubscriptionsSet(email, options) {
|
|
|
121
114
|
},
|
|
122
115
|
});
|
|
123
116
|
}
|
|
124
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Send the import in route-sized calls (one consent evidence record covers
|
|
119
|
+
* every call: the same source vouched for the whole file) and add up the
|
|
120
|
+
* counts. Invalid indexes refer to the file's own rows.
|
|
121
|
+
*/
|
|
122
|
+
async function runSubscriptionsImport(options) {
|
|
125
123
|
const list = requireFlag(options.list, "--list");
|
|
126
124
|
const source = requireFlag(options.source, "--source");
|
|
127
125
|
const status = readOption(options.status) ?? "subscribed";
|
|
128
126
|
requireSubscribeEvidence(options, status);
|
|
129
|
-
const
|
|
127
|
+
const read = readSubscriptionImportFile(requireFlag(options.file, "--file"));
|
|
128
|
+
const { entries, ignored } = await keepKnownProperties(read);
|
|
130
129
|
const evidence = evidenceBody(options);
|
|
131
|
-
|
|
130
|
+
const chunks = chunkContactImport(entries);
|
|
131
|
+
let last = {};
|
|
132
|
+
const totals = { received: 0, created: 0, updated: 0, unchanged: 0, kept_cleaned: 0, people_written: 0, people_matched: 0 };
|
|
133
|
+
const invalid = [];
|
|
134
|
+
let offset = 0;
|
|
135
|
+
for (const chunk of chunks) {
|
|
136
|
+
last = await requestOxygen("/api/cli/subscriptions/import", {
|
|
137
|
+
method: "POST",
|
|
138
|
+
body: {
|
|
139
|
+
list,
|
|
140
|
+
entries: chunk,
|
|
141
|
+
status,
|
|
142
|
+
source,
|
|
143
|
+
...(evidence ? { evidence } : {}),
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
for (const key of Object.keys(totals))
|
|
147
|
+
totals[key] += Number(last[key] ?? 0);
|
|
148
|
+
for (const entry of last.invalid ?? [])
|
|
149
|
+
invalid.push({ ...entry, index: entry.index + offset });
|
|
150
|
+
offset += chunk.length;
|
|
151
|
+
}
|
|
152
|
+
if (chunks.length === 1 && ignored.length === 0)
|
|
153
|
+
return last;
|
|
154
|
+
const written = totals.created + totals.updated;
|
|
155
|
+
return {
|
|
156
|
+
...last,
|
|
157
|
+
...totals,
|
|
158
|
+
invalid,
|
|
159
|
+
calls: chunks.length,
|
|
160
|
+
...(ignored.length > 0 ? { ignored_columns: ignored } : {}),
|
|
161
|
+
changed: written > 0,
|
|
162
|
+
effect_outcome: written > 0 ? (invalid.length > 0 ? "partial" : "applied") : totals.received === 0 ? "no_data" : "no_change",
|
|
163
|
+
message: `${written} of ${totals.received} addresses changed on ${list} in ${chunks.length} call(s)`
|
|
164
|
+
+ (ignored.length > 0 ? `; columns that are not contact properties were skipped: ${ignored.join(", ")}` : "")
|
|
165
|
+
+ ".",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** --property key=value, repeatable; key= (empty) clears the property. */
|
|
169
|
+
function readPropertyFlags(values) {
|
|
170
|
+
if (!values || values.length === 0)
|
|
171
|
+
return undefined;
|
|
172
|
+
const properties = {};
|
|
173
|
+
for (const value of values) {
|
|
174
|
+
const at = value.indexOf("=");
|
|
175
|
+
if (at <= 0) {
|
|
176
|
+
throw new OxygenError("invalid_request", "--property takes key=value (key= clears it).", { details: { field: "properties", value }, exitCode: 2 });
|
|
177
|
+
}
|
|
178
|
+
const raw = value.slice(at + 1);
|
|
179
|
+
properties[value.slice(0, at).trim()] = raw === "" ? null : raw;
|
|
180
|
+
}
|
|
181
|
+
return properties;
|
|
182
|
+
}
|
|
183
|
+
function contactFieldBody(options) {
|
|
184
|
+
const properties = readPropertyFlags(options.property);
|
|
185
|
+
return {
|
|
186
|
+
// An explicit empty flag clears the field on the person.
|
|
187
|
+
...(options.firstName !== undefined ? { first_name: options.firstName === "" ? null : options.firstName } : {}),
|
|
188
|
+
...(options.lastName !== undefined ? { last_name: options.lastName === "" ? null : options.lastName } : {}),
|
|
189
|
+
...(properties ? { properties } : {}),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function collect(value, previous = []) {
|
|
193
|
+
return [...previous, value];
|
|
194
|
+
}
|
|
195
|
+
function runContactsList(options) {
|
|
196
|
+
const params = new URLSearchParams({ list: requireFlag(options.list, "--list") });
|
|
197
|
+
for (const [key, value] of [["search", options.search], ["status", options.status], ["after", options.after], ["limit", options.limit]]) {
|
|
198
|
+
const read = readOption(value);
|
|
199
|
+
if (read)
|
|
200
|
+
params.set(key, read);
|
|
201
|
+
}
|
|
202
|
+
return requestOxygen(`/api/cli/subscriptions/contacts?${params.toString()}`);
|
|
203
|
+
}
|
|
204
|
+
function runContactsAdd(email, options) {
|
|
205
|
+
const list = requireFlag(options.list, "--list");
|
|
206
|
+
const status = readOption(options.status) ?? "subscribed";
|
|
207
|
+
const note = readOption(options.note);
|
|
208
|
+
if (!note)
|
|
209
|
+
requireSubscribeEvidence(options, status);
|
|
210
|
+
const source = readOption(options.source);
|
|
211
|
+
const evidence = evidenceBody(options);
|
|
212
|
+
return requestOxygen("/api/cli/subscriptions/contacts", {
|
|
132
213
|
method: "POST",
|
|
133
214
|
body: {
|
|
134
215
|
list,
|
|
135
|
-
|
|
216
|
+
email,
|
|
136
217
|
status,
|
|
137
|
-
source,
|
|
218
|
+
...(source ? { source } : {}),
|
|
219
|
+
...(note ? { note } : {}),
|
|
138
220
|
...(evidence ? { evidence } : {}),
|
|
221
|
+
...contactFieldBody(options),
|
|
139
222
|
},
|
|
140
223
|
});
|
|
141
224
|
}
|
|
225
|
+
function runContactsEdit(email, options) {
|
|
226
|
+
return requestOxygen("/api/cli/subscriptions/contacts", {
|
|
227
|
+
method: "PATCH",
|
|
228
|
+
body: { list: requireFlag(options.list, "--list"), email, ...contactFieldBody(options) },
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function runContactsRemove(email, options) {
|
|
232
|
+
const params = new URLSearchParams({ list: requireFlag(options.list, "--list"), email });
|
|
233
|
+
return requestOxygen(`/api/cli/subscriptions/contacts?${params.toString()}`, { method: "DELETE" });
|
|
234
|
+
}
|
|
235
|
+
async function runSubscriptionsExport(options) {
|
|
236
|
+
const list = requireFlag(options.list, "--list");
|
|
237
|
+
const output = resolve(requireFlag(options.output, "--output"));
|
|
238
|
+
const params = new URLSearchParams({ list, format: "csv" });
|
|
239
|
+
const saved = await requestOxygen(`/api/cli/subscriptions/contacts/export?${params.toString()}`, {
|
|
240
|
+
binaryOutputPath: output,
|
|
241
|
+
});
|
|
242
|
+
return { list, path: saved.path, bytes: saved.bytes, message: `Saved ${list}'s contacts to ${saved.path}.` };
|
|
243
|
+
}
|
|
142
244
|
function addEvidenceFlags(command) {
|
|
143
245
|
return command
|
|
144
246
|
.option("--notice-version <version>", "Version of the consent notice the person saw (with --notice-text-hash).")
|
|
@@ -158,18 +260,64 @@ export function registerSubscriptionsFamily(program, handleAsyncAction) {
|
|
|
158
260
|
await handleAsyncAction("subscriptions lists list", options, () => requestOxygen("/api/cli/subscriptions/lists"));
|
|
159
261
|
}))
|
|
160
262
|
.addCommand(new Command("create")
|
|
161
|
-
.description("Create a subscription list.")
|
|
263
|
+
.description("Create a subscription list or a topic. A topic (--kind topic --parent <list>) is a narrower subject inside an audience list; a newsletter on a topic mails only its subscribers. --default opt_out puts the list's subscribers on the topic (same consent); opt_in waits for each recipient's choice on their preference page.")
|
|
162
264
|
.argument("<key>", "Stable key: lowercase letters, digits, - and _, such as newsletter.")
|
|
163
265
|
.requiredOption("--name <name>", "Display name.")
|
|
164
|
-
.requiredOption("--kind <kind>", "marketing, product_updates or
|
|
266
|
+
.requiredOption("--kind <kind>", "marketing, product_updates, onboarding or topic.")
|
|
165
267
|
.option("--description <text>", "What subscribers get.")
|
|
268
|
+
.option("--visibility <visibility>", "public (default; on the preference page) or private (shown only to subscribers).")
|
|
269
|
+
.option("--default <subscription>", "Topics: opt_in (default) or opt_out (joined with the parent list).")
|
|
270
|
+
.option("--parent <key-or-id>", "Topics: the audience list this topic belongs to.")
|
|
166
271
|
.option("--json", "Print a JSON envelope.")
|
|
167
272
|
.action(async (key, options) => {
|
|
168
273
|
await handleAsyncAction("subscriptions lists create", options, () => {
|
|
169
274
|
const description = readOption(options.description);
|
|
275
|
+
const visibility = readOption(options.visibility);
|
|
276
|
+
const defaultSubscription = readOption(options.default);
|
|
277
|
+
const parent = readOption(options.parent);
|
|
170
278
|
return requestOxygen("/api/cli/subscriptions/lists", {
|
|
171
279
|
method: "POST",
|
|
172
|
-
body: {
|
|
280
|
+
body: {
|
|
281
|
+
key,
|
|
282
|
+
name: options.name,
|
|
283
|
+
kind: options.kind,
|
|
284
|
+
...(description ? { description } : {}),
|
|
285
|
+
...(visibility ? { visibility } : {}),
|
|
286
|
+
...(defaultSubscription ? { default_subscription: defaultSubscription } : {}),
|
|
287
|
+
...(parent ? { parent } : {}),
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}))
|
|
292
|
+
.addCommand(new Command("update")
|
|
293
|
+
.description("Change a list or topic: name, description, kind, visibility, default subscription or parent. Turning a topic opt_out puts its list's current subscribers on it, each with their own consent evidence. The key never changes.")
|
|
294
|
+
.argument("<list>", "List key or id.")
|
|
295
|
+
.option("--name <name>", "Display name.")
|
|
296
|
+
.option("--kind <kind>", "marketing, product_updates, onboarding or topic.")
|
|
297
|
+
.option("--description <text>", "What subscribers get (\"\" clears it).")
|
|
298
|
+
.option("--visibility <visibility>", "public or private.")
|
|
299
|
+
.option("--default <subscription>", "Topics: opt_in or opt_out.")
|
|
300
|
+
.option("--parent <key-or-id>", "Topics: the audience list (\"\" clears it).")
|
|
301
|
+
.option("--json", "Print a JSON envelope.")
|
|
302
|
+
.action(async (list, options) => {
|
|
303
|
+
await handleAsyncAction("subscriptions lists update", options, () => {
|
|
304
|
+
const name = readOption(options.name);
|
|
305
|
+
const kind = readOption(options.kind);
|
|
306
|
+
const visibility = readOption(options.visibility);
|
|
307
|
+
const defaultSubscription = readOption(options.default);
|
|
308
|
+
const description = clearableOption(options.description);
|
|
309
|
+
const parent = clearableOption(options.parent);
|
|
310
|
+
return requestOxygen("/api/cli/subscriptions/lists", {
|
|
311
|
+
method: "PATCH",
|
|
312
|
+
body: {
|
|
313
|
+
list,
|
|
314
|
+
...(name ? { name } : {}),
|
|
315
|
+
...(kind ? { kind } : {}),
|
|
316
|
+
...(visibility ? { visibility } : {}),
|
|
317
|
+
...(defaultSubscription ? { default_subscription: defaultSubscription } : {}),
|
|
318
|
+
...("value" in description ? { description: description.value } : {}),
|
|
319
|
+
...("value" in parent ? { parent: parent.value } : {}),
|
|
320
|
+
},
|
|
173
321
|
});
|
|
174
322
|
});
|
|
175
323
|
})))
|
|
@@ -191,7 +339,7 @@ export function registerSubscriptionsFamily(program, handleAsyncAction) {
|
|
|
191
339
|
await handleAsyncAction("subscriptions set", options, () => runSubscriptionsSet(email, options));
|
|
192
340
|
}))
|
|
193
341
|
.addCommand(addEvidenceFlags(new Command("import")
|
|
194
|
-
.description("Import
|
|
342
|
+
.description("Import addresses onto one list under one consent evidence record. --file is JSON (emails or { email, first_name, last_name, properties }) or CSV with an email column; first_name, last_name and contact-property columns go to each person on Records People (created or matched by email). Large files are sent in batches. Addresses cleaned by a complaint or hard bounce stay off; invalid rows are reported by index.")
|
|
195
343
|
.requiredOption("--list <key-or-id>", "Subscription list key or id.")
|
|
196
344
|
.requiredOption("--file <path>", "JSON or CSV file of addresses.")
|
|
197
345
|
.requiredOption("--source <source>", "Where these addresses came from, e.g. webinar_2026_10.")
|
|
@@ -199,5 +347,103 @@ export function registerSubscriptionsFamily(program, handleAsyncAction) {
|
|
|
199
347
|
.option("--json", "Print a JSON envelope.")
|
|
200
348
|
.action(async (options) => {
|
|
201
349
|
await handleAsyncAction("subscriptions import", options, () => runSubscriptionsImport(options));
|
|
202
|
-
}))
|
|
350
|
+
}))
|
|
351
|
+
.addCommand(new Command("export")
|
|
352
|
+
.description("Save every contact of a list as CSV: each status, the consent that subscribed it (when, source, basis or notice), name and contact properties.")
|
|
353
|
+
.requiredOption("--list <key-or-id>", "Subscription list key or id.")
|
|
354
|
+
.requiredOption("--output <path>", "CSV file to create (must not exist).")
|
|
355
|
+
.option("--json", "Print a JSON envelope.")
|
|
356
|
+
.action(async (options) => {
|
|
357
|
+
await handleAsyncAction("subscriptions export", options, () => runSubscriptionsExport(options));
|
|
358
|
+
}))
|
|
359
|
+
.addCommand(registerContactsCommands(handleAsyncAction))
|
|
360
|
+
.addCommand(registerPropertiesCommands(handleAsyncAction)));
|
|
361
|
+
}
|
|
362
|
+
function addContactFieldFlags(command) {
|
|
363
|
+
return command
|
|
364
|
+
.requiredOption("--list <key-or-id>", "Subscription list key or id.")
|
|
365
|
+
.option("--first-name <name>", "First name on the person's Records People record (\"\" clears it).")
|
|
366
|
+
.option("--last-name <name>", "Last name on the person's Records People record (\"\" clears it).")
|
|
367
|
+
.option("--property <key=value>", "A contact property, repeatable; key= clears it. List keys with subscriptions properties list.", collect);
|
|
368
|
+
}
|
|
369
|
+
function registerContactsCommands(handleAsyncAction) {
|
|
370
|
+
return new Command("contacts")
|
|
371
|
+
.description("Contacts of a subscription list: each address with its status, consent source and its Records person (name and contact properties).")
|
|
372
|
+
.addCommand(new Command("list")
|
|
373
|
+
.description("List a list's contacts with status, source, subscribed date, name and properties; search by email or name.")
|
|
374
|
+
.requiredOption("--list <key-or-id>", "Subscription list key or id.")
|
|
375
|
+
.option("--search <text>", "Match the email or the person's name.")
|
|
376
|
+
.option("--status <status>", "Only subscribed, unsubscribed, pending or cleaned.")
|
|
377
|
+
.option("--after <email>", "next_cursor of the previous page.")
|
|
378
|
+
.option("--limit <n>", "Contacts per page, 1 to 200 (default 50).")
|
|
379
|
+
.option("--json", "Print a JSON envelope.")
|
|
380
|
+
.action(async (options) => {
|
|
381
|
+
await handleAsyncAction("subscriptions contacts list", options, () => runContactsList(options));
|
|
382
|
+
}))
|
|
383
|
+
.addCommand(addEvidenceFlags(addContactFieldFlags(new Command("add")
|
|
384
|
+
.description("Add one contact to a list by hand, with consent evidence (--note says where consent came from), and write its name and properties to Records People.")
|
|
385
|
+
.argument("<email>", "Email address."))
|
|
386
|
+
.option("--status <status>", "Status to set (default subscribed).")
|
|
387
|
+
.option("--source <source>", "Where this contact came from (default manual).")
|
|
388
|
+
.option("--note <text>", "How you got this person's consent; recorded as the consent basis with your name."))
|
|
389
|
+
.option("--json", "Print a JSON envelope.")
|
|
390
|
+
.action(async (email, options) => {
|
|
391
|
+
await handleAsyncAction("subscriptions contacts add", options, () => runContactsAdd(email, options));
|
|
392
|
+
}))
|
|
393
|
+
.addCommand(addContactFieldFlags(new Command("edit")
|
|
394
|
+
.description("Edit a contact's name or properties on its Records People record. Status and consent stay as they are.")
|
|
395
|
+
.argument("<email>", "Email address."))
|
|
396
|
+
.option("--json", "Print a JSON envelope.")
|
|
397
|
+
.action(async (email, options) => {
|
|
398
|
+
await handleAsyncAction("subscriptions contacts edit", options, () => runContactsEdit(email, options));
|
|
399
|
+
}))
|
|
400
|
+
.addCommand(new Command("remove")
|
|
401
|
+
.description("Remove a contact from a list. Its consent history is kept and its Records person is untouched; to stop mail but keep it listed, use subscriptions set --status unsubscribed.")
|
|
402
|
+
.argument("<email>", "Email address.")
|
|
403
|
+
.requiredOption("--list <key-or-id>", "Subscription list key or id.")
|
|
404
|
+
.option("--json", "Print a JSON envelope.")
|
|
405
|
+
.action(async (email, options) => {
|
|
406
|
+
await handleAsyncAction("subscriptions contacts remove", options, () => runContactsRemove(email, options));
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
function registerPropertiesCommands(handleAsyncAction) {
|
|
410
|
+
return new Command("properties")
|
|
411
|
+
.description("Contact properties: text or number fields on Records People that newsletters use as {{contact.<key>}}.")
|
|
412
|
+
.addCommand(new Command("list")
|
|
413
|
+
.description("List contact properties (key, label, type).")
|
|
414
|
+
.option("--json", "Print a JSON envelope.")
|
|
415
|
+
.action(async (options) => {
|
|
416
|
+
await handleAsyncAction("subscriptions properties list", options, () => requestOxygen("/api/cli/subscriptions/properties"));
|
|
417
|
+
}))
|
|
418
|
+
.addCommand(new Command("create")
|
|
419
|
+
.description("Create a contact property: a new text or number field on Records People.")
|
|
420
|
+
.argument("<key>", "Field key, e.g. plan; {{contact.plan}} in a template.")
|
|
421
|
+
.option("--label <label>", "Display name (default the key).")
|
|
422
|
+
.option("--type <type>", "string or number (default string).")
|
|
423
|
+
.option("--json", "Print a JSON envelope.")
|
|
424
|
+
.action(async (key, options) => {
|
|
425
|
+
await handleAsyncAction("subscriptions properties create", options, () => {
|
|
426
|
+
const label = readOption(options.label);
|
|
427
|
+
const type = readOption(options.type);
|
|
428
|
+
return requestOxygen("/api/cli/subscriptions/properties", {
|
|
429
|
+
method: "POST",
|
|
430
|
+
body: { key, ...(label ? { label } : {}), ...(type ? { type } : {}) },
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
}))
|
|
434
|
+
.addCommand(new Command("update")
|
|
435
|
+
.description("Rename a contact property. Its key never changes.")
|
|
436
|
+
.argument("<key>", "Property key.")
|
|
437
|
+
.requiredOption("--label <label>", "New display name.")
|
|
438
|
+
.option("--json", "Print a JSON envelope.")
|
|
439
|
+
.action(async (key, options) => {
|
|
440
|
+
await handleAsyncAction("subscriptions properties update", options, () => requestOxygen("/api/cli/subscriptions/properties", { method: "PATCH", body: { key, label: options.label } }));
|
|
441
|
+
}))
|
|
442
|
+
.addCommand(new Command("delete")
|
|
443
|
+
.description("Delete a contact property: the People field is removed and its values archived (restorable on the People table).")
|
|
444
|
+
.argument("<key>", "Property key.")
|
|
445
|
+
.option("--json", "Print a JSON envelope.")
|
|
446
|
+
.action(async (key, options) => {
|
|
447
|
+
await handleAsyncAction("subscriptions properties delete", options, () => requestOxygen(`/api/cli/subscriptions/properties?${new URLSearchParams({ key }).toString()}`, { method: "DELETE" }));
|
|
448
|
+
}));
|
|
203
449
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** The CLI's per-user config directory; `OXYGEN_CONFIG_DIR` overrides it. */
|
|
2
|
+
export declare function resolveConfigDir(env?: NodeJS.ProcessEnv): string;
|
|
3
|
+
/** Own raw credential IO. The same live environment is read at every original
|
|
4
|
+
* operation boundary; the application still owns documents and profiles.
|
|
5
|
+
* Construct once at the application's original promisify initialization point. */
|
|
6
|
+
export declare function createCredentialStorage(): {
|
|
7
|
+
readSystemCredentialPayload: (env: NodeJS.ProcessEnv) => Promise<string | null>;
|
|
8
|
+
writeSystemCredentialPayload: (payload: string, env: NodeJS.ProcessEnv) => Promise<void>;
|
|
9
|
+
deleteSystemCredentialPayload: (env: NodeJS.ProcessEnv) => Promise<void>;
|
|
10
|
+
};
|