@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 +21 -0
- package/README.md +340 -0
- package/dist/chunk-KGM34MYU.js +93 -0
- package/dist/chunk-KGM34MYU.js.map +1 -0
- package/dist/chunk-WFHYWZX5.js +47 -0
- package/dist/chunk-WFHYWZX5.js.map +1 -0
- package/dist/chunk-ZSYSHJVZ.js +1527 -0
- package/dist/chunk-ZSYSHJVZ.js.map +1 -0
- package/dist/cli-main.cjs +1567 -0
- package/dist/cli-main.cjs.map +1 -0
- package/dist/cli-main.d.cts +1 -0
- package/dist/cli-main.d.ts +1 -0
- package/dist/cli-main.js +15 -0
- package/dist/cli-main.js.map +1 -0
- package/dist/index.cjs +1817 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +977 -0
- package/dist/index.d.ts +977 -0
- package/dist/index.js +202 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-main.cjs +1522 -0
- package/dist/mcp-main.cjs.map +1 -0
- package/dist/mcp-main.d.cts +1 -0
- package/dist/mcp-main.d.ts +1 -0
- package/dist/mcp-main.js +15 -0
- package/dist/mcp-main.js.map +1 -0
- package/package.json +72 -0
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":[]}
|