@msareen/knowledge-hub-builder 0.1.5 → 0.1.8
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/.bundle_template/sources.yaml +1 -0
- package/AGENTS.md +20 -0
- package/README.md +106 -10
- package/SPEC.md +217 -3
- package/document/faq.md +1 -0
- package/package.json +1 -1
- package/scripts/cli.ts +86 -17
- package/scripts/hubs.ts +698 -0
- package/scripts/ingest/exclude.ts +37 -0
- package/scripts/ingest/files.ts +8 -2
- package/scripts/ingest/folder.ts +13 -3
- package/scripts/ingest/index.ts +10 -2
- package/scripts/init.ts +27 -13
- package/scripts/lib/args.ts +9 -5
- package/scripts/lib/create.ts +39 -0
- package/scripts/lib/log.ts +40 -0
- package/scripts/lib/registry.ts +306 -0
- package/scripts/lib/relocate.ts +165 -0
- package/scripts/lib/schema.ts +75 -0
- package/scripts/lib/upgrade.ts +181 -12
- package/skills/ingest/SKILL.md +47 -8
package/AGENTS.md
CHANGED
|
@@ -141,6 +141,22 @@ From outside, pass `--hub <dir>` or set `$KHB_HUB`.
|
|
|
141
141
|
| `khb ingest <bundle>` | acquire + extract every source in `sources.yaml` → `raw/`; maintains `log.md`. Name the bundle — with none it lists the hub's bundles and stops, unless there is nothing to choose between (no bundles, or only `default`), where it uses `default` |
|
|
142
142
|
| `khb export <bundle> [dest]` | standalone copy: bundle + common patterns, shareable alone |
|
|
143
143
|
|
|
144
|
+
These five work **outside** any hub, against a per-machine shortcut list at
|
|
145
|
+
`~/.khb/hubs-config.json` (`%USERPROFILE%\.khb` on Windows). They hold no knowledge — only
|
|
146
|
+
paths — and fill themselves in as hubs are used, so they need no maintenance.
|
|
147
|
+
|
|
148
|
+
| Command | Purpose |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `khb list` | every hub on this machine, with its description and path |
|
|
151
|
+
| `khb go [name\|N]` | open a hub — prints its path, then launches your agent there. Bare `khb` is this: one hub asks, several offer a list, none walks the user through creating the first. `--path` prints only the path |
|
|
152
|
+
| `khb agent [name]` | which agent `khb go` launches — `claude`, `codex`, a custom `--command`, or `none` |
|
|
153
|
+
| `khb update [new-path]` | repair the hub: `--path`/`-p` repoints the list and rewrites every old-path reference after a move (needs no arguments — the move is detected and announced by any khb command run there); `--schema`/`-s` backfills a bundle's `sources.yaml` to the current schema (e.g. a newly added `exclude:` field). No flag runs both. `--dry-run` first. Unrelated to `khb upgrade`, which only touches package-owned contract docs, never bundle content |
|
|
154
|
+
| `khb forget <name>` | drop a shortcut; the hub folder is untouched |
|
|
155
|
+
|
|
156
|
+
A hub's `name` and `description` in that list come from its own `khb.json`, so they travel
|
|
157
|
+
with it. Set them at creation with `khb init --name --description`, or edit the marker's
|
|
158
|
+
`name`/`description` keys — those two are yours, unlike the rest of the file.
|
|
159
|
+
|
|
144
160
|
There is no `khb catalog` command — cataloging is entirely a judgement pass.
|
|
145
161
|
|
|
146
162
|
## Ingest, then catalog — two steps, in that order
|
|
@@ -158,6 +174,10 @@ The single exception is a hub with nothing to choose between: with no bundles at
|
|
|
158
174
|
unnamed ingest lands in `default`, created on the spot, and where `default` is the only
|
|
159
175
|
bundle it lands there as it stands.
|
|
160
176
|
|
|
177
|
+
When a run finishes, **offer the catalog pass** for the bundle just ingested — report what
|
|
178
|
+
landed and how many rows are uncurated, then ask before curating anything. Neither stopping
|
|
179
|
+
silently at the summary nor cataloging unasked is right.
|
|
180
|
+
|
|
161
181
|
**Catalog** (`skills/catalog/SKILL.md`) is the judgement half, one bundle at a time: read
|
|
162
182
|
each `raw/` file, split it into concepts, give each OKF frontmatter, link them, register
|
|
163
183
|
them in `index.md`. When the runtime supports parallel agents, fan them out over the raw
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# knowledge-hub-builder
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<img src="images/demo.gif" alt="
|
|
4
|
+
<img src="images/demo.gif" alt="khb init in the shell, then an agent session: the ingest skill asks which bundle owns the material, runs khb ingest as a tool call, offers to catalog, writes the concept docs, then answers a question from them" width="600">
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
**KHB (Knowledge Hub Builder)** is a local, markdown-based knowledge base maintained with
|
|
@@ -27,8 +27,17 @@ KHB requires [Bun](https://bun.sh).
|
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
29
|
bun install -g @msareen/knowledge-hub-builder
|
|
30
|
+
khb
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
On a machine with no hubs yet, `khb` walks you through the first one — where it lives, what
|
|
34
|
+
to call it, which agent opens it, and a first bundle. Every question has a default, so
|
|
35
|
+
holding Enter produces a working hub.
|
|
36
|
+
|
|
37
|
+
To do the same by hand:
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
```bash
|
|
40
|
+
khb init ~/my-knowledge --description "Personal knowledge"
|
|
32
41
|
cd ~/my-knowledge
|
|
33
42
|
|
|
34
43
|
khb new-bundle personal "My accounts, plans, records, and reference material"
|
|
@@ -40,6 +49,7 @@ Add sources to `bundles/personal/sources.yaml`:
|
|
|
40
49
|
sources:
|
|
41
50
|
- type: folder
|
|
42
51
|
path: /absolute/path/to/documents
|
|
52
|
+
exclude: [drafts/] # optional — skip paths/globs before ingesting
|
|
43
53
|
- type: files
|
|
44
54
|
paths:
|
|
45
55
|
- /absolute/path/to/one.pdf
|
|
@@ -62,6 +72,9 @@ or ask the agent:
|
|
|
62
72
|
The agent discovers the KHB workflow skills from the hub and performs the catalog pass
|
|
63
73
|
after ingestion.
|
|
64
74
|
|
|
75
|
+
Later, from any terminal, `khb` on its own takes you back to the hub and starts your agent
|
|
76
|
+
there — see [Moving between hubs](#moving-between-hubs).
|
|
77
|
+
|
|
65
78
|
## How It Works
|
|
66
79
|
|
|
67
80
|
### 1. Ingest
|
|
@@ -69,20 +82,25 @@ after ingestion.
|
|
|
69
82
|
`khb ingest <bundle>` reads `sources.yaml` and writes extracted markdown under
|
|
70
83
|
`bundles/<bundle>/raw/`.
|
|
71
84
|
|
|
72
|
-
Supported
|
|
85
|
+
Supported out of the box, with no system tools and no further installation:
|
|
73
86
|
|
|
74
87
|
- text, markdown, CSV, JSON, and YAML
|
|
75
88
|
- PDF and DOCX
|
|
76
89
|
- ODT, XLSX, and PPTX
|
|
90
|
+
- OCR for images and scanned PDFs, applied automatically when a PDF has no text layer
|
|
91
|
+
|
|
92
|
+
OCR ships with KHB: `@hyzyla/pdfium`, `sharp`, and `tesseract.js` are ordinary dependencies,
|
|
93
|
+
so installing KHB pulls them down whether or not you ever ingest a scan. That costs roughly
|
|
94
|
+
75 MB of WASM plus `sharp`'s native binaries — the price of an ingest that never stalls
|
|
95
|
+
waiting for a setup step.
|
|
77
96
|
|
|
78
|
-
|
|
97
|
+
One extractor is genuinely optional:
|
|
79
98
|
|
|
80
|
-
-
|
|
81
|
-
|
|
99
|
+
- audio and video transcription needs a `whisper` or `faster-whisper` executable on `PATH`
|
|
100
|
+
(`pip install -U openai-whisper`)
|
|
82
101
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
CLI prints that location.
|
|
102
|
+
Without it, KHB leaves a pending row in `log.md` and prints the required setup rather than
|
|
103
|
+
failing the run.
|
|
86
104
|
|
|
87
105
|
Every raw markdown file carries provenance:
|
|
88
106
|
|
|
@@ -220,7 +238,7 @@ Commands can be run directly or requested through the matching agent skill.
|
|
|
220
238
|
|
|
221
239
|
| Command | Purpose |
|
|
222
240
|
|---|---|
|
|
223
|
-
| `khb init [dir]` | Create a hub |
|
|
241
|
+
| `khb init [dir] [--name N] [--description "…"]` | Create a hub |
|
|
224
242
|
| `khb upgrade` | Refresh package-owned contracts and skills (also runs automatically on version drift) |
|
|
225
243
|
| `khb new-bundle <name> ["scope"]` | Create and register a bundle |
|
|
226
244
|
| `khb ingest [bundle] [--force]` | Acquire and extract declared sources |
|
|
@@ -235,6 +253,84 @@ Additional ingest flags:
|
|
|
235
253
|
|
|
236
254
|
Run against a hub outside the current directory with `--hub <dir>` or `$KHB_HUB`.
|
|
237
255
|
|
|
256
|
+
### Moving between hubs
|
|
257
|
+
|
|
258
|
+
These four run **outside** any hub, from any terminal:
|
|
259
|
+
|
|
260
|
+
| Command | Purpose |
|
|
261
|
+
|---|---|
|
|
262
|
+
| `khb` | Open a hub. One registered hub asks; several show a list and take a pick; none walks you through creating the first |
|
|
263
|
+
| `khb list [--json]` | Every hub on this machine, with its description and path |
|
|
264
|
+
| `khb go <name\|N> [--path]` | Open one by name or list position. `--path` prints just the path, for `cd "$(khb go --path work)"` |
|
|
265
|
+
| `khb agent [name] [--command X] [--args "…"]` | Which agent `khb go` launches — `claude`, `codex`, anything on your PATH, or `none` to just print the path |
|
|
266
|
+
| `khb update [new-path] [--path\|-p] [--schema\|-s] [--from <old>] [--dry-run]` | Repair the hub: path references after a move, and/or backfill `sources.yaml` to the current schema. No flag runs both |
|
|
267
|
+
| `khb forget <name>` | Drop a hub from the list. The folder is untouched |
|
|
268
|
+
|
|
269
|
+
`khb go` prints the hub's path and then starts your agent there, so a bare `khb` from a
|
|
270
|
+
cold terminal ends with an agent open on the right folder. No program can change its
|
|
271
|
+
parent shell's directory, which is why the `cd` line is printed rather than performed.
|
|
272
|
+
|
|
273
|
+
The list lives in `~/.khb/hubs-config.json` (`%USERPROFILE%\.khb` on Windows). It holds
|
|
274
|
+
paths and one launch command — no knowledge — and fills itself in: any khb command run
|
|
275
|
+
inside a hub registers it, so hubs you already had show up without a migration step.
|
|
276
|
+
Delete the file and the next command in each hub puts it back.
|
|
277
|
+
|
|
278
|
+
A hub's name and description come from its own `khb.json`, so they travel with the hub
|
|
279
|
+
rather than living in one machine's list. Set them with `khb init --name --description`,
|
|
280
|
+
or edit those two keys in the marker later.
|
|
281
|
+
|
|
282
|
+
### `khb update` — repairing a hub
|
|
283
|
+
|
|
284
|
+
Two independent repairs, run together or apart:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
khb update --dry-run # what would change, both halves
|
|
288
|
+
khb update # apply both
|
|
289
|
+
khb update --path # only repoint paths after a move
|
|
290
|
+
khb update --schema # only backfill sources.yaml to the current schema
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**`--path`/`-p`** — after you move the folder, this repoints the shortcut list (a moved hub
|
|
294
|
+
shows as `MISSING` in `khb list` until you do) and rewrites the absolute paths recorded
|
|
295
|
+
*inside* the hub that named the old location: `sources.yaml` entries, `source:` headers in
|
|
296
|
+
`raw/`, `log.md` rows, `resource:` front matter. It needs no old path: the hub records where
|
|
297
|
+
it lives in its own `khb.json` (`path`), so any khb command run in a moved hub notices and
|
|
298
|
+
says so —
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
khb: this hub was at D:\kb\old and is now at D:\kb\new.
|
|
302
|
+
khb: repair them: khb update --path (--dry-run to preview)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
— and `--path` reads the old location straight out of the marker. That record travels with
|
|
306
|
+
the folder, so it works after `~/.khb` is deleted, on a second machine, or on a hub a
|
|
307
|
+
colleague handed you. Move a hub twice before repairing it and both former homes are
|
|
308
|
+
rewritten in one pass. For a hub last touched by a khb too old to have recorded a location,
|
|
309
|
+
the old path is worked out from the registry entry that now points at nothing, matched on
|
|
310
|
+
the identity stamp in `khb.json`. If it can't prove which entry that is, it asks; `--from
|
|
311
|
+
<old-path>` tells it outright.
|
|
312
|
+
|
|
313
|
+
The rewrite is a literal substitution with three guards: every spelling of the path moves
|
|
314
|
+
(native, forward-slashed, and JSON-escaped as `raw/` headers store it), each rewritten to
|
|
315
|
+
the same spelling of the new path; matches must end at a path boundary, so moving `…/old`
|
|
316
|
+
never touches a sibling `…/older`; and the new path is matched too and rewritten to itself,
|
|
317
|
+
which is what makes overlapping moves safe — lifting a hub out of its parent, or pushing it
|
|
318
|
+
down into a subdirectory of where it stood — and makes a second run a no-op.
|
|
319
|
+
`.git/`, `node_modules/` and the `inbox/` cache are skipped, as are binaries.
|
|
320
|
+
|
|
321
|
+
**`--schema`/`-s`** — a bundle's `sources.yaml` can predate a field khb's since learned about
|
|
322
|
+
(e.g. `exclude:`), with no way to discover it short of reading the docs. This backfills
|
|
323
|
+
missing optional fields with their default, per bundle, preserving your comments and
|
|
324
|
+
formatting. `khb upgrade` mentions when either half of `update` has something pending, as a
|
|
325
|
+
printed hint — it never runs `update` for you.
|
|
326
|
+
|
|
327
|
+
Both `khb update` and `khb ingest` state their whole plan before writing anything, then
|
|
328
|
+
report as they go — a position per file for work slow enough to matter, a live counter for
|
|
329
|
+
work that isn't. The counter goes to stderr, so redirected stdout stays clean.
|
|
330
|
+
|
|
331
|
+
Not to be confused with `khb upgrade`, which refreshes a hub's package-owned contract docs
|
|
332
|
+
and has nothing to do with moving anything.
|
|
333
|
+
|
|
238
334
|
To update the installed package:
|
|
239
335
|
|
|
240
336
|
```bash
|
package/SPEC.md
CHANGED
|
@@ -115,6 +115,8 @@ acting on them** — an agent can never read a protocol the CLI no longer implem
|
|
|
115
115
|
├── scripts/
|
|
116
116
|
│ ├── cli.ts # subcommand dispatch; --hub flag; the version drift check
|
|
117
117
|
│ ├── init.ts # khb init / khb upgrade
|
|
118
|
+
│ ├── hubs.ts # khb list / go / agent / update / forget — the only
|
|
119
|
+
│ │ # commands that run outside a hub
|
|
118
120
|
│ ├── new-bundle.ts # scaffold from .bundle_template, register in outer.index.md
|
|
119
121
|
│ ├── export.ts # bundle + common patterns → standalone shareable folder
|
|
120
122
|
│ ├── lint.ts # enforce skills/lint/SKILL.md across the hub
|
|
@@ -124,6 +126,9 @@ acting on them** — an agent can never read a protocol the CLI no longer implem
|
|
|
124
126
|
│ ├── extract.ts # every local extractor + the content-hash cache
|
|
125
127
|
│ ├── ledger.ts # log.md read/write
|
|
126
128
|
│ ├── paths.ts # package-side paths — importing it never needs a hub
|
|
129
|
+
│ ├── registry.ts # ~/.khb/hubs-config.json: where this machine's hubs are
|
|
130
|
+
│ ├── relocate.ts # khb update --path's path rewriter — pure text, no judgement
|
|
131
|
+
│ ├── schema.ts # khb update --schema: sources.yaml field diff/apply
|
|
127
132
|
│ ├── upgrade.ts # the refresh itself: `khb upgrade` and the drift check
|
|
128
133
|
│ └── util.ts # hub resolution + shared helpers
|
|
129
134
|
├── .bundle_template/ # copied by `khb new-bundle`
|
|
@@ -143,6 +148,210 @@ Rule 3 is the normal path: `cd` anywhere inside the hub and run `khb lint`. One
|
|
|
143
148
|
consequence worth stating — a hub is identified by its marker file, not its name, so
|
|
144
149
|
hubs may be renamed or moved freely, and nested hubs resolve to the innermost one.
|
|
145
150
|
|
|
151
|
+
### 2d. The machine registry — `~/.khb/hubs-config.json`
|
|
152
|
+
|
|
153
|
+
Rules 1–3 all assume you already know where the hub is. From a cold terminal in an
|
|
154
|
+
unrelated folder, you don't — and a person with a personal hub, a work hub and a client
|
|
155
|
+
hub has three paths to remember. So khb keeps **one file per machine** recording where the
|
|
156
|
+
hubs on it are:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
~/.khb/ # %USERPROFILE%\.khb on Windows; $KHB_HOME overrides
|
|
160
|
+
└── hubs-config.json
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"version": 1,
|
|
166
|
+
"defaultAgent": "claude",
|
|
167
|
+
"agents": { "claude": { "command": "claude", "args": [] },
|
|
168
|
+
"codex": { "command": "codex", "args": [] } },
|
|
169
|
+
"hubs": [
|
|
170
|
+
{ "name": "my-knowledge", "description": "Personal — work + home",
|
|
171
|
+
"path": "D:\\code\\my-knowledge", "added": "…", "lastUsed": "…" }
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Three properties define it:
|
|
177
|
+
|
|
178
|
+
- **It is a shortcut list, never knowledge.** It holds paths and one launch command.
|
|
179
|
+
Delete it and nothing is lost — the next command run inside each hub puts it back.
|
|
180
|
+
- **It fills itself in.** Every khb command that resolves a hub registers it, so hubs made
|
|
181
|
+
before the registry existed appear the first time anything is run in them. There is no
|
|
182
|
+
migration and no `register` command to remember. `khb forget <name>` drops a shortcut and
|
|
183
|
+
never touches the folder.
|
|
184
|
+
- **The hub is the authority on its own identity.** `name` and `description` are read out
|
|
185
|
+
of the hub's `khb.json` (`khb init --name --description`, or edit the file), so a hub
|
|
186
|
+
moved to another machine or cloned by a colleague describes itself the same way there.
|
|
187
|
+
Only when the marker says nothing does khb fall back to the folder name and a summary of
|
|
188
|
+
the bundles inside. `khb upgrade` merges rather than replaces the marker, so keys khb
|
|
189
|
+
does not own survive an upgrade.
|
|
190
|
+
|
|
191
|
+
The commands over it are `khb list`, `khb go`, `khb agent`, `khb update` and `khb forget` —
|
|
192
|
+
the only ones that run **outside** a hub, and therefore the only ones that skip hub
|
|
193
|
+
resolution and the version drift check. A bare `khb` is `khb go`: one hub asks to open it,
|
|
194
|
+
several show the list and take a pick, none prints the help.
|
|
195
|
+
|
|
196
|
+
`khb go` ends by launching your configured agent with the hub as its working directory.
|
|
197
|
+
No process can change its parent shell's directory, so `khb go` prints the `cd` line for
|
|
198
|
+
the human and passes the path to the agent as cwd — `khb go --path <name>` prints only the
|
|
199
|
+
path, for `cd "$(khb go --path work)"`. `khb agent none` turns the launch off entirely.
|
|
200
|
+
|
|
201
|
+
Every path stored in or compared against the registry is canonicalized first (`realpath`,
|
|
202
|
+
case-folded on Windows). One directory has several true names — `C:\Users\MANASV~1\…` and
|
|
203
|
+
`C:\Users\Manasvi Sareen\…` are the same folder, as is anything reached through a symlink —
|
|
204
|
+
and without this a single hub lists twice and `khb update --path` cannot tell it has already
|
|
205
|
+
been repaired.
|
|
206
|
+
|
|
207
|
+
### 2e. `khb update` — repairing a hub
|
|
208
|
+
|
|
209
|
+
Two independent repairs, selectable together or apart:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
khb update [new-path] [--path|-p] [--schema|-s] [--from <old-path>] [--dry-run]
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
No flag runs both. `--path`/`-p` repairs a moved hub; `--schema`/`-s` backfills a bundle's
|
|
216
|
+
`sources.yaml` to the current field set (§ scripts/lib/schema.ts). They are unrelated repairs
|
|
217
|
+
that happen to share a plan-then-write-then-report shape, so one command with two switches
|
|
218
|
+
beat two commands that would each need the same hub-resolution and dry-run plumbing.
|
|
219
|
+
|
|
220
|
+
#### `--path` — a hub that moved
|
|
221
|
+
|
|
222
|
+
Moving a hub folder breaks two things, and this half fixes both:
|
|
223
|
+
|
|
224
|
+
1. **The shortcut list** still points at the old folder. `khb list` marks it `MISSING`.
|
|
225
|
+
2. **Absolute paths recorded inside the hub** that named the old location — `sources.yaml`
|
|
226
|
+
entries, `source:` headers in `raw/`, `log.md` rows, `resource:` front matter — are now
|
|
227
|
+
dangling.
|
|
228
|
+
|
|
229
|
+
Run from inside the moved hub, or name it. Nothing else is needed, because **the hub records
|
|
230
|
+
its own location**. `khb.json` carries a `path` key — canonical, plus `pathAs` for the
|
|
231
|
+
spelling khb was invoked through when the two differ — and any khb command run in a hub
|
|
232
|
+
compares it against where the hub actually is. A mismatch is the move, stated by the only
|
|
233
|
+
witness that travelled with the folder:
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
khb: this hub was at D:\kb\old and is now at D:\kb\new.
|
|
237
|
+
khb: absolute paths recorded inside it still name the old location.
|
|
238
|
+
khb: repair them: khb update --path (--dry-run to preview)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The old location moves into `movedFrom`, where it waits until `--path` works it off.
|
|
242
|
+
A hub moved twice before anyone repaired it lists both former homes and all of them are
|
|
243
|
+
rewritten in the one pass. The key costs a string and is inert — nothing reads it but this
|
|
244
|
+
repair — but it is what makes the repair need no arguments, and it survives what the registry
|
|
245
|
+
does not: a deleted `~/.khb`, a first run on a second machine, a folder handed to a colleague.
|
|
246
|
+
|
|
247
|
+
Failing that — a hub last touched by a khb too old to have recorded a location — the old path
|
|
248
|
+
is *inferred, on proof only*: each registry entry records the `created` stamp from the hub's
|
|
249
|
+
marker, minted once at `khb init`, so the dead entry carrying this hub's stamp is provably the
|
|
250
|
+
same hub. A mere name match is circumstantial and is put to the user as a question rather than
|
|
251
|
+
acted on. `--from` settles it either way, and is still the answer when the move predates all
|
|
252
|
+
of this.
|
|
253
|
+
|
|
254
|
+
The rewrite itself is a **conversion, not an interpretation**, which is what keeps it in the
|
|
255
|
+
CLI rather than in an agent pass (§ AGENTS.md, division of labor): the same substring in
|
|
256
|
+
and out, no judgement about what a path means. Three properties make it safe to run
|
|
257
|
+
unattended:
|
|
258
|
+
|
|
259
|
+
- **Every spelling moves.** A path appears natively (`D:\a\b`), forward-slashed (`D:/a/b`),
|
|
260
|
+
and backslash-escaped inside JSON (`D:\\a\\b`, how `raw/` headers and `log.md` store a
|
|
261
|
+
source). All are matched, and each is rewritten to *the same spelling* of the new path, so
|
|
262
|
+
a JSON-escaped source stays JSON-escaped.
|
|
263
|
+
- **Matches end at a path boundary.** Moving `…/old` never touches `…/older`, a sibling
|
|
264
|
+
whose name merely starts the same way.
|
|
265
|
+
- **The new path is shielded from itself.** It is matched too, and rewritten to itself. That
|
|
266
|
+
is what makes an *overlapping* move safe — a hub lifted out of its parent (`…/kb/hub` →
|
|
267
|
+
`…/kb`) or pushed down into a subdirectory of where it stood (`…/kb` → `…/kb/hub`). In the
|
|
268
|
+
second case the old path occurs inside every already-correct reference, and without the
|
|
269
|
+
shield each would have the move applied to it a second time. Claiming those matches for an
|
|
270
|
+
identity rewrite also makes the whole command idempotent: run it twice and the second run
|
|
271
|
+
changes nothing. Only old and new naming *the same directory* is refused, there being no
|
|
272
|
+
move to repair.
|
|
273
|
+
|
|
274
|
+
`.git/`, `node_modules/` and the `inbox/` extraction cache are not walked; binary files and
|
|
275
|
+
anything over 8 MB are skipped. `--dry-run` reports the file-by-file hit count and writes
|
|
276
|
+
nothing.
|
|
277
|
+
|
|
278
|
+
Like `khb ingest`, it states the whole plan before writing anything and reports progress as
|
|
279
|
+
it walks (§2g).
|
|
280
|
+
|
|
281
|
+
#### `--schema` — a bundle's `sources.yaml` predates a field
|
|
282
|
+
|
|
283
|
+
A `sources.yaml` written before a field existed (e.g. `folder`/`files` sources gained
|
|
284
|
+
`exclude:`) has no way to discover it short of reading the docs. `scripts/lib/schema.ts`
|
|
285
|
+
holds the current, optional, backfillable field list per source `type`; this half diffs
|
|
286
|
+
every bundle's `sources.yaml` against it and, for anything missing, stages a default value —
|
|
287
|
+
or, for a field the schema has since deprecated, stages its removal. Comment-preserving:
|
|
288
|
+
it edits the parsed YAML document node-by-node (the `yaml` package's `Document` API) rather
|
|
289
|
+
than reserializing from scratch, so hand-written comments and structure in an untouched part
|
|
290
|
+
of the file survive. The one caveat is cosmetic, not correctness: re-serializing the whole
|
|
291
|
+
document can normalize whitespace in *other*, unrelated flow-style content in the same file.
|
|
292
|
+
|
|
293
|
+
There is no persisted schema version anywhere — the "schema" is just this file's current
|
|
294
|
+
shape, diffed fresh on every run, the same way `khb upgrade`'s `MANAGED`/`RETIRED` lists are
|
|
295
|
+
static and re-checked rather than tracked historically. `--dry-run` prints the changes and
|
|
296
|
+
writes nothing; otherwise it applies immediately, same convention as `--path`.
|
|
297
|
+
|
|
298
|
+
`khb upgrade` prints a one-line hint when either half of `update` has something pending —
|
|
299
|
+
never a prompt, since `upgrade` already runs unattended inside unrelated commands on version
|
|
300
|
+
drift, and a blocking question there would interrupt work that has nothing to do with either
|
|
301
|
+
repair.
|
|
302
|
+
|
|
303
|
+
The name says what it does, now that it does two things belonging to neither `khb upgrade`
|
|
304
|
+
(package-owned contract docs only) nor to a single-purpose `update-path`. The original
|
|
305
|
+
one-letter-from-`upgrade` objection to the shorter name held while `update` meant only path
|
|
306
|
+
repair; a second, unrelated repair under the same verb resolves it — `update` repairs what
|
|
307
|
+
the user's own bundles record, `upgrade` refreshes what the package owns, and the two no
|
|
308
|
+
longer read as near-synonyms.
|
|
309
|
+
|
|
310
|
+
### 2f. First run — the wizard
|
|
311
|
+
|
|
312
|
+
A bare `khb` on a machine with no hubs has a terminal in front of it and knows the one
|
|
313
|
+
thing the user needs to hear, so it asks rather than referring them to the docs. Five
|
|
314
|
+
questions, every one with a default that Enter accepts: where the hub goes, what to call
|
|
315
|
+
it, a one-line description, which agent opens it, and a first bundle.
|
|
316
|
+
|
|
317
|
+
Three things keep it honest:
|
|
318
|
+
|
|
319
|
+
- **It asks only what `khb init` takes as flags**, and calls the same `createHub()`. A hub
|
|
320
|
+
born in the wizard is byte-identical to one made by hand — there is no second creation
|
|
321
|
+
path to drift.
|
|
322
|
+
- **Agents are detected, not guessed.** `claude` and `codex` are probed with `--version`
|
|
323
|
+
and the ones present are marked; the first found is the default. Any other command can be
|
|
324
|
+
typed instead, or `none`.
|
|
325
|
+
- **A path that is already a hub is adopted, not overwritten.** The machine simply had not
|
|
326
|
+
heard of it, so it joins the list under its own name and the wizard stops there.
|
|
327
|
+
|
|
328
|
+
No terminal, or `--path` (a script asking for a path), falls back to three lines of
|
|
329
|
+
guidance. `khb list` on an empty machine likewise reports rather than starts a
|
|
330
|
+
conversation.
|
|
331
|
+
|
|
332
|
+
### 2g. Progress on the mechanical passes
|
|
333
|
+
|
|
334
|
+
`khb`'s half of the split is the deterministic half, and some of it is slow: a scanned PDF
|
|
335
|
+
is seconds per page, a video minutes per file, and a hub that has accumulated thousands of
|
|
336
|
+
raw documents takes a visible moment to walk. A silent process is indistinguishable from a
|
|
337
|
+
hung one, so every mechanical pass reports (`scripts/lib/log.ts`):
|
|
338
|
+
|
|
339
|
+
- **The plan, before any work.** Which hub, which bundle, which extractors are armed, how
|
|
340
|
+
many sources — or for `update --path`, the old path, the new path, and whether anything
|
|
341
|
+
will be written; for `update --schema`, which bundles and fields. A `--hub`/`$KHB_HUB`
|
|
342
|
+
run can target a folder you did not expect, and that must be visible before the first
|
|
343
|
+
write, not after.
|
|
344
|
+
- **Position within the run.** `[3/57]` per unit where a unit is slow enough to deserve its
|
|
345
|
+
own line (ingest's per-file extraction), and a single rewritten counter line where the
|
|
346
|
+
units are fast and numerous (`update --path` checking files).
|
|
347
|
+
- **An outcome per unit, and a closing summary** with wall-clock time.
|
|
348
|
+
|
|
349
|
+
The transient counter writes to **stderr** and repaints at ~12fps; with no terminal it
|
|
350
|
+
degrades to a milestone line every 500 units. So `khb … > out.txt` keeps clean, complete
|
|
351
|
+
stdout, and a CI log still shows the walk moving. There is no `--quiet`: the per-unit line
|
|
352
|
+
is the audit trail for a pass that rewrites files, and a run you have to repeat to find out
|
|
353
|
+
what it did is worse than a noisy one.
|
|
354
|
+
|
|
146
355
|
## 3. Routing model
|
|
147
356
|
|
|
148
357
|
- `outer.index.md` lists every bundle with a one-line scope and "route here when" hints.
|
|
@@ -195,6 +404,7 @@ Each bundle declares its sources in `sources.yaml`:
|
|
|
195
404
|
sources:
|
|
196
405
|
- type: folder # local disk
|
|
197
406
|
path: /abs/path/to/project-x
|
|
407
|
+
exclude: [drafts/] # optional — skip paths/globs before ingesting
|
|
198
408
|
- type: web
|
|
199
409
|
urls:
|
|
200
410
|
- https://example.com/design-doc
|
|
@@ -282,11 +492,15 @@ puts them on the CLI side of the §Division-of-labor line.
|
|
|
282
492
|
| DOCX | `mammoth`, `pandoc` if present | bundled | high |
|
|
283
493
|
| ODT, PPTX | `fflate` + XML | bundled | high |
|
|
284
494
|
| XLSX | `fflate` → one markdown table per sheet | bundled | high |
|
|
285
|
-
| scanned PDF | `pdfium` + `tesseract.js` (WASM) |
|
|
286
|
-
| Images (png/jpg/webp/tif) | `tesseract.js` |
|
|
495
|
+
| scanned PDF | `pdfium` + `tesseract.js` (WASM) | bundled, ~75 MB | low |
|
|
496
|
+
| Images (png/jpg/webp/tif) | `tesseract.js` | bundled, ~75 MB | low |
|
|
287
497
|
| Audio, video | `whisper` / `faster-whisper` | opt-in, pip | low |
|
|
288
498
|
|
|
289
|
-
|
|
499
|
+
The OCR stack is bundled rather than opt-in: an ingest that stops to ask for an install is
|
|
500
|
+
worse than an install that carries WASM nobody uses. Transcription stays opt-in because it is
|
|
501
|
+
a Python executable, not something a JS package manager can pull down.
|
|
502
|
+
|
|
503
|
+
A missing dep degrades to a ledger row with an empty `raw` and a printed install
|
|
290
504
|
hint — never to a failed run. `quality: low` output is a standing invitation for the catalog
|
|
291
505
|
pass to re-read the original: a vision read of a chart or a scanned table recovers what OCR
|
|
292
506
|
drops, and rewrites the raw file with `extract_tool: claude-vision`.
|
package/document/faq.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@msareen/knowledge-hub-builder",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Knowledge Hub Builder — a bundle-of-bundles knowledge base you build with an agent",
|
|
5
5
|
"keywords": ["knowledge-base", "okf", "agent", "wiki", "bundles", "claude", "codex", "rag"],
|
|
6
6
|
"license": "MIT",
|
package/scripts/cli.ts
CHANGED
|
@@ -3,16 +3,39 @@
|
|
|
3
3
|
// so nothing that resolves a hub may be imported at module scope.
|
|
4
4
|
import { version, findHub, markerIn, MARKER } from "./lib/paths";
|
|
5
5
|
|
|
6
|
-
const COMMANDS: Record<string, { load: () => Promise<unknown>;
|
|
7
|
-
init: { load: () => import("./init"),
|
|
8
|
-
upgrade: { load: () => import("./init"),
|
|
9
|
-
"new-bundle": { load: () => import("./new-bundle"),
|
|
10
|
-
ingest: {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
const COMMANDS: Record<string, { load: () => Promise<unknown>; usage: string; desc: string }> = {
|
|
7
|
+
init: { load: () => import("./init"), usage: 'khb init [dir] [--name N] [--description "…"]', desc: "create a hub here (or in dir)" },
|
|
8
|
+
upgrade: { load: () => import("./init"), usage: "khb upgrade", desc: "refresh this hub's contract docs" },
|
|
9
|
+
"new-bundle": { load: () => import("./new-bundle"), usage: 'khb new-bundle <name> ["scope"]', desc: "scaffold a bundle + register it" },
|
|
10
|
+
ingest: {
|
|
11
|
+
load: () => import("./ingest/index"),
|
|
12
|
+
usage: "khb ingest <bundle> [--force]",
|
|
13
|
+
desc: "acquire + extract declared sources → raw/ (name required once the hub has a bundle other than 'default')",
|
|
14
|
+
},
|
|
15
|
+
lint: { load: () => import("./lint"), usage: "khb lint", desc: "validate the hub against skills/lint/SKILL.md" },
|
|
16
|
+
visualize: {
|
|
17
|
+
load: () => import("./visualize"),
|
|
18
|
+
usage: "khb visualize [--port N] [--no-open]",
|
|
19
|
+
desc: "serve the live bundle graph in your browser; aliases: vis, viz",
|
|
20
|
+
},
|
|
21
|
+
export: { load: () => import("./export"), usage: "khb export <bundle> [dest]", desc: "standalone copy of one bundle" },
|
|
22
|
+
list: { load: () => import("./hubs"), usage: "khb list [--json]", desc: "every hub on this machine" },
|
|
23
|
+
go: { load: () => import("./hubs"), usage: "khb go [name|N] [--path]", desc: "open a hub with your agent (bare 'khb' picks one)" },
|
|
24
|
+
agent: { load: () => import("./hubs"), usage: "khb agent [name] [--command X]", desc: "which agent 'khb go' launches" },
|
|
25
|
+
update: {
|
|
26
|
+
load: () => import("./hubs"),
|
|
27
|
+
usage: "khb update [new] [--path|-p] [--schema|-s] [--dry-run]",
|
|
28
|
+
desc: "repair a moved hub's paths and/or backfill sources.yaml to the current schema",
|
|
29
|
+
},
|
|
30
|
+
forget: { load: () => import("./hubs"), usage: "khb forget <name>", desc: "drop a hub from the list (folder untouched)" },
|
|
14
31
|
};
|
|
15
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Commands that work *outside* a hub, against ~/.khb/hubs-config.json. They must not
|
|
35
|
+
* resolve or upgrade a hub — their whole job is running before you are in one.
|
|
36
|
+
*/
|
|
37
|
+
const REGISTRY_COMMANDS = new Set(["list", "go", "agent", "forget", "update"]);
|
|
38
|
+
|
|
16
39
|
// Short forms that just resolve to a canonical command above — kept out of COMMANDS
|
|
17
40
|
// itself so help text lists each command once. `-v` is taken by --version, so
|
|
18
41
|
// `visualize` gets word-shaped aliases instead of a letter one.
|
|
@@ -34,15 +57,39 @@ if (hubAt >= 0) {
|
|
|
34
57
|
}
|
|
35
58
|
|
|
36
59
|
const cmd0 = argv.shift();
|
|
37
|
-
|
|
60
|
+
// Bare `khb` is the way in from a cold terminal: pick a hub and open it. Help stays one
|
|
61
|
+
// word away, and is what you get when no hub has ever been registered.
|
|
62
|
+
const cmd = !cmd0 ? "go" : (ALIASES[cmd0] ?? cmd0);
|
|
63
|
+
|
|
64
|
+
if (cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
65
|
+
const width = Math.max(...Object.values(COMMANDS).map((c) => c.usage.length));
|
|
66
|
+
const printSection = (title: string, names: string[]) => {
|
|
67
|
+
console.log(title);
|
|
68
|
+
for (const name of names) {
|
|
69
|
+
const c = COMMANDS[name];
|
|
70
|
+
console.log(` ${c.usage.padEnd(width)} ${c.desc}`);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
38
73
|
|
|
39
|
-
|
|
40
|
-
console.log(
|
|
74
|
+
console.log(`khb ${version()} — Knowledge Hub Builder`);
|
|
75
|
+
console.log();
|
|
41
76
|
console.log(`khb is the supporting tool: it handles deterministic extraction, file plumbing,`);
|
|
42
77
|
console.log(`validation, and export. Your AI agent — Claude, Codex, Gemini, or another`);
|
|
43
|
-
console.log(`compatible agent — follows the workflow skills and orchestrates the knowledge work
|
|
44
|
-
|
|
45
|
-
|
|
78
|
+
console.log(`compatible agent — follows the workflow skills and orchestrates the knowledge work.`);
|
|
79
|
+
console.log();
|
|
80
|
+
|
|
81
|
+
printSection(
|
|
82
|
+
"In a hub:",
|
|
83
|
+
Object.keys(COMMANDS).filter((name) => !REGISTRY_COMMANDS.has(name)),
|
|
84
|
+
);
|
|
85
|
+
console.log();
|
|
86
|
+
printSection(
|
|
87
|
+
"Anywhere on this machine (no hub needed) — config and maintenance:",
|
|
88
|
+
Object.keys(COMMANDS).filter((name) => REGISTRY_COMMANDS.has(name)),
|
|
89
|
+
);
|
|
90
|
+
console.log();
|
|
91
|
+
|
|
92
|
+
console.log(`Global: --hub <dir> operate on that hub instead of searching upward from cwd`);
|
|
46
93
|
console.log(`Docs: https://github.com/msareen/knowledge-hub-builder`);
|
|
47
94
|
process.exit(0);
|
|
48
95
|
}
|
|
@@ -63,11 +110,31 @@ if (!entry) {
|
|
|
63
110
|
// stamped at an older version than the installed khb is stating an older contract than
|
|
64
111
|
// the one the CLI now implements. Rather than let the two disagree, refresh the hub in
|
|
65
112
|
// place before running the command — `khb upgrade` touches nothing the user wrote.
|
|
66
|
-
// `init` has no hub yet, `upgrade` does this itself,
|
|
67
|
-
|
|
113
|
+
// `init` has no hub yet, `upgrade` does this itself, the registry commands run outside
|
|
114
|
+
// any hub, and $KHB_NO_AUTO_UPGRADE opts out.
|
|
115
|
+
if (cmd !== "init" && cmd !== "upgrade" && !REGISTRY_COMMANDS.has(cmd)) {
|
|
68
116
|
const hub = findHub();
|
|
69
117
|
if (hub) {
|
|
70
|
-
|
|
118
|
+
// Any command run in a hub is proof the hub exists and is in use — record it, so the
|
|
119
|
+
// machine registry fills itself in without a migration or a `khb register` to recall.
|
|
120
|
+
const { registerHub, touchHub } = await import("./lib/registry");
|
|
121
|
+
registerHub(hub);
|
|
122
|
+
touchHub(hub);
|
|
123
|
+
|
|
124
|
+
// …and proof of where it is. The hub records its own location in its marker, so a move
|
|
125
|
+
// is noticed by the hub itself rather than inferred from a registry that may have been
|
|
126
|
+
// deleted or never have seen this machine. Must run before the drift check below:
|
|
127
|
+
// that restamps the marker, and would overwrite the old location before anyone read it.
|
|
128
|
+
const { recordLocation } = await import("./lib/upgrade");
|
|
129
|
+
const { moved } = recordLocation(hub);
|
|
130
|
+
if (moved) {
|
|
131
|
+
console.error(`khb: this hub was at ${moved} and is now at ${hub}.`);
|
|
132
|
+
console.error(`khb: absolute paths recorded inside it still name the old location.`);
|
|
133
|
+
console.error(`khb: repair them: khb update --path (--dry-run to preview)`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (hub && !process.env.KHB_NO_AUTO_UPGRADE) {
|
|
137
|
+
const { hubVersion, upgradeHub, updateHint } = await import("./lib/upgrade");
|
|
71
138
|
// A marker under a pre-rename name is drift too, even at a matching version.
|
|
72
139
|
if (hubVersion(hub) !== version() || markerIn(hub) !== MARKER) {
|
|
73
140
|
const { from, to, pruned, renamed } = upgradeHub(hub);
|
|
@@ -77,6 +144,8 @@ if (cmd !== "init" && cmd !== "upgrade" && !process.env.KHB_NO_AUTO_UPGRADE) {
|
|
|
77
144
|
);
|
|
78
145
|
if (renamed) console.error(`khb: renamed ${renamed} -> khb.json`);
|
|
79
146
|
if (pruned.length) console.error(`khb: removed (no longer part of the contract): ${pruned.join(", ")}`);
|
|
147
|
+
const hint = updateHint(hub);
|
|
148
|
+
if (hint) console.error(hint);
|
|
80
149
|
}
|
|
81
150
|
}
|
|
82
151
|
}
|