@wrangled/cli 0.3.21 → 0.3.23
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 +14 -4
- package/dist/main.js +113 -13
- package/dist/skill-zip.js +8 -2
- package/package.json +1 -1
package/dist/guide.js
CHANGED
|
@@ -35,9 +35,10 @@ To read the whole library with your own tools, such as "every email we have" or
|
|
|
35
35
|
|
|
36
36
|
## Know-how library: Creative Cartel
|
|
37
37
|
|
|
38
|
-
For what has worked for others, such as "how do members test new hooks" or "what do people say about advertorials", search the know-how library: \`${cli} knowledge search "<question>"\`. Today it holds Creative Cartel's library (\`creative-cartel\`), copied into this server every Monday afternoon. It works only when the key has library access (\`${cli} whoami\` says); without it the answer is \`knowledge_not_allowed\`. It finds snippets by their words and by their meaning, and returns them short, never whole documents: each at most 280 characters, with its title, link, kind, caveat, evidence and the day its library was copied in. Evidence \`reported member statement\` is a member's claim, not a checked result; weigh it that way and say so when you use it. The library writes no answer for you: read the snippets and decide. Use your own words: saving text that repeats 12 words in a row from anything in the library is refused with \`copies_library_text\`.
|
|
38
|
+
For what has worked for others, such as "how do members test new hooks" or "what do people say about advertorials", search the know-how library: \`${cli} knowledge search "<question>"\`. Today it holds Creative Cartel's library (\`creative-cartel\`) the team's own Exodus copy and ad frameworks (\`exodus\`), and Meta's advertising standards (\`meta-policy\`), all copied into this server every Monday afternoon. It works only when the key has library access (\`${cli} whoami\` says); without it the answer is \`knowledge_not_allowed\`. It finds snippets by their words and by their meaning, and returns them short, never whole documents: each at most 280 characters, with its title, link, kind, caveat, evidence and the day its library was copied in. Evidence \`reported member statement\` is a member's claim, not a checked result; weigh it that way and say so when you use it. Evidence \`team method\` is the team's own framework: reason with it, never paste it into client work. The library writes no answer for you: read the snippets and decide. Use your own words: saving text that repeats 12 words in a row from anything in the library is refused with \`copies_library_text\`.
|
|
39
39
|
|
|
40
40
|
- 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.
|
|
41
|
+
- \`--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.
|
|
41
42
|
- \`knowledge_not_synced\` means the library has not been copied into this server yet, so nothing was searched; the weekly sync fills it.
|
|
42
43
|
|
|
43
44
|
## Query workflow: schema first
|
|
@@ -87,7 +88,7 @@ A webhook address lets a tool such as a form or webinar platform push events int
|
|
|
87
88
|
1. \`${cli} tools list\` shows each tool id (\`provider:tool\`, such as \`kie:nano-banana\`), whether it runs sync or async, and what it makes. \`--format json\` adds each tool's description, cost, and input JSON Schema.
|
|
88
89
|
2. \`${cli} tools run kie:nano-banana --input '{"prompt": "...", "aspectRatio": "1:1"}' --idempotency-key <key>\` starts a run and waits for it (up to \`--wait\` seconds, default 300). Tools cost money. Always pass an idempotency key, and reuse it when you retry, so a retry returns the same run instead of paying twice.
|
|
89
90
|
3. A run that is still going prints its id. Check it with \`${cli} tools get <run-id>\`. Image results list an \`asset\` link. Save the file with \`${cli} creatives download <asset-link>\`, or fetch the link with your API key as a bearer token; it redirects to a copy that works for 10 minutes.
|
|
90
|
-
4. Record a succeeded run in the ledger with its lineage: \`${cli} creatives create --run <run-id> --lineage '{"persona": "...", "painPoint": "...", "desiredOutcome": "...", "angle": "...", "sourceAd": {"metaAdId": "..."}}'\`. Each output gets a SKU such as \`DLC-000123\`. The SKU goes in the Meta ad name.
|
|
91
|
+
4. Record a succeeded run in the ledger with its lineage: \`${cli} creatives create --run <run-id> --lineage '{"persona": "...", "painPoint": "...", "desiredOutcome": "...", "angle": "...", "sourceAd": {"metaAdId": "..."}}'\`. Each output gets a SKU such as \`DLC-000123\`. The SKU goes in the Meta ad name. Lineage is free-text notes; give the ad's marketing tags with \`--tags '<json>'\` or \`--tags @tags.json\`, the same fields as ad tags (see Ad tags), such as \`{"offer_key": "course", "landing_funnel_key": "webinar_to_course", "landing_step_key": "registration_page", "persona": "near_retirement", "angle": "story", "format": "image"}\`. Every output of the run gets them. Values come from \`${cli} grammar\`; persona and concept from this client's lists, and offer, funnel and step from its confirmed map (\`${cli} funnels show\`), where the step must be in that funnel and \`reply\` or \`no_link\` take no step. A value off them is refused with the ones to use. Recording the same run again returns its creatives as they are, tags included; \`${cli} creatives tag <sku> --file tags.json\` is how to change a creative's tags. Your tags read proposed until an Owner or Admin confirms them on the creative's page in Content; after that only a person changes them.
|
|
91
92
|
5. \`${cli} creatives list\` and \`${cli} creatives show <sku>\` read the ledger. \`${cli} creatives download <sku>\` saves a creative's image or video as \`<sku>.<ext>\` in the current folder; \`--out <path>\` names the file or a folder, and \`--open\` opens it. It also takes an asset path or link, such as a \`meta_ad_creatives\` \`image_path\`. \`--format json\` prints \`{ path, bytes, contentType }\`. Copy has no file; its text is in \`${cli} creatives show\`. For performance, query the \`creatives\` table or the \`creative_performance_daily\` view with \`${cli} warehouse query\`.
|
|
92
93
|
|
|
93
94
|
## Landing pages
|
|
@@ -178,13 +179,19 @@ To have the server tag ads for you, run \`${cli} ads autotag\` (\`--limit <n>\`,
|
|
|
178
179
|
|
|
179
180
|
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\`.
|
|
180
181
|
|
|
182
|
+
## Content tags
|
|
183
|
+
|
|
184
|
+
Emails, text messages and landing pages in the content library get the same grammar's tags, by content item id. \`${cli} content tags [<item-id>]\` lists them (\`--kind email|text|page\`, \`--status proposed|confirmed|rejected|replaced\`, \`--limit <n>\`, \`--json\`); by default the live ones. Each row names the version of the item's words it describes (\`contentHash\`). Only the parts that fit the kind are filled: an email's or text's job (invite, reminder, last chance and so on), no job for a page, and no secondary hook for a text, whose format (sms or mms) comes from whether it carries a picture.
|
|
185
|
+
|
|
186
|
+
To have the server tag them, run \`${cli} content autotag\` (\`--limit <n>\`, 1 to 100, default 25; \`--kind email,text\` to narrow; item ids as arguments for those items; \`--json\`). It takes the items whose current words have no tags yet, the ones that brought in the most sales first, then the most clicks, then the newest, so a repeat run spends nothing on what it already tagged. Items that read the same, such as one email sent to several lists, are asked about once and share the tags. An email whose body was never stored is asked only whether it is marketing, its hook and its job. When an item's words change, its next run replaces its tags, even confirmed ones, since a person confirmed the old words. \`${cli} content to-check\` lists the emails and texts whose tags wait for a person, most sales first, then most clicks; the same rows are in the \`content_tags\` table.
|
|
187
|
+
|
|
181
188
|
## Ad words
|
|
182
189
|
|
|
183
190
|
Tag ads, emails, texts and pages only with the words \`${cli} grammar\` lists (\`--format json\` for the same lists as data): each part's question and allowed values. Persona (who it talks to) and Concept (the one idea it sells) are this client's own lists; the other parts are shared by every client. A persona or concept marked not yet confirmed may be used. A retired one is never used for new tags; an old tag with it counts as the value it was merged into. To suggest new personas and concepts, run \`${cli} grammar propose --file values.json\` with \`{"values": [{"part": "persona", "key": "age_50s_retirement", "label": "In their 50s, worried about retirement", "definition": "People in their 50s who fear their savings will not last.", "examples": ["<SKU>"]}]}\`. You only suggest: a value already on the list is left as it is, and a person confirms, renames, merges or retires values in the app. The \`grammar\` playbook (\`${cli} skill install grammar\` installs it as the \`${GRAMMAR_SKILL_NAME}\` skill; \`${cli} docs grammar\` prints it) walks an agent from the client's ads to a first draft.
|
|
184
191
|
|
|
185
192
|
## What customers keep saying
|
|
186
193
|
|
|
187
|
-
Every hour an AI reads the workspace's
|
|
194
|
+
Every hour an AI reads what is new in the workspace's Facebook and Instagram comments, Google reviews, Reddit posts and comments, recorded calls and meetings (Fathom, Zoom, CallRail) and chats (GoHighLevel texts and chat, Closebot), pulls out openers (lines that would open an ad), objections (why people hesitate), pains (problems they live with), desires (results they want) and phrases (exact wording worth copying into an ad), and groups the ones that say the same thing. From calls and chats only the customer's words count, and names, emails, phone numbers and handles are taken out first. \`${cli} voice\` lists the groups, most common first (\`--kind opener|objection|pain|desire|phrase\`, \`--limit <n>\` up to 200, \`--json\`), each with how many sources said it (a conversation counts once), their likes, and up to 3 real quotes. \`${cli} voice --since <YYYY-MM-DD> --sort useful\` lists single lines said since that day instead, the most usable in an ad first: specific, first-person, recent and often said; \`--json\` adds each line's link. In \`--json\`, each quote's \`kind\` (a line's \`source\`) and \`ref\` go 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.
|
|
188
195
|
|
|
189
196
|
## The weekly story
|
|
190
197
|
|
|
@@ -259,7 +266,8 @@ ${cli} tools get <run-id> show a tool run, waiting for it to finish (-
|
|
|
259
266
|
${cli} creatives list list creatives (--status, --kind, --limit)
|
|
260
267
|
${cli} creatives show <sku> show one creative with its spec and lineage
|
|
261
268
|
${cli} creatives download <sku | asset-path | asset-link> [--out <path>] [--open] save a creative's image or video
|
|
262
|
-
${cli} creatives create --run <run-id> [--lineage '<json>' | @file.json] record a run's output with a new SKU
|
|
269
|
+
${cli} creatives create --run <run-id> [--lineage '<json>' | @file.json] [--tags '<json>' | @file.json] record a run's output with a new SKU
|
|
270
|
+
${cli} creatives tag <sku> --file <tags.json> replace a creative's marketing tags, proposed until a person confirms them
|
|
263
271
|
${cli} content create --kind <kind> [--body ...] [--file <path>] [--note <why>] [--plan <plan-id>] save made content with a new SKU
|
|
264
272
|
${cli} content search [--text <words>] [--mode word|meaning] [--kind <kind>] [--platform <p>] [--origin <o>] [--from <date>] [--to <date>] [--sort <column>] [--order asc|desc] [--limit <n>] find content with its numbers
|
|
265
273
|
${cli} content pull <dir> [--kind <kind>] [--platform <p>] [--origin <o>] [--media] [--full] copy the library to a folder of Markdown files; rerun for changes
|
|
@@ -290,6 +298,8 @@ ${cli} facts [--kind <kind>] [--subject <type>[:<ref>]] [--status <status>] [--j
|
|
|
290
298
|
${cli} facts propose --file <fact.json> suggest a fact for a person to confirm
|
|
291
299
|
${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
|
|
292
300
|
${cli} ads autotag [--limit <n>] [--ad <id>] have the server propose tags for the biggest spenders not yet tagged
|
|
301
|
+
${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
|
|
302
|
+
${cli} content autotag [<item-id> ...] [--kind email|text|page] [--limit <n>] have the server propose tags for the emails, texts and pages worth most not yet tagged
|
|
293
303
|
${cli} grammar [--format json] every list ads are tagged from, this client's personas and concepts included
|
|
294
304
|
${cli} grammar propose --file <values.json> suggest personas and concepts for a person to confirm
|
|
295
305
|
${cli} voice [--kind opener|objection|phrase] [--limit <n>] [--json] what customers keep saying, grouped and counted, with real quotes
|
package/dist/main.js
CHANGED
|
@@ -15,6 +15,7 @@ import { SKILL_NAME_PATTERN } from "./skill-zip.js";
|
|
|
15
15
|
import { installSkillFolder, operatorConnection, packSkill, parseSkillRef, readSkillFolder, unpackSkill, } from "./skills.js";
|
|
16
16
|
const DEFAULT_WAIT_SECONDS = 300;
|
|
17
17
|
const SERVER_WAIT_CAP = 50;
|
|
18
|
+
const VOICE_USAGE = "voice [--kind opener|objection|pain|desire|phrase] [--since <YYYY-MM-DD>] [--sort common|useful] [--limit <n>] [--json]";
|
|
18
19
|
const commands = {
|
|
19
20
|
login: {
|
|
20
21
|
usage: `login (paste the key when asked, pipe it on stdin, or set ${envPrefix}_API_KEY)`,
|
|
@@ -500,7 +501,7 @@ const commands = {
|
|
|
500
501
|
tool: creative.spec.tool,
|
|
501
502
|
prompt: creative.spec.input?.prompt,
|
|
502
503
|
};
|
|
503
|
-
const shown = ["sku", "kind", "status", "tool", "prompt", "lineage", "assetUrl", "copyText", "metaAdId", "toolRunId", "createdAt"];
|
|
504
|
+
const shown = ["sku", "kind", "status", "tool", "prompt", "lineage", "tags", "tagsStatus", "tagsConfirmedBy", "assetUrl", "copyText", "metaAdId", "toolRunId", "createdAt"];
|
|
504
505
|
console.log(renderTable(["field", "value"], shown.filter((f) => fields[f] != null).map((f) => ({ field: f, value: fields[f] }))));
|
|
505
506
|
},
|
|
506
507
|
},
|
|
@@ -522,18 +523,33 @@ const commands = {
|
|
|
522
523
|
},
|
|
523
524
|
},
|
|
524
525
|
"creatives create": {
|
|
525
|
-
usage: "creatives create --run <tool-run-id> [--lineage '<json>' | @file.json]",
|
|
526
|
+
usage: "creatives create --run <tool-run-id> [--lineage '<json>' | @file.json] [--tags '<json>' | @file.json]",
|
|
526
527
|
async run(args, options) {
|
|
527
|
-
if (args.length > 0 || !options.run)
|
|
528
|
-
throw new CliError("Usage: creatives create --run <tool-run-id> [--lineage '<json>' | @file.json]", exitCodes.usage);
|
|
528
|
+
if (args.length > 0 || !options.run) {
|
|
529
|
+
throw new CliError("Usage: creatives create --run <tool-run-id> [--lineage '<json>' | @file.json] [--tags '<json>' | @file.json]", exitCodes.usage);
|
|
530
|
+
}
|
|
529
531
|
const lineage = options.lineage === undefined ? undefined : await parseJson(options.lineage, "--lineage");
|
|
532
|
+
const tags = options.tags === undefined ? undefined : await parseJson(options.tags, "--tags");
|
|
530
533
|
const { creatives } = await api(await resolveConnection(options.apiUrl), "/api/v1/creatives", {
|
|
531
534
|
runId: options.run,
|
|
532
535
|
...(lineage === undefined ? {} : { lineage }),
|
|
536
|
+
...(tags === undefined ? {} : { tags }),
|
|
533
537
|
});
|
|
534
538
|
if (options.format === "json")
|
|
535
539
|
return printJson({ creatives });
|
|
536
|
-
console.log(renderTable(["sku", "kind", "status", "assetUrl"], creatives));
|
|
540
|
+
console.log(renderTable(["sku", "kind", "status", "tagsStatus", "assetUrl"], creatives));
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
"creatives tag": {
|
|
544
|
+
usage: "creatives tag <sku> --file <tags.json> | < tags.json replace a creative's marketing tags; a person confirms them in the app",
|
|
545
|
+
async run(args, options) {
|
|
546
|
+
if (args.length !== 1)
|
|
547
|
+
throw new CliError("Usage: creatives tag <sku> --file <tags.json> | < tags.json", exitCodes.usage);
|
|
548
|
+
const tags = await readToolInput(options);
|
|
549
|
+
const { creative } = await api(await resolveConnection(options.apiUrl), `/api/v1/creatives/${encodeURIComponent(args[0])}/tags`, { tags }, "PATCH");
|
|
550
|
+
if (options.format === "json")
|
|
551
|
+
return printJson({ creative });
|
|
552
|
+
console.log(`Tagged ${creative.sku}. The tags read ${creative.tagsStatus} until a person confirms them in the app.`);
|
|
537
553
|
},
|
|
538
554
|
},
|
|
539
555
|
"content create": {
|
|
@@ -1110,21 +1126,30 @@ const commands = {
|
|
|
1110
1126
|
},
|
|
1111
1127
|
},
|
|
1112
1128
|
voice: {
|
|
1113
|
-
usage:
|
|
1129
|
+
usage: `${VOICE_USAGE} what customers keep saying, most common first, or with --sort useful the most usable single lines`,
|
|
1114
1130
|
async run(args, options) {
|
|
1115
1131
|
if (args.length > 0)
|
|
1116
|
-
throw new CliError(
|
|
1132
|
+
throw new CliError(`Usage: ${VOICE_USAGE}`, exitCodes.usage);
|
|
1117
1133
|
const query = new URLSearchParams();
|
|
1118
1134
|
if (options.kind)
|
|
1119
1135
|
query.set("kind", options.kind);
|
|
1136
|
+
if (options.since)
|
|
1137
|
+
query.set("since", options.since);
|
|
1138
|
+
if (options.sort)
|
|
1139
|
+
query.set("sort", options.sort);
|
|
1120
1140
|
if (options.limit)
|
|
1121
1141
|
query.set("limit", options.limit);
|
|
1122
|
-
const
|
|
1142
|
+
const read = await api(await resolveConnection(options.apiUrl), `${VOICE_PATH}${query.size ? `?${query}` : ""}`);
|
|
1123
1143
|
if (options.format === "json")
|
|
1124
|
-
return printJson(
|
|
1125
|
-
if (
|
|
1144
|
+
return printJson(read);
|
|
1145
|
+
if ("lines" in read) {
|
|
1146
|
+
if (read.lines.length === 0)
|
|
1147
|
+
return console.log("Nothing said in that time yet.");
|
|
1148
|
+
return console.log(renderTable(VOICE_LINE_COLUMNS, read.lines.map(voiceLineRow)));
|
|
1149
|
+
}
|
|
1150
|
+
if (read.clusters.length === 0)
|
|
1126
1151
|
return console.log("Nothing grouped yet.");
|
|
1127
|
-
console.log(renderTable(VOICE_COLUMNS, clusters.map(voiceRow)));
|
|
1152
|
+
console.log(renderTable(VOICE_COLUMNS, read.clusters.map(voiceRow)));
|
|
1128
1153
|
},
|
|
1129
1154
|
},
|
|
1130
1155
|
"ads tags": {
|
|
@@ -1186,6 +1211,63 @@ const commands = {
|
|
|
1186
1211
|
console.log("Stopped early: the run's tool spending cap was reached.");
|
|
1187
1212
|
},
|
|
1188
1213
|
},
|
|
1214
|
+
"content tags": {
|
|
1215
|
+
usage: "content tags [<item-id>] [--kind email|text|page] [--status <status>] [--limit <n>] [--json] the marketing tags of emails, texts and landing pages",
|
|
1216
|
+
async run(args, options) {
|
|
1217
|
+
if (args.length > 1)
|
|
1218
|
+
throw new CliError("Usage: content tags [<item-id>] [--kind email|text|page] [--status <status>] [--limit <n>] [--json]", exitCodes.usage);
|
|
1219
|
+
const filters = { item: args[0], kind: options.kind, status: options.status, limit: options.limit };
|
|
1220
|
+
const query = new URLSearchParams(Object.entries(filters).filter((entry) => entry[1] !== undefined));
|
|
1221
|
+
const { tags } = await api(await resolveConnection(options.apiUrl), `${CONTENT_TAGS_PATH}${query.size ? `?${query}` : ""}`);
|
|
1222
|
+
if (options.format === "json")
|
|
1223
|
+
return printJson({ tags });
|
|
1224
|
+
if (tags.length === 0)
|
|
1225
|
+
return console.log("No content tags.");
|
|
1226
|
+
console.log(renderTable(CONTENT_TAG_COLUMNS, tags.map(contentTagRow)));
|
|
1227
|
+
},
|
|
1228
|
+
},
|
|
1229
|
+
"content to-check": {
|
|
1230
|
+
usage: "content to-check [--limit <n>] [--json] the emails and texts whose tags wait for a person, most sales first, then most clicks",
|
|
1231
|
+
async run(args, options) {
|
|
1232
|
+
if (args.length > 0)
|
|
1233
|
+
throw new CliError("Usage: content to-check [--limit <n>] [--json]", exitCodes.usage);
|
|
1234
|
+
const query = new URLSearchParams({ to_check: "1", ...(options.limit ? { limit: options.limit } : {}) });
|
|
1235
|
+
const { tags } = await api(await resolveConnection(options.apiUrl), `${CONTENT_TAGS_PATH}?${query}`);
|
|
1236
|
+
if (options.format === "json")
|
|
1237
|
+
return printJson({ tags });
|
|
1238
|
+
if (tags.length === 0)
|
|
1239
|
+
return console.log("No email or text tags wait for a person.");
|
|
1240
|
+
console.log(renderTable(["item", "kind", "revenue", "clicks", ...CONTENT_TAG_COLUMNS.slice(4)], tags.map((t) => ({ ...contentTagRow(t), revenue: t.revenue, clicks: t.clicks }))));
|
|
1241
|
+
},
|
|
1242
|
+
},
|
|
1243
|
+
"content autotag": {
|
|
1244
|
+
usage: "content autotag [<item-id> ...] [--kind email|text|page] [--limit <n>] [--json] propose marketing tags for the emails, texts and landing pages worth most that have none",
|
|
1245
|
+
async run(args, options) {
|
|
1246
|
+
const body = {
|
|
1247
|
+
...(args.length > 0 ? { item_ids: args } : {}),
|
|
1248
|
+
...(options.kind ? { kinds: options.kind.split(",") } : {}),
|
|
1249
|
+
...(options.limit ? { limit: Number(options.limit) } : {}),
|
|
1250
|
+
};
|
|
1251
|
+
const result = await api(await resolveConnection(options.apiUrl), CONTENT_AUTOTAG_PATH, body);
|
|
1252
|
+
if (options.format === "json")
|
|
1253
|
+
return printJson(result);
|
|
1254
|
+
const plural = (n) => `${n} ${n === 1 ? "item" : "items"}`;
|
|
1255
|
+
console.log(`Proposed tags for ${plural(result.proposed.length)}, asking about ${result.groups} ${result.groups === 1 ? "set of words" : "sets of words"} for $${result.costUsd.toFixed(4)}. A person confirms or rejects them in the app.`);
|
|
1256
|
+
if (result.proposed.length > 0)
|
|
1257
|
+
console.log(renderTable(["item", "id", "confidence"], result.proposed.map((p) => ({ item: p.itemId, id: p.id, confidence: p.confidence }))));
|
|
1258
|
+
if (result.kept.length > 0)
|
|
1259
|
+
console.log(`Kept as they are, since a person confirmed their tags: ${result.kept.join(", ")}.`);
|
|
1260
|
+
for (const reason of ["not_marketing", "gate_unclear", "no_content", "not_found", "changed"]) {
|
|
1261
|
+
const ids = result.skipped.filter((s) => s.reason === reason).map((s) => s.itemId);
|
|
1262
|
+
if (ids.length > 0)
|
|
1263
|
+
console.log(`${CONTENT_SKIP_REASONS[reason]}: ${ids.join(", ")}.`);
|
|
1264
|
+
}
|
|
1265
|
+
for (const failure of result.failed)
|
|
1266
|
+
console.log(`Failed for ${failure.itemIds.join(", ")}: ${failure.error}`);
|
|
1267
|
+
if (result.stopped === "tool_cap")
|
|
1268
|
+
console.log("Stopped early: the run's tool spending cap was reached.");
|
|
1269
|
+
},
|
|
1270
|
+
},
|
|
1189
1271
|
"facts show": {
|
|
1190
1272
|
usage: "facts show <id> one fact with its value and evidence",
|
|
1191
1273
|
async run(args, options) {
|
|
@@ -1716,12 +1798,17 @@ const AD_TAGS_PATH = "/api/v1/ad-tags";
|
|
|
1716
1798
|
const AD_AUTOTAG_PATH = "/api/v1/ad-tags/autotag";
|
|
1717
1799
|
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" };
|
|
1718
1800
|
const AD_TAG_COLUMNS = ["id", "ad", "status", "confidence", "offer", "persona", "angle", "hook", "format", "author"];
|
|
1801
|
+
const CONTENT_TAGS_PATH = "/api/v1/content-tags";
|
|
1802
|
+
const CONTENT_SKIP_REASONS = { ...SKIP_REASONS, not_found: "No such email, text or landing page", changed: "Its words changed while it was being tagged, so nothing was saved; the next run tags them" };
|
|
1803
|
+
const CONTENT_AUTOTAG_PATH = "/api/v1/content-tags/autotag";
|
|
1804
|
+
const CONTENT_TAG_COLUMNS = ["id", "item", "kind", "status", "confidence", "hook", "job", "offer", "angle", "format"];
|
|
1719
1805
|
const GRAMMAR_PATH = "/api/v1/grammar";
|
|
1720
1806
|
const VOICE_LISTS_PATH = "/api/v1/voice-lists";
|
|
1721
1807
|
const VOICE_SUGGESTIONS_PATH = "/api/v1/voice-lists/suggestions";
|
|
1722
1808
|
const FACT_COLUMNS = ["id", "kind", "subject", "key", "status", "summary"];
|
|
1723
1809
|
const VOICE_PATH = "/api/v1/voice";
|
|
1724
1810
|
const VOICE_COLUMNS = ["ref", "kind", "said by", "likes", "label", "top quote"];
|
|
1811
|
+
const VOICE_LINE_COLUMNS = ["date", "kind", "source", "quote", "ref"];
|
|
1725
1812
|
function knowledgeBlock(r, i) {
|
|
1726
1813
|
const lines = [`${i + 1}. ${r.title}`, ` ${r.kind}, ${r.evidence.replaceAll("_", " ")} (${r.source}${r.syncedAt ? `, copied in ${r.syncedAt.slice(0, 10)}` : ""})`];
|
|
1727
1814
|
if (r.url)
|
|
@@ -1735,9 +1822,16 @@ function adTagRow(t) {
|
|
|
1735
1822
|
const { offer_key, persona, angle, hook_kind, hook_spot, format, style } = t.tags;
|
|
1736
1823
|
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 };
|
|
1737
1824
|
}
|
|
1825
|
+
function contentTagRow(t) {
|
|
1826
|
+
const { hook_kind, hook_spot, job, offer_key, angle, format } = t.tags;
|
|
1827
|
+
return { id: t.id, item: t.itemId, kind: t.kind, status: t.status, confidence: t.confidence, hook: [hook_kind, hook_spot].filter(Boolean).join(" in ") || null, job, offer: offer_key, angle, format };
|
|
1828
|
+
}
|
|
1738
1829
|
function voiceRow(c) {
|
|
1739
1830
|
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 };
|
|
1740
1831
|
}
|
|
1832
|
+
function voiceLineRow(l) {
|
|
1833
|
+
return { date: l.date.slice(0, 10), kind: l.kind, source: l.source, quote: excerpt(l.quote, 80), ref: l.ref };
|
|
1834
|
+
}
|
|
1741
1835
|
function factRow(f) {
|
|
1742
1836
|
return { id: f.id, kind: f.kind, subject: `${f.subjectType}:${f.subjectRef}`, key: f.key, status: f.status, summary: excerpt(f.summary, 80) };
|
|
1743
1837
|
}
|
|
@@ -1852,9 +1946,11 @@ Options:
|
|
|
1852
1946
|
--from, --to <n> with content diff, the two versions to compare (default: the one before the current one, and the current one)
|
|
1853
1947
|
--sort <column> how content search sorts: recent (default) or a content_scorecard number, such as click_rate or roas
|
|
1854
1948
|
--order desc|asc content search's sort direction (default: desc, best or newest first)
|
|
1855
|
-
--limit <n> how many results content search, creatives list, campaigns, voice
|
|
1856
|
-
|
|
1949
|
+
--limit <n> how many results content search, creatives list, campaigns, voice, ads tags, content tags and content to-check
|
|
1950
|
+
return, or how many ads ads autotag tags and how many items content autotag tags
|
|
1951
|
+
--library <slug> with knowledge search, only this library: creative-cartel, exodus or meta-policy
|
|
1857
1952
|
--ad <id> with ads tags, only this Meta ad's tags; with ads autotag, only this ad
|
|
1953
|
+
--kind email|text|page with content tags and content autotag, only that kind (content autotag takes a comma list)
|
|
1858
1954
|
--max-seconds <n> stop a runner's run after this many seconds, if that comes before its deadline
|
|
1859
1955
|
--workspace <slug> for worker promote and worker drop: the workspace whose hand they change
|
|
1860
1956
|
-h, --help show this help
|
|
@@ -1888,6 +1984,7 @@ function parseOptions(argv) {
|
|
|
1888
1984
|
limit: { type: "string" },
|
|
1889
1985
|
run: { type: "string" },
|
|
1890
1986
|
lineage: { type: "string" },
|
|
1987
|
+
tags: { type: "string" },
|
|
1891
1988
|
sources: { type: "string" },
|
|
1892
1989
|
codex: { type: "boolean", default: false },
|
|
1893
1990
|
agent: { type: "string" },
|
|
@@ -1904,6 +2001,7 @@ function parseOptions(argv) {
|
|
|
1904
2001
|
force: { type: "boolean", default: false },
|
|
1905
2002
|
yes: { type: "boolean", default: false },
|
|
1906
2003
|
from: { type: "string" },
|
|
2004
|
+
since: { type: "string" },
|
|
1907
2005
|
to: { type: "string" },
|
|
1908
2006
|
grain: { type: "string" },
|
|
1909
2007
|
channel: { type: "string" },
|
|
@@ -1981,6 +2079,7 @@ function parseOptions(argv) {
|
|
|
1981
2079
|
limit: values.limit,
|
|
1982
2080
|
run: values.run,
|
|
1983
2081
|
lineage: values.lineage,
|
|
2082
|
+
tags: values.tags,
|
|
1984
2083
|
sources: values.sources,
|
|
1985
2084
|
codex: values.codex,
|
|
1986
2085
|
agent: values.agent ?? (process.env[AGENT_ENV] || "meta_ads"),
|
|
@@ -1998,6 +2097,7 @@ function parseOptions(argv) {
|
|
|
1998
2097
|
force: values.force,
|
|
1999
2098
|
yes: values.yes,
|
|
2000
2099
|
from: values.from,
|
|
2100
|
+
since: values.since,
|
|
2001
2101
|
to: values.to,
|
|
2002
2102
|
grain: values.grain,
|
|
2003
2103
|
channel: values.channel,
|
package/dist/skill-zip.js
CHANGED
|
@@ -41,10 +41,16 @@ export function unsafeFileName(name) {
|
|
|
41
41
|
return `empty or . segment in ${name}`;
|
|
42
42
|
return null;
|
|
43
43
|
}
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* The time stamped on every zipped file: the earliest a zip can hold. The library recognises a skill version by its
|
|
46
|
+
* zip's sha256, so the same files must zip to the same bytes on every run. fflate reads the date in local time, and
|
|
47
|
+
* this one is built in local time, so it encodes the same in every time zone.
|
|
48
|
+
*/
|
|
49
|
+
const ZIP_MTIME = new Date(1980, 0, 1);
|
|
50
|
+
/** Zips the files; each name is a `/`-separated path inside the skill folder. The same files always give the same bytes. */
|
|
45
51
|
export function zipSkillFiles(files) {
|
|
46
52
|
const unix = (mode) => ({ os: 3, attrs: ((0o100000 | mode) << 16) >>> 0 });
|
|
47
|
-
return zipSync(Object.fromEntries(files.map((f) => [f.name, [f.data, unix(f.executable ? 0o755 : 0o644)]])), { level: 9 });
|
|
53
|
+
return zipSync(Object.fromEntries(files.map((f) => [f.name, [f.data, unix(f.executable ? 0o755 : 0o644)]])), { level: 9, mtime: ZIP_MTIME });
|
|
48
54
|
}
|
|
49
55
|
/** Reads and checks a skill's zip without trusting any size or name it declares. Throws SkillZipError naming the problem. */
|
|
50
56
|
export function readSkillZip(zip, skillName) {
|