@wrangled/cli 0.3.24 → 0.3.25

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": {
@@ -1867,6 +1899,7 @@ function printReport(title, report, labels, numbers) {
1867
1899
  const agentPath = (options) => `/api/v1/agents/${encodeURIComponent(options.agent)}`;
1868
1900
  const FACTS_PATH = "/api/v1/facts";
1869
1901
  const AD_TAGS_PATH = "/api/v1/ad-tags";
1902
+ const KNOWLEDGE_TAGS_PATH = "/api/v1/knowledge/tags";
1870
1903
  const AD_AUTOTAG_PATH = "/api/v1/ad-tags/autotag";
1871
1904
  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
1905
  const AD_TAG_COLUMNS = ["id", "ad", "status", "confidence", "offer", "persona", "angle", "hook", "format", "author"];
@@ -1888,8 +1921,16 @@ function knowledgeBlock(r, i) {
1888
1921
  lines.push(` ${r.snippet}`);
1889
1922
  if (r.caveat)
1890
1923
  lines.push(` Caveat: ${r.caveat}`);
1924
+ if (r.entryId) {
1925
+ const tags = Object.entries(r.tags ?? {}).flatMap(([field, values]) => values.map((value) => `${field} ${value}`));
1926
+ lines.push(` Entry ${r.entryId}${tags.length > 0 ? `, tagged ${tags.join(", ")}` : ""}`);
1927
+ }
1891
1928
  return lines.join("\n");
1892
1929
  }
1930
+ function yourAdRow(a) {
1931
+ const results = a.results === null ? null : `${a.results} ${a.resultType ?? ""}`.trim();
1932
+ 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 };
1933
+ }
1893
1934
  function adTagRow(t) {
1894
1935
  const { offer_key, persona, angle, hook_kind, hook_spot, format, style } = t.tags;
1895
1936
  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 };
@@ -2022,6 +2063,10 @@ Options:
2022
2063
  --limit <n> how many results content search, creatives list, campaigns, voice, ads tags, content tags and content to-check
2023
2064
  return, or how many ads ads autotag tags and how many items content autotag tags
2024
2065
  --library <slug> with knowledge search, only this library: creative-cartel, exodus or meta-policy
2066
+ --angle, --hook-kind, --ad-format, --awareness <value> with knowledge search, only library entries tagged that way, and
2067
+ your own ads tagged the same way with their results; with knowledge tag, the tags to put on an entry
2068
+ --confidence high|medium|low with knowledge tag, how sure you are of the tags
2069
+ --remove with knowledge tag, take the tags off instead
2025
2070
  --ad <id> with ads tags, only this Meta ad's tags; with ads autotag, only this ad
2026
2071
  --kind email|text|page with content tags and content autotag, only that kind (content autotag takes a comma list)
2027
2072
  --max-seconds <n> stop a runner's run after this many seconds, if that comes before its deadline
@@ -2122,6 +2167,12 @@ function parseOptions(argv) {
2122
2167
  step: { type: "string" },
2123
2168
  workspace: { type: "string" },
2124
2169
  "max-seconds": { type: "string" },
2170
+ angle: { type: "string" },
2171
+ "hook-kind": { type: "string" },
2172
+ "ad-format": { type: "string" },
2173
+ awareness: { type: "string" },
2174
+ confidence: { type: "string" },
2175
+ remove: { type: "boolean", default: false },
2125
2176
  help: { type: "boolean", short: "h", default: false },
2126
2177
  },
2127
2178
  });
@@ -2219,6 +2270,9 @@ function parseOptions(argv) {
2219
2270
  step: values.step,
2220
2271
  workspace: values.workspace,
2221
2272
  maxSeconds,
2273
+ entryTags: Object.fromEntries(Object.entries(entryTagFlags).flatMap(([field, flag]) => (values[flag] === undefined ? [] : [[field, values[flag]]]))),
2274
+ confidence: values.confidence,
2275
+ remove: values.remove,
2222
2276
  },
2223
2277
  };
2224
2278
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrangled/cli",
3
- "version": "0.3.24",
3
+ "version": "0.3.25",
4
4
  "description": "Command-line access to your marketing data warehouse, for people and coding agents",
5
5
  "type": "module",
6
6
  "bin": {