@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 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-presets
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 library`) 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.",
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
- var NO_CORPUS_TAIL = 'this company has NO review corpus connected. 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.';
42197
- var LEAD2 = {
42198
- // Search reranks the whole corpus, so an empty result is a property of the
42199
- // corpus far more often than of the query — but only an unfiltered `list`
42200
- // proves it, so send it there once rather than letting it re-query.
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
- hints.push(
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
- hints.push(`${LEAD2[command]}${NO_CORPUS_TAIL}`);
42215
- return hints;
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 = emptyCorpusHints({
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
- ...emptyCorpusHints({
42431
- command: "search",
42432
- resultCount: data.length,
42433
- activeFilters: activeFilterFlags(args, FILTER_FLAGS2)
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