@saasontools/strauss-kb 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Assaf Kamil
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,340 @@
1
+ # @saasontools/strauss-kb
2
+
3
+ A knowledge base is a directory of small markdown records. Copy the directory
4
+ and you have the whole thing — nothing outside it is needed to read, search,
5
+ adjudicate, or trace it.
6
+
7
+ This package is that directory's library, its command line, and its MCP server.
8
+ All three project one command table, so a capability exists in every surface or
9
+ in none.
10
+
11
+ The point of the format is **standing**, not storage. A search engine answers
12
+ "does this match?"; a knowledge base also has to answer "is this still what we
13
+ hold?" — and the two disagree in a predictable direction, because a superseded
14
+ record is usually the older, longer, more general one and its replacement is
15
+ usually a narrowing. Every result therefore arrives flagged rather than
16
+ filtered.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install -g @saasontools/strauss-kb
22
+ ```
23
+
24
+ Global install is the supported path. The consumers of the CLI are agent skills
25
+ that shell out to `strauss-kb` by name, many times per session and from whatever
26
+ directory the work happens to be in — so the binary has to be on `PATH` without
27
+ a per-project setup step, and per-call resolution latency is paid on every call.
28
+ The trade-off accepted is that the version is machine-wide and not pinned by the
29
+ consuming project; the on-disk format is the compatibility contract, and the
30
+ reader is deliberately tolerant of records it did not write (unknown frontmatter
31
+ keys are preserved, a missing status defaults).
32
+
33
+ Two alternatives work and are not the documented convention:
34
+
35
+ | | Command | When it fits |
36
+ | -------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
37
+ | Per invocation | `npx -y @saasontools/strauss-kb@0.1` | Pinned and zero-install; adds resolution latency to every call and needs a warm npx cache or a network. |
38
+ | Project-local | `pnpm add -D @saasontools/strauss-kb` then `pnpm exec strauss-kb` | Pinned per repository and offline after install; bare `strauss-kb` does not resolve outside that repository, so skills cannot use one spelling. |
39
+
40
+ As a library:
41
+
42
+ ```bash
43
+ npm install @saasontools/strauss-kb
44
+ ```
45
+
46
+ Ships ESM and CommonJS. A consumer that transpiles per-file to CommonJS without
47
+ bundling can `require()` it without depending on its Node version honouring
48
+ `require(esm)`.
49
+
50
+ ## What is in a base
51
+
52
+ ```
53
+ <kb>/
54
+ <type>.<slug>.md records
55
+ INDEX.md index derived, store-owned
56
+ log.jsonl history primary, append-only
57
+ .index.sqlite search derived, gitignored
58
+ ```
59
+
60
+ The default base is `.strauss/kb` relative to the working directory;
61
+ `--bundle PATH` addresses any other. A scratch base under a worktree and a
62
+ committed base versioned beside the code it describes are the same format with
63
+ different lifetimes. Nothing promotes one to the other.
64
+
65
+ `INDEX.md` and `log.jsonl` are store-owned and differ in kind — treating them
66
+ alike is how the history gets lost:
67
+
68
+ | | `INDEX.md` | `log.jsonl` |
69
+ | ------- | ------------------------------------------ | ------------------------------------------- |
70
+ | Nature | derived — recomputable from frontmatter | primary — records events nothing else holds |
71
+ | Write | full regenerate | append |
72
+ | Repair | rebuilt when it disagrees with the records | malformed lines reported, never rewritten |
73
+ | If lost | reconstructed free | gone |
74
+
75
+ Repair-on-read, not coordination, is what lets both exist without a lock. The
76
+ index is _eventually_ correct: a writer whose scan predated another's record
77
+ publishes a briefly stale index, and the next read through the store settles it.
78
+
79
+ ## Records
80
+
81
+ The filename is the identity. `fact.auth-retries.md` has concept id
82
+ `fact.auth-retries` — `<type>.<slug>`, both halves kebab-case. One record per
83
+ file, so parallel writers never merge; they only choose distinct names.
84
+
85
+ Records are [OKF](https://github.com/GoogleCloudPlatform/knowledge-catalog)
86
+ concepts. `type` is the only key OKF requires; `title`, `description`,
87
+ `resource`, `tags`, `sources`, `generated`, `verified`, and `stale_after` are
88
+ OKF's. Unknown keys are preserved rather than stripped, as OKF requires of
89
+ consumers.
90
+
91
+ Anything prefixed `strauss_` is this package's extension, namespaced so a later
92
+ OKF version defining the same name cannot collide:
93
+
94
+ | Key | Meaning |
95
+ | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
96
+ | `strauss_status` | `draft`, `proposed`, `accepted`, `open`, `resolved`, `rejected`, `superseded`. Parses with a default of `draft`. |
97
+ | `strauss_supersedes` / `strauss_superseded_by` | Both directions of a supersession, written together. |
98
+ | `strauss_anchors` | `{ file, symbol? }` — where the record attaches in the code. Symbolic, because a line number written mid-change is wrong by the end of it. |
99
+ | `strauss_assumption` | The claim has no source, said as a field rather than as a fake entry in `sources`. |
100
+ | `strauss_answered` | Who resolved an open question, and when. |
101
+ | `strauss_verify` | Checks that would confirm the record still holds. |
102
+ | `strauss_materiality` / `strauss_confidence` / `strauss_owner` | `blocking`/`important`/`non-blocking`, `low`/`medium`/`high`, and a name. |
103
+
104
+ Edges are markdown links in the body, as OKF specifies — untyped, with the kind
105
+ conveyed by the surrounding prose. Broken links are legal: records are routinely
106
+ written before the ones they point at exist.
107
+
108
+ Twelve record types differ only in what their body answers and where they start
109
+ in the lifecycle — `fact`, `requirement`, `constraint`, `decision`,
110
+ `assumption`, `open-question`, `risk`, `contract`, `flow`, `affected-system`,
111
+ `test-obligation`, `source-note`. `strauss-kb types` prints each one's purpose,
112
+ body sections, and initial status; a section a type does not define is rejected
113
+ rather than written.
114
+
115
+ Do not work from memory on the frontmatter contract — `strauss-kb schema` emits
116
+ JSON Schema generated from the code that enforces it, so it cannot drift from
117
+ what a write will accept.
118
+
119
+ ```yaml
120
+ ---
121
+ type: decision
122
+ title: Compare-and-swap rather than a lock
123
+ description: A stale lock hold blocks every later writer.
124
+ generated: { by: agent, at: 2026-08-16T09:14:00Z }
125
+ verified: []
126
+ strauss_status: accepted
127
+ strauss_anchors:
128
+ - { file: src/kb-store.ts, symbol: KbStore.setStatus }
129
+ ---
130
+
131
+ ## Decision
132
+
133
+ Read-modify-write checks a content digest immediately before publishing.
134
+
135
+ ## Rejected
136
+
137
+ A lock file. It closes the window and adds a stale-hold failure mode that is
138
+ worse than the residue.
139
+ ```
140
+
141
+ ## Writes
142
+
143
+ Records are staged to a sibling file and published atomically, so a concurrent
144
+ reader sees a whole record or none. Publication uses `link`, which fails when
145
+ the name is taken — two writers choosing one concept id is a 409 the caller must
146
+ answer, by picking a more specific slug or by saying it meant to replace.
147
+ `rename` is used only when the caller passes `overwrite`.
148
+
149
+ `supersede` writes both directions, so a backlink cannot drift in normal use and
150
+ `validate` drops to catching hand-edits.
151
+
152
+ Records are never deleted. Superseding keeps the earlier reasoning inspectable,
153
+ which is what a later `trace` reads.
154
+
155
+ ## CLI
156
+
157
+ ```
158
+ strauss-kb [--bundle PATH] <command> [args]
159
+
160
+ write <type> < record.json Write one record.
161
+ write-decision < decision.json Write a decision, with the rejected alternative as a field.
162
+ no-decision <reason...> Claim in one sentence that there was nothing to decide.
163
+ status <concept-id> <status> Move a record's status, compare-and-swap.
164
+ supersede <concept-id> <replacement-id> Mark a record superseded, linking both directions.
165
+ answer <concept-id> <answer...> Resolve an open question and append the answer.
166
+ load [type] [--budget N] Hand over the whole base, each record with its standing.
167
+ query <text...> Search; every match arrives flagged with its standing.
168
+ trace <concept-id> [edges...] How a position was arrived at, as a timeline.
169
+ list [type] Every record, optionally narrowed to one type.
170
+ index The index, rebuilt if it disagrees with the records.
171
+ log What touched what, and when.
172
+ validate Cross-record checks. Exits 1 when it reports a problem.
173
+ schema JSON Schema for the format.
174
+ types The twelve types, their sections and initial status.
175
+
176
+ --bundle PATH defaults to ./.strauss/kb
177
+ STRAUSS_KB_ACTOR names the writer in the log
178
+ ```
179
+
180
+ Results go to stdout as JSON — `index` is markdown, which is what it is. Errors
181
+ go to stderr and exit 1. `validate` is the one command whose exit code is not
182
+ just "did it run": a check that reports a problem succeeded as a command and
183
+ failed as a check, so it exits 1 with its findings on stdout.
184
+
185
+ ```bash
186
+ strauss-kb --bundle .strauss/kb write fact <<'JSON'
187
+ {
188
+ "slug": "cache-key-includes-region",
189
+ "title": "The cache key includes the region",
190
+ "why": "A region-less key serves one region another region's data.",
191
+ "sections": { "Claim": "Every key is prefixed with the region." },
192
+ "anchors": [{ "file": "src/cache/order-cache.ts", "symbol": "OrderCache.get" }]
193
+ }
194
+ JSON
195
+
196
+ strauss-kb query cache key region
197
+ strauss-kb validate || echo "problems above"
198
+ ```
199
+
200
+ ## MCP server
201
+
202
+ `strauss-kb-mcp` speaks stdio and takes no API key and no required environment.
203
+ Every CLI verb is a tool: `kb_write`, `kb_write_decision`, `kb_no_decision`,
204
+ `kb_status`, `kb_supersede`, `kb_answer`, `kb_load`, `kb_query`, `kb_trace`,
205
+ `kb_list`, `kb_index`, `kb_log`, `kb_validate`, `kb_schema`, `kb_types`. Every
206
+ tool but `kb_schema` and `kb_types` takes a `bundlePath`; those two describe the
207
+ format rather than any one base.
208
+
209
+ ```json
210
+ {
211
+ "mcpServers": {
212
+ "strauss-kb": { "command": "strauss-kb-mcp" }
213
+ }
214
+ }
215
+ ```
216
+
217
+ The tool descriptions carry the judgment a schema cannot: that an unsourced
218
+ claim is an `assumption` and not a `fact` with a vague source, that a conflict
219
+ between two records belongs in a `risk` or a superseding `decision` rather than
220
+ being quietly resolved, and that `kb_load` is usually the right first call.
221
+
222
+ `STRAUSS_KB_ACTOR` names the writer in the log. Diagnostics go to stderr,
223
+ because stdout is the JSON-RPC transport.
224
+
225
+ ## Library
226
+
227
+ ```ts
228
+ import { KbStore, composeRecord, matchToDiff } from "@saasontools/strauss-kb";
229
+
230
+ const store = new KbStore();
231
+ await store.write(
232
+ ".strauss/kb",
233
+ composeRecord("decision", input, "agent", new Date().toISOString()),
234
+ );
235
+
236
+ const hits = await store.query(".strauss/kb", "cache key");
237
+ for (const hit of hits) {
238
+ hit.standing; // current | superseded | rejected | unsettled | open
239
+ hit.heads; // where the supersession chain ends
240
+ hit.warnings; // rejected, broken-chain, forked-chain, stale, unverified…
241
+ }
242
+ ```
243
+
244
+ `matchToDiff` answers a different question from `query`: given a structural
245
+ description of a diff, which records are anchored to each hunk. It takes hunks
246
+ and optional symbol ranges rather than a patch, so this package carries no diff
247
+ parser, and it degrades to file-level precision — labelled as such — when a
248
+ symbol cannot be resolved.
249
+
250
+ ## Retrieval
251
+
252
+ Three axes decide whether a record answers a question, and only one is a search
253
+ problem:
254
+
255
+ | Axis | Source | Question |
256
+ | --------- | ------------------------------------------------------- | ------------------------------- |
257
+ | Relevance | BM25 where an index exists, substring where it does not | does this match? |
258
+ | Standing | `strauss_status`, the supersession chain | is this still what we hold? |
259
+ | Freshness | `stale_after`, `verified[]` | has anyone confirmed it lately? |
260
+
261
+ **Load before you search.** These bases run to a few thousand tokens — twenty
262
+ records measured at about 3,000 — so the first thing to try is taking all of it.
263
+ On nine questions whose wording appears in no record, a reader holding the whole
264
+ base answered eight; embedding search over the same records answered four. Two
265
+ of those differences are structural rather than matters of degree: a reader can
266
+ say no record answers the question, where vector search returns its nearest
267
+ neighbour whatever the distance; and a reader picks the record that answers the
268
+ question rather than the one nearest the topic.
269
+
270
+ `load` refuses rather than truncating when a base exceeds its budget (25,000
271
+ tokens by default). A truncated base is indistinguishable from a complete one,
272
+ so a caller would answer "that was never decided" from a slice it did not know
273
+ was a slice. Superseded records come back as name, replacement and date only —
274
+ their bodies no longer hold, and a body read later in a long session outlives
275
+ the qualifier that said so. `trace` still reaches them by id.
276
+
277
+ **Flag, never filter.** `query` returns every hit with its standing, because a
278
+ filtered result set is invisible — the caller cannot tell it missed anything.
279
+ The single exception is narrow: a superseded record is dropped only when its
280
+ replacement is also in the results, so the thread is never lost.
281
+
282
+ **Trace inverts the point query.** In a query a `rejected` record is the most
283
+ dangerous thing retrievable — a well-formed assertion of what someone decided
284
+ _not_ to do. In a history it is the content. `trace` follows supersession,
285
+ shared code anchors, and shared sources, and orders by `generated.at`; ranking a
286
+ history is meaningless when the sequence is the point.
287
+
288
+ Chain resolution happens on read. A stored head would need rewriting on every
289
+ ancestor whenever a chain grows, which is derived state that goes stale. The
290
+ walk follows both pointers, so a hand-edit that left one side behind cannot
291
+ return a record the base openly claims is replaced. A cycle terminates with
292
+ `chain-cycle`; a fork reports every head rather than presenting a guess as a
293
+ fact; a missing replacement is `broken-chain` with no head — the case that needs
294
+ the most care, because returning the stale record unmarked looks exactly like
295
+ success.
296
+
297
+ ## Optional search tier
298
+
299
+ `@tobilu/qmd` is an **optional peer dependency** providing BM25 (`searchLex`,
300
+ no model download) over a `.index.sqlite` per base, rebuilt when a record is
301
+ newer than the index.
302
+
303
+ ```bash
304
+ npm install -g @tobilu/qmd # alongside a global strauss-kb
305
+ ```
306
+
307
+ With it absent — the default — `query` falls back to a substring scan over
308
+ concept ids, titles, descriptions, and bodies. Nothing throws, no answer changes
309
+ shape, and only recall degrades. Measured against that fallback on a
310
+ twenty-record base, the lexical tier wins on word forms (`pages` finds a record
311
+ saying only `page`) and on little else: eight of nine probe queries returned
312
+ exactly what substring returned.
313
+
314
+ The vector tier is deliberately off. It does close the semantic gap — "why not
315
+ just use a mutex" finds a record about compare-and-swap that no lexical match
316
+ can — but its scores do not separate right from wrong. A wrong hit scored 0.318
317
+ against a correct one at 0.295, and any threshold that drops the first drops the
318
+ second. Scores are evidence for a reader to weigh, not a filter to apply before
319
+ one.
320
+
321
+ qmd is used as a library, never through its own MCP server: that would let a
322
+ caller reach a base without going through the store, and its default markdown
323
+ glob returns `INDEX.md` as a search hit.
324
+
325
+ ## Constraints worth knowing
326
+
327
+ **The store is the sole accessor, not merely the sole writer.** Excluding
328
+ store-owned files from listings and repairing the index on read hold only while
329
+ everything goes through one door. Reading one record by a concept id you already
330
+ hold is the exception — no invariant, deterministic path.
331
+
332
+ **Cross-base questions are unaskable.** Supersession, traces, and search stop at
333
+ the directory boundary. "Was this settled somewhere else?" is answered by a
334
+ person choosing which base to open. That is the price of a base that can be
335
+ copied, deleted, or handed over whole, and it is what keeps the search index
336
+ disposable.
337
+
338
+ ## License
339
+
340
+ MIT
@@ -0,0 +1,93 @@
1
+ import {
2
+ KB_COMMANDS,
3
+ KB_COMMANDS_BY_NAME,
4
+ KB_DIR,
5
+ KbStore
6
+ } from "./chunk-ZSYSHJVZ.js";
7
+
8
+ // src/cli.ts
9
+ import { join } from "path";
10
+ async function runKbCli(argv) {
11
+ const { bundle, rest } = takeBundle(argv);
12
+ const name = rest[0] ?? "";
13
+ if (!name || name === "-h" || name === "--help") {
14
+ process.stdout.write(usage());
15
+ return;
16
+ }
17
+ const command = KB_COMMANDS_BY_NAME.get(name);
18
+ if (!command) die(`unknown command ${name}`);
19
+ const raw = await command.fromArgv(rest, bundle, readStdin);
20
+ const parsed = command.input.safeParse(raw);
21
+ if (!parsed.success) {
22
+ die(
23
+ `${name}: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ")}`
24
+ );
25
+ }
26
+ const store = new KbStore({
27
+ warn: (entry) => process.stderr.write(`${JSON.stringify(entry)}
28
+ `)
29
+ });
30
+ const result = await command.run(
31
+ {
32
+ store,
33
+ actor: process.env.STRAUSS_KB_ACTOR ?? "unknown",
34
+ now: () => (/* @__PURE__ */ new Date()).toISOString()
35
+ },
36
+ parsed.data
37
+ );
38
+ if (command.failsWhen?.(result)) process.exitCode = 1;
39
+ process.stdout.write(
40
+ typeof result === "string" ? result.endsWith("\n") ? result : `${result}
41
+ ` : `${JSON.stringify(result, null, 2)}
42
+ `
43
+ );
44
+ }
45
+ function takeBundle(argv) {
46
+ const at = argv.indexOf("--bundle");
47
+ if (at === -1) {
48
+ return { bundle: join(process.cwd(), KB_DIR), rest: argv };
49
+ }
50
+ const bundle = argv[at + 1];
51
+ if (!bundle) die("--bundle requires a path");
52
+ return { bundle, rest: [...argv.slice(0, at), ...argv.slice(at + 2)] };
53
+ }
54
+ function readStdin() {
55
+ return new Promise((resolve, reject) => {
56
+ let text = "";
57
+ process.stdin.setEncoding("utf8");
58
+ process.stdin.on("data", (chunk) => text += chunk);
59
+ process.stdin.on("end", () => resolve(text));
60
+ process.stdin.on("error", reject);
61
+ });
62
+ }
63
+ function die(message) {
64
+ process.stderr.write(`strauss-kb: error: ${message}
65
+ `);
66
+ process.exit(1);
67
+ }
68
+ function summarise(description) {
69
+ const first = description.split("\n")[0] ?? "";
70
+ const sentence = first.includes(". ") ? `${first.slice(0, first.indexOf(". "))}.` : first;
71
+ return sentence.length > 78 ? `${sentence.slice(0, 75)}\u2026` : sentence;
72
+ }
73
+ function usage() {
74
+ const width = Math.max(...KB_COMMANDS.map((command) => command.usage.length));
75
+ return [
76
+ "strauss-kb \u2014 knowledge base commands",
77
+ "",
78
+ "Usage: strauss-kb [--bundle PATH] <command> [args]",
79
+ "",
80
+ ...KB_COMMANDS.map(
81
+ (command) => ` ${command.usage.padEnd(width)} ${summarise(command.description)}`
82
+ ),
83
+ "",
84
+ ` --bundle PATH defaults to ./${KB_DIR}`,
85
+ " STRAUSS_KB_ACTOR names the writer in the log",
86
+ ""
87
+ ].join("\n");
88
+ }
89
+
90
+ export {
91
+ runKbCli
92
+ };
93
+ //# sourceMappingURL=chunk-KGM34MYU.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.js\";\nimport { KB_DIR, KbStore } from \"./kb-store.js\";\n\nexport async function runKbCli(argv: string[]): Promise<void> {\n const { bundle, rest } = takeBundle(argv);\n const name = rest[0] ?? \"\";\n\n if (!name || name === \"-h\" || name === \"--help\") {\n process.stdout.write(usage());\n return;\n }\n\n const command = KB_COMMANDS_BY_NAME.get(name);\n if (!command) die(`unknown command ${name}`);\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)) process.exitCode = 1;\n process.stdout.write(\n typeof result === \"string\"\n ? result.endsWith(\"\\n\")\n ? result\n : `${result}\\n`\n : `${JSON.stringify(result, null, 2)}\\n`,\n );\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 \" STRAUSS_KB_ACTOR names the writer in the log\",\n \"\",\n ].join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,47 @@
1
+ import {
2
+ KB_COMMANDS,
3
+ KbStore
4
+ } from "./chunk-ZSYSHJVZ.js";
5
+
6
+ // src/mcp.ts
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ function createKbMcpServer() {
10
+ const server = new McpServer({ name: "strauss-kb", version: "0.1.0" });
11
+ const store = new KbStore({
12
+ warn: (entry) => process.stderr.write(`${JSON.stringify(entry)}
13
+ `)
14
+ });
15
+ const ctx = {
16
+ store,
17
+ actor: process.env.STRAUSS_KB_ACTOR ?? "mcp",
18
+ now: () => (/* @__PURE__ */ new Date()).toISOString()
19
+ };
20
+ for (const command of KB_COMMANDS) {
21
+ server.registerTool(
22
+ command.tool,
23
+ { description: command.description, inputSchema: command.input.shape },
24
+ async (args) => {
25
+ const result = await command.run(ctx, command.input.parse(args));
26
+ return {
27
+ content: [
28
+ {
29
+ type: "text",
30
+ text: typeof result === "string" ? result : JSON.stringify(result, null, 2)
31
+ }
32
+ ]
33
+ };
34
+ }
35
+ );
36
+ }
37
+ return server;
38
+ }
39
+ async function runKbMcpServer() {
40
+ await createKbMcpServer().connect(new StdioServerTransport());
41
+ }
42
+
43
+ export {
44
+ createKbMcpServer,
45
+ runKbMcpServer
46
+ };
47
+ //# sourceMappingURL=chunk-WFHYWZX5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mcp.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { KB_COMMANDS } from \"./commands.js\";\nimport { KbStore } from \"./kb-store.js\";\n\n/**\n * A knowledge base's own MCP server, over stdio.\n *\n * Standalone because a base is self-contained: a directory of markdown that\n * needs no database, no HTTP surface, and no running service to read. Folding\n * these tools into a larger server would make every consumer start that server\n * to open files it could open itself.\n *\n * Every tool is a projection of `KB_COMMANDS`, which the CLI also projects, so\n * the two cannot drift.\n */\nexport function createKbMcpServer(): McpServer {\n const server = new McpServer({ name: \"strauss-kb\", version: \"0.1.0\" });\n const store = new KbStore({\n warn: (entry) => process.stderr.write(`${JSON.stringify(entry)}\\n`),\n });\n const ctx = {\n store,\n actor: process.env.STRAUSS_KB_ACTOR ?? \"mcp\",\n now: () => new Date().toISOString(),\n };\n\n for (const command of KB_COMMANDS) {\n server.registerTool(\n command.tool,\n { description: command.description, inputSchema: command.input.shape },\n async (args: unknown) => {\n const result = await command.run(ctx, command.input.parse(args));\n return {\n content: [\n {\n type: \"text\" as const,\n text:\n typeof result === \"string\"\n ? result\n : JSON.stringify(result, null, 2),\n },\n ],\n };\n },\n );\n }\n\n return server;\n}\n\nexport async function runKbMcpServer(): Promise<void> {\n await createKbMcpServer().connect(new StdioServerTransport());\n}\n"],"mappings":";;;;;;AAAA,SAAS,iBAAiB;AAC1B,SAAS,4BAA4B;AAe9B,SAAS,oBAA+B;AAC7C,QAAM,SAAS,IAAI,UAAU,EAAE,MAAM,cAAc,SAAS,QAAQ,CAAC;AACrE,QAAM,QAAQ,IAAI,QAAQ;AAAA,IACxB,MAAM,CAAC,UAAU,QAAQ,OAAO,MAAM,GAAG,KAAK,UAAU,KAAK,CAAC;AAAA,CAAI;AAAA,EACpE,CAAC;AACD,QAAM,MAAM;AAAA,IACV;AAAA,IACA,OAAO,QAAQ,IAAI,oBAAoB;AAAA,IACvC,KAAK,OAAM,oBAAI,KAAK,GAAE,YAAY;AAAA,EACpC;AAEA,aAAW,WAAW,aAAa;AACjC,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,EAAE,aAAa,QAAQ,aAAa,aAAa,QAAQ,MAAM,MAAM;AAAA,MACrE,OAAO,SAAkB;AACvB,cAAM,SAAS,MAAM,QAAQ,IAAI,KAAK,QAAQ,MAAM,MAAM,IAAI,CAAC;AAC/D,eAAO;AAAA,UACL,SAAS;AAAA,YACP;AAAA,cACE,MAAM;AAAA,cACN,MACE,OAAO,WAAW,WACd,SACA,KAAK,UAAU,QAAQ,MAAM,CAAC;AAAA,YACtC;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,eAAsB,iBAAgC;AACpD,QAAM,kBAAkB,EAAE,QAAQ,IAAI,qBAAqB,CAAC;AAC9D;","names":[]}