@wrangled/cli 0.3.44 → 0.3.46

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/main.js CHANGED
@@ -4,7 +4,7 @@ import { mkdir, readFile, stat } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { basename, dirname, extname, join, resolve } from "node:path";
6
6
  import { parseArgs } from "node:util";
7
- import { api, ApiError, apiDownload, apiText, apiUpload, CliError, exitCodes, getSigned, IMPORT_LIMITS, IMPORT_NAME_PATTERN, putSigned, readCredentials, removeCredentials, resolveApiUrl, resolveConnection, UnreadableCredentialsError, writeCredentials, entryTagFlags, streamSigned, } from "./api.js";
7
+ import { api, ApiError, apiDownload, apiText, apiUpload, CliError, exitCodes, getSigned, IMPORT_LIMITS, IMPORT_NAME_PATTERN, putSigned, CLIENT_FILE, parseClientName, readSavedLogins, removeCredentials, resolveApiUrl, resolveConnection, resolveLogin, UnreadableCredentialsError, writeCredentials, entryTagFlags, streamSigned, } from "./api.js";
8
8
  import { brand, envPrefix } from "./brand.js";
9
9
  import { firstSymlink, inRunFolder, writeFileAtomic } from "./files.js";
10
10
  import { guide, skill } from "./guide.js";
@@ -19,41 +19,47 @@ const SERVER_WAIT_CAP = 50;
19
19
  const VOICE_USAGE = "voice [--kind opener|objection|pain|desire|phrase] [--since <YYYY-MM-DD>] [--sort common|useful] [--limit <n>] [--json]";
20
20
  const commands = {
21
21
  login: {
22
- usage: `login (paste the key when asked, pipe it on stdin, or set ${envPrefix}_API_KEY)`,
22
+ usage: `login [--client <name>] (paste the key when asked, pipe it on stdin, or set ${envPrefix}_API_KEY); --client saves it under that client's name`,
23
23
  async run(args, options) {
24
24
  if (args.length > 0) {
25
25
  throw new CliError(`login takes no arguments. Pipe the key on stdin or set ${envPrefix}_API_KEY.`, exitCodes.usage);
26
26
  }
27
+ const client = options.client === undefined ? undefined : parseClientName(options.client);
27
28
  const apiKey = await readKeyFromUser();
28
- const stored = await readCredentials().catch((error) => {
29
+ const saved = await readSavedLogins().catch((error) => {
29
30
  if (error instanceof UnreadableCredentialsError)
30
31
  return null;
31
32
  throw error;
32
33
  });
33
- const apiUrl = resolveApiUrl(options.apiUrl, stored);
34
+ const apiUrl = resolveApiUrl(options.apiUrl, (client === undefined ? undefined : saved?.clients[client]) ?? saved?.default ?? null);
34
35
  const me = await api({ apiUrl, apiKey }, "/api/v1/me");
35
- const path = await writeCredentials({ apiKey, apiUrl });
36
+ const path = await writeCredentials({ apiKey, apiUrl }, { client });
36
37
  if (options.format === "json")
37
38
  return printJson(me);
38
- console.log(`Logged in to ${me.workspace.name} (${me.workspace.slug}) at ${apiUrl}.`);
39
+ console.log(`Logged in to ${me.workspace.name} (${me.workspace.slug}) at ${apiUrl}${client === undefined ? "" : ` as client ${client}`}.`);
39
40
  console.log(`Saved credentials to ${path}.`);
40
41
  },
41
42
  },
42
43
  logout: {
43
- usage: "logout",
44
- async run() {
45
- await removeCredentials();
46
- console.log("Logged out.");
44
+ usage: "logout [--client <name>] (without --client, removes the default login and keeps named clients)",
45
+ async run(_args, options) {
46
+ const client = options.client === undefined ? undefined : parseClientName(options.client);
47
+ await removeCredentials(client);
48
+ console.log(client === undefined ? "Logged out." : `Logged out of client ${client}.`);
47
49
  },
48
50
  },
49
51
  whoami: {
50
- usage: "whoami",
52
+ usage: "whoami (also says which client's key is in use and why)",
51
53
  async run(_args, options) {
52
- const me = await api(await resolveConnection(options.apiUrl), "/api/v1/me");
54
+ const { connection, choice } = await resolveLogin(options);
55
+ const me = await api(connection, "/api/v1/me");
56
+ const client = "client" in choice ? choice.client : null;
57
+ const clientFile = choice.source === "folder" ? choice.path : null;
53
58
  if (options.format === "json")
54
- return printJson(me);
59
+ return printJson({ ...me, client, source: choice.source, clientFile });
55
60
  const { name, slug, id } = me.workspace;
56
61
  console.log(renderTable(["workspace", "slug", "id", "library access"], [{ workspace: name, slug, id, "library access": me.knowledge ? "on" : "off" }]));
62
+ console.log(`\n${clientReason(choice)}`);
57
63
  },
58
64
  },
59
65
  "knowledge search": {
@@ -72,7 +78,7 @@ const commands = {
72
78
  query.set("library", options.library);
73
79
  for (const [field, value] of filters)
74
80
  query.set(field, value);
75
- const answer = await api(await resolveConnection(options.apiUrl), `/api/v1/knowledge/search?${query}`);
81
+ const answer = await api(await resolveConnection(options), `/api/v1/knowledge/search?${query}`);
76
82
  if (options.format === "json")
77
83
  return printJson(answer);
78
84
  console.log(answer.results.length === 0 ? "Nothing in the library matched." : answer.results.map(knowledgeBlock).join("\n\n"));
@@ -102,7 +108,7 @@ const commands = {
102
108
  })),
103
109
  }
104
110
  : await readToolInput(options);
105
- const result = await api(await resolveConnection(options.apiUrl), KNOWLEDGE_TAGS_PATH, body);
111
+ const result = await api(await resolveConnection(options), KNOWLEDGE_TAGS_PATH, body);
106
112
  if (options.format === "json")
107
113
  return printJson(result);
108
114
  console.log(`Tagged ${result.tagged}, took off ${result.removed}.`);
@@ -121,7 +127,7 @@ const commands = {
121
127
  if (options.search !== undefined)
122
128
  query.set("search", options.search);
123
129
  const path = `/api/v1/warehouse/schema${query.size > 0 ? `?${query}` : ""}`;
124
- const connection = await resolveConnection(options.apiUrl);
130
+ const connection = await resolveConnection(options);
125
131
  if (options.summary) {
126
132
  const summary = await api(connection, path);
127
133
  if (options.format === "json")
@@ -162,7 +168,7 @@ const commands = {
162
168
  throw new CliError("--all saves to a file: add --out <file.csv | file.ndjson | file.json>.", exitCodes.usage);
163
169
  }
164
170
  const sql = await readSql(args, options.file);
165
- const connection = await resolveConnection(options.apiUrl);
171
+ const connection = await resolveConnection(options);
166
172
  if (options.all)
167
173
  return saveWholeAnswer(connection, sql, options.out, options.format);
168
174
  const text = await apiText(connection, "/api/v1/warehouse/query", { sql });
@@ -182,7 +188,7 @@ const commands = {
182
188
  if (args.length > 0)
183
189
  throw new CliError("Usage: funnel [--from <date>] [--to <date>] [--grain day|week|month]", exitCodes.usage);
184
190
  const query = reportQuery(options, { grain: options.grain });
185
- const { funnel } = await api(await resolveConnection(options.apiUrl), `/api/v1/reports/funnel${query}`);
191
+ const { funnel } = await api(await resolveConnection(options), `/api/v1/reports/funnel${query}`);
186
192
  if (options.format === "json")
187
193
  return printJson({ funnel });
188
194
  printReport(`Funnel by ${funnel.grain}`, funnel, ["period", "currency"], FUNNEL_NUMBERS);
@@ -195,7 +201,7 @@ const commands = {
195
201
  if (args.length > 0)
196
202
  throw new CliError("Usage: money [--from <date>] [--to <date>] [--grain day|week|month]", exitCodes.usage);
197
203
  const query = reportQuery(options, { grain: options.grain });
198
- const { money } = await api(await resolveConnection(options.apiUrl), `/api/v1/reports/money${query}`);
204
+ const { money } = await api(await resolveConnection(options), `/api/v1/reports/money${query}`);
199
205
  if (options.format === "json")
200
206
  return printJson({ money });
201
207
  printReport(`Money by ${money.grain}`, money, ["period", "currency"], MONEY_NUMBERS);
@@ -218,7 +224,7 @@ const commands = {
218
224
  if (args.length > 0)
219
225
  throw new CliError("Usage: campaigns [--from <date>] [--to <date>] [--channel <channel>] [--limit <n>]", exitCodes.usage);
220
226
  const query = reportQuery(options, { channel: options.channel, limit: options.limit });
221
- const { campaignResults } = await api(await resolveConnection(options.apiUrl), `/api/v1/reports/campaigns${query}`);
227
+ const { campaignResults } = await api(await resolveConnection(options), `/api/v1/reports/campaigns${query}`);
222
228
  if (options.format === "json")
223
229
  return printJson({ campaignResults });
224
230
  const title = campaignResults.channel === null ? "Campaigns, best first" : `${campaignResults.channel} campaigns, best first`;
@@ -229,7 +235,7 @@ const commands = {
229
235
  "tools list": {
230
236
  usage: "tools list",
231
237
  async run(_args, options) {
232
- const { tools } = await api(await resolveConnection(options.apiUrl), "/api/v1/tools");
238
+ const { tools } = await api(await resolveConnection(options), "/api/v1/tools");
233
239
  if (options.format === "json")
234
240
  return printJson({ tools });
235
241
  console.log(renderTable(["id", "mode", "output", "title"], tools));
@@ -242,7 +248,7 @@ const commands = {
242
248
  if (!tool || rest.length > 0)
243
249
  throw new CliError("Usage: tools run <tool> [--input '<json>']", exitCodes.usage);
244
250
  const input = await readToolInput(options);
245
- const connection = await resolveConnection(options.apiUrl);
251
+ const connection = await resolveConnection(options);
246
252
  const body = {
247
253
  input,
248
254
  ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
@@ -257,7 +263,7 @@ const commands = {
257
263
  async run(args, options) {
258
264
  if (args.length !== 1)
259
265
  throw new CliError("Usage: tools get <run-id>", exitCodes.usage);
260
- const connection = await resolveConnection(options.apiUrl);
266
+ const connection = await resolveConnection(options);
261
267
  const { run } = await api(connection, `/api/v1/runs/${encodeURIComponent(args[0])}`);
262
268
  printRun(await waitForRun(connection, run, options.wait), options.format);
263
269
  },
@@ -267,7 +273,7 @@ const commands = {
267
273
  async run(args, options) {
268
274
  if (args.length > 0)
269
275
  throw new CliError("Usage: sources list", exitCodes.usage);
270
- const { sources } = await api(await resolveConnection(options.apiUrl), "/api/v1/sources");
276
+ const { sources } = await api(await resolveConnection(options), "/api/v1/sources");
271
277
  if (options.format === "json")
272
278
  return printJson({ sources });
273
279
  if (sources.length === 0)
@@ -296,7 +302,7 @@ const commands = {
296
302
  async run(args, options) {
297
303
  if (args.length !== 1)
298
304
  throw new CliError("Usage: sources show <id>", exitCodes.usage);
299
- const connection = await resolveConnection(options.apiUrl);
305
+ const connection = await resolveConnection(options);
300
306
  const path = `/api/v1/sources/${encodeURIComponent(args[0])}`;
301
307
  const { source } = await api(connection, path);
302
308
  const { runs } = await api(connection, `${path}/runs?limit=${SOURCE_RUNS_SHOWN}`);
@@ -340,7 +346,7 @@ const commands = {
340
346
  const [id, ...assignments] = args;
341
347
  if (!id)
342
348
  throw new CliError("Usage: sources settings <id> [<key>=<value> ...]", exitCodes.usage);
343
- const connection = await resolveConnection(options.apiUrl);
349
+ const connection = await resolveConnection(options);
344
350
  const path = `/api/v1/sources/${encodeURIComponent(id)}/settings`;
345
351
  let { settings } = await api(connection, path);
346
352
  if (assignments.length > 0) {
@@ -366,7 +372,7 @@ const commands = {
366
372
  async run(args, options) {
367
373
  if (args.length > 0)
368
374
  throw new CliError("Usage: intake list", exitCodes.usage);
369
- const { endpoints } = await api(await resolveConnection(options.apiUrl), INTAKE_PATH);
375
+ const { endpoints } = await api(await resolveConnection(options), INTAKE_PATH);
370
376
  if (options.format === "json")
371
377
  return printJson({ endpoints });
372
378
  if (endpoints.length === 0)
@@ -396,7 +402,7 @@ const commands = {
396
402
  ...(retentionDays === undefined ? {} : { retentionDays }),
397
403
  ...(signingKey === undefined ? {} : { signingKey }),
398
404
  };
399
- printIntakeToken(await api(await resolveConnection(options.apiUrl), INTAKE_PATH, body), options.format);
405
+ printIntakeToken(await api(await resolveConnection(options), INTAKE_PATH, body), options.format);
400
406
  },
401
407
  },
402
408
  "intake rotate": {
@@ -405,7 +411,7 @@ const commands = {
405
411
  if (args.length !== 1)
406
412
  throw new CliError("Usage: intake rotate <id>", exitCodes.usage);
407
413
  const path = `${intakePath(args[0])}/rotate`;
408
- printIntakeToken(await api(await resolveConnection(options.apiUrl), path, {}), options.format);
414
+ printIntakeToken(await api(await resolveConnection(options), path, {}), options.format);
409
415
  },
410
416
  },
411
417
  "intake signing-key": {
@@ -415,7 +421,7 @@ const commands = {
415
421
  throw new CliError("Usage: intake signing-key <id> --signing-key-stdin", exitCodes.usage);
416
422
  const signingKey = await readSigningKey();
417
423
  const path = `${intakePath(args[0])}/signing-key`;
418
- const { endpoint } = await api(await resolveConnection(options.apiUrl), path, { signingKey }, "PUT");
424
+ const { endpoint } = await api(await resolveConnection(options), path, { signingKey }, "PUT");
419
425
  if (options.format === "json")
420
426
  return printJson({ endpoint });
421
427
  console.log(`Replaced the ${endpoint.kind} signing key. Every ${endpoint.kind} address in the workspace now checks against it.`);
@@ -462,7 +468,7 @@ const commands = {
462
468
  if (!options.yes) {
463
469
  throw new CliError("This deletes the address and every event it has saved. Add --yes to confirm: intake delete <id> --yes", exitCodes.usage);
464
470
  }
465
- const result = await api(await resolveConnection(options.apiUrl), intakePath(args[0]), undefined, "DELETE");
471
+ const result = await api(await resolveConnection(options), intakePath(args[0]), undefined, "DELETE");
466
472
  if (options.format === "json")
467
473
  return printJson(result);
468
474
  console.log("Deleted the address and its saved events.");
@@ -476,7 +482,7 @@ const commands = {
476
482
  throw new CliError(usage, exitCodes.usage);
477
483
  const name = importName(options.table);
478
484
  const file = await readImportFile(args[0]);
479
- const connection = await resolveConnection(options.apiUrl);
485
+ const connection = await resolveConnection(options);
480
486
  const result = await uploadCsv(connection, name, file);
481
487
  if (options.format === "json")
482
488
  return printJson(result);
@@ -495,7 +501,7 @@ const commands = {
495
501
  throw new CliError(`This drops the table import_${name} and all its rows. Add --yes to confirm: import remove --table ${name} --yes`, exitCodes.usage);
496
502
  }
497
503
  const path = `/api/v1/imports/csv?table=${encodeURIComponent(name)}`;
498
- const result = await api(await resolveConnection(options.apiUrl), path, undefined, "DELETE");
504
+ const result = await api(await resolveConnection(options), path, undefined, "DELETE");
499
505
  if (options.format === "json")
500
506
  return printJson(result);
501
507
  console.log(`Removed ${result.table} and its rows.`);
@@ -512,7 +518,7 @@ const commands = {
512
518
  if (options.limit)
513
519
  query.set("limit", options.limit);
514
520
  const path = `/api/v1/creatives${query.size ? `?${query}` : ""}`;
515
- const { creatives } = await api(await resolveConnection(options.apiUrl), path);
521
+ const { creatives } = await api(await resolveConnection(options), path);
516
522
  if (options.format === "json")
517
523
  return printJson({ creatives });
518
524
  if (creatives.length === 0)
@@ -526,7 +532,7 @@ const commands = {
526
532
  async run(args, options) {
527
533
  if (args.length !== 1)
528
534
  throw new CliError("Usage: creatives show <sku>", exitCodes.usage);
529
- const { creative } = await api(await resolveConnection(options.apiUrl), `/api/v1/creatives/${encodeURIComponent(args[0])}`);
535
+ const { creative } = await api(await resolveConnection(options), `/api/v1/creatives/${encodeURIComponent(args[0])}`);
530
536
  if (options.format === "json")
531
537
  return printJson({ creative });
532
538
  const fields = {
@@ -539,20 +545,11 @@ const commands = {
539
545
  },
540
546
  },
541
547
  "creatives download": {
542
- usage: "creatives download <sku | asset-path | asset-link> [--out <path>] [--open]",
548
+ usage: "creatives download <sku | media-id | asset-path | asset-link> [--out <path>] [--open]",
543
549
  async run(args, options) {
544
550
  if (args.length !== 1)
545
- throw new CliError("Usage: creatives download <sku | asset-path | asset-link> [--out <path>] [--open]", exitCodes.usage);
546
- const connection = await resolveConnection(options.apiUrl);
547
- const target = await downloadTarget(args[0], connection);
548
- const { bytes, contentType } = await apiDownload(connection, `${ASSET_ROUTE}${target.assetPath.split("/").map(encodeURIComponent).join("/")}`);
549
- const path = await savePath(options.out, extname(target.fileName) ? target.fileName : `${target.fileName}.${extensionFor(contentType)}`);
550
- await saveFile(path, bytes);
551
- if (options.open)
552
- openFile(resolve(path));
553
- if (options.format === "json")
554
- return printJson({ path: resolve(path), bytes: bytes.byteLength, contentType });
555
- console.log(`Saved ${target.label} to ${path} (${fileSize(bytes.byteLength)}).`);
551
+ throw new CliError("Usage: creatives download <sku | media-id | asset-path | asset-link> [--out <path>] [--open]", exitCodes.usage);
552
+ await download(args[0], options);
556
553
  },
557
554
  },
558
555
  "creatives create": {
@@ -563,7 +560,7 @@ const commands = {
563
560
  }
564
561
  const lineage = options.lineage === undefined ? undefined : await parseJson(options.lineage, "--lineage");
565
562
  const tags = options.tags === undefined ? undefined : await parseJson(options.tags, "--tags");
566
- const { creatives } = await api(await resolveConnection(options.apiUrl), "/api/v1/creatives", {
563
+ const { creatives } = await api(await resolveConnection(options), "/api/v1/creatives", {
567
564
  runId: options.run,
568
565
  ...(lineage === undefined ? {} : { lineage }),
569
566
  ...(tags === undefined ? {} : { tags }),
@@ -579,18 +576,60 @@ const commands = {
579
576
  if (args.length !== 1)
580
577
  throw new CliError("Usage: creatives tag <sku> --file <tags.json> | < tags.json", exitCodes.usage);
581
578
  const tags = await readToolInput(options);
582
- const { creative } = await api(await resolveConnection(options.apiUrl), `/api/v1/creatives/${encodeURIComponent(args[0])}/tags`, { tags }, "PATCH");
579
+ const { creative } = await api(await resolveConnection(options), `/api/v1/creatives/${encodeURIComponent(args[0])}/tags`, { tags }, "PATCH");
583
580
  if (options.format === "json")
584
581
  return printJson({ creative });
585
582
  console.log(`Tagged ${creative.sku}. The tags read ${creative.tagsStatus} until a person confirms them in the app.`);
586
583
  },
587
584
  },
585
+ "media list": {
586
+ usage: "media list [--sku <sku>] [--run <tool-run-id>] [--tool <tool>] [--kind image|video] [--since <date>] [--until <date>] [--limit <n>] [--cursor <media-id>]",
587
+ async run(args, options) {
588
+ if (args.length > 0)
589
+ throw new CliError("Usage: media list [--sku <sku>] [--run <tool-run-id>] [--tool <tool>] [--kind image|video] [--since <date>] [--until <date>] [--limit <n>] [--cursor <media-id>]", exitCodes.usage);
590
+ const query = new URLSearchParams();
591
+ for (const name of ["sku", "run", "tool", "kind", "since", "until", "limit", "cursor"]) {
592
+ if (options[name])
593
+ query.set(name, options[name]);
594
+ }
595
+ const page = await api(await resolveConnection(options), `/api/v1/media${query.size ? `?${query}` : ""}`);
596
+ if (options.format === "json")
597
+ return printJson(page);
598
+ if (page.media.length === 0)
599
+ return console.log("No made images or videos match.");
600
+ const rows = page.media.map((m) => ({ ...m, prompt: excerpt(m.prompt ?? ""), cost: costLabel(m), sku: m.sku ?? "" }));
601
+ console.log(renderTable(["id", "tool", "prompt", "cost", "sku", "createdAt", "assetUrl"], rows));
602
+ if (page.nextCursor)
603
+ console.log(`\nMore: add --cursor ${page.nextCursor}`);
604
+ },
605
+ },
606
+ "media get": {
607
+ usage: "media get <media-id>",
608
+ async run(args, options) {
609
+ if (args.length !== 1)
610
+ throw new CliError("Usage: media get <media-id>", exitCodes.usage);
611
+ const { media } = await api(await resolveConnection(options), mediaPath(args[0]));
612
+ if (options.format === "json")
613
+ return printJson({ media });
614
+ const fields = { ...media, cost: costLabel(media) };
615
+ const shown = ["id", "tool", "model", "prompt", "input", "cost", "contentType", "sku", "creativeStatus", "assetUrl", "workerRunId", "createdAt"];
616
+ console.log(renderTable(["field", "value"], shown.filter((f) => fields[f] != null).map((f) => ({ field: f, value: fields[f] }))));
617
+ },
618
+ },
619
+ "media download": {
620
+ usage: "media download <media-id> [--out <path>] [--open]",
621
+ async run(args, options) {
622
+ if (args.length !== 1 || !parseMediaId(args[0]))
623
+ throw new CliError("Usage: media download <media-id> [--out <path>] [--open]", exitCodes.usage);
624
+ await download(args[0], options);
625
+ },
626
+ },
588
627
  "content create": {
589
628
  usage: "content create --kind <kind> [--name <name>] [--subject <s>] [--preview <s>] [--headline <s>] [--body <text> | @file] [--html @file] [--fields '<json>' | @file.json] [--file <path> ...] [--tool-run <id> ...] [--run <agent-run-id>] [--note <why>] [--plan <plan-id> [--role <role>]]",
590
629
  async run(args, options) {
591
630
  if (args.length > 0 || !options.kind)
592
631
  throw new CliError("Usage: content create --kind ad|email|sms|landing_page|video|social_post [...]", exitCodes.usage);
593
- const connection = await resolveConnection(options.apiUrl);
632
+ const connection = await resolveConnection(options);
594
633
  const content = await api(connection, "/api/v1/content", {
595
634
  kind: options.kind,
596
635
  ...(await contentInput(options, connection)),
@@ -608,7 +647,7 @@ const commands = {
608
647
  }
609
648
  if (options.language !== undefined && options.transcript === undefined)
610
649
  throw new CliError("--language goes with --transcript.", exitCodes.usage);
611
- const connection = await resolveConnection(options.apiUrl);
650
+ const connection = await resolveConnection(options);
612
651
  const content = await api(connection, "/api/v1/content/captures", {
613
652
  kind: options.kind,
614
653
  ...(await contentInput(options, connection)),
@@ -624,7 +663,7 @@ const commands = {
624
663
  async run(args, options) {
625
664
  if (args.length > 0)
626
665
  throw new CliError("Usage: guide", exitCodes.usage);
627
- const guide = await api(await resolveConnection(options.apiUrl), "/api/v1/guide");
666
+ const guide = await api(await resolveConnection(options), "/api/v1/guide");
628
667
  if (options.format === "json")
629
668
  return printJson(guide);
630
669
  process.stdout.write(guide.text);
@@ -636,7 +675,7 @@ const commands = {
636
675
  if (args.length > 0)
637
676
  throw new CliError("Usage: brief [--from <date>] [--to <date>] [--channel <channel>] [--campaign <id>] [--top <n>]", exitCodes.usage);
638
677
  const query = reportQuery(options, { channel: options.channel, campaignId: options.campaign, top: options.top });
639
- const brief = await api(await resolveConnection(options.apiUrl), `/api/v1/brief${query}`);
678
+ const brief = await api(await resolveConnection(options), `/api/v1/brief${query}`);
640
679
  if (options.format === "json")
641
680
  return printJson(brief);
642
681
  process.stdout.write(brief.text);
@@ -652,7 +691,7 @@ const commands = {
652
691
  for (const [name, value] of Object.entries({ step: options.step, funnel: options.funnel, page: options.page }))
653
692
  if (value !== undefined)
654
693
  query.set(name, value);
655
- const brief = await api(await resolveConnection(options.apiUrl), `/api/v1/pages/brief?${query}`);
694
+ const brief = await api(await resolveConnection(options), `/api/v1/pages/brief?${query}`);
656
695
  if (options.format === "json")
657
696
  return printJson(brief);
658
697
  process.stdout.write(brief.text);
@@ -663,7 +702,7 @@ const commands = {
663
702
  async run(args, options) {
664
703
  if (args.length > 0)
665
704
  throw new CliError("Usage: pages results [--from <date>] [--to <date>] [--page <address | page key | item id>]", exitCodes.usage);
666
- const results = await api(await resolveConnection(options.apiUrl), `/api/v1/pages/results${reportQuery(options, { page: options.page })}`);
705
+ const results = await api(await resolveConnection(options), `/api/v1/pages/results${reportQuery(options, { page: options.page })}`);
667
706
  if (options.format === "json")
668
707
  return printJson(results);
669
708
  printPageResults(results);
@@ -674,7 +713,7 @@ const commands = {
674
713
  async run(args, options) {
675
714
  if (args.length > 0)
676
715
  throw new CliError("Usage: messages results [--from <date>] [--to <date>] [--channel email|text] [--sequence <workflow id>]", exitCodes.usage);
677
- const results = await api(await resolveConnection(options.apiUrl), `/api/v1/messages/results${reportQuery(options, { channel: options.channel, sequence: options.sequence })}`);
716
+ const results = await api(await resolveConnection(options), `/api/v1/messages/results${reportQuery(options, { channel: options.channel, sequence: options.sequence })}`);
678
717
  if (options.format === "json")
679
718
  return printJson(results);
680
719
  printMessageResults(results);
@@ -689,7 +728,7 @@ const commands = {
689
728
  for (const [name, value] of Object.entries({ ...winnersPeriod(options), top: options.top, format: options.entryTags.format }))
690
729
  if (value !== undefined)
691
730
  query.set(name, value);
692
- const report = await api(await resolveConnection(options.apiUrl), `/api/v1/winners${query.size ? `?${query}` : ""}`);
731
+ const report = await api(await resolveConnection(options), `/api/v1/winners${query.size ? `?${query}` : ""}`);
693
732
  if (options.format === "json")
694
733
  return printJson(report);
695
734
  process.stdout.write(winnersText(report));
@@ -701,7 +740,7 @@ const commands = {
701
740
  if (args.length > 0)
702
741
  throw new CliError("Usage: winners send-to-exodus [--week <date> | --days <n> | --from <date> --to <date>] [--dry-run]", exitCodes.usage);
703
742
  const period = Object.entries(winnersPeriod(options)).filter(([, value]) => value !== undefined);
704
- const sent = await api(await resolveConnection(options.apiUrl), "/api/v1/winners/exodus", { ...Object.fromEntries(period), dryRun: options.dryRun });
743
+ const sent = await api(await resolveConnection(options), "/api/v1/winners/exodus", { ...Object.fromEntries(period), dryRun: options.dryRun });
705
744
  if (options.format === "json")
706
745
  printJson(sent);
707
746
  else
@@ -716,7 +755,7 @@ const commands = {
716
755
  async run(args, options) {
717
756
  if (args.length > 0)
718
757
  throw new CliError("Usage: winners goals", exitCodes.usage);
719
- printWinnersGoals(await api(await resolveConnection(options.apiUrl), "/api/v1/winners/goals"), options.format);
758
+ printWinnersGoals(await api(await resolveConnection(options), "/api/v1/winners/goals"), options.format);
720
759
  },
721
760
  },
722
761
  "winners goals set": {
@@ -724,7 +763,7 @@ const commands = {
724
763
  async run(args, options) {
725
764
  if (args.length === 0)
726
765
  throw new CliError("Usage: winners goals set <goal> [<goal> ...]", exitCodes.usage);
727
- printWinnersGoals(await api(await resolveConnection(options.apiUrl), "/api/v1/winners/goals", { goals: args }, "PUT"), options.format);
766
+ printWinnersGoals(await api(await resolveConnection(options), "/api/v1/winners/goals", { goals: args }, "PUT"), options.format);
728
767
  },
729
768
  },
730
769
  "winners goals reset": {
@@ -732,7 +771,7 @@ const commands = {
732
771
  async run(args, options) {
733
772
  if (args.length > 0)
734
773
  throw new CliError("Usage: winners goals reset", exitCodes.usage);
735
- printWinnersGoals(await api(await resolveConnection(options.apiUrl), "/api/v1/winners/goals", { goals: null }, "PUT"), options.format);
774
+ printWinnersGoals(await api(await resolveConnection(options), "/api/v1/winners/goals", { goals: null }, "PUT"), options.format);
736
775
  },
737
776
  },
738
777
  examples: {
@@ -740,7 +779,7 @@ const commands = {
740
779
  async run(args, options) {
741
780
  if (args.length > 0)
742
781
  throw new CliError("Usage: examples", exitCodes.usage);
743
- const { examples } = await api(await resolveConnection(options.apiUrl), "/api/v1/examples");
782
+ const { examples } = await api(await resolveConnection(options), "/api/v1/examples");
744
783
  if (options.format === "json")
745
784
  return printJson({ examples });
746
785
  for (const e of examples) {
@@ -758,7 +797,7 @@ const commands = {
758
797
  throw new CliError("Usage: content search [--text <words>] [--kind <kind>] [--sort <column>] [...]; put several words in quotes", exitCodes.usage);
759
798
  const filters = { text: options.text, mode: options.mode, kind: options.kind, platform: options.platform, origin: options.origin, from: options.from, to: options.to, sort: options.sort, order: options.order, limit: options.limit };
760
799
  const query = new URLSearchParams(Object.entries(filters).filter((entry) => entry[1] !== undefined));
761
- const { results } = await api(await resolveConnection(options.apiUrl), `/api/v1/content${query.size ? `?${query}` : ""}`);
800
+ const { results } = await api(await resolveConnection(options), `/api/v1/content${query.size ? `?${query}` : ""}`);
762
801
  if (options.format === "json")
763
802
  return printJson({ results });
764
803
  if (results.length === 0)
@@ -784,7 +823,7 @@ const commands = {
784
823
  async run(args, options) {
785
824
  if (args.length !== 1)
786
825
  throw new CliError("Usage: content show <sku | item-id>", exitCodes.usage);
787
- const content = await api(await resolveConnection(options.apiUrl), `/api/v1/content/${encodeURIComponent(args[0])}`);
826
+ const content = await api(await resolveConnection(options), `/api/v1/content/${encodeURIComponent(args[0])}`);
788
827
  printContent(content, options.format);
789
828
  },
790
829
  },
@@ -794,7 +833,7 @@ const commands = {
794
833
  if (args.length !== 1)
795
834
  throw new CliError("Usage: content pull <dir> [--kind <kind>] [--platform <platform>] [--origin discovered|made] [--media] [--full]", exitCodes.usage);
796
835
  const filters = Object.fromEntries(Object.entries({ kind: options.kind, platform: options.platform, origin: options.origin }).filter((e) => e[1] !== undefined));
797
- const report = await pullContent(await resolveConnection(options.apiUrl), args[0], filters, { media: options.media, full: options.full });
836
+ const report = await pullContent(await resolveConnection(options), args[0], filters, { media: options.media, full: options.full });
798
837
  if (options.format === "json")
799
838
  return printJson(report);
800
839
  const media = options.media ? `, ${report.media} new media file${report.media === 1 ? "" : "s"}` : "";
@@ -810,7 +849,7 @@ const commands = {
810
849
  if (args.length !== 1)
811
850
  throw new CliError("Usage: content diff <sku | item-id> [--from <n>] [--to <n>]", exitCodes.usage);
812
851
  const query = new URLSearchParams(Object.entries({ from: options.from, to: options.to }).filter((entry) => entry[1] !== undefined));
813
- const diff = await api(await resolveConnection(options.apiUrl), `/api/v1/content/${encodeURIComponent(args[0])}/diff${query.size ? `?${query}` : ""}`);
852
+ const diff = await api(await resolveConnection(options), `/api/v1/content/${encodeURIComponent(args[0])}/diff${query.size ? `?${query}` : ""}`);
814
853
  if (options.format === "json")
815
854
  return printJson(diff);
816
855
  printContentDiff(diff);
@@ -821,7 +860,7 @@ const commands = {
821
860
  async run(args, options) {
822
861
  if (args.length !== 1)
823
862
  throw new CliError("Usage: content edit <sku | item-id> [--body <text> | @file] [--note <why>] [...]", exitCodes.usage);
824
- const connection = await resolveConnection(options.apiUrl);
863
+ const connection = await resolveConnection(options);
825
864
  const input = await contentInput(options, connection);
826
865
  if (Object.keys(input).every((k) => k === "runId" || k === "note")) {
827
866
  throw new CliError("content edit needs something to change, such as --body, --subject or --file.", exitCodes.usage);
@@ -835,7 +874,7 @@ const commands = {
835
874
  async run(args, options) {
836
875
  if (args.length !== 2)
837
876
  throw new CliError("Usage: content status <sku | item-id> <status> [--platform <platform> [--object-type <type>] --external-id <id>]", exitCodes.usage);
838
- const content = await api(await resolveConnection(options.apiUrl), `/api/v1/content/${encodeURIComponent(args[0])}/status`, {
877
+ const content = await api(await resolveConnection(options), `/api/v1/content/${encodeURIComponent(args[0])}/status`, {
839
878
  status: args[1],
840
879
  ...(options.platform === undefined ? {} : { platform: options.platform }),
841
880
  ...(options.objectType === undefined ? {} : { objectType: options.objectType }),
@@ -853,7 +892,7 @@ const commands = {
853
892
  const version = options.version === undefined ? undefined : Number(options.version);
854
893
  if (version !== undefined && !(Number.isInteger(version) && version >= 1))
855
894
  throw new CliError("--version must be a whole number from 1.", exitCodes.usage);
856
- const { publication } = await api(await resolveConnection(options.apiUrl), `/api/v1/content/${encodeURIComponent(args[0])}/publish`, {
895
+ const { publication } = await api(await resolveConnection(options), `/api/v1/content/${encodeURIComponent(args[0])}/publish`, {
857
896
  publisher: options.to,
858
897
  target: await parseJson(options.target, "--target"),
859
898
  idempotencyKey: options.idempotencyKey,
@@ -869,7 +908,7 @@ const commands = {
869
908
  async run(args, options) {
870
909
  if (args.length !== 1)
871
910
  throw new CliError("Usage: content reconcile <publication-id>", exitCodes.usage);
872
- const { publication } = await api(await resolveConnection(options.apiUrl), `/api/v1/publications/${encodeURIComponent(args[0])}/reconcile`, {});
911
+ const { publication } = await api(await resolveConnection(options), `/api/v1/publications/${encodeURIComponent(args[0])}/reconcile`, {});
873
912
  printPublication(publication, options.format);
874
913
  },
875
914
  },
@@ -879,7 +918,7 @@ const commands = {
879
918
  if (args.length !== 0 || options.plan === undefined || !options.idempotencyKey) {
880
919
  throw new CliError("Usage: meta adset publish --plan '<json>' --idempotency-key <key> [--run <run-id>]", exitCodes.usage);
881
920
  }
882
- const { adsetPublication } = await api(await resolveConnection(options.apiUrl), "/api/v1/meta/adsets", {
921
+ const { adsetPublication } = await api(await resolveConnection(options), "/api/v1/meta/adsets", {
883
922
  plan: await parseJson(options.plan, "--plan"),
884
923
  idempotencyKey: options.idempotencyKey,
885
924
  ...(options.run === undefined ? {} : { runId: options.run }),
@@ -892,7 +931,7 @@ const commands = {
892
931
  async run(args, options) {
893
932
  if (args.length !== 1)
894
933
  throw new CliError("Usage: meta adset show <adset-publication-id>", exitCodes.usage);
895
- const { adsetPublication } = await api(await resolveConnection(options.apiUrl), `/api/v1/meta/adsets/${encodeURIComponent(args[0])}`);
934
+ const { adsetPublication } = await api(await resolveConnection(options), `/api/v1/meta/adsets/${encodeURIComponent(args[0])}`);
896
935
  printAdSetPublication(adsetPublication, options.format);
897
936
  },
898
937
  },
@@ -901,7 +940,7 @@ const commands = {
901
940
  async run(args, options) {
902
941
  if (args.length !== 1)
903
942
  throw new CliError("Usage: meta adset reconcile <adset-publication-id>", exitCodes.usage);
904
- const { adsetPublication } = await api(await resolveConnection(options.apiUrl), `/api/v1/meta/adsets/${encodeURIComponent(args[0])}/reconcile`, {});
943
+ const { adsetPublication } = await api(await resolveConnection(options), `/api/v1/meta/adsets/${encodeURIComponent(args[0])}/reconcile`, {});
905
944
  printAdSetPublication(adsetPublication, options.format);
906
945
  },
907
946
  },
@@ -910,7 +949,7 @@ const commands = {
910
949
  async run(args, options) {
911
950
  if (args.length !== 1)
912
951
  throw new CliError("Usage: content unpublish <deployment-id> [--force]", exitCodes.usage);
913
- const { deployment } = await api(await resolveConnection(options.apiUrl), `/api/v1/deployments/${encodeURIComponent(args[0])}/unpublish`, options.force ? { force: true } : {});
952
+ const { deployment } = await api(await resolveConnection(options), `/api/v1/deployments/${encodeURIComponent(args[0])}/unpublish`, options.force ? { force: true } : {});
914
953
  if (options.format === "json")
915
954
  return printJson({ deployment });
916
955
  printFields({
@@ -928,7 +967,7 @@ const commands = {
928
967
  async run(args, options) {
929
968
  if (args.length !== 1)
930
969
  throw new CliError("Usage: content publish-record <sku | item-id> --file <publication.json>", exitCodes.usage);
931
- const { publication } = await api(await resolveConnection(options.apiUrl), `/api/v1/content/${encodeURIComponent(args[0])}/publications`, await readToolInput(options));
970
+ const { publication } = await api(await resolveConnection(options), `/api/v1/content/${encodeURIComponent(args[0])}/publications`, await readToolInput(options));
932
971
  printPublication(publication, options.format);
933
972
  },
934
973
  },
@@ -939,7 +978,7 @@ const commands = {
939
978
  throw new CliError("Usage: content publish-finish <publication-id> --status sending|published|failed|not_sent_practice [--file <outcome.json>]", exitCodes.usage);
940
979
  }
941
980
  const outcome = await readToolInput(options);
942
- const { publication } = await api(await resolveConnection(options.apiUrl), `/api/v1/publications/${encodeURIComponent(args[0])}`, { ...outcome, status: options.status }, "PATCH");
981
+ const { publication } = await api(await resolveConnection(options), `/api/v1/publications/${encodeURIComponent(args[0])}`, { ...outcome, status: options.status }, "PATCH");
943
982
  printPublication(publication, options.format);
944
983
  },
945
984
  },
@@ -948,7 +987,7 @@ const commands = {
948
987
  async run(args, options) {
949
988
  if (args.length > 0)
950
989
  throw new CliError("Usage: agent start", exitCodes.usage);
951
- const start = await api(await resolveConnection(options.apiUrl), `${agentPath(options)}/runs`, {});
990
+ const start = await api(await resolveConnection(options), `${agentPath(options)}/runs`, {});
952
991
  if (options.format === "json")
953
992
  return printJson(start);
954
993
  const { run, quota, brandProfile } = start;
@@ -968,7 +1007,7 @@ const commands = {
968
1007
  async run(args, options) {
969
1008
  if (args.length !== 1)
970
1009
  throw new CliError("Usage: agent show <run-id>", exitCodes.usage);
971
- const shown = await api(await resolveConnection(options.apiUrl), `${agentPath(options)}/runs/${encodeURIComponent(args[0])}`);
1010
+ const shown = await api(await resolveConnection(options), `${agentPath(options)}/runs/${encodeURIComponent(args[0])}`);
972
1011
  if (options.format === "json")
973
1012
  return printJson(shown);
974
1013
  const { run, proposals } = shown;
@@ -982,7 +1021,7 @@ const commands = {
982
1021
  usage: "agent runs [--limit <n>]",
983
1022
  async run(_args, options) {
984
1023
  const query = options.limit ? `?limit=${encodeURIComponent(options.limit)}` : "";
985
- const { runs } = await api(await resolveConnection(options.apiUrl), `${agentPath(options)}/runs${query}`);
1024
+ const { runs } = await api(await resolveConnection(options), `${agentPath(options)}/runs${query}`);
986
1025
  if (options.format === "json")
987
1026
  return printJson({ runs });
988
1027
  if (runs.length === 0)
@@ -997,7 +1036,7 @@ const commands = {
997
1036
  throw new CliError("Usage: agent finish <run-id> --status succeeded|failed [--file report.md]", exitCodes.usage);
998
1037
  }
999
1038
  const report = await readText(options.file, "the report");
1000
- const { run } = await api(await resolveConnection(options.apiUrl), `${agentPath(options)}/runs/${encodeURIComponent(args[0])}/finish`, { status: options.status, report });
1039
+ const { run } = await api(await resolveConnection(options), `${agentPath(options)}/runs/${encodeURIComponent(args[0])}/finish`, { status: options.status, report });
1001
1040
  if (options.format === "json")
1002
1041
  return printJson({ run });
1003
1042
  console.log(`Finished run ${run.id} as ${run.status}.`);
@@ -1008,7 +1047,7 @@ const commands = {
1008
1047
  async run(args, options) {
1009
1048
  if (args.length > 0)
1010
1049
  throw new CliError("Usage: agent draft --agent p3_refresh", exitCodes.usage);
1011
- const body = await api(await resolveConnection(options.apiUrl), `${agentPath(options)}/draft`);
1050
+ const body = await api(await resolveConnection(options), `${agentPath(options)}/draft`);
1012
1051
  if (options.format === "json")
1013
1052
  return printJson(body);
1014
1053
  const { draft } = body;
@@ -1049,7 +1088,7 @@ const commands = {
1049
1088
  "brand get": {
1050
1089
  usage: "brand get",
1051
1090
  async run(_args, options) {
1052
- const { profile } = await api(await resolveConnection(options.apiUrl), "/api/v1/brand-profile");
1091
+ const { profile } = await api(await resolveConnection(options), "/api/v1/brand-profile");
1053
1092
  if (options.format === "json")
1054
1093
  return printJson({ profile });
1055
1094
  if (!profile)
@@ -1065,7 +1104,7 @@ const commands = {
1065
1104
  throw new CliError("Usage: brand set [--file <path>] [--sources '<json>' | @file.json]", exitCodes.usage);
1066
1105
  const markdown = await readText(options.file, "the brand profile");
1067
1106
  const sources = options.sources === undefined ? [] : await parseJson(options.sources, "--sources");
1068
- const { profile } = await api(await resolveConnection(options.apiUrl), "/api/v1/brand-profile", { markdown, sources }, "PUT");
1107
+ const { profile } = await api(await resolveConnection(options), "/api/v1/brand-profile", { markdown, sources }, "PUT");
1069
1108
  if (options.format === "json")
1070
1109
  return printJson({ profile });
1071
1110
  console.log(`Saved brand profile version ${profile.version}.`);
@@ -1076,7 +1115,7 @@ const commands = {
1076
1115
  async run(args, options) {
1077
1116
  if (args.length !== 1)
1078
1117
  throw new CliError("Usage: content add-page <url>", exitCodes.usage);
1079
- const { page } = await api(await resolveConnection(options.apiUrl), "/api/v1/content/pages", { url: args[0] });
1118
+ const { page } = await api(await resolveConnection(options), "/api/v1/content/pages", { url: args[0] });
1080
1119
  if (options.format === "json")
1081
1120
  return printJson({ page });
1082
1121
  console.log(renderTable(["key", "status", "version", "changed", "headline", "blocks"], [{ ...page, blocks: page.blockCount }]));
@@ -1090,7 +1129,7 @@ const commands = {
1090
1129
  async run(args, options) {
1091
1130
  if (args.length !== 2)
1092
1131
  throw new CliError("Usage: content track-link <sku> <url> [--channel facebook|email|sms|web] [--campaign <id>]", exitCodes.usage);
1093
- const { url } = await api(await resolveConnection(options.apiUrl), "/api/v1/content/track-link", {
1132
+ const { url } = await api(await resolveConnection(options), "/api/v1/content/track-link", {
1094
1133
  sku: args[0],
1095
1134
  url: args[1],
1096
1135
  channel: options.channel,
@@ -1106,7 +1145,7 @@ const commands = {
1106
1145
  async run(args, options) {
1107
1146
  if (args.length !== 1)
1108
1147
  throw new CliError("Usage: trail <plan-id | sku | item-id>", exitCodes.usage);
1109
- const connection = await resolveConnection(options.apiUrl);
1148
+ const connection = await resolveConnection(options);
1110
1149
  const ref = args[0];
1111
1150
  const trailOf = (query) => api(connection, `/api/v1/trail?${query}=${encodeURIComponent(ref)}`);
1112
1151
  // Plans and items both have uuid ids: an id that names no plan is read as an item.
@@ -1126,7 +1165,7 @@ const commands = {
1126
1165
  if (args.length > 0)
1127
1166
  throw new CliError("Usage: funnels show [--version <n>]", exitCodes.usage);
1128
1167
  const query = options.version === undefined ? "" : `?version=${wholeNumber(options.version, "--version")}`;
1129
- const view = await api(await resolveConnection(options.apiUrl), `${FUNNELS_PATH}${query}`);
1168
+ const view = await api(await resolveConnection(options), `${FUNNELS_PATH}${query}`);
1130
1169
  if (options.format === "json")
1131
1170
  return printJson(view);
1132
1171
  process.stdout.write(view.text);
@@ -1137,7 +1176,7 @@ const commands = {
1137
1176
  async run(args, options) {
1138
1177
  if (args.length > 0)
1139
1178
  throw new CliError("Usage: funnels numbers [--funnel <key>] [--from <date>] [--to <date>]", exitCodes.usage);
1140
- const view = await api(await resolveConnection(options.apiUrl), `${FUNNELS_PATH}/numbers${reportQuery(options, { funnel: options.funnel })}`);
1179
+ const view = await api(await resolveConnection(options), `${FUNNELS_PATH}/numbers${reportQuery(options, { funnel: options.funnel })}`);
1141
1180
  if (options.format === "json")
1142
1181
  return printJson(view);
1143
1182
  process.stdout.write(view.text);
@@ -1148,7 +1187,7 @@ const commands = {
1148
1187
  async run(args, options) {
1149
1188
  if (args.length > 0 || options.from !== undefined)
1150
1189
  throw new CliError("Usage: funnels leaks [--funnel <key>] [--to <date>]", exitCodes.usage);
1151
- const view = await api(await resolveConnection(options.apiUrl), `${FUNNELS_PATH}/leaks${reportQuery(options, { funnel: options.funnel })}`);
1190
+ const view = await api(await resolveConnection(options), `${FUNNELS_PATH}/leaks${reportQuery(options, { funnel: options.funnel })}`);
1152
1191
  if (options.format === "json")
1153
1192
  return printJson(view);
1154
1193
  process.stdout.write(view.text);
@@ -1159,7 +1198,7 @@ const commands = {
1159
1198
  async run(args, options) {
1160
1199
  if (args.length > 0)
1161
1200
  throw new CliError("Usage: funnels evidence", exitCodes.usage);
1162
- const view = await api(await resolveConnection(options.apiUrl), `${FUNNELS_PATH}/evidence`);
1201
+ const view = await api(await resolveConnection(options), `${FUNNELS_PATH}/evidence`);
1163
1202
  if (options.format === "json")
1164
1203
  return printJson(view);
1165
1204
  process.stdout.write(view.text);
@@ -1177,7 +1216,7 @@ const commands = {
1177
1216
  const map = await parseJson(`@${options.file}`, "--file");
1178
1217
  const evidence = options.evidence === undefined ? undefined : await parseJson(options.evidence, "--evidence");
1179
1218
  const questions = options.questions === undefined ? undefined : await parseJson(options.questions, "--questions");
1180
- const connection = await resolveConnection(options.apiUrl);
1219
+ const connection = await resolveConnection(options);
1181
1220
  const basedOn = options.basedOn === undefined ? null : wholeNumber(options.basedOn, "--based-on");
1182
1221
  const saved = await api(connection, `${FUNNELS_PATH}/draft`, { map, note: options.note, evidence, based_on: basedOn, counts_from: options.countsFrom, questions }, "PUT");
1183
1222
  if (options.format === "json")
@@ -1207,7 +1246,7 @@ const commands = {
1207
1246
  "funnels versions": {
1208
1247
  usage: "funnels versions [--diff <a> <b>] every version of the map, or what changed from version a to b",
1209
1248
  async run(args, options) {
1210
- const connection = await resolveConnection(options.apiUrl);
1249
+ const connection = await resolveConnection(options);
1211
1250
  if (options.diff !== undefined) {
1212
1251
  if (args.length !== 1)
1213
1252
  throw new CliError("Usage: funnels versions --diff <a> <b>", exitCodes.usage);
@@ -1254,7 +1293,7 @@ const commands = {
1254
1293
  }
1255
1294
  if (options.status)
1256
1295
  query.set("status", options.status);
1257
- const { facts } = await api(await resolveConnection(options.apiUrl), `${FACTS_PATH}${query.size ? `?${query}` : ""}`);
1296
+ const { facts } = await api(await resolveConnection(options), `${FACTS_PATH}${query.size ? `?${query}` : ""}`);
1258
1297
  if (options.format === "json")
1259
1298
  return printJson({ facts });
1260
1299
  if (facts.length === 0)
@@ -1276,7 +1315,7 @@ const commands = {
1276
1315
  query.set("sort", options.sort);
1277
1316
  if (options.limit)
1278
1317
  query.set("limit", options.limit);
1279
- const read = await api(await resolveConnection(options.apiUrl), `${VOICE_PATH}${query.size ? `?${query}` : ""}`);
1318
+ const read = await api(await resolveConnection(options), `${VOICE_PATH}${query.size ? `?${query}` : ""}`);
1280
1319
  if (options.format === "json")
1281
1320
  return printJson(read);
1282
1321
  if ("lines" in read) {
@@ -1301,7 +1340,7 @@ const commands = {
1301
1340
  query.set("status", options.status);
1302
1341
  if (options.limit)
1303
1342
  query.set("limit", options.limit);
1304
- const { tags } = await api(await resolveConnection(options.apiUrl), `${AD_TAGS_PATH}${query.size ? `?${query}` : ""}`);
1343
+ const { tags } = await api(await resolveConnection(options), `${AD_TAGS_PATH}${query.size ? `?${query}` : ""}`);
1305
1344
  if (options.format === "json")
1306
1345
  return printJson({ tags });
1307
1346
  if (tags.length === 0)
@@ -1314,7 +1353,7 @@ const commands = {
1314
1353
  async run(args, options) {
1315
1354
  if (args.length > 0)
1316
1355
  throw new CliError("Usage: ads tag --file <tags.json> | < tags.json", exitCodes.usage);
1317
- const result = await api(await resolveConnection(options.apiUrl), AD_TAGS_PATH, await readToolInput(options));
1356
+ const result = await api(await resolveConnection(options), AD_TAGS_PATH, await readToolInput(options));
1318
1357
  if (options.format === "json")
1319
1358
  return printJson(result);
1320
1359
  console.log(`Proposed tags for ${result.proposed.length} ${result.proposed.length === 1 ? "ad" : "ads"}. A person confirms or rejects them in the app.`);
@@ -1328,7 +1367,7 @@ const commands = {
1328
1367
  if (args.length > 0)
1329
1368
  throw new CliError("Usage: ads autotag [--limit <n>] [--ad <id>] [--json]", exitCodes.usage);
1330
1369
  const body = { ...(options.ad ? { ad_ids: [options.ad] } : {}), ...(options.limit ? { limit: Number(options.limit) } : {}) };
1331
- const result = await api(await resolveConnection(options.apiUrl), AD_AUTOTAG_PATH, body);
1370
+ const result = await api(await resolveConnection(options), AD_AUTOTAG_PATH, body);
1332
1371
  if (options.format === "json")
1333
1372
  return printJson(result);
1334
1373
  const plural = (n) => `${n} ${n === 1 ? "ad" : "ads"}`;
@@ -1355,7 +1394,7 @@ const commands = {
1355
1394
  async run(args, options) {
1356
1395
  if (args.length > 0)
1357
1396
  throw new CliError("Usage: ads tag-formats [--limit <n>] [--json]", exitCodes.usage);
1358
- const connection = await resolveConnection(options.apiUrl);
1397
+ const connection = await resolveConnection(options);
1359
1398
  const total = { tagged: 0, no_format: 0 };
1360
1399
  let after = null;
1361
1400
  for (;;) {
@@ -1384,7 +1423,7 @@ const commands = {
1384
1423
  throw new CliError("Usage: content tags [<item-id>] [--kind email|text|page] [--status <status>] [--limit <n>] [--json]", exitCodes.usage);
1385
1424
  const filters = { item: args[0], kind: options.kind, status: options.status, limit: options.limit };
1386
1425
  const query = new URLSearchParams(Object.entries(filters).filter((entry) => entry[1] !== undefined));
1387
- const { tags } = await api(await resolveConnection(options.apiUrl), `${CONTENT_TAGS_PATH}${query.size ? `?${query}` : ""}`);
1426
+ const { tags } = await api(await resolveConnection(options), `${CONTENT_TAGS_PATH}${query.size ? `?${query}` : ""}`);
1388
1427
  if (options.format === "json")
1389
1428
  return printJson({ tags });
1390
1429
  if (tags.length === 0)
@@ -1398,7 +1437,7 @@ const commands = {
1398
1437
  if (args.length > 0)
1399
1438
  throw new CliError("Usage: content to-check [--limit <n>] [--json]", exitCodes.usage);
1400
1439
  const query = new URLSearchParams({ to_check: "1", ...(options.limit ? { limit: options.limit } : {}) });
1401
- const { tags } = await api(await resolveConnection(options.apiUrl), `${CONTENT_TAGS_PATH}?${query}`);
1440
+ const { tags } = await api(await resolveConnection(options), `${CONTENT_TAGS_PATH}?${query}`);
1402
1441
  if (options.format === "json")
1403
1442
  return printJson({ tags });
1404
1443
  if (tags.length === 0)
@@ -1414,7 +1453,7 @@ const commands = {
1414
1453
  ...(options.kind ? { kinds: options.kind.split(",") } : {}),
1415
1454
  ...(options.limit ? { limit: Number(options.limit) } : {}),
1416
1455
  };
1417
- const result = await api(await resolveConnection(options.apiUrl), CONTENT_AUTOTAG_PATH, body);
1456
+ const result = await api(await resolveConnection(options), CONTENT_AUTOTAG_PATH, body);
1418
1457
  if (options.format === "json")
1419
1458
  return printJson(result);
1420
1459
  const plural = (n) => `${n} ${n === 1 ? "item" : "items"}`;
@@ -1439,7 +1478,7 @@ const commands = {
1439
1478
  async run(args, options) {
1440
1479
  if (args.length !== 1)
1441
1480
  throw new CliError("Usage: facts show <id>", exitCodes.usage);
1442
- const { fact } = await api(await resolveConnection(options.apiUrl), `${FACTS_PATH}/${encodeURIComponent(args[0])}`);
1481
+ const { fact } = await api(await resolveConnection(options), `${FACTS_PATH}/${encodeURIComponent(args[0])}`);
1443
1482
  if (options.format === "json")
1444
1483
  return printJson({ fact });
1445
1484
  const nameKey = fact.key === NAME_KEY ? nameKeyLines(fact.value) : null;
@@ -1454,7 +1493,7 @@ const commands = {
1454
1493
  async run(args, options) {
1455
1494
  if (args.length > 0)
1456
1495
  throw new CliError("Usage: facts propose --file <fact.json> | < fact.json", exitCodes.usage);
1457
- const { fact } = await api(await resolveConnection(options.apiUrl), FACTS_PATH, await readToolInput(options));
1496
+ const { fact } = await api(await resolveConnection(options), FACTS_PATH, await readToolInput(options));
1458
1497
  if (options.format === "json")
1459
1498
  return printJson({ fact });
1460
1499
  console.log(`Proposed fact ${fact.id}. A person confirms or rejects it in the app.`);
@@ -1474,7 +1513,7 @@ const commands = {
1474
1513
  for (const dimension of options.dimension ?? [])
1475
1514
  params.append("dimension", dimension);
1476
1515
  const query = params.size > 0 ? `?${params}` : "";
1477
- const { lessons } = await api(await resolveConnection(options.apiUrl), `${LESSONS_PATH}${query}`);
1516
+ const { lessons } = await api(await resolveConnection(options), `${LESSONS_PATH}${query}`);
1478
1517
  if (options.format === "json")
1479
1518
  return printJson({ lessons });
1480
1519
  if (lessons.length === 0) {
@@ -1489,7 +1528,7 @@ const commands = {
1489
1528
  async run(args, options) {
1490
1529
  if (args.length > 0)
1491
1530
  throw new CliError("Usage: lessons add --file <lesson.json> | < lesson.json", exitCodes.usage);
1492
- const recorded = await api(await resolveConnection(options.apiUrl), LESSONS_PATH, await readToolInput(options));
1531
+ const recorded = await api(await resolveConnection(options), LESSONS_PATH, await readToolInput(options));
1493
1532
  if (options.format === "json")
1494
1533
  return printJson(recorded);
1495
1534
  const { lesson, outcome } = recorded;
@@ -1513,7 +1552,7 @@ const commands = {
1513
1552
  query.set("funnel", options.funnel);
1514
1553
  if (options.limit)
1515
1554
  query.set("limit", options.limit);
1516
- const { tests } = await api(await resolveConnection(options.apiUrl), `${TESTS_PATH}${query.size ? `?${query}` : ""}`);
1555
+ const { tests } = await api(await resolveConnection(options), `${TESTS_PATH}${query.size ? `?${query}` : ""}`);
1517
1556
  if (options.format === "json")
1518
1557
  return printJson({ tests });
1519
1558
  if (tests.length === 0)
@@ -1526,7 +1565,7 @@ const commands = {
1526
1565
  async run(args, options) {
1527
1566
  if (args.length !== 1)
1528
1567
  throw new CliError("Usage: tests show <id|test_id>", exitCodes.usage);
1529
- const { test } = await api(await resolveConnection(options.apiUrl), testPath(args[0]));
1568
+ const { test } = await api(await resolveConnection(options), testPath(args[0]));
1530
1569
  if (options.format === "json")
1531
1570
  return printJson({ test });
1532
1571
  printTest(test);
@@ -1537,7 +1576,7 @@ const commands = {
1537
1576
  async run(args, options) {
1538
1577
  if (args.length > 0)
1539
1578
  throw new CliError("Usage: tests draft --file <test.json> | < test.json", exitCodes.usage);
1540
- const { test } = await api(await resolveConnection(options.apiUrl), TESTS_PATH, await readToolInput(options));
1579
+ const { test } = await api(await resolveConnection(options), TESTS_PATH, await readToolInput(options));
1541
1580
  if (options.format === "json")
1542
1581
  return printJson({ test });
1543
1582
  console.log(`Drafted test ${test.test_id} (${test.id}).`);
@@ -1548,7 +1587,7 @@ const commands = {
1548
1587
  async run(args, options) {
1549
1588
  if (args.length !== 1)
1550
1589
  throw new CliError("Usage: tests start <id|test_id>", exitCodes.usage);
1551
- const { test } = await api(await resolveConnection(options.apiUrl), `${testPath(args[0])}/start`, {});
1590
+ const { test } = await api(await resolveConnection(options), `${testPath(args[0])}/start`, {});
1552
1591
  if (options.format === "json")
1553
1592
  return printJson({ test });
1554
1593
  console.log(`Test ${test.test_id} is running from ${test.started_on}.`);
@@ -1559,7 +1598,7 @@ const commands = {
1559
1598
  async run(args, options) {
1560
1599
  if (args.length !== 1)
1561
1600
  throw new CliError('Usage: tests abandon <id|test_id> [--note "<why>"]', exitCodes.usage);
1562
- const { test } = await api(await resolveConnection(options.apiUrl), `${testPath(args[0])}/abandon`, options.note ? { note: options.note } : {});
1601
+ const { test } = await api(await resolveConnection(options), `${testPath(args[0])}/abandon`, options.note ? { note: options.note } : {});
1563
1602
  if (options.format === "json")
1564
1603
  return printJson({ test });
1565
1604
  console.log(`Abandoned test ${test.test_id}.`);
@@ -1570,7 +1609,7 @@ const commands = {
1570
1609
  async run(args, options) {
1571
1610
  if (args.length !== 1)
1572
1611
  throw new CliError("Usage: tests results <id|test_id>", exitCodes.usage);
1573
- const { results } = await api(await resolveConnection(options.apiUrl), `${testPath(args[0])}/results`);
1612
+ const { results } = await api(await resolveConnection(options), `${testPath(args[0])}/results`);
1574
1613
  if (options.format === "json")
1575
1614
  return printJson({ results });
1576
1615
  printResults(results);
@@ -1582,7 +1621,7 @@ const commands = {
1582
1621
  if (args.length !== 1 || !options.decision) {
1583
1622
  throw new CliError('Usage: tests decide <id|test_id> --decision <scale|continue|iterate|hold|stop> [--note "<why>"]', exitCodes.usage);
1584
1623
  }
1585
- const decided = await api(await resolveConnection(options.apiUrl), `${testPath(args[0])}/decide`, options.note ? { decision: options.decision, note: options.note } : { decision: options.decision });
1624
+ const decided = await api(await resolveConnection(options), `${testPath(args[0])}/decide`, options.note ? { decision: options.decision, note: options.note } : { decision: options.decision });
1586
1625
  if (options.format === "json")
1587
1626
  return printJson(decided);
1588
1627
  const { test, lesson, next_test: next } = decided;
@@ -1598,7 +1637,7 @@ const commands = {
1598
1637
  async run(args, options) {
1599
1638
  if (args.length !== 1 || !options.note)
1600
1639
  throw new CliError('Usage: lessons retire <id> --note "<why it is no longer true>"', exitCodes.usage);
1601
- const { lesson } = await api(await resolveConnection(options.apiUrl), `${LESSONS_PATH}/${encodeURIComponent(args[0])}/retire`, { reason: options.note });
1640
+ const { lesson } = await api(await resolveConnection(options), `${LESSONS_PATH}/${encodeURIComponent(args[0])}/retire`, { reason: options.note });
1602
1641
  if (options.format === "json")
1603
1642
  return printJson({ lesson });
1604
1643
  console.log(`Retired lesson ${lesson.id}.`);
@@ -1609,7 +1648,7 @@ const commands = {
1609
1648
  async run(args, options) {
1610
1649
  if (args.length > 0)
1611
1650
  throw new CliError("Usage: grammar | grammar propose --file <values.json>", exitCodes.usage);
1612
- const answer = await api(await resolveConnection(options.apiUrl), GRAMMAR_PATH);
1651
+ const answer = await api(await resolveConnection(options), GRAMMAR_PATH);
1613
1652
  if (options.format === "json")
1614
1653
  return printJson(answer);
1615
1654
  process.stdout.write(answer.text);
@@ -1620,7 +1659,7 @@ const commands = {
1620
1659
  async run(args, options) {
1621
1660
  if (args.length > 0)
1622
1661
  throw new CliError("Usage: grammar propose --file <values.json> | < values.json", exitCodes.usage);
1623
- const result = await api(await resolveConnection(options.apiUrl), `${GRAMMAR_PATH}/proposals`, await readToolInput(options));
1662
+ const result = await api(await resolveConnection(options), `${GRAMMAR_PATH}/proposals`, await readToolInput(options));
1624
1663
  if (options.format === "json")
1625
1664
  return printJson(result);
1626
1665
  const named = (refs) => refs.map((r) => `${r.part} ${r.key}`).join(", ");
@@ -1634,7 +1673,7 @@ const commands = {
1634
1673
  async run(args, options) {
1635
1674
  if (args.length > 0)
1636
1675
  throw new CliError("Usage: voice lists", exitCodes.usage);
1637
- const lists = await api(await resolveConnection(options.apiUrl), VOICE_LISTS_PATH);
1676
+ const lists = await api(await resolveConnection(options), VOICE_LISTS_PATH);
1638
1677
  if (options.format === "json")
1639
1678
  return printJson(lists);
1640
1679
  const rows = [
@@ -1655,7 +1694,7 @@ const commands = {
1655
1694
  if (!kind || !["competitor", "subreddit", "search_phrase"].includes(kind) || value === undefined || rest.length > 0) {
1656
1695
  throw new CliError('Usage: voice suggest competitor|subreddit|search_phrase "<value>" [--note <why>]; put a value with spaces in quotes', exitCodes.usage);
1657
1696
  }
1658
- const result = await api(await resolveConnection(options.apiUrl), VOICE_SUGGESTIONS_PATH, { kind, value, note: options.note });
1697
+ const result = await api(await resolveConnection(options), VOICE_SUGGESTIONS_PATH, { kind, value, note: options.note });
1659
1698
  if (options.format === "json")
1660
1699
  return printJson(result);
1661
1700
  if (result.outcome === "already_listed")
@@ -1670,7 +1709,7 @@ const commands = {
1670
1709
  async run(args, options) {
1671
1710
  if (args.length > 0)
1672
1711
  throw new CliError("Usage: story | story list | story show <version> | story submit --file <story.json>", exitCodes.usage);
1673
- const { story } = await api(await resolveConnection(options.apiUrl), `${STORIES_PATH}/latest`);
1712
+ const { story } = await api(await resolveConnection(options), `${STORIES_PATH}/latest`);
1674
1713
  if (options.format === "json")
1675
1714
  return printJson({ story });
1676
1715
  if (!story)
@@ -1683,7 +1722,7 @@ const commands = {
1683
1722
  async run(args, options) {
1684
1723
  if (args.length > 0)
1685
1724
  throw new CliError("Usage: story list", exitCodes.usage);
1686
- const { stories } = await api(await resolveConnection(options.apiUrl), STORIES_PATH);
1725
+ const { stories } = await api(await resolveConnection(options), STORIES_PATH);
1687
1726
  if (options.format === "json")
1688
1727
  return printJson({ stories });
1689
1728
  if (stories.length === 0)
@@ -1706,7 +1745,7 @@ const commands = {
1706
1745
  if (args.length !== 1)
1707
1746
  throw new CliError("Usage: story show <version>", exitCodes.usage);
1708
1747
  // `show latest` reaches the newest-story route, which answers null before the first story.
1709
- const { story } = await api(await resolveConnection(options.apiUrl), `${STORIES_PATH}/${encodeURIComponent(args[0])}`);
1748
+ const { story } = await api(await resolveConnection(options), `${STORIES_PATH}/${encodeURIComponent(args[0])}`);
1710
1749
  if (options.format === "json")
1711
1750
  return printJson({ story });
1712
1751
  if (!story)
@@ -1719,7 +1758,7 @@ const commands = {
1719
1758
  async run(args, options) {
1720
1759
  if (args.length > 0)
1721
1760
  throw new CliError("Usage: story submit --file <story.json> | < story.json", exitCodes.usage);
1722
- const { story } = await api(await resolveConnection(options.apiUrl), STORIES_PATH, await readToolInput(options));
1761
+ const { story } = await api(await resolveConnection(options), STORIES_PATH, await readToolInput(options));
1723
1762
  if (options.format === "json")
1724
1763
  return printJson({ story });
1725
1764
  console.log(`Saved story version ${story.version}.`);
@@ -1731,7 +1770,7 @@ const commands = {
1731
1770
  if (args.length > 0 || !options.name || !options.goal || !options.channel) {
1732
1771
  throw new CliError("Usage: plans create --name <name> --goal <text> --channel meta|ghl|klaviyo|web [--target '<json>'] [--landing <sku> | --landing-url <url>] [--funnel <key> --step <key>]", exitCodes.usage);
1733
1772
  }
1734
- const { plan } = await api(await resolveConnection(options.apiUrl), "/api/v1/launch-plans", {
1773
+ const { plan } = await api(await resolveConnection(options), "/api/v1/launch-plans", {
1735
1774
  name: options.name,
1736
1775
  goal: await textOrFile(options.goal),
1737
1776
  channel: options.channel,
@@ -1750,7 +1789,7 @@ const commands = {
1750
1789
  async run(args, options) {
1751
1790
  if (args.length !== 1)
1752
1791
  throw new CliError("Usage: plans show <plan-id>", exitCodes.usage);
1753
- const { plan } = await api(await resolveConnection(options.apiUrl), planPath(args[0]));
1792
+ const { plan } = await api(await resolveConnection(options), planPath(args[0]));
1754
1793
  printPlan(plan, options.format);
1755
1794
  },
1756
1795
  },
@@ -1762,7 +1801,7 @@ const commands = {
1762
1801
  const position = options.position === undefined ? undefined : Number(options.position);
1763
1802
  if (position !== undefined && (!Number.isInteger(position) || position < 0))
1764
1803
  throw new CliError("--position must be a whole number from 0.", exitCodes.usage);
1765
- const { plan } = await api(await resolveConnection(options.apiUrl), `${planPath(args[0])}/items`, {
1804
+ const { plan } = await api(await resolveConnection(options), `${planPath(args[0])}/items`, {
1766
1805
  item: args[1],
1767
1806
  ...(options.role === undefined ? {} : { role: options.role }),
1768
1807
  ...(position === undefined ? {} : { position }),
@@ -1776,7 +1815,7 @@ const commands = {
1776
1815
  if (args.length !== 2)
1777
1816
  throw new CliError("Usage: plans remove <plan-id> <sku | item-id>", exitCodes.usage);
1778
1817
  const path = `${planPath(args[0])}/items?item=${encodeURIComponent(args[1])}`;
1779
- const { plan } = await api(await resolveConnection(options.apiUrl), path, undefined, "DELETE");
1818
+ const { plan } = await api(await resolveConnection(options), path, undefined, "DELETE");
1780
1819
  printPlan(plan, options.format);
1781
1820
  },
1782
1821
  },
@@ -1785,7 +1824,7 @@ const commands = {
1785
1824
  async run(args, options) {
1786
1825
  if (args.length !== 2)
1787
1826
  throw new CliError("Usage: plans status <plan-id> draft|ready|publishing|live|done|cancelled", exitCodes.usage);
1788
- const { plan } = await api(await resolveConnection(options.apiUrl), planPath(args[0]), { status: args[1] }, "PATCH");
1827
+ const { plan } = await api(await resolveConnection(options), planPath(args[0]), { status: args[1] }, "PATCH");
1789
1828
  printPlan(plan, options.format);
1790
1829
  },
1791
1830
  },
@@ -1794,7 +1833,7 @@ const commands = {
1794
1833
  async run(args, options) {
1795
1834
  if (args.length > 0)
1796
1835
  throw new CliError("Usage: proposals create --input '<json>'", exitCodes.usage);
1797
- const { proposal } = await api(await resolveConnection(options.apiUrl), "/api/v1/proposals", await readToolInput(options));
1836
+ const { proposal } = await api(await resolveConnection(options), "/api/v1/proposals", await readToolInput(options));
1798
1837
  if (options.format === "json")
1799
1838
  return printJson({ proposal });
1800
1839
  console.log(renderTable(PROPOSAL_COLUMNS, [proposalRow(proposal)]));
@@ -1813,7 +1852,7 @@ const commands = {
1813
1852
  if (options.limit)
1814
1853
  query.set("limit", options.limit);
1815
1854
  const path = `/api/v1/proposals${query.size ? `?${query}` : ""}`;
1816
- const { proposals } = await api(await resolveConnection(options.apiUrl), path);
1855
+ const { proposals } = await api(await resolveConnection(options), path);
1817
1856
  if (options.format === "json")
1818
1857
  return printJson({ proposals });
1819
1858
  if (proposals.length === 0)
@@ -1841,7 +1880,7 @@ const commands = {
1841
1880
  if (options.harness !== undefined)
1842
1881
  query.set("harnesses", options.harness);
1843
1882
  const path = `/api/operator/skills/${name}/versions${query.size ? `?${query}` : ""}`;
1844
- const published = await apiUpload(await operatorConnection(options.apiUrl), path, zip, "application/zip");
1883
+ const published = await apiUpload(await operatorConnection(options), path, zip, "application/zip");
1845
1884
  if (options.format === "json")
1846
1885
  return printJson(published);
1847
1886
  console.log(`${published.name} version ${published.version}`);
@@ -1853,7 +1892,7 @@ const commands = {
1853
1892
  async run(args, options) {
1854
1893
  if (args.length > 0)
1855
1894
  throw new CliError("Usage: skill list", exitCodes.usage);
1856
- const { skills } = await api(await operatorConnection(options.apiUrl), "/api/operator/skills");
1895
+ const { skills } = await api(await operatorConnection(options), "/api/operator/skills");
1857
1896
  if (options.format === "json")
1858
1897
  return printJson({ skills });
1859
1898
  if (skills.length === 0)
@@ -1893,7 +1932,7 @@ const commands = {
1893
1932
  async run(args, options) {
1894
1933
  if (args.length > 0)
1895
1934
  throw new CliError("Usage: worker list", exitCodes.usage);
1896
- const { workers } = await api(await resolveConnection(options.apiUrl), "/api/v1/workers");
1935
+ const { workers } = await api(await resolveConnection(options), "/api/v1/workers");
1897
1936
  if (options.format === "json")
1898
1937
  return printJson({ workers });
1899
1938
  if (workers.length === 0)
@@ -1917,7 +1956,7 @@ const commands = {
1917
1956
  async run(args, options) {
1918
1957
  if (args.length !== 1)
1919
1958
  throw new CliError("Usage: worker runs <hand-id | skill> [--limit <n>]", exitCodes.usage);
1920
- const connection = await resolveConnection(options.apiUrl);
1959
+ const connection = await resolveConnection(options);
1921
1960
  const id = await workerId(connection, args[0]);
1922
1961
  const query = options.limit === undefined ? "" : `?limit=${encodeURIComponent(options.limit)}`;
1923
1962
  const { runs } = await api(connection, `/api/v1/workers/${encodeURIComponent(id)}/runs${query}`);
@@ -1933,7 +1972,7 @@ const commands = {
1933
1972
  async run(args, options) {
1934
1973
  if (args.length !== 1)
1935
1974
  throw new CliError("Usage: worker run-now <hand-id | skill>", exitCodes.usage);
1936
- const connection = await resolveConnection(options.apiUrl);
1975
+ const connection = await resolveConnection(options);
1937
1976
  const id = await workerId(connection, args[0]);
1938
1977
  const queued = await api(connection, `/api/v1/workers/${encodeURIComponent(id)}/run-now`, {});
1939
1978
  if (options.format === "json")
@@ -1993,6 +2032,18 @@ const commands = {
1993
2032
  },
1994
2033
  },
1995
2034
  };
2035
+ function clientReason(choice) {
2036
+ switch (choice.source) {
2037
+ case "env":
2038
+ return `Using the key in ${envPrefix}_API_KEY, which beats --client, client files and saved logins.`;
2039
+ case "flag":
2040
+ return `Using client ${choice.client}, from --client.`;
2041
+ case "folder":
2042
+ return `Using client ${choice.client}, from the client file ${choice.path}.`;
2043
+ case "default":
2044
+ return `Using the default login (no --client flag and no ${CLIENT_FILE} file in this folder or above it).`;
2045
+ }
2046
+ }
1996
2047
  /** Runs `work` with the runner's settings from the environment; SIGINT or SIGTERM stops the run in hand, reported failed. */
1997
2048
  async function withRunner(options, work) {
1998
2049
  const token = process.env[RUNNER_TOKEN_ENV];
@@ -2032,7 +2083,7 @@ async function installLibrarySkill(ref, options) {
2032
2083
  if (options.codex && options.project)
2033
2084
  throw new CliError("--codex installs a skill for every Codex run, under CODEX_HOME; drop --project", exitCodes.usage);
2034
2085
  const query = ref.version === null ? "" : `?version=${ref.version}`;
2035
- const download = await api(await operatorConnection(options.apiUrl), `/api/operator/skills/${ref.name}/download${query}`);
2086
+ const download = await api(await operatorConnection(options), `/api/operator/skills/${ref.name}/download${query}`);
2036
2087
  const files = unpackSkill(download, await getSigned(download.url));
2037
2088
  const { skillsDir, above } = options.codex ? { skillsDir: codexSkillsDir(), above: [] } : claudeSkillsDir(options.project);
2038
2089
  const path = await installSkillFolder(skillsDir, download.name, files, above);
@@ -2509,6 +2560,8 @@ ${Object.values(commands)
2509
2560
  Options:
2510
2561
  --format table|json output format (default: table)
2511
2562
  --api-url <url> API base URL
2563
+ --client <name> use that client's saved login (login --client <name> saves it); beats a ${CLIENT_FILE} file, which
2564
+ names the client for its folder and every folder below it; ${envPrefix}_API_KEY beats both
2512
2565
  --wait <seconds> how long tools run and tools get wait for a run (default: ${DEFAULT_WAIT_SECONDS}; 0 returns at once)
2513
2566
  --agent <kind> which agent the agent commands use: meta_ads (default) or p3_refresh; in a scheduled hand's run, skill
2514
2567
  (the run's skill's agent) is the default
@@ -2519,11 +2572,11 @@ Options:
2519
2572
  for warehouse schema, a table to describe in full; repeat for several
2520
2573
  --summary warehouse schema: one short row per table (kind, source, column count, summary)
2521
2574
  --search <words> warehouse schema: describe in full the tables whose name, description or columns mention a word
2522
- --out <path> the file, or existing folder, creatives download and warehouse query save to; warehouse query
2575
+ --out <path> the file, or existing folder, creatives download, media download and warehouse query save to; warehouse query
2523
2576
  writes CSV for a .csv path, otherwise JSON
2524
2577
  --all with warehouse query --out, save the whole answer (up to 1,000,000 rows or 50 MiB) instead of
2525
2578
  the first 1,000 rows or 5 MiB; .ndjson or .jsonl saves one JSON row per line
2526
- --open open the file creatives download saved
2579
+ --open open the file creatives download or media download saved
2527
2580
  --media with content pull, also save each item's files and HTML under media/ in the folder
2528
2581
  --full with content pull, fetch every item, not only what changed since the last pull (brings new numbers)
2529
2582
  --force content unpublish: take the page down even while delivering Meta ads link to it
@@ -2548,7 +2601,14 @@ Options:
2548
2601
  --source <text> where the content content capture saves came from, in words
2549
2602
  --url <url> the address of the content content capture saves
2550
2603
  --tool-run <id> a succeeded tool run content create and edit link; repeat for several
2551
- --run <id> the agent run making the content (content), or the tool run to record (creatives create)
2604
+ --run <id> the agent run making the content (content), the tool run to record (creatives create), or the tool
2605
+ run whose outputs media list shows
2606
+ --sku <sku> with media list, only the output that became this creative
2607
+ --tool <tool> with media list, only this tool's outputs, such as kie:nano-banana
2608
+ --kind image|video with media list, only images or only videos
2609
+ --since, --until <YYYY-MM-DD | ISO time> with media list, made at or after --since and before --until (a day: through
2610
+ its end, in UTC)
2611
+ --cursor <media-id> with media list, the next page: the id it printed after "More"
2552
2612
  --platform, --external-id where content status launched says it was published, and that platform's id for it
2553
2613
  --object-type <type> what that platform's sync calls it (campaign_message or flow_message on klaviyo); needed where
2554
2614
  the sync matches several kinds, so its next read joins the item
@@ -2580,9 +2640,9 @@ Options:
2580
2640
  --from, --to <n> with content diff, the two versions to compare (default: the one before the current one, and the current one)
2581
2641
  --sort <column> how content search sorts: recent (default) or a content_scorecard number, such as click_rate or roas
2582
2642
  --order desc|asc content search's sort direction (default: desc, best or newest first)
2583
- --limit <n> how many results content search, creatives list, campaigns, voice, ads tags, content tags and content to-check
2584
- return, how many ads ads autotag tags and how many items content autotag tags, or how many
2585
- ads each ads tag-formats batch looks at (default 2000)
2643
+ --limit <n> how many results content search, creatives list, media list, campaigns, voice, ads tags, content tags
2644
+ and content to-check return, how many ads ads autotag tags and how many items content autotag
2645
+ tags, or how many ads each ads tag-formats batch looks at (default 2000)
2586
2646
  --library <slug> with knowledge search, only this library: creative-cartel, exodus or meta-policy
2587
2647
  --angle, --hook-kind, --ad-format, --awareness <value> with knowledge search, only library entries tagged that way, and
2588
2648
  your own ads tagged the same way with their results; with knowledge tag, the tags to put on an entry;
@@ -2619,6 +2679,7 @@ function parseOptions(argv) {
2619
2679
  format: { type: "string", default: "table" },
2620
2680
  json: { type: "boolean", default: false },
2621
2681
  "api-url": { type: "string" },
2682
+ client: { type: "string" },
2622
2683
  file: { type: "string", multiple: true },
2623
2684
  project: { type: "boolean", default: false },
2624
2685
  input: { type: "string" },
@@ -2648,6 +2709,10 @@ function parseOptions(argv) {
2648
2709
  yes: { type: "boolean", default: false },
2649
2710
  from: { type: "string" },
2650
2711
  since: { type: "string" },
2712
+ until: { type: "string" },
2713
+ sku: { type: "string" },
2714
+ tool: { type: "string" },
2715
+ cursor: { type: "string" },
2651
2716
  to: { type: "string" },
2652
2717
  grain: { type: "string" },
2653
2718
  channel: { type: "string" },
@@ -2727,6 +2792,7 @@ function parseOptions(argv) {
2727
2792
  options: {
2728
2793
  format: values.json ? "json" : values.format,
2729
2794
  apiUrl: values["api-url"],
2795
+ client: values.client,
2730
2796
  file: values.file?.at(-1),
2731
2797
  files: values.file ?? [],
2732
2798
  project: values.project,
@@ -2758,6 +2824,10 @@ function parseOptions(argv) {
2758
2824
  yes: values.yes,
2759
2825
  from: values.from,
2760
2826
  since: values.since,
2827
+ until: values.until,
2828
+ sku: values.sku,
2829
+ tool: values.tool,
2830
+ cursor: values.cursor,
2761
2831
  to: values.to,
2762
2832
  grain: values.grain,
2763
2833
  channel: values.channel,
@@ -2822,7 +2892,7 @@ function parseOptions(argv) {
2822
2892
  async function changeCandidate(change, args, options) {
2823
2893
  if (args.length !== 1 || !options.workspace)
2824
2894
  throw new CliError(`Usage: worker ${change} <skill> --workspace <slug>`, exitCodes.usage);
2825
- return api(await operatorConnection(options.apiUrl), `/api/operator/workers/${change}`, { workspace: options.workspace, skill: args[0] });
2895
+ return api(await operatorConnection(options), `/api/operator/workers/${change}`, { workspace: options.workspace, skill: args[0] });
2826
2896
  }
2827
2897
  function findCommand(positionals) {
2828
2898
  const three = positionals.slice(0, 3).join(" ");
@@ -3369,8 +3439,44 @@ async function readImportFile(path) {
3369
3439
  return readFile(path);
3370
3440
  }
3371
3441
  const ASSET_ROUTE = "/api/v1/assets/";
3372
- /** What `creatives download` fetches, from a SKU, an asset path, or an asset link as `creatives show` prints it. */
3442
+ /** Saves a creative's or a made output's file, as `creatives download` and `media download` do. */
3443
+ async function download(arg, options) {
3444
+ const connection = await resolveConnection(options);
3445
+ const target = await downloadTarget(arg, connection);
3446
+ const { bytes, contentType } = await apiDownload(connection, `${ASSET_ROUTE}${target.assetPath.split("/").map(encodeURIComponent).join("/")}`);
3447
+ const path = await savePath(options.out, extname(target.fileName) ? target.fileName : `${target.fileName}.${extensionFor(contentType)}`);
3448
+ await saveFile(path, bytes);
3449
+ if (options.open)
3450
+ openFile(resolve(path));
3451
+ if (options.format === "json")
3452
+ return printJson({ path: resolve(path), bytes: bytes.byteLength, contentType });
3453
+ console.log(`Saved ${target.label} to ${path} (${fileSize(bytes.byteLength)}).`);
3454
+ }
3455
+ const MEDIA_ID = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}):(0|[1-9]\d{0,3})$/i;
3456
+ function parseMediaId(raw) {
3457
+ const match = MEDIA_ID.exec(raw);
3458
+ return match ? { runId: match[1].toLowerCase(), index: Number(match[2]) } : null;
3459
+ }
3460
+ // A media id's colon is sent as it is, so the route reads the id whether or not the server decodes the path.
3461
+ function mediaPath(raw) {
3462
+ const id = parseMediaId(raw);
3463
+ if (!id)
3464
+ throw new CliError(`"${excerpt(raw)}" is not a media id; media list prints them as <run id>:<index>.`, exitCodes.usage);
3465
+ return `/api/v1/media/${id.runId}:${id.index}`;
3466
+ }
3467
+ function costLabel(media) {
3468
+ return `${media.costCents}¢${media.costIsEstimate ? " (estimate)" : ""}`;
3469
+ }
3470
+ /** What `creatives download` fetches, from a SKU, a media id, an asset path, or an asset link as `creatives show` prints it. */
3373
3471
  async function downloadTarget(arg, connection) {
3472
+ const output = parseMediaId(arg);
3473
+ if (output) {
3474
+ const { media } = await api(connection, mediaPath(arg));
3475
+ if (!media.assetPath)
3476
+ throw new CliError(`${media.id} has no stored file to download.`, exitCodes.error);
3477
+ const name = media.sku ?? `${output.runId}-${output.index}`;
3478
+ return { label: media.sku ?? media.id, assetPath: media.assetPath, fileName: `${name}${extname(media.assetPath)}` };
3479
+ }
3374
3480
  const routeAt = arg.indexOf(ASSET_ROUTE);
3375
3481
  if (routeAt !== -1 || arg.includes("/")) {
3376
3482
  const assetPath = routeAt === -1 ? arg : decodeLinkPath(arg.slice(routeAt + ASSET_ROUTE.length).replace(/[?#].*$/, ""));
@@ -3571,7 +3677,7 @@ function printIntakeToken({ endpoint, address, token, link, subscription }, form
3571
3677
  console.error(`The token for ${endpoint.label} is shown only now. Rotate it if it is lost.`);
3572
3678
  }
3573
3679
  async function patchIntake(id, body, options, done) {
3574
- const { endpoint } = await api(await resolveConnection(options.apiUrl), intakePath(id), body, "PATCH");
3680
+ const { endpoint } = await api(await resolveConnection(options), intakePath(id), body, "PATCH");
3575
3681
  if (options.format === "json")
3576
3682
  return printJson({ endpoint });
3577
3683
  console.log(done(endpoint));