@wrangled/cli 0.3.11 → 0.3.13
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 +9 -0
- package/dist/main.js +49 -3
- package/package.json +1 -1
package/dist/guide.js
CHANGED
|
@@ -166,6 +166,14 @@ Kinds are \`exclusion\`, \`gap\`, \`baseline\`, \`change\`, \`definition\`, \`ca
|
|
|
166
166
|
|
|
167
167
|
To suggest a fact, write it as JSON and run \`${cli} facts propose --file fact.json\` (or pipe it on stdin): \`{"kind": "gap", "subject_type": "table", "subject_ref": "meta_insights_daily", "key": "history_start", "value": {"table": "meta_insights_daily", "from": "2026-04-07"}, "summary": "Facebook ad numbers only go back to April 7, 2026.", "evidence": {"query": "SELECT min(date) FROM meta_insights_daily"}}\`. You only suggest: a person confirms, fixes or rejects it in the app. A fact with the same kind, subject and key already open is refused as \`fact_exists\`. Evidence is counts, queries and ids, never names, emails or phones.
|
|
168
168
|
|
|
169
|
+
## Ad tags
|
|
170
|
+
|
|
171
|
+
Ads not made here (built by hand or by an agency, names in no pattern) get the marketing grammar's tags (docs/marketing-grammar.md) by Meta ad id. \`${cli} ads tags\` lists them (\`--ad <id>\`, \`--status proposed|confirmed|rejected|replaced\`, \`--limit <n>\`, \`--json\`); by default the live ones, proposed and confirmed. A proposed tag is usable at once, but not yet confirmed; only an Owner or Admin confirms, rejects or changes tags, in the app.
|
|
172
|
+
|
|
173
|
+
To suggest tags, write them as JSON and run \`${cli} ads tag --file tags.json\` (or pipe it on stdin), up to 500 ads at a time: \`{"ads": [{"ad_id": "120214567890123", "tags": {"angle": "discovery", "desire": "money", "hook_spot": "spoken", "hook_text": "$29,000 is the smallest lead", "hook_kind": "big_win", "format": "video", "style": "presenter", "tag_reasons": {"worry": "none"}}, "confidence": "medium", "evidence": {"ad_name": "County lead v2", "transcript_start": "$29,000 is the smallest lead"}}]}\`. The tag fields are \`marketing\`, \`offer_key\`, \`landing_funnel_key\`, \`landing_step_key\`, \`persona\`, \`awareness\`, \`concept\`, \`angle\`, \`desire\`, \`hook_spot\`, \`hook_text\`, \`hook_kind\`, \`hook2_spot\`, \`hook2_kind\`, \`format\`, \`style\`, \`speaker\`, \`cta\`, \`proof\` and \`worry\`; leave out what you did not judge. A part with no value goes in \`tag_reasons\` as \`none\` (nothing on its list applies) or \`unclear\` (too little to judge, never \`high\` confidence). Confidence is \`high\`, \`medium\` or \`low\`. A value off its list is refused with the allowed values. New tags replace an ad's proposed ones, which stay as \`replaced\`; an ad whose tags a person confirmed is kept as it is.
|
|
174
|
+
|
|
175
|
+
For what worked, query the \`ad_performance_tagged\` view: every Meta ad's daily results with its live tags next to them, \`tags_source\` (\`ad_tags\`, or null when untagged) and \`tags_status\`.
|
|
176
|
+
|
|
169
177
|
## What customers keep saying
|
|
170
178
|
|
|
171
179
|
Every hour an AI reads the workspace's new Facebook and Instagram comments, Google reviews and Reddit posts and comments, pulls out openers (lines that would open an ad), objections (why people hesitate) and phrases (customers' own words for their problem or result), and groups the ones that say the same thing. Names, emails, phone numbers and handles are taken out first. \`${cli} voice\` lists the groups, most common first (\`--kind opener|objection|phrase\`, \`--limit <n>\` up to 200, \`--json\`), each with how many rows said it, their likes, and up to 3 real quotes. In \`--json\`, each quote's \`{kind, ref, quote}\` goes straight into a creative's \`researchSources\`, and a group itself can be cited as its \`ref\`, \`voice_clusters:<id>\`. The same rows are in the \`voice_clusters\` and \`voice_insights\` tables.
|
|
@@ -272,6 +280,7 @@ ${cli} funnels evidence prices paid, buyer paths, GoHighLevel funnels, calenda
|
|
|
272
280
|
${cli} funnels draft --file <map.json> [--note <why>] [--evidence '<json>' | @file.json] [--based-on <n>] [--counts-from fix|<date>] [--questions @file.json] save the whole map; it goes live when every step can be counted
|
|
273
281
|
${cli} facts [--kind <kind>] [--subject <type>[:<ref>]] [--status <status>] [--json] | facts show <id> settled facts about the data, to treat as true
|
|
274
282
|
${cli} facts propose --file <fact.json> suggest a fact for a person to confirm
|
|
283
|
+
${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
|
|
275
284
|
${cli} voice [--kind opener|objection|phrase] [--limit <n>] [--json] what customers keep saying, grouped and counted, with real quotes
|
|
276
285
|
${cli} voice lists where that voice comes from: the competitors whose ads, and the subreddits and Reddit search phrases whose posts, are saved, and suggestions waiting
|
|
277
286
|
${cli} voice suggest competitor|subreddit|search_phrase "<value>" [--note <why>] suggest a competitor, subreddit or Reddit search phrase for a person to approve
|
package/dist/main.js
CHANGED
|
@@ -1027,12 +1027,16 @@ const commands = {
|
|
|
1027
1027
|
const counts = options.countsFrom === undefined || options.countsFrom === "fix" ? "it counts for past dates too, as a fix" : `it counts from ${options.countsFrom}, as a change`;
|
|
1028
1028
|
console.log(saved.applied
|
|
1029
1029
|
? `Map version ${saved.version.version} is live${based}: ${counts}. A person can undo it in the app.`
|
|
1030
|
-
:
|
|
1030
|
+
: saved.held === "undone_by_person"
|
|
1031
|
+
? `Saved version ${saved.version.version}${based}. It is on hold: it counts nothing until a person puts it live.`
|
|
1032
|
+
: `Saved version ${saved.version.version}${based}. It is on hold: it counts nothing until every step can be counted.`);
|
|
1031
1033
|
if (saved.reapplied.length > 0)
|
|
1032
1034
|
console.log(`\nAnswers put back into the map: ${saved.reapplied.join(", ")}`);
|
|
1033
1035
|
if (saved.asked.length > 0)
|
|
1034
1036
|
console.log(`\nQuestions open for a person, biggest money at stake first: ${saved.asked.join(", ")}`);
|
|
1035
|
-
const [left, warnings] = [saved.warnings.filter((w) => w.message.startsWith("question ")), saved.warnings.filter((w) => !w.message.startsWith("question "))];
|
|
1037
|
+
const [left, warnings] = [saved.warnings.filter((w) => w.message.startsWith("question ")), saved.warnings.filter((w) => !w.message.startsWith("question ") && w.path !== "")];
|
|
1038
|
+
for (const w of saved.warnings.filter((w) => w.path === ""))
|
|
1039
|
+
console.log(`\n${w.message}`);
|
|
1036
1040
|
if (warnings.length > 0)
|
|
1037
1041
|
console.log(`\n${saved.applied ? "Not in synced data yet" : "To fill in"}:\n${warnings.map((w) => `- ${w.path}: ${w.message}`).join("\n")}`);
|
|
1038
1042
|
if (left.length > 0)
|
|
@@ -1117,6 +1121,39 @@ const commands = {
|
|
|
1117
1121
|
console.log(renderTable(VOICE_COLUMNS, clusters.map(voiceRow)));
|
|
1118
1122
|
},
|
|
1119
1123
|
},
|
|
1124
|
+
"ads tags": {
|
|
1125
|
+
usage: "ads tags [--ad <id>] [--status <status>] [--limit <n>] [--json] the marketing tags of ads not made here, by Meta ad id",
|
|
1126
|
+
async run(args, options) {
|
|
1127
|
+
if (args.length > 0)
|
|
1128
|
+
throw new CliError("Usage: ads tags [--ad <id>] [--status <status>] [--limit <n>] [--json]", exitCodes.usage);
|
|
1129
|
+
const query = new URLSearchParams();
|
|
1130
|
+
if (options.ad)
|
|
1131
|
+
query.set("ad", options.ad);
|
|
1132
|
+
if (options.status)
|
|
1133
|
+
query.set("status", options.status);
|
|
1134
|
+
if (options.limit)
|
|
1135
|
+
query.set("limit", options.limit);
|
|
1136
|
+
const { tags } = await api(await resolveConnection(options.apiUrl), `${AD_TAGS_PATH}${query.size ? `?${query}` : ""}`);
|
|
1137
|
+
if (options.format === "json")
|
|
1138
|
+
return printJson({ tags });
|
|
1139
|
+
if (tags.length === 0)
|
|
1140
|
+
return console.log("No ad tags.");
|
|
1141
|
+
console.log(renderTable(AD_TAG_COLUMNS, tags.map(adTagRow)));
|
|
1142
|
+
},
|
|
1143
|
+
},
|
|
1144
|
+
"ads tag": {
|
|
1145
|
+
usage: "ads tag --file <tags.json> | < tags.json suggest tags for up to 500 ads; a person confirms them in the app",
|
|
1146
|
+
async run(args, options) {
|
|
1147
|
+
if (args.length > 0)
|
|
1148
|
+
throw new CliError("Usage: ads tag --file <tags.json> | < tags.json", exitCodes.usage);
|
|
1149
|
+
const result = await api(await resolveConnection(options.apiUrl), AD_TAGS_PATH, await readToolInput(options));
|
|
1150
|
+
if (options.format === "json")
|
|
1151
|
+
return printJson(result);
|
|
1152
|
+
console.log(`Proposed tags for ${result.proposed.length} ${result.proposed.length === 1 ? "ad" : "ads"}. A person confirms or rejects them in the app.`);
|
|
1153
|
+
if (result.kept.length > 0)
|
|
1154
|
+
console.log(`Kept as they are, since a person confirmed their tags: ${result.kept.map((k) => k.adId).join(", ")}.`);
|
|
1155
|
+
},
|
|
1156
|
+
},
|
|
1120
1157
|
"facts show": {
|
|
1121
1158
|
usage: "facts show <id> one fact with its value and evidence",
|
|
1122
1159
|
async run(args, options) {
|
|
@@ -1618,6 +1655,8 @@ function printReport(title, report, labels, numbers) {
|
|
|
1618
1655
|
}
|
|
1619
1656
|
const agentPath = (options) => `/api/v1/agents/${encodeURIComponent(options.agent)}`;
|
|
1620
1657
|
const FACTS_PATH = "/api/v1/facts";
|
|
1658
|
+
const AD_TAGS_PATH = "/api/v1/ad-tags";
|
|
1659
|
+
const AD_TAG_COLUMNS = ["id", "ad", "status", "confidence", "offer", "persona", "angle", "hook", "format", "author"];
|
|
1621
1660
|
const VOICE_LISTS_PATH = "/api/v1/voice-lists";
|
|
1622
1661
|
const VOICE_SUGGESTIONS_PATH = "/api/v1/voice-lists/suggestions";
|
|
1623
1662
|
const FACT_COLUMNS = ["id", "kind", "subject", "key", "status", "summary"];
|
|
@@ -1632,6 +1671,10 @@ function knowledgeBlock(r, i) {
|
|
|
1632
1671
|
lines.push(` Caveat: ${r.caveat}`);
|
|
1633
1672
|
return lines.join("\n");
|
|
1634
1673
|
}
|
|
1674
|
+
function adTagRow(t) {
|
|
1675
|
+
const { offer_key, persona, angle, hook_kind, hook_spot, format, style } = t.tags;
|
|
1676
|
+
return { id: t.id, ad: t.adId, status: t.status, confidence: t.confidence, offer: offer_key, persona, angle, hook: [hook_kind, hook_spot].filter(Boolean).join(" in ") || null, format: [format, style].filter(Boolean).join(", ") || null, author: t.author };
|
|
1677
|
+
}
|
|
1635
1678
|
function voiceRow(c) {
|
|
1636
1679
|
return { ref: c.ref, kind: c.kind, "said by": c.supportCount, likes: c.likeSum, label: excerpt(c.label, 60), "top quote": c.evidence[0] ? excerpt(c.evidence[0].quote, 60) : null };
|
|
1637
1680
|
}
|
|
@@ -1747,7 +1790,8 @@ Options:
|
|
|
1747
1790
|
--from, --to <n> with content diff, the two versions to compare (default: the one before the current one, and the current one)
|
|
1748
1791
|
--sort <column> how content search sorts: recent (default) or a content_scorecard number, such as click_rate or roas
|
|
1749
1792
|
--order desc|asc content search's sort direction (default: desc, best or newest first)
|
|
1750
|
-
--limit <n> how many results content search, creatives list, campaigns and
|
|
1793
|
+
--limit <n> how many results content search, creatives list, campaigns, voice and ads tags return
|
|
1794
|
+
--ad <id> with ads tags, only this Meta ad's tags
|
|
1751
1795
|
--max-seconds <n> stop a runner's run after this many seconds, if that comes before its deadline
|
|
1752
1796
|
--workspace <slug> for worker promote and worker drop: the workspace whose hand they change
|
|
1753
1797
|
-h, --help show this help
|
|
@@ -1777,6 +1821,7 @@ function parseOptions(argv) {
|
|
|
1777
1821
|
wait: { type: "string", default: String(DEFAULT_WAIT_SECONDS) },
|
|
1778
1822
|
status: { type: "string" },
|
|
1779
1823
|
kind: { type: "string" },
|
|
1824
|
+
ad: { type: "string" },
|
|
1780
1825
|
limit: { type: "string" },
|
|
1781
1826
|
run: { type: "string" },
|
|
1782
1827
|
lineage: { type: "string" },
|
|
@@ -1867,6 +1912,7 @@ function parseOptions(argv) {
|
|
|
1867
1912
|
wait,
|
|
1868
1913
|
status: values.status,
|
|
1869
1914
|
kind: values.kind,
|
|
1915
|
+
ad: values.ad,
|
|
1870
1916
|
limit: values.limit,
|
|
1871
1917
|
run: values.run,
|
|
1872
1918
|
lineage: values.lineage,
|