@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 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 Creative Cartel know-how library (keys with library access)
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
- if (args.length !== 1)
63
- throw new CliError('Usage: knowledge search "<question>" [--limit <n>] [--kind <kind>] [--library <slug>]; put the question in quotes', exitCodes.usage);
64
- const query = new URLSearchParams({ q: args[0] });
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 { results } = await api(await resolveConnection(options.apiUrl), `/api/v1/knowledge/search?${query}`);
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({ results });
74
- if (results.length === 0)
75
- return console.log("Nothing in the library matched.");
76
- console.log(results.map(knowledgeBlock).join("\n\n"));
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
- printFields({ ...factRow(fact), summary: fact.summary, value: fact.value, evidence: fact.evidence, author: fact.author, "confirmed by": fact.confirmedBy, "decided by": fact.decidedBy, replaces: fact.replaces });
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrangled/cli",
3
- "version": "0.3.24",
3
+ "version": "0.3.26",
4
4
  "description": "Command-line access to your marketing data warehouse, for people and coding agents",
5
5
  "type": "module",
6
6
  "bin": {