@wrangled/cli 0.3.46 → 0.3.47

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
@@ -142,6 +142,10 @@ A webhook address lets a tool such as a form or webinar platform push events int
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
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 }\`.
144
144
 
145
+ ### The client's own provider keys
146
+
147
+ A run bills the shared account unless the client has its own key for that provider. A key whose "Can manage settings" switch an admin turned on (Keys page) saves one: \`${cli} secrets set kie < key.txt\`, or \`--file <path>\`. Never type the key on the command line; it stays in the shell history, so the command refuses it. A key with several fields (genesis, vercel) is a JSON object, such as \`{"token": "...", "teamId": "..."}\`. \`${cli} secrets list\` shows which key each provider's runs use (the client's own, the shared one, or none), its field names, when and by whom it was saved, and when a run last read it, never the key itself. \`${cli} secrets remove kie\` puts runs back on the shared key. Meta's key is its source's, changed by connecting the Meta Ads source. A hand's run key can never save or remove keys.
148
+
145
149
  ## Landing pages
146
150
 
147
151
  The warehouse's content library keeps each landing page's words in order, read about once a day: one \`landing_page\` item per page in \`content_items\`, its versions in \`content_versions\` (\`fields->'blocks'\` holds the headings, paragraphs, lists, buttons, image descriptions and form field labels), and the list of pages with when each was last read in \`content_pages\`. Pages come from Meta ad links and GA4 landing pages. Add any other page with \`${cli} content add-page <url>\`: it is read at once, and the output shows its key, whether it could be read (\`ok\`, \`not_found\`, \`blocked\` by robots.txt, \`failed\`, \`not_html\`), and its version. A link that redirects, such as a link shortener, is recorded under the page it leads to.
@@ -325,6 +329,9 @@ ${cli} import remove --table <name> --yes drop the table import_<name> and its
325
329
  ${cli} tools list list generation and research tools
326
330
  ${cli} tools run <tool> --input '<json>' [--idempotency-key <key>] [--wait <seconds>]
327
331
  ${cli} tools get <run-id> show a tool run, waiting for it to finish (--wait)
332
+ ${cli} secrets list which provider keys the client has and which key runs use; never the keys themselves
333
+ ${cli} secrets set <provider> [--file <path>] save the client's own provider key from stdin or a file (keys with "Can manage settings")
334
+ ${cli} secrets remove <provider> remove it, so runs use the shared key again
328
335
  ${cli} creatives list list creatives (--status, --kind, --limit)
329
336
  ${cli} creatives show <sku> show one creative with its spec and lineage
330
337
  ${cli} creatives download <sku | media-id | asset-path | asset-link> [--out <path>] [--open] save a creative's image or video
package/dist/main.js CHANGED
@@ -7,6 +7,7 @@ import { parseArgs } from "node:util";
7
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
+ import { secretBody, secretProvider, SECRETS_SET_USAGE } from "./secrets.js";
10
11
  import { guide, skill } from "./guide.js";
11
12
  import { PULL_STATE_FILE, pullContent } from "./pull.js";
12
13
  import { AGENTS_MD_MARKER, metaAdsAgentsMd, PLAYBOOK_CHOICES, playbooks } from "./playbooks.js";
@@ -367,6 +368,57 @@ const commands = {
367
368
  console.log(renderTable(["key", "label", "value"], settings.fields.map((f) => ({ key: f.key, label: f.label, value: settings.values[f.key] ?? "" }))));
368
369
  },
369
370
  },
371
+ "secrets list": {
372
+ usage: "secrets list (which provider keys the client has; never the keys themselves)",
373
+ async run(args, options) {
374
+ if (args.length > 0)
375
+ throw new CliError("Usage: secrets list", exitCodes.usage);
376
+ const { secrets } = await api(await resolveConnection(options), SECRETS_PATH);
377
+ if (options.format === "json")
378
+ return printJson({ secrets });
379
+ const rows = secrets.map((s) => ({
380
+ provider: s.provider,
381
+ "key in use": secretSourceLabel(s),
382
+ fields: s.fields.join(", "),
383
+ saved: s.updatedAt ?? "",
384
+ "saved by": s.updatedBy ?? "",
385
+ "last used": s.lastUsedAt ?? (s.source === "workspace" && s.changedWith === "secrets" ? "never" : ""),
386
+ }));
387
+ console.log(renderTable(["provider", "key in use", "fields", "saved", "saved by", "last used"], rows));
388
+ },
389
+ },
390
+ "secrets set": {
391
+ usage: SECRETS_SET_USAGE,
392
+ async run(args, options) {
393
+ const provider = secretProvider(args);
394
+ const text = options.file
395
+ ? await readFile(await inRunFolder(options.file), "utf8")
396
+ : process.stdin.isTTY
397
+ ? await promptHidden(`Paste the ${provider} key: `)
398
+ : await readStdin();
399
+ const connection = await resolveConnection(options);
400
+ const { secret } = await api(connection, secretPath(provider), secretBody(text), "PUT");
401
+ if (options.format === "json")
402
+ return printJson({ secret });
403
+ console.log(`Saved the client's own ${secret.provider} key. Its ${secret.provider} runs now use it instead of the shared one.`);
404
+ },
405
+ },
406
+ "secrets remove": {
407
+ usage: "secrets remove <provider> (runs go back to the shared key)",
408
+ async run(args, options) {
409
+ if (args.length !== 1)
410
+ throw new CliError("Usage: secrets remove <provider>", exitCodes.usage);
411
+ const result = await api(await resolveConnection(options), secretPath(args[0]), undefined, "DELETE");
412
+ if (options.format === "json")
413
+ return printJson(result);
414
+ const { provider } = result.secret;
415
+ if (!result.removed)
416
+ return console.log(`The client had no ${provider} key of its own; nothing changed.`);
417
+ console.log(result.secret.source === "platform"
418
+ ? `Removed the client's own ${provider} key. Its runs now use the shared key.`
419
+ : `Removed the client's own ${provider} key. No shared key is set, so its ${provider} runs fail until a key is saved.`);
420
+ },
421
+ },
370
422
  "intake list": {
371
423
  usage: "intake list",
372
424
  async run(args, options) {
@@ -2595,7 +2647,7 @@ Options:
2595
2647
  --days <n> winners: the last n complete days, ending yesterday, instead of a week (at most 90); or give winners
2596
2648
  --from and --to for any dates
2597
2649
  --body, --html <text | @file> the words, or the full HTML, content create, capture and edit save
2598
- --file <path> an image, video or audio file, up to 50 MB, content create, capture and edit save; repeat for several
2650
+ --file <path> an image, video or audio file, up to 50 MB, content create, capture and edit save; repeat for several; for secrets set, the file holding the key
2599
2651
  --transcript <text | @file> what is said in the first video or audio file content capture saves, so none is paid for
2600
2652
  --language <code> the spoken language of --transcript, such as en
2601
2653
  --source <text> where the content content capture saves came from, in words
@@ -2894,6 +2946,15 @@ async function changeCandidate(change, args, options) {
2894
2946
  throw new CliError(`Usage: worker ${change} <skill> --workspace <slug>`, exitCodes.usage);
2895
2947
  return api(await operatorConnection(options), `/api/operator/workers/${change}`, { workspace: options.workspace, skill: args[0] });
2896
2948
  }
2949
+ const SECRETS_PATH = "/api/v1/secrets";
2950
+ function secretPath(provider) {
2951
+ return `${SECRETS_PATH}/${encodeURIComponent(provider)}`;
2952
+ }
2953
+ function secretSourceLabel(secret) {
2954
+ if (secret.source === "workspace")
2955
+ return secret.changedWith === "meta_source" ? "the Meta source's" : "the client's own";
2956
+ return secret.source === "platform" ? "the shared key" : "none";
2957
+ }
2897
2958
  function findCommand(positionals) {
2898
2959
  const three = positionals.slice(0, 3).join(" ");
2899
2960
  if (Object.hasOwn(commands, three))
@@ -0,0 +1,35 @@
1
+ import { CliError, exitCodes } from "./api.js";
2
+ import { brand } from "./brand.js";
3
+ export const SECRETS_SET_USAGE = "secrets set <provider> [--file <path>] (the key on stdin or in the file; JSON {\"field\": \"...\"} for a key with several fields)";
4
+ /**
5
+ * The provider `secrets set` names. A second argument is refused without being echoed: it is most likely the key,
6
+ * which would then sit in the shell's history.
7
+ */
8
+ export function secretProvider(args) {
9
+ if (args.length > 1) {
10
+ throw new CliError(`Never pass the key as an argument: it stays in your shell history. Pipe it on stdin (\`${brand.slug} secrets set <provider> < key.txt\`) or name a file with --file.`, exitCodes.usage);
11
+ }
12
+ if (args.length === 0)
13
+ throw new CliError(`Usage: ${SECRETS_SET_USAGE}`, exitCodes.usage);
14
+ return args[0];
15
+ }
16
+ /** A JSON object of strings is a multi-field key; anything else is the key itself. Error messages never repeat the text. */
17
+ export function secretBody(text) {
18
+ const trimmed = text.trim();
19
+ if (!trimmed)
20
+ throw new CliError("No key given. Pipe it on stdin or name a file with --file.", exitCodes.usage);
21
+ if (!trimmed.startsWith("{"))
22
+ return { value: trimmed };
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(trimmed);
26
+ }
27
+ catch {
28
+ throw new CliError('The key starts with "{" but is not valid JSON. A key with several fields is a JSON object, such as {"token": "...", "teamId": "..."}.', exitCodes.usage);
29
+ }
30
+ const fields = parsed;
31
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || Object.values(fields).some((v) => typeof v !== "string")) {
32
+ throw new CliError('A key with several fields is a JSON object of strings, such as {"token": "...", "teamId": "..."}.', exitCodes.usage);
33
+ }
34
+ return { fields: fields };
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrangled/cli",
3
- "version": "0.3.46",
3
+ "version": "0.3.47",
4
4
  "description": "Command-line access to your marketing data warehouse, for people and coding agents",
5
5
  "type": "module",
6
6
  "bin": {