@wrangled/cli 0.3.48 → 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/guide.js CHANGED
@@ -17,6 +17,24 @@ export const funnelTestExample = {
17
17
  split_method: "adzara_ads",
18
18
  evidence: [{ kind: "customer_words", ref: "sales call 2026-09-30", note: "I didn't know what the webinar was about." }],
19
19
  };
20
+ /** The dashboard the guide shows; the app's parser must accept it (src/lib/cli-consistency.test.ts). */
21
+ export const dashboardExample = {
22
+ title: "Daily ads and leads",
23
+ dateRange: { default: "30d", picker: true },
24
+ panels: [
25
+ {
26
+ id: "spend",
27
+ title: "Ad spend by day",
28
+ sql: "SELECT date, sum(spend) AS spend FROM daily_funnel WHERE date BETWEEN {{from}} AND {{to}} GROUP BY date ORDER BY date",
29
+ chart: "line",
30
+ x: "date",
31
+ y: ["spend"],
32
+ format: { spend: "money" },
33
+ width: 2,
34
+ },
35
+ { id: "leads", title: "Leads", sql: "SELECT sum(leads) AS leads FROM daily_funnel WHERE date BETWEEN {{from}} AND {{to}}", chart: "number", y: ["leads"], format: { leads: "count" } },
36
+ ],
37
+ };
20
38
  export const guide = `# ${brand.name} CLI
21
39
 
22
40
  \`${cli}\` gives read-only SQL access to one client's marketing data warehouse (ad spend, orders, and other synced sources), runs paid generation and research tools for that client, and keeps the creative ledger. Each API key belongs to exactly one workspace, and every command works only in that workspace.
@@ -266,6 +284,20 @@ Each section's \`audience\` is \`staff\`, \`client\` or \`both\`; a person on st
266
284
 
267
285
  The story hand writes it each week. Its playbook installs with \`${cli} skill install story\` as the \`${STORY_SKILL_NAME}\` skill and prints with \`${cli} docs story\`.
268
286
 
287
+ ## Team dashboards
288
+
289
+ A team dashboard is a page of charts the team opens for one client, such as daily ad spend, leads and cost per lead with a date picker. You write it as a short spec; ${brand.name} saves it and reads every number live each time someone opens it, so nothing is copied out. \`${cli} dashboards create --file spec.json\` publishes one (or pipe the spec on stdin):
290
+
291
+ \`\`\`json
292
+ ${JSON.stringify(dashboardExample)}
293
+ \`\`\`
294
+
295
+ Each panel is one SQL query and how to draw it: \`chart\` is \`line\`, \`bar\`, \`area\`, \`table\` or \`number\`. A line, bar or area panel names its \`x\` column and its \`y\` columns; a number panel one \`y\` column. \`format\` marks columns as \`money\`, \`count\` or \`percent\`, and \`width\` 2 takes the full row. \`dateRange.default\` is \`7d\`, \`30d\`, \`90d\`, \`this-month\` or \`last-month\`, and \`picker\` shows a date picker. Write \`{{from}}\` and \`{{to}}\` where the page's first and last day go; the server binds them as dates in the client's reporting time zone, and refuses any other \`{{...}}\`. A dashboard has at most 20 panels and 20 KB of SQL in each.
296
+
297
+ Before saving, the server runs every panel once over the default range as this workspace's reader. A panel that fails, or lacks a column it names, refuses the whole spec with the panel's id and, when it knows one, a hint at the right name. The answer has \`url\`, the link that opens the dashboard for this client: give it to the person who asked.
298
+
299
+ \`${cli} dashboards update <slug> --file spec.json\` publishes the next version of the whole spec, \`${cli} dashboards rollback <slug> --version <n>\` publishes an earlier version's spec again as the next version, and \`${cli} dashboards archive <slug>\` takes one down. \`${cli} dashboards list\` lists them, and \`${cli} dashboards show <slug> --data\` prints the spec, every version, and each panel's rows now (\`--from\` and \`--to\` pick the days), so you can check the numbers.
300
+
269
301
  ## The Facebook ads agent
270
302
 
271
303
  The daily Facebook ads agent follows its own playbook: install it with \`${cli} skill install meta-ads\` (or \`--codex\` for an AGENTS.md in the current directory) and read it with \`${cli} docs meta-ads\`. It works in runs (\`${cli} agent start\`, \`${cli} agent finish\`), reads and suggests settled facts (\`${cli} facts\`), keeps a brand profile (\`${cli} brand\`), and makes proposals (\`${cli} proposals\`) that a person approves in the web app. Nothing it does is sent to Meta.
@@ -383,6 +415,8 @@ ${cli} voice lists where that voice comes from: the competitors
383
415
  ${cli} voice suggest competitor|subreddit|search_phrase "<value>" [--note <why>] suggest a competitor, subreddit or Reddit search phrase for a person to approve
384
416
  ${cli} story [--json] | story list | story show <version> the weekly story, its versions, and the rows behind each sentence
385
417
  ${cli} story submit --file <story.json> save the next version of the weekly story
418
+ ${cli} dashboards list | dashboards show <slug> [--data] [--from <date>] [--to <date>] the team dashboards, one's panels and versions, and its numbers now
419
+ ${cli} dashboards create --file <spec.json> | dashboards update <slug> --file <spec.json> | dashboards rollback <slug> --version <n> | dashboards archive <slug>
386
420
  ${cli} worker list | worker runs <hand-id | skill> [--limit <n>] | worker run-now <hand-id | skill> this workspace's scheduled hands, their runs and costs, and Run now
387
421
  ${cli} skill install [${PLAYBOOK_CHOICES}] [--project] [--codex] install a Claude Code skill (or the playbook for Codex)
388
422
  ${cli} docs [${PLAYBOOK_CHOICES}] print this guide, or a hand's playbook
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) {
@@ -1806,6 +1848,83 @@ const commands = {
1806
1848
  printStory(story);
1807
1849
  },
1808
1850
  },
1851
+ "dashboards list": {
1852
+ usage: "dashboards list the team dashboards, the most recently changed first, with the link that opens each",
1853
+ async run(args, options) {
1854
+ if (args.length > 0)
1855
+ throw new CliError("Usage: dashboards list", exitCodes.usage);
1856
+ const { dashboards } = await api(await resolveConnection(options), DASHBOARDS_PATH);
1857
+ if (options.format === "json")
1858
+ return printJson({ dashboards });
1859
+ if (dashboards.length === 0)
1860
+ return console.log("No dashboards yet.");
1861
+ console.log(renderTable(["slug", "title", "version", "updated", "url"], dashboards.map((d) => ({ ...d, updated: d.updatedAt }))));
1862
+ },
1863
+ },
1864
+ "dashboards show": {
1865
+ usage: "dashboards show <slug> [--data] [--from <date>] [--to <date>] one dashboard's panels and versions; --data reads every panel's rows now",
1866
+ async run(args, options) {
1867
+ if (args.length !== 1)
1868
+ throw new CliError("Usage: dashboards show <slug> [--data] [--from <date>] [--to <date>]", exitCodes.usage);
1869
+ if (!options.data && (options.from !== undefined || options.to !== undefined))
1870
+ throw new CliError("--from and --to go with --data.", exitCodes.usage);
1871
+ const connection = await resolveConnection(options);
1872
+ const { dashboard } = await api(connection, dashboardPath(args[0]));
1873
+ let data;
1874
+ if (options.data) {
1875
+ const range = new URLSearchParams(Object.entries({ from: options.from, to: options.to }).filter((e) => e[1] !== undefined));
1876
+ data = JSON.parse(await apiText(connection, `${dashboardPath(args[0])}/data${range.size > 0 ? `?${range}` : ""}`), keepRawNumbers);
1877
+ }
1878
+ if (options.format === "json")
1879
+ return printJson(data ? { dashboard, data } : { dashboard });
1880
+ printDashboard(dashboard, data);
1881
+ },
1882
+ },
1883
+ "dashboards create": {
1884
+ usage: "dashboards create --file <spec.json> | < spec.json publish a new dashboard once every panel runs",
1885
+ async run(args, options) {
1886
+ if (args.length > 0)
1887
+ throw new CliError("Usage: dashboards create --file <spec.json> | < spec.json", exitCodes.usage);
1888
+ const { dashboard } = await api(await resolveConnection(options), DASHBOARDS_PATH, await readToolInput(options));
1889
+ if (options.format === "json")
1890
+ return printJson({ dashboard });
1891
+ console.log(`Published dashboard ${dashboard.slug}, version ${dashboard.version}.\n${dashboard.url}`);
1892
+ },
1893
+ },
1894
+ "dashboards update": {
1895
+ usage: "dashboards update <slug> --file <spec.json> | < spec.json publish the dashboard's next version once every panel runs",
1896
+ async run(args, options) {
1897
+ if (args.length !== 1)
1898
+ throw new CliError("Usage: dashboards update <slug> --file <spec.json> | < spec.json", exitCodes.usage);
1899
+ const { dashboard } = await api(await resolveConnection(options), dashboardPath(args[0]), await readToolInput(options), "PUT");
1900
+ if (options.format === "json")
1901
+ return printJson({ dashboard });
1902
+ console.log(`Published dashboard ${dashboard.slug}, version ${dashboard.version}.\n${dashboard.url}`);
1903
+ },
1904
+ },
1905
+ "dashboards rollback": {
1906
+ usage: "dashboards rollback <slug> --version <n> publish an earlier version's spec again, as the next version",
1907
+ async run(args, options) {
1908
+ if (args.length !== 1 || options.version === undefined)
1909
+ throw new CliError("Usage: dashboards rollback <slug> --version <n>", exitCodes.usage);
1910
+ const version = wholeNumber(options.version, "--version");
1911
+ const { dashboard } = await api(await resolveConnection(options), `${dashboardPath(args[0])}/rollback`, { version });
1912
+ if (options.format === "json")
1913
+ return printJson({ dashboard });
1914
+ console.log(`Published version ${version}'s spec as dashboard ${dashboard.slug}, version ${dashboard.version}.\n${dashboard.url}`);
1915
+ },
1916
+ },
1917
+ "dashboards archive": {
1918
+ usage: "dashboards archive <slug> take a dashboard down; its versions are kept, and publishing at its slug brings it back",
1919
+ async run(args, options) {
1920
+ if (args.length !== 1)
1921
+ throw new CliError("Usage: dashboards archive <slug>", exitCodes.usage);
1922
+ const { archived } = await api(await resolveConnection(options), dashboardPath(args[0]), undefined, "DELETE");
1923
+ if (options.format === "json")
1924
+ return printJson({ archived });
1925
+ console.log(`Archived dashboard ${archived.slug}.`);
1926
+ },
1927
+ },
1809
1928
  "story submit": {
1810
1929
  usage: "story submit --file <story.json> | < story.json save the next version; the server runs each query and keeps its rows",
1811
1930
  async run(args, options) {
@@ -2626,6 +2745,26 @@ function nameKeyLines(value) {
2626
2745
  return ["", "Name patterns:", ...(patterns.length > 0 ? patterns : ["- none yet"]), "", "Open questions:", ...(questions.length > 0 ? questions : ["- none"])];
2627
2746
  }
2628
2747
  const STORIES_PATH = "/api/v1/stories";
2748
+ const DASHBOARDS_PATH = "/api/v1/dashboards";
2749
+ const dashboardPath = (slug) => `${DASHBOARDS_PATH}/${encodeURIComponent(slug)}`;
2750
+ function printDashboard(dashboard, data) {
2751
+ console.log(`${dashboard.title}: version ${dashboard.version}, updated ${dashboard.updatedAt}.\n${dashboard.url}`);
2752
+ const range = dashboard.spec.dateRange;
2753
+ if (range)
2754
+ console.log(`Default range ${range.default}${range.picker ? ", with a date picker" : ""}.`);
2755
+ console.log(`\n${renderTable(["panel", "title", "chart", "x", "y"], dashboard.spec.panels.map((p) => ({ panel: p.id, title: p.title, chart: p.chart, x: p.x, y: p.y?.join(", ") })))}`);
2756
+ console.log(`\nVersions:\n${renderTable(["version", "by", "at"], dashboard.versions.map((v) => ({ version: v.version, by: v.createdBy, at: v.createdAt })))}`);
2757
+ if (!data)
2758
+ return;
2759
+ console.log(`\nNumbers from ${data.range.from} to ${data.range.to}:`);
2760
+ for (const panel of data.panels) {
2761
+ console.log(`\n${panel.id}`);
2762
+ if ("error" in panel)
2763
+ console.log(`failed: ${panel.error.message}${panel.error.hint ? `\nHint: ${panel.error.hint}` : ""}`);
2764
+ else
2765
+ console.log(`${renderTable(panel.columns.map((c) => c.name), panel.rows)}${panel.truncated ? `\n(cut off at the ${panel.truncated.replace("_", " ")})` : ""}`);
2766
+ }
2767
+ }
2629
2768
  function printStory(story) {
2630
2769
  const corrects = story.previousId ? `, correcting ${story.previousId}` : "";
2631
2770
  console.log(`Story version ${story.version}, up to ${story.periodEnd}, saved ${story.createdAt} by ${story.author}${corrects}.`);
@@ -2720,13 +2859,14 @@ Options:
2720
2859
  --source <text> where the content content capture saves came from, in words
2721
2860
  --url <url> the address of the content content capture saves
2722
2861
  --tool-run <id> a succeeded tool run content create and edit link; repeat for several
2723
- --run <id> the agent run making the content (content), the tool run to record (creatives create), or the tool
2724
- 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
2725
2865
  --sku <sku> with media list, only the output that became this creative
2726
2866
  --tool <tool> with media list, only this tool's outputs, such as kie:nano-banana
2727
2867
  --kind image|video with media list, only images or only videos
2728
2868
  --since, --until <YYYY-MM-DD | ISO time> with media list, made at or after --since and before --until (a day: through
2729
- its end, in UTC)
2869
+ its end, in UTC); with meta changes, only changes from --since on
2730
2870
  --cursor <media-id> with media list, the next page: the id it printed after "More"
2731
2871
  --platform, --external-id where content status launched says it was published, and that platform's id for it
2732
2872
  --object-type <type> what that platform's sync calls it (campaign_message or flow_message on klaviyo); needed where
@@ -2819,6 +2959,7 @@ function parseOptions(argv) {
2819
2959
  ad: { type: "string" },
2820
2960
  limit: { type: "string" },
2821
2961
  run: { type: "string" },
2962
+ reason: { type: "string" },
2822
2963
  lineage: { type: "string" },
2823
2964
  tags: { type: "string" },
2824
2965
  sources: { type: "string" },
@@ -2902,6 +3043,7 @@ function parseOptions(argv) {
2902
3043
  confidence: { type: "string" },
2903
3044
  remove: { type: "boolean", default: false },
2904
3045
  "dry-run": { type: "boolean", default: false },
3046
+ data: { type: "boolean", default: false },
2905
3047
  skill: { type: "string" },
2906
3048
  cron: { type: "string" },
2907
3049
  "time-zone": { type: "string" },
@@ -2943,6 +3085,7 @@ function parseOptions(argv) {
2943
3085
  ad: values.ad,
2944
3086
  limit: values.limit,
2945
3087
  run: values.run,
3088
+ reason: values.reason,
2946
3089
  lineage: values.lineage,
2947
3090
  tags: values.tags,
2948
3091
  sources: values.sources,
@@ -3024,6 +3167,7 @@ function parseOptions(argv) {
3024
3167
  confidence: values.confidence,
3025
3168
  remove: values.remove,
3026
3169
  dryRun: values["dry-run"],
3170
+ data: values.data,
3027
3171
  schedule: {
3028
3172
  skill: values.skill,
3029
3173
  name: values.name,
@@ -3482,6 +3626,54 @@ function printPublication(publication, format) {
3482
3626
  finished: publication.finishedAt,
3483
3627
  });
3484
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
+ }
3485
3677
  function printAdSetPublication(publication, format) {
3486
3678
  if (format === "json")
3487
3679
  return printJson({ adsetPublication: publication });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrangled/cli",
3
- "version": "0.3.48",
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": {