@anakonn/ankk 0.1.1 → 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +78 -0
  2. package/dist/index.js +496 -71
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -30,6 +30,61 @@ ankk upgrade --dry-run
30
30
  The CLI uses the `latest` npm dist-tag. Release versions use three-part numeric
31
31
  SemVer such as `0.1.1`.
32
32
 
33
+ ## Initial Setup
34
+
35
+ Create `~/.ankk/config.json` after installing the CLI:
36
+
37
+ ```bash
38
+ ankk config setup
39
+ ```
40
+
41
+ For CI or other non-interactive environments:
42
+
43
+ ```bash
44
+ ANKK_API_KEY=spk_... ankk config setup --base-url https://api-public.ankk.app --api-key-env ANKK_API_KEY --yes
45
+ ```
46
+
47
+ The setup command creates `~/.ankk/` when needed and writes the config file with
48
+ owner-only permissions. It never prints the full API key.
49
+
50
+ Runtime config precedence is:
51
+
52
+ 1. CLI flags: `--api-key`, `--base-url`, `--config`
53
+ 2. Environment variables: `ANKK_API_KEY`, `ANKK_BASE_URL`
54
+ 3. Config file: `~/.ankk/config.json`
55
+ 4. Built-in default base URL: `https://api-public.ankk.app`
56
+
57
+ Inspect the resolved config without exposing the full key:
58
+
59
+ ```bash
60
+ ankk config show
61
+ ```
62
+
63
+ ## SNS Content
64
+
65
+ Use `content` for Ankk-managed work items: create, schedule, inspect publish
66
+ state, cancel scheduled content, and delete mutable content.
67
+
68
+ Use `posts` for provider posts that already exist on SNS providers. Scheduled
69
+ content appears in `content`, not `posts`. Provider-origin posts may appear in
70
+ `posts` even when Ankk did not create them. An Ankk content item appears in
71
+ `posts` only after it has provider post identity such as `provider_post_id`.
72
+
73
+ Publish immediately from a JSON request body:
74
+
75
+ ```bash
76
+ ankk content publish --file payload.json
77
+ ```
78
+
79
+ Schedule the same payload with an ISO timestamp:
80
+
81
+ ```bash
82
+ ankk content publish --file payload.json --scheduled-for 2026-06-18T09:00:00Z
83
+ ```
84
+
85
+ If `payload.json` also contains `scheduled_for`, it must match `--scheduled-for`.
86
+ Relative inputs such as `10m` are not supported yet.
87
+
33
88
  ## Publish
34
89
 
35
90
  Public npm publish runs automatically when CLI-related changes reach the
@@ -87,6 +142,29 @@ Use live production pull only for dogfooding or deployed-contract comparison:
87
142
  (cd apps/cli && bun run src/entry/index.ts --json openapi pull --dry-run)
88
143
  ```
89
144
 
145
+ ## Content Payload Examples
146
+
147
+ TikTok photo publishing uses `provider_settings.tiktok.postMode` because TikTok
148
+ separates Direct Post from upload-to-inbox. Photo URLs must be publicly
149
+ reachable and allowed by the app's TikTok verified URL prefix/domain.
150
+
151
+ ```json
152
+ {
153
+ "brand_ref": "<brand_ref>",
154
+ "connection_id": "<tiktok_connection_id>",
155
+ "idempotency_key": "tiktok-photo-upload-to-inbox-2026-06-17",
156
+ "sns_type": "tiktok",
157
+ "text": "Photo caption",
158
+ "media": [{ "asset_ref": "https://cdn.example.com/photo.jpg" }],
159
+ "provider_settings": {
160
+ "content_type": "photo",
161
+ "tiktok": {
162
+ "postMode": "upload_to_inbox"
163
+ }
164
+ }
165
+ }
166
+ ```
167
+
90
168
  ## Smoke
91
169
 
92
170
  Use environment variables or `~/.ankk/config.json` for credentials. Do not paste
package/dist/index.js CHANGED
@@ -2157,7 +2157,7 @@ var {
2157
2157
  // package.json
2158
2158
  var package_default = {
2159
2159
  name: "@anakonn/ankk",
2160
- version: "0.1.1",
2160
+ version: "0.1.2",
2161
2161
  description: "Bun-first CLI for the ankk public API.",
2162
2162
  private: false,
2163
2163
  license: "UNLICENSED",
@@ -2684,6 +2684,23 @@ var messageFromBody = (body) => {
2684
2684
  return typeof error === "string" && error.trim() ? error : undefined;
2685
2685
  };
2686
2686
 
2687
+ // src/commander-output.ts
2688
+ var configureCommanderOutput = (program2, io) => {
2689
+ program2.configureOutput({
2690
+ writeErr: (chunk) => {
2691
+ io.stderr.write(chunk);
2692
+ },
2693
+ writeOut: (chunk) => {
2694
+ io.stdout.write(chunk);
2695
+ }
2696
+ });
2697
+ };
2698
+ var isSuccessfulCommanderExit = (error) => typeof error === "object" && error !== null && ("exitCode" in error) && error.exitCode === 0;
2699
+
2700
+ // src/config-command.ts
2701
+ import { createInterface } from "readline/promises";
2702
+ import { Writable } from "stream";
2703
+
2687
2704
  // src/config.ts
2688
2705
  import { mkdir, readFile, writeFile } from "fs/promises";
2689
2706
  import { dirname, join } from "path";
@@ -16990,6 +17007,23 @@ var loadConfig = async (path) => {
16990
17007
  throw error51;
16991
17008
  }
16992
17009
  };
17010
+ var configFileExists = async (path) => {
17011
+ try {
17012
+ await readFile(path, "utf8");
17013
+ return true;
17014
+ } catch (error51) {
17015
+ if (isMissingFileError(error51))
17016
+ return false;
17017
+ throw error51;
17018
+ }
17019
+ };
17020
+ var writeConfig = async (path, config2) => {
17021
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
17022
+ await writeFile(path, `${JSON.stringify(config2, null, 2)}
17023
+ `, {
17024
+ mode: 384
17025
+ });
17026
+ };
16993
17027
  var resolveRuntimeConfig = async ({
16994
17028
  apiKey,
16995
17029
  baseUrl,
@@ -17015,6 +17049,315 @@ var redactApiKey = (apiKey) => {
17015
17049
  var normalizeBaseUrl = (value) => value.replace(/\/+$/, "");
17016
17050
  var isMissingFileError = (error51) => typeof error51 === "object" && error51 !== null && ("code" in error51) && error51.code === "ENOENT";
17017
17051
 
17052
+ // src/output.ts
17053
+ var defaultIo = {
17054
+ stderr: process.stderr,
17055
+ stdout: process.stdout
17056
+ };
17057
+ var writeOutput = (io, mode, value, human) => {
17058
+ if (mode === "json") {
17059
+ io.stdout.write(`${JSON.stringify(value, null, 2)}
17060
+ `);
17061
+ return;
17062
+ }
17063
+ io.stdout.write(`${human}
17064
+ `);
17065
+ };
17066
+ var writeError = (io, error51, mode = "human") => {
17067
+ if (mode === "json") {
17068
+ io.stderr.write(`${JSON.stringify(errorJson(error51), null, 2)}
17069
+ `);
17070
+ return;
17071
+ }
17072
+ if (error51 instanceof CliHttpError) {
17073
+ io.stderr.write(`${error51.message}
17074
+ `);
17075
+ return;
17076
+ }
17077
+ if (error51 instanceof Error) {
17078
+ io.stderr.write(`${error51.message}
17079
+ `);
17080
+ return;
17081
+ }
17082
+ io.stderr.write(`Unexpected CLI error.
17083
+ `);
17084
+ };
17085
+ var errorJson = (error51) => {
17086
+ if (error51 instanceof CliHttpError) {
17087
+ const body = error51.body && typeof error51.body === "object" && !Array.isArray(error51.body) ? error51.body : {};
17088
+ return {
17089
+ error: "http_error",
17090
+ message: error51.message,
17091
+ ...body,
17092
+ status: error51.status
17093
+ };
17094
+ }
17095
+ if (error51 instanceof Error) {
17096
+ return {
17097
+ error: "cli_error",
17098
+ message: error51.message
17099
+ };
17100
+ }
17101
+ return {
17102
+ error: "unexpected_cli_error",
17103
+ message: "Unexpected CLI error."
17104
+ };
17105
+ };
17106
+
17107
+ // src/config-command.ts
17108
+ var registerConfigCommands = ({
17109
+ env,
17110
+ fetch,
17111
+ io,
17112
+ program: program2,
17113
+ prompt = createDefaultPrompt(io)
17114
+ }) => {
17115
+ const config2 = program2.command("config").description("Manage CLI config");
17116
+ config2.command("show").description("Show resolved CLI config").action(async () => {
17117
+ const options = program2.opts();
17118
+ const runtimeConfig = await resolveRuntimeConfig({
17119
+ apiKey: options.apiKey,
17120
+ baseUrl: options.baseUrl,
17121
+ configPath: options.config ?? defaultConfigPath({ env }),
17122
+ env
17123
+ });
17124
+ const value = {
17125
+ api_key: redactApiKey(runtimeConfig.apiKey),
17126
+ base_url: runtimeConfig.baseUrl,
17127
+ config_path: runtimeConfig.configPath
17128
+ };
17129
+ writeOutput(io, outputMode(options), value, `base_url: ${value.base_url}
17130
+ api_key: ${value.api_key ?? "(not set)"}
17131
+ config_path: ${value.config_path}`);
17132
+ });
17133
+ config2.command("setup").description("Create or update the CLI config file").option("--api-key <key>", "Public API key to store").option("--api-key-env <name>", "Read the public API key from an environment variable").option("--base-url <url>", "API base URL to store").option("--skip-health", "Do not run the health check after writing config").option("--yes", "Overwrite existing config and use defaults without prompting").action(async (options) => {
17134
+ const globalOptions = program2.opts();
17135
+ const configPath = globalOptions.config ?? defaultConfigPath({ env });
17136
+ const existingConfig = await loadConfig(configPath);
17137
+ const exists = await configFileExists(configPath);
17138
+ const overwrite = exists ? Boolean(options.yes) || await prompt.confirm({
17139
+ defaultValue: false,
17140
+ message: `Overwrite existing config at ${configPath}?`
17141
+ }) : true;
17142
+ if (!overwrite) {
17143
+ const value2 = {
17144
+ config_path: configPath,
17145
+ wrote: false
17146
+ };
17147
+ writeOutput(io, outputMode(globalOptions), value2, `Config unchanged: ${configPath}`);
17148
+ return;
17149
+ }
17150
+ const baseUrl = await setupBaseUrl({
17151
+ defaultValue: globalOptions.baseUrl ?? env.ANKK_BASE_URL ?? existingConfig.base_url ?? defaultBaseUrl,
17152
+ optionValue: options.baseUrl,
17153
+ prompt,
17154
+ yes: Boolean(options.yes)
17155
+ });
17156
+ const apiKey = await setupApiKey({
17157
+ env,
17158
+ existingValue: existingConfig.api_key,
17159
+ globalValue: globalOptions.apiKey,
17160
+ optionEnvName: options.apiKeyEnv,
17161
+ optionValue: options.apiKey,
17162
+ prompt,
17163
+ yes: Boolean(options.yes)
17164
+ });
17165
+ await writeConfig(configPath, {
17166
+ api_key: apiKey,
17167
+ base_url: baseUrl
17168
+ });
17169
+ const health = options.skipHealth ? null : await fetchHealth({ baseUrl, fetch });
17170
+ const value = {
17171
+ api_key: redactApiKey(apiKey),
17172
+ base_url: baseUrl,
17173
+ config_path: configPath,
17174
+ health,
17175
+ next_commands: ["ankk brands list", "ankk config show"],
17176
+ wrote: true
17177
+ };
17178
+ writeOutput(io, outputMode(globalOptions), value, [
17179
+ `Config saved: ${configPath}`,
17180
+ `base_url: ${baseUrl}`,
17181
+ `api_key: ${redactApiKey(apiKey)}`,
17182
+ ...health ? [`health: ${health.service} ${health.status}`] : [],
17183
+ "next: ankk brands list"
17184
+ ].join(`
17185
+ `));
17186
+ });
17187
+ };
17188
+ var setupBaseUrl = async ({
17189
+ defaultValue,
17190
+ optionValue,
17191
+ prompt,
17192
+ yes
17193
+ }) => {
17194
+ const value = optionValue ?? (yes ? defaultValue : await prompt.text({ defaultValue, message: "API base URL" }));
17195
+ return normalizeBaseUrl2(value || defaultValue);
17196
+ };
17197
+ var setupApiKey = async ({
17198
+ env,
17199
+ existingValue,
17200
+ globalValue,
17201
+ optionEnvName,
17202
+ optionValue,
17203
+ prompt,
17204
+ yes
17205
+ }) => {
17206
+ const envValue = optionEnvName ? env[optionEnvName] : undefined;
17207
+ if (optionEnvName && !envValue) {
17208
+ throw new Error(`API key environment variable ${optionEnvName} is not set.`);
17209
+ }
17210
+ const value = optionValue ?? globalValue ?? envValue ?? env.ANKK_API_KEY ?? (yes ? existingValue : await prompt.secret({ message: "API key" }));
17211
+ if (!value) {
17212
+ throw new Error("API key is required. Pass --api-key-env ANKK_API_KEY, --api-key, or set ANKK_API_KEY.");
17213
+ }
17214
+ return value;
17215
+ };
17216
+ var fetchHealth = async ({ baseUrl, fetch }) => {
17217
+ const response = await fetch(`${baseUrl}/v1/health`);
17218
+ if (!response.ok)
17219
+ throw new Error(`Config saved, but health check failed with HTTP ${response.status}.`);
17220
+ const body = await response.json();
17221
+ if (!isHealthResponse(body))
17222
+ throw new Error("Config saved, but health check response was invalid.");
17223
+ return body;
17224
+ };
17225
+ var createDefaultPrompt = (io) => {
17226
+ const prompt = {
17227
+ async confirm({ defaultValue = false, message }) {
17228
+ const suffix = defaultValue ? " [Y/n]" : " [y/N]";
17229
+ const answer = await prompt.text({ message: `${message}${suffix}` });
17230
+ const normalized = answer.trim().toLowerCase();
17231
+ if (!normalized)
17232
+ return defaultValue;
17233
+ return normalized === "y" || normalized === "yes";
17234
+ },
17235
+ async secret({ message }) {
17236
+ io.stderr.write(`${message}: `);
17237
+ const reader = createInterface({
17238
+ input: process.stdin,
17239
+ output: new MuteWritable,
17240
+ terminal: true
17241
+ });
17242
+ try {
17243
+ return (await reader.question("")).trim();
17244
+ } finally {
17245
+ reader.close();
17246
+ io.stderr.write(`
17247
+ `);
17248
+ }
17249
+ },
17250
+ async text({ defaultValue, message }) {
17251
+ const suffix = defaultValue ? ` [${defaultValue}]` : "";
17252
+ const reader = createInterface({
17253
+ input: process.stdin,
17254
+ output: process.stderr
17255
+ });
17256
+ try {
17257
+ const value = (await reader.question(`${message}${suffix}: `)).trim();
17258
+ return value || defaultValue || "";
17259
+ } finally {
17260
+ reader.close();
17261
+ }
17262
+ }
17263
+ };
17264
+ return prompt;
17265
+ };
17266
+
17267
+ class MuteWritable extends Writable {
17268
+ _write(_chunk, _encoding, callback) {
17269
+ callback();
17270
+ }
17271
+ }
17272
+ var normalizeBaseUrl2 = (value) => {
17273
+ const normalized = value.replace(/\/+$/, "");
17274
+ new URL(normalized);
17275
+ return normalized;
17276
+ };
17277
+ var outputMode = (options) => options.json ? "json" : "human";
17278
+ var isHealthResponse = (value) => {
17279
+ if (!value || typeof value !== "object")
17280
+ return false;
17281
+ const record2 = value;
17282
+ return typeof record2.service === "string" && typeof record2.status === "string";
17283
+ };
17284
+
17285
+ // src/content-publish.ts
17286
+ var applyScheduledForOption = (body, scheduledFor) => {
17287
+ if (!scheduledFor)
17288
+ return body;
17289
+ const normalized = normalizeIsoTimestamp(scheduledFor, "--scheduled-for");
17290
+ const existing = body.scheduled_for;
17291
+ if (existing === undefined || existing === null) {
17292
+ return { ...body, scheduled_for: normalized };
17293
+ }
17294
+ if (typeof existing !== "string") {
17295
+ throw new Error("scheduled_for in SNS content publish body must be an ISO timestamp string or null.");
17296
+ }
17297
+ if (normalizeIsoTimestamp(existing, "scheduled_for") !== normalized) {
17298
+ throw new Error("scheduled_for in SNS content publish body conflicts with --scheduled-for.");
17299
+ }
17300
+ return { ...body, scheduled_for: normalized };
17301
+ };
17302
+ var formatContentPublishResult = (response, body) => {
17303
+ const lines = [`content_id: ${response.content_id}`, `publish_job_id: ${response.publish_job_id}`, `status: ${response.status}`];
17304
+ if (response.scheduled_for) {
17305
+ lines.push(`scheduled_for: ${response.scheduled_for}`);
17306
+ const brandRef = typeof body.brand_ref === "string" ? body.brand_ref : "<brand_ref>";
17307
+ lines.push(`next: ankk content cancel ${response.content_id} --brand-ref ${brandRef}`);
17308
+ }
17309
+ return lines.join(`
17310
+ `);
17311
+ };
17312
+ var normalizeIsoTimestamp = (value, name) => {
17313
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value)) {
17314
+ throw new Error(`${name} must be an ISO timestamp, for example 2026-06-18T09:00:00Z.`);
17315
+ }
17316
+ const timestamp = new Date(value);
17317
+ if (Number.isNaN(timestamp.getTime())) {
17318
+ throw new Error(`${name} must be an ISO timestamp, for example 2026-06-18T09:00:00Z.`);
17319
+ }
17320
+ return timestamp.toISOString();
17321
+ };
17322
+
17323
+ // src/content-output.ts
17324
+ var formatSnsContent = (response) => {
17325
+ const lines = [
17326
+ `content_id: ${response.content_id}`,
17327
+ `status: ${response.status}`,
17328
+ `sns_type: ${response.sns_type}`,
17329
+ `connection_id: ${response.connection_id}`
17330
+ ];
17331
+ if (response.title)
17332
+ lines.push(`title: ${response.title}`);
17333
+ if (response.provider_post_id)
17334
+ lines.push(`provider_post_id: ${response.provider_post_id}`);
17335
+ if (response.provider_post_status)
17336
+ lines.push(`provider_post_status: ${response.provider_post_status}`);
17337
+ if (response.permalink)
17338
+ lines.push(`permalink: ${response.permalink}`);
17339
+ if (response.latest_publish_job) {
17340
+ lines.push(`latest_publish_job: ${response.latest_publish_job.status} (${response.latest_publish_job.publish_job_id})`);
17341
+ if (response.latest_publish_job.scheduled_for)
17342
+ lines.push(`scheduled_for: ${response.latest_publish_job.scheduled_for}`);
17343
+ if (response.latest_publish_job.last_error_code)
17344
+ lines.push(`last_error_code: ${response.latest_publish_job.last_error_code}`);
17345
+ if (response.latest_publish_job.last_error_message)
17346
+ lines.push(`last_error_message: ${response.latest_publish_job.last_error_message}`);
17347
+ }
17348
+ if (response.latest_delete_job) {
17349
+ lines.push(`latest_delete_job: ${response.latest_delete_job.status} (${response.latest_delete_job.delete_job_id})`);
17350
+ if (response.latest_delete_job.scheduled_for)
17351
+ lines.push(`delete_scheduled_for: ${response.latest_delete_job.scheduled_for}`);
17352
+ if (response.latest_delete_job.last_error_code)
17353
+ lines.push(`delete_last_error_code: ${response.latest_delete_job.last_error_code}`);
17354
+ if (response.latest_delete_job.last_error_message)
17355
+ lines.push(`delete_last_error_message: ${response.latest_delete_job.last_error_message}`);
17356
+ }
17357
+ return lines.join(`
17358
+ `);
17359
+ };
17360
+
17018
17361
  // src/json-input.ts
17019
17362
  import { readFile as readFile2 } from "fs/promises";
17020
17363
  var readJsonInput = async (path, { readStdin = readProcessStdin } = {}) => {
@@ -17030,6 +17373,98 @@ var readJsonInput = async (path, { readStdin = readProcessStdin } = {}) => {
17030
17373
  };
17031
17374
  var readProcessStdin = async () => Bun.stdin.text();
17032
17375
 
17376
+ // src/media-upload.ts
17377
+ import { readFile as readFile3, stat } from "fs/promises";
17378
+ import { basename } from "path";
17379
+ var runMediaUploadCommand = async ({
17380
+ brandRef,
17381
+ client,
17382
+ contentType,
17383
+ fetch,
17384
+ file: file2,
17385
+ io,
17386
+ outputMode: outputMode2
17387
+ }) => {
17388
+ const upload = await readMediaUploadFile(file2, contentType);
17389
+ const prepared = responseOrThrow(await client.POST("/v1/media/uploads", {
17390
+ body: {
17391
+ brand_ref: brandRef,
17392
+ content_type: upload.contentType,
17393
+ filename: upload.filename,
17394
+ size: upload.size
17395
+ }
17396
+ }));
17397
+ const uploaded = await fetch(prepared.upload_url, {
17398
+ body: upload.data,
17399
+ headers: {
17400
+ "content-type": prepared.content_type
17401
+ },
17402
+ method: prepared.method
17403
+ });
17404
+ if (!uploaded.ok) {
17405
+ throw new Error(`Media upload failed with HTTP ${uploaded.status}.`);
17406
+ }
17407
+ writeOutput(io, outputMode2, {
17408
+ ...prepared,
17409
+ uploaded: true
17410
+ }, `asset_ref: ${prepared.asset_ref}`);
17411
+ };
17412
+ var mediaUploadPolicies = {
17413
+ ".gif": { contentType: "image/gif", maxSize: 20 * 1024 * 1024 },
17414
+ ".jpeg": { contentType: "image/jpeg", maxSize: 20 * 1024 * 1024 },
17415
+ ".jpg": { contentType: "image/jpeg", maxSize: 20 * 1024 * 1024 },
17416
+ ".mov": { contentType: "video/quicktime", maxSize: 500 * 1024 * 1024 },
17417
+ ".mp4": { contentType: "video/mp4", maxSize: 500 * 1024 * 1024 },
17418
+ ".png": { contentType: "image/png", maxSize: 20 * 1024 * 1024 },
17419
+ ".webm": { contentType: "video/webm", maxSize: 500 * 1024 * 1024 },
17420
+ ".webp": { contentType: "image/webp", maxSize: 20 * 1024 * 1024 }
17421
+ };
17422
+ var readMediaUploadFile = async (file2, contentTypeOverride) => {
17423
+ const filename = basename(file2);
17424
+ const extension = mediaExtension(filename);
17425
+ const extensionPolicy = mediaUploadPolicies[extension];
17426
+ const contentType = contentTypeOverride?.trim().toLowerCase() || extensionPolicy.contentType;
17427
+ const contentTypePolicy = mediaUploadPolicyForContentType(contentType);
17428
+ if (!contentTypePolicy) {
17429
+ throw new Error(`Unsupported media content type: ${contentType}`);
17430
+ }
17431
+ if (!contentTypePolicy.extensions.includes(extension)) {
17432
+ throw new Error(`Media file extension does not match content type: ${filename} (${contentType})`);
17433
+ }
17434
+ const info = await stat(file2);
17435
+ if (!info.isFile()) {
17436
+ throw new Error(`Media path is not a file: ${file2}`);
17437
+ }
17438
+ if (info.size <= 0) {
17439
+ throw new Error("Media file is empty.");
17440
+ }
17441
+ if (info.size > contentTypePolicy.maxSize) {
17442
+ throw new Error(`Media file exceeds ${contentTypePolicy.maxSize} bytes.`);
17443
+ }
17444
+ return {
17445
+ contentType,
17446
+ data: await readFile3(file2),
17447
+ filename,
17448
+ size: info.size
17449
+ };
17450
+ };
17451
+ var mediaExtension = (filename) => {
17452
+ const index = filename.lastIndexOf(".");
17453
+ const extension = index > 0 ? filename.slice(index).toLowerCase() : "";
17454
+ if (extension in mediaUploadPolicies)
17455
+ return extension;
17456
+ throw new Error(`Unsupported media file extension: ${filename}`);
17457
+ };
17458
+ var mediaUploadPolicyForContentType = (contentType) => {
17459
+ const extensions = Object.entries(mediaUploadPolicies).filter(([, policy]) => policy.contentType === contentType).map(([extension]) => extension);
17460
+ if (extensions.length === 0)
17461
+ return null;
17462
+ return {
17463
+ extensions,
17464
+ maxSize: mediaUploadPolicies[extensions[0] ?? ".jpg"].maxSize
17465
+ };
17466
+ };
17467
+
17033
17468
  // src/openapi.ts
17034
17469
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
17035
17470
  import { dirname as dirname2 } from "path";
@@ -17052,49 +17487,24 @@ var writeOpenApiSnapshot = async (path, spec) => {
17052
17487
  `);
17053
17488
  };
17054
17489
 
17055
- // src/output.ts
17056
- var defaultIo = {
17057
- stderr: process.stderr,
17058
- stdout: process.stdout
17059
- };
17060
- var writeOutput = (io, mode, value, human) => {
17061
- if (mode === "json") {
17062
- io.stdout.write(`${JSON.stringify(value, null, 2)}
17063
- `);
17064
- return;
17065
- }
17066
- io.stdout.write(`${human}
17067
- `);
17068
- };
17069
- var writeError = (io, error51) => {
17070
- if (error51 instanceof CliHttpError) {
17071
- io.stderr.write(`${error51.message}
17072
- `);
17073
- return;
17074
- }
17075
- if (error51 instanceof Error) {
17076
- io.stderr.write(`${error51.message}
17077
- `);
17078
- return;
17079
- }
17080
- io.stderr.write(`Unexpected CLI error.
17081
- `);
17082
- };
17083
-
17084
17490
  // src/program.ts
17085
17491
  var runCli = async ({
17086
17492
  argv,
17087
17493
  commandRunner = runProcess,
17088
17494
  env = process.env,
17089
17495
  fetch = globalThis.fetch,
17090
- io = defaultIo
17496
+ io = defaultIo,
17497
+ prompt
17091
17498
  }) => {
17092
- const program2 = createProgram({ commandRunner, env, fetch, io });
17499
+ const program2 = createProgram({ commandRunner, env, fetch, io, prompt });
17093
17500
  try {
17094
17501
  await program2.parseAsync(argv, { from: "user" });
17095
17502
  return 0;
17096
17503
  } catch (error51) {
17097
- writeError(io, error51);
17504
+ if (isSuccessfulCommanderExit(error51)) {
17505
+ return 0;
17506
+ }
17507
+ writeError(io, error51, program2.opts().json ? "json" : "human");
17098
17508
  return 1;
17099
17509
  }
17100
17510
  };
@@ -17102,25 +17512,27 @@ var createProgram = ({
17102
17512
  commandRunner = runProcess,
17103
17513
  env = process.env,
17104
17514
  fetch = globalThis.fetch,
17105
- io = defaultIo
17515
+ io = defaultIo,
17516
+ prompt
17106
17517
  } = {}) => {
17107
17518
  const program2 = new Command;
17108
- program2.name("ankk").description("CLI for the ankk public API").option("--base-url <url>", "API base URL").option("--api-key <key>", "Public API key").option("--config <path>", "Config file path").option("--json", "Print JSON output").exitOverride();
17519
+ configureCommanderOutput(program2, io);
17520
+ program2.name("ankk").description("CLI for the ankk public API").version(packageVersion, "-V, --version", "Print version").option("--base-url <url>", "API base URL").option("--api-key <key>", "Public API key").option("--config <path>", "Config file path").option("--json", "Print JSON output").exitOverride();
17109
17521
  program2.command("health").description("Check api-public health").action(async () => {
17110
17522
  const context = await resolveCommandContext({ env, fetch, io, program: program2 });
17111
- const result = await fetchHealth({
17523
+ const result = await fetchHealth2({
17112
17524
  baseUrl: context.config.baseUrl,
17113
17525
  fetch
17114
17526
  });
17115
17527
  writeOutput(context.io, context.outputMode, result, `${result.service}: ${result.status}`);
17116
17528
  });
17117
17529
  program2.command("upgrade").description("Upgrade the ankk CLI global installation").option("--dry-run", "Print the upgrade command without running it").action(async (options) => {
17118
- const outputMode = program2.opts().json ? "json" : "human";
17530
+ const outputMode2 = program2.opts().json ? "json" : "human";
17119
17531
  const result = await upgradeCli({
17120
17532
  commandRunner,
17121
17533
  dryRun: Boolean(options.dryRun)
17122
17534
  });
17123
- writeOutput(io, outputMode, result, options.dryRun ? `Would run: ${result.command.join(" ")}` : `Upgraded ${result.package} via ${result.tag}`);
17535
+ writeOutput(io, outputMode2, result, options.dryRun ? `Would run: ${result.command.join(" ")}` : `Upgraded ${result.package} via ${result.tag}`);
17124
17536
  });
17125
17537
  const brands = program2.command("brands").description("Manage brands available to the API key");
17126
17538
  brands.command("list").description("List brands available to the API key").action(async () => {
@@ -17178,8 +17590,8 @@ var createProgram = ({
17178
17590
  }));
17179
17591
  writeOutput(context.io, context.outputMode, result, formatGenericRead(result));
17180
17592
  });
17181
- const content = program2.command("content").description("Read SNS contents");
17182
- withBrandRef(content.command("list").description("List SNS contents")).option("--connection-id <connection_id>", "Connection id filter").option("--status <status>", "Content status filter").action(async (options) => {
17593
+ const content = program2.command("content").description("Create, schedule, and manage Ankk SNS content work items");
17594
+ withBrandRef(content.command("list").description("List Ankk SNS content work items")).option("--connection-id <connection_id>", "Connection id filter").option("--status <status>", "Content status filter").action(async (options) => {
17183
17595
  const context = await resolveCommandContext({ env, fetch, io, program: program2 });
17184
17596
  const result = responseOrThrow(await context.client.GET("/v1/content", {
17185
17597
  params: queryParams({
@@ -17190,7 +17602,7 @@ var createProgram = ({
17190
17602
  }));
17191
17603
  writeOutput(context.io, context.outputMode, result, formatGenericRead(result));
17192
17604
  });
17193
- withBrandRef(content.command("get").description("Get SNS content").argument("<content_id>", "Content id")).option("--connection-id <connection_id>", "Connection id filter").option("--status <status>", "Content status filter").action(async (contentId, options) => {
17605
+ withBrandRef(content.command("get").description("Get an Ankk SNS content work item").argument("<content_id>", "Content id")).option("--connection-id <connection_id>", "Connection id filter").option("--status <status>", "Content status filter").action(async (contentId, options) => {
17194
17606
  const context = await resolveCommandContext({ env, fetch, io, program: program2 });
17195
17607
  const result = responseOrThrow(await context.client.GET("/v1/content/{content_id}", {
17196
17608
  params: {
@@ -17204,15 +17616,28 @@ var createProgram = ({
17204
17616
  })
17205
17617
  }
17206
17618
  }));
17207
- writeOutput(context.io, context.outputMode, result, formatGenericRead(result));
17619
+ writeOutput(context.io, context.outputMode, result, formatSnsContent(result));
17208
17620
  });
17209
- content.command("publish").description("Publish or schedule SNS content").requiredOption("--file <path>", "JSON request body file, or - for stdin").action(async (options) => {
17621
+ content.command("publish").description("Publish or schedule SNS content").requiredOption("--file <path>", "JSON request body file, or - for stdin").option("--scheduled-for <timestamp>", "Schedule publish at an ISO timestamp, for example 2026-06-18T09:00:00Z").action(async (options) => {
17210
17622
  const context = await resolveCommandContext({ env, fetch, io, program: program2 });
17211
- const body = readObjectBody(await readJsonInput(options.file), "SNS content publish body");
17623
+ const body = applyScheduledForOption(readObjectBody(await readJsonInput(options.file), "SNS content publish body"), options.scheduledFor);
17212
17624
  const result = responseOrThrow(await context.client.POST("/v1/content", {
17213
17625
  body: apiBody(body)
17214
17626
  }));
17215
- writeOutput(context.io, context.outputMode, result, formatGenericRead(result));
17627
+ writeOutput(context.io, context.outputMode, result, formatContentPublishResult(result, body));
17628
+ });
17629
+ const media = program2.command("media").description("Upload media for SNS publishing");
17630
+ withBrandRef(media.command("upload").description("Upload an image or video file").argument("<file>", "Media file path")).option("--content-type <content_type>", "Override detected MIME type").action(async (file2, options) => {
17631
+ const context = await resolveCommandContext({ env, fetch, io, program: program2 });
17632
+ await runMediaUploadCommand({
17633
+ brandRef: options.brandRef,
17634
+ client: context.client,
17635
+ contentType: options.contentType,
17636
+ fetch,
17637
+ file: file2,
17638
+ io: context.io,
17639
+ outputMode: context.outputMode
17640
+ });
17216
17641
  });
17217
17642
  withBrandRef(content.command("cancel").description("Cancel scheduled SNS content").argument("<content_id>", "Content id")).option("--file <path>", "JSON request body file, or - for stdin").action(async (contentId, options) => {
17218
17643
  const context = await resolveCommandContext({ env, fetch, io, program: program2 });
@@ -17241,7 +17666,7 @@ var createProgram = ({
17241
17666
  writeOutput(context.io, context.outputMode, result, formatGenericRead(result));
17242
17667
  });
17243
17668
  const references = program2.command("references").description("Create SNS references");
17244
- references.command("create").description("Create or get a public SNS reference").requiredOption("--file <path>", "JSON request body file, or - for stdin").action(async (options) => {
17669
+ references.command("create").description("Create or get a public post reference where direct provider lookup is supported").requiredOption("--file <path>", "JSON request body file, or - for stdin").action(async (options) => {
17245
17670
  const context = await resolveCommandContext({ env, fetch, io, program: program2 });
17246
17671
  const body = readObjectBody(await readJsonInput(options.file), "SNS reference body");
17247
17672
  const result = responseOrThrow(await context.client.POST("/v1/references", {
@@ -17249,8 +17674,8 @@ var createProgram = ({
17249
17674
  }));
17250
17675
  writeOutput(context.io, context.outputMode, result, formatGenericRead(result));
17251
17676
  });
17252
- const posts = program2.command("posts").description("Read posts");
17253
- withBrandRef(posts.command("list").description("List posts")).action(async (options) => {
17677
+ const posts = program2.command("posts").description("Read or delete provider posts that exist on SNS providers");
17678
+ withBrandRef(posts.command("list").description("List provider posts")).action(async (options) => {
17254
17679
  const context = await resolveCommandContext({ env, fetch, io, program: program2 });
17255
17680
  const result = responseOrThrow(await context.client.GET("/v1/posts", {
17256
17681
  params: queryParams({
@@ -17259,7 +17684,7 @@ var createProgram = ({
17259
17684
  }));
17260
17685
  writeOutput(context.io, context.outputMode, result, formatGenericRead(result));
17261
17686
  });
17262
- withBrandRef(posts.command("get").description("Get post").argument("<post_id>", "Post id")).action(async (postId, options) => {
17687
+ withBrandRef(posts.command("get").description("Get a provider post").argument("<post_id>", "Post id returned by posts list")).action(async (postId, options) => {
17263
17688
  const context = await resolveCommandContext({ env, fetch, io, program: program2 });
17264
17689
  const result = responseOrThrow(await context.client.GET("/v1/posts/{post_id}", {
17265
17690
  params: {
@@ -17273,7 +17698,7 @@ var createProgram = ({
17273
17698
  }));
17274
17699
  writeOutput(context.io, context.outputMode, result, formatGenericRead(result));
17275
17700
  });
17276
- withBrandRef(posts.command("delete").description("Delete post").argument("<post_id>", "Post id")).action(async (postId, options) => {
17701
+ withBrandRef(posts.command("delete").description("Delete a provider post").argument("<post_id>", "Post id returned by posts list")).action(async (postId, options) => {
17277
17702
  const context = await resolveCommandContext({ env, fetch, io, program: program2 });
17278
17703
  const result = responseOrThrow(await context.client.DELETE("/v1/posts/{post_id}", {
17279
17704
  params: {
@@ -17287,7 +17712,7 @@ var createProgram = ({
17287
17712
  }));
17288
17713
  writeOutput(context.io, context.outputMode, result, formatGenericRead(result));
17289
17714
  });
17290
- const comments = program2.command("comments").description("Read comments");
17715
+ const comments = program2.command("comments").description("Read and create comments");
17291
17716
  withBrandRef(comments.command("list").description("List comments")).action(async (options) => {
17292
17717
  const context = await resolveCommandContext({ env, fetch, io, program: program2 });
17293
17718
  const result = responseOrThrow(await context.client.GET("/v1/comments", {
@@ -17311,6 +17736,14 @@ var createProgram = ({
17311
17736
  }));
17312
17737
  writeOutput(context.io, context.outputMode, result, formatGenericRead(result));
17313
17738
  });
17739
+ comments.command("create").description("Create a top-level comment on a provider post").requiredOption("--file <path>", "JSON request body file, or - for stdin").action(async (options) => {
17740
+ const context = await resolveCommandContext({ env, fetch, io, program: program2 });
17741
+ const body = readObjectBody(await readJsonInput(options.file), "Comment create body");
17742
+ const result = responseOrThrow(await context.client.POST("/v1/comments", {
17743
+ body: apiBody(body)
17744
+ }));
17745
+ writeOutput(context.io, context.outputMode, result, formatGenericRead(result));
17746
+ });
17314
17747
  comments.command("moderate").description("Create comment moderation action").argument("<comment_id>", "Comment id").requiredOption("--file <path>", "JSON request body file, or - for stdin").action(async (commentId, options) => {
17315
17748
  const context = await resolveCommandContext({ env, fetch, io, program: program2 });
17316
17749
  const body = readObjectBody(await readJsonInput(options.file), "Comment moderation body");
@@ -17371,7 +17804,7 @@ var createProgram = ({
17371
17804
  }));
17372
17805
  writeOutput(context.io, context.outputMode, result, formatGenericRead(result));
17373
17806
  });
17374
- withBrandRef(analytics.command("overview").description("Get social analytics overview")).option("--from <date>", "Start date").option("--to <date>", "End date").action(async (options) => {
17807
+ withBrandRef(analytics.command("overview").description("Get compact brand-level analytics counters using the same default 30-day range as analytics get")).option("--from <date>", "Start date").option("--to <date>", "End date").action(async (options) => {
17375
17808
  const context = await resolveCommandContext({ env, fetch, io, program: program2 });
17376
17809
  const result = responseOrThrow(await context.client.GET("/v1/analytics/overview", {
17377
17810
  params: queryParams({
@@ -17414,7 +17847,7 @@ var createProgram = ({
17414
17847
  }));
17415
17848
  writeOutput(context.io, context.outputMode, result, formatGenericRead(result));
17416
17849
  });
17417
- webhooks.command("update").description("Update webhook subscription").argument("<webhook_id>", "Webhook id").requiredOption("--file <path>", "JSON request body file, or - for stdin").action(async (webhookId, options) => {
17850
+ withBrandRef(webhooks.command("update").description("Update webhook subscription").argument("<webhook_id>", "Webhook id").requiredOption("--file <path>", "JSON request body file, or - for stdin")).action(async (webhookId, options) => {
17418
17851
  const context = await resolveCommandContext({ env, fetch, io, program: program2 });
17419
17852
  const body = readObjectBody(await readJsonInput(options.file), "Webhook update body");
17420
17853
  const result = responseOrThrow(await context.client.PATCH("/v1/webhooks/{webhook_id}", {
@@ -17422,7 +17855,10 @@ var createProgram = ({
17422
17855
  params: {
17423
17856
  path: {
17424
17857
  webhook_id: webhookId
17425
- }
17858
+ },
17859
+ ...queryParams({
17860
+ brand_ref: options.brandRef
17861
+ })
17426
17862
  }
17427
17863
  }));
17428
17864
  writeOutput(context.io, context.outputMode, result, formatGenericRead(result));
@@ -17441,18 +17877,7 @@ var createProgram = ({
17441
17877
  }));
17442
17878
  writeOutput(context.io, context.outputMode, result, formatGenericRead(result));
17443
17879
  });
17444
- const config2 = program2.command("config").description("Manage CLI config");
17445
- config2.command("show").description("Show resolved CLI config").action(async () => {
17446
- const context = await resolveCommandContext({ env, fetch, io, program: program2 });
17447
- const value = {
17448
- api_key: redactApiKey(context.config.apiKey),
17449
- base_url: context.config.baseUrl,
17450
- config_path: context.config.configPath
17451
- };
17452
- writeOutput(context.io, context.outputMode, value, `base_url: ${value.base_url}
17453
- api_key: ${value.api_key ?? "(not set)"}
17454
- config_path: ${value.config_path}`);
17455
- });
17880
+ registerConfigCommands({ env, fetch, io, program: program2, prompt });
17456
17881
  const openapi = program2.command("openapi").description("Manage api-public OpenAPI schema");
17457
17882
  openapi.command("pull").description("Pull the api-public OpenAPI schema").option("--dry-run", "Fetch and report without writing").option("--output <path>", "Snapshot output path").action(async (options) => {
17458
17883
  const context = await resolveCommandContext({ env, fetch, io, program: program2 });
@@ -17485,7 +17910,7 @@ var resolveCommandContext = async ({
17485
17910
  configPath,
17486
17911
  env
17487
17912
  });
17488
- const outputMode = options.json ? "json" : "human";
17913
+ const outputMode2 = options.json ? "json" : "human";
17489
17914
  return {
17490
17915
  client: createApiPublicClient({
17491
17916
  apiKey: config2.apiKey,
@@ -17494,7 +17919,7 @@ var resolveCommandContext = async ({
17494
17919
  }),
17495
17920
  config: config2,
17496
17921
  io,
17497
- outputMode
17922
+ outputMode: outputMode2
17498
17923
  };
17499
17924
  };
17500
17925
  var safeDefaultConfigPath = (env) => defaultConfigPath({ env });
@@ -17581,17 +18006,17 @@ var readBodyOrBrandRef = async (options, name) => {
17581
18006
  brand_ref: options.brandRef
17582
18007
  };
17583
18008
  };
17584
- var fetchHealth = async ({ baseUrl, fetch }) => {
18009
+ var fetchHealth2 = async ({ baseUrl, fetch }) => {
17585
18010
  const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/v1/health`);
17586
18011
  if (!response.ok)
17587
18012
  throw new Error(`Health check failed with HTTP ${response.status}.`);
17588
18013
  const body = await response.json();
17589
- if (!isHealthResponse(body)) {
18014
+ if (!isHealthResponse2(body)) {
17590
18015
  throw new Error("Health check response was invalid.");
17591
18016
  }
17592
18017
  return body;
17593
18018
  };
17594
- var isHealthResponse = (value) => {
18019
+ var isHealthResponse2 = (value) => {
17595
18020
  if (!value || typeof value !== "object")
17596
18021
  return false;
17597
18022
  const record2 = value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anakonn/ankk",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Bun-first CLI for the ankk public API.",
5
5
  "private": false,
6
6
  "license": "UNLICENSED",