@msareen/knowledge-hub-builder 0.1.3
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/.agents/skills/catalog/SKILL.md +7 -0
- package/.agents/skills/export/SKILL.md +7 -0
- package/.agents/skills/ingest/SKILL.md +7 -0
- package/.agents/skills/lint/SKILL.md +7 -0
- package/.agents/skills/new-bundle/SKILL.md +7 -0
- package/.agents/skills/query/SKILL.md +7 -0
- package/.agents/skills/visualize/SKILL.md +7 -0
- package/.bundle_template/index.md +9 -0
- package/.bundle_template/log.md +10 -0
- package/.bundle_template/raw/.gitkeep +15 -0
- package/.bundle_template/refs.md +6 -0
- package/.bundle_template/sources.yaml +13 -0
- package/.claude/skills/catalog/SKILL.md +7 -0
- package/.claude/skills/export/SKILL.md +7 -0
- package/.claude/skills/ingest/SKILL.md +7 -0
- package/.claude/skills/lint/SKILL.md +7 -0
- package/.claude/skills/new-bundle/SKILL.md +7 -0
- package/.claude/skills/query/SKILL.md +7 -0
- package/.claude/skills/visualize/SKILL.md +7 -0
- package/AGENTS.md +167 -0
- package/CLAUDE.md +13 -0
- package/README.md +289 -0
- package/SPEC.md +354 -0
- package/document/faq.md +156 -0
- package/package.json +52 -0
- package/scripts/cli.ts +66 -0
- package/scripts/export.ts +42 -0
- package/scripts/ingest/acquire.ts +189 -0
- package/scripts/ingest/exts.ts +29 -0
- package/scripts/ingest/files.ts +29 -0
- package/scripts/ingest/folder.ts +44 -0
- package/scripts/ingest/index.ts +125 -0
- package/scripts/ingest/protect.ts +42 -0
- package/scripts/ingest/web.ts +54 -0
- package/scripts/init.ts +93 -0
- package/scripts/lib/args.ts +8 -0
- package/scripts/lib/extract.ts +384 -0
- package/scripts/lib/graph-page.ts +477 -0
- package/scripts/lib/graph.ts +117 -0
- package/scripts/lib/ledger.ts +136 -0
- package/scripts/lib/log.ts +53 -0
- package/scripts/lib/paths.ts +55 -0
- package/scripts/lib/scaffold.ts +56 -0
- package/scripts/lib/util.ts +154 -0
- package/scripts/lint.ts +165 -0
- package/scripts/new-bundle.ts +17 -0
- package/scripts/visualize.ts +104 -0
- package/skills/catalog/SKILL.md +164 -0
- package/skills/export/SKILL.md +31 -0
- package/skills/ingest/SKILL.md +229 -0
- package/skills/lint/SKILL.md +69 -0
- package/skills/new-bundle/SKILL.md +25 -0
- package/skills/query/SKILL.md +114 -0
- package/skills/visualize/SKILL.md +33 -0
- package/templates/hub/gitattributes +12 -0
- package/templates/hub/gitignore +12 -0
- package/templates/hub/outer.index.md +13 -0
package/SPEC.md
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# KHB — Specification
|
|
2
|
+
|
|
3
|
+
**KHB — Knowledge Hub Builder.** Distributed as
|
|
4
|
+
`@msareen/knowledge-hub-builder`. Install the tooling once; run `khb init` wherever the knowledge should
|
|
5
|
+
live (OneDrive, a shared drive, a private repo) to create a **hub**. KHB supplies the
|
|
6
|
+
rules and tooling and holds no knowledge; the hub holds all of it and is yours.
|
|
7
|
+
|
|
8
|
+
A personal knowledge system built as a **bundle of bundles**: independent knowledge bundles
|
|
9
|
+
joined by a thin router, navigable by any coding/knowledge agent (Claude, Codex, or other).
|
|
10
|
+
The common contract and canonical workflows are agent-agnostic. Runtime-specific shims
|
|
11
|
+
only handle discovery: Codex reads `AGENTS.md` and `.agents/skills/`; Claude imports the
|
|
12
|
+
contract through `CLAUDE.md` and discovers `.claude/skills/`.
|
|
13
|
+
|
|
14
|
+
Lineage: the operating model (immutable raw sources → LLM-curated wiki → schema, driven
|
|
15
|
+
by ingest/query/lint) is [Karpathy's LLM Wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f);
|
|
16
|
+
the file format is [Google's OKF](https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf) v0.1.
|
|
17
|
+
KHB's own contribution is the bundle-of-bundles layer over both — see §1.
|
|
18
|
+
|
|
19
|
+
## 1. Core ideas
|
|
20
|
+
|
|
21
|
+
1. **Bundle** — a self-contained *logical* unit owning its content and ingestion sources.
|
|
22
|
+
What makes a bundle is a human decision about ownership, not a classification of subject
|
|
23
|
+
matter: a person, a team, a client, a project. One bundle holds as many topics as its
|
|
24
|
+
owner has — a team bundle carries its roadmap, its incidents and its vendor notes
|
|
25
|
+
together, grouped by subdirectory, because they share a custodian and a context.
|
|
26
|
+
Bundles are never split automatically; splitting is an explicit instruction, never a
|
|
27
|
+
default behavior of ingest, catalog or query.
|
|
28
|
+
Bundles are *lean*: the common rules (AGENTS.md and the `skills/` protocols) live once
|
|
29
|
+
at the hub root, not duplicated per bundle. When a bundle must travel alone,
|
|
30
|
+
`khb export <bundle>` produces a standalone folder with the common patterns
|
|
31
|
+
injected — independence on demand rather than boilerplate everywhere.
|
|
32
|
+
2. **Bundle of bundles** — the hub root is itself a bundle whose content is *routing*, not
|
|
33
|
+
knowledge. `outer.index.md` is the router: it points at bundles and larger topics, and
|
|
34
|
+
details route downward into a bundle's own `index.md`.
|
|
35
|
+
3. **No collation by copying** — when two topics must be combined, content is never merged
|
|
36
|
+
across bundles. Instead: the requesting bundle records a **reference** (`refs.md`), and the
|
|
37
|
+
query resolves by *querying into* the other bundle. This keeps bundles independent and
|
|
38
|
+
collision-free. Two agents can work the two bundles in parallel and join results at the end.
|
|
39
|
+
4. **Ingest, don't wiki-sprawl** — external material (folders on disk, Confluence, Azure
|
|
40
|
+
DevOps, internet links) is pulled *into* a bundle by ingesters rather than being linked as
|
|
41
|
+
living parts of the wiki. The wiki stays canonical; sources are provenance.
|
|
42
|
+
5. **Acquisition and interpretation are separate steps.** `khb ingest` converts bytes to
|
|
43
|
+
markdown in `raw/` and stops. `catalog` — an agent pass, no command — turns that text
|
|
44
|
+
into concepts. Splitting them means a bad extraction is a re-run, not a re-think, and
|
|
45
|
+
every concept traces back through a provenance header to the original file.
|
|
46
|
+
6. **Extract to markdown, locally** — binary/opaque formats (PDF, DOCX, XLSX, images,
|
|
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.
|
|
49
|
+
Lossy routes (OCR, ASR) are marked `quality: low` rather than hidden.
|
|
50
|
+
7. **Bun** is the scripting language for all tooling and third-party interfaces.
|
|
51
|
+
|
|
52
|
+
## 2. Two directories, one of them yours
|
|
53
|
+
|
|
54
|
+
KHB is two things that must not be confused: an installed **package** (tooling, no
|
|
55
|
+
knowledge) and a **hub** (knowledge, no tooling). Nothing you write ever lands in the
|
|
56
|
+
package, and the package is never a place to keep bundles.
|
|
57
|
+
|
|
58
|
+
### 2a. The hub — created by `khb init`, lives wherever you want
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
my-knowledge/ # ~/OneDrive/my-knowledge, a private repo, a share…
|
|
62
|
+
├── khb.json # THE MARKER — how `khb` finds this hub; records its version
|
|
63
|
+
├── outer.index.md # THE router — bundle-of-bundles index
|
|
64
|
+
├── bundles/
|
|
65
|
+
│ └── <bundle>/ # OKF bundle, lean: content + routing, no agent boilerplate
|
|
66
|
+
│ ├── index.md # OKF index: progressive disclosure of this bundle
|
|
67
|
+
│ ├── refs.md # cross-bundle references (the ONLY way out of a bundle)
|
|
68
|
+
│ ├── sources.yaml # ingestion sources for this bundle
|
|
69
|
+
│ ├── log.md # ingest ledger: source → sha256 → raw → curated
|
|
70
|
+
│ ├── <group>/ # free-form subdirectories of concept .md files
|
|
71
|
+
│ │ ├── index.md # (each may carry its own index)
|
|
72
|
+
│ │ └── <concept>.md
|
|
73
|
+
│ └── raw/ # ingested/extracted material, pre-curation (gitignored)
|
|
74
|
+
├── inbox/extracted/ # hub-wide extraction cache, keyed by content hash (gitignored)
|
|
75
|
+
│
|
|
76
|
+
│ ── below: package-owned copies, refreshed by `khb upgrade`, never hand-edited ──
|
|
77
|
+
├── AGENTS.md # common contract; Codex discovers this directly
|
|
78
|
+
├── CLAUDE.md # Claude shim — imports AGENTS.md
|
|
79
|
+
├── .agents/skills/<name>/ # Codex discovery adapter → canonical skill
|
|
80
|
+
├── .claude/skills/<name>/ # Claude discovery adapter → canonical skill
|
|
81
|
+
├── SPEC.md # this file
|
|
82
|
+
└── skills/<name>/SKILL.md # one self-contained workflow protocol per folder:
|
|
83
|
+
# query routing, reading, and query-time learning
|
|
84
|
+
# ingest acquire + extract → raw/ (mechanical)
|
|
85
|
+
# catalog raw/ → concept docs (subagent fan-out)
|
|
86
|
+
# lint structural rules L1–L9
|
|
87
|
+
# new-bundle / export / visualize
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The contract docs are *copied into* the hub rather than read from the package because an
|
|
91
|
+
agent is opened on the hub folder and must find its rules there, without knowing where
|
|
92
|
+
`khb` is installed. They are package-owned: `khb upgrade` overwrites them in place and
|
|
93
|
+
leaves `bundles/` and `outer.index.md` alone.
|
|
94
|
+
|
|
95
|
+
### 2b. The package — `@msareen/knowledge-hub-builder`, installed once
|
|
96
|
+
|
|
97
|
+
> **None of the following is in your hub.** This section describes the *installed tool*,
|
|
98
|
+
> which lives in your global package directory — not in the folder you are working in. A
|
|
99
|
+
> hub contains exactly what §2a lists: `khb.json`, `outer.index.md`, `bundles/`, and the
|
|
100
|
+
> package-owned contract docs. There is no `scripts/` and no `package.json` in a hub, so
|
|
101
|
+
> paths below are package-internal and will not resolve from the hub root. To run the tool,
|
|
102
|
+
> call `khb <command>` — never reach for a file in this tree.
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
@msareen/knowledge-hub-builder/ # installed once, globally — NOT part of a hub
|
|
106
|
+
├── package.json # bin: khb → scripts/cli.ts
|
|
107
|
+
├── scripts/
|
|
108
|
+
│ ├── cli.ts # subcommand dispatch; owns the global --hub flag
|
|
109
|
+
│ ├── init.ts # khb init / khb upgrade
|
|
110
|
+
│ ├── new-bundle.ts # scaffold from .bundle_template, register in outer.index.md
|
|
111
|
+
│ ├── export.ts # bundle + common patterns → standalone shareable folder
|
|
112
|
+
│ ├── lint.ts # enforce skills/lint/SKILL.md across the hub
|
|
113
|
+
│ ├── visualize.ts # serve the live bundle graph from indexes + refs
|
|
114
|
+
│ ├── ingest/ # folder.ts / files.ts / web.ts → acquire.ts → bundle/raw
|
|
115
|
+
│ └── lib/
|
|
116
|
+
│ ├── extract.ts # every local extractor + the content-hash cache
|
|
117
|
+
│ ├── ledger.ts # log.md read/write
|
|
118
|
+
│ ├── paths.ts # package-side paths — importing it never needs a hub
|
|
119
|
+
│ └── util.ts # hub resolution + shared helpers
|
|
120
|
+
├── .bundle_template/ # copied by `khb new-bundle`
|
|
121
|
+
├── templates/hub/ # copied by `khb init`
|
|
122
|
+
└── AGENTS.md, skills/, … # the masters that `khb init`/`upgrade` copy into hubs
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 2c. Hub resolution
|
|
126
|
+
|
|
127
|
+
`khb` locates the hub in this order, and refuses to guess if none is found:
|
|
128
|
+
|
|
129
|
+
1. `--hub <dir>` on the command line;
|
|
130
|
+
2. `$KHB_HUB`;
|
|
131
|
+
3. the nearest ancestor of the working directory containing `khb.json`.
|
|
132
|
+
|
|
133
|
+
Rule 3 is the normal path: `cd` anywhere inside the hub and run `khb lint`. One
|
|
134
|
+
consequence worth stating — a hub is identified by its marker file, not its name, so
|
|
135
|
+
hubs may be renamed or moved freely, and nested hubs resolve to the innermost one.
|
|
136
|
+
|
|
137
|
+
## 3. Routing model
|
|
138
|
+
|
|
139
|
+
- `outer.index.md` lists every bundle with a one-line scope and "route here when" hints.
|
|
140
|
+
It contains **no knowledge**, only routing. An agent always starts at `AGENTS.md`, which
|
|
141
|
+
sends it to `outer.index.md`, which sends it into exactly one bundle.
|
|
142
|
+
- Inside a bundle, `index.md` routes to concept docs and subdirectory indexes
|
|
143
|
+
(progressive disclosure, OKF §6). Same rule: index = routing only.
|
|
144
|
+
- Keep hops shallow: outer index → bundle index → (subdirectory index →) concept.
|
|
145
|
+
If a bundle needs deep index chains, consider splitting it into two bundles.
|
|
146
|
+
|
|
147
|
+
## 3b. Concept model (OKF v0.1)
|
|
148
|
+
|
|
149
|
+
Bundle content follows the [Open Knowledge Format (OKF)](https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf) v0.1:
|
|
150
|
+
|
|
151
|
+
- A **concept** is any non-reserved `.md` file in the bundle — not a special folder.
|
|
152
|
+
Concept ID = its path without `.md`. It may describe a tangible asset (table, API)
|
|
153
|
+
or an abstract idea (metric, decision, playbook).
|
|
154
|
+
- **Front matter**: `type` required (free-form, e.g. `Metric`, `Playbook`); `title`,
|
|
155
|
+
`description`, `resource`, `tags`, `timestamp` recommended; extra keys allowed.
|
|
156
|
+
- **Grouping** is free-form subdirectories chosen per domain (`tables/`,
|
|
157
|
+
`references/metrics/`, `notes/`, …) — structure carries no fixed semantics.
|
|
158
|
+
- **Indexes**: `index.md` may appear in any directory for progressive disclosure
|
|
159
|
+
(`* [Title](path.md) - description`). `log.md` optionally records history.
|
|
160
|
+
- **Links** between concepts are plain markdown links (bundle-root form
|
|
161
|
+
`/tables/x.md` preferred); relationship semantics live in prose, not link types.
|
|
162
|
+
Broken links are tolerated (not-yet-written knowledge).
|
|
163
|
+
- Conventional body headings: `# Schema`, `# Examples`, `# Citations`.
|
|
164
|
+
|
|
165
|
+
KHB additions on top of OKF: `refs.md` (reserved; the only cross-bundle
|
|
166
|
+
pointer), `sources.yaml` (ingestion provenance), `raw/` (uncurated ingested material,
|
|
167
|
+
exempt from OKF conformance), and the outer bundle-of-bundles router.
|
|
168
|
+
|
|
169
|
+
## 4. Cross-bundle references and collation
|
|
170
|
+
|
|
171
|
+
- A bundle may only reach outside itself through its `refs.md`. Each entry names the target
|
|
172
|
+
bundle, the reason, and optionally the specific note.
|
|
173
|
+
- **Collation protocol** (also in `skills/query/SKILL.md`): to answer a question spanning bundles A and B:
|
|
174
|
+
1. Resolve in A. Note where A's `refs.md` points to B.
|
|
175
|
+
2. Open B **through its own index.md** — never jump straight to a B note from A's text.
|
|
176
|
+
3. Join the two answers in the response, not in the files.
|
|
177
|
+
Parallelizable: agent 1 handles A, agent 2 handles B, results merged by the orchestrator.
|
|
178
|
+
- Inline links from a note in A directly into a note in B are a lint error. Notes may say
|
|
179
|
+
"see refs → <bundle>", nothing more specific. This is what keeps bundles independent.
|
|
180
|
+
|
|
181
|
+
## 5. Ingestion
|
|
182
|
+
|
|
183
|
+
Each bundle declares its sources in `sources.yaml`:
|
|
184
|
+
|
|
185
|
+
```yaml
|
|
186
|
+
sources:
|
|
187
|
+
- type: folder # local disk
|
|
188
|
+
path: /abs/path/to/project-x
|
|
189
|
+
- type: web
|
|
190
|
+
urls:
|
|
191
|
+
- https://example.com/design-doc
|
|
192
|
+
# Confluence and ADO are listed for the record but ingested by the agent
|
|
193
|
+
# via MCP/CLI, not by scripts — see "scripting philosophy" below.
|
|
194
|
+
- type: confluence
|
|
195
|
+
space: PROJX
|
|
196
|
+
- type: ado
|
|
197
|
+
org: myorg
|
|
198
|
+
project: ProjectX
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`khb ingest <bundle>` is **one flat phase**: it walks every scripted source, extracts what it
|
|
202
|
+
can, and writes `raw/<type>/<file>.md` with a provenance header. It does not interpret
|
|
203
|
+
content — that is §5b. Re-running is incremental: a source whose content hash is unchanged
|
|
204
|
+
and whose `raw/` copy still exists is skipped; `--force` re-acquires.
|
|
205
|
+
|
|
206
|
+
**A source is identified by its bytes, not its path.** When a file turns up at an unseen
|
|
207
|
+
path, khb looks for a row with the same hash whose own path has since vanished; exactly one
|
|
208
|
+
match means the file moved, and that row is re-pointed at the new path — keeping its `raw/`
|
|
209
|
+
file (never renamed, because concepts cite it) and its `curated` value, and correcting the
|
|
210
|
+
raw file's `source:` header. Without this, moving a file reads as a deletion plus an
|
|
211
|
+
unrelated arrival: a duplicate raw file, a duplicate row with an empty `curated`, and
|
|
212
|
+
eventually a duplicate concept for material already cataloged. A source's `raw/` filename is
|
|
213
|
+
likewise fixed for life, so a file that moves and *then* changes re-extracts over the same
|
|
214
|
+
file rather than stranding the one its citations point at.
|
|
215
|
+
|
|
216
|
+
Two look-alikes are deliberately excluded, since both would rewire provenance on a guess: a
|
|
217
|
+
**copy** (the twin's path still exists — two real sources, both ingested) and an **ambiguous**
|
|
218
|
+
match (several vanished rows share the hash). Each is reported and left to judgement.
|
|
219
|
+
|
|
220
|
+
The bundle argument is optional. With none, ingest targets `default` and scaffolds it if the
|
|
221
|
+
hub has no such bundle, so a hub with zero bundles still has a landing path. Only that name
|
|
222
|
+
is auto-created — an explicit name that doesn't resolve is an error, not a scaffold request.
|
|
223
|
+
`default` is a holding area, not a tier: its contents are cataloged like any bundle's. Its
|
|
224
|
+
material moves only when a human says which bundle should own it — a topic emerging inside
|
|
225
|
+
it is not itself a reason to move anything.
|
|
226
|
+
|
|
227
|
+
Every raw file carries its origin, so a lossy extraction is always recoverable:
|
|
228
|
+
|
|
229
|
+
```yaml
|
|
230
|
+
---
|
|
231
|
+
source: /abs/path/to/original.pdf
|
|
232
|
+
fetched: 2026-07-23T09:14:02Z
|
|
233
|
+
sha256: db2ee470c95d
|
|
234
|
+
extract_tool: tesseract.js
|
|
235
|
+
quality: low # high = real text; low = OCR or transcript, verify against source
|
|
236
|
+
---
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Scripting philosophy: script every deterministic conversion, script no API wrappers.**
|
|
240
|
+
Folder, files and web are scripted, as is every extractor (§6). Confluence, ADO and git
|
|
241
|
+
hosts are reached through their MCP servers or official CLIs by the agent, which writes into
|
|
242
|
+
the same `raw/` shape — no API wrapper code to maintain here. If agent-free refresh of one
|
|
243
|
+
of those ever becomes a real need, promote it to a script then.
|
|
244
|
+
|
|
245
|
+
## 5b. Catalog
|
|
246
|
+
|
|
247
|
+
Turning `raw/` into concept docs is a separate, agent-only step — `skills/catalog/SKILL.md`,
|
|
248
|
+
with no CLI command, because nothing about it is mechanical. Per bundle: read each raw file,
|
|
249
|
+
split it into concepts, give each OKF frontmatter, link them, register them in `index.md`,
|
|
250
|
+
and fill the `curated` column of `log.md`. Parallelized by fanning cheap subagents over the
|
|
251
|
+
raw files, with one hard rule — subagents write concept docs, the orchestrator alone writes
|
|
252
|
+
`index.md`, `log.md` and `refs.md`.
|
|
253
|
+
|
|
254
|
+
## 6. Extraction
|
|
255
|
+
|
|
256
|
+
Extraction is deterministic, so `khb` owns all of it. Common formats use bundled pure-JS
|
|
257
|
+
libraries with no system install; results are cached hub-wide by content hash
|
|
258
|
+
(`inbox/extracted/<sha256>.md`) and reused across bundles. Nothing here contacts a model:
|
|
259
|
+
tesseract and whisper are local binaries, expensive in CPU but reproducible, which is what
|
|
260
|
+
puts them on the CLI side of the §Division-of-labor line.
|
|
261
|
+
|
|
262
|
+
| Format | Tool | Deps | Quality |
|
|
263
|
+
|---|---|---|---|
|
|
264
|
+
| PDF | `unpdf` (pdf.js), `pdftotext` if present | bundled | high |
|
|
265
|
+
| DOCX | `mammoth`, `pandoc` if present | bundled | high |
|
|
266
|
+
| ODT, PPTX | `fflate` + XML | bundled | high |
|
|
267
|
+
| XLSX | `fflate` → one markdown table per sheet | bundled | high |
|
|
268
|
+
| scanned PDF | `pdfium` + `tesseract.js` (WASM) | opt-in, ~75 MB | low |
|
|
269
|
+
| Images (png/jpg/webp/tif) | `tesseract.js` | opt-in, ~75 MB | low |
|
|
270
|
+
| Audio, video | `whisper` / `faster-whisper` | opt-in, pip | low |
|
|
271
|
+
|
|
272
|
+
Missing optional deps degrade to a ledger row with an empty `raw` and a printed install
|
|
273
|
+
hint — never to a failed run. `quality: low` output is a standing invitation for the catalog
|
|
274
|
+
pass to re-read the original: a vision read of a chart or a scanned table recovers what OCR
|
|
275
|
+
drops, and rewrites the raw file with `extract_tool: claude-vision`.
|
|
276
|
+
|
|
277
|
+
## 7. Lint
|
|
278
|
+
|
|
279
|
+
`khb lint` enforces (details in `skills/lint/SKILL.md`):
|
|
280
|
+
- every bundle has `index.md`, `refs.md`, `sources.yaml`
|
|
281
|
+
- every bundle is registered in `outer.index.md`; nothing in `outer.index.md` is dangling
|
|
282
|
+
- every concept doc is listed in an index and carries OKF frontmatter (`type` required)
|
|
283
|
+
- `refs.md` targets exist; no cross-bundle inline links from concept docs
|
|
284
|
+
- index files contain links only (routing, not content)
|
|
285
|
+
|
|
286
|
+
## 8. Visualizer
|
|
287
|
+
|
|
288
|
+
`khb visualize [--port N]` scans `outer.index.md`, every bundle `index.md`/`refs.md`, and
|
|
289
|
+
every concept doc's markdown links, and serves an interactive graph from a local server —
|
|
290
|
+
`aliases: vis, viz` (`-v` is taken by `khb --version`). Two zoom levels: bundles as nodes
|
|
291
|
+
with refs as directed edges (note counts as node size), and — click a bundle — its
|
|
292
|
+
concepts as nodes with the markdown links between them as edges. Click a concept to open
|
|
293
|
+
a panel that fetches its full body from `/api/file`. A refresh button rescans the hub and
|
|
294
|
+
refetches the graph over `/api/graph`, for watching a hub change live during a catalog
|
|
295
|
+
pass.
|
|
296
|
+
|
|
297
|
+
The inner view is **clustered by folder, not free-floating**: each concept is anchored to
|
|
298
|
+
the region of its top-level subdirectory (`tables/`, `notes/`, …), drawn as a labelled
|
|
299
|
+
hull, and links that cross folders pull far more weakly than links inside one. The
|
|
300
|
+
bundle's own organisation is therefore the visible structure, and the graph reads as
|
|
301
|
+
traffic between regions instead of a single hairball. Deeper nesting collapses into its
|
|
302
|
+
top-level folder — a handful of labelled regions beats one region per directory.
|
|
303
|
+
|
|
304
|
+
The canvas **pans and zooms** (wheel to zoom at the cursor, drag the background to pan,
|
|
305
|
+
`fit` / `F` to reframe). The layout is settled before the first paint and then framed by a
|
|
306
|
+
fit pass with a floor on the zoom, so a small hub opens zoomed in rather than as three dots
|
|
307
|
+
in an empty canvas, and a large one opens whole.
|
|
308
|
+
|
|
309
|
+
The outer view seats bundles on a ring **just wide enough to hold them side by side**, with
|
|
310
|
+
short-range repulsion and a firm pull to the centre: the hub opens as one compact group
|
|
311
|
+
that reads at a glance, and zoom — not distance — is what makes it usable.
|
|
312
|
+
|
|
313
|
+
Labels are drawn in **screen space at a fixed size**, not in world space, and any label
|
|
314
|
+
whose box would collide with one already drawn is dropped — larger nodes claim their name
|
|
315
|
+
first, and the hovered node always keeps its own. Text therefore stays legible and sparse
|
|
316
|
+
at every zoom level instead of piling into an unreadable puddle; zooming in reveals the
|
|
317
|
+
labels that were culled. A node's label is its front-matter `title`, clipped short; the
|
|
318
|
+
untruncated title and full path appear in the panel when it is clicked, and in the hover
|
|
319
|
+
strip at the bottom.
|
|
320
|
+
|
|
321
|
+
A concept's `type` is encoded as **colour**, never as sub-text, and is spelled out in words
|
|
322
|
+
in the hover strip and the panel chip — there is no legend and no shape vocabulary to
|
|
323
|
+
learn. Hovering a node dims everything it isn't linked to. The top bar is deliberately
|
|
324
|
+
thin: back, fit, a dark/light toggle remembered in `localStorage`, and refresh. Every
|
|
325
|
+
colour the canvas draws comes from the active theme so the graph and the surrounding chrome
|
|
326
|
+
stay in step.
|
|
327
|
+
|
|
328
|
+
Folder regions are laid out on an **ellipse sized from the folders themselves** — each
|
|
329
|
+
folder's disc grows with its file count, and the ring is only wide enough for neighbouring
|
|
330
|
+
discs to clear each other — so two folders sit side by side rather than a screen and a half
|
|
331
|
+
apart. The fit pass has a zoom ceiling but no floor: a large graph zooms out until it is
|
|
332
|
+
whole rather than opening cropped.
|
|
333
|
+
|
|
334
|
+
Unpinned, it binds a random free port (and picks another if `--port N` is already taken)
|
|
335
|
+
and opens the URL in the default browser; `--no-open` just prints it. The page heartbeats
|
|
336
|
+
the server and beacons on unload, so the server exits on its own once the browser tab
|
|
337
|
+
closes rather than lingering as a background process.
|
|
338
|
+
|
|
339
|
+
## 9. Agent workflow summary
|
|
340
|
+
|
|
341
|
+
```
|
|
342
|
+
build: sources.yaml → khb ingest → raw/*.md (+ provenance, + log.md row)
|
|
343
|
+
→ catalog (agent) → concept docs + index.md entries + curated column
|
|
344
|
+
→ khb lint
|
|
345
|
+
|
|
346
|
+
query: question → AGENTS.md → outer.index.md → bundle/index.md → concept docs
|
|
347
|
+
↘ (spanning?) refs.md → other bundle via ITS index
|
|
348
|
+
↘ (durable synthesis?) propose a new concept → on confirm,
|
|
349
|
+
write it, link it both ways, index it, log it
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
The query arm writing back is what makes the hub denser with use rather than merely larger:
|
|
353
|
+
a question answered by joining two concepts leaves that join behind for the next one. It is
|
|
354
|
+
always proposed and never silent — see `skills/query/SKILL.md`.
|
package/document/faq.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Frequently Asked Questions
|
|
2
|
+
|
|
3
|
+
## What is KHB?
|
|
4
|
+
|
|
5
|
+
KHB (Knowledge Hub Builder) is a local, markdown-based system for building and maintaining
|
|
6
|
+
a bundle-of-bundles knowledge base with an AI agent.
|
|
7
|
+
|
|
8
|
+
The `khb` CLI is the supporting tool. It handles deterministic work such as extraction,
|
|
9
|
+
file plumbing, validation, visualization, and export. The bundled workflow skills tell an
|
|
10
|
+
AI agent—such as Claude, Codex, Gemini, or another compatible agent—how to ingest, catalog,
|
|
11
|
+
query, and maintain the knowledge. The agent follows those skills and orchestrates the
|
|
12
|
+
overall workflow.
|
|
13
|
+
|
|
14
|
+
The hub itself remains an ordinary folder of markdown files, indexes, source declarations,
|
|
15
|
+
and provenance records. The CLI holds no knowledge and does not call a model.
|
|
16
|
+
|
|
17
|
+
## What are the main KHB workflow stages?
|
|
18
|
+
|
|
19
|
+
KHB has three main stages:
|
|
20
|
+
|
|
21
|
+
1. **Ingest** converts declared sources into provenance-bearing markdown under
|
|
22
|
+
`bundles/<bundle>/raw/`.
|
|
23
|
+
2. **Catalog** interprets one bundle's raw material, creates canonical concept documents,
|
|
24
|
+
and updates that bundle's index and ledger.
|
|
25
|
+
3. **Query** routes a question through the indexes and answers from curated concept
|
|
26
|
+
documents.
|
|
27
|
+
|
|
28
|
+
Ingest is mechanical and local. Catalog and query require an agent because they involve
|
|
29
|
+
judgement.
|
|
30
|
+
|
|
31
|
+
## Does cataloging move everything into one main bundle?
|
|
32
|
+
|
|
33
|
+
No. Ingest and catalog operate on the same owner-defined bundle. For example, material
|
|
34
|
+
ingested into `client-a` is cataloged into concepts inside `bundles/client-a/`.
|
|
35
|
+
|
|
36
|
+
Bundles are units of ownership, such as a person, team, project, or client. They are not
|
|
37
|
+
subject categories, and an agent must not create, split, merge, or reorganize them without
|
|
38
|
+
explicit instruction.
|
|
39
|
+
|
|
40
|
+
## What happens when there are multiple bundles?
|
|
41
|
+
|
|
42
|
+
Cataloging handles exactly one bundle at a time. Name the bundle in the request, such as:
|
|
43
|
+
|
|
44
|
+
> Catalog the `client-a` bundle.
|
|
45
|
+
|
|
46
|
+
If multiple bundles are possible and none was identified, the agent asks which bundle to
|
|
47
|
+
use instead of guessing.
|
|
48
|
+
|
|
49
|
+
During a query, the agent starts at `outer.index.md` and selects one bundle. If the
|
|
50
|
+
question genuinely spans bundles, it follows `refs.md`, enters each additional bundle
|
|
51
|
+
through its own `index.md`, and combines the results only in the answer.
|
|
52
|
+
|
|
53
|
+
## What does `khb ingest [bundle] [--force]` do?
|
|
54
|
+
|
|
55
|
+
The command reads the selected bundle's `sources.yaml`, acquires every supported declared
|
|
56
|
+
source, extracts it into markdown under `raw/`, and updates `log.md`.
|
|
57
|
+
|
|
58
|
+
It does not summarize, label, organize, or create concepts. Unchanged content hashes are
|
|
59
|
+
skipped by default; `--force` reacquires everything.
|
|
60
|
+
|
|
61
|
+
When no bundle is named, KHB uses the `default` bundle and creates it if necessary. An
|
|
62
|
+
explicitly named bundle must already exist.
|
|
63
|
+
|
|
64
|
+
## Should I run ingestion manually or ask an agent?
|
|
65
|
+
|
|
66
|
+
Either works. Running `khb ingest client-a` manually performs the deterministic extraction.
|
|
67
|
+
You can instead ask:
|
|
68
|
+
|
|
69
|
+
> Ingest the `client-a` bundle.
|
|
70
|
+
|
|
71
|
+
The agent invokes the same command, checks its summary, handles supported authenticated
|
|
72
|
+
sources, and reports anything pending. Manual execution is useful in scripts or when only
|
|
73
|
+
extraction is needed.
|
|
74
|
+
|
|
75
|
+
## Where are ingestion sources declared?
|
|
76
|
+
|
|
77
|
+
Each bundle has its own declaration file:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
bundles/<bundle>/sources.yaml
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For example:
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
sources:
|
|
87
|
+
- type: folder
|
|
88
|
+
path: /absolute/path/to/documents
|
|
89
|
+
- type: files
|
|
90
|
+
paths:
|
|
91
|
+
- /absolute/path/to/one.pdf
|
|
92
|
+
- /absolute/path/to/two.xlsx
|
|
93
|
+
- type: web
|
|
94
|
+
urls:
|
|
95
|
+
- https://en.wikipedia.org/wiki/Example
|
|
96
|
+
- type: confluence
|
|
97
|
+
space: PROJX
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The `sources` list may contain one or many source declarations of different types.
|
|
101
|
+
|
|
102
|
+
## When are sources added to `sources.yaml`?
|
|
103
|
+
|
|
104
|
+
Sources are declared before ingestion. The usual sequence is:
|
|
105
|
+
|
|
106
|
+
1. Create or select the owner bundle.
|
|
107
|
+
2. Add its source locations to `sources.yaml`.
|
|
108
|
+
3. Run ingestion.
|
|
109
|
+
4. Catalog the resulting raw material.
|
|
110
|
+
|
|
111
|
+
Declaring a source records where material should come from; it does not copy anything by
|
|
112
|
+
itself.
|
|
113
|
+
|
|
114
|
+
## What if I ask for ingestion without identifying the sources?
|
|
115
|
+
|
|
116
|
+
The ingest workflow inspects the bundle's existing `sources.yaml` and asks which files,
|
|
117
|
+
folders, URLs, or services to ingest. It includes existing declarations so they can be
|
|
118
|
+
confirmed or replaced.
|
|
119
|
+
|
|
120
|
+
The agent does not infer sources from nearby files, change `sources.yaml`, or run ingestion
|
|
121
|
+
until the user answers.
|
|
122
|
+
|
|
123
|
+
## Can one bundle ingest Wikipedia, Confluence, and Azure DevOps material?
|
|
124
|
+
|
|
125
|
+
Yes. Public pages such as Wikipedia can be declared as `web` sources and fetched by
|
|
126
|
+
`khb ingest`.
|
|
127
|
+
|
|
128
|
+
Authenticated systems such as Confluence and Azure DevOps are also declared in
|
|
129
|
+
`sources.yaml`, but the KHB CLI does not authenticate to them. The agent pulls their data
|
|
130
|
+
through an available MCP connector or official CLI, writes the same provenance-bearing
|
|
131
|
+
markdown shape under `raw/`, and updates `log.md`. If access is unavailable, the agent
|
|
132
|
+
reports the blocked source.
|
|
133
|
+
|
|
134
|
+
## Does cataloging delete the raw files?
|
|
135
|
+
|
|
136
|
+
No. Cataloging reads raw files and creates canonical concept documents elsewhere in the
|
|
137
|
+
same bundle. The raw material remains derived evidence and is not used directly to answer
|
|
138
|
+
queries.
|
|
139
|
+
|
|
140
|
+
The bundle's `log.md` connects each source and raw file to the concept documents curated
|
|
141
|
+
from it.
|
|
142
|
+
|
|
143
|
+
## Can a query create or update concepts?
|
|
144
|
+
|
|
145
|
+
Yes, but never silently. If answering produces durable, reusable knowledge that existing
|
|
146
|
+
concepts do not cover, the agent offers to:
|
|
147
|
+
|
|
148
|
+
- fold the knowledge into an existing concept; or
|
|
149
|
+
- create a new concept and link it to the concepts from which it was derived.
|
|
150
|
+
|
|
151
|
+
The agent explains what it would write and where, then waits for confirmation. After
|
|
152
|
+
approval, it updates the concept links, `index.md`, and `log.md`, and runs `khb lint`.
|
|
153
|
+
One-off conversational answers are not saved.
|
|
154
|
+
|
|
155
|
+
If a query reveals that an existing concept is wrong or stale, the agent similarly offers
|
|
156
|
+
to repair it in place before making any change.
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@msareen/knowledge-hub-builder",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Knowledge Hub Builder — a bundle-of-bundles knowledge base you build with an agent",
|
|
5
|
+
"keywords": ["knowledge-base", "okf", "agent", "wiki", "bundles", "claude", "codex", "rag"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/msareen/knowledge-hub-builder.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/msareen/knowledge-hub-builder#readme",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"bin": {
|
|
14
|
+
"khb": "./scripts/cli.ts"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"scripts/",
|
|
18
|
+
"skills/",
|
|
19
|
+
"templates/",
|
|
20
|
+
".bundle_template/",
|
|
21
|
+
".agents/skills/",
|
|
22
|
+
".claude/skills/",
|
|
23
|
+
"AGENTS.md",
|
|
24
|
+
"CLAUDE.md",
|
|
25
|
+
"SPEC.md",
|
|
26
|
+
"README.md",
|
|
27
|
+
"document/"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"bun": ">=1.0.0"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"khb": "bun scripts/cli.ts",
|
|
37
|
+
"lint": "bun scripts/cli.ts lint",
|
|
38
|
+
"visualize": "bun scripts/cli.ts visualize",
|
|
39
|
+
"new-bundle": "bun scripts/cli.ts new-bundle",
|
|
40
|
+
"export": "bun scripts/cli.ts export",
|
|
41
|
+
"ingest": "bun scripts/cli.ts ingest"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@hyzyla/pdfium": "^2.1.13",
|
|
45
|
+
"fflate": "^0.8.3",
|
|
46
|
+
"mammoth": "^1.12.0",
|
|
47
|
+
"sharp": "^0.35.3",
|
|
48
|
+
"tesseract.js": "^7.0.0",
|
|
49
|
+
"unpdf": "^1.6.2",
|
|
50
|
+
"yaml": "^2.4.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/scripts/cli.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// khb — the CLI. Subcommands are loaded lazily: `init` must run before a hub exists,
|
|
3
|
+
// so nothing that resolves a hub may be imported at module scope.
|
|
4
|
+
import { version } from "./lib/paths";
|
|
5
|
+
|
|
6
|
+
const COMMANDS: Record<string, { load: () => Promise<unknown>; help: string }> = {
|
|
7
|
+
init: { load: () => import("./init"), help: "khb init [dir] create a hub here (or in dir)" },
|
|
8
|
+
upgrade: { load: () => import("./init"), help: "khb upgrade refresh this hub's contract docs" },
|
|
9
|
+
"new-bundle": { load: () => import("./new-bundle"), help: 'khb new-bundle <name> ["scope"] scaffold a bundle + register it' },
|
|
10
|
+
ingest: { load: () => import("./ingest/index"), help: "khb ingest [bundle] [--force] acquire + extract declared sources → raw/ (default: 'default')" },
|
|
11
|
+
lint: { load: () => import("./lint"), help: "khb lint validate the hub against skills/lint/SKILL.md" },
|
|
12
|
+
visualize: { load: () => import("./visualize"), help: "khb visualize [--port N] [--no-open] serve the live bundle graph in your browser; aliases: vis, viz" },
|
|
13
|
+
export: { load: () => import("./export"), help: "khb export <bundle> [dest] standalone copy of one bundle" },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Short forms that just resolve to a canonical command above — kept out of COMMANDS
|
|
17
|
+
// itself so help text lists each command once. `-v` is taken by --version, so
|
|
18
|
+
// `visualize` gets word-shaped aliases instead of a letter one.
|
|
19
|
+
const ALIASES: Record<string, string> = { vis: "visualize", viz: "visualize" };
|
|
20
|
+
|
|
21
|
+
const argv = process.argv.slice(2);
|
|
22
|
+
|
|
23
|
+
// --hub <dir> is global: strip it here so subcommands never see it, and hand it to
|
|
24
|
+
// util.ts through the environment (same channel as $KHB_HUB).
|
|
25
|
+
const hubAt = argv.indexOf("--hub");
|
|
26
|
+
if (hubAt >= 0) {
|
|
27
|
+
const dir = argv[hubAt + 1];
|
|
28
|
+
if (!dir) {
|
|
29
|
+
console.error("--hub needs a directory");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
process.env.KHB_HUB = dir;
|
|
33
|
+
argv.splice(hubAt, 2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const cmd0 = argv.shift();
|
|
37
|
+
const cmd = cmd0 && ALIASES[cmd0] ? ALIASES[cmd0] : cmd0;
|
|
38
|
+
|
|
39
|
+
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
40
|
+
console.log(`khb ${version()} — Knowledge Hub Builder\n`);
|
|
41
|
+
console.log(`khb is the supporting tool: it handles deterministic extraction, file plumbing,`);
|
|
42
|
+
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.\n`);
|
|
44
|
+
for (const c of Object.values(COMMANDS)) console.log(" " + c.help);
|
|
45
|
+
console.log(`\nGlobal: --hub <dir> operate on that hub instead of searching upward from cwd`);
|
|
46
|
+
console.log(`Docs: https://github.com/msareen/knowledge-hub-builder`);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (cmd === "--version" || cmd === "-v") {
|
|
51
|
+
console.log(version());
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const entry = COMMANDS[cmd];
|
|
56
|
+
if (!entry) {
|
|
57
|
+
console.error(`Unknown command: ${cmd}`);
|
|
58
|
+
console.error(`Try: khb help`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Subcommand modules parse process.argv.slice(2) themselves — reshape it so they see
|
|
63
|
+
// their own arguments and not the subcommand name.
|
|
64
|
+
process.argv = [process.argv[0], process.argv[1], ...argv];
|
|
65
|
+
process.env.KHB_SUBCOMMAND = cmd;
|
|
66
|
+
await entry.load();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// khb export <bundle> [dest] — export a bundle as a standalone, shareable unit.
|
|
2
|
+
// Bundles stay lean in the hub (common patterns live at hub root); export injects those
|
|
3
|
+
// patterns so the exported folder works alone with any agent.
|
|
4
|
+
import { cpSync, writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { HUB, bundleDir, join } from "./lib/util";
|
|
6
|
+
import { detail, totalElapsed } from "./lib/log";
|
|
7
|
+
|
|
8
|
+
const [name, destArg] = process.argv.slice(2);
|
|
9
|
+
if (!name) { console.error("Usage: khb export <bundle> [dest]"); process.exit(1); }
|
|
10
|
+
|
|
11
|
+
const src = bundleDir(name);
|
|
12
|
+
const dest = destArg ?? join(HUB, "export", name);
|
|
13
|
+
if (existsSync(dest)) { console.error(`Destination exists: ${dest}`); process.exit(1); }
|
|
14
|
+
|
|
15
|
+
console.log(`khb export → ${name}`);
|
|
16
|
+
detail(`from: ${src}`);
|
|
17
|
+
detail(`to: ${dest}`);
|
|
18
|
+
|
|
19
|
+
mkdirSync(dest, { recursive: true });
|
|
20
|
+
detail(`copying bundle/ …`);
|
|
21
|
+
cpSync(src, join(dest, "bundle"), { recursive: true });
|
|
22
|
+
|
|
23
|
+
// Inject the hub's package-owned copies (kept current by `khb upgrade`). AGENTS.md and
|
|
24
|
+
// skills/ are canonical; the shims/adapters make them discoverable by Claude and Codex.
|
|
25
|
+
for (const item of ["AGENTS.md", "CLAUDE.md", "skills", ".agents/skills", ".claude/skills"]) {
|
|
26
|
+
const target = join(dest, item);
|
|
27
|
+
mkdirSync(join(target, ".."), { recursive: true });
|
|
28
|
+
cpSync(join(HUB, item), target, { recursive: true });
|
|
29
|
+
detail(`copying ${item}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// standalone router: one-bundle outer index
|
|
33
|
+
detail("writing outer.index.md + README.md");
|
|
34
|
+
const scope = (readFileSync(join(HUB, "outer.index.md"), "utf8")
|
|
35
|
+
.split("\n").find((l) => l.includes(`[${name}]`)) ?? "").split("|")[2]?.trim() ?? "";
|
|
36
|
+
writeFileSync(join(dest, "outer.index.md"),
|
|
37
|
+
`# outer.index — exported bundle\n\n| Bundle | Scope | Route here when |\n|---|---|---|\n| [${name}](bundle/index.md) | ${scope} | always — single-bundle export |\n`);
|
|
38
|
+
|
|
39
|
+
writeFileSync(join(dest, "README.md"),
|
|
40
|
+
`# ${name} (exported KHB bundle)\n\nExported: ${new Date().toISOString()}\nOrigin: KHB bundle-of-bundles repo.\n\nStandalone unit: start at AGENTS.md → outer.index.md → bundle/index.md.\nWorkflow protocols (query, ingest, lint, …) live in skills/<name>/SKILL.md and are discoverable by Claude and Codex.\nNote: refs.md entries pointing at other bundles will not resolve here.\n`);
|
|
41
|
+
|
|
42
|
+
console.log(`\nExported to ${dest} in ${totalElapsed()} (bundle + agent contracts, skills, single-bundle router)`);
|