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