@wrangled/cli 0.3.45 → 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/guide.js CHANGED
@@ -140,6 +140,7 @@ A webhook address lets a tool such as a form or webinar platform push events int
140
140
  3. A run that is still going prints its id. Check it with \`${cli} tools get <run-id>\`. Image results list an \`asset\` link. Save the file with \`${cli} creatives download <asset-link>\`, or fetch the link with your API key as a bearer token; it redirects to a copy that works for 10 minutes.
141
141
  4. Record a succeeded run in the ledger with its lineage: \`${cli} creatives create --run <run-id> --lineage '{"persona": "...", "painPoint": "...", "desiredOutcome": "...", "angle": "...", "sourceAd": {"metaAdId": "..."}}'\`. Each output gets a SKU such as \`DLC-000123\`. The SKU goes in the Meta ad name. Lineage is free-text notes; give the ad's marketing tags with \`--tags '<json>'\` or \`--tags @tags.json\`, the same fields as ad tags (see Ad tags), such as \`{"offer_key": "course", "landing_funnel_key": "webinar_to_course", "landing_step_key": "registration_page", "persona": "near_retirement", "angle": "story", "format": "image"}\`. Every output of the run gets them. Values come from \`${cli} grammar\`; persona and concept from this client's lists, and offer, funnel and step from its confirmed map (\`${cli} funnels show\`), where the step must be in that funnel and \`reply\` or \`no_link\` take no step. A value off them is refused with the ones to use. Recording the same run again returns its creatives as they are, tags included; \`${cli} creatives tag <sku> --file tags.json\` is how to change a creative's tags. Your tags read proposed until an Owner or Admin confirms them on the creative's page in Content; after that only a person changes them.
142
142
  5. \`${cli} creatives list\` and \`${cli} creatives show <sku>\` read the ledger. \`${cli} creatives download <sku>\` saves a creative's image or video as \`<sku>.<ext>\` in the current folder; \`--out <path>\` names the file or a folder, and \`--open\` opens it. It also takes an asset path or link, such as a \`meta_ad_creatives\` \`image_path\`. \`--format json\` prints \`{ path, bytes, contentType }\`. Copy has no file; its text is in \`${cli} creatives show\`. For performance, query the \`creatives\` table or the \`creative_performance_daily\` view with \`${cli} warehouse query\`.
143
+ 6. \`${cli} media list\` lists every image and video the tools made, newest first, whether or not it became a creative: its media id (\`<run id>:<index>\`), tool, prompt, cost and SKU, if any. Narrow it with \`--sku <sku>\`, \`--run <run-id>\` (one batch), \`--tool kie:nano-banana\`, \`--kind image|video\`, \`--since\` and \`--until\` (a day such as \`2026-10-08\`, which \`--until\` counts through its end in UTC, or an ISO time), and \`--limit <n>\` (default 50, at most 200). When more remain it prints the \`--cursor <media-id>\` that shows the next page. A run's cost is split evenly across its outputs and marked as an estimate when the provider did not report a charge. \`${cli} media get <media-id>\` shows the full input that made one, to reuse a good prompt; \`${cli} media download <media-id>\` saves its file, named by its SKU when it has one (\`${cli} creatives download\` takes a media id too). \`--format json\` prints \`{ media, nextCursor }\`.
143
144
 
144
145
  ## Landing pages
145
146
 
@@ -326,9 +327,12 @@ ${cli} tools run <tool> --input '<json>' [--idempotency-key <key>] [--wait <seco
326
327
  ${cli} tools get <run-id> show a tool run, waiting for it to finish (--wait)
327
328
  ${cli} creatives list list creatives (--status, --kind, --limit)
328
329
  ${cli} creatives show <sku> show one creative with its spec and lineage
329
- ${cli} creatives download <sku | asset-path | asset-link> [--out <path>] [--open] save a creative's image or video
330
+ ${cli} creatives download <sku | media-id | asset-path | asset-link> [--out <path>] [--open] save a creative's image or video
330
331
  ${cli} creatives create --run <run-id> [--lineage '<json>' | @file.json] [--tags '<json>' | @file.json] record a run's output with a new SKU
331
332
  ${cli} creatives tag <sku> --file <tags.json> replace a creative's marketing tags, proposed until a person confirms them
333
+ ${cli} media list list every made image and video (--sku, --run, --tool, --kind, --since, --until, --limit, --cursor)
334
+ ${cli} media get <media-id> show the tool, model, full input and cost that made one
335
+ ${cli} media download <media-id> [--out <path>] [--open] save a made image or video
332
336
  ${cli} content create --kind <kind> [--body ...] [--file <path>] [--note <why>] [--plan <plan-id>] save made content with a new SKU
333
337
  ${cli} content search [--text <words>] [--mode word|meaning] [--kind <kind>] [--platform <p>] [--origin <o>] [--from <date>] [--to <date>] [--sort <column>] [--order asc|desc] [--limit <n>] find content with its numbers
334
338
  ${cli} content pull <dir> [--kind <kind>] [--platform <p>] [--origin <o>] [--media] [--full] copy the library to a folder of Markdown files; rerun for changes
package/dist/main.js CHANGED
@@ -545,20 +545,11 @@ const commands = {
545
545
  },
546
546
  },
547
547
  "creatives download": {
548
- 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]",
549
549
  async run(args, options) {
550
550
  if (args.length !== 1)
551
- throw new CliError("Usage: creatives download <sku | asset-path | asset-link> [--out <path>] [--open]", exitCodes.usage);
552
- const connection = await resolveConnection(options);
553
- const target = await downloadTarget(args[0], connection);
554
- const { bytes, contentType } = await apiDownload(connection, `${ASSET_ROUTE}${target.assetPath.split("/").map(encodeURIComponent).join("/")}`);
555
- const path = await savePath(options.out, extname(target.fileName) ? target.fileName : `${target.fileName}.${extensionFor(contentType)}`);
556
- await saveFile(path, bytes);
557
- if (options.open)
558
- openFile(resolve(path));
559
- if (options.format === "json")
560
- return printJson({ path: resolve(path), bytes: bytes.byteLength, contentType });
561
- 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);
562
553
  },
563
554
  },
564
555
  "creatives create": {
@@ -591,6 +582,48 @@ const commands = {
591
582
  console.log(`Tagged ${creative.sku}. The tags read ${creative.tagsStatus} until a person confirms them in the app.`);
592
583
  },
593
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
+ },
594
627
  "content create": {
595
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>]]",
596
629
  async run(args, options) {
@@ -2539,11 +2572,11 @@ Options:
2539
2572
  for warehouse schema, a table to describe in full; repeat for several
2540
2573
  --summary warehouse schema: one short row per table (kind, source, column count, summary)
2541
2574
  --search <words> warehouse schema: describe in full the tables whose name, description or columns mention a word
2542
- --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
2543
2576
  writes CSV for a .csv path, otherwise JSON
2544
2577
  --all with warehouse query --out, save the whole answer (up to 1,000,000 rows or 50 MiB) instead of
2545
2578
  the first 1,000 rows or 5 MiB; .ndjson or .jsonl saves one JSON row per line
2546
- --open open the file creatives download saved
2579
+ --open open the file creatives download or media download saved
2547
2580
  --media with content pull, also save each item's files and HTML under media/ in the folder
2548
2581
  --full with content pull, fetch every item, not only what changed since the last pull (brings new numbers)
2549
2582
  --force content unpublish: take the page down even while delivering Meta ads link to it
@@ -2568,7 +2601,14 @@ Options:
2568
2601
  --source <text> where the content content capture saves came from, in words
2569
2602
  --url <url> the address of the content content capture saves
2570
2603
  --tool-run <id> a succeeded tool run content create and edit link; repeat for several
2571
- --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"
2572
2612
  --platform, --external-id where content status launched says it was published, and that platform's id for it
2573
2613
  --object-type <type> what that platform's sync calls it (campaign_message or flow_message on klaviyo); needed where
2574
2614
  the sync matches several kinds, so its next read joins the item
@@ -2600,9 +2640,9 @@ Options:
2600
2640
  --from, --to <n> with content diff, the two versions to compare (default: the one before the current one, and the current one)
2601
2641
  --sort <column> how content search sorts: recent (default) or a content_scorecard number, such as click_rate or roas
2602
2642
  --order desc|asc content search's sort direction (default: desc, best or newest first)
2603
- --limit <n> how many results content search, creatives list, campaigns, voice, ads tags, content tags and content to-check
2604
- return, how many ads ads autotag tags and how many items content autotag tags, or how many
2605
- 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)
2606
2646
  --library <slug> with knowledge search, only this library: creative-cartel, exodus or meta-policy
2607
2647
  --angle, --hook-kind, --ad-format, --awareness <value> with knowledge search, only library entries tagged that way, and
2608
2648
  your own ads tagged the same way with their results; with knowledge tag, the tags to put on an entry;
@@ -2669,6 +2709,10 @@ function parseOptions(argv) {
2669
2709
  yes: { type: "boolean", default: false },
2670
2710
  from: { type: "string" },
2671
2711
  since: { type: "string" },
2712
+ until: { type: "string" },
2713
+ sku: { type: "string" },
2714
+ tool: { type: "string" },
2715
+ cursor: { type: "string" },
2672
2716
  to: { type: "string" },
2673
2717
  grain: { type: "string" },
2674
2718
  channel: { type: "string" },
@@ -2780,6 +2824,10 @@ function parseOptions(argv) {
2780
2824
  yes: values.yes,
2781
2825
  from: values.from,
2782
2826
  since: values.since,
2827
+ until: values.until,
2828
+ sku: values.sku,
2829
+ tool: values.tool,
2830
+ cursor: values.cursor,
2783
2831
  to: values.to,
2784
2832
  grain: values.grain,
2785
2833
  channel: values.channel,
@@ -3391,8 +3439,44 @@ async function readImportFile(path) {
3391
3439
  return readFile(path);
3392
3440
  }
3393
3441
  const ASSET_ROUTE = "/api/v1/assets/";
3394
- /** 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. */
3395
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
+ }
3396
3480
  const routeAt = arg.indexOf(ASSET_ROUTE);
3397
3481
  if (routeAt !== -1 || arg.includes("/")) {
3398
3482
  const assetPath = routeAt === -1 ? arg : decodeLinkPath(arg.slice(routeAt + ASSET_ROUTE.length).replace(/[?#].*$/, ""));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrangled/cli",
3
- "version": "0.3.45",
3
+ "version": "0.3.46",
4
4
  "description": "Command-line access to your marketing data warehouse, for people and coding agents",
5
5
  "type": "module",
6
6
  "bin": {