maeve-cli 0.9.0
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/LICENSE +21 -0
- package/README.md +547 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +2328 -0
- package/examples/create-content.json +13 -0
- package/package.json +49 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2328 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
5
|
+
import { pathToFileURL } from "url";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
|
|
8
|
+
// src/commands/analytics.ts
|
|
9
|
+
import { writeFile } from "fs/promises";
|
|
10
|
+
|
|
11
|
+
// src/analytics-schema.ts
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
var uuid = z.string().uuid();
|
|
14
|
+
var analyticsDays = z.union([z.literal("all"), z.coerce.number().int().min(1).max(90)]);
|
|
15
|
+
var titlePageSchema = z.object({
|
|
16
|
+
enabled: z.boolean(),
|
|
17
|
+
title: z.string().max(200).optional(),
|
|
18
|
+
description: z.string().max(1e3).optional()
|
|
19
|
+
}).strict();
|
|
20
|
+
var reportSectionsSchema = z.object({
|
|
21
|
+
demographics: z.boolean().optional(),
|
|
22
|
+
topPosts: z.boolean().optional()
|
|
23
|
+
}).strict();
|
|
24
|
+
var analyticsReportSchema = z.object({
|
|
25
|
+
integrationId: uuid.optional(),
|
|
26
|
+
integrationIds: z.array(uuid).min(1).max(20).optional(),
|
|
27
|
+
days: analyticsDays.optional(),
|
|
28
|
+
titlePage: titlePageSchema.optional(),
|
|
29
|
+
sections: reportSectionsSchema.optional()
|
|
30
|
+
}).strict().refine((payload) => Boolean(payload.integrationId || payload.integrationIds?.length), {
|
|
31
|
+
message: "Report payload must include integrationId or integrationIds."
|
|
32
|
+
}).refine((payload) => new Set(payload.integrationIds ?? []).size === (payload.integrationIds ?? []).length, {
|
|
33
|
+
message: "integrationIds must be unique."
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// src/client.ts
|
|
37
|
+
import { createReadStream } from "fs";
|
|
38
|
+
import { basename } from "path";
|
|
39
|
+
|
|
40
|
+
// src/credentials.ts
|
|
41
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
42
|
+
import { homedir } from "os";
|
|
43
|
+
import { dirname, join } from "path";
|
|
44
|
+
function getCredentialFilePath(env = process.env, platform = process.platform) {
|
|
45
|
+
const override = env.MAEVE_CLI_AUTH_FILE ?? env.EZIBREEZY_CLI_AUTH_FILE;
|
|
46
|
+
if (override) return override;
|
|
47
|
+
if (platform === "win32") {
|
|
48
|
+
return join(env.APPDATA || join(homedir(), "AppData", "Roaming"), "Maeve", "cli-auth.json");
|
|
49
|
+
}
|
|
50
|
+
if (platform === "darwin") {
|
|
51
|
+
return join(homedir(), "Library", "Application Support", "Maeve", "cli-auth.json");
|
|
52
|
+
}
|
|
53
|
+
return join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "maeve", "cli-auth.json");
|
|
54
|
+
}
|
|
55
|
+
function getLegacyCredentialFilePath(env = process.env, platform = process.platform) {
|
|
56
|
+
if (env.MAEVE_CLI_AUTH_FILE ?? env.EZIBREEZY_CLI_AUTH_FILE) return void 0;
|
|
57
|
+
if (platform === "win32") {
|
|
58
|
+
return join(env.APPDATA || join(homedir(), "AppData", "Roaming"), "EziBreezy", "cli-auth.json");
|
|
59
|
+
}
|
|
60
|
+
if (platform === "darwin") {
|
|
61
|
+
return join(homedir(), "Library", "Application Support", "EziBreezy", "cli-auth.json");
|
|
62
|
+
}
|
|
63
|
+
return join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "ezibreezy", "cli-auth.json");
|
|
64
|
+
}
|
|
65
|
+
function migrateLegacyCredentialFile(env = process.env, platform = process.platform) {
|
|
66
|
+
const legacyPath = getLegacyCredentialFilePath(env, platform);
|
|
67
|
+
if (!legacyPath) return;
|
|
68
|
+
const targetPath = getCredentialFilePath(env, platform);
|
|
69
|
+
if (targetPath === legacyPath) return;
|
|
70
|
+
if (existsSync(targetPath) || !existsSync(legacyPath)) return;
|
|
71
|
+
try {
|
|
72
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
73
|
+
writeFileSync(targetPath, readFileSync(legacyPath, "utf8"), { mode: 384 });
|
|
74
|
+
restrictCredentialFilePermissions(targetPath);
|
|
75
|
+
rmSync(legacyPath, { force: true });
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function readCredentialFile(filePath = getCredentialFilePath()) {
|
|
80
|
+
if (!existsSync(filePath)) return { profiles: {} };
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
|
83
|
+
return { profiles: parsed.profiles && typeof parsed.profiles === "object" ? parsed.profiles : {} };
|
|
84
|
+
} catch {
|
|
85
|
+
return { profiles: {} };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function getStoredCredential(apiUrl, filePath = getCredentialFilePath()) {
|
|
89
|
+
return readCredentialFile(filePath).profiles[apiUrl];
|
|
90
|
+
}
|
|
91
|
+
function writeStoredCredential(apiUrl, credential, filePath = getCredentialFilePath()) {
|
|
92
|
+
const existing = readCredentialFile(filePath);
|
|
93
|
+
const next = {
|
|
94
|
+
profiles: {
|
|
95
|
+
...existing.profiles,
|
|
96
|
+
[apiUrl]: credential
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
100
|
+
writeFileSync(filePath, `${JSON.stringify(next, null, 2)}
|
|
101
|
+
`, { mode: 384 });
|
|
102
|
+
restrictCredentialFilePermissions(filePath);
|
|
103
|
+
}
|
|
104
|
+
function deleteStoredCredential(apiUrl, filePath = getCredentialFilePath()) {
|
|
105
|
+
const existing = readCredentialFile(filePath);
|
|
106
|
+
if (!existing.profiles[apiUrl]) return false;
|
|
107
|
+
delete existing.profiles[apiUrl];
|
|
108
|
+
const hasProfiles = Object.keys(existing.profiles).length > 0;
|
|
109
|
+
if (!hasProfiles) {
|
|
110
|
+
rmSync(filePath, { force: true });
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
writeFileSync(filePath, `${JSON.stringify(existing, null, 2)}
|
|
114
|
+
`, { mode: 384 });
|
|
115
|
+
restrictCredentialFilePermissions(filePath);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
function restrictCredentialFilePermissions(filePath) {
|
|
119
|
+
if (process.platform === "win32") return;
|
|
120
|
+
try {
|
|
121
|
+
chmodSync(filePath, 384);
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/output.ts
|
|
127
|
+
import { ZodError } from "zod";
|
|
128
|
+
var CliError = class extends Error {
|
|
129
|
+
code;
|
|
130
|
+
status;
|
|
131
|
+
details;
|
|
132
|
+
constructor(message, options = {}) {
|
|
133
|
+
super(message);
|
|
134
|
+
this.name = "CliError";
|
|
135
|
+
this.code = options.code ?? "CLI_ERROR";
|
|
136
|
+
this.status = options.status;
|
|
137
|
+
this.details = options.details;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
var SENSITIVE_KEY_PATTERN = /(?:api[-_]?key|access[-_]?token|refresh[-_]?token|id[-_]?token|client[-_]?secret|secret|password|authorization|cookie|set-cookie)/i;
|
|
141
|
+
var API_KEY_VALUE_PATTERN = /\bezb_(?:live|test)_[A-Za-z0-9._-]+/g;
|
|
142
|
+
var CLI_TOKEN_VALUE_PATTERN = /\bezb_cli_[A-Za-z0-9._~+/=-]+/g;
|
|
143
|
+
var BEARER_VALUE_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi;
|
|
144
|
+
function redactString(value) {
|
|
145
|
+
return value.replace(API_KEY_VALUE_PATTERN, "[redacted-api-key]").replace(CLI_TOKEN_VALUE_PATTERN, "[redacted-cli-token]").replace(BEARER_VALUE_PATTERN, "Bearer [redacted]");
|
|
146
|
+
}
|
|
147
|
+
function sanitizeForOutput(value) {
|
|
148
|
+
if (typeof value === "string") return redactString(value);
|
|
149
|
+
if (Array.isArray(value)) return value.map((item) => sanitizeForOutput(item));
|
|
150
|
+
if (!value || typeof value !== "object") return value;
|
|
151
|
+
const sanitized = {};
|
|
152
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
153
|
+
sanitized[key] = SENSITIVE_KEY_PATTERN.test(key) ? "[redacted]" : sanitizeForOutput(nestedValue);
|
|
154
|
+
}
|
|
155
|
+
return sanitized;
|
|
156
|
+
}
|
|
157
|
+
function writeJson(data, meta = null) {
|
|
158
|
+
process.stdout.write(`${JSON.stringify({ data, meta }, null, 2)}
|
|
159
|
+
`);
|
|
160
|
+
}
|
|
161
|
+
function writeRawJson(value) {
|
|
162
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
163
|
+
`);
|
|
164
|
+
}
|
|
165
|
+
function formatError(error) {
|
|
166
|
+
if (error instanceof CliError) {
|
|
167
|
+
return {
|
|
168
|
+
error: {
|
|
169
|
+
code: error.code,
|
|
170
|
+
message: redactString(error.message),
|
|
171
|
+
...error.status ? { status: error.status } : {},
|
|
172
|
+
...error.details !== void 0 ? { details: sanitizeForOutput(error.details) } : {}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (error instanceof ZodError) {
|
|
177
|
+
return {
|
|
178
|
+
error: {
|
|
179
|
+
code: "VALIDATION_ERROR",
|
|
180
|
+
message: "Input validation failed.",
|
|
181
|
+
details: error.issues
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (error instanceof Error) {
|
|
186
|
+
return {
|
|
187
|
+
error: {
|
|
188
|
+
code: "UNEXPECTED_ERROR",
|
|
189
|
+
message: redactString(error.message)
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
error: {
|
|
195
|
+
code: "UNEXPECTED_ERROR",
|
|
196
|
+
message: "Unexpected error."
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function writeError(error) {
|
|
201
|
+
process.stderr.write(`${JSON.stringify(formatError(error), null, 2)}
|
|
202
|
+
`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/config.ts
|
|
206
|
+
var DEFAULT_API_URL = "https://api.maevesocial.com";
|
|
207
|
+
function cleanBaseUrl(value) {
|
|
208
|
+
return value.replace(/\/+$/, "");
|
|
209
|
+
}
|
|
210
|
+
function resolveConfig(command, env = process.env) {
|
|
211
|
+
const options = command?.optsWithGlobals?.() ?? {};
|
|
212
|
+
const envApiUrl = env.MAEVE_API_URL ?? env.EZIBREEZY_API_URL;
|
|
213
|
+
const apiUrl = options.apiUrl || envApiUrl || DEFAULT_API_URL;
|
|
214
|
+
const namedEnvApiKey = options.apiKeyEnv ? env[options.apiKeyEnv] : void 0;
|
|
215
|
+
const defaultEnvApiKey = options.apiKeyEnv ? void 0 : env.MAEVE_API_KEY ?? env.EZIBREEZY_API_KEY;
|
|
216
|
+
const apiKey = options.apiKey || namedEnvApiKey || defaultEnvApiKey;
|
|
217
|
+
const cleanApiUrl = cleanBaseUrl(apiUrl);
|
|
218
|
+
const storedCredential = getStoredCredential(cleanApiUrl, getCredentialFilePath(env));
|
|
219
|
+
const storedExpiry = storedCredential ? new Date(storedCredential.expiresAt).getTime() : Number.NaN;
|
|
220
|
+
const storedCliLoginExpired = storedCredential ? !Number.isFinite(storedExpiry) || storedExpiry <= Date.now() : false;
|
|
221
|
+
const storedCliToken = !apiKey && storedCredential && !storedCliLoginExpired ? storedCredential.token : void 0;
|
|
222
|
+
const credentialSource = options.apiKey ? "flag_api_key" : namedEnvApiKey ? "named_env_api_key" : defaultEnvApiKey ? "env_api_key" : storedCliToken ? "stored_cli_login" : void 0;
|
|
223
|
+
const credentialType = credentialSource === "flag_api_key" || credentialSource === "named_env_api_key" || credentialSource === "env_api_key" ? "api_key" : credentialSource === "stored_cli_login" ? "cli_login" : void 0;
|
|
224
|
+
return {
|
|
225
|
+
apiUrl: cleanApiUrl,
|
|
226
|
+
apiKey,
|
|
227
|
+
bearerToken: apiKey || storedCliToken,
|
|
228
|
+
apiUrlSource: options.apiUrl ? "flag" : envApiUrl ? "env" : "default",
|
|
229
|
+
apiKeySource: options.apiKey ? "flag" : namedEnvApiKey ? "named_env" : defaultEnvApiKey ? "env" : void 0,
|
|
230
|
+
apiKeyEnvName: options.apiKeyEnv,
|
|
231
|
+
namedEnvApiKeyConfigured: Boolean(namedEnvApiKey),
|
|
232
|
+
credentialSource,
|
|
233
|
+
credentialType,
|
|
234
|
+
storedCliLoginConfigured: Boolean(storedCredential),
|
|
235
|
+
storedCliLoginExpired
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function requireBearerCredential(config) {
|
|
239
|
+
const token = config.bearerToken || config.apiKey;
|
|
240
|
+
if (!token) {
|
|
241
|
+
if (config.apiKeyEnvName && !config.namedEnvApiKeyConfigured) {
|
|
242
|
+
throw new CliError(`Missing credentials. Environment variable ${config.apiKeyEnvName} is not set.`, {
|
|
243
|
+
code: "MISSING_CREDENTIALS"
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
throw new CliError(
|
|
247
|
+
"Missing credentials. Set MAEVE_API_KEY, pass --api-key-env <name>, pass --api-key, or run maeve auth:login.",
|
|
248
|
+
{
|
|
249
|
+
code: "MISSING_CREDENTIALS"
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return token;
|
|
254
|
+
}
|
|
255
|
+
function addConnectionOptions(command) {
|
|
256
|
+
return command.option("--api-url <url>", "Maeve API base URL.").option("--api-key-env <name>", "Name of an environment variable containing a Maeve API key.").option("--api-key <key>", "Maeve API key. Prefer MAEVE_API_KEY for local shells.");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/client.ts
|
|
260
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
261
|
+
var DEFAULT_UPLOAD_TIMEOUT_MS = 15 * 6e4;
|
|
262
|
+
var CLI_USER_AGENT = "maeve-cli";
|
|
263
|
+
function appendQuery(url, query = {}) {
|
|
264
|
+
for (const [key, value] of Object.entries(query)) {
|
|
265
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
266
|
+
if (Array.isArray(value)) {
|
|
267
|
+
for (const item of value) url.searchParams.append(key, item);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
url.searchParams.set(key, String(value));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async function parseApiResponse(response) {
|
|
274
|
+
if (response.status === 204) return null;
|
|
275
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
276
|
+
if (!contentType.includes("application/json")) {
|
|
277
|
+
return response.text();
|
|
278
|
+
}
|
|
279
|
+
return response.json();
|
|
280
|
+
}
|
|
281
|
+
function apiErrorMessage(payload, fallback) {
|
|
282
|
+
if (!payload || typeof payload !== "object") return fallback;
|
|
283
|
+
const source = payload;
|
|
284
|
+
if (typeof source.message === "string") return source.message;
|
|
285
|
+
if (Array.isArray(source.message)) return source.message.join(", ");
|
|
286
|
+
if (typeof source.error === "string") return source.error;
|
|
287
|
+
return fallback;
|
|
288
|
+
}
|
|
289
|
+
function isAbortError(error) {
|
|
290
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
291
|
+
}
|
|
292
|
+
function filenameFromContentDisposition(value) {
|
|
293
|
+
if (!value) return void 0;
|
|
294
|
+
const encodedMatch = /filename\*=UTF-8''([^;]+)/i.exec(value);
|
|
295
|
+
if (encodedMatch?.[1]) return decodeURIComponent(encodedMatch[1].replace(/^"|"$/g, ""));
|
|
296
|
+
const match = /filename="?([^";]+)"?/i.exec(value);
|
|
297
|
+
return match?.[1];
|
|
298
|
+
}
|
|
299
|
+
function createApiClient(config, fetchImpl = fetch) {
|
|
300
|
+
const bearerToken = requireBearerCredential(config);
|
|
301
|
+
async function fetchWithTimeout(input, init, timeoutMs) {
|
|
302
|
+
const controller = new AbortController();
|
|
303
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
304
|
+
try {
|
|
305
|
+
return await fetchImpl(input, {
|
|
306
|
+
...init,
|
|
307
|
+
signal: controller.signal
|
|
308
|
+
});
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (isAbortError(error)) {
|
|
311
|
+
throw new CliError(`Request timed out after ${timeoutMs}ms.`, {
|
|
312
|
+
code: "REQUEST_TIMEOUT",
|
|
313
|
+
details: { timeoutMs }
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
throw error;
|
|
317
|
+
} finally {
|
|
318
|
+
clearTimeout(timer);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function request(method, path, options = {}) {
|
|
322
|
+
const url = new URL(path, `${config.apiUrl}/`);
|
|
323
|
+
appendQuery(url, options.query);
|
|
324
|
+
const response = await fetchWithTimeout(
|
|
325
|
+
url,
|
|
326
|
+
{
|
|
327
|
+
method,
|
|
328
|
+
headers: {
|
|
329
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
330
|
+
"User-Agent": CLI_USER_AGENT,
|
|
331
|
+
Accept: "application/json",
|
|
332
|
+
...options.body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
333
|
+
...options.headers
|
|
334
|
+
},
|
|
335
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
336
|
+
},
|
|
337
|
+
options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
|
|
338
|
+
);
|
|
339
|
+
const payload = await parseApiResponse(response);
|
|
340
|
+
if (!response.ok) {
|
|
341
|
+
throw new CliError(apiErrorMessage(payload, `API request failed with status ${response.status}.`), {
|
|
342
|
+
code: "API_ERROR",
|
|
343
|
+
status: response.status,
|
|
344
|
+
details: payload
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return payload;
|
|
348
|
+
}
|
|
349
|
+
async function download(method, path, options = {}) {
|
|
350
|
+
const url = new URL(path, `${config.apiUrl}/`);
|
|
351
|
+
appendQuery(url, options.query);
|
|
352
|
+
const response = await fetchWithTimeout(
|
|
353
|
+
url,
|
|
354
|
+
{
|
|
355
|
+
method,
|
|
356
|
+
headers: {
|
|
357
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
358
|
+
"User-Agent": CLI_USER_AGENT,
|
|
359
|
+
Accept: "application/pdf",
|
|
360
|
+
...options.body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
361
|
+
...options.headers
|
|
362
|
+
},
|
|
363
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
364
|
+
},
|
|
365
|
+
options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
|
|
366
|
+
);
|
|
367
|
+
if (!response.ok) {
|
|
368
|
+
const payload = await parseApiResponse(response);
|
|
369
|
+
throw new CliError(apiErrorMessage(payload, `API request failed with status ${response.status}.`), {
|
|
370
|
+
code: "API_ERROR",
|
|
371
|
+
status: response.status,
|
|
372
|
+
details: payload
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
body: Buffer.from(await response.arrayBuffer()),
|
|
377
|
+
contentType: response.headers.get("content-type") ?? "application/octet-stream",
|
|
378
|
+
filename: filenameFromContentDisposition(response.headers.get("content-disposition"))
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
async function uploadFile(path, filePath, contentType, fileSize, body, uploadTimeoutMs = DEFAULT_UPLOAD_TIMEOUT_MS) {
|
|
382
|
+
const upload = await request("POST", path, { body });
|
|
383
|
+
const uploadInit = {
|
|
384
|
+
method: "PUT",
|
|
385
|
+
headers: {
|
|
386
|
+
"Content-Type": contentType,
|
|
387
|
+
"Content-Length": String(fileSize)
|
|
388
|
+
},
|
|
389
|
+
body: createReadStream(filePath),
|
|
390
|
+
duplex: "half"
|
|
391
|
+
};
|
|
392
|
+
const response = await fetchWithTimeout(upload.data.uploadUrl, uploadInit, uploadTimeoutMs);
|
|
393
|
+
if (!response.ok) {
|
|
394
|
+
const payload = await parseApiResponse(response);
|
|
395
|
+
throw new CliError(apiErrorMessage(payload, `File upload failed with status ${response.status}.`), {
|
|
396
|
+
code: "UPLOAD_ERROR",
|
|
397
|
+
status: response.status,
|
|
398
|
+
details: payload
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
return request("POST", `${path}/${upload.data.sessionId}/complete`, {
|
|
402
|
+
body: {}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
return { request, download, uploadFile };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/files.ts
|
|
409
|
+
import { readFile, stat } from "fs/promises";
|
|
410
|
+
import { extname } from "path";
|
|
411
|
+
var CONTENT_TYPES = {
|
|
412
|
+
".jpg": "image/jpeg",
|
|
413
|
+
".jpeg": "image/jpeg",
|
|
414
|
+
".png": "image/png",
|
|
415
|
+
".gif": "image/gif",
|
|
416
|
+
".webp": "image/webp",
|
|
417
|
+
".avif": "image/avif",
|
|
418
|
+
".mp4": "video/mp4",
|
|
419
|
+
".mov": "video/quicktime"
|
|
420
|
+
};
|
|
421
|
+
async function readJsonFile(filePath) {
|
|
422
|
+
try {
|
|
423
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
424
|
+
} catch (error) {
|
|
425
|
+
if (error instanceof SyntaxError) {
|
|
426
|
+
throw new CliError(`Invalid JSON file: ${filePath}`, { code: "INVALID_JSON" });
|
|
427
|
+
}
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async function getUploadFileInfo(filePath) {
|
|
432
|
+
const extension = extname(filePath).toLowerCase();
|
|
433
|
+
const contentType = CONTENT_TYPES[extension];
|
|
434
|
+
if (!contentType) {
|
|
435
|
+
throw new CliError(`Unsupported upload file type: ${extension || "unknown"}`, {
|
|
436
|
+
code: "UNSUPPORTED_FILE_TYPE",
|
|
437
|
+
details: { supportedExtensions: Object.keys(CONTENT_TYPES) }
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
const file = await stat(filePath);
|
|
441
|
+
if (!file.isFile()) {
|
|
442
|
+
throw new CliError(`Upload path is not a file: ${filePath}`, { code: "INVALID_FILE" });
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
filename: filePath.split(/[\\/]/).pop() ?? filePath,
|
|
446
|
+
contentType,
|
|
447
|
+
fileSize: file.size
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/commands/analytics.ts
|
|
452
|
+
function registerAnalyticsCommands(program) {
|
|
453
|
+
addConnectionOptions(program.command("analytics:summary")).description("Fetch analytics summary for a workspace or one integration.").requiredOption("-w, --workspace <id>", "Workspace ID.").option("--days <range>", "Analytics lookback window: 1-90 or all.", "30").option("--integration <id>", "Integration ID. Uses account analytics instead of aggregate analytics.").option("--integration-ids <ids>", "Comma-separated integration IDs for aggregate analytics.").action(async (options, command) => {
|
|
454
|
+
if (options.integration && options.integrationIds) {
|
|
455
|
+
throw new CliError("Use either --integration or --integration-ids, not both.", { code: "INVALID_OPTIONS" });
|
|
456
|
+
}
|
|
457
|
+
const client = createApiClient(resolveConfig(command));
|
|
458
|
+
const path = options.integration ? `/v1/workspaces/${options.workspace}/analytics` : `/v1/workspaces/${options.workspace}/analytics/aggregate`;
|
|
459
|
+
writeRawJson(
|
|
460
|
+
await client.request("GET", path, {
|
|
461
|
+
query: {
|
|
462
|
+
days: options.days,
|
|
463
|
+
integrationId: options.integration,
|
|
464
|
+
integrationIds: options.integrationIds
|
|
465
|
+
}
|
|
466
|
+
})
|
|
467
|
+
);
|
|
468
|
+
});
|
|
469
|
+
addConnectionOptions(program.command("analytics:health")).description("Fetch analytics health for an integration.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--integration <id>", "Integration ID.").action(async (options, command) => {
|
|
470
|
+
const client = createApiClient(resolveConfig(command));
|
|
471
|
+
writeRawJson(
|
|
472
|
+
await client.request("GET", `/v1/workspaces/${options.workspace}/analytics/health`, {
|
|
473
|
+
query: { integrationId: options.integration }
|
|
474
|
+
})
|
|
475
|
+
);
|
|
476
|
+
});
|
|
477
|
+
addConnectionOptions(program.command("analytics:posts")).description("List post analytics for an integration.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--integration <id>", "Integration ID.").option("--limit <number>", "Page size.").option("--offset <number>", "Page offset.").action(async (options, command) => {
|
|
478
|
+
const client = createApiClient(resolveConfig(command));
|
|
479
|
+
writeRawJson(
|
|
480
|
+
await client.request("GET", `/v1/workspaces/${options.workspace}/analytics/posts`, {
|
|
481
|
+
query: {
|
|
482
|
+
integrationId: options.integration,
|
|
483
|
+
limit: options.limit,
|
|
484
|
+
offset: options.offset
|
|
485
|
+
}
|
|
486
|
+
})
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
addConnectionOptions(program.command("analytics:posts-aggregate")).description("List aggregated post analytics for a workspace.").requiredOption("-w, --workspace <id>", "Workspace ID.").option("--days <range>", "Analytics lookback window: 1-90 or all.", "30").option("--integration-ids <ids>", "Comma-separated integration IDs to include.").option("--limit <number>", "Page size.").option("--offset <number>", "Page offset.").option("--sort-by <field>", "Sort field: recent, engagement, or views.").action(async (options, command) => {
|
|
490
|
+
const client = createApiClient(resolveConfig(command));
|
|
491
|
+
writeRawJson(
|
|
492
|
+
await client.request("GET", `/v1/workspaces/${options.workspace}/analytics/posts/aggregate`, {
|
|
493
|
+
query: {
|
|
494
|
+
days: options.days,
|
|
495
|
+
integrationIds: options.integrationIds,
|
|
496
|
+
limit: options.limit,
|
|
497
|
+
offset: options.offset,
|
|
498
|
+
sortBy: options.sortBy
|
|
499
|
+
}
|
|
500
|
+
})
|
|
501
|
+
);
|
|
502
|
+
});
|
|
503
|
+
addConnectionOptions(program.command("analytics:post")).description("Fetch analytics for a single published content item.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <postId>", "Published content ID.").action(async (options, command) => {
|
|
504
|
+
const client = createApiClient(resolveConfig(command));
|
|
505
|
+
writeRawJson(await client.request("GET", `/v1/workspaces/${options.workspace}/analytics/posts/${options.id}`));
|
|
506
|
+
});
|
|
507
|
+
addConnectionOptions(program.command("analytics:demographics")).description("Fetch audience demographics for an integration.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--integration <id>", "Integration ID.").action(async (options, command) => {
|
|
508
|
+
const client = createApiClient(resolveConfig(command));
|
|
509
|
+
writeRawJson(
|
|
510
|
+
await client.request("GET", `/v1/workspaces/${options.workspace}/analytics/demographics`, {
|
|
511
|
+
query: { integrationId: options.integration }
|
|
512
|
+
})
|
|
513
|
+
);
|
|
514
|
+
});
|
|
515
|
+
addConnectionOptions(program.command("analytics:report")).description("Generate an agency-plan analytics PDF report.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--provider <provider>", "Report provider: instagram, facebook, youtube, tiktok, pinterest, or threads.").requiredOption("--json <file>", "JSON file containing report integration IDs and report options.").requiredOption("--output <file>", "PDF output file.").option("--yes", "Confirm report generation.").action(
|
|
516
|
+
async (options, command) => {
|
|
517
|
+
if (!options.yes) {
|
|
518
|
+
throw new CliError("analytics:report requires --yes because PDF generation can be expensive.", {
|
|
519
|
+
code: "INVALID_OPTIONS"
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
const payload = analyticsReportSchema.parse(await readJsonFile(options.json));
|
|
523
|
+
const client = createApiClient(resolveConfig(command));
|
|
524
|
+
const report = await client.download(
|
|
525
|
+
"POST",
|
|
526
|
+
`/v1/workspaces/${options.workspace}/analytics/report/${options.provider}`,
|
|
527
|
+
{ body: payload }
|
|
528
|
+
);
|
|
529
|
+
await writeFile(options.output, report.body);
|
|
530
|
+
writeJson({
|
|
531
|
+
output: options.output,
|
|
532
|
+
bytes: report.body.length,
|
|
533
|
+
contentType: report.contentType,
|
|
534
|
+
filename: report.filename ?? null
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/commands/auth.ts
|
|
541
|
+
import { spawn } from "child_process";
|
|
542
|
+
function sleep(ms) {
|
|
543
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
544
|
+
}
|
|
545
|
+
async function parseJsonResponse(response) {
|
|
546
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
547
|
+
if (!contentType.includes("application/json")) return response.text();
|
|
548
|
+
return response.json();
|
|
549
|
+
}
|
|
550
|
+
async function requestJson(apiUrl, path, body, bearerToken) {
|
|
551
|
+
const response = await fetch(new URL(path, `${apiUrl}/`), {
|
|
552
|
+
method: "POST",
|
|
553
|
+
headers: {
|
|
554
|
+
Accept: "application/json",
|
|
555
|
+
"Content-Type": "application/json",
|
|
556
|
+
...bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}
|
|
557
|
+
},
|
|
558
|
+
body: JSON.stringify(body)
|
|
559
|
+
});
|
|
560
|
+
const payload = await parseJsonResponse(response);
|
|
561
|
+
return { payload, response };
|
|
562
|
+
}
|
|
563
|
+
function deviceFlowError(payload) {
|
|
564
|
+
if (!payload || typeof payload !== "object") return {};
|
|
565
|
+
const source = payload;
|
|
566
|
+
if (source.error && typeof source.error === "object") return source.error;
|
|
567
|
+
return source;
|
|
568
|
+
}
|
|
569
|
+
function openBrowser(url) {
|
|
570
|
+
const command = process.platform === "win32" ? { file: process.env.ComSpec ?? "cmd.exe", args: ["/c", "start", '""', url] } : process.platform === "darwin" ? { file: "open", args: [url] } : { file: "xdg-open", args: [url] };
|
|
571
|
+
const child = spawn(command.file, command.args, {
|
|
572
|
+
detached: true,
|
|
573
|
+
stdio: "ignore",
|
|
574
|
+
windowsHide: true
|
|
575
|
+
});
|
|
576
|
+
child.unref();
|
|
577
|
+
}
|
|
578
|
+
async function login(command) {
|
|
579
|
+
const config = resolveConfig(command);
|
|
580
|
+
const { payload: device, response } = await requestJson(
|
|
581
|
+
config.apiUrl,
|
|
582
|
+
"/v1/cli-auth/device",
|
|
583
|
+
{
|
|
584
|
+
client_id: "maeve-cli"
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
if (!response.ok) {
|
|
588
|
+
const error = deviceFlowError(device);
|
|
589
|
+
throw new CliError(error.error_description || error.message || "Could not start CLI login.", {
|
|
590
|
+
code: error.error?.toUpperCase() || "CLI_LOGIN_FAILED",
|
|
591
|
+
status: response.status,
|
|
592
|
+
details: error
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
const options = command.opts();
|
|
596
|
+
process.stderr.write(`Open this URL to approve CLI login:
|
|
597
|
+
${device.verification_uri_complete}
|
|
598
|
+
|
|
599
|
+
`);
|
|
600
|
+
process.stderr.write(`User code: ${device.user_code}
|
|
601
|
+
`);
|
|
602
|
+
if (options.browser !== false) {
|
|
603
|
+
try {
|
|
604
|
+
openBrowser(device.verification_uri_complete);
|
|
605
|
+
} catch {
|
|
606
|
+
process.stderr.write(`Could not open a browser automatically. Use the URL above.
|
|
607
|
+
`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
let intervalSeconds = Math.max(0, Number(device.interval ?? 5));
|
|
611
|
+
const deadline = Date.now() + Number(device.expires_in ?? 600) * 1e3;
|
|
612
|
+
while (Date.now() < deadline) {
|
|
613
|
+
await sleep(intervalSeconds * 1e3);
|
|
614
|
+
const { payload, response: tokenResponse } = await requestJson(
|
|
615
|
+
config.apiUrl,
|
|
616
|
+
"/v1/cli-auth/device/token",
|
|
617
|
+
{
|
|
618
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
619
|
+
device_code: device.device_code,
|
|
620
|
+
client_id: "maeve-cli"
|
|
621
|
+
}
|
|
622
|
+
);
|
|
623
|
+
if (tokenResponse.ok) {
|
|
624
|
+
const token = payload;
|
|
625
|
+
const expiresAt = new Date(Date.now() + token.expires_in * 1e3).toISOString();
|
|
626
|
+
writeStoredCredential(config.apiUrl, {
|
|
627
|
+
authType: "cli_login",
|
|
628
|
+
token: token.access_token,
|
|
629
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
630
|
+
expiresAt
|
|
631
|
+
});
|
|
632
|
+
writeJson({
|
|
633
|
+
apiUrl: config.apiUrl,
|
|
634
|
+
authType: "cli_login",
|
|
635
|
+
tokenType: token.token_type,
|
|
636
|
+
expiresAt
|
|
637
|
+
});
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const error = deviceFlowError(payload);
|
|
641
|
+
if (error.error === "authorization_pending") {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (error.error === "slow_down") {
|
|
645
|
+
const retryAfter = Number(tokenResponse.headers.get("retry-after"));
|
|
646
|
+
intervalSeconds = Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter : intervalSeconds + 5;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (error.error === "access_denied") {
|
|
650
|
+
throw new CliError("CLI login was denied.", { code: "CLI_LOGIN_DENIED", status: tokenResponse.status });
|
|
651
|
+
}
|
|
652
|
+
if (error.error === "expired_token") {
|
|
653
|
+
throw new CliError("CLI login expired before it was approved.", {
|
|
654
|
+
code: "CLI_LOGIN_EXPIRED",
|
|
655
|
+
status: tokenResponse.status
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
throw new CliError(error.error_description || error.message || "CLI login failed.", {
|
|
659
|
+
code: error.error?.toUpperCase() || "CLI_LOGIN_FAILED",
|
|
660
|
+
status: tokenResponse.status,
|
|
661
|
+
details: error
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
throw new CliError("CLI login expired before it was approved.", { code: "CLI_LOGIN_EXPIRED" });
|
|
665
|
+
}
|
|
666
|
+
async function logout(command) {
|
|
667
|
+
const config = resolveConfig(command);
|
|
668
|
+
const filePath = getCredentialFilePath(process.env);
|
|
669
|
+
const storedCredential = getStoredCredential(config.apiUrl, filePath);
|
|
670
|
+
let serverRevoked = false;
|
|
671
|
+
if (storedCredential?.token) {
|
|
672
|
+
try {
|
|
673
|
+
const { response } = await requestJson(config.apiUrl, "/v1/cli-auth/logout", {}, storedCredential.token);
|
|
674
|
+
serverRevoked = response.ok || response.status === 401 || response.status === 403;
|
|
675
|
+
} catch {
|
|
676
|
+
serverRevoked = false;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const removed = deleteStoredCredential(config.apiUrl, filePath);
|
|
680
|
+
writeJson({
|
|
681
|
+
apiUrl: config.apiUrl,
|
|
682
|
+
removed,
|
|
683
|
+
serverRevoked
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
function registerAuthCommands(program) {
|
|
687
|
+
addConnectionOptions(program.command("auth:login")).description("Sign in locally with a browser-based CLI login.").option("--no-browser", "Print the verification URL without opening a browser.").action(async (_options, command) => {
|
|
688
|
+
await login(command);
|
|
689
|
+
});
|
|
690
|
+
addConnectionOptions(program.command("auth:logout")).description("Remove the stored CLI login token and revoke it server-side when possible.").action(async (_options, command) => {
|
|
691
|
+
await logout(command);
|
|
692
|
+
});
|
|
693
|
+
addConnectionOptions(program.command("auth:status")).description("Show non-secret CLI authentication status.").action((_options, command) => {
|
|
694
|
+
const config = resolveConfig(command);
|
|
695
|
+
writeJson({
|
|
696
|
+
apiUrl: config.apiUrl,
|
|
697
|
+
apiUrlSource: config.apiUrlSource,
|
|
698
|
+
flagApiKeyConfigured: config.apiKeySource === "flag",
|
|
699
|
+
envApiKeyConfigured: config.apiKeySource === "env",
|
|
700
|
+
namedEnvApiKeyConfigured: Boolean(config.namedEnvApiKeyConfigured),
|
|
701
|
+
apiKeyEnvName: config.apiKeyEnvName ?? null,
|
|
702
|
+
apiKeyConfigured: Boolean(config.apiKey),
|
|
703
|
+
apiKeySource: config.apiKeySource ?? null,
|
|
704
|
+
storedCliLoginConfigured: Boolean(config.storedCliLoginConfigured),
|
|
705
|
+
storedCliLoginExpired: Boolean(config.storedCliLoginExpired),
|
|
706
|
+
activeCredentialSource: config.credentialSource ?? null,
|
|
707
|
+
activeCredentialType: config.credentialType ?? null,
|
|
708
|
+
credentialFile: getCredentialFilePath(process.env)
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
addConnectionOptions(program.command("auth:whoami")).description("Show the current authenticated user and accessible workspaces.").action(async (_options, command) => {
|
|
712
|
+
const client = createApiClient(resolveConfig(command));
|
|
713
|
+
writeRawJson(await client.request("GET", "/v1/cli-auth/me"));
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// src/approval-schema.ts
|
|
718
|
+
import { z as z2 } from "zod";
|
|
719
|
+
var uuid2 = z2.string().uuid();
|
|
720
|
+
var nonEmptyString = z2.string().min(1);
|
|
721
|
+
var nullableString = z2.string().nullable().optional();
|
|
722
|
+
var approvalPolicy = z2.enum(["any", "all"]);
|
|
723
|
+
var workflowMode = z2.enum(["client", "internal_client"]);
|
|
724
|
+
var attachmentContentType = z2.enum(["image/jpeg", "image/png", "image/gif", "image/webp"]);
|
|
725
|
+
var reviewLifecyclePhase = z2.enum(["none", "internal", "client"]);
|
|
726
|
+
var reviewLifecycleState = z2.enum([
|
|
727
|
+
"none",
|
|
728
|
+
"internal_review_pending",
|
|
729
|
+
"internal_review_approved",
|
|
730
|
+
"internal_changes_requested",
|
|
731
|
+
"internal_review_withdrawn",
|
|
732
|
+
"waiting_internal_before_client",
|
|
733
|
+
"ready_to_send_to_client",
|
|
734
|
+
"client_review_active",
|
|
735
|
+
"client_review_outdated",
|
|
736
|
+
"client_changes_requested",
|
|
737
|
+
"client_review_approved",
|
|
738
|
+
"client_review_admin_overridden",
|
|
739
|
+
"client_review_cancelled",
|
|
740
|
+
"client_review_expired"
|
|
741
|
+
]);
|
|
742
|
+
var reviewLifecycleActor = z2.enum(["none", "agency", "author", "approver", "client", "system"]);
|
|
743
|
+
var reviewLifecycleNextAction = z2.enum([
|
|
744
|
+
"none",
|
|
745
|
+
"request_internal_approval",
|
|
746
|
+
"complete_internal_review",
|
|
747
|
+
"address_internal_changes",
|
|
748
|
+
"send_to_client",
|
|
749
|
+
"notify_client",
|
|
750
|
+
"wait_for_client",
|
|
751
|
+
"address_client_changes",
|
|
752
|
+
"refresh_client_share",
|
|
753
|
+
"schedule",
|
|
754
|
+
"publish",
|
|
755
|
+
"set_intended_publish_time"
|
|
756
|
+
]);
|
|
757
|
+
var reviewLifecycleBlockerCode = z2.enum([
|
|
758
|
+
"missing_schedule",
|
|
759
|
+
"internal_approval_pending",
|
|
760
|
+
"internal_changes_requested",
|
|
761
|
+
"client_review_not_sent",
|
|
762
|
+
"client_review_pending",
|
|
763
|
+
"client_review_outdated",
|
|
764
|
+
"client_changes_requested",
|
|
765
|
+
"client_review_cancelled",
|
|
766
|
+
"client_review_expired",
|
|
767
|
+
"missing_review_snapshot",
|
|
768
|
+
"review_snapshot_hash_mismatch"
|
|
769
|
+
]);
|
|
770
|
+
var reviewLifecycleSharedVersionStatus = z2.enum([
|
|
771
|
+
"not_shared",
|
|
772
|
+
"current",
|
|
773
|
+
"stale",
|
|
774
|
+
"missing_snapshot",
|
|
775
|
+
"hash_mismatch"
|
|
776
|
+
]);
|
|
777
|
+
var clientReviewBatchStatus = z2.enum(["open", "waiting_internal", "pending", "completed", "expired", "cancelled"]);
|
|
778
|
+
var clientReviewItemStatus = z2.enum(["pending", "approved", "changes_requested", "admin_overridden", "cancelled"]);
|
|
779
|
+
var approvalRequestSchema = z2.object({
|
|
780
|
+
approverIds: z2.array(uuid2).min(1),
|
|
781
|
+
policy: approvalPolicy
|
|
782
|
+
});
|
|
783
|
+
var approvalDecisionSchema = z2.object({
|
|
784
|
+
decision: z2.enum(["approve", "reject"]),
|
|
785
|
+
override: z2.boolean().optional(),
|
|
786
|
+
reason: z2.string().max(2e3).optional()
|
|
787
|
+
}).refine((payload) => !(payload.decision === "reject" || payload.override === true) || Boolean(payload.reason?.trim()), {
|
|
788
|
+
message: "reason is required when rejecting or using override."
|
|
789
|
+
});
|
|
790
|
+
var approvalCommentSchema = z2.object({
|
|
791
|
+
body: z2.string().max(2e3).optional(),
|
|
792
|
+
attachmentId: uuid2.optional()
|
|
793
|
+
}).refine((payload) => Boolean(payload.body?.trim() || payload.attachmentId), {
|
|
794
|
+
message: "Comment payload must include body or attachmentId."
|
|
795
|
+
});
|
|
796
|
+
var approvalAttachmentInitSchema = z2.object({
|
|
797
|
+
contentId: uuid2,
|
|
798
|
+
batchId: uuid2.optional(),
|
|
799
|
+
audience: z2.enum(["internal", "client"]),
|
|
800
|
+
filename: nonEmptyString.max(255),
|
|
801
|
+
contentType: attachmentContentType,
|
|
802
|
+
fileSize: z2.number().int().min(1)
|
|
803
|
+
});
|
|
804
|
+
var approvalReactionSchema = z2.object({
|
|
805
|
+
emoji: nonEmptyString.max(16)
|
|
806
|
+
});
|
|
807
|
+
var clientReviewerSchema = z2.object({
|
|
808
|
+
name: nonEmptyString.max(255),
|
|
809
|
+
email: z2.string().email().max(320)
|
|
810
|
+
});
|
|
811
|
+
var clientReviewConfigSchema = z2.object({
|
|
812
|
+
reviewers: z2.array(clientReviewerSchema).min(1),
|
|
813
|
+
policy: approvalPolicy,
|
|
814
|
+
expiresAt: nullableString,
|
|
815
|
+
inviteNote: nullableString.refine((value) => value === void 0 || value === null || value.length <= 2e3, {
|
|
816
|
+
message: "inviteNote must be 2000 characters or fewer."
|
|
817
|
+
}),
|
|
818
|
+
batchLabel: nullableString.refine((value) => value === void 0 || value === null || value.length <= 255, {
|
|
819
|
+
message: "batchLabel must be 255 characters or fewer."
|
|
820
|
+
})
|
|
821
|
+
});
|
|
822
|
+
var internalReviewConfigSchema = z2.object({
|
|
823
|
+
approverIds: z2.array(uuid2).min(1),
|
|
824
|
+
policy: approvalPolicy
|
|
825
|
+
});
|
|
826
|
+
var clientReviewCreateSchema = z2.object({
|
|
827
|
+
contentIds: z2.array(uuid2).min(1),
|
|
828
|
+
workflowMode,
|
|
829
|
+
client: clientReviewConfigSchema,
|
|
830
|
+
internal: internalReviewConfigSchema.optional()
|
|
831
|
+
}).refine((payload) => payload.workflowMode !== "internal_client" || Boolean(payload.internal), {
|
|
832
|
+
message: "internal is required when workflowMode is internal_client."
|
|
833
|
+
});
|
|
834
|
+
var clientReviewPostMutationSchema = z2.object({
|
|
835
|
+
contentId: uuid2
|
|
836
|
+
});
|
|
837
|
+
var clientReviewOverrideSchema = z2.object({
|
|
838
|
+
contentId: uuid2,
|
|
839
|
+
reason: nonEmptyString.max(2e3)
|
|
840
|
+
});
|
|
841
|
+
var clientReviewParticipantUpdateSchema = z2.object({
|
|
842
|
+
participantId: uuid2,
|
|
843
|
+
email: z2.string().email().max(320),
|
|
844
|
+
name: z2.string().max(255).optional()
|
|
845
|
+
});
|
|
846
|
+
var clientReviewCommentSchema = z2.object({
|
|
847
|
+
contentId: uuid2,
|
|
848
|
+
body: z2.string().max(2e3).optional(),
|
|
849
|
+
attachmentId: uuid2.optional()
|
|
850
|
+
}).refine((payload) => Boolean(payload.body?.trim() || payload.attachmentId), {
|
|
851
|
+
message: "Comment payload must include body or attachmentId."
|
|
852
|
+
});
|
|
853
|
+
var reviewLifecycleSchema = z2.object({
|
|
854
|
+
phase: reviewLifecyclePhase,
|
|
855
|
+
state: reviewLifecycleState,
|
|
856
|
+
actor: reviewLifecycleActor,
|
|
857
|
+
nextAction: reviewLifecycleNextAction,
|
|
858
|
+
blocksPublishing: z2.boolean(),
|
|
859
|
+
blockers: z2.array(
|
|
860
|
+
z2.object({
|
|
861
|
+
code: reviewLifecycleBlockerCode,
|
|
862
|
+
message: z2.string().optional(),
|
|
863
|
+
details: z2.record(z2.string(), z2.unknown()).optional()
|
|
864
|
+
}).strict()
|
|
865
|
+
),
|
|
866
|
+
progress: z2.object({
|
|
867
|
+
policy: approvalPolicy.nullable(),
|
|
868
|
+
requestedCount: z2.number().int().min(0),
|
|
869
|
+
approvedCount: z2.number().int().min(0),
|
|
870
|
+
changesRequestedCount: z2.number().int().min(0).optional()
|
|
871
|
+
}).strict().nullable().optional(),
|
|
872
|
+
clientReview: z2.object({
|
|
873
|
+
batchId: uuid2.nullable(),
|
|
874
|
+
itemId: uuid2.nullable(),
|
|
875
|
+
batchStatus: clientReviewBatchStatus.nullable(),
|
|
876
|
+
itemStatus: clientReviewItemStatus.nullable(),
|
|
877
|
+
isClientFacing: z2.boolean(),
|
|
878
|
+
sharedVersionStatus: reviewLifecycleSharedVersionStatus,
|
|
879
|
+
hasSentLinkBefore: z2.boolean().optional(),
|
|
880
|
+
readyForClientSend: z2.boolean().optional()
|
|
881
|
+
}).strict().nullable().optional(),
|
|
882
|
+
labels: z2.object({
|
|
883
|
+
phase: z2.string().optional(),
|
|
884
|
+
state: z2.string().optional(),
|
|
885
|
+
nextAction: z2.string().optional()
|
|
886
|
+
}).strict().optional()
|
|
887
|
+
}).strict();
|
|
888
|
+
var reviewLifecycleResponseSchema = z2.object({
|
|
889
|
+
reviewLifecycle: reviewLifecycleSchema.nullable().optional()
|
|
890
|
+
}).passthrough();
|
|
891
|
+
|
|
892
|
+
// src/commands/client-reviews.ts
|
|
893
|
+
function registerClientReviewCommands(program) {
|
|
894
|
+
addConnectionOptions(program.command("client-reviews:create")).description("Create a client review batch. Requires agency plan and client review send capability.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--json <file>", "JSON file containing the client review batch payload.").action(async (options, command) => {
|
|
895
|
+
const payload = clientReviewCreateSchema.parse(await readJsonFile(options.json));
|
|
896
|
+
const client = createApiClient(resolveConfig(command));
|
|
897
|
+
writeRawJson(
|
|
898
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/content/client-review-batches`, {
|
|
899
|
+
body: payload
|
|
900
|
+
})
|
|
901
|
+
);
|
|
902
|
+
});
|
|
903
|
+
addConnectionOptions(program.command("client-reviews:open")).description(
|
|
904
|
+
"List open client review batches that have not had links sent. Requires agency plan and client review read capability."
|
|
905
|
+
).requiredOption("-w, --workspace <id>", "Workspace ID.").option("--workflow-mode <mode>", "Workflow mode: client or internal_client.").action(async (options, command) => {
|
|
906
|
+
const client = createApiClient(resolveConfig(command));
|
|
907
|
+
writeRawJson(
|
|
908
|
+
await client.request("GET", `/v1/workspaces/${options.workspace}/content/client-review-batches/open`, {
|
|
909
|
+
query: { workflowMode: options.workflowMode }
|
|
910
|
+
})
|
|
911
|
+
);
|
|
912
|
+
});
|
|
913
|
+
addConnectionOptions(program.command("client-reviews:get")).description("Get one client review batch. Requires agency plan.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--batch <batchId>", "Client review batch ID.").action(async (options, command) => {
|
|
914
|
+
const client = createApiClient(resolveConfig(command));
|
|
915
|
+
writeRawJson(
|
|
916
|
+
await client.request(
|
|
917
|
+
"GET",
|
|
918
|
+
`/v1/workspaces/${options.workspace}/content/client-review-batches/${options.batch}`
|
|
919
|
+
)
|
|
920
|
+
);
|
|
921
|
+
});
|
|
922
|
+
addConnectionOptions(program.command("client-reviews:add-post")).description("Add content to an open client review batch. Requires agency plan and client review send capability.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--batch <batchId>", "Client review batch ID.").requiredOption("--json <file>", "JSON file containing contentId.").action(async (options, command) => {
|
|
923
|
+
const payload = clientReviewPostMutationSchema.parse(await readJsonFile(options.json));
|
|
924
|
+
const client = createApiClient(resolveConfig(command));
|
|
925
|
+
writeRawJson(
|
|
926
|
+
await client.request(
|
|
927
|
+
"POST",
|
|
928
|
+
`/v1/workspaces/${options.workspace}/content/client-review-batches/${options.batch}/add-post`,
|
|
929
|
+
{ body: payload }
|
|
930
|
+
)
|
|
931
|
+
);
|
|
932
|
+
});
|
|
933
|
+
addConnectionOptions(program.command("client-reviews:remove-post")).description(
|
|
934
|
+
"Remove content from an open client review batch. Requires agency plan and client review send capability."
|
|
935
|
+
).requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--batch <batchId>", "Client review batch ID.").requiredOption("--json <file>", "JSON file containing contentId.").action(async (options, command) => {
|
|
936
|
+
const payload = clientReviewPostMutationSchema.parse(await readJsonFile(options.json));
|
|
937
|
+
const client = createApiClient(resolveConfig(command));
|
|
938
|
+
writeRawJson(
|
|
939
|
+
await client.request(
|
|
940
|
+
"POST",
|
|
941
|
+
`/v1/workspaces/${options.workspace}/content/client-review-batches/${options.batch}/remove-post`,
|
|
942
|
+
{ body: payload }
|
|
943
|
+
)
|
|
944
|
+
);
|
|
945
|
+
});
|
|
946
|
+
addConnectionOptions(program.command("client-reviews:send")).description(
|
|
947
|
+
"Send a client review batch. Requires agency plan and client review send capability; may notify reviewers."
|
|
948
|
+
).requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--batch <batchId>", "Client review batch ID.").option("--yes", "Confirm reviewer notifications.").action(async (options, command) => {
|
|
949
|
+
if (!options.yes) {
|
|
950
|
+
throw new CliError("client-reviews:send requires --yes because it may notify reviewers.", {
|
|
951
|
+
code: "INVALID_OPTIONS"
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
const client = createApiClient(resolveConfig(command));
|
|
955
|
+
writeRawJson(
|
|
956
|
+
await client.request(
|
|
957
|
+
"POST",
|
|
958
|
+
`/v1/workspaces/${options.workspace}/content/client-review-batches/${options.batch}/send`
|
|
959
|
+
)
|
|
960
|
+
);
|
|
961
|
+
});
|
|
962
|
+
addConnectionOptions(program.command("client-reviews:resend")).description(
|
|
963
|
+
"Resend a client review batch. Requires agency plan and client review send capability; may notify reviewers."
|
|
964
|
+
).requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--batch <batchId>", "Client review batch ID.").option("--yes", "Confirm reviewer notifications.").action(async (options, command) => {
|
|
965
|
+
if (!options.yes) {
|
|
966
|
+
throw new CliError("client-reviews:resend requires --yes because it may notify reviewers.", {
|
|
967
|
+
code: "INVALID_OPTIONS"
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
const client = createApiClient(resolveConfig(command));
|
|
971
|
+
writeRawJson(
|
|
972
|
+
await client.request(
|
|
973
|
+
"POST",
|
|
974
|
+
`/v1/workspaces/${options.workspace}/content/client-review-batches/${options.batch}/resend`
|
|
975
|
+
)
|
|
976
|
+
);
|
|
977
|
+
});
|
|
978
|
+
addConnectionOptions(program.command("client-reviews:cancel")).description("Cancel a client review batch. Requires agency plan and client review send capability.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--batch <batchId>", "Client review batch ID.").action(async (options, command) => {
|
|
979
|
+
const client = createApiClient(resolveConfig(command));
|
|
980
|
+
writeRawJson(
|
|
981
|
+
await client.request(
|
|
982
|
+
"POST",
|
|
983
|
+
`/v1/workspaces/${options.workspace}/content/client-review-batches/${options.batch}/cancel`
|
|
984
|
+
)
|
|
985
|
+
);
|
|
986
|
+
});
|
|
987
|
+
addConnectionOptions(program.command("client-reviews:override")).description("Override a client review batch item. Requires agency plan and content review override capability.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--batch <batchId>", "Client review batch ID.").requiredOption("--json <file>", "JSON file containing contentId and reason.").action(async (options, command) => {
|
|
988
|
+
const payload = clientReviewOverrideSchema.parse(await readJsonFile(options.json));
|
|
989
|
+
const client = createApiClient(resolveConfig(command));
|
|
990
|
+
writeRawJson(
|
|
991
|
+
await client.request(
|
|
992
|
+
"POST",
|
|
993
|
+
`/v1/workspaces/${options.workspace}/content/client-review-batches/${options.batch}/override`,
|
|
994
|
+
{ body: payload }
|
|
995
|
+
)
|
|
996
|
+
);
|
|
997
|
+
});
|
|
998
|
+
addConnectionOptions(program.command("client-reviews:update-participant")).description("Update a client review participant. Requires agency plan and client review send capability.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--batch <batchId>", "Client review batch ID.").requiredOption("--json <file>", "JSON file containing participantId and email.").action(async (options, command) => {
|
|
999
|
+
const payload = clientReviewParticipantUpdateSchema.parse(await readJsonFile(options.json));
|
|
1000
|
+
const client = createApiClient(resolveConfig(command));
|
|
1001
|
+
writeRawJson(
|
|
1002
|
+
await client.request(
|
|
1003
|
+
"POST",
|
|
1004
|
+
`/v1/workspaces/${options.workspace}/content/client-review-batches/${options.batch}/update-participant`,
|
|
1005
|
+
{ body: payload }
|
|
1006
|
+
)
|
|
1007
|
+
);
|
|
1008
|
+
});
|
|
1009
|
+
addConnectionOptions(program.command("client-reviews:comment")).description(
|
|
1010
|
+
"Add an internal client-review thread comment. Requires agency plan and client review comment capability."
|
|
1011
|
+
).requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--batch <batchId>", "Client review batch ID.").requiredOption("--json <file>", "JSON file containing contentId and body or attachmentId.").action(async (options, command) => {
|
|
1012
|
+
const payload = clientReviewCommentSchema.parse(await readJsonFile(options.json));
|
|
1013
|
+
const client = createApiClient(resolveConfig(command));
|
|
1014
|
+
writeRawJson(
|
|
1015
|
+
await client.request(
|
|
1016
|
+
"POST",
|
|
1017
|
+
`/v1/workspaces/${options.workspace}/content/client-review-batches/${options.batch}/comment`,
|
|
1018
|
+
{ body: payload }
|
|
1019
|
+
)
|
|
1020
|
+
);
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// src/commands/content.ts
|
|
1025
|
+
import { randomUUID } from "crypto";
|
|
1026
|
+
|
|
1027
|
+
// src/content-schema.ts
|
|
1028
|
+
import { z as z3 } from "zod";
|
|
1029
|
+
var uuid3 = z3.string().uuid();
|
|
1030
|
+
var isoWithTimezone = z3.string().refine(
|
|
1031
|
+
(value) => {
|
|
1032
|
+
const date = new Date(value);
|
|
1033
|
+
return !Number.isNaN(date.getTime()) && (value.endsWith("Z") || /[+-]\d{2}:\d{2}$/.test(value));
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
message: "scheduledAt must be an ISO8601 date string with explicit timezone, e.g. 2026-05-01T10:00:00Z."
|
|
1037
|
+
}
|
|
1038
|
+
);
|
|
1039
|
+
var captionOverridesSchema = z3.object({
|
|
1040
|
+
x: z3.string().max(25e3).optional(),
|
|
1041
|
+
linkedin: z3.string().max(25e3).optional(),
|
|
1042
|
+
youtube: z3.string().max(25e3).optional(),
|
|
1043
|
+
instagram: z3.string().max(25e3).optional(),
|
|
1044
|
+
facebook: z3.string().max(25e3).optional(),
|
|
1045
|
+
threads: z3.string().max(25e3).optional(),
|
|
1046
|
+
tiktok: z3.string().max(25e3).optional(),
|
|
1047
|
+
pinterest: z3.string().max(25e3).optional()
|
|
1048
|
+
}).strict();
|
|
1049
|
+
var captionsSchema = z3.object({
|
|
1050
|
+
canonical: z3.string().max(25e3).nullable().optional(),
|
|
1051
|
+
overrides: captionOverridesSchema.optional()
|
|
1052
|
+
}).strict();
|
|
1053
|
+
var contentMediaCoverSchema = z3.object({
|
|
1054
|
+
coverMediaId: uuid3.optional(),
|
|
1055
|
+
thumbOffsetMs: z3.number().int().min(0).optional()
|
|
1056
|
+
}).strict().nullable();
|
|
1057
|
+
var contentMediaSchema = z3.array(
|
|
1058
|
+
z3.object({
|
|
1059
|
+
mediaId: uuid3,
|
|
1060
|
+
order: z3.number().int().min(0).optional(),
|
|
1061
|
+
crops: z3.record(z3.string(), z3.unknown()).optional(),
|
|
1062
|
+
userTags: z3.array(
|
|
1063
|
+
z3.object({
|
|
1064
|
+
username: z3.string().min(1),
|
|
1065
|
+
x: z3.number().min(0).max(1),
|
|
1066
|
+
y: z3.number().min(0).max(1)
|
|
1067
|
+
}).strict()
|
|
1068
|
+
).optional(),
|
|
1069
|
+
productTags: z3.array(
|
|
1070
|
+
z3.object({
|
|
1071
|
+
productId: z3.string().min(1),
|
|
1072
|
+
productName: z3.string().max(500).optional(),
|
|
1073
|
+
x: z3.number().min(0).max(1).optional(),
|
|
1074
|
+
y: z3.number().min(0).max(1).optional()
|
|
1075
|
+
}).strict()
|
|
1076
|
+
).optional(),
|
|
1077
|
+
cover: contentMediaCoverSchema.optional()
|
|
1078
|
+
}).strict()
|
|
1079
|
+
).max(10);
|
|
1080
|
+
var unsupportedSettingsKeys = [
|
|
1081
|
+
"contentTitle",
|
|
1082
|
+
"platformTitles",
|
|
1083
|
+
"selectedChannels",
|
|
1084
|
+
"platformMediaIds",
|
|
1085
|
+
"platformThreadMessages",
|
|
1086
|
+
"platformFirstComments",
|
|
1087
|
+
"reviewDraft",
|
|
1088
|
+
"instagramCoverUrl",
|
|
1089
|
+
"instagramCollaborators",
|
|
1090
|
+
"pinterestCoverUrl",
|
|
1091
|
+
"mediaCrops",
|
|
1092
|
+
"userTags",
|
|
1093
|
+
"productTags",
|
|
1094
|
+
"coverUrl",
|
|
1095
|
+
"thumbnailUrl",
|
|
1096
|
+
"facebookPostType",
|
|
1097
|
+
"youtubePostType",
|
|
1098
|
+
"youtubeThumbnailFileSize"
|
|
1099
|
+
];
|
|
1100
|
+
var facebookPagePostCtaTypes = [
|
|
1101
|
+
"LEARN_MORE",
|
|
1102
|
+
"SIGN_UP",
|
|
1103
|
+
"DOWNLOAD",
|
|
1104
|
+
"WATCH_MORE",
|
|
1105
|
+
"BOOK_TRAVEL",
|
|
1106
|
+
"LISTEN_MUSIC",
|
|
1107
|
+
"USE_APP",
|
|
1108
|
+
"INSTALL_APP",
|
|
1109
|
+
"NO_BUTTON"
|
|
1110
|
+
];
|
|
1111
|
+
var threadsReplyControlValues = [
|
|
1112
|
+
"everyone",
|
|
1113
|
+
"accounts_you_follow",
|
|
1114
|
+
"mentioned_only",
|
|
1115
|
+
"parent_post_author_only",
|
|
1116
|
+
"followers_only"
|
|
1117
|
+
];
|
|
1118
|
+
var settingsSchema = z3.object({
|
|
1119
|
+
collaborators: z3.array(z3.string()).optional(),
|
|
1120
|
+
locationId: z3.string().optional(),
|
|
1121
|
+
facebookLinkUrl: z3.string().optional(),
|
|
1122
|
+
facebookCtaType: z3.enum(facebookPagePostCtaTypes).optional(),
|
|
1123
|
+
facebookLocationId: z3.string().optional(),
|
|
1124
|
+
xCommunityUrl: z3.string().optional(),
|
|
1125
|
+
topicTag: z3.string().optional(),
|
|
1126
|
+
linkAttachment: z3.string().optional(),
|
|
1127
|
+
isGhostPost: z3.boolean().optional(),
|
|
1128
|
+
replyControl: z3.enum(threadsReplyControlValues).optional(),
|
|
1129
|
+
privacy_level: z3.string().optional(),
|
|
1130
|
+
disable_comment: z3.boolean().optional(),
|
|
1131
|
+
disable_duet: z3.boolean().optional(),
|
|
1132
|
+
disable_stitch: z3.boolean().optional(),
|
|
1133
|
+
brand_content_toggle: z3.boolean().optional(),
|
|
1134
|
+
brand_organic_toggle: z3.boolean().optional(),
|
|
1135
|
+
is_aigc: z3.boolean().optional(),
|
|
1136
|
+
tiktok_post_to_drafts: z3.boolean().optional(),
|
|
1137
|
+
photo_cover_index: z3.number().optional(),
|
|
1138
|
+
auto_add_music: z3.boolean().optional(),
|
|
1139
|
+
privacyStatus: z3.enum(["public", "unlisted", "private"]).optional(),
|
|
1140
|
+
categoryId: z3.string().optional(),
|
|
1141
|
+
tags: z3.array(z3.string()).optional(),
|
|
1142
|
+
madeForKids: z3.boolean().optional(),
|
|
1143
|
+
notifySubscribers: z3.boolean().optional(),
|
|
1144
|
+
embeddable: z3.boolean().optional(),
|
|
1145
|
+
license: z3.enum(["youtube", "creativeCommon"]).optional(),
|
|
1146
|
+
boardId: z3.string().optional(),
|
|
1147
|
+
link: z3.string().optional(),
|
|
1148
|
+
altText: z3.string().optional()
|
|
1149
|
+
}).strict().superRefine((settings, context) => {
|
|
1150
|
+
for (const key of unsupportedSettingsKeys) {
|
|
1151
|
+
if (Object.prototype.hasOwnProperty.call(settings, key)) {
|
|
1152
|
+
context.addIssue({
|
|
1153
|
+
code: "custom",
|
|
1154
|
+
path: [key],
|
|
1155
|
+
message: `settings.${key} is not a provider setting. Use canonical content fields instead.`
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}).optional();
|
|
1160
|
+
var createContentBaseSchema = z3.object({
|
|
1161
|
+
intent: z3.enum(["draft", "schedule", "publish_now"]).default("draft"),
|
|
1162
|
+
integrationId: uuid3,
|
|
1163
|
+
internalTitle: z3.string().max(500).nullable().optional(),
|
|
1164
|
+
publishTitle: z3.string().max(500).nullable().optional(),
|
|
1165
|
+
notes: z3.string().max(1e5).optional(),
|
|
1166
|
+
captions: captionsSchema.optional(),
|
|
1167
|
+
contentMedia: contentMediaSchema.optional(),
|
|
1168
|
+
scheduledAt: isoWithTimezone.optional(),
|
|
1169
|
+
settings: settingsSchema,
|
|
1170
|
+
postType: z3.enum(["post", "reel", "story", "thread"]).optional(),
|
|
1171
|
+
firstComment: z3.string().optional(),
|
|
1172
|
+
shareToFeed: z3.boolean().optional(),
|
|
1173
|
+
pillarIds: z3.array(uuid3).optional(),
|
|
1174
|
+
formatIds: z3.array(uuid3).optional(),
|
|
1175
|
+
labelIds: z3.array(uuid3).optional(),
|
|
1176
|
+
campaignId: uuid3.nullable().optional(),
|
|
1177
|
+
campaignPhaseId: uuid3.nullable().optional(),
|
|
1178
|
+
priority: z3.enum(["urgent", "high", "medium", "low"]).optional(),
|
|
1179
|
+
assigneeIds: z3.array(uuid3).optional(),
|
|
1180
|
+
threadMessages: z3.array(
|
|
1181
|
+
z3.object({
|
|
1182
|
+
captions: captionsSchema,
|
|
1183
|
+
contentMedia: contentMediaSchema.optional()
|
|
1184
|
+
}).strict()
|
|
1185
|
+
).max(20).optional()
|
|
1186
|
+
}).strict();
|
|
1187
|
+
var createContentSchema = createContentBaseSchema.superRefine((payload, ctx) => {
|
|
1188
|
+
if (payload.intent === "draft" && payload.scheduledAt !== void 0) {
|
|
1189
|
+
ctx.addIssue({
|
|
1190
|
+
code: "custom",
|
|
1191
|
+
path: ["scheduledAt"],
|
|
1192
|
+
message: "scheduledAt is only allowed when intent is schedule."
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
if (payload.intent === "schedule" && payload.scheduledAt === void 0) {
|
|
1196
|
+
ctx.addIssue({
|
|
1197
|
+
code: "custom",
|
|
1198
|
+
path: ["scheduledAt"],
|
|
1199
|
+
message: "scheduledAt is required when intent is schedule."
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
if (payload.intent === "publish_now" && payload.scheduledAt !== void 0) {
|
|
1203
|
+
ctx.addIssue({
|
|
1204
|
+
code: "custom",
|
|
1205
|
+
path: ["scheduledAt"],
|
|
1206
|
+
message: "scheduledAt is not allowed when intent is publish_now."
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
var updateContentSchema = createContentBaseSchema.omit({ intent: true }).partial().refine((payload) => Object.keys(payload).length > 0, {
|
|
1211
|
+
message: "Update payload must include at least one field."
|
|
1212
|
+
});
|
|
1213
|
+
var scheduleContentSchema = z3.object({
|
|
1214
|
+
scheduledAt: isoWithTimezone
|
|
1215
|
+
});
|
|
1216
|
+
var workflowStatusSchema = z3.object({
|
|
1217
|
+
workflowStatus: z3.enum(["idea", "drafting"])
|
|
1218
|
+
});
|
|
1219
|
+
var updateContentNotesSchema = z3.object({
|
|
1220
|
+
notes: z3.string().max(1e5)
|
|
1221
|
+
});
|
|
1222
|
+
var updatePublishedCaptionSchema = z3.object({
|
|
1223
|
+
message: z3.string().max(25e3)
|
|
1224
|
+
}).strict();
|
|
1225
|
+
|
|
1226
|
+
// src/commands/content.ts
|
|
1227
|
+
function registerContentCommands(program) {
|
|
1228
|
+
addConnectionOptions(program.command("content:create")).description("Create content from a JSON payload file.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--json <file>", "JSON file containing the content payload.").option("--idempotency-key <key>", "Idempotency key for safe create retries. Generated by default.").option("--yes", 'Confirm immediate publishing when the payload uses intent "publish_now".').action(
|
|
1229
|
+
async (options, command) => {
|
|
1230
|
+
const payload = createContentSchema.parse(await readJsonFile(options.json));
|
|
1231
|
+
if (payload.intent === "publish_now" && !options.yes) {
|
|
1232
|
+
throw new CliError(
|
|
1233
|
+
"content:create requires --yes when intent is publish_now because it queues an external publish.",
|
|
1234
|
+
{
|
|
1235
|
+
code: "INVALID_OPTIONS"
|
|
1236
|
+
}
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
const client = createApiClient(resolveConfig(command));
|
|
1240
|
+
const idempotencyKey = options.idempotencyKey?.trim() || `cli-content-create-${randomUUID()}`;
|
|
1241
|
+
writeRawJson(
|
|
1242
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/content`, {
|
|
1243
|
+
body: payload,
|
|
1244
|
+
headers: {
|
|
1245
|
+
"Idempotency-Key": idempotencyKey
|
|
1246
|
+
}
|
|
1247
|
+
})
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
);
|
|
1251
|
+
addConnectionOptions(program.command("content:list")).description("List content for a workspace.").requiredOption("-w, --workspace <id>", "Workspace ID.").option("--status <status>", "Content status: draft, scheduled, sent, or failed.").option("--workflow-status <status>", "Workflow status filter.").option("--workflow-statuses <statuses>", "Comma-separated workflow status filters.").option("--include-review-lifecycle", "Include reviewLifecycle on list items.").option("--review-lifecycle-phase <phase>", "Filter by reviewLifecycle.phase.").option("--review-lifecycle-state <state>", "Filter by reviewLifecycle.state.").option("--review-lifecycle-actor <actor>", "Filter by reviewLifecycle.actor.").option("--review-lifecycle-next-action <action>", "Filter by reviewLifecycle.nextAction.").option("--review-lifecycle-blocks-publishing <boolean>", "Filter by reviewLifecycle.blocksPublishing.").option("--review-lifecycle-blocker <code>", "Filter by reviewLifecycle blocker code.").option("--review-lifecycle-batch <batchId>", "Filter by reviewLifecycle.clientReview.batchId.").option("--review-lifecycle-item <itemId>", "Filter by reviewLifecycle.clientReview.itemId.").option("--integration <id>", "Integration ID filter.").option("--limit <number>", "Page size.").option("--offset <number>", "Page offset.").option("--sort-by <field>", "Sort field.").option("--sort-direction <direction>", "Sort direction.").action(async (options, command) => {
|
|
1252
|
+
const client = createApiClient(resolveConfig(command));
|
|
1253
|
+
writeRawJson(
|
|
1254
|
+
await client.request("GET", `/v1/workspaces/${options.workspace}/content`, {
|
|
1255
|
+
query: {
|
|
1256
|
+
status: options.status,
|
|
1257
|
+
workflowStatus: options.workflowStatus,
|
|
1258
|
+
workflowStatuses: options.workflowStatuses,
|
|
1259
|
+
includeReviewLifecycle: options.includeReviewLifecycle,
|
|
1260
|
+
reviewLifecyclePhase: options.reviewLifecyclePhase,
|
|
1261
|
+
reviewLifecycleState: options.reviewLifecycleState,
|
|
1262
|
+
reviewLifecycleActor: options.reviewLifecycleActor,
|
|
1263
|
+
reviewLifecycleNextAction: options.reviewLifecycleNextAction,
|
|
1264
|
+
reviewLifecycleBlocksPublishing: options.reviewLifecycleBlocksPublishing,
|
|
1265
|
+
reviewLifecycleBlocker: options.reviewLifecycleBlocker,
|
|
1266
|
+
reviewLifecycleBatchId: options.reviewLifecycleBatch,
|
|
1267
|
+
reviewLifecycleItemId: options.reviewLifecycleItem,
|
|
1268
|
+
integrationId: options.integration,
|
|
1269
|
+
limit: options.limit,
|
|
1270
|
+
offset: options.offset,
|
|
1271
|
+
sortBy: options.sortBy,
|
|
1272
|
+
sortDirection: options.sortDirection
|
|
1273
|
+
}
|
|
1274
|
+
})
|
|
1275
|
+
);
|
|
1276
|
+
});
|
|
1277
|
+
addConnectionOptions(program.command("content:update")).description("Update a draft or scheduled content item from a JSON payload file.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").requiredOption("--json <file>", "JSON file containing fields to update.").action(async (options, command) => {
|
|
1278
|
+
const payload = updateContentSchema.parse(await readJsonFile(options.json));
|
|
1279
|
+
const client = createApiClient(resolveConfig(command));
|
|
1280
|
+
writeRawJson(
|
|
1281
|
+
await client.request("PATCH", `/v1/workspaces/${options.workspace}/content/${options.id}`, {
|
|
1282
|
+
body: payload
|
|
1283
|
+
})
|
|
1284
|
+
);
|
|
1285
|
+
});
|
|
1286
|
+
addConnectionOptions(program.command("content:schedule")).description("Schedule or reschedule a content item.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").requiredOption("--scheduled-at <iso>", "ISO8601 timestamp with explicit timezone.").action(async (options, command) => {
|
|
1287
|
+
const payload = scheduleContentSchema.parse({ scheduledAt: options.scheduledAt });
|
|
1288
|
+
const client = createApiClient(resolveConfig(command));
|
|
1289
|
+
writeRawJson(
|
|
1290
|
+
await client.request("PATCH", `/v1/workspaces/${options.workspace}/content/${options.id}/schedule`, {
|
|
1291
|
+
body: payload
|
|
1292
|
+
})
|
|
1293
|
+
);
|
|
1294
|
+
});
|
|
1295
|
+
addConnectionOptions(program.command("content:intended-time")).description("Set intended publish time for draft or pending approval content without scheduling it.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").requiredOption("--scheduled-at <iso>", "ISO8601 timestamp with explicit timezone.").action(async (options, command) => {
|
|
1296
|
+
const payload = scheduleContentSchema.parse({ scheduledAt: options.scheduledAt });
|
|
1297
|
+
const client = createApiClient(resolveConfig(command));
|
|
1298
|
+
writeRawJson(
|
|
1299
|
+
await client.request(
|
|
1300
|
+
"PATCH",
|
|
1301
|
+
`/v1/workspaces/${options.workspace}/content/${options.id}/intended-publish-time`,
|
|
1302
|
+
{
|
|
1303
|
+
body: payload
|
|
1304
|
+
}
|
|
1305
|
+
)
|
|
1306
|
+
);
|
|
1307
|
+
});
|
|
1308
|
+
addConnectionOptions(program.command("content:notes")).description("Replace the rich-text notes attached to a content item. Notes are internal and never published.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").option("--notes <html>", "Notes content (rich-text HTML). Pass empty string to clear.").option("--json <file>", 'JSON file with shape { "notes": "..." }. Use this for multi-line content.').action(async (options, command) => {
|
|
1309
|
+
const raw = options.json ? await readJsonFile(options.json) : { notes: options.notes ?? "" };
|
|
1310
|
+
if (options.json && options.notes !== void 0) {
|
|
1311
|
+
throw new CliError("Pass either --notes or --json, not both.");
|
|
1312
|
+
}
|
|
1313
|
+
if (!options.json && options.notes === void 0) {
|
|
1314
|
+
throw new CliError("Must pass either --notes or --json.");
|
|
1315
|
+
}
|
|
1316
|
+
const payload = updateContentNotesSchema.parse(raw);
|
|
1317
|
+
const client = createApiClient(resolveConfig(command));
|
|
1318
|
+
writeRawJson(
|
|
1319
|
+
await client.request("PATCH", `/v1/workspaces/${options.workspace}/content/${options.id}/notes`, {
|
|
1320
|
+
body: payload
|
|
1321
|
+
})
|
|
1322
|
+
);
|
|
1323
|
+
});
|
|
1324
|
+
addConnectionOptions(program.command("content:workflow")).description("Update a content item workflow status. Allowed statuses: idea, drafting (In Progress).").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").requiredOption("--status <status>", "Workflow status: idea or drafting (In Progress).").action(async (options, command) => {
|
|
1325
|
+
const payload = workflowStatusSchema.parse({ workflowStatus: options.status });
|
|
1326
|
+
const client = createApiClient(resolveConfig(command));
|
|
1327
|
+
writeRawJson(
|
|
1328
|
+
await client.request("PATCH", `/v1/workspaces/${options.workspace}/content/${options.id}/workflow-status`, {
|
|
1329
|
+
body: payload
|
|
1330
|
+
})
|
|
1331
|
+
);
|
|
1332
|
+
});
|
|
1333
|
+
addConnectionOptions(program.command("content:get")).description("Get one content item.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").action(async (options, command) => {
|
|
1334
|
+
const client = createApiClient(resolveConfig(command));
|
|
1335
|
+
writeRawJson(await client.request("GET", `/v1/workspaces/${options.workspace}/content/${options.id}`));
|
|
1336
|
+
});
|
|
1337
|
+
addConnectionOptions(program.command("content:publish")).description("Publish a draft or scheduled content item now.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").option("--yes", "Confirm immediate publishing.").action(async (options, command) => {
|
|
1338
|
+
if (!options.yes) {
|
|
1339
|
+
throw new CliError("content:publish requires --yes because it queues an external publish.", {
|
|
1340
|
+
code: "INVALID_OPTIONS"
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
const client = createApiClient(resolveConfig(command));
|
|
1344
|
+
writeRawJson(await client.request("POST", `/v1/workspaces/${options.workspace}/content/${options.id}/publish`));
|
|
1345
|
+
});
|
|
1346
|
+
addConnectionOptions(program.command("content:published-caption")).description("Edit the caption on an already-published Facebook content item.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").requiredOption("--json <file>", 'JSON file containing { "message": "..." }.').option("--yes", "Confirm editing already-published provider content.").action(async (options, command) => {
|
|
1347
|
+
if (!options.yes) {
|
|
1348
|
+
throw new CliError(
|
|
1349
|
+
"content:published-caption requires --yes because it edits already-published provider content.",
|
|
1350
|
+
{
|
|
1351
|
+
code: "INVALID_OPTIONS"
|
|
1352
|
+
}
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
const payload = updatePublishedCaptionSchema.parse(await readJsonFile(options.json));
|
|
1356
|
+
const client = createApiClient(resolveConfig(command));
|
|
1357
|
+
writeRawJson(
|
|
1358
|
+
await client.request("PATCH", `/v1/workspaces/${options.workspace}/content/${options.id}/published-caption`, {
|
|
1359
|
+
body: payload
|
|
1360
|
+
})
|
|
1361
|
+
);
|
|
1362
|
+
});
|
|
1363
|
+
addConnectionOptions(program.command("content:archive")).description("Archive a published content item.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").action(async (options, command) => {
|
|
1364
|
+
const client = createApiClient(resolveConfig(command));
|
|
1365
|
+
writeRawJson(await client.request("POST", `/v1/workspaces/${options.workspace}/content/${options.id}/archive`));
|
|
1366
|
+
});
|
|
1367
|
+
addConnectionOptions(program.command("content:restore")).description("Restore an archived content item.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").action(async (options, command) => {
|
|
1368
|
+
const client = createApiClient(resolveConfig(command));
|
|
1369
|
+
writeRawJson(await client.request("POST", `/v1/workspaces/${options.workspace}/content/${options.id}/restore`));
|
|
1370
|
+
});
|
|
1371
|
+
addConnectionOptions(program.command("content:retry")).description("Retry a failed content item.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").action(async (options, command) => {
|
|
1372
|
+
const client = createApiClient(resolveConfig(command));
|
|
1373
|
+
writeRawJson(await client.request("POST", `/v1/workspaces/${options.workspace}/content/${options.id}/retry`));
|
|
1374
|
+
});
|
|
1375
|
+
addConnectionOptions(program.command("content:failed-count")).description("Get the failed content count for a workspace.").requiredOption("-w, --workspace <id>", "Workspace ID.").action(async (options, command) => {
|
|
1376
|
+
const client = createApiClient(resolveConfig(command));
|
|
1377
|
+
writeRawJson(await client.request("GET", `/v1/workspaces/${options.workspace}/content/failed-count`));
|
|
1378
|
+
});
|
|
1379
|
+
addConnectionOptions(program.command("content:pending-approval-count")).description("Get the pending approval count for a workspace. Requires agency plan.").requiredOption("-w, --workspace <id>", "Workspace ID.").action(async (options, command) => {
|
|
1380
|
+
const client = createApiClient(resolveConfig(command));
|
|
1381
|
+
writeRawJson(await client.request("GET", `/v1/workspaces/${options.workspace}/content/pending-approval-count`));
|
|
1382
|
+
});
|
|
1383
|
+
addConnectionOptions(program.command("content:approval-history")).description("List approval history for a workspace. Requires agency plan.").requiredOption("-w, --workspace <id>", "Workspace ID.").option("--limit <number>", "Page size.").option("--cursor <cursor>", "Pagination cursor.").option("--search <text>", "Search approval history.").option("--workflow-path <path>", "Workflow path: all, internal, client, or internal_client.").option("--outcome <outcome>", "Outcome filter.").option("--platform <platform>", "Platform filter.").option("--reviewer <text>", "Reviewer filter.").option("--date-from <iso>", "ISO8601 start date.").option("--date-to <iso>", "ISO8601 end date.").action(async (options, command) => {
|
|
1384
|
+
const client = createApiClient(resolveConfig(command));
|
|
1385
|
+
writeRawJson(
|
|
1386
|
+
await client.request("GET", `/v1/workspaces/${options.workspace}/content/approval-history`, {
|
|
1387
|
+
query: {
|
|
1388
|
+
limit: options.limit,
|
|
1389
|
+
cursor: options.cursor,
|
|
1390
|
+
search: options.search,
|
|
1391
|
+
workflowPath: options.workflowPath,
|
|
1392
|
+
outcome: options.outcome,
|
|
1393
|
+
platform: options.platform,
|
|
1394
|
+
reviewer: options.reviewer,
|
|
1395
|
+
dateFrom: options.dateFrom,
|
|
1396
|
+
dateTo: options.dateTo
|
|
1397
|
+
}
|
|
1398
|
+
})
|
|
1399
|
+
);
|
|
1400
|
+
});
|
|
1401
|
+
addConnectionOptions(program.command("content:approval-history:client-batch")).description("Get client batch approval history detail. Requires agency plan.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--batch <batchId>", "Client review batch ID.").action(async (options, command) => {
|
|
1402
|
+
const client = createApiClient(resolveConfig(command));
|
|
1403
|
+
writeRawJson(
|
|
1404
|
+
await client.request(
|
|
1405
|
+
"GET",
|
|
1406
|
+
`/v1/workspaces/${options.workspace}/content/approval-history/client-batches/${options.batch}`
|
|
1407
|
+
)
|
|
1408
|
+
);
|
|
1409
|
+
});
|
|
1410
|
+
addConnectionOptions(program.command("content:approval-history:internal")).description("Get internal approval history detail. Requires agency plan.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--record <recordId>", "Approval record ID.").action(async (options, command) => {
|
|
1411
|
+
const client = createApiClient(resolveConfig(command));
|
|
1412
|
+
writeRawJson(
|
|
1413
|
+
await client.request(
|
|
1414
|
+
"GET",
|
|
1415
|
+
`/v1/workspaces/${options.workspace}/content/approval-history/internal/${options.record}`
|
|
1416
|
+
)
|
|
1417
|
+
);
|
|
1418
|
+
});
|
|
1419
|
+
addConnectionOptions(program.command("content:comment-attachments:init")).description(
|
|
1420
|
+
"Initialize an approval comment attachment upload. Requires agency plan and content review comment capability."
|
|
1421
|
+
).requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--json <file>", "JSON file containing the attachment upload payload.").action(async (options, command) => {
|
|
1422
|
+
const payload = approvalAttachmentInitSchema.parse(await readJsonFile(options.json));
|
|
1423
|
+
const client = createApiClient(resolveConfig(command));
|
|
1424
|
+
writeRawJson(
|
|
1425
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/content/comment-attachments/init`, {
|
|
1426
|
+
body: payload
|
|
1427
|
+
})
|
|
1428
|
+
);
|
|
1429
|
+
});
|
|
1430
|
+
addConnectionOptions(program.command("content:comment-attachments:complete")).description(
|
|
1431
|
+
"Complete an approval comment attachment upload. Requires agency plan and content review comment capability."
|
|
1432
|
+
).requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--attachment <attachmentId>", "Attachment ID.").action(async (options, command) => {
|
|
1433
|
+
const client = createApiClient(resolveConfig(command));
|
|
1434
|
+
writeRawJson(
|
|
1435
|
+
await client.request(
|
|
1436
|
+
"POST",
|
|
1437
|
+
`/v1/workspaces/${options.workspace}/content/comment-attachments/${options.attachment}/complete`
|
|
1438
|
+
)
|
|
1439
|
+
);
|
|
1440
|
+
});
|
|
1441
|
+
addConnectionOptions(program.command("content:comment-attachments:abort")).description(
|
|
1442
|
+
"Abort an approval comment attachment upload. Requires agency plan and content review comment capability."
|
|
1443
|
+
).requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--attachment <attachmentId>", "Attachment ID.").action(async (options, command) => {
|
|
1444
|
+
const client = createApiClient(resolveConfig(command));
|
|
1445
|
+
writeRawJson(
|
|
1446
|
+
await client.request(
|
|
1447
|
+
"POST",
|
|
1448
|
+
`/v1/workspaces/${options.workspace}/content/comment-attachments/${options.attachment}/abort`
|
|
1449
|
+
)
|
|
1450
|
+
);
|
|
1451
|
+
});
|
|
1452
|
+
addConnectionOptions(program.command("content:activity:react")).description("Add a reaction to an approval activity. Requires agency plan and content review comment capability.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--activity <activityId>", "Activity ID.").requiredOption("--json <file>", "JSON file containing emoji.").action(async (options, command) => {
|
|
1453
|
+
const payload = approvalReactionSchema.parse(await readJsonFile(options.json));
|
|
1454
|
+
const client = createApiClient(resolveConfig(command));
|
|
1455
|
+
writeRawJson(
|
|
1456
|
+
await client.request(
|
|
1457
|
+
"POST",
|
|
1458
|
+
`/v1/workspaces/${options.workspace}/content/activity/${options.activity}/reactions`,
|
|
1459
|
+
{
|
|
1460
|
+
body: payload
|
|
1461
|
+
}
|
|
1462
|
+
)
|
|
1463
|
+
);
|
|
1464
|
+
});
|
|
1465
|
+
addConnectionOptions(program.command("content:activity:unreact")).description(
|
|
1466
|
+
"Remove a reaction from an approval activity. Requires agency plan and content review comment capability."
|
|
1467
|
+
).requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--activity <activityId>", "Activity ID.").requiredOption("--emoji <emoji>", "Reaction emoji to remove.").action(async (options, command) => {
|
|
1468
|
+
const client = createApiClient(resolveConfig(command));
|
|
1469
|
+
writeRawJson(
|
|
1470
|
+
await client.request(
|
|
1471
|
+
"DELETE",
|
|
1472
|
+
`/v1/workspaces/${options.workspace}/content/activity/${options.activity}/reactions/${encodeURIComponent(options.emoji)}`
|
|
1473
|
+
)
|
|
1474
|
+
);
|
|
1475
|
+
});
|
|
1476
|
+
addConnectionOptions(program.command("content:request-approval")).description(
|
|
1477
|
+
"Request internal approval for a content item. Requires agency plan and content review request capability; may notify approvers."
|
|
1478
|
+
).requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").requiredOption("--json <file>", "JSON file containing approverIds and policy.").option("--yes", "Confirm approver notifications.").action(async (options, command) => {
|
|
1479
|
+
if (!options.yes) {
|
|
1480
|
+
throw new CliError("content:request-approval requires --yes because it may notify approvers.", {
|
|
1481
|
+
code: "INVALID_OPTIONS"
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
const payload = approvalRequestSchema.parse(await readJsonFile(options.json));
|
|
1485
|
+
const client = createApiClient(resolveConfig(command));
|
|
1486
|
+
writeRawJson(
|
|
1487
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/content/${options.id}/request-approval`, {
|
|
1488
|
+
body: payload
|
|
1489
|
+
})
|
|
1490
|
+
);
|
|
1491
|
+
});
|
|
1492
|
+
addConnectionOptions(program.command("content:decision")).description("Submit an internal approval decision. Requires agency plan and content review decision capability.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").requiredOption("--json <file>", "JSON file containing decision, optional override, and reason.").action(async (options, command) => {
|
|
1493
|
+
const payload = approvalDecisionSchema.parse(await readJsonFile(options.json));
|
|
1494
|
+
const client = createApiClient(resolveConfig(command));
|
|
1495
|
+
writeRawJson(
|
|
1496
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/content/${options.id}/decision`, {
|
|
1497
|
+
body: payload
|
|
1498
|
+
})
|
|
1499
|
+
);
|
|
1500
|
+
});
|
|
1501
|
+
addConnectionOptions(program.command("content:withdraw")).description("Withdraw an internal approval request. Requires agency plan and content review withdraw capability.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").action(async (options, command) => {
|
|
1502
|
+
const client = createApiClient(resolveConfig(command));
|
|
1503
|
+
writeRawJson(await client.request("POST", `/v1/workspaces/${options.workspace}/content/${options.id}/withdraw`));
|
|
1504
|
+
});
|
|
1505
|
+
addConnectionOptions(program.command("content:resubmit")).description(
|
|
1506
|
+
"Resubmit rejected content for approval. Requires agency plan and content review request capability; may notify approvers."
|
|
1507
|
+
).requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").requiredOption("--json <file>", "JSON file containing approverIds and policy.").option("--yes", "Confirm approver notifications.").action(async (options, command) => {
|
|
1508
|
+
if (!options.yes) {
|
|
1509
|
+
throw new CliError("content:resubmit requires --yes because it may notify approvers.", {
|
|
1510
|
+
code: "INVALID_OPTIONS"
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
const payload = approvalRequestSchema.parse(await readJsonFile(options.json));
|
|
1514
|
+
const client = createApiClient(resolveConfig(command));
|
|
1515
|
+
writeRawJson(
|
|
1516
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/content/${options.id}/resubmit`, {
|
|
1517
|
+
body: payload
|
|
1518
|
+
})
|
|
1519
|
+
);
|
|
1520
|
+
});
|
|
1521
|
+
addConnectionOptions(program.command("content:history")).description("Get approval activity history for one content item. Requires agency plan.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").action(async (options, command) => {
|
|
1522
|
+
const client = createApiClient(resolveConfig(command));
|
|
1523
|
+
writeRawJson(await client.request("GET", `/v1/workspaces/${options.workspace}/content/${options.id}/history`));
|
|
1524
|
+
});
|
|
1525
|
+
addConnectionOptions(program.command("content:comment")).description(
|
|
1526
|
+
"Add an internal approval comment to one content item. Requires agency plan and content review comment capability."
|
|
1527
|
+
).requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").requiredOption("--json <file>", "JSON file containing body or attachmentId.").action(async (options, command) => {
|
|
1528
|
+
const payload = approvalCommentSchema.parse(await readJsonFile(options.json));
|
|
1529
|
+
const client = createApiClient(resolveConfig(command));
|
|
1530
|
+
writeRawJson(
|
|
1531
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/content/${options.id}/comment`, {
|
|
1532
|
+
body: payload
|
|
1533
|
+
})
|
|
1534
|
+
);
|
|
1535
|
+
});
|
|
1536
|
+
addConnectionOptions(program.command("content:reopen-client-review")).description("Reopen client review for one content item. Requires agency plan and client review send capability.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").action(async (options, command) => {
|
|
1537
|
+
const client = createApiClient(resolveConfig(command));
|
|
1538
|
+
writeRawJson(
|
|
1539
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/content/${options.id}/reopen-client-review`)
|
|
1540
|
+
);
|
|
1541
|
+
});
|
|
1542
|
+
addConnectionOptions(program.command("content:delete")).description("Delete a content item.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <contentId>", "Content ID.").action(async (options, command) => {
|
|
1543
|
+
const client = createApiClient(resolveConfig(command));
|
|
1544
|
+
await client.request("DELETE", `/v1/workspaces/${options.workspace}/content/${options.id}`);
|
|
1545
|
+
writeJson({ id: options.id, deleted: true });
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// src/grid-planner-schema.ts
|
|
1550
|
+
import { z as z4 } from "zod";
|
|
1551
|
+
var uuidV4 = z4.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, "Expected a UUIDv4 string.");
|
|
1552
|
+
var placeholderSettingsSchema = z4.object({
|
|
1553
|
+
title: z4.string().trim().min(1).max(40),
|
|
1554
|
+
color: z4.string().regex(/^#[0-9a-fA-F]{6}$/)
|
|
1555
|
+
}).strict();
|
|
1556
|
+
var settingsSchema2 = z4.object({
|
|
1557
|
+
aspectRatio: z4.number().min(0.1).max(10).nullable().optional(),
|
|
1558
|
+
placeholder: placeholderSettingsSchema.optional()
|
|
1559
|
+
}).strict();
|
|
1560
|
+
var gridPlannerCreateSchema = z4.object({
|
|
1561
|
+
integrationId: uuidV4,
|
|
1562
|
+
kind: z4.enum(["visual_only", "linked_post", "placeholder"]),
|
|
1563
|
+
mediaIds: z4.array(uuidV4).min(1).optional(),
|
|
1564
|
+
linkedContentId: uuidV4.optional(),
|
|
1565
|
+
coverMediaId: uuidV4.nullable().optional(),
|
|
1566
|
+
note: z4.string().optional(),
|
|
1567
|
+
position: z4.number().int().min(0).optional(),
|
|
1568
|
+
settings: settingsSchema2.optional()
|
|
1569
|
+
}).strict().superRefine((payload, context) => {
|
|
1570
|
+
if (payload.kind === "visual_only" && !payload.mediaIds) {
|
|
1571
|
+
context.addIssue({
|
|
1572
|
+
code: "custom",
|
|
1573
|
+
path: ["mediaIds"],
|
|
1574
|
+
message: "mediaIds is required when kind is visual_only."
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
if (payload.kind === "linked_post" && !payload.linkedContentId) {
|
|
1578
|
+
context.addIssue({
|
|
1579
|
+
code: "custom",
|
|
1580
|
+
path: ["linkedContentId"],
|
|
1581
|
+
message: "linkedContentId is required when kind is linked_post."
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
if (payload.kind === "placeholder") {
|
|
1585
|
+
if (!payload.settings?.placeholder) {
|
|
1586
|
+
context.addIssue({
|
|
1587
|
+
code: "custom",
|
|
1588
|
+
path: ["settings", "placeholder"],
|
|
1589
|
+
message: "settings.placeholder is required when kind is placeholder."
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
if (payload.mediaIds) {
|
|
1593
|
+
context.addIssue({
|
|
1594
|
+
code: "custom",
|
|
1595
|
+
path: ["mediaIds"],
|
|
1596
|
+
message: "mediaIds cannot be set when kind is placeholder."
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
if (payload.linkedContentId) {
|
|
1600
|
+
context.addIssue({
|
|
1601
|
+
code: "custom",
|
|
1602
|
+
path: ["linkedContentId"],
|
|
1603
|
+
message: "linkedContentId cannot be set when kind is placeholder."
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
if (payload.coverMediaId) {
|
|
1607
|
+
context.addIssue({
|
|
1608
|
+
code: "custom",
|
|
1609
|
+
path: ["coverMediaId"],
|
|
1610
|
+
message: "coverMediaId cannot be set when kind is placeholder."
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
if (payload.note) {
|
|
1614
|
+
context.addIssue({
|
|
1615
|
+
code: "custom",
|
|
1616
|
+
path: ["note"],
|
|
1617
|
+
message: "note cannot be set when kind is placeholder."
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
if (payload.mediaIds && new Set(payload.mediaIds).size !== payload.mediaIds.length) {
|
|
1622
|
+
context.addIssue({
|
|
1623
|
+
code: "custom",
|
|
1624
|
+
path: ["mediaIds"],
|
|
1625
|
+
message: "mediaIds must contain unique UUIDs."
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
var gridPlannerUpdateSchema = z4.object({
|
|
1630
|
+
integrationId: uuidV4,
|
|
1631
|
+
note: z4.string().nullable().optional(),
|
|
1632
|
+
settings: settingsSchema2.optional()
|
|
1633
|
+
}).strict();
|
|
1634
|
+
var gridPlannerReorderSchema = z4.object({
|
|
1635
|
+
integrationId: uuidV4,
|
|
1636
|
+
itemIds: z4.array(uuidV4).min(1).optional(),
|
|
1637
|
+
items: z4.array(
|
|
1638
|
+
z4.object({
|
|
1639
|
+
itemId: uuidV4,
|
|
1640
|
+
position: z4.number().int().min(0)
|
|
1641
|
+
}).strict()
|
|
1642
|
+
).min(1).optional()
|
|
1643
|
+
}).strict().refine((payload) => Boolean(payload.itemIds) !== Boolean(payload.items), {
|
|
1644
|
+
path: ["itemIds"],
|
|
1645
|
+
message: "Provide either itemIds or items."
|
|
1646
|
+
}).refine((payload) => !payload.itemIds || new Set(payload.itemIds).size === payload.itemIds.length, {
|
|
1647
|
+
path: ["itemIds"],
|
|
1648
|
+
message: "itemIds must contain unique UUIDs."
|
|
1649
|
+
}).refine((payload) => !payload.items || new Set(payload.items.map((item) => item.itemId)).size === payload.items.length, {
|
|
1650
|
+
path: ["items"],
|
|
1651
|
+
message: "items must contain unique itemIds."
|
|
1652
|
+
}).refine(
|
|
1653
|
+
(payload) => !payload.items || new Set(payload.items.map((item) => item.position)).size === payload.items.length,
|
|
1654
|
+
{
|
|
1655
|
+
path: ["items"],
|
|
1656
|
+
message: "items must contain unique positions."
|
|
1657
|
+
}
|
|
1658
|
+
);
|
|
1659
|
+
var gridPlannerReplaceMediaSchema = z4.object({
|
|
1660
|
+
integrationId: uuidV4,
|
|
1661
|
+
mediaIds: z4.array(uuidV4).min(1).max(10)
|
|
1662
|
+
}).strict().refine((payload) => new Set(payload.mediaIds).size === payload.mediaIds.length, {
|
|
1663
|
+
path: ["mediaIds"],
|
|
1664
|
+
message: "mediaIds must contain unique UUIDs."
|
|
1665
|
+
});
|
|
1666
|
+
var gridPlannerSetCoverSchema = z4.object({
|
|
1667
|
+
integrationId: uuidV4,
|
|
1668
|
+
mediaId: uuidV4
|
|
1669
|
+
}).strict();
|
|
1670
|
+
var gridPlannerPromoteSchema = z4.object({
|
|
1671
|
+
integrationId: uuidV4
|
|
1672
|
+
}).strict();
|
|
1673
|
+
|
|
1674
|
+
// src/commands/grid-planner.ts
|
|
1675
|
+
function registerGridPlannerCommands(program) {
|
|
1676
|
+
addConnectionOptions(program.command("grid:list")).description("List grid planner items for one integration.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--integration <id>", "Integration ID.").action(async (options, command) => {
|
|
1677
|
+
const client = createApiClient(resolveConfig(command));
|
|
1678
|
+
writeRawJson(
|
|
1679
|
+
await client.request("GET", `/v1/workspaces/${options.workspace}/grid-planner/items`, {
|
|
1680
|
+
query: { integrationId: options.integration }
|
|
1681
|
+
})
|
|
1682
|
+
);
|
|
1683
|
+
});
|
|
1684
|
+
addConnectionOptions(program.command("grid:create")).description("Create a grid planner item from a JSON payload file. Requires grid planner manage capability.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--json <file>", "JSON file containing the grid planner item payload.").action(async (options, command) => {
|
|
1685
|
+
const payload = gridPlannerCreateSchema.parse(await readJsonFile(options.json));
|
|
1686
|
+
const client = createApiClient(resolveConfig(command));
|
|
1687
|
+
writeRawJson(
|
|
1688
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/grid-planner/items`, {
|
|
1689
|
+
body: payload
|
|
1690
|
+
})
|
|
1691
|
+
);
|
|
1692
|
+
});
|
|
1693
|
+
addConnectionOptions(program.command("grid:update")).description("Update a grid planner item from a JSON payload file. Requires grid planner manage capability.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--item <itemId>", "Grid planner item ID.").requiredOption("--json <file>", "JSON file containing integrationId plus fields to update.").action(async (options, command) => {
|
|
1694
|
+
const payload = gridPlannerUpdateSchema.parse(await readJsonFile(options.json));
|
|
1695
|
+
const client = createApiClient(resolveConfig(command));
|
|
1696
|
+
writeRawJson(
|
|
1697
|
+
await client.request("PATCH", `/v1/workspaces/${options.workspace}/grid-planner/items/${options.item}`, {
|
|
1698
|
+
body: payload
|
|
1699
|
+
})
|
|
1700
|
+
);
|
|
1701
|
+
});
|
|
1702
|
+
addConnectionOptions(program.command("grid:delete")).description("Delete one grid planner item. Requires grid planner manage capability.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--item <itemId>", "Grid planner item ID.").option("--yes", "Confirm deleting the grid planner item.").action(async (options, command) => {
|
|
1703
|
+
if (!options.yes) {
|
|
1704
|
+
throw new CliError("grid:delete requires --yes because it deletes a planner item.", {
|
|
1705
|
+
code: "INVALID_OPTIONS"
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
const client = createApiClient(resolveConfig(command));
|
|
1709
|
+
writeRawJson(
|
|
1710
|
+
await client.request("DELETE", `/v1/workspaces/${options.workspace}/grid-planner/items/${options.item}`)
|
|
1711
|
+
);
|
|
1712
|
+
});
|
|
1713
|
+
addConnectionOptions(program.command("grid:reorder")).description(
|
|
1714
|
+
"Reorder all grid planner items for one integration from a JSON payload file. Requires grid planner manage capability."
|
|
1715
|
+
).requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--json <file>", "JSON file containing integrationId and the complete itemIds order.").action(async (options, command) => {
|
|
1716
|
+
const payload = gridPlannerReorderSchema.parse(await readJsonFile(options.json));
|
|
1717
|
+
const client = createApiClient(resolveConfig(command));
|
|
1718
|
+
writeRawJson(
|
|
1719
|
+
await client.request("PATCH", `/v1/workspaces/${options.workspace}/grid-planner/items/reorder`, {
|
|
1720
|
+
body: payload
|
|
1721
|
+
})
|
|
1722
|
+
);
|
|
1723
|
+
});
|
|
1724
|
+
addConnectionOptions(program.command("grid:replace-media")).description(
|
|
1725
|
+
"Replace a visual-only planner item carousel from a JSON payload file. Requires grid planner manage capability."
|
|
1726
|
+
).requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--item <itemId>", "Grid planner item ID.").requiredOption("--json <file>", "JSON file containing integrationId and mediaIds.").action(async (options, command) => {
|
|
1727
|
+
const payload = gridPlannerReplaceMediaSchema.parse(await readJsonFile(options.json));
|
|
1728
|
+
const client = createApiClient(resolveConfig(command));
|
|
1729
|
+
writeRawJson(
|
|
1730
|
+
await client.request("PUT", `/v1/workspaces/${options.workspace}/grid-planner/items/${options.item}/media`, {
|
|
1731
|
+
body: payload
|
|
1732
|
+
})
|
|
1733
|
+
);
|
|
1734
|
+
});
|
|
1735
|
+
addConnectionOptions(program.command("grid:set-cover")).description(
|
|
1736
|
+
"Set a visual-only planner item cover media item from a JSON payload file. Requires grid planner manage capability."
|
|
1737
|
+
).requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--item <itemId>", "Grid planner item ID.").requiredOption("--json <file>", "JSON file containing integrationId and mediaId.").action(async (options, command) => {
|
|
1738
|
+
const payload = gridPlannerSetCoverSchema.parse(await readJsonFile(options.json));
|
|
1739
|
+
const client = createApiClient(resolveConfig(command));
|
|
1740
|
+
writeRawJson(
|
|
1741
|
+
await client.request("PUT", `/v1/workspaces/${options.workspace}/grid-planner/items/${options.item}/cover`, {
|
|
1742
|
+
body: payload
|
|
1743
|
+
})
|
|
1744
|
+
);
|
|
1745
|
+
});
|
|
1746
|
+
addConnectionOptions(program.command("grid:remove-cover")).description("Remove cover media from a visual-only planner item. Requires grid planner manage capability.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--item <itemId>", "Grid planner item ID.").action(async (options, command) => {
|
|
1747
|
+
const client = createApiClient(resolveConfig(command));
|
|
1748
|
+
writeRawJson(
|
|
1749
|
+
await client.request("DELETE", `/v1/workspaces/${options.workspace}/grid-planner/items/${options.item}/cover`)
|
|
1750
|
+
);
|
|
1751
|
+
});
|
|
1752
|
+
addConnectionOptions(program.command("grid:promote")).description(
|
|
1753
|
+
"Promote a planner mockup to a draft from a JSON payload file. Requires grid planner manage capability."
|
|
1754
|
+
).requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--item <itemId>", "Grid planner item ID.").requiredOption("--json <file>", "JSON file containing integrationId.").option("--yes", "Confirm creating a draft and linking the planner item.").action(async (options, command) => {
|
|
1755
|
+
if (!options.yes) {
|
|
1756
|
+
throw new CliError(
|
|
1757
|
+
"grid:promote requires --yes because it creates a content draft and links the planner item.",
|
|
1758
|
+
{
|
|
1759
|
+
code: "INVALID_OPTIONS"
|
|
1760
|
+
}
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1763
|
+
const payload = gridPlannerPromoteSchema.parse(await readJsonFile(options.json));
|
|
1764
|
+
const client = createApiClient(resolveConfig(command));
|
|
1765
|
+
writeRawJson(
|
|
1766
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/grid-planner/items/${options.item}/promote`, {
|
|
1767
|
+
body: payload
|
|
1768
|
+
})
|
|
1769
|
+
);
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// src/commands/hashtags.ts
|
|
1774
|
+
import { z as z5 } from "zod";
|
|
1775
|
+
var hashtagGroupSchema = z5.object({
|
|
1776
|
+
name: z5.string().trim().min(1).max(100),
|
|
1777
|
+
content: z5.string().trim().min(1)
|
|
1778
|
+
});
|
|
1779
|
+
function registerHashtagCommands(program) {
|
|
1780
|
+
addConnectionOptions(program.command("hashtags:list")).description("List saved hashtag groups for a workspace.").requiredOption("-w, --workspace <id>", "Workspace ID.").action(async (options, command) => {
|
|
1781
|
+
const client = createApiClient(resolveConfig(command));
|
|
1782
|
+
writeRawJson(await client.request("GET", `/v1/workspaces/${options.workspace}/hashtags`));
|
|
1783
|
+
});
|
|
1784
|
+
addConnectionOptions(program.command("hashtags:create")).description("Create a hashtag group from a JSON payload file.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--json <file>", "JSON file containing name and content.").action(async (options, command) => {
|
|
1785
|
+
const payload = hashtagGroupSchema.parse(await readJsonFile(options.json));
|
|
1786
|
+
const client = createApiClient(resolveConfig(command));
|
|
1787
|
+
writeRawJson(
|
|
1788
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/hashtags`, {
|
|
1789
|
+
body: payload
|
|
1790
|
+
})
|
|
1791
|
+
);
|
|
1792
|
+
});
|
|
1793
|
+
addConnectionOptions(program.command("hashtags:update")).description("Update a hashtag group from a JSON payload file.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <hashtagGroupId>", "Hashtag group ID.").requiredOption("--json <file>", "JSON file containing name and content.").action(async (options, command) => {
|
|
1794
|
+
const payload = hashtagGroupSchema.parse(await readJsonFile(options.json));
|
|
1795
|
+
const client = createApiClient(resolveConfig(command));
|
|
1796
|
+
writeRawJson(
|
|
1797
|
+
await client.request("PATCH", `/v1/workspaces/${options.workspace}/hashtags/${options.id}`, {
|
|
1798
|
+
body: payload
|
|
1799
|
+
})
|
|
1800
|
+
);
|
|
1801
|
+
});
|
|
1802
|
+
addConnectionOptions(program.command("hashtags:delete")).description("Delete a hashtag group.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <hashtagGroupId>", "Hashtag group ID.").action(async (options, command) => {
|
|
1803
|
+
const client = createApiClient(resolveConfig(command));
|
|
1804
|
+
writeRawJson(await client.request("DELETE", `/v1/workspaces/${options.workspace}/hashtags/${options.id}`));
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
// src/inbox-schema.ts
|
|
1809
|
+
import { z as z6 } from "zod";
|
|
1810
|
+
var uuid4 = z6.string().uuid();
|
|
1811
|
+
var inboxAttachmentSchema = z6.object({
|
|
1812
|
+
type: z6.enum(["image", "video", "audio", "file"]),
|
|
1813
|
+
url: z6.url()
|
|
1814
|
+
});
|
|
1815
|
+
var inboxReplySchema = z6.object({
|
|
1816
|
+
content: z6.string().max(2e3).optional(),
|
|
1817
|
+
parentMessageId: uuid4.optional(),
|
|
1818
|
+
attachment: inboxAttachmentSchema.optional()
|
|
1819
|
+
}).refine((payload) => Boolean(payload.content?.trim() || payload.attachment), {
|
|
1820
|
+
message: "Reply payload must include content or attachment."
|
|
1821
|
+
});
|
|
1822
|
+
var inboxNoteSchema = z6.object({
|
|
1823
|
+
content: z6.string().min(1).max(5e3)
|
|
1824
|
+
});
|
|
1825
|
+
var inboxModerateSchema = z6.object({
|
|
1826
|
+
action: z6.enum(["hide", "unhide", "delete"])
|
|
1827
|
+
});
|
|
1828
|
+
var inboxMarkAllReadSchema = z6.object({
|
|
1829
|
+
platform: z6.enum(["instagram", "facebook", "threads"]).optional(),
|
|
1830
|
+
status: z6.enum(["open", "archived", "all"]).optional(),
|
|
1831
|
+
integrationId: z6.string().min(1).optional(),
|
|
1832
|
+
messageType: z6.enum(["comment", "dm", "review", "mention"]).optional()
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
// src/commands/inbox.ts
|
|
1836
|
+
function registerInboxCommands(program) {
|
|
1837
|
+
addConnectionOptions(program.command("inbox:threads")).description("List inbox threads for a workspace.").requiredOption("-w, --workspace <id>", "Workspace ID.").option("--status <status>", "Thread status: open, archived, or all.").option("--read <state>", "Read state: read, unread, or all.").option("--platform <platform>", "Platform filter.").option("--integration <id>", "Integration ID filter.").option("--message-type <type>", "Message type filter.").option("--search <text>", "Search text.").option("--page <number>", "Page number.").option("--limit <number>", "Page size.").action(
|
|
1838
|
+
async (options, command) => {
|
|
1839
|
+
const client = createApiClient(resolveConfig(command));
|
|
1840
|
+
writeRawJson(
|
|
1841
|
+
await client.request("GET", `/v1/workspaces/${options.workspace}/inbox/threads`, {
|
|
1842
|
+
query: {
|
|
1843
|
+
status: options.status,
|
|
1844
|
+
read: options.read,
|
|
1845
|
+
platform: options.platform,
|
|
1846
|
+
integrationId: options.integration,
|
|
1847
|
+
messageType: options.messageType,
|
|
1848
|
+
search: options.search,
|
|
1849
|
+
page: options.page,
|
|
1850
|
+
limit: options.limit
|
|
1851
|
+
}
|
|
1852
|
+
})
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
);
|
|
1856
|
+
addConnectionOptions(program.command("inbox:messages")).description("List messages for an inbox thread.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--thread <threadId>", "Inbox thread ID.").action(async (options, command) => {
|
|
1857
|
+
const client = createApiClient(resolveConfig(command));
|
|
1858
|
+
writeRawJson(
|
|
1859
|
+
await client.request("GET", `/v1/workspaces/${options.workspace}/inbox/threads/${options.thread}/messages`)
|
|
1860
|
+
);
|
|
1861
|
+
});
|
|
1862
|
+
addConnectionOptions(program.command("inbox:stats")).description("Get inbox stats for a workspace.").requiredOption("-w, --workspace <id>", "Workspace ID.").action(async (options, command) => {
|
|
1863
|
+
const client = createApiClient(resolveConfig(command));
|
|
1864
|
+
writeRawJson(await client.request("GET", `/v1/workspaces/${options.workspace}/inbox/stats`));
|
|
1865
|
+
});
|
|
1866
|
+
addConnectionOptions(program.command("inbox:read")).description("Mark an inbox thread as read.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--thread <threadId>", "Inbox thread ID.").action(async (options, command) => {
|
|
1867
|
+
const client = createApiClient(resolveConfig(command));
|
|
1868
|
+
writeRawJson(
|
|
1869
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/inbox/threads/${options.thread}/read`)
|
|
1870
|
+
);
|
|
1871
|
+
});
|
|
1872
|
+
addConnectionOptions(program.command("inbox:read-all")).description("Mark matching inbox threads as read from a JSON filter payload.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--json <file>", "JSON file containing read-all filters.").option("--yes", "Confirm marking matching threads as read.").action(async (options, command) => {
|
|
1873
|
+
if (!options.yes) {
|
|
1874
|
+
throw new CliError("inbox:read-all requires --yes because it can mark many customer messages as read.", {
|
|
1875
|
+
code: "INVALID_OPTIONS"
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
const payload = inboxMarkAllReadSchema.parse(await readJsonFile(options.json));
|
|
1879
|
+
const client = createApiClient(resolveConfig(command));
|
|
1880
|
+
writeRawJson(
|
|
1881
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/inbox/threads/read-all`, {
|
|
1882
|
+
body: payload
|
|
1883
|
+
})
|
|
1884
|
+
);
|
|
1885
|
+
});
|
|
1886
|
+
addConnectionOptions(program.command("inbox:unread")).description("Mark an inbox thread as unread.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--thread <threadId>", "Inbox thread ID.").action(async (options, command) => {
|
|
1887
|
+
const client = createApiClient(resolveConfig(command));
|
|
1888
|
+
writeRawJson(
|
|
1889
|
+
await client.request("DELETE", `/v1/workspaces/${options.workspace}/inbox/threads/${options.thread}/read`)
|
|
1890
|
+
);
|
|
1891
|
+
});
|
|
1892
|
+
addConnectionOptions(program.command("inbox:reply")).description("Reply publicly to an inbox thread. Requires inbox reply capability.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--thread <threadId>", "Inbox thread ID.").requiredOption("--json <file>", "JSON file containing reply content, parentMessageId, or attachment.").option("--yes", "Confirm sending a public reply.").action(async (options, command) => {
|
|
1893
|
+
if (!options.yes) {
|
|
1894
|
+
throw new CliError("inbox:reply requires --yes because it sends a public reply.", {
|
|
1895
|
+
code: "INVALID_OPTIONS"
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
const payload = inboxReplySchema.parse(await readJsonFile(options.json));
|
|
1899
|
+
const client = createApiClient(resolveConfig(command));
|
|
1900
|
+
writeRawJson(
|
|
1901
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/inbox/threads/${options.thread}/reply`, {
|
|
1902
|
+
body: payload
|
|
1903
|
+
})
|
|
1904
|
+
);
|
|
1905
|
+
});
|
|
1906
|
+
addConnectionOptions(program.command("inbox:note")).description("Add an internal note to an inbox thread. Requires inbox note capability.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--thread <threadId>", "Inbox thread ID.").requiredOption("--json <file>", "JSON file containing note content.").action(async (options, command) => {
|
|
1907
|
+
const payload = inboxNoteSchema.parse(await readJsonFile(options.json));
|
|
1908
|
+
const client = createApiClient(resolveConfig(command));
|
|
1909
|
+
writeRawJson(
|
|
1910
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/inbox/threads/${options.thread}/notes`, {
|
|
1911
|
+
body: payload
|
|
1912
|
+
})
|
|
1913
|
+
);
|
|
1914
|
+
});
|
|
1915
|
+
addConnectionOptions(program.command("inbox:moderate")).description("Moderate a public inbox message. Requires inbox moderate capability.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--message <messageId>", "Inbox message ID.").requiredOption("--json <file>", "JSON file containing moderation action.").option("--yes", "Confirm moderating public content.").action(async (options, command) => {
|
|
1916
|
+
if (!options.yes) {
|
|
1917
|
+
throw new CliError("inbox:moderate requires --yes because it moderates public content.", {
|
|
1918
|
+
code: "INVALID_OPTIONS"
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
const payload = inboxModerateSchema.parse(await readJsonFile(options.json));
|
|
1922
|
+
const client = createApiClient(resolveConfig(command));
|
|
1923
|
+
writeRawJson(
|
|
1924
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/inbox/messages/${options.message}/moderate`, {
|
|
1925
|
+
body: payload
|
|
1926
|
+
})
|
|
1927
|
+
);
|
|
1928
|
+
});
|
|
1929
|
+
addConnectionOptions(program.command("inbox:retry-message")).description("Retry a failed outgoing inbox message. Requires inbox retry-failed capability.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--message <messageId>", "Inbox message ID.").option("--yes", "Confirm retrying an external send.").action(async (options, command) => {
|
|
1930
|
+
if (!options.yes) {
|
|
1931
|
+
throw new CliError("inbox:retry-message requires --yes because it can retry an external send.", {
|
|
1932
|
+
code: "INVALID_OPTIONS"
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
const client = createApiClient(resolveConfig(command));
|
|
1936
|
+
writeRawJson(
|
|
1937
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/inbox/messages/${options.message}/retry`)
|
|
1938
|
+
);
|
|
1939
|
+
});
|
|
1940
|
+
addConnectionOptions(program.command("inbox:delete-failed")).description("Delete a failed outgoing inbox message. Requires inbox delete-failed capability.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--message <messageId>", "Inbox message ID.").option("--yes", "Confirm deleting a failed outgoing message.").action(async (options, command) => {
|
|
1941
|
+
if (!options.yes) {
|
|
1942
|
+
throw new CliError("inbox:delete-failed requires --yes because it deletes a failed outgoing message.", {
|
|
1943
|
+
code: "INVALID_OPTIONS"
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
const client = createApiClient(resolveConfig(command));
|
|
1947
|
+
writeRawJson(
|
|
1948
|
+
await client.request("DELETE", `/v1/workspaces/${options.workspace}/inbox/messages/${options.message}/failed`)
|
|
1949
|
+
);
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// src/commands/integrations.ts
|
|
1954
|
+
function registerIntegrationCommands(program) {
|
|
1955
|
+
addConnectionOptions(program.command("integrations:list")).description("List social integrations for a workspace.").requiredOption("-w, --workspace <id>", "Workspace ID.").action(async (options, command) => {
|
|
1956
|
+
const client = createApiClient(resolveConfig(command));
|
|
1957
|
+
writeRawJson(await client.request("GET", `/v1/workspaces/${options.workspace}/integrations`));
|
|
1958
|
+
});
|
|
1959
|
+
addConnectionOptions(program.command("integrations:capabilities")).description("Get safe publishing capabilities and dynamic option keys for an integration.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("-i, --integration <id>", "Integration ID.").action(async (options, command) => {
|
|
1960
|
+
const client = createApiClient(resolveConfig(command));
|
|
1961
|
+
writeRawJson(
|
|
1962
|
+
await client.request(
|
|
1963
|
+
"GET",
|
|
1964
|
+
`/v1/workspaces/${options.workspace}/integrations/${options.integration}/capabilities`
|
|
1965
|
+
)
|
|
1966
|
+
);
|
|
1967
|
+
});
|
|
1968
|
+
addConnectionOptions(program.command("integrations:options")).description("Fetch safe read-only provider options returned by integrations:capabilities.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("-i, --integration <id>", "Integration ID.").requiredOption("--key <optionKey>", "Option key, for example pinterest-boards or youtube-video-categories.").option("--json <file>", "JSON file containing option input, such as query or regionCode.").action(
|
|
1969
|
+
async (options, command) => {
|
|
1970
|
+
const payload = options.json ? await readJsonFile(options.json) : {};
|
|
1971
|
+
const client = createApiClient(resolveConfig(command));
|
|
1972
|
+
writeRawJson(
|
|
1973
|
+
await client.request(
|
|
1974
|
+
"POST",
|
|
1975
|
+
`/v1/workspaces/${options.workspace}/integrations/${options.integration}/options/${options.key}`,
|
|
1976
|
+
{ body: payload }
|
|
1977
|
+
)
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1980
|
+
);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// src/media-schema.ts
|
|
1984
|
+
import { z as z7 } from "zod";
|
|
1985
|
+
var uuid5 = z7.string().uuid();
|
|
1986
|
+
var hexColor = z7.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, "Color must be a hex color.");
|
|
1987
|
+
var mediaUpdateSchema = z7.object({
|
|
1988
|
+
filename: z7.string().min(1).max(255).optional(),
|
|
1989
|
+
altText: z7.string().max(500).optional(),
|
|
1990
|
+
isFavorite: z7.boolean().optional(),
|
|
1991
|
+
folderId: uuid5.nullable().optional()
|
|
1992
|
+
}).refine((payload) => Object.keys(payload).length > 0, {
|
|
1993
|
+
message: "Media update payload must include at least one field."
|
|
1994
|
+
});
|
|
1995
|
+
var createMediaFolderSchema = z7.object({
|
|
1996
|
+
name: z7.string().min(1).max(255),
|
|
1997
|
+
parentId: uuid5.optional()
|
|
1998
|
+
});
|
|
1999
|
+
var updateMediaFolderSchema = z7.object({
|
|
2000
|
+
name: z7.string().min(1).max(255)
|
|
2001
|
+
});
|
|
2002
|
+
var moveMediaFolderSchema = z7.object({
|
|
2003
|
+
parentId: uuid5.nullable().optional()
|
|
2004
|
+
}).refine((payload) => Object.prototype.hasOwnProperty.call(payload, "parentId"), {
|
|
2005
|
+
message: "Folder move payload must include parentId. Use null to move to root."
|
|
2006
|
+
});
|
|
2007
|
+
var createMediaLabelSchema = z7.object({
|
|
2008
|
+
name: z7.string().min(1).max(50),
|
|
2009
|
+
color: hexColor.optional()
|
|
2010
|
+
});
|
|
2011
|
+
var updateMediaLabelSchema = z7.object({
|
|
2012
|
+
name: z7.string().min(1).max(50).optional(),
|
|
2013
|
+
color: hexColor.optional()
|
|
2014
|
+
}).refine((payload) => Object.keys(payload).length > 0, {
|
|
2015
|
+
message: "Media label update payload must include at least one field."
|
|
2016
|
+
});
|
|
2017
|
+
var mediaIdsSchema = z7.object({
|
|
2018
|
+
mediaIds: z7.array(uuid5).min(1).max(100)
|
|
2019
|
+
});
|
|
2020
|
+
var mediaBulkMoveSchema = mediaIdsSchema.extend({
|
|
2021
|
+
folderId: uuid5.nullable().optional()
|
|
2022
|
+
});
|
|
2023
|
+
var mediaLabelIdsSchema = z7.object({
|
|
2024
|
+
labelIds: z7.array(uuid5).min(1).max(50)
|
|
2025
|
+
});
|
|
2026
|
+
var mediaBulkLabelSchema = mediaIdsSchema.extend({
|
|
2027
|
+
labelIds: z7.array(uuid5).min(1).max(50)
|
|
2028
|
+
});
|
|
2029
|
+
|
|
2030
|
+
// src/commands/media.ts
|
|
2031
|
+
function registerMediaCommands(program) {
|
|
2032
|
+
addConnectionOptions(program.command("media:list")).description("List media for a workspace.").requiredOption("-w, --workspace <id>", "Workspace ID.").option("--folder-id <id>", "Folder ID filter.").option("--root-only", "Only return media in the workspace root folder.").option("--label-ids <ids>", "Comma-separated media label IDs.").option("--type <type>", "Media type: image, video, or gif.").option("--search <text>", "Search media filenames and metadata.").option("--favorite", "Only return favorite media.").option("--used", "Only return media already used by content.").option("--unused", "Only return media not used by content.").option("--state <state>", "Media lifecycle state: active, deleted, or all.").option("--limit <number>", "Page size.").option("--cursor <cursor>", "Pagination cursor.").option("--sort-by <field>", "Sort field.").option("--order <direction>", "Sort direction.").action(async (options, command) => {
|
|
2033
|
+
const client = createApiClient(resolveConfig(command));
|
|
2034
|
+
writeRawJson(
|
|
2035
|
+
await client.request("GET", `/v1/workspaces/${options.workspace}/media`, {
|
|
2036
|
+
query: {
|
|
2037
|
+
folderId: options.folderId,
|
|
2038
|
+
rootOnly: options.rootOnly,
|
|
2039
|
+
labelIds: options.labelIds,
|
|
2040
|
+
type: options.type,
|
|
2041
|
+
search: options.search,
|
|
2042
|
+
isFavorite: options.favorite,
|
|
2043
|
+
isUsed: options.used,
|
|
2044
|
+
isUnused: options.unused,
|
|
2045
|
+
state: options.state,
|
|
2046
|
+
limit: options.limit,
|
|
2047
|
+
cursor: options.cursor,
|
|
2048
|
+
sortBy: options.sortBy,
|
|
2049
|
+
order: options.order
|
|
2050
|
+
}
|
|
2051
|
+
})
|
|
2052
|
+
);
|
|
2053
|
+
});
|
|
2054
|
+
addConnectionOptions(program.command("media:upload")).description("Upload a media file to a workspace.").argument("<file>", "File to upload.").requiredOption("-w, --workspace <id>", "Workspace ID.").action(async (filePath, options, command) => {
|
|
2055
|
+
const client = createApiClient(resolveConfig(command));
|
|
2056
|
+
const file = await getUploadFileInfo(filePath);
|
|
2057
|
+
writeRawJson(
|
|
2058
|
+
await client.uploadFile(
|
|
2059
|
+
`/v1/workspaces/${options.workspace}/media/upload`,
|
|
2060
|
+
filePath,
|
|
2061
|
+
file.contentType,
|
|
2062
|
+
file.fileSize,
|
|
2063
|
+
file
|
|
2064
|
+
)
|
|
2065
|
+
);
|
|
2066
|
+
});
|
|
2067
|
+
addConnectionOptions(program.command("media:get")).description("Get one media item.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <mediaId>", "Media ID.").action(async (options, command) => {
|
|
2068
|
+
const client = createApiClient(resolveConfig(command));
|
|
2069
|
+
writeRawJson(await client.request("GET", `/v1/workspaces/${options.workspace}/media/${options.id}`));
|
|
2070
|
+
});
|
|
2071
|
+
addConnectionOptions(program.command("media:update")).description("Update one media item from a JSON payload file.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <mediaId>", "Media ID.").requiredOption("--json <file>", "JSON file containing fields to update.").action(async (options, command) => {
|
|
2072
|
+
const payload = mediaUpdateSchema.parse(await readJsonFile(options.json));
|
|
2073
|
+
const client = createApiClient(resolveConfig(command));
|
|
2074
|
+
writeRawJson(
|
|
2075
|
+
await client.request("PATCH", `/v1/workspaces/${options.workspace}/media/${options.id}`, {
|
|
2076
|
+
body: payload
|
|
2077
|
+
})
|
|
2078
|
+
);
|
|
2079
|
+
});
|
|
2080
|
+
addConnectionOptions(program.command("media:download-url")).description("Create a temporary media download URL.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <mediaId>", "Media ID.").action(async (options, command) => {
|
|
2081
|
+
const client = createApiClient(resolveConfig(command));
|
|
2082
|
+
writeRawJson(await client.request("GET", `/v1/workspaces/${options.workspace}/media/${options.id}/download-url`));
|
|
2083
|
+
});
|
|
2084
|
+
addConnectionOptions(program.command("media:view-url")).description("Create a temporary media view URL.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <mediaId>", "Media ID.").action(async (options, command) => {
|
|
2085
|
+
const client = createApiClient(resolveConfig(command));
|
|
2086
|
+
writeRawJson(await client.request("GET", `/v1/workspaces/${options.workspace}/media/${options.id}/view-url`));
|
|
2087
|
+
});
|
|
2088
|
+
addConnectionOptions(program.command("media:archive")).description("Archive one media item.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <mediaId>", "Media ID.").action(async (options, command) => {
|
|
2089
|
+
const client = createApiClient(resolveConfig(command));
|
|
2090
|
+
writeRawJson(await client.request("POST", `/v1/workspaces/${options.workspace}/media/${options.id}/archive`));
|
|
2091
|
+
});
|
|
2092
|
+
addConnectionOptions(program.command("media:delete")).description("Move one media item to the Bin.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <mediaId>", "Media ID.").action(async (options, command) => {
|
|
2093
|
+
const client = createApiClient(resolveConfig(command));
|
|
2094
|
+
writeRawJson(await client.request("DELETE", `/v1/workspaces/${options.workspace}/media/${options.id}`));
|
|
2095
|
+
});
|
|
2096
|
+
addConnectionOptions(program.command("media:folders:list")).description("List media folders.").requiredOption("-w, --workspace <id>", "Workspace ID.").option("--parent-id <id>", "Parent folder ID, or root for top-level folders.").action(async (options, command) => {
|
|
2097
|
+
const client = createApiClient(resolveConfig(command));
|
|
2098
|
+
writeRawJson(
|
|
2099
|
+
await client.request("GET", `/v1/workspaces/${options.workspace}/media/folders`, {
|
|
2100
|
+
query: { parentId: options.parentId }
|
|
2101
|
+
})
|
|
2102
|
+
);
|
|
2103
|
+
});
|
|
2104
|
+
addConnectionOptions(program.command("media:folders:get")).description("Get one media folder.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <folderId>", "Folder ID.").action(async (options, command) => {
|
|
2105
|
+
const client = createApiClient(resolveConfig(command));
|
|
2106
|
+
writeRawJson(await client.request("GET", `/v1/workspaces/${options.workspace}/media/folders/${options.id}`));
|
|
2107
|
+
});
|
|
2108
|
+
addConnectionOptions(program.command("media:folders:path")).description("Get the path for one media folder.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <folderId>", "Folder ID.").action(async (options, command) => {
|
|
2109
|
+
const client = createApiClient(resolveConfig(command));
|
|
2110
|
+
writeRawJson(await client.request("GET", `/v1/workspaces/${options.workspace}/media/folders/${options.id}/path`));
|
|
2111
|
+
});
|
|
2112
|
+
addConnectionOptions(program.command("media:folders:create")).description("Create a media folder from a JSON payload file.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--json <file>", "JSON file containing the folder payload.").action(async (options, command) => {
|
|
2113
|
+
const payload = createMediaFolderSchema.parse(await readJsonFile(options.json));
|
|
2114
|
+
const client = createApiClient(resolveConfig(command));
|
|
2115
|
+
writeRawJson(
|
|
2116
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/media/folders`, {
|
|
2117
|
+
body: payload
|
|
2118
|
+
})
|
|
2119
|
+
);
|
|
2120
|
+
});
|
|
2121
|
+
addConnectionOptions(program.command("media:folders:update")).description("Update a media folder from a JSON payload file.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <folderId>", "Folder ID.").requiredOption("--json <file>", "JSON file containing fields to update.").action(async (options, command) => {
|
|
2122
|
+
const payload = updateMediaFolderSchema.parse(await readJsonFile(options.json));
|
|
2123
|
+
const client = createApiClient(resolveConfig(command));
|
|
2124
|
+
writeRawJson(
|
|
2125
|
+
await client.request("PATCH", `/v1/workspaces/${options.workspace}/media/folders/${options.id}`, {
|
|
2126
|
+
body: payload
|
|
2127
|
+
})
|
|
2128
|
+
);
|
|
2129
|
+
});
|
|
2130
|
+
addConnectionOptions(program.command("media:folders:move")).description("Move a media folder from a JSON payload file.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <folderId>", "Folder ID.").requiredOption("--json <file>", "JSON file containing parentId; use null for root.").action(async (options, command) => {
|
|
2131
|
+
const payload = moveMediaFolderSchema.parse(await readJsonFile(options.json));
|
|
2132
|
+
const client = createApiClient(resolveConfig(command));
|
|
2133
|
+
writeRawJson(
|
|
2134
|
+
await client.request("PATCH", `/v1/workspaces/${options.workspace}/media/folders/${options.id}/move`, {
|
|
2135
|
+
body: payload
|
|
2136
|
+
})
|
|
2137
|
+
);
|
|
2138
|
+
});
|
|
2139
|
+
addConnectionOptions(program.command("media:folders:delete")).description("Delete one media folder.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <folderId>", "Folder ID.").action(async (options, command) => {
|
|
2140
|
+
const client = createApiClient(resolveConfig(command));
|
|
2141
|
+
writeRawJson(await client.request("DELETE", `/v1/workspaces/${options.workspace}/media/folders/${options.id}`));
|
|
2142
|
+
});
|
|
2143
|
+
addConnectionOptions(program.command("media:labels:list")).description("List media labels.").requiredOption("-w, --workspace <id>", "Workspace ID.").option("--search <text>", "Search media labels.").action(async (options, command) => {
|
|
2144
|
+
const client = createApiClient(resolveConfig(command));
|
|
2145
|
+
writeRawJson(
|
|
2146
|
+
await client.request("GET", `/v1/workspaces/${options.workspace}/media/labels`, {
|
|
2147
|
+
query: { search: options.search }
|
|
2148
|
+
})
|
|
2149
|
+
);
|
|
2150
|
+
});
|
|
2151
|
+
addConnectionOptions(program.command("media:labels:create")).description("Create a media label from a JSON payload file.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--json <file>", "JSON file containing the media label payload.").action(async (options, command) => {
|
|
2152
|
+
const payload = createMediaLabelSchema.parse(await readJsonFile(options.json));
|
|
2153
|
+
const client = createApiClient(resolveConfig(command));
|
|
2154
|
+
writeRawJson(
|
|
2155
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/media/labels`, {
|
|
2156
|
+
body: payload
|
|
2157
|
+
})
|
|
2158
|
+
);
|
|
2159
|
+
});
|
|
2160
|
+
addConnectionOptions(program.command("media:labels:update")).description("Update a media label from a JSON payload file.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <labelId>", "Media label ID.").requiredOption("--json <file>", "JSON file containing fields to update.").action(async (options, command) => {
|
|
2161
|
+
const payload = updateMediaLabelSchema.parse(await readJsonFile(options.json));
|
|
2162
|
+
const client = createApiClient(resolveConfig(command));
|
|
2163
|
+
writeRawJson(
|
|
2164
|
+
await client.request("PATCH", `/v1/workspaces/${options.workspace}/media/labels/${options.id}`, {
|
|
2165
|
+
body: payload
|
|
2166
|
+
})
|
|
2167
|
+
);
|
|
2168
|
+
});
|
|
2169
|
+
addConnectionOptions(program.command("media:labels:delete")).description("Delete one media label.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <labelId>", "Media label ID.").action(async (options, command) => {
|
|
2170
|
+
const client = createApiClient(resolveConfig(command));
|
|
2171
|
+
writeRawJson(await client.request("DELETE", `/v1/workspaces/${options.workspace}/media/labels/${options.id}`));
|
|
2172
|
+
});
|
|
2173
|
+
addConnectionOptions(program.command("media:labels:attach")).description("Attach media labels to one media item from a JSON payload file.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <mediaId>", "Media ID.").requiredOption("--json <file>", "JSON file containing labelIds.").action(async (options, command) => {
|
|
2174
|
+
const payload = mediaLabelIdsSchema.parse(await readJsonFile(options.json));
|
|
2175
|
+
const client = createApiClient(resolveConfig(command));
|
|
2176
|
+
writeRawJson(
|
|
2177
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/media/${options.id}/labels`, {
|
|
2178
|
+
body: payload
|
|
2179
|
+
})
|
|
2180
|
+
);
|
|
2181
|
+
});
|
|
2182
|
+
addConnectionOptions(program.command("media:labels:detach")).description("Detach media labels from one media item from a JSON payload file.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--id <mediaId>", "Media ID.").requiredOption("--json <file>", "JSON file containing labelIds.").action(async (options, command) => {
|
|
2183
|
+
const payload = mediaLabelIdsSchema.parse(await readJsonFile(options.json));
|
|
2184
|
+
const client = createApiClient(resolveConfig(command));
|
|
2185
|
+
writeRawJson(
|
|
2186
|
+
await client.request("DELETE", `/v1/workspaces/${options.workspace}/media/${options.id}/labels`, {
|
|
2187
|
+
body: payload
|
|
2188
|
+
})
|
|
2189
|
+
);
|
|
2190
|
+
});
|
|
2191
|
+
addConnectionOptions(program.command("media:bulk-archive")).description("Archive media in bulk from a JSON payload file.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--json <file>", "JSON file containing mediaIds.").action(async (options, command) => {
|
|
2192
|
+
const payload = mediaIdsSchema.parse(await readJsonFile(options.json));
|
|
2193
|
+
const client = createApiClient(resolveConfig(command));
|
|
2194
|
+
writeRawJson(
|
|
2195
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/media/bulk-archive`, {
|
|
2196
|
+
body: payload
|
|
2197
|
+
})
|
|
2198
|
+
);
|
|
2199
|
+
});
|
|
2200
|
+
addConnectionOptions(program.command("media:bulk-move")).description("Move media in bulk from a JSON payload file.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--json <file>", "JSON file containing mediaIds and optional folderId.").action(async (options, command) => {
|
|
2201
|
+
const payload = mediaBulkMoveSchema.parse(await readJsonFile(options.json));
|
|
2202
|
+
const client = createApiClient(resolveConfig(command));
|
|
2203
|
+
writeRawJson(
|
|
2204
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/media/bulk-move`, {
|
|
2205
|
+
body: payload
|
|
2206
|
+
})
|
|
2207
|
+
);
|
|
2208
|
+
});
|
|
2209
|
+
addConnectionOptions(program.command("media:bulk-label")).description("Attach labels to media in bulk from a JSON payload file.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--json <file>", "JSON file containing mediaIds and labelIds.").action(async (options, command) => {
|
|
2210
|
+
const payload = mediaBulkLabelSchema.parse(await readJsonFile(options.json));
|
|
2211
|
+
const client = createApiClient(resolveConfig(command));
|
|
2212
|
+
writeRawJson(
|
|
2213
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/media/bulk-label`, {
|
|
2214
|
+
body: payload
|
|
2215
|
+
})
|
|
2216
|
+
);
|
|
2217
|
+
});
|
|
2218
|
+
addConnectionOptions(program.command("media:bulk-unlabel")).description("Detach labels from media in bulk from a JSON payload file.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--json <file>", "JSON file containing mediaIds and labelIds.").action(async (options, command) => {
|
|
2219
|
+
const payload = mediaBulkLabelSchema.parse(await readJsonFile(options.json));
|
|
2220
|
+
const client = createApiClient(resolveConfig(command));
|
|
2221
|
+
writeRawJson(
|
|
2222
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/media/bulk-unlabel`, {
|
|
2223
|
+
body: payload
|
|
2224
|
+
})
|
|
2225
|
+
);
|
|
2226
|
+
});
|
|
2227
|
+
addConnectionOptions(program.command("media:bulk-delete")).description("Move media to the Bin in bulk from a JSON payload file.").requiredOption("-w, --workspace <id>", "Workspace ID.").requiredOption("--json <file>", "JSON file containing mediaIds.").option("--yes", "Confirm deletion of the explicit media IDs.").action(async (options, command) => {
|
|
2228
|
+
if (!options.yes) {
|
|
2229
|
+
throw new CliError("media:bulk-delete requires --yes.", { code: "INVALID_OPTIONS" });
|
|
2230
|
+
}
|
|
2231
|
+
const payload = mediaIdsSchema.parse(await readJsonFile(options.json));
|
|
2232
|
+
const client = createApiClient(resolveConfig(command));
|
|
2233
|
+
writeRawJson(
|
|
2234
|
+
await client.request("POST", `/v1/workspaces/${options.workspace}/media/bulk-delete`, {
|
|
2235
|
+
body: payload
|
|
2236
|
+
})
|
|
2237
|
+
);
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
// src/commands/taxonomy.ts
|
|
2242
|
+
function registerTaxonomyCommands(program) {
|
|
2243
|
+
addConnectionOptions(program.command("taxonomy:labels")).description("List workspace labels usable in content labelIds.").requiredOption("-w, --workspace <id>", "Workspace ID.").option("--search <text>", "Case-insensitive label name search.").action(async (options, command) => {
|
|
2244
|
+
const client = createApiClient(resolveConfig(command));
|
|
2245
|
+
writeRawJson(
|
|
2246
|
+
await client.request("GET", `/v1/workspaces/${options.workspace}/labels`, {
|
|
2247
|
+
query: { search: options.search }
|
|
2248
|
+
})
|
|
2249
|
+
);
|
|
2250
|
+
});
|
|
2251
|
+
addConnectionOptions(program.command("taxonomy:pillars")).description("List workspace content pillars usable in content pillarIds.").requiredOption("-w, --workspace <id>", "Workspace ID.").action(async (options, command) => {
|
|
2252
|
+
const client = createApiClient(resolveConfig(command));
|
|
2253
|
+
writeRawJson(await client.request("GET", `/v1/workspaces/${options.workspace}/pillars`));
|
|
2254
|
+
});
|
|
2255
|
+
addConnectionOptions(program.command("taxonomy:formats")).description("List workspace content formats usable in content formatIds.").requiredOption("-w, --workspace <id>", "Workspace ID.").action(async (options, command) => {
|
|
2256
|
+
const client = createApiClient(resolveConfig(command));
|
|
2257
|
+
writeRawJson(await client.request("GET", `/v1/workspaces/${options.workspace}/formats`));
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// src/commands/workspaces.ts
|
|
2262
|
+
function registerWorkspaceCommands(program) {
|
|
2263
|
+
addConnectionOptions(program.command("workspaces:list")).description("List workspaces accessible to the API key.").action(async (_options, command) => {
|
|
2264
|
+
const client = createApiClient(resolveConfig(command));
|
|
2265
|
+
writeRawJson(await client.request("GET", "/v1/workspaces"));
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
// src/index.ts
|
|
2270
|
+
function isCommanderError(error) {
|
|
2271
|
+
return error instanceof Error && "code" in error && typeof error.code === "string" && "exitCode" in error;
|
|
2272
|
+
}
|
|
2273
|
+
function readPackageVersion() {
|
|
2274
|
+
try {
|
|
2275
|
+
const packageJson = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
|
|
2276
|
+
return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
|
|
2277
|
+
} catch {
|
|
2278
|
+
return "0.0.0";
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
function createProgram() {
|
|
2282
|
+
const program = new Command();
|
|
2283
|
+
program.name("maeve").description("Automate the Maeve public API from the command line.").version(readPackageVersion()).option("--api-url <url>", "Maeve API base URL.").option("--api-key-env <name>", "Name of an environment variable containing a Maeve API key.").option("--api-key <key>", "Maeve API key. Prefer MAEVE_API_KEY for local shells.").exitOverride().configureOutput({
|
|
2284
|
+
writeErr: () => void 0
|
|
2285
|
+
});
|
|
2286
|
+
registerAuthCommands(program);
|
|
2287
|
+
registerWorkspaceCommands(program);
|
|
2288
|
+
registerIntegrationCommands(program);
|
|
2289
|
+
registerMediaCommands(program);
|
|
2290
|
+
registerContentCommands(program);
|
|
2291
|
+
registerClientReviewCommands(program);
|
|
2292
|
+
registerTaxonomyCommands(program);
|
|
2293
|
+
registerHashtagCommands(program);
|
|
2294
|
+
registerAnalyticsCommands(program);
|
|
2295
|
+
registerInboxCommands(program);
|
|
2296
|
+
registerGridPlannerCommands(program);
|
|
2297
|
+
return program;
|
|
2298
|
+
}
|
|
2299
|
+
async function runCli(argv = process.argv) {
|
|
2300
|
+
const program = createProgram();
|
|
2301
|
+
try {
|
|
2302
|
+
await program.parseAsync(argv);
|
|
2303
|
+
} catch (error) {
|
|
2304
|
+
if (isCommanderError(error)) {
|
|
2305
|
+
if (error.exitCode === 0) {
|
|
2306
|
+
process.exitCode = 0;
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
writeError(
|
|
2310
|
+
new CliError(error.message.replace(/^error:\s*/i, ""), {
|
|
2311
|
+
code: "USAGE_ERROR",
|
|
2312
|
+
details: { commanderCode: error.code }
|
|
2313
|
+
})
|
|
2314
|
+
);
|
|
2315
|
+
} else {
|
|
2316
|
+
writeError(error);
|
|
2317
|
+
}
|
|
2318
|
+
process.exitCode = 1;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
2322
|
+
migrateLegacyCredentialFile();
|
|
2323
|
+
void runCli();
|
|
2324
|
+
}
|
|
2325
|
+
export {
|
|
2326
|
+
createProgram,
|
|
2327
|
+
runCli
|
|
2328
|
+
};
|