@wrangled/cli 0.3.49 → 0.3.50

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/api.js CHANGED
@@ -21,6 +21,8 @@ export class UnreadableCredentialsError extends CliError {
21
21
  * in src/lib/knowledge/tags.ts. format's flag is --ad-format, because --format picks the output.
22
22
  */
23
23
  export const entryTagFlags = { angle: "angle", hook_kind: "hook-kind", format: "ad-format", awareness: "awareness" };
24
+ /** The most ads or ad sets one meta ads|adset pause|resume sends; the server refuses more. */
25
+ export const META_IDS_MAX = 10;
24
26
  /** The days before a sale in which a click or reply helps it, as the app counts them (MESSAGE_CREDIT_DAYS). */
25
27
  export const MESSAGE_CREDIT_DAYS = 7;
26
28
  /** Copies of the server's IMPORT_LIMITS and import name rule; src/lib/cli-consistency.test.ts keeps them in step. */
package/dist/main.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
+ import { randomUUID } from "node:crypto";
3
4
  import { mkdir, readFile, stat } from "node:fs/promises";
4
5
  import { homedir } from "node:os";
5
6
  import { basename, dirname, extname, join, resolve } from "node:path";
6
7
  import { parseArgs } from "node:util";
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
+ import { api, ApiError, apiDownload, apiText, apiUpload, CliError, exitCodes, getSigned, IMPORT_LIMITS, META_IDS_MAX, IMPORT_NAME_PATTERN, putSigned, CLIENT_FILE, parseClientName, readSavedLogins, removeCredentials, resolveApiUrl, resolveConnection, resolveLogin, UnreadableCredentialsError, writeCredentials, entryTagFlags, streamSigned, } from "./api.js";
8
9
  import { brand, envPrefix } from "./brand.js";
9
10
  import { firstSymlink, inRunFolder, writeFileAtomic } from "./files.js";
10
11
  import { scheduleCreateBody, scheduleIdFor, SCHEDULES_CREATE_USAGE } from "./schedules.js";
@@ -17,6 +18,7 @@ import { AGENT_ENV, HARNESS_COMMAND_ENV, RUNNER_TOKEN_ENV, runLoop, runOnce } fr
17
18
  import { SKILL_NAME_PATTERN } from "./skill-zip.js";
18
19
  import { installSkillFolder, operatorConnection, packSkill, parseSkillRef, readSkillFolder, unpackSkill, } from "./skills.js";
19
20
  const DEFAULT_WAIT_SECONDS = 300;
21
+ const META_CHANGE_COLUMNS = ["id", "level", "object", "change", "status", "when", "by", "reason"];
20
22
  const SERVER_WAIT_CAP = 50;
21
23
  const VOICE_USAGE = "voice [--kind opener|objection|pain|desire|phrase] [--since <YYYY-MM-DD>] [--sort common|useful] [--limit <n>] [--json]";
22
24
  const commands = {
@@ -997,6 +999,46 @@ const commands = {
997
999
  printAdSetPublication(adsetPublication, options.format);
998
1000
  },
999
1001
  },
1002
+ "meta ads pause": metaStatusCommand("ads", "pause"),
1003
+ "meta ads resume": metaStatusCommand("ads", "resume"),
1004
+ "meta adset pause": metaStatusCommand("adset", "pause"),
1005
+ "meta adset resume": metaStatusCommand("adset", "resume"),
1006
+ "meta changes": {
1007
+ usage: "meta changes [--since <YYYY-MM-DD>] the log of Meta ads and ad sets turned on or off, newest first",
1008
+ async run(args, options) {
1009
+ if (args.length !== 0)
1010
+ throw new CliError("Usage: meta changes [--since <YYYY-MM-DD>]", exitCodes.usage);
1011
+ const query = new URLSearchParams();
1012
+ if (options.since)
1013
+ query.set("since", options.since);
1014
+ const { changes } = await api(await resolveConnection(options), `/api/v1/meta/changes${query.size ? `?${query}` : ""}`);
1015
+ if (options.format === "json")
1016
+ return printJson({ changes });
1017
+ console.log(changes.length ? renderTable(META_CHANGE_COLUMNS, changes.map(metaChangeRow)) : "No Meta changes yet.");
1018
+ },
1019
+ },
1020
+ "meta changes undo": {
1021
+ usage: 'meta changes undo <change-id> --reason "<why>" [--idempotency-key <key>] put the ad or ad set back the way it was',
1022
+ async run(args, options) {
1023
+ if (args.length !== 1 || !options.reason)
1024
+ throw new CliError('Usage: meta changes undo <change-id> --reason "<why>" [--idempotency-key <key>]', exitCodes.usage);
1025
+ const idempotencyKey = options.idempotencyKey ?? announceKey(`undo-${randomUUID()}`);
1026
+ const result = await api(await resolveConnection(options), `/api/v1/meta/changes/${encodeURIComponent(args[0])}/undo`, {
1027
+ reason: options.reason,
1028
+ idempotencyKey,
1029
+ });
1030
+ printMetaStatusResult(result, options.format);
1031
+ },
1032
+ },
1033
+ "meta changes reconcile": {
1034
+ usage: "meta changes reconcile <change-id> settle a change Meta's answer was lost for, from what Meta reads now",
1035
+ async run(args, options) {
1036
+ if (args.length !== 1)
1037
+ throw new CliError("Usage: meta changes reconcile <change-id>", exitCodes.usage);
1038
+ const { change } = await api(await resolveConnection(options), `/api/v1/meta/changes/${encodeURIComponent(args[0])}/reconcile`, {});
1039
+ printMetaStatusResult({ changes: [change], unchanged: [] }, options.format);
1040
+ },
1041
+ },
1000
1042
  "content unpublish": {
1001
1043
  usage: "content unpublish <deployment-id> [--force]",
1002
1044
  async run(args, options) {
@@ -2817,13 +2859,14 @@ Options:
2817
2859
  --source <text> where the content content capture saves came from, in words
2818
2860
  --url <url> the address of the content content capture saves
2819
2861
  --tool-run <id> a succeeded tool run content create and edit link; repeat for several
2820
- --run <id> the agent run making the content (content), the tool run to record (creatives create), or the tool
2821
- run whose outputs media list shows
2862
+ --run <id> the agent run making the content (content) or the Meta change (meta ads|adset), the tool run to
2863
+ record (creatives create), or the tool run whose outputs media list shows
2864
+ --reason <why> why: what meta ads|adset pause|resume and meta changes undo record in the change log
2822
2865
  --sku <sku> with media list, only the output that became this creative
2823
2866
  --tool <tool> with media list, only this tool's outputs, such as kie:nano-banana
2824
2867
  --kind image|video with media list, only images or only videos
2825
2868
  --since, --until <YYYY-MM-DD | ISO time> with media list, made at or after --since and before --until (a day: through
2826
- its end, in UTC)
2869
+ its end, in UTC); with meta changes, only changes from --since on
2827
2870
  --cursor <media-id> with media list, the next page: the id it printed after "More"
2828
2871
  --platform, --external-id where content status launched says it was published, and that platform's id for it
2829
2872
  --object-type <type> what that platform's sync calls it (campaign_message or flow_message on klaviyo); needed where
@@ -2916,6 +2959,7 @@ function parseOptions(argv) {
2916
2959
  ad: { type: "string" },
2917
2960
  limit: { type: "string" },
2918
2961
  run: { type: "string" },
2962
+ reason: { type: "string" },
2919
2963
  lineage: { type: "string" },
2920
2964
  tags: { type: "string" },
2921
2965
  sources: { type: "string" },
@@ -3041,6 +3085,7 @@ function parseOptions(argv) {
3041
3085
  ad: values.ad,
3042
3086
  limit: values.limit,
3043
3087
  run: values.run,
3088
+ reason: values.reason,
3044
3089
  lineage: values.lineage,
3045
3090
  tags: values.tags,
3046
3091
  sources: values.sources,
@@ -3581,6 +3626,54 @@ function printPublication(publication, format) {
3581
3626
  finished: publication.finishedAt,
3582
3627
  });
3583
3628
  }
3629
+ function metaStatusCommand(noun, verb) {
3630
+ const usage = `meta ${noun} ${verb} <${noun === "ads" ? "ad" : "adset"}-id>... --reason "<why>" [--run <run-id>] [--idempotency-key <key>]`;
3631
+ return {
3632
+ usage: `${usage} ${verb === "pause" ? "turn off" : "turn on"} up to ${META_IDS_MAX} ${noun === "ads" ? "ads" : "ad sets"} on the control list`,
3633
+ async run(args, options) {
3634
+ if (args.length === 0 || args.length > META_IDS_MAX || !options.reason)
3635
+ throw new CliError(`Usage: ${usage}`, exitCodes.usage);
3636
+ const idempotencyKey = options.idempotencyKey ?? announceKey(`${verb}-${randomUUID()}`);
3637
+ const result = await api(await resolveConnection(options), "/api/v1/meta/changes", {
3638
+ level: noun === "ads" ? "ad" : "adset",
3639
+ ids: args,
3640
+ status: verb === "pause" ? "paused" : "active",
3641
+ reason: options.reason,
3642
+ idempotencyKey,
3643
+ ...(options.run === undefined ? {} : { runId: options.run }),
3644
+ });
3645
+ printMetaStatusResult(result, options.format);
3646
+ },
3647
+ };
3648
+ }
3649
+ function metaChangeRow(c) {
3650
+ return {
3651
+ id: c.id,
3652
+ level: c.level,
3653
+ object: c.objectId,
3654
+ change: `${c.before.status} -> ${c.after.status}`,
3655
+ status: c.undoneBy ? `${c.status} (undone by ${c.undoneBy})` : c.error ? `${c.status}: ${c.error}` : c.status,
3656
+ when: c.createdAt,
3657
+ by: c.requestedBy,
3658
+ reason: excerpt(c.reason, 60),
3659
+ };
3660
+ }
3661
+ /** Told on stderr before the request goes out, so a request whose answer is lost can be sent again with the same key. */
3662
+ function announceKey(key) {
3663
+ console.error(`Idempotency key: ${key} (if no answer comes, send the same command again with --idempotency-key ${key})`);
3664
+ return key;
3665
+ }
3666
+ function printMetaStatusResult(result, format) {
3667
+ if (format === "json")
3668
+ return printJson(result);
3669
+ if (result.changes.length > 0)
3670
+ console.log(renderTable(META_CHANGE_COLUMNS, result.changes.map(metaChangeRow)));
3671
+ for (const u of result.unchanged)
3672
+ console.log(`${u.level === "ad" ? "Ad" : "Ad set"} ${u.objectId} was already ${u.status}; nothing was sent.`);
3673
+ for (const c of result.changes.filter((c) => c.status === "sending")) {
3674
+ console.log(`Meta's answer for change ${c.id} was lost. Run \`${brand.slug} meta changes reconcile ${c.id}\` in a minute to settle it.`);
3675
+ }
3676
+ }
3584
3677
  function printAdSetPublication(publication, format) {
3585
3678
  if (format === "json")
3586
3679
  return printJson({ adsetPublication: publication });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrangled/cli",
3
- "version": "0.3.49",
3
+ "version": "0.3.50",
4
4
  "description": "Command-line access to your marketing data warehouse, for people and coding agents",
5
5
  "type": "module",
6
6
  "bin": {