@koda-sl/baker-cli 0.202.0 → 0.204.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/README.md +22 -0
- package/dist/cli.js +252 -28
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,8 +86,20 @@ All commands return a JSON envelope:
|
|
|
86
86
|
|
|
87
87
|
// Dry-run
|
|
88
88
|
{ "ok": true, "dryRun": true, "operation": "images.delete", "params": { "id": "abc123" } }
|
|
89
|
+
|
|
90
|
+
// Unknown option — the command stops instead of running without it
|
|
91
|
+
{ "ok": false, "error": { "code": "UNKNOWN_ARGUMENT", "message": "--dimension-filter is not an option of `baker ga4 query`.", "fix": { "action": "check_schema", "explanation": "Did you mean --dimensions? Run `baker schema ga4 query` ..." }, "retryable": false } }
|
|
89
92
|
```
|
|
90
93
|
|
|
94
|
+
### Unknown options are refused
|
|
95
|
+
|
|
96
|
+
An option a command does not declare stops the run with `UNKNOWN_ARGUMENT`, naming the
|
|
97
|
+
closest real one. Previously such an option was accepted and ignored, so a call that
|
|
98
|
+
looked like it had filtered or narrowed a result was in fact answering the unfiltered
|
|
99
|
+
question — and nothing in the output said so. Both spellings of an option work
|
|
100
|
+
(`--run-id` and `--runId`), as they always did. Run `baker schema <command>` to see
|
|
101
|
+
every option a command takes.
|
|
102
|
+
|
|
91
103
|
Use `--output` to change format:
|
|
92
104
|
|
|
93
105
|
| Format | Description | Best for | Available on |
|
|
@@ -2505,6 +2517,16 @@ List testimonials with optional filters.
|
|
|
2505
2517
|
baker testimonials list --source google --sentiment positive --limit 20
|
|
2506
2518
|
```
|
|
2507
2519
|
|
|
2520
|
+
When `list` or `search` returns nothing, the envelope carries `meta.empty` explaining why, checked against the company's connected review sources rather than inferred from the empty result:
|
|
2521
|
+
|
|
2522
|
+
| `meta.empty.reason` | Meaning | What to do |
|
|
2523
|
+
| --- | --- | --- |
|
|
2524
|
+
| `filtered_out` | Filters were applied, so the corpus itself was not examined | Drop every filter and re-run once |
|
|
2525
|
+
| `no_corpus_connected` | The company has no review source connected | Terminal — source proof another way; no rewording will help |
|
|
2526
|
+
| `corpus_empty` | A source is connected but holds no testimonials yet | Ingestion may still be running; do not report the client as having no reviews |
|
|
2527
|
+
|
|
2528
|
+
If the source check itself fails, `meta.empty` is omitted entirely and the hint says so — an unverified absence is never reported as one.
|
|
2529
|
+
|
|
2508
2530
|
### `baker testimonials tags`
|
|
2509
2531
|
|
|
2510
2532
|
List the available testimonial tag names — built-in defaults plus the company's custom tags. Use it before filtering with `--tags`. Defaults to a markdown list (`--output json` for the `{ ok, data }` envelope).
|
package/dist/cli.js
CHANGED
|
@@ -18463,7 +18463,7 @@ Performance (the workhorse):
|
|
|
18463
18463
|
baker ads meta insights --object act_123 --level ad --intent ranking \u2014 flag creatives to refresh
|
|
18464
18464
|
baker ads meta insights --object act_123 --level adset --breakdowns publisher_platform,age \u2014 placement \xD7 age
|
|
18465
18465
|
baker ads meta insights --object act_123 --level ad --since 2026-01-01 --until 2026-03-31 \u2014 auto-async
|
|
18466
|
-
baker ads meta insights --list-
|
|
18466
|
+
baker ads meta insights --list-intents
|
|
18467
18467
|
|
|
18468
18468
|
Audit & review:
|
|
18469
18469
|
baker ads meta activities --account-id act_123 --days 14
|
|
@@ -40550,7 +40550,7 @@ registerSchema({
|
|
|
40550
40550
|
},
|
|
40551
40551
|
"from-video": {
|
|
40552
40552
|
type: "string",
|
|
40553
|
-
description: "The same, but from the VIDEO LIBRARY: comma-separated video ids (see `baker videos
|
|
40553
|
+
description: "The same, but from the VIDEO LIBRARY: comma-separated video ids (see `baker videos search`) to build on. Use this for footage the client uploaded or synced rather than something the Studio rendered. Counts against the same 10 source clips as --from-clip. The first use of a given video spends a minute encoding a reusable copy and is refused with a 'try again shortly' message; every use after that is instant.",
|
|
40554
40554
|
required: false
|
|
40555
40555
|
},
|
|
40556
40556
|
model: {
|
|
@@ -42193,26 +42193,85 @@ var getCommand5 = defineCommand193({
|
|
|
42193
42193
|
import { defineCommand as defineCommand194 } from "citty";
|
|
42194
42194
|
|
|
42195
42195
|
// src/commands/testimonials/emptyCorpusHints.ts
|
|
42196
|
-
|
|
42197
|
-
|
|
42198
|
-
|
|
42199
|
-
|
|
42200
|
-
|
|
42201
|
-
search: "No hits. Search reranks the whole corpus, so an empty result is nearly always an empty library rather than a missed query \u2014 confirm once with `baker testimonials list --limit 1`. If that is empty too, ",
|
|
42202
|
-
list: "No rows on an unfiltered browse, which is proof rather than a hint: "
|
|
42203
|
-
};
|
|
42204
|
-
function emptyCorpusHints({ command, resultCount, activeFilters }) {
|
|
42196
|
+
function resolveEmptyReason({
|
|
42197
|
+
resultCount,
|
|
42198
|
+
activeFilters,
|
|
42199
|
+
sourceCount
|
|
42200
|
+
}) {
|
|
42205
42201
|
if (resultCount > 0) {
|
|
42206
|
-
return
|
|
42202
|
+
return null;
|
|
42207
42203
|
}
|
|
42208
|
-
const hints = [];
|
|
42209
42204
|
if (activeFilters.length > 0) {
|
|
42210
|
-
|
|
42205
|
+
return "filtered_out";
|
|
42206
|
+
}
|
|
42207
|
+
return sourceCount === 0 ? "no_corpus_connected" : "corpus_empty";
|
|
42208
|
+
}
|
|
42209
|
+
var NO_CORPUS_EXIT = 'Testimonials are ingested from the client\'s Google/Trustpilot reviews \u2014 an agent cannot write one \u2014 so no rewording will find quotes. Stop searching and source proof another way: a result or case study the client already published, a named-customer logo (`baker images logo`), or a founder claim attributed as a claim. Otherwise ship without a testimonial block and file the gap: `baker actions create --name "Collect customer quotes for <page>" --priority medium --tags <slug>`. Never invent, paraphrase-into-existence, or pad with unrelated praise.';
|
|
42210
|
+
function emptyReasonDetail(reason, sourceCount, activeFilters) {
|
|
42211
|
+
if (reason === "filtered_out") {
|
|
42212
|
+
return `No rows matched, and ${activeFilters.join(", ")} narrowed this read. The corpus was not checked \u2014 drop every filter to see whether the library itself is empty.`;
|
|
42213
|
+
}
|
|
42214
|
+
if (reason === "no_corpus_connected") {
|
|
42215
|
+
return "This company has no review source connected \u2014 checked against its source list, not inferred from the empty result.";
|
|
42216
|
+
}
|
|
42217
|
+
return `${sourceCount} review source${sourceCount === 1 ? " is" : "s are"} connected but hold no testimonials yet \u2014 the source is there, the rows are not. Ingestion may still be running.`;
|
|
42218
|
+
}
|
|
42219
|
+
async function measureEmptyCorpus({
|
|
42220
|
+
command,
|
|
42221
|
+
resultCount,
|
|
42222
|
+
activeFilters
|
|
42223
|
+
}) {
|
|
42224
|
+
if (resultCount > 0) {
|
|
42225
|
+
return { hints: [] };
|
|
42226
|
+
}
|
|
42227
|
+
if (activeFilters.length > 0) {
|
|
42228
|
+
const reason2 = "filtered_out";
|
|
42229
|
+
return {
|
|
42230
|
+
hints: emptyCorpusHints({ command, reason: reason2, sourceCount: 0, activeFilters }),
|
|
42231
|
+
empty: { reason: reason2, detail: emptyReasonDetail(reason2, 0, activeFilters) }
|
|
42232
|
+
};
|
|
42233
|
+
}
|
|
42234
|
+
let sourceCount;
|
|
42235
|
+
try {
|
|
42236
|
+
const sources = await apiGet("/api/testimonials/sources");
|
|
42237
|
+
sourceCount = Array.isArray(sources) ? sources.length : 0;
|
|
42238
|
+
} catch {
|
|
42239
|
+
return {
|
|
42240
|
+
hints: [
|
|
42241
|
+
"No rows, and the check for connected review sources failed \u2014 so this is NOT evidence that the client has no reviews. Do not record an absence you could not verify. Re-run once; if it fails again, say the library could not be read rather than that it is empty."
|
|
42242
|
+
]
|
|
42243
|
+
};
|
|
42244
|
+
}
|
|
42245
|
+
const reason = resolveEmptyReason({ resultCount, activeFilters, sourceCount });
|
|
42246
|
+
if (reason === null) {
|
|
42247
|
+
return { hints: [] };
|
|
42248
|
+
}
|
|
42249
|
+
return {
|
|
42250
|
+
hints: emptyCorpusHints({ command, reason, sourceCount, activeFilters }),
|
|
42251
|
+
empty: { reason, detail: emptyReasonDetail(reason, sourceCount, activeFilters) }
|
|
42252
|
+
};
|
|
42253
|
+
}
|
|
42254
|
+
function emptyCorpusHints({
|
|
42255
|
+
command,
|
|
42256
|
+
reason,
|
|
42257
|
+
sourceCount,
|
|
42258
|
+
activeFilters
|
|
42259
|
+
}) {
|
|
42260
|
+
if (reason === null) {
|
|
42261
|
+
return [];
|
|
42262
|
+
}
|
|
42263
|
+
if (reason === "filtered_out") {
|
|
42264
|
+
return [
|
|
42211
42265
|
`No rows \u2014 ${activeFilters.join(", ")} narrowed this. Drop every filter and re-run once before touching the query; a filter combination and an empty library look identical from here.`
|
|
42212
|
-
|
|
42266
|
+
];
|
|
42213
42267
|
}
|
|
42214
|
-
|
|
42215
|
-
|
|
42268
|
+
const lead = command === "search" ? "No hits" : "No rows";
|
|
42269
|
+
if (reason === "no_corpus_connected") {
|
|
42270
|
+
return [`${lead}, and this company's source list is empty: there is no review corpus to search. ${NO_CORPUS_EXIT}`];
|
|
42271
|
+
}
|
|
42272
|
+
return [
|
|
42273
|
+
`${lead}. ${sourceCount} review source${sourceCount === 1 ? " is" : "s are"} connected but no testimonials have landed yet \u2014 so this is a timing or ingestion gap, not a missing connection, and re-querying will not change it. Ingestion may still be running: say so rather than reporting the client has no reviews. ${NO_CORPUS_EXIT}`
|
|
42274
|
+
];
|
|
42216
42275
|
}
|
|
42217
42276
|
|
|
42218
42277
|
// src/commands/testimonials/list.ts
|
|
@@ -42287,14 +42346,14 @@ var listCommand18 = defineCommand194({
|
|
|
42287
42346
|
run: async ({ args }) => {
|
|
42288
42347
|
try {
|
|
42289
42348
|
const data = await apiGet("/api/testimonials", buildListParams(args));
|
|
42290
|
-
const hints =
|
|
42349
|
+
const { hints, empty } = await measureEmptyCorpus({
|
|
42291
42350
|
command: "list",
|
|
42292
42351
|
resultCount: data.length,
|
|
42293
42352
|
activeFilters: activeFilterFlags(args, FILTER_FLAGS)
|
|
42294
42353
|
});
|
|
42295
42354
|
const outputFormat = args.output || "json";
|
|
42296
42355
|
writeOutput(
|
|
42297
|
-
{ ok: true, data, ...hints.length > 0 ? { hints } : {} },
|
|
42356
|
+
{ ok: true, data, ...empty ? { meta: { empty } } : {}, ...hints.length > 0 ? { hints } : {} },
|
|
42298
42357
|
outputFormat,
|
|
42299
42358
|
args.fields ? args.fields.split(",") : void 0,
|
|
42300
42359
|
args.full,
|
|
@@ -42426,17 +42485,15 @@ var searchCommand3 = defineCommand195({
|
|
|
42426
42485
|
const body = buildSearchRequest(query, args);
|
|
42427
42486
|
const data = await apiPost("/api/testimonials/search", body);
|
|
42428
42487
|
const languageHint = languageBiasHint(data, body.language);
|
|
42429
|
-
const hints =
|
|
42430
|
-
|
|
42431
|
-
|
|
42432
|
-
|
|
42433
|
-
|
|
42434
|
-
|
|
42435
|
-
...languageHint ? [languageHint] : []
|
|
42436
|
-
];
|
|
42488
|
+
const { hints: emptyHints, empty } = await measureEmptyCorpus({
|
|
42489
|
+
command: "search",
|
|
42490
|
+
resultCount: data.length,
|
|
42491
|
+
activeFilters: activeFilterFlags(args, FILTER_FLAGS2)
|
|
42492
|
+
});
|
|
42493
|
+
const hints = [...emptyHints, ...languageHint ? [languageHint] : []];
|
|
42437
42494
|
const outputFormat = args.output || "json";
|
|
42438
42495
|
writeOutput(
|
|
42439
|
-
{ ok: true, data, ...hints.length > 0 ? { hints } : {} },
|
|
42496
|
+
{ ok: true, data, ...empty ? { meta: { empty } } : {}, ...hints.length > 0 ? { hints } : {} },
|
|
42440
42497
|
outputFormat,
|
|
42441
42498
|
args.fields ? args.fields.split(",") : void 0,
|
|
42442
42499
|
args.full,
|
|
@@ -43985,6 +44042,172 @@ Full guide: __tooling__/docs/tools/baker/winning-ads.md`
|
|
|
43985
44042
|
}
|
|
43986
44043
|
});
|
|
43987
44044
|
|
|
44045
|
+
// src/unknown-flags.ts
|
|
44046
|
+
var ALWAYS_ACCEPTED = /* @__PURE__ */ new Set(["help", "h", "version"]);
|
|
44047
|
+
function subCommandsOf(command) {
|
|
44048
|
+
const sub = command.subCommands;
|
|
44049
|
+
return sub && typeof sub === "object" ? sub : null;
|
|
44050
|
+
}
|
|
44051
|
+
function argsOf(command) {
|
|
44052
|
+
const args = command.args;
|
|
44053
|
+
return args && typeof args === "object" ? args : null;
|
|
44054
|
+
}
|
|
44055
|
+
function resolveCommand2(root, argv) {
|
|
44056
|
+
let command = root;
|
|
44057
|
+
for (const token of argv) {
|
|
44058
|
+
if (token === "--" || token.startsWith("-")) {
|
|
44059
|
+
break;
|
|
44060
|
+
}
|
|
44061
|
+
const subCommands = subCommandsOf(command);
|
|
44062
|
+
if (!subCommands) {
|
|
44063
|
+
break;
|
|
44064
|
+
}
|
|
44065
|
+
const next = subCommands[token];
|
|
44066
|
+
if (next === void 0) {
|
|
44067
|
+
break;
|
|
44068
|
+
}
|
|
44069
|
+
if (typeof next !== "object") {
|
|
44070
|
+
return { command, resolved: false };
|
|
44071
|
+
}
|
|
44072
|
+
command = next;
|
|
44073
|
+
}
|
|
44074
|
+
return { command, resolved: true };
|
|
44075
|
+
}
|
|
44076
|
+
function typeOf(def) {
|
|
44077
|
+
return def && typeof def === "object" ? def.type : void 0;
|
|
44078
|
+
}
|
|
44079
|
+
function acceptedNames(command) {
|
|
44080
|
+
const args = argsOf(command);
|
|
44081
|
+
if (!args) {
|
|
44082
|
+
return null;
|
|
44083
|
+
}
|
|
44084
|
+
const names = new Set(ALWAYS_ACCEPTED);
|
|
44085
|
+
for (const [name, def] of Object.entries(args)) {
|
|
44086
|
+
if (typeOf(def) === "positional") {
|
|
44087
|
+
continue;
|
|
44088
|
+
}
|
|
44089
|
+
for (const spelling of [name, kebab(name), camel(name)]) {
|
|
44090
|
+
names.add(spelling);
|
|
44091
|
+
if (typeOf(def) === "boolean") {
|
|
44092
|
+
names.add(`no-${spelling}`);
|
|
44093
|
+
}
|
|
44094
|
+
}
|
|
44095
|
+
}
|
|
44096
|
+
return names;
|
|
44097
|
+
}
|
|
44098
|
+
function kebab(name) {
|
|
44099
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
44100
|
+
}
|
|
44101
|
+
function camel(name) {
|
|
44102
|
+
return name.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
44103
|
+
}
|
|
44104
|
+
function findUnknownFlags(root, argv) {
|
|
44105
|
+
const { command, resolved } = resolveCommand2(root, argv);
|
|
44106
|
+
if (!resolved) {
|
|
44107
|
+
return [];
|
|
44108
|
+
}
|
|
44109
|
+
const accepted = acceptedNames(command);
|
|
44110
|
+
if (!accepted) {
|
|
44111
|
+
return [];
|
|
44112
|
+
}
|
|
44113
|
+
const unknown = [];
|
|
44114
|
+
for (const token of argv) {
|
|
44115
|
+
if (token === "--") {
|
|
44116
|
+
break;
|
|
44117
|
+
}
|
|
44118
|
+
if (!token.startsWith("--")) {
|
|
44119
|
+
continue;
|
|
44120
|
+
}
|
|
44121
|
+
const name = token.slice(2).split("=")[0] ?? "";
|
|
44122
|
+
if (name.length > 0 && !accepted.has(name) && !unknown.includes(token)) {
|
|
44123
|
+
unknown.push(token);
|
|
44124
|
+
}
|
|
44125
|
+
}
|
|
44126
|
+
return unknown;
|
|
44127
|
+
}
|
|
44128
|
+
function closestFlag(unknown, command) {
|
|
44129
|
+
const accepted = acceptedNames(command);
|
|
44130
|
+
if (!accepted) {
|
|
44131
|
+
return null;
|
|
44132
|
+
}
|
|
44133
|
+
const name = unknown.replace(/^--/, "").split("=")[0] ?? "";
|
|
44134
|
+
const scored = [...accepted].filter((candidate) => !ALWAYS_ACCEPTED.has(candidate)).map((candidate) => ({
|
|
44135
|
+
candidate,
|
|
44136
|
+
prefix: sharedPrefix(name, candidate),
|
|
44137
|
+
distance: editDistance(name, candidate)
|
|
44138
|
+
})).filter(({ prefix, distance }) => prefix >= 4 || distance <= 3).sort((a, b) => b.prefix - a.prefix || a.distance - b.distance);
|
|
44139
|
+
return scored[0] ? `--${scored[0].candidate}` : null;
|
|
44140
|
+
}
|
|
44141
|
+
function sharedPrefix(a, b) {
|
|
44142
|
+
let i = 0;
|
|
44143
|
+
while (i < a.length && i < b.length && a[i] === b[i]) {
|
|
44144
|
+
i++;
|
|
44145
|
+
}
|
|
44146
|
+
return i;
|
|
44147
|
+
}
|
|
44148
|
+
function editDistance(a, b) {
|
|
44149
|
+
let previous = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
44150
|
+
for (let i = 1; i <= a.length; i++) {
|
|
44151
|
+
const current = [i];
|
|
44152
|
+
for (let j = 1; j <= b.length; j++) {
|
|
44153
|
+
const substitution = (previous[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
|
|
44154
|
+
current.push(Math.min((previous[j] ?? 0) + 1, (current[j - 1] ?? 0) + 1, substitution));
|
|
44155
|
+
}
|
|
44156
|
+
previous = current;
|
|
44157
|
+
}
|
|
44158
|
+
return previous[b.length] ?? 0;
|
|
44159
|
+
}
|
|
44160
|
+
function commandFor(root, argv) {
|
|
44161
|
+
const { command, resolved } = resolveCommand2(root, argv);
|
|
44162
|
+
return resolved ? command : null;
|
|
44163
|
+
}
|
|
44164
|
+
function unknownFlagEnvelope(unknown, commandPath, suggestion) {
|
|
44165
|
+
const list = unknown.join(", ");
|
|
44166
|
+
return {
|
|
44167
|
+
ok: false,
|
|
44168
|
+
error: {
|
|
44169
|
+
code: "UNKNOWN_ARGUMENT",
|
|
44170
|
+
message: `${list} ${unknown.length > 1 ? "are not options" : "is not an option"} of \`baker ${commandPath}\`.`,
|
|
44171
|
+
fix: {
|
|
44172
|
+
action: "check_schema",
|
|
44173
|
+
explanation: (suggestion ? `Did you mean ${suggestion}? ` : "") + `Run \`baker schema ${commandPath}\` to see every option this command takes. Do not re-run the same call: until this version an unrecognised option was accepted and ignored, so a command that looked like it had filtered or narrowed the result was in fact answering the unfiltered question.`
|
|
44174
|
+
},
|
|
44175
|
+
retryable: false
|
|
44176
|
+
}
|
|
44177
|
+
};
|
|
44178
|
+
}
|
|
44179
|
+
function commandPathOf(root, argv) {
|
|
44180
|
+
const path37 = [];
|
|
44181
|
+
let command = root;
|
|
44182
|
+
for (const token of argv) {
|
|
44183
|
+
if (token === "--" || token.startsWith("-")) {
|
|
44184
|
+
break;
|
|
44185
|
+
}
|
|
44186
|
+
const subCommands = subCommandsOf(command);
|
|
44187
|
+
const next = subCommands?.[token];
|
|
44188
|
+
if (next === void 0 || typeof next !== "object") {
|
|
44189
|
+
break;
|
|
44190
|
+
}
|
|
44191
|
+
path37.push(token);
|
|
44192
|
+
command = next;
|
|
44193
|
+
}
|
|
44194
|
+
return path37.join(" ");
|
|
44195
|
+
}
|
|
44196
|
+
function refuseUnknownFlags(root, argv) {
|
|
44197
|
+
const unknown = findUnknownFlags(root, argv);
|
|
44198
|
+
if (unknown.length === 0) {
|
|
44199
|
+
return;
|
|
44200
|
+
}
|
|
44201
|
+
const command = commandFor(root, argv);
|
|
44202
|
+
const first = unknown[0];
|
|
44203
|
+
const suggestion = command && first ? closestFlag(first, command) : null;
|
|
44204
|
+
process.stdout.write(
|
|
44205
|
+
`${JSON.stringify(unknownFlagEnvelope(unknown, commandPathOf(root, argv), suggestion), null, 2)}
|
|
44206
|
+
`
|
|
44207
|
+
);
|
|
44208
|
+
process.exit(1);
|
|
44209
|
+
}
|
|
44210
|
+
|
|
43988
44211
|
// src/version.ts
|
|
43989
44212
|
import { readFileSync as readFileSync14 } from "fs";
|
|
43990
44213
|
function packageJsonUrl() {
|
|
@@ -44044,5 +44267,6 @@ Introspection: Run 'baker schema <command>' to inspect argument schemas.`
|
|
|
44044
44267
|
});
|
|
44045
44268
|
installStreamTaps();
|
|
44046
44269
|
logInvocation(process.argv.slice(2));
|
|
44270
|
+
refuseUnknownFlags(main, process.argv.slice(2));
|
|
44047
44271
|
runMain(main);
|
|
44048
44272
|
//# sourceMappingURL=cli.js.map
|