@saasontools/strauss-kb 0.1.18 → 0.1.19

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.
@@ -2,7 +2,7 @@ import {
2
2
  KB_COMMANDS,
3
3
  KbStore,
4
4
  VERSION
5
- } from "./chunk-GSOTMWZZ.js";
5
+ } from "./chunk-MNQNHYWL.js";
6
6
 
7
7
  // src/mcp.ts
8
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -46,4 +46,4 @@ export {
46
46
  createKbMcpServer,
47
47
  runKbMcpServer
48
48
  };
49
- //# sourceMappingURL=chunk-ROGVYSMV.js.map
49
+ //# sourceMappingURL=chunk-ZWSCLHG6.js.map
package/dist/cli-main.cjs CHANGED
@@ -2177,14 +2177,17 @@ function asBudgets(value) {
2177
2177
  if (value === null || typeof value !== "object") return {};
2178
2178
  const table2 = value;
2179
2179
  const pick = (key, min) => {
2180
- const raw = table2[key];
2181
- return typeof raw === "number" && Number.isInteger(raw) && raw >= min ? raw : void 0;
2180
+ const raw2 = table2[key];
2181
+ return typeof raw2 === "number" && Number.isInteger(raw2) && raw2 >= min ? raw2 : void 0;
2182
2182
  };
2183
2183
  const budgetTokens = pick("budgetTokens", 1);
2184
2184
  const fullUnderTokens = pick("fullUnderTokens", 0);
2185
+ const raw = table2["excludeTags"];
2186
+ const excludeTags = Array.isArray(raw) ? raw.filter((tag) => typeof tag === "string" && tag !== "") : void 0;
2185
2187
  return {
2186
2188
  ...budgetTokens ? { budgetTokens } : {},
2187
- ...fullUnderTokens !== void 0 ? { fullUnderTokens } : {}
2189
+ ...fullUnderTokens !== void 0 ? { fullUnderTokens } : {},
2190
+ ...excludeTags ? { excludeTags } : {}
2188
2191
  };
2189
2192
  }
2190
2193
  function contextProfileBudgets(manifest, profile) {
@@ -2471,6 +2474,9 @@ async function unpinBase(workspaceDir, bundlePath2) {
2471
2474
  var import_zod6 = require("zod");
2472
2475
  var bundlePath = import_zod6.z.string().min(1).describe("Absolute path to the knowledge base directory.");
2473
2476
  var conceptId = import_zod6.z.string().min(1).describe("e.g. decision.cursor-v2");
2477
+ var TAGS = import_zod6.z.array(import_zod6.z.string().min(1)).optional().describe(
2478
+ "Keep only records carrying every one of these frontmatter tags. Matched exactly."
2479
+ );
2474
2480
  var REPO_ROOT = import_zod6.z.string().min(1).optional().describe(
2475
2481
  "Where the anchored source lives, for the drift check. Defaults to the working directory."
2476
2482
  );
@@ -2492,6 +2498,41 @@ function argvFlag(argv, name) {
2492
2498
  }
2493
2499
  return value;
2494
2500
  }
2501
+ function argvFlags(argv, name) {
2502
+ const values = [];
2503
+ for (const [at2, arg] of argv.entries()) {
2504
+ if (arg.startsWith(`${name}=`)) {
2505
+ const value = arg.slice(name.length + 1);
2506
+ if (!value) throw new KbMissingFlagValueError(name);
2507
+ values.push(value);
2508
+ } else if (arg === name) {
2509
+ const value = argv[at2 + 1];
2510
+ if (value === void 0 || value.startsWith("--")) {
2511
+ throw new KbMissingFlagValueError(name);
2512
+ }
2513
+ values.push(value);
2514
+ }
2515
+ }
2516
+ return values;
2517
+ }
2518
+ function argvWithout(argv, ...names) {
2519
+ const kept = [];
2520
+ for (let at2 = 0; at2 < argv.length; at2 += 1) {
2521
+ const arg = argv[at2];
2522
+ if (names.some((name) => arg.startsWith(`${name}=`))) continue;
2523
+ if (names.includes(arg)) {
2524
+ at2 += 1;
2525
+ continue;
2526
+ }
2527
+ kept.push(arg);
2528
+ }
2529
+ return kept;
2530
+ }
2531
+ function argvPositional(argv, ...names) {
2532
+ return argvWithout(argv.slice(1), ...names).find(
2533
+ (arg) => !arg.startsWith("--")
2534
+ );
2535
+ }
2495
2536
 
2496
2537
  // src/commands/anchor-resolve.ts
2497
2538
  function resolverSummary(results) {
@@ -2924,6 +2965,13 @@ function successors(record, byId) {
2924
2965
  return { records, missing: missing2 };
2925
2966
  }
2926
2967
 
2968
+ // src/kb-tags.ts
2969
+ function matchesTags(record, filter) {
2970
+ if (!filter.tags?.length && !filter.excludeTags?.length) return true;
2971
+ const carried = new Set(record.frontmatter.tags ?? []);
2972
+ return (filter.tags ?? []).every((tag) => carried.has(tag)) && !(filter.excludeTags ?? []).some((tag) => carried.has(tag));
2973
+ }
2974
+
2927
2975
  // src/catalog.ts
2928
2976
  var EMPTY_STANDINGS = {
2929
2977
  current: 0,
@@ -2934,7 +2982,7 @@ var EMPTY_STANDINGS = {
2934
2982
  };
2935
2983
  function catalog(bundle, options = {}) {
2936
2984
  const wanted = options.type ? bundle.filter((record) => record.frontmatter.type === options.type) : bundle;
2937
- const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).map((hit) => ({
2985
+ const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).filter((hit) => matchesTags(hit.record, options)).map((hit) => ({
2938
2986
  conceptId: hit.record.conceptId,
2939
2987
  type: hit.record.frontmatter.type,
2940
2988
  title: hit.record.frontmatter.title ?? null,
@@ -2974,25 +3022,39 @@ function renderCatalogLine(entry) {
2974
3022
  var catalogCommand = define({
2975
3023
  name: "catalog",
2976
3024
  tool: "kb_catalog",
2977
- usage: "catalog [type]",
3025
+ usage: "catalog [type] [--tag T]...",
2978
3026
  description: "Lists every record as one line \u2014 concept id, type, title, standing, and a stale flag \u2014 at roughly thirty tokens each. Pick this over kb_load once kb_load refuses: kb_catalog never refuses. Superseded records show only their replacement; fetch bodies with kb_load, kb_pack, kb_query, or kb_trace.",
2979
3027
  input: import_zod10.z.object({
2980
3028
  bundlePath,
2981
- type: import_zod10.z.enum(KB_RECORD_TYPES).optional()
2982
- }),
2983
- fromArgv: (argv, path) => ({
2984
- bundlePath: path,
2985
- ...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {}
3029
+ type: import_zod10.z.enum(KB_RECORD_TYPES).optional(),
3030
+ tags: TAGS
2986
3031
  }),
2987
- run: async ({ store }, { bundlePath: path, type }) => render(
2988
- await store.catalog(path, { ...type ? { type } : {} }),
3032
+ fromArgv: (argv, path) => {
3033
+ const tags = argvFlags(argv, "--tag");
3034
+ const type = argvPositional(argv, "--tag");
3035
+ return {
3036
+ bundlePath: path,
3037
+ ...type ? { type } : {},
3038
+ ...tags.length ? { tags } : {}
3039
+ };
3040
+ },
3041
+ run: async ({ store }, { bundlePath: path, type, tags }) => render(
3042
+ await store.catalog(path, {
3043
+ ...type ? { type } : {},
3044
+ ...tags ? { tags } : {}
3045
+ }),
2989
3046
  path,
2990
- type
3047
+ type,
3048
+ tags
2991
3049
  )
2992
3050
  });
2993
- function render(result, bundle, type) {
3051
+ function render(result, bundle, type, tags) {
3052
+ const narrowed = [
3053
+ ...type ? [type] : [],
3054
+ ...tags?.length ? [`tags: ${tags.join(", ")}`] : []
3055
+ ].join(" \xB7 ");
2994
3056
  const lines = [
2995
- `# KB Catalog${type ? ` \u2014 ${type}` : ""}`,
3057
+ `# KB Catalog${narrowed ? ` \u2014 ${narrowed}` : ""}`,
2996
3058
  `bundle: ${bundle}`,
2997
3059
  `${count(result.recordCount, "record")}: ${standingCounts(result)}`
2998
3060
  ];
@@ -3004,7 +3066,7 @@ function render(result, bundle, type) {
3004
3066
  lines.push("");
3005
3067
  if (!result.entries.length) {
3006
3068
  lines.push(
3007
- type ? `(no records of type ${type})` : "(no records \u2014 this base is empty)"
3069
+ narrowed ? `(no records matching ${narrowed})` : "(no records \u2014 this base is empty)"
3008
3070
  );
3009
3071
  } else {
3010
3072
  for (const entry of result.entries) lines.push(renderCatalogLine(entry));
@@ -3091,7 +3153,7 @@ function preamble() {
3091
3153
  "tokens."
3092
3154
  ].join("\n");
3093
3155
  }
3094
- async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, budgetTokens) {
3156
+ async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, budgetTokens, excludeTags) {
3095
3157
  const bundle = await store.list(absolutePath);
3096
3158
  if (bundle.length === 0) {
3097
3159
  return {
@@ -3105,7 +3167,8 @@ async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, b
3105
3167
  let degradedFrom;
3106
3168
  if (fullCap > 0) {
3107
3169
  const full = await store.load(absolutePath, {
3108
- budgetTokens: fullCap
3170
+ budgetTokens: fullCap,
3171
+ excludeTags
3109
3172
  });
3110
3173
  if (!full.loaded && pinMode === "full") {
3111
3174
  degradedFrom = { approxTokens: full.approxTokens };
@@ -3135,7 +3198,9 @@ async function renderBase(store, path, absolutePath, fullUnderTokens, pinMode, b
3135
3198
  };
3136
3199
  }
3137
3200
  }
3138
- const adjudicated = adjudicate(bundle, bundle);
3201
+ const adjudicated = adjudicate(bundle, bundle).filter(
3202
+ (hit) => matchesTags(hit.record, { excludeTags })
3203
+ );
3139
3204
  const lines = adjudicated.filter((hit) => hit.standing !== "superseded").map((hit) => renderIndexLine(hit.record));
3140
3205
  const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(
3141
3206
  (hit) => `- \`${hit.record.conceptId}\` \u2192 superseded by ${hit.heads.map((head) => `\`${head.conceptId}\``).join(", ") || "(missing replacement)"}`
@@ -3156,6 +3221,7 @@ async function buildContext(store, workspaceDir, options = {}) {
3156
3221
  const fromManifest = mergedContextBudgets(merged, options.profile);
3157
3222
  budgetTokens = options.budgetTokens ?? fromManifest.budgetTokens ?? builtin.budgetTokens ?? DEFAULT_CONTEXT_BUDGET;
3158
3223
  fullUnderTokens = options.fullUnderTokens ?? fromManifest.fullUnderTokens ?? builtin.fullUnderTokens ?? 0;
3224
+ const excludeTags = options.excludeTags ?? fromManifest.excludeTags ?? builtin.excludeTags ?? [];
3159
3225
  const pins = merged.pins.filter(
3160
3226
  (pin) => !pin.profiles?.length || !options.profile || pin.profiles.includes(options.profile)
3161
3227
  );
@@ -3176,7 +3242,8 @@ async function buildContext(store, workspaceDir, options = {}) {
3176
3242
  pin.absolutePath,
3177
3243
  fullUnderTokens,
3178
3244
  pin.mode,
3179
- budgetTokens
3245
+ budgetTokens,
3246
+ excludeTags
3180
3247
  ),
3181
3248
  frozen: pin.frozen === true
3182
3249
  }))
@@ -3297,7 +3364,7 @@ ${CONTEXT_END}` : null;
3297
3364
  var contextCommand = define({
3298
3365
  name: "context",
3299
3366
  tool: "kb_context",
3300
- usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
3367
+ usage: "context [--profile NAME] [--budget N] [--full-under N] [--exclude-tag T]... [--format json] [--event NAME]",
3301
3368
  description: "Index block of pinned bases (ids, titles, standing) for injection at context birth. Takes no bundlePath \u2014 reads the workspace pin manifests. Empty when nothing is pinned; refuses over budget rather than truncating. Budget precedence: flags, then the manifest `context[profile]` over `context.default`, then the built-in profile, then package defaults.",
3302
3369
  input: import_zod11.z.object({
3303
3370
  budgetTokens: import_zod11.z.number().int().positive().optional().describe(
@@ -3309,6 +3376,9 @@ var contextCommand = define({
3309
3376
  profile: import_zod11.z.string().optional().describe(
3310
3377
  "Named budget set: built-ins are session-start (full-under 1500), compact and turn (budget 2500); the manifests' `context` tables override per repo. Unknown names fall through to defaults rather than failing."
3311
3378
  ),
3379
+ excludeTags: import_zod11.z.array(import_zod11.z.string().min(1)).optional().describe(
3380
+ "Frontmatter tags whose records stay out of the block. The base stays pinned and stays readable by tool; resolved like the budgets."
3381
+ ),
3312
3382
  format: import_zod11.z.enum(["markdown", "json"]).optional().describe(
3313
3383
  "CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
3314
3384
  ),
@@ -3322,19 +3392,22 @@ var contextCommand = define({
3322
3392
  const profile = argvFlag(argv, "--profile");
3323
3393
  const format = argvFlag(argv, "--format");
3324
3394
  const event = argvFlag(argv, "--event");
3395
+ const excludeTags = argvFlags(argv, "--exclude-tag");
3325
3396
  return {
3326
3397
  ...budget ? { budgetTokens: Number(budget) } : {},
3327
3398
  ...fullUnder ? { fullUnderTokens: Number(fullUnder) } : {},
3328
3399
  ...profile ? { profile } : {},
3400
+ ...excludeTags.length ? { excludeTags } : {},
3329
3401
  ...format ? { format } : {},
3330
3402
  ...event ? { event } : {}
3331
3403
  };
3332
3404
  },
3333
- run: async ({ store }, { budgetTokens, fullUnderTokens, profile, format, event }) => {
3405
+ run: async ({ store }, { budgetTokens, fullUnderTokens, profile, excludeTags, format, event }) => {
3334
3406
  const result = await buildContext(store, process.cwd(), {
3335
3407
  ...budgetTokens ? { budgetTokens } : {},
3336
3408
  ...fullUnderTokens ? { fullUnderTokens } : {},
3337
3409
  ...profile ? { profile } : {},
3410
+ ...excludeTags ? { excludeTags } : {},
3338
3411
  // Degradations — a full pin that could not fit, a refused block — go
3339
3412
  // to stderr as well as into the block itself: stderr is diagnostics on
3340
3413
  // both surfaces (hooks discard it, MCP logs it), so an operator can
@@ -4615,17 +4688,31 @@ var import_zod15 = require("zod");
4615
4688
  var listCommand = define({
4616
4689
  name: "list",
4617
4690
  tool: "kb_list",
4618
- usage: "list [type]",
4619
- description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
4620
- input: import_zod15.z.object({ bundlePath, type: import_zod15.z.enum(KB_RECORD_TYPES).optional() }),
4621
- fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
4622
- run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
4623
- conceptId: record.conceptId,
4624
- title: record.frontmatter.title ?? null,
4625
- description: record.frontmatter.description ?? null,
4626
- status: record.frontmatter.strauss_status,
4627
- anchors: record.frontmatter.strauss_anchors ?? []
4628
- }))
4691
+ usage: "list [type] [--tag T]...",
4692
+ description: "Every record, optionally one type or tag. For enumerating; use kb_query for a question.",
4693
+ input: import_zod15.z.object({
4694
+ bundlePath,
4695
+ type: import_zod15.z.enum(KB_RECORD_TYPES).optional(),
4696
+ tags: TAGS
4697
+ }),
4698
+ fromArgv: (argv, path) => {
4699
+ const tags = argvFlags(argv, "--tag");
4700
+ const type = argvPositional(argv, "--tag");
4701
+ return {
4702
+ bundlePath: path,
4703
+ ...type ? { type } : {},
4704
+ ...tags.length ? { tags } : {}
4705
+ };
4706
+ },
4707
+ run: async ({ store }, { bundlePath: path, type, tags }) => (await store.list(path, type, { ...tags ? { tags } : {} })).map(
4708
+ (record) => ({
4709
+ conceptId: record.conceptId,
4710
+ title: record.frontmatter.title ?? null,
4711
+ description: record.frontmatter.description ?? null,
4712
+ status: record.frontmatter.strauss_status,
4713
+ anchors: record.frontmatter.strauss_anchors ?? []
4714
+ })
4715
+ )
4629
4716
  });
4630
4717
 
4631
4718
  // src/commands/load.ts
@@ -4876,31 +4963,33 @@ var import_zod22 = require("zod");
4876
4963
  var queryCommand = define({
4877
4964
  name: "query",
4878
4965
  tool: "kb_query",
4879
- usage: "query <text...> [--repo-root PATH]",
4966
+ usage: "query <text...> [--tag T]... [--repo-root PATH]",
4880
4967
  description: "Search; every hit carries its standing. Flagged, never filtered: a superseded hit returns with its replacement, a rejected one is marked. Prefer kb_load when the base fits its budget \u2014 a full read beats search. Results are volatile: place them at the tail, not the cached prefix. Never read record files directly.",
4881
4968
  input: import_zod22.z.object({
4882
4969
  bundlePath,
4883
4970
  text: import_zod22.z.string().optional(),
4884
4971
  type: import_zod22.z.enum(KB_RECORD_TYPES).optional(),
4885
4972
  includeNonCurrent: import_zod22.z.boolean().optional(),
4973
+ tags: TAGS,
4886
4974
  repoRoot: REPO_ROOT
4887
4975
  }),
4888
- // `--repo-root` is a flag, so its value must not fall into the search text.
4976
+ // Both are flags, so neither's value may fall into the search text.
4889
4977
  fromArgv: (argv, path) => {
4890
4978
  const repoRoot = argvFlag(argv, "--repo-root");
4891
- const words = argv.slice(1);
4892
- const flag = words.indexOf("--repo-root");
4893
- if (flag !== -1) words.splice(flag, 2);
4979
+ const tags = argvFlags(argv, "--tag");
4980
+ const words = argvWithout(argv.slice(1), "--repo-root", "--tag");
4894
4981
  return {
4895
4982
  bundlePath: path,
4896
4983
  text: words.join(" ").trim(),
4897
4984
  includeNonCurrent: true,
4985
+ ...tags.length ? { tags } : {},
4898
4986
  ...repoRoot !== void 0 ? { repoRoot } : {}
4899
4987
  };
4900
4988
  },
4901
- run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, repoRoot }) => (await store.query(path, text ?? "", {
4989
+ run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, tags, repoRoot }) => (await store.query(path, text ?? "", {
4902
4990
  ...type ? { type } : {},
4903
4991
  includeNonCurrent: includeNonCurrent === true,
4992
+ ...tags ? { tags } : {},
4904
4993
  ...repoRoot !== void 0 ? { repoRoot } : {}
4905
4994
  })).map((hit) => ({
4906
4995
  conceptId: hit.record.conceptId,
@@ -5899,13 +5988,16 @@ var KbStore = class {
5899
5988
  return this.parse(conceptId2, raw);
5900
5989
  }
5901
5990
  /**
5902
- * Every record in the bundle, optionally narrowed to one type.
5991
+ * Every record in the bundle, optionally narrowed to one type and to the
5992
+ * records carrying every tag in `filter.tags`. Selection only — `excludeTags`
5993
+ * is not taken here, because `query`, `catalog` and `load` read through this
5994
+ * and must adjudicate over the whole base.
5903
5995
  *
5904
5996
  * A file that fails to parse is skipped and logged rather than thrown: one
5905
5997
  * malformed record — hand-edited, or written by a producer we don't know —
5906
5998
  * must not make the whole bundle unreadable.
5907
5999
  */
5908
- async list(bundlePath2, type) {
6000
+ async list(bundlePath2, type, filter = {}) {
5909
6001
  const root = this.root(bundlePath2);
5910
6002
  let names;
5911
6003
  try {
@@ -5919,7 +6011,9 @@ var KbStore = class {
5919
6011
  DEFAULT_IO_CONCURRENCY,
5920
6012
  async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises10.readFile)((0, import_node_path11.join)(root, name), "utf8"))
5921
6013
  );
5922
- return records.filter((record) => record !== null);
6014
+ return records.filter(
6015
+ (record) => record !== null && matchesTags(record, filter)
6016
+ );
5923
6017
  }
5924
6018
  /**
5925
6019
  * Moves a record's status, preserving everything else.
@@ -6059,9 +6153,10 @@ ${answer}
6059
6153
  /* @__PURE__ */ new Date(),
6060
6154
  await this.detectDrift(narrowed, options.repoRoot)
6061
6155
  );
6062
- if (options.includeNonCurrent) return adjudicated;
6063
- const present = new Set(adjudicated.map((hit) => hit.record.conceptId));
6064
- return adjudicated.filter(
6156
+ const kept = adjudicated.filter((hit) => matchesTags(hit.record, options));
6157
+ if (options.includeNonCurrent) return kept;
6158
+ const present = new Set(kept.map((hit) => hit.record.conceptId));
6159
+ return kept.filter(
6065
6160
  (hit) => hit.standing !== "superseded" || !hit.heads.some((head) => present.has(head.conceptId))
6066
6161
  );
6067
6162
  }
@@ -6164,14 +6259,17 @@ ${answer}
6164
6259
  /* @__PURE__ */ new Date(),
6165
6260
  await this.detectDrift(wanted, options.repoRoot)
6166
6261
  );
6167
- const records = adjudicated.filter((hit) => hit.standing !== "superseded");
6168
- const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
6262
+ const kept = adjudicated.filter(
6263
+ (hit) => matchesTags(hit.record, { excludeTags: options.excludeTags })
6264
+ );
6265
+ const records = kept.filter((hit) => hit.standing !== "superseded");
6266
+ const superseded = kept.filter((hit) => hit.standing === "superseded").map(stub);
6169
6267
  const approxTokens2 = records.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
6170
6268
  const bundleDigestValue = bundleDigest(records, superseded);
6171
6269
  if (!options.all && approxTokens2 > budgetTokens) {
6172
6270
  return {
6173
6271
  loaded: false,
6174
- recordCount: wanted.length,
6272
+ recordCount: kept.length,
6175
6273
  approxTokens: approxTokens2,
6176
6274
  budgetTokens,
6177
6275
  message: refusalMessage({
@@ -6184,7 +6282,7 @@ ${answer}
6184
6282
  }
6185
6283
  return {
6186
6284
  loaded: true,
6187
- recordCount: wanted.length,
6285
+ recordCount: kept.length,
6188
6286
  tokensLoaded: approxTokens2,
6189
6287
  budgetTokens: options.all ? null : budgetTokens,
6190
6288
  records,
@@ -6545,7 +6643,7 @@ function normalizeActor(id) {
6545
6643
  }
6546
6644
 
6547
6645
  // src/version.ts
6548
- var VERSION = true ? "0.1.18" : "0.0.0-dev";
6646
+ var VERSION = true ? "0.1.19" : "0.0.0-dev";
6549
6647
 
6550
6648
  // src/cli.ts
6551
6649
  async function runKbCli(argv) {