postemi 0.0.0-stage → 1.0.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/dist/index.js ADDED
@@ -0,0 +1,2480 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __defProps = Object.defineProperties;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
8
+ var __getOwnPropNames = Object.getOwnPropertyNames;
9
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
10
+ var __getProtoOf = Object.getPrototypeOf;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
13
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
14
+ var __spreadValues = (a, b) => {
15
+ for (var prop in b || (b = {}))
16
+ if (__hasOwnProp.call(b, prop))
17
+ __defNormalProp(a, prop, b[prop]);
18
+ if (__getOwnPropSymbols)
19
+ for (var prop of __getOwnPropSymbols(b)) {
20
+ if (__propIsEnum.call(b, prop))
21
+ __defNormalProp(a, prop, b[prop]);
22
+ }
23
+ return a;
24
+ };
25
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
26
+ var __objRest = (source, exclude) => {
27
+ var target = {};
28
+ for (var prop in source)
29
+ if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
30
+ target[prop] = source[prop];
31
+ if (source != null && __getOwnPropSymbols)
32
+ for (var prop of __getOwnPropSymbols(source)) {
33
+ if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
34
+ target[prop] = source[prop];
35
+ }
36
+ return target;
37
+ };
38
+ var __copyProps = (to, from, except, desc) => {
39
+ if (from && typeof from === "object" || typeof from === "function") {
40
+ for (let key of __getOwnPropNames(from))
41
+ if (!__hasOwnProp.call(to, key) && key !== except)
42
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
43
+ }
44
+ return to;
45
+ };
46
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
47
+ // If the importer is in node compatibility mode or this is not an ESM
48
+ // file that has been converted to a CommonJS file using a Babel-
49
+ // compatible transform (i.e. "__esModule" has not been set), then set
50
+ // "default" to the CommonJS "module.exports" for node compatibility.
51
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
52
+ mod
53
+ ));
54
+
55
+ // src/index.ts
56
+ var import_yargs = __toESM(require("yargs"));
57
+ var import_helpers = require("yargs/helpers");
58
+
59
+ // src/api.ts
60
+ var import_crypto = require("crypto");
61
+
62
+ // src/brand.ts
63
+ var BRAND_ID = true ? "postemi" : "posty";
64
+ var IS_POSTEMI = BRAND_ID === "postemi";
65
+ var POSTY = {
66
+ name: "Posty",
67
+ cli: "posty",
68
+ envPrefix: "POSTY",
69
+ configDirName: ".posty",
70
+ defaultApiUrl: "https://posty.hu/api",
71
+ defaultAuthServer: "https://posty.hu/api",
72
+ clientName: "Posty CLI",
73
+ homepage: "https://posty.hu",
74
+ termsUrl: "https://posty.hu/aszf",
75
+ privacyUrl: "https://posty.hu/adatvedelem",
76
+ signupUrl: "https://posty.hu/regisztracio",
77
+ exampleTimezone: "Europe/Budapest",
78
+ exampleTimezoneCity: "Budapest",
79
+ tierList: "alap, pro, kreator or csapat",
80
+ basicPlanName: "Alap"
81
+ };
82
+ var POSTEMI = {
83
+ name: "Postemi",
84
+ cli: "postemi",
85
+ envPrefix: "POSTEMI",
86
+ configDirName: ".postemi",
87
+ // api.postemi.com serves the API at the root (its proxy adds /api), the
88
+ // same way api.posty.hu does.
89
+ defaultApiUrl: "https://api.postemi.com",
90
+ defaultAuthServer: "https://api.postemi.com",
91
+ clientName: "Postemi CLI",
92
+ homepage: "https://postemi.com",
93
+ termsUrl: "https://postemi.com/terms",
94
+ privacyUrl: "https://postemi.com/privacy",
95
+ signupUrl: "https://postemi.com/app/signup",
96
+ exampleTimezone: "Europe/London",
97
+ exampleTimezoneCity: "London",
98
+ tierList: "basic, pro, creator or team",
99
+ basicPlanName: "Basic"
100
+ };
101
+ var BRAND = IS_POSTEMI ? POSTEMI : POSTY;
102
+ var envName = (name) => `${BRAND.envPrefix}_${name}`;
103
+ function readEnv(name) {
104
+ const own = process.env[envName(name)];
105
+ if (own || !IS_POSTEMI) return own;
106
+ return process.env[`POSTY_${name}`];
107
+ }
108
+ function envSourceName(name) {
109
+ if (IS_POSTEMI && !process.env[envName(name)] && process.env[`POSTY_${name}`]) {
110
+ return `POSTY_${name}`;
111
+ }
112
+ return envName(name);
113
+ }
114
+ function writeEnv(name, value) {
115
+ process.env[envName(name)] = value;
116
+ }
117
+ var POSTEMI_TIER_ALIASES = {
118
+ basic: "alap",
119
+ team: "csapat"
120
+ };
121
+ function tierForApi(tier) {
122
+ var _a;
123
+ if (!IS_POSTEMI) return tier;
124
+ return (_a = POSTEMI_TIER_ALIASES[tier.trim().toLowerCase()]) != null ? _a : tier;
125
+ }
126
+ var NETWORK_REASONS = {
127
+ ENOTFOUND: "the host name does not resolve",
128
+ EAI_AGAIN: "the host name could not be looked up",
129
+ ECONNREFUSED: "the connection was refused",
130
+ ECONNRESET: "the connection was reset",
131
+ ETIMEDOUT: "the connection timed out",
132
+ UND_ERR_CONNECT_TIMEOUT: "the connection timed out",
133
+ CERT_HAS_EXPIRED: "the TLS certificate has expired",
134
+ DEPTH_ZERO_SELF_SIGNED_CERT: "the TLS certificate is self-signed"
135
+ };
136
+ function networkError(prefix, error, variable = "API_URL") {
137
+ var _a, _b, _c;
138
+ const plain = `${prefix}: ${error == null ? void 0 : error.message}`;
139
+ if (!IS_POSTEMI) return plain;
140
+ const code = (_b = (_a = error == null ? void 0 : error.cause) == null ? void 0 : _a.code) != null ? _b : error == null ? void 0 : error.code;
141
+ const reason = code ? (_c = NETWORK_REASONS[code]) != null ? _c : code : null;
142
+ return reason ? `${prefix}: ${reason} (${code}). Check the connection, or point ${envName(variable)} at another server.` : plain;
143
+ }
144
+
145
+ // src/api.ts
146
+ function serverSentence(body) {
147
+ var _a, _b, _c;
148
+ const raw = (body != null ? body : "").trim();
149
+ let json = null;
150
+ try {
151
+ json = JSON.parse(raw);
152
+ } catch (e) {
153
+ return raw;
154
+ }
155
+ const pick = (value) => {
156
+ if (typeof value === "string" && value.trim()) return value.trim();
157
+ if (Array.isArray(value)) {
158
+ const parts = value.filter((part) => typeof part === "string" && part.trim());
159
+ return parts.length ? parts.join("; ") : null;
160
+ }
161
+ return null;
162
+ };
163
+ return (_c = (_b = (_a = pick(json == null ? void 0 : json.message)) != null ? _a : pick(json == null ? void 0 : json.msg)) != null ? _b : pick(json == null ? void 0 : json.detail)) != null ? _c : raw;
164
+ }
165
+ var ApiError = class extends Error {
166
+ constructor(status2, body) {
167
+ super(`API Error (${status2}): ${serverSentence(body)}`);
168
+ this.status = status2;
169
+ this.body = body;
170
+ this.name = "ApiError";
171
+ }
172
+ /** The credential itself was rejected (missing, unknown, expired). 401 only. */
173
+ get isAuthError() {
174
+ return this.status === 401;
175
+ }
176
+ /**
177
+ * A valid credential that is not allowed to do this: a scoped key on a
178
+ * route that needs a full-workspace one, a role below the payer, a closed
179
+ * registration. The body says which; the fix is never `auth:login`.
180
+ */
181
+ get isForbidden() {
182
+ return this.status === 403;
183
+ }
184
+ /** The body as JSON when it is JSON, else null. */
185
+ get json() {
186
+ try {
187
+ return JSON.parse(this.body);
188
+ } catch (e) {
189
+ return null;
190
+ }
191
+ }
192
+ /**
193
+ * The 403 of the scope guard: the credential is VALID and lacks a
194
+ * permission. Logging in again changes nothing; a key with the scope
195
+ * (or a higher role for its owner) does. Null for any other answer.
196
+ */
197
+ get missingScope() {
198
+ var _a, _b;
199
+ if (this.status !== 403) return null;
200
+ const body = this.json;
201
+ const msg = String((_b = (_a = body == null ? void 0 : body.msg) != null ? _a : body == null ? void 0 : body.message) != null ? _b : this.body);
202
+ if (!/missing the required permission/i.test(msg)) return null;
203
+ const required = Array.isArray(body == null ? void 0 : body.required) ? body.required : [];
204
+ const granted = Array.isArray(body == null ? void 0 : body.granted) ? body.granted : [];
205
+ const fromText = (msg.split(":")[1] || "").split(",").map((scope) => scope.trim()).filter(Boolean);
206
+ const missing = required.length ? required.filter((scope) => !granted.includes(scope)) : fromText;
207
+ return { missing, granted, role: body == null ? void 0 : body.role };
208
+ }
209
+ /**
210
+ * The 401 of the plan gate: the credential is valid, the workspace has no
211
+ * plan with API access. The billing commands still work.
212
+ */
213
+ get isPlanMissing() {
214
+ return this.status === 401 && /no subscription found|does not include api access|no plan with api access/i.test(
215
+ this.body
216
+ );
217
+ }
218
+ get isRateLimit() {
219
+ return this.status === 429;
220
+ }
221
+ /**
222
+ * The 403 a multi-workspace key gets when no workspace was named. Not an
223
+ * auth failure: the key is fine, the request is missing a choice, and the
224
+ * fix is `workspaces:use`, not `auth:login`.
225
+ */
226
+ get isWorkspaceChoice() {
227
+ return this.status === 403 && this.body.includes("spans several workspaces");
228
+ }
229
+ };
230
+ var PostyAPI = class {
231
+ constructor(config) {
232
+ this.apiKey = config.apiKey;
233
+ this.workspaceId = config.workspaceId;
234
+ this.apiUrl = config.apiUrl || BRAND.defaultApiUrl;
235
+ }
236
+ async request(endpoint, options = {}) {
237
+ const url = `${this.apiUrl}${endpoint}`;
238
+ const headers = __spreadValues(__spreadValues({
239
+ "Content-Type": "application/json",
240
+ Authorization: this.apiKey
241
+ }, this.workspaceHeader()), options.headers);
242
+ let response;
243
+ try {
244
+ response = await fetch(url, __spreadProps(__spreadValues({}, options), {
245
+ headers
246
+ }));
247
+ } catch (error) {
248
+ throw new Error(networkError(`Could not reach ${this.apiUrl}`, error));
249
+ }
250
+ if (!response.ok) {
251
+ const error = await response.text();
252
+ throw new ApiError(response.status, error);
253
+ }
254
+ return await response.json();
255
+ }
256
+ /**
257
+ * ONE IDEMPOTENCY KEY PER INVOCATION, REUSED ON ITS OWN RETRY.
258
+ *
259
+ * A timeout cannot tell a lost request from a lost answer, and resending a
260
+ * post blindly can publish it twice on a real account. So the post carries
261
+ * an `Idempotency-Key` (a fresh UUID for this command), and the one retry
262
+ * below, after a network failure or a 5xx, sends the SAME key: the server
263
+ * then hands back the post it already created instead of making another.
264
+ */
265
+ async createPost(data, idempotencyKey = (0, import_crypto.randomUUID)()) {
266
+ const send = () => this.request("/public/v1/posts", {
267
+ method: "POST",
268
+ body: JSON.stringify(data),
269
+ headers: { "Idempotency-Key": idempotencyKey }
270
+ });
271
+ try {
272
+ return await send();
273
+ } catch (error) {
274
+ const retryable = !(error instanceof ApiError) || [500, 502, 503, 504].includes(error.status);
275
+ if (!retryable) throw error;
276
+ await new Promise((resolve) => setTimeout(resolve, 1500));
277
+ return send();
278
+ }
279
+ }
280
+ async listPosts(filters = {}) {
281
+ const queryString = new URLSearchParams(
282
+ Object.entries(filters).reduce((acc, [key, value]) => {
283
+ if (value !== void 0 && value !== null) {
284
+ acc[key] = String(value);
285
+ }
286
+ return acc;
287
+ }, {})
288
+ ).toString();
289
+ const endpoint = queryString ? `/public/v1/posts?${queryString}` : "/public/v1/posts";
290
+ return this.request(endpoint, {
291
+ method: "GET"
292
+ });
293
+ }
294
+ /** One post (one channel) with its thread or comments. Other channels of its group stay. */
295
+ async deletePost(id) {
296
+ return this.request(`/public/v1/posts/${encodeURIComponent(id)}`, {
297
+ method: "DELETE"
298
+ });
299
+ }
300
+ /** The whole group: every channel of one multi-channel post. */
301
+ async deletePostGroup(group) {
302
+ return this.request(`/public/v1/posts/group/${encodeURIComponent(group)}`, {
303
+ method: "DELETE"
304
+ });
305
+ }
306
+ async findSlot(integrationId) {
307
+ return this.request(
308
+ `/public/v1/find-slot/${encodeURIComponent(integrationId)}`,
309
+ {
310
+ method: "GET"
311
+ }
312
+ );
313
+ }
314
+ async upload(file, filename) {
315
+ var _a;
316
+ const formData = new FormData();
317
+ const extension = ((_a = filename.split(".").pop()) == null ? void 0 : _a.toLowerCase()) || "";
318
+ const mimeTypes = {
319
+ png: "image/png",
320
+ jpg: "image/jpeg",
321
+ jpeg: "image/jpeg",
322
+ gif: "image/gif",
323
+ webp: "image/webp",
324
+ avif: "image/avif",
325
+ bmp: "image/bmp",
326
+ tif: "image/tiff",
327
+ tiff: "image/tiff",
328
+ mp4: "video/mp4"
329
+ };
330
+ const type = mimeTypes[extension] || "application/octet-stream";
331
+ const blob = new Blob([new Uint8Array(file)], { type });
332
+ formData.append("file", blob, filename);
333
+ const url = `${this.apiUrl}/public/v1/upload`;
334
+ let response;
335
+ try {
336
+ response = await fetch(url, {
337
+ method: "POST",
338
+ body: formData,
339
+ headers: __spreadValues({
340
+ Authorization: this.apiKey
341
+ }, this.workspaceHeader())
342
+ });
343
+ } catch (error) {
344
+ throw new Error(networkError(`Could not reach ${this.apiUrl}`, error));
345
+ }
346
+ if (!response.ok) {
347
+ const error = await response.text();
348
+ throw new ApiError(response.status, error);
349
+ }
350
+ return await response.json();
351
+ }
352
+ workspaceHeader() {
353
+ return this.workspaceId ? { showorg: this.workspaceId } : {};
354
+ }
355
+ async listWorkspaces() {
356
+ return await this.request("/public/v1/workspaces", {
357
+ method: "GET"
358
+ });
359
+ }
360
+ async getMissingContent(postId) {
361
+ return this.request(`/public/v1/posts/${postId}/missing`, {
362
+ method: "GET"
363
+ });
364
+ }
365
+ async updateReleaseId(postId, releaseId) {
366
+ return this.request(`/public/v1/posts/${postId}/release-id`, {
367
+ method: "PUT",
368
+ body: JSON.stringify({ releaseId })
369
+ });
370
+ }
371
+ async changePostStatus(postId, status2) {
372
+ return this.request(`/public/v1/posts/${postId}/status`, {
373
+ method: "PUT",
374
+ body: JSON.stringify({ status: status2 })
375
+ });
376
+ }
377
+ async getAnalytics(integrationId, date) {
378
+ return this.request(`/public/v1/analytics/${integrationId}?date=${encodeURIComponent(date)}`, {
379
+ method: "GET"
380
+ });
381
+ }
382
+ async getPostAnalytics(postId, date) {
383
+ return this.request(`/public/v1/analytics/post/${postId}?date=${encodeURIComponent(date)}`, {
384
+ method: "GET"
385
+ });
386
+ }
387
+ async listIntegrations(group) {
388
+ const query = group ? `?group=${encodeURIComponent(group)}` : "";
389
+ return this.request(`/public/v1/integrations${query}`, {
390
+ method: "GET"
391
+ });
392
+ }
393
+ async listGroups() {
394
+ return this.request("/public/v1/groups", {
395
+ method: "GET"
396
+ });
397
+ }
398
+ async getIntegrationSettings(integrationId) {
399
+ return this.request(`/public/v1/integration-settings/${integrationId}`, {
400
+ method: "GET"
401
+ });
402
+ }
403
+ async triggerIntegrationTool(integrationId, methodName, data) {
404
+ return this.request(`/public/v1/integration-trigger/${integrationId}`, {
405
+ method: "POST",
406
+ body: JSON.stringify({ methodName, data })
407
+ });
408
+ }
409
+ /**
410
+ * A browser upload link for a file this process does not hold. `upload`
411
+ * covers the file on disk; this covers the one on the person's phone.
412
+ */
413
+ async createUploadLink() {
414
+ return await this.request("/public/v1/upload-link", {
415
+ method: "POST",
416
+ body: JSON.stringify({})
417
+ });
418
+ }
419
+ async getUploadLinkFiles(id) {
420
+ return await this.request(
421
+ `/public/v1/upload-link/${encodeURIComponent(id)}`,
422
+ { method: "GET" }
423
+ );
424
+ }
425
+ /*
426
+ BILLING. These four answer without the plan gate on the server, so they
427
+ work for a workspace that has no subscription yet: they are how it gets
428
+ one. The checkout and the portal need an OAuth token or a full-workspace
429
+ key (the "whole workspace" box on the `auth:login` approval page);
430
+ a scoped key is refused with a sentence that says so.
431
+ */
432
+ async getPlans() {
433
+ return await this.request("/public/v1/plans", { method: "GET" });
434
+ }
435
+ async getSubscription() {
436
+ return await this.request("/public/v1/subscription", {
437
+ method: "GET"
438
+ });
439
+ }
440
+ /** Nothing is charged by this call; the person pays on the returned link. */
441
+ async createSubscriptionCheckout(tier, period) {
442
+ return await this.request("/public/v1/subscriptions/checkout", {
443
+ method: "POST",
444
+ body: JSON.stringify({ tier, period })
445
+ });
446
+ }
447
+ async getSubscriptionPortal() {
448
+ return await this.request("/public/v1/subscription/portal", {
449
+ method: "GET"
450
+ });
451
+ }
452
+ /*
453
+ FOTOAI (fotoai.hu). Generation with the key owner's OWN FotoAI account and
454
+ credits; the outputs are saved into this workspace's media library. A key
455
+ whose owner has not connected FotoAI gets 409 FOTOAI_NOT_CONNECTED with
456
+ `details.connect_url`, the page the person opens once. Needs media:write.
457
+ */
458
+ async fotoAiStatus() {
459
+ return this.request("/public/v1/fotoai/status", { method: "GET" });
460
+ }
461
+ async fotoAiModels(category) {
462
+ const query = category ? `?category=${encodeURIComponent(category)}` : "";
463
+ return this.request(`/public/v1/fotoai/models${query}`, { method: "GET" });
464
+ }
465
+ /** Spends nothing: the price, the balance and a quote_id valid 10 minutes. */
466
+ async fotoAiQuote(body) {
467
+ return this.request("/public/v1/fotoai/quote", {
468
+ method: "POST",
469
+ body: JSON.stringify(body)
470
+ });
471
+ }
472
+ /** SPENDS the key owner's FotoAI credits, at most the quoted price. */
473
+ async fotoAiGenerate(body) {
474
+ return this.request("/public/v1/fotoai/generations", {
475
+ method: "POST",
476
+ body: JSON.stringify(body)
477
+ });
478
+ }
479
+ async fotoAiGeneration(id) {
480
+ return this.request(`/public/v1/fotoai/generations/${encodeURIComponent(id)}`, {
481
+ method: "GET"
482
+ });
483
+ }
484
+ /** Imports any FotoAI generation of the key owner into the media library; repeatable. */
485
+ async fotoAiImport(id) {
486
+ return this.request(
487
+ `/public/v1/fotoai/generations/${encodeURIComponent(id)}/import`,
488
+ { method: "POST" }
489
+ );
490
+ }
491
+ };
492
+
493
+ // src/commands/auth.ts
494
+ var import_fs2 = require("fs");
495
+ var import_path2 = require("path");
496
+ var import_os2 = require("os");
497
+ var import_readline = require("readline");
498
+
499
+ // src/settings.ts
500
+ var import_fs = require("fs");
501
+ var import_path = require("path");
502
+ var import_os = require("os");
503
+ var SETTINGS_DIR = (0, import_path.join)((0, import_os.homedir)(), BRAND.configDirName);
504
+ var SETTINGS_FILE = (0, import_path.join)(SETTINGS_DIR, "config.json");
505
+ var KNOWN_SETTINGS = ["timezone", "workspace"];
506
+ function settingsFilePath() {
507
+ return SETTINGS_FILE;
508
+ }
509
+ function loadSettings() {
510
+ try {
511
+ if (!(0, import_fs.existsSync)(SETTINGS_FILE)) return {};
512
+ const data = JSON.parse((0, import_fs.readFileSync)(SETTINGS_FILE, "utf-8"));
513
+ return data && typeof data === "object" ? data : {};
514
+ } catch (e) {
515
+ return {};
516
+ }
517
+ }
518
+ function getSetting(key) {
519
+ const value = loadSettings()[key];
520
+ return typeof value === "string" && value ? value : void 0;
521
+ }
522
+ function saveSetting(key, value) {
523
+ if (!(0, import_fs.existsSync)(SETTINGS_DIR)) {
524
+ (0, import_fs.mkdirSync)(SETTINGS_DIR, { recursive: true, mode: 448 });
525
+ }
526
+ const settings = loadSettings();
527
+ settings[key] = value;
528
+ (0, import_fs.writeFileSync)(SETTINGS_FILE, JSON.stringify(settings, null, 2) + "\n", {
529
+ encoding: "utf-8"
530
+ });
531
+ }
532
+
533
+ // src/output.ts
534
+ function result(data) {
535
+ console.log(JSON.stringify(data, null, 2));
536
+ }
537
+ function status(message) {
538
+ console.error(message);
539
+ }
540
+ function fail(context, error) {
541
+ console.error(`\u274C ${context}: ${(error == null ? void 0 : error.message) || error}`);
542
+ if (error instanceof ApiError && error.isWorkspaceChoice) {
543
+ console.error(
544
+ `This key spans several workspaces and none is selected. Run "${BRAND.cli} workspaces:list", then "${BRAND.cli} workspaces:use <id>" (or pass --workspace <id>).`
545
+ );
546
+ } else if (error instanceof ApiError && error.missingScope) {
547
+ const { missing, role } = error.missingScope;
548
+ console.error(
549
+ `This key is valid but lacks the permission ${missing.join(", ") || "this command needs"}${role ? ` (the key owner's role: ${role})` : ""}. Create a key that includes it in ${BRAND.name} (Settings, Developers), or ask a workspace owner.`
550
+ );
551
+ } else if (error instanceof ApiError && error.isPlanMissing) {
552
+ console.error(
553
+ `The credential is valid, but this workspace has no plan with API access yet. Run "${BRAND.cli} billing:plans", then "${BRAND.cli} billing:subscribe --tier <slug> --period <monthly|yearly>".`
554
+ );
555
+ } else if (error instanceof ApiError && error.isForbidden) {
556
+ console.error(
557
+ "The credential is valid but not allowed to do this. The message above says why."
558
+ );
559
+ } else if (error instanceof ApiError && error.isAuthError) {
560
+ console.error(
561
+ `Your credentials were rejected. Run "${BRAND.cli} auth:login" to re-authenticate, or check ${envName("API_KEY")}.`
562
+ );
563
+ }
564
+ if (error instanceof ApiError && error.isRateLimit) {
565
+ console.error(
566
+ "Rate limit hit (per key, per route, per hour). Back off \u2014 do not retry in a loop."
567
+ );
568
+ }
569
+ process.exit(1);
570
+ }
571
+
572
+ // src/dates.ts
573
+ var HAS_DESIGNATOR = /(Z|[+-]\d{2}:?\d{2})$/i;
574
+ var NAIVE_SHAPE = /^(\d{4})-(\d{2})-(\d{2})([T ](\d{2}):(\d{2})(:(\d{2}))?)?$/;
575
+ var OFFSET_SHAPED = /^(Etc\/)?(UTC|GMT)?\s*[+-]?\d/i;
576
+ function assertIanaTimezone(name, origin) {
577
+ const value = name.trim();
578
+ if (!value || OFFSET_SHAPED.test(value)) {
579
+ throw new Error(
580
+ `Invalid timezone "${name}" (from ${origin}): numeric offsets are not accepted as timezones \u2014 DST would be your problem to compute. Use an IANA name like "${BRAND.exampleTimezone}" (or put the offset inside the date string itself, e.g. "2026-12-31T12:00:00+01:00").`
581
+ );
582
+ }
583
+ try {
584
+ new Intl.DateTimeFormat("en-US", { timeZone: value });
585
+ } catch (e) {
586
+ throw new Error(
587
+ `Unknown timezone "${name}" (from ${origin}). Use an IANA name like "${BRAND.exampleTimezone}" or "UTC".`
588
+ );
589
+ }
590
+ return value;
591
+ }
592
+ function resolveTimezone(flagValue) {
593
+ if (flagValue !== void 0) {
594
+ return {
595
+ name: assertIanaTimezone(flagValue, "--timezone"),
596
+ source: "--timezone"
597
+ };
598
+ }
599
+ const env = readEnv("TIMEZONE");
600
+ if (env) {
601
+ return {
602
+ name: assertIanaTimezone(env, envName("TIMEZONE")),
603
+ source: envName("TIMEZONE")
604
+ };
605
+ }
606
+ const stored = getSetting("timezone");
607
+ if (stored) {
608
+ return {
609
+ name: assertIanaTimezone(stored, `~/${BRAND.configDirName}/config.json`),
610
+ source: `config (${BRAND.cli} config:set timezone)`
611
+ };
612
+ }
613
+ return null;
614
+ }
615
+ function tzOffsetMinutes(instant, timeZone) {
616
+ const parts = new Intl.DateTimeFormat("en-US", {
617
+ timeZone,
618
+ year: "numeric",
619
+ month: "2-digit",
620
+ day: "2-digit",
621
+ hour: "2-digit",
622
+ minute: "2-digit",
623
+ second: "2-digit",
624
+ hour12: false
625
+ }).formatToParts(instant);
626
+ const get = (type) => {
627
+ var _a;
628
+ return Number((_a = parts.find((p) => p.type === type)) == null ? void 0 : _a.value);
629
+ };
630
+ const asUtc = Date.UTC(
631
+ get("year"),
632
+ get("month") - 1,
633
+ get("day"),
634
+ get("hour") % 24,
635
+ get("minute"),
636
+ get("second")
637
+ );
638
+ return Math.round((asUtc - instant.getTime()) / 6e4);
639
+ }
640
+ function zonedToUtc(y, mo, d, h, mi, s, timeZone) {
641
+ const guess = Date.UTC(y, mo - 1, d, h, mi, s);
642
+ let offset = tzOffsetMinutes(new Date(guess), timeZone);
643
+ let instant = guess - offset * 6e4;
644
+ const refined = tzOffsetMinutes(new Date(instant), timeZone);
645
+ if (refined !== offset) {
646
+ instant = guess - refined * 6e4;
647
+ }
648
+ return new Date(instant);
649
+ }
650
+ function naiveDateError(input, flagName) {
651
+ return new Error(
652
+ `Date "${input}" has no timezone, and no timezone is configured. The server would read it as UTC, silently shifting your post. Fix one of:
653
+ 1. Explicit offset in the date: ${flagName} "2026-12-31T12:00:00+01:00" (or ...T12:00:00Z for UTC)
654
+ 2. Timezone flag: --timezone ${BRAND.exampleTimezone}
655
+ 3. Environment variable: export ${envName("TIMEZONE")}=${BRAND.exampleTimezone}
656
+ 4. Save it once: ${BRAND.cli} config:set timezone ${BRAND.exampleTimezone}`
657
+ );
658
+ }
659
+ function resolveDateInput(input, timezone, flagName = "--date") {
660
+ var _a, _b, _c;
661
+ const value = input.trim();
662
+ if (HAS_DESIGNATOR.test(value)) {
663
+ const parsed = new Date(value);
664
+ if (isNaN(parsed.getTime())) {
665
+ throw new Error(
666
+ `Invalid date "${input}". Use ISO 8601, e.g. "2026-12-31T12:00:00Z" or "2026-12-31T12:00:00+01:00".`
667
+ );
668
+ }
669
+ return { iso: parsed.toISOString(), appliedTimezone: null };
670
+ }
671
+ const m = NAIVE_SHAPE.exec(value);
672
+ if (!m) {
673
+ throw new Error(
674
+ `Invalid date "${input}". Use ISO 8601 with an explicit offset ("2026-12-31T12:00:00Z"), or "YYYY-MM-DD HH:mm" together with a timezone (--timezone ${BRAND.exampleTimezone}).`
675
+ );
676
+ }
677
+ if (!timezone) {
678
+ throw naiveDateError(input, flagName);
679
+ }
680
+ const instant = zonedToUtc(
681
+ Number(m[1]),
682
+ Number(m[2]),
683
+ Number(m[3]),
684
+ Number((_a = m[5]) != null ? _a : 0),
685
+ Number((_b = m[6]) != null ? _b : 0),
686
+ Number((_c = m[8]) != null ? _c : 0),
687
+ timezone.name
688
+ );
689
+ if (isNaN(instant.getTime())) {
690
+ throw new Error(`Invalid date "${input}".`);
691
+ }
692
+ return { iso: instant.toISOString(), appliedTimezone: timezone };
693
+ }
694
+
695
+ // src/commands/auth.ts
696
+ var CREDENTIALS_DIR = (0, import_path2.join)((0, import_os2.homedir)(), BRAND.configDirName);
697
+ var CREDENTIALS_FILE = (0, import_path2.join)(CREDENTIALS_DIR, "credentials.json");
698
+ var DEFAULT_AUTH_SERVER = BRAND.defaultAuthServer;
699
+ var DEFAULT_API_URL = BRAND.defaultApiUrl;
700
+ function loadCredentials() {
701
+ try {
702
+ if (!(0, import_fs2.existsSync)(CREDENTIALS_FILE)) return null;
703
+ const data = JSON.parse((0, import_fs2.readFileSync)(CREDENTIALS_FILE, "utf-8"));
704
+ if (!data.accessToken) return null;
705
+ return data;
706
+ } catch (e) {
707
+ return null;
708
+ }
709
+ }
710
+ function saveCredentials(credentials) {
711
+ if (!(0, import_fs2.existsSync)(CREDENTIALS_DIR)) {
712
+ (0, import_fs2.mkdirSync)(CREDENTIALS_DIR, { recursive: true, mode: 448 });
713
+ }
714
+ (0, import_fs2.writeFileSync)(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { encoding: "utf-8", mode: 384 });
715
+ (0, import_fs2.chmodSync)(CREDENTIALS_FILE, 384);
716
+ }
717
+ function deleteCredentials() {
718
+ if ((0, import_fs2.existsSync)(CREDENTIALS_FILE)) {
719
+ (0, import_fs2.unlinkSync)(CREDENTIALS_FILE);
720
+ }
721
+ }
722
+ function openBrowser(url) {
723
+ const { spawn } = require("child_process");
724
+ const opts = { stdio: "ignore", detached: true };
725
+ const child = process.platform === "darwin" ? spawn("open", [url], opts) : process.platform === "win32" ? spawn("cmd", ["/c", "start", "", url], opts) : spawn("xdg-open", [url], opts);
726
+ child.on("error", () => {
727
+ });
728
+ child.unref();
729
+ }
730
+ function sleep(ms) {
731
+ return new Promise((resolve) => setTimeout(resolve, ms));
732
+ }
733
+ function ask(question) {
734
+ const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stderr });
735
+ return new Promise(
736
+ (resolve) => rl.question(question, (answer) => {
737
+ rl.close();
738
+ resolve(answer);
739
+ })
740
+ );
741
+ }
742
+ async function maybeConfigureTimezone() {
743
+ if (getSetting("timezone")) return;
744
+ if (!process.stdin.isTTY || !process.stderr.isTTY) return;
745
+ const detected = Intl.DateTimeFormat().resolvedOptions().timeZone;
746
+ const answer = (await ask(`
747
+ Scheduling timezone: ${detected} \u2014 OK? [Y/n] `)).trim().toLowerCase();
748
+ let timezone = detected;
749
+ if (answer === "n" || answer === "no") {
750
+ const typed = (await ask(`Enter an IANA timezone name (e.g. ${BRAND.exampleTimezone}), or leave empty to skip: `)).trim();
751
+ if (!typed) {
752
+ console.error(
753
+ `Skipped. Dates without an explicit offset will be refused until you run: ${BRAND.cli} config:set timezone <IANA name>`
754
+ );
755
+ return;
756
+ }
757
+ try {
758
+ timezone = assertIanaTimezone(typed, "auth:login");
759
+ } catch (error) {
760
+ console.error(error.message);
761
+ console.error(
762
+ `Skipped. Set it later with: ${BRAND.cli} config:set timezone <IANA name>`
763
+ );
764
+ return;
765
+ }
766
+ } else if (answer && answer !== "y" && answer !== "yes") {
767
+ console.error(
768
+ `Skipped. Set it later with: ${BRAND.cli} config:set timezone <IANA name>`
769
+ );
770
+ return;
771
+ }
772
+ saveSetting("timezone", timezone);
773
+ console.error(
774
+ `\u2705 Scheduling timezone saved: ${timezone} (change it with "${BRAND.cli} config:set timezone <IANA name>")`
775
+ );
776
+ }
777
+ async function chooseWorkspace(creds) {
778
+ let workspaces;
779
+ try {
780
+ workspaces = await new PostyAPI({
781
+ apiKey: creds.accessToken,
782
+ apiUrl: creds.apiUrl
783
+ }).listWorkspaces();
784
+ } catch (e) {
785
+ return;
786
+ }
787
+ if (workspaces.length === 1) {
788
+ saveCredentials(__spreadProps(__spreadValues({}, creds), { organizationId: workspaces[0].id }));
789
+ console.error(`\u{1F3E2} Workspace: ${workspaces[0].name}`);
790
+ return;
791
+ }
792
+ if (workspaces.length < 2) return;
793
+ console.error("\nThis key reaches several workspaces:");
794
+ workspaces.forEach((w, i) => console.error(` ${i + 1}. ${w.name} (${w.id})`));
795
+ if (!process.stdin.isTTY || !process.stderr.isTTY) {
796
+ console.error(
797
+ `Pick one with: ${BRAND.cli} workspaces:use <id> (or pass --workspace <id> on each command)`
798
+ );
799
+ return;
800
+ }
801
+ const answer = (await ask(`Work in which one? [1-${workspaces.length}] `)).trim();
802
+ const index = Number(answer) - 1;
803
+ const chosen = workspaces[index];
804
+ if (!chosen) {
805
+ console.error(`Skipped. Pick one later with: ${BRAND.cli} workspaces:use <id>`);
806
+ return;
807
+ }
808
+ saveCredentials(__spreadProps(__spreadValues({}, creds), { organizationId: chosen.id }));
809
+ console.error(`\u2705 Workspace: ${chosen.name} (change it with "${BRAND.cli} workspaces:use <id>")`);
810
+ }
811
+ async function authLogin(argv) {
812
+ const authServer = argv.authServer || readEnv("AUTH_SERVER") || DEFAULT_AUTH_SERVER;
813
+ console.log("\u{1F510} Starting device authorization flow...\n");
814
+ let deviceCode;
815
+ let userCode;
816
+ let verificationUri;
817
+ let expiresIn;
818
+ let interval;
819
+ try {
820
+ const response = await fetch(`${authServer}/device/code`, {
821
+ method: "POST",
822
+ headers: { "Content-Type": "application/json" },
823
+ // Shown on the approval page, so somebody can tell the login they just
824
+ // started from one they did not.
825
+ body: JSON.stringify(__spreadValues({
826
+ client_name: readEnv("CLIENT_NAME") || BRAND.clientName
827
+ }, IS_POSTEMI ? { brand: "postemi" } : {}))
828
+ });
829
+ if (!response.ok) {
830
+ const error = await response.text();
831
+ console.error(`\u274C Failed to start authorization (${response.status}): ${error}`);
832
+ process.exit(1);
833
+ }
834
+ const data = await response.json();
835
+ deviceCode = data.device_code;
836
+ userCode = data.user_code;
837
+ verificationUri = data.verification_uri;
838
+ expiresIn = data.expires_in;
839
+ interval = data.interval || 5;
840
+ } catch (error) {
841
+ console.error(`\u274C ${networkError(`Could not reach auth server at ${authServer}`, error, "AUTH_SERVER")}`);
842
+ process.exit(1);
843
+ }
844
+ console.log(" Your authorization code:\n");
845
+ console.log(` \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510`);
846
+ console.log(` \u2502 ${userCode} \u2502`);
847
+ console.log(` \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
848
+ `);
849
+ console.log(` Open this URL and enter the code above:`);
850
+ console.log(` ${verificationUri}
851
+ `);
852
+ openBrowser(`${verificationUri}?code=${encodeURIComponent(userCode)}`);
853
+ console.log(" Waiting for authorization...\n");
854
+ const deadline = Date.now() + expiresIn * 1e3;
855
+ while (Date.now() < deadline) {
856
+ await sleep(interval * 1e3);
857
+ try {
858
+ const response = await fetch(`${authServer}/device/token`, {
859
+ method: "POST",
860
+ headers: { "Content-Type": "application/json" },
861
+ body: JSON.stringify({ device_code: deviceCode })
862
+ });
863
+ const data = await response.json();
864
+ if (response.ok && data.access_token) {
865
+ const creds = {
866
+ accessToken: data.access_token,
867
+ /*
868
+ Posty stores the API base the server names. The Postemi build
869
+ stores the host it logged in on (api.postemi.com): the server
870
+ names Posty's base for every brand, and the Postemi host is what
871
+ makes the API answer as Postemi.
872
+ */
873
+ apiUrl: IS_POSTEMI ? String(authServer).replace(/\/+$/, "") : data.api_url || DEFAULT_API_URL,
874
+ organizationId: data.organization_id
875
+ };
876
+ saveCredentials(creds);
877
+ console.log("\u2705 Successfully authenticated!");
878
+ console.log(`\u{1F4C1} Credentials saved to ${CREDENTIALS_FILE}`);
879
+ await chooseWorkspace(creds);
880
+ await maybeConfigureTimezone();
881
+ return;
882
+ }
883
+ if (data.error === "authorization_pending") {
884
+ continue;
885
+ }
886
+ if (data.error === "expired_token") {
887
+ console.error("\u274C Authorization expired. Please try again.");
888
+ process.exit(1);
889
+ }
890
+ console.error(`\u274C Authorization failed: ${data.error}`);
891
+ process.exit(1);
892
+ } catch (e) {
893
+ continue;
894
+ }
895
+ }
896
+ console.error("\u274C Authorization timed out. Please try again.");
897
+ process.exit(1);
898
+ }
899
+ var TERMS_URL = BRAND.termsUrl;
900
+ var PRIVACY_URL = BRAND.privacyUrl;
901
+ async function signupRequest(apiUrl, path, body) {
902
+ const response = await fetch(`${apiUrl}/public/v1${path}`, {
903
+ method: "POST",
904
+ headers: { "Content-Type": "application/json" },
905
+ body: JSON.stringify(body)
906
+ });
907
+ const text = await response.text();
908
+ let data = text;
909
+ try {
910
+ data = JSON.parse(text);
911
+ } catch (e) {
912
+ }
913
+ return { status: response.status, data };
914
+ }
915
+ function shellWord(value) {
916
+ return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
917
+ }
918
+ function signupTimezone(argv) {
919
+ const chosen = argv.timezone || getSetting("timezone");
920
+ if (chosen) return String(chosen);
921
+ try {
922
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || void 0;
923
+ } catch (e) {
924
+ return void 0;
925
+ }
926
+ }
927
+ function refusal(data) {
928
+ var _a;
929
+ if (typeof data === "string") return data;
930
+ const message = (_a = data == null ? void 0 : data.msg) != null ? _a : data == null ? void 0 : data.message;
931
+ return Array.isArray(message) ? message.join("; ") : String(message != null ? message : JSON.stringify(data));
932
+ }
933
+ async function authSignup(argv) {
934
+ const email = String(argv.email || "").trim();
935
+ const apiUrl = String(
936
+ argv.apiUrl || readEnv("API_URL") || DEFAULT_API_URL
937
+ ).replace(/\/+$/, "");
938
+ const language = argv.language ? String(argv.language) : void 0;
939
+ const timezone = signupTimezone(argv);
940
+ const interactive = !!process.stdin.isTTY && !!process.stderr.isTTY;
941
+ if (!email.includes("@")) {
942
+ console.error("\u274C Give the e-mail address of the new account with --email.");
943
+ process.exit(1);
944
+ }
945
+ let code = argv.code ? String(argv.code).trim() : "";
946
+ try {
947
+ if (!code) {
948
+ const started = await signupRequest(apiUrl, "/signup/start", __spreadValues({
949
+ email
950
+ }, language ? { language } : {}));
951
+ if (started.status !== 202 && started.status !== 200) {
952
+ console.error(
953
+ `\u274C Could not send the code (HTTP ${started.status}): ${refusal(started.data)}`
954
+ );
955
+ process.exit(1);
956
+ }
957
+ status(`\u{1F4E7} A six-digit code was sent to ${email}. It is valid for 10 minutes.`);
958
+ status(" Accounts created this way have no free trial; a plan is chosen and paid for next.");
959
+ if (!interactive) {
960
+ const nextCommand = [
961
+ `${BRAND.cli} auth:signup`,
962
+ `--email ${shellWord(email)}`,
963
+ "--code <code>",
964
+ "--accept-terms",
965
+ ...argv.apiUrl ? [`--api-url ${shellWord(apiUrl)}`] : [],
966
+ ...language ? [`--language ${language}`] : [],
967
+ ...argv.workspaceName ? [`--workspace-name ${shellWord(String(argv.workspaceName))}`] : [],
968
+ ...timezone ? [`--timezone ${shellWord(timezone)}`] : []
969
+ ].join(" ");
970
+ result({
971
+ codeSent: true,
972
+ email,
973
+ trial: false,
974
+ terms: TERMS_URL,
975
+ privacy: PRIVACY_URL,
976
+ timezone: timezone != null ? timezone : null,
977
+ language: language != null ? language : null,
978
+ next: `Ask the person for the code, show them ${TERMS_URL} and ${PRIVACY_URL}, and when they agree run: ${nextCommand}`
979
+ });
980
+ return;
981
+ }
982
+ code = (await ask("Code from the e-mail: ")).trim();
983
+ if (!code) {
984
+ console.error("\u274C No code given. Run the command again with --code <code>.");
985
+ process.exit(1);
986
+ }
987
+ }
988
+ if (!/^[0-9]{6}$/.test(code)) {
989
+ console.error("\u274C The code is the six digits from the e-mail.");
990
+ process.exit(1);
991
+ }
992
+ status("");
993
+ status("To create the account the person must agree to:");
994
+ status(` Terms of service: ${TERMS_URL}`);
995
+ status(` Privacy notice: ${PRIVACY_URL}`);
996
+ let accepted = !!argv.acceptTerms;
997
+ if (!accepted && interactive) {
998
+ const answer = (await ask("Do you agree to both? [y/N] ")).trim().toLowerCase();
999
+ accepted = answer === "y" || answer === "yes" || answer === "i" || answer === "igen";
1000
+ }
1001
+ if (!accepted) {
1002
+ console.error(
1003
+ "\u274C No account was created: the terms were not accepted. Show the person the two links above; if they agree, run the command again with --accept-terms."
1004
+ );
1005
+ process.exit(1);
1006
+ }
1007
+ const completed = await signupRequest(apiUrl, "/signup/complete", __spreadValues(__spreadValues(__spreadValues({
1008
+ email,
1009
+ code,
1010
+ acceptTerms: true
1011
+ }, argv.workspaceName ? { workspaceName: String(argv.workspaceName) } : {}), timezone ? { timezone: String(timezone) } : {}), language ? { language } : {}));
1012
+ if (completed.status === 409) {
1013
+ console.error(`\u274C ${refusal(completed.data)}`);
1014
+ console.error(` Sign in instead: ${BRAND.cli} auth:login`);
1015
+ process.exit(1);
1016
+ }
1017
+ if (completed.status !== 201 && completed.status !== 200) {
1018
+ console.error(
1019
+ `\u274C The account was not created (HTTP ${completed.status}): ${refusal(completed.data)}`
1020
+ );
1021
+ process.exit(1);
1022
+ }
1023
+ const data = completed.data || {};
1024
+ if (!data.apiKey) {
1025
+ console.error(`\u274C The server answered without a key. Sign in with: ${BRAND.cli} auth:login`);
1026
+ process.exit(1);
1027
+ }
1028
+ if (loadCredentials()) {
1029
+ status("\u2139\uFE0F The credentials stored before are replaced by the new account's key.");
1030
+ }
1031
+ saveCredentials({
1032
+ accessToken: data.apiKey,
1033
+ apiUrl,
1034
+ organizationId: data.workspaceId
1035
+ });
1036
+ status(`\u2705 Account created. Workspace: ${data.workspaceName}`);
1037
+ status(`\u{1F4C1} Key saved to ${CREDENTIALS_FILE}`);
1038
+ status("");
1039
+ status("No free trial on this account. Next:");
1040
+ status(` ${BRAND.cli} billing:plans`);
1041
+ status(` ${BRAND.cli} billing:subscribe --tier <slug> --period <monthly|yearly>`);
1042
+ status(`The person opens the checkout link and pays. This key then works for every command only on a plan with API access (apiAndMcpAccess: true in billing:plans); ${BRAND.basicPlanName} has none, so say so before the person picks it.`);
1043
+ result({
1044
+ created: true,
1045
+ workspaceId: data.workspaceId,
1046
+ workspaceName: data.workspaceName,
1047
+ trial: false,
1048
+ credentials: CREDENTIALS_FILE,
1049
+ next: [
1050
+ `${BRAND.cli} billing:plans`,
1051
+ `${BRAND.cli} billing:subscribe --tier <slug> --period <monthly|yearly>`,
1052
+ `${BRAND.cli} billing:status`
1053
+ ],
1054
+ note: `This key keeps working after the payment only on a plan with apiAndMcpAccess: true in billing:plans. ${BRAND.basicPlanName} has no API access: tell the person before they pay for it.`
1055
+ });
1056
+ } catch (error) {
1057
+ console.error(`\u274C ${networkError(`Could not reach the ${BRAND.name} API at ${apiUrl}`, error)}`);
1058
+ process.exit(1);
1059
+ }
1060
+ }
1061
+ async function authLogout() {
1062
+ const creds = loadCredentials();
1063
+ if (!creds) {
1064
+ console.log("\u2139\uFE0F No stored credentials found.");
1065
+ return;
1066
+ }
1067
+ deleteCredentials();
1068
+ console.log("\u2705 Credentials removed.");
1069
+ }
1070
+ async function authStatus() {
1071
+ var _a;
1072
+ const envKey = readEnv("API_KEY");
1073
+ const creds = loadCredentials();
1074
+ const hint = (token) => `${token.split("_")[0]}_\u2026`;
1075
+ const methodOf = (token) => token.startsWith("pos_") ? "oauth2" : "api-key";
1076
+ const label = (m) => m === "oauth2" ? "OAuth token" : "API key";
1077
+ let apiKey;
1078
+ let apiUrl;
1079
+ let method;
1080
+ let source;
1081
+ if (creds) {
1082
+ method = methodOf(creds.accessToken);
1083
+ source = "credentials-file";
1084
+ apiKey = creds.accessToken;
1085
+ apiUrl = creds.apiUrl;
1086
+ status(`\u{1F510} Authentication method: ${label(method)} (stored by auth:login or auth:signup)`);
1087
+ status(`\u{1F4E1} API URL: ${creds.apiUrl}`);
1088
+ status(`\u{1F511} Credential: ${hint(creds.accessToken)}`);
1089
+ if (creds.organizationId) {
1090
+ status(`\u{1F3E2} Workspace: ${creds.organizationId}`);
1091
+ }
1092
+ status(`\u{1F4C1} Credentials file: ${CREDENTIALS_FILE}`);
1093
+ } else if (envKey) {
1094
+ method = methodOf(envKey);
1095
+ source = "environment";
1096
+ apiKey = envKey;
1097
+ apiUrl = readEnv("API_URL") || DEFAULT_API_URL;
1098
+ status(`\u{1F511} Authentication method: ${label(method)} (${envSourceName("API_KEY")})`);
1099
+ status(`\u{1F511} Credential: ${hint(envKey)}`);
1100
+ } else {
1101
+ status("\u274C Not authenticated.");
1102
+ status("");
1103
+ status("Options:");
1104
+ status(` 1. Device login: ${BRAND.cli} auth:login`);
1105
+ status(` 2. New account: ${BRAND.cli} auth:signup --email <address>`);
1106
+ status(` 3. API Key: export ${envName("API_KEY")}=your_api_key`);
1107
+ result({ authenticated: false, reason: "no-credentials" });
1108
+ process.exit(1);
1109
+ }
1110
+ status("");
1111
+ status("\u{1F504} Verifying credentials...");
1112
+ const workspaceId = getWorkspaceId();
1113
+ const base = { method, source, apiUrl, workspace: workspaceId != null ? workspaceId : null };
1114
+ let subscription;
1115
+ try {
1116
+ subscription = await new PostyAPI({
1117
+ apiKey,
1118
+ apiUrl,
1119
+ workspaceId
1120
+ }).getSubscription();
1121
+ } catch (error) {
1122
+ if (error instanceof ApiError && error.isWorkspaceChoice) {
1123
+ status("\u2705 Credentials are valid, but this key spans several workspaces and none is selected.");
1124
+ status(` Run: ${BRAND.cli} workspaces:list, then ${BRAND.cli} workspaces:use <id>`);
1125
+ result(__spreadProps(__spreadValues({ authenticated: true }, base), { reason: "choose-workspace" }));
1126
+ process.exit(1);
1127
+ }
1128
+ if (error instanceof ApiError && error.status === 401) {
1129
+ status("\u274C Credentials are expired or invalid. Please re-authenticate.");
1130
+ status(
1131
+ creds ? ` Run: ${BRAND.cli} auth:login` : ` Update your ${envName("API_KEY")} environment variable.`
1132
+ );
1133
+ result(__spreadProps(__spreadValues({ authenticated: false }, base), { reason: "rejected" }));
1134
+ process.exit(1);
1135
+ }
1136
+ if (error instanceof ApiError) {
1137
+ status(`\u26A0\uFE0F Could not verify credentials (HTTP ${error.status}): ${serverSentence(error.body)}`);
1138
+ result(__spreadProps(__spreadValues({ authenticated: null }, base), { reason: `http-${error.status}` }));
1139
+ process.exit(1);
1140
+ }
1141
+ status(`\u26A0\uFE0F Could not reach API to verify credentials: ${error.message}`);
1142
+ result(__spreadProps(__spreadValues({ authenticated: null }, base), { reason: "unreachable" }));
1143
+ process.exit(1);
1144
+ }
1145
+ if (!subscription.apiAndMcpAccess) {
1146
+ const noPlan = subscription.state === "none" || subscription.state === "cancelled" || subscription.state === "read_only";
1147
+ status(
1148
+ noPlan ? "\u2705 Credentials are valid. This workspace has no plan yet, so only the billing commands work." : `\u2705 Credentials are valid. The current plan (${subscription.plan}) has no API access, so only the billing commands work.`
1149
+ );
1150
+ status(` Next: ${BRAND.cli} billing:plans, then ${BRAND.cli} billing:subscribe --tier <slug> --period <monthly|yearly>`);
1151
+ if (subscription.pendingCheckout) {
1152
+ status(` An unpaid checkout is waiting: ${subscription.pendingCheckout.checkoutUrl}`);
1153
+ }
1154
+ result(__spreadProps(__spreadValues({
1155
+ authenticated: true
1156
+ }, base), {
1157
+ apiAccess: false,
1158
+ plan: subscription.plan,
1159
+ subscriptionState: subscription.state,
1160
+ reason: noPlan ? "no-plan" : "plan-without-api-access"
1161
+ }));
1162
+ return;
1163
+ }
1164
+ let integrations = null;
1165
+ try {
1166
+ const list = await new PostyAPI({
1167
+ apiKey,
1168
+ apiUrl,
1169
+ workspaceId
1170
+ }).listIntegrations();
1171
+ integrations = Array.isArray(list) ? list.length : null;
1172
+ } catch (e) {
1173
+ }
1174
+ status(
1175
+ `\u2705 Credentials are valid. Plan: ${(_a = subscription.plan) != null ? _a : "none"}${integrations === null ? "" : `, ${integrations} channel(s) connected`}.`
1176
+ );
1177
+ result(__spreadProps(__spreadValues({
1178
+ authenticated: true
1179
+ }, base), {
1180
+ apiAccess: true,
1181
+ plan: subscription.plan,
1182
+ subscriptionState: subscription.state,
1183
+ integrations
1184
+ }));
1185
+ }
1186
+
1187
+ // src/config.ts
1188
+ function getWorkspaceId() {
1189
+ const fromEnv = readEnv("WORKSPACE");
1190
+ if (fromEnv) return fromEnv;
1191
+ const creds = loadCredentials();
1192
+ if (creds == null ? void 0 : creds.organizationId) return creds.organizationId;
1193
+ return getSetting("workspace");
1194
+ }
1195
+ function getConfig() {
1196
+ const workspaceId = getWorkspaceId();
1197
+ const creds = loadCredentials();
1198
+ if (creds) {
1199
+ return {
1200
+ apiKey: creds.accessToken,
1201
+ apiUrl: creds.apiUrl,
1202
+ workspaceId
1203
+ };
1204
+ }
1205
+ const apiKey = readEnv("API_KEY");
1206
+ const apiUrl = readEnv("API_URL");
1207
+ if (!apiKey) {
1208
+ console.error("\u274C Error: No authentication found.");
1209
+ console.error("Options:");
1210
+ console.error(` 1. Device login: ${BRAND.cli} auth:login`);
1211
+ console.error(` 2. API Key: export ${envName("API_KEY")}=your_api_key`);
1212
+ process.exit(1);
1213
+ }
1214
+ return {
1215
+ apiKey,
1216
+ apiUrl,
1217
+ workspaceId
1218
+ };
1219
+ }
1220
+
1221
+ // src/commands/posts.ts
1222
+ var import_fs3 = require("fs");
1223
+ var pairContentsWithMedia = (argv) => {
1224
+ const read = (token, next, long, short) => {
1225
+ if (token === `--${long}` || token === `-${short}`) {
1226
+ return next === void 0 ? null : { value: next, consumed: 2 };
1227
+ }
1228
+ if (token.startsWith(`--${long}=`)) {
1229
+ return { value: token.slice(long.length + 3), consumed: 1 };
1230
+ }
1231
+ return null;
1232
+ };
1233
+ const pairs = [];
1234
+ for (let i = 0; i < argv.length; ) {
1235
+ const content = read(argv[i], argv[i + 1], "content", "c");
1236
+ if (content) {
1237
+ pairs.push({ content: content.value });
1238
+ i += content.consumed;
1239
+ continue;
1240
+ }
1241
+ const media = read(argv[i], argv[i + 1], "media", "m");
1242
+ if (media) {
1243
+ if (!pairs.length) {
1244
+ console.error("\u274C --media/-m must come after the --content/-c it belongs to");
1245
+ process.exit(1);
1246
+ }
1247
+ const last = pairs[pairs.length - 1];
1248
+ last.media = last.media ? `${last.media},${media.value}` : media.value;
1249
+ i += media.consumed;
1250
+ continue;
1251
+ }
1252
+ i += 1;
1253
+ }
1254
+ return pairs;
1255
+ };
1256
+ function timezoneForArgs(args) {
1257
+ try {
1258
+ return resolveTimezone(args.timezone);
1259
+ } catch (error) {
1260
+ console.error(`\u274C ${error.message}`);
1261
+ process.exit(1);
1262
+ }
1263
+ }
1264
+ function echoInterpretation(input, resolved) {
1265
+ if (resolved.appliedTimezone) {
1266
+ status(
1267
+ `\u2139\uFE0F Interpreted "${input}" as ${resolved.appliedTimezone.name} (via ${resolved.appliedTimezone.source}) \u2192 ${resolved.iso}`
1268
+ );
1269
+ }
1270
+ }
1271
+ async function getMissingContent(args) {
1272
+ const config = getConfig();
1273
+ const api = new PostyAPI(config);
1274
+ try {
1275
+ const res = await api.getMissingContent(args.id);
1276
+ result(res);
1277
+ return res;
1278
+ } catch (error) {
1279
+ fail("Failed to get missing content", error);
1280
+ }
1281
+ }
1282
+ async function connectPost(args) {
1283
+ const config = getConfig();
1284
+ const api = new PostyAPI(config);
1285
+ try {
1286
+ const res = await api.updateReleaseId(args.id, args.releaseId);
1287
+ status(`\u2705 Post ${args.id} connected to release ${args.releaseId}`);
1288
+ result(res);
1289
+ return res;
1290
+ } catch (error) {
1291
+ fail("Failed to connect post", error);
1292
+ }
1293
+ }
1294
+ async function findSlot(args) {
1295
+ const config = getConfig();
1296
+ const api = new PostyAPI(config);
1297
+ try {
1298
+ const res = await api.findSlot(args.id);
1299
+ result(res);
1300
+ return res;
1301
+ } catch (error) {
1302
+ fail("Failed to find a free slot", error);
1303
+ }
1304
+ }
1305
+ async function createPost(args) {
1306
+ const config = getConfig();
1307
+ const api = new PostyAPI(config);
1308
+ let postData;
1309
+ if (args.json) {
1310
+ try {
1311
+ const jsonPath = args.json;
1312
+ if (!(0, import_fs3.existsSync)(jsonPath)) {
1313
+ console.error(`\u274C JSON file not found: ${jsonPath}`);
1314
+ process.exit(1);
1315
+ }
1316
+ const jsonContent = (0, import_fs3.readFileSync)(jsonPath, "utf-8");
1317
+ postData = JSON.parse(jsonContent);
1318
+ } catch (error) {
1319
+ console.error(`\u274C Failed to parse JSON file: ${error.message}`);
1320
+ process.exit(1);
1321
+ }
1322
+ } else {
1323
+ const integrations = args.integrations ? args.integrations.split(",").map((id) => id.trim()) : [];
1324
+ if (integrations.length === 0) {
1325
+ console.error("\u274C At least one integration ID is required");
1326
+ console.error("Use -i or --integrations to specify integration IDs");
1327
+ console.error(`Run "${BRAND.cli} integrations:list" to see available integrations`);
1328
+ process.exit(1);
1329
+ }
1330
+ const contents = Array.isArray(args.content) ? args.content : [args.content];
1331
+ if (!contents[0]) {
1332
+ console.error("\u274C At least one -c/--content is required");
1333
+ process.exit(1);
1334
+ }
1335
+ const pairs = pairContentsWithMedia(process.argv);
1336
+ if (pairs.length && pairs.length !== contents.length) {
1337
+ console.error(
1338
+ `\u274C Could not match --media to --content reliably (parsed ${pairs.length} content flags, yargs saw ${contents.length}).`
1339
+ );
1340
+ console.error("Pass the post structure with --json instead.");
1341
+ process.exit(1);
1342
+ }
1343
+ const values = (pairs.length ? pairs : contents.map((content) => ({ content, media: void 0 }))).map(({ content, media }) => ({
1344
+ content,
1345
+ image: media ? media.split(",").filter((img) => img.trim()).map((img) => ({
1346
+ id: Math.random().toString(36).substring(7),
1347
+ path: img.trim()
1348
+ })) : [],
1349
+ delay: (args == null ? void 0 : args.delay) || 0
1350
+ }));
1351
+ let settings = void 0;
1352
+ if (args.settings) {
1353
+ try {
1354
+ settings = typeof args.settings === "string" ? JSON.parse(args.settings) : args.settings;
1355
+ } catch (error) {
1356
+ console.error(`\u274C Failed to parse settings JSON: ${error.message}`);
1357
+ process.exit(1);
1358
+ }
1359
+ }
1360
+ const type = args.type || "schedule";
1361
+ let dateIso;
1362
+ if (!args.date && type === "now") {
1363
+ dateIso = (/* @__PURE__ */ new Date()).toISOString();
1364
+ } else {
1365
+ const timezone = timezoneForArgs(args);
1366
+ try {
1367
+ const resolved = resolveDateInput(args.date, timezone, "--date");
1368
+ dateIso = resolved.iso;
1369
+ echoInterpretation(args.date, resolved);
1370
+ } catch (error) {
1371
+ console.error(`\u274C ${error.message}`);
1372
+ process.exit(1);
1373
+ }
1374
+ }
1375
+ postData = {
1376
+ type,
1377
+ creationMethod: "CLI",
1378
+ date: dateIso,
1379
+ shortLink: args.shortLink !== false,
1380
+ tags: [],
1381
+ posts: integrations.map((integrationId) => ({
1382
+ integration: { id: integrationId },
1383
+ value: values,
1384
+ settings
1385
+ }))
1386
+ };
1387
+ }
1388
+ try {
1389
+ const res = await api.createPost(postData);
1390
+ status("\u2705 Post created successfully!");
1391
+ if ((postData == null ? void 0 : postData.type) && postData.type !== "draft") {
1392
+ status(
1393
+ "Note: a key without posts:publish creates a DRAFT even when you asked to schedule \u2014 check the type in the response."
1394
+ );
1395
+ }
1396
+ result(res);
1397
+ return res;
1398
+ } catch (error) {
1399
+ fail("Failed to create post", error);
1400
+ }
1401
+ }
1402
+ async function listPosts(args) {
1403
+ const config = getConfig();
1404
+ const api = new PostyAPI(config);
1405
+ const defaultStartDate = /* @__PURE__ */ new Date();
1406
+ defaultStartDate.setDate(defaultStartDate.getDate() - 30);
1407
+ const defaultEndDate = /* @__PURE__ */ new Date();
1408
+ defaultEndDate.setDate(defaultEndDate.getDate() + 30);
1409
+ const timezone = args.startDate || args.endDate ? timezoneForArgs(args) : null;
1410
+ const resolve = (label, value, fallback) => {
1411
+ if (!value) return fallback.toISOString();
1412
+ try {
1413
+ const resolved = resolveDateInput(value, timezone, label);
1414
+ echoInterpretation(value, resolved);
1415
+ return resolved.iso;
1416
+ } catch (error) {
1417
+ console.error(`\u274C ${error.message}`);
1418
+ process.exit(1);
1419
+ }
1420
+ };
1421
+ const filters = {
1422
+ startDate: resolve("--startDate", args.startDate, defaultStartDate),
1423
+ endDate: resolve("--endDate", args.endDate, defaultEndDate)
1424
+ };
1425
+ if (args.customer) {
1426
+ filters.customer = args.customer;
1427
+ }
1428
+ try {
1429
+ const res = await api.listPosts(filters);
1430
+ result(res);
1431
+ return res;
1432
+ } catch (error) {
1433
+ fail("Failed to list posts", error);
1434
+ }
1435
+ }
1436
+ async function changePostStatus(args) {
1437
+ const config = getConfig();
1438
+ const api = new PostyAPI(config);
1439
+ try {
1440
+ const res = await api.changePostStatus(args.id, args.status);
1441
+ status(`\u2705 Post ${args.id} status changed to ${args.status}`);
1442
+ result(res);
1443
+ return res;
1444
+ } catch (error) {
1445
+ fail("Failed to change post status", error);
1446
+ }
1447
+ }
1448
+ async function deletePost(args) {
1449
+ const config = getConfig();
1450
+ const api = new PostyAPI(config);
1451
+ try {
1452
+ const res = await api.deletePost(args.id);
1453
+ status(`\u2705 Post ${args.id} deleted successfully!`);
1454
+ result(res);
1455
+ return res;
1456
+ } catch (error) {
1457
+ fail("Failed to delete post", error);
1458
+ }
1459
+ }
1460
+ async function deletePostGroup(args) {
1461
+ const config = getConfig();
1462
+ const api = new PostyAPI(config);
1463
+ try {
1464
+ const res = await api.deletePostGroup(args.group);
1465
+ status(`\u2705 Post group ${args.group} deleted from every channel`);
1466
+ result(res);
1467
+ return res;
1468
+ } catch (error) {
1469
+ fail("Failed to delete post group", error);
1470
+ }
1471
+ }
1472
+
1473
+ // src/commands/integrations.ts
1474
+ async function listIntegrations(args) {
1475
+ const config = getConfig();
1476
+ const api = new PostyAPI(config);
1477
+ try {
1478
+ const res = await api.listIntegrations(args == null ? void 0 : args.group);
1479
+ result(res);
1480
+ return res;
1481
+ } catch (error) {
1482
+ fail("Failed to list integrations", error);
1483
+ }
1484
+ }
1485
+ async function listGroups() {
1486
+ const config = getConfig();
1487
+ const api = new PostyAPI(config);
1488
+ try {
1489
+ const res = await api.listGroups();
1490
+ result(res);
1491
+ return res;
1492
+ } catch (error) {
1493
+ fail("Failed to list groups", error);
1494
+ }
1495
+ }
1496
+ async function getIntegrationSettings(args) {
1497
+ const config = getConfig();
1498
+ const api = new PostyAPI(config);
1499
+ try {
1500
+ const res = await api.getIntegrationSettings(args.id);
1501
+ result(res);
1502
+ return res;
1503
+ } catch (error) {
1504
+ fail("Failed to get integration settings", error);
1505
+ }
1506
+ }
1507
+ async function triggerIntegrationTool(args) {
1508
+ const config = getConfig();
1509
+ const api = new PostyAPI(config);
1510
+ let data = {};
1511
+ if (args.data) {
1512
+ try {
1513
+ data = JSON.parse(args.data);
1514
+ } catch (error) {
1515
+ console.error(`\u274C Failed to parse data JSON: ${error.message}`);
1516
+ process.exit(1);
1517
+ }
1518
+ }
1519
+ try {
1520
+ const res = await api.triggerIntegrationTool(args.id, args.method, data);
1521
+ result(res);
1522
+ return res;
1523
+ } catch (error) {
1524
+ fail("Failed to trigger tool", error);
1525
+ }
1526
+ }
1527
+
1528
+ // src/commands/analytics.ts
1529
+ function daysArg(args) {
1530
+ var _a;
1531
+ const days = String((_a = args.days) != null ? _a : "7");
1532
+ if (!/^\d+$/.test(days) || Number(days) < 1) {
1533
+ console.error("\u274C --days must be a positive whole number of days, e.g. --days 30");
1534
+ process.exit(1);
1535
+ }
1536
+ return days;
1537
+ }
1538
+ async function getAnalytics(args) {
1539
+ const config = getConfig();
1540
+ const api = new PostyAPI(config);
1541
+ try {
1542
+ const res = await api.getAnalytics(args.id, daysArg(args));
1543
+ result(res);
1544
+ return res;
1545
+ } catch (error) {
1546
+ fail("Failed to get analytics", error);
1547
+ }
1548
+ }
1549
+ async function getPostAnalytics(args) {
1550
+ const config = getConfig();
1551
+ const api = new PostyAPI(config);
1552
+ try {
1553
+ const res = await api.getPostAnalytics(args.id, daysArg(args));
1554
+ result(res);
1555
+ return res;
1556
+ } catch (error) {
1557
+ fail("Failed to get post analytics", error);
1558
+ }
1559
+ }
1560
+
1561
+ // src/commands/upload.ts
1562
+ var import_fs4 = require("fs");
1563
+ var import_path3 = require("path");
1564
+ async function uploadFile(args) {
1565
+ const config = getConfig();
1566
+ const api = new PostyAPI(config);
1567
+ if (!(0, import_fs4.existsSync)(args.file)) {
1568
+ console.error(`\u274C File not found: ${args.file}`);
1569
+ process.exit(1);
1570
+ }
1571
+ try {
1572
+ const fileBuffer = (0, import_fs4.readFileSync)(args.file);
1573
+ const filename = (0, import_path3.basename)(args.file) || "file";
1574
+ const res = await api.upload(fileBuffer, filename);
1575
+ status("\u2705 File uploaded successfully!");
1576
+ result(res);
1577
+ return res;
1578
+ } catch (error) {
1579
+ fail("Failed to upload file", error);
1580
+ }
1581
+ }
1582
+ async function createUploadLink() {
1583
+ const config = getConfig();
1584
+ const api = new PostyAPI(config);
1585
+ try {
1586
+ const res = await api.createUploadLink();
1587
+ status(
1588
+ `\u{1F517} Upload link created. Open it in a browser, drop the files, then run: ${BRAND.cli} upload:files ${res.id}`
1589
+ );
1590
+ result(res);
1591
+ return res;
1592
+ } catch (error) {
1593
+ fail("Failed to create an upload link", error);
1594
+ }
1595
+ }
1596
+ async function listUploadLinkFiles(args) {
1597
+ const config = getConfig();
1598
+ const api = new PostyAPI(config);
1599
+ try {
1600
+ const res = await api.getUploadLinkFiles(args.id);
1601
+ if (res.status === "empty") {
1602
+ status('\u23F3 Nothing has arrived through this link yet. Ask the person to check the page says "uploaded", then run this again.');
1603
+ } else {
1604
+ status(`\u2705 ${res.count} file(s) arrived. Use each .path in posts:create -m`);
1605
+ }
1606
+ result(res);
1607
+ return res;
1608
+ } catch (error) {
1609
+ fail("Failed to read the upload link", error);
1610
+ }
1611
+ }
1612
+
1613
+ // src/commands/config.ts
1614
+ function assertKnownKey(key) {
1615
+ if (!KNOWN_SETTINGS.includes(key)) {
1616
+ console.error(
1617
+ `\u274C Unknown setting "${key}". Known settings: ${KNOWN_SETTINGS.join(", ")}`
1618
+ );
1619
+ process.exit(1);
1620
+ }
1621
+ return key;
1622
+ }
1623
+ async function configSet(args) {
1624
+ const key = assertKnownKey(args.key);
1625
+ let value = String(args.value);
1626
+ if (key === "timezone") {
1627
+ try {
1628
+ value = assertIanaTimezone(value, "config:set");
1629
+ } catch (error) {
1630
+ console.error(`\u274C ${error.message}`);
1631
+ process.exit(1);
1632
+ }
1633
+ }
1634
+ saveSetting(key, value);
1635
+ status(`\u2705 ${key} = ${value} saved to ${settingsFilePath()}`);
1636
+ result({ [key]: value });
1637
+ }
1638
+ async function configGet(args) {
1639
+ var _a;
1640
+ const settings = loadSettings();
1641
+ if (args.key) {
1642
+ const key = assertKnownKey(args.key);
1643
+ result({ [key]: (_a = settings[key]) != null ? _a : null });
1644
+ return;
1645
+ }
1646
+ result(settings);
1647
+ }
1648
+
1649
+ // src/commands/workspaces.ts
1650
+ async function listWorkspaces() {
1651
+ const api = new PostyAPI(getConfig());
1652
+ try {
1653
+ const res = await api.listWorkspaces();
1654
+ result(res);
1655
+ return res;
1656
+ } catch (error) {
1657
+ fail("Failed to list workspaces", error);
1658
+ }
1659
+ }
1660
+ async function useWorkspace(args) {
1661
+ const id = String(args.id || "").trim();
1662
+ if (!id) {
1663
+ fail("Failed to select workspace", new Error(`Give the workspace id, from "${BRAND.cli} workspaces:list"`));
1664
+ }
1665
+ const api = new PostyAPI(__spreadProps(__spreadValues({}, getConfig()), { workspaceId: void 0 }));
1666
+ let workspaces;
1667
+ try {
1668
+ workspaces = await api.listWorkspaces();
1669
+ } catch (error) {
1670
+ fail("Failed to select workspace", error);
1671
+ }
1672
+ const chosen = workspaces.find((w) => w.id === id);
1673
+ if (!chosen) {
1674
+ fail(
1675
+ "Failed to select workspace",
1676
+ new Error(
1677
+ `This credential cannot act on workspace ${id}. Available: ${workspaces.map((w) => `${w.name} (${w.id})`).join(", ") || "none"}`
1678
+ )
1679
+ );
1680
+ }
1681
+ const creds = loadCredentials();
1682
+ if (creds) {
1683
+ saveCredentials(__spreadProps(__spreadValues({}, creds), { organizationId: chosen.id }));
1684
+ status(`\u2705 Workspace set to ${chosen.name} (saved with your login)`);
1685
+ } else {
1686
+ saveSetting("workspace", chosen.id);
1687
+ status(`\u2705 Workspace set to ${chosen.name} (saved to ${settingsFilePath()})`);
1688
+ }
1689
+ result({ id: chosen.id, name: chosen.name, role: chosen.role });
1690
+ }
1691
+ async function currentWorkspace() {
1692
+ const id = getWorkspaceId();
1693
+ const api = new PostyAPI(getConfig());
1694
+ try {
1695
+ const workspaces = await api.listWorkspaces();
1696
+ const current = workspaces.find((w) => w.current) || null;
1697
+ if (!current && workspaces.length > 1) {
1698
+ status(
1699
+ `No workspace selected and this key spans several. Run: ${BRAND.cli} workspaces:use <id>`
1700
+ );
1701
+ }
1702
+ result(current != null ? current : id ? { id, name: null } : null);
1703
+ } catch (error) {
1704
+ fail("Failed to read the current workspace", error);
1705
+ }
1706
+ }
1707
+
1708
+ // src/commands/billing.ts
1709
+ async function billingPlans() {
1710
+ const api = new PostyAPI(getConfig());
1711
+ try {
1712
+ result(await api.getPlans());
1713
+ } catch (error) {
1714
+ fail("Failed to read the plans", error);
1715
+ }
1716
+ }
1717
+ async function billingSubscribe(args) {
1718
+ const tier = String(args.tier || "").trim();
1719
+ const period = String(args.period || "").trim().toLowerCase();
1720
+ if (!tier) {
1721
+ fail(
1722
+ "Failed to start the subscription",
1723
+ new Error(`Give the plan with --tier (${BRAND.tierList}; see "${BRAND.cli} billing:plans")`)
1724
+ );
1725
+ }
1726
+ if (period !== "monthly" && period !== "yearly") {
1727
+ fail(
1728
+ "Failed to start the subscription",
1729
+ new Error("Give the period with --period monthly or --period yearly")
1730
+ );
1731
+ }
1732
+ const api = new PostyAPI(getConfig());
1733
+ try {
1734
+ const checkout = await api.createSubscriptionCheckout(tierForApi(tier), period);
1735
+ status(
1736
+ `Checkout link for ${checkout.plan} (${checkout.period.toLowerCase()}, ${checkout.amount} ${checkout.currency}${checkout.trial ? `, ${checkout.trialDays}-day free trial` : ""}). The link expires at ${checkout.expiresAt}.`
1737
+ );
1738
+ status(
1739
+ `The subscriber opens this link and pays in Stripe Checkout with their own Link wallet or card. Do not pay with a one-time or agent-issued card: the subscription renews and a one-time card fails at the first renewal. After paying, run "${BRAND.cli} billing:status" to confirm.`
1740
+ );
1741
+ const _a = checkout, { note: _note } = _a, json = __objRest(_a, ["note"]);
1742
+ if (checkout.apiAndMcpAccess === false) {
1743
+ status(
1744
+ `The ${checkout.plan} plan has no API or MCP access: after the payment this key still reaches only the billing commands, and the person uses ${BRAND.name} in the web app. Tell the person before they pay; a plan with apiAndMcpAccess: true in "${BRAND.cli} billing:plans" keeps the CLI working.`
1745
+ );
1746
+ }
1747
+ if (args.open) {
1748
+ openBrowser(checkout.checkoutUrl);
1749
+ status("Opened the checkout in the browser.");
1750
+ }
1751
+ result(json);
1752
+ } catch (error) {
1753
+ fail("Failed to start the subscription", error);
1754
+ }
1755
+ }
1756
+ async function billingStatus() {
1757
+ const api = new PostyAPI(getConfig());
1758
+ try {
1759
+ const subscription = await api.getSubscription();
1760
+ if (subscription.pendingCheckout) {
1761
+ const pending = subscription.pendingCheckout;
1762
+ const when = (iso) => iso ? new Date(iso).toLocaleString() : "an unknown time";
1763
+ status(
1764
+ `A checkout for ${pending.plan}${pending.period ? ` (${pending.period.toLowerCase()})` : ""} started at ${when(pending.startedAt)} has not been paid yet. The link is in pendingCheckout.checkoutUrl and expires at ${when(pending.expiresAt)}.`
1765
+ );
1766
+ }
1767
+ result(subscription);
1768
+ } catch (error) {
1769
+ fail("Failed to read the subscription", error);
1770
+ }
1771
+ }
1772
+ async function billingManage(args) {
1773
+ const api = new PostyAPI(getConfig());
1774
+ try {
1775
+ const portal = await api.getSubscriptionPortal();
1776
+ const _a = portal, { note } = _a, json = __objRest(_a, ["note"]);
1777
+ status(note);
1778
+ if (args.open) {
1779
+ openBrowser(portal.portalUrl);
1780
+ status("Opened the billing portal in the browser.");
1781
+ }
1782
+ result(json);
1783
+ } catch (error) {
1784
+ fail("Failed to open the billing portal", error);
1785
+ }
1786
+ }
1787
+
1788
+ // src/commands/fotoai.ts
1789
+ var import_crypto2 = require("crypto");
1790
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
1791
+ var failFotoAi = (context, error) => {
1792
+ if (error instanceof ApiError) {
1793
+ let body = null;
1794
+ try {
1795
+ body = JSON.parse(error.body);
1796
+ } catch (e) {
1797
+ }
1798
+ if (error.status === 404 && !(body == null ? void 0 : body.code)) {
1799
+ status("\u274C FotoAI is not available on this Posty server yet.");
1800
+ process.exit(1);
1801
+ }
1802
+ if (body == null ? void 0 : body.code) {
1803
+ const details = body.details || {};
1804
+ status(`\u274C ${context}: ${body.msg || body.code} (${body.code})`);
1805
+ if (details.connect_url) {
1806
+ status(`Connect FotoAI in Posty once: ${details.connect_url}`);
1807
+ }
1808
+ if (details.topup_url) {
1809
+ status(`FotoAI credits can be bought at ${details.topup_url}`);
1810
+ }
1811
+ if (typeof details.quote === "number") {
1812
+ status(`The new price is ${details.quote / 1e3} credits. Quote again and confirm it.`);
1813
+ }
1814
+ if (body.retryAfter) {
1815
+ status(`Try again in ${body.retryAfter} seconds.`);
1816
+ }
1817
+ process.exit(1);
1818
+ }
1819
+ }
1820
+ fail(context, error);
1821
+ };
1822
+ var paramsOf = (args) => {
1823
+ const params = {};
1824
+ if (args.aspect) params.aspect = String(args.aspect);
1825
+ if (args.duration) params.duration = Number(args.duration);
1826
+ if (args.resolution) params.resolution = String(args.resolution);
1827
+ return params;
1828
+ };
1829
+ async function fotoAiStatus() {
1830
+ const api = new PostyAPI(getConfig());
1831
+ try {
1832
+ const current = await api.fotoAiStatus();
1833
+ if (!current.connected) {
1834
+ status(
1835
+ current.needsReconnect ? `The FotoAI connection has expired. Connect again in Posty: ${current.connectUrl}` : `FotoAI is not connected. The person connects it once in Posty: ${current.connectUrl}`
1836
+ );
1837
+ }
1838
+ result(current);
1839
+ } catch (error) {
1840
+ failFotoAi("Failed to read the FotoAI status", error);
1841
+ }
1842
+ }
1843
+ async function fotoAiModels(args) {
1844
+ const api = new PostyAPI(getConfig());
1845
+ try {
1846
+ const catalog = await api.fotoAiModels(args.category);
1847
+ const models = (catalog.models || []).map((m) => {
1848
+ var _a, _b;
1849
+ return {
1850
+ id: m.id,
1851
+ name: m.name,
1852
+ category: m.category,
1853
+ can_use: !!m.can_use,
1854
+ estimated_seconds: (_a = m.estimated_seconds) != null ? _a : null,
1855
+ max_duration_seconds: (_b = m.max_duration_seconds) != null ? _b : null,
1856
+ base_price_credits: (() => {
1857
+ var _a2;
1858
+ const prices = (((_a2 = m.price) == null ? void 0 : _a2.configs) || []).map((c) => Number(c.millicredits)).filter((n) => Number.isFinite(n) && n > 0);
1859
+ return prices.length ? Math.min(...prices) / 1e3 : null;
1860
+ })(),
1861
+ params: (m.params || []).map((p) => {
1862
+ var _a2;
1863
+ return {
1864
+ key: p.key,
1865
+ default: (_a2 = p.default) != null ? _a2 : null,
1866
+ options: (p.options || []).map((o) => o.value)
1867
+ };
1868
+ })
1869
+ };
1870
+ });
1871
+ result({ catalog_version: catalog.catalog_version || null, models });
1872
+ } catch (error) {
1873
+ failFotoAi("Failed to list the FotoAI models", error);
1874
+ }
1875
+ }
1876
+ async function fotoAiGet(args) {
1877
+ var _a, _b;
1878
+ const api = new PostyAPI(getConfig());
1879
+ const deadline = Date.now() + Math.max(0, Number(args.wait) || 0) * 1e3;
1880
+ try {
1881
+ let current = await api.fotoAiImport(String(args.id));
1882
+ while ((["queued", "running"].includes(current.status) || current.status === "completed" && !((_a = current.media) == null ? void 0 : _a.length) && !current.import_error) && Date.now() < deadline) {
1883
+ await sleep2(3e3);
1884
+ current = await api.fotoAiImport(String(args.id));
1885
+ }
1886
+ if ((_b = current.media) == null ? void 0 : _b.length) {
1887
+ status(`Saved in the media library. Attach each media[].path with posts:create.`);
1888
+ } else if (["queued", "running"].includes(current.status)) {
1889
+ status(`Still ${current.status}. Run "${BRAND.cli} fotoai:get ${current.id} --wait 60" again.`);
1890
+ }
1891
+ result(current);
1892
+ } catch (error) {
1893
+ failFotoAi("Failed to read the FotoAI generation", error);
1894
+ }
1895
+ }
1896
+ async function fotoAiGenerate(args) {
1897
+ var _a;
1898
+ const api = new PostyAPI(getConfig());
1899
+ const request = {
1900
+ model: String(args.model),
1901
+ prompt: String(args.prompt),
1902
+ params: paramsOf(args)
1903
+ };
1904
+ let quote;
1905
+ try {
1906
+ quote = await api.fotoAiQuote(request);
1907
+ } catch (error) {
1908
+ failFotoAi("Failed to price the FotoAI generation", error);
1909
+ }
1910
+ const balance = typeof quote.available_millicredits === "number" ? `, balance ${quote.available_millicredits / 1e3} credits` : "";
1911
+ if (quote.insufficient) {
1912
+ status(
1913
+ `Not enough FotoAI credits: this costs ${quote.credits} credits${balance}.${quote.topup_url ? ` Buy credits at ${quote.topup_url}` : ""}`
1914
+ );
1915
+ result({ generated: false, quote });
1916
+ process.exit(1);
1917
+ }
1918
+ if (!args.yes) {
1919
+ status(
1920
+ `This costs ${quote.credits} FotoAI credits${balance}. Nothing was spent. Run the same command with --yes to generate.`
1921
+ );
1922
+ result({ generated: false, quote });
1923
+ return;
1924
+ }
1925
+ try {
1926
+ const started = await api.fotoAiGenerate(__spreadProps(__spreadValues({}, request), {
1927
+ quote_id: quote.quote_id,
1928
+ idempotency_key: args.idempotencyKey || (0, import_crypto2.randomUUID)()
1929
+ }));
1930
+ status(`Generating (about ${(_a = started.estimated_seconds) != null ? _a : "?"} seconds), id ${started.id}.`);
1931
+ if (!Number(args.wait)) {
1932
+ status(`Run "${BRAND.cli} fotoai:get ${started.id} --wait 60" for the media paths.`);
1933
+ result(started);
1934
+ return;
1935
+ }
1936
+ await fotoAiGet({ id: started.id, wait: args.wait });
1937
+ } catch (error) {
1938
+ failFotoAi("Failed to start the FotoAI generation", error);
1939
+ }
1940
+ }
1941
+
1942
+ // src/index.ts
1943
+ var cli = (0, import_yargs.default)((0, import_helpers.hideBin)(process.argv)).scriptName(BRAND.cli).usage("$0 <command> [options]").option("workspace", {
1944
+ alias: "w",
1945
+ describe: `Workspace id this command acts on (overrides "${BRAND.cli} workspaces:use" and ${envName("WORKSPACE")})`,
1946
+ type: "string",
1947
+ global: true
1948
+ }).middleware((argv) => {
1949
+ if (argv.workspace) {
1950
+ writeEnv("WORKSPACE", String(argv.workspace));
1951
+ }
1952
+ }).command(
1953
+ "posts:create",
1954
+ "Create a new post (schedule, draft, or publish now)",
1955
+ (yargs2) => {
1956
+ return yargs2.option("content", {
1957
+ alias: "c",
1958
+ describe: "Post/comment content (repeat for a thread: first is the post, the rest are comments)",
1959
+ type: "string"
1960
+ }).option("media", {
1961
+ alias: "m",
1962
+ describe: `Comma-separated media URLs for the corresponding -c (repeatable; URLs must come from "${BRAND.cli} upload")`,
1963
+ type: "string"
1964
+ }).option("integrations", {
1965
+ alias: "i",
1966
+ describe: `Comma-separated list of integration IDs (from "${BRAND.cli} integrations:list")`,
1967
+ type: "string"
1968
+ }).option("date", {
1969
+ alias: "s",
1970
+ describe: `Publish date. ALWAYS give ISO 8601 with an explicit offset ("2026-12-31T12:00:00Z" / "+01:00"), or a datetime without offset ("2026-12-31 12:00") together with a timezone (--timezone / ${envName("TIMEZONE")} / ${BRAND.cli} config:set timezone). A naive date with no timezone configured is an error. Required unless --type now or --json.`,
1971
+ type: "string"
1972
+ }).option("timezone", {
1973
+ describe: `IANA timezone name (e.g. ${BRAND.exampleTimezone}) used to interpret --date when it has no explicit offset. Numeric offsets are not accepted here \u2014 put those in the date string.`,
1974
+ type: "string"
1975
+ }).option("type", {
1976
+ alias: "t",
1977
+ describe: 'Post type: "schedule" (default), "draft", or "now" (publish immediately)',
1978
+ type: "string",
1979
+ choices: ["schedule", "draft", "now"],
1980
+ default: "schedule"
1981
+ }).option("delay", {
1982
+ alias: "d",
1983
+ describe: "Delay in MINUTES between comments (default: 0)",
1984
+ type: "number",
1985
+ default: 0
1986
+ }).option("json", {
1987
+ alias: "j",
1988
+ describe: "Path to JSON file with the full post structure (see examples/)",
1989
+ type: "string"
1990
+ }).option("shortLink", {
1991
+ describe: "Use short links",
1992
+ type: "boolean",
1993
+ default: true
1994
+ }).option("settings", {
1995
+ describe: `Platform-specific settings as JSON string (schema: "${BRAND.cli} integrations:settings <id>")`,
1996
+ type: "string"
1997
+ }).check((argv) => {
1998
+ if (!argv.json && !argv.content) {
1999
+ throw new Error("Either --content or --json is required");
2000
+ }
2001
+ if (!argv.json && !argv.integrations) {
2002
+ throw new Error("--integrations is required when not using --json");
2003
+ }
2004
+ if (!argv.json && !argv.date && argv.type !== "now") {
2005
+ throw new Error("--date is required when not using --json (or use --type now to publish immediately)");
2006
+ }
2007
+ return true;
2008
+ }).example(
2009
+ '$0 posts:create -c "Hello World!" -s "2026-12-31T12:00:00Z" -i "integration-id"',
2010
+ "Simple scheduled post (explicit UTC)"
2011
+ ).example(
2012
+ `$0 posts:create -c "Merry Christmas! \u{1F384}" -s "2026-12-24 18:00" --timezone ${BRAND.exampleTimezone} -i "integration-id"`,
2013
+ "Local wall-clock time with an explicit IANA timezone"
2014
+ ).example(
2015
+ '$0 posts:create -c "This goes out right now" -t now -i "integration-id"',
2016
+ "Publish immediately (no --date needed)"
2017
+ ).example(
2018
+ '$0 posts:create -c "Draft post" -s "2026-12-31T12:00:00Z" -t draft -i "integration-id"',
2019
+ "Create draft post"
2020
+ ).example(
2021
+ '$0 posts:create -c "Main post" -m "img1.jpg,img2.jpg" -s "2026-12-31T12:00:00Z" -i "integration-id"',
2022
+ `Post with multiple images (each URL from "${BRAND.cli} upload")`
2023
+ ).example(
2024
+ '$0 posts:create -c "Main post" -m "img1.jpg" -c "First comment" -m "img2.jpg" -c "Second comment" -m "img3.jpg,img4.jpg" -s "2026-12-31T12:00:00Z" -i "integration-id"',
2025
+ "Post with comments, each having their own media"
2026
+ ).example(
2027
+ '$0 posts:create -c "Thread 1/3" -c "Thread 2/3" -c "Thread 3/3" -d 5 -s "2026-12-31T12:00:00Z" -i "integration-id"',
2028
+ "X (Twitter) thread with 5 minute delay between comments"
2029
+ ).example(
2030
+ "$0 posts:create --json ./post.json",
2031
+ "Complex post from JSON file"
2032
+ ).example(
2033
+ `$0 posts:create -c "Tweet content" -s "2026-12-31T12:00:00Z" --settings '{"who_can_reply_post":"everyone"}' -i "integration-id"`,
2034
+ "X (Twitter) post with reply settings"
2035
+ ).example(
2036
+ `$0 posts:create -c "Video description" -s "2026-12-31T12:00:00Z" --settings '{"title":"My Video","type":"public"}' -i "youtube-id"`,
2037
+ "YouTube post with title and visibility"
2038
+ );
2039
+ },
2040
+ createPost
2041
+ ).command(
2042
+ "posts:list",
2043
+ "List posts in a date range",
2044
+ (yargs2) => {
2045
+ return yargs2.option("startDate", {
2046
+ describe: "Start date (ISO 8601 with explicit offset, or use --timezone). Default: 30 days ago",
2047
+ type: "string"
2048
+ }).option("endDate", {
2049
+ describe: "End date (ISO 8601 with explicit offset, or use --timezone). Default: 30 days from now",
2050
+ type: "string"
2051
+ }).option("timezone", {
2052
+ describe: `IANA timezone name (e.g. ${BRAND.exampleTimezone}) used to interpret --startDate/--endDate when they have no explicit offset`,
2053
+ type: "string"
2054
+ }).option("customer", {
2055
+ describe: "Customer (group) ID (optional)",
2056
+ type: "string"
2057
+ }).example("$0 posts:list", "List posts (last 30 days to next 30 days)").example(
2058
+ '$0 posts:list --startDate "2026-01-01T00:00:00Z" --endDate "2026-12-31T23:59:59Z"',
2059
+ "List posts for a specific date range"
2060
+ ).example(
2061
+ '$0 posts:list --customer "customer-id"',
2062
+ "List posts for a specific customer"
2063
+ );
2064
+ },
2065
+ listPosts
2066
+ ).command(
2067
+ "posts:delete <id>",
2068
+ "Delete one post (one channel). Other channels of the same group stay scheduled",
2069
+ (yargs2) => {
2070
+ return yargs2.positional("id", {
2071
+ describe: "Post ID to delete (the id field of posts:list)",
2072
+ type: "string"
2073
+ }).example("$0 posts:delete abc123", "Delete only post abc123, with its thread or comments").epilogue("To delete a post from every channel it went to, use posts:delete-group <group>.");
2074
+ },
2075
+ deletePost
2076
+ ).command(
2077
+ "posts:delete-group <group>",
2078
+ "Delete a whole group: the post on every channel it went to",
2079
+ (yargs2) => {
2080
+ return yargs2.positional("group", {
2081
+ describe: "Group ID (the group field of posts:list)",
2082
+ type: "string"
2083
+ }).example("$0 posts:delete-group grp_9f8e7d6c", "Delete every channel of group grp_9f8e7d6c");
2084
+ },
2085
+ deletePostGroup
2086
+ ).command(
2087
+ "posts:find-slot <id>",
2088
+ "Find the next free publishing slot for an integration",
2089
+ (yargs2) => {
2090
+ return yargs2.positional("id", {
2091
+ describe: "Integration ID",
2092
+ type: "string"
2093
+ }).example(
2094
+ "$0 posts:find-slot integration-123",
2095
+ 'Returns {"date": "..."} \u2014 the next free datetime, usable as posts:create --date'
2096
+ );
2097
+ },
2098
+ findSlot
2099
+ ).command(
2100
+ "posts:missing <id>",
2101
+ "List available content from the provider for a post with missing release ID",
2102
+ (yargs2) => {
2103
+ return yargs2.positional("id", {
2104
+ describe: "Post ID",
2105
+ type: "string"
2106
+ }).example(
2107
+ "$0 posts:missing post-123",
2108
+ "Get available content to connect to a post"
2109
+ );
2110
+ },
2111
+ getMissingContent
2112
+ ).command(
2113
+ "posts:status <id>",
2114
+ "Change a post status between draft and schedule",
2115
+ (yargs2) => {
2116
+ return yargs2.positional("id", {
2117
+ describe: "Post ID",
2118
+ type: "string"
2119
+ }).option("status", {
2120
+ alias: "s",
2121
+ describe: 'New status: "draft" or "schedule"',
2122
+ type: "string",
2123
+ choices: ["draft", "schedule"],
2124
+ demandOption: true
2125
+ }).example(
2126
+ "$0 posts:status post-123 --status draft",
2127
+ "Move a scheduled post back to draft (stops the running workflow)"
2128
+ ).example(
2129
+ "$0 posts:status post-123 --status schedule",
2130
+ "Schedule a draft post so it is queued for publishing"
2131
+ );
2132
+ },
2133
+ changePostStatus
2134
+ ).command(
2135
+ "posts:connect <id>",
2136
+ "Connect a post to its published content by updating the release ID",
2137
+ (yargs2) => {
2138
+ return yargs2.positional("id", {
2139
+ describe: "Post ID",
2140
+ type: "string"
2141
+ }).option("release-id", {
2142
+ describe: "The platform-specific content ID to connect",
2143
+ type: "string",
2144
+ demandOption: true
2145
+ }).example(
2146
+ '$0 posts:connect post-123 --release-id "7321456789012345678"',
2147
+ "Connect a post to its published content"
2148
+ );
2149
+ },
2150
+ connectPost
2151
+ ).command(
2152
+ "integrations:list",
2153
+ "List all connected integrations",
2154
+ (yargs2) => {
2155
+ return yargs2.option("group", {
2156
+ describe: "Filter integrations by group (customer) ID",
2157
+ type: "string"
2158
+ }).example("$0 integrations:list", "List all connected integrations").example(
2159
+ '$0 integrations:list --group "customer-id"',
2160
+ "List integrations for a specific group"
2161
+ );
2162
+ },
2163
+ listIntegrations
2164
+ ).command(
2165
+ "integrations:groups",
2166
+ "List all groups (customers)",
2167
+ {},
2168
+ listGroups
2169
+ ).command(
2170
+ "integrations:settings <id>",
2171
+ "Get settings schema, rules, max length and tools for an integration",
2172
+ (yargs2) => {
2173
+ return yargs2.positional("id", {
2174
+ describe: "Integration ID",
2175
+ type: "string"
2176
+ }).example(
2177
+ "$0 integrations:settings youtube-456",
2178
+ "Get settings schema for a YouTube integration"
2179
+ ).example(
2180
+ "$0 integrations:settings instagram-123",
2181
+ "Get settings schema and tools for an Instagram integration"
2182
+ );
2183
+ },
2184
+ getIntegrationSettings
2185
+ ).command(
2186
+ "integrations:trigger <id> <method>",
2187
+ "Trigger an integration tool to fetch additional data",
2188
+ (yargs2) => {
2189
+ return yargs2.positional("id", {
2190
+ describe: "Integration ID",
2191
+ type: "string"
2192
+ }).positional("method", {
2193
+ describe: 'Method name from the "tools" array of integrations:settings',
2194
+ type: "string"
2195
+ }).option("data", {
2196
+ alias: "d",
2197
+ describe: "Data to pass to the tool as JSON string",
2198
+ type: "string"
2199
+ }).example(
2200
+ `$0 integrations:trigger instagram-123 audioSearch -d '{"q":"lofi","type":"music"}'`,
2201
+ "Search Instagram audio for a Reel (the only tool on a supported channel)"
2202
+ );
2203
+ },
2204
+ triggerIntegrationTool
2205
+ ).command(
2206
+ "analytics:platform <id>",
2207
+ "Get analytics for a specific integration/channel",
2208
+ (yargs2) => {
2209
+ return yargs2.positional("id", {
2210
+ describe: "Integration ID",
2211
+ type: "string"
2212
+ }).option("days", {
2213
+ alias: ["d", "date"],
2214
+ describe: "Number of days to look back (default: 7)",
2215
+ type: "string",
2216
+ default: "7"
2217
+ }).example(
2218
+ "$0 analytics:platform integration-123",
2219
+ "Get last 7 days of analytics"
2220
+ ).example(
2221
+ "$0 analytics:platform integration-123 -d 30",
2222
+ "Get last 30 days of analytics"
2223
+ );
2224
+ },
2225
+ getAnalytics
2226
+ ).command(
2227
+ "analytics:post <id>",
2228
+ "Get analytics for a specific post",
2229
+ (yargs2) => {
2230
+ return yargs2.positional("id", {
2231
+ describe: "Post ID",
2232
+ type: "string"
2233
+ }).option("days", {
2234
+ alias: ["d", "date"],
2235
+ describe: "Number of days to look back (default: 7)",
2236
+ type: "string",
2237
+ default: "7"
2238
+ }).example(
2239
+ "$0 analytics:post post-123",
2240
+ "Get last 7 days of post analytics"
2241
+ ).example(
2242
+ "$0 analytics:post post-123 -d 30",
2243
+ "Get last 30 days of post analytics"
2244
+ );
2245
+ },
2246
+ getPostAnalytics
2247
+ ).command(
2248
+ "upload <file>",
2249
+ "Upload a media file; returns the URL to pass to posts:create -m",
2250
+ (yargs2) => {
2251
+ return yargs2.positional("file", {
2252
+ describe: "File path to upload (jpeg/png/gif/webp/avif/bmp/tiff up to 10 MB, mp4 up to 1 GB)",
2253
+ type: "string"
2254
+ }).example("$0 upload ./image.png", "Upload an image; use the returned .path in posts:create -m");
2255
+ },
2256
+ uploadFile
2257
+ ).command(
2258
+ "upload:link",
2259
+ "Get a browser upload link for a file that is not on this machine (e.g. on a phone)",
2260
+ (yargs2) => {
2261
+ return yargs2.example(
2262
+ "$0 upload:link",
2263
+ "Returns {id, url, expiresAt}. Show the url to the person; they drop files on it, signed out. Then run upload:files <id>"
2264
+ );
2265
+ },
2266
+ createUploadLink
2267
+ ).command(
2268
+ "upload:files <id>",
2269
+ "List the files that arrived through an upload link; each .path goes to posts:create -m",
2270
+ (yargs2) => {
2271
+ return yargs2.positional("id", {
2272
+ describe: `The id returned by "${BRAND.cli} upload:link"`,
2273
+ type: "string"
2274
+ }).example(
2275
+ "$0 upload:files kR3mQ7xP2nLa",
2276
+ 'Returns {status, count, files:[{id,name,path,type}]}; status "empty" means nothing has arrived yet'
2277
+ );
2278
+ },
2279
+ listUploadLinkFiles
2280
+ ).command(
2281
+ "workspaces:list",
2282
+ 'List the workspaces this credential can act on ("current" marks the active one)',
2283
+ {},
2284
+ listWorkspaces
2285
+ ).command(
2286
+ "workspaces:use <id>",
2287
+ "Make every following command act on this workspace",
2288
+ (yargs2) => {
2289
+ return yargs2.positional("id", {
2290
+ describe: `Workspace id, from "${BRAND.cli} workspaces:list"`,
2291
+ type: "string"
2292
+ }).example("$0 workspaces:use 3f2a\u2026", "Switch the CLI to that workspace");
2293
+ },
2294
+ useWorkspace
2295
+ ).command(
2296
+ "workspaces:current",
2297
+ "Show the workspace commands act on now",
2298
+ {},
2299
+ currentWorkspace
2300
+ ).command(
2301
+ "billing:plans",
2302
+ IS_POSTEMI ? "Show the Postemi plans: prices, limits, the trial rule and the current tier" : "Show the Posty plans: prices in forint, limits, the trial rule and the current tier",
2303
+ {},
2304
+ billingPlans
2305
+ ).command(
2306
+ "billing:subscribe",
2307
+ "Get a Stripe Checkout link for a plan. Nothing is charged; the subscriber opens the link and pays with their own wallet or card",
2308
+ (yargs2) => {
2309
+ return yargs2.option("tier", {
2310
+ describe: `The plan: ${BRAND.tierList} (see "${BRAND.cli} billing:plans")`,
2311
+ type: "string",
2312
+ demandOption: true
2313
+ }).option("period", {
2314
+ describe: "How often the plan renews",
2315
+ type: "string",
2316
+ choices: ["monthly", "yearly"],
2317
+ demandOption: true
2318
+ }).option("open", {
2319
+ describe: "Also open the link in this machine's browser",
2320
+ type: "boolean",
2321
+ default: false
2322
+ }).example(
2323
+ "$0 billing:subscribe --tier pro --period yearly",
2324
+ "Print the checkout link as JSON"
2325
+ ).example(
2326
+ "$0 billing:subscribe --tier pro --period yearly --open",
2327
+ "Print it and open it in the browser"
2328
+ );
2329
+ },
2330
+ billingSubscribe
2331
+ ).command(
2332
+ "billing:status",
2333
+ "Show the subscription state (none, trialing, active, past_due, read_only, cancelled) and any unpaid checkout",
2334
+ {},
2335
+ billingStatus
2336
+ ).command(
2337
+ "billing:manage",
2338
+ "Get a link to the Stripe billing portal (plan change, cancel, invoices, card)",
2339
+ (yargs2) => {
2340
+ return yargs2.option("open", {
2341
+ describe: "Also open the link in this machine's browser",
2342
+ type: "boolean",
2343
+ default: false
2344
+ });
2345
+ },
2346
+ billingManage
2347
+ );
2348
+ if (!IS_POSTEMI) cli.command(
2349
+ "fotoai:status",
2350
+ "Show whether FotoAI (fotoai.hu) is connected for the key owner, as whom, and the FotoAI credit balance",
2351
+ {},
2352
+ fotoAiStatus
2353
+ ).command(
2354
+ "fotoai:models",
2355
+ "List the FotoAI image and video models with their parameters and prices",
2356
+ (yargs2) => {
2357
+ return yargs2.option("category", {
2358
+ describe: "Only image or only video models",
2359
+ type: "string",
2360
+ choices: ["image", "video"]
2361
+ });
2362
+ },
2363
+ fotoAiModels
2364
+ ).command(
2365
+ "fotoai:generate",
2366
+ "Price, and with --yes generate, an image or video with the key owner's own FotoAI credits; the result is saved into the media library",
2367
+ (yargs2) => {
2368
+ return yargs2.option("model", { alias: "m", describe: `A model id from "${BRAND.cli} fotoai:models"`, type: "string", demandOption: true }).option("prompt", { alias: "p", describe: "What to generate", type: "string", demandOption: true }).option("aspect", { describe: "Aspect ratio, e.g. 1:1, 4:5, 9:16, 16:9", type: "string" }).option("duration", { describe: "Video length in seconds (video models)", type: "number" }).option("resolution", { describe: "Resolution, when the model offers one (e.g. 2K)", type: "string" }).option("yes", { describe: "Spend the credits. Without it only the price is shown", type: "boolean", default: false }).option("wait", { describe: "Wait up to this many seconds for the result", type: "number", default: 0 }).option("idempotency-key", { describe: "Reuse on a retry so nothing is charged twice", type: "string" }).example('$0 fotoai:generate -m image.seedream-5-lite -p "k\xE1v\xE9 a teraszon" --aspect 4:5', "Show the price only").example('$0 fotoai:generate -m image.seedream-5-lite -p "k\xE1v\xE9 a teraszon" --aspect 4:5 --yes --wait 60', "Generate and print the media paths");
2369
+ },
2370
+ fotoAiGenerate
2371
+ ).command(
2372
+ "fotoai:get <id>",
2373
+ "Show a FotoAI generation; once done, its outputs saved into the media library with paths for posts:create",
2374
+ (yargs2) => {
2375
+ return yargs2.positional("id", { describe: "The generation id", type: "string" }).option("wait", { describe: "Wait up to this many seconds while it runs", type: "number", default: 0 });
2376
+ },
2377
+ fotoAiGet
2378
+ );
2379
+ cli.command(
2380
+ "config:set <key> <value>",
2381
+ `Save a CLI setting to ~/${BRAND.configDirName}/config.json`,
2382
+ (yargs2) => {
2383
+ return yargs2.positional("key", {
2384
+ describe: `Setting name. Known: "timezone" (an IANA name), "workspace" (an id; prefer "${BRAND.cli} workspaces:use")`,
2385
+ type: "string"
2386
+ }).positional("value", {
2387
+ describe: "Setting value",
2388
+ type: "string"
2389
+ }).example(
2390
+ `$0 config:set timezone ${BRAND.exampleTimezone}`,
2391
+ `Interpret dates written without an explicit offset in ${BRAND.exampleTimezoneCity} time`
2392
+ );
2393
+ },
2394
+ configSet
2395
+ ).command(
2396
+ "config:get [key]",
2397
+ "Show saved CLI settings (all, or one key)",
2398
+ (yargs2) => {
2399
+ return yargs2.positional("key", {
2400
+ describe: 'Setting name. Known: "timezone"',
2401
+ type: "string"
2402
+ }).example("$0 config:get", "Show all saved settings as JSON").example("$0 config:get timezone", "Show the saved timezone");
2403
+ },
2404
+ configGet
2405
+ ).command(
2406
+ "auth:login",
2407
+ "Authenticate using the OAuth2 device flow (opens a browser)",
2408
+ (yargs2) => {
2409
+ return yargs2.option("auth-server", {
2410
+ describe: `Auth server URL (default: ${BRAND.defaultApiUrl})`,
2411
+ type: "string"
2412
+ }).example(
2413
+ "$0 auth:login",
2414
+ "Login via OAuth2 device flow"
2415
+ );
2416
+ },
2417
+ authLogin
2418
+ );
2419
+ cli.command(
2420
+ "auth:signup",
2421
+ `Create a NEW ${BRAND.name} account with a code sent to the e-mail address, and store its key. No free trial: the next step is billing:subscribe`,
2422
+ (yargs2) => {
2423
+ return yargs2.option("email", {
2424
+ describe: "E-mail address of the new account; the code is sent there",
2425
+ type: "string",
2426
+ demandOption: true
2427
+ }).option("code", {
2428
+ describe: "The six-digit code from the e-mail (asked for when omitted on a terminal)",
2429
+ type: "string"
2430
+ }).option("accept-terms", {
2431
+ describe: `The person has read ${BRAND.termsUrl} and ${BRAND.privacyUrl} and agrees`,
2432
+ type: "boolean",
2433
+ default: false
2434
+ }).option("workspace-name", {
2435
+ describe: "Name of the new workspace (default: the part of the address before the @)",
2436
+ type: "string"
2437
+ }).option("timezone", {
2438
+ describe: `The person's IANA timezone for the account, e.g. ${BRAND.exampleTimezone} (default: the one saved by config:set, else the system time zone)`,
2439
+ type: "string"
2440
+ }).option("language", {
2441
+ describe: "Language of the e-mail and the account",
2442
+ type: "string",
2443
+ choices: ["hu", "en", "de", "cs"]
2444
+ }).option("api-url", {
2445
+ describe: `API base URL (default: ${envName("API_URL")}, then ${BRAND.defaultApiUrl})`,
2446
+ type: "string"
2447
+ }).example(
2448
+ "$0 auth:signup --email anna@example.com",
2449
+ "Send the code, then ask for it and for the terms on the terminal"
2450
+ ).example(
2451
+ `$0 auth:signup --email anna@example.com --code 123456 --accept-terms --workspace-name "${IS_POSTEMI ? "Anna's Cafe" : "Anna K\xE1v\xE9z\xF3"}"`,
2452
+ "Finish in one run (an agent, after the person read the code and agreed)"
2453
+ );
2454
+ },
2455
+ authSignup
2456
+ );
2457
+ cli.command(
2458
+ "auth:logout",
2459
+ "Remove stored OAuth2 credentials",
2460
+ {},
2461
+ authLogout
2462
+ ).command(
2463
+ "auth:status",
2464
+ "Show current authentication status and verify the credentials",
2465
+ {},
2466
+ authStatus
2467
+ ).demandCommand(1, `Specify a command. Run "${BRAND.cli} --help" for the list.`).strict().help().alias("h", "help").version().alias("v", "version").wrap(Math.min(110, process.stdout.columns || 110)).epilogue(
2468
+ `Output contract: results are JSON on stdout; status and errors go to stderr. Every failure exits 1.
2469
+
2470
+ Authentication:
2471
+ Device login: ${BRAND.cli} auth:login
2472
+ API Key: export ${envName("API_KEY")}=your_api_key
2473
+
2474
+ Workspaces: a credential may reach several. "${BRAND.cli} workspaces:list" shows them, "${BRAND.cli} workspaces:use <id>" picks the one commands act on (or pass --workspace <id> / ${envName("WORKSPACE")}).
2475
+
2476
+ Dates: ALWAYS pass an explicit offset ("2026-12-31T12:00:00Z" / "+01:00"), or pass --timezone with an IANA name (e.g. ${BRAND.exampleTimezone}). A date without either is resolved via ${envName("TIMEZONE")}, then the timezone saved by "${BRAND.cli} config:set timezone" \u2014 and is an ERROR when none of those is set. Numeric offsets are never accepted as timezone values.
2477
+
2478
+ For more information, visit: ${BRAND.homepage}`
2479
+ ).parse();
2480
+ //# sourceMappingURL=index.js.map