@wrangled/cli 0.2.0 → 0.3.0
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/dlc-map-example.js +111 -0
- package/dist/guide.js +60 -23
- package/dist/main.js +387 -89
- package/dist/playbooks.js +213 -43
- package/dist/runner/claude.js +1 -1
- package/dist/runner/run.js +3 -3
- package/dist/skills.js +1 -1
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -9,7 +9,7 @@ import { brand, envPrefix } from "./brand.js";
|
|
|
9
9
|
import { firstSymlink, inRunFolder, writeFileAtomic } from "./files.js";
|
|
10
10
|
import { guide, skill } from "./guide.js";
|
|
11
11
|
import { PULL_STATE_FILE, pullContent } from "./pull.js";
|
|
12
|
-
import { AGENTS_MD_MARKER,
|
|
12
|
+
import { AGENTS_MD_MARKER, metaAdsAgentsMd, PLAYBOOK_CHOICES, playbooks } from "./playbooks.js";
|
|
13
13
|
import { AGENT_ENV, HARNESS_COMMAND_ENV, RUNNER_TOKEN_ENV, runLoop, runOnce } from "./runner/run.js";
|
|
14
14
|
import { SKILL_NAME_PATTERN } from "./skill-zip.js";
|
|
15
15
|
import { installSkillFolder, operatorConnection, packSkill, parseSkillRef, readSkillFolder, unpackSkill, } from "./skills.js";
|
|
@@ -102,20 +102,18 @@ const commands = {
|
|
|
102
102
|
},
|
|
103
103
|
},
|
|
104
104
|
"warehouse query": {
|
|
105
|
-
usage: 'warehouse query "<sql>" | --file <path> | < query.sql',
|
|
105
|
+
usage: 'warehouse query "<sql>" | --file <path> | < query.sql [--out <file.csv | file.json>]',
|
|
106
106
|
async run(args, options) {
|
|
107
107
|
const sql = await readSql(args, options.file);
|
|
108
108
|
const text = await apiText(await resolveConnection(options.apiUrl), "/api/v1/warehouse/query", { sql });
|
|
109
|
+
if (options.out !== undefined)
|
|
110
|
+
return saveQueryResult(text, options.out, options.format);
|
|
109
111
|
if (options.format === "json")
|
|
110
112
|
return console.log(text);
|
|
111
|
-
const result =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
console.log(`(${result.rows.length} ${result.rows.length === 1 ? "row" : "rows"})`);
|
|
116
|
-
if (result.truncated) {
|
|
117
|
-
console.error(`Results were cut off (${result.truncated}). Aggregate in SQL or add LIMIT.`);
|
|
118
|
-
}
|
|
113
|
+
const result = queryResultWithExactNumbers(text);
|
|
114
|
+
console.log(renderTable(result.columns.map((c) => c.name), result.rows));
|
|
115
|
+
console.log(`(${rowCount(result.rows.length)})`);
|
|
116
|
+
warnIfTruncated(result);
|
|
119
117
|
},
|
|
120
118
|
},
|
|
121
119
|
funnel: {
|
|
@@ -142,6 +140,9 @@ const commands = {
|
|
|
142
140
|
printReport(`Money by ${money.grain}`, money, ["period", "currency"], MONEY_NUMBERS);
|
|
143
141
|
if (money.by_source.length > 0)
|
|
144
142
|
console.log(`\nMoney in by payment source:\n${renderTable(["source", "currency", "gross", "refunds", "net", "orders"], money.by_source)}`);
|
|
143
|
+
if (money.store_orders_in_stripe?.length) {
|
|
144
|
+
console.log(`\nStore orders paid through Stripe, counted under stripe:\n${renderTable(["source", "currency", "orders", "total"], money.store_orders_in_stripe)}`);
|
|
145
|
+
}
|
|
145
146
|
if (money.refund_days.length > 0)
|
|
146
147
|
console.log(`\nBiggest refund days:\n${renderTable(["date", "currency", "refunds"], money.refund_days)}`);
|
|
147
148
|
},
|
|
@@ -258,8 +259,9 @@ const commands = {
|
|
|
258
259
|
const missing = source.missingAccess ?? [];
|
|
259
260
|
if (missing.length > 0)
|
|
260
261
|
console.log(`\nMissing access:\n${missing.map((m) => `- ${m.message}`).join("\n")}`);
|
|
261
|
-
|
|
262
|
-
|
|
262
|
+
const tables = source.tables.map((t) => ({ ...t, rows: t.rowsEstimated ? `about ${t.rows}` : t.rows }));
|
|
263
|
+
if (tables.length > 0)
|
|
264
|
+
console.log(`\n${renderTable(["name", "rows", "earliest", "latest"], tables)}`);
|
|
263
265
|
if (runs.length === 0)
|
|
264
266
|
return console.log("\nNo sync runs yet.");
|
|
265
267
|
const runRows = runs.map((r) => ({ ...r, error: r.error && excerpt(r.error) }));
|
|
@@ -479,11 +481,7 @@ const commands = {
|
|
|
479
481
|
const target = await downloadTarget(args[0], connection);
|
|
480
482
|
const { bytes, contentType } = await apiDownload(connection, `${ASSET_ROUTE}${target.assetPath.split("/").map(encodeURIComponent).join("/")}`);
|
|
481
483
|
const path = await savePath(options.out, extname(target.fileName) ? target.fileName : `${target.fileName}.${extensionFor(contentType)}`);
|
|
482
|
-
await
|
|
483
|
-
if (error.code === "ENOENT")
|
|
484
|
-
throw new CliError(`No folder at ${dirname(path)} to save into.`, exitCodes.usage);
|
|
485
|
-
throw error;
|
|
486
|
-
});
|
|
484
|
+
await saveFile(path, bytes);
|
|
487
485
|
if (options.open)
|
|
488
486
|
openFile(resolve(path));
|
|
489
487
|
if (options.format === "json")
|
|
@@ -551,6 +549,34 @@ const commands = {
|
|
|
551
549
|
process.stdout.write(guide.text);
|
|
552
550
|
},
|
|
553
551
|
},
|
|
552
|
+
brief: {
|
|
553
|
+
usage: "brief [--from <YYYY-MM-DD>] [--to <YYYY-MM-DD>] [--channel <channel>] [--campaign <id>] [--top <n>] (start here before writing new ads or a campaign plan)",
|
|
554
|
+
async run(args, options) {
|
|
555
|
+
if (args.length > 0)
|
|
556
|
+
throw new CliError("Usage: brief [--from <date>] [--to <date>] [--channel <channel>] [--campaign <id>] [--top <n>]", exitCodes.usage);
|
|
557
|
+
const query = reportQuery(options, { channel: options.channel, campaignId: options.campaign, top: options.top });
|
|
558
|
+
const brief = await api(await resolveConnection(options.apiUrl), `/api/v1/brief${query}`);
|
|
559
|
+
if (options.format === "json")
|
|
560
|
+
return printJson(brief);
|
|
561
|
+
process.stdout.write(brief.text);
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
examples: {
|
|
565
|
+
usage: "examples (plain questions with SQL that answers them, for the tables this workspace has)",
|
|
566
|
+
async run(args, options) {
|
|
567
|
+
if (args.length > 0)
|
|
568
|
+
throw new CliError("Usage: examples", exitCodes.usage);
|
|
569
|
+
const { examples } = await api(await resolveConnection(options.apiUrl), "/api/v1/examples");
|
|
570
|
+
if (options.format === "json")
|
|
571
|
+
return printJson({ examples });
|
|
572
|
+
for (const e of examples) {
|
|
573
|
+
console.log(`## ${e.id}\n\n${e.question}\nReads: ${e.needs.join(", ")}`);
|
|
574
|
+
if (e.notes)
|
|
575
|
+
console.log(`Note: ${e.notes}`);
|
|
576
|
+
console.log(`\n\`\`\`sql\n${e.sql}\n\`\`\`\n`);
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
},
|
|
554
580
|
"content search": {
|
|
555
581
|
usage: "content search [--text <words>] [--mode word|meaning] [--kind <kind>] [--platform <platform>] [--origin discovered|made] [--from <YYYY-MM-DD>] [--to <YYYY-MM-DD>] [--sort recent|<scorecard column>] [--order desc|asc] [--limit <n>]",
|
|
556
582
|
async run(args, options) {
|
|
@@ -672,6 +698,38 @@ const commands = {
|
|
|
672
698
|
printPublication(publication, options.format);
|
|
673
699
|
},
|
|
674
700
|
},
|
|
701
|
+
"meta adset publish": {
|
|
702
|
+
usage: "meta adset publish --plan '<json>' --idempotency-key <key> [--run <run-id>]",
|
|
703
|
+
async run(args, options) {
|
|
704
|
+
if (args.length !== 0 || options.plan === undefined || !options.idempotencyKey) {
|
|
705
|
+
throw new CliError("Usage: meta adset publish --plan '<json>' --idempotency-key <key> [--run <run-id>]", exitCodes.usage);
|
|
706
|
+
}
|
|
707
|
+
const { adsetPublication } = await api(await resolveConnection(options.apiUrl), "/api/v1/meta/adsets", {
|
|
708
|
+
plan: await parseJson(options.plan, "--plan"),
|
|
709
|
+
idempotencyKey: options.idempotencyKey,
|
|
710
|
+
...(options.run === undefined ? {} : { runId: options.run }),
|
|
711
|
+
});
|
|
712
|
+
printAdSetPublication(adsetPublication, options.format);
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
"meta adset show": {
|
|
716
|
+
usage: "meta adset show <adset-publication-id>",
|
|
717
|
+
async run(args, options) {
|
|
718
|
+
if (args.length !== 1)
|
|
719
|
+
throw new CliError("Usage: meta adset show <adset-publication-id>", exitCodes.usage);
|
|
720
|
+
const { adsetPublication } = await api(await resolveConnection(options.apiUrl), `/api/v1/meta/adsets/${encodeURIComponent(args[0])}`);
|
|
721
|
+
printAdSetPublication(adsetPublication, options.format);
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
"meta adset reconcile": {
|
|
725
|
+
usage: "meta adset reconcile <adset-publication-id>",
|
|
726
|
+
async run(args, options) {
|
|
727
|
+
if (args.length !== 1)
|
|
728
|
+
throw new CliError("Usage: meta adset reconcile <adset-publication-id>", exitCodes.usage);
|
|
729
|
+
const { adsetPublication } = await api(await resolveConnection(options.apiUrl), `/api/v1/meta/adsets/${encodeURIComponent(args[0])}/reconcile`, {});
|
|
730
|
+
printAdSetPublication(adsetPublication, options.format);
|
|
731
|
+
},
|
|
732
|
+
},
|
|
675
733
|
"content unpublish": {
|
|
676
734
|
usage: "content unpublish <deployment-id> [--force]",
|
|
677
735
|
async run(args, options) {
|
|
@@ -726,7 +784,6 @@ const commands = {
|
|
|
726
784
|
...(start.result ? [{ field: "result", value: `${start.result.name} (${start.result.metaActionTypes.join(", ") || "no Meta source"})` }] : []),
|
|
727
785
|
...(quota ? [{ field: "ideas today", value: `${quota.creativeProposalsToday} of ${quota.ideasPerDay} used, ${quota.remainingToday} left` }] : []),
|
|
728
786
|
{ field: "brand profile", value: brandProfile ? `version ${brandProfile.version}, ${brandProfile.ageDays} days old` : "none yet" },
|
|
729
|
-
...start.memory.map((note) => ({ field: `memory ${note.key}`, value: note.value ? `${note.value.length} characters` : "empty" })),
|
|
730
787
|
];
|
|
731
788
|
console.log(renderTable(["field", "value"], rows));
|
|
732
789
|
},
|
|
@@ -814,36 +871,6 @@ const commands = {
|
|
|
814
871
|
console.log(`\n${rows.length ? renderTable(Object.keys(rows[0]), rows) : "No P1 winners."}`);
|
|
815
872
|
},
|
|
816
873
|
},
|
|
817
|
-
"memory get": {
|
|
818
|
-
usage: "memory get [baselines|watch_items|changes_log|gotchas]",
|
|
819
|
-
async run(args, options) {
|
|
820
|
-
if (args.length > 1)
|
|
821
|
-
throw new CliError("Usage: memory get [<note>]", exitCodes.usage);
|
|
822
|
-
const connection = await resolveConnection(options.apiUrl);
|
|
823
|
-
if (args[0]) {
|
|
824
|
-
const { note } = await api(connection, `${agentPath(options)}/memory/${encodeURIComponent(args[0])}`);
|
|
825
|
-
if (options.format === "json")
|
|
826
|
-
return printJson({ note });
|
|
827
|
-
return void process.stdout.write(note.value ? `${note.value.replace(/\n?$/, "\n")}` : "");
|
|
828
|
-
}
|
|
829
|
-
const { memory } = await api(connection, `${agentPath(options)}/memory`);
|
|
830
|
-
if (options.format === "json")
|
|
831
|
-
return printJson({ memory });
|
|
832
|
-
console.log(memory.map((n) => `## ${n.key}${n.updatedAt ? ` (updated ${n.updatedAt})` : ""}\n\n${n.value || "(empty)"}`).join("\n\n"));
|
|
833
|
-
},
|
|
834
|
-
},
|
|
835
|
-
"memory set": {
|
|
836
|
-
usage: "memory set <note> [--file <path> | < note.md]",
|
|
837
|
-
async run(args, options) {
|
|
838
|
-
if (args.length !== 1)
|
|
839
|
-
throw new CliError("Usage: memory set <note> [--file <path>]", exitCodes.usage);
|
|
840
|
-
const value = await readText(options.file, "the note");
|
|
841
|
-
const { note } = await api(await resolveConnection(options.apiUrl), `${agentPath(options)}/memory/${encodeURIComponent(args[0])}`, { value }, "PUT");
|
|
842
|
-
if (options.format === "json")
|
|
843
|
-
return printJson({ note });
|
|
844
|
-
console.log(`Saved ${note.key} (${note.value.length} characters).`);
|
|
845
|
-
},
|
|
846
|
-
},
|
|
847
874
|
"brand get": {
|
|
848
875
|
usage: "brand get",
|
|
849
876
|
async run(_args, options) {
|
|
@@ -918,11 +945,147 @@ const commands = {
|
|
|
918
945
|
printTrail(trail, options.format);
|
|
919
946
|
},
|
|
920
947
|
},
|
|
948
|
+
"funnels show": {
|
|
949
|
+
usage: "funnels show [--version <n>] the offers and funnels map as text: the confirmed map (or that version) and the draft on hold",
|
|
950
|
+
async run(args, options) {
|
|
951
|
+
if (args.length > 0)
|
|
952
|
+
throw new CliError("Usage: funnels show [--version <n>]", exitCodes.usage);
|
|
953
|
+
const query = options.version === undefined ? "" : `?version=${wholeNumber(options.version, "--version")}`;
|
|
954
|
+
const view = await api(await resolveConnection(options.apiUrl), `${FUNNELS_PATH}${query}`);
|
|
955
|
+
if (options.format === "json")
|
|
956
|
+
return printJson(view);
|
|
957
|
+
process.stdout.write(view.text);
|
|
958
|
+
},
|
|
959
|
+
},
|
|
960
|
+
"funnels numbers": {
|
|
961
|
+
usage: "funnels numbers [--funnel <key>] [--from <YYYY-MM-DD>] [--to <YYYY-MM-DD>] how many reached each funnel step and moved on from the one before",
|
|
962
|
+
async run(args, options) {
|
|
963
|
+
if (args.length > 0)
|
|
964
|
+
throw new CliError("Usage: funnels numbers [--funnel <key>] [--from <date>] [--to <date>]", exitCodes.usage);
|
|
965
|
+
const view = await api(await resolveConnection(options.apiUrl), `${FUNNELS_PATH}/numbers${reportQuery(options, { funnel: options.funnel })}`);
|
|
966
|
+
if (options.format === "json")
|
|
967
|
+
return printJson(view);
|
|
968
|
+
process.stdout.write(view.text);
|
|
969
|
+
},
|
|
970
|
+
},
|
|
971
|
+
"funnels evidence": {
|
|
972
|
+
usage: "funnels evidence what the data says the client sells: prices paid, buyer paths, GoHighLevel funnels, calendars and tags, webinars, pages, campaigns, past campaigns",
|
|
973
|
+
async run(args, options) {
|
|
974
|
+
if (args.length > 0)
|
|
975
|
+
throw new CliError("Usage: funnels evidence", exitCodes.usage);
|
|
976
|
+
const view = await api(await resolveConnection(options.apiUrl), `${FUNNELS_PATH}/evidence`);
|
|
977
|
+
if (options.format === "json")
|
|
978
|
+
return printJson(view);
|
|
979
|
+
process.stdout.write(view.text);
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
"funnels draft": {
|
|
983
|
+
usage: "funnels draft --file <map.json> [--note <why>] [--evidence '<json>' | @file.json] [--based-on <n>] save a whole map; it counts at once when every step can be counted",
|
|
984
|
+
async run(args, options) {
|
|
985
|
+
const usage = "Usage: funnels draft --file <map.json> [--note <why>] [--evidence '<json>' | @file.json] [--based-on <n>]";
|
|
986
|
+
if (args.length > 0 || !options.file)
|
|
987
|
+
throw new CliError(usage, exitCodes.usage);
|
|
988
|
+
const map = await parseJson(`@${options.file}`, "--file");
|
|
989
|
+
const evidence = options.evidence === undefined ? undefined : await parseJson(options.evidence, "--evidence");
|
|
990
|
+
const connection = await resolveConnection(options.apiUrl);
|
|
991
|
+
const basedOn = options.basedOn === undefined ? null : wholeNumber(options.basedOn, "--based-on");
|
|
992
|
+
const saved = await api(connection, `${FUNNELS_PATH}/draft`, { map, note: options.note, evidence, based_on: basedOn }, "PUT");
|
|
993
|
+
if (options.format === "json")
|
|
994
|
+
return printJson(saved);
|
|
995
|
+
const based = basedOn === null ? "" : `, based on version ${basedOn}`;
|
|
996
|
+
console.log(saved.applied
|
|
997
|
+
? `Map version ${saved.version.version} is live${based}: it counts from now. A person can undo it in the app.`
|
|
998
|
+
: `Saved version ${saved.version.version}${based}. It is on hold: it counts nothing until every step can be counted.`);
|
|
999
|
+
if (saved.warnings.length > 0)
|
|
1000
|
+
console.log(`\n${saved.applied ? "Not in synced data yet" : "To fill in"}:\n${saved.warnings.map((w) => `- ${w.path}: ${w.message}`).join("\n")}`);
|
|
1001
|
+
if (saved.weak.length > 0)
|
|
1002
|
+
console.log(`\nNot backed up by synced data:\n${saved.weak.map((w) => `- ${w.path}: ${w.reason}`).join("\n")}`);
|
|
1003
|
+
},
|
|
1004
|
+
},
|
|
1005
|
+
"funnels versions": {
|
|
1006
|
+
usage: "funnels versions [--diff <a> <b>] every version of the map, or what changed from version a to b",
|
|
1007
|
+
async run(args, options) {
|
|
1008
|
+
const connection = await resolveConnection(options.apiUrl);
|
|
1009
|
+
if (options.diff !== undefined) {
|
|
1010
|
+
if (args.length !== 1)
|
|
1011
|
+
throw new CliError("Usage: funnels versions --diff <a> <b>", exitCodes.usage);
|
|
1012
|
+
const pair = [options.diff, args[0]].map((v) => wholeNumber(v, "--diff")).join(",");
|
|
1013
|
+
const diff = await api(connection, `${FUNNELS_PATH}/versions?diff=${pair}`);
|
|
1014
|
+
if (options.format === "json")
|
|
1015
|
+
return printJson(diff);
|
|
1016
|
+
process.stdout.write(diff.text);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
if (args.length > 0)
|
|
1020
|
+
throw new CliError("Usage: funnels versions [--diff <a> <b>]", exitCodes.usage);
|
|
1021
|
+
const { versions } = await api(connection, `${FUNNELS_PATH}/versions`);
|
|
1022
|
+
if (options.format === "json")
|
|
1023
|
+
return printJson({ versions });
|
|
1024
|
+
if (versions.length === 0)
|
|
1025
|
+
return console.log("No map versions yet.");
|
|
1026
|
+
const rows = versions.map((v) => ({
|
|
1027
|
+
version: v.version,
|
|
1028
|
+
status: v.status,
|
|
1029
|
+
"created by": v.createdBy,
|
|
1030
|
+
"created at": v.createdAt,
|
|
1031
|
+
"confirmed by": v.confirmedBy,
|
|
1032
|
+
"confirmed at": v.confirmedAt,
|
|
1033
|
+
note: v.note && excerpt(v.note),
|
|
1034
|
+
}));
|
|
1035
|
+
console.log(renderTable(["version", "status", "created by", "created at", "confirmed by", "confirmed at", "note"], rows));
|
|
1036
|
+
},
|
|
1037
|
+
},
|
|
1038
|
+
facts: {
|
|
1039
|
+
usage: "facts [--kind <kind>] [--subject <type>[:<ref>]] [--status <status>] [--json] settled facts about this workspace's data, to treat as true",
|
|
1040
|
+
async run(args, options) {
|
|
1041
|
+
if (args.length > 0)
|
|
1042
|
+
throw new CliError("Usage: facts [--kind <kind>] [--subject <type>[:<ref>]] [--status <status>] [--json]", exitCodes.usage);
|
|
1043
|
+
const query = new URLSearchParams();
|
|
1044
|
+
if (options.kind)
|
|
1045
|
+
query.set("kind", options.kind);
|
|
1046
|
+
if (options.subject) {
|
|
1047
|
+
const colon = options.subject.indexOf(":");
|
|
1048
|
+
query.set("subject_type", colon === -1 ? options.subject : options.subject.slice(0, colon));
|
|
1049
|
+
if (colon !== -1 && colon < options.subject.length - 1)
|
|
1050
|
+
query.set("subject_ref", options.subject.slice(colon + 1));
|
|
1051
|
+
}
|
|
1052
|
+
if (options.status)
|
|
1053
|
+
query.set("status", options.status);
|
|
1054
|
+
const { facts } = await api(await resolveConnection(options.apiUrl), `${FACTS_PATH}${query.size ? `?${query}` : ""}`);
|
|
1055
|
+
if (options.format === "json")
|
|
1056
|
+
return printJson({ facts });
|
|
1057
|
+
if (facts.length === 0)
|
|
1058
|
+
return console.log("No facts.");
|
|
1059
|
+
console.log(renderTable(FACT_COLUMNS, facts.map(factRow)));
|
|
1060
|
+
},
|
|
1061
|
+
},
|
|
1062
|
+
"facts show": {
|
|
1063
|
+
usage: "facts show <id> one fact with its value and evidence",
|
|
1064
|
+
async run(args, options) {
|
|
1065
|
+
if (args.length !== 1)
|
|
1066
|
+
throw new CliError("Usage: facts show <id>", exitCodes.usage);
|
|
1067
|
+
const { fact } = await api(await resolveConnection(options.apiUrl), `${FACTS_PATH}/${encodeURIComponent(args[0])}`);
|
|
1068
|
+
if (options.format === "json")
|
|
1069
|
+
return printJson({ fact });
|
|
1070
|
+
printFields({ ...factRow(fact), summary: fact.summary, value: fact.value, evidence: fact.evidence, author: fact.author, "confirmed by": fact.confirmedBy, replaces: fact.replaces });
|
|
1071
|
+
},
|
|
1072
|
+
},
|
|
1073
|
+
"facts propose": {
|
|
1074
|
+
usage: "facts propose --file <fact.json> | < fact.json suggest a fact; a person confirms it in the app",
|
|
1075
|
+
async run(args, options) {
|
|
1076
|
+
if (args.length > 0)
|
|
1077
|
+
throw new CliError("Usage: facts propose --file <fact.json> | < fact.json", exitCodes.usage);
|
|
1078
|
+
const { fact } = await api(await resolveConnection(options.apiUrl), FACTS_PATH, await readToolInput(options));
|
|
1079
|
+
if (options.format === "json")
|
|
1080
|
+
return printJson({ fact });
|
|
1081
|
+
console.log(`Proposed fact ${fact.id}. A person confirms or rejects it in the app.`);
|
|
1082
|
+
},
|
|
1083
|
+
},
|
|
921
1084
|
"plans create": {
|
|
922
|
-
usage: "plans create --name <name> --goal <text> --channel meta|ghl|klaviyo|web [--target '<json>' | @file.json] [--landing <sku | item-id> | --landing-url <url>] [--run <agent-run-id>]",
|
|
1085
|
+
usage: "plans create --name <name> --goal <text> --channel meta|ghl|klaviyo|web [--target '<json>' | @file.json] [--landing <sku | item-id> | --landing-url <url>] [--funnel <key> --step <key>] [--run <agent-run-id>]",
|
|
923
1086
|
async run(args, options) {
|
|
924
1087
|
if (args.length > 0 || !options.name || !options.goal || !options.channel) {
|
|
925
|
-
throw new CliError("Usage: plans create --name <name> --goal <text> --channel meta|ghl|klaviyo|web [--target '<json>'] [--landing <sku> | --landing-url <url>]", exitCodes.usage);
|
|
1088
|
+
throw new CliError("Usage: plans create --name <name> --goal <text> --channel meta|ghl|klaviyo|web [--target '<json>'] [--landing <sku> | --landing-url <url>] [--funnel <key> --step <key>]", exitCodes.usage);
|
|
926
1089
|
}
|
|
927
1090
|
const { plan } = await api(await resolveConnection(options.apiUrl), "/api/v1/launch-plans", {
|
|
928
1091
|
name: options.name,
|
|
@@ -931,6 +1094,8 @@ const commands = {
|
|
|
931
1094
|
...(options.target === undefined ? {} : { target: await parseJson(options.target, "--target") }),
|
|
932
1095
|
...(options.landing === undefined ? {} : { landingItem: options.landing }),
|
|
933
1096
|
...(options.landingUrl === undefined ? {} : { landingUrl: options.landingUrl }),
|
|
1097
|
+
...(options.funnel === undefined ? {} : { funnelKey: options.funnel }),
|
|
1098
|
+
...(options.step === undefined ? {} : { stepKey: options.step }),
|
|
934
1099
|
...(options.run === undefined ? {} : { runId: options.run }),
|
|
935
1100
|
});
|
|
936
1101
|
printPlan(plan, options.format);
|
|
@@ -1054,9 +1219,9 @@ const commands = {
|
|
|
1054
1219
|
},
|
|
1055
1220
|
},
|
|
1056
1221
|
"skill install": {
|
|
1057
|
-
usage:
|
|
1222
|
+
usage: `skill install [${PLAYBOOK_CHOICES}|<name>[@<version>]] [--project] [--codex]`,
|
|
1058
1223
|
async run(args, options) {
|
|
1059
|
-
const usage =
|
|
1224
|
+
const usage = `Usage: skill install [${PLAYBOOK_CHOICES}|<name>[@<version>]] [--project] [--codex]`;
|
|
1060
1225
|
const [which] = args;
|
|
1061
1226
|
if (args.length > 1)
|
|
1062
1227
|
throw new CliError(usage, exitCodes.usage);
|
|
@@ -1067,15 +1232,15 @@ const commands = {
|
|
|
1067
1232
|
if (which === "meta-ads")
|
|
1068
1233
|
return installAgentsMd(join(process.cwd(), "AGENTS.md"));
|
|
1069
1234
|
if (!playbook)
|
|
1070
|
-
throw new CliError(
|
|
1235
|
+
throw new CliError(`--codex installs a playbook: skill install ${PLAYBOOK_CHOICES} --codex`, exitCodes.usage);
|
|
1071
1236
|
if (options.project)
|
|
1072
1237
|
throw new CliError(`--codex installs ${playbook.name} for every Codex run, under CODEX_HOME; drop --project`, exitCodes.usage);
|
|
1073
|
-
const path = await writeSkill(codexSkillsDir(), playbook.name, playbook.skill, []);
|
|
1238
|
+
const path = await writeSkill(codexSkillsDir(), playbook.name, playbook.skill, playbook.files, []);
|
|
1074
1239
|
return console.log(`Installed the ${playbook.name} skill for Codex to ${path}.`);
|
|
1075
1240
|
}
|
|
1076
|
-
const [name, content] = playbook ? [playbook.name, playbook.skill] : [brand.slug, skill];
|
|
1241
|
+
const [name, content, files] = playbook ? [playbook.name, playbook.skill, playbook.files] : [brand.slug, skill, []];
|
|
1077
1242
|
const { skillsDir, above } = claudeSkillsDir(options.project);
|
|
1078
|
-
const path = await writeSkill(skillsDir, name, content, above);
|
|
1243
|
+
const path = await writeSkill(skillsDir, name, content, files, above);
|
|
1079
1244
|
console.log(`Installed the ${name} skill to ${path}.`);
|
|
1080
1245
|
},
|
|
1081
1246
|
},
|
|
@@ -1088,7 +1253,7 @@ const commands = {
|
|
|
1088
1253
|
if (options.format === "json")
|
|
1089
1254
|
return printJson({ workers });
|
|
1090
1255
|
if (workers.length === 0)
|
|
1091
|
-
return console.log("No
|
|
1256
|
+
return console.log("No scheduled hands yet.");
|
|
1092
1257
|
const rows = workers.map((w) => ({
|
|
1093
1258
|
id: w.id,
|
|
1094
1259
|
skill: w.skill,
|
|
@@ -1104,10 +1269,10 @@ const commands = {
|
|
|
1104
1269
|
},
|
|
1105
1270
|
},
|
|
1106
1271
|
"worker runs": {
|
|
1107
|
-
usage: "worker runs <
|
|
1272
|
+
usage: "worker runs <hand-id | skill> [--limit <n>]",
|
|
1108
1273
|
async run(args, options) {
|
|
1109
1274
|
if (args.length !== 1)
|
|
1110
|
-
throw new CliError("Usage: worker runs <
|
|
1275
|
+
throw new CliError("Usage: worker runs <hand-id | skill> [--limit <n>]", exitCodes.usage);
|
|
1111
1276
|
const connection = await resolveConnection(options.apiUrl);
|
|
1112
1277
|
const id = await workerId(connection, args[0]);
|
|
1113
1278
|
const query = options.limit === undefined ? "" : `?limit=${encodeURIComponent(options.limit)}`;
|
|
@@ -1120,10 +1285,10 @@ const commands = {
|
|
|
1120
1285
|
},
|
|
1121
1286
|
},
|
|
1122
1287
|
"worker run-now": {
|
|
1123
|
-
usage: "worker run-now <
|
|
1288
|
+
usage: "worker run-now <hand-id | skill>",
|
|
1124
1289
|
async run(args, options) {
|
|
1125
1290
|
if (args.length !== 1)
|
|
1126
|
-
throw new CliError("Usage: worker run-now <
|
|
1291
|
+
throw new CliError("Usage: worker run-now <hand-id | skill>", exitCodes.usage);
|
|
1127
1292
|
const connection = await resolveConnection(options.apiUrl);
|
|
1128
1293
|
const id = await workerId(connection, args[0]);
|
|
1129
1294
|
const queued = await api(connection, `/api/v1/workers/${encodeURIComponent(id)}/run-now`, {});
|
|
@@ -1140,7 +1305,7 @@ const commands = {
|
|
|
1140
1305
|
const { promoted } = await changeCandidate("promote", args, options);
|
|
1141
1306
|
if (options.format === "json")
|
|
1142
1307
|
return printJson({ promoted });
|
|
1143
|
-
console.log(`${promoted.skill} version ${promoted.version} is now in use on ${promoted.moved}
|
|
1308
|
+
console.log(`${promoted.skill} version ${promoted.version} is now in use on ${promoted.moved} hand${promoted.moved === 1 ? "" : "s"}.`);
|
|
1144
1309
|
},
|
|
1145
1310
|
},
|
|
1146
1311
|
"worker drop": {
|
|
@@ -1175,11 +1340,11 @@ const commands = {
|
|
|
1175
1340
|
},
|
|
1176
1341
|
},
|
|
1177
1342
|
docs: {
|
|
1178
|
-
usage:
|
|
1343
|
+
usage: `docs [${PLAYBOOK_CHOICES}]`,
|
|
1179
1344
|
async run(args) {
|
|
1180
1345
|
const [which] = args;
|
|
1181
1346
|
if (args.length > 1 || (which !== undefined && !isPlaybook(which)))
|
|
1182
|
-
throw new CliError(
|
|
1347
|
+
throw new CliError(`Usage: docs [${PLAYBOOK_CHOICES}]`, exitCodes.usage);
|
|
1183
1348
|
process.stdout.write(which === undefined ? guide : playbooks[which].text);
|
|
1184
1349
|
},
|
|
1185
1350
|
},
|
|
@@ -1208,10 +1373,6 @@ async function withRunner(options, work) {
|
|
|
1208
1373
|
process.off("SIGTERM", onSignal);
|
|
1209
1374
|
}
|
|
1210
1375
|
}
|
|
1211
|
-
const playbooks = {
|
|
1212
|
-
"meta-ads": { name: META_ADS_SKILL_NAME, skill: metaAdsSkill, text: metaAdsPlaybook },
|
|
1213
|
-
"p3-refresh": { name: P3_REFRESH_SKILL_NAME, skill: p3RefreshSkill, text: p3RefreshPlaybook },
|
|
1214
|
-
};
|
|
1215
1376
|
const isPlaybook = (name) => Object.hasOwn(playbooks, name);
|
|
1216
1377
|
const codexSkillsDir = () => join(process.env.CODEX_HOME || join(homedir(), ".codex"), "skills");
|
|
1217
1378
|
/** Claude Code's skills folder, in the user's home or (with --project) the current folder. */
|
|
@@ -1232,15 +1393,20 @@ async function installLibrarySkill(ref, options) {
|
|
|
1232
1393
|
const path = await installSkillFolder(skillsDir, download.name, files, above);
|
|
1233
1394
|
console.log(`installed ${download.name} version ${download.version} to ${path}`);
|
|
1234
1395
|
}
|
|
1235
|
-
/**
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1396
|
+
/**
|
|
1397
|
+
* Writes <skillsDir>/<name>/SKILL.md and any files beside it, refusing to follow a symlink at any of `above` or the
|
|
1398
|
+
* skill's own paths. Returns the SKILL.md path.
|
|
1399
|
+
*/
|
|
1400
|
+
async function writeSkill(skillsDir, name, content, files, above) {
|
|
1401
|
+
const written = [{ name: "SKILL.md", content }, ...files];
|
|
1402
|
+
const paths = await Promise.all(written.map((f) => inRunFolder(join(skillsDir, name, f.name))));
|
|
1403
|
+
const linked = await firstSymlink([...above, join(skillsDir, name), ...paths]);
|
|
1239
1404
|
if (linked)
|
|
1240
1405
|
throw new CliError(`Refusing to write through the symlink at ${linked}. Remove it and try again.`, exitCodes.error);
|
|
1241
|
-
await mkdir(
|
|
1242
|
-
|
|
1243
|
-
|
|
1406
|
+
await mkdir(join(skillsDir, name), { recursive: true });
|
|
1407
|
+
for (const [i, file] of written.entries())
|
|
1408
|
+
await writeFileAtomic(paths[i], file.content, 0o644);
|
|
1409
|
+
return paths[0];
|
|
1244
1410
|
}
|
|
1245
1411
|
const SOURCE_RUNS_SHOWN = 10;
|
|
1246
1412
|
/** What the source is retrying: "the whole sync since <time>", and each failing part as "<part> since <time>". */
|
|
@@ -1290,6 +1456,11 @@ function printReport(title, report, labels, numbers) {
|
|
|
1290
1456
|
console.log(renderTable(["currency", ...numbers], report.totals));
|
|
1291
1457
|
}
|
|
1292
1458
|
const agentPath = (options) => `/api/v1/agents/${encodeURIComponent(options.agent)}`;
|
|
1459
|
+
const FACTS_PATH = "/api/v1/facts";
|
|
1460
|
+
const FACT_COLUMNS = ["id", "kind", "subject", "key", "status", "summary"];
|
|
1461
|
+
function factRow(f) {
|
|
1462
|
+
return { id: f.id, kind: f.kind, subject: `${f.subjectType}:${f.subjectRef}`, key: f.key, status: f.status, summary: excerpt(f.summary, 80) };
|
|
1463
|
+
}
|
|
1293
1464
|
const PROPOSAL_COLUMNS = ["id", "kind", "status", "target", "change", "reason"];
|
|
1294
1465
|
function proposalChange(p) {
|
|
1295
1466
|
if (p.kind === "creative")
|
|
@@ -1331,7 +1502,7 @@ Options:
|
|
|
1331
1502
|
--format table|json output format (default: table)
|
|
1332
1503
|
--api-url <url> API base URL
|
|
1333
1504
|
--wait <seconds> how long tools run and tools get wait for a run (default: ${DEFAULT_WAIT_SECONDS}; 0 returns at once)
|
|
1334
|
-
--agent <kind> which agent the agent
|
|
1505
|
+
--agent <kind> which agent the agent commands use: meta_ads (default) or p3_refresh; in a scheduled hand's run, skill
|
|
1335
1506
|
(the run's skill's agent) is the default
|
|
1336
1507
|
--kind <kind> what sends to a new intake address: generic (default), ghl_workflow, aloware, webinarfuel, stealth_seminar or mailgun
|
|
1337
1508
|
--retention-days <n> how many days intake create keeps each event (default: 90)
|
|
@@ -1340,18 +1511,21 @@ Options:
|
|
|
1340
1511
|
for warehouse schema, a table to describe in full; repeat for several
|
|
1341
1512
|
--summary warehouse schema: one short row per table (kind, source, column count, summary)
|
|
1342
1513
|
--search <words> warehouse schema: describe in full the tables whose name, description or columns mention a word
|
|
1343
|
-
--out <path> the file, or existing folder, creatives download
|
|
1514
|
+
--out <path> the file, or existing folder, creatives download and warehouse query save to; warehouse query
|
|
1515
|
+
writes CSV for a .csv path, otherwise JSON
|
|
1344
1516
|
--open open the file creatives download saved
|
|
1345
1517
|
--media with content pull, also save each item's files and HTML under media/ in the folder
|
|
1346
1518
|
--full with content pull, fetch every item, not only what changed since the last pull (brings new numbers)
|
|
1347
1519
|
--force content unpublish: take the page down even while delivering Meta ads link to it
|
|
1520
|
+
--plan <json> meta adset publish: the ad set to make, {"kind": "copy", ...} or {"kind": "create", ...}
|
|
1348
1521
|
--yes confirm intake delete or import remove, which delete saved data for good
|
|
1349
|
-
--from <YYYY-MM-DD> first day funnel, campaigns and
|
|
1350
|
-
--to <YYYY-MM-DD> last day funnel, campaigns and
|
|
1522
|
+
--from <YYYY-MM-DD> first day funnel, campaigns, money and brief cover (default: 29 days before --to)
|
|
1523
|
+
--to <YYYY-MM-DD> last day funnel, campaigns, money and brief cover (default: today in the workspace reporting time zone)
|
|
1351
1524
|
--grain <grain> how funnel and money group days: day (default), week (starting Monday) or month
|
|
1352
|
-
--channel <channel> only this channel's campaigns: meta, google, affiliate, a utm_source, or
|
|
1525
|
+
--channel <channel> only this channel's campaigns (and, with brief, its ads): meta, google, affiliate, a utm_source, unknown or recorded_in_crm;
|
|
1353
1526
|
for content track-link, where the link goes: facebook, email, sms or web (default)
|
|
1354
|
-
--campaign <id> the campaign content track-link puts in utm_campaign
|
|
1527
|
+
--campaign <id> the campaign content track-link puts in utm_campaign; with brief, the one campaign it covers
|
|
1528
|
+
--top <n> how many best ads, worst ads, pages and campaigns brief shows (default: 5, at most 20)
|
|
1355
1529
|
--body, --html <text | @file> the words, or the full HTML, content create, capture and edit save
|
|
1356
1530
|
--file <path> an image, video or audio file, up to 50 MB, content create, capture and edit save; repeat for several
|
|
1357
1531
|
--transcript <text | @file> what is said in the first video or audio file content capture saves, so none is paid for
|
|
@@ -1362,6 +1536,14 @@ Options:
|
|
|
1362
1536
|
--run <id> the agent run making the content (content), or the tool run to record (creatives create)
|
|
1363
1537
|
--platform, --external-id where content status launched says it was published, and that platform's id for it
|
|
1364
1538
|
--notes <text> what changed in the version skill publish adds
|
|
1539
|
+
--version <n> the map version funnels show prints (default: the confirmed one)
|
|
1540
|
+
--based-on <n> the map version funnels draft was written from; refused when someone saved since
|
|
1541
|
+
(leave it out only for the first map, when there is neither a map nor a draft)
|
|
1542
|
+
--evidence <json | @file.json> what funnels draft was written from: counts and ids, never names, emails or phones
|
|
1543
|
+
--diff <a> <b> with funnels versions, what changed from version a to version b
|
|
1544
|
+
--funnel <key> only this funnel's steps in funnels numbers (default: every funnel in the confirmed map);
|
|
1545
|
+
with plans create and --step, the funnel whose step the push feeds
|
|
1546
|
+
--step <key> with plans create, the step of --funnel the push feeds, as on the confirmed map
|
|
1365
1547
|
--harness <list> which agents skill publish marks the skill for: claude, codex, or claude,codex (default)
|
|
1366
1548
|
--text <words> words content search finds in a subject, headline, body or video transcript
|
|
1367
1549
|
--mode word|meaning how content search matches --text: word (default, the words themselves) or meaning (close in meaning, nearest first; no --sort)
|
|
@@ -1372,13 +1554,13 @@ Options:
|
|
|
1372
1554
|
--order desc|asc content search's sort direction (default: desc, best or newest first)
|
|
1373
1555
|
--limit <n> how many results content search, creatives list and campaigns return
|
|
1374
1556
|
--max-seconds <n> stop a runner's run after this many seconds, if that comes before its deadline
|
|
1375
|
-
--workspace <slug>
|
|
1557
|
+
--workspace <slug> for worker promote and worker drop: the workspace whose hand they change
|
|
1376
1558
|
-h, --help show this help
|
|
1377
1559
|
|
|
1378
1560
|
skill publish, skill list and skill install <name> use the skill library, which takes the operator token in
|
|
1379
1561
|
${envPrefix}_OPERATOR_TOKEN instead of an API key, as do worker promote and worker drop. worker promote makes the
|
|
1380
|
-
version a
|
|
1381
|
-
stops trying it.
|
|
1562
|
+
version a scheduled hand is trying the one in use, there and on every hand of the skill on an older version; worker
|
|
1563
|
+
drop stops trying it.
|
|
1382
1564
|
|
|
1383
1565
|
runner and runner once take the runner token in ${RUNNER_TOKEN_ENV}. ${HARNESS_COMMAND_ENV}, if set, names a program
|
|
1384
1566
|
the runner starts instead of Claude Code or Codex.
|
|
@@ -1391,6 +1573,7 @@ function parseOptions(argv) {
|
|
|
1391
1573
|
allowPositionals: true,
|
|
1392
1574
|
options: {
|
|
1393
1575
|
format: { type: "string", default: "table" },
|
|
1576
|
+
json: { type: "boolean", default: false },
|
|
1394
1577
|
"api-url": { type: "string" },
|
|
1395
1578
|
file: { type: "string", multiple: true },
|
|
1396
1579
|
project: { type: "boolean", default: false },
|
|
@@ -1421,6 +1604,7 @@ function parseOptions(argv) {
|
|
|
1421
1604
|
grain: { type: "string" },
|
|
1422
1605
|
channel: { type: "string" },
|
|
1423
1606
|
campaign: { type: "string" },
|
|
1607
|
+
top: { type: "string" },
|
|
1424
1608
|
name: { type: "string" },
|
|
1425
1609
|
subject: { type: "string" },
|
|
1426
1610
|
preview: { type: "string" },
|
|
@@ -1451,6 +1635,11 @@ function parseOptions(argv) {
|
|
|
1451
1635
|
position: { type: "string" },
|
|
1452
1636
|
plan: { type: "string" },
|
|
1453
1637
|
version: { type: "string" },
|
|
1638
|
+
"based-on": { type: "string" },
|
|
1639
|
+
evidence: { type: "string" },
|
|
1640
|
+
diff: { type: "string" },
|
|
1641
|
+
funnel: { type: "string" },
|
|
1642
|
+
step: { type: "string" },
|
|
1454
1643
|
workspace: { type: "string" },
|
|
1455
1644
|
"max-seconds": { type: "string" },
|
|
1456
1645
|
help: { type: "boolean", short: "h", default: false },
|
|
@@ -1470,7 +1659,7 @@ function parseOptions(argv) {
|
|
|
1470
1659
|
positionals,
|
|
1471
1660
|
help: values.help,
|
|
1472
1661
|
options: {
|
|
1473
|
-
format: values.format,
|
|
1662
|
+
format: values.json ? "json" : values.format,
|
|
1474
1663
|
apiUrl: values["api-url"],
|
|
1475
1664
|
file: values.file?.at(-1),
|
|
1476
1665
|
files: values.file ?? [],
|
|
@@ -1503,6 +1692,7 @@ function parseOptions(argv) {
|
|
|
1503
1692
|
grain: values.grain,
|
|
1504
1693
|
channel: values.channel,
|
|
1505
1694
|
campaign: values.campaign,
|
|
1695
|
+
top: values.top,
|
|
1506
1696
|
name: values.name,
|
|
1507
1697
|
subject: values.subject,
|
|
1508
1698
|
preview: values.preview,
|
|
@@ -1533,18 +1723,26 @@ function parseOptions(argv) {
|
|
|
1533
1723
|
position: values.position,
|
|
1534
1724
|
plan: values.plan,
|
|
1535
1725
|
version: values.version,
|
|
1726
|
+
basedOn: values["based-on"],
|
|
1727
|
+
evidence: values.evidence,
|
|
1728
|
+
diff: values.diff,
|
|
1729
|
+
funnel: values.funnel,
|
|
1730
|
+
step: values.step,
|
|
1536
1731
|
workspace: values.workspace,
|
|
1537
1732
|
maxSeconds,
|
|
1538
1733
|
},
|
|
1539
1734
|
};
|
|
1540
1735
|
}
|
|
1541
|
-
/** Promotes or drops the candidate version of a workspace's
|
|
1736
|
+
/** Promotes or drops the candidate version of a workspace's scheduled hand for a skill, with the operator token. */
|
|
1542
1737
|
async function changeCandidate(change, args, options) {
|
|
1543
1738
|
if (args.length !== 1 || !options.workspace)
|
|
1544
1739
|
throw new CliError(`Usage: worker ${change} <skill> --workspace <slug>`, exitCodes.usage);
|
|
1545
1740
|
return api(await operatorConnection(options.apiUrl), `/api/operator/workers/${change}`, { workspace: options.workspace, skill: args[0] });
|
|
1546
1741
|
}
|
|
1547
1742
|
function findCommand(positionals) {
|
|
1743
|
+
const three = positionals.slice(0, 3).join(" ");
|
|
1744
|
+
if (Object.hasOwn(commands, three))
|
|
1745
|
+
return { command: commands[three], args: positionals.slice(3) };
|
|
1548
1746
|
const two = positionals.slice(0, 2).join(" ");
|
|
1549
1747
|
if (Object.hasOwn(commands, two))
|
|
1550
1748
|
return { command: commands[two], args: positionals.slice(2) };
|
|
@@ -1740,6 +1938,14 @@ async function uploadContentFile(connection, data, contentType) {
|
|
|
1740
1938
|
await putSigned(upload.url, data, contentType);
|
|
1741
1939
|
return upload.uploadId;
|
|
1742
1940
|
}
|
|
1941
|
+
/** What the CLI says after a link: that its platform no longer lists it, or that it was edited there. */
|
|
1942
|
+
function linkNote(link) {
|
|
1943
|
+
if (link.removedAt)
|
|
1944
|
+
return ` (no longer on ${link.platform})`;
|
|
1945
|
+
if (link.platformChangedAt)
|
|
1946
|
+
return ` (changed on ${link.platform} since it was published)`;
|
|
1947
|
+
return "";
|
|
1948
|
+
}
|
|
1743
1949
|
function printContent(content, format) {
|
|
1744
1950
|
if (format === "json")
|
|
1745
1951
|
return printJson(content);
|
|
@@ -1758,7 +1964,7 @@ function printContent(content, format) {
|
|
|
1758
1964
|
html: version.htmlUrl,
|
|
1759
1965
|
files: version.media.length ? version.media.map((m) => m.url).join(" ") : null,
|
|
1760
1966
|
links: item.links.length
|
|
1761
|
-
? item.links.map((l) => `${l.platform} ${l.objectType} ${l.externalId}${
|
|
1967
|
+
? item.links.map((l) => `${l.platform} ${l.objectType} ${l.externalId}${linkNote(l)}`).join(", ")
|
|
1762
1968
|
: null,
|
|
1763
1969
|
by: version.capturedBy,
|
|
1764
1970
|
note: version.note,
|
|
@@ -1769,10 +1975,20 @@ function printContent(content, format) {
|
|
|
1769
1975
|
deployments: content.deployments?.length ? content.deployments.map(deploymentLine).join(", ") : null,
|
|
1770
1976
|
versions: content.versions && content.versions.length > 1 ? content.versions.length : null,
|
|
1771
1977
|
transcript: content.transcripts?.map((t) => (t.text ? excerpt(t.text, 80) : t.status)).join(" | ") || null,
|
|
1978
|
+
description: descriptionLine(content.descriptions?.find((d) => d.version === version.version)),
|
|
1772
1979
|
...scoreFields(content.scorecards ?? []),
|
|
1773
1980
|
};
|
|
1774
1981
|
printFields(rows);
|
|
1775
1982
|
}
|
|
1983
|
+
/** The current version's picture description in one line: its kind, and the words printed on it. */
|
|
1984
|
+
function descriptionLine(d) {
|
|
1985
|
+
if (!d)
|
|
1986
|
+
return null;
|
|
1987
|
+
if (!d.description)
|
|
1988
|
+
return d.status;
|
|
1989
|
+
const words = d.description.on_image_text.trim();
|
|
1990
|
+
return words ? `${d.description.visual_kind}: "${excerpt(words, 80)}"` : d.description.visual_kind;
|
|
1991
|
+
}
|
|
1776
1992
|
function printContentDiff(diff) {
|
|
1777
1993
|
const stamp = (sign, v) => `${sign} version ${v.version}, ${v.capturedAt} by ${v.capturedBy}${v.note ? `: ${v.note}` : ""}`;
|
|
1778
1994
|
const value = (v) => (typeof v === "string" ? v : JSON.stringify(v));
|
|
@@ -1794,6 +2010,10 @@ function printContentDiff(diff) {
|
|
|
1794
2010
|
lines.push("", "No changes.");
|
|
1795
2011
|
console.log(lines.join("\n"));
|
|
1796
2012
|
}
|
|
2013
|
+
function feedsText(funnel) {
|
|
2014
|
+
const sells = funnel.sellsOffer === null ? "" : `, which sells ${funnel.sellsOffer.name}`;
|
|
2015
|
+
return `step ${funnel.step.position} (${funnel.step.name}) of ${funnel.name}${sells}`;
|
|
2016
|
+
}
|
|
1797
2017
|
const planPath = (id) => `/api/v1/launch-plans/${encodeURIComponent(id)}`;
|
|
1798
2018
|
function printPlan(plan, format) {
|
|
1799
2019
|
if (format === "json")
|
|
@@ -1808,6 +2028,9 @@ function printPlan(plan, format) {
|
|
|
1808
2028
|
status: plan.status,
|
|
1809
2029
|
next: plan.moves.length ? plan.moves.join(", ") : null,
|
|
1810
2030
|
by: plan.createdBy,
|
|
2031
|
+
feeds: plan.funnel && feedsText(plan.funnel),
|
|
2032
|
+
"funnel note": plan.funnelNote,
|
|
2033
|
+
"funnel steps": plan.funnel?.steps.map((s) => `${s.position}. ${s.name}`).join(", "),
|
|
1811
2034
|
});
|
|
1812
2035
|
console.log("");
|
|
1813
2036
|
if (plan.items.length === 0)
|
|
@@ -1816,12 +2039,12 @@ function printPlan(plan, format) {
|
|
|
1816
2039
|
}
|
|
1817
2040
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1818
2041
|
const dollars = (cents) => `$${(cents / 100).toFixed(2)}`;
|
|
1819
|
-
/** The workspace's
|
|
2042
|
+
/** The workspace's scheduled hand for the skill named `ref`, else the one whose id it is: a skill's name may look like a uuid. */
|
|
1820
2043
|
async function workerId(connection, ref) {
|
|
1821
2044
|
const { workers } = await api(connection, "/api/v1/workers");
|
|
1822
2045
|
const found = workers.find((w) => w.skill === ref) ?? workers.find((w) => w.id === ref.toLowerCase());
|
|
1823
2046
|
if (!found)
|
|
1824
|
-
throw new CliError(`unknown_worker: no
|
|
2047
|
+
throw new CliError(`unknown_worker: no scheduled hand with skill or id ${JSON.stringify(ref)} in this workspace`, exitCodes.error);
|
|
1825
2048
|
return found.id;
|
|
1826
2049
|
}
|
|
1827
2050
|
function workerRunRow(r) {
|
|
@@ -1921,6 +2144,27 @@ function printPublication(publication, format) {
|
|
|
1921
2144
|
finished: publication.finishedAt,
|
|
1922
2145
|
});
|
|
1923
2146
|
}
|
|
2147
|
+
function printAdSetPublication(publication, format) {
|
|
2148
|
+
if (format === "json")
|
|
2149
|
+
return printJson({ adsetPublication: publication });
|
|
2150
|
+
const { verification, budget } = publication;
|
|
2151
|
+
printFields({
|
|
2152
|
+
"adset publication": publication.id,
|
|
2153
|
+
kind: publication.kind,
|
|
2154
|
+
account: publication.accountId,
|
|
2155
|
+
campaign: publication.campaignId,
|
|
2156
|
+
source: publication.sourceAdsetId,
|
|
2157
|
+
name: publication.name,
|
|
2158
|
+
"external id": publication.externalId,
|
|
2159
|
+
status: publication.status,
|
|
2160
|
+
error: publication.error,
|
|
2161
|
+
verified: verification && (verification.ok ? "yes" : `no: ${verification.problems.join("; ")}`),
|
|
2162
|
+
budget: budget && (budget.dailyBudget !== null ? `${budget.dailyBudget} a day` : budget.lifetimeBudget !== null ? `${budget.lifetimeBudget} lifetime` : "the campaign's"),
|
|
2163
|
+
recorded: publication.recordedAt,
|
|
2164
|
+
sent: publication.sentAt,
|
|
2165
|
+
finished: publication.finishedAt,
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
1924
2168
|
async function readToolInput(options) {
|
|
1925
2169
|
let text;
|
|
1926
2170
|
if (options.input !== undefined)
|
|
@@ -2047,6 +2291,53 @@ function decodeLinkPath(encoded) {
|
|
|
2047
2291
|
throw new CliError(`"${excerpt(encoded)}" is not a valid asset link.`, exitCodes.usage);
|
|
2048
2292
|
}
|
|
2049
2293
|
}
|
|
2294
|
+
async function saveFile(path, content) {
|
|
2295
|
+
await writeFileAtomic(path, content, 0o644).catch((error) => {
|
|
2296
|
+
if (error.code === "ENOENT")
|
|
2297
|
+
throw new CliError(`No folder at ${dirname(path)} to save into.`, exitCodes.usage);
|
|
2298
|
+
throw error;
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
/** Saves every row the server returned: CSV for a `.csv` path, otherwise the response body as it came. */
|
|
2302
|
+
async function saveQueryResult(text, out, format) {
|
|
2303
|
+
const path = await savePath(out, "results.json");
|
|
2304
|
+
const result = queryResultWithExactNumbers(text);
|
|
2305
|
+
await saveFile(path, extname(path).toLowerCase() === ".csv" ? toCsv(text) : text);
|
|
2306
|
+
if (format === "json")
|
|
2307
|
+
printJson({ path: resolve(path), rowCount: result.rows.length, truncated: result.truncated });
|
|
2308
|
+
else
|
|
2309
|
+
console.log(`Saved ${rowCount(result.rows.length)} to ${path}`);
|
|
2310
|
+
warnIfTruncated(result);
|
|
2311
|
+
}
|
|
2312
|
+
/** Top-level numbers keep the digits the server sent, so a numeric column is never rounded. */
|
|
2313
|
+
function queryResultWithExactNumbers(text) {
|
|
2314
|
+
const result = JSON.parse(text);
|
|
2315
|
+
const exact = JSON.parse(text, keepNumberDigits);
|
|
2316
|
+
const rows = result.rows.map((row, i) => Object.fromEntries(Object.entries(row).map(([k, v]) => [k, typeof v === "number" ? exact.rows[i][k] : v])));
|
|
2317
|
+
return { ...result, rows };
|
|
2318
|
+
}
|
|
2319
|
+
function rowCount(n) {
|
|
2320
|
+
return `${n.toLocaleString("en-US")} ${n === 1 ? "row" : "rows"}`;
|
|
2321
|
+
}
|
|
2322
|
+
function warnIfTruncated(result) {
|
|
2323
|
+
if (result.truncated)
|
|
2324
|
+
console.error(`Results were cut off (${result.truncated}). Aggregate in SQL or add LIMIT.`);
|
|
2325
|
+
}
|
|
2326
|
+
/** Every number keeps the digits the server sent, nested ones too, and json columns are written as JSON text. */
|
|
2327
|
+
function toCsv(text) {
|
|
2328
|
+
const result = JSON.parse(text, keepRawNumbers);
|
|
2329
|
+
const json = new Set(result.columns.filter((c) => c.type === "json" || c.type === "jsonb").map((c) => c.name));
|
|
2330
|
+
const field = (name, value) => (value != null && json.has(name) ? JSON.stringify(value) : cell(value));
|
|
2331
|
+
const names = result.columns.map((c) => c.name);
|
|
2332
|
+
const line = (values) => values.map(csvField).join(",");
|
|
2333
|
+
return [line(names), ...result.rows.map((row) => line(names.map((name) => field(name, row[name]))))].map((l) => `${l}\n`).join("");
|
|
2334
|
+
}
|
|
2335
|
+
function keepRawNumbers(_key, value, context) {
|
|
2336
|
+
return typeof value === "number" && context?.source !== undefined ? JSON.rawJSON(context.source) : value;
|
|
2337
|
+
}
|
|
2338
|
+
function csvField(value) {
|
|
2339
|
+
return /[",\r\n]/.test(value) ? `"${value.replaceAll('"', '""')}"` : value;
|
|
2340
|
+
}
|
|
2050
2341
|
/** `--out` names the file, or an existing folder to save into under the default name. */
|
|
2051
2342
|
async function savePath(out, fileName) {
|
|
2052
2343
|
if (out === undefined)
|
|
@@ -2074,6 +2365,13 @@ function openFile(path) {
|
|
|
2074
2365
|
child.unref();
|
|
2075
2366
|
}
|
|
2076
2367
|
const INTAKE_PATH = "/api/v1/intake-endpoints";
|
|
2368
|
+
const FUNNELS_PATH = "/api/v1/funnels";
|
|
2369
|
+
function wholeNumber(value, flag) {
|
|
2370
|
+
const n = Number(value);
|
|
2371
|
+
if (!Number.isInteger(n) || n < 1)
|
|
2372
|
+
throw new CliError(`${flag} must be a whole number from 1.`, exitCodes.usage);
|
|
2373
|
+
return n;
|
|
2374
|
+
}
|
|
2077
2375
|
const intakePath = (id) => `${INTAKE_PATH}/${encodeURIComponent(id)}`;
|
|
2078
2376
|
function parseRetentionDays(text) {
|
|
2079
2377
|
const days = Number(text);
|