@wrangled/cli 0.3.43 → 0.3.45
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 +140 -23
- package/dist/guide.js +9 -8
- package/dist/main.js +222 -143
- package/dist/playbooks.js +17 -17
- package/dist/runner/run.js +1 -1
- package/dist/skills.js +3 -8
- package/package.json +1 -1
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,
|
|
7
|
+
import { api, ApiError, apiDownload, apiText, apiUpload, CliError, exitCodes, getSigned, IMPORT_LIMITS, IMPORT_NAME_PATTERN, putSigned, CLIENT_FILE, parseClientName, readSavedLogins, removeCredentials, resolveApiUrl, resolveConnection, resolveLogin, 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";
|
|
@@ -19,41 +19,47 @@ const SERVER_WAIT_CAP = 50;
|
|
|
19
19
|
const VOICE_USAGE = "voice [--kind opener|objection|pain|desire|phrase] [--since <YYYY-MM-DD>] [--sort common|useful] [--limit <n>] [--json]";
|
|
20
20
|
const commands = {
|
|
21
21
|
login: {
|
|
22
|
-
usage: `login (paste the key when asked, pipe it on stdin, or set ${envPrefix}_API_KEY)`,
|
|
22
|
+
usage: `login [--client <name>] (paste the key when asked, pipe it on stdin, or set ${envPrefix}_API_KEY); --client saves it under that client's name`,
|
|
23
23
|
async run(args, options) {
|
|
24
24
|
if (args.length > 0) {
|
|
25
25
|
throw new CliError(`login takes no arguments. Pipe the key on stdin or set ${envPrefix}_API_KEY.`, exitCodes.usage);
|
|
26
26
|
}
|
|
27
|
+
const client = options.client === undefined ? undefined : parseClientName(options.client);
|
|
27
28
|
const apiKey = await readKeyFromUser();
|
|
28
|
-
const
|
|
29
|
+
const saved = await readSavedLogins().catch((error) => {
|
|
29
30
|
if (error instanceof UnreadableCredentialsError)
|
|
30
31
|
return null;
|
|
31
32
|
throw error;
|
|
32
33
|
});
|
|
33
|
-
const apiUrl = resolveApiUrl(options.apiUrl,
|
|
34
|
+
const apiUrl = resolveApiUrl(options.apiUrl, (client === undefined ? undefined : saved?.clients[client]) ?? saved?.default ?? null);
|
|
34
35
|
const me = await api({ apiUrl, apiKey }, "/api/v1/me");
|
|
35
|
-
const path = await writeCredentials({ apiKey, apiUrl });
|
|
36
|
+
const path = await writeCredentials({ apiKey, apiUrl }, { client });
|
|
36
37
|
if (options.format === "json")
|
|
37
38
|
return printJson(me);
|
|
38
|
-
console.log(`Logged in to ${me.workspace.name} (${me.workspace.slug}) at ${apiUrl}.`);
|
|
39
|
+
console.log(`Logged in to ${me.workspace.name} (${me.workspace.slug}) at ${apiUrl}${client === undefined ? "" : ` as client ${client}`}.`);
|
|
39
40
|
console.log(`Saved credentials to ${path}.`);
|
|
40
41
|
},
|
|
41
42
|
},
|
|
42
43
|
logout: {
|
|
43
|
-
usage: "logout",
|
|
44
|
-
async run() {
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
usage: "logout [--client <name>] (without --client, removes the default login and keeps named clients)",
|
|
45
|
+
async run(_args, options) {
|
|
46
|
+
const client = options.client === undefined ? undefined : parseClientName(options.client);
|
|
47
|
+
await removeCredentials(client);
|
|
48
|
+
console.log(client === undefined ? "Logged out." : `Logged out of client ${client}.`);
|
|
47
49
|
},
|
|
48
50
|
},
|
|
49
51
|
whoami: {
|
|
50
|
-
usage: "whoami",
|
|
52
|
+
usage: "whoami (also says which client's key is in use and why)",
|
|
51
53
|
async run(_args, options) {
|
|
52
|
-
const
|
|
54
|
+
const { connection, choice } = await resolveLogin(options);
|
|
55
|
+
const me = await api(connection, "/api/v1/me");
|
|
56
|
+
const client = "client" in choice ? choice.client : null;
|
|
57
|
+
const clientFile = choice.source === "folder" ? choice.path : null;
|
|
53
58
|
if (options.format === "json")
|
|
54
|
-
return printJson(me);
|
|
59
|
+
return printJson({ ...me, client, source: choice.source, clientFile });
|
|
55
60
|
const { name, slug, id } = me.workspace;
|
|
56
61
|
console.log(renderTable(["workspace", "slug", "id", "library access"], [{ workspace: name, slug, id, "library access": me.knowledge ? "on" : "off" }]));
|
|
62
|
+
console.log(`\n${clientReason(choice)}`);
|
|
57
63
|
},
|
|
58
64
|
},
|
|
59
65
|
"knowledge search": {
|
|
@@ -72,7 +78,7 @@ const commands = {
|
|
|
72
78
|
query.set("library", options.library);
|
|
73
79
|
for (const [field, value] of filters)
|
|
74
80
|
query.set(field, value);
|
|
75
|
-
const answer = await api(await resolveConnection(options
|
|
81
|
+
const answer = await api(await resolveConnection(options), `/api/v1/knowledge/search?${query}`);
|
|
76
82
|
if (options.format === "json")
|
|
77
83
|
return printJson(answer);
|
|
78
84
|
console.log(answer.results.length === 0 ? "Nothing in the library matched." : answer.results.map(knowledgeBlock).join("\n\n"));
|
|
@@ -102,7 +108,7 @@ const commands = {
|
|
|
102
108
|
})),
|
|
103
109
|
}
|
|
104
110
|
: await readToolInput(options);
|
|
105
|
-
const result = await api(await resolveConnection(options
|
|
111
|
+
const result = await api(await resolveConnection(options), KNOWLEDGE_TAGS_PATH, body);
|
|
106
112
|
if (options.format === "json")
|
|
107
113
|
return printJson(result);
|
|
108
114
|
console.log(`Tagged ${result.tagged}, took off ${result.removed}.`);
|
|
@@ -121,7 +127,7 @@ const commands = {
|
|
|
121
127
|
if (options.search !== undefined)
|
|
122
128
|
query.set("search", options.search);
|
|
123
129
|
const path = `/api/v1/warehouse/schema${query.size > 0 ? `?${query}` : ""}`;
|
|
124
|
-
const connection = await resolveConnection(options
|
|
130
|
+
const connection = await resolveConnection(options);
|
|
125
131
|
if (options.summary) {
|
|
126
132
|
const summary = await api(connection, path);
|
|
127
133
|
if (options.format === "json")
|
|
@@ -162,7 +168,7 @@ const commands = {
|
|
|
162
168
|
throw new CliError("--all saves to a file: add --out <file.csv | file.ndjson | file.json>.", exitCodes.usage);
|
|
163
169
|
}
|
|
164
170
|
const sql = await readSql(args, options.file);
|
|
165
|
-
const connection = await resolveConnection(options
|
|
171
|
+
const connection = await resolveConnection(options);
|
|
166
172
|
if (options.all)
|
|
167
173
|
return saveWholeAnswer(connection, sql, options.out, options.format);
|
|
168
174
|
const text = await apiText(connection, "/api/v1/warehouse/query", { sql });
|
|
@@ -182,7 +188,7 @@ const commands = {
|
|
|
182
188
|
if (args.length > 0)
|
|
183
189
|
throw new CliError("Usage: funnel [--from <date>] [--to <date>] [--grain day|week|month]", exitCodes.usage);
|
|
184
190
|
const query = reportQuery(options, { grain: options.grain });
|
|
185
|
-
const { funnel } = await api(await resolveConnection(options
|
|
191
|
+
const { funnel } = await api(await resolveConnection(options), `/api/v1/reports/funnel${query}`);
|
|
186
192
|
if (options.format === "json")
|
|
187
193
|
return printJson({ funnel });
|
|
188
194
|
printReport(`Funnel by ${funnel.grain}`, funnel, ["period", "currency"], FUNNEL_NUMBERS);
|
|
@@ -195,7 +201,7 @@ const commands = {
|
|
|
195
201
|
if (args.length > 0)
|
|
196
202
|
throw new CliError("Usage: money [--from <date>] [--to <date>] [--grain day|week|month]", exitCodes.usage);
|
|
197
203
|
const query = reportQuery(options, { grain: options.grain });
|
|
198
|
-
const { money } = await api(await resolveConnection(options
|
|
204
|
+
const { money } = await api(await resolveConnection(options), `/api/v1/reports/money${query}`);
|
|
199
205
|
if (options.format === "json")
|
|
200
206
|
return printJson({ money });
|
|
201
207
|
printReport(`Money by ${money.grain}`, money, ["period", "currency"], MONEY_NUMBERS);
|
|
@@ -218,7 +224,7 @@ const commands = {
|
|
|
218
224
|
if (args.length > 0)
|
|
219
225
|
throw new CliError("Usage: campaigns [--from <date>] [--to <date>] [--channel <channel>] [--limit <n>]", exitCodes.usage);
|
|
220
226
|
const query = reportQuery(options, { channel: options.channel, limit: options.limit });
|
|
221
|
-
const { campaignResults } = await api(await resolveConnection(options
|
|
227
|
+
const { campaignResults } = await api(await resolveConnection(options), `/api/v1/reports/campaigns${query}`);
|
|
222
228
|
if (options.format === "json")
|
|
223
229
|
return printJson({ campaignResults });
|
|
224
230
|
const title = campaignResults.channel === null ? "Campaigns, best first" : `${campaignResults.channel} campaigns, best first`;
|
|
@@ -229,7 +235,7 @@ const commands = {
|
|
|
229
235
|
"tools list": {
|
|
230
236
|
usage: "tools list",
|
|
231
237
|
async run(_args, options) {
|
|
232
|
-
const { tools } = await api(await resolveConnection(options
|
|
238
|
+
const { tools } = await api(await resolveConnection(options), "/api/v1/tools");
|
|
233
239
|
if (options.format === "json")
|
|
234
240
|
return printJson({ tools });
|
|
235
241
|
console.log(renderTable(["id", "mode", "output", "title"], tools));
|
|
@@ -242,7 +248,7 @@ const commands = {
|
|
|
242
248
|
if (!tool || rest.length > 0)
|
|
243
249
|
throw new CliError("Usage: tools run <tool> [--input '<json>']", exitCodes.usage);
|
|
244
250
|
const input = await readToolInput(options);
|
|
245
|
-
const connection = await resolveConnection(options
|
|
251
|
+
const connection = await resolveConnection(options);
|
|
246
252
|
const body = {
|
|
247
253
|
input,
|
|
248
254
|
...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
@@ -257,7 +263,7 @@ const commands = {
|
|
|
257
263
|
async run(args, options) {
|
|
258
264
|
if (args.length !== 1)
|
|
259
265
|
throw new CliError("Usage: tools get <run-id>", exitCodes.usage);
|
|
260
|
-
const connection = await resolveConnection(options
|
|
266
|
+
const connection = await resolveConnection(options);
|
|
261
267
|
const { run } = await api(connection, `/api/v1/runs/${encodeURIComponent(args[0])}`);
|
|
262
268
|
printRun(await waitForRun(connection, run, options.wait), options.format);
|
|
263
269
|
},
|
|
@@ -267,7 +273,7 @@ const commands = {
|
|
|
267
273
|
async run(args, options) {
|
|
268
274
|
if (args.length > 0)
|
|
269
275
|
throw new CliError("Usage: sources list", exitCodes.usage);
|
|
270
|
-
const { sources } = await api(await resolveConnection(options
|
|
276
|
+
const { sources } = await api(await resolveConnection(options), "/api/v1/sources");
|
|
271
277
|
if (options.format === "json")
|
|
272
278
|
return printJson({ sources });
|
|
273
279
|
if (sources.length === 0)
|
|
@@ -296,7 +302,7 @@ const commands = {
|
|
|
296
302
|
async run(args, options) {
|
|
297
303
|
if (args.length !== 1)
|
|
298
304
|
throw new CliError("Usage: sources show <id>", exitCodes.usage);
|
|
299
|
-
const connection = await resolveConnection(options
|
|
305
|
+
const connection = await resolveConnection(options);
|
|
300
306
|
const path = `/api/v1/sources/${encodeURIComponent(args[0])}`;
|
|
301
307
|
const { source } = await api(connection, path);
|
|
302
308
|
const { runs } = await api(connection, `${path}/runs?limit=${SOURCE_RUNS_SHOWN}`);
|
|
@@ -340,7 +346,7 @@ const commands = {
|
|
|
340
346
|
const [id, ...assignments] = args;
|
|
341
347
|
if (!id)
|
|
342
348
|
throw new CliError("Usage: sources settings <id> [<key>=<value> ...]", exitCodes.usage);
|
|
343
|
-
const connection = await resolveConnection(options
|
|
349
|
+
const connection = await resolveConnection(options);
|
|
344
350
|
const path = `/api/v1/sources/${encodeURIComponent(id)}/settings`;
|
|
345
351
|
let { settings } = await api(connection, path);
|
|
346
352
|
if (assignments.length > 0) {
|
|
@@ -366,7 +372,7 @@ const commands = {
|
|
|
366
372
|
async run(args, options) {
|
|
367
373
|
if (args.length > 0)
|
|
368
374
|
throw new CliError("Usage: intake list", exitCodes.usage);
|
|
369
|
-
const { endpoints } = await api(await resolveConnection(options
|
|
375
|
+
const { endpoints } = await api(await resolveConnection(options), INTAKE_PATH);
|
|
370
376
|
if (options.format === "json")
|
|
371
377
|
return printJson({ endpoints });
|
|
372
378
|
if (endpoints.length === 0)
|
|
@@ -396,7 +402,7 @@ const commands = {
|
|
|
396
402
|
...(retentionDays === undefined ? {} : { retentionDays }),
|
|
397
403
|
...(signingKey === undefined ? {} : { signingKey }),
|
|
398
404
|
};
|
|
399
|
-
printIntakeToken(await api(await resolveConnection(options
|
|
405
|
+
printIntakeToken(await api(await resolveConnection(options), INTAKE_PATH, body), options.format);
|
|
400
406
|
},
|
|
401
407
|
},
|
|
402
408
|
"intake rotate": {
|
|
@@ -405,7 +411,7 @@ const commands = {
|
|
|
405
411
|
if (args.length !== 1)
|
|
406
412
|
throw new CliError("Usage: intake rotate <id>", exitCodes.usage);
|
|
407
413
|
const path = `${intakePath(args[0])}/rotate`;
|
|
408
|
-
printIntakeToken(await api(await resolveConnection(options
|
|
414
|
+
printIntakeToken(await api(await resolveConnection(options), path, {}), options.format);
|
|
409
415
|
},
|
|
410
416
|
},
|
|
411
417
|
"intake signing-key": {
|
|
@@ -415,7 +421,7 @@ const commands = {
|
|
|
415
421
|
throw new CliError("Usage: intake signing-key <id> --signing-key-stdin", exitCodes.usage);
|
|
416
422
|
const signingKey = await readSigningKey();
|
|
417
423
|
const path = `${intakePath(args[0])}/signing-key`;
|
|
418
|
-
const { endpoint } = await api(await resolveConnection(options
|
|
424
|
+
const { endpoint } = await api(await resolveConnection(options), path, { signingKey }, "PUT");
|
|
419
425
|
if (options.format === "json")
|
|
420
426
|
return printJson({ endpoint });
|
|
421
427
|
console.log(`Replaced the ${endpoint.kind} signing key. Every ${endpoint.kind} address in the workspace now checks against it.`);
|
|
@@ -462,7 +468,7 @@ const commands = {
|
|
|
462
468
|
if (!options.yes) {
|
|
463
469
|
throw new CliError("This deletes the address and every event it has saved. Add --yes to confirm: intake delete <id> --yes", exitCodes.usage);
|
|
464
470
|
}
|
|
465
|
-
const result = await api(await resolveConnection(options
|
|
471
|
+
const result = await api(await resolveConnection(options), intakePath(args[0]), undefined, "DELETE");
|
|
466
472
|
if (options.format === "json")
|
|
467
473
|
return printJson(result);
|
|
468
474
|
console.log("Deleted the address and its saved events.");
|
|
@@ -476,7 +482,7 @@ const commands = {
|
|
|
476
482
|
throw new CliError(usage, exitCodes.usage);
|
|
477
483
|
const name = importName(options.table);
|
|
478
484
|
const file = await readImportFile(args[0]);
|
|
479
|
-
const connection = await resolveConnection(options
|
|
485
|
+
const connection = await resolveConnection(options);
|
|
480
486
|
const result = await uploadCsv(connection, name, file);
|
|
481
487
|
if (options.format === "json")
|
|
482
488
|
return printJson(result);
|
|
@@ -495,7 +501,7 @@ const commands = {
|
|
|
495
501
|
throw new CliError(`This drops the table import_${name} and all its rows. Add --yes to confirm: import remove --table ${name} --yes`, exitCodes.usage);
|
|
496
502
|
}
|
|
497
503
|
const path = `/api/v1/imports/csv?table=${encodeURIComponent(name)}`;
|
|
498
|
-
const result = await api(await resolveConnection(options
|
|
504
|
+
const result = await api(await resolveConnection(options), path, undefined, "DELETE");
|
|
499
505
|
if (options.format === "json")
|
|
500
506
|
return printJson(result);
|
|
501
507
|
console.log(`Removed ${result.table} and its rows.`);
|
|
@@ -512,7 +518,7 @@ const commands = {
|
|
|
512
518
|
if (options.limit)
|
|
513
519
|
query.set("limit", options.limit);
|
|
514
520
|
const path = `/api/v1/creatives${query.size ? `?${query}` : ""}`;
|
|
515
|
-
const { creatives } = await api(await resolveConnection(options
|
|
521
|
+
const { creatives } = await api(await resolveConnection(options), path);
|
|
516
522
|
if (options.format === "json")
|
|
517
523
|
return printJson({ creatives });
|
|
518
524
|
if (creatives.length === 0)
|
|
@@ -526,7 +532,7 @@ const commands = {
|
|
|
526
532
|
async run(args, options) {
|
|
527
533
|
if (args.length !== 1)
|
|
528
534
|
throw new CliError("Usage: creatives show <sku>", exitCodes.usage);
|
|
529
|
-
const { creative } = await api(await resolveConnection(options
|
|
535
|
+
const { creative } = await api(await resolveConnection(options), `/api/v1/creatives/${encodeURIComponent(args[0])}`);
|
|
530
536
|
if (options.format === "json")
|
|
531
537
|
return printJson({ creative });
|
|
532
538
|
const fields = {
|
|
@@ -543,7 +549,7 @@ const commands = {
|
|
|
543
549
|
async run(args, options) {
|
|
544
550
|
if (args.length !== 1)
|
|
545
551
|
throw new CliError("Usage: creatives download <sku | asset-path | asset-link> [--out <path>] [--open]", exitCodes.usage);
|
|
546
|
-
const connection = await resolveConnection(options
|
|
552
|
+
const connection = await resolveConnection(options);
|
|
547
553
|
const target = await downloadTarget(args[0], connection);
|
|
548
554
|
const { bytes, contentType } = await apiDownload(connection, `${ASSET_ROUTE}${target.assetPath.split("/").map(encodeURIComponent).join("/")}`);
|
|
549
555
|
const path = await savePath(options.out, extname(target.fileName) ? target.fileName : `${target.fileName}.${extensionFor(contentType)}`);
|
|
@@ -563,7 +569,7 @@ const commands = {
|
|
|
563
569
|
}
|
|
564
570
|
const lineage = options.lineage === undefined ? undefined : await parseJson(options.lineage, "--lineage");
|
|
565
571
|
const tags = options.tags === undefined ? undefined : await parseJson(options.tags, "--tags");
|
|
566
|
-
const { creatives } = await api(await resolveConnection(options
|
|
572
|
+
const { creatives } = await api(await resolveConnection(options), "/api/v1/creatives", {
|
|
567
573
|
runId: options.run,
|
|
568
574
|
...(lineage === undefined ? {} : { lineage }),
|
|
569
575
|
...(tags === undefined ? {} : { tags }),
|
|
@@ -579,7 +585,7 @@ const commands = {
|
|
|
579
585
|
if (args.length !== 1)
|
|
580
586
|
throw new CliError("Usage: creatives tag <sku> --file <tags.json> | < tags.json", exitCodes.usage);
|
|
581
587
|
const tags = await readToolInput(options);
|
|
582
|
-
const { creative } = await api(await resolveConnection(options
|
|
588
|
+
const { creative } = await api(await resolveConnection(options), `/api/v1/creatives/${encodeURIComponent(args[0])}/tags`, { tags }, "PATCH");
|
|
583
589
|
if (options.format === "json")
|
|
584
590
|
return printJson({ creative });
|
|
585
591
|
console.log(`Tagged ${creative.sku}. The tags read ${creative.tagsStatus} until a person confirms them in the app.`);
|
|
@@ -590,7 +596,7 @@ const commands = {
|
|
|
590
596
|
async run(args, options) {
|
|
591
597
|
if (args.length > 0 || !options.kind)
|
|
592
598
|
throw new CliError("Usage: content create --kind ad|email|sms|landing_page|video|social_post [...]", exitCodes.usage);
|
|
593
|
-
const connection = await resolveConnection(options
|
|
599
|
+
const connection = await resolveConnection(options);
|
|
594
600
|
const content = await api(connection, "/api/v1/content", {
|
|
595
601
|
kind: options.kind,
|
|
596
602
|
...(await contentInput(options, connection)),
|
|
@@ -608,7 +614,7 @@ const commands = {
|
|
|
608
614
|
}
|
|
609
615
|
if (options.language !== undefined && options.transcript === undefined)
|
|
610
616
|
throw new CliError("--language goes with --transcript.", exitCodes.usage);
|
|
611
|
-
const connection = await resolveConnection(options
|
|
617
|
+
const connection = await resolveConnection(options);
|
|
612
618
|
const content = await api(connection, "/api/v1/content/captures", {
|
|
613
619
|
kind: options.kind,
|
|
614
620
|
...(await contentInput(options, connection)),
|
|
@@ -624,7 +630,7 @@ const commands = {
|
|
|
624
630
|
async run(args, options) {
|
|
625
631
|
if (args.length > 0)
|
|
626
632
|
throw new CliError("Usage: guide", exitCodes.usage);
|
|
627
|
-
const guide = await api(await resolveConnection(options
|
|
633
|
+
const guide = await api(await resolveConnection(options), "/api/v1/guide");
|
|
628
634
|
if (options.format === "json")
|
|
629
635
|
return printJson(guide);
|
|
630
636
|
process.stdout.write(guide.text);
|
|
@@ -636,7 +642,7 @@ const commands = {
|
|
|
636
642
|
if (args.length > 0)
|
|
637
643
|
throw new CliError("Usage: brief [--from <date>] [--to <date>] [--channel <channel>] [--campaign <id>] [--top <n>]", exitCodes.usage);
|
|
638
644
|
const query = reportQuery(options, { channel: options.channel, campaignId: options.campaign, top: options.top });
|
|
639
|
-
const brief = await api(await resolveConnection(options
|
|
645
|
+
const brief = await api(await resolveConnection(options), `/api/v1/brief${query}`);
|
|
640
646
|
if (options.format === "json")
|
|
641
647
|
return printJson(brief);
|
|
642
648
|
process.stdout.write(brief.text);
|
|
@@ -652,7 +658,7 @@ const commands = {
|
|
|
652
658
|
for (const [name, value] of Object.entries({ step: options.step, funnel: options.funnel, page: options.page }))
|
|
653
659
|
if (value !== undefined)
|
|
654
660
|
query.set(name, value);
|
|
655
|
-
const brief = await api(await resolveConnection(options
|
|
661
|
+
const brief = await api(await resolveConnection(options), `/api/v1/pages/brief?${query}`);
|
|
656
662
|
if (options.format === "json")
|
|
657
663
|
return printJson(brief);
|
|
658
664
|
process.stdout.write(brief.text);
|
|
@@ -663,7 +669,7 @@ const commands = {
|
|
|
663
669
|
async run(args, options) {
|
|
664
670
|
if (args.length > 0)
|
|
665
671
|
throw new CliError("Usage: pages results [--from <date>] [--to <date>] [--page <address | page key | item id>]", exitCodes.usage);
|
|
666
|
-
const results = await api(await resolveConnection(options
|
|
672
|
+
const results = await api(await resolveConnection(options), `/api/v1/pages/results${reportQuery(options, { page: options.page })}`);
|
|
667
673
|
if (options.format === "json")
|
|
668
674
|
return printJson(results);
|
|
669
675
|
printPageResults(results);
|
|
@@ -674,14 +680,14 @@ const commands = {
|
|
|
674
680
|
async run(args, options) {
|
|
675
681
|
if (args.length > 0)
|
|
676
682
|
throw new CliError("Usage: messages results [--from <date>] [--to <date>] [--channel email|text] [--sequence <workflow id>]", exitCodes.usage);
|
|
677
|
-
const results = await api(await resolveConnection(options
|
|
683
|
+
const results = await api(await resolveConnection(options), `/api/v1/messages/results${reportQuery(options, { channel: options.channel, sequence: options.sequence })}`);
|
|
678
684
|
if (options.format === "json")
|
|
679
685
|
return printJson(results);
|
|
680
686
|
printMessageResults(results);
|
|
681
687
|
},
|
|
682
688
|
},
|
|
683
689
|
winners: {
|
|
684
|
-
usage: "winners [--week <YYYY-MM-DD> | --days <n> | --from <YYYY-MM-DD> --to <YYYY-MM-DD>] [--top <n>] [--ad-format image|video|carousel] (which Facebook ads and landing pages won and flopped, judged on
|
|
690
|
+
usage: "winners [--week <YYYY-MM-DD> | --days <n> | --from <YYYY-MM-DD> --to <YYYY-MM-DD>] [--top <n>] [--ad-format image|video|carousel] (which Facebook ads and landing pages won and flopped, judged on each client's goal; see winners goals)",
|
|
685
691
|
async run(args, options) {
|
|
686
692
|
if (args.length > 0)
|
|
687
693
|
throw new CliError("Usage: winners [--week <date> | --days <n> | --from <date> --to <date>] [--top <n>] [--ad-format image|video|carousel]", exitCodes.usage);
|
|
@@ -689,7 +695,7 @@ const commands = {
|
|
|
689
695
|
for (const [name, value] of Object.entries({ ...winnersPeriod(options), top: options.top, format: options.entryTags.format }))
|
|
690
696
|
if (value !== undefined)
|
|
691
697
|
query.set(name, value);
|
|
692
|
-
const report = await api(await resolveConnection(options
|
|
698
|
+
const report = await api(await resolveConnection(options), `/api/v1/winners${query.size ? `?${query}` : ""}`);
|
|
693
699
|
if (options.format === "json")
|
|
694
700
|
return printJson(report);
|
|
695
701
|
process.stdout.write(winnersText(report));
|
|
@@ -701,7 +707,7 @@ const commands = {
|
|
|
701
707
|
if (args.length > 0)
|
|
702
708
|
throw new CliError("Usage: winners send-to-exodus [--week <date> | --days <n> | --from <date> --to <date>] [--dry-run]", exitCodes.usage);
|
|
703
709
|
const period = Object.entries(winnersPeriod(options)).filter(([, value]) => value !== undefined);
|
|
704
|
-
const sent = await api(await resolveConnection(options
|
|
710
|
+
const sent = await api(await resolveConnection(options), "/api/v1/winners/exodus", { ...Object.fromEntries(period), dryRun: options.dryRun });
|
|
705
711
|
if (options.format === "json")
|
|
706
712
|
printJson(sent);
|
|
707
713
|
else
|
|
@@ -711,12 +717,36 @@ const commands = {
|
|
|
711
717
|
throw new CliError(`${failed} of ${sent.pages.length} ${sent.pages.length === 1 ? "page was" : "pages were"} not sent; see above.`, exitCodes.error);
|
|
712
718
|
},
|
|
713
719
|
},
|
|
720
|
+
"winners goals": {
|
|
721
|
+
usage: "winners goals (the goals this client's ads are judged on, in order, whether each has data, and which one winners uses now)",
|
|
722
|
+
async run(args, options) {
|
|
723
|
+
if (args.length > 0)
|
|
724
|
+
throw new CliError("Usage: winners goals", exitCodes.usage);
|
|
725
|
+
printWinnersGoals(await api(await resolveConnection(options), "/api/v1/winners/goals"), options.format);
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
"winners goals set": {
|
|
729
|
+
usage: "winners goals set <goal> [<goal> ...] (judge this client's ads on these goals, the first with data first: sales, sales:triple_whale, booked_calls, meta_results, first_leads)",
|
|
730
|
+
async run(args, options) {
|
|
731
|
+
if (args.length === 0)
|
|
732
|
+
throw new CliError("Usage: winners goals set <goal> [<goal> ...]", exitCodes.usage);
|
|
733
|
+
printWinnersGoals(await api(await resolveConnection(options), "/api/v1/winners/goals", { goals: args }, "PUT"), options.format);
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
"winners goals reset": {
|
|
737
|
+
usage: "winners goals reset (go back to the default goals: Triple Whale's orders, then first-touch sales, then first leads)",
|
|
738
|
+
async run(args, options) {
|
|
739
|
+
if (args.length > 0)
|
|
740
|
+
throw new CliError("Usage: winners goals reset", exitCodes.usage);
|
|
741
|
+
printWinnersGoals(await api(await resolveConnection(options), "/api/v1/winners/goals", { goals: null }, "PUT"), options.format);
|
|
742
|
+
},
|
|
743
|
+
},
|
|
714
744
|
examples: {
|
|
715
745
|
usage: "examples (plain questions with SQL that answers them, for the tables this workspace has)",
|
|
716
746
|
async run(args, options) {
|
|
717
747
|
if (args.length > 0)
|
|
718
748
|
throw new CliError("Usage: examples", exitCodes.usage);
|
|
719
|
-
const { examples } = await api(await resolveConnection(options
|
|
749
|
+
const { examples } = await api(await resolveConnection(options), "/api/v1/examples");
|
|
720
750
|
if (options.format === "json")
|
|
721
751
|
return printJson({ examples });
|
|
722
752
|
for (const e of examples) {
|
|
@@ -734,7 +764,7 @@ const commands = {
|
|
|
734
764
|
throw new CliError("Usage: content search [--text <words>] [--kind <kind>] [--sort <column>] [...]; put several words in quotes", exitCodes.usage);
|
|
735
765
|
const filters = { text: options.text, mode: options.mode, kind: options.kind, platform: options.platform, origin: options.origin, from: options.from, to: options.to, sort: options.sort, order: options.order, limit: options.limit };
|
|
736
766
|
const query = new URLSearchParams(Object.entries(filters).filter((entry) => entry[1] !== undefined));
|
|
737
|
-
const { results } = await api(await resolveConnection(options
|
|
767
|
+
const { results } = await api(await resolveConnection(options), `/api/v1/content${query.size ? `?${query}` : ""}`);
|
|
738
768
|
if (options.format === "json")
|
|
739
769
|
return printJson({ results });
|
|
740
770
|
if (results.length === 0)
|
|
@@ -760,7 +790,7 @@ const commands = {
|
|
|
760
790
|
async run(args, options) {
|
|
761
791
|
if (args.length !== 1)
|
|
762
792
|
throw new CliError("Usage: content show <sku | item-id>", exitCodes.usage);
|
|
763
|
-
const content = await api(await resolveConnection(options
|
|
793
|
+
const content = await api(await resolveConnection(options), `/api/v1/content/${encodeURIComponent(args[0])}`);
|
|
764
794
|
printContent(content, options.format);
|
|
765
795
|
},
|
|
766
796
|
},
|
|
@@ -770,7 +800,7 @@ const commands = {
|
|
|
770
800
|
if (args.length !== 1)
|
|
771
801
|
throw new CliError("Usage: content pull <dir> [--kind <kind>] [--platform <platform>] [--origin discovered|made] [--media] [--full]", exitCodes.usage);
|
|
772
802
|
const filters = Object.fromEntries(Object.entries({ kind: options.kind, platform: options.platform, origin: options.origin }).filter((e) => e[1] !== undefined));
|
|
773
|
-
const report = await pullContent(await resolveConnection(options
|
|
803
|
+
const report = await pullContent(await resolveConnection(options), args[0], filters, { media: options.media, full: options.full });
|
|
774
804
|
if (options.format === "json")
|
|
775
805
|
return printJson(report);
|
|
776
806
|
const media = options.media ? `, ${report.media} new media file${report.media === 1 ? "" : "s"}` : "";
|
|
@@ -786,7 +816,7 @@ const commands = {
|
|
|
786
816
|
if (args.length !== 1)
|
|
787
817
|
throw new CliError("Usage: content diff <sku | item-id> [--from <n>] [--to <n>]", exitCodes.usage);
|
|
788
818
|
const query = new URLSearchParams(Object.entries({ from: options.from, to: options.to }).filter((entry) => entry[1] !== undefined));
|
|
789
|
-
const diff = await api(await resolveConnection(options
|
|
819
|
+
const diff = await api(await resolveConnection(options), `/api/v1/content/${encodeURIComponent(args[0])}/diff${query.size ? `?${query}` : ""}`);
|
|
790
820
|
if (options.format === "json")
|
|
791
821
|
return printJson(diff);
|
|
792
822
|
printContentDiff(diff);
|
|
@@ -797,7 +827,7 @@ const commands = {
|
|
|
797
827
|
async run(args, options) {
|
|
798
828
|
if (args.length !== 1)
|
|
799
829
|
throw new CliError("Usage: content edit <sku | item-id> [--body <text> | @file] [--note <why>] [...]", exitCodes.usage);
|
|
800
|
-
const connection = await resolveConnection(options
|
|
830
|
+
const connection = await resolveConnection(options);
|
|
801
831
|
const input = await contentInput(options, connection);
|
|
802
832
|
if (Object.keys(input).every((k) => k === "runId" || k === "note")) {
|
|
803
833
|
throw new CliError("content edit needs something to change, such as --body, --subject or --file.", exitCodes.usage);
|
|
@@ -811,7 +841,7 @@ const commands = {
|
|
|
811
841
|
async run(args, options) {
|
|
812
842
|
if (args.length !== 2)
|
|
813
843
|
throw new CliError("Usage: content status <sku | item-id> <status> [--platform <platform> [--object-type <type>] --external-id <id>]", exitCodes.usage);
|
|
814
|
-
const content = await api(await resolveConnection(options
|
|
844
|
+
const content = await api(await resolveConnection(options), `/api/v1/content/${encodeURIComponent(args[0])}/status`, {
|
|
815
845
|
status: args[1],
|
|
816
846
|
...(options.platform === undefined ? {} : { platform: options.platform }),
|
|
817
847
|
...(options.objectType === undefined ? {} : { objectType: options.objectType }),
|
|
@@ -829,7 +859,7 @@ const commands = {
|
|
|
829
859
|
const version = options.version === undefined ? undefined : Number(options.version);
|
|
830
860
|
if (version !== undefined && !(Number.isInteger(version) && version >= 1))
|
|
831
861
|
throw new CliError("--version must be a whole number from 1.", exitCodes.usage);
|
|
832
|
-
const { publication } = await api(await resolveConnection(options
|
|
862
|
+
const { publication } = await api(await resolveConnection(options), `/api/v1/content/${encodeURIComponent(args[0])}/publish`, {
|
|
833
863
|
publisher: options.to,
|
|
834
864
|
target: await parseJson(options.target, "--target"),
|
|
835
865
|
idempotencyKey: options.idempotencyKey,
|
|
@@ -845,7 +875,7 @@ const commands = {
|
|
|
845
875
|
async run(args, options) {
|
|
846
876
|
if (args.length !== 1)
|
|
847
877
|
throw new CliError("Usage: content reconcile <publication-id>", exitCodes.usage);
|
|
848
|
-
const { publication } = await api(await resolveConnection(options
|
|
878
|
+
const { publication } = await api(await resolveConnection(options), `/api/v1/publications/${encodeURIComponent(args[0])}/reconcile`, {});
|
|
849
879
|
printPublication(publication, options.format);
|
|
850
880
|
},
|
|
851
881
|
},
|
|
@@ -855,7 +885,7 @@ const commands = {
|
|
|
855
885
|
if (args.length !== 0 || options.plan === undefined || !options.idempotencyKey) {
|
|
856
886
|
throw new CliError("Usage: meta adset publish --plan '<json>' --idempotency-key <key> [--run <run-id>]", exitCodes.usage);
|
|
857
887
|
}
|
|
858
|
-
const { adsetPublication } = await api(await resolveConnection(options
|
|
888
|
+
const { adsetPublication } = await api(await resolveConnection(options), "/api/v1/meta/adsets", {
|
|
859
889
|
plan: await parseJson(options.plan, "--plan"),
|
|
860
890
|
idempotencyKey: options.idempotencyKey,
|
|
861
891
|
...(options.run === undefined ? {} : { runId: options.run }),
|
|
@@ -868,7 +898,7 @@ const commands = {
|
|
|
868
898
|
async run(args, options) {
|
|
869
899
|
if (args.length !== 1)
|
|
870
900
|
throw new CliError("Usage: meta adset show <adset-publication-id>", exitCodes.usage);
|
|
871
|
-
const { adsetPublication } = await api(await resolveConnection(options
|
|
901
|
+
const { adsetPublication } = await api(await resolveConnection(options), `/api/v1/meta/adsets/${encodeURIComponent(args[0])}`);
|
|
872
902
|
printAdSetPublication(adsetPublication, options.format);
|
|
873
903
|
},
|
|
874
904
|
},
|
|
@@ -877,7 +907,7 @@ const commands = {
|
|
|
877
907
|
async run(args, options) {
|
|
878
908
|
if (args.length !== 1)
|
|
879
909
|
throw new CliError("Usage: meta adset reconcile <adset-publication-id>", exitCodes.usage);
|
|
880
|
-
const { adsetPublication } = await api(await resolveConnection(options
|
|
910
|
+
const { adsetPublication } = await api(await resolveConnection(options), `/api/v1/meta/adsets/${encodeURIComponent(args[0])}/reconcile`, {});
|
|
881
911
|
printAdSetPublication(adsetPublication, options.format);
|
|
882
912
|
},
|
|
883
913
|
},
|
|
@@ -886,7 +916,7 @@ const commands = {
|
|
|
886
916
|
async run(args, options) {
|
|
887
917
|
if (args.length !== 1)
|
|
888
918
|
throw new CliError("Usage: content unpublish <deployment-id> [--force]", exitCodes.usage);
|
|
889
|
-
const { deployment } = await api(await resolveConnection(options
|
|
919
|
+
const { deployment } = await api(await resolveConnection(options), `/api/v1/deployments/${encodeURIComponent(args[0])}/unpublish`, options.force ? { force: true } : {});
|
|
890
920
|
if (options.format === "json")
|
|
891
921
|
return printJson({ deployment });
|
|
892
922
|
printFields({
|
|
@@ -904,7 +934,7 @@ const commands = {
|
|
|
904
934
|
async run(args, options) {
|
|
905
935
|
if (args.length !== 1)
|
|
906
936
|
throw new CliError("Usage: content publish-record <sku | item-id> --file <publication.json>", exitCodes.usage);
|
|
907
|
-
const { publication } = await api(await resolveConnection(options
|
|
937
|
+
const { publication } = await api(await resolveConnection(options), `/api/v1/content/${encodeURIComponent(args[0])}/publications`, await readToolInput(options));
|
|
908
938
|
printPublication(publication, options.format);
|
|
909
939
|
},
|
|
910
940
|
},
|
|
@@ -915,7 +945,7 @@ const commands = {
|
|
|
915
945
|
throw new CliError("Usage: content publish-finish <publication-id> --status sending|published|failed|not_sent_practice [--file <outcome.json>]", exitCodes.usage);
|
|
916
946
|
}
|
|
917
947
|
const outcome = await readToolInput(options);
|
|
918
|
-
const { publication } = await api(await resolveConnection(options
|
|
948
|
+
const { publication } = await api(await resolveConnection(options), `/api/v1/publications/${encodeURIComponent(args[0])}`, { ...outcome, status: options.status }, "PATCH");
|
|
919
949
|
printPublication(publication, options.format);
|
|
920
950
|
},
|
|
921
951
|
},
|
|
@@ -924,7 +954,7 @@ const commands = {
|
|
|
924
954
|
async run(args, options) {
|
|
925
955
|
if (args.length > 0)
|
|
926
956
|
throw new CliError("Usage: agent start", exitCodes.usage);
|
|
927
|
-
const start = await api(await resolveConnection(options
|
|
957
|
+
const start = await api(await resolveConnection(options), `${agentPath(options)}/runs`, {});
|
|
928
958
|
if (options.format === "json")
|
|
929
959
|
return printJson(start);
|
|
930
960
|
const { run, quota, brandProfile } = start;
|
|
@@ -944,7 +974,7 @@ const commands = {
|
|
|
944
974
|
async run(args, options) {
|
|
945
975
|
if (args.length !== 1)
|
|
946
976
|
throw new CliError("Usage: agent show <run-id>", exitCodes.usage);
|
|
947
|
-
const shown = await api(await resolveConnection(options
|
|
977
|
+
const shown = await api(await resolveConnection(options), `${agentPath(options)}/runs/${encodeURIComponent(args[0])}`);
|
|
948
978
|
if (options.format === "json")
|
|
949
979
|
return printJson(shown);
|
|
950
980
|
const { run, proposals } = shown;
|
|
@@ -958,7 +988,7 @@ const commands = {
|
|
|
958
988
|
usage: "agent runs [--limit <n>]",
|
|
959
989
|
async run(_args, options) {
|
|
960
990
|
const query = options.limit ? `?limit=${encodeURIComponent(options.limit)}` : "";
|
|
961
|
-
const { runs } = await api(await resolveConnection(options
|
|
991
|
+
const { runs } = await api(await resolveConnection(options), `${agentPath(options)}/runs${query}`);
|
|
962
992
|
if (options.format === "json")
|
|
963
993
|
return printJson({ runs });
|
|
964
994
|
if (runs.length === 0)
|
|
@@ -973,7 +1003,7 @@ const commands = {
|
|
|
973
1003
|
throw new CliError("Usage: agent finish <run-id> --status succeeded|failed [--file report.md]", exitCodes.usage);
|
|
974
1004
|
}
|
|
975
1005
|
const report = await readText(options.file, "the report");
|
|
976
|
-
const { run } = await api(await resolveConnection(options
|
|
1006
|
+
const { run } = await api(await resolveConnection(options), `${agentPath(options)}/runs/${encodeURIComponent(args[0])}/finish`, { status: options.status, report });
|
|
977
1007
|
if (options.format === "json")
|
|
978
1008
|
return printJson({ run });
|
|
979
1009
|
console.log(`Finished run ${run.id} as ${run.status}.`);
|
|
@@ -984,7 +1014,7 @@ const commands = {
|
|
|
984
1014
|
async run(args, options) {
|
|
985
1015
|
if (args.length > 0)
|
|
986
1016
|
throw new CliError("Usage: agent draft --agent p3_refresh", exitCodes.usage);
|
|
987
|
-
const body = await api(await resolveConnection(options
|
|
1017
|
+
const body = await api(await resolveConnection(options), `${agentPath(options)}/draft`);
|
|
988
1018
|
if (options.format === "json")
|
|
989
1019
|
return printJson(body);
|
|
990
1020
|
const { draft } = body;
|
|
@@ -1025,7 +1055,7 @@ const commands = {
|
|
|
1025
1055
|
"brand get": {
|
|
1026
1056
|
usage: "brand get",
|
|
1027
1057
|
async run(_args, options) {
|
|
1028
|
-
const { profile } = await api(await resolveConnection(options
|
|
1058
|
+
const { profile } = await api(await resolveConnection(options), "/api/v1/brand-profile");
|
|
1029
1059
|
if (options.format === "json")
|
|
1030
1060
|
return printJson({ profile });
|
|
1031
1061
|
if (!profile)
|
|
@@ -1041,7 +1071,7 @@ const commands = {
|
|
|
1041
1071
|
throw new CliError("Usage: brand set [--file <path>] [--sources '<json>' | @file.json]", exitCodes.usage);
|
|
1042
1072
|
const markdown = await readText(options.file, "the brand profile");
|
|
1043
1073
|
const sources = options.sources === undefined ? [] : await parseJson(options.sources, "--sources");
|
|
1044
|
-
const { profile } = await api(await resolveConnection(options
|
|
1074
|
+
const { profile } = await api(await resolveConnection(options), "/api/v1/brand-profile", { markdown, sources }, "PUT");
|
|
1045
1075
|
if (options.format === "json")
|
|
1046
1076
|
return printJson({ profile });
|
|
1047
1077
|
console.log(`Saved brand profile version ${profile.version}.`);
|
|
@@ -1052,7 +1082,7 @@ const commands = {
|
|
|
1052
1082
|
async run(args, options) {
|
|
1053
1083
|
if (args.length !== 1)
|
|
1054
1084
|
throw new CliError("Usage: content add-page <url>", exitCodes.usage);
|
|
1055
|
-
const { page } = await api(await resolveConnection(options
|
|
1085
|
+
const { page } = await api(await resolveConnection(options), "/api/v1/content/pages", { url: args[0] });
|
|
1056
1086
|
if (options.format === "json")
|
|
1057
1087
|
return printJson({ page });
|
|
1058
1088
|
console.log(renderTable(["key", "status", "version", "changed", "headline", "blocks"], [{ ...page, blocks: page.blockCount }]));
|
|
@@ -1066,7 +1096,7 @@ const commands = {
|
|
|
1066
1096
|
async run(args, options) {
|
|
1067
1097
|
if (args.length !== 2)
|
|
1068
1098
|
throw new CliError("Usage: content track-link <sku> <url> [--channel facebook|email|sms|web] [--campaign <id>]", exitCodes.usage);
|
|
1069
|
-
const { url } = await api(await resolveConnection(options
|
|
1099
|
+
const { url } = await api(await resolveConnection(options), "/api/v1/content/track-link", {
|
|
1070
1100
|
sku: args[0],
|
|
1071
1101
|
url: args[1],
|
|
1072
1102
|
channel: options.channel,
|
|
@@ -1082,7 +1112,7 @@ const commands = {
|
|
|
1082
1112
|
async run(args, options) {
|
|
1083
1113
|
if (args.length !== 1)
|
|
1084
1114
|
throw new CliError("Usage: trail <plan-id | sku | item-id>", exitCodes.usage);
|
|
1085
|
-
const connection = await resolveConnection(options
|
|
1115
|
+
const connection = await resolveConnection(options);
|
|
1086
1116
|
const ref = args[0];
|
|
1087
1117
|
const trailOf = (query) => api(connection, `/api/v1/trail?${query}=${encodeURIComponent(ref)}`);
|
|
1088
1118
|
// Plans and items both have uuid ids: an id that names no plan is read as an item.
|
|
@@ -1102,7 +1132,7 @@ const commands = {
|
|
|
1102
1132
|
if (args.length > 0)
|
|
1103
1133
|
throw new CliError("Usage: funnels show [--version <n>]", exitCodes.usage);
|
|
1104
1134
|
const query = options.version === undefined ? "" : `?version=${wholeNumber(options.version, "--version")}`;
|
|
1105
|
-
const view = await api(await resolveConnection(options
|
|
1135
|
+
const view = await api(await resolveConnection(options), `${FUNNELS_PATH}${query}`);
|
|
1106
1136
|
if (options.format === "json")
|
|
1107
1137
|
return printJson(view);
|
|
1108
1138
|
process.stdout.write(view.text);
|
|
@@ -1113,7 +1143,7 @@ const commands = {
|
|
|
1113
1143
|
async run(args, options) {
|
|
1114
1144
|
if (args.length > 0)
|
|
1115
1145
|
throw new CliError("Usage: funnels numbers [--funnel <key>] [--from <date>] [--to <date>]", exitCodes.usage);
|
|
1116
|
-
const view = await api(await resolveConnection(options
|
|
1146
|
+
const view = await api(await resolveConnection(options), `${FUNNELS_PATH}/numbers${reportQuery(options, { funnel: options.funnel })}`);
|
|
1117
1147
|
if (options.format === "json")
|
|
1118
1148
|
return printJson(view);
|
|
1119
1149
|
process.stdout.write(view.text);
|
|
@@ -1124,7 +1154,7 @@ const commands = {
|
|
|
1124
1154
|
async run(args, options) {
|
|
1125
1155
|
if (args.length > 0 || options.from !== undefined)
|
|
1126
1156
|
throw new CliError("Usage: funnels leaks [--funnel <key>] [--to <date>]", exitCodes.usage);
|
|
1127
|
-
const view = await api(await resolveConnection(options
|
|
1157
|
+
const view = await api(await resolveConnection(options), `${FUNNELS_PATH}/leaks${reportQuery(options, { funnel: options.funnel })}`);
|
|
1128
1158
|
if (options.format === "json")
|
|
1129
1159
|
return printJson(view);
|
|
1130
1160
|
process.stdout.write(view.text);
|
|
@@ -1135,7 +1165,7 @@ const commands = {
|
|
|
1135
1165
|
async run(args, options) {
|
|
1136
1166
|
if (args.length > 0)
|
|
1137
1167
|
throw new CliError("Usage: funnels evidence", exitCodes.usage);
|
|
1138
|
-
const view = await api(await resolveConnection(options
|
|
1168
|
+
const view = await api(await resolveConnection(options), `${FUNNELS_PATH}/evidence`);
|
|
1139
1169
|
if (options.format === "json")
|
|
1140
1170
|
return printJson(view);
|
|
1141
1171
|
process.stdout.write(view.text);
|
|
@@ -1153,7 +1183,7 @@ const commands = {
|
|
|
1153
1183
|
const map = await parseJson(`@${options.file}`, "--file");
|
|
1154
1184
|
const evidence = options.evidence === undefined ? undefined : await parseJson(options.evidence, "--evidence");
|
|
1155
1185
|
const questions = options.questions === undefined ? undefined : await parseJson(options.questions, "--questions");
|
|
1156
|
-
const connection = await resolveConnection(options
|
|
1186
|
+
const connection = await resolveConnection(options);
|
|
1157
1187
|
const basedOn = options.basedOn === undefined ? null : wholeNumber(options.basedOn, "--based-on");
|
|
1158
1188
|
const saved = await api(connection, `${FUNNELS_PATH}/draft`, { map, note: options.note, evidence, based_on: basedOn, counts_from: options.countsFrom, questions }, "PUT");
|
|
1159
1189
|
if (options.format === "json")
|
|
@@ -1183,7 +1213,7 @@ const commands = {
|
|
|
1183
1213
|
"funnels versions": {
|
|
1184
1214
|
usage: "funnels versions [--diff <a> <b>] every version of the map, or what changed from version a to b",
|
|
1185
1215
|
async run(args, options) {
|
|
1186
|
-
const connection = await resolveConnection(options
|
|
1216
|
+
const connection = await resolveConnection(options);
|
|
1187
1217
|
if (options.diff !== undefined) {
|
|
1188
1218
|
if (args.length !== 1)
|
|
1189
1219
|
throw new CliError("Usage: funnels versions --diff <a> <b>", exitCodes.usage);
|
|
@@ -1230,7 +1260,7 @@ const commands = {
|
|
|
1230
1260
|
}
|
|
1231
1261
|
if (options.status)
|
|
1232
1262
|
query.set("status", options.status);
|
|
1233
|
-
const { facts } = await api(await resolveConnection(options
|
|
1263
|
+
const { facts } = await api(await resolveConnection(options), `${FACTS_PATH}${query.size ? `?${query}` : ""}`);
|
|
1234
1264
|
if (options.format === "json")
|
|
1235
1265
|
return printJson({ facts });
|
|
1236
1266
|
if (facts.length === 0)
|
|
@@ -1252,7 +1282,7 @@ const commands = {
|
|
|
1252
1282
|
query.set("sort", options.sort);
|
|
1253
1283
|
if (options.limit)
|
|
1254
1284
|
query.set("limit", options.limit);
|
|
1255
|
-
const read = await api(await resolveConnection(options
|
|
1285
|
+
const read = await api(await resolveConnection(options), `${VOICE_PATH}${query.size ? `?${query}` : ""}`);
|
|
1256
1286
|
if (options.format === "json")
|
|
1257
1287
|
return printJson(read);
|
|
1258
1288
|
if ("lines" in read) {
|
|
@@ -1277,7 +1307,7 @@ const commands = {
|
|
|
1277
1307
|
query.set("status", options.status);
|
|
1278
1308
|
if (options.limit)
|
|
1279
1309
|
query.set("limit", options.limit);
|
|
1280
|
-
const { tags } = await api(await resolveConnection(options
|
|
1310
|
+
const { tags } = await api(await resolveConnection(options), `${AD_TAGS_PATH}${query.size ? `?${query}` : ""}`);
|
|
1281
1311
|
if (options.format === "json")
|
|
1282
1312
|
return printJson({ tags });
|
|
1283
1313
|
if (tags.length === 0)
|
|
@@ -1290,7 +1320,7 @@ const commands = {
|
|
|
1290
1320
|
async run(args, options) {
|
|
1291
1321
|
if (args.length > 0)
|
|
1292
1322
|
throw new CliError("Usage: ads tag --file <tags.json> | < tags.json", exitCodes.usage);
|
|
1293
|
-
const result = await api(await resolveConnection(options
|
|
1323
|
+
const result = await api(await resolveConnection(options), AD_TAGS_PATH, await readToolInput(options));
|
|
1294
1324
|
if (options.format === "json")
|
|
1295
1325
|
return printJson(result);
|
|
1296
1326
|
console.log(`Proposed tags for ${result.proposed.length} ${result.proposed.length === 1 ? "ad" : "ads"}. A person confirms or rejects them in the app.`);
|
|
@@ -1304,7 +1334,7 @@ const commands = {
|
|
|
1304
1334
|
if (args.length > 0)
|
|
1305
1335
|
throw new CliError("Usage: ads autotag [--limit <n>] [--ad <id>] [--json]", exitCodes.usage);
|
|
1306
1336
|
const body = { ...(options.ad ? { ad_ids: [options.ad] } : {}), ...(options.limit ? { limit: Number(options.limit) } : {}) };
|
|
1307
|
-
const result = await api(await resolveConnection(options
|
|
1337
|
+
const result = await api(await resolveConnection(options), AD_AUTOTAG_PATH, body);
|
|
1308
1338
|
if (options.format === "json")
|
|
1309
1339
|
return printJson(result);
|
|
1310
1340
|
const plural = (n) => `${n} ${n === 1 ? "ad" : "ads"}`;
|
|
@@ -1331,7 +1361,7 @@ const commands = {
|
|
|
1331
1361
|
async run(args, options) {
|
|
1332
1362
|
if (args.length > 0)
|
|
1333
1363
|
throw new CliError("Usage: ads tag-formats [--limit <n>] [--json]", exitCodes.usage);
|
|
1334
|
-
const connection = await resolveConnection(options
|
|
1364
|
+
const connection = await resolveConnection(options);
|
|
1335
1365
|
const total = { tagged: 0, no_format: 0 };
|
|
1336
1366
|
let after = null;
|
|
1337
1367
|
for (;;) {
|
|
@@ -1360,7 +1390,7 @@ const commands = {
|
|
|
1360
1390
|
throw new CliError("Usage: content tags [<item-id>] [--kind email|text|page] [--status <status>] [--limit <n>] [--json]", exitCodes.usage);
|
|
1361
1391
|
const filters = { item: args[0], kind: options.kind, status: options.status, limit: options.limit };
|
|
1362
1392
|
const query = new URLSearchParams(Object.entries(filters).filter((entry) => entry[1] !== undefined));
|
|
1363
|
-
const { tags } = await api(await resolveConnection(options
|
|
1393
|
+
const { tags } = await api(await resolveConnection(options), `${CONTENT_TAGS_PATH}${query.size ? `?${query}` : ""}`);
|
|
1364
1394
|
if (options.format === "json")
|
|
1365
1395
|
return printJson({ tags });
|
|
1366
1396
|
if (tags.length === 0)
|
|
@@ -1374,7 +1404,7 @@ const commands = {
|
|
|
1374
1404
|
if (args.length > 0)
|
|
1375
1405
|
throw new CliError("Usage: content to-check [--limit <n>] [--json]", exitCodes.usage);
|
|
1376
1406
|
const query = new URLSearchParams({ to_check: "1", ...(options.limit ? { limit: options.limit } : {}) });
|
|
1377
|
-
const { tags } = await api(await resolveConnection(options
|
|
1407
|
+
const { tags } = await api(await resolveConnection(options), `${CONTENT_TAGS_PATH}?${query}`);
|
|
1378
1408
|
if (options.format === "json")
|
|
1379
1409
|
return printJson({ tags });
|
|
1380
1410
|
if (tags.length === 0)
|
|
@@ -1390,7 +1420,7 @@ const commands = {
|
|
|
1390
1420
|
...(options.kind ? { kinds: options.kind.split(",") } : {}),
|
|
1391
1421
|
...(options.limit ? { limit: Number(options.limit) } : {}),
|
|
1392
1422
|
};
|
|
1393
|
-
const result = await api(await resolveConnection(options
|
|
1423
|
+
const result = await api(await resolveConnection(options), CONTENT_AUTOTAG_PATH, body);
|
|
1394
1424
|
if (options.format === "json")
|
|
1395
1425
|
return printJson(result);
|
|
1396
1426
|
const plural = (n) => `${n} ${n === 1 ? "item" : "items"}`;
|
|
@@ -1415,7 +1445,7 @@ const commands = {
|
|
|
1415
1445
|
async run(args, options) {
|
|
1416
1446
|
if (args.length !== 1)
|
|
1417
1447
|
throw new CliError("Usage: facts show <id>", exitCodes.usage);
|
|
1418
|
-
const { fact } = await api(await resolveConnection(options
|
|
1448
|
+
const { fact } = await api(await resolveConnection(options), `${FACTS_PATH}/${encodeURIComponent(args[0])}`);
|
|
1419
1449
|
if (options.format === "json")
|
|
1420
1450
|
return printJson({ fact });
|
|
1421
1451
|
const nameKey = fact.key === NAME_KEY ? nameKeyLines(fact.value) : null;
|
|
@@ -1430,7 +1460,7 @@ const commands = {
|
|
|
1430
1460
|
async run(args, options) {
|
|
1431
1461
|
if (args.length > 0)
|
|
1432
1462
|
throw new CliError("Usage: facts propose --file <fact.json> | < fact.json", exitCodes.usage);
|
|
1433
|
-
const { fact } = await api(await resolveConnection(options
|
|
1463
|
+
const { fact } = await api(await resolveConnection(options), FACTS_PATH, await readToolInput(options));
|
|
1434
1464
|
if (options.format === "json")
|
|
1435
1465
|
return printJson({ fact });
|
|
1436
1466
|
console.log(`Proposed fact ${fact.id}. A person confirms or rejects it in the app.`);
|
|
@@ -1450,7 +1480,7 @@ const commands = {
|
|
|
1450
1480
|
for (const dimension of options.dimension ?? [])
|
|
1451
1481
|
params.append("dimension", dimension);
|
|
1452
1482
|
const query = params.size > 0 ? `?${params}` : "";
|
|
1453
|
-
const { lessons } = await api(await resolveConnection(options
|
|
1483
|
+
const { lessons } = await api(await resolveConnection(options), `${LESSONS_PATH}${query}`);
|
|
1454
1484
|
if (options.format === "json")
|
|
1455
1485
|
return printJson({ lessons });
|
|
1456
1486
|
if (lessons.length === 0) {
|
|
@@ -1465,7 +1495,7 @@ const commands = {
|
|
|
1465
1495
|
async run(args, options) {
|
|
1466
1496
|
if (args.length > 0)
|
|
1467
1497
|
throw new CliError("Usage: lessons add --file <lesson.json> | < lesson.json", exitCodes.usage);
|
|
1468
|
-
const recorded = await api(await resolveConnection(options
|
|
1498
|
+
const recorded = await api(await resolveConnection(options), LESSONS_PATH, await readToolInput(options));
|
|
1469
1499
|
if (options.format === "json")
|
|
1470
1500
|
return printJson(recorded);
|
|
1471
1501
|
const { lesson, outcome } = recorded;
|
|
@@ -1489,7 +1519,7 @@ const commands = {
|
|
|
1489
1519
|
query.set("funnel", options.funnel);
|
|
1490
1520
|
if (options.limit)
|
|
1491
1521
|
query.set("limit", options.limit);
|
|
1492
|
-
const { tests } = await api(await resolveConnection(options
|
|
1522
|
+
const { tests } = await api(await resolveConnection(options), `${TESTS_PATH}${query.size ? `?${query}` : ""}`);
|
|
1493
1523
|
if (options.format === "json")
|
|
1494
1524
|
return printJson({ tests });
|
|
1495
1525
|
if (tests.length === 0)
|
|
@@ -1502,7 +1532,7 @@ const commands = {
|
|
|
1502
1532
|
async run(args, options) {
|
|
1503
1533
|
if (args.length !== 1)
|
|
1504
1534
|
throw new CliError("Usage: tests show <id|test_id>", exitCodes.usage);
|
|
1505
|
-
const { test } = await api(await resolveConnection(options
|
|
1535
|
+
const { test } = await api(await resolveConnection(options), testPath(args[0]));
|
|
1506
1536
|
if (options.format === "json")
|
|
1507
1537
|
return printJson({ test });
|
|
1508
1538
|
printTest(test);
|
|
@@ -1513,7 +1543,7 @@ const commands = {
|
|
|
1513
1543
|
async run(args, options) {
|
|
1514
1544
|
if (args.length > 0)
|
|
1515
1545
|
throw new CliError("Usage: tests draft --file <test.json> | < test.json", exitCodes.usage);
|
|
1516
|
-
const { test } = await api(await resolveConnection(options
|
|
1546
|
+
const { test } = await api(await resolveConnection(options), TESTS_PATH, await readToolInput(options));
|
|
1517
1547
|
if (options.format === "json")
|
|
1518
1548
|
return printJson({ test });
|
|
1519
1549
|
console.log(`Drafted test ${test.test_id} (${test.id}).`);
|
|
@@ -1524,7 +1554,7 @@ const commands = {
|
|
|
1524
1554
|
async run(args, options) {
|
|
1525
1555
|
if (args.length !== 1)
|
|
1526
1556
|
throw new CliError("Usage: tests start <id|test_id>", exitCodes.usage);
|
|
1527
|
-
const { test } = await api(await resolveConnection(options
|
|
1557
|
+
const { test } = await api(await resolveConnection(options), `${testPath(args[0])}/start`, {});
|
|
1528
1558
|
if (options.format === "json")
|
|
1529
1559
|
return printJson({ test });
|
|
1530
1560
|
console.log(`Test ${test.test_id} is running from ${test.started_on}.`);
|
|
@@ -1535,7 +1565,7 @@ const commands = {
|
|
|
1535
1565
|
async run(args, options) {
|
|
1536
1566
|
if (args.length !== 1)
|
|
1537
1567
|
throw new CliError('Usage: tests abandon <id|test_id> [--note "<why>"]', exitCodes.usage);
|
|
1538
|
-
const { test } = await api(await resolveConnection(options
|
|
1568
|
+
const { test } = await api(await resolveConnection(options), `${testPath(args[0])}/abandon`, options.note ? { note: options.note } : {});
|
|
1539
1569
|
if (options.format === "json")
|
|
1540
1570
|
return printJson({ test });
|
|
1541
1571
|
console.log(`Abandoned test ${test.test_id}.`);
|
|
@@ -1546,7 +1576,7 @@ const commands = {
|
|
|
1546
1576
|
async run(args, options) {
|
|
1547
1577
|
if (args.length !== 1)
|
|
1548
1578
|
throw new CliError("Usage: tests results <id|test_id>", exitCodes.usage);
|
|
1549
|
-
const { results } = await api(await resolveConnection(options
|
|
1579
|
+
const { results } = await api(await resolveConnection(options), `${testPath(args[0])}/results`);
|
|
1550
1580
|
if (options.format === "json")
|
|
1551
1581
|
return printJson({ results });
|
|
1552
1582
|
printResults(results);
|
|
@@ -1558,7 +1588,7 @@ const commands = {
|
|
|
1558
1588
|
if (args.length !== 1 || !options.decision) {
|
|
1559
1589
|
throw new CliError('Usage: tests decide <id|test_id> --decision <scale|continue|iterate|hold|stop> [--note "<why>"]', exitCodes.usage);
|
|
1560
1590
|
}
|
|
1561
|
-
const decided = await api(await resolveConnection(options
|
|
1591
|
+
const decided = await api(await resolveConnection(options), `${testPath(args[0])}/decide`, options.note ? { decision: options.decision, note: options.note } : { decision: options.decision });
|
|
1562
1592
|
if (options.format === "json")
|
|
1563
1593
|
return printJson(decided);
|
|
1564
1594
|
const { test, lesson, next_test: next } = decided;
|
|
@@ -1574,7 +1604,7 @@ const commands = {
|
|
|
1574
1604
|
async run(args, options) {
|
|
1575
1605
|
if (args.length !== 1 || !options.note)
|
|
1576
1606
|
throw new CliError('Usage: lessons retire <id> --note "<why it is no longer true>"', exitCodes.usage);
|
|
1577
|
-
const { lesson } = await api(await resolveConnection(options
|
|
1607
|
+
const { lesson } = await api(await resolveConnection(options), `${LESSONS_PATH}/${encodeURIComponent(args[0])}/retire`, { reason: options.note });
|
|
1578
1608
|
if (options.format === "json")
|
|
1579
1609
|
return printJson({ lesson });
|
|
1580
1610
|
console.log(`Retired lesson ${lesson.id}.`);
|
|
@@ -1585,7 +1615,7 @@ const commands = {
|
|
|
1585
1615
|
async run(args, options) {
|
|
1586
1616
|
if (args.length > 0)
|
|
1587
1617
|
throw new CliError("Usage: grammar | grammar propose --file <values.json>", exitCodes.usage);
|
|
1588
|
-
const answer = await api(await resolveConnection(options
|
|
1618
|
+
const answer = await api(await resolveConnection(options), GRAMMAR_PATH);
|
|
1589
1619
|
if (options.format === "json")
|
|
1590
1620
|
return printJson(answer);
|
|
1591
1621
|
process.stdout.write(answer.text);
|
|
@@ -1596,7 +1626,7 @@ const commands = {
|
|
|
1596
1626
|
async run(args, options) {
|
|
1597
1627
|
if (args.length > 0)
|
|
1598
1628
|
throw new CliError("Usage: grammar propose --file <values.json> | < values.json", exitCodes.usage);
|
|
1599
|
-
const result = await api(await resolveConnection(options
|
|
1629
|
+
const result = await api(await resolveConnection(options), `${GRAMMAR_PATH}/proposals`, await readToolInput(options));
|
|
1600
1630
|
if (options.format === "json")
|
|
1601
1631
|
return printJson(result);
|
|
1602
1632
|
const named = (refs) => refs.map((r) => `${r.part} ${r.key}`).join(", ");
|
|
@@ -1610,7 +1640,7 @@ const commands = {
|
|
|
1610
1640
|
async run(args, options) {
|
|
1611
1641
|
if (args.length > 0)
|
|
1612
1642
|
throw new CliError("Usage: voice lists", exitCodes.usage);
|
|
1613
|
-
const lists = await api(await resolveConnection(options
|
|
1643
|
+
const lists = await api(await resolveConnection(options), VOICE_LISTS_PATH);
|
|
1614
1644
|
if (options.format === "json")
|
|
1615
1645
|
return printJson(lists);
|
|
1616
1646
|
const rows = [
|
|
@@ -1631,7 +1661,7 @@ const commands = {
|
|
|
1631
1661
|
if (!kind || !["competitor", "subreddit", "search_phrase"].includes(kind) || value === undefined || rest.length > 0) {
|
|
1632
1662
|
throw new CliError('Usage: voice suggest competitor|subreddit|search_phrase "<value>" [--note <why>]; put a value with spaces in quotes', exitCodes.usage);
|
|
1633
1663
|
}
|
|
1634
|
-
const result = await api(await resolveConnection(options
|
|
1664
|
+
const result = await api(await resolveConnection(options), VOICE_SUGGESTIONS_PATH, { kind, value, note: options.note });
|
|
1635
1665
|
if (options.format === "json")
|
|
1636
1666
|
return printJson(result);
|
|
1637
1667
|
if (result.outcome === "already_listed")
|
|
@@ -1646,7 +1676,7 @@ const commands = {
|
|
|
1646
1676
|
async run(args, options) {
|
|
1647
1677
|
if (args.length > 0)
|
|
1648
1678
|
throw new CliError("Usage: story | story list | story show <version> | story submit --file <story.json>", exitCodes.usage);
|
|
1649
|
-
const { story } = await api(await resolveConnection(options
|
|
1679
|
+
const { story } = await api(await resolveConnection(options), `${STORIES_PATH}/latest`);
|
|
1650
1680
|
if (options.format === "json")
|
|
1651
1681
|
return printJson({ story });
|
|
1652
1682
|
if (!story)
|
|
@@ -1659,7 +1689,7 @@ const commands = {
|
|
|
1659
1689
|
async run(args, options) {
|
|
1660
1690
|
if (args.length > 0)
|
|
1661
1691
|
throw new CliError("Usage: story list", exitCodes.usage);
|
|
1662
|
-
const { stories } = await api(await resolveConnection(options
|
|
1692
|
+
const { stories } = await api(await resolveConnection(options), STORIES_PATH);
|
|
1663
1693
|
if (options.format === "json")
|
|
1664
1694
|
return printJson({ stories });
|
|
1665
1695
|
if (stories.length === 0)
|
|
@@ -1682,7 +1712,7 @@ const commands = {
|
|
|
1682
1712
|
if (args.length !== 1)
|
|
1683
1713
|
throw new CliError("Usage: story show <version>", exitCodes.usage);
|
|
1684
1714
|
// `show latest` reaches the newest-story route, which answers null before the first story.
|
|
1685
|
-
const { story } = await api(await resolveConnection(options
|
|
1715
|
+
const { story } = await api(await resolveConnection(options), `${STORIES_PATH}/${encodeURIComponent(args[0])}`);
|
|
1686
1716
|
if (options.format === "json")
|
|
1687
1717
|
return printJson({ story });
|
|
1688
1718
|
if (!story)
|
|
@@ -1695,7 +1725,7 @@ const commands = {
|
|
|
1695
1725
|
async run(args, options) {
|
|
1696
1726
|
if (args.length > 0)
|
|
1697
1727
|
throw new CliError("Usage: story submit --file <story.json> | < story.json", exitCodes.usage);
|
|
1698
|
-
const { story } = await api(await resolveConnection(options
|
|
1728
|
+
const { story } = await api(await resolveConnection(options), STORIES_PATH, await readToolInput(options));
|
|
1699
1729
|
if (options.format === "json")
|
|
1700
1730
|
return printJson({ story });
|
|
1701
1731
|
console.log(`Saved story version ${story.version}.`);
|
|
@@ -1707,7 +1737,7 @@ const commands = {
|
|
|
1707
1737
|
if (args.length > 0 || !options.name || !options.goal || !options.channel) {
|
|
1708
1738
|
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);
|
|
1709
1739
|
}
|
|
1710
|
-
const { plan } = await api(await resolveConnection(options
|
|
1740
|
+
const { plan } = await api(await resolveConnection(options), "/api/v1/launch-plans", {
|
|
1711
1741
|
name: options.name,
|
|
1712
1742
|
goal: await textOrFile(options.goal),
|
|
1713
1743
|
channel: options.channel,
|
|
@@ -1726,7 +1756,7 @@ const commands = {
|
|
|
1726
1756
|
async run(args, options) {
|
|
1727
1757
|
if (args.length !== 1)
|
|
1728
1758
|
throw new CliError("Usage: plans show <plan-id>", exitCodes.usage);
|
|
1729
|
-
const { plan } = await api(await resolveConnection(options
|
|
1759
|
+
const { plan } = await api(await resolveConnection(options), planPath(args[0]));
|
|
1730
1760
|
printPlan(plan, options.format);
|
|
1731
1761
|
},
|
|
1732
1762
|
},
|
|
@@ -1738,7 +1768,7 @@ const commands = {
|
|
|
1738
1768
|
const position = options.position === undefined ? undefined : Number(options.position);
|
|
1739
1769
|
if (position !== undefined && (!Number.isInteger(position) || position < 0))
|
|
1740
1770
|
throw new CliError("--position must be a whole number from 0.", exitCodes.usage);
|
|
1741
|
-
const { plan } = await api(await resolveConnection(options
|
|
1771
|
+
const { plan } = await api(await resolveConnection(options), `${planPath(args[0])}/items`, {
|
|
1742
1772
|
item: args[1],
|
|
1743
1773
|
...(options.role === undefined ? {} : { role: options.role }),
|
|
1744
1774
|
...(position === undefined ? {} : { position }),
|
|
@@ -1752,7 +1782,7 @@ const commands = {
|
|
|
1752
1782
|
if (args.length !== 2)
|
|
1753
1783
|
throw new CliError("Usage: plans remove <plan-id> <sku | item-id>", exitCodes.usage);
|
|
1754
1784
|
const path = `${planPath(args[0])}/items?item=${encodeURIComponent(args[1])}`;
|
|
1755
|
-
const { plan } = await api(await resolveConnection(options
|
|
1785
|
+
const { plan } = await api(await resolveConnection(options), path, undefined, "DELETE");
|
|
1756
1786
|
printPlan(plan, options.format);
|
|
1757
1787
|
},
|
|
1758
1788
|
},
|
|
@@ -1761,7 +1791,7 @@ const commands = {
|
|
|
1761
1791
|
async run(args, options) {
|
|
1762
1792
|
if (args.length !== 2)
|
|
1763
1793
|
throw new CliError("Usage: plans status <plan-id> draft|ready|publishing|live|done|cancelled", exitCodes.usage);
|
|
1764
|
-
const { plan } = await api(await resolveConnection(options
|
|
1794
|
+
const { plan } = await api(await resolveConnection(options), planPath(args[0]), { status: args[1] }, "PATCH");
|
|
1765
1795
|
printPlan(plan, options.format);
|
|
1766
1796
|
},
|
|
1767
1797
|
},
|
|
@@ -1770,7 +1800,7 @@ const commands = {
|
|
|
1770
1800
|
async run(args, options) {
|
|
1771
1801
|
if (args.length > 0)
|
|
1772
1802
|
throw new CliError("Usage: proposals create --input '<json>'", exitCodes.usage);
|
|
1773
|
-
const { proposal } = await api(await resolveConnection(options
|
|
1803
|
+
const { proposal } = await api(await resolveConnection(options), "/api/v1/proposals", await readToolInput(options));
|
|
1774
1804
|
if (options.format === "json")
|
|
1775
1805
|
return printJson({ proposal });
|
|
1776
1806
|
console.log(renderTable(PROPOSAL_COLUMNS, [proposalRow(proposal)]));
|
|
@@ -1789,7 +1819,7 @@ const commands = {
|
|
|
1789
1819
|
if (options.limit)
|
|
1790
1820
|
query.set("limit", options.limit);
|
|
1791
1821
|
const path = `/api/v1/proposals${query.size ? `?${query}` : ""}`;
|
|
1792
|
-
const { proposals } = await api(await resolveConnection(options
|
|
1822
|
+
const { proposals } = await api(await resolveConnection(options), path);
|
|
1793
1823
|
if (options.format === "json")
|
|
1794
1824
|
return printJson({ proposals });
|
|
1795
1825
|
if (proposals.length === 0)
|
|
@@ -1817,7 +1847,7 @@ const commands = {
|
|
|
1817
1847
|
if (options.harness !== undefined)
|
|
1818
1848
|
query.set("harnesses", options.harness);
|
|
1819
1849
|
const path = `/api/operator/skills/${name}/versions${query.size ? `?${query}` : ""}`;
|
|
1820
|
-
const published = await apiUpload(await operatorConnection(options
|
|
1850
|
+
const published = await apiUpload(await operatorConnection(options), path, zip, "application/zip");
|
|
1821
1851
|
if (options.format === "json")
|
|
1822
1852
|
return printJson(published);
|
|
1823
1853
|
console.log(`${published.name} version ${published.version}`);
|
|
@@ -1829,7 +1859,7 @@ const commands = {
|
|
|
1829
1859
|
async run(args, options) {
|
|
1830
1860
|
if (args.length > 0)
|
|
1831
1861
|
throw new CliError("Usage: skill list", exitCodes.usage);
|
|
1832
|
-
const { skills } = await api(await operatorConnection(options
|
|
1862
|
+
const { skills } = await api(await operatorConnection(options), "/api/operator/skills");
|
|
1833
1863
|
if (options.format === "json")
|
|
1834
1864
|
return printJson({ skills });
|
|
1835
1865
|
if (skills.length === 0)
|
|
@@ -1869,7 +1899,7 @@ const commands = {
|
|
|
1869
1899
|
async run(args, options) {
|
|
1870
1900
|
if (args.length > 0)
|
|
1871
1901
|
throw new CliError("Usage: worker list", exitCodes.usage);
|
|
1872
|
-
const { workers } = await api(await resolveConnection(options
|
|
1902
|
+
const { workers } = await api(await resolveConnection(options), "/api/v1/workers");
|
|
1873
1903
|
if (options.format === "json")
|
|
1874
1904
|
return printJson({ workers });
|
|
1875
1905
|
if (workers.length === 0)
|
|
@@ -1893,7 +1923,7 @@ const commands = {
|
|
|
1893
1923
|
async run(args, options) {
|
|
1894
1924
|
if (args.length !== 1)
|
|
1895
1925
|
throw new CliError("Usage: worker runs <hand-id | skill> [--limit <n>]", exitCodes.usage);
|
|
1896
|
-
const connection = await resolveConnection(options
|
|
1926
|
+
const connection = await resolveConnection(options);
|
|
1897
1927
|
const id = await workerId(connection, args[0]);
|
|
1898
1928
|
const query = options.limit === undefined ? "" : `?limit=${encodeURIComponent(options.limit)}`;
|
|
1899
1929
|
const { runs } = await api(connection, `/api/v1/workers/${encodeURIComponent(id)}/runs${query}`);
|
|
@@ -1909,7 +1939,7 @@ const commands = {
|
|
|
1909
1939
|
async run(args, options) {
|
|
1910
1940
|
if (args.length !== 1)
|
|
1911
1941
|
throw new CliError("Usage: worker run-now <hand-id | skill>", exitCodes.usage);
|
|
1912
|
-
const connection = await resolveConnection(options
|
|
1942
|
+
const connection = await resolveConnection(options);
|
|
1913
1943
|
const id = await workerId(connection, args[0]);
|
|
1914
1944
|
const queued = await api(connection, `/api/v1/workers/${encodeURIComponent(id)}/run-now`, {});
|
|
1915
1945
|
if (options.format === "json")
|
|
@@ -1969,6 +1999,18 @@ const commands = {
|
|
|
1969
1999
|
},
|
|
1970
2000
|
},
|
|
1971
2001
|
};
|
|
2002
|
+
function clientReason(choice) {
|
|
2003
|
+
switch (choice.source) {
|
|
2004
|
+
case "env":
|
|
2005
|
+
return `Using the key in ${envPrefix}_API_KEY, which beats --client, client files and saved logins.`;
|
|
2006
|
+
case "flag":
|
|
2007
|
+
return `Using client ${choice.client}, from --client.`;
|
|
2008
|
+
case "folder":
|
|
2009
|
+
return `Using client ${choice.client}, from the client file ${choice.path}.`;
|
|
2010
|
+
case "default":
|
|
2011
|
+
return `Using the default login (no --client flag and no ${CLIENT_FILE} file in this folder or above it).`;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
1972
2014
|
/** Runs `work` with the runner's settings from the environment; SIGINT or SIGTERM stops the run in hand, reported failed. */
|
|
1973
2015
|
async function withRunner(options, work) {
|
|
1974
2016
|
const token = process.env[RUNNER_TOKEN_ENV];
|
|
@@ -2008,7 +2050,7 @@ async function installLibrarySkill(ref, options) {
|
|
|
2008
2050
|
if (options.codex && options.project)
|
|
2009
2051
|
throw new CliError("--codex installs a skill for every Codex run, under CODEX_HOME; drop --project", exitCodes.usage);
|
|
2010
2052
|
const query = ref.version === null ? "" : `?version=${ref.version}`;
|
|
2011
|
-
const download = await api(await operatorConnection(options
|
|
2053
|
+
const download = await api(await operatorConnection(options), `/api/operator/skills/${ref.name}/download${query}`);
|
|
2012
2054
|
const files = unpackSkill(download, await getSigned(download.url));
|
|
2013
2055
|
const { skillsDir, above } = options.codex ? { skillsDir: codexSkillsDir(), above: [] } : claudeSkillsDir(options.project);
|
|
2014
2056
|
const path = await installSkillFolder(skillsDir, download.name, files, above);
|
|
@@ -2071,16 +2113,21 @@ function money(value, currency) {
|
|
|
2071
2113
|
/** A model that splits credit between ads gives fractional counts: show tenths at most, grouped as the server writes them. */
|
|
2072
2114
|
const tenths = (n) => n.toLocaleString("en-US", { maximumFractionDigits: 1 });
|
|
2073
2115
|
const counted = (n, one) => `${tenths(n)} ${tenths(n) === "1" ? one : `${one}s`}`;
|
|
2116
|
+
const SALE = { one: "sale", many: "sales" };
|
|
2117
|
+
const FIRST_LEAD = { one: "first lead", many: "first leads" };
|
|
2118
|
+
const winnersWords = (report) => report.goal === undefined ? { primary: SALE, fallback: FIRST_LEAD } : { primary: report.goal, fallback: report.fallbackGoal ?? null };
|
|
2119
|
+
const countOf = (n, { one, many }) => `${tenths(n)} ${tenths(n) === "1" ? one : many}`;
|
|
2074
2120
|
/** An ad's line: name, SKU and campaign, its numbers, its tags, then why it is on the list. */
|
|
2075
|
-
function winnersAdLines(ad, currency) {
|
|
2121
|
+
function winnersAdLines(ad, currency, words) {
|
|
2076
2122
|
const n = ad.numbers;
|
|
2077
2123
|
const onSales = ad.judgedOn === "sales";
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2124
|
+
// Ads are judged on the fallback only when there is one; first leads stand in for a list the server never fills.
|
|
2125
|
+
const goal = onSales ? words.primary : (words.fallback ?? FIRST_LEAD);
|
|
2126
|
+
const [costPer, number] = onSales ? [n.costPerSale, n.sales] : [n.costPerSignup, n.signups];
|
|
2127
|
+
const cost = costPer === null ? `no ${goal.many}` : `${money(costPer, currency)} per ${goal.one}`;
|
|
2081
2128
|
const name = [ad.name ?? `Ad ${ad.adId}`, ad.sku && `[${ad.sku}]`, ad.campaignName && `(${ad.campaignName})`].filter(Boolean).join(" ");
|
|
2082
2129
|
const tags = Object.entries(ad.tags).map(([field, value]) => `${field}: ${value}`).join(", ");
|
|
2083
|
-
const count =
|
|
2130
|
+
const count = number !== null && countOf(number, goal);
|
|
2084
2131
|
const facebook = n.facebookResults == null ? "" : `, Facebook counts ${tenths(n.facebookResults)}`;
|
|
2085
2132
|
return `- ${name}: spent ${money(n.spend, currency)}, ${count ? `${count}, ` : ""}${cost}${facebook}${tags ? `. Tags: ${tags}` : ""}\n ${ad.pickedBecause}\n`;
|
|
2086
2133
|
}
|
|
@@ -2097,14 +2144,14 @@ function winnersPageLines(page, currency) {
|
|
|
2097
2144
|
const bought = onSales && n.salesPerVisit !== null ? `, ${Math.round(n.salesPerVisit * 1000) / 10}% of visitors bought` : "";
|
|
2098
2145
|
return `- ${ad}${name}: ${counted(Math.round(n.visits), "visit")}, spent ${money(n.spend, currency)}, ${count ? `${count}, ` : ""}${cost}${bought}\n ${page.pickedBecause}\n`;
|
|
2099
2146
|
}
|
|
2100
|
-
function winnersWindowText(title, window) {
|
|
2147
|
+
function winnersWindowText(title, window, words) {
|
|
2101
2148
|
const head = `\n## ${title}\n`;
|
|
2102
2149
|
if (window.sections.length === 0)
|
|
2103
2150
|
return `${head}\nNo Facebook ad spent in these dates.\n`;
|
|
2104
2151
|
return (head +
|
|
2105
2152
|
window.sections
|
|
2106
2153
|
.map((s) => {
|
|
2107
|
-
const list = (heading, ads) => `\n${heading}\n${ads.length === 0 ? "- none\n" : ads.map((ad) => winnersAdLines(ad, s.currency)).join("")}`;
|
|
2154
|
+
const list = (heading, ads) => `\n${heading}\n${ads.length === 0 ? "- none\n" : ads.map((ad) => winnersAdLines(ad, s.currency, words)).join("")}`;
|
|
2108
2155
|
const places = (heading, rows) => `\n${heading}\n${rows.length === 0 ? "- none\n" : rows.map((row) => winnersPageLines(row, s.currency)).join("")}`;
|
|
2109
2156
|
const placeLists = (what, lists) => places(`${what}: winners`, lists.winners) +
|
|
2110
2157
|
places(`${what}: flops`, lists.flops) +
|
|
@@ -2115,15 +2162,18 @@ function winnersWindowText(title, window) {
|
|
|
2115
2162
|
? ""
|
|
2116
2163
|
: placeLists("Landing pages", s.pages) +
|
|
2117
2164
|
(any(s.pairs) ? placeLists("Ad and page pairs", s.pairs) : "\nAd and page pairs: none to compare. No ad sent people to two pages, and no page got people from two ads, with enough visits.\n");
|
|
2165
|
+
const { primary, fallback } = words;
|
|
2118
2166
|
const account = s.salesSource === "not_connected"
|
|
2119
|
-
?
|
|
2167
|
+
? `${primary.many} are not connected`
|
|
2120
2168
|
: s.accountCostPerSale === null
|
|
2121
|
-
?
|
|
2122
|
-
: `the account paid ${money(s.accountCostPerSale, s.currency)} per
|
|
2123
|
-
const
|
|
2124
|
-
const
|
|
2125
|
-
|
|
2126
|
-
|
|
2169
|
+
? `no ${primary.one} credited to a Facebook ad in these dates`
|
|
2170
|
+
: `the account paid ${money(s.accountCostPerSale, s.currency)} per ${primary.one}`;
|
|
2171
|
+
const facebookWords = { one: `Facebook ${primary.one}`, many: `Facebook ${primary.many}` };
|
|
2172
|
+
const tied = s.adSales === null || !s.facebookSales ? "" : ` ${tenths(s.adSales)} of ${countOf(s.facebookSales, facebookWords)} could be tied to a specific ad.`;
|
|
2173
|
+
const tests = s.tests.map((t) => `- Test ${t.testId}${t.hypothesisId ? ` (idea ${t.hypothesisId})` : ""}: ${counted(t.ads.length, "ad")}, spent ${money(t.spend, s.currency)}, ${countOf(t.sales, primary)}` +
|
|
2174
|
+
`${t.costPerSale === null ? "" : `, ${money(t.costPerSale, s.currency)} per ${primary.one}`}\n`);
|
|
2175
|
+
const onFallback = fallback === null ? "" : `, ${s.adsJudgedOnSignups} judged on ${fallback.many}`;
|
|
2176
|
+
return (`\nIn ${s.currency}: ${account}.${tied} ${s.adsJudgedOnSales} judged on ${primary.many}${onFallback}.\n` +
|
|
2127
2177
|
s.notes.map((note) => `${note}\n`).join("") +
|
|
2128
2178
|
list("Winners", s.winners) +
|
|
2129
2179
|
list("Flops", s.flops) +
|
|
@@ -2173,18 +2223,37 @@ function winnersSendText(sent) {
|
|
|
2173
2223
|
lines.push("Left out:", ...sent.skipped.map((s) => ` ${s.adId}: ${s.reason}`));
|
|
2174
2224
|
return `${lines.join("\n")}\n`;
|
|
2175
2225
|
}
|
|
2176
|
-
/** How an older server, which sent no rule, credited a sale. */
|
|
2226
|
+
/** How an older server, which sent no rule and judged only on sales, credited a sale. */
|
|
2177
2227
|
const FIRST_TOUCH_SALE_RULE = "Each sale goes to the Facebook ad that first brought the buyer in; payments typed into GoHighLevel by hand never count toward an ad.";
|
|
2178
2228
|
/**
|
|
2179
|
-
* The winners report as plain Markdown: what
|
|
2180
|
-
* the last 30 days, each currency apart.
|
|
2229
|
+
* The winners report as plain Markdown: which goal the ads are judged on and what one means, then the dates asked for and,
|
|
2230
|
+
* unless they are the same dates, the last 30 days, each currency apart.
|
|
2181
2231
|
*/
|
|
2182
2232
|
function winnersText(report) {
|
|
2183
2233
|
const { week, last30Days } = report;
|
|
2234
|
+
const words = winnersWords(report);
|
|
2184
2235
|
return (`# Winners and flops for ${report.workspace}${report.format ? `, ${report.format} ads only` : ""}\n\n` +
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
(
|
|
2236
|
+
(report.goalSentence ? `${report.goalSentence}\n` : "") +
|
|
2237
|
+
`A ${words.primary.one} means ${report.saleMeans}. ${report.saleRule ?? FIRST_TOUCH_SALE_RULE}\n` +
|
|
2238
|
+
winnersWindowText(winnersDatesTitle(report.window), week, words) +
|
|
2239
|
+
(week.from === last30Days.from && week.to === last30Days.to ? "" : winnersWindowText(`Last 30 days, ${last30Days.from} to ${last30Days.to}`, last30Days, words)));
|
|
2240
|
+
}
|
|
2241
|
+
function printWinnersGoals(goals, format) {
|
|
2242
|
+
if (format === "json")
|
|
2243
|
+
return printJson(goals);
|
|
2244
|
+
process.stdout.write(winnersGoalsText(goals));
|
|
2245
|
+
}
|
|
2246
|
+
/** The goals in order, each with whether its data is connected, then which one winners judges on now and why. */
|
|
2247
|
+
function winnersGoalsText({ goals, isDefault, chosen }) {
|
|
2248
|
+
const rows = goals.map((g, i) => ({
|
|
2249
|
+
order: String(i + 1),
|
|
2250
|
+
goal: g.source === "first_touch" || g.source === "meta" ? g.kind : `${g.kind}:${g.source}`,
|
|
2251
|
+
counts: g.label,
|
|
2252
|
+
data: g.measurable ? "connected" : "not connected",
|
|
2253
|
+
}));
|
|
2254
|
+
return (`${renderTable(["order", "goal", "counts", "data"], rows)}\n\n` +
|
|
2255
|
+
`${isDefault ? "This is the default list; no goals have been set for this client." : "These goals were set for this client."}\n` +
|
|
2256
|
+
`${chosen.sentence}\n`);
|
|
2188
2257
|
}
|
|
2189
2258
|
function reportQuery(options, extra) {
|
|
2190
2259
|
const query = new URLSearchParams();
|
|
@@ -2458,6 +2527,8 @@ ${Object.values(commands)
|
|
|
2458
2527
|
Options:
|
|
2459
2528
|
--format table|json output format (default: table)
|
|
2460
2529
|
--api-url <url> API base URL
|
|
2530
|
+
--client <name> use that client's saved login (login --client <name> saves it); beats a ${CLIENT_FILE} file, which
|
|
2531
|
+
names the client for its folder and every folder below it; ${envPrefix}_API_KEY beats both
|
|
2461
2532
|
--wait <seconds> how long tools run and tools get wait for a run (default: ${DEFAULT_WAIT_SECONDS}; 0 returns at once)
|
|
2462
2533
|
--agent <kind> which agent the agent commands use: meta_ads (default) or p3_refresh; in a scheduled hand's run, skill
|
|
2463
2534
|
(the run's skill's agent) is the default
|
|
@@ -2549,6 +2620,12 @@ ${envPrefix}_OPERATOR_TOKEN instead of an API key, as do worker promote and work
|
|
|
2549
2620
|
version a scheduled hand is trying the one in use, there and on every hand of the skill on an older version; worker
|
|
2550
2621
|
drop stops trying it.
|
|
2551
2622
|
|
|
2623
|
+
winners judges a client's Facebook ads on its goals, tried in order: the first goal whose data is connected is the one
|
|
2624
|
+
used, and ads too new to judge on it are judged on the next connected goal of another kind. sales means first-touch
|
|
2625
|
+
sales from every connected payment source; to judge on Stripe sales, connect Stripe, then put sales first.
|
|
2626
|
+
sales:triple_whale means the orders Triple Whale credits to each ad. Meta's applications (submit_application_website)
|
|
2627
|
+
are judged through meta_results once the Meta source's result types are set to them.
|
|
2628
|
+
|
|
2552
2629
|
runner and runner once take the runner token in ${RUNNER_TOKEN_ENV}. ${HARNESS_COMMAND_ENV}, if set, names a program
|
|
2553
2630
|
the runner starts instead of Claude Code or Codex.
|
|
2554
2631
|
|
|
@@ -2562,6 +2639,7 @@ function parseOptions(argv) {
|
|
|
2562
2639
|
format: { type: "string", default: "table" },
|
|
2563
2640
|
json: { type: "boolean", default: false },
|
|
2564
2641
|
"api-url": { type: "string" },
|
|
2642
|
+
client: { type: "string" },
|
|
2565
2643
|
file: { type: "string", multiple: true },
|
|
2566
2644
|
project: { type: "boolean", default: false },
|
|
2567
2645
|
input: { type: "string" },
|
|
@@ -2670,6 +2748,7 @@ function parseOptions(argv) {
|
|
|
2670
2748
|
options: {
|
|
2671
2749
|
format: values.json ? "json" : values.format,
|
|
2672
2750
|
apiUrl: values["api-url"],
|
|
2751
|
+
client: values.client,
|
|
2673
2752
|
file: values.file?.at(-1),
|
|
2674
2753
|
files: values.file ?? [],
|
|
2675
2754
|
project: values.project,
|
|
@@ -2765,7 +2844,7 @@ function parseOptions(argv) {
|
|
|
2765
2844
|
async function changeCandidate(change, args, options) {
|
|
2766
2845
|
if (args.length !== 1 || !options.workspace)
|
|
2767
2846
|
throw new CliError(`Usage: worker ${change} <skill> --workspace <slug>`, exitCodes.usage);
|
|
2768
|
-
return api(await operatorConnection(options
|
|
2847
|
+
return api(await operatorConnection(options), `/api/operator/workers/${change}`, { workspace: options.workspace, skill: args[0] });
|
|
2769
2848
|
}
|
|
2770
2849
|
function findCommand(positionals) {
|
|
2771
2850
|
const three = positionals.slice(0, 3).join(" ");
|
|
@@ -3514,7 +3593,7 @@ function printIntakeToken({ endpoint, address, token, link, subscription }, form
|
|
|
3514
3593
|
console.error(`The token for ${endpoint.label} is shown only now. Rotate it if it is lost.`);
|
|
3515
3594
|
}
|
|
3516
3595
|
async function patchIntake(id, body, options, done) {
|
|
3517
|
-
const { endpoint } = await api(await resolveConnection(options
|
|
3596
|
+
const { endpoint } = await api(await resolveConnection(options), intakePath(id), body, "PATCH");
|
|
3518
3597
|
if (options.format === "json")
|
|
3519
3598
|
return printJson({ endpoint });
|
|
3520
3599
|
console.log(done(endpoint));
|