@msareen/knowledge-hub-builder 0.1.7 → 0.2.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.
@@ -4,6 +4,7 @@
4
4
  sources: []
5
5
  # - type: folder
6
6
  # path: /abs/path/to/some-folder
7
+ # exclude: [] # optional — skip these paths/globs before ingesting
7
8
  # - type: web
8
9
  # urls: []
9
10
  # - type: confluence
package/AGENTS.md CHANGED
@@ -73,8 +73,10 @@ One hard boundary governs every workflow and every future change to the tooling:
73
73
 
74
74
  - **`khb` converts bytes to text.** Hashing, caching, file plumbing, ledger-keeping, and
75
75
  *every* local extractor: PDF/DOCX/ODT/XLSX/PPTX libraries, tesseract OCR for scans and
76
- images, whisper for audio and video. All of it deterministic, offline, and free of
77
- charge. It **never contacts a model** not directly, not by shelling out.
76
+ images, whisper.cpp (via `vno`) or whisper for audio and video, and the caption reader
77
+ that spares them the job whenever a `.vtt`/`.srt` sits beside the recording. All of it
78
+ deterministic, offline, and free of charge. It **never contacts a model** — not directly,
79
+ not by shelling out.
78
80
  - **The agent decides what the text means.** Splitting a document into concepts, labeling
79
81
  and linking them, curating `raw/` into the wiki, escalating a bad OCR to a vision read,
80
82
  and judging when a query has produced a new concept worth keeping.
@@ -136,11 +138,27 @@ From outside, pass `--hub <dir>` or set `$KHB_HUB`.
136
138
  |---|---|
137
139
  | `khb lint` | validate structure against `skills/lint/SKILL.md` |
138
140
  | `khb upgrade` | refresh this hub's package-owned contract docs — runs by itself whenever `khb.json`'s stamped version differs from the installed khb, so these docs always match the CLI |
139
- | `khb visualize` | regenerate `visualizer/graph.html` |
141
+ | `khb visualize [--port N] [--no-open]` | serve the live bundle graph in your browser; aliases `vis`, `viz` |
140
142
  | `khb new-bundle <name>` | scaffold + register a bundle |
141
143
  | `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
144
  | `khb export <bundle> [dest]` | standalone copy: bundle + common patterns, shareable alone |
143
145
 
146
+ These five work **outside** any hub, against a per-machine shortcut list at
147
+ `~/.khb/hubs-config.json` (`%USERPROFILE%\.khb` on Windows). They hold no knowledge — only
148
+ paths — and fill themselves in as hubs are used, so they need no maintenance.
149
+
150
+ | Command | Purpose |
151
+ |---|---|
152
+ | `khb list` | every hub on this machine, with its description and path |
153
+ | `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 |
154
+ | `khb agent [name]` | which agent `khb go` launches — `claude`, `codex`, a custom `--command`, or `none` |
155
+ | `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 |
156
+ | `khb forget <name>` | drop a shortcut; the hub folder is untouched |
157
+
158
+ A hub's `name` and `description` in that list come from its own `khb.json`, so they travel
159
+ with it. Set them at creation with `khb init --name --description`, or edit the marker's
160
+ `name`/`description` keys — those two are yours, unlike the rest of the file.
161
+
144
162
  There is no `khb catalog` command — cataloging is entirely a judgement pass.
145
163
 
146
164
  ## Ingest, then catalog — two steps, in that order
@@ -148,8 +166,10 @@ There is no `khb catalog` command — cataloging is entirely a judgement pass.
148
166
  **Ingest** (`skills/ingest/SKILL.md`) is mechanical and flat: `khb ingest <bundle>` pulls
149
167
  every declared source into `raw/` as markdown with a provenance header, extracting
150
168
  everything it can locally — text, PDF, DOCX, ODT, XLSX, PPTX, images by OCR, audio and
151
- video by whisper. Sources behind an authenticated API (Confluence, ADO, git hosts) you pull
152
- yourself via MCP/CLI into the same `raw/` shape. Ingest never interprets content.
169
+ video by whisper or, where a recording has a `.vtt`/`.srt` beside it, from those captions,
170
+ the pair acquired as a single source. Sources behind an authenticated API (Confluence, ADO,
171
+ git hosts) you pull yourself via MCP/CLI into the same `raw/` shape. Ingest never interprets
172
+ content.
153
173
 
154
174
  When the user has not named a bundle, **ask** which existing bundle owns the material or
155
175
  whether to start a new one, and for an existing bundle ask whether to re-ingest what its
package/README.md CHANGED
@@ -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
- khb init ~/my-knowledge
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
@@ -75,6 +88,8 @@ Supported out of the box, with no system tools and no further installation:
75
88
  - PDF and DOCX
76
89
  - ODT, XLSX, and PPTX
77
90
  - OCR for images and scanned PDFs, applied automatically when a PDF has no text layer
91
+ - subtitle files (`.vtt`, `.srt`) — and a video or audio file with one beside it is read
92
+ from the captions rather than transcribed, as one source rather than two
78
93
 
79
94
  OCR ships with KHB: `@hyzyla/pdfium`, `sharp`, and `tesseract.js` are ordinary dependencies,
80
95
  so installing KHB pulls them down whether or not you ever ingest a scan. That costs roughly
@@ -83,8 +98,14 @@ waiting for a setup step.
83
98
 
84
99
  One extractor is genuinely optional:
85
100
 
86
- - audio and video transcription needs a `whisper` or `faster-whisper` executable on `PATH`
87
- (`pip install -U openai-whisper`)
101
+ - audio and video transcription needs a transcriber on `PATH`. KHB prefers
102
+ [`vno`](https://www.npmjs.com/package/@msareen/voice-notes-organizer)
103
+ (`npm install -g @msareen/voice-notes-organizer`), which wraps whisper.cpp and installs
104
+ its own ffmpeg and model, and falls back to `whisper` / `faster-whisper`
105
+ (`pip install -U openai-whisper`). KHB checks `vno status` first: a vno that is installed
106
+ but not yet set up leaves recordings pending with `run: vno setup` and holds up nothing
107
+ else in the run. A recording that already has a `.vtt` or `.srt` beside
108
+ it needs neither — those captions are read instead
88
109
 
89
110
  Without it, KHB leaves a pending row in `log.md` and prints the required setup rather than
90
111
  failing the run.
@@ -225,10 +246,10 @@ Commands can be run directly or requested through the matching agent skill.
225
246
 
226
247
  | Command | Purpose |
227
248
  |---|---|
228
- | `khb init [dir]` | Create a hub |
249
+ | `khb init [dir] [--name N] [--description "…"]` | Create a hub |
229
250
  | `khb upgrade` | Refresh package-owned contracts and skills (also runs automatically on version drift) |
230
251
  | `khb new-bundle <name> ["scope"]` | Create and register a bundle |
231
- | `khb ingest [bundle] [--force]` | Acquire and extract declared sources |
252
+ | `khb ingest [bundle] [--force] [--skip-ocr] [--skip-audio]` | Acquire and extract declared sources |
232
253
  | `khb lint` | Validate routing, bundle structure, and OKF metadata |
233
254
  | `khb visualize [--port N] [--no-open]` (aliases: `vis`, `viz`) | Serve the live bundle graph on a random free port and open it in your default browser — pan/zoomable cross-bundle map, drill into a bundle for its folder-clustered concepts, rebuild-on-refresh, exits when you close the tab |
234
255
  | `khb export <bundle> [dest]` | Export one standalone bundle |
@@ -240,6 +261,84 @@ Additional ingest flags:
240
261
 
241
262
  Run against a hub outside the current directory with `--hub <dir>` or `$KHB_HUB`.
242
263
 
264
+ ### Moving between hubs
265
+
266
+ These four run **outside** any hub, from any terminal:
267
+
268
+ | Command | Purpose |
269
+ |---|---|
270
+ | `khb` | Open a hub. One registered hub asks; several show a list and take a pick; none walks you through creating the first |
271
+ | `khb list [--json]` | Every hub on this machine, with its description and path |
272
+ | `khb go <name\|N> [--path]` | Open one by name or list position. `--path` prints just the path, for `cd "$(khb go --path work)"` |
273
+ | `khb agent [name] [--command X] [--args "…"]` | Which agent `khb go` launches — `claude`, `codex`, anything on your PATH, or `none` to just print the path |
274
+ | `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 |
275
+ | `khb forget <name>` | Drop a hub from the list. The folder is untouched |
276
+
277
+ `khb go` prints the hub's path and then starts your agent there, so a bare `khb` from a
278
+ cold terminal ends with an agent open on the right folder. No program can change its
279
+ parent shell's directory, which is why the `cd` line is printed rather than performed.
280
+
281
+ The list lives in `~/.khb/hubs-config.json` (`%USERPROFILE%\.khb` on Windows). It holds
282
+ paths and one launch command — no knowledge — and fills itself in: any khb command run
283
+ inside a hub registers it, so hubs you already had show up without a migration step.
284
+ Delete the file and the next command in each hub puts it back.
285
+
286
+ A hub's name and description come from its own `khb.json`, so they travel with the hub
287
+ rather than living in one machine's list. Set them with `khb init --name --description`,
288
+ or edit those two keys in the marker later.
289
+
290
+ ### `khb update` — repairing a hub
291
+
292
+ Two independent repairs, run together or apart:
293
+
294
+ ```bash
295
+ khb update --dry-run # what would change, both halves
296
+ khb update # apply both
297
+ khb update --path # only repoint paths after a move
298
+ khb update --schema # only backfill sources.yaml to the current schema
299
+ ```
300
+
301
+ **`--path`/`-p`** — after you move the folder, this repoints the shortcut list (a moved hub
302
+ shows as `MISSING` in `khb list` until you do) and rewrites the absolute paths recorded
303
+ *inside* the hub that named the old location: `sources.yaml` entries, `source:` headers in
304
+ `raw/`, `log.md` rows, `resource:` front matter. It needs no old path: the hub records where
305
+ it lives in its own `khb.json` (`path`), so any khb command run in a moved hub notices and
306
+ says so —
307
+
308
+ ```
309
+ khb: this hub was at D:\kb\old and is now at D:\kb\new.
310
+ khb: repair them: khb update --path (--dry-run to preview)
311
+ ```
312
+
313
+ — and `--path` reads the old location straight out of the marker. That record travels with
314
+ the folder, so it works after `~/.khb` is deleted, on a second machine, or on a hub a
315
+ colleague handed you. Move a hub twice before repairing it and both former homes are
316
+ rewritten in one pass. For a hub last touched by a khb too old to have recorded a location,
317
+ the old path is worked out from the registry entry that now points at nothing, matched on
318
+ the identity stamp in `khb.json`. If it can't prove which entry that is, it asks; `--from
319
+ <old-path>` tells it outright.
320
+
321
+ The rewrite is a literal substitution with three guards: every spelling of the path moves
322
+ (native, forward-slashed, and JSON-escaped as `raw/` headers store it), each rewritten to
323
+ the same spelling of the new path; matches must end at a path boundary, so moving `…/old`
324
+ never touches a sibling `…/older`; and the new path is matched too and rewritten to itself,
325
+ which is what makes overlapping moves safe — lifting a hub out of its parent, or pushing it
326
+ down into a subdirectory of where it stood — and makes a second run a no-op.
327
+ `.git/`, `node_modules/` and the `inbox/` cache are skipped, as are binaries.
328
+
329
+ **`--schema`/`-s`** — a bundle's `sources.yaml` can predate a field khb's since learned about
330
+ (e.g. `exclude:`), with no way to discover it short of reading the docs. This backfills
331
+ missing optional fields with their default, per bundle, preserving your comments and
332
+ formatting. `khb upgrade` mentions when either half of `update` has something pending, as a
333
+ printed hint — it never runs `update` for you.
334
+
335
+ Both `khb update` and `khb ingest` state their whole plan before writing anything, then
336
+ report as they go — a position per file for work slow enough to matter, a live counter for
337
+ work that isn't. The counter goes to stderr, so redirected stdout stays clean.
338
+
339
+ Not to be confused with `khb upgrade`, which refreshes a hub's package-owned contract docs
340
+ and has nothing to do with moving anything.
341
+
243
342
  To update the installed package:
244
343
 
245
344
  ```bash
package/SPEC.md CHANGED
@@ -45,7 +45,8 @@ KHB's own contribution is the bundle-of-bundles layer over both — see §1.
45
45
  every concept traces back through a provenance header to the original file.
46
46
  6. **Extract to markdown, locally** — binary/opaque formats (PDF, DOCX, XLSX, images,
47
47
  audio) are converted to markdown so every bundle's knowledge is plain, greppable text.
48
- Every extractor is local and deterministic: pure-JS libraries, tesseract WASM, whisper.
48
+ Every extractor is local and deterministic: pure-JS libraries, tesseract WASM,
49
+ whisper.cpp.
49
50
  Lossy routes (OCR, ASR) are marked `quality: low` rather than hidden.
50
51
  7. **Bun** is the scripting language for all tooling and third-party interfaces.
51
52
 
@@ -115,6 +116,8 @@ acting on them** — an agent can never read a protocol the CLI no longer implem
115
116
  ├── scripts/
116
117
  │ ├── cli.ts # subcommand dispatch; --hub flag; the version drift check
117
118
  │ ├── init.ts # khb init / khb upgrade
119
+ │ ├── hubs.ts # khb list / go / agent / update / forget — the only
120
+ │ │ # commands that run outside a hub
118
121
  │ ├── new-bundle.ts # scaffold from .bundle_template, register in outer.index.md
119
122
  │ ├── export.ts # bundle + common patterns → standalone shareable folder
120
123
  │ ├── lint.ts # enforce skills/lint/SKILL.md across the hub
@@ -124,6 +127,9 @@ acting on them** — an agent can never read a protocol the CLI no longer implem
124
127
  │ ├── extract.ts # every local extractor + the content-hash cache
125
128
  │ ├── ledger.ts # log.md read/write
126
129
  │ ├── paths.ts # package-side paths — importing it never needs a hub
130
+ │ ├── registry.ts # ~/.khb/hubs-config.json: where this machine's hubs are
131
+ │ ├── relocate.ts # khb update --path's path rewriter — pure text, no judgement
132
+ │ ├── schema.ts # khb update --schema: sources.yaml field diff/apply
127
133
  │ ├── upgrade.ts # the refresh itself: `khb upgrade` and the drift check
128
134
  │ └── util.ts # hub resolution + shared helpers
129
135
  ├── .bundle_template/ # copied by `khb new-bundle`
@@ -143,6 +149,210 @@ Rule 3 is the normal path: `cd` anywhere inside the hub and run `khb lint`. One
143
149
  consequence worth stating — a hub is identified by its marker file, not its name, so
144
150
  hubs may be renamed or moved freely, and nested hubs resolve to the innermost one.
145
151
 
152
+ ### 2d. The machine registry — `~/.khb/hubs-config.json`
153
+
154
+ Rules 1–3 all assume you already know where the hub is. From a cold terminal in an
155
+ unrelated folder, you don't — and a person with a personal hub, a work hub and a client
156
+ hub has three paths to remember. So khb keeps **one file per machine** recording where the
157
+ hubs on it are:
158
+
159
+ ```
160
+ ~/.khb/ # %USERPROFILE%\.khb on Windows; $KHB_HOME overrides
161
+ └── hubs-config.json
162
+ ```
163
+
164
+ ```json
165
+ {
166
+ "version": 1,
167
+ "defaultAgent": "claude",
168
+ "agents": { "claude": { "command": "claude", "args": [] },
169
+ "codex": { "command": "codex", "args": [] } },
170
+ "hubs": [
171
+ { "name": "my-knowledge", "description": "Personal — work + home",
172
+ "path": "D:\\code\\my-knowledge", "added": "…", "lastUsed": "…" }
173
+ ]
174
+ }
175
+ ```
176
+
177
+ Three properties define it:
178
+
179
+ - **It is a shortcut list, never knowledge.** It holds paths and one launch command.
180
+ Delete it and nothing is lost — the next command run inside each hub puts it back.
181
+ - **It fills itself in.** Every khb command that resolves a hub registers it, so hubs made
182
+ before the registry existed appear the first time anything is run in them. There is no
183
+ migration and no `register` command to remember. `khb forget <name>` drops a shortcut and
184
+ never touches the folder.
185
+ - **The hub is the authority on its own identity.** `name` and `description` are read out
186
+ of the hub's `khb.json` (`khb init --name --description`, or edit the file), so a hub
187
+ moved to another machine or cloned by a colleague describes itself the same way there.
188
+ Only when the marker says nothing does khb fall back to the folder name and a summary of
189
+ the bundles inside. `khb upgrade` merges rather than replaces the marker, so keys khb
190
+ does not own survive an upgrade.
191
+
192
+ The commands over it are `khb list`, `khb go`, `khb agent`, `khb update` and `khb forget` —
193
+ the only ones that run **outside** a hub, and therefore the only ones that skip hub
194
+ resolution and the version drift check. A bare `khb` is `khb go`: one hub asks to open it,
195
+ several show the list and take a pick, none prints the help.
196
+
197
+ `khb go` ends by launching your configured agent with the hub as its working directory.
198
+ No process can change its parent shell's directory, so `khb go` prints the `cd` line for
199
+ the human and passes the path to the agent as cwd — `khb go --path <name>` prints only the
200
+ path, for `cd "$(khb go --path work)"`. `khb agent none` turns the launch off entirely.
201
+
202
+ Every path stored in or compared against the registry is canonicalized first (`realpath`,
203
+ case-folded on Windows). One directory has several true names — `C:\Users\MANASV~1\…` and
204
+ `C:\Users\Manasvi Sareen\…` are the same folder, as is anything reached through a symlink —
205
+ and without this a single hub lists twice and `khb update --path` cannot tell it has already
206
+ been repaired.
207
+
208
+ ### 2e. `khb update` — repairing a hub
209
+
210
+ Two independent repairs, selectable together or apart:
211
+
212
+ ```
213
+ khb update [new-path] [--path|-p] [--schema|-s] [--from <old-path>] [--dry-run]
214
+ ```
215
+
216
+ No flag runs both. `--path`/`-p` repairs a moved hub; `--schema`/`-s` backfills a bundle's
217
+ `sources.yaml` to the current field set (§ scripts/lib/schema.ts). They are unrelated repairs
218
+ that happen to share a plan-then-write-then-report shape, so one command with two switches
219
+ beat two commands that would each need the same hub-resolution and dry-run plumbing.
220
+
221
+ #### `--path` — a hub that moved
222
+
223
+ Moving a hub folder breaks two things, and this half fixes both:
224
+
225
+ 1. **The shortcut list** still points at the old folder. `khb list` marks it `MISSING`.
226
+ 2. **Absolute paths recorded inside the hub** that named the old location — `sources.yaml`
227
+ entries, `source:` headers in `raw/`, `log.md` rows, `resource:` front matter — are now
228
+ dangling.
229
+
230
+ Run from inside the moved hub, or name it. Nothing else is needed, because **the hub records
231
+ its own location**. `khb.json` carries a `path` key — canonical, plus `pathAs` for the
232
+ spelling khb was invoked through when the two differ — and any khb command run in a hub
233
+ compares it against where the hub actually is. A mismatch is the move, stated by the only
234
+ witness that travelled with the folder:
235
+
236
+ ```
237
+ khb: this hub was at D:\kb\old and is now at D:\kb\new.
238
+ khb: absolute paths recorded inside it still name the old location.
239
+ khb: repair them: khb update --path (--dry-run to preview)
240
+ ```
241
+
242
+ The old location moves into `movedFrom`, where it waits until `--path` works it off.
243
+ A hub moved twice before anyone repaired it lists both former homes and all of them are
244
+ rewritten in the one pass. The key costs a string and is inert — nothing reads it but this
245
+ repair — but it is what makes the repair need no arguments, and it survives what the registry
246
+ does not: a deleted `~/.khb`, a first run on a second machine, a folder handed to a colleague.
247
+
248
+ Failing that — a hub last touched by a khb too old to have recorded a location — the old path
249
+ is *inferred, on proof only*: each registry entry records the `created` stamp from the hub's
250
+ marker, minted once at `khb init`, so the dead entry carrying this hub's stamp is provably the
251
+ same hub. A mere name match is circumstantial and is put to the user as a question rather than
252
+ acted on. `--from` settles it either way, and is still the answer when the move predates all
253
+ of this.
254
+
255
+ The rewrite itself is a **conversion, not an interpretation**, which is what keeps it in the
256
+ CLI rather than in an agent pass (§ AGENTS.md, division of labor): the same substring in
257
+ and out, no judgement about what a path means. Three properties make it safe to run
258
+ unattended:
259
+
260
+ - **Every spelling moves.** A path appears natively (`D:\a\b`), forward-slashed (`D:/a/b`),
261
+ and backslash-escaped inside JSON (`D:\\a\\b`, how `raw/` headers and `log.md` store a
262
+ source). All are matched, and each is rewritten to *the same spelling* of the new path, so
263
+ a JSON-escaped source stays JSON-escaped.
264
+ - **Matches end at a path boundary.** Moving `…/old` never touches `…/older`, a sibling
265
+ whose name merely starts the same way.
266
+ - **The new path is shielded from itself.** It is matched too, and rewritten to itself. That
267
+ is what makes an *overlapping* move safe — a hub lifted out of its parent (`…/kb/hub` →
268
+ `…/kb`) or pushed down into a subdirectory of where it stood (`…/kb` → `…/kb/hub`). In the
269
+ second case the old path occurs inside every already-correct reference, and without the
270
+ shield each would have the move applied to it a second time. Claiming those matches for an
271
+ identity rewrite also makes the whole command idempotent: run it twice and the second run
272
+ changes nothing. Only old and new naming *the same directory* is refused, there being no
273
+ move to repair.
274
+
275
+ `.git/`, `node_modules/` and the `inbox/` extraction cache are not walked; binary files and
276
+ anything over 8 MB are skipped. `--dry-run` reports the file-by-file hit count and writes
277
+ nothing.
278
+
279
+ Like `khb ingest`, it states the whole plan before writing anything and reports progress as
280
+ it walks (§2g).
281
+
282
+ #### `--schema` — a bundle's `sources.yaml` predates a field
283
+
284
+ A `sources.yaml` written before a field existed (e.g. `folder`/`files` sources gained
285
+ `exclude:`) has no way to discover it short of reading the docs. `scripts/lib/schema.ts`
286
+ holds the current, optional, backfillable field list per source `type`; this half diffs
287
+ every bundle's `sources.yaml` against it and, for anything missing, stages a default value —
288
+ or, for a field the schema has since deprecated, stages its removal. Comment-preserving:
289
+ it edits the parsed YAML document node-by-node (the `yaml` package's `Document` API) rather
290
+ than reserializing from scratch, so hand-written comments and structure in an untouched part
291
+ of the file survive. The one caveat is cosmetic, not correctness: re-serializing the whole
292
+ document can normalize whitespace in *other*, unrelated flow-style content in the same file.
293
+
294
+ There is no persisted schema version anywhere — the "schema" is just this file's current
295
+ shape, diffed fresh on every run, the same way `khb upgrade`'s `MANAGED`/`RETIRED` lists are
296
+ static and re-checked rather than tracked historically. `--dry-run` prints the changes and
297
+ writes nothing; otherwise it applies immediately, same convention as `--path`.
298
+
299
+ `khb upgrade` prints a one-line hint when either half of `update` has something pending —
300
+ never a prompt, since `upgrade` already runs unattended inside unrelated commands on version
301
+ drift, and a blocking question there would interrupt work that has nothing to do with either
302
+ repair.
303
+
304
+ The name says what it does, now that it does two things belonging to neither `khb upgrade`
305
+ (package-owned contract docs only) nor to a single-purpose `update-path`. The original
306
+ one-letter-from-`upgrade` objection to the shorter name held while `update` meant only path
307
+ repair; a second, unrelated repair under the same verb resolves it — `update` repairs what
308
+ the user's own bundles record, `upgrade` refreshes what the package owns, and the two no
309
+ longer read as near-synonyms.
310
+
311
+ ### 2f. First run — the wizard
312
+
313
+ A bare `khb` on a machine with no hubs has a terminal in front of it and knows the one
314
+ thing the user needs to hear, so it asks rather than referring them to the docs. Five
315
+ questions, every one with a default that Enter accepts: where the hub goes, what to call
316
+ it, a one-line description, which agent opens it, and a first bundle.
317
+
318
+ Three things keep it honest:
319
+
320
+ - **It asks only what `khb init` takes as flags**, and calls the same `createHub()`. A hub
321
+ born in the wizard is byte-identical to one made by hand — there is no second creation
322
+ path to drift.
323
+ - **Agents are detected, not guessed.** `claude` and `codex` are probed with `--version`
324
+ and the ones present are marked; the first found is the default. Any other command can be
325
+ typed instead, or `none`.
326
+ - **A path that is already a hub is adopted, not overwritten.** The machine simply had not
327
+ heard of it, so it joins the list under its own name and the wizard stops there.
328
+
329
+ No terminal, or `--path` (a script asking for a path), falls back to three lines of
330
+ guidance. `khb list` on an empty machine likewise reports rather than starts a
331
+ conversation.
332
+
333
+ ### 2g. Progress on the mechanical passes
334
+
335
+ `khb`'s half of the split is the deterministic half, and some of it is slow: a scanned PDF
336
+ is seconds per page, a video minutes per file, and a hub that has accumulated thousands of
337
+ raw documents takes a visible moment to walk. A silent process is indistinguishable from a
338
+ hung one, so every mechanical pass reports (`scripts/lib/log.ts`):
339
+
340
+ - **The plan, before any work.** Which hub, which bundle, which extractors are armed, how
341
+ many sources — or for `update --path`, the old path, the new path, and whether anything
342
+ will be written; for `update --schema`, which bundles and fields. A `--hub`/`$KHB_HUB`
343
+ run can target a folder you did not expect, and that must be visible before the first
344
+ write, not after.
345
+ - **Position within the run.** `[3/57]` per unit where a unit is slow enough to deserve its
346
+ own line (ingest's per-file extraction), and a single rewritten counter line where the
347
+ units are fast and numerous (`update --path` checking files).
348
+ - **An outcome per unit, and a closing summary** with wall-clock time.
349
+
350
+ The transient counter writes to **stderr** and repaints at ~12fps; with no terminal it
351
+ degrades to a milestone line every 500 units. So `khb … > out.txt` keeps clean, complete
352
+ stdout, and a CI log still shows the walk moving. There is no `--quiet`: the per-unit line
353
+ is the audit trail for a pass that rewrites files, and a run you have to repeat to find out
354
+ what it did is worse than a noisy one.
355
+
146
356
  ## 3. Routing model
147
357
 
148
358
  - `outer.index.md` lists every bundle with a one-line scope and "route here when" hints.
@@ -195,6 +405,7 @@ Each bundle declares its sources in `sources.yaml`:
195
405
  sources:
196
406
  - type: folder # local disk
197
407
  path: /abs/path/to/project-x
408
+ exclude: [drafts/] # optional — skip paths/globs before ingesting
198
409
  - type: web
199
410
  urls:
200
411
  - https://example.com/design-doc
@@ -284,11 +495,23 @@ puts them on the CLI side of the §Division-of-labor line.
284
495
  | XLSX | `fflate` → one markdown table per sheet | bundled | high |
285
496
  | scanned PDF | `pdfium` + `tesseract.js` (WASM) | bundled, ~75 MB | low |
286
497
  | Images (png/jpg/webp/tif) | `tesseract.js` | bundled, ~75 MB | low |
287
- | Audio, video | `whisper` / `faster-whisper` | opt-in, pip | low |
498
+ | Audio, video | `vno` (whisper.cpp), else `whisper` / `faster-whisper` | opt-in, npm or pip | low |
499
+ | Captions (vtt/srt) | built-in reader | bundled | high |
500
+
501
+ A recording that has a caption sidecar beside it — `talk.vtt`, `talk.en.vtt`, `talk.srt` —
502
+ is read from the sidecar rather than transcribed, and the two are acquired as one source:
503
+ one ledger row under the recording, no row and no `raw/` file for the sidecar. The pair's
504
+ content hash covers both files, so correcting a caption re-ingests the recording. Where two
505
+ sidecars disagree about language khb transcribes instead: choosing an audience is not a
506
+ conversion decision, and §Division-of-labor puts choices on the agent's side of the line.
288
507
 
289
508
  The OCR stack is bundled rather than opt-in: an ingest that stops to ask for an install is
290
- worse than an install that carries WASM nobody uses. Transcription stays opt-in because it is
291
- a Python executable, not something a JS package manager can pull down.
509
+ worse than an install that carries WASM nobody uses. Transcription stays opt-in because it
510
+ is an external executable, not something khb's own dependency tree can carry: `vno`
511
+ (@msareen/voice-notes-organizer) where `vno status` reports it ready, since whisper.cpp is
512
+ faster than the Python whisper and hands back WebVTT the caption reader can anchor, else
513
+ `whisper` / `faster-whisper`. A vno that is installed but not set up degrades to the
514
+ fallback, or to pending rows, and never to a failed run.
292
515
 
293
516
  A missing dep degrades to a ledger row with an empty `raw` and a printed install
294
517
  hint — never to a failed run. `quality: low` output is a standing invitation for the catalog
package/document/faq.md CHANGED
@@ -90,6 +90,7 @@ For example:
90
90
  sources:
91
91
  - type: folder
92
92
  path: /absolute/path/to/documents
93
+ exclude: [drafts/] # optional — skip paths/globs before ingesting
93
94
  - type: files
94
95
  paths:
95
96
  - /absolute/path/to/one.pdf
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@msareen/knowledge-hub-builder",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
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",