@saasontools/strauss-kb 0.1.7 → 0.1.9

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/ARCHITECTURE.md CHANGED
@@ -72,6 +72,95 @@ replacement's first log format was `·`-delimited, with a splitter to read it
72
72
  back. Both are gone: the log is JSONL and the schema is emitted from Zod, so
73
73
  `strauss-kb schema` is the contract rather than a description of one.
74
74
 
75
+ ## Cross-worktree log safety
76
+
77
+ `log.jsonl` is append-only and the one artifact nothing can rebuild, so how it
78
+ merges across worktrees matters more than how any record file does. Records
79
+ already don't need this: one concept id is one file, so two writers choosing
80
+ distinct ids never merge at all, and `link`-based publish (above) turns the
81
+ one case where they collide into a 409 rather than a merge. The log has no
82
+ such escape — every writer appends into the same file by design — so it needs
83
+ an actual merge strategy, not just atomicity per write.
84
+
85
+ The append itself already had it: `record()` uses `appendFile`, which opens
86
+ `O_APPEND` and is one `write(2)` for an entry this small, so two processes
87
+ appending locally interleave whole lines, never a torn one. What was missing
88
+ was git's merge of two worktrees' independently-appended logs — the default
89
+ line-level merge can conflict, or silently keep one side, on lines that were
90
+ never in conflict, since both sides only ever added, never edited, a line.
91
+
92
+ The fix is `.gitattributes: log.jsonl text eol=lf merge=union`, written by
93
+ `record()` — every path that appends a log line (`write`, `setStatus`,
94
+ `verify`, `supersede`), not `write()` alone — on first use
95
+ (`kb-store.ts#ensureGitattributes`). `union` is a merge driver git ships —
96
+ nothing to configure beyond the attribute — that keeps both sides' added
97
+ lines; `eol=lf` closes a second divergence path, below. The alternative
98
+ considered and dropped was a lock file coordinating writes across worktrees
99
+ the way `mutate`'s CAS coordinates a single record: it would need to span
100
+ process boundaries and survive a crashed holder, which is the same
101
+ stale-lock failure mode rejected above, for a smaller problem than the one it
102
+ solves there.
103
+
104
+ Decisions worth naming because a later reader could reopen them:
105
+
106
+ - **A `.gitattributes` that exists but declares no merge strategy for the
107
+ log gets the line appended, not left alone.** The alternative — leave a
108
+ user's own file untouched — reads as more conservative, but a bundle that
109
+ already has a `.gitattributes` in front of it is exactly the bundle most
110
+ likely to be shared across worktrees or forks, so leaving it without union
111
+ merge is the worse of the two failure modes. A file that _does_ already
112
+ declare a merge strategy for `log.jsonl` — this one or a user's own, such
113
+ as `merge=ours` — is left alone entirely: gitattributes resolves repeated
114
+ lines for one pattern by "last one wins", so appending a second `merge=`
115
+ line would silently override rather than coexist. Recognizing "already
116
+ declared" needed a real tokenizer (`hasMergeDeclaration`) rather than
117
+ exact-string matching against the line this module writes — the first cut
118
+ missed a tab or doubled space between tokens (false negative, a harmless
119
+ duplicate line) and could never recognize a user's own `merge=ours` as a
120
+ decision already made (the one case where appending anything is wrong).
121
+ - **A `.gitattributes` that fails to _read_ is never treated as "missing".**
122
+ The first cut folded every `readFile` failure — permissions, a transient
123
+ `EMFILE`, the path being a directory — into "doesn't exist yet" and took
124
+ the create branch, which is a truncating write: a real `.gitattributes`
125
+ hit by a transient read error would be silently replaced with just the
126
+ union-merge line. Only `ENOENT` means missing; anything else is reported
127
+ as a failure and the file is left exactly as it was. The create branch
128
+ also uses `wx` (exclusive create) rather than a plain write, so a
129
+ concurrent writer that created the file between the read and this write
130
+ fails loudly into the same best-effort catch instead of the second writer
131
+ truncating the first one's file.
132
+ - **Union merge does not preserve line order, so `kb_log`'s reader sorts by
133
+ `at` rather than trusting file order.** Sorting there, once, is cheaper
134
+ than trying to make every future merge order-preserving. `at` is now
135
+ validated as an actual ISO-8601 timestamp (`z.iso.datetime()`, matching
136
+ exactly what `record()` writes) rather than any non-empty string — a value
137
+ that parses as JSON and matches the schema's shape but isn't really a
138
+ timestamp would otherwise sort unpredictably instead of failing, and
139
+ `parseLog` already has a place for "well-formed but wrong" to go: reported
140
+ as malformed, same as any other schema mismatch, never silently repaired.
141
+ - **A union merge can keep the exact same line twice** — a cherry-pick or
142
+ rebase that carried one worktree's entry into the other's history before
143
+ the merge, not two independent writes agreeing by chance: `record()` mints
144
+ its own `at` per call, so two entries equal on every field including `at`
145
+ cannot be genuine. `parseLog` dedupes entries that are byte-for-byte equal
146
+ after parsing and keeps everything else, including two entries that agree
147
+ on every field except `at` — that pair is two real events. The
148
+ alternative — leave duplicates visible and call it "genuine repeat
149
+ ambiguity" — was rejected: there's no ambiguity to preserve, since the
150
+ only way to produce an exact duplicate is the merge itself.
151
+ - **A read-then-append race across processes on the append branch — two
152
+ processes both reading a `.gitattributes` without the line, both
153
+ appending it — is left unguarded, not a reason to lock.** `appendFile` is
154
+ `O_APPEND`, so the outcome is two copies of the same line, never a torn
155
+ write, and `hasMergeDeclaration` sees a duplicate declaration as "already
156
+ declared" on the very next call. Cheap residue, not corruption — the same
157
+ trade the lock-file alternative above was rejected for, at a smaller
158
+ scale.
159
+
160
+ GitHub does not run merge drivers for a PR it merges server-side — see the
161
+ README's "Cross-worktree writes" section. The driver only helps a merge a
162
+ local git client actually performs.
163
+
75
164
  ## Rejected for now: a base registry
76
165
 
77
166
  Cross-base questions are unaskable by construction — supersession, traces, and
package/README.md CHANGED
@@ -54,6 +54,7 @@ bundling can `require()` it without depending on its Node version honouring
54
54
  <type>.<slug>.md records
55
55
  INDEX.md index derived, store-owned
56
56
  log.jsonl history primary, append-only
57
+ .gitattributes merge store-owned, written on first write
57
58
  .index.sqlite search derived, gitignored
58
59
  ```
59
60
 
@@ -76,6 +77,46 @@ Repair-on-read, not coordination, is what lets both exist without a lock. The
76
77
  index is _eventually_ correct: a writer whose scan predated another's record
77
78
  publishes a briefly stale index, and the next read through the store settles it.
78
79
 
80
+ ### Cross-worktree writes
81
+
82
+ A committed base is routinely written from more than one worktree at once —
83
+ each records into the same `log.jsonl`, and a plain git merge of two branches
84
+ that both appended lines resolves that file at the line level, same as any
85
+ other text file. That is the wrong merge for an append-only log: git's default
86
+ picks a side, or conflicts, on lines that both branches only ever meant to add
87
+ to.
88
+
89
+ So the first write to a base (through `write`, or whichever call happens to
90
+ append the first log line) declares a merge driver for its log: it writes
91
+ `log.jsonl text eol=lf merge=union` into the base's `.gitattributes` if that
92
+ file does not exist yet, and appends the line if the file exists but declares
93
+ no merge strategy for `log.jsonl` yet — a `.gitattributes` a user put there
94
+ first is respected, never overwritten wholesale, and a line that already
95
+ gives `log.jsonl` _any_ merge strategy — this one or the user's own choice
96
+ such as `merge=ours` — is left alone rather than layered under a second,
97
+ possibly conflicting one. `union` is one of git's built-in merge drivers; the
98
+ attribute alone is enough; nothing else needs configuring. `eol=lf` pins line
99
+ endings to `\n` regardless of a checkout's `core.autocrlf`, so a Windows
100
+ checkout normalizing the file on checkout can't leave it with mixed endings
101
+ against the raw `\n` every append writes. With it, a merge of two branches
102
+ that both appended to `log.jsonl` keeps both sides' lines instead of picking
103
+ one.
104
+
105
+ A union merge does not preserve line order, and can occasionally keep the
106
+ same line twice (a cherry-pick or rebase that carried one side's entry into
107
+ the other's history before the merge). `kb_log`'s reader (`kb-log.ts`) sorts
108
+ entries by `at` and drops exact duplicates before returning them, so neither
109
+ is something a caller has to account for.
110
+
111
+ **This applies to a local `git merge`, not to GitHub.** GitHub computes pull
112
+ request merges (and the merge/squash/rebase buttons) through its own service,
113
+ which does not read `.gitattributes` merge-driver declarations — a PR that
114
+ merges two branches' `log.jsonl` appends on GitHub gets git's ordinary
115
+ line-level merge (or a conflict) even with the attribute in place. The union
116
+ driver only fires for a merge actually run by a local git client, which covers
117
+ worktrees pulling from and pushing to each other directly, but not a merge
118
+ GitHub itself performs.
119
+
79
120
  ## Records
80
121
 
81
122
  The filename is the identity. `fact.auth-retries.md` has concept id
@@ -224,6 +265,8 @@ strauss-kb [--bundle PATH] <command> [args]
224
265
  index The index, rebuilt if it disagrees with the records.
225
266
  log What touched what, and when.
226
267
  validate Cross-record checks. Exits 1 when it reports a problem.
268
+ doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--strict]
269
+ Health sweep: what expired, went unconfirmed, aged, or was orphaned.
227
270
  schema JSON Schema for the format.
228
271
  types The twelve types, their sections and initial status.
229
272
  pin [bundle-path] [flags] Pin a base. --mode, --profiles, --frozen; --local/--user pick the layer.
@@ -233,14 +276,20 @@ strauss-kb [--bundle PATH] <command> [args]
233
276
  sync-instructions <file> Plant the context block between sentinels in an instruction file.
234
277
 
235
278
  --bundle PATH defaults to ./.strauss/kb
279
+ --json the machine shape, where a command prints a table
280
+ -- everything after it is text, not flags
236
281
  STRAUSS_KB_ACTOR names the writer in the log
237
282
  ```
238
283
 
239
284
  Results go to stdout as JSON — `index` and `pack` are markdown, which is what
240
- they are. Errors
241
- go to stderr and exit 1. `validate` is the one command whose exit code is not
242
- just "did it run": a check that reports a problem succeeded as a command and
243
- failed as a check, so it exits 1 with its findings on stdout.
285
+ they are, and `doctor` prints a table unless `--json` asks for the object
286
+ behind it. `--json` is refused rather than ignored on the commands that have
287
+ only one form, since a flag that quietly does nothing reads as one that
288
+ worked; `--` ends flag parsing, for the verbs that end in free prose. Errors
289
+ go to stderr and exit 1. `validate` and `doctor --strict` are the commands
290
+ whose exit code is not just "did it run": a check that reports a problem
291
+ succeeded as a command and failed as a check, so it exits 1 with its findings
292
+ on stdout.
244
293
 
245
294
  ```bash
246
295
  strauss-kb --bundle .strauss/kb write fact <<'JSON'
@@ -262,7 +311,8 @@ strauss-kb validate || echo "problems above"
262
311
  `strauss-kb-mcp` speaks stdio and takes no API key and no required environment.
263
312
  Every CLI verb is a tool: `kb_write`, `kb_write_decision`, `kb_no_decision`,
264
313
  `kb_status`, `kb_supersede`, `kb_answer`, `kb_verify`, `kb_load`, `kb_pack`, `kb_query`,
265
- `kb_trace`, `kb_list`, `kb_index`, `kb_log`, `kb_validate`, `kb_schema`, `kb_types`,
314
+ `kb_trace`, `kb_list`, `kb_index`, `kb_log`, `kb_validate`, `kb_doctor`,
315
+ `kb_schema`, `kb_types`,
266
316
  `kb_pin`, `kb_unpin`, `kb_pins`, `kb_context`. Most tools take a `bundlePath`;
267
317
  `kb_schema` and `kb_types` describe the format rather than any one base, and
268
318
  `kb_pins` and `kb_context` read the workspace pin manifests instead. The one
@@ -401,6 +451,83 @@ fact; a missing replacement is `broken-chain` with no head — the case that nee
401
451
  the most care, because returning the stale record unmarked looks exactly like
402
452
  success.
403
453
 
454
+ ## Health
455
+
456
+ `doctor` sweeps a whole base and reports what has decayed. It is read-only —
457
+ nothing is re-dated, re-verified, superseded, or deleted — because every
458
+ finding is a judgment somebody has to make: whether a claim still holds, which
459
+ question is worth answering, which island to link or drop.
460
+
461
+ It exists because decay is invisible from inside a single record. A stale
462
+ record reads exactly like a live one, a question nobody answered reads exactly
463
+ like one nobody asked, and a record nothing links to is reachable only by
464
+ someone who already knows it is there. `validate` is the narrower neighbour:
465
+ it asks only whether pointers between records agree.
466
+
467
+ | Check | Reports |
468
+ | ---------------------- | ------------------------------------------------------------------------------ |
469
+ | `expired` | `stale_after` is in the past — or is not a readable date, which is no better. |
470
+ | `expiring` | `stale_after` falls inside the next `--expiring-days` (30). |
471
+ | `unverified` | `verified[]` is empty and the record is over `--unverified-days` (90) old. |
472
+ | `aging` | Still `open` or `proposed` after `--aging-days` (90). |
473
+ | `orphaned` | No other record links to it, by body link or supersession. |
474
+ | `broken-supersession` | A chain that does not resolve: no replacement, a missing one, a cycle, a fork. |
475
+ | `superseded-but-cited` | A record that still holds, whose body links to one that does not. |
476
+
477
+ The last check's name is for its common case: a rejected target counts too, and
478
+ is the worse half — a superseded record at least names its replacement, while a
479
+ rejected one is a well-formed assertion of what someone decided _not_ to do,
480
+ cited by a record the reader trusts.
481
+
482
+ ```bash
483
+ strauss-kb doctor # the table
484
+ strauss-kb doctor --json # the object behind it
485
+ strauss-kb doctor --strict # exit 1 if anything has expired
486
+ strauss-kb doctor --unverified-days 30 # a stricter confirmation window
487
+ ```
488
+
489
+ All seven groups are reported even when empty. A check that found nothing and
490
+ a check that never ran look identical in a report that only lists findings,
491
+ which is the whole value of a sweep.
492
+
493
+ Judgments the checks make, worth knowing before reading a report:
494
+
495
+ - **Superseded and rejected records sit out the freshness checks.** A replaced
496
+ record whose date has passed needs no repair, and reporting it would bury the
497
+ records that do. They stay in the graph checks, where standing is not the
498
+ question.
499
+ - **A date-only `stale_after` expires at UTC midnight.** `2026-09-01` parses as
500
+ `2026-09-01T00:00:00Z`, so a record goes stale at the start of its date: a
501
+ sweep run at exactly that instant still calls it expiring, and one a minute
502
+ later calls it expired. That is `adjudicate`'s comparison rather than a
503
+ second one — two readings of the same field disagreeing about the day would
504
+ be worse than either.
505
+ - **Age is read from `generated.at`, exclusively.** A record carrying no
506
+ timestamp is not reported as aging or unverified — without a start there is
507
+ no duration, and inventing one would flag every foreign record as overdue
508
+ (adjudication still warns `unverified` on it at read time). Exactly N days
509
+ old is not yet "older than N".
510
+ - **`orphaned` counts incoming links only, and reads supersession one way.** A
511
+ record that cites five others and is cited by none is precisely the island:
512
+ reachable if you already know it exists. The replacement references what it
513
+ replaced, never the reverse — taken symmetrically, a dead record would vouch
514
+ for its own replacement and an old→new pair nothing else touches would rescue
515
+ itself. Shared anchors and shared sources are co-location rather than
516
+ reference, so they do not rescue a record either.
517
+ - **A record citing the one it replaced is not superseded-but-cited.** That
518
+ link is the history working as designed, and reporting it would put a finding
519
+ on every correctly performed supersession.
520
+ - **A replacement pointer is checked whatever the status says.** The store
521
+ writes `strauss_status` and `strauss_superseded_by` in one mutation, so a
522
+ record left `accepted` while naming a replacement was hand-edited — and
523
+ adjudication reads it as current no matter what the pointer says, which is
524
+ what makes it worth naming.
525
+
526
+ `--strict` gates on expiry alone. The other six report debt a reader decides
527
+ about; an expired record is the base itself saying it would stop standing
528
+ behind something, which is the one finding a pipeline can act on without a
529
+ judgment call.
530
+
404
531
  ## Living in an agent session
405
532
 
406
533
  Long sessions lose a knowledge base twice over: attention decays, and
@@ -4,13 +4,14 @@ import {
4
4
  KB_DIR,
5
5
  KbStore,
6
6
  VERSION
7
- } from "./chunk-GKUQOJEK.js";
7
+ } from "./chunk-OFDWRMY6.js";
8
8
 
9
9
  // src/cli.ts
10
10
  import { join } from "path";
11
11
  async function runKbCli(argv) {
12
- const { bundle, rest } = takeBundle(argv);
13
- const name = rest[0] ?? "";
12
+ const { flags, literal } = takeLiteral(argv);
13
+ const { bundle, rest: withFlags } = takeBundle(flags);
14
+ const name = withFlags[0] ?? "";
14
15
  if (!name || name === "-h" || name === "--help") {
15
16
  process.stdout.write(usage());
16
17
  return;
@@ -22,6 +23,14 @@ async function runKbCli(argv) {
22
23
  }
23
24
  const command = KB_COMMANDS_BY_NAME.get(name);
24
25
  if (!command) die(`unknown command ${name}`);
26
+ const json = withFlags.includes("--json");
27
+ if (json && !command.render) {
28
+ die(`${name} takes no --json: its result is already the machine shape`);
29
+ }
30
+ const rest = [
31
+ ...json ? withFlags.filter((argument) => argument !== "--json") : withFlags,
32
+ ...literal
33
+ ];
25
34
  const raw = await command.fromArgv(rest, bundle, readStdin);
26
35
  const parsed = command.input.safeParse(raw);
27
36
  if (!parsed.success) {
@@ -41,13 +50,16 @@ async function runKbCli(argv) {
41
50
  },
42
51
  parsed.data
43
52
  );
44
- if (command.failsWhen?.(result)) process.exitCode = 1;
53
+ if (command.failsWhen?.(result, parsed.data)) process.exitCode = 1;
45
54
  if (result === "") return;
46
- process.stdout.write(
47
- typeof result === "string" ? result.endsWith("\n") ? result : `${result}
48
- ` : `${JSON.stringify(result, null, 2)}
49
- `
50
- );
55
+ const text = command.render && !json ? command.render(result) : typeof result === "string" ? result : JSON.stringify(result, null, 2);
56
+ process.stdout.write(text.endsWith("\n") ? text : `${text}
57
+ `);
58
+ }
59
+ function takeLiteral(argv) {
60
+ const at = argv.indexOf("--");
61
+ if (at === -1) return { flags: argv, literal: [] };
62
+ return { flags: argv.slice(0, at), literal: argv.slice(at + 1) };
51
63
  }
52
64
  function takeBundle(argv) {
53
65
  const at = argv.indexOf("--bundle");
@@ -89,6 +101,8 @@ function usage() {
89
101
  ),
90
102
  "",
91
103
  ` --bundle PATH defaults to ./${KB_DIR}`,
104
+ " --json the machine shape, where a command prints a table",
105
+ " -- everything after it is text, not flags",
92
106
  " --version the installed package version",
93
107
  " STRAUSS_KB_ACTOR names the writer in the log",
94
108
  ""
@@ -98,4 +112,4 @@ function usage() {
98
112
  export {
99
113
  runKbCli
100
114
  };
101
- //# sourceMappingURL=chunk-GKCG4P3L.js.map
115
+ //# sourceMappingURL=chunk-KVEEISYQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["/**\n * strauss-kb — a knowledge base's command line.\n *\n * A dispatcher over `KB_COMMANDS`, which the MCP server also projects. Nothing\n * command-specific lives here beyond turning argv into the object both\n * surfaces pass.\n */\nimport { join } from \"node:path\";\nimport { KB_COMMANDS, KB_COMMANDS_BY_NAME } from \"./commands/index.js\";\nimport { KB_DIR, KbStore } from \"./kb-store.js\";\nimport { VERSION } from \"./version.js\";\n\nexport async function runKbCli(argv: string[]): Promise<void> {\n const { flags, literal } = takeLiteral(argv);\n const { bundle, rest: withFlags } = takeBundle(flags);\n const name = withFlags[0] ?? \"\";\n\n if (!name || name === \"-h\" || name === \"--help\") {\n process.stdout.write(usage());\n return;\n }\n\n // The plugin in front of this CLI updates from a marketplace while the CLI\n // updates from npm, and neither prompts for the other. Answering \"which one\n // is installed\" is what makes that skew diagnosable instead of mysterious.\n if (name === \"--version\" || name === \"-v\") {\n process.stdout.write(`${VERSION}\\n`);\n return;\n }\n\n const command = KB_COMMANDS_BY_NAME.get(name);\n if (!command) die(`unknown command ${name}`);\n\n // Output shape, not an argument: stripped before the command sees argv, so a\n // positional adapter never has to know the flag exists. Refused rather than\n // ignored where a command has only one form — a flag that silently does\n // nothing teaches a caller that it worked.\n const json = withFlags.includes(\"--json\");\n if (json && !command.render) {\n die(`${name} takes no --json: its result is already the machine shape`);\n }\n const rest = [\n ...(json\n ? withFlags.filter((argument) => argument !== \"--json\")\n : withFlags),\n ...literal,\n ];\n\n const raw = await command.fromArgv(rest, bundle, readStdin);\n const parsed = command.input.safeParse(raw);\n if (!parsed.success) {\n die(\n `${name}: ${parsed.error.issues\n .map((issue) => `${issue.path.join(\".\") || \"(root)\"}: ${issue.message}`)\n .join(\"; \")}`,\n );\n }\n\n const store = new KbStore({\n warn: (entry) => process.stderr.write(`${JSON.stringify(entry)}\\n`),\n });\n const result = await command.run(\n {\n store,\n actor: process.env.STRAUSS_KB_ACTOR ?? \"unknown\",\n now: () => new Date().toISOString(),\n },\n parsed.data,\n );\n\n // A check reporting a problem succeeded as a command and failed as a check;\n // the command says which, rather than the dispatcher knowing their names.\n if (command.failsWhen?.(result, parsed.data)) process.exitCode = 1;\n // An empty string is deliberate silence — `context` with nothing pinned\n // runs from hooks at every session start, and even a bare newline is noise\n // injected into a fresh context.\n if (result === \"\") return;\n const text =\n command.render && !json\n ? command.render(result)\n : typeof result === \"string\"\n ? result\n : JSON.stringify(result, null, 2);\n process.stdout.write(text.endsWith(\"\\n\") ? text : `${text}\\n`);\n}\n\n/**\n * Everything after a bare `--` is text, never flags.\n *\n * Several verbs end in free prose — `no-decision`, `answer`, `query` — and a\n * reason that happens to contain `--json` or `--bundle` would otherwise lose a\n * word to the flag scan, or two. The sentinel is the usual escape, and the\n * token itself is dropped while the order of everything else is kept, so the\n * positional adapters still index the same way.\n */\nfunction takeLiteral(argv: string[]): { flags: string[]; literal: string[] } {\n const at = argv.indexOf(\"--\");\n if (at === -1) return { flags: argv, literal: [] };\n return { flags: argv.slice(0, at), literal: argv.slice(at + 1) };\n}\n\n/**\n * `--bundle` addresses a base directly; without it the command works on the\n * one under the current directory. A base belongs to whatever prompted it, so\n * the default cannot be the only option.\n */\nfunction takeBundle(argv: string[]): { bundle: string; rest: string[] } {\n const at = argv.indexOf(\"--bundle\");\n if (at === -1) {\n return { bundle: join(process.cwd(), KB_DIR), rest: argv };\n }\n const bundle = argv[at + 1];\n if (!bundle) die(\"--bundle requires a path\");\n return { bundle, rest: [...argv.slice(0, at), ...argv.slice(at + 2)] };\n}\n\nfunction readStdin(): Promise<string> {\n return new Promise((resolve, reject) => {\n let text = \"\";\n process.stdin.setEncoding(\"utf8\");\n process.stdin.on(\"data\", (chunk) => (text += chunk));\n process.stdin.on(\"end\", () => resolve(text));\n process.stdin.on(\"error\", reject);\n });\n}\n\nfunction die(message: string): never {\n process.stderr.write(`strauss-kb: error: ${message}\\n`);\n process.exit(1);\n}\n\n/** First sentence, capped — the full text is what an MCP client shows. */\nfunction summarise(description: string): string {\n const first = description.split(\"\\n\")[0] ?? \"\";\n const sentence = first.includes(\". \")\n ? `${first.slice(0, first.indexOf(\". \"))}.`\n : first;\n return sentence.length > 78 ? `${sentence.slice(0, 75)}…` : sentence;\n}\n\nfunction usage(): string {\n const width = Math.max(...KB_COMMANDS.map((command) => command.usage.length));\n return [\n \"strauss-kb — knowledge base commands\",\n \"\",\n \"Usage: strauss-kb [--bundle PATH] <command> [args]\",\n \"\",\n ...KB_COMMANDS.map(\n (command) =>\n ` ${command.usage.padEnd(width)} ${summarise(command.description)}`,\n ),\n \"\",\n ` --bundle PATH defaults to ./${KB_DIR}`,\n \" --json the machine shape, where a command prints a table\",\n \" -- everything after it is text, not flags\",\n \" --version the installed package version\",\n \" STRAUSS_KB_ACTOR names the writer in the log\",\n \"\",\n ].join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
@@ -2,7 +2,7 @@ import {
2
2
  KB_COMMANDS,
3
3
  KbStore,
4
4
  VERSION
5
- } from "./chunk-GKUQOJEK.js";
5
+ } from "./chunk-OFDWRMY6.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-LCQKARFK.js.map
49
+ //# sourceMappingURL=chunk-MWWDD23L.js.map