@wrangled/cli 0.3.32 → 0.3.33

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
@@ -3,6 +3,20 @@ import { brand, envPrefix } from "./brand.js";
3
3
  import { BACK_TAG_SKILL_NAME, GRAMMAR_SKILL_NAME, LEARN_NAMES_SKILL_NAME, LESSONS_SKILL_NAME, MAP_OFFERS_SKILL_NAME, PLAYBOOK_CHOICES, STORY_SKILL_NAME } from "./playbooks.js";
4
4
  import { PULL_STATE_FILE } from "./pull.js";
5
5
  const cli = brand.slug;
6
+ /** The funnel test the guide shows; the app's parser must accept it (src/lib/cli-consistency.test.ts). */
7
+ export const funnelTestExample = {
8
+ hypothesis: "A video at the top of the sign-up page will get more people to sign up, because buyers on calls say they did not know what the webinar was about.",
9
+ funnel_key: "webinar",
10
+ step_key: "signup",
11
+ variable: "top of the page: letter to video",
12
+ control: { label: "letter at the top", url: "https://example.com/signup" },
13
+ variants: [{ label: "video at the top", url: "https://example.com/signup-video" }],
14
+ metric: "step_conversion",
15
+ expected_change: "sign-ups per visitor from 22% to 26%",
16
+ min_people_per_arm: 400,
17
+ split_method: "adzara_ads",
18
+ evidence: [{ kind: "customer_words", ref: "sales call 2026-09-30", note: "I didn't know what the webinar was about." }],
19
+ };
6
20
  export const guide = `# ${brand.name} CLI
7
21
 
8
22
  \`${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.
@@ -38,6 +52,12 @@ Write one as JSON, \`{"text": "...", "dimension": "hook", "evidence": [{"ad": "<
38
52
 
39
53
  \`${cli} winners send-to-exodus\` sends the week's winners to Exodus so it makes new ads from them: one package per Facebook page, each winner with its reason, its numbers, and a note of the lessons it proves, persona lessons and the week's freshest customer words. Winners whose page, words or format are unknown are left out and listed with why. \`--week <YYYY-MM-DD>\` picks the week (default: the latest complete week). \`--dry-run\` asks Exodus what it would create or update and writes nothing. Sending the same week again is safe: an unchanged package is not sent twice, and Exodus updates each ad in place. A real send needs the workspace's Exodus brand and a Scrape Creators key on the Exodus account.
40
54
 
55
+ ## Trying one change: funnel tests
56
+
57
+ A funnel test writes down one change to a page or funnel step, tried against what runs now: the idea, the funnel and step (\`funnel_key\` and \`step_key\` from the funnels map), the one thing that changes, the control and 1 to 3 changed versions, the number that judges it (\`metric\`: step_conversion, sales_per_visitor, revenue_per_visitor or cost_per_sale) with the change expected and the people each side needs, how traffic is split (\`split_method\`: adzara_ads, before_after, or other with a \`split_note\`), and the evidence it should work (each \`{kind, ref, note}\`, kind one of leak, lesson, customer_words, page, ad or other). A test changes one thing: a \`variable\` naming two is refused, and so is a test with no metric.
58
+
59
+ Write it as JSON, such as \`${JSON.stringify(funnelTestExample)}\`, and run \`${cli} tests draft --file test.json\`. It gets Adzara-style labels from the change and this month (\`hyp-<slug>\` and \`test-<yyyy-mm>-<slug>\`); give \`hypothesis_id\` and \`test_id\` to reuse Adzara's own, so the results of the ads Adzara publishes for the test (\`ad_test_results_daily\`, \`ad_hypothesis_results_daily\`) join on the same label. Approving a test happens outside ${brand.name} for now. \`${cli} tests start <test_id>\` marks a draft as running from today, and changes nothing live: the page still goes live through its own approval, and the ads through Adzara. \`${cli} tests abandon <test_id> --note "<why>"\` drops a draft or running test. \`${cli} tests [--status <status>] [--funnel <funnel_key>] [--limit <n>]\` lists them, newest first (50 unless \`--limit\`, at most 200), and \`${cli} tests show <test_id>\` shows one.
60
+
41
61
  ## Content: search before SQL
42
62
 
43
63
  To find content, such as "the 10 emails with the best click rate", "our 5 best videos by return on ad spend" or "every landing page that mentions the guarantee", use \`${cli} content search\` before writing SQL on the \`content_\` tables.
@@ -325,6 +345,7 @@ ${cli} funnels draft --file <map.json> [--note <why>] [--evidence '<json>' | @fi
325
345
  ${cli} facts [--kind <kind>] [--subject <type>[:<ref>]] [--status <status>] [--json] | facts show <id> settled facts about the data, to treat as true
326
346
  ${cli} facts propose --file <fact.json> suggest a fact for a person to confirm
327
347
  ${cli} lessons [--status active|retired] | lessons add --file <lesson.json> | lessons retire <id> --note "<why>" lessons from winners and flops, writing one, retiring one
348
+ ${cli} tests [--status <status>] [--funnel <key>] [--limit <n>] | tests show <id> | tests draft --file <test.json> | tests start <id> | tests abandon <id> [--note "<why>"] funnel tests, one change each
328
349
  ${cli} ads tags [--ad <id>] [--status <status>] [--limit <n>] | ads tag --file <tags.json> the marketing tags of ads not made here, and suggesting them
329
350
  ${cli} ads autotag [--limit <n>] [--ad <id>] have the server propose tags for the biggest spenders not yet tagged
330
351
  ${cli} content tags [<item-id>] [--kind email|text|page] | content to-check the marketing tags of emails, texts and landing pages, and those waiting for a person
package/dist/main.js CHANGED
@@ -1431,6 +1431,70 @@ const commands = {
1431
1431
  : `Lesson ${lesson.id} already counted this week (${weeks}); its proof was updated.`);
1432
1432
  },
1433
1433
  },
1434
+ tests: {
1435
+ usage: "tests [list] [--status <status>] [--funnel <funnel_key>] [--limit <n>] the page and funnel tests written down, newest first (50 unless --limit, at most 200)",
1436
+ async run(args, options) {
1437
+ if (!(args.length === 0 || (args.length === 1 && args[0] === "list")))
1438
+ throw new CliError("Usage: tests [list] [--status <status>] [--funnel <funnel_key>] [--limit <n>]", exitCodes.usage);
1439
+ const query = new URLSearchParams();
1440
+ if (options.status)
1441
+ query.set("status", options.status);
1442
+ if (options.funnel)
1443
+ query.set("funnel", options.funnel);
1444
+ if (options.limit)
1445
+ query.set("limit", options.limit);
1446
+ const { tests } = await api(await resolveConnection(options.apiUrl), `${TESTS_PATH}${query.size ? `?${query}` : ""}`);
1447
+ if (options.format === "json")
1448
+ return printJson({ tests });
1449
+ if (tests.length === 0)
1450
+ return console.log("No funnel tests.");
1451
+ console.log(renderTable(TEST_COLUMNS, tests.map(testRow)));
1452
+ },
1453
+ },
1454
+ "tests show": {
1455
+ usage: "tests show <id|test_id> one funnel test in full",
1456
+ async run(args, options) {
1457
+ if (args.length !== 1)
1458
+ throw new CliError("Usage: tests show <id|test_id>", exitCodes.usage);
1459
+ const { test } = await api(await resolveConnection(options.apiUrl), testPath(args[0]));
1460
+ if (options.format === "json")
1461
+ return printJson({ test });
1462
+ printTest(test);
1463
+ },
1464
+ },
1465
+ "tests draft": {
1466
+ usage: "tests draft --file <test.json> | < test.json write down a funnel test: one change against a control, a metric and the evidence",
1467
+ async run(args, options) {
1468
+ if (args.length > 0)
1469
+ throw new CliError("Usage: tests draft --file <test.json> | < test.json", exitCodes.usage);
1470
+ const { test } = await api(await resolveConnection(options.apiUrl), TESTS_PATH, await readToolInput(options));
1471
+ if (options.format === "json")
1472
+ return printJson({ test });
1473
+ console.log(`Drafted test ${test.test_id} (${test.id}).`);
1474
+ },
1475
+ },
1476
+ "tests start": {
1477
+ usage: "tests start <id|test_id> mark a draft test as running from today; nothing goes live",
1478
+ async run(args, options) {
1479
+ if (args.length !== 1)
1480
+ throw new CliError("Usage: tests start <id|test_id>", exitCodes.usage);
1481
+ const { test } = await api(await resolveConnection(options.apiUrl), `${testPath(args[0])}/start`, {});
1482
+ if (options.format === "json")
1483
+ return printJson({ test });
1484
+ console.log(`Test ${test.test_id} is running from ${test.started_on}.`);
1485
+ },
1486
+ },
1487
+ "tests abandon": {
1488
+ usage: 'tests abandon <id|test_id> [--note "<why>"] drop a test that has not ended',
1489
+ async run(args, options) {
1490
+ if (args.length !== 1)
1491
+ throw new CliError('Usage: tests abandon <id|test_id> [--note "<why>"]', exitCodes.usage);
1492
+ const { test } = await api(await resolveConnection(options.apiUrl), `${testPath(args[0])}/abandon`, options.note ? { note: options.note } : {});
1493
+ if (options.format === "json")
1494
+ return printJson({ test });
1495
+ console.log(`Abandoned test ${test.test_id}.`);
1496
+ },
1497
+ },
1434
1498
  "lessons retire": {
1435
1499
  usage: 'lessons retire <id> --note "<why it is no longer true>" retire a lesson the numbers no longer back; it is kept with why',
1436
1500
  async run(args, options) {
@@ -2065,6 +2129,32 @@ const GRAMMAR_PATH = "/api/v1/grammar";
2065
2129
  const VOICE_LISTS_PATH = "/api/v1/voice-lists";
2066
2130
  const VOICE_SUGGESTIONS_PATH = "/api/v1/voice-lists/suggestions";
2067
2131
  const LESSONS_PATH = "/api/v1/lessons";
2132
+ const TESTS_PATH = "/api/v1/funnel-tests";
2133
+ const TEST_COLUMNS = ["test_id", "status", "funnel", "step", "change", "metric"];
2134
+ const testPath = (ref) => `${TESTS_PATH}/${encodeURIComponent(ref)}`;
2135
+ function testRow(t) {
2136
+ return { test_id: t.test_id, status: t.status, funnel: t.funnel_key, step: t.step_key, change: excerpt(t.variable, 50), metric: t.metric };
2137
+ }
2138
+ function printTest(t) {
2139
+ const arm = (a) => [a.label, a.page && `page ${a.page}`, a.url, a.item_id && `item ${a.item_id}`, a.asset_version && `version ${a.asset_version}`].filter(Boolean).join(" · ");
2140
+ const lines = [
2141
+ `${t.test_id} (${t.status}) idea ${t.hypothesis_id} id ${t.id}`,
2142
+ `Idea: ${t.hypothesis}`,
2143
+ `Where: ${t.funnel_key} / ${t.step_key}`,
2144
+ `The one change: ${t.variable}`,
2145
+ `Stays the same: ${arm(t.control)}`,
2146
+ ...t.variants.map((v, i) => `${t.variants.length === 1 ? "Changed" : `Changed ${i + 1}`}: ${arm(v)}`),
2147
+ `Judged by: ${t.metric}, ${t.expected_change}, ${t.min_people_per_arm} people each side`,
2148
+ `Split: ${t.split_method}${t.split_note ? `, ${t.split_note}` : ""}`,
2149
+ "Evidence:",
2150
+ ...t.evidence.map((e) => ` ${e.kind}: ${e.ref}${e.note ? ` (${e.note})` : ""}`),
2151
+ ];
2152
+ if (t.started_on)
2153
+ lines.push(`Started: ${t.started_on}${t.ended_on ? `, ended ${t.ended_on}` : ""}`);
2154
+ if (t.decision_note)
2155
+ lines.push(`Note: ${t.decision_note}`);
2156
+ console.log(lines.join("\n"));
2157
+ }
2068
2158
  const LESSON_COLUMNS = ["id", "seen", "last week", "dimension", "lesson"];
2069
2159
  function lessonRow(l) {
2070
2160
  const row = { id: l.id, seen: l.timesSeen, "last week": l.lastSeenWeek, dimension: l.dimension ?? "", lesson: excerpt(l.text, 100) };
@@ -2223,7 +2313,8 @@ Options:
2223
2313
  --object-type <type> what that platform's sync calls it (campaign_message or flow_message on klaviyo); needed where
2224
2314
  the sync matches several kinds, so its next read joins the item
2225
2315
  --notes <text> what changed in the version skill publish adds
2226
- --note <why> why: what content create and edit changed, or why lessons retire says a lesson is no longer true
2316
+ --note <why> why: what content create and edit changed, why lessons retire says a lesson is no longer true,
2317
+ or why tests abandon drops one
2227
2318
  --version <n> the map version funnels show prints (default: the confirmed one)
2228
2319
  --based-on <n> the map version funnels draft was written from; refused when someone saved since
2229
2320
  (leave it out only for the first map, when there is neither a map nor a draft)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrangled/cli",
3
- "version": "0.3.32",
3
+ "version": "0.3.33",
4
4
  "description": "Command-line access to your marketing data warehouse, for people and coding agents",
5
5
  "type": "module",
6
6
  "bin": {