@wrangled/cli 0.3.24 → 0.3.26
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 +5 -0
- package/dist/guide.js +4 -1
- package/dist/main.js +90 -11
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -16,6 +16,11 @@ export class UnreadableCredentialsError extends CliError {
|
|
|
16
16
|
super(`The saved credentials at ${path} are unreadable. Run \`${brand.slug} login\` again.`, exitCodes.auth);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* The grammar fields library entries are tagged with, and the CLI flag for each; the server's list is entryTagFields
|
|
21
|
+
* in src/lib/knowledge/tags.ts. format's flag is --ad-format, because --format picks the output.
|
|
22
|
+
*/
|
|
23
|
+
export const entryTagFlags = { angle: "angle", hook_kind: "hook-kind", format: "ad-format", awareness: "awareness" };
|
|
19
24
|
/** Copies of the server's IMPORT_LIMITS and import name rule; src/lib/cli-consistency.test.ts keeps them in step. */
|
|
20
25
|
export const IMPORT_LIMITS = { bytes: 50 * 1024 * 1024, rows: 1_000_000, columns: 500 };
|
|
21
26
|
export const IMPORT_NAME_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
package/dist/guide.js
CHANGED
|
@@ -44,6 +44,8 @@ For what has worked for others, such as "how do members test new hooks" or "what
|
|
|
44
44
|
- The question is 1 to 4000 characters. \`--limit\` is 1 to 12 (default 5). \`--kind\` (message, transcript, document, note) keeps one kind of source. \`--library <slug>\` keeps one library.
|
|
45
45
|
- \`--library meta-policy\` searches Meta's own advertising standards, copied from Meta's site every Monday. Before you write or judge an ad, check its claims there, such as income, health or before-and-after claims: \`${cli} knowledge search --library meta-policy "income claims"\`. Its evidence is \`meta_published_policy\`, Meta's published rule; Meta's reviewers still decide each ad.
|
|
46
46
|
- \`knowledge_not_synced\` means the library has not been copied into this server yet, so nothing was searched; the weekly sync fills it.
|
|
47
|
+
- Library entries carry the marketing grammar's tags: \`--angle\`, \`--hook-kind\`, \`--ad-format\` and \`--awareness\` (the ad tag fields angle, hook_kind, format and awareness, with the same values). Give any of them to keep to entries tagged that way; the question is then optional, and without one the entries come surest tags first. With a tag filter the answer also lists this workspace's own Meta ads whose live tags hold the same values, with their lifetime spend and results, biggest spend first (at most 20): what the library says next to how it went for this client.
|
|
48
|
+
- Each result shows its entry id and tags. Tag an entry you read with \`${cli} knowledge tag <entry-id> --hook-kind big_win --confidence medium\` (several flags at once is fine), take a wrong tag off with \`--remove\`, or tag a batch with \`${cli} knowledge tag --file tags.json\`, where the file is \`{"tags": [{"entry_id", "field", "value", "confidence"}]}\` (up to 500; \`"remove": true\` in place of confidence takes one off). Values off the grammar's list are refused.
|
|
47
49
|
|
|
48
50
|
## Query workflow: schema first
|
|
49
51
|
|
|
@@ -244,7 +246,8 @@ A scheduled hand's run acts as its skill's agent, the way the Facebook ads agent
|
|
|
244
246
|
${cli} login validate a key (prompted, piped, or ${envPrefix}_API_KEY) and save it
|
|
245
247
|
${cli} logout delete saved credentials
|
|
246
248
|
${cli} whoami show the current workspace and library access
|
|
247
|
-
${cli} knowledge search "<question>" [--limit <n>] [--kind <kind>] short, cited snippets from the
|
|
249
|
+
${cli} knowledge search "<question>" [--limit <n>] [--kind <kind>] [--hook-kind <v> ...] short, cited snippets from the know-how libraries (keys with library access)
|
|
250
|
+
${cli} knowledge tag <entry-id> --hook-kind <v> --confidence <c> tag a library entry with the marketing grammar (or --file tags.json)
|
|
248
251
|
${cli} guide what this workspace's data holds, how fresh it is, and what is missing: read it first
|
|
249
252
|
${cli} brief [--from <date>] [--to <date>] [--channel <channel>] [--campaign <id>] [--top <n>] the offer, voice, best and worst ads, pages, results and audience: start here before writing ads
|
|
250
253
|
${cli} winners [--week <date>] [--top <n>] which Facebook ads won and flopped this week and over 30 days, judged on first-touch sales
|
package/dist/main.js
CHANGED
|
@@ -4,7 +4,7 @@ import { mkdir, readFile, stat } from "node:fs/promises";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
|
-
import { api, ApiError, apiDownload, apiText, apiUpload, CliError, exitCodes, getSigned, IMPORT_LIMITS, IMPORT_NAME_PATTERN, putSigned, readCredentials, removeCredentials, resolveApiUrl, resolveConnection, UnreadableCredentialsError, writeCredentials, streamSigned, } from "./api.js";
|
|
7
|
+
import { api, ApiError, apiDownload, apiText, apiUpload, CliError, exitCodes, getSigned, IMPORT_LIMITS, IMPORT_NAME_PATTERN, putSigned, readCredentials, removeCredentials, resolveApiUrl, resolveConnection, 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
10
|
import { guide, skill } from "./guide.js";
|
|
@@ -57,23 +57,55 @@ const commands = {
|
|
|
57
57
|
},
|
|
58
58
|
},
|
|
59
59
|
"knowledge search": {
|
|
60
|
-
usage: 'knowledge search "<question>" [--limit <n>] [--kind message|transcript|document|note] [--library <slug>]',
|
|
60
|
+
usage: 'knowledge search ["<question>"] [--limit <n>] [--kind message|transcript|document|note] [--library <slug>] [--angle <v>] [--hook-kind <v>] [--ad-format <v>] [--awareness <v>]',
|
|
61
61
|
async run(args, options) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
const filters = Object.entries(options.entryTags);
|
|
63
|
+
if (args.length > 1 || (args.length === 0 && filters.length === 0)) {
|
|
64
|
+
throw new CliError('Usage: knowledge search "<question>" [--limit <n>] [--kind <kind>] [--library <slug>] [--angle <v>] [--hook-kind <v>] [--ad-format <v>] [--awareness <v>]; put the question in quotes, or give a tag filter', exitCodes.usage);
|
|
65
|
+
}
|
|
66
|
+
const query = new URLSearchParams(args[0] === undefined ? {} : { q: args[0] });
|
|
65
67
|
if (options.limit !== undefined)
|
|
66
68
|
query.set("limit", options.limit);
|
|
67
69
|
if (options.kind !== undefined)
|
|
68
70
|
query.set("kind", options.kind);
|
|
69
71
|
if (options.library !== undefined)
|
|
70
72
|
query.set("library", options.library);
|
|
71
|
-
const
|
|
73
|
+
for (const [field, value] of filters)
|
|
74
|
+
query.set(field, value);
|
|
75
|
+
const answer = await api(await resolveConnection(options.apiUrl), `/api/v1/knowledge/search?${query}`);
|
|
72
76
|
if (options.format === "json")
|
|
73
|
-
return printJson(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
return printJson(answer);
|
|
78
|
+
console.log(answer.results.length === 0 ? "Nothing in the library matched." : answer.results.map(knowledgeBlock).join("\n\n"));
|
|
79
|
+
if (answer.yourAds === undefined)
|
|
80
|
+
return;
|
|
81
|
+
const tagged = filters.map(([field, value]) => `${field} ${value}`).join(", ");
|
|
82
|
+
if (answer.yourAds.length === 0)
|
|
83
|
+
return console.log(`\nNone of your ads is tagged ${tagged}.`);
|
|
84
|
+
console.log(`\nYour ads tagged ${tagged}, biggest spend first:`);
|
|
85
|
+
console.log(renderTable(["ad", "name", "tags", "spend", "results"], answer.yourAds.map(yourAdRow)));
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
"knowledge tag": {
|
|
89
|
+
usage: "knowledge tag <entry-id> [--angle <v>] [--hook-kind <v>] [--ad-format <v>] [--awareness <v>] --confidence high|medium|low [--remove] | --file <tags.json> tag library entries with the marketing grammar",
|
|
90
|
+
async run(args, options) {
|
|
91
|
+
const usage = "Usage: knowledge tag <entry-id> [--angle <v>] [--hook-kind <v>] [--ad-format <v>] [--awareness <v>] --confidence high|medium|low [--remove], or knowledge tag --file <tags.json>";
|
|
92
|
+
const filters = Object.entries(options.entryTags);
|
|
93
|
+
if (args.length > 1 || (args.length === 1 && filters.length === 0))
|
|
94
|
+
throw new CliError(usage, exitCodes.usage);
|
|
95
|
+
const body = args.length === 1
|
|
96
|
+
? {
|
|
97
|
+
tags: filters.map(([field, value]) => ({
|
|
98
|
+
entry_id: args[0],
|
|
99
|
+
field,
|
|
100
|
+
value,
|
|
101
|
+
...(options.remove ? { remove: true } : { confidence: options.confidence }),
|
|
102
|
+
})),
|
|
103
|
+
}
|
|
104
|
+
: await readToolInput(options);
|
|
105
|
+
const result = await api(await resolveConnection(options.apiUrl), KNOWLEDGE_TAGS_PATH, body);
|
|
106
|
+
if (options.format === "json")
|
|
107
|
+
return printJson(result);
|
|
108
|
+
console.log(`Tagged ${result.tagged}, took off ${result.removed}.`);
|
|
77
109
|
},
|
|
78
110
|
},
|
|
79
111
|
"warehouse schema": {
|
|
@@ -1294,7 +1326,11 @@ const commands = {
|
|
|
1294
1326
|
const { fact } = await api(await resolveConnection(options.apiUrl), `${FACTS_PATH}/${encodeURIComponent(args[0])}`);
|
|
1295
1327
|
if (options.format === "json")
|
|
1296
1328
|
return printJson({ fact });
|
|
1297
|
-
|
|
1329
|
+
const nameKey = fact.key === NAME_KEY ? nameKeyLines(fact.value) : null;
|
|
1330
|
+
const value = nameKey ? { means: fact.value.means } : { value: fact.value };
|
|
1331
|
+
printFields({ ...factRow(fact), summary: fact.summary, ...value, evidence: fact.evidence, author: fact.author, "confirmed by": fact.confirmedBy, "decided by": fact.decidedBy, replaces: fact.replaces });
|
|
1332
|
+
if (nameKey)
|
|
1333
|
+
console.log(nameKey.join("\n"));
|
|
1298
1334
|
},
|
|
1299
1335
|
},
|
|
1300
1336
|
"facts propose": {
|
|
@@ -1867,6 +1903,7 @@ function printReport(title, report, labels, numbers) {
|
|
|
1867
1903
|
const agentPath = (options) => `/api/v1/agents/${encodeURIComponent(options.agent)}`;
|
|
1868
1904
|
const FACTS_PATH = "/api/v1/facts";
|
|
1869
1905
|
const AD_TAGS_PATH = "/api/v1/ad-tags";
|
|
1906
|
+
const KNOWLEDGE_TAGS_PATH = "/api/v1/knowledge/tags";
|
|
1870
1907
|
const AD_AUTOTAG_PATH = "/api/v1/ad-tags/autotag";
|
|
1871
1908
|
const SKIP_REASONS = { not_marketing: "Not marketing, so only that answer was saved", gate_unclear: "Could not tell whether it is marketing, so only that was saved", no_content: "Nothing to read", not_found: "No such ad" };
|
|
1872
1909
|
const AD_TAG_COLUMNS = ["id", "ad", "status", "confidence", "offer", "persona", "angle", "hook", "format", "author"];
|
|
@@ -1888,8 +1925,16 @@ function knowledgeBlock(r, i) {
|
|
|
1888
1925
|
lines.push(` ${r.snippet}`);
|
|
1889
1926
|
if (r.caveat)
|
|
1890
1927
|
lines.push(` Caveat: ${r.caveat}`);
|
|
1928
|
+
if (r.entryId) {
|
|
1929
|
+
const tags = Object.entries(r.tags ?? {}).flatMap(([field, values]) => values.map((value) => `${field} ${value}`));
|
|
1930
|
+
lines.push(` Entry ${r.entryId}${tags.length > 0 ? `, tagged ${tags.join(", ")}` : ""}`);
|
|
1931
|
+
}
|
|
1891
1932
|
return lines.join("\n");
|
|
1892
1933
|
}
|
|
1934
|
+
function yourAdRow(a) {
|
|
1935
|
+
const results = a.results === null ? null : `${a.results} ${a.resultType ?? ""}`.trim();
|
|
1936
|
+
return { ad: a.adId, name: a.adName, tags: `${a.tagStatus}, ${a.tagConfidence}`, spend: a.spend === null ? null : `${a.spend.toFixed(2)} ${a.currency ?? ""}`.trim(), results };
|
|
1937
|
+
}
|
|
1893
1938
|
function adTagRow(t) {
|
|
1894
1939
|
const { offer_key, persona, angle, hook_kind, hook_spot, format, style } = t.tags;
|
|
1895
1940
|
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 };
|
|
@@ -1907,6 +1952,27 @@ function voiceLineRow(l) {
|
|
|
1907
1952
|
function factRow(f) {
|
|
1908
1953
|
return { id: f.id, kind: f.kind, subject: `${f.subjectType}:${f.subjectRef}`, key: f.key, status: f.status, summary: excerpt(f.summary, 80) };
|
|
1909
1954
|
}
|
|
1955
|
+
/** The definition key that holds a workspace's name key: the server's NameKey, read here without its schema. */
|
|
1956
|
+
const NAME_KEY = "ad_name_key";
|
|
1957
|
+
const NAME_LEVELS = { campaign: "campaign", ad_set: "ad set", ad: "ad" };
|
|
1958
|
+
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1959
|
+
const arrayOf = (v) => (Array.isArray(v) ? v.filter(isObject) : []);
|
|
1960
|
+
/** A name key as lines a person reads: each pattern with an example name and what its segments mean, then the questions left. Null when the value holds none. */
|
|
1961
|
+
function nameKeyLines(value) {
|
|
1962
|
+
const nameKey = isObject(value) && isObject(value.name_key) ? value.name_key : null;
|
|
1963
|
+
if (!nameKey || !isObject(nameKey.levels))
|
|
1964
|
+
return null;
|
|
1965
|
+
const levels = nameKey.levels;
|
|
1966
|
+
const patterns = Object.entries(NAME_LEVELS).flatMap(([level, label]) => arrayOf(levels[level]).map((pattern) => {
|
|
1967
|
+
const segments = arrayOf(pattern.segments).map((s) => {
|
|
1968
|
+
const meaning = typeof s.label === "string" ? s.label : String(s.meaning ?? "unknown").replaceAll("_", " ");
|
|
1969
|
+
return `${String(s.name)} = ${meaning}${s.certainty === "guess" ? " (a guess)" : ""}`;
|
|
1970
|
+
});
|
|
1971
|
+
return `- ${label} names like ${String(pattern.example)}: ${segments.join("; ")}`;
|
|
1972
|
+
}));
|
|
1973
|
+
const questions = arrayOf(nameKey.open_questions).map((q) => `- ${String(q.text)}`);
|
|
1974
|
+
return ["", "Name patterns:", ...(patterns.length > 0 ? patterns : ["- none yet"]), "", "Open questions:", ...(questions.length > 0 ? questions : ["- none"])];
|
|
1975
|
+
}
|
|
1910
1976
|
const STORIES_PATH = "/api/v1/stories";
|
|
1911
1977
|
function printStory(story) {
|
|
1912
1978
|
const corrects = story.previousId ? `, correcting ${story.previousId}` : "";
|
|
@@ -2022,6 +2088,10 @@ Options:
|
|
|
2022
2088
|
--limit <n> how many results content search, creatives list, campaigns, voice, ads tags, content tags and content to-check
|
|
2023
2089
|
return, or how many ads ads autotag tags and how many items content autotag tags
|
|
2024
2090
|
--library <slug> with knowledge search, only this library: creative-cartel, exodus or meta-policy
|
|
2091
|
+
--angle, --hook-kind, --ad-format, --awareness <value> with knowledge search, only library entries tagged that way, and
|
|
2092
|
+
your own ads tagged the same way with their results; with knowledge tag, the tags to put on an entry
|
|
2093
|
+
--confidence high|medium|low with knowledge tag, how sure you are of the tags
|
|
2094
|
+
--remove with knowledge tag, take the tags off instead
|
|
2025
2095
|
--ad <id> with ads tags, only this Meta ad's tags; with ads autotag, only this ad
|
|
2026
2096
|
--kind email|text|page with content tags and content autotag, only that kind (content autotag takes a comma list)
|
|
2027
2097
|
--max-seconds <n> stop a runner's run after this many seconds, if that comes before its deadline
|
|
@@ -2122,6 +2192,12 @@ function parseOptions(argv) {
|
|
|
2122
2192
|
step: { type: "string" },
|
|
2123
2193
|
workspace: { type: "string" },
|
|
2124
2194
|
"max-seconds": { type: "string" },
|
|
2195
|
+
angle: { type: "string" },
|
|
2196
|
+
"hook-kind": { type: "string" },
|
|
2197
|
+
"ad-format": { type: "string" },
|
|
2198
|
+
awareness: { type: "string" },
|
|
2199
|
+
confidence: { type: "string" },
|
|
2200
|
+
remove: { type: "boolean", default: false },
|
|
2125
2201
|
help: { type: "boolean", short: "h", default: false },
|
|
2126
2202
|
},
|
|
2127
2203
|
});
|
|
@@ -2219,6 +2295,9 @@ function parseOptions(argv) {
|
|
|
2219
2295
|
step: values.step,
|
|
2220
2296
|
workspace: values.workspace,
|
|
2221
2297
|
maxSeconds,
|
|
2298
|
+
entryTags: Object.fromEntries(Object.entries(entryTagFlags).flatMap(([field, flag]) => (values[flag] === undefined ? [] : [[field, values[flag]]]))),
|
|
2299
|
+
confidence: values.confidence,
|
|
2300
|
+
remove: values.remove,
|
|
2222
2301
|
},
|
|
2223
2302
|
};
|
|
2224
2303
|
}
|